Skip to content

Commit 9aa6fb2

Browse files
authored
Sonar tmpfile codemod (#393)
* new sonar tmpfile codemod * update integration tests * add assertions for sonar integration test * add docs
1 parent 06807ba commit 9aa6fb2

File tree

11 files changed

+141
-38
lines changed

11 files changed

+141
-38
lines changed

integration_tests/test_sonar_fix_assert_tuple.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ class TestFixAssertTuple(SonarIntegrationTest):
88
code_path = "tests/samples/fix_assert_tuple.py"
99
replacement_lines = [(1, "assert 1 == 1\n"), (2, "assert 2 == 2\n")]
1010
expected_diff = "--- \n+++ \n@@ -1 +1,2 @@\n-assert (1 == 1, 2 == 2)\n+assert 1 == 1\n+assert 2 == 2\n"
11-
sonar_issues_json = "tests/samples/sonar_issues.json"
1211
expected_line_change = "1"
1312
change_description = FixAssertTupleTransform.change_description
1413
num_changes = 2
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from codemodder.codemods.test import SonarIntegrationTest
2+
from core_codemods.sonar.sonar_tempfile_mktemp import SonarTempfileMktemp
3+
from core_codemods.tempfile_mktemp import TempfileMktempTransformer
4+
5+
6+
class TestTempfileMktemp(SonarIntegrationTest):
7+
codemod = SonarTempfileMktemp
8+
code_path = "tests/samples/tempfile_mktemp.py"
9+
replacement_lines = [(3, "tempfile.mkstemp()\n")]
10+
expected_diff = "--- \n+++ \n@@ -1,3 +1,3 @@\n import tempfile\n \n-tempfile.mktemp()\n+tempfile.mkstemp()\n"
11+
expected_line_change = "3"
12+
change_description = TempfileMktempTransformer.change_description

integration_tests/test_tempfile_mktemp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from codemodder.codemods.test import BaseIntegrationTest
2-
from core_codemods.tempfile_mktemp import TempfileMktemp
2+
from core_codemods.tempfile_mktemp import TempfileMktemp, TempfileMktempTransformer
33

44

55
class TestTempfileMktemp(BaseIntegrationTest):
@@ -13,4 +13,4 @@ class TestTempfileMktemp(BaseIntegrationTest):
1313
replacement_lines = [(3, "tempfile.mkstemp()\n")]
1414
expected_diff = '--- \n+++ \n@@ -1,4 +1,4 @@\n import tempfile\n \n-tempfile.mktemp()\n+tempfile.mkstemp()\n var = "hello"\n'
1515
expected_line_change = "3"
16-
change_description = TempfileMktemp.change_description
16+
change_description = TempfileMktempTransformer.change_description

src/codemodder/codemods/sonar.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class SonarCodemod(SASTCodemod):
1414
def origin(self):
1515
return "sonar"
1616

17+
@property
18+
def rule_id(self):
19+
return self._metadata.tool.rule_id
20+
1721
@classmethod
1822
def from_core_codemod(
1923
cls,

src/codemodder/codemods/test/integration_utils.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import jsonschema
1212

1313
from codemodder import __version__, registry
14+
from codemodder.sonar_results import SonarResultSet
1415

1516
from .validations import execute_code
1617

@@ -115,19 +116,6 @@ def _assert_run_fields(self, run, output_path):
115116
assert run["directory"] == os.path.abspath(self.code_dir)
116117
assert run["sarifs"] == []
117118

118-
def _assert_sonar_fields(self, result):
119-
assert result["detectionTool"]["name"] == "Sonar"
120-
assert (
121-
result["detectionTool"]["rule"]["id"]
122-
== self.codemod_instance._metadata.tool.rule_id
123-
)
124-
assert (
125-
result["detectionTool"]["rule"]["name"]
126-
== self.codemod_instance._metadata.tool.rule_name
127-
)
128-
# TODO: empty array until we add findings metadata
129-
assert result["detectionTool"]["findings"] == []
130-
131119
def _assert_results_fields(self, results, output_path):
132120
assert len(results) == 1
133121
result = results[0]
@@ -146,13 +134,7 @@ def _assert_results_fields(self, results, output_path):
146134
]:
147135
assert reference["url"] == reference["description"]
148136

149-
if self.sonar_issues_json:
150-
assert self.codemod_instance._metadata.tool is not None
151-
assert (
152-
result["references"][-1]["description"]
153-
== self.codemod_instance._metadata.tool.rule_name
154-
)
155-
self._assert_sonar_fields(result)
137+
self._assert_sonar_fields(result)
156138

157139
assert len(result["changeset"]) == self.num_changed_files
158140

@@ -169,6 +151,9 @@ def _assert_results_fields(self, results, output_path):
169151
assert line_change["lineNumber"] == int(self.expected_line_change)
170152
assert line_change["description"] == self.change_description
171153

154+
def _assert_sonar_fields(self, result):
155+
del result
156+
172157
def _assert_codetf_output(self, codetf_schema):
173158
with open(self.output_path, "r", encoding="utf-8") as f:
174159
codetf = json.load(f)
@@ -277,6 +262,7 @@ def setup_class(cls):
277262
# in parallel at this time since they would all override the same
278263
# tests/samples/requirements.txt file, unless we change that to
279264
# a temporary file.
265+
cls.check_sonar_issues()
280266

281267
@classmethod
282268
def teardown_class(cls):
@@ -286,6 +272,37 @@ def teardown_class(cls):
286272
with open(cls.code_path, mode="w", encoding="utf-8") as f:
287273
f.write(cls.original_code)
288274

275+
@classmethod
276+
def check_sonar_issues(cls):
277+
sonar_results = SonarResultSet.from_json(cls.sonar_issues_json)
278+
279+
assert (
280+
cls.codemod.rule_id in sonar_results
281+
), f"Make sure to add a sonar issue for {cls.codemod.rule_id} in {cls.sonar_issues_json}"
282+
results_for_codemod = sonar_results[cls.codemod.rule_id]
283+
file_path = pathlib.Path(cls.code_filename)
284+
assert (
285+
file_path in results_for_codemod
286+
), f"Make sure to add a sonar issue for file `{cls.code_filename}` under rule `{cls.codemod.rule_id}` in {cls.sonar_issues_json}"
287+
288+
def _assert_sonar_fields(self, result):
289+
assert self.codemod_instance._metadata.tool is not None
290+
assert (
291+
result["references"][-1]["description"]
292+
== self.codemod_instance._metadata.tool.rule_name
293+
)
294+
assert result["detectionTool"]["name"] == "Sonar"
295+
assert (
296+
result["detectionTool"]["rule"]["id"]
297+
== self.codemod_instance._metadata.tool.rule_id
298+
)
299+
assert (
300+
result["detectionTool"]["rule"]["name"]
301+
== self.codemod_instance._metadata.tool.rule_name
302+
)
303+
# TODO: empty array until we add findings metadata
304+
assert result["detectionTool"]["findings"] == []
305+
289306

290307
def original_and_expected_from_code_path(code_path, replacements):
291308
"""

src/codemodder/scripts/generate_docs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,11 @@ class DocMetadata:
314314
guidance_explained=CORE_METADATA["fix-missing-self-or-cls"].guidance_explained,
315315
need_sarif="Yes (Sonar)",
316316
),
317+
"secure-tempfile-S5445": DocMetadata(
318+
importance=CORE_METADATA["secure-tempfile"].importance,
319+
guidance_explained=CORE_METADATA["secure-tempfile"].guidance_explained,
320+
need_sarif="Yes (Sonar)",
321+
),
317322
}
318323

319324

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from .sonar.sonar_remove_assertion_in_pytest_raises import (
5858
SonarRemoveAssertionInPytestRaises,
5959
)
60+
from .sonar.sonar_tempfile_mktemp import SonarTempfileMktemp
6061
from .sql_parameterization import SQLQueryParameterization
6162
from .str_concat_in_seq_literal import StrConcatInSeqLiteral
6263
from .subprocess_shell_false import SubprocessShellFalse
@@ -146,5 +147,6 @@
146147
SonarDjangoJsonResponseType,
147148
SonarJwtDecodeVerify,
148149
SonarFixMissingSelfOrCls,
150+
SonarTempfileMktemp,
149151
],
150152
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from codemodder.codemods.sonar import SonarCodemod
2+
from core_codemods.tempfile_mktemp import TempfileMktemp
3+
4+
SonarTempfileMktemp = SonarCodemod.from_core_codemod(
5+
name="secure-tempfile-S5445",
6+
other=TempfileMktemp,
7+
rule_id="python:S5445",
8+
rule_name="Insecure temporary file creation methods should not be used",
9+
rule_url="https://rules.sonarsource.com/python/type/Vulnerability/RSPEC-5445/",
10+
)
Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1+
from codemodder.codemods.libcst_transformer import (
2+
LibcstResultTransformer,
3+
LibcstTransformerPipeline,
4+
)
5+
from codemodder.codemods.semgrep import SemgrepRuleDetector
16
from codemodder.codemods.utils_mixin import NameResolutionMixin
2-
from core_codemods.api import Metadata, Reference, ReviewGuidance, SimpleCodemod
7+
from core_codemods.api import CoreCodemod, Metadata, Reference, ReviewGuidance
38

49

5-
class TempfileMktemp(SimpleCodemod, NameResolutionMixin):
6-
metadata = Metadata(
10+
class TempfileMktempTransformer(LibcstResultTransformer, NameResolutionMixin):
11+
change_description = "Replaces `tempfile.mktemp` with `tempfile.mkstemp`."
12+
_module_name = "tempfile"
13+
14+
def on_result_found(self, original_node, updated_node):
15+
maybe_name = self.get_aliased_prefix_name(original_node, self._module_name)
16+
if (maybe_name := maybe_name or self._module_name) == self._module_name:
17+
self.add_needed_import(self._module_name)
18+
self.remove_unused_import(original_node)
19+
return self.update_call_target(updated_node, maybe_name, "mkstemp")
20+
21+
22+
TempfileMktemp = CoreCodemod(
23+
metadata=Metadata(
724
name="secure-tempfile",
825
summary="Upgrade and Secure Temp File Creation",
926
review_guidance=ReviewGuidance.MERGE_WITHOUT_REVIEW,
@@ -12,22 +29,16 @@ class TempfileMktemp(SimpleCodemod, NameResolutionMixin):
1229
url="https://docs.python.org/3/library/tempfile.html#tempfile.mktemp"
1330
),
1431
],
15-
)
16-
change_description = "Replaces `tempfile.mktemp` with `tempfile.mkstemp`."
17-
18-
_module_name = "tempfile"
19-
detector_pattern = """
32+
),
33+
detector=SemgrepRuleDetector(
34+
"""
2035
rules:
2136
- patterns:
2237
- pattern: tempfile.mktemp(...)
2338
- pattern-inside: |
2439
import tempfile
2540
...
2641
"""
27-
28-
def on_result_found(self, original_node, updated_node):
29-
maybe_name = self.get_aliased_prefix_name(original_node, self._module_name)
30-
if (maybe_name := maybe_name or self._module_name) == self._module_name:
31-
self.add_needed_import(self._module_name)
32-
self.remove_unused_import(original_node)
33-
return self.update_call_target(updated_node, maybe_name, "mkstemp")
42+
),
43+
transformer=LibcstTransformerPipeline(TempfileMktempTransformer),
44+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from core_codemods.sonar.sonar_tempfile_mktemp import SonarTempfileMktemp
5+
6+
7+
class TestSonarTempfileMktemp(BaseSASTCodemodTest):
8+
codemod = SonarTempfileMktemp
9+
tool = "sonar"
10+
11+
def test_name(self):
12+
assert self.codemod.name == "secure-tempfile-S5445"
13+
14+
def test_simple(self, tmpdir):
15+
input_code = """
16+
import tempfile
17+
18+
tempfile.mktemp()
19+
"""
20+
expected = """
21+
import tempfile
22+
23+
tempfile.mkstemp()
24+
"""
25+
issues = {
26+
"issues": [
27+
{
28+
"rule": "python:S5445",
29+
"status": "OPEN",
30+
"component": "code.py",
31+
"textRange": {
32+
"startLine": 4,
33+
"endLine": 4,
34+
"startOffset": 0,
35+
"endOffset": 17,
36+
},
37+
}
38+
]
39+
}
40+
self.run_and_assert(tmpdir, input_code, expected, results=json.dumps(issues))

0 commit comments

Comments
 (0)