Skip to content

Commit 5a57c1f

Browse files
authored
Add new "evaluations" format support to Anchorectl parser (#12425)
* 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) * Added tests for the new format to verify correct parsing * Fixed linter errors * Update AnchoreCTL Policies Report documentation for clarity and format support * Removed unnecessary text from anchorectl_policies
1 parent 7fa55fc commit 5a57c1f

File tree

7 files changed

+227
-22
lines changed

7 files changed

+227
-22
lines changed

docs/content/en/connecting_your_tools/parsers/file/anchorectl_policies.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
title: "AnchoreCTL Policies Report"
33
toc_hide: true
44
---
5-
AnchoreCTLs JSON policies report format
5+
AnchoreCTLs JSON policies report format. Both legacy list-based format and new evaluation-based format are supported.
6+
7+
## Usage
8+
9+
To generate a policy report that can be imported into DefectDojo:
10+
11+
```bash
12+
# Evaluate policies and output to JSON format
13+
anchorectl policy evaluate -o json > policy_report.json
14+
```
615

716
### Sample Scan Data
817
Sample AnchoreCTL Policies Report scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/anchorectl_policies).

dojo/tools/anchorectl_policies/parser.py

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def get_label_for_scan_types(self, scan_type):
1717
return "AnchoreCTL Policies Report"
1818

1919
def get_description_for_scan_types(self, scan_type):
20-
return "AnchoreCTLs JSON policies report format."
20+
return "AnchoreCTLs JSON policies report format. Both legacy list-based format and new evaluation-based format (from anchorectl policy evaluate -o json) are supported."
2121

2222
def get_findings(self, filename, test):
2323
content = filename.read()
@@ -29,33 +29,81 @@ def get_findings(self, filename, test):
2929
find_date = datetime.now()
3030
items = []
3131

32+
# Handle new AnchoreCTL format (object with evaluations array)
33+
if isinstance(data, dict) and "evaluations" in data:
34+
logger.info("Detected new AnchoreCTL policies format")
35+
processed_data = []
36+
# Process each evaluation in the evaluations array
37+
for evaluation in data.get("evaluations", []):
38+
# Only process evaluations with findings
39+
if evaluation.get("numberOfFindings", 0) > 0 and evaluation.get("details"):
40+
processed_item = {
41+
"detail": [],
42+
"digest": data.get("imageDigest", ""),
43+
"finalAction": evaluation.get("finalAction", ""),
44+
"finalActionReason": evaluation.get("finalActionReason", ""),
45+
"lastEvaluation": evaluation.get("evaluationTime", ""),
46+
"policyId": data.get("policyId", ""),
47+
"status": evaluation.get("status", ""),
48+
"tag": data.get("evaluatedTag", ""),
49+
}
50+
51+
# Process details if they exist
52+
for detail in evaluation.get("details", []):
53+
processed_item["detail"].append(detail)
54+
55+
processed_data.append(processed_item)
56+
57+
data = processed_data
58+
3259
if not isinstance(data, list):
33-
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"
60+
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"
3461
raise TypeError(msg)
3562

3663
for image in data:
37-
if not isinstance(image, dict) or image.get("detail") is None or not isinstance(image.get("detail"), list):
38-
msg = "This doesn't look like a valid Anchore CTRL Policies report, missing 'detail' list object key for image"
64+
# Skip empty images
65+
if len(data) == 0:
66+
continue
67+
68+
# Check for valid structure
69+
if not isinstance(image, dict):
70+
msg = "This doesn't look like a valid Anchore CTRL Policies report, expected dict object for image"
71+
raise TypeError(msg)
72+
73+
# Handle legacy format with detail field
74+
if image.get("detail") is not None and isinstance(image.get("detail"), list):
75+
details = image.get("detail")
76+
# Handle newer format that might have details under a different structure
77+
elif image.get("details") is not None and isinstance(image.get("details"), list):
78+
details = image.get("details")
79+
else:
80+
msg = "This doesn't look like a valid Anchore CTRL Policies report, missing 'detail' or 'details' list object key for image"
3981
raise ValueError(msg)
4082

41-
for result in image["detail"]:
83+
# Process each finding detail
84+
for result in details:
4285
try:
43-
gate = result["gate"]
44-
description = result["description"]
45-
policy_id = result["policyId"]
46-
status = result["status"]
47-
image_name = result["tag"]
48-
trigger_id = result["triggerId"]
49-
repo, tag = image_name.split(":", 2)
86+
# Extract fields with fallbacks for different formats
87+
gate = result.get("gate", "unknown")
88+
description = result.get("description", "No description provided")
89+
policy_id = result.get("policyId", image.get("policyId", "unknown"))
90+
status = result.get("status", "unknown")
91+
92+
# Handle image tag from different possible locations
93+
image_name = result.get("tag", image.get("tag", "unknown:latest"))
94+
95+
trigger_id = result.get("triggerId", "unknown")
96+
97+
# Split repo and tag safely
98+
if ":" in image_name:
99+
repo, tag = image_name.split(":", 1)
100+
else:
101+
repo = image_name
102+
tag = "latest"
103+
50104
severity, active = get_severity(status, description)
51105
vulnerability_id = extract_vulnerability_id(trigger_id)
52-
title = (
53-
policy_id
54-
+ " - gate|"
55-
+ gate
56-
+ " - trigger|"
57-
+ trigger_id
58-
)
106+
title = policy_id + " - gate|" + gate + " - trigger|" + trigger_id
59107
find = Finding(
60108
title=title,
61109
test=test,
@@ -74,8 +122,10 @@ def get_findings(self, filename, test):
74122
find.unsaved_vulnerability_ids = [vulnerability_id]
75123
items.append(find)
76124
except (KeyError, IndexError) as err:
77-
msg = f"Invalid format: {err} key not found"
78-
raise ValueError(msg)
125+
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'."
126+
logger.warning(msg)
127+
# Continue processing other findings instead of failing completely
128+
continue
79129
return items
80130

81131

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"evaluatedTag": "test/testimage:testtag",
3+
"policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6",
4+
"imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20",
5+
"evaluations": [
6+
{
7+
"evaluationTime": "2022-09-20T08:25:52Z",
8+
"status": "fail",
9+
"finalAction": "stop",
10+
"finalActionReason": "policy_evaluation",
11+
"numberOfFindings": 3,
12+
"details": [
13+
{
14+
"description": "HIGH Vulnerability found in non-os package type (test) - /usr/local/bin/testbinary (CVE-2022-1234)",
15+
"gate": "vulnerabilities",
16+
"imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b",
17+
"policyId": "SoftwareChecks",
18+
"status": "stop",
19+
"tag": "test/testimage:testtag",
20+
"triggerId": "CVE-2022-1234+test",
21+
"triggerName": "package"
22+
},
23+
{
24+
"description": "MEDIUM Vulnerability found in non-os package type (test2) - /usr/local/bin/testbinary (fixed in: 1.2.3)(GHSA-1234-abcd-5678)",
25+
"gate": "vulnerabilities",
26+
"imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b",
27+
"policyId": "SoftwareChecks",
28+
"status": "stop",
29+
"tag": "test/testimage:testtag",
30+
"triggerId": "GHSA-1234-abcd-5678+test2",
31+
"triggerName": "package"
32+
},
33+
{
34+
"description": "User root found as effective user, which is not on the allowed list",
35+
"gate": "dockerfile",
36+
"imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b",
37+
"policyId": "RootUser",
38+
"status": "warn",
39+
"tag": "test/testimage:testtag",
40+
"triggerId": "b2605c2ddbdb02b8e2365c9248dada5a",
41+
"triggerName": "effective_user"
42+
}
43+
]
44+
}
45+
]
46+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"evaluatedTag": "test/testimage:testtag",
3+
"policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6",
4+
"imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20",
5+
"evaluations": [
6+
{
7+
"evaluationTime": "2022-09-20T08:25:52Z",
8+
"status": "pass",
9+
"finalAction": "go",
10+
"finalActionReason": "policy_evaluation",
11+
"numberOfFindings": 0,
12+
"details": []
13+
}
14+
]
15+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"evaluatedTag": "test/testimage:testtag",
3+
"policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6",
4+
"imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20",
5+
"evaluations": [
6+
{
7+
"evaluationTime": "2022-09-20T08:25:52Z",
8+
"status": "fail",
9+
"finalAction": "stop",
10+
"finalActionReason": "policy_evaluation",
11+
"numberOfFindings": 1,
12+
"details": [
13+
{
14+
"description": "User root found as effective user, which is not on the allowed list",
15+
"gate": "dockerfile",
16+
"imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b",
17+
"policyId": "RootUser",
18+
"status": "warn",
19+
"tag": "test/testimage:testtag",
20+
"triggerId": "b2605c2ddbdb02b8e2365c9248dada5a",
21+
"triggerName": "effective_user"
22+
}
23+
]
24+
}
25+
]
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"evaluatedTag": "test/testimage:testtag",
3+
"policyId": "9e104ade-7b57-4cdc-93fb-4949bf3b36b6",
4+
"imageDigest": "sha256:8htz0bf942cfcd6hg8cf6435afd318b65d23e4c1a80044304c6e3ed20",
5+
"evaluations": [
6+
{
7+
"evaluationTime": "2022-09-20T08:25:52Z",
8+
"status": "fail",
9+
"finalAction": "stop",
10+
"finalActionReason": "policy_evaluation",
11+
"numberOfFindings": 1,
12+
"details": [
13+
{
14+
"description": "CRITICAL User root found as effective user, which is not on the allowed list",
15+
"gate": "dockerfile",
16+
"imageId": "d26f0119b9634091a541b081dd8bdca435ab52e114e4b4328575c0bc2c69768b",
17+
"policyId": "RootUser",
18+
"status": "warn",
19+
"tag": "test/testimage:testtag",
20+
"triggerId": "b2605c2ddbdb02b8e2365c9248dada5a",
21+
"triggerName": "effective_user"
22+
}
23+
]
24+
}
25+
]
26+
}

unittests/tools/test_anchorectl_policies_parser.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,36 @@ def test_anchore_engine_parser_has_one_finding_and_description_has_severity(self
3535
self.assertEqual(singleFinding.severity, "Critical")
3636
self.assertEqual(singleFinding.title, "RootUser - gate|dockerfile - trigger|b2605c2ddbdb02b8e2365c9248dada5a")
3737
self.assertEqual(singleFinding.description, "CRITICAL User root found as effective user, which is not on the allowed list")
38+
39+
# Tests for the new AnchoreCTL format
40+
def test_new_format_anchore_engine_parser_has_no_finding(self):
41+
with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_no_violation.json").open(encoding="utf-8") as testfile:
42+
parser = AnchoreCTLPoliciesParser()
43+
findings = parser.get_findings(testfile, Test())
44+
self.assertEqual(0, len(findings))
45+
46+
def test_new_format_anchore_engine_parser_has_one_finding_and_it_is_correctly_parsed(self):
47+
with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_one_violation.json").open(encoding="utf-8") as testfile:
48+
parser = AnchoreCTLPoliciesParser()
49+
findings = parser.get_findings(testfile, Test())
50+
self.assertEqual(1, len(findings))
51+
singleFinding = findings[0]
52+
self.assertEqual(singleFinding.severity, "Medium")
53+
self.assertEqual(singleFinding.title, "RootUser - gate|dockerfile - trigger|b2605c2ddbdb02b8e2365c9248dada5a")
54+
self.assertEqual(singleFinding.description, "User root found as effective user, which is not on the allowed list")
55+
56+
def test_new_format_anchore_engine_parser_has_many_findings(self):
57+
with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_many_violations.json").open(encoding="utf-8") as testfile:
58+
parser = AnchoreCTLPoliciesParser()
59+
findings = parser.get_findings(testfile, Test())
60+
self.assertEqual(3, len(findings))
61+
62+
def test_new_format_anchore_engine_parser_has_one_finding_and_description_has_severity(self):
63+
with (get_unit_tests_scans_path("anchorectl_policies") / "new_format_one_violation_description_severity.json").open(encoding="utf-8") as testfile:
64+
parser = AnchoreCTLPoliciesParser()
65+
findings = parser.get_findings(testfile, Test())
66+
self.assertEqual(1, len(findings))
67+
singleFinding = findings[0]
68+
self.assertEqual(singleFinding.severity, "Critical")
69+
self.assertEqual(singleFinding.title, "RootUser - gate|dockerfile - trigger|b2605c2ddbdb02b8e2365c9248dada5a")
70+
self.assertEqual(singleFinding.description, "CRITICAL User root found as effective user, which is not on the allowed list")

0 commit comments

Comments
 (0)