Skip to content

Commit bfad520

Browse files
authored
Prowler Scan Parser (#13831)
* Draft of prowler parser * prowler parser lint changes * Ruff fixes and formatting * D203 fix * D300 fix * added unittests for all cloud types in both file types * added docs for prowler parser * added docs for prowler parser * security and bug resistance for prowler parser * Update prowler unittests
1 parent aace359 commit bfad520

17 files changed

+2882
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: "Prowler Scan"
3+
toc_hide: true
4+
---
5+
This parser imports Prowler Scan files in JSON and CSV format. The AWS, GCP, Azure, and Kubernetes could types are supported by this parser.
6+
7+
### Sample Scan Data
8+
Sample Prowler scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/prowler).
9+
10+
### Default Deduplication Hashcode Fields
11+
By default, DefectDojo identifies duplicate Findings using these [hashcode fields](https://docs.defectdojo.com/en/working_with_findings/finding_deduplication/about_deduplication/):
12+
13+
- title
14+
- description

dojo/fixtures/test_type.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,12 @@
4747
},
4848
"model": "dojo.test_type",
4949
"pk": 7
50+
},
51+
{
52+
"fields": {
53+
"name": "Prowler"
54+
},
55+
"model": "dojo.test_type",
56+
"pk": 8
5057
}
5158
]

dojo/tools/prowler/__init__.py

Whitespace-only changes.

dojo/tools/prowler/parser.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from dojo.tools.prowler.parser_csv import ProwlerParserCSV
2+
from dojo.tools.prowler.parser_json import ProwlerParserJSON
3+
4+
5+
class ProwlerParser:
6+
7+
"""Prowler is an Open Cloud Security that automates security and compliance in cloud environments. This parser is for Prowler JSON files and Prowler CSV files."""
8+
9+
def get_scan_types(self):
10+
return ["Prowler Scan"]
11+
12+
def get_label_for_scan_types(self, scan_type):
13+
return "Prowler Scan"
14+
15+
def get_description_for_scan_types(self, scan_type):
16+
return "Prowler report file can be imported in JSON format or in CSV format."
17+
18+
def get_findings(self, filename, test):
19+
name = getattr(filename, "name", str(filename)).lower()
20+
if name.endswith(".csv"):
21+
return ProwlerParserCSV().get_findings(filename, test)
22+
if name.endswith(".json"):
23+
return ProwlerParserJSON().get_findings(filename, test)
24+
return []

dojo/tools/prowler/parser_csv.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import csv
2+
import hashlib
3+
import io
4+
5+
from dojo.models import Finding
6+
7+
8+
class ProwlerParserCSV:
9+
10+
"""Parser for Prowler CSV (semicolon-separated)."""
11+
12+
def get_findings(self, filename, test):
13+
if filename is None:
14+
return []
15+
16+
content = filename.read()
17+
if isinstance(content, bytes):
18+
content = content.decode("utf-8")
19+
reader = csv.DictReader(io.StringIO(content), delimiter=";")
20+
csvarray = []
21+
for row in reader:
22+
csvarray.append(row)
23+
24+
dupes = {}
25+
for row in csvarray:
26+
# Skip vulnerability if the status is "PASS", continue parsing is status is "FAIL" or "MANUAL"
27+
if row.get("STATUS") == "PASS":
28+
continue
29+
30+
provider = row.get("PROVIDER", "N/A").upper()
31+
32+
description = (
33+
"**Cloud Type** : "
34+
+ provider
35+
+ "\n\n"
36+
+ "**Description** : "
37+
+ row.get("DESCRIPTION", "N/A")
38+
+ "\n\n"
39+
+ "**Service Name** : "
40+
+ row.get("SERVICE_NAME", "N/A")
41+
+ "\n\n"
42+
+ "**Status Detail** : "
43+
+ row.get("STATUS_EXTENDED", "N/A")
44+
+ "\n\n"
45+
+ "**Finding Created Time** : "
46+
+ row.get("TIMESTAMP", "N/A")
47+
+ "\n\n"
48+
+ "**Region** : "
49+
+ row.get("REGION", "N/A")
50+
+ "\n\n"
51+
+ "**Notes** : "
52+
+ row.get("NOTES", "N/A")
53+
)
54+
55+
related = row.get("RELATED_URL", "")
56+
additional = row.get("ADDITIONAL_URLS", "")
57+
if related:
58+
description += "\n\n**Related URL** : " + related
59+
if additional:
60+
description += "\n\n**Additional URLs** : " + additional
61+
62+
mitigation = (
63+
"**Remediation Recommendation** : "
64+
+ row.get("REMEDIATION_RECOMMENDATION_TEXT", "N/A")
65+
+ "\n\n"
66+
+ "**Remediation Recommendation URL** : "
67+
+ row.get("REMEDIATION_RECOMMENDATION_URL", "N/A")
68+
+ "\n\n"
69+
+ "**Remediation Code Native IaC** : "
70+
+ row.get("REMEDIATION_CODE_NATIVEIAC", "N/A")
71+
+ "\n\n"
72+
+ "**Remediation Code Terraform** : "
73+
+ row.get("REMEDIATION_CODE_TERRAFORM", "N/A")
74+
+ "\n\n"
75+
+ "**Remediation Code CLI** : "
76+
+ row.get("REMEDIATION_CODE_CLI", "N/A")
77+
+ "\n\n"
78+
+ "**Other Remediation Info** : "
79+
+ row.get("REMEDIATION_CODE_OTHER", "N/A")
80+
)
81+
82+
title = row.get("CHECK_TITLE", "")
83+
severity = self.convert_severity(row.get("SEVERITY"))
84+
impact = row.get("RISK", "")
85+
compliance = row.get("COMPLIANCE", "N/A")
86+
87+
# If compliance is not 'N/A', break info into multiple lines
88+
references = "\n".join(part.strip() for part in compliance.split("|")) if compliance != "N/A" else "N/A"
89+
90+
finding = Finding(
91+
title=title,
92+
test=test,
93+
description=description,
94+
severity=severity,
95+
references=references,
96+
mitigation=mitigation,
97+
impact=impact,
98+
static_finding=False,
99+
dynamic_finding=True,
100+
)
101+
102+
key = hashlib.sha256((finding.title + "|" + finding.description).encode("utf-8")).hexdigest()
103+
if key not in dupes:
104+
dupes[key] = finding
105+
106+
return list(dupes.values())
107+
108+
def convert_severity(self, severity: str) -> str:
109+
"""Convert severity value"""
110+
if not severity:
111+
return "Info"
112+
113+
s = severity.lower()
114+
if s == "critical":
115+
return "Critical"
116+
if s == "high":
117+
return "High"
118+
if s == "medium":
119+
return "Medium"
120+
if s == "low":
121+
return "Low"
122+
return "Info"

dojo/tools/prowler/parser_json.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import hashlib
2+
import json
3+
4+
from dojo.models import Finding
5+
6+
7+
class ProwlerParserJSON:
8+
9+
"""This parser is for Prowler JSON files."""
10+
11+
def get_findings(self, file, test):
12+
data = json.load(file)
13+
14+
dupes = {}
15+
for node in data:
16+
# Skip vulnerability if the status is "PASS", continue parsing is status is "FAIL" or "MANUAL"
17+
if node.get("status_code") == "PASS":
18+
continue
19+
20+
cloudtype = self.get_cloud_type(node)
21+
description = (
22+
"**Cloud Type** : "
23+
+ cloudtype
24+
+ "\n\n"
25+
+ "**Finding Description** : "
26+
+ node.get("finding_info", {}).get("desc", "N/A")
27+
+ "\n\n"
28+
+ "**Product Name** : "
29+
+ node.get("metadata", {}).get("product", {}).get("name", "N/A")
30+
+ "\n\n"
31+
+ "**Status Detail** : "
32+
+ node.get("status_detail", "N/A")
33+
+ "\n\n"
34+
+ "**Finding Created Time** : "
35+
+ node.get("finding_info", {}).get("created_time_dt", "N/A")
36+
)
37+
# Add cloud type sepecific information to description
38+
description = self.add_cloud_type_metadata(node, cloudtype, description)
39+
40+
title = node.get("message", "")
41+
severity = self.convert_severity(node.get("severity"))
42+
mitigation = (
43+
"**Remediation Description** : "
44+
+ node.get("remediation", {}).get("desc", "N/A")
45+
+ "\n\n"
46+
+ "**Remediation References** : "
47+
+ ", ".join(node.get("remediation", {}).get("references", []))
48+
)
49+
impact = node.get("risk_details", "")
50+
compliance = node.get("unmapped", {}).get("compliance", {})
51+
references = "**Related URL** : " + node.get("unmapped", {}).get("related_url", "")
52+
# Add data presnet in scan to References
53+
for key, values in compliance.items():
54+
joined = ", ".join(values)
55+
# Ex: CIS-1.10 : 1.2.16
56+
references += f"\n\n**{key}** : {joined}"
57+
58+
finding = Finding(
59+
title=title,
60+
test=test,
61+
description=description,
62+
severity=severity,
63+
references=references,
64+
mitigation=mitigation,
65+
impact=impact,
66+
static_finding=False,
67+
dynamic_finding=True,
68+
)
69+
70+
# internal de-duplication
71+
dupe_key = hashlib.sha256(str(description + title).encode("utf-8")).hexdigest()
72+
if dupe_key in dupes:
73+
find = dupes[dupe_key]
74+
if finding.description:
75+
find.description += "\n" + finding.description
76+
# find.unsaved_endpoints.extend(finding.unsaved_endpoints)
77+
dupes[dupe_key] = find
78+
else:
79+
dupes[dupe_key] = finding
80+
81+
return list(dupes.values())
82+
83+
def convert_severity(self, severity: str) -> str:
84+
"""Convert severity value"""
85+
if not severity:
86+
return "Info"
87+
88+
s = severity.lower()
89+
if s == "critical":
90+
return "Critical"
91+
if s == "high":
92+
return "High"
93+
if s == "medium":
94+
return "Medium"
95+
if s == "low":
96+
return "Low"
97+
return "Info"
98+
99+
def get_cloud_type(self, node: dict) -> str:
100+
"""Determine the cloud type of a Prowler JSON finding. Returns one of: AWS, Azure, Kubernetes, GCP, or N/A"""
101+
# Check for GCP, AWS, or Azure
102+
account_type = node.get("cloud", {}).get("provider")
103+
if account_type:
104+
account_type.lower()
105+
if account_type == "gcp":
106+
return "GCP"
107+
if account_type == "aws":
108+
return "AWS"
109+
if account_type == "azure":
110+
return "AZURE"
111+
112+
# Check for Kubernetes
113+
for resource in node.get("resources", []):
114+
namespace = resource.get("data", {}).get("metadata", {}).get("namespace")
115+
if namespace is not None:
116+
return "KUBERNETES"
117+
118+
# No Cloud Type information was found
119+
return "N/A"
120+
121+
def add_cloud_type_metadata(self, node: dict, cloudtype: str, description: str) -> str:
122+
# Add metadata for GCP, AWS, and Azure
123+
if cloudtype in {"GCP", "AWS", "AZURE"}:
124+
description += "\n\n" + "**" + cloudtype + " Region** : " + node.get("cloud", {}).get("region", "N/A")
125+
return description
126+
127+
# Add metadata for Kubernetes
128+
if cloudtype == "KUBERNETES":
129+
for resource in node.get("resources", []):
130+
pod = resource.get("data", {}).get("metadata", {}).get("name")
131+
namespace = resource.get("data", {}).get("metadata", {}).get("namespace")
132+
if pod is not None:
133+
description += "\n\n" + "**Pod Name** : " + pod
134+
if namespace is not None:
135+
description += "\n\n" + "**Namespace** : " + namespace
136+
return description
137+
return description
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION;ADDITIONAL_URLS
2+
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;accessanalyzer_enabled;Check if IAM Access Analyzer is enabled;IAM;FAIL;IAM Access Analyzer in account <account_uid> is not enabled.;False;accessanalyzer;;low;Other;<resource_uid>;<resource_name>;;;aws;<region>;Check if IAM Access Analyzer is enabled;AWS IAM Access Analyzer helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. IAM Access Analyzer uses a form of mathematical analysis called automated reasoning, which applies logic and mathematical inference to determine all possible access paths allowed by a resource policy.;https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html;Enable IAM Access Analyzer for all accounts, create analyzer and take action over it is recommendations (IAM Access Analyzer is available at no additional cost).;https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html;;;aws accessanalyzer create-analyzer --analyzer-name <NAME> --type <ACCOUNT|ORGANIZATION>;;CIS-1.4: 1.20 | CIS-1.5: 1.20 | KISA-ISMS-P-2023: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | CIS-2.0: 1.20 | KISA-ISMS-P-2023-korean: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | AWS-Account-Security-Onboarding: Enabled security services, Create analyzers in each active regions, Verify that events are present in SecurityHub aggregated view | CIS-3.0: 1.20;;;;;<prowler_version>;https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-getting-started.html | https://aws.amazon.com/iam/features/analyze-access/
3+
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_maintain_current_contact_details;Maintain current contact details.;IAM;MANUAL;Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information.;False;account;;medium;Other;<resource_uid>;<account_uid>;;;aws;<region>;Maintain current contact details.;Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.;;Using the Billing and Cost Management console complete contact details.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;No command available.;https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console;CIS-1.4: 1.1 | CIS-1.5: 1.1 | KISA-ISMS-P-2023: 2.1.3 | CIS-2.0: 1.1 | KISA-ISMS-P-2023-korean: 2.1.3 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.1 | ENS-RD2022: op.ext.7.aws.am.1;;;;;<prowler_version>;https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-getting-started.html | https://aws.amazon.com/iam/features/analyze-access/
4+
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_maintain_different_contact_details_to_security_billing_and_operations;Maintain different contact details to security, billing and operations.;IAM;FAIL;SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact.;False;account;;medium;Other;<resource_uid>;<account_uid>;;;aws;<region>;Maintain different contact details to security, billing and operations.;Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;Using the Billing and Cost Management console complete contact details.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;;https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console;KISA-ISMS-P-2023: 2.1.3 | KISA-ISMS-P-2023-korean: 2.1.3;;;;;<prowler_version>;https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-getting-started.html | https://aws.amazon.com/iam/features/analyze-access/
5+
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_security_contact_information_is_registered;Ensure security contact information is registered.;IAM;MANUAL;Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section.;False;account;;medium;Other;<resource_uid>:root;<account_uid>;;;aws;<region>;Ensure security contact information is registered.;AWS provides customers with the option of specifying the contact information for accounts security team. It is recommended that this information be provided. Specifying security-specific contact information will help ensure that security advisories sent by AWS reach the team in your organization that is best equipped to respond to them.;;Go to the My Account section and complete alternate contacts.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;No command available.;https://docs.prowler.com/checks/aws/iam-policies/iam_19#aws-console;CIS-1.4: 1.2 | CIS-1.5: 1.2 | AWS-Foundational-Security-Best-Practices: account, acm | KISA-ISMS-P-2023: 2.1.3, 2.2.1 | CIS-2.0: 1.2 | KISA-ISMS-P-2023-korean: 2.1.3, 2.2.1 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.2 | ENS-RD2022: op.ext.7.aws.am.1;;;;;<prowler_version>;https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-getting-started.html | https://aws.amazon.com/iam/features/analyze-access/

0 commit comments

Comments
 (0)