Skip to content

Commit 17dc065

Browse files
catenacybervictorjulien
authored andcommitted
ci: check uint keywords with their size
So, that if a keyword advertises uint16, it can indeed parse a uint16 and is not just a uint8
1 parent 399ee1e commit 17dc065

1 file changed

Lines changed: 88 additions & 47 deletions

File tree

scripts/check-uint-keywords.py

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222

2323
MULTI_UINT_RE = re.compile(r"multi .*uint\d+")
2424

25+
# Per-integer-width checks: feature word -> rule option to test
26+
UINT_CHECKS: dict[str, tuple[re.Pattern[str], str]] = {
27+
"uint8": (re.compile(r"\buint8\b"), ">1"),
28+
"uint16": (re.compile(r"\buint16\b"), ">0x101"),
29+
"uint32": (re.compile(r"\buint32\b"), ">0x10001"),
30+
"uint64": (re.compile(r"\buint64\b"), ">0x100000001"),
31+
}
32+
2533

2634
def resolve_suricata_bin(repo_root: Path, configured: Optional[str]) -> Path:
2735
if configured:
@@ -41,8 +49,8 @@ def resolve_suricata_bin(repo_root: Path, configured: Optional[str]) -> Path:
4149
)
4250

4351

44-
def list_multi_uint_keywords(suricata_bin: Path) -> list[str]:
45-
"""Return keyword names whose features column matches 'multi .*uint<N>'."""
52+
def list_keywords(suricata_bin: Path) -> list[tuple[str, str]]:
53+
"""Return (name, features) pairs for all keywords from --list-keywords=csv."""
4654
proc = subprocess.run(
4755
[str(suricata_bin), "--list-keywords=csv"],
4856
check=False,
@@ -51,39 +59,43 @@ def list_multi_uint_keywords(suricata_bin: Path) -> list[str]:
5159
)
5260
output = proc.stdout or proc.stderr
5361
reader = csv.reader(io.StringIO(output), delimiter=";")
54-
keywords = []
62+
result = []
5563
for i, row in enumerate(reader):
5664
if i == 0:
57-
# header row
5865
continue
5966
if len(row) < 4:
6067
continue
61-
name = row[0].strip()
62-
features = row[3].strip()
63-
if MULTI_UINT_RE.search(features):
64-
keywords.append(name)
65-
return keywords
68+
result.append((row[0].strip(), row[3].strip()))
69+
return result
6670

6771

6872
SID_RE = re.compile(r"sid:(?P<sid>\d+)")
6973

7074

7175
def check_keywords(
72-
keywords: list[str],
76+
entries: list[tuple[str, str]],
7377
suricata_bin: Path,
7478
suricata_yaml: Path,
75-
) -> dict[str, list[str]]:
76-
"""Write all rules to one file, run suricata -T once, return keyword->errors map."""
77-
sid_to_keyword: dict[int, str] = {}
78-
rules = []
79-
for sid, keyword in enumerate(keywords, start=1):
80-
sid_to_keyword[sid] = keyword
81-
rules.append(
82-
f'alert ip any any -> any any '
83-
f'(msg:"check {keyword} multi uint"; {keyword}: >1,all; sid:{sid};)\n'
84-
)
79+
) -> dict[int, list[str]]:
80+
"""Write one rule per (keyword, option) entry, run suricata -T once.
8581
86-
with tempfile.TemporaryDirectory(prefix="multi-uint-check-") as tmpdir:
82+
Returns a mapping of entry index (0-based) -> error lines for failed rules.
83+
"""
84+
rules = []
85+
for sid, (keyword, option) in enumerate(entries, start=1):
86+
if keyword == "bsize":
87+
# bsize is a special case: it requires a sticky buffer first.
88+
rules.append(
89+
f'alert ip any any -> any any '
90+
f'(msg:"check {keyword} {option}"; http.uri; {keyword}: {option}; sid:{sid};)\n'
91+
)
92+
else:
93+
rules.append(
94+
f'alert ip any any -> any any '
95+
f'(msg:"check {keyword} {option}"; {keyword}: {option}; sid:{sid};)\n'
96+
)
97+
98+
with tempfile.TemporaryDirectory(prefix="uint-check-") as tmpdir:
8799
rule_file = Path(tmpdir) / "test.rules"
88100
rule_file.write_text("".join(rules))
89101
cmd = [
@@ -100,17 +112,16 @@ def check_keywords(
100112
text=True,
101113
)
102114

103-
# Attribute each error line to the keyword via the sid embedded in the message.
104-
keyword_errors: dict[str, list[str]] = {}
105-
current_sid: Optional[int] = None
115+
# Attribute each error line to its entry via the sid embedded in the message.
116+
errors_by_idx: dict[int, list[str]] = {}
117+
current_idx: Optional[int] = None
106118
for line in proc.stderr.splitlines():
107119
m = SID_RE.search(line)
108120
if m:
109-
current_sid = int(m.group("sid"))
110-
if current_sid is not None and current_sid in sid_to_keyword:
111-
kw = sid_to_keyword[current_sid]
112-
keyword_errors.setdefault(kw, []).append(line)
113-
return keyword_errors
121+
current_idx = int(m.group("sid")) - 1 # convert to 0-based
122+
if current_idx is not None and 0 <= current_idx < len(entries):
123+
errors_by_idx.setdefault(current_idx, []).append(line)
124+
return errors_by_idx
114125

115126

116127
def main() -> int:
@@ -141,31 +152,61 @@ def main() -> int:
141152
f"suricata.yaml not found: {suricata_yaml}. Use --suricata-yaml."
142153
)
143154

144-
keywords = list_multi_uint_keywords(suricata_bin)
145-
if not keywords:
146-
print("No multi-uint keywords found.")
147-
return 0
155+
all_kw = list_keywords(suricata_bin)
156+
157+
# Build the flat list of (keyword, option) entries for a single suricata run,
158+
# alongside metadata needed for reporting.
159+
# Each entry: (group_label, keyword, option)
160+
groups: list[tuple[str, str, str]] = []
148161

149-
print(f"Testing {len(keywords)} multi-uint keyword(s): {', '.join(keywords)}\n")
162+
for name, features in all_kw:
163+
if MULTI_UINT_RE.search(features):
164+
groups.append(("multi-uint", name, ">1,all"))
150165

151-
keyword_errors = check_keywords(keywords, suricata_bin, suricata_yaml)
166+
for type_name, (pattern, option) in UINT_CHECKS.items():
167+
for name, features in all_kw:
168+
if pattern.search(features):
169+
groups.append((type_name, name, option))
152170

153-
for keyword in keywords:
154-
status = "FAIL" if keyword in keyword_errors else "OK"
155-
print(f" [{status}] {keyword}")
171+
if not groups:
172+
print("No matching keywords found.")
173+
return 0
156174

157-
if keyword_errors:
158-
print(f"\n{len(keyword_errors)} keyword(s) failed:\n")
159-
for keyword in keywords:
160-
if keyword not in keyword_errors:
161-
continue
162-
errors = "\n".join(keyword_errors[keyword])
163-
print(f" keyword: {keyword}")
164-
print(f" suricata output:\n " + errors.replace("\n", "\n "))
175+
entries = [(kw, opt) for _, kw, opt in groups]
176+
print(f"Running {len(entries)} check(s) across {len(set(kw for _, kw, _ in groups))} keyword(s)...\n")
177+
178+
errors_by_idx = check_keywords(entries, suricata_bin, suricata_yaml)
179+
180+
# Report grouped by label
181+
seen_labels: list[str] = []
182+
for label in [g[0] for g in groups]:
183+
if label not in seen_labels:
184+
seen_labels.append(label)
185+
186+
any_failure = False
187+
failures: list[tuple[str, str, str, str]] = [] # (label, keyword, option, output)
188+
189+
for label in seen_labels:
190+
label_entries = [(i, kw, opt) for i, (lbl, kw, opt) in enumerate(groups) if lbl == label]
191+
print(f"--- {label} ---")
192+
for idx, keyword, option in label_entries:
193+
failed = idx in errors_by_idx
194+
status = "FAIL" if failed else "OK"
195+
print(f" [{status}] {keyword}: {option}")
196+
if failed:
197+
any_failure = True
198+
failures.append((label, keyword, option, "\n".join(errors_by_idx[idx])))
199+
print()
200+
201+
if any_failure:
202+
print(f"{len(failures)} check(s) failed:\n")
203+
for label, keyword, option, output in failures:
204+
print(f" [{label}] {keyword}: {option}")
205+
print(f" suricata output:\n " + output.replace("\n", "\n "))
165206
print()
166207
return 1
167208

168-
print("\nAll keywords passed.")
209+
print("All checks passed.")
169210
return 0
170211

171212

0 commit comments

Comments
 (0)