1package ufw
2
3import (
4 "bufio"
5 "regexp"
6 "strconv"
7 "strings"
8)
9
10func ParseRules(output string) []Rule {
11 var rules []Rule
12 reNumbered := regexp.MustCompile(`^\[\s*(\d+)\]\s+(.+)$`)
13 scanner := bufio.NewScanner(strings.NewReader(output))
14
15 for scanner.Scan() {
16 line := scanner.Text()
17
18 if shouldSkipLine(line) {
19 continue
20 }
21
22 matches := reNumbered.FindStringSubmatch(line)
23 if len(matches) < 3 {
24 continue
25 }
26
27 num, _ := strconv.Atoi(matches[1])
28 ruleLine := strings.TrimSpace(matches[2])
29
30 rule := parseRuleLine(num, ruleLine)
31 rules = append(rules, rule)
32 }
33
34 return rules
35}
36
37func shouldSkipLine(line string) bool {
38 trimmed := strings.TrimSpace(line)
39
40 if trimmed == "" {
41 return true
42 }
43
44 if strings.Contains(line, "Status:") {
45 return true
46 }
47
48 if strings.Contains(line, "To") && strings.Contains(line, "Action") && strings.Contains(line, "From") {
49 return true
50 }
51
52 if strings.HasPrefix(trimmed, "--") || strings.HasPrefix(trimmed, "==") {
53 return true
54 }
55
56 return false
57}
58
59func parseRuleLine(num int, line string) Rule {
60 rule := Rule{
61 Num: num,
62 Raw: line,
63 }
64
65 rule.IPv6 = strings.Contains(line, "(v6)")
66
67 commentIdx := strings.Index(line, "#")
68 if commentIdx >= 0 {
69 rule.Comment = strings.TrimSpace(line[commentIdx+1:])
70 line = strings.TrimSpace(line[:commentIdx])
71 }
72
73 parts := splitByMultipleSpaces(line)
74
75 if len(parts) < 3 {
76 return rule
77 }
78
79 actionIndex := findActionIndex(parts)
80 if actionIndex == -1 {
81 return rule
82 }
83
84 actionParts := strings.Fields(parts[actionIndex])
85 if len(actionParts) > 0 {
86 rule.Action = actionParts[0]
87 }
88
89 if actionIndex > 0 {
90 toField := strings.Join(parts[:actionIndex], " ")
91 rule.ToDest, rule.ToPort, rule.ToProtocol = parseDestination(toField)
92 }
93
94 if actionIndex+1 < len(parts) {
95 fromField := strings.Join(parts[actionIndex+1:], " ")
96 rule.FromSource, rule.FromPort = parseSource(fromField)
97 }
98
99 return rule
100}
101
102func findActionIndex(parts []string) int {
103 for i, part := range parts {
104 upper := strings.ToUpper(strings.Fields(part)[0])
105 if upper == ActionAllow || upper == ActionDeny || upper == ActionReject || upper == ActionLimit {
106 return i
107 }
108 }
109 return -1
110}
111
112func splitByMultipleSpaces(s string) []string {
113 re := regexp.MustCompile(`\s{2,}`)
114 parts := re.Split(s, -1)
115
116 cleaned := make([]string, 0, len(parts))
117 for _, part := range parts {
118 trimmed := strings.TrimSpace(part)
119 if trimmed != "" {
120 cleaned = append(cleaned, trimmed)
121 }
122 }
123
124 return cleaned
125}
126
127func parseDestination(s string) (dest, port, protocol string) {
128 s = strings.TrimSpace(s)
129
130 if strings.HasPrefix(s, "Anywhere") {
131 return s, "any", "any"
132 }
133
134 spaceParts := strings.Fields(s)
135 if len(spaceParts) >= 2 && isIPOrCIDR(spaceParts[0]) {
136 dest = spaceParts[0]
137 port, protocol = parsePortSpec(strings.Join(spaceParts[1:], " "))
138 return dest, port, protocol
139 }
140
141 if isIPOrCIDR(s) {
142 return s, "any", "any"
143 }
144
145 port, protocol = parsePortSpec(s)
146 return "Anywhere", port, protocol
147}
148
149func parseSource(s string) (source, port string) {
150 s = strings.TrimSpace(s)
151
152 if strings.HasPrefix(s, "Anywhere") {
153 return s, "any"
154 }
155
156 if strings.Contains(s, " ") {
157 parts := strings.Fields(s)
158 if len(parts) >= 2 {
159 return parts[0], parts[1]
160 }
161 }
162
163 return s, "any"
164}
165
166func isIPOrCIDR(s string) bool {
167 if strings.Contains(s, ".") {
168 parts := strings.Split(s, "/")
169 if strings.Contains(parts[0], ".") {
170 return true
171 }
172 }
173
174 colonCount := strings.Count(s, ":")
175 if colonCount > 1 {
176 return true
177 }
178
179 return false
180}
181
182func parsePortSpec(s string) (port, protocol string) {
183 s = strings.TrimSpace(s)
184
185 if strings.Contains(s, "/") {
186 parts := strings.SplitN(s, "/", 2)
187 if len(parts) == 2 {
188 portNum := parts[0]
189 protoWithExtra := parts[1]
190
191 protoWords := strings.Fields(protoWithExtra)
192 if len(protoWords) > 0 {
193 protocol = strings.ToLower(protoWords[0])
194 return portNum, protocol
195 }
196 }
197 }
198
199 if strings.Contains(s, ":") && !strings.Contains(s, "::") {
200 return s, "any"
201 }
202
203 return s, "any"
204}