From fe7a8e76706041ef6ed95170fdaaf11c9a85f8f5 Mon Sep 17 00:00:00 2001 From: Cosmel Villalobos Date: Sun, 11 May 2025 20:11:45 -0600 Subject: [PATCH 1/5] Fix AnchoreCTL Policies parser to support new format with evaluations array This commit updates the AnchoreCTL Policies parser to support both the legacy and new format reports generated by the AnchoreCTL tool. Changes: - Added detection for the new format which has an object with evaluations array instead of a root-level list - Implemented conversion logic to transform the new format into a compatible structure for parsing - Improved error handling with more descriptive messages - Made field extraction more robust with proper fallbacks between formats The parser now successfully processes both: - Legacy format (list at root level) - New format from anchorectl policy evaluate -o json (object with evaluations array) --- dojo/tools/anchorectl_policies/parser.py | 84 ++++++++++++++++++++---- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/dojo/tools/anchorectl_policies/parser.py b/dojo/tools/anchorectl_policies/parser.py index 38873a5e795..448ce4decf1 100644 --- a/dojo/tools/anchorectl_policies/parser.py +++ b/dojo/tools/anchorectl_policies/parser.py @@ -17,7 +17,7 @@ def get_label_for_scan_types(self, scan_type): return "AnchoreCTL Policies Report" def get_description_for_scan_types(self, scan_type): - return "AnchoreCTLs JSON policies report format." + return "AnchoreCTLs JSON policies report format. Both legacy list-based format and new evaluation-based format (from anchorectl policy evaluate -o json) are supported." def get_findings(self, filename, test): content = filename.read() @@ -29,24 +29,78 @@ def get_findings(self, filename, test): find_date = datetime.now() items = [] + # Handle new AnchoreCTL format (object with evaluations array) + if isinstance(data, dict) and "evaluations" in data: + logger.info("Detected new AnchoreCTL policies format") + processed_data = [] + # Process each evaluation in the evaluations array + for evaluation in data.get("evaluations", []): + # Only process evaluations with findings + if evaluation.get("numberOfFindings", 0) > 0 and evaluation.get("details"): + processed_item = { + "detail": [], + "digest": data.get("imageDigest", ""), + "finalAction": evaluation.get("finalAction", ""), + "finalActionReason": evaluation.get("finalActionReason", ""), + "lastEvaluation": evaluation.get("evaluationTime", ""), + "policyId": data.get("policyId", ""), + "status": evaluation.get("status", ""), + "tag": data.get("evaluatedTag", "") + } + + # Process details if they exist + for detail in evaluation.get("details", []): + processed_item["detail"].append(detail) + + processed_data.append(processed_item) + + data = processed_data + if not isinstance(data, list): - msg = "This doesn't look like a valid Anchore CTRL Policies report: Expected a list with image data at the root of the JSON data" + msg = "This doesn't look like a valid Anchore CTRL Policies report: Expected a list with image data at the root of the JSON data or an object with 'evaluations' array" raise TypeError(msg) for image in data: - if not isinstance(image, dict) or image.get("detail") is None or not isinstance(image.get("detail"), list): - msg = "This doesn't look like a valid Anchore CTRL Policies report, missing 'detail' list object key for image" + # Skip empty images + if len(data) == 0: + continue + + # Check for valid structure + if not isinstance(image, dict): + msg = "This doesn't look like a valid Anchore CTRL Policies report, expected dict object for image" + raise ValueError(msg) + + # Handle legacy format with detail field + if image.get("detail") is not None and isinstance(image.get("detail"), list): + details = image.get("detail") + # Handle newer format that might have details under a different structure + elif image.get("details") is not None and isinstance(image.get("details"), list): + details = image.get("details") + else: + msg = "This doesn't look like a valid Anchore CTRL Policies report, missing 'detail' or 'details' list object key for image" raise ValueError(msg) - for result in image["detail"]: + # Process each finding detail + for result in details: try: - gate = result["gate"] - description = result["description"] - policy_id = result["policyId"] - status = result["status"] - image_name = result["tag"] - trigger_id = result["triggerId"] - repo, tag = image_name.split(":", 2) + # Extract fields with fallbacks for different formats + gate = result.get("gate", "unknown") + description = result.get("description", "No description provided") + policy_id = result.get("policyId", image.get("policyId", "unknown")) + status = result.get("status", "unknown") + + # Handle image tag from different possible locations + image_name = result.get("tag", image.get("tag", "unknown:latest")) + + trigger_id = result.get("triggerId", "unknown") + + # Split repo and tag safely + if ":" in image_name: + repo, tag = image_name.split(":", 1) + else: + repo = image_name + tag = "latest" + severity, active = get_severity(status, description) vulnerability_id = extract_vulnerability_id(trigger_id) title = ( @@ -74,8 +128,10 @@ def get_findings(self, filename, test): find.unsaved_vulnerability_ids = [vulnerability_id] items.append(find) except (KeyError, IndexError) as err: - msg = f"Invalid format: {err} key not found" - raise ValueError(msg) + msg = f"Invalid format or missing key: {err}. This parser supports both legacy AnchoreCTL format and the new format from 'anchorectl policy evaluate -o json'." + logger.warning(msg) + # Continue processing other findings instead of failing completely + continue return items From 49a24591eafe9c62bb76a714aeaca07fea879bef Mon Sep 17 00:00:00 2001 From: Cosmel Villalobos Date: Sun, 11 May 2025 20:14:37 -0600 Subject: [PATCH 2/5] Added tests for the new format to verify correct parsing --- .../new_format_many_violations.json | 46 +++++++++++++++++++ .../new_format_no_violation.json | 15 ++++++ .../new_format_one_violation.json | 26 +++++++++++ ...at_one_violation_description_severity.json | 26 +++++++++++ .../tools/test_anchorectl_policies_parser.py | 33 +++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 unittests/scans/anchorectl_policies/new_format_many_violations.json create mode 100644 unittests/scans/anchorectl_policies/new_format_no_violation.json create mode 100644 unittests/scans/anchorectl_policies/new_format_one_violation.json create mode 100644 unittests/scans/anchorectl_policies/new_format_one_violation_description_severity.json diff --git a/unittests/scans/anchorectl_policies/new_format_many_violations.json b/unittests/scans/anchorectl_policies/new_format_many_violations.json new file mode 100644 index 00000000000..8c7d108c2e7 --- /dev/null +++ b/unittests/scans/anchorectl_policies/new_format_many_violations.json @@ -0,0 +1,46 @@ +{ + "evaluatedTag": "test/testimage:testtag", + "policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6", + "imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20", + "evaluations": [ + { + "evaluationTime": "2022-09-20T08:25:52Z", + "status": "fail", + "finalAction": "stop", + "finalActionReason": "policy_evaluation", + "numberOfFindings": 3, + "details": [ + { + "description": "HIGH Vulnerability found in non-os package type (test) - /usr/local/bin/testbinary (CVE-2022-1234)", + "gate": "vulnerabilities", + "imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b", + "policyId": "SoftwareChecks", + "status": "stop", + "tag": "test/testimage:testtag", + "triggerId": "CVE-2022-1234+test", + "triggerName": "package" + }, + { + "description": "MEDIUM Vulnerability found in non-os package type (test2) - /usr/local/bin/testbinary (fixed in: 1.2.3)(GHSA-1234-abcd-5678)", + "gate": "vulnerabilities", + "imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b", + "policyId": "SoftwareChecks", + "status": "stop", + "tag": "test/testimage:testtag", + "triggerId": "GHSA-1234-abcd-5678+test2", + "triggerName": "package" + }, + { + "description": "User root found as effective user, which is not on the allowed list", + "gate": "dockerfile", + "imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b", + "policyId": "RootUser", + "status": "warn", + "tag": "test/testimage:testtag", + "triggerId": "b2605c2ddbdb02b8e2365c9248dada5a", + "triggerName": "effective_user" + } + ] + } + ] +} diff --git a/unittests/scans/anchorectl_policies/new_format_no_violation.json b/unittests/scans/anchorectl_policies/new_format_no_violation.json new file mode 100644 index 00000000000..8a2ace3b59b --- /dev/null +++ b/unittests/scans/anchorectl_policies/new_format_no_violation.json @@ -0,0 +1,15 @@ +{ + "evaluatedTag": "test/testimage:testtag", + "policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6", + "imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20", + "evaluations": [ + { + "evaluationTime": "2022-09-20T08:25:52Z", + "status": "pass", + "finalAction": "go", + "finalActionReason": "policy_evaluation", + "numberOfFindings": 0, + "details": [] + } + ] +} diff --git a/unittests/scans/anchorectl_policies/new_format_one_violation.json b/unittests/scans/anchorectl_policies/new_format_one_violation.json new file mode 100644 index 00000000000..80edc498c75 --- /dev/null +++ b/unittests/scans/anchorectl_policies/new_format_one_violation.json @@ -0,0 +1,26 @@ +{ + "evaluatedTag": "test/testimage:testtag", + "policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6", + "imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20", + "evaluations": [ + { + "evaluationTime": "2022-09-20T08:25:52Z", + "status": "fail", + "finalAction": "stop", + "finalActionReason": "policy_evaluation", + "numberOfFindings": 1, + "details": [ + { + "description": "User root found as effective user, which is not on the allowed list", + "gate": "dockerfile", + "imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b", + "policyId": "RootUser", + "status": "warn", + "tag": "test/testimage:testtag", + "triggerId": "b2605c2ddbdb02b8e2365c9248dada5a", + "triggerName": "effective_user" + } + ] + } + ] +} diff --git a/unittests/scans/anchorectl_policies/new_format_one_violation_description_severity.json b/unittests/scans/anchorectl_policies/new_format_one_violation_description_severity.json new file mode 100644 index 00000000000..5080d3bc4fa --- /dev/null +++ b/unittests/scans/anchorectl_policies/new_format_one_violation_description_severity.json @@ -0,0 +1,26 @@ +{ + "evaluatedTag": "test/testimage:testtag", + "policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6", + "imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20", + "evaluations": [ + { + "evaluationTime": "2022-09-20T08:25:52Z", + "status": "fail", + "finalAction": "stop", + "finalActionReason": "policy_evaluation", + "numberOfFindings": 1, + "details": [ + { + "description": "CRITICAL User root found as effective user, which is not on the allowed list", + "gate": "dockerfile", + "imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b", + "policyId": "RootUser", + "status": "warn", + "tag": "test/testimage:testtag", + "triggerId": "b2605c2ddbdb02b8e2365c9248dada5a", + "triggerName": "effective_user" + } + ] + } + ] +} diff --git a/unittests/tools/test_anchorectl_policies_parser.py b/unittests/tools/test_anchorectl_policies_parser.py index f27f80c9791..ba4ad95fdb5 100644 --- a/unittests/tools/test_anchorectl_policies_parser.py +++ b/unittests/tools/test_anchorectl_policies_parser.py @@ -35,3 +35,36 @@ def test_anchore_engine_parser_has_one_finding_and_description_has_severity(self self.assertEqual(singleFinding.severity, "Critical") self.assertEqual(singleFinding.title, "RootUser - gate|dockerfile - trigger|b2605c2ddbdb02b8e2365c9248dada5a") self.assertEqual(singleFinding.description, "CRITICAL User root found as effective user, which is not on the allowed list") + + # Tests for the new AnchoreCTL format + def test_new_format_anchore_engine_parser_has_no_finding(self): + with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_no_violation.json").open(encoding="utf-8") as testfile: + parser = AnchoreCTLPoliciesParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_new_format_anchore_engine_parser_has_one_finding_and_it_is_correctly_parsed(self): + with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_one_violation.json").open(encoding="utf-8") as testfile: + parser = AnchoreCTLPoliciesParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + singleFinding = findings[0] + self.assertEqual(singleFinding.severity, "Medium") + self.assertEqual(singleFinding.title, "RootUser - gate|dockerfile - trigger|b2605c2ddbdb02b8e2365c9248dada5a") + self.assertEqual(singleFinding.description, "User root found as effective user, which is not on the allowed list") + + def test_new_format_anchore_engine_parser_has_many_findings(self): + with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_many_violations.json").open(encoding="utf-8") as testfile: + parser = AnchoreCTLPoliciesParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(3, len(findings)) + + def test_new_format_anchore_engine_parser_has_one_finding_and_description_has_severity(self): + with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_one_violation_description_severity.json").open(encoding="utf-8") as testfile: + parser = AnchoreCTLPoliciesParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + singleFinding = findings[0] + self.assertEqual(singleFinding.severity, "Critical") + self.assertEqual(singleFinding.title, "RootUser - gate|dockerfile - trigger|b2605c2ddbdb02b8e2365c9248dada5a") + self.assertEqual(singleFinding.description, "CRITICAL User root found as effective user, which is not on the allowed list") From 65c916fa156c6789d8073d00802ccb1fdcb7b3f7 Mon Sep 17 00:00:00 2001 From: Cosmel Villalobos Date: Mon, 12 May 2025 08:41:56 -0600 Subject: [PATCH 3/5] Fixed linter errors --- dojo/tools/anchorectl_policies/parser.py | 32 ++++++++----------- .../tools/test_anchorectl_policies_parser.py | 2 +- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/dojo/tools/anchorectl_policies/parser.py b/dojo/tools/anchorectl_policies/parser.py index 448ce4decf1..be46c6573a2 100644 --- a/dojo/tools/anchorectl_policies/parser.py +++ b/dojo/tools/anchorectl_policies/parser.py @@ -45,17 +45,17 @@ def get_findings(self, filename, test): "lastEvaluation": evaluation.get("evaluationTime", ""), "policyId": data.get("policyId", ""), "status": evaluation.get("status", ""), - "tag": data.get("evaluatedTag", "") + "tag": data.get("evaluatedTag", ""), } - + # Process details if they exist for detail in evaluation.get("details", []): processed_item["detail"].append(detail) - + processed_data.append(processed_item) - + data = processed_data - + if not isinstance(data, list): msg = "This doesn't look like a valid Anchore CTRL Policies report: Expected a list with image data at the root of the JSON data or an object with 'evaluations' array" raise TypeError(msg) @@ -64,12 +64,12 @@ def get_findings(self, filename, test): # Skip empty images if len(data) == 0: continue - + # Check for valid structure if not isinstance(image, dict): msg = "This doesn't look like a valid Anchore CTRL Policies report, expected dict object for image" - raise ValueError(msg) - + raise TypeError(msg) + # Handle legacy format with detail field if image.get("detail") is not None and isinstance(image.get("detail"), list): details = image.get("detail") @@ -88,28 +88,22 @@ def get_findings(self, filename, test): description = result.get("description", "No description provided") policy_id = result.get("policyId", image.get("policyId", "unknown")) status = result.get("status", "unknown") - + # Handle image tag from different possible locations image_name = result.get("tag", image.get("tag", "unknown:latest")) - + trigger_id = result.get("triggerId", "unknown") - + # Split repo and tag safely if ":" in image_name: repo, tag = image_name.split(":", 1) else: repo = image_name tag = "latest" - + severity, active = get_severity(status, description) vulnerability_id = extract_vulnerability_id(trigger_id) - title = ( - policy_id - + " - gate|" - + gate - + " - trigger|" - + trigger_id - ) + title = policy_id + " - gate|" + gate + " - trigger|" + trigger_id find = Finding( title=title, test=test, diff --git a/unittests/tools/test_anchorectl_policies_parser.py b/unittests/tools/test_anchorectl_policies_parser.py index ba4ad95fdb5..0ce141091f6 100644 --- a/unittests/tools/test_anchorectl_policies_parser.py +++ b/unittests/tools/test_anchorectl_policies_parser.py @@ -35,7 +35,7 @@ def test_anchore_engine_parser_has_one_finding_and_description_has_severity(self self.assertEqual(singleFinding.severity, "Critical") self.assertEqual(singleFinding.title, "RootUser - gate|dockerfile - trigger|b2605c2ddbdb02b8e2365c9248dada5a") self.assertEqual(singleFinding.description, "CRITICAL User root found as effective user, which is not on the allowed list") - + # Tests for the new AnchoreCTL format def test_new_format_anchore_engine_parser_has_no_finding(self): with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_no_violation.json").open(encoding="utf-8") as testfile: From f9bc57b0c43c06f1a2a20147ff0d47a75acdbb48 Mon Sep 17 00:00:00 2001 From: Cosmel Villalobos Date: Fri, 16 May 2025 11:06:04 -0600 Subject: [PATCH 4/5] Update AnchoreCTL Policies Report documentation for clarity and format support --- .../parsers/file/anchorectl_policies.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md b/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md index 8ff36f72396..ef2715ef327 100644 --- a/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md +++ b/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md @@ -2,7 +2,24 @@ title: "AnchoreCTL Policies Report" toc_hide: true --- -AnchoreCTLs JSON policies report format +AnchoreCTLs JSON policies report format. Both legacy list-based format and new evaluation-based format are supported. + +## Usage + +To generate a policy report that can be imported into DefectDojo: + +```bash +# Evaluate policies and output to JSON format +anchorectl policy evaluate -o json > policy_report.json +``` + +In DefectDojo, select "AnchoreCTL Policies Report" as the scan type when uploading the file. + +### Format Support + +The parser supports both: +- Legacy format (list of objects at the root level) +- New format from `anchorectl policy evaluate -o json` (object with evaluations array) ### Sample Scan Data Sample AnchoreCTL Policies Report scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/anchorectl_policies). \ No newline at end of file From 202011fee4807ad5f137a83b228879d9c9633c76 Mon Sep 17 00:00:00 2001 From: Cosmel Villalobos Date: Fri, 16 May 2025 11:53:19 -0600 Subject: [PATCH 5/5] Removed unnecessary text from anchorectl_policies --- .../parsers/file/anchorectl_policies.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md b/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md index ef2715ef327..18dc798e633 100644 --- a/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md +++ b/docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md @@ -13,13 +13,5 @@ To generate a policy report that can be imported into DefectDojo: anchorectl policy evaluate -o json > policy_report.json ``` -In DefectDojo, select "AnchoreCTL Policies Report" as the scan type when uploading the file. - -### Format Support - -The parser supports both: -- Legacy format (list of objects at the root level) -- New format from `anchorectl policy evaluate -o json` (object with evaluations array) - ### Sample Scan Data Sample AnchoreCTL Policies Report scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/anchorectl_policies). \ No newline at end of file