Skip to content

Commit ee59ee6

Browse files
authored
Populate Sonar findings (#567)
* from_finding > from_result * report sonar findings * test unfixed sonar
1 parent d1f06e3 commit ee59ee6

File tree

4 files changed

+109
-38
lines changed

4 files changed

+109
-38
lines changed

src/core_codemods/defectdojo/results.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,28 @@
1212

1313
class DefectDojoLocation(Location):
1414
@classmethod
15-
def from_finding(cls, finding: dict) -> Self:
15+
def from_result(cls, result: dict) -> Self:
1616
return cls(
17-
file=Path(finding["file_path"]),
17+
file=Path(result["file_path"]),
1818
# TODO: parse snippet from "description" field?
19-
start=LineInfo(finding["line"]),
20-
end=LineInfo(finding["line"]),
19+
start=LineInfo(result["line"]),
20+
end=LineInfo(result["line"]),
2121
)
2222

2323

2424
class DefectDojoResult(SASTResult):
2525
@classmethod
26-
def from_finding(cls, finding: dict) -> Self:
26+
def from_result(cls, result: dict) -> Self:
2727
return cls(
28-
finding_id=finding["id"],
29-
rule_id=finding["title"],
30-
locations=[DefectDojoLocation.from_finding(finding)],
28+
finding_id=result["id"],
29+
rule_id=result["title"],
30+
locations=[DefectDojoLocation.from_result(result)],
3131
finding=Finding(
32-
id=str(finding["id"]),
32+
id=str(result["id"]),
3333
rule=Rule(
3434
# TODO: it's possible that these fields actually come from the codemod and not the result
35-
id=str(finding["title"]),
36-
name=str(finding["title"]),
35+
id=str(result["title"]),
36+
name=str(result["title"]),
3737
url=None,
3838
),
3939
),
@@ -62,7 +62,7 @@ def from_json(cls, json_file: str | Path) -> Self:
6262
data = json.load(file)
6363

6464
result_set = cls()
65-
for finding in data.get("results"):
66-
result_set.add_result(DefectDojoResult.from_finding(finding))
65+
for result in data.get("results"):
66+
result_set.add_result(DefectDojoResult.from_result(result))
6767

6868
return result_set

src/core_codemods/sonar/results.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import libcst as cst
77
from typing_extensions import Self
88

9+
from codemodder.codetf import Finding, Rule
910
from codemodder.logging import logger
10-
from codemodder.result import LineInfo, Location, Result, ResultSet
11+
from codemodder.result import LineInfo, Location, ResultSet, SASTResult
1112

1213

1314
class SonarLocation(Location):
@@ -20,10 +21,10 @@ def from_json_location(cls, json_location) -> Self:
2021
return cls(file=file, start=start, end=end)
2122

2223

23-
class SonarResult(Result):
24+
class SonarResult(SASTResult):
2425

2526
@classmethod
26-
def from_result(cls, result) -> Self:
27+
def from_result(cls, result: dict) -> Self:
2728
# Sonar issues have `rule` as key while hotspots call it `ruleKey`
2829
if not (rule_id := result.get("rule", None) or result.get("ruleKey", None)):
2930
raise ValueError("Could not extract rule id from sarif result.")
@@ -36,7 +37,21 @@ def from_result(cls, result) -> Self:
3637
]
3738
for flow in result.get("flows", [])
3839
]
39-
return cls(rule_id=rule_id, locations=locations, codeflows=all_flows)
40+
41+
return cls(
42+
finding_id=rule_id,
43+
rule_id=rule_id,
44+
locations=locations,
45+
codeflows=all_flows,
46+
finding=Finding(
47+
id=rule_id,
48+
rule=Rule(
49+
id=rule_id,
50+
name=rule_id,
51+
url=None,
52+
),
53+
),
54+
)
4055

4156
def match_location(self, pos, node):
4257
match node:

tests/codemods/sonar/test_sonar_django_json_response_type.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def test_name(self):
1414
assert self.codemod.name == "django-json-response-type-S5131"
1515

1616
def test_simple(self, tmpdir):
17+
rule_id = "pythonsecurity:S5131"
1718
input_code = """
1819
from django.http import HttpResponse
1920
import json
@@ -33,7 +34,7 @@ def foo(request):
3334
issues = {
3435
"issues": [
3536
{
36-
"rule": "pythonsecurity:S5131",
37+
"rule": rule_id,
3738
"status": "OPEN",
3839
"component": "code.py",
3940
"textRange": {
@@ -45,4 +46,11 @@ def foo(request):
4546
}
4647
]
4748
}
48-
self.run_and_assert(tmpdir, input_code, expected, results=json.dumps(issues))
49+
changes = self.run_and_assert(
50+
tmpdir, input_code, expected, results=json.dumps(issues)
51+
)
52+
assert changes is not None
53+
assert changes[0].changes[0].findings is not None
54+
assert changes[0].changes[0].findings[0].id == rule_id
55+
assert changes[0].changes[0].findings[0].rule.id == rule_id
56+
assert changes[0].changes[0].findings[0].rule.name == rule_id

tests/test_libcst_transformer.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import mock
12
from libcst._exceptions import ParserSyntaxError
23

34
from codemodder.codemods.libcst_transformer import (
@@ -7,10 +8,54 @@
78
from codemodder.context import CodemodExecutionContext
89
from codemodder.file_context import FileContext
910
from core_codemods.defectdojo.results import DefectDojoResult
11+
from core_codemods.sonar.results import SonarResult
12+
13+
FILE_PATH = mock.MagicMock()
14+
DEFECTDOJO_RESULTS = [
15+
DefectDojoResult.from_result(
16+
{
17+
"id": 1,
18+
"title": "python.django.security.audit.secure-cookies.django-secure-set-cookie",
19+
"file_path": FILE_PATH,
20+
"line": 2,
21+
},
22+
)
23+
]
24+
25+
SONAR_RESULTS = [
26+
SonarResult.from_result(
27+
{
28+
"rule": "python:S1716",
29+
"textRange": {
30+
"startLine": 1,
31+
"endLine": 1,
32+
"startOffset": 13,
33+
"endOffset": 14,
34+
},
35+
"component": FILE_PATH,
36+
"flows": [
37+
{
38+
"locations": [
39+
{
40+
"component": FILE_PATH,
41+
"textRange": {
42+
"startLine": 1,
43+
"endLine": 1,
44+
"startOffset": 8,
45+
"endOffset": 9,
46+
},
47+
}
48+
]
49+
}
50+
],
51+
"status": "OPEN",
52+
}
53+
)
54+
]
1055

1156

1257
def _apply_and_assert(mocker, transformer):
13-
file_context = FileContext("home", mocker.MagicMock())
58+
file_context = FileContext("home", FILE_PATH)
1459
execution_context = CodemodExecutionContext(
1560
directory=mocker.MagicMock(),
1661
dry_run=True,
@@ -30,21 +75,11 @@ def _apply_and_assert(mocker, transformer):
3075
assert file_context.unfixed_findings == []
3176

3277

33-
def _apply_and_assert_with_tool(mocker, transformer, reason):
34-
file_path = mocker.MagicMock()
78+
def _apply_and_assert_with_tool(mocker, transformer, reason, results):
3579
file_context = FileContext(
3680
"home",
37-
file_path,
38-
results=[
39-
DefectDojoResult.from_finding(
40-
{
41-
"id": 1,
42-
"title": "python.django.security.audit.secure-cookies.django-secure-set-cookie",
43-
"file_path": file_path,
44-
"line": 2,
45-
},
46-
)
47-
],
81+
FILE_PATH,
82+
results=results,
4883
)
4984
execution_context = CodemodExecutionContext(
5085
directory=mocker.MagicMock(),
@@ -54,7 +89,7 @@ def _apply_and_assert_with_tool(mocker, transformer, reason):
5489
repo_manager=mocker.MagicMock(),
5590
path_include=[],
5691
path_exclude=[],
57-
tool_result_files_map={"sonar": ["results.json"]},
92+
tool_result_files_map={"DOESNTMATTER": ["testing.json"]},
5893
)
5994
pipeline = LibcstTransformerPipeline(transformer)
6095
pipeline.apply(
@@ -84,18 +119,31 @@ def test_transformer_error(mocker, caplog):
84119
assert "Failed to transform file" in caplog.text
85120

86121

87-
def test_parse_error_with_tool(mocker, caplog):
122+
def test_parse_error_with_defectdojo(mocker, caplog):
88123
mocker.patch(
89124
"codemodder.codemods.libcst_transformer.cst.parse_module",
90125
side_effect=ParserSyntaxError,
91126
)
92127
transformer = mocker.MagicMock(spec=LibcstResultTransformer)
93-
_apply_and_assert_with_tool(mocker, transformer, "Failed to parse file")
128+
_apply_and_assert_with_tool(
129+
mocker, transformer, "Failed to parse file", DEFECTDOJO_RESULTS
130+
)
94131
assert "Failed to parse file" in caplog.text
95132

96133

97-
def test_transformer_error_with_tool(mocker, caplog):
134+
def test_transformer_error_with_defectdojo(mocker, caplog):
98135
transformer = mocker.MagicMock(spec=LibcstResultTransformer)
99136
transformer.transform.side_effect = ParserSyntaxError
100-
_apply_and_assert_with_tool(mocker, transformer, "Failed to transform file")
137+
_apply_and_assert_with_tool(
138+
mocker, transformer, "Failed to transform file", DEFECTDOJO_RESULTS
139+
)
140+
assert "Failed to transform file" in caplog.text
141+
142+
143+
def test_transformer_error_with_sonar(mocker, caplog):
144+
transformer = mocker.MagicMock(spec=LibcstResultTransformer)
145+
transformer.transform.side_effect = ParserSyntaxError
146+
_apply_and_assert_with_tool(
147+
mocker, transformer, "Failed to transform file", SONAR_RESULTS
148+
)
101149
assert "Failed to transform file" in caplog.text

0 commit comments

Comments
 (0)