2222
2323MULTI_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
2634def 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
6872SID_RE = re .compile (r"sid:(?P<sid>\d+)" )
6973
7074
7175def 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
116127def 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 ("\n All keywords passed." )
209+ print ("All checks passed." )
169210 return 0
170211
171212
0 commit comments