diff --git a/docs/content/supported_tools/parsers/file/orca_security.md b/docs/content/supported_tools/parsers/file/orca_security.md new file mode 100644 index 00000000000..2d9c290b1be --- /dev/null +++ b/docs/content/supported_tools/parsers/file/orca_security.md @@ -0,0 +1,170 @@ +--- +title: "Orca Security Alerts" +toc_hide: true +--- + +The [Orca Security](https://orca.security/) parser for DefectDojo supports imports from CSV and JSON formats. This document details the parsing of Orca Security alert exports into DefectDojo field mappings and unmapped fields. + +## Supported File Types + +The Orca Security parser accepts CSV and JSON file formats. To generate these files from Orca Security: + +1. Log into the Orca Security console +2. Navigate to the Alerts page +3. Apply desired filters (scope, severity, status) +4. Click "Export" and select either CSV or JSON format +5. Save the exported file +6. Upload to DefectDojo using the "Orca Security Alerts" scan type + +The parser auto-detects the format: files starting with `[` are treated as JSON, otherwise CSV. + +## Default Deduplication Hashcode Fields + +By default, DefectDojo identifies duplicate Findings using the [hashcode deduplication algorithm](https://docs.defectdojo.com/en/working_with_findings/finding_deduplication/about_deduplication/) with the following fields: + +- title +- component_name + +### Sample Scan Data + +Sample Orca Security scans can be found in the [sample scan data folder](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/orca_security). + +## Link To Tool + +- [Orca Security](https://orca.security/) +- [Orca Security Documentation](https://docs.orcasecurity.io/) + +## CSV Format + +### Total Fields in CSV + +- Total data fields: 12 +- Total data fields parsed: 12 +- Total data fields NOT parsed: 0 + +### CSV Format Field Mapping Details + +
+Click to expand Field Mapping Table + +| Source Field | DefectDojo Field | Notes | +| ------------ | ---------------- | ----- | +| Title | title | Truncated at 500 characters with "..." suffix | +| OrcaScore | severity | Float mapped to severity string (see Severity Conversion) | +| OrcaScore | severity_justification | Stored as "OrcaScore: X.X" | +| Category | description | Included in structured markdown description | +| Inventory.Name | component_name | Cloud resource name | +| CloudAccount.Name | description | Included in description and used for dedup hash | +| Source | service | Orca resource identifier populates service field | +| Source | description | Also included in description | +| Status | active | "open" = active, all else = inactive | +| CreatedAt | date | ISO 8601 parsed to date object | +| LastSeen | description | Included in description | +| Labels | tags | JSON-encoded array parsed and stored as finding tags | + +
+ +### Additional Finding Field Settings (CSV Format) + +
+Click to expand Additional Settings Table + +| Finding Field | Default Value | Notes | +|---------------|---------------|-------| +| static_finding | True | CSPM scan data is static analysis | +| dynamic_finding | False | Not a dynamic/runtime scan | +| active | Varies | Based on Status field ("open" = True) | +| mitigation | Not set | Orca exports do not include remediation text | + +
+ +## JSON Format + +### Total Fields in JSON + +- Total data fields: 10 +- Total data fields parsed: 10 +- Total data fields NOT parsed: 0 + +### JSON Format Field Mapping Details + +
+Click to expand Field Mapping Table + +| Source Field | DefectDojo Field | Notes | +| ------------ | ---------------- | ----- | +| Title | title | Truncated at 500 characters with "..." suffix | +| OrcaScore | severity | Float mapped to severity string (see Severity Conversion) | +| OrcaScore | severity_justification | Stored as "OrcaScore: X.X" | +| Category | description | Included in structured markdown description | +| Inventory.Name | component_name | Nested object, cloud resource name | +| CloudAccount.Name | description | Nested object, included in description and dedup hash | +| Source | service | Orca resource identifier populates service field | +| Source | description | Also included in description | +| Status | active | "open" = active, all else = inactive | +| CreatedAt | date | ISO 8601 parsed to date object | +| LastSeen | description | Included in description | +| Labels | tags | Array of strings stored as finding tags | + +
+ +### Additional Finding Field Settings (JSON Format) + +
+Click to expand Additional Settings Table + +| Finding Field | Default Value | Notes | +|---------------|---------------|-------| +| static_finding | True | CSPM scan data is static analysis | +| dynamic_finding | False | Not a dynamic/runtime scan | +| active | Varies | Based on Status field ("open" = True) | +| mitigation | Not set | Orca exports do not include remediation text | + +
+ +## Special Processing Notes + +### Date Processing + +The parser uses `dateutil.parser.parse()` to handle ISO 8601 date formats from Orca Security exports. The datetime is converted to a date object using `.date()`. Invalid or missing date strings return `None`. + +### Severity Conversion + +OrcaScore (float 0-10) is converted to DefectDojo severity levels: +- `0` or missing → Info +- `0.1 - 3.9` → Low +- `4.0 - 6.9` → Medium +- `7.0 - 8.9` → High +- `9.0 - 10.0` → Critical + +The conversion uses `float()` with error handling — non-numeric values default to Info severity. + +### Severity Justification + +The OrcaScore is also stored in the `severity_justification` field as "OrcaScore: X.X". This preserves the original numeric score for reference while the severity field contains the mapped categorical value. + +### Description Construction + +The parser builds a structured markdown description from all available alert fields. Each field is formatted as a bold label followed by its value, separated by double newlines. Fields with empty values are omitted. The description includes: Title, Category, Source, Inventory name, Cloud Account name, Orca Score, Status, Created date, Last Seen date, and Labels. + +### Title Format + +Finding titles use the alert's Title field directly. Titles longer than 500 characters are truncated with a "..." suffix. Alerts with no title receive the default "Orca Security Alert". + +### Service Field + +The Source field from Orca Security populates the DefectDojo `service` field. This represents the cloud resource or service that generated the alert. + +### Mitigation Construction + +Orca Security CSV and JSON exports do not include remediation or mitigation text. The mitigation field is not populated by this parser. + +### Deduplication + +Deduplication uses the hashcode algorithm configured in `settings.dist.py` with the fields `title` and `component_name`. This ensures findings with the same alert title on the same resource are deduplicated across reimports. Each row/item in the export becomes one Finding with no internal deduplication. + +### Tags Handling + +Labels from Orca Security are stored as finding tags using the `unsaved_tags` field. This makes labels searchable and filterable in DefectDojo. + +In CSV format, the Labels column contains a JSON-encoded array of strings. The parser uses `json.loads()` to parse this embedded JSON. If parsing fails, the raw string is used as a single tag. In JSON format, Labels is a native array of strings. diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 4bf0fbc651e..b329d15c074 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1488,6 +1488,7 @@ def saml2_attrib_map_format(din): "Snyk Issue API Scan": ["vuln_id_from_tool", "file_path"], "OpenReports": ["vulnerability_ids", "component_name", "component_version", "severity"], "n0s1 Scanner": ["description"], + "Orca Security Alerts": ["title", "component_name"], } # Override the hardcoded settings here via the env var @@ -1752,6 +1753,7 @@ def saml2_attrib_map_format(din): "OpenVAS Parser v2": DEDUPE_ALGO_HASH_CODE, "Snyk Issue API Scan": DEDUPE_ALGO_HASH_CODE, "OpenReports": DEDUPE_ALGO_HASH_CODE, + "Orca Security Alerts": DEDUPE_ALGO_HASH_CODE, } # Override the hardcoded settings here via the env var diff --git a/dojo/tools/orca_security/__init__.py b/dojo/tools/orca_security/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/orca_security/csv_parser.py b/dojo/tools/orca_security/csv_parser.py new file mode 100644 index 00000000000..6ca3c5790e2 --- /dev/null +++ b/dojo/tools/orca_security/csv_parser.py @@ -0,0 +1,102 @@ +""" +CSV parser for Orca Security alert exports. + +This module handles parsing of Orca Security alerts exported in CSV format. +The CSV export contains one row per alert with columns for all alert metadata. + +Expected CSV columns: + OrcaScore, Title, Category, Inventory, Inventory.Name, CloudAccount, + CloudAccount.Name, Source, Status, CreatedAt, LastSeen, Labels + +Note: The Labels column contains a JSON-encoded array of strings within the CSV. +""" +import csv +import io +import json + +from dojo.models import Finding +from dojo.tools.orca_security.helpers import ( + build_description, + build_severity_justification, + map_orca_severity, + parse_date, + truncate_title, +) + + +class OrcaSecurityCSVParser: + + """Parse Orca Security CSV alert exports.""" + + def parse(self, content): + """ + Parse CSV content and return a list of Finding objects. + + Args: + content: String containing the CSV file content + + Returns: + list[Finding]: List of DefectDojo Finding objects + + """ + reader = csv.DictReader(io.StringIO(content), delimiter=",", quotechar='"') + findings = [] + + for row in reader: + # Extract all fields from the CSV row + title_raw = (row.get("Title") or "").strip() + category = (row.get("Category") or "").strip() + source = (row.get("Source") or "").strip() + inventory_name = (row.get("Inventory.Name") or "").strip() + cloud_account_name = (row.get("CloudAccount.Name") or "").strip() + orca_score_raw = (row.get("OrcaScore") or "").strip() + status = (row.get("Status") or "").strip() + created_at = (row.get("CreatedAt") or "").strip() + last_seen = (row.get("LastSeen") or "").strip() + labels_raw = (row.get("Labels") or "").strip() + + # Parse labels from JSON string embedded in CSV + # Orca exports labels as a JSON array within the CSV cell + labels = [] + if labels_raw: + try: + labels = json.loads(labels_raw) + except (json.JSONDecodeError, TypeError): + # If JSON parsing fails, treat the raw string as a single label + labels = [labels_raw] + + # Transform fields for DefectDojo + title = truncate_title(title_raw) + severity = map_orca_severity(orca_score_raw) + + # Build structured description with all alert metadata + description = build_description( + title_raw, category, source, inventory_name, cloud_account_name, + orca_score_raw, status, created_at, last_seen, labels, + ) + + # Create the Finding object with all mapped fields + finding = Finding( + title=title, + severity=severity, + description=description, + # Preserve original OrcaScore in severity_justification + severity_justification=build_severity_justification(orca_score_raw), + static_finding=True, # CSPM scan data is static analysis + dynamic_finding=False, + service=source or None, # Source identifies the cloud resource/service + component_name=inventory_name or None, # Inventory is the specific resource + date=parse_date(created_at), + ) + + # Set active status based on Orca's status field + # "open" alerts are active, all other statuses (closed, resolved, etc.) are inactive + finding.active = status.lower() == "open" if status else True + + # Store labels as tags for searchability in DefectDojo + if labels: + finding.unsaved_tags = labels + + findings.append(finding) + + return findings diff --git a/dojo/tools/orca_security/helpers.py b/dojo/tools/orca_security/helpers.py new file mode 100644 index 00000000000..f3f2b4dc2a7 --- /dev/null +++ b/dojo/tools/orca_security/helpers.py @@ -0,0 +1,164 @@ +""" +Shared helper functions for the Orca Security parser. + +This module contains utility functions used by both the CSV and JSON parsers +to ensure consistent behavior across input formats. +""" +from dateutil import parser as dateutil_parser + + +def map_orca_severity(score): + """ + Map OrcaScore (float 0-10) to DefectDojo severity string. + + Orca Security uses a numeric score from 0-10 to indicate severity. + This function converts that to DefectDojo's categorical severity levels. + + Mapping thresholds: + - 0 or invalid -> Info + - 0.1 - 3.9 -> Low + - 4.0 - 6.9 -> Medium + - 7.0 - 8.9 -> High + - 9.0 - 10.0 -> Critical + + Args: + score: The OrcaScore value (can be float, int, string, or None) + + Returns: + str: DefectDojo severity level ("Info", "Low", "Medium", "High", "Critical") + + """ + try: + score = float(score) + except (TypeError, ValueError): + return "Info" + if score <= 0: + return "Info" + if score < 4.0: + return "Low" + if score < 7.0: + return "Medium" + if score < 9.0: + return "High" + return "Critical" + + +def build_severity_justification(orca_score): + """ + Build severity justification string from OrcaScore. + + Preserves the original numeric score in the severity_justification field + so users can see the exact Orca score that determined the severity level. + + Args: + orca_score: The OrcaScore value (can be float, int, string, or None) + + Returns: + str or None: "OrcaScore: X.X" if valid score, None otherwise + + """ + if orca_score is None: + return None + try: + score = float(orca_score) + except (TypeError, ValueError): + return None + else: + return f"OrcaScore: {score}" + + +def parse_date(date_string): + """ + Parse ISO 8601 date string into a Python date object. + + Orca Security exports dates in ISO 8601 format (e.g., "2025-01-15T10:30:00+00:00"). + This function extracts just the date portion for the finding's date field. + + Args: + date_string: ISO 8601 formatted date string, or None/empty string + + Returns: + date or None: Python date object if parsing succeeds, None otherwise + + """ + if not date_string: + return None + try: + return dateutil_parser.parse(date_string).date() + except (ValueError, TypeError): + return None + + +def truncate_title(title, max_length=500): + """ + Truncate title to maximum length with ellipsis suffix. + + DefectDojo has a limit on title length. This function ensures titles + fit within that limit while indicating truncation occurred. + + Args: + title: The original title string, or None/empty string + max_length: Maximum allowed length (default 500 characters) + + Returns: + str: Original title if within limit, truncated with "..." if over, + or "Orca Security Alert" if title is empty/None + + """ + if not title: + return "Orca Security Alert" + if len(title) <= max_length: + return title + return title[: max_length - 3] + "..." + + +def build_description(title, category, source, inventory_name, cloud_account_name, + orca_score, status, created_at, last_seen, labels): + """ + Build a structured markdown description from alert fields. + + Creates a formatted description containing all relevant alert metadata. + Each field is displayed as a bold label followed by its value. + Empty/None fields are omitted from the output. + + Args: + title: Alert title + category: Alert category (e.g., "IAM misconfigurations") + source: Source resource identifier + inventory_name: Name of the affected inventory/resource + cloud_account_name: Name of the cloud account + orca_score: Numeric OrcaScore (0-10) + status: Alert status (e.g., "open", "closed") + created_at: ISO 8601 creation timestamp + last_seen: ISO 8601 last seen timestamp + labels: List of label strings or single label string + + Returns: + str: Markdown-formatted description with all non-empty fields + + """ + parts = [] + if title: + parts.append(f"**Title:** {title}") + if category: + parts.append(f"**Category:** {category}") + if source: + parts.append(f"**Source:** {source}") + if inventory_name: + parts.append(f"**Inventory:** {inventory_name}") + if cloud_account_name: + parts.append(f"**Cloud Account:** {cloud_account_name}") + if orca_score is not None: + parts.append(f"**Orca Score:** {orca_score}") + if status: + parts.append(f"**Status:** {status}") + if created_at: + parts.append(f"**Created:** {created_at}") + if last_seen: + parts.append(f"**Last Seen:** {last_seen}") + if labels: + # Convert list to comma-separated string + labels_str = ", ".join(str(lbl) for lbl in labels) if isinstance(labels, list) else str(labels) + if labels_str: + parts.append(f"**Labels:** {labels_str}") + return "\n\n".join(parts) if parts else "No details available." diff --git a/dojo/tools/orca_security/json_parser.py b/dojo/tools/orca_security/json_parser.py new file mode 100644 index 00000000000..36b95362e9e --- /dev/null +++ b/dojo/tools/orca_security/json_parser.py @@ -0,0 +1,107 @@ +""" +JSON parser for Orca Security alert exports. + +This module handles parsing of Orca Security alerts exported in JSON format. +The JSON export is an array of alert objects with nested structures for +CloudAccount and Inventory fields. + +Expected JSON structure: + [ + { + "Title": "...", + "OrcaScore": 5.1, + "Category": "...", + "Source": "...", + "Status": "open", + "CreatedAt": "2025-01-15T10:30:00+00:00", + "LastSeen": "2025-02-01T12:00:00+00:00", + "Labels": ["label1", "label2"], + "CloudAccount": {"Name": "..."}, + "Inventory": {"Name": "..."} + }, + ... + ] +""" +import json + +from dojo.models import Finding +from dojo.tools.orca_security.helpers import ( + build_description, + build_severity_justification, + map_orca_severity, + parse_date, + truncate_title, +) + + +class OrcaSecurityJSONParser: + + """Parse Orca Security JSON alert exports.""" + + def parse(self, content): + """ + Parse JSON content and return a list of Finding objects. + + Args: + content: String containing the JSON file content (array of alerts) + + Returns: + list[Finding]: List of DefectDojo Finding objects + + """ + data = json.loads(content) + findings = [] + + for item in data: + # Extract top-level fields + title_raw = (item.get("Title") or "").strip() + category = (item.get("Category") or "").strip() + source = (item.get("Source") or "").strip() + status = (item.get("Status") or "").strip() + created_at = (item.get("CreatedAt") or "").strip() + last_seen = (item.get("LastSeen") or "").strip() + orca_score = item.get("OrcaScore") # Keep as numeric, not string + labels = item.get("Labels") or [] # Already a list in JSON + + # Extract nested fields from CloudAccount and Inventory objects + cloud_account = item.get("CloudAccount") or {} + cloud_account_name = (cloud_account.get("Name") or "").strip() + + inventory = item.get("Inventory") or {} + inventory_name = (inventory.get("Name") or "").strip() + + # Transform fields for DefectDojo + title = truncate_title(title_raw) + severity = map_orca_severity(orca_score) + + # Build structured description with all alert metadata + description = build_description( + title_raw, category, source, inventory_name, cloud_account_name, + orca_score, status, created_at, last_seen, labels, + ) + + # Create the Finding object with all mapped fields + finding = Finding( + title=title, + severity=severity, + description=description, + # Preserve original OrcaScore in severity_justification + severity_justification=build_severity_justification(orca_score), + static_finding=True, # CSPM scan data is static analysis + dynamic_finding=False, + service=source or None, # Source identifies the cloud resource/service + component_name=inventory_name or None, # Inventory is the specific resource + date=parse_date(created_at), + ) + + # Set active status based on Orca's status field + # "open" alerts are active, all other statuses (closed, resolved, etc.) are inactive + finding.active = status.lower() == "open" if status else True + + # Store labels as tags for searchability in DefectDojo + if labels: + finding.unsaved_tags = labels + + findings.append(finding) + + return findings diff --git a/dojo/tools/orca_security/parser.py b/dojo/tools/orca_security/parser.py new file mode 100644 index 00000000000..94d3beca11b --- /dev/null +++ b/dojo/tools/orca_security/parser.py @@ -0,0 +1,47 @@ +from dojo.tools.orca_security.csv_parser import OrcaSecurityCSVParser +from dojo.tools.orca_security.json_parser import OrcaSecurityJSONParser + + +class OrcaSecurityParser: + + """Parser for Orca Security alert exports (CSV and JSON).""" + + ID = "Orca Security Alerts" + + def get_scan_types(self): + """Return the scan type identifier for this parser.""" + return [self.ID] + + def get_label_for_scan_types(self, scan_type): + """Return the human-readable label for this scan type.""" + return scan_type + + def get_description_for_scan_types(self, scan_type): + """Return the description shown in the DefectDojo UI.""" + return "Import Orca Security alerts (CSV or JSON export)." + + def get_findings(self, filename, test): + """ + Parse an Orca Security export file and return findings. + + This method auto-detects the file format (CSV vs JSON) by examining + the file content. JSON files start with '[' (array), while CSV files + start with the header row. + + Args: + filename: File-like object containing the Orca Security export + test: DefectDojo Test object to associate findings with + + Returns: + list[Finding]: List of DefectDojo Finding objects + + """ + content = filename.read() + if isinstance(content, bytes): + content = content.decode("utf-8", errors="replace") + content_strip = content.strip() + + # Auto-detect format: JSON arrays start with '[', CSV starts with headers + if content_strip.startswith("["): + return OrcaSecurityJSONParser().parse(content_strip) + return OrcaSecurityCSVParser().parse(content_strip) diff --git a/unittests/scans/orca_security/many_vulns.csv b/unittests/scans/orca_security/many_vulns.csv new file mode 100644 index 00000000000..d5267b8e760 --- /dev/null +++ b/unittests/scans/orca_security/many_vulns.csv @@ -0,0 +1,6 @@ +OrcaScore,Title,Category,Inventory,Inventory.Name,CloudAccount,CloudAccount.Name,Source,Status,CreatedAt,LastSeen,Labels +2.0,Low severity test finding,Best practices,1,ResourceA,1,account-dev,ResourceA,open,2025-01-01T08:00:00+00:00,2025-02-01T08:00:00+00:00,"[""CSPM""]" +5.1,Unused role with policy found,IAM misconfigurations,1,TestRole_abc123,1,account-test,TestRole_abc123,open,2025-01-15T10:30:00+00:00,2025-02-01T12:00:00+00:00,"[""CSPM"",""source: Orca Scan""]" +7.5,Public S3 bucket detected,Data at risk,1,my-public-bucket,1,account-prod,my-public-bucket,open,2025-02-01T14:00:00+00:00,2025-02-03T09:00:00+00:00,"[""CSPM"",""mitre: initial access""]" +9.5,Critical IAM root access key active,IAM misconfigurations,1,root,1,account-prod,root,open,2025-02-02T16:00:00+00:00,2025-02-03T16:00:00+00:00,"[""CSPM"",""critical""]" +0,Informational security note,Best practices,1,InfoResource,1,account-dev,InfoResource,closed,2024-12-01T00:00:00+00:00,2025-01-01T00:00:00+00:00,"[]" diff --git a/unittests/scans/orca_security/many_vulns.json b/unittests/scans/orca_security/many_vulns.json new file mode 100644 index 00000000000..85f75c5479e --- /dev/null +++ b/unittests/scans/orca_security/many_vulns.json @@ -0,0 +1,62 @@ +[ + { + "Title": "Low severity test finding", + "Labels": ["CSPM"], + "CreatedAt": "2025-01-01T08:00:00+00:00", + "Status": "open", + "Category": "Best practices", + "OrcaScore": 2.0, + "Source": "ResourceA", + "LastSeen": "2025-02-01T08:00:00+00:00", + "CloudAccount": {"Name": "account-dev"}, + "Inventory": {"Name": "ResourceA"} + }, + { + "Title": "Unused role with policy found", + "Labels": ["CSPM", "source: Orca Scan"], + "CreatedAt": "2025-01-15T10:30:00+00:00", + "Status": "open", + "Category": "IAM misconfigurations", + "OrcaScore": 5.1, + "Source": "TestRole_abc123", + "LastSeen": "2025-02-01T12:00:00+00:00", + "CloudAccount": {"Name": "account-test"}, + "Inventory": {"Name": "TestRole_abc123"} + }, + { + "Title": "Public S3 bucket detected", + "Labels": ["CSPM", "mitre: initial access"], + "CreatedAt": "2025-02-01T14:00:00+00:00", + "Status": "open", + "Category": "Data at risk", + "OrcaScore": 7.5, + "Source": "my-public-bucket", + "LastSeen": "2025-02-03T09:00:00+00:00", + "CloudAccount": {"Name": "account-prod"}, + "Inventory": {"Name": "my-public-bucket"} + }, + { + "Title": "Critical IAM root access key active", + "Labels": ["CSPM", "critical"], + "CreatedAt": "2025-02-02T16:00:00+00:00", + "Status": "open", + "Category": "IAM misconfigurations", + "OrcaScore": 9.5, + "Source": "root", + "LastSeen": "2025-02-03T16:00:00+00:00", + "CloudAccount": {"Name": "account-prod"}, + "Inventory": {"Name": "root"} + }, + { + "Title": "Informational security note", + "Labels": [], + "CreatedAt": "2024-12-01T00:00:00+00:00", + "Status": "closed", + "Category": "Best practices", + "OrcaScore": 0, + "Source": "InfoResource", + "LastSeen": "2025-01-01T00:00:00+00:00", + "CloudAccount": {"Name": "account-dev"}, + "Inventory": {"Name": "InfoResource"} + } +] diff --git a/unittests/scans/orca_security/no_vuln.csv b/unittests/scans/orca_security/no_vuln.csv new file mode 100644 index 00000000000..c00978d384e --- /dev/null +++ b/unittests/scans/orca_security/no_vuln.csv @@ -0,0 +1 @@ +OrcaScore,Title,Category,Inventory,Inventory.Name,CloudAccount,CloudAccount.Name,Source,Status,CreatedAt,LastSeen,Labels \ No newline at end of file diff --git a/unittests/scans/orca_security/no_vuln.json b/unittests/scans/orca_security/no_vuln.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/unittests/scans/orca_security/no_vuln.json @@ -0,0 +1 @@ +[] diff --git a/unittests/scans/orca_security/one_vuln.csv b/unittests/scans/orca_security/one_vuln.csv new file mode 100644 index 00000000000..93e3ebf5705 --- /dev/null +++ b/unittests/scans/orca_security/one_vuln.csv @@ -0,0 +1,2 @@ +OrcaScore,Title,Category,Inventory,Inventory.Name,CloudAccount,CloudAccount.Name,Source,Status,CreatedAt,LastSeen,Labels +5.1,Unused role with policy found,IAM misconfigurations,1,TestRole_abc123,1,test-account,TestRole_abc123,open,2025-01-15T10:30:00+00:00,2025-02-01T12:00:00+00:00,"[""CSPM"",""source: Orca Scan""]" diff --git a/unittests/scans/orca_security/one_vuln.json b/unittests/scans/orca_security/one_vuln.json new file mode 100644 index 00000000000..9e134fe52af --- /dev/null +++ b/unittests/scans/orca_security/one_vuln.json @@ -0,0 +1,14 @@ +[ + { + "Title": "Unused role with policy found", + "Labels": ["CSPM", "source: Orca Scan"], + "CreatedAt": "2025-01-15T10:30:00+00:00", + "Status": "open", + "Category": "IAM misconfigurations", + "OrcaScore": 5.1, + "Source": "TestRole_abc123", + "LastSeen": "2025-02-01T12:00:00+00:00", + "CloudAccount": {"Name": "test-account"}, + "Inventory": {"Name": "TestRole_abc123"} + } +] diff --git a/unittests/tools/test_orca_security_parser.py b/unittests/tools/test_orca_security_parser.py new file mode 100644 index 00000000000..95d3ac9c472 --- /dev/null +++ b/unittests/tools/test_orca_security_parser.py @@ -0,0 +1,107 @@ +from dojo.models import Test +from dojo.tools.orca_security.parser import OrcaSecurityParser +from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path + + +class TestOrcaSecurityParser(DojoTestCase): + + # --- CSV Tests --- + + def test_parse_csv_no_findings(self): + with (get_unit_tests_scans_path("orca_security") / "no_vuln.csv").open(encoding="utf-8") as testfile: + parser = OrcaSecurityParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_parse_csv_one_finding(self): + with (get_unit_tests_scans_path("orca_security") / "one_vuln.csv").open(encoding="utf-8") as testfile: + parser = OrcaSecurityParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + finding = findings[0] + self.assertEqual("Unused role with policy found", finding.title) + self.assertEqual("Medium", finding.severity) + self.assertTrue(finding.active) + self.assertTrue(finding.static_finding) + self.assertFalse(finding.dynamic_finding) + self.assertEqual("TestRole_abc123", finding.component_name) + self.assertEqual("TestRole_abc123", finding.service) + self.assertEqual("OrcaScore: 5.1", finding.severity_justification) + self.assertIn("IAM misconfigurations", finding.description) + self.assertEqual(["CSPM", "source: Orca Scan"], finding.unsaved_tags) + + def test_parse_csv_many_findings(self): + with (get_unit_tests_scans_path("orca_security") / "many_vulns.csv").open(encoding="utf-8") as testfile: + parser = OrcaSecurityParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(5, len(findings)) + + # Check severity mapping across all levels + severities = [f.severity for f in findings] + self.assertIn("Low", severities) + self.assertIn("Medium", severities) + self.assertIn("High", severities) + self.assertIn("Critical", severities) + self.assertIn("Info", severities) + + # Check inactive finding (last one, status=closed) + closed_finding = findings[4] + self.assertFalse(closed_finding.active) + self.assertEqual("Info", closed_finding.severity) + + # --- JSON Tests --- + + def test_parse_json_no_findings(self): + with (get_unit_tests_scans_path("orca_security") / "no_vuln.json").open(encoding="utf-8") as testfile: + parser = OrcaSecurityParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_parse_json_one_finding(self): + with (get_unit_tests_scans_path("orca_security") / "one_vuln.json").open(encoding="utf-8") as testfile: + parser = OrcaSecurityParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + finding = findings[0] + self.assertEqual("Unused role with policy found", finding.title) + self.assertEqual("Medium", finding.severity) + self.assertTrue(finding.active) + self.assertTrue(finding.static_finding) + self.assertFalse(finding.dynamic_finding) + self.assertEqual("TestRole_abc123", finding.component_name) + self.assertEqual("TestRole_abc123", finding.service) + self.assertEqual("OrcaScore: 5.1", finding.severity_justification) + self.assertIn("IAM misconfigurations", finding.description) + self.assertEqual(["CSPM", "source: Orca Scan"], finding.unsaved_tags) + + def test_parse_json_many_findings(self): + with (get_unit_tests_scans_path("orca_security") / "many_vulns.json").open(encoding="utf-8") as testfile: + parser = OrcaSecurityParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(5, len(findings)) + + # Check severity mapping across all levels + severities = [f.severity for f in findings] + self.assertIn("Low", severities) + self.assertIn("Medium", severities) + self.assertIn("High", severities) + self.assertIn("Critical", severities) + self.assertIn("Info", severities) + + # Check inactive finding (last one, status=closed) + closed_finding = findings[4] + self.assertFalse(closed_finding.active) + self.assertEqual("Info", closed_finding.severity) + + # --- Cross-format consistency tests --- + + def test_date_is_parsed(self): + """CreatedAt should be parsed into a date object.""" + with (get_unit_tests_scans_path("orca_security") / "one_vuln.json").open(encoding="utf-8") as testfile: + parser = OrcaSecurityParser() + findings = parser.get_findings(testfile, Test()) + finding = findings[0] + self.assertIsNotNone(finding.date) + self.assertEqual(2025, finding.date.year) + self.assertEqual(1, finding.date.month) + self.assertEqual(15, finding.date.day)