Skip to content

Commit 9ef4226

Browse files
committed
Add comprehensive tests for SARIF output module
Cover finding_to_sarif_result, to_sarif, to_sarif_json, and _build_rules with 20 test cases including severity mapping, deduplication, CWE help URI generation, and edge cases.
1 parent 58619d8 commit 9ef4226

File tree

1 file changed

+145
-98
lines changed

1 file changed

+145
-98
lines changed

tests/test_sarif.py

Lines changed: 145 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,177 @@
1-
"""Tests for SARIF output."""
1+
"""Tests for SARIF 2.1.0 output formatting."""
22

33
from __future__ import annotations
44

55
import json
66

7+
import pytest
8+
9+
from ai_sec_scan import __version__
710
from ai_sec_scan.models import Finding, ScanResult, Severity
8-
from ai_sec_scan.sarif import to_sarif, to_sarif_json
11+
from ai_sec_scan.sarif import finding_to_sarif_result, to_sarif, to_sarif_json, _build_rules
12+
13+
14+
def _finding(**overrides) -> Finding:
15+
defaults = dict(
16+
file_path="src/app.py",
17+
line_start=42,
18+
severity=Severity.HIGH,
19+
title="Hardcoded Secret",
20+
description="API key found in source",
21+
recommendation="Store secrets in environment variables",
22+
)
23+
defaults.update(overrides)
24+
return Finding(**defaults)
925

1026

11-
def _make_result(findings: list[Finding] | None = None) -> ScanResult:
27+
def _result(findings: list[Finding] | None = None) -> ScanResult:
1228
return ScanResult(
1329
findings=findings or [],
14-
files_scanned=1,
30+
files_scanned=2,
1531
scan_duration=0.5,
16-
provider="test",
17-
model="test-model",
32+
provider="anthropic",
33+
model="claude-3.5-sonnet",
1834
)
1935

2036

21-
def _make_finding(**kwargs) -> Finding: # type: ignore[no-untyped-def]
22-
defaults = {
23-
"file_path": "app.py",
24-
"line_start": 1,
25-
"severity": Severity.HIGH,
26-
"title": "Test Issue",
27-
"description": "Test description",
28-
"recommendation": "Test fix",
29-
}
30-
defaults.update(kwargs)
31-
return Finding(**defaults)
37+
class TestFindingToSarifResult:
38+
def test_basic_finding_maps_correctly(self) -> None:
39+
f = _finding(cwe_id="CWE-798")
40+
result = finding_to_sarif_result(f)
3241

42+
assert result["ruleId"] == "CWE-798"
43+
assert result["level"] == "error"
44+
assert "Hardcoded Secret" in result["message"]["text"]
45+
loc = result["locations"][0]["physicalLocation"]
46+
assert loc["artifactLocation"]["uri"] == "src/app.py"
47+
assert loc["region"]["startLine"] == 42
3348

34-
class TestSarif:
35-
def test_empty_sarif_structure(self) -> None:
36-
sarif = to_sarif(_make_result())
37-
assert sarif["version"] == "2.1.0"
38-
assert "$schema" in sarif
39-
assert len(sarif["runs"]) == 1
40-
assert sarif["runs"][0]["results"] == []
49+
def test_line_end_included_when_set(self) -> None:
50+
f = _finding(line_end=50)
51+
result = finding_to_sarif_result(f)
52+
region = result["locations"][0]["physicalLocation"]["region"]
53+
assert region["endLine"] == 50
4154

42-
def test_sarif_tool_info(self) -> None:
43-
sarif = to_sarif(_make_result())
44-
driver = sarif["runs"][0]["tool"]["driver"]
45-
assert driver["name"] == "ai-sec-scan"
46-
assert "frankentini" in driver["informationUri"]
47-
48-
def test_sarif_with_finding(self) -> None:
49-
finding = _make_finding(
50-
line_start=42,
51-
cwe_id="CWE-798",
52-
owasp_category="A07:2021",
53-
)
54-
sarif = to_sarif(_make_result([finding]))
55-
results = sarif["runs"][0]["results"]
56-
assert len(results) == 1
57-
assert results[0]["ruleId"] == "CWE-798"
58-
assert results[0]["level"] == "error"
59-
loc = results[0]["locations"][0]["physicalLocation"]
60-
assert loc["region"]["startLine"] == 42
61-
assert loc["artifactLocation"]["uri"] == "app.py"
62-
63-
def test_sarif_properties(self) -> None:
64-
finding = _make_finding(cwe_id="CWE-89", owasp_category="A03:2021")
65-
sarif = to_sarif(_make_result([finding]))
66-
props = sarif["runs"][0]["results"][0]["properties"]
67-
assert props["cwe"] == "CWE-89"
68-
assert props["owasp"] == "A03:2021"
69-
70-
def test_sarif_no_properties_without_metadata(self) -> None:
71-
finding = _make_finding(cwe_id=None, owasp_category=None)
72-
sarif = to_sarif(_make_result([finding]))
73-
assert "properties" not in sarif["runs"][0]["results"][0]
74-
75-
def test_sarif_level_mapping(self) -> None:
76-
for sev, expected in [
55+
def test_line_end_absent_when_none(self) -> None:
56+
f = _finding(line_end=None)
57+
result = finding_to_sarif_result(f)
58+
region = result["locations"][0]["physicalLocation"]["region"]
59+
assert "endLine" not in region
60+
61+
def test_rule_id_falls_back_to_title(self) -> None:
62+
f = _finding(cwe_id=None)
63+
result = finding_to_sarif_result(f)
64+
assert result["ruleId"] == "hardcoded-secret"
65+
66+
def test_severity_level_mapping(self) -> None:
67+
for sev, expected_level in [
7768
(Severity.CRITICAL, "error"),
7869
(Severity.HIGH, "error"),
7970
(Severity.MEDIUM, "warning"),
8071
(Severity.LOW, "warning"),
8172
(Severity.INFO, "note"),
8273
]:
83-
finding = _make_finding(severity=sev)
84-
sarif = to_sarif(_make_result([finding]))
85-
assert sarif["runs"][0]["results"][0]["level"] == expected
74+
f = _finding(severity=sev)
75+
result = finding_to_sarif_result(f)
76+
assert result["level"] == expected_level
77+
78+
def test_properties_include_cwe_and_owasp(self) -> None:
79+
f = _finding(cwe_id="CWE-89", owasp_category="A03:2021")
80+
result = finding_to_sarif_result(f)
81+
assert result["properties"]["cwe"] == "CWE-89"
82+
assert result["properties"]["owasp"] == "A03:2021"
83+
84+
def test_properties_omitted_when_no_extras(self) -> None:
85+
f = _finding(cwe_id=None, owasp_category=None)
86+
result = finding_to_sarif_result(f)
87+
assert "properties" not in result
88+
89+
def test_recommendation_in_message(self) -> None:
90+
f = _finding(recommendation="Use parameterized queries")
91+
result = finding_to_sarif_result(f)
92+
assert "Use parameterized queries" in result["message"]["text"]
93+
94+
95+
class TestToSarif:
96+
def test_schema_and_version(self) -> None:
97+
doc = to_sarif(_result())
98+
assert doc["version"] == "2.1.0"
99+
assert "$schema" in doc
100+
101+
def test_tool_info(self) -> None:
102+
doc = to_sarif(_result())
103+
driver = doc["runs"][0]["tool"]["driver"]
104+
assert driver["name"] == "ai-sec-scan"
105+
assert driver["version"] == __version__
106+
assert "github.com" in driver["informationUri"]
107+
108+
def test_empty_findings_produce_empty_results(self) -> None:
109+
doc = to_sarif(_result([]))
110+
assert doc["runs"][0]["results"] == []
111+
assert doc["runs"][0]["tool"]["driver"]["rules"] == []
86112

87-
def test_sarif_json_is_valid(self) -> None:
88-
finding = _make_finding()
89-
output = to_sarif_json(_make_result([finding]))
113+
def test_findings_ordered_by_severity(self) -> None:
114+
findings = [
115+
_finding(severity=Severity.LOW, title="Low thing"),
116+
_finding(severity=Severity.CRITICAL, title="Critical thing"),
117+
]
118+
doc = to_sarif(_result(findings))
119+
results = doc["runs"][0]["results"]
120+
assert results[0]["level"] == "error"
121+
assert results[1]["level"] == "warning"
122+
123+
def test_invocation_marked_successful(self) -> None:
124+
doc = to_sarif(_result())
125+
invocations = doc["runs"][0]["invocations"]
126+
assert len(invocations) == 1
127+
assert invocations[0]["executionSuccessful"] is True
128+
129+
130+
class TestToSarifJson:
131+
def test_output_is_valid_json(self) -> None:
132+
findings = [_finding(cwe_id="CWE-79")]
133+
output = to_sarif_json(_result(findings))
90134
parsed = json.loads(output)
91135
assert parsed["version"] == "2.1.0"
92136

93-
def test_sarif_line_range(self) -> None:
94-
finding = _make_finding(line_start=10, line_end=20)
95-
sarif = to_sarif(_make_result([finding]))
96-
region = sarif["runs"][0]["results"][0]["locations"][0]["physicalLocation"]["region"]
97-
assert region["startLine"] == 10
98-
assert region["endLine"] == 20
99-
100-
def test_sarif_no_end_line(self) -> None:
101-
finding = _make_finding(line_start=5)
102-
sarif = to_sarif(_make_result([finding]))
103-
region = sarif["runs"][0]["results"][0]["locations"][0]["physicalLocation"]["region"]
104-
assert region["startLine"] == 5
105-
assert "endLine" not in region
137+
def test_custom_indent(self) -> None:
138+
output_2 = to_sarif_json(_result(), indent=2)
139+
output_4 = to_sarif_json(_result(), indent=4)
140+
# 4-space indent produces longer output
141+
assert len(output_4) > len(output_2)
142+
106143

107-
def test_sarif_rule_dedup(self) -> None:
144+
class TestBuildRules:
145+
def test_deduplicates_by_rule_id(self) -> None:
108146
findings = [
109-
_make_finding(file_path="a.py", line_start=1, cwe_id="CWE-89"),
110-
_make_finding(file_path="b.py", line_start=5, cwe_id="CWE-89"),
147+
_finding(cwe_id="CWE-89", title="SQL Injection A"),
148+
_finding(cwe_id="CWE-89", title="SQL Injection B"),
111149
]
112-
sarif = to_sarif(_make_result(findings))
113-
rules = sarif["runs"][0]["tool"]["driver"]["rules"]
150+
rules = _build_rules(findings)
114151
assert len(rules) == 1
115152
assert rules[0]["id"] == "CWE-89"
116-
# But still two results
117-
assert len(sarif["runs"][0]["results"]) == 2
118-
119-
def test_sarif_rule_help_uri(self) -> None:
120-
finding = _make_finding(cwe_id="CWE-89")
121-
sarif = to_sarif(_make_result([finding]))
122-
rule = sarif["runs"][0]["tool"]["driver"]["rules"][0]
123-
assert "cwe.mitre.org" in rule["helpUri"]
124-
assert "89" in rule["helpUri"]
125-
126-
def test_sarif_invocations(self) -> None:
127-
sarif = to_sarif(_make_result())
128-
invocations = sarif["runs"][0]["invocations"]
129-
assert len(invocations) == 1
130-
assert invocations[0]["executionSuccessful"] is True
153+
154+
def test_different_cwe_ids_produce_separate_rules(self) -> None:
155+
findings = [
156+
_finding(cwe_id="CWE-89", title="SQLi"),
157+
_finding(cwe_id="CWE-79", title="XSS"),
158+
]
159+
rules = _build_rules(findings)
160+
ids = {r["id"] for r in rules}
161+
assert ids == {"CWE-89", "CWE-79"}
162+
163+
def test_help_uri_generated_for_cwe(self) -> None:
164+
findings = [_finding(cwe_id="CWE-89")]
165+
rules = _build_rules(findings)
166+
assert rules[0]["helpUri"] == "https://cwe.mitre.org/data/definitions/89.html"
167+
168+
def test_no_help_uri_without_cwe(self) -> None:
169+
findings = [_finding(cwe_id=None)]
170+
rules = _build_rules(findings)
171+
assert "helpUri" not in rules[0]
172+
173+
def test_rule_descriptions(self) -> None:
174+
findings = [_finding(title="XSS", description="Cross-site scripting found")]
175+
rules = _build_rules(findings)
176+
assert rules[0]["shortDescription"]["text"] == "XSS"
177+
assert rules[0]["fullDescription"]["text"] == "Cross-site scripting found"

0 commit comments

Comments
 (0)