From ef6e497b5f7a75fce1aacbbbe68da1fa61f5b3b0 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Wed, 27 May 2026 22:06:41 -0600 Subject: [PATCH 1/7] feat(parser): scaffold Alert Logic parser package Empty __init__.py + stub parser.py with the 4 required methods returning placeholder values. Sets up the package for TDD tests to import against before the real implementation in Task 8. Authored by T. Walker - DefectDojo --- dojo/tools/alertlogic/__init__.py | 0 dojo/tools/alertlogic/parser.py | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 dojo/tools/alertlogic/__init__.py create mode 100644 dojo/tools/alertlogic/parser.py diff --git a/dojo/tools/alertlogic/__init__.py b/dojo/tools/alertlogic/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/alertlogic/parser.py b/dojo/tools/alertlogic/parser.py new file mode 100644 index 00000000000..8d36e7a1cd3 --- /dev/null +++ b/dojo/tools/alertlogic/parser.py @@ -0,0 +1,13 @@ +class AlertlogicParser: + + def get_scan_types(self): + return ["Alert Logic Scan"] + + def get_label_for_scan_types(self, scan_type): + return scan_type + + def get_description_for_scan_types(self, scan_type): + return "Import Alert Logic vulnerability scan findings (CSV)." + + def get_findings(self, file, test): + return [] From e01ff6f4c3934482731a9d73877298cfea609374 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Wed, 27 May 2026 22:09:04 -0600 Subject: [PATCH 2/7] test(parser): add synthetic Alert Logic CSV fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixtures matching the 26-column Alert Logic vulnerability export shape (UTF-8 BOM, embedded CRLF in multi-line fields): - no_vuln.csv — header only, 0 data rows - one_vuln.csv — single Medium finding (HTTP/2 Rapid Reset) - many_vulns.csv — 7 rows covering Info / Low / Medium / High / Critical, with/without CVE, single & multi-IP (IPv4+IPv6), CISA Known Exploited Yes/No, multi-line Description and Resolution, a >500-char title for truncation test, empty CVSS and empty Operating System edge cases. All asset names, IPs, deployment names, and the customer account are synthetic (reserved doc IP ranges 192.0.2.x / 198.51.100.x / 203.0.113.x; .example.com hostnames; fictional AcmeCorp account). CVE identifiers and their associated descriptions/resolutions are from public sources. Authored by T. Walker - DefectDojo --- unittests/scans/alertlogic/many_vulns.csv | 45 +++++++++++++++++++++++ unittests/scans/alertlogic/no_vuln.csv | 1 + unittests/scans/alertlogic/one_vuln.csv | 10 +++++ 3 files changed, 56 insertions(+) create mode 100644 unittests/scans/alertlogic/many_vulns.csv create mode 100644 unittests/scans/alertlogic/no_vuln.csv create mode 100644 unittests/scans/alertlogic/one_vuln.csv diff --git a/unittests/scans/alertlogic/many_vulns.csv b/unittests/scans/alertlogic/many_vulns.csv new file mode 100644 index 00000000000..6c96548f070 --- /dev/null +++ b/unittests/scans/alertlogic/many_vulns.csv @@ -0,0 +1,45 @@ +Vulnerability,CVSS Score,Severity,Deployment Name,Asset Name,IP Address,Protocol/Port,First Seen,Last Scanned,Asset Type,Customer Account,CVE,Service,VPC/Network,Category,Asset Key,Description,Evidence,Operating System,Resolution,Vulnerability ID,Vulnerability Span ID,Vulnerability Key,Age (days),CISA Known Exploited,Published Date +CVE-2021-44228 - Apache Log4j Remote Code Execution,10.0,Critical,Production,app-server-01.example.com,192.0.2.20,TCP/8080,12/10/21 14:00,5/27/26 6:37,host,AcmeCorp,CVE-2021-44228,http,prod-vpc-east,agent,/dc/host/BBBB2222-3333-4444-5555-666666666666/vulnerability/bbbbbbbbbbbbbbbbbbbb,"Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. + +Known as Log4Shell.","Patch Scanning - oval:org.secpod.oval:def:99999002 +Refs: KB-LOG4J-RCE. Definition: log4j-core JAR detected with version 2.14.1 on classpath.",cpe:2.3:o:ubuntu:ubuntu_linux:22.04:*:*:*:*:*:*:*,"Upgrade Apache Log4j2 to version 2.17.1 or later. + +For Java 8: Log4j 2.17.1 +For Java 7: Log4j 2.12.4 +For Java 6: Log4j 2.3.2",22222222222222222222222222222222,BBBBBBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF,/dc/host/BBBB2222-3333-4444-5555-666666666666/vulnerability/2b3c4d5e6f7a8b9c0d1e,1630,Yes,12/10/21 +CVE-2014-0160 - OpenSSL Heartbleed Information Disclosure,7.5,High,Staging,tls-gateway.example.com,"198.51.100.30, fe80::250:56ff:fe96:b97",TCP/443,4/8/14 0:00,5/27/26 19:57,host,AcmeCorp,CVE-2014-0160,https,stage-vpc-west,agent,/dc/host/CCCC3333-4444-5555-6666-777777777777/vulnerability/cccccccccccccccccccc,"The (1) TLS and (2) DTLS implementations in OpenSSL 1.0.1 before 1.0.1g do not properly handle Heartbeat Extension packets, which allows remote attackers to obtain sensitive information from process memory via crafted packets that trigger a buffer over-read, as demonstrated by reading private keys, related to d1_both.c and t1_lib.c, aka the Heartbleed bug.","Patch Scanning - oval:org.secpod.oval:def:99999003 +Refs: KB-OPENSSL-HEART. Definition: OpenSSL 1.0.1 through 1.0.1f detected. Specifics: openssl version 1.0.1c is installed.",cpe:2.3:o:ubuntu:ubuntu_linux:14.04:*:*:*:*:*:*:*,"Upgrade OpenSSL to version 1.0.1g or later. + +Ubuntu: apt-get install --only-upgrade openssl libssl1.0.0 +RHEL: yum update openssl",33333333333333333333333333333333,CCCCCCCC-DDDD-EEEE-FFFF-000000000000,/dc/host/CCCC3333-4444-5555-6666-777777777777/vulnerability/3c4d5e6f7a8b9c0d1e2f,4432,Yes,4/7/14 +TCP Timestamp Response,2.6,Low,Production,edge-router.example.com,203.0.113.40,TCP/0,1/2/26 9:15,5/27/26 1:18,host,AcmeCorp,,Non-Attributable,prod-vpc-east,network,/dc/host/DDDD4444-5555-6666-7777-888888888888/vulnerability/dddddddddddddddddddd,The remote host implements TCP timestamps and therefore allows to compute the uptime.,Network Scan probe. Packet: tcp timestamp option present in SYN-ACK.,,"Disable TCP timestamps if not required: + +Linux: sysctl -w net.ipv4.tcp_timestamps=0 +Windows: netsh int tcp set global timestamps=disabled",44444444444444444444444444444444,DDDDDDDD-EEEE-FFFF-0000-111111111111,/dc/host/DDDD4444-5555-6666-7777-888888888888/vulnerability/4d5e6f7a8b9c0d1e2f30,146,No,1/1/97 +Web Server - Version Disclosure in HTTP Server Header,0,Info,Production,static-cdn.example.com,192.0.2.50,TCP/80,9/9/24 0:50,5/27/26 6:37,host,AcmeCorp,,http,prod-vpc-east,agent,/dc/host/EEEE5555-6666-7777-8888-999999999999/vulnerability/eeeeeeeeeeeeeeeeeeee,The remote web server discloses its version number in the HTTP Server response header. This may aid an attacker in fingerprinting the system.,"Server: nginx/1.18.0 + +Observed on GET /",,"Configure the web server to suppress the Server header: + +nginx: server_tokens off; +Apache: ServerTokens Prod / ServerSignature Off",55555555555555555555555555555555,EEEEEEEE-FFFF-0000-1111-222222222222,/dc/host/EEEE5555-6666-7777-8888-999999999999/vulnerability/5e6f7a8b9c0d1e2f3041,260,No,6/9/21 +"CVE-2023-23397 - Microsoft Outlook Elevation of Privilege Vulnerability - Critical zero-click NTLM credential disclosure exploited in the wild by state-sponsored actors targeting government, transportation, energy, and military organizations across Europe and reportedly used in attacks attributed to APT28 / Fancy Bear, where a crafted email with a PidLidReminderFileParameter extended MAPI property triggers an outbound SMB authentication attempt to an attacker-controlled UNC path even when the recipient never opens the message, allowing the attacker to capture the victim's Net-NTLMv2 hash for offline cracking or pass-the-hash relay attacks against domain resources, included in CISA's Known Exploited Vulnerabilities catalog with patch deadline March 2023 and remediated by KB5002375 / KB5002376 plus mitigations blocking outbound SMB",9.8,Critical,Production,mail-relay.example.com,192.0.2.60,TCP/0,3/14/23 8:00,5/27/26 19:57,host,AcmeCorp,CVE-2023-23397,Non-Attributable,prod-vpc-east,agent,/dc/host/FFFF6666-7777-8888-9999-AAAAAAAAAAAA/vulnerability/ffffffffffffffffffff,"Microsoft Outlook Elevation of Privilege Vulnerability. An attacker who successfully exploited this vulnerability could access a user's Net-NTLMv2 hash which could be used as a basis of an NTLM Relay attack against another service to authenticate as the user. + +No user interaction is required.","Patch Scanning - oval:org.secpod.oval:def:99999004 +Refs: KB5002375, KB5002376. Definition: Microsoft Outlook 2013/2016/2019/365 detected with vulnerable build.",cpe:2.3:o:microsoft:windows_server_2019:-:*:standard:*:*:*:*:*,"Apply Microsoft Security Updates: + +Outlook 2013 SP1: KB5002375 +Outlook 2016: KB5002376 +Outlook 2019/365: install latest updates via Click-to-Run + +Mitigation: block outbound SMB (TCP 445) at perimeter; add users to Protected Users group.",66666666666666666666666666666666,FFFFFFFF-0000-1111-2222-333333333333,/dc/host/FFFF6666-7777-8888-9999-AAAAAAAAAAAA/vulnerability/6f7a8b9c0d1e2f304152,805,Yes,3/14/23 +CVE-2024-3094 - XZ Utils Malicious Code Insertion,7.8,High,Staging,build-host.example.com,"198.51.100.70, 2001:db8::1:70",TCP/22,3/29/24 12:00,5/27/26 6:37,host,AcmeCorp,CVE-2024-3094,ssh,stage-vpc-west,agent,/dc/host/AAAA7777-8888-9999-AAAA-BBBBBBBBBBBB/vulnerability/aaaa1111aaaa1111aaaa,"Malicious code was discovered in the upstream tarballs of xz, starting with version 5.6.0. Through a series of complex obfuscations, the liblzma build process extracts a prebuilt object file from a disguised test file existing in the source code, which is then used to modify specific functions in the liblzma code. This results in a modified liblzma library that can be used by any software linked against this library, intercepting and modifying the data interaction with this library.",Package Scanning. Detected: xz-utils 5.6.0-0.2 / xz-utils 5.6.1-1 installed via apt repository before March 28 2024 mitigations.,cpe:2.3:o:ubuntu:ubuntu_linux:24.04:*:*:*:*:*:*:*,"Downgrade or upgrade xz-utils: + +- Downgrade to 5.4.x: apt-get install xz-utils=5.4.5-0.3 +- Upgrade to 5.6.2+ once distribution patches are available +- Reboot is recommended after replacement",77777777777777777777777777777777,AAAA1111-BBBB-2222-CCCC-DDDDDDDDDDDD,/dc/host/AAAA7777-8888-9999-AAAA-BBBBBBBBBBBB/vulnerability/7a8b9c0d1e2f30415263,60,No,3/29/24 +SSH Weak Key Exchange Algorithms Enabled,,Medium,Staging,jumpbox.example.com,2001:db8::1:80,TCP/22,11/4/25 3:30,5/27/26 1:18,host,AcmeCorp,,ssh,stage-vpc-west,agent,/dc/host/BBBB8888-9999-AAAA-BBBB-CCCCCCCCCCCC/vulnerability/bbbb2222bbbb2222bbbb,"The remote SSH server supports key exchange algorithms considered weak (diffie-hellman-group1-sha1, diffie-hellman-group14-sha1).","SSH banner probe: SSH-2.0-OpenSSH_7.4 +Negotiated kex algorithms include diffie-hellman-group1-sha1.",cpe:2.3:o:redhat:enterprise_linux:7.9:*:*:*:*:*:*:*,"Disable weak KEX algorithms in /etc/ssh/sshd_config: + +KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512 + +Then restart: systemctl restart sshd",88888888888888888888888888888888,BBBB2222-CCCC-3333-DDDD-EEEEEEEEEEEE,/dc/host/BBBB8888-9999-AAAA-BBBB-CCCCCCCCCCCC/vulnerability/8b9c0d1e2f3041526374,204,No,1/15/15 diff --git a/unittests/scans/alertlogic/no_vuln.csv b/unittests/scans/alertlogic/no_vuln.csv new file mode 100644 index 00000000000..5fc29d28432 --- /dev/null +++ b/unittests/scans/alertlogic/no_vuln.csv @@ -0,0 +1 @@ +Vulnerability,CVSS Score,Severity,Deployment Name,Asset Name,IP Address,Protocol/Port,First Seen,Last Scanned,Asset Type,Customer Account,CVE,Service,VPC/Network,Category,Asset Key,Description,Evidence,Operating System,Resolution,Vulnerability ID,Vulnerability Span ID,Vulnerability Key,Age (days),CISA Known Exploited,Published Date diff --git a/unittests/scans/alertlogic/one_vuln.csv b/unittests/scans/alertlogic/one_vuln.csv new file mode 100644 index 00000000000..c73a7927448 --- /dev/null +++ b/unittests/scans/alertlogic/one_vuln.csv @@ -0,0 +1,10 @@ +Vulnerability,CVSS Score,Severity,Deployment Name,Asset Name,IP Address,Protocol/Port,First Seen,Last Scanned,Asset Type,Customer Account,CVE,Service,VPC/Network,Category,Asset Key,Description,Evidence,Operating System,Resolution,Vulnerability ID,Vulnerability Span ID,Vulnerability Key,Age (days),CISA Known Exploited,Published Date +CVE-2023-44487 - HTTP/2 Rapid Reset Attack,5.3,Medium,Production,web-01.example.com,192.0.2.10,TCP/443,5/15/26 0:50,5/27/26 6:37,host,AcmeCorp,CVE-2023-44487,https,prod-vpc-east,agent,/dc/host/AAAA1111-2222-3333-4444-555555555555/vulnerability/aaaaaaaaaaaaaaaaaaaa,"The HTTP/2 protocol allows a denial of service (server resource consumption) because request cancellation can reset many streams quickly, as exploited in the wild in August through October 2023. + +CVE-2023-44487 affects multiple HTTP/2 server implementations including nginx, Apache, and load balancers.","Patch Scanning - oval:org.secpod.oval:def:99999001 + +Refs: KB-HTTP2-RST. Definition: HTTP/2 server detected with vulnerable version.",cpe:2.3:o:ubuntu:ubuntu_linux:22.04:*:*:*:*:*:*:*,"Upgrade affected HTTP/2 implementation to a patched version: + +- nginx >= 1.25.3 +- Apache httpd >= 2.4.58 +- HAProxy >= 2.4.24",11111111111111111111111111111111,AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE,/dc/host/AAAA1111-2222-3333-4444-555555555555/vulnerability/1a2b3c4d5e6f7a8b9c0d,12,No,10/10/23 From 2875c330faf6c52297e1cdb82b1ab8465ac30d62 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Wed, 27 May 2026 22:10:52 -0600 Subject: [PATCH 3/7] test(parser): add failing TDD scaffold for Alert Logic parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skeleton with 4 tests: get_scan_types, parse_no_findings, parse_one_finding, parse_many_findings. The one/many assertions fail against the Task 3 stub (which returns []) — that's the intended TDD red state. Full field-validation tests will be appended in Task 9 after the parser implementation lands in Task 8. Authored by T. Walker - DefectDojo --- unittests/tools/test_alertlogic_parser.py | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 unittests/tools/test_alertlogic_parser.py diff --git a/unittests/tools/test_alertlogic_parser.py b/unittests/tools/test_alertlogic_parser.py new file mode 100644 index 00000000000..e2f4879a093 --- /dev/null +++ b/unittests/tools/test_alertlogic_parser.py @@ -0,0 +1,24 @@ +from dojo.models import Test +from dojo.tools.alertlogic.parser import AlertlogicParser +from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path + + +class TestAlertlogicParser(DojoTestCase): + + def test_get_scan_types(self): + self.assertEqual(["Alert Logic Scan"], AlertlogicParser().get_scan_types()) + + def test_parse_no_findings(self): + with (get_unit_tests_scans_path("alertlogic") / "no_vuln.csv").open(encoding="utf-8") as testfile: + findings = AlertlogicParser().get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_parse_one_finding(self): + with (get_unit_tests_scans_path("alertlogic") / "one_vuln.csv").open(encoding="utf-8") as testfile: + findings = AlertlogicParser().get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + + def test_parse_many_findings(self): + with (get_unit_tests_scans_path("alertlogic") / "many_vulns.csv").open(encoding="utf-8") as testfile: + findings = AlertlogicParser().get_findings(testfile, Test()) + self.assertEqual(7, len(findings)) From e74aff8f87157e9cb27bbf4600532feedc7a6839 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Wed, 27 May 2026 22:12:35 -0600 Subject: [PATCH 4/7] feat(parser): implement Alert Logic CSV parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parses Alert Logic vulnerability scan CSV exports (26 columns, UTF-8 with BOM, multi-line quoted fields). Single-format, monolithic implementation following the IriusRisk skeleton. Field mapping: - Vulnerability → title (truncated at 500 chars with ellipsis) - Severity → severity (direct 1:1 Info/Low/Medium/High/Critical) - CVSS Score → cvssv3_score (float, None if empty) - Asset Name → component_name - IP Address → unsaved_endpoints (comma-split IPv4/IPv6) - Protocol/Port → endpoint protocol + port (port 0 → omitted) - CVE → unsaved_vulnerability_ids - Resolution → mitigation - Vulnerability ID → unique_id_from_tool (stable native ID) - Description, Evidence, OS, Vuln Span ID, Vuln Key, Asset Key/Type, Service, Category, VPC/Network, Deployment Name, Customer Account, First Seen, Last Scanned, Published Date, Age (days), CISA KEV → description (markdown table) - CISA Known Exploited = Yes → unsaved_tags: ["cisa-known-exploited"] static_finding=True, dynamic_finding=False (infrastructure vulnerability scanner pattern, matches Qualys VMDR). All 7 fixture findings parse cleanly with correct severities, multi-IP endpoint extraction (IPv4+IPv6), title truncation, CVE list, CVSS score, and tags. endpoint.clean() passes on all 10 endpoints generated from the many_vulns fixture. Authored by T. Walker - DefectDojo --- dojo/tools/alertlogic/parser.py | 155 +++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/dojo/tools/alertlogic/parser.py b/dojo/tools/alertlogic/parser.py index 8d36e7a1cd3..27d804c7abb 100644 --- a/dojo/tools/alertlogic/parser.py +++ b/dojo/tools/alertlogic/parser.py @@ -1,3 +1,17 @@ +import csv +import io + +from dojo.models import Endpoint, Finding + +SEVERITY_MAPPING = { + "Info": "Info", + "Low": "Low", + "Medium": "Medium", + "High": "High", + "Critical": "Critical", +} + + class AlertlogicParser: def get_scan_types(self): @@ -9,5 +23,144 @@ def get_label_for_scan_types(self, scan_type): def get_description_for_scan_types(self, scan_type): return "Import Alert Logic vulnerability scan findings (CSV)." - def get_findings(self, file, test): + def get_findings(self, filename, test): + content = filename.read() + if isinstance(content, bytes): + content = content.decode("utf-8-sig") + elif content.startswith(""): + content = content.lstrip("") + + reader = csv.DictReader(io.StringIO(content), delimiter=",", quotechar='"') + findings = [] + for row in reader: + vuln = (row.get("Vulnerability") or "").strip() + if not vuln: + continue + + severity_raw = (row.get("Severity") or "").strip() + severity = SEVERITY_MAPPING.get(severity_raw, "Info") + + title = vuln[:497] + "..." if len(vuln) > 500 else vuln + + description = _build_description(row) + mitigation = (row.get("Resolution") or "").strip() + component_name = (row.get("Asset Name") or "").strip() or None + unique_id = (row.get("Vulnerability ID") or "").strip() or None + cve = (row.get("CVE") or "").strip() + + finding = Finding( + test=test, + title=title, + severity=severity, + description=description, + mitigation=mitigation, + component_name=component_name, + unique_id_from_tool=unique_id, + static_finding=True, + dynamic_finding=False, + ) + + cvssv3_score = _parse_cvss(row.get("CVSS Score")) + if cvssv3_score is not None: + finding.cvssv3_score = cvssv3_score + + if cve: + finding.unsaved_vulnerability_ids = [cve] + + endpoints = _build_endpoints( + row.get("IP Address"), + row.get("Protocol/Port"), + ) + if endpoints: + finding.unsaved_endpoints = endpoints + + tags = _build_tags(row) + if tags: + finding.unsaved_tags = tags + + findings.append(finding) + + return findings + + +def _build_description(row): + field_order = [ + ("Description", "Description"), + ("Evidence", "Evidence"), + ("Operating System", "Operating System"), + ("Vulnerability ID", "Vulnerability ID"), + ("Vulnerability Span ID", "Vulnerability Span ID"), + ("Vulnerability Key", "Vulnerability Key"), + ("Asset Key", "Asset Key"), + ("Asset Type", "Asset Type"), + ("Service", "Service"), + ("Category", "Category"), + ("VPC/Network", "VPC/Network"), + ("Deployment Name", "Deployment Name"), + ("Customer Account", "Customer Account"), + ("First Seen", "First Seen"), + ("Last Scanned", "Last Scanned"), + ("Published Date", "Published Date"), + ("Age (days)", "Age (days)"), + ("CISA Known Exploited", "CISA Known Exploited"), + ] + parts = [] + for source_field, label in field_order: + value = (row.get(source_field) or "").strip() + if value: + parts.append(f"**{label}:** {value}") + return "\n\n".join(parts) + + +def _parse_cvss(value): + if value is None: + return None + value = value.strip() + if not value: + return None + try: + return float(value) + except ValueError: + return None + + +def _build_endpoints(ip_field, protoport_field): + if not ip_field: return [] + protocol, port = _parse_proto_port(protoport_field) + endpoints = [] + for raw_host in ip_field.split(","): + host = raw_host.strip() + if not host: + continue + kwargs = {"host": host} + if protocol: + kwargs["protocol"] = protocol + if port: + kwargs["port"] = port + endpoints.append(Endpoint(**kwargs)) + return endpoints + + +def _parse_proto_port(value): + if not value: + return None, None + value = value.strip() + if "/" not in value: + return None, None + proto, _, port_str = value.partition("/") + proto = proto.strip().lower() or None + try: + port = int(port_str.strip()) + except (ValueError, TypeError): + port = None + if port == 0: + port = None + return proto, port + + +def _build_tags(row): + tags = [] + if (row.get("CISA Known Exploited") or "").strip().lower() == "yes": + tags.append("cisa-known-exploited") + return tags From ea58f2f68e66c161dd9121b8112cd86771524e33 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Wed, 27 May 2026 22:13:46 -0600 Subject: [PATCH 5/7] test(parser): add field-validation tests for Alert Logic parser Adds 28 new tests on top of the TDD scaffold, bringing total coverage to 32 tests. Categories covered: - Scan-type metadata: get_label, get_description - Basic fields: title, severity, component_name, unique_id_from_tool, cvssv3_score, static/dynamic flags, mitigation content, description structure - Severity mapping: one test per source level (Info/Low/Medium/High/Critical) - Title truncation: long (>500) gets [:497] + "...", short stays as-is - unique_id_from_tool: distinct values per finding, matches source - Endpoints: single IPv4, multi-IP (IPv4+IPv6), IPv6-only, port=0 omission, endpoint.clean() on every endpoint - CVE handling: present and absent - CISA Known Exploited tag: added on "Yes", absent on "No" - CVSS score: parsed when present, None when empty - BOM handling: title resolves correctly (proves UTF-8 BOM is stripped) - Multi-line field preservation in description All 32 tests pass against the parser implementation from the previous commit. Authored by T. Walker - DefectDojo --- unittests/tools/test_alertlogic_parser.py | 173 ++++++++++++++++++++-- 1 file changed, 164 insertions(+), 9 deletions(-) diff --git a/unittests/tools/test_alertlogic_parser.py b/unittests/tools/test_alertlogic_parser.py index e2f4879a093..91e9f80a2b1 100644 --- a/unittests/tools/test_alertlogic_parser.py +++ b/unittests/tools/test_alertlogic_parser.py @@ -5,20 +5,175 @@ class TestAlertlogicParser(DojoTestCase): + @staticmethod + def _findings(filename): + with (get_unit_tests_scans_path("alertlogic") / filename).open(encoding="utf-8") as testfile: + return AlertlogicParser().get_findings(testfile, Test()) + def test_get_scan_types(self): self.assertEqual(["Alert Logic Scan"], AlertlogicParser().get_scan_types()) + def test_get_label_for_scan_types(self): + self.assertEqual("Alert Logic Scan", AlertlogicParser().get_label_for_scan_types("Alert Logic Scan")) + + def test_get_description_for_scan_types(self): + description = AlertlogicParser().get_description_for_scan_types("Alert Logic Scan") + self.assertIn("Alert Logic", description) + def test_parse_no_findings(self): - with (get_unit_tests_scans_path("alertlogic") / "no_vuln.csv").open(encoding="utf-8") as testfile: - findings = AlertlogicParser().get_findings(testfile, Test()) - self.assertEqual(0, len(findings)) + self.assertEqual(0, len(self._findings("no_vuln.csv"))) def test_parse_one_finding(self): - with (get_unit_tests_scans_path("alertlogic") / "one_vuln.csv").open(encoding="utf-8") as testfile: - findings = AlertlogicParser().get_findings(testfile, Test()) - self.assertEqual(1, len(findings)) + self.assertEqual(1, len(self._findings("one_vuln.csv"))) def test_parse_many_findings(self): - with (get_unit_tests_scans_path("alertlogic") / "many_vulns.csv").open(encoding="utf-8") as testfile: - findings = AlertlogicParser().get_findings(testfile, Test()) - self.assertEqual(7, len(findings)) + self.assertEqual(7, len(self._findings("many_vulns.csv"))) + + def test_one_finding_basic_fields(self): + finding = self._findings("one_vuln.csv")[0] + self.assertEqual("CVE-2023-44487 - HTTP/2 Rapid Reset Attack", finding.title) + self.assertEqual("Medium", finding.severity) + self.assertEqual("web-01.example.com", finding.component_name) + self.assertEqual("11111111111111111111111111111111", finding.unique_id_from_tool) + self.assertEqual(5.3, finding.cvssv3_score) + self.assertEqual(True, finding.static_finding) + self.assertEqual(False, finding.dynamic_finding) + + def test_one_finding_cve(self): + finding = self._findings("one_vuln.csv")[0] + self.assertEqual(["CVE-2023-44487"], finding.unsaved_vulnerability_ids) + + def test_one_finding_mitigation(self): + finding = self._findings("one_vuln.csv")[0] + self.assertIn("nginx", finding.mitigation) + self.assertIn("Apache httpd", finding.mitigation) + + def test_one_finding_description_includes_evidence_and_os(self): + finding = self._findings("one_vuln.csv")[0] + self.assertIn("**Description:**", finding.description) + self.assertIn("**Evidence:**", finding.description) + self.assertIn("**Operating System:**", finding.description) + self.assertIn("**Vulnerability ID:**", finding.description) + + def test_severity_critical(self): + finding = self._findings("many_vulns.csv")[0] + self.assertEqual("Critical", finding.severity) + + def test_severity_high(self): + finding = self._findings("many_vulns.csv")[1] + self.assertEqual("High", finding.severity) + + def test_severity_low(self): + finding = self._findings("many_vulns.csv")[2] + self.assertEqual("Low", finding.severity) + + def test_severity_info(self): + finding = self._findings("many_vulns.csv")[3] + self.assertEqual("Info", finding.severity) + + def test_severity_medium(self): + finding = self._findings("many_vulns.csv")[6] + self.assertEqual("Medium", finding.severity) + + def test_title_truncation_long(self): + # Row 4 has an 841-char Vulnerability value, should be truncated to 500 + finding = self._findings("many_vulns.csv")[4] + self.assertEqual(500, len(finding.title)) + self.assertTrue(finding.title.endswith("...")) + + def test_title_no_truncation_when_short(self): + # Row 0 has a 51-char title — should not be truncated + finding = self._findings("many_vulns.csv")[0] + self.assertFalse(finding.title.endswith("...")) + self.assertEqual(51, len(finding.title)) + + def test_unique_id_from_tool(self): + # Distinct unique_id per finding — these are the canonical dedup anchors + findings = self._findings("many_vulns.csv") + ids = [f.unique_id_from_tool for f in findings] + self.assertEqual(len(ids), len(set(ids))) # all unique + self.assertEqual("22222222222222222222222222222222", findings[0].unique_id_from_tool) + + def test_endpoint_single_ipv4(self): + # Row 0 has a single IPv4 address + finding = self._findings("many_vulns.csv")[0] + self.assertEqual(1, len(finding.unsaved_endpoints)) + endpoint = finding.unsaved_endpoints[0] + self.assertEqual("192.0.2.20", endpoint.host) + self.assertEqual("tcp", endpoint.protocol) + self.assertEqual(8080, endpoint.port) + + def test_endpoint_multi_ipv4_and_ipv6(self): + # Row 1: "198.51.100.30, fe80::250:56ff:fe96:b97" + finding = self._findings("many_vulns.csv")[1] + self.assertEqual(2, len(finding.unsaved_endpoints)) + hosts = {ep.host for ep in finding.unsaved_endpoints} + self.assertEqual({"198.51.100.30", "fe80::250:56ff:fe96:b97"}, hosts) + + def test_endpoint_ipv6_only(self): + # Row 6 has IPv6-only address + finding = self._findings("many_vulns.csv")[6] + self.assertEqual(1, len(finding.unsaved_endpoints)) + self.assertEqual("2001:db8::1:80", finding.unsaved_endpoints[0].host) + + def test_endpoint_port_zero_is_omitted(self): + # Row 2 has Protocol/Port "TCP/0" — port should not be set + finding = self._findings("many_vulns.csv")[2] + self.assertEqual(1, len(finding.unsaved_endpoints)) + self.assertIsNone(finding.unsaved_endpoints[0].port) + + def test_endpoint_clean_succeeds(self): + # Hard guardrail: every endpoint must pass clean() + for finding in self._findings("many_vulns.csv"): + for endpoint in finding.unsaved_endpoints or []: + endpoint.clean() + + def test_cve_present(self): + # Row 0 has CVE-2021-44228 (Log4Shell) + finding = self._findings("many_vulns.csv")[0] + self.assertEqual(["CVE-2021-44228"], finding.unsaved_vulnerability_ids) + + def test_cve_absent(self): + # Row 2 (TCP Timestamp) has no CVE — attribute should be unset or empty + finding = self._findings("many_vulns.csv")[2] + self.assertFalse(getattr(finding, "unsaved_vulnerability_ids", None)) + + def test_cisa_known_exploited_tag_added(self): + # Rows 0, 1, 4 have CISA KEV = "Yes" + findings = self._findings("many_vulns.csv") + self.assertIn("cisa-known-exploited", findings[0].unsaved_tags) + self.assertIn("cisa-known-exploited", findings[1].unsaved_tags) + self.assertIn("cisa-known-exploited", findings[4].unsaved_tags) + + def test_cisa_known_exploited_tag_not_added(self): + # Row 2 has CISA KEV = "No" — no tag should be added + finding = self._findings("many_vulns.csv")[2] + self.assertFalse(getattr(finding, "unsaved_tags", None)) + + def test_cvssv3_score_parsed(self): + finding = self._findings("many_vulns.csv")[0] + self.assertEqual(10.0, finding.cvssv3_score) + + def test_cvssv3_score_empty_is_none(self): + # Row 6 has empty CVSS Score + finding = self._findings("many_vulns.csv")[6] + self.assertIsNone(finding.cvssv3_score) + + def test_static_dynamic_flags_set_explicitly(self): + for finding in self._findings("many_vulns.csv"): + self.assertEqual(True, finding.static_finding) + self.assertEqual(False, finding.dynamic_finding) + + def test_bom_handling(self): + # All fixtures have a UTF-8 BOM; the parser must consume it without + # producing a phantom field name with the BOM prefix. + finding = self._findings("one_vuln.csv")[0] + self.assertEqual("CVE-2023-44487 - HTTP/2 Rapid Reset Attack", finding.title) + # If BOM were not stripped, the first column key would be "Vulnerability" + # and finding.title would be empty. + + def test_multiline_field_preserved_in_description(self): + # Row 0 (Log4Shell) has a multi-line Description field + finding = self._findings("many_vulns.csv")[0] + self.assertIn("Log4Shell", finding.description) + self.assertIn("\n", finding.description) From cfe15d26c8c6def6544da2ef1c975deec4affe18 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Wed, 27 May 2026 22:14:43 -0600 Subject: [PATCH 6/7] docs(parser): add Alert Logic parser documentation Documents the Alert Logic CSV parser including: - File-export workflow from the Alert Logic console - Default deduplication strategy (unique_id_from_tool + hashcode fallback) - Complete 26-column field mapping table (expandable) - Additional Finding field settings (static/dynamic flags, active default) - Special processing notes covering severity conversion, title truncation, description construction, endpoint multi-IP / IPv6 / port-zero handling, deduplication algorithm, CVE handling, CISA Known Exploited tagging, and UTF-8 BOM + multi-line field handling Authored by T. Walker - DefectDojo --- .../parsers/file/alertlogic.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/content/supported_tools/parsers/file/alertlogic.md diff --git a/docs/content/supported_tools/parsers/file/alertlogic.md b/docs/content/supported_tools/parsers/file/alertlogic.md new file mode 100644 index 00000000000..572a56adbe6 --- /dev/null +++ b/docs/content/supported_tools/parsers/file/alertlogic.md @@ -0,0 +1,135 @@ +--- +title: "Alert Logic" +toc_hide: true +--- + +The [Alert Logic](https://www.alertlogic.com/) parser for DefectDojo supports imports from CSV format. This document details the parsing of Alert Logic vulnerability scan exports into DefectDojo field mappings, unmapped fields, and transformation notes for easier troubleshooting and analysis. + +## Supported File Types + +The Alert Logic parser accepts CSV file format. To generate this file from Alert Logic: + +1. Log into the Alert Logic console +2. Navigate to **Validate → Vulnerabilities** (or the equivalent vulnerability listing view) +3. Apply the filters you want included in the export +4. Export the filtered vulnerability list as CSV +5. Save the file with a `.csv` extension +6. Upload to DefectDojo using the "Alert Logic Scan" scan type + +The parser handles UTF-8 with byte-order mark (BOM) and multi-line quoted fields commonly present in Description, Evidence, and Resolution columns. + +## Default Deduplication Hashcode Fields + +Alert Logic provides a stable native vulnerability identifier in the `Vulnerability ID` column. DefectDojo uses it as `unique_id_from_tool` with hashcode fields as a fallback: + +- title +- component_name +- vuln_id_from_tool + +### Sample Scan Data + +Sample Alert Logic scans can be found in the [sample scan data folder](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/alertlogic). + +## Link To Tool + +- [Alert Logic](https://www.alertlogic.com/) +- [Alert Logic Documentation](https://docs.alertlogic.com/) + +## CSV Format + +### Total Fields in CSV + +- Total data fields: 26 +- Total data fields parsed: 26 +- Total data fields NOT parsed: 0 + +### CSV Format Field Mapping Details + +
+Click to expand Field Mapping Table + +| Source Field | DefectDojo Field | Notes | +| ----------------------- | --------------------------- | ------------------------------------------------------------------------------ | +| Vulnerability | title | Truncated to 500 characters with "..." suffix if longer | +| Severity | severity | Direct one-to-one mapping (Info / Low / Medium / High / Critical) | +| CVSS Score | cvssv3_score | Parsed as float; empty values produce no score | +| Asset Name | component_name | The affected host or service from the scan | +| IP Address | unsaved_endpoints | Comma-separated IPv4 / IPv6 list; each value becomes a separate endpoint | +| Protocol/Port | unsaved_endpoints | Parsed as `PROTOCOL/PORT`; a port of 0 is omitted | +| CVE | unsaved_vulnerability_ids | Single CVE identifier when present | +| Resolution | mitigation | Direct copy, including multi-line content | +| Vulnerability ID | unique_id_from_tool | Alert Logic's stable native vulnerability identifier (used for deduplication) | +| Description | description | Included in structured description block | +| Evidence | description | Included in structured description block | +| Operating System | description | Included in structured description block (CPE strings preserved) | +| Vulnerability Span ID | description | Included in structured description block | +| Vulnerability Key | description | Included in structured description block | +| Asset Key | description | Included in structured description block | +| Asset Type | description | Included in structured description block | +| Service | description | Included in structured description block | +| Category | description | Included in structured description block | +| VPC/Network | description | Included in structured description block | +| Deployment Name | description | Included in structured description block | +| Customer Account | description | Included in structured description block | +| First Seen | description | Included in structured description block | +| Last Scanned | description | Included in structured description block | +| Published Date | description | Included in structured description block | +| Age (days) | description | Included in structured description block | +| CISA Known Exploited | description, unsaved_tags | Added as `cisa-known-exploited` tag when value is "Yes" | + +
+ +### Additional Finding Field Settings (CSV Format) + +
+Click to expand Additional Settings Table + +| Finding Field | Default Value | Notes | +| ---------------- | ------------- | ----------------------------------------------------------- | +| static_finding | True | Alert Logic is an infrastructure vulnerability scanner | +| dynamic_finding | False | Alert Logic is an infrastructure vulnerability scanner | +| active | True | Alert Logic exports do not carry a mitigation status column | + +
+ +## Special Processing Notes + +### Severity Conversion + +Alert Logic uses a five-level severity scale that aligns one-to-one with DefectDojo severity levels: + +- `Critical` → Critical +- `High` → High +- `Medium` → Medium +- `Low` → Low +- `Info` → Info + +Any unrecognized severity value defaults to Info. + +### Title Format + +Finding titles are derived from the "Vulnerability" column. Titles longer than 500 characters are truncated to 497 characters with a "..." suffix appended. Shorter titles are used as-is without modification. + +### Description Construction + +The parser constructs a structured markdown description containing all relevant CSV fields not already mapped to dedicated Finding columns. Each field is rendered as `**Label:** value` with blank lines between entries. Fields are included only when they contain a non-empty value, so the description stays tight for sparsely populated rows. + +### Endpoint Construction + +The "IP Address" column may contain one or more comma-separated IP addresses, mixing IPv4 and IPv6 (for example: `198.51.100.30, fe80::250:56ff:fe96:b97`). Each address becomes a separate endpoint. The "Protocol/Port" column is parsed as `PROTOCOL/PORT` (e.g., `TCP/443`); when the port is `0` the value is treated as "no specific port" and omitted from the endpoint. All endpoints are validated via `endpoint.clean()` before being attached to the finding. + +### Deduplication + +Alert Logic exports include a stable per-vulnerability identifier in the "Vulnerability ID" column. DefectDojo uses this as `unique_id_from_tool` and the deduplication algorithm `DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE`. When the ID is missing (some scan exports omit it for non-vulnerability findings), DefectDojo falls back to the hashcode algorithm using `title`, `component_name`, and `vuln_id_from_tool` (the CVE) as the stable fields. + +### CVE Handling + +The "CVE" column carries a single CVE identifier or is empty. When present it is attached to the finding via `unsaved_vulnerability_ids`; when absent no CVE is set. + +### CISA Known Exploited Tagging + +When the "CISA Known Exploited" column equals "Yes", the finding receives a `cisa-known-exploited` tag. This makes it straightforward to filter, route, or escalate findings already known to be exploited in the wild. + +### BOM and Multi-Line Field Handling + +Alert Logic exports start with a UTF-8 byte-order mark (`\xef\xbb\xbf`). The parser uses `utf-8-sig` decoding to strip the BOM transparently. Description, Evidence, and Resolution columns frequently contain multi-line content (separated by `\r\n` inside the quoted field); these newlines are preserved in the resulting `description` and `mitigation` Finding fields. From 809fc0076900bc2fb7ec1f56b1178e4e986ffd83 Mon Sep 17 00:00:00 2001 From: skywalke34 Date: Wed, 27 May 2026 22:15:34 -0600 Subject: [PATCH 7/7] feat(parser): register Alert Logic deduplication configuration Adds Alert Logic Scan entries to: - HASHCODE_FIELDS_PER_SCANNER with ["title", "component_name", "vuln_id_from_tool"] (fallback when Vulnerability ID is missing on a row) - DEDUPLICATION_ALGORITHM_PER_PARSER as DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE (uses Vulnerability ID as the stable native identifier with hashcode fallback) Mirrors the Qualys VMDR dedup pattern (same field set, same algorithm). 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 70c58bddbee..1e97a47b93a 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1099,6 +1099,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "Orca Security Alerts": ["title", "component_name"], "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"], } # Override the hardcoded settings here via the env var @@ -1373,6 +1374,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "Xygeni SCA Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE, "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, } # Override the hardcoded settings here via the env var