diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/__init__.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/__init__.py index 792aa32..3d2b09b 100644 --- a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/__init__.py +++ b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/__init__.py @@ -1,11 +1,13 @@ from codemodder.registry import CodemodCollection from .custom_codemod import CustomCodemod +from .semgrep_codemod import CustomSemgrepCodemod # This name is used by the entry point in pyproject.toml to register the codemods registry = CodemodCollection( origin="{{ cookiecutter.project_slug }}", codemods=[ CustomCodemod, + CustomSemgrepCodemod, ], ) diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/__init__.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/__init__.py index 1e208cf..fa1994b 100644 --- a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/__init__.py +++ b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/__init__.py @@ -3,4 +3,4 @@ Reference, ReviewGuidance, ) -from .codemod import SimpleCodemod, CustomCodemod +from .codemod import SimpleCodemod, CustomCodemod, SASTCodemod diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/codemod.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/codemod.py index ebb1317..ab5daff 100644 --- a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/codemod.py +++ b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/api/codemod.py @@ -1,52 +1,43 @@ -from pathlib import Path -from codemodder.codemods.api import BaseCodemod, SimpleCodemod as _SimpleCodemod -from codemodder.codemods.base_codemod import Metadata -from codemodder.codemods.base_detector import BaseDetector -from codemodder.codemods.base_transformer import BaseTransformerPipeline -from codemodder.context import CodemodExecutionContext +from abc import ABCMeta +from codemodder.codemods.api import ( + FindAndFixCodemod, + RemediationCodemod, + SimpleCodemod as _SimpleCodemod, +) -class CustomCodemod(BaseCodemod): + +class CustomCodemodDocsMixin: """ - Base class for all codemods provided by this package. + Mixin for all codemods with docs provided by this package. """ @property - def origin(self): - return "{{ cookiecutter.project_slug }}" + def docs_module_path(self): + return "{{ cookiecutter.project_slug }}.docs" + + +class CustomCodemod(CustomCodemodDocsMixin, FindAndFixCodemod): + """ + Base class for all find-and-fix codemods provided by this package. + """ @property - def docs_module_path(self): + def origin(self): return "{{ cookiecutter.project_slug }}" -class SASTCodemod(CustomCodemod): - requested_rules: list[str] - - def __init__( - self, - *, - metadata: Metadata, - detector: BaseDetector | None = None, - transformer: BaseTransformerPipeline, - requested_rules: list[str] | None = None, - ): - super().__init__(metadata=metadata, detector=detector, transformer=transformer) - self.requested_rules = [self.name] - if requested_rules: - self.requested_rules.extend(requested_rules) +class SASTCodemod(CustomCodemodDocsMixin, RemediationCodemod, metaclass=ABCMeta): + """ + Base class for all SAST codemods provided by this package. - def apply( - self, - context: CodemodExecutionContext, - files_to_analyze: list[Path], - ) -> None: - self._apply(context, files_to_analyze, self.requested_rules) + Child classes must define the origin attribute, which must be a string that corresponds to the tool that generated the findings. + """ class SimpleCodemod(_SimpleCodemod): """ - Base class for all codemods in this package with a single detector and transformer. + Convenience base class for all custom codemods with a single detector and transformer. """ codemod_base = CustomCodemod diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/docs/__init__.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/semgrep_codemod.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/semgrep_codemod.py new file mode 100644 index 0000000..2765416 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/semgrep_codemod.py @@ -0,0 +1,69 @@ +import libcst as cst + +from codemodder.codemods.api import Metadata, RemediationCodemod, ReviewGuidance +from codemodder.codemods.semgrep import SemgrepSarifFileDetector +from codemodder.codemods.libcst_transformer import ( + LibcstResultTransformer, + LibcstTransformerPipeline, +) + +from .api import SASTCodemod + + +class SemgrepCodemod(SASTCodemod): + """Base class for all custom codemods that remediate Semgrep findings.""" + + @property + def origin(self): + """ + Remediation codemods must define an origin that corresponds to the tool that generated the findings. + """ + return "semgrep" + + +class CustomSemgrepCodemodTransformer(LibcstResultTransformer): + def on_result_found( + self, + original_node: cst.CSTNode, + updated_node: cst.CSTNode, + ) -> cst.CSTNode: + """ + This method is called for each result found by the detector. + + It should return the updated node, which will be used to replace the original node in the file. + It is intended to support relatively simply transformations that can be applied to a single node at a time. + It applies to a limited subset of CST nodes and is not intended to be used for more complex transformations. + The `LibcstResultTransformer` also exposes the entire LibCST visitor API for more complex transformations. + In those cases, the `visit_*` methods should be overridden instead of this method. + + Examples of codemods using both APIs can be found in the `codemodder` project: + https://github.com/pixee/codemodder-python/tree/main/src/core_codemods + """ + return updated_node + + +CustomSemgrepCodemod = SemgrepCodemod( + metadata=Metadata( + # The name should be a unique identifier for the codemod + # The origin and language are automatically added to the name by the framework to generate the full ID + name="custom-semgrep-codemod", + # The summary should be a single line that describes the purpose of the codemod + summary="Custom Semgrep codemod", + # The description should provide more detailed information about the codemod + # Longer-form descriptions can be provided in the docs/ directory + description="This is a custom Semgrep codemod.", + # Review guidance should reflect the level of review that the changes made by the codemod require + review_guidance=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW, + references=[], + ), + transformer=LibcstTransformerPipeline(CustomSemgrepCodemodTransformer), + # This is the detector that will be used to process Semgrep SARIF files in the project + # It automatically processes Semgrep sarif files given by the --sarif CLI flag + detector=SemgrepSarifFileDetector(), + # Modify this to match the file extensions of your project + default_extensions=[".py"], + # Update this with fully qualified Semgrep rule IDs that this codemod remediates + requested_rules=[ + "python.framework.security.problem.fake-rule-id", + ], +)