5050
5151# RFC5424 format patterns
5252FIELD_PATTERNS_RFC5424 = {
53- "dst" : re .compile (r'\bDST="([^"]+)"' ),
54- "proto" : re .compile (r'\bPROTO="(\w+)"' ),
55- "host" : re .compile (r'\bDSTHOST="([^"]+)"' ),
56- "port" : re .compile (r'\bDPT="(\d+)"' ),
53+ "dst" : re .compile (r'\bDST="([^"]+)"' ),
54+ "proto" : re .compile (r'\bPROTO="(\w+)"' ),
55+ "host" : re .compile (r'\bDSTHOST="([^"]+)"' ),
56+ "port" : re .compile (r'\bDPT="(\d+)"' ),
57+ "path" : re .compile (r'\bPATH="([^"]+)"' ),
58+ "cmdline" : re .compile (r'\bCMDLINE="([^"]*)"' ),
5759}
5860
5961
62+ def classify_caller (path : str , cmdline : str ) -> str :
63+ """Classify caller into a category based on binary path and command line."""
64+ import os
65+ basename = os .path .basename (path )
66+
67+ # Direct binary classifications
68+ if basename == "clairctl" :
69+ return "clair"
70+ if basename == "oc-mirror" or "/oc-mirror" in path :
71+ return "oc-mirror"
72+ if basename == "oc" :
73+ return "oc"
74+ if basename in ("podman" , "skopeo" ):
75+ return "podman"
76+ if basename == "openshift-install" :
77+ return "openshift-install"
78+
79+ # Python calls — classify by cmdline keywords
80+ if basename .startswith ("python" ):
81+ if "pip" in cmdline or "pip3" in cmdline :
82+ return "pip"
83+ if "dnf" in cmdline :
84+ return "dnf"
85+ if "ansible-galaxy" in cmdline :
86+ return "ansible"
87+ if "ansible-tmp-" in cmdline or "/ansible/tmp/" in cmdline :
88+ return "ansible"
89+ # Generic python caller without specific markers
90+ return "other"
91+
92+ # Fallback
93+ return "other"
94+
95+
6096def is_excluded (ip_str : str ) -> bool :
6197 try :
6298 addr = ipaddress .ip_address (ip_str )
@@ -92,7 +128,7 @@ def parse_message(message: str) -> Optional[Dict]:
92128
93129
94130def parse_message_rfc5424 (message : str ) -> Optional [Dict ]:
95- """Parse RFC5424 syslog format: DST="ip" DSTHOST="host" DPT="port" PROTO="proto" """
131+ """Parse RFC5424 syslog format: DST="ip" DSTHOST="host" DPT="port" PROTO="proto" PATH="..." CMDLINE="..." """
96132 fields = {}
97133 for name , pat in FIELD_PATTERNS_RFC5424 .items ():
98134 m = pat .search (message )
@@ -118,7 +154,12 @@ def parse_message_rfc5424(message: str) -> Optional[Dict]:
118154 except ValueError :
119155 return None
120156
121- return {"dns" : host , "protocol" : proto , "port" : port }
157+ # Classify caller from PATH and CMDLINE
158+ path = fields .get ("path" , "" )
159+ cmdline = fields .get ("cmdline" , "" )
160+ category = classify_caller (path , cmdline ) if path else "other"
161+
162+ return {"dns" : host , "protocol" : proto , "port" : port , "category" : category }
122163
123164
124165def parse_message_old (message : str ) -> Optional [Dict ]:
@@ -152,12 +193,13 @@ def parse_message_old(message: str) -> Optional[Dict]:
152193 except ValueError :
153194 return None
154195
155- return {"dns" : host , "protocol" : proto , "port" : port }
196+ # Old format doesn't have PATH/CMDLINE — category is unknown
197+ return {"dns" : host , "protocol" : proto , "port" : port , "category" : "other" }
156198
157199
158200def parse_log_file (path : str ) -> List [Dict ]:
159201 """Read a journald JSON or plain text log file and return deduplicated, sorted connection entries."""
160- seen : Set [Tuple [str , str , int ]] = set ()
202+ seen : Set [Tuple [str , str , int , str ]] = set ()
161203 entries : List [Dict ] = []
162204
163205 opener = gzip .open if path .endswith (".gz" ) else open
@@ -184,23 +226,36 @@ def parse_log_file(path: str) -> List[Dict]:
184226 if not entry :
185227 continue
186228
187- key = (entry ["dns" ], entry ["protocol" ], entry ["port" ])
229+ key = (entry ["dns" ], entry ["protocol" ], entry ["port" ], entry [ "category" ] )
188230 if key not in seen :
189231 seen .add (key )
190232 entries .append (entry )
191233
192- return sorted (entries , key = lambda e : (e ["dns" ], e ["protocol" ], e ["port" ]))
234+ return sorted (entries , key = lambda e : (e ["category" ], e [ " dns" ], e ["protocol" ], e ["port" ]))
193235
194236
195237def render_yaml (mode : str , entries : List [Dict ]) -> str :
238+ """Render entries as grouped YAML, sorted by category then dns."""
239+ from collections import defaultdict
240+
196241 lines = [f" { mode } :" ]
197242 if not entries :
198- lines .append (" []" )
199- else :
200- for e in entries :
201- lines .append (f" - dns: { e ['dns' ]} " )
202- lines .append (f" protocol: { e ['protocol' ]} " )
203- lines .append (f" port: { e ['port' ]} " )
243+ lines .append (" {}" )
244+ return "\n " .join (lines )
245+
246+ # Group by category
247+ by_category = defaultdict (list )
248+ for e in entries :
249+ by_category [e ["category" ]].append (e )
250+
251+ # Sort categories alphabetically
252+ for category in sorted (by_category .keys ()):
253+ lines .append (f" { category } :" )
254+ for e in by_category [category ]:
255+ lines .append (f" - dns: { e ['dns' ]} " )
256+ lines .append (f" protocol: { e ['protocol' ]} " )
257+ lines .append (f" port: { e ['port' ]} " )
258+
204259 return "\n " .join (lines )
205260
206261
0 commit comments