import json import os import tempfile import pytest from prowler.lib.outputs.sarif.sarif import SARIF, SARIF_SCHEMA_URL, SARIF_VERSION from tests.lib.outputs.fixtures.fixtures import generate_finding_output class TestSARIF: def test_transform_fail_finding(self): finding = generate_finding_output( status="FAIL", status_extended="S3 bucket is not encrypted", severity="high", resource_name="main.tf", service_name="s3", check_id="s3_encryption_check", check_title="S3 Bucket Encryption", ) sarif = SARIF(findings=[finding], file_path=None) assert sarif.data[0]["$schema"] == SARIF_SCHEMA_URL assert sarif.data[0]["version"] == SARIF_VERSION assert len(sarif.data[0]["runs"]) == 1 run = sarif.data[0]["runs"][0] assert run["tool"]["driver"]["name"] == "Prowler" assert len(run["tool"]["driver"]["rules"]) == 1 assert len(run["results"]) == 1 rule = run["tool"]["driver"]["rules"][0] assert rule["id"] == "s3_encryption_check" assert rule["shortDescription"]["text"] == "S3 Bucket Encryption" assert rule["defaultConfiguration"]["level"] == "error" assert rule["properties"]["security-severity"] == "7.0" result = run["results"][0] assert result["ruleId"] == "s3_encryption_check" assert result["ruleIndex"] == 0 assert result["level"] == "error" assert result["message"]["text"] == "S3 bucket is not encrypted" def test_transform_pass_finding_excluded(self): finding = generate_finding_output(status="PASS", severity="high") sarif = SARIF(findings=[finding], file_path=None) run = sarif.data[0]["runs"][0] assert len(run["results"]) == 0 assert len(run["tool"]["driver"]["rules"]) == 0 def test_transform_muted_finding_excluded(self): finding = generate_finding_output(status="FAIL", severity="high", muted=True) sarif = SARIF(findings=[finding], file_path=None) run = sarif.data[0]["runs"][0] assert len(run["results"]) == 0 assert len(run["tool"]["driver"]["rules"]) == 0 @pytest.mark.parametrize( "severity,expected_level,expected_security_severity", [ ("critical", "error", "9.0"), ("high", "error", "7.0"), ("medium", "warning", "4.0"), ("low", "note", "2.0"), ("informational", "note", "0.0"), ], ) def test_transform_severity_mapping( self, severity, expected_level, expected_security_severity ): finding = generate_finding_output( status="FAIL", severity=severity, ) sarif = SARIF(findings=[finding], file_path=None) run = sarif.data[0]["runs"][0] result = run["results"][0] rule = run["tool"]["driver"]["rules"][0] assert result["level"] == expected_level assert rule["defaultConfiguration"]["level"] == expected_level assert rule["properties"]["security-severity"] == expected_security_severity def test_transform_multiple_findings_dedup_rules(self): findings = [ generate_finding_output( status="FAIL", resource_name="file1.tf", status_extended="Finding in file1", ), generate_finding_output( status="FAIL", resource_name="file2.tf", status_extended="Finding in file2", ), ] sarif = SARIF(findings=findings, file_path=None) run = sarif.data[0]["runs"][0] assert len(run["tool"]["driver"]["rules"]) == 1 assert len(run["results"]) == 2 assert run["results"][0]["ruleIndex"] == 0 assert run["results"][1]["ruleIndex"] == 0 def test_transform_multiple_different_rules(self): findings = [ generate_finding_output( status="FAIL", service_name="alpha", check_id="alpha_check_one", status_extended="Finding A", ), generate_finding_output( status="FAIL", service_name="beta", check_id="beta_check_two", status_extended="Finding B", ), ] sarif = SARIF(findings=findings, file_path=None) run = sarif.data[0]["runs"][0] assert len(run["tool"]["driver"]["rules"]) == 2 assert run["results"][0]["ruleIndex"] == 0 assert run["results"][1]["ruleIndex"] == 1 def test_transform_location_with_line_range(self): finding = generate_finding_output( status="FAIL", resource_name="modules/s3/main.tf", ) finding.raw = {"resource_line_range": "10:25"} sarif = SARIF(findings=[finding], file_path=None) result = sarif.data[0]["runs"][0]["results"][0] location = result["locations"][0]["physicalLocation"] assert location["artifactLocation"]["uri"] == "modules/s3/main.tf" assert location["region"]["startLine"] == 10 assert location["region"]["endLine"] == 25 def test_transform_location_without_line_range(self): finding = generate_finding_output( status="FAIL", resource_name="main.tf", ) sarif = SARIF(findings=[finding], file_path=None) result = sarif.data[0]["runs"][0]["results"][0] location = result["locations"][0]["physicalLocation"] assert location["artifactLocation"]["uri"] == "main.tf" assert "region" not in location def test_transform_no_resource_name(self): finding = generate_finding_output( status="FAIL", resource_name="", ) sarif = SARIF(findings=[finding], file_path=None) result = sarif.data[0]["runs"][0]["results"][0] assert "locations" not in result def test_batch_write_data_to_file(self): finding = generate_finding_output( status="FAIL", status_extended="test finding", resource_name="main.tf", ) with tempfile.NamedTemporaryFile( mode="w", suffix=".sarif", delete=False ) as tmp: tmp_path = tmp.name sarif = SARIF( findings=[finding], file_path=tmp_path, ) sarif.batch_write_data_to_file() with open(tmp_path) as f: content = json.load(f) assert content["$schema"] == SARIF_SCHEMA_URL assert content["version"] == SARIF_VERSION assert len(content["runs"][0]["results"]) == 1 os.unlink(tmp_path) def test_sarif_schema_structure(self): finding = generate_finding_output( status="FAIL", severity="critical", resource_name="infra/main.tf", service_name="iac", check_id="iac_misconfig_check", check_title="IaC Misconfiguration", description="Checks for misconfigurations", remediation_recommendation_text="Fix the configuration", ) finding.raw = {"resource_line_range": "5:15"} sarif = SARIF(findings=[finding], file_path=None) doc = sarif.data[0] assert "$schema" in doc assert "version" in doc assert "runs" in doc run = doc["runs"][0] assert "tool" in run assert "driver" in run["tool"] driver = run["tool"]["driver"] assert "name" in driver assert "version" in driver assert "informationUri" in driver assert "rules" in driver rule = driver["rules"][0] assert "id" in rule assert "shortDescription" in rule assert "fullDescription" in rule assert "help" in rule assert "defaultConfiguration" in rule assert "properties" in rule assert "tags" in rule["properties"] assert "security-severity" in rule["properties"] result = run["results"][0] assert "ruleId" in result assert "ruleIndex" in result assert "level" in result assert "message" in result assert "locations" in result loc = result["locations"][0]["physicalLocation"] assert "artifactLocation" in loc assert "uri" in loc["artifactLocation"] assert "region" in loc assert "startLine" in loc["region"] assert "endLine" in loc["region"] def test_transform_helpuri_present_when_related_url_set(self): finding = generate_finding_output( status="FAIL", provider="iac", related_url="https://docs.example.com/check", ) sarif = SARIF(findings=[finding], file_path=None) rule = sarif.data[0]["runs"][0]["tool"]["driver"]["rules"][0] assert rule["helpUri"] == "https://docs.example.com/check" def test_transform_helpuri_absent_when_related_url_empty(self): finding = generate_finding_output( status="FAIL", related_url="", ) sarif = SARIF(findings=[finding], file_path=None) rule = sarif.data[0]["runs"][0]["tool"]["driver"]["rules"][0] assert "helpUri" not in rule def test_location_with_non_numeric_line_range(self): finding = generate_finding_output( status="FAIL", resource_name="main.tf", ) finding.raw = {"resource_line_range": "abc:def"} sarif = SARIF(findings=[finding], file_path=None) location = sarif.data[0]["runs"][0]["results"][0]["locations"][0][ "physicalLocation" ] assert "region" not in location def test_location_with_single_value_line_range(self): finding = generate_finding_output( status="FAIL", resource_name="main.tf", ) finding.raw = {"resource_line_range": "10"} sarif = SARIF(findings=[finding], file_path=None) location = sarif.data[0]["runs"][0]["results"][0]["locations"][0][ "physicalLocation" ] assert "region" not in location def test_location_with_zero_line_numbers(self): finding = generate_finding_output( status="FAIL", resource_name="main.tf", ) finding.raw = {"resource_line_range": "0:0"} sarif = SARIF(findings=[finding], file_path=None) location = sarif.data[0]["runs"][0]["results"][0]["locations"][0][ "physicalLocation" ] assert "region" not in location def test_only_pass_findings(self): findings = [ generate_finding_output(status="PASS"), generate_finding_output(status="PASS"), ] sarif = SARIF(findings=findings, file_path=None) run = sarif.data[0]["runs"][0] assert len(run["results"]) == 0 assert len(run["tool"]["driver"]["rules"]) == 0