-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
jwt.decode sonar codemod #326
Changes from all commits
957a2c0
c2e915d
7640b2d
a416a78
d19c6dc
dce788f
a5cb40b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from core_codemods.sonar.sonar_jwt_decode_verify import ( | ||
SonarJwtDecodeVerify, | ||
JwtDecodeVerifySonarTransformer, | ||
) | ||
from codemodder.codemods.test import ( | ||
BaseIntegrationTest, | ||
original_and_expected_from_code_path, | ||
) | ||
|
||
|
||
class TestJwtDecodeVerify(BaseIntegrationTest): | ||
codemod = SonarJwtDecodeVerify | ||
code_path = "tests/samples/jwt_decode_verify.py" | ||
original_code, expected_new_code = original_and_expected_from_code_path( | ||
code_path, | ||
[ | ||
( | ||
10, | ||
"""decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=True)\n""", | ||
), | ||
( | ||
11, | ||
"""decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True})\n""", | ||
), | ||
], | ||
) | ||
sonar_issues_json = "tests/samples/sonar_issues.json" | ||
|
||
expected_diff = '--- \n+++ \n@@ -8,7 +8,7 @@\n \n encoded_jwt = jwt.encode(payload, SECRET_KEY, algorithm="HS256")\n \n-decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=False)\n-decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": False})\n+decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=True)\n+decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True})\n \n var = "something"\n' | ||
expected_line_change = "11" | ||
num_changes = 2 | ||
change_description = JwtDecodeVerifySonarTransformer.change_description |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,8 @@ def __init__( | |
def filter_by_result(self, node): | ||
pos_to_match = self.node_position(node) | ||
if self.results is None: | ||
# Returning True here means codemods without detectors (and results) | ||
# will still run their transformations. | ||
return True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I documented this because it was strange to return True here (do we filter by result or not filter by result?). Then I realized the logic here is that this method is used for all codemods - those with and those without results. We still want to analyze codemods even if there are no results. Maybe later on we can have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I encountered this too. We're using |
||
return any(result.match_location(pos_to_match, node) for result in self.results) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import libcst as cst | ||
from codemodder.codemods.base_codemod import Reference | ||
from codemodder.result import same_line, fuzzy_column_match | ||
from codemodder.codemods.sonar import SonarCodemod | ||
from codemodder.codemods.libcst_transformer import ( | ||
LibcstTransformerPipeline, | ||
) | ||
from core_codemods.jwt_decode_verify import JwtDecodeVerify, JwtDecodeVerifyTransformer | ||
|
||
|
||
class JwtDecodeVerifySonarTransformer(JwtDecodeVerifyTransformer): | ||
def filter_by_result(self, node) -> bool: | ||
""" | ||
Special case result-matching for this rule because the sonar | ||
results returned have a start/end column for the verify keyword | ||
within the `decode` call, not for the entire call like semgrep returns. | ||
""" | ||
match node: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This solution makes sense to me and it works for now. In the longer term, I think we should filter the Sonar/SAST results before they ever reach the transformer. This means we would add some logic to each detector that requires it. This would mean implementing some kind of visitor that validates each of the detected locations before passing the results to the transformer. There's a bit of a performance impact in this case since it effectively means another pass on the file, but it only applies to files where there are already results. I'm saying this not because I think we need this change right now but because I've encountered similar issues with remediating another SAST tool and I think that updating the filtering logic here for each case is going to become cumbersome. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very interesting! The detector for this particular codemod is semgrep so that's also a little wrinkle. |
||
case cst.Call(): | ||
pos_to_match = self.node_position(node) | ||
return any( | ||
self.match_location(pos_to_match, result) | ||
for result in self.results or [] | ||
) | ||
return False | ||
|
||
def match_location(self, pos, result): | ||
return any( | ||
same_line(pos, location) and fuzzy_column_match(pos, location) | ||
for location in result.locations | ||
) | ||
Comment on lines
+29
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you encounter any examples where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're going to have to give me an example because adding Or perhaps Im' not understanding your comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the simplest terms, does anything break if you remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there are no location matches without fuzzy matching. |
||
|
||
|
||
SonarJwtDecodeVerify = SonarCodemod.from_core_codemod( | ||
name="jwt-decode-verify-S5659", | ||
other=JwtDecodeVerify, | ||
rules=["python:S5659"], | ||
new_references=[ | ||
Reference(url="https://rules.sonarsource.com/python/RSPEC-5659/"), | ||
], | ||
transformer=LibcstTransformerPipeline(JwtDecodeVerifySonarTransformer), | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import json | ||
from core_codemods.sonar.sonar_jwt_decode_verify import SonarJwtDecodeVerify | ||
from codemodder.codemods.test import BaseSASTCodemodTest | ||
|
||
|
||
class TestSonarJwtDecodeVerify(BaseSASTCodemodTest): | ||
codemod = SonarJwtDecodeVerify | ||
tool = "sonar" | ||
|
||
def test_name(self): | ||
assert self.codemod.name == "jwt-decode-verify-S5659" | ||
|
||
def test_simple(self, tmpdir): | ||
input_code = """ | ||
import jwt | ||
|
||
SECRET_KEY = "mysecretkey" | ||
payload = { | ||
"user_id": 123, | ||
"username": "john", | ||
} | ||
|
||
encoded_jwt = jwt.encode(payload, SECRET_KEY, algorithm="HS256") | ||
decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=False) | ||
decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": False}) | ||
""" | ||
expected = """ | ||
import jwt | ||
|
||
SECRET_KEY = "mysecretkey" | ||
payload = { | ||
"user_id": 123, | ||
"username": "john", | ||
} | ||
|
||
encoded_jwt = jwt.encode(payload, SECRET_KEY, algorithm="HS256") | ||
decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=True) | ||
decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True}) | ||
""" | ||
issues = { | ||
"issues": [ | ||
{ | ||
"rule": "python:S5659", | ||
"status": "OPEN", | ||
"component": f"{tmpdir / 'code.py'}", | ||
"textRange": { | ||
"startLine": 11, | ||
"endLine": 11, | ||
"startOffset": 76, | ||
"endOffset": 88, | ||
}, | ||
}, | ||
{ | ||
"rule": "python:S5659", | ||
"status": "OPEN", | ||
"component": f"{tmpdir / 'code.py'}", | ||
"textRange": { | ||
"startLine": 12, | ||
"endLine": 12, | ||
"startOffset": 84, | ||
"endOffset": 111, | ||
}, | ||
}, | ||
] | ||
} | ||
self.run_and_assert( | ||
tmpdir, input_code, expected, results=json.dumps(issues), num_changes=2 | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized this int test was importing the wrong codemod