From c0a14fa3ea56f5a6a9ddc82a0acc9f3bc09afdf6 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Mon, 8 Jun 2026 18:33:06 -0600 Subject: [PATCH 1/6] feat(parser): add PICUS Breach and Attack Simulation parser Add a PicusParser that ingests Picus BAS result CSV exports under the "PICUS Scan" scan type. Each row is an attack action; findings are active when the threat was Not Blocked (an open control gap) and inactive when blocked. Severity is taken from threatSeverity, MITRE tactic/technique/ sub-technique and attack category become tags, and CVE/CWE are mapped when present. Authored by T. Walker - DefectDojo --- dojo/tools/picus/__init__.py | 0 dojo/tools/picus/parser.py | 133 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 dojo/tools/picus/__init__.py create mode 100644 dojo/tools/picus/parser.py diff --git a/dojo/tools/picus/__init__.py b/dojo/tools/picus/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/picus/parser.py b/dojo/tools/picus/parser.py new file mode 100644 index 00000000000..e1d28826921 --- /dev/null +++ b/dojo/tools/picus/parser.py @@ -0,0 +1,133 @@ +import csv +import io + +from dojo.models import Finding + +SEVERITY_MAPPING = { + "Critical": "Critical", + "High": "High", + "Medium": "Medium", + "Low": "Low", + "Info": "Info", + "Informational": "Info", +} + + +class PicusParser: + + """Parser for Picus Breach and Attack Simulation (BAS) CSV result exports.""" + + def get_scan_types(self): + return ["PICUS Scan"] + + def get_label_for_scan_types(self, scan_type): + return scan_type + + def get_description_for_scan_types(self, scan_type): + return "Import Picus Breach and Attack Simulation results (CSV)." + + def get_findings(self, filename, test): + content = filename.read() + if isinstance(content, bytes): + content = content.decode("utf-8-sig") + reader = csv.DictReader(io.StringIO(content), delimiter=",", quotechar='"') + + return [self._build_finding(row, test) for row in reader] + + def _build_finding(self, row, test): + def get(key): + return (row.get(key) or "").strip() + + threat_name = get("threatName") + action_name = get("actionName") + prevention = get("threatPreventionResult") + + title = f"{threat_name} - {action_name}" if action_name else threat_name + if len(title) > 500: + title = title[:497] + "..." + + severity = SEVERITY_MAPPING.get(get("threatSeverity"), "Info") + + description = self._build_description(get) + mitigation = self._build_mitigation(get) + + finding = Finding( + test=test, + title=title, + severity=severity, + description=description, + mitigation=mitigation, + component_name=get("affectedProducts") or None, + # In BAS, a finding is active when the attack was NOT blocked. + active=prevention == "Not Blocked", + static_finding=False, + dynamic_finding=True, + ) + + # actionId is Picus's native, run-stable action identifier; it drives + # hashcode deduplication so the same action matches across re-imports. + action_id = get("actionId") + if action_id: + finding.vuln_id_from_tool = action_id + + cwe = get("cwe") + if cwe.isdigit(): + finding.cwe = int(cwe) + + cves = [c.strip() for c in get("cve").split(",") if c.strip()] + if cves: + finding.unsaved_vulnerability_ids = cves + + tags = self._build_tags(get) + if tags: + finding.unsaved_tags = tags + + return finding + + def _build_tags(self, get): + tags = [] + for key in ("actionMitreTactic", "actionMitreTechnique", "actionMitreSubtechnique", "attackCategory"): + value = get(key) + if value and value not in tags: + tags.append(value) + return tags + + def _build_mitigation(self, get): + prevention = get("threatPreventionResult") + if prevention == "Not Blocked": + return ( + "The simulated attack was NOT blocked by existing preventive controls. " + "Review and tune the relevant security controls to block this technique." + ) + if prevention == "Blocked": + return "The simulated attack was blocked by existing preventive controls." + return "" + + def _build_description(self, get): + fields = [ + ("Threat", "threatName"), + ("Action", "actionName"), + ("Action Description", "actionDescription"), + ("Attack Category", "attackCategory"), + ("Attack Modules", "attackModules"), + ("Threat Severity", "threatSeverity"), + ("Prevention Result", "threatPreventionResult"), + ("Detection Log Result", "threatDetectionLogResult"), + ("Detection Alert Result", "threatDetectionAlertResult"), + ("MITRE Tactic", "actionMitreTactic"), + ("MITRE Technique", "actionMitreTechnique"), + ("MITRE Sub-technique", "actionMitreSubtechnique"), + ("Affected OS", "affectedOs"), + ("Affected Platforms", "affectedPlatforms"), + ("Affected Products", "affectedProducts"), + ("Action Payload", "actionPayload"), + ("Simulation Run Id", "simulationRunId"), + ("Action Id", "actionId"), + ] + lines = ["| Field | Value |", "| --- | --- |"] + for label, key in fields: + value = get(key) + if value: + value = value.replace("|", "\\|") + lines.append(f"| {label} | {value} |") + return "\n".join(lines) From 6e35e281ea1802941d6818fe2c98ff84f5299826 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Mon, 8 Jun 2026 18:33:14 -0600 Subject: [PATCH 2/6] test(parser): add PICUS parser fixtures and unit tests Add no_vuln, one_vuln, and many_vulns CSV fixtures plus 20 unit tests covering severity mapping, active/inactive logic, vuln_id_from_tool, CVE/CWE extraction, MITRE tags, title truncation, and the markdown description. All fixtures use fabricated, anonymized data. Authored by T. Walker - DefectDojo --- unittests/scans/picus/many_vulns.csv | 7 ++ unittests/scans/picus/no_vuln.csv | 1 + unittests/scans/picus/one_vuln.csv | 2 + unittests/tools/test_picus_parser.py | 138 +++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 unittests/scans/picus/many_vulns.csv create mode 100644 unittests/scans/picus/no_vuln.csv create mode 100644 unittests/scans/picus/one_vuln.csv create mode 100644 unittests/tools/test_picus_parser.py diff --git a/unittests/scans/picus/many_vulns.csv b/unittests/scans/picus/many_vulns.csv new file mode 100644 index 00000000000..092fc46b38e --- /dev/null +++ b/unittests/scans/picus/many_vulns.csv @@ -0,0 +1,7 @@ +simulationRunId,threatId,threatName,attackModules,threatSeverity,threatReleaseDateEpoch,threatReleaseDateDateTimeUTC,threatPreventionResult,threatDetectionLogResult,threatDetectionAlertResult,actionId,actionName,actionDescription,affectedOs,affectedPlatforms,affectedProducts,isPrivileged,actionMitreTactic,actionMitreTechnique,actionMitreSubtechnique,nistCapabilities,actionPayload,actionPayloadOutputTabLink,actionPreventionResult,actionPreventionSources,actionFileName,actionSha1,actionSha256,actionMd5,cve,cwe,attackStartedTimeUTC,attackEndedTimeUTC,attackLoggedTimeUTC,attackAlertedTimeUTC,actionProtocol,actionProtocolPreventionResult,actionProtocolPreventionSources,actionProtocolDetectionLogResult,actionProtocolDetectionAlertResult,actionDetectionIntegration,actionDetectionLogResult,actionDetectionAlertResult,attackCategory,actionLogsTabLink,genericMitigationsTabLink,vendorName,platform,productVersion,signatureId,signatureName,severity,signatureVersion,detectionContentTabLink,attackerUser,tenantId,subscription,location,rewindStatus,output,executionSteps,rewindSteps,manualRewindSteps +2001,1,Fileless Malware via PowerShell,Endpoint,Critical,,02/10/2023,Not Blocked,Not Logged,Not Alerted,1001,PowerShell Download Cradle Execution,Executes an in-memory PowerShell download cradle to retrieve a payload without touching disk,Windows,Platform X,Product Y,No,TA0002,T1059,T1059.001,,,,,,,,,,CVE-2021-44228,78,02/10/2023,02/10/2023,,,,,,,,,,,Malicious Code,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001001,SIG-T1059,High,v1,,Anonymous1,341,,,,,,, +2001,1,Credential Dumping,Endpoint,High,,02/10/2023,Blocked,Logged,Alerted,1002,LSASS Memory Dump via comsvcs.dll,Dumps LSASS process memory to extract credentials,Windows,Platform X,Product Y,No,TA0006,T1003,T1003.001,,,,,,,,,,,200,02/10/2023,02/10/2023,,,,,,,,,,,Data Theft,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001002,SIG-T1003,High,v1,,Anonymous1,341,,,,,,, +2001,1,Lateral Movement Attempt,Endpoint,Medium,,02/10/2023,Not Blocked,Logged,Not Alerted,1003,SMB Share Enumeration,Enumerates accessible SMB shares across the subnet,Windows,Platform X,Product Y,No,TA0008,T1021,T1021.002,,,,,,,,,,,,02/10/2023,02/10/2023,,,,,,,,,,,Reconnaissance,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001003,SIG-T1021,Medium,v1,,Anonymous1,341,,,,,,, +2001,1,Benign Beacon Test,Endpoint,Low,,02/10/2023,Blocked,Logged,Alerted,1004,Low Risk Connectivity Check,Performs a low-risk outbound connectivity check,Windows,Platform X,Product Y,No,TA0011,T1071,,,,,,,,,,,,,02/10/2023,02/10/2023,,,,,,,,,,,Network Attack,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001004,SIG-T1071,Low,v1,,Anonymous1,341,,,,,,, +2001,1,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,Endpoint,Critical,,02/10/2023,Not Blocked,Not Logged,Not Alerted,1005,YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,Threat and action names are intentionally long to validate title truncation behavior,Windows,Platform X,Product Y,No,TA0040,T1486,,,,,,,,,,,,,02/10/2023,02/10/2023,,,,,,,,,,,Malicious Code,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001005,SIG-T1486,Critical,v1,,Anonymous1,341,,,,,,, +2001,1,Phishing Payload Delivery,Endpoint,High,,02/10/2023,Not Blocked,Not Logged,Alerted,1006,Macro-Enabled Document Drop,Delivers a macro-enabled document that downloads a second-stage payload,Windows,Platform X,Product Y,No,TA0001,T1566,T1566.001,,,,,,,,,,CVE-2017-11882,119,02/10/2023,02/10/2023,,,,,,,,,,,Social Engineering,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001006,SIG-T1566,Medium,v1,,Anonymous1,341,,,,,,, diff --git a/unittests/scans/picus/no_vuln.csv b/unittests/scans/picus/no_vuln.csv new file mode 100644 index 00000000000..6a1c2c94113 --- /dev/null +++ b/unittests/scans/picus/no_vuln.csv @@ -0,0 +1 @@ +simulationRunId,threatId,threatName,attackModules,threatSeverity,threatReleaseDateEpoch,threatReleaseDateDateTimeUTC,threatPreventionResult,threatDetectionLogResult,threatDetectionAlertResult,actionId,actionName,actionDescription,affectedOs,affectedPlatforms,affectedProducts,isPrivileged,actionMitreTactic,actionMitreTechnique,actionMitreSubtechnique,nistCapabilities,actionPayload,actionPayloadOutputTabLink,actionPreventionResult,actionPreventionSources,actionFileName,actionSha1,actionSha256,actionMd5,cve,cwe,attackStartedTimeUTC,attackEndedTimeUTC,attackLoggedTimeUTC,attackAlertedTimeUTC,actionProtocol,actionProtocolPreventionResult,actionProtocolPreventionSources,actionProtocolDetectionLogResult,actionProtocolDetectionAlertResult,actionDetectionIntegration,actionDetectionLogResult,actionDetectionAlertResult,attackCategory,actionLogsTabLink,genericMitigationsTabLink,vendorName,platform,productVersion,signatureId,signatureName,severity,signatureVersion,detectionContentTabLink,attackerUser,tenantId,subscription,location,rewindStatus,output,executionSteps,rewindSteps,manualRewindSteps diff --git a/unittests/scans/picus/one_vuln.csv b/unittests/scans/picus/one_vuln.csv new file mode 100644 index 00000000000..dfd2e30657c --- /dev/null +++ b/unittests/scans/picus/one_vuln.csv @@ -0,0 +1,2 @@ +simulationRunId,threatId,threatName,attackModules,threatSeverity,threatReleaseDateEpoch,threatReleaseDateDateTimeUTC,threatPreventionResult,threatDetectionLogResult,threatDetectionAlertResult,actionId,actionName,actionDescription,affectedOs,affectedPlatforms,affectedProducts,isPrivileged,actionMitreTactic,actionMitreTechnique,actionMitreSubtechnique,nistCapabilities,actionPayload,actionPayloadOutputTabLink,actionPreventionResult,actionPreventionSources,actionFileName,actionSha1,actionSha256,actionMd5,cve,cwe,attackStartedTimeUTC,attackEndedTimeUTC,attackLoggedTimeUTC,attackAlertedTimeUTC,actionProtocol,actionProtocolPreventionResult,actionProtocolPreventionSources,actionProtocolDetectionLogResult,actionProtocolDetectionAlertResult,actionDetectionIntegration,actionDetectionLogResult,actionDetectionAlertResult,attackCategory,actionLogsTabLink,genericMitigationsTabLink,vendorName,platform,productVersion,signatureId,signatureName,severity,signatureVersion,detectionContentTabLink,attackerUser,tenantId,subscription,location,rewindStatus,output,executionSteps,rewindSteps,manualRewindSteps +2001,1,Fileless Malware via PowerShell,Endpoint,Critical,,02/10/2023,Not Blocked,Not Logged,Not Alerted,1001,PowerShell Download Cradle Execution,Executes an in-memory PowerShell download cradle to retrieve a payload without touching disk,Windows,Platform X,Product Y,No,TA0002,T1059,T1059.001,,,,,,,,,,CVE-2021-44228,78,02/10/2023,02/10/2023,,,,,,,,,,,Malicious Code,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001001,SIG-T1059,High,v1,,Anonymous1,341,,,,,,, diff --git a/unittests/tools/test_picus_parser.py b/unittests/tools/test_picus_parser.py new file mode 100644 index 00000000000..c8e5ac13603 --- /dev/null +++ b/unittests/tools/test_picus_parser.py @@ -0,0 +1,138 @@ +from dojo.models import Test +from dojo.tools.picus.parser import PicusParser +from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path + + +class TestPicusParser(DojoTestCase): + + def test_parse_no_findings(self): + with (get_unit_tests_scans_path("picus") / "no_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_parse_one_finding(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + + def test_parse_many_findings(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(6, len(findings)) + + def test_title_combines_threat_and_action(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual( + "Fileless Malware via PowerShell - PowerShell Download Cradle Execution", + findings[0].title, + ) + + def test_severity_critical(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual("Critical", findings[0].severity) + + def test_severity_high(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual("High", findings[1].severity) + + def test_severity_medium(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual("Medium", findings[2].severity) + + def test_severity_low(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual("Low", findings[3].severity) + + def test_severity_uses_threat_severity_not_action_severity(self): + # Row 0 has threatSeverity=Critical but action-level severity=High. + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual("Critical", findings[0].severity) + + def test_active_when_not_blocked(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertTrue(findings[0].active) + + def test_inactive_when_blocked(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertFalse(findings[1].active) + + def test_vuln_id_from_action_id(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual("1001", findings[0].vuln_id_from_tool) + + def test_cve_extracted(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(["CVE-2021-44228"], findings[0].unsaved_vulnerability_ids) + + def test_cwe_extracted(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(78, findings[0].cwe) + + def test_no_cve_when_field_empty(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + # Row index 1 (Credential Dumping) has no CVE. + self.assertIsNone(findings[1].unsaved_vulnerability_ids) + + def test_mitre_tags(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual( + ["TA0002", "T1059", "T1059.001", "Malicious Code"], + findings[0].unsaved_tags, + ) + + def test_component_name_from_affected_products(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual("Product Y", findings[0].component_name) + + def test_title_truncated_at_500_chars(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + # Row index 4 has 300+300 char threat/action names. + self.assertEqual(500, len(findings[4].title)) + self.assertTrue(findings[4].title.endswith("...")) + + def test_static_and_dynamic_flags(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertFalse(findings[0].static_finding) + self.assertTrue(findings[0].dynamic_finding) + + def test_description_is_markdown_table(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertIn("| Field | Value |", findings[0].description) + self.assertIn("| Attack Category | Malicious Code |", findings[0].description) From 52edddb335a5ff4c3a47e4258e4611b5215f1b72 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Mon, 8 Jun 2026 18:33:20 -0600 Subject: [PATCH 3/6] feat(parser): register PICUS deduplication config Register "PICUS Scan" with DEDUPE_ALGO_HASH_CODE keyed on the single stable field vuln_id_from_tool (the native Picus actionId). Keying on actionId alone lets re-imported runs match prior findings so statuses update across runs rather than creating duplicates. Authored by T. Walker - DefectDojo --- dojo/settings/settings.dist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index abe31dec9f9..6599d4e7e04 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1108,6 +1108,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "Xygeni SCA Scan": ["vulnerability_ids", "component_name", "component_version"], "Qualys VMDR": ["title", "component_name", "vuln_id_from_tool"], "Alert Logic Scan": ["title", "component_name", "vuln_id_from_tool"], + "PICUS Scan": ["vuln_id_from_tool"], } # Override the hardcoded settings here via the env var @@ -1383,6 +1384,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "Xygeni Secrets Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL, "Qualys VMDR": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE, "Alert Logic Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE, + "PICUS Scan": DEDUPE_ALGO_HASH_CODE, } # Override the hardcoded settings here via the env var From f92f866a001b3a62d94ff649055054643ef08980 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Mon, 8 Jun 2026 18:33:27 -0600 Subject: [PATCH 4/6] docs(parser): add PICUS parser documentation Document supported file types, field mapping, severity mapping, BAS active/inactive semantics, and the actionId-based hashcode deduplication. Authored by T. Walker - DefectDojo --- .../supported_tools/parsers/file/picus.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/content/supported_tools/parsers/file/picus.md diff --git a/docs/content/supported_tools/parsers/file/picus.md b/docs/content/supported_tools/parsers/file/picus.md new file mode 100644 index 00000000000..884d39a3de1 --- /dev/null +++ b/docs/content/supported_tools/parsers/file/picus.md @@ -0,0 +1,126 @@ +--- +title: "PICUS Scan" +toc_hide: true +--- + +The [Picus Security](https://www.picussecurity.com/) parser for DefectDojo supports imports from CSV format. Picus is a Breach and Attack Simulation (BAS) platform that runs simulated attacks against an environment and reports whether existing security controls prevented, logged, and alerted on each simulated action. This document details how Picus result CSV exports are mapped into DefectDojo Findings, which fields are parsed, and the BAS-specific transformation notes. + +## Supported File Types + +The Picus parser accepts CSV file format. Picus exports a separate CSV per attack vector (for example Email, Endpoint, Network, and Web), but all share an identical column schema, so the same parser handles every export. + +To import Picus results into DefectDojo: + +1. Log into your Picus console +2. Run or open the simulation whose results you want to import +3. Export the results as CSV (one file per attack vector) +4. Save each file with a `.csv` extension +5. Upload each CSV to DefectDojo using the "PICUS Scan" scan type + +DefectDojo imports a single scan file at a time and does not unpack archives. If the export is delivered as a `.rar` or `.zip`, extract it first and import each CSV individually. Each file becomes its own import, which keeps the attack vectors (Email, Endpoint, Network, Web) grouped separately. + +## Default Deduplication Hashcode Fields + +Picus findings deduplicate using the hashcode algorithm on a single stable [hashcode field](https://docs.defectdojo.com/en/working_with_findings/finding_deduplication/about_deduplication/), the tool-native action identifier: + +- vuln_id_from_tool (populated from the Picus `actionId`) + +### Sample Scan Data + +Sample Picus scans can be found in the [sample scan data folder](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/picus). + +## Link To Tool + +- [Picus Security](https://www.picussecurity.com/) +- [Picus Documentation](https://docs.picussecurity.com/) + +## CSV Format + +### Total Fields in CSV + +- Total data fields: 63 +- Total data fields parsed into dedicated Finding fields or the structured description: 20 +- Remaining fields (detection-integration, protocol, hash, and tab-link columns) are not currently mapped + +### CSV Format Field Mapping Details + +
+Click to expand Field Mapping Table + +| Source Field | DefectDojo Field | Notes | +| ---------------------- | ------------------------- | -------------------------------------------------------------------------------------- | +| threatName + actionName | title | Combined as "threatName - actionName"; truncated to 500 characters with "..." if longer | +| threatSeverity | severity | Mapped to DefectDojo severity levels; defaults to Info if unrecognized | +| threatPreventionResult | active | "Not Blocked" sets active=True (control gap); any other value sets active=False | +| threatPreventionResult | mitigation | Drives a recommendation describing whether controls blocked the simulated attack | +| actionId | vuln_id_from_tool | Native Picus action identifier; drives hashcode deduplication across re-imports | +| cve | unsaved_vulnerability_ids | Comma-separated CVEs split into a list; omitted when empty | +| cwe | cwe | Parsed to integer when the field contains digits; omitted otherwise | +| actionMitreTactic | unsaved_tags | Added as a tag when present | +| actionMitreTechnique | unsaved_tags | Added as a tag when present | +| actionMitreSubtechnique | unsaved_tags | Added as a tag when present | +| attackCategory | unsaved_tags | Added as a tag when present | +| affectedProducts | component_name | The affected product reported by the simulation | +| threatName | description | Included in the structured description table | +| actionName | description | Included in the structured description table | +| actionDescription | description | Included in the structured description table | +| attackModules | description | Included in the structured description table | +| threatDetectionLogResult | description | Included in the structured description table | +| threatDetectionAlertResult | description | Included in the structured description table | +| affectedOs | description | Included in the structured description table | +| affectedPlatforms | description | Included in the structured description table | +| actionPayload | description | Included in the structured description table | + +
+ +### Additional Finding Field Settings (CSV Format) + +
+Click to expand Additional Settings Table + +| Finding Field | Default Value | Notes | +| --------------- | ----------------------------- | --------------------------------------------------------------------------- | +| static_finding | False | Picus results reflect runtime simulation behavior, not static analysis | +| dynamic_finding | True | Picus results reflect runtime simulation behavior | +| active | True when "Not Blocked" | A simulated attack that was not blocked is an open control gap | + +
+ +## Special Processing Notes + +### Breach and Attack Simulation Semantics + +Picus is a BAS tool, so each CSV row is a simulated attack *action* rather than a discovered vulnerability. The value to DefectDojo is whether a security control stopped the simulated attack. The parser imports every action as a Finding and uses `threatPreventionResult` to decide the finding's active state: + +- `Not Blocked` → active Finding (the control failed to stop the attack — an open gap) +- `Blocked` (or any other value) → inactive Finding (the control mitigated the attack) + +This preserves the full simulation history while surfacing unmitigated gaps as the actionable findings. + +### Severity Mapping + +Severity is taken from the `threatSeverity` column (the inherent risk of the threat scenario), not the per-action `severity` column: + +- `Critical` → Critical +- `High` → High +- `Medium` → Medium +- `Low` → Low +- `Info` / `Informational` → Info + +Any unrecognized value defaults to Info. + +### Title Format + +Finding titles combine the threat and action names as "threatName - actionName". When only a threat name is present, it is used alone. Titles longer than 500 characters are truncated to 497 characters with a "..." suffix. + +### Description Construction + +The parser builds a markdown table containing the threat, action, attack category, MITRE references, detection/prevention results, affected asset details, payload, and the Picus simulation/action identifiers. Empty source fields are omitted, and pipe characters in values are escaped so the table renders correctly. + +### Deduplication + +Deduplication uses the hashcode algorithm keyed solely on `vuln_id_from_tool`, which the parser populates from the native Picus `actionId`. The `actionId` is stable across simulation runs (the same attack action keeps the same identifier), while `simulationRunId` changes on every run. Keying on `actionId` alone — and deliberately excluding `simulationRunId` — means that when a later run's CSV is re-imported into the same test, each action matches its prior finding. This lets DefectDojo update the status of existing findings (for example, closing an action that was previously "Not Blocked" once a control begins blocking it) rather than creating duplicates. Picus simulations span an asset class such as Network or Email, so deduplication is expected to operate within a single asset/engagement rather than across asset types. + +### Unmapped Fields + +Detection-integration, protocol-level prevention/detection, file-hash, signature, and tab-link columns are retained in the source CSV but are not currently mapped to Finding fields. The most operationally relevant columns are surfaced either as dedicated Finding fields or within the structured description table. From 11d5befac5f4d1f44328d32e87ed44a36e7722eb Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Fri, 12 Jun 2026 17:24:01 -0600 Subject: [PATCH 5/6] feat(parser): enrich PICUS mitigation with control posture and triage references Build the mitigation field from the prevent -> log -> alert control posture so analysts can see which control layer failed, plus any Picus mitigation/triage references present in the export (mitigation guidance, detection content, payload output, action logs, detection signature). Return None instead of an empty string when no mitigation data is available (addresses PR review feedback). Add fixture coverage for the reference links and six mitigation unit tests. Authored by T. Walker - DefectDojo --- dojo/tools/picus/parser.py | 42 ++++++++++++++++++++++---- unittests/scans/picus/one_vuln.csv | 2 +- unittests/tools/test_picus_parser.py | 44 ++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/dojo/tools/picus/parser.py b/dojo/tools/picus/parser.py index e1d28826921..017b9e5471e 100644 --- a/dojo/tools/picus/parser.py +++ b/dojo/tools/picus/parser.py @@ -94,14 +94,46 @@ def _build_tags(self, get): def _build_mitigation(self, get): prevention = get("threatPreventionResult") + + lines = [] if prevention == "Not Blocked": - return ( + lines.append( "The simulated attack was NOT blocked by existing preventive controls. " - "Review and tune the relevant security controls to block this technique." + "Review and tune the relevant security controls to block this technique.", ) - if prevention == "Blocked": - return "The simulated attack was blocked by existing preventive controls." - return "" + elif prevention == "Blocked": + lines.append("The simulated attack was blocked by existing preventive controls.") + + # Which control layer failed (prevent -> log -> alert) tells the analyst + # which gap to close first. + posture = [ + ("Prevention", prevention), + ("Logging", get("threatDetectionLogResult")), + ("Alerting", get("threatDetectionAlertResult")), + ] + posture = [(label, value) for label, value in posture if value] + if posture: + lines.append("\n**Control posture**") + lines.extend(f"- {label}: {value}" for label, value in posture) + + # Links and identifiers from the export that help triage and build a fix. + references = [ + ("Picus mitigation guidance", get("genericMitigationsTabLink")), + ("Detection content", get("detectionContentTabLink")), + ("Action payload output", get("actionPayloadOutputTabLink")), + ("Action logs", get("actionLogsTabLink")), + ] + signature = " ".join( + part for part in (get("signatureName"), f"({get('signatureId')})" if get("signatureId") else "") if part + ) + if signature: + references.append(("Detection signature", signature)) + references = [(label, value) for label, value in references if value] + if references: + lines.append("\n**Mitigation & triage references**") + lines.extend(f"- {label}: {value}" for label, value in references) + + return "\n".join(lines) if lines else None def _build_description(self, get): fields = [ diff --git a/unittests/scans/picus/one_vuln.csv b/unittests/scans/picus/one_vuln.csv index dfd2e30657c..e6b39dabd36 100644 --- a/unittests/scans/picus/one_vuln.csv +++ b/unittests/scans/picus/one_vuln.csv @@ -1,2 +1,2 @@ simulationRunId,threatId,threatName,attackModules,threatSeverity,threatReleaseDateEpoch,threatReleaseDateDateTimeUTC,threatPreventionResult,threatDetectionLogResult,threatDetectionAlertResult,actionId,actionName,actionDescription,affectedOs,affectedPlatforms,affectedProducts,isPrivileged,actionMitreTactic,actionMitreTechnique,actionMitreSubtechnique,nistCapabilities,actionPayload,actionPayloadOutputTabLink,actionPreventionResult,actionPreventionSources,actionFileName,actionSha1,actionSha256,actionMd5,cve,cwe,attackStartedTimeUTC,attackEndedTimeUTC,attackLoggedTimeUTC,attackAlertedTimeUTC,actionProtocol,actionProtocolPreventionResult,actionProtocolPreventionSources,actionProtocolDetectionLogResult,actionProtocolDetectionAlertResult,actionDetectionIntegration,actionDetectionLogResult,actionDetectionAlertResult,attackCategory,actionLogsTabLink,genericMitigationsTabLink,vendorName,platform,productVersion,signatureId,signatureName,severity,signatureVersion,detectionContentTabLink,attackerUser,tenantId,subscription,location,rewindStatus,output,executionSteps,rewindSteps,manualRewindSteps -2001,1,Fileless Malware via PowerShell,Endpoint,Critical,,02/10/2023,Not Blocked,Not Logged,Not Alerted,1001,PowerShell Download Cradle Execution,Executes an in-memory PowerShell download cradle to retrieve a payload without touching disk,Windows,Platform X,Product Y,No,TA0002,T1059,T1059.001,,,,,,,,,,CVE-2021-44228,78,02/10/2023,02/10/2023,,,,,,,,,,,Malicious Code,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001001,SIG-T1059,High,v1,,Anonymous1,341,,,,,,, +2001,1,Fileless Malware via PowerShell,Endpoint,Critical,,02/10/2023,Not Blocked,Not Logged,Not Alerted,1001,PowerShell Download Cradle Execution,Executes an in-memory PowerShell download cradle to retrieve a payload without touching disk,Windows,Platform X,Product Y,No,TA0002,T1059,T1059.001,,,https://sample[.]com,,,,,,,CVE-2021-44228,78,02/10/2023,02/10/2023,,,,,,,,,,,Malicious Code,https://sample[.]com,https://sample[.]com,Vendor X,Platform X,v1,10001001,SIG-T1059,High,v1,https://sample[.]com,Anonymous1,341,,,,,,, diff --git a/unittests/tools/test_picus_parser.py b/unittests/tools/test_picus_parser.py index c8e5ac13603..3c706e468d4 100644 --- a/unittests/tools/test_picus_parser.py +++ b/unittests/tools/test_picus_parser.py @@ -136,3 +136,47 @@ def test_description_is_markdown_table(self): findings = parser.get_findings(testfile, Test()) self.assertIn("| Field | Value |", findings[0].description) self.assertIn("| Attack Category | Malicious Code |", findings[0].description) + + def test_mitigation_not_blocked_sentence(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + self.assertIn("was NOT blocked by existing preventive controls", findings[0].mitigation) + + def test_mitigation_blocked_sentence(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + # Row index 1 (Credential Dumping) was Blocked. + self.assertIn("was blocked by existing preventive controls", findings[1].mitigation) + + def test_mitigation_includes_control_posture(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + mitigation = findings[0].mitigation + self.assertIn("**Control posture**", mitigation) + self.assertIn("- Prevention: Not Blocked", mitigation) + self.assertIn("- Logging: Not Logged", mitigation) + self.assertIn("- Alerting: Not Alerted", mitigation) + + def test_mitigation_includes_reference_links(self): + with (get_unit_tests_scans_path("picus") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + mitigation = findings[0].mitigation + self.assertIn("**Mitigation & triage references**", mitigation) + self.assertIn("- Picus mitigation guidance: https://sample[.]com", mitigation) + self.assertIn("- Detection content: https://sample[.]com", mitigation) + self.assertIn("- Action payload output: https://sample[.]com", mitigation) + self.assertIn("- Action logs: https://sample[.]com", mitigation) + self.assertIn("- Detection signature: SIG-T1059 (10001001)", mitigation) + + def test_mitigation_omits_absent_reference_fields(self): + with (get_unit_tests_scans_path("picus") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = PicusParser() + findings = parser.get_findings(testfile, Test()) + # many_vulns.csv leaves detection-content / payload-output links empty. + mitigation = findings[0].mitigation + self.assertNotIn("- Detection content:", mitigation) + self.assertNotIn("- Action payload output:", mitigation) From c8593b19b56b9fa11ff57900fa41fcfedd3116fd Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Fri, 12 Jun 2026 17:29:55 -0600 Subject: [PATCH 6/6] docs(parser): document PICUS enriched mitigation field Update the field-mapping table, add a Mitigation Construction section, and refresh the mapped-field count to cover the control-posture block and the mitigation/triage references (mitigation guidance, detection content, payload output, action logs, detection signature) now emitted in the mitigation field. Authored by T. Walker - DefectDojo --- .../supported_tools/parsers/file/picus.md | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/content/supported_tools/parsers/file/picus.md b/docs/content/supported_tools/parsers/file/picus.md index 884d39a3de1..45f271863f4 100644 --- a/docs/content/supported_tools/parsers/file/picus.md +++ b/docs/content/supported_tools/parsers/file/picus.md @@ -39,8 +39,8 @@ Sample Picus scans can be found in the [sample scan data folder](https://github. ### Total Fields in CSV - Total data fields: 63 -- Total data fields parsed into dedicated Finding fields or the structured description: 20 -- Remaining fields (detection-integration, protocol, hash, and tab-link columns) are not currently mapped +- Total data fields parsed into dedicated Finding fields, the structured description, or the mitigation: 26 +- Remaining fields (detection-integration, protocol-level, file-hash, and other signature/metadata columns) are not currently mapped ### CSV Format Field Mapping Details @@ -52,7 +52,14 @@ Sample Picus scans can be found in the [sample scan data folder](https://github. | threatName + actionName | title | Combined as "threatName - actionName"; truncated to 500 characters with "..." if longer | | threatSeverity | severity | Mapped to DefectDojo severity levels; defaults to Info if unrecognized | | threatPreventionResult | active | "Not Blocked" sets active=True (control gap); any other value sets active=False | -| threatPreventionResult | mitigation | Drives a recommendation describing whether controls blocked the simulated attack | +| threatPreventionResult | mitigation | Recommendation sentence plus the "Prevention" line of the control-posture block | +| threatDetectionLogResult | mitigation | "Logging" line of the mitigation control-posture block | +| threatDetectionAlertResult | mitigation | "Alerting" line of the mitigation control-posture block | +| genericMitigationsTabLink | mitigation | Picus mitigation-guidance link; included in the mitigation references when present | +| detectionContentTabLink | mitigation | Detection-content link; included in the mitigation references when present | +| actionPayloadOutputTabLink | mitigation | Payload-output link; included in the mitigation references when present | +| actionLogsTabLink | mitigation | Action-logs link; included in the mitigation references when present | +| signatureName + signatureId | mitigation | Detection signature reference; included in the mitigation references when present | | actionId | vuln_id_from_tool | Native Picus action identifier; drives hashcode deduplication across re-imports | | cve | unsaved_vulnerability_ids | Comma-separated CVEs split into a list; omitted when empty | | cwe | cwe | Parsed to integer when the field contains digits; omitted otherwise | @@ -117,10 +124,20 @@ Finding titles combine the threat and action names as "threatName - actionName". The parser builds a markdown table containing the threat, action, attack category, MITRE references, detection/prevention results, affected asset details, payload, and the Picus simulation/action identifiers. Empty source fields are omitted, and pipe characters in values are escaped so the table renders correctly. +### Mitigation Construction + +The mitigation field is assembled to give an analyst what they need to remediate the gap, not just a status sentence. It contains up to three parts, and any part with no underlying data is omitted: + +1. **Recommendation sentence** — derived from `threatPreventionResult` (a prompt to tune controls when the attack was "Not Blocked", or a confirmation when it was "Blocked"). +2. **Control posture** — the prevent → log → alert results (`threatPreventionResult`, `threatDetectionLogResult`, `threatDetectionAlertResult`) so the analyst can see which control layer failed and where to focus first. +3. **Mitigation & triage references** — the Picus links and identifiers that help build a fix: mitigation guidance (`genericMitigationsTabLink`), detection content (`detectionContentTabLink`), payload output (`actionPayloadOutputTabLink`), action logs (`actionLogsTabLink`), and the detection signature (`signatureName` / `signatureId`). Each reference is included only when present in the export. + +When none of these parts has data, the mitigation field is left unset rather than written as an empty string. + ### Deduplication Deduplication uses the hashcode algorithm keyed solely on `vuln_id_from_tool`, which the parser populates from the native Picus `actionId`. The `actionId` is stable across simulation runs (the same attack action keeps the same identifier), while `simulationRunId` changes on every run. Keying on `actionId` alone — and deliberately excluding `simulationRunId` — means that when a later run's CSV is re-imported into the same test, each action matches its prior finding. This lets DefectDojo update the status of existing findings (for example, closing an action that was previously "Not Blocked" once a control begins blocking it) rather than creating duplicates. Picus simulations span an asset class such as Network or Email, so deduplication is expected to operate within a single asset/engagement rather than across asset types. ### Unmapped Fields -Detection-integration, protocol-level prevention/detection, file-hash, signature, and tab-link columns are retained in the source CSV but are not currently mapped to Finding fields. The most operationally relevant columns are surfaced either as dedicated Finding fields or within the structured description table. +Detection-integration, protocol-level prevention/detection, file-hash, and remaining signature/metadata columns are retained in the source CSV but are not currently mapped to Finding fields. The most operationally relevant columns are surfaced either as dedicated Finding fields, within the structured description table, or in the mitigation references.