diff --git a/docs/content/supported_tools/parsers/file/cloudflare_insights.md b/docs/content/supported_tools/parsers/file/cloudflare_insights.md new file mode 100644 index 00000000000..035ed4f7033 --- /dev/null +++ b/docs/content/supported_tools/parsers/file/cloudflare_insights.md @@ -0,0 +1,22 @@ +--- +title: "Cloudflare Insights" +toc_hide: true +--- + +Import Cloudflare Insights findings using the **CSV export** provided by Cloudflare. + +### Sample Scan Data +Sample Cloudflare Insights files can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/cloudflare_insights). + +### Supported Fields +The parser supports the following CSV columns: + +- `severity` +- `issue_class` +- `subject` +- `issue_type` +- `status` +- `insight` *(optional)* +- `detection_method` *(optional)* +- `risk` *(optional)* +- `recommended_action` diff --git a/dojo/tools/cloudflare_insights/__init__.py b/dojo/tools/cloudflare_insights/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/cloudflare_insights/parser.py b/dojo/tools/cloudflare_insights/parser.py new file mode 100644 index 00000000000..e47065b1e61 --- /dev/null +++ b/dojo/tools/cloudflare_insights/parser.py @@ -0,0 +1,128 @@ +import csv +import io +from urllib.parse import urlparse + +from dojo.models import Endpoint, Finding + + +class CloudflareInsightsParser: + + """ + DefectDojo parser for Cloudflare Insights CSV exports. + + Expected columns: + - severity + - issue_class + - subject (used as Endpoint host; not repeated in description) + - issue_type + - scan_performed_on (ignored) + - status + - insight (optional) + - detection_method (optional) + - risk (optional) + - recommended_action (used as mitigation if present) + """ + + def get_scan_types(self): + return ["Cloudflare Insights"] + + def get_label_for_scan_types(self, scan_type): + return scan_type + + def get_description_for_scan_types(self, scan_type): + return "Import Cloudflare Insights (CSV export)." + + def _map_severity(self, value): + normalized = value.strip().lower() + mapping = { + "low": "Low", + "moderate": "Medium", + "critical": "Critical", + "high": "High", # optional: Cloudflare occasionally uses this + } + return mapping.get(normalized, "Info") + + def _extract_host_from_subject(self, subject: str) -> str | None: + if not subject: + return None + s = subject.strip() + if not s: + return None + parsed = urlparse(s) + netloc = parsed.netloc + if not netloc and ("." in s or ":" in s or s.startswith("localhost")): + parsed2 = urlparse(f"http://{s}") + netloc = parsed2.netloc + host = netloc or s + if ":" in host: + host = host.split(":", 1)[0] + host = host.strip().strip("/").strip() + + return host or None + + def _is_inactive_status(self, status: str) -> bool: + inactive_markers = {"resolved", "mitigated", "closed", "fixed"} + return bool(status) and status.strip().lower() in inactive_markers + + def get_findings(self, filename, test): + content = filename.read() + if isinstance(content, bytes): + content = content.decode("utf-8", errors="replace") + + reader = csv.DictReader( + io.StringIO(content), + delimiter=",", + quotechar='"', + skipinitialspace=True, + ) + findings = [] + for row in reader: + severity_raw = (row.get("severity") or "").strip() + issue_class = (row.get("issue_class") or "").strip() + subject = (row.get("subject") or "").strip() + issue_type = (row.get("issue_type") or "").strip() + status = (row.get("status") or "").strip() + insight = (row.get("insight") or "").strip() + detection_method = (row.get("detection_method") or "").strip() + risk = (row.get("risk") or "").strip() + recommended_action = (row.get("recommended_action") or "").strip() + mapped_severity = self._map_severity(severity_raw) + if issue_type and subject: + title = f"{issue_type}: {subject}" + elif issue_type: + title = issue_type + elif subject: + title = subject + else: + title = "Cloudflare Insight" + description_parts = [] + if issue_class: + description_parts.append(f"**Issue class**: {issue_class}") + if issue_type: + description_parts.append(f"**Issue type**: {issue_type}") + if status: + description_parts.append(f"**Status**: {status}") + if insight: + description_parts.append(f"**Insight**: {insight}") + if detection_method: + description_parts.append(f"**Detection method**: {detection_method}") + if risk: + description_parts.append(f"**Risk**: {risk}") + description = "\n\n".join(description_parts) + finding = Finding( + test=test, + title=title, + severity=mapped_severity, + description=description, + mitigation=recommended_action, + references="Not provided!", + static_finding=False, + dynamic_finding=True, + ) + finding.active = not self._is_inactive_status(status) + host = self._extract_host_from_subject(subject) + if host: + finding.unsaved_endpoints = [Endpoint(host=host, port=None)] + findings.append(finding) + + return findings diff --git a/unittests/scans/cloudflare_insights/many_findings.csv b/unittests/scans/cloudflare_insights/many_findings.csv new file mode 100644 index 00000000000..48a00cbbcd0 --- /dev/null +++ b/unittests/scans/cloudflare_insights/many_findings.csv @@ -0,0 +1,15 @@ +severity,issue_class,subject,issue_type,scan_performed_on,status,insight,detection_method,risk,recommended_action +Moderate,Unproxied 'A' Records,domain1.com,Exposed infrastructure,2024-07-05T05:30:57.976844Z,Active,,,,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." +Moderate,Unproxied 'A' Records,domain2.com,Exposed infrastructure,2024-07-05T05:31:39.692808Z,Active,,,,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." +Low,Security.txt not configured,domain3.com,Configuration suggestion,2024-12-01T05:43:45.712676Z,Active,Security.txt not configured. Configure and manage the Security.txt file to improve the website's vulnerability disclosure process,We evaluated the Security Settings configured for this domain and found that Security.txt is not enabled.,"The absence of Security.txt insights creates a lack of a clear, accessible method for researchers to report vulnerabilities. This can lead to security issues going unnoticed or under-reported, increasing the risk of exploitation.","Configure Security.txt file. " +Low,Security.txt not configured,domain4.com,Configuration suggestion,2024-12-01T05:43:44.252529Z,Active,Security.txt not configured. Configure and manage the Security.txt file to improve the website's vulnerability disclosure process,We evaluated the Security Settings configured for this domain and found that Security.txt is not enabled.,"The absence of Security.txt insights creates a lack of a clear, accessible method for researchers to report vulnerabilities. This can lead to security issues going unnoticed or under-reported, increasing the risk of exploitation.","Configure Security.txt file. " +Moderate,Unproxied CNAME Records,domain5.com,Exposed infrastructure,2024-07-08T03:37:16.031911Z,Active,,,,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." +Moderate,Unproxied 'A' Records,domain6.com,Exposed infrastructure,2024-07-02T12:55:57.798974Z,Active,,,,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." +Low,Security.txt not configured,domain7.com,Configuration suggestion,2025-03-25T17:33:40.070204Z,Active,Security.txt not configured. Configure and manage the Security.txt file to improve the website's vulnerability disclosure process,We evaluated the Security Settings configured for this domain and found that Security.txt is not enabled.,"The absence of Security.txt insights creates a lack of a clear, accessible method for researchers to report vulnerabilities. This can lead to security issues going unnoticed or under-reported, increasing the risk of exploitation.","Configure Security.txt file. " +Low,Security.txt not configured,domain8.com,Configuration suggestion,2025-03-25T17:33:41.970652Z,Active,Security.txt not configured. Configure and manage the Security.txt file to improve the website's vulnerability disclosure process,We evaluated the Security Settings configured for this domain and found that Security.txt is not enabled.,"The absence of Security.txt insights creates a lack of a clear, accessible method for researchers to report vulnerabilities. This can lead to security issues going unnoticed or under-reported, increasing the risk of exploitation.","Configure Security.txt file. " +Moderate,Unproxied 'A' Records,domain9.com,Exposed infrastructure,2024-07-05T05:30:46.435059Z,Active,,,,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." +Low,Security.txt not configured,domain10.com,Configuration suggestion,2024-11-29T05:32:39.671608Z,Active,Security.txt not configured. Configure and manage the Security.txt file to improve the website's vulnerability disclosure process,We evaluated the Security Settings configured for this domain and found that Security.txt is not enabled.,"The absence of Security.txt insights creates a lack of a clear, accessible method for researchers to report vulnerabilities. This can lead to security issues going unnoticed or under-reported, increasing the risk of exploitation.","Configure Security.txt file. " +Low,Security.txt not configured,domain11.com,Configuration suggestion,2025-03-06T15:16:53.931468Z,Active,Security.txt not configured. Configure and manage the Security.txt file to improve the website's vulnerability disclosure process,We evaluated the Security Settings configured for this domain and found that Security.txt is not enabled.,"The absence of Security.txt insights creates a lack of a clear, accessible method for researchers to report vulnerabilities. This can lead to security issues going unnoticed or under-reported, increasing the risk of exploitation.","Configure Security.txt file. " +Moderate,Unproxied CNAME Records,domain12.com,Exposed infrastructure,2026-01-02T12:29:43.13416Z,Active,Unproxied CNAME Records. This DNS record is not proxied by Cloudflare. Your origin server is directly exposed and has a higher risk of a DDoS attack.,We reviewed your Cloudflare DNS settings and checked whether your hostname accepts connections on either port 80 or 443.,DDoS Attack,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." +Moderate,Unproxied 'A' Records,domain13.com,Exposed infrastructure,2024-07-02T12:57:30.878124Z,Active,,,,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." +Critical,Managed Rules not deployed,domain14.com,Configuration suggestion,2024-07-01T17:44:27.896818Z,Active,Managed Rules not deployed. We have detected that you have not enabled the Cloudflare Managed Rules feature on your zone.,We evaluated your websites and you have no Managed Rules deployed.,Insufficient protection for vulnerabilities targeting Web and API applications,Turn on Managed Rules. Deploy Cloudflare Managed Rules on your zone to protect your web application against common vulnerabilities in web applications. \ No newline at end of file diff --git a/unittests/scans/cloudflare_insights/one_finding.csv b/unittests/scans/cloudflare_insights/one_finding.csv new file mode 100644 index 00000000000..9d376771de0 --- /dev/null +++ b/unittests/scans/cloudflare_insights/one_finding.csv @@ -0,0 +1,2 @@ +severity,issue_class,subject,issue_type,scan_performed_on,status,insight,detection_method,risk,recommended_action +Moderate,Unproxied 'A' Records,domain.com,Exposed infrastructure,2024-07-06T13:50:15.536086Z,Active,,,,"Configure Cloudflare to proxy the DNS record. By setting up Cloudflare as your hostname's reverse proxy, Cloudflare protects origin servers from DDoS attacks by hiding their IP addresses. You can configure Cloudflare to proxy your hostname in your DNS settings." \ No newline at end of file diff --git a/unittests/tools/test_cloudflare_insights_parser.py b/unittests/tools/test_cloudflare_insights_parser.py new file mode 100644 index 00000000000..8d48280df82 --- /dev/null +++ b/unittests/tools/test_cloudflare_insights_parser.py @@ -0,0 +1,24 @@ +from dojo.models import Test +from dojo.tools.cloudflare_insights.parser import CloudflareInsightsParser +from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path + + +class TestCloudflareInsightsParser(DojoTestCase): + + def test_cloudflare_insights_parser_with_one_finding(self): + with (get_unit_tests_scans_path("cloudflare_insights") / "one_finding.csv").open(encoding="utf-8") as testfile: + parser = CloudflareInsightsParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + finding = findings[0] + self.assertEqual("Exposed infrastructure: domain.com", finding.title) + self.assertEqual("Medium", finding.severity) + + def test_cloudflare_insights_parser_with_many_findings(self): + with (get_unit_tests_scans_path("cloudflare_insights") / "many_findings.csv").open(encoding="utf-8") as testfile: + parser = CloudflareInsightsParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(14, len(findings)) + finding = findings[0] + self.assertEqual("Exposed infrastructure: domain1.com", finding.title) + self.assertEqual("Medium", finding.severity)