@@ -1166,11 +1166,9 @@ async def _build_result_contract(
11661166 structured_result["finding_groups"] = build_finding_groups(normalized_findings)
11671167 structured_result["asset_summary"] = build_asset_summary(normalized_findings, asset_services)
11681168 structured_result["scan_diff"] = build_scan_diff(normalized_findings, previous_findings)
1169- severity_counts: Dict[str, int] = {}
1170- for f in normalized_findings:
1171- sev = str(f.get("severity", "info")).lower()
1172- severity_counts[sev] = severity_counts.get(sev, 0) + 1
1173- structured_result["severity_counts"] = severity_counts
1169+ from collections import Counter
1170+ severity_counts = Counter(str(f.get("severity", "info")).lower() for f in normalized_findings)
1171+ structured_result["severity_counts"] = dict(severity_counts)
11741172 structured_result["count"] = len(normalized_findings)
11751173 return structured_result, previous_findings, asset_services
11761174
@@ -1299,20 +1297,104 @@ async def _upsert_findings_and_report(self, db, task_id: str, owner_id: str, plu
12991297 )
13001298 findings_data: List[Dict[str, Any]] = []
13011299 async with db.transaction():
1300+ insert_sql = """ INSERT INTO findings (
1301+ id , owner_id , task_id , plugin_id , title , category , severity , target , description ,
1302+ remediation , proof , cvss , cve , metadata_json , discovered_at , exploitability ,
1303+ confidence , validated , validation_method , confidence_reason , finding_kind ,
1304+ finding_group_id , asset_id , first_seen_at , last_seen_at , occurrence_count ,
1305+ corroborating_sources_json , evidence_count , analyst_status , retest_status ,
1306+ evidence_json , asset_refs_json , service_fingerprint , cpe , references_json ,
1307+ asset_exposure , risk_score , risk_factors_json
1308+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1309+ ON CONFLICT (owner_id , finding_group_id ) DO UPDATE SET
1310+ occurrence_count = occurrence_count + 1 ,
1311+ last_seen_at = excluded .last_seen_at ,
1312+ evidence_count = excluded .evidence_count ,
1313+ corroborating_sources_json = excluded .corroborating_sources_json """
1314+
1315+ batch_data = []
13021316 for finding in structured_result.get("findings", []):
1303- findings_data.append(
1304- await self._persist_finding(
1305- db,
1306- owner_id=owner_id,
1307- task_id=task_id,
1308- plugin_id=plugin_id,
1309- target=target,
1310- finding=finding,
1311- )
1317+ finding_id = generate_id("finding")
1318+ discovered = datetime.now(timezone.utc)
1319+ target_value = str(finding.get("target") or target)
1320+ metadata = finding.get("metadata", {}) if isinstance(finding.get("metadata"), dict) else {}
1321+ evidence = finding.get("evidence", []) if isinstance(finding.get("evidence"), list) else []
1322+ asset_refs = finding.get("asset_refs", []) if isinstance(finding.get("asset_refs"), list) else []
1323+ exploitability = finding.get("exploitability")
1324+ asset_exposure = finding.get("asset_exposure")
1325+ confidence = finding.get("confidence")
1326+ finding_group_id = self._compute_finding_group_id(
1327+ owner_id=owner_id,
1328+ target=target_value,
1329+ title=finding["title"],
1330+ category=finding["category"],
1331+ severity=finding["severity"],
1332+ host=finding.get("host"),
1333+ port=finding.get("port"),
1334+ path=finding.get("path"),
1335+ cve=finding.get("cve"),
1336+ )
1337+ references = finding.get("references", []) if isinstance(finding.get("references"), list) else []
1338+ corroborating_sources = finding.get("corroborating_sources", []) if isinstance(finding.get("corroborating_sources"), list) else []
1339+ first_seen_at = str(finding.get("first_seen_at") or to_utc_iso(discovered))
1340+ last_seen_at = str(finding.get("last_seen_at") or to_utc_iso(discovered))
1341+ occurrence_count = int(finding.get("occurrence_count") or 1)
1342+ evidence_count = int(finding.get("evidence_count") or len(evidence))
1343+ risk_score = compute_risk_score(
1344+ severity=finding["severity"],
1345+ exploitability=exploitability,
1346+ asset_exposure=asset_exposure,
1347+ discovered_at=discovered,
1348+ confidence=confidence,
13121349 )
1350+ risk_factors = compute_risk_factors(
1351+ severity=finding["severity"],
1352+ exploitability=exploitability,
1353+ asset_exposure=asset_exposure,
1354+ discovered_at=discovered,
1355+ confidence=confidence,
1356+ risk_score=risk_score,
1357+ )
1358+
1359+ batch_data.append((
1360+ finding_id, owner_id, task_id, plugin_id, finding["title"], finding["category"],
1361+ finding["severity"], target_value, finding["description"], finding.get("remediation", ""),
1362+ finding.get("proof"), finding.get("cvss"), finding.get("cve"), json.dumps(metadata),
1363+ to_utc_iso(discovered), exploitability, confidence, 1 if finding.get("validated") else 0,
1364+ finding.get("validation_method"), finding.get("confidence_reason"),
1365+ str(finding.get("finding_kind") or "observation"), finding_group_id, finding.get("asset_id"),
1366+ first_seen_at, last_seen_at, occurrence_count, json.dumps(corroborating_sources),
1367+ evidence_count, str(finding.get("analyst_status") or "new"),
1368+ str(finding.get("retest_status") or "not_requested"), json.dumps(evidence),
1369+ json.dumps(asset_refs), finding.get("service_fingerprint"), finding.get("cpe"),
1370+ json.dumps(references), asset_exposure, risk_score, json.dumps(risk_factors)
1371+ ))
1372+
1373+ findings_data.append({
1374+ **finding,
1375+ "id": finding_id,
1376+ "plugin_id": plugin_id,
1377+ "target": target_value,
1378+ "discovered_at": to_utc_iso(discovered),
1379+ "metadata": metadata,
1380+ "evidence": evidence,
1381+ "asset_refs": asset_refs,
1382+ "references": references,
1383+ "corroborating_sources": corroborating_sources,
1384+ "first_seen_at": first_seen_at,
1385+ "last_seen_at": last_seen_at,
1386+ "occurrence_count": occurrence_count,
1387+ "evidence_count": evidence_count,
1388+ "risk_score": risk_score,
1389+ "risk_factors": risk_factors,
1390+ })
1391+
1392+ if batch_data:
1393+ await db.executemany(insert_sql, batch_data)
13131394
13141395 structured_result["findings"] = findings_data
1315- structured_result["severity_counts"] = self._build_severity_counts(findings_data)
1396+ from collections import Counter
1397+ structured_result["severity_counts"] = dict(Counter(str(f.get("severity", "info")).lower() for f in findings_data))
13161398 structured_result["finding_groups"] = build_finding_groups(findings_data)
13171399 structured_result["asset_summary"] = build_asset_summary(findings_data, asset_services)
13181400 structured_result["scan_diff"] = build_scan_diff(findings_data, previous_findings)
0 commit comments