@@ -21,29 +21,21 @@ def get_label_for_scan_types(self, scan_type):
2121 def get_description_for_scan_types (self , scan_type ):
2222 return "PingCastle XML export"
2323
24- # --- public API ---
25-
2624 def get_findings (self , file , test ):
2725 tree = parse (file )
2826 root = tree .getroot ()
2927 dupes = {}
3028 report_date = self ._parse_datetime (root .findtext ("GenerationDate" ))
3129 domain_fqdn = root .findtext ("DomainFQDN" ) or ""
32- # Collect DC info & endpoints for enrichment
3330 dc_infos , dc_endpoints = self ._collect_domain_controllers (root )
34- # Store (not in dupes; summary is standalone)
3531 findings = []
36-
37- # 1) Risk rules -> findings
3832 for rr in root .findall ("RiskRules/HealthcheckRiskRule" ):
3933 points = self ._safe_int (rr .findtext ("Points" ))
4034 category = rr .findtext ("Category" ) or ""
4135 model = rr .findtext ("Model" ) or ""
4236 risk_id = rr .findtext ("RiskId" ) or ""
4337 rationale = rr .findtext ("Rationale" ) or ""
44-
4538 severity = self ._map_points_to_severity (points )
46-
4739 title = f"[PingCastle] { risk_id } ({ category } /{ model } )"
4840 description = self ._compose_risk_rule_description (
4941 domain_fqdn = domain_fqdn ,
@@ -55,7 +47,6 @@ def get_findings(self, file, test):
5547 dc_infos = dc_infos ,
5648 root = root ,
5749 )
58-
5950 finding = Finding (
6051 title = title ,
6152 test = test ,
@@ -67,40 +58,27 @@ def get_findings(self, file, test):
6758 )
6859 if report_date :
6960 finding .date = report_date
70-
71- # CVE detection inside rationale (rare but helpful)
7261 cves = list (self .CVE_REGEX .findall (rationale or "" ))
7362 if cves :
7463 finding .unsaved_vulnerability_ids = cves
75-
76- # Attach endpoints: DC-specific risks get DC endpoints; others get domain endpoint
7764 finding .unsaved_endpoints = []
7865 if self ._is_dc_specific_risk (risk_id ):
7966 finding .unsaved_endpoints .extend (dc_endpoints )
8067 elif domain_fqdn :
8168 finding .unsaved_endpoints .append (Endpoint (host = domain_fqdn ))
82-
83- # Special enrichment for A-DC-Coerce: include RPC interfaces details if present
8469 if risk_id == "A-DC-Coerce" :
8570 self ._enrich_coerce_with_rpc_interfaces (finding , dc_infos )
86-
87- # Special enrichment for A-DC-Spooler: reflect RemoteSpoolerDetected
8871 if risk_id == "A-DC-Spooler" :
8972 self ._enrich_spooler_status (finding , dc_infos )
90-
91- # Special enrichment for password length (A-MinPwdLen): add current GPO settings if present
9273 if risk_id == "A-MinPwdLen" :
9374 self ._enrich_password_policy (finding , root )
94-
95- # De-duplicate by risk_id
9675 dupe_key = risk_id
9776 if dupe_key in dupes :
9877 existing = dupes [dupe_key ]
9978 existing .description += "\n \n -----\n \n " + finding .description
10079 existing .unsaved_endpoints .extend (finding .unsaved_endpoints )
10180 else :
10281 dupes [dupe_key ] = finding
103-
10482 findings .extend (list (dupes .values ()))
10583 return findings
10684
@@ -125,10 +103,7 @@ def _compose_risk_rule_description(
125103 lines .append (f"**Points**: `{ points } `" )
126104 if rationale :
127105 lines .append (f"**Rationale**: { rationale } " )
128-
129- # Context snippets, depending on category/model/risk_id
130106 if risk_id .startswith ("A-DC-" ) or "DomainControllers" in root .tag :
131- # Attach short DC summary table
132107 if dc_infos :
133108 lines .append ("\n #### Domain Controllers" )
134109 for dc in dc_infos :
@@ -149,15 +124,13 @@ def _collect_domain_controllers(self, root):
149124 remote_spooler = dc .findtext ("RemoteSpoolerDetected" ) or "false"
150125 ip_elems = dc .findall ("IP/string" )
151126 ips = [ip_elem .text for ip_elem in ip_elems if ip_elem is not None and ip_elem .text ]
152-
153127 dc_info = {
154128 "name" : name ,
155129 "os" : os ,
156130 "remote_spooler" : remote_spooler .lower () == "true" ,
157131 "ips" : ips ,
158132 "rpc_interfaces" : [],
159133 }
160-
161134 # RPC interfaces
162135 for rpc in dc .findall ("RPCInterfacesOpen/HealthcheckDCRPCInterface" ):
163136 dc_info ["rpc_interfaces" ].append ({
@@ -166,9 +139,7 @@ def _collect_domain_controllers(self, root):
166139 "opnum" : rpc .attrib .get ("OpNum" , "" ),
167140 "function" : rpc .attrib .get ("Function" , "" ),
168141 })
169-
170142 dc_infos .append (dc_info )
171-
172143 # Endpoints: DC name + IPs
173144 if name :
174145 endpoints .append (Endpoint (host = name ))
@@ -264,12 +235,10 @@ def _is_dc_specific_risk(risk_id: str, model: str = "", rationale: str = "") ->
264235 rid = (risk_id or "" ).strip ()
265236 mod = (model or "" ).strip ()
266237 rat = (rationale or "" ).strip ().lower ()
267-
268238 # 1) Explicit prefixes commonly used by PingCastle for DC-specific checks
269239 dc_prefixes = ("A-DC-" , "S-DC-" )
270240 if rid .startswith (dc_prefixes ):
271241 return True
272-
273242 # 2) Known DC-specific RiskIds (extend as needed)
274243 dc_specific_ids = {
275244 "A-DC-Spooler" ,
@@ -279,11 +248,9 @@ def _is_dc_specific_risk(risk_id: str, model: str = "", rationale: str = "") ->
279248 }
280249 if rid in dc_specific_ids :
281250 return True
282-
283251 # 3) Model hints: "Audit" with a DC-focused RiskId, or PassTheCredential but DC scoped
284252 if mod == "Audit" and rid .endswith ("DC" ):
285253 return True
286-
287254 # 4) Rationale heuristic: mentions DC presence/quantity
288255 # examples: "remotely accessible from 1 DC", "on domain controllers", "DCs"
289256 dc_markers = (
0 commit comments