99
1010
1111class PingCastleParser :
12+
1213 CVE_REGEX = re .compile (r"(CVE-\d{4}-\d{4,7})" , re .IGNORECASE )
13- # keep severity order for minimal contextual bumping
14+
1415 _SEVERITY_ORDER = ["Info" , "Low" , "Medium" , "High" , "Critical" ]
1516
1617 def get_scan_types (self ):
@@ -36,16 +37,18 @@ def get_findings(self, file, test):
3637 model = rr .findtext ("Model" ) or ""
3738 risk_id = rr .findtext ("RiskId" ) or ""
3839 rationale = rr .findtext ("Rationale" ) or ""
39- # base severity from points
40+
4041 severity = self ._map_points_to_severity (points )
41- # minimal contextual bumping (CVE mention, missing mitigation, DC scope, Exposure category)
4242 severity = self ._apply_contextual_bump (
4343 severity = severity ,
4444 category = category ,
4545 model = model ,
4646 risk_id = risk_id ,
4747 rationale = rationale ,
4848 )
49+ if not severity or severity not in self ._SEVERITY_ORDER :
50+ severity = "Info"
51+
4952 title = f"[PingCastle] { risk_id } ({ category } /{ model } )"
5053 description = self ._compose_risk_rule_description (
5154 domain_fqdn = domain_fqdn ,
@@ -92,8 +95,6 @@ def get_findings(self, file, test):
9295 findings .extend (list (dupes .values ()))
9396 return findings
9497
95- # --- helpers ---
96-
9798 def _compose_risk_rule_description (
9899 self ,
99100 domain_fqdn ,
@@ -140,7 +141,6 @@ def _collect_domain_controllers(self, root):
140141 "ips" : ips ,
141142 "rpc_interfaces" : [],
142143 }
143- # RPC interfaces
144144 for rpc in dc .findall ("RPCInterfacesOpen/HealthcheckDCRPCInterface" ):
145145 dc_info ["rpc_interfaces" ].append ({
146146 "ip" : rpc .attrib .get ("IP" , "" ),
@@ -149,7 +149,6 @@ def _collect_domain_controllers(self, root):
149149 "function" : rpc .attrib .get ("Function" , "" ),
150150 })
151151 dc_infos .append (dc_info )
152- # Endpoints: DC name + IPs
153152 if name :
154153 endpoints .append (Endpoint (host = name ))
155154 endpoints .extend (Endpoint (host = ip ) for ip in ips )
@@ -244,23 +243,19 @@ def _is_dc_specific_risk(risk_id: str, model: str = "", rationale: str = "") ->
244243 rid = (risk_id or "" ).strip ()
245244 mod = (model or "" ).strip ()
246245 rat = (rationale or "" ).strip ().lower ()
247- # 1) Explicit prefixes commonly used by PingCastle for DC-specific checks
248246 dc_prefixes = ("A-DC-" , "S-DC-" )
249247 if rid .startswith (dc_prefixes ):
250248 return True
251- # 2) Known DC-specific RiskIds (extend as needed)
252249 dc_specific_ids = {
253250 "A-DC-Spooler" ,
254251 "A-DC-Coerce" ,
255- "A-AuditDC" , # audit policy on domain controllers
256- "S-DC-SubnetMissing" , # DC subnet declaration incomplete
252+ "A-AuditDC" ,
253+ "S-DC-SubnetMissing" ,
257254 }
258255 if rid in dc_specific_ids :
259256 return True
260- # 3) Model hints: "Audit" with a DC-focused RiskId, or PassTheCredential but DC scoped
261257 if mod == "Audit" and rid .endswith ("DC" ):
262258 return True
263- # 4) Rationale heuristic: mentions DC presence/quantity
264259 dc_markers = (
265260 " from " ,
266261 " dc" ,
@@ -270,7 +265,6 @@ def _is_dc_specific_risk(risk_id: str, model: str = "", rationale: str = "") ->
270265 )
271266 return bool (any (marker in rat for marker in dc_markers ))
272267
273- # --- minimal contextual bumping ---
274268 def _apply_contextual_bump (self , severity : str , category : str = "" , model : str = "" ,
275269 risk_id : str = "" , rationale : str = "" ) -> str :
276270 """
@@ -280,22 +274,22 @@ def _apply_contextual_bump(self, severity: str, category: str = "", model: str =
280274 - If DC-specific -> bump by 1 level.
281275 - If category is 'Exposure' -> bump by 1 level.
282276 """
283- current = severity
284- idx = self ._SEVERITY_ORDER .index (current )
277+ if not severity or severity not in self ._SEVERITY_ORDER :
278+ severity = "Info"
279+ idx = self ._SEVERITY_ORDER .index (severity )
285280 rat = (rationale or "" ).lower ()
286281 cat = (category or "" ).strip ().lower ()
287282
288- # CVE present
289283 if self .CVE_REGEX .search (rationale or "" ):
290284 idx = min (idx + 1 , len (self ._SEVERITY_ORDER ) - 1 )
291285 mitigation_markers = ("mitigation" , "not set" , "disabled" , "missing" , "not enabled" , "enable" )
292286 if any (m in rat for m in mitigation_markers ):
293287 idx = max (idx , self ._SEVERITY_ORDER .index ("Medium" ))
294288
295- # DC-specific
296289 if self ._is_dc_specific_risk (risk_id , model , rationale ):
297290 idx = min (idx + 1 , len (self ._SEVERITY_ORDER ) - 1 )
298291
299- # Exposure category
300292 if cat == "exposure" :
301293 idx = min (idx + 1 , len (self ._SEVERITY_ORDER ) - 1 )
294+
295+ return self ._SEVERITY_ORDER [idx ]
0 commit comments