Skip to content

Commit c16fe9e

Browse files
authored
Sonar jinja2 (#435)
* separate codemod * add sonar jinja2 autoescape sonar codemod * use functools.cache
1 parent 013b758 commit c16fe9e

File tree

11 files changed

+193
-29
lines changed

11 files changed

+193
-29
lines changed

integration_tests/test_jinja2_autoescape.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from codemodder.codemods.test import BaseIntegrationTest
2-
from core_codemods.enable_jinja2_autoescape import EnableJinja2Autoescape
2+
from core_codemods.enable_jinja2_autoescape import (
3+
EnableJinja2Autoescape,
4+
EnableJinja2AutoescapeTransformer,
5+
)
36

47

58
class TestEnableJinja2Autoescape(BaseIntegrationTest):
@@ -17,4 +20,4 @@ class TestEnableJinja2Autoescape(BaseIntegrationTest):
1720
expected_diff = "--- \n+++ \n@@ -1,4 +1,4 @@\n from jinja2 import Environment\n \n-env = Environment()\n-env = Environment(autoescape=False)\n+env = Environment(autoescape=True)\n+env = Environment(autoescape=True)\n"
1821
expected_line_change = "3"
1922
num_changes = 2
20-
change_description = EnableJinja2Autoescape.change_description
23+
change_description = EnableJinja2AutoescapeTransformer.change_description
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from codemodder.codemods.test import SonarIntegrationTest
2+
from core_codemods.enable_jinja2_autoescape import EnableJinja2AutoescapeTransformer
3+
from core_codemods.sonar.sonar_enable_jinja2_autoescape import (
4+
SonarEnableJinja2Autoescape,
5+
)
6+
7+
8+
class TestSonarEnableJinja2Autoescape(SonarIntegrationTest):
9+
codemod = SonarEnableJinja2Autoescape
10+
code_path = "tests/samples/jinja2_autoescape.py"
11+
replacement_lines = [
12+
(3, "env = Environment(autoescape=True)\n"),
13+
(4, "env = Environment(autoescape=True)\n"),
14+
]
15+
expected_diff = "--- \n+++ \n@@ -1,4 +1,4 @@\n from jinja2 import Environment\n \n-env = Environment()\n-env = Environment(autoescape=False)\n+env = Environment(autoescape=True)\n+env = Environment(autoescape=True)\n"
16+
expected_line_change = "3"
17+
num_changes = 2
18+
change_description = EnableJinja2AutoescapeTransformer.change_description

src/codemodder/codemods/sonar.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import cache
12
from pathlib import Path
23

34
from codemodder.codemods.base_codemod import Metadata, Reference, ToolMetadata
@@ -52,23 +53,23 @@ def from_core_codemod(
5253

5354

5455
class SonarDetector(BaseDetector):
55-
_lazy_cache = None
56-
5756
def apply(
5857
self,
5958
codemod_id: str,
6059
context: CodemodExecutionContext,
6160
files_to_analyze: list[Path],
6261
) -> ResultSet:
63-
if not self._lazy_cache:
64-
self._lazy_cache = process_sonar_findings(
65-
context.tool_result_files_map.get("sonar", [])
66-
)
67-
return self._lazy_cache
62+
sonar_findings = process_sonar_findings(
63+
tuple(
64+
context.tool_result_files_map.get("sonar", ())
65+
) # Convert list to tuple for cache hashability
66+
)
67+
return sonar_findings
6868

6969

70-
def process_sonar_findings(sonar_json_files: list[str]) -> SonarResultSet:
70+
@cache
71+
def process_sonar_findings(sonar_json_files: tuple[str]) -> SonarResultSet:
7172
combined_result_set = SonarResultSet()
72-
for file in sonar_json_files or []:
73+
for file in sonar_json_files or ():
7374
combined_result_set |= SonarResultSet.from_json(file)
7475
return combined_result_set

src/codemodder/codemods/test/integration_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def check_sonar_issues(cls):
285285
from codemodder.codemods.sonar import process_sonar_findings
286286

287287
sonar_results = process_sonar_findings(
288-
[cls.sonar_issues_json, cls.sonar_hotspots_json]
288+
(cls.sonar_issues_json, cls.sonar_hotspots_json)
289289
)
290290

291291
assert (

src/codemodder/scripts/generate_docs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ class DocMetadata:
324324
guidance_explained=CORE_METADATA["secure-random"].guidance_explained,
325325
need_sarif="Yes (Sonar)",
326326
),
327+
"enable-jinja2-autoescape-S5247": DocMetadata(
328+
importance=CORE_METADATA["enable-jinja2-autoescape"].importance,
329+
guidance_explained=CORE_METADATA["enable-jinja2-autoescape"].guidance_explained,
330+
need_sarif="Yes (Sonar)",
331+
),
327332
}
328333

329334

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from .secure_random import SecureRandom
4848
from .sonar.sonar_django_json_response_type import SonarDjangoJsonResponseType
4949
from .sonar.sonar_django_receiver_on_top import SonarDjangoReceiverOnTop
50+
from .sonar.sonar_enable_jinja2_autoescape import SonarEnableJinja2Autoescape
5051
from .sonar.sonar_exception_without_raise import SonarExceptionWithoutRaise
5152
from .sonar.sonar_fix_assert_tuple import SonarFixAssertTuple
5253
from .sonar.sonar_fix_missing_self_or_cls import SonarFixMissingSelfOrCls
@@ -150,5 +151,6 @@
150151
SonarFixMissingSelfOrCls,
151152
SonarTempfileMktemp,
152153
SonarSecureRandom,
154+
SonarEnableJinja2Autoescape,
153155
],
154156
)
Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1-
from codemodder.codemods.libcst_transformer import NewArg
2-
from core_codemods.api import Metadata, Reference, ReviewGuidance, SimpleCodemod
1+
from codemodder.codemods.libcst_transformer import (
2+
LibcstResultTransformer,
3+
LibcstTransformerPipeline,
4+
NewArg,
5+
)
6+
from codemodder.codemods.semgrep import SemgrepRuleDetector
7+
from core_codemods.api import CoreCodemod, Metadata, Reference, ReviewGuidance
38

49

5-
class EnableJinja2Autoescape(SimpleCodemod):
6-
metadata = Metadata(
10+
class EnableJinja2AutoescapeTransformer(LibcstResultTransformer):
11+
change_description = (
12+
"Sets the `autoescape` parameter in jinja2.Environment to `True`."
13+
)
14+
15+
def on_result_found(self, original_node, updated_node):
16+
new_args = self.replace_args(
17+
original_node,
18+
[NewArg(name="autoescape", value="True", add_if_missing=True)],
19+
)
20+
return self.update_arg_target(updated_node, new_args)
21+
22+
23+
EnableJinja2Autoescape = CoreCodemod(
24+
metadata=Metadata(
725
name="enable-jinja2-autoescape",
826
summary="Enable Jinja2 Autoescape",
927
review_guidance=ReviewGuidance.MERGE_AFTER_REVIEW,
@@ -13,11 +31,9 @@ class EnableJinja2Autoescape(SimpleCodemod):
1331
url="https://jinja.palletsprojects.com/en/3.1.x/api/#autoescaping"
1432
),
1533
],
16-
)
17-
change_description = (
18-
"Sets the `autoescape` parameter in jinja2.Environment to `True`."
19-
)
20-
detector_pattern = """
34+
),
35+
detector=SemgrepRuleDetector(
36+
"""
2137
rules:
2238
- pattern-either:
2339
- patterns:
@@ -35,10 +51,6 @@ class EnableJinja2Autoescape(SimpleCodemod):
3551
import aiohttp_jinja2
3652
...
3753
"""
38-
39-
def on_result_found(self, original_node, updated_node):
40-
new_args = self.replace_args(
41-
original_node,
42-
[NewArg(name="autoescape", value="True", add_if_missing=True)],
43-
)
44-
return self.update_arg_target(updated_node, new_args)
54+
),
55+
transformer=LibcstTransformerPipeline(EnableJinja2AutoescapeTransformer),
56+
)
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.enable_jinja2_autoescape import EnableJinja2Autoescape
3+
4+
SonarEnableJinja2Autoescape = SonarCodemod.from_core_codemod(
5+
name="enable-jinja2-autoescape-S5247",
6+
other=EnableJinja2Autoescape,
7+
rule_id="python:S5247",
8+
rule_name="Disabling auto-escaping in template engines is security-sensitive",
9+
rule_url="https://rules.sonarsource.com/python/RSPEC-5247/",
10+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import json
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from core_codemods.sonar.sonar_enable_jinja2_autoescape import (
5+
SonarEnableJinja2Autoescape,
6+
)
7+
8+
9+
class TestEnableJinja2Autoescape(BaseSASTCodemodTest):
10+
codemod = SonarEnableJinja2Autoescape
11+
tool = "sonar"
12+
13+
def test_name(self):
14+
assert self.codemod.name == "enable-jinja2-autoescape-S5247"
15+
16+
def test_simple(self, tmpdir):
17+
input_code = """
18+
from jinja2 import Environment
19+
env = Environment()
20+
env = Environment(autoescape=False)
21+
"""
22+
expected_output = """
23+
from jinja2 import Environment
24+
env = Environment(autoescape=True)
25+
env = Environment(autoescape=True)
26+
"""
27+
hotspots = {
28+
"hotspots": [
29+
{
30+
"rule": "python:S5247",
31+
"status": "OPEN",
32+
"component": "code.py",
33+
"textRange": {
34+
"startLine": 3,
35+
"endLine": 3,
36+
"startOffset": 6,
37+
"endOffset": 19,
38+
},
39+
},
40+
{
41+
"rule": "python:S5247",
42+
"status": "OPEN",
43+
"component": "code.py",
44+
"textRange": {
45+
"startLine": 4,
46+
"endLine": 4,
47+
"startOffset": 6,
48+
"endOffset": 35,
49+
},
50+
},
51+
]
52+
}
53+
self.run_and_assert(
54+
tmpdir,
55+
input_code,
56+
expected_output,
57+
results=json.dumps(hotspots),
58+
num_changes=2,
59+
)

tests/samples/jinja2_autoescape.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from jinja2 import Environment
2+
3+
env = Environment()
4+
env = Environment(autoescape=False)

0 commit comments

Comments
 (0)