Skip to content

Commit 0d45f59

Browse files
authored
New RSA key size transformer and semgrep codemod (#711)
* add debug statements * new semgrep codemod for rsa key size * document * refactor
1 parent 294ee15 commit 0d45f59

File tree

5 files changed

+174
-1
lines changed

5 files changed

+174
-1
lines changed

src/codemodder/codemods/libcst_transformer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,11 @@ def apply(
278278
return None
279279

280280
if not file_context.codemod_changes:
281+
logger.debug("No changes produced for %s", file_path)
281282
return None
282283

283284
if not (diff := create_diff_from_tree(source_tree, tree)):
285+
logger.debug("No code diff produced for %s", file_path)
284286
return None
285287

286288
change_set = ChangeSet(

src/codemodder/scripts/generate_docs.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,12 @@ class DocMetadata:
339339
importance="Medium",
340340
guidance_explained="Our change provides the most secure way to create cookies in Django. However, it's possible you have configured your Django application configurations to use secure cookies. In these cases, using the default parameters for `set_cookie` is safe.",
341341
need_sarif="Yes (Semgrep)",
342-
)
342+
),
343+
"rsa-key-size": DocMetadata(
344+
importance="Medium",
345+
guidance_explained="This codemod updates the key size for RSA to the `2048` recommended minimum. Since this is an important security fix that follows modern cryptographic standards, we believe this change can be safely merged without review.",
346+
need_sarif="Yes (Semgrep)",
347+
),
343348
}
344349
ALL_CODEMODS_METADATA = (
345350
CORE_CODEMODS | DEFECTDOJO_CODEMODS | SONAR_CODEMODS | SEMGREP_CODEMODS

src/core_codemods/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from .semgrep.semgrep_enable_jinja2_autoescape import SemgrepEnableJinja2Autoescape
5959
from .semgrep.semgrep_harden_pyyaml import SemgrepHardenPyyaml
6060
from .semgrep.semgrep_jwt_decode_verify import SemgrepJwtDecodeVerify
61+
from .semgrep.semgrep_rsa_key_size import SemgrepRsaKeySize
6162
from .semgrep.semgrep_subprocess_shell_false import SemgrepSubprocessShellFalse
6263
from .semgrep.semgrep_use_defused_xml import SemgrepUseDefusedXml
6364
from .sonar.sonar_break_or_continue_out_of_loop import SonarBreakOrContinueOutOfLoop
@@ -208,5 +209,6 @@
208209
SemgrepSubprocessShellFalse,
209210
SemgrepDjangoSecureSetCookie,
210211
SemgrepHardenPyyaml,
212+
SemgrepRsaKeySize,
211213
],
212214
)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import libcst as cst
2+
3+
from codemodder.codemods.base_codemod import (
4+
Metadata,
5+
ReviewGuidance,
6+
ToolMetadata,
7+
ToolRule,
8+
)
9+
from codemodder.codemods.libcst_transformer import (
10+
LibcstResultTransformer,
11+
LibcstTransformerPipeline,
12+
NewArg,
13+
)
14+
from codemodder.codemods.semgrep import SemgrepSarifFileDetector
15+
from codemodder.result import fuzzy_column_match, same_line
16+
from core_codemods.semgrep.api import SemgrepCodemod, semgrep_url_from_id
17+
18+
RSA_KEYSIZE = "2048"
19+
20+
21+
class RsaKeySizeTransformer(LibcstResultTransformer):
22+
change_description = "Change the RSA key size to 2048"
23+
24+
def on_result_found(self, original_node, updated_node):
25+
if len(original_node.args) < 2:
26+
return original_node
27+
28+
if original_node.args[1].keyword is None:
29+
new_args = [original_node.args[0], self.make_new_arg(RSA_KEYSIZE)]
30+
else:
31+
new_args = self.replace_args(
32+
original_node,
33+
[NewArg(name="key_size", value=RSA_KEYSIZE, add_if_missing=False)],
34+
)
35+
return self.update_arg_target(updated_node, new_args)
36+
37+
def filter_by_result(self, node) -> bool:
38+
"""
39+
Special case result-matching for this rule because the SAST
40+
results returned have a start/end column for the key_size keyword
41+
within the call, not for the entire call.
42+
"""
43+
match node:
44+
case cst.Call():
45+
pos_to_match = self.node_position(node)
46+
return any(
47+
self.match_location(pos_to_match, result)
48+
for result in self.results or []
49+
)
50+
return False
51+
52+
def match_location(self, pos, result):
53+
return any(
54+
same_line(pos, location) and fuzzy_column_match(pos, location)
55+
for location in result.locations
56+
)
57+
58+
59+
SemgrepRsaKeySize = SemgrepCodemod(
60+
metadata=Metadata(
61+
name="rsa-key-size",
62+
summary=RsaKeySizeTransformer.change_description.title(),
63+
description=RsaKeySizeTransformer.change_description.title(),
64+
review_guidance=ReviewGuidance.MERGE_WITHOUT_REVIEW,
65+
tool=ToolMetadata(
66+
name="Semgrep",
67+
rules=[
68+
ToolRule(
69+
id=(
70+
rule_id := "python.cryptography.security.insufficient-rsa-key-size.insufficient-rsa-key-size"
71+
),
72+
name="insufficient-rsa-key-size",
73+
url=semgrep_url_from_id(rule_id),
74+
)
75+
],
76+
),
77+
references=[],
78+
),
79+
transformer=LibcstTransformerPipeline(RsaKeySizeTransformer),
80+
detector=SemgrepSarifFileDetector(),
81+
requested_rules=[rule_id],
82+
)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import json
2+
3+
from codemodder.codemods.test import BaseSASTCodemodTest
4+
from core_codemods.semgrep.semgrep_rsa_key_size import SemgrepRsaKeySize
5+
6+
7+
class TestSemgrepRsaKeySize(BaseSASTCodemodTest):
8+
codemod = SemgrepRsaKeySize
9+
tool = "semgrep"
10+
11+
def test_name(self):
12+
assert self.codemod.name == "rsa-key-size"
13+
14+
def _run_and_assert_with_results(self, tmpdir, input_code, expected_output):
15+
results = {
16+
"runs": [
17+
{
18+
"results": [
19+
{
20+
"fingerprints": {"matchBasedId/v1": "123"},
21+
"locations": [
22+
{
23+
"physicalLocation": {
24+
"artifactLocation": {
25+
"uri": "code.py",
26+
"uriBaseId": "%SRCROOT%",
27+
},
28+
"region": {
29+
"endColumn": 37,
30+
"endLine": 3,
31+
"snippet": {
32+
"text": "rsa.generate_private_key(65537, 1024)"
33+
},
34+
"startColumn": 33,
35+
"startLine": 3,
36+
},
37+
}
38+
}
39+
],
40+
"message": {
41+
"text": "Detected an insufficient key size for RSA. NIST recommends a key size of 2048 or higher."
42+
},
43+
"properties": {},
44+
"ruleId": "python.cryptography.security.insufficient-rsa-key-size.insufficient-rsa-key-size",
45+
}
46+
]
47+
}
48+
]
49+
}
50+
self.run_and_assert(
51+
tmpdir,
52+
input_code,
53+
expected_output,
54+
results=json.dumps(results),
55+
)
56+
57+
def test_keysize_arg(self, tmpdir):
58+
input_code = """\
59+
from cryptography.hazmat.primitives.asymmetric import rsa
60+
61+
rsa.generate_private_key(65537, 1024)
62+
"""
63+
expected_output = """\
64+
from cryptography.hazmat.primitives.asymmetric import rsa
65+
66+
rsa.generate_private_key(65537, 2048)
67+
"""
68+
69+
self._run_and_assert_with_results(tmpdir, input_code, expected_output)
70+
71+
def test_keysize_kwarg(self, tmpdir):
72+
input_code = """\
73+
from cryptography.hazmat.primitives.asymmetric import rsa
74+
75+
rsa.generate_private_key(65537, key_size=1024)
76+
"""
77+
expected_output = """\
78+
from cryptography.hazmat.primitives.asymmetric import rsa
79+
80+
rsa.generate_private_key(65537, key_size=2048)
81+
"""
82+
self._run_and_assert_with_results(tmpdir, input_code, expected_output)

0 commit comments

Comments
 (0)