Skip to content

Commit

Permalink
Script to automate codemod docs (#73)
Browse files Browse the repository at this point in the history
* script

* working doc gen script

* add references to codemods

* make description = url automated

* update all codemod summary fields

* test docs

* update codemod metadata

* fix descriptions
  • Loading branch information
clavedeluna authored Oct 16, 2023
1 parent 1b76913 commit 8128ee6
Show file tree
Hide file tree
Showing 37 changed files with 459 additions and 54 deletions.
6 changes: 5 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ repos:
rev: 23.9.1
hooks:
- id: black
exclude: samples/
exclude: |
(?x)^(
samples/.*|
core_codemods/docs/.*
)$
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.1
hooks:
Expand Down
6 changes: 6 additions & 0 deletions integration_tests/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ def _assert_results_fields(self, results, output_path):
assert len(results) == 1
result = results[0]
assert result["codemod"] == self.codemod_wrapper.id
assert result["references"] == self.codemod_wrapper.references

# TODO: once we add description for each url.
for reference in result["references"]:
assert reference["url"] == reference["description"]

assert len(result["changeset"]) == self.num_changed_files

# A codemod may change multiple files. For now we will
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ dependencies = [

[project.scripts]
codemodder = "codemodder.codemodder:main"
generate-docs = 'scripts.generate_docs:main'


[project.entry-points.codemods]
core = "core_codemods:registry"
Expand Down
1 change: 1 addition & 0 deletions src/codemodder/codemods/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def __init_subclass__(cls):
cls.DESCRIPTION, # pylint: disable=no-member
cls.NAME, # pylint: disable=no-member
cls.REVIEW_GUIDANCE, # pylint: disable=no-member
cls.REFERENCES, # pylint: disable=no-member
)

# This is a little bit hacky, but it also feels like the right solution?
Expand Down
18 changes: 17 additions & 1 deletion src/codemodder/codemods/base_codemod.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from typing import List, ClassVar

Expand All @@ -21,6 +21,22 @@ class CodemodMetadata:
DESCRIPTION: str # TODO: this field should be optional
NAME: str
REVIEW_GUIDANCE: ReviewGuidance
REFERENCES: list = field(default_factory=list)

# TODO: remove post_init update_references once we add description for each url.
def __post_init__(self):
object.__setattr__(self, "REFERENCES", self.update_references(self.REFERENCES))

@staticmethod
def update_references(references):
updated_references = []
for reference in references:
updated_reference = dict(
reference
) # Create a copy to avoid modifying the original dict
updated_reference["description"] = updated_reference["url"]
updated_references.append(updated_reference)
return updated_references


class BaseCodemod:
Expand Down
2 changes: 1 addition & 1 deletion src/codemodder/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def compile_results(self, codemods: list[CodemodExecutorWrapper]):
"codemod": codemod.id,
"summary": codemod.summary,
"description": codemod.description,
"references": [],
"references": codemod.references,
"properties": {},
"failedFiles": [],
"changeset": [change.to_json() for change in changeset],
Expand Down
8 changes: 8 additions & 0 deletions src/codemodder/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ def description(self):
# TODO: temporary workaround
return self.METADATA.DESCRIPTION

@property
def review_guidance(self):
return self.METADATA.REVIEW_GUIDANCE.name.replace("_", " ").title()

@property
def references(self):
return self.METADATA.REFERENCES

@property
def yaml_files(self):
return [
Expand Down
2 changes: 1 addition & 1 deletion src/codemodder/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def _validate_codemod(self, codemod):
for k, v in asdict(codemod.METADATA).items():
if v is NotImplemented:
raise NotImplementedError(f"METADATA.{k} not defined for {codemod}")
if not v:
if k != "REFERENCES" and not v:
raise NotImplementedError(
f"METADATA.{k} should not be None or empty for {codemod}"
)
Expand Down
15 changes: 13 additions & 2 deletions src/core_codemods/django_debug_flag_on.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,22 @@

class DjangoDebugFlagOn(SemgrepCodemod, Codemod):
METADATA = CodemodMetadata(
DESCRIPTION=("Flips django's debug flag if on."),
DESCRIPTION="Flip `Django` debug flag to off.",
NAME="django-debug-flag-on",
REVIEW_GUIDANCE=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
REFERENCES=[
{
"url": "https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure",
"description": "",
},
{
"url": "https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-DEBUG",
"description": "",
},
],
)
SUMMARY = CHANGE_DESCRIPTION = "Flip Django debug flag to off"
SUMMARY = "Disable Django Debug Mode"
CHANGE_DESCRIPTION = METADATA.DESCRIPTION
YAML_FILES = [
"django-debug-flag-on.yaml",
]
Expand Down
14 changes: 12 additions & 2 deletions src/core_codemods/django_session_cookie_secure_off.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,19 @@ class DjangoSessionCookieSecureOff(SemgrepCodemod, Codemod):
METADATA = CodemodMetadata(
DESCRIPTION=("Sets Django's `SESSION_COOKIE_SECURE` flag if off or missing."),
NAME="django-session-cookie-secure-off",
REVIEW_GUIDANCE=ReviewGuidance.MERGE_AFTER_REVIEW,
REVIEW_GUIDANCE=ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
REFERENCES=[
{
"url": "https://owasp.org/www-community/controls/SecureCookieAttribute",
"description": "",
},
{
"url": "https://docs.djangoproject.com/en/4.2/ref/settings/#session-cookie-secure",
"description": "",
},
],
)
SUMMARY = "Secure setting for Django `SESSION_COOKIE_SECURE` flag"
SUMMARY = "Secure Setting for Django `SESSION_COOKIE_SECURE` flag"
CHANGE_DESCRIPTION = METADATA.DESCRIPTION
YAML_FILES = [
"detect-django-settings.yaml",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
This codemod enables autoescaping of HTML content in `jinja2`. Unfortunately, the jinja2
default behavior is to not autoescape when rendering templates, which makes your applications
vulnerable to Cross-Site Scripting (XSS) attacks.
potentially vulnerable to Cross-Site Scripting (XSS) attacks.

Our codemod checks if you forgot to enable autoescape or if you explicitly disabled it. The change looks as follows:

Expand All @@ -13,4 +13,3 @@ Our codemod checks if you forgot to enable autoescape or if you explicitly disab
+ env = Environment(autoescape=True, loader=some_loader)
...
```

8 changes: 3 additions & 5 deletions src/core_codemods/docs/pixee_python_jwt-decode-verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ Our change looks as follows:
- decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], verify=False)
+ 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": False, "verify_exp": False})
+ decoded_payload = jwt.decode(
encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True, "verify_exp": True})
- decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": False, "verify_exp": False})
+ decoded_payload = jwt.decode(encoded_jwt, SECRET_KEY, algorithms=["HS256"], options={"verify_signature": True, "verify_exp": True})
```

Any `verify` parameter not listed relies on the secure `True` default value.
Any `verify` parameter not listed relies on the secure `True` default value.
2 changes: 0 additions & 2 deletions src/core_codemods/docs/pixee_python_unused-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,3 @@ import b

b.function()
```

If you have feedback on this codemod, [please let us know](mailto:[email protected])!
13 changes: 10 additions & 3 deletions src/core_codemods/enable_jinja2_autoescape.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@

class EnableJinja2Autoescape(SemgrepCodemod):
NAME = "enable-jinja2-autoescape"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW
SUMMARY = "Enable jinja2 autoescape"
DESCRIPTION = "Makes the `autoescape` parameter to jinja2.Environment be `True`."
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
SUMMARY = "Enable Jinja2 Autoescape"
DESCRIPTION = "Sets the `autoescape` parameter in jinja2.Environment to `True`."
REFERENCES = [
{"url": "https://owasp.org/www-community/attacks/xss/", "description": ""},
{
"url": "https://jinja.palletsprojects.com/en/3.1.x/api/#autoescaping",
"description": "",
},
]

@classmethod
def rule(cls):
Expand Down
4 changes: 2 additions & 2 deletions src/core_codemods/fix_mutable_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class FixMutableParams(BaseCodemod):
NAME = "fix-mutable-params"
SUMMARY = "Replace Mutable Default Parameters"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
DESCRIPTION = "Replace mutable parameter with None"

DESCRIPTION = "Replace mutable parameter with `None`."
REFERENCES: list = []
_BUILTIN_TO_LITERAL = {
"list": cst.List(elements=[]),
"dict": cst.Dict(elements=[]),
Expand Down
8 changes: 7 additions & 1 deletion src/core_codemods/harden_pyyaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
class HardenPyyaml(SemgrepCodemod):
NAME = "harden-pyyaml"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
SUMMARY = "Use SafeLoader when loading YAML"
SUMMARY = "Use SafeLoader in `yaml.load()` Calls"
DESCRIPTION = "Ensures all calls to yaml.load use `SafeLoader`."
REFERENCES = [
{
"url": "https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data",
"description": "",
}
]

@classmethod
def rule(cls):
Expand Down
8 changes: 7 additions & 1 deletion src/core_codemods/harden_ruamel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
class HardenRuamel(SemgrepCodemod):
NAME = "harden-ruamel"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
SUMMARY = "Use safe YAML loading in ruamel.yaml"
SUMMARY = "Use `typ='safe'` in ruamel.yaml() Calls"
DESCRIPTION = "Ensures all unsafe calls to ruamel.yaml.YAML use `typ='safe'`."
REFERENCES = [
{
"url": "https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data",
"description": "",
}
]

@classmethod
def rule(cls):
Expand Down
18 changes: 15 additions & 3 deletions src/core_codemods/https_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,24 @@

class HTTPSConnection(BaseCodemod, Codemod):
METADATA = CodemodMetadata(
DESCRIPTION=("Enforce HTTPS connection"),
DESCRIPTION="Enforce HTTPS connection for `urllib3`.",
NAME="https-connection",
REVIEW_GUIDANCE=ReviewGuidance.MERGE_WITHOUT_REVIEW,
REFERENCES=[
{
"url": "https://owasp.org/www-community/vulnerabilities/Insecure_Transport",
"description": "",
},
{
"url": "https://urllib3.readthedocs.io/en/stable/reference/urllib3.connectionpool.html#urllib3.HTTPConnectionPool",
"description": "",
},
],
)
CHANGE_DESCRIPTION = METADATA.DESCRIPTION
SUMMARY = (
"Changes HTTPConnectionPool to HTTPSConnectionPool to Enforce Secure Connection"
)
CHANGE_DESCRIPTION = "Enforce HTTPS connection"
SUMMARY = "Changes HTTPConnectionPool to HTTPSConnectionPool to enforce secure connection."

METADATA_DEPENDENCIES = (PositionProvider,)

Expand Down
11 changes: 9 additions & 2 deletions src/core_codemods/jwt_decode_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
class JwtDecodeVerify(SemgrepCodemod):
NAME = "jwt-decode-verify"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
SUMMARY = "Enable all verifications in `jwt.decode` call."
DESCRIPTION = "Makes any of the multiple `verify` parameters to a `jwt.decode` call be `True`."
SUMMARY = "Verify JWT Decode"
DESCRIPTION = "Enable all verifications in `jwt.decode` call."
REFERENCES = [
{"url": "https://pyjwt.readthedocs.io/en/stable/api.html", "description": ""},
{
"url": "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens",
"description": "",
},
]

@classmethod
def rule(cls):
Expand Down
5 changes: 4 additions & 1 deletion src/core_codemods/limit_readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
class LimitReadline(SemgrepCodemod):
NAME = "limit-readline"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW
SUMMARY = "Limit the size of readline() calls"
SUMMARY = "Limit readline()"
DESCRIPTION = "Adds a size limit argument to readline() calls."
REFERENCES = [
{"url": "https://cwe.mitre.org/data/definitions/400.html", "description": ""}
]

@classmethod
def rule(cls):
Expand Down
18 changes: 16 additions & 2 deletions src/core_codemods/lxml_safe_parser_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,22 @@
class LxmlSafeParserDefaults(SemgrepCodemod):
NAME = "safe-lxml-parser-defaults"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
SUMMARY = "Use safe defaults for lxml parsers"
DESCRIPTION = "Replace lxml parser parameters with safe defaults"
SUMMARY = "Use Safe Defaults for `lxml` Parsers"
DESCRIPTION = "Replace `lxml` parser parameters with safe defaults."
REFERENCES = [
{
"url": "https://lxml.de/apidoc/lxml.etree.html#lxml.etree.XMLParser",
"description": "",
},
{
"url": "https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing",
"description": "",
},
{
"url": "https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html",
"description": "",
},
]

@classmethod
def rule(cls):
Expand Down
18 changes: 16 additions & 2 deletions src/core_codemods/lxml_safe_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,24 @@
class LxmlSafeParsing(SemgrepCodemod):
NAME = "safe-lxml-parsing"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
SUMMARY = "Use safe parsers in lxml parsing functions"
SUMMARY = "Use Safe Parsers in `lxml` Parsing Functions"
DESCRIPTION = (
"Call `lxml.etree.parse` and `lxml.etree.fromstring` with a safe parser"
"Call `lxml.etree.parse` and `lxml.etree.fromstring` with a safe parser."
)
REFERENCES = [
{
"url": "https://lxml.de/apidoc/lxml.etree.html#lxml.etree.XMLParser",
"description": "",
},
{
"url": "https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing",
"description": "",
},
{
"url": "https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html",
"description": "",
},
]

@classmethod
def rule(cls):
Expand Down
5 changes: 3 additions & 2 deletions src/core_codemods/order_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@

class OrderImports(BaseCodemod, Codemod):
METADATA = CodemodMetadata(
DESCRIPTION=("Formats and order imports by categories"),
DESCRIPTION=("Formats and orders imports by categories."),
NAME="order-imports",
REVIEW_GUIDANCE=ReviewGuidance.MERGE_WITHOUT_REVIEW,
REFERENCES=[],
)
SUMMARY = "Order imports by categories"
SUMMARY = "Order Imports"
CHANGE_DESCRIPTION = "Ordered and formatted import block below this line"

METADATA_DEPENDENCIES = (PositionProvider,)
Expand Down
12 changes: 11 additions & 1 deletion src/core_codemods/process_creation_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@
class ProcessSandbox(SemgrepCodemod):
NAME = "sandbox-process-creation"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW
SUMMARY = "Sandbox process creation"
SUMMARY = "Sandbox Process Creation"
DESCRIPTION = (
"Replaces subprocess.{func} with more secure safe_command library functions."
)
REFERENCES = [
{
"url": "https://github.com/pixee/python-security/blob/main/src/security/safe_command/api.py",
"description": "",
},
{
"url": "https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html",
"description": "",
},
]

@classmethod
def rule(cls):
Expand Down
Loading

0 comments on commit 8128ee6

Please sign in to comment.