Skip to content

Commit 133ec48

Browse files
authored
Add detection tool metadata to CodeTF results (#366)
* Add detectionTool field to CodeTF * Add detection tool metadata for Sonar codemods
1 parent 866599e commit 133ec48

15 files changed

Lines changed: 144 additions & 57 deletions

src/codemodder/codemods/base_codemod.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import functools
24
import importlib.resources
35
from abc import ABCMeta, abstractmethod
@@ -11,7 +13,7 @@
1113
from codemodder.code_directory import file_line_patterns
1214
from codemodder.codemods.base_detector import BaseDetector
1315
from codemodder.codemods.base_transformer import BaseTransformerPipeline
14-
from codemodder.codetf import Reference
16+
from codemodder.codetf import DetectionTool, Reference, Rule
1517
from codemodder.context import CodemodExecutionContext
1618
from codemodder.file_context import FileContext
1719
from codemodder.logging import logger
@@ -31,6 +33,15 @@ class Metadata:
3133
review_guidance: ReviewGuidance
3234
references: list[Reference] = field(default_factory=list)
3335
description: str | None = None
36+
tool: ToolMetadata | None = None
37+
38+
39+
@dataclass
40+
class ToolMetadata:
41+
name: str
42+
rule_id: str
43+
rule_name: str
44+
rule_url: str
3445

3546

3647
class BaseCodemod(metaclass=ABCMeta):
@@ -89,6 +100,20 @@ def id(self) -> str:
89100
def summary(self):
90101
return self._metadata.summary
91102

103+
@property
104+
def detection_tool(self) -> DetectionTool | None:
105+
if self._metadata.tool is None:
106+
return None
107+
108+
return DetectionTool(
109+
name=self._metadata.tool.name,
110+
rule=Rule(
111+
id=self._metadata.tool.rule_id,
112+
name=self._metadata.tool.rule_name,
113+
url=self._metadata.tool.rule_url,
114+
),
115+
)
116+
92117
@cached_property
93118
def docs_module(self) -> Traversable:
94119
return importlib.resources.files(self.docs_module_path)

src/codemodder/codemods/sonar.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pathlib import Path
22

3-
from codemodder.codemods.base_codemod import Metadata, Reference
3+
from codemodder.codemods.base_codemod import Metadata, Reference, ToolMetadata
44
from codemodder.codemods.base_detector import BaseDetector
55
from codemodder.codemods.base_transformer import BaseTransformerPipeline
66
from codemodder.context import CodemodExecutionContext
@@ -19,26 +19,31 @@ def from_core_codemod(
1919
cls,
2020
name: str,
2121
other: CoreCodemod,
22-
rules: list[str],
22+
rule_id: str,
23+
rule_name: str,
24+
rule_url: str,
2325
transformer: BaseTransformerPipeline | None = None,
24-
new_references: list[Reference] | None = None,
2526
):
2627
return SonarCodemod(
2728
metadata=Metadata(
2829
name=name,
2930
summary="Sonar: " + other.summary,
3031
review_guidance=other._metadata.review_guidance,
3132
references=(
32-
other.references
33-
if not new_references
34-
else other.references + new_references
33+
other.references + [Reference(url=rule_url, description=rule_name)]
3534
),
36-
description=f"This codemod acts upon the following Sonar rules: {str(rules)[1:-1]}.\n\n"
35+
description=f"This codemod acts upon the following Sonar rules: {rule_id}.\n\n"
3736
+ other.description,
37+
tool=ToolMetadata(
38+
name="Sonar",
39+
rule_id=rule_id,
40+
rule_name=rule_name,
41+
rule_url=rule_url,
42+
),
3843
),
3944
transformer=transformer if transformer else other.transformer,
4045
detector=SonarDetector(),
41-
requested_rules=rules,
46+
requested_rules=[rule_id],
4247
)
4348

4449

src/codemodder/codemods/test/integration_utils.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ def _assert_run_fields(self, run, output_path):
9595
assert run["directory"] == os.path.abspath(SAMPLES_DIR)
9696
assert run["sarifs"] == []
9797

98+
def _assert_sonar_fields(self, result):
99+
assert result["detectionTool"]["name"] == "Sonar"
100+
assert (
101+
result["detectionTool"]["rule"]["id"]
102+
== self.codemod_instance._metadata.tool.rule_id
103+
)
104+
assert (
105+
result["detectionTool"]["rule"]["name"]
106+
== self.codemod_instance._metadata.tool.rule_name
107+
)
108+
# TODO: empty array until we add findings metadata
109+
assert result["detectionTool"]["findings"] == []
110+
98111
def _assert_results_fields(self, results, output_path):
99112
assert len(results) == 1
100113
result = results[0]
@@ -104,10 +117,23 @@ def _assert_results_fields(self, results, output_path):
104117
for ref in self.codemod_instance.references
105118
]
106119

107-
# TODO: once we add description for each url.
108-
for reference in result["references"]:
120+
assert ("detectionTool" in result) == bool(self.sonar_issues_json)
121+
122+
# TODO: if/when we add description for each url
123+
for reference in result["references"][
124+
# Last reference for Sonar has a different description
125+
: (-1 if self.sonar_issues_json else None)
126+
]:
109127
assert reference["url"] == reference["description"]
110128

129+
if self.sonar_issues_json:
130+
assert self.codemod_instance._metadata.tool is not None
131+
assert (
132+
result["references"][-1]["description"]
133+
== self.codemod_instance._metadata.tool.rule_name
134+
)
135+
self._assert_sonar_fields(result)
136+
111137
assert len(result["changeset"]) == self.num_changed_files
112138

113139
# A codemod may change multiple files. For now we will

src/codemodder/codetf.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,34 @@ def validate_description(self):
7272
return self
7373

7474

75+
class Rule(BaseModel):
76+
id: str
77+
name: str
78+
url: Optional[str] = None
79+
80+
81+
class Finding(BaseModel):
82+
id: str
83+
fixed: bool
84+
reason: Optional[str] = None
85+
86+
@model_validator(mode="after")
87+
def validate_reason(self):
88+
assert self.fixed or self.reason, "reason is required if fixed is False"
89+
return self
90+
91+
92+
class DetectionTool(BaseModel):
93+
name: str
94+
rule: Rule
95+
findings: list[Finding] = []
96+
97+
7598
class Result(BaseModel):
7699
codemod: str
77100
summary: str
78101
description: str
102+
detectionTool: Optional[DetectionTool] = None
79103
references: Optional[list[Reference]] = None
80104
properties: Optional[dict] = None
81105
failedFiles: Optional[list[str]] = None

src/codemodder/context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ def compile_results(self, codemods: list[BaseCodemod]) -> list[CodeTFResult]:
158158
codemod=codemod.id,
159159
summary=codemod.summary,
160160
description=self.add_description(codemod),
161+
detectionTool=codemod.detection_tool,
161162
references=codemod.references,
162163
properties={},
163164
failedFiles=[str(file) for file in self.get_failures(codemod.id)],
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from codemodder.codemods.base_codemod import Reference
21
from codemodder.codemods.sonar import SonarCodemod
32
from core_codemods.django_json_response_type import DjangoJsonResponseType
43

54
SonarDjangoJsonResponseType = SonarCodemod.from_core_codemod(
65
name="django-json-response-type-S5131",
76
other=DjangoJsonResponseType,
8-
rules=["pythonsecurity:S5131"],
9-
new_references=[
10-
Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-5131/"),
11-
],
7+
rule_id="pythonsecurity:S5131",
8+
rule_name="Endpoints should not be vulnerable to reflected XSS attacks (Django)",
9+
rule_url="https://rules.sonarsource.com/python/type/Bug/RSPEC-5131/",
1210
)
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from codemodder.codemods.base_codemod import Reference
21
from codemodder.codemods.sonar import SonarCodemod
32
from core_codemods.django_receiver_on_top import DjangoReceiverOnTop
43

54
SonarDjangoReceiverOnTop = SonarCodemod.from_core_codemod(
65
name="django-receiver-on-top-S6552",
76
other=DjangoReceiverOnTop,
8-
rules=["python:S6552"],
9-
new_references=[
10-
Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-6552/"),
11-
],
7+
rule_id="python:S6552",
8+
rule_name="Django signal handler functions should have the `@receiver` decorator on top of all other decorators",
9+
rule_url="https://rules.sonarsource.com/python/type/Bug/RSPEC-6552/",
1210
)
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from codemodder.codemods.base_codemod import Reference
21
from codemodder.codemods.sonar import SonarCodemod
32
from core_codemods.exception_without_raise import ExceptionWithoutRaise
43

54
SonarExceptionWithoutRaise = SonarCodemod.from_core_codemod(
65
name="exception-without-raise-S3984",
76
other=ExceptionWithoutRaise,
8-
rules=["python:S3984"],
9-
new_references=[
10-
Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-3984/"),
11-
],
7+
rule_id="python:S3984",
8+
rule_name="Exceptions should not be created without being raised",
9+
rule_url="https://rules.sonarsource.com/python/type/Bug/RSPEC-3984/",
1210
)
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from codemodder.codemods.base_codemod import Reference
21
from codemodder.codemods.sonar import SonarCodemod
32
from core_codemods.fix_assert_tuple import FixAssertTuple
43

54
SonarFixAssertTuple = SonarCodemod.from_core_codemod(
65
name="fix-assert-tuple-S5905",
76
other=FixAssertTuple,
8-
rules=["python:S5905"],
9-
new_references=[
10-
Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-5905/"),
11-
],
7+
rule_id="python:S5905",
8+
rule_name="Assert should not be called on a tuple literal",
9+
rule_url="https://rules.sonarsource.com/python/type/Bug/RSPEC-5905/",
1210
)
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from codemodder.codemods.base_codemod import Reference
21
from codemodder.codemods.sonar import SonarCodemod
32
from core_codemods.flask_json_response_type import FlaskJsonResponseType
43

54
SonarFlaskJsonResponseType = SonarCodemod.from_core_codemod(
65
name="flask-json-response-type-S5131",
76
other=FlaskJsonResponseType,
8-
rules=["pythonsecurity:S5131"],
9-
new_references=[
10-
Reference(url="https://rules.sonarsource.com/python/type/Bug/RSPEC-5131/"),
11-
],
7+
rule_id="pythonsecurity:S5131",
8+
rule_name="Endpoints should not be vulnerable to reflected XSS attacks (Flask)",
9+
rule_url="https://rules.sonarsource.com/python/type/Bug/RSPEC-5131/",
1210
)

0 commit comments

Comments
 (0)