Skip to content

Commit f632411

Browse files
authored
Implement prototype codemods for DefectDojo remediation (#438)
* First steps towards defectdojo codemod * Implement initial DefectDojo (semgrep) codemod * Implement semgrep:avoid-insecure-deserialization for DefectDojo
1 parent c91d54c commit f632411

21 files changed

+623
-124
lines changed

integration_tests/test_harden_pyyaml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import yaml
22

33
from codemodder.codemods.test import BaseIntegrationTest
4-
from core_codemods.harden_pyyaml import HardenPyyaml
4+
from core_codemods.harden_pyyaml import HardenPyyaml, HardenPyyamlTransformer
55

66

77
class TestHardenPyyaml(BaseIntegrationTest):
@@ -17,6 +17,6 @@ class TestHardenPyyaml(BaseIntegrationTest):
1717
]
1818
expected_diff = '--- \n+++ \n@@ -1,4 +1,4 @@\n import yaml\n \n data = b"!!python/object/apply:subprocess.Popen \\\\n- ls"\n-deserialized_data = yaml.load(data, Loader=yaml.Loader)\n+deserialized_data = yaml.load(data, Loader=yaml.SafeLoader)\n'
1919
expected_line_change = "4"
20-
change_description = HardenPyyaml.change_description
20+
change_description = HardenPyyamlTransformer.change_description
2121
# expected exception because the yaml.SafeLoader protects against unsafe code
2222
allowed_exceptions = (yaml.constructor.ConstructorError,)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ all = [
8181
[project.entry-points.codemods]
8282
core = "core_codemods:registry"
8383
sonar = "core_codemods:sonar_registry"
84+
defectdojo = "core_codemods:defectdojo_registry"
8485

8586
[tool.setuptools]
8687

src/codemodder/codemodder.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,13 @@ def run(original_args) -> int:
156156
logger.info("codemodder: python/%s", __version__)
157157
logger.info("command: %s %s", Path(sys.argv[0]).name, " ".join(original_args))
158158

159-
# TODO: sonar files should be _parsed_ here as well
160159
# TODO: this should be dict[str, list[Path]]
161-
162160
tool_result_files_map: DefaultDict[str, list[str]] = detect_sarif_tools(
163161
[Path(name) for name in argv.sarif or []]
164162
)
165-
(
166-
tool_result_files_map["sonar"].extend(argv.sonar_issues_json)
167-
if argv.sonar_issues_json
168-
else None
169-
)
170-
(
171-
tool_result_files_map["sonar"].extend(argv.sonar_hotspots_json)
172-
if argv.sonar_hotspots_json
173-
else None
174-
)
163+
tool_result_files_map["sonar"].extend(argv.sonar_issues_json or [])
164+
tool_result_files_map["sonar"].extend(argv.sonar_hotspots_json or [])
165+
tool_result_files_map["defectdojo"] = argv.defectdojo_findings_json or []
175166

176167
repo_manager = PythonRepoManager(Path(argv.directory))
177168
context = CodemodExecutionContext(

src/codemodder/codemods/libcst_transformer.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,13 @@ def lineno_for_node(self, node):
118118
def add_dependency(self, dependency: Dependency):
119119
self.file_context.add_dependency(dependency)
120120

121-
def report_change(self, original_node):
121+
def report_change(self, original_node, description: str | None = None):
122122
line_number = self.lineno_for_node(original_node)
123123
self.file_context.codemod_changes.append(
124-
Change(lineNumber=line_number, description=self.change_description)
124+
Change(
125+
lineNumber=line_number,
126+
description=description or self.change_description,
127+
)
125128
)
126129

127130
def remove_unused_import(self, original_node):

src/codemodder/codemods/test/utils.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,7 @@ def run_and_assert(
146146
tmp_file_path.write_text(dedent(input_code))
147147

148148
tmp_results_file_path = Path(tmpdir) / "sast_results"
149-
150-
with open(tmp_results_file_path, "w", encoding="utf-8") as results_file:
151-
results_file.write(results)
149+
tmp_results_file_path.write_text(results)
152150

153151
files_to_check = files or [tmp_file_path]
154152

src/codemodder/result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
@dataclass
1717
class LineInfo:
1818
line: int
19-
column: int
20-
snippet: str | None
19+
column: int = -1
20+
snippet: str | None = None
2121

2222

2323
@dataclass

src/codemodder/scripts/generate_docs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,16 @@ class DocMetadata:
329329
guidance_explained=CORE_METADATA["enable-jinja2-autoescape"].guidance_explained,
330330
need_sarif="Yes (Sonar)",
331331
),
332+
"django-secure-set-cookie": DocMetadata(
333+
importance="Medium",
334+
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.",
335+
need_sarif="Yes (DefectDojo)",
336+
),
337+
"avoid-insecure-deserialization": DocMetadata(
338+
importance="High",
339+
guidance_explained="This change is generally safe and will prevent deserialization vulnerabilities.",
340+
need_sarif="Yes (DefectDojo)",
341+
),
332342
}
333343

334344

src/codemodder/sonar_results.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from dataclasses import replace
3+
from functools import cache
34
from pathlib import Path
45

56
import libcst as cst
@@ -45,6 +46,7 @@ def match_location(self, pos, node):
4546

4647
class SonarResultSet(ResultSet):
4748
@classmethod
49+
@cache
4850
def from_json(cls, json_file: str | Path) -> Self:
4951
try:
5052
with open(json_file, "r", encoding="utf-8") as file:

src/core_codemods/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
from .add_requests_timeouts import AddRequestsTimeouts
44
from .combine_startswith_endswith import CombineStartswithEndswith
5+
from .defectdojo.semgrep.avoid_insecure_deserialization import (
6+
AvoidInsecureDeserialization,
7+
)
8+
from .defectdojo.semgrep.django_secure_set_cookie import DjangoSecureSetCookie
59
from .django_debug_flag_on import DjangoDebugFlagOn
610
from .django_json_response_type import DjangoJsonResponseType
711
from .django_model_without_dunder_str import DjangoModelWithoutDunderStr
@@ -154,3 +158,11 @@
154158
SonarEnableJinja2Autoescape,
155159
],
156160
)
161+
162+
defectdojo_registry = CodemodCollection(
163+
origin="defectdojo",
164+
codemods=[
165+
AvoidInsecureDeserialization,
166+
DjangoSecureSetCookie,
167+
],
168+
)

src/core_codemods/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# ruff: noqa: F401
22
from codemodder.codemods.api import Metadata, Reference, ReviewGuidance
33

4-
from .core_codemod import CoreCodemod, ImportModifierCodemod, SimpleCodemod
4+
from .core_codemod import CoreCodemod, ImportModifierCodemod, SASTCodemod, SimpleCodemod

0 commit comments

Comments
 (0)