diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4af0054 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,42 @@ +name: Lint + +env: + PYTHON_VERSION: "3.12" + +# Report on every PR, including changes outside the Python test paths. +# Required checks need a result even when a PR changes no Python files. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: lint-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + ruff: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + persist-credentials: false + - name: ๐Ÿ setup python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: ๐Ÿ› ๏ธ install deps + run: | + python -m pip install --upgrade pip + pip install uv + uv sync --locked --extra dev + - name: ๐Ÿงน ruff check + run: uv run --locked --extra dev ruff check + - name: ๐ŸŽจ ruff format + run: uv run --locked --extra dev ruff format --check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..27baed3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: local + hooks: + + # Use the project's locked Ruff version, shared with CI and make lint. + - id: ruff-check + name: ruff check + entry: uv run --locked --extra dev ruff check --force-exclude --fix + language: system + types_or: [python, pyi] + require_serial: true + + - id: ruff-format + name: ruff format + entry: uv run --locked --extra dev ruff format --force-exclude + language: system + types_or: [python, pyi] + require_serial: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b2dde4e..cd5395e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). [Network access for Tier 1](docs/parameters.md#network-access-for-tier-1). ### Changed +- Ruff replaces Black and Flake8 for development. Linting and formatting are + enforced on every pull request and push to `main`, with matching pre-commit + hooks and `make lint` / `make format` commands using the locked tool version. +- Functions are limited to complexity 12 and eight arguments. Existing exceptions + are marked individually and obsolete suppressions fail lint checks. Vulnerable + scanner fixtures and the generated CWE catalog are excluded from lint cleanup. +- Release-validation scripts and tests also support Python 3.10 through the + existing TOML compatibility dependency. +- Notification formatters use timezone-aware UTC timestamps while preserving + their trailing-`Z` JSON representation. - The images set `NODE_USE_ENV_PROXY=1`, so Socket Tier 1 honors `HTTPS_PROXY` and `NO_PROXY`. The Socket npm CLI does not apply `HTTPS_PROXY` to its own requests, and Tier 1 behind a proxy failed with `getaddrinfo EAI_AGAIN diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b3abcd7 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: lint lint-fix format format-check hooks test + +lint: + uv run --locked --extra dev ruff check + uv run --locked --extra dev ruff format --check + +lint-fix: + uv run --locked --extra dev ruff check --fix + +format: + uv run --locked --extra dev ruff format + +format-check: + uv run --locked --extra dev ruff format --check + +hooks: + uv run --locked --extra dev pre-commit install + +test: + uv run --locked --extra dev pytest tests/ diff --git a/README.md b/README.md index 8a3cffa..2088e6c 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,7 @@ socket-basics --workspace app_tests/pygoat \ import pytest from socket_basics.module import new_function + def test_new_feature(): result = new_function(input_data) assert result == expected_output diff --git a/docs/github-pr-comment-guide.md b/docs/github-pr-comment-guide.md index ca8d21b..74b57d5 100644 --- a/docs/github-pr-comment-guide.md +++ b/docs/github-pr-comment-guide.md @@ -556,32 +556,37 @@ Socket Basics is designed to support **any security scanner**. To add enhancemen ```python from socket_basics.core.notification import github_pr_helpers as helpers + def format_notifications(data, config=None): # 1. Get feature flags (handles all config sources automatically) flags = helpers.get_feature_flags(config) # 2. Use shared utilities to build your content file_link = helpers.format_file_location_link( - filepath, line_start=line_num, repository=flags['repository'], - commit_hash=flags['commit_hash'], enable_links=flags['enable_links'] + filepath, + line_start=line_num, + repository=flags["repository"], + commit_hash=flags["commit_hash"], + enable_links=flags["enable_links"], ) code_block = helpers.format_code_block( - code_snippet, filepath=filepath, - enable_fencing=flags['enable_code_fencing'] + code_snippet, filepath=filepath, enable_fencing=flags["enable_code_fencing"] ) collapsible = helpers.create_collapsible_section( - title, content, severity_counts, - auto_expand=(critical and not flags['collapse_non_critical']) + title, + content, + severity_counts, + auto_expand=(critical and not flags["collapse_non_critical"]), ) # 3. Wrap in the standard PR comment section (logo, scan link, markers) wrapped = helpers.wrap_pr_comment_section( - 'my-scanner', title, body_content, flags['full_scan_url'] + "my-scanner", title, body_content, flags["full_scan_url"] ) - return [{'title': title, 'content': wrapped}] + return [{"title": title, "content": wrapped}] ``` **That's it!** Your tool instantly gets: diff --git a/pyproject.toml b/pyproject.toml index f0f5575..7a5f306 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ dependencies = [ dev = [ "pytest>=9.1.1", "pytest-cov>=7.1.0", - "black>=26.5.1", - "flake8>=5.0.0", + "ruff==0.17.4", + "pre-commit==4.6.2", ] [project.urls] @@ -69,12 +69,180 @@ include = [ [dependency-groups] dev = [] -[tool.black] -line-length = 100 -target-version = ["py39"] - [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] + +[tool.ruff] +# The formatter owns physical layout. E501 and W291/W293 are unselected: +# whitespace inside help text, log messages, and Markdown strings is content. +target-version = "py310" +line-length = 100 +# Scanner targets intentionally contain vulnerable code; the CWE table is generated. +extend-exclude = ["app_tests", "tests/fixtures", "socket_basics/core/connector/opengrep/cwe_catalog.py"] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +[tool.ruff.lint] +select = [ + # --- Baseline correctness ------------------------------------------- + "E4", # pycodestyle: import formatting + "E7", # pycodestyle: statement-level errors + "E9", # pycodestyle: syntax/IO errors + "W605", # invalid escape sequence in a string literal + "F", # Pyflakes: undefined names, unused imports, star imports + "I", # isort: deterministic import order + "PLE", # Pylint errors: unambiguous bugs, never style + + # --- Complexity guardrails ------------------------------------------ + "C901", # McCabe cyclomatic complexity + "PLR0913", # too many arguments in a function definition + + # --- Bug classes that matter for a CLI shipped to other people ------- + "ASYNC", # blocking calls inside async functions + "B", # bugbear: loop-variable capture, missing `raise ... from` + "C4", # comprehension misuse + "DTZ", # naive datetimes: scan timestamps must be tz-aware + "EXE", # shebang / executable-bit mismatches in a packaged wheel + "FLY", # static str.join() that should be an f-string + "FURB", # modern-Python refactors + "ICN", # conventional import aliases + "INT", # gettext f-string traps + "LOG", # logging misuse + "PERF", # avoidable per-iteration work + "PIE", # dead code and redundant constructs + "PLW", # Pylint warnings: unchecked subprocess, loop-var rebinding + "RET", # unreachable or redundant return flow + "RSE", # raise Exception() -- not raise Exception + "RUF", # Ruff-native checks (incl. RUF100, see below) + "S", # bandit: request timeouts, unsafe subprocess, weak randomness + "SIM", # simplifiable branches and context managers + "SLOT", # __slots__ on subclasses of str/tuple/namedtuple + "T10", # leftover breakpoint()/pdb.set_trace() reaching a release + "T20", # stray print(): stdout is a contract for a CLI, not a debug log + "TID", # relative imports + "UP", # pyupgrade: the floor is Python 3.11 + "YTT", # incorrect sys.version comparisons + + # Individually selected because their siblings are style opinions + # (PLR2004 magic values, PLC0415 deliberate lazy imports, TRY003 + # message length) that would cost more than they catch here. + "PLR1714", # repeated equality comparison -> `in` + "PLR1722", # `exit()` -> `sys.exit()`: `exit` only exists when `site` ran + "PLR1730", # if-statement that should be min()/max() + "PLR5501", # else-if that should be elif + "TRY201", # `raise` instead of re-raising the bound name + + # --- Keeping the suppressions honest -------------------------------- + "PGH", # no blanket `# noqa` / `# type: ignore` -- codes required + "RUF100", # unused `# noqa` -- makes the grandfather list self-cleaning +] + +ignore = [ + # Keep expected failures readable without requiring tracebacks. + "TRY400", + "TRY401", + "SIM108", # retain comments on if/else branches + "PERF401", # retain readable loops for multi-line dictionary construction + "S603", # external tools use argument lists; shell=True remains checked + "S607", # external tools intentionally resolve through PATH + "TID252", # package-local relative imports are intentional + "S110", # optional finding metadata may be malformed; skip without logging payloads + "S112", # skip unsupported or malformed findings during best-effort parsing + "PERF203", # recover from individual finding parse errors inside scanning loops +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# The information glyph is deliberate UI in the PR-comment markup. +allowed-confusables = ["โ„น"] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.mccabe] +# Existing over-limit functions have explicit suppressions. Split new functions +# that exceed the limit; RUF100 rejects suppressions that become unnecessary. +max-complexity = 12 + +[tool.ruff.lint.pylint] +# Allow ordinary CLI constructors while bounding argument-heavy interfaces. +max-args = 8 + +[tool.ruff.lint.per-file-ignores] +# Tests use assertions, placeholder credentials, and fixed fixture paths. +"tests/**" = [ + "S101", # asserts are the point + "S105", # throwaway credentials in fixtures + "S106", + "S108", # fixed temp paths in fixtures + "T201", # diagnostic prints; pytest captures stdout anyway + "RUF012", # ClassVar annotations on test-local constants are noise + "S311", # deterministic fuzz seeds are not credentials +] + +# Developer tooling, not shipped in the wheel. stdout is their entire interface. +".hooks/**" = ["T201", "S310"] # fetches a hardcoded PyPI JSON endpoint +"scripts/**" = ["T201"] +"socket_basics/core/notification/console.py" = ["T201"] +"socket_basics/core/notification/json_notifier.py" = ["T201"] + +[tool.ruff.lint.isort] +known-first-party = ["socket_basics"] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/scripts/check_core_tools.py b/scripts/check_core_tools.py old mode 100644 new mode 100755 index 57f035d..4dc6d80 --- a/scripts/check_core_tools.py +++ b/scripts/check_core_tools.py @@ -56,9 +56,10 @@ import re import sys import urllib.request +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any REPO_ROOT = Path(__file__).resolve().parent.parent # The three published/test images pin core tools independently, so scoring must @@ -122,14 +123,14 @@ class Tool: # Dockerfiles / uv.lock; no leading-v normalization -- as written). read_pinned: Callable[[], list[str]] # Returns the latest upstream version tag (as published). - discover_latest: Callable[[], Optional[str]] + discover_latest: Callable[[], str | None] # Builds a Socket PURL for a given version string. purl: Callable[[str], str] note: str = "" # Optional fallback coordinate scored when `purl` has no Socket coverage # (e.g. pkg:github). Returns a fully-formed PURL string (or None). Used for # reporting only -- a proxy is never build-failing. - proxy_purl: Callable[[], Optional[str]] | None = None + proxy_purl: Callable[[], str | None] | None = None proxy_label: str = "" # Whether Socket is expected to have data for this tool's primary PURL. # When True, a pinned version with no matching analysis row is treated as @@ -139,51 +140,51 @@ class Tool: # and the proxy provides report-only signal instead. socket_coverage: bool = True pinned: list[str] = field(default_factory=list) - latest: Optional[str] = None - resolved_proxy_purl: Optional[str] = None + latest: str | None = None + resolved_proxy_purl: str | None = None analyses: dict[str, dict[str, Any]] = field(default_factory=dict) # โ”€โ”€ HTTP helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -def _get_json(url: str, token: Optional[str] = None) -> Any: - req = urllib.request.Request(url, headers={"User-Agent": "socket-basics-core-tool-watch"}) +def _get_json(url: str, token: str | None = None) -> Any: + req = urllib.request.Request(url, headers={"User-Agent": "socket-basics-core-tool-watch"}) # noqa: S310 # explicit metadata URL if token: req.add_header("Authorization", f"Bearer {token}") - with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 (trusted hosts) + with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 # explicit metadata source URL return json.loads(resp.read().decode("utf-8")) -def _github_latest_release(repo: str) -> Optional[str]: +def _github_latest_release(repo: str) -> str | None: token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") try: data = _get_json(f"https://api.github.com/repos/{repo}/releases/latest", token) return data.get("tag_name") - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f" ! GitHub latest-release lookup failed for {repo}: {exc}", file=sys.stderr) return None -def _pypi_latest(package: str) -> Optional[str]: +def _pypi_latest(package: str) -> str | None: try: data = _get_json(f"https://pypi.org/pypi/{package}/json") return data.get("info", {}).get("version") - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f" ! PyPI latest lookup failed for {package}: {exc}", file=sys.stderr) return None -def _npm_latest(package: str) -> Optional[str]: +def _npm_latest(package: str) -> str | None: try: data = _get_json(f"https://registry.npmjs.org/{package}/latest") return data.get("version") - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f" ! npm latest lookup failed for {package}: {exc}", file=sys.stderr) return None -def _ghcr_latest(org: str, package: str) -> Optional[str]: +def _ghcr_latest(org: str, package: str) -> str | None: """Newest stable semver tag on an organization-owned GHCR package.""" token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") if not token: @@ -194,8 +195,7 @@ def _ghcr_latest(org: str, package: str) -> Optional[str]: return None try: versions = _get_json( - f"https://api.github.com/orgs/{org}/packages/container/{package}/versions" - "?per_page=100", + f"https://api.github.com/orgs/{org}/packages/container/{package}/versions?per_page=100", token, ) tags = [ @@ -210,12 +210,12 @@ def _ghcr_latest(org: str, package: str) -> Optional[str]: if match: stable_versions.append((tuple(int(g or 0) for g in match.groups()), tag)) return max(stable_versions)[1] if stable_versions else None - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f" ! GHCR latest-version lookup failed for {org}/{package}: {exc}", file=sys.stderr) return None -def _pypi_purl(package: str) -> Optional[str]: +def _pypi_purl(package: str) -> str | None: """Latest-version PyPI PURL for a package, or None if discovery fails.""" v = _pypi_latest(package) return f"pkg:pypi/{package}@{v}" if v else None @@ -304,7 +304,7 @@ def _read_locked_versions(package: str) -> list[str]: def _strip_v(v: str) -> str: - return v[1:] if v.startswith("v") else v + return v.removeprefix("v") def _ensure_v(v: str) -> str: @@ -356,7 +356,9 @@ def build_tools() -> list[Tool]: # The Socket distribution is rebuilt from unmodified upstream # source. Score that Go module while release discovery follows the # Socket-controlled artifact that Basics actually consumes. - purl=lambda v: f"pkg:golang/github.com/aquasecurity/trivy@{_ensure_v(_strip_socket_rebuild(v))}", + purl=lambda v: ( + f"pkg:golang/github.com/aquasecurity/trivy@{_ensure_v(_strip_socket_rebuild(v))}" + ), note="Release drift follows the Socket-built ghcr.io/socketdev/trivy package " "(produced by SocketDev/trivy-dist and mirrored privately to Docker Hub), not " "Aqua's release feed. Socket scoring uses the corresponding upstream Go module " @@ -514,7 +516,7 @@ def _match_analysis(analyses: dict[str, dict[str, Any]], purl: str) -> dict[str, # โ”€โ”€ report rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -def render_markdown(tools: list[Tool], token_present: bool, discovery_complete: bool = True) -> str: +def render_markdown(tools: list[Tool], token_present: bool, discovery_complete: bool = True) -> str: # noqa: C901 lines: list[str] = [] lines.append("## Core tool supply-chain watch\n") if not discovery_complete: @@ -536,17 +538,17 @@ def render_markdown(tools: list[Tool], token_present: bool, discovery_complete: current = all(_strip_v(p) == _strip_v(t.latest) for p in t.pinned) drift = "โœ… current" if current else f"โฌ†๏ธ `{t.latest}`" - def verdict(version: Optional[str]) -> str: + def verdict(version: str | None, tool=t) -> str: if not version: return "โ€”" if not token_present: return "skipped" - a = _match_analysis(t.analyses, t.purl(version)) + a = _match_analysis(tool.analyses, tool.purl(version)) suffix = "" - if not a and t.resolved_proxy_purl: - a = _match_analysis(t.analyses, t.resolved_proxy_purl) + if not a and tool.resolved_proxy_purl: + a = _match_analysis(tool.analyses, tool.resolved_proxy_purl) if a: - suffix = f" _(via {t.proxy_label})_" + suffix = f" _(via {tool.proxy_label})_" if not a: return "no data" if a.get("status") == "pending": @@ -581,7 +583,7 @@ def verdict(version: Optional[str]) -> str: # โ”€โ”€ main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -def main() -> int: +def main() -> int: # noqa: C901 parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--mode", choices=["build", "watch"], default="watch") parser.add_argument( @@ -636,7 +638,7 @@ def main() -> int: print(f"== Scoring {len(purls)} PURLs through Socket ==") try: analyses = analyze_purls(purls, token) - except Exception as exc: # noqa: BLE001 + except Exception as exc: print(f"! Socket analysis failed: {exc}", file=sys.stderr) scoring_error = True for t in tools: diff --git a/scripts/check_release_docs.py b/scripts/check_release_docs.py old mode 100644 new mode 100755 index f90b5a6..67f106c --- a/scripts/check_release_docs.py +++ b/scripts/check_release_docs.py @@ -15,10 +15,14 @@ import argparse import re import sys -import tomllib from dataclasses import dataclass from pathlib import Path +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib + REPO_ROOT = Path(__file__).resolve().parent.parent PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" DOCKERFILE_PATH = REPO_ROOT / "Dockerfile" @@ -212,13 +216,17 @@ def main() -> int: mismatches = check_docs(version) if mismatches: - print(f"Documentation version references are out of sync (release {version}, Dockerfile tool pins):") + print( + f"Documentation version references are out of sync (release {version}, Dockerfile tool pins):" + ) for mismatch in mismatches: print(f" - {mismatch}") print("Run: python3 scripts/check_release_docs.py --write") return 1 - print(f"Documentation version references are in sync: release {version}, tool pins {read_tool_pins()}") + print( + f"Documentation version references are in sync: release {version}, tool pins {read_tool_pins()}" + ) return 0 diff --git a/scripts/enrich_rules.py b/scripts/enrich_rules.py old mode 100644 new mode 100755 index 0cd3cda..a42ca89 --- a/scripts/enrich_rules.py +++ b/scripts/enrich_rules.py @@ -18,6 +18,7 @@ import os import re import sys + import yaml # --------------------------------------------------------------------------- @@ -26,48 +27,89 @@ CWE_TO_SUBCATEGORY = { # injection - "CWE-78": "injection", "CWE-79": "xss", "CWE-89": "injection", - "CWE-90": "injection", "CWE-91": "injection", "CWE-94": "injection", - "CWE-95": "injection", "CWE-98": "injection", "CWE-74": "injection", - "CWE-88": "injection", "CWE-134": "injection", "CWE-943": "injection", - "CWE-915": "injection", "CWE-1321": "injection", "CWE-1336": "injection", - "CWE-470": "injection", "CWE-611": "injection", "CWE-117": "injection", + "CWE-78": "injection", + "CWE-79": "xss", + "CWE-89": "injection", + "CWE-90": "injection", + "CWE-91": "injection", + "CWE-94": "injection", + "CWE-95": "injection", + "CWE-98": "injection", + "CWE-74": "injection", + "CWE-88": "injection", + "CWE-134": "injection", + "CWE-943": "injection", + "CWE-1321": "injection", + "CWE-1336": "injection", + "CWE-470": "injection", + "CWE-611": "injection", + "CWE-117": "injection", # crypto - "CWE-208": "crypto", "CWE-295": "crypto", "CWE-310": "crypto", - "CWE-319": "crypto", "CWE-322": "crypto", "CWE-326": "crypto", - "CWE-327": "crypto", "CWE-330": "crypto", "CWE-338": "crypto", + "CWE-208": "crypto", + "CWE-295": "crypto", + "CWE-310": "crypto", + "CWE-319": "crypto", + "CWE-322": "crypto", + "CWE-326": "crypto", + "CWE-327": "crypto", + "CWE-330": "crypto", + "CWE-338": "crypto", "CWE-347": "crypto", # authentication - "CWE-259": "authentication", "CWE-287": "authentication", - "CWE-384": "authentication", "CWE-521": "authentication", - "CWE-798": "authentication", "CWE-916": "authentication", + "CWE-259": "authentication", + "CWE-287": "authentication", + "CWE-384": "authentication", + "CWE-521": "authentication", + "CWE-798": "authentication", + "CWE-916": "authentication", # access-control - "CWE-22": "access-control", "CWE-73": "access-control", - "CWE-601": "access-control", "CWE-639": "access-control", - "CWE-862": "access-control", "CWE-863": "access-control", + "CWE-22": "access-control", + "CWE-73": "access-control", + "CWE-601": "access-control", + "CWE-639": "access-control", + "CWE-862": "access-control", + "CWE-863": "access-control", "CWE-915": "access-control", # mass assignment could go here too # configuration - "CWE-16": "configuration", "CWE-200": "configuration", - "CWE-209": "configuration", "CWE-250": "configuration", - "CWE-276": "configuration", "CWE-489": "configuration", - "CWE-614": "configuration", "CWE-693": "configuration", - "CWE-732": "configuration", "CWE-926": "configuration", - "CWE-942": "configuration", "CWE-477": "configuration", - "CWE-1104": "configuration", "CWE-1059": "configuration", + "CWE-16": "configuration", + "CWE-200": "configuration", + "CWE-209": "configuration", + "CWE-250": "configuration", + "CWE-276": "configuration", + "CWE-489": "configuration", + "CWE-614": "configuration", + "CWE-693": "configuration", + "CWE-732": "configuration", + "CWE-926": "configuration", + "CWE-942": "configuration", + "CWE-477": "configuration", + "CWE-1104": "configuration", + "CWE-1059": "configuration", # integrity - "CWE-345": "integrity", "CWE-353": "integrity", - "CWE-494": "integrity", "CWE-502": "integrity", + "CWE-345": "integrity", + "CWE-353": "integrity", + "CWE-494": "integrity", + "CWE-502": "integrity", # logging - "CWE-532": "logging", "CWE-778": "logging", + "CWE-532": "logging", + "CWE-778": "logging", # error-handling - "CWE-248": "error-handling", "CWE-396": "error-handling", - "CWE-703": "error-handling", "CWE-755": "error-handling", + "CWE-248": "error-handling", + "CWE-396": "error-handling", + "CWE-703": "error-handling", + "CWE-755": "error-handling", # design - "CWE-20": "design", "CWE-307": "design", "CWE-362": "design", - "CWE-367": "design", "CWE-667": "design", "CWE-697": "design", + "CWE-20": "design", + "CWE-307": "design", + "CWE-362": "design", + "CWE-367": "design", + "CWE-667": "design", + "CWE-697": "design", "CWE-704": "design", # dos - "CWE-400": "dos", "CWE-409": "dos", "CWE-1333": "dos", + "CWE-400": "dos", + "CWE-409": "dos", + "CWE-1333": "dos", # file-operations "CWE-377": "file-operations", # upload @@ -75,12 +117,19 @@ # ssrf "CWE-918": "ssrf", # deprecated (memory safety for C/C++) - "CWE-119": "deprecated", "CWE-120": "deprecated", "CWE-131": "deprecated", - "CWE-190": "deprecated", "CWE-242": "deprecated", "CWE-401": "deprecated", - "CWE-415": "deprecated", "CWE-416": "deprecated", "CWE-476": "deprecated", + "CWE-119": "deprecated", + "CWE-120": "deprecated", + "CWE-131": "deprecated", + "CWE-190": "deprecated", + "CWE-242": "deprecated", + "CWE-401": "deprecated", + "CWE-415": "deprecated", + "CWE-416": "deprecated", + "CWE-476": "deprecated", "CWE-479": "deprecated", # misc - "CWE-312": "crypto", "CWE-522": "crypto", + "CWE-312": "crypto", + "CWE-522": "crypto", "CWE-352": "access-control", } @@ -109,19 +158,230 @@ CWE_TO_OWASP = { # A01:2021 โ€” Broken Access Control - **{f"CWE-{n}": "A01:2021" for n in [22, 23, 35, 59, 200, 219, 264, 275, 276, 284, 285, 352, 359, 377, 402, 425, 441, 497, 538, 540, 548, 552, 566, 601, 639, 651, 668, 706, 862, 863, 913, 922, 1275]}, + **{ + f"CWE-{n}": "A01:2021" + for n in [ + 22, + 23, + 35, + 59, + 200, + 219, + 264, + 275, + 276, + 284, + 285, + 352, + 359, + 377, + 402, + 425, + 441, + 497, + 538, + 540, + 548, + 552, + 566, + 601, + 639, + 651, + 668, + 706, + 862, + 863, + 913, + 922, + 1275, + ] + }, # A02:2021 โ€” Cryptographic Failures - **{f"CWE-{n}": "A02:2021" for n in [261, 296, 310, 319, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 335, 336, 337, 338, 340, 347, 523, 720, 757, 759, 760, 780, 798, 916]}, + **{ + f"CWE-{n}": "A02:2021" + for n in [ + 261, + 296, + 310, + 319, + 321, + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329, + 330, + 331, + 335, + 336, + 337, + 338, + 340, + 347, + 523, + 720, + 757, + 759, + 760, + 780, + 798, + 916, + ] + }, # A03:2021 โ€” Injection - **{f"CWE-{n}": "A03:2021" for n in [20, 74, 75, 77, 78, 79, 80, 83, 87, 88, 89, 90, 91, 93, 94, 95, 96, 97, 98, 99, 100, 113, 116, 117, 134, 138, 184, 470, 471, 564, 610, 643, 644, 652, 917, 943, 1236, 1321, 1336]}, + **{ + f"CWE-{n}": "A03:2021" + for n in [ + 20, + 74, + 75, + 77, + 78, + 79, + 80, + 83, + 87, + 88, + 89, + 90, + 91, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 113, + 116, + 117, + 134, + 138, + 184, + 470, + 471, + 564, + 610, + 643, + 644, + 652, + 917, + 943, + 1236, + 1321, + 1336, + ] + }, # A04:2021 โ€” Insecure Design - **{f"CWE-{n}": "A04:2021" for n in [73, 183, 209, 213, 235, 256, 257, 266, 269, 280, 311, 312, 313, 316, 419, 430, 434, 444, 451, 472, 501, 522, 525, 539, 579, 598, 602, 642, 646, 650, 653, 656, 657, 799, 807, 840, 841, 927, 1021, 1173]}, + **{ + f"CWE-{n}": "A04:2021" + for n in [ + 73, + 183, + 209, + 213, + 235, + 256, + 257, + 266, + 269, + 280, + 311, + 312, + 313, + 316, + 419, + 430, + 434, + 444, + 451, + 472, + 501, + 522, + 525, + 539, + 579, + 598, + 602, + 642, + 646, + 650, + 653, + 656, + 657, + 799, + 807, + 840, + 841, + 927, + 1021, + 1173, + ] + }, # A05:2021 โ€” Security Misconfiguration - **{f"CWE-{n}": "A05:2021" for n in [2, 11, 13, 15, 16, 260, 315, 489, 497, 520, 526, 537, 541, 547, 611, 614, 693, 732, 756, 776, 942, 1004, 1032, 1174]}, + **{ + f"CWE-{n}": "A05:2021" + for n in [ + 2, + 11, + 13, + 15, + 16, + 260, + 315, + 489, + 497, + 520, + 526, + 537, + 541, + 547, + 611, + 614, + 693, + 732, + 756, + 776, + 942, + 1004, + 1032, + 1174, + ] + }, # A06:2021 โ€” Vulnerable and Outdated Components **{f"CWE-{n}": "A06:2021" for n in [477, 1104, 1059]}, # A07:2021 โ€” Auth Failures - **{f"CWE-{n}": "A07:2021" for n in [255, 259, 287, 288, 290, 294, 295, 297, 300, 302, 304, 306, 307, 346, 384, 521, 613, 620, 640, 798, 940, 1216]}, + **{ + f"CWE-{n}": "A07:2021" + for n in [ + 255, + 259, + 287, + 288, + 290, + 294, + 295, + 297, + 300, + 302, + 304, + 306, + 307, + 346, + 384, + 521, + 613, + 620, + 640, + 798, + 940, + 1216, + ] + }, # A08:2021 โ€” Data Integrity Failures **{f"CWE-{n}": "A08:2021" for n in [345, 353, 426, 494, 502, 565, 784, 829, 830, 915]}, # A09:2021 โ€” Logging Failures @@ -164,10 +424,12 @@ def parse_metadata_from_yaml(filepath): rules = [] for rule in data.get("rules", []): meta = rule.get("metadata", {}) - rules.append({ - "id": rule.get("id", ""), - "metadata": meta, - }) + rules.append( + { + "id": rule.get("id", ""), + "metadata": meta, + } + ) return rules @@ -220,9 +482,8 @@ def find_metadata_blocks(content): else: break continue - else: - # Line doesn't match field pattern -- end of metadata block - break + # Line doesn't match field pattern -- end of metadata block + break blocks.append((start_line, j, meta_indent, fields)) i = j else: @@ -242,25 +503,27 @@ def enrich_file(filepath, stats): blocks = find_metadata_blocks(content) if len(blocks) != len(rules_meta): - print(f"WARNING: {filepath}: found {len(blocks)} metadata blocks but " - f"{len(rules_meta)} rules. Skipping file.") + print( + f"WARNING: {filepath}: found {len(blocks)} metadata blocks but " + f"{len(rules_meta)} rules. Skipping file." + ) return content lines = content.split("\n") insertions = [] # (line_number, lines_to_insert) - for idx, (start_line, end_line, meta_indent, existing_fields) in enumerate(blocks): + for idx, (_start_line, end_line, meta_indent, existing_fields) in enumerate(blocks): rule = rules_meta[idx] meta = rule["metadata"] - rule_id = rule["id"] + rule["id"] field_indent = meta_indent + " " new_lines = [] cwe = str(meta.get("cwe", "")) existing_subcategory = meta.get("subcategory") - existing_owasp = meta.get("owasp") - existing_vuln_class = meta.get("vulnerability_class") - existing_references = meta.get("references") + meta.get("owasp") + meta.get("vulnerability_class") + meta.get("references") existing_framework = meta.get("framework") # 1. Add subcategory if missing @@ -276,18 +539,21 @@ def enrich_file(filepath, stats): sc = subcategory or existing_subcategory if sc and sc in SUBCATEGORY_TO_VULN_CLASS: vc = SUBCATEGORY_TO_VULN_CLASS[sc] - new_lines.append(f"{field_indent}vulnerability_class: \"{vc}\"") + new_lines.append(f'{field_indent}vulnerability_class: "{vc}"') stats["vulnerability_class_added"] += 1 # 3. Add owasp if missing if "owasp" not in existing_fields and cwe in CWE_TO_OWASP: owasp = CWE_TO_OWASP[cwe] - new_lines.append(f"{field_indent}owasp: \"{owasp}\"") + new_lines.append(f'{field_indent}owasp: "{owasp}"') stats["owasp_added"] += 1 # 4. Add references for framework rules if missing - if ("references" not in existing_fields and existing_framework - and str(existing_framework) in FRAMEWORK_REFS): + if ( + "references" not in existing_fields + and existing_framework + and str(existing_framework) in FRAMEWORK_REFS + ): refs = FRAMEWORK_REFS[str(existing_framework)] new_lines.append(f"{field_indent}references:") for ref in refs: @@ -310,8 +576,9 @@ def enrich_file(filepath, stats): def main(): - rules_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "socket_basics", "rules") + rules_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "socket_basics", "rules" + ) if not os.path.isdir(rules_dir): print(f"ERROR: Rules directory not found: {rules_dir}") @@ -340,9 +607,9 @@ def main(): stats["files_processed"] += 1 print(f" Processed {fname} ({len(rules_meta)} rules)") - print(f"\n{'='*60}") - print(f"Enrichment complete!") - print(f"{'='*60}") + print(f"\n{'=' * 60}") + print("Enrichment complete!") + print(f"{'=' * 60}") print(f"Files processed: {stats['files_processed']}") print(f"Rules processed: {stats['rules_processed']}") print(f"subcategory added: {stats['subcategory_added']}") @@ -351,7 +618,7 @@ def main(): print(f"references added: {stats['references_added']}") # Verify all files still parse - print(f"\nVerifying YAML syntax...") + print("\nVerifying YAML syntax...") errors = 0 for fname in yml_files: filepath = os.path.join(rules_dir, fname) @@ -372,7 +639,7 @@ def main(): print(f"All {len(yml_files)} files parse successfully.") # Count final field coverage - print(f"\nFinal field coverage:") + print("\nFinal field coverage:") total_rules = 0 field_counts = { "subcategory": 0, @@ -393,7 +660,7 @@ def main(): print(f" Total rules: {total_rules}") for field, count in field_counts.items(): - print(f" {field}: {count}/{total_rules} ({count*100//total_rules}%)") + print(f" {field}: {count}/{total_rules} ({count * 100 // total_rules}%)") if __name__ == "__main__": diff --git a/scripts/prep_release.py b/scripts/prep_release.py index d02b010..78bea5c 100755 --- a/scripts/prep_release.py +++ b/scripts/prep_release.py @@ -23,10 +23,11 @@ python scripts/prep_release.py --version 2.2.0 --date 2026-07-29 python scripts/prep_release.py --version 2.2.0 --dry-run """ + from __future__ import annotations import argparse -import datetime +import datetime as dt import re import subprocess import sys @@ -40,10 +41,12 @@ # pyproject.toml is the canonical version source; version.py, __init__.py, and # action.yml are derived from it by scripts/sync_release_version.py. -PYPROJECT_PATTERN = re.compile(r'^version = "(\d+\.\d+\.\d+)"$', re.M) +PYPROJECT_PATTERN = re.compile(r'^version = "(\d+\.\d+\.\d+)"$', re.MULTILINE) -def _bump_file(path: Path, pattern: re.Pattern[str], replacement: str, version: str) -> tuple[str, str]: +def _bump_file( + path: Path, pattern: re.Pattern[str], replacement: str, version: str +) -> tuple[str, str]: """Return (old_version, new_content) without writing.""" content = path.read_text() match = pattern.search(content) @@ -52,7 +55,9 @@ def _bump_file(path: Path, pattern: re.Pattern[str], replacement: str, version: old = match.group(match.lastindex or 0) if match.lastindex else match.group(1) new_content, count = pattern.subn(replacement.format(v=version), content, count=1) if count != 1: - sys.exit(f"error: expected exactly one version in {path.relative_to(ROOT)}, replaced {count}") + sys.exit( + f"error: expected exactly one version in {path.relative_to(ROOT)}, replaced {count}" + ) return old, new_content @@ -63,17 +68,19 @@ def _stamp_changelog(version: str, date: str) -> str: if f"## [{version}]" in content: sys.exit(f"error: CHANGELOG.md already has a [{version}] section") - match = re.search(r"^## \[Unreleased\]\n(.*?)(?=^## \[)", content, re.M | re.S) + match = re.search(r"^## \[Unreleased\]\n(.*?)(?=^## \[)", content, re.MULTILINE | re.DOTALL) if not match: sys.exit("error: could not find an [Unreleased] section followed by a release section") body = match.group(1).strip("\n") if not body.strip(): - sys.exit("error: [Unreleased] is empty โ€” nothing to release. " - "Feature PRs should add their entries there before release prep.") + sys.exit( + "error: [Unreleased] is empty โ€” nothing to release. " + "Feature PRs should add their entries there before release prep." + ) stamped = f"## [Unreleased]\n\n## [{version}] - {date}\n\n{body}\n\n" - return content[:match.start()] + stamped + content[match.end():] + return content[: match.start()] + stamped + content[match.end() :] def _refresh_lock(dry_run: bool) -> None: @@ -102,24 +109,38 @@ def _sync_release_docs(version: str, dry_run: bool) -> None: def main() -> None: - parser = argparse.ArgumentParser(description="Prepare version bumps and changelog for a release PR.") - parser.add_argument("--version", required=True, help="Release version without v prefix, e.g. 2.2.0") - parser.add_argument("--date", default=datetime.date.today().isoformat(), - help="Release date for the changelog section (default: today)") + parser = argparse.ArgumentParser( + description="Prepare version bumps and changelog for a release PR." + ) + parser.add_argument( + "--version", required=True, help="Release version without v prefix, e.g. 2.2.0" + ) + parser.add_argument( + "--date", + default=dt.date.today().isoformat(), # noqa: DTZ011 # release date uses the local calendar + help="Release date for the changelog section (default: today)", + ) parser.add_argument("--dry-run", action="store_true", help="Report changes without writing") args = parser.parse_args() if not SEMVER_RE.match(args.version): sys.exit(f"error: version must be X.Y.Z (got {args.version!r})") - tags = subprocess.run(["git", "tag", "--list", f"v{args.version}", args.version], - cwd=ROOT, capture_output=True, text=True).stdout.split() + tags = subprocess.run( + ["git", "tag", "--list", f"v{args.version}", args.version], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ).stdout.split() if tags: sys.exit(f"error: tag for {args.version} already exists: {', '.join(tags)}") # Validate and render every change first; only write once all succeed, # so a failure never leaves a half-modified tree. - old, pyproject_content = _bump_file(PYPROJECT, PYPROJECT_PATTERN, 'version = "{v}"', args.version) + old, pyproject_content = _bump_file( + PYPROJECT, PYPROJECT_PATTERN, 'version = "{v}"', args.version + ) changelog_content = _stamp_changelog(args.version, args.date) if not args.dry_run: @@ -129,8 +150,11 @@ def main() -> None: if args.dry_run: print("dry-run: skipping sync_release_version.py --write") else: - subprocess.run([sys.executable, str(ROOT / "scripts" / "sync_release_version.py"), "--write"], - cwd=ROOT, check=True) + subprocess.run( + [sys.executable, str(ROOT / "scripts" / "sync_release_version.py"), "--write"], + cwd=ROOT, + check=True, + ) _sync_release_docs(args.version, args.dry_run) diff --git a/scripts/preview_pr_comments.py b/scripts/preview_pr_comments.py old mode 100644 new mode 100755 index edb761a..ec4f87a --- a/scripts/preview_pr_comments.py +++ b/scripts/preview_pr_comments.py @@ -28,8 +28,8 @@ class MockConfig(dict): def __getattr__(self, key): try: return self[key] - except KeyError: - raise AttributeError(key) + except KeyError as error: + raise AttributeError(key) from error def make_mock_config( @@ -131,7 +131,7 @@ def make_mock_config( "ruleId": "sql-injection-extra", "startLine": 31, "endLine": 33, - "codeSnippet": 'query = f"SELECT * FROM users WHERE name = \'{request.GET[\'name\']}\'" \ncursor.execute(query)', + "codeSnippet": "query = f\"SELECT * FROM users WHERE name = '{request.GET['name']}'\" \ncursor.execute(query)", }, }, }, @@ -340,26 +340,35 @@ def _trivy_dockerfile_fixture(): def run_opengrep(config): from socket_basics.core.connector.opengrep.github_pr import format_notifications + return format_notifications(OPENGREP_FIXTURES, config=config) def run_trivy_image(config): from socket_basics.core.connector.trivy.github_pr import format_notifications - return format_notifications(_trivy_image_fixture(), item_name="example-app:latest", scan_type="image", config=config) + + return format_notifications( + _trivy_image_fixture(), item_name="example-app:latest", scan_type="image", config=config + ) def run_trivy_dockerfile(config): from socket_basics.core.connector.trivy.github_pr import format_notifications - return format_notifications(_trivy_dockerfile_fixture(), item_name="Dockerfile", scan_type="dockerfile", config=config) + + return format_notifications( + _trivy_dockerfile_fixture(), item_name="Dockerfile", scan_type="dockerfile", config=config + ) def run_trufflehog(config): from socket_basics.core.connector.trufflehog.github_pr import format_notifications + return format_notifications(TRUFFLEHOG_FIXTURES, config=config) def run_tier1(config): from socket_basics.core.connector.socket_tier1.github_pr import format_notifications + return format_notifications(TIER1_FIXTURES, config=config) @@ -382,7 +391,7 @@ def main(): parser.add_argument( "--scanner", default="all", - choices=list(SCANNERS.keys()) + ["all"], + choices=[*list(SCANNERS.keys()), "all"], help="Which scanner to preview (default: all)", ) args = parser.parse_args() diff --git a/scripts/rewrite_messages.py b/scripts/rewrite_messages.py old mode 100644 new mode 100755 index 6523947..6723666 --- a/scripts/rewrite_messages.py +++ b/scripts/rewrite_messages.py @@ -16,6 +16,7 @@ import os import re import sys + import yaml # --------------------------------------------------------------------------- @@ -122,292 +123,870 @@ FIX_BY_CWE_LANG = { # SQL Injection (CWE-89) - ("CWE-89", "python"): "Use parameterized queries: cursor.execute('SELECT * FROM t WHERE id = %s', (user_id,)). For Django, use the ORM or QuerySet API. For SQLAlchemy, use bound parameters.", - ("CWE-89", "java"): "Use PreparedStatement with parameter binding: ps = conn.prepareStatement('SELECT * FROM t WHERE id = ?'); ps.setString(1, userId);", - ("CWE-89", "javascript_typescript"): "Use parameterized queries with your database driver, e.g., db.query('SELECT * FROM t WHERE id = $1', [userId]) for pg, or use an ORM like Prisma or Sequelize.", - ("CWE-89", "go"): "Use parameterized queries: db.Query('SELECT * FROM t WHERE id = ?', userId). Never concatenate user input into SQL strings.", - ("CWE-89", "php"): "Use PDO prepared statements: $stmt = $pdo->prepare('SELECT * FROM t WHERE id = ?'); $stmt->execute([$userId]);", - ("CWE-89", "ruby"): "Use parameterized queries with ActiveRecord: User.where('id = ?', user_id) or use the ORM query interface.", - ("CWE-89", "dotnet"): "Use parameterized queries with SqlCommand: cmd.Parameters.AddWithValue(\"@id\", userId); or use Entity Framework LINQ queries.", - ("CWE-89", "kotlin"): "Use PreparedStatement with parameter binding or use an ORM like Exposed or Room with parameterized queries.", - ("CWE-89", "scala"): "Use Slick's type-safe query DSL or JDBC PreparedStatement with parameter binding.", - ("CWE-89", "elixir"): "Use Ecto parameterized queries: Repo.all(from u in User, where: u.id == ^user_id). Never interpolate user input into raw SQL.", - ("CWE-89", "erlang"): "Use parameterized queries with your database driver, e.g., epgsql:equery(C, \"SELECT * FROM t WHERE id = $1\", [UserId]).", - ("CWE-89", "rust"): "Use parameterized queries with sqlx: sqlx::query('SELECT * FROM t WHERE id = $1').bind(user_id). Never format user input into SQL strings.", - ("CWE-89", "c_cpp"): "Use parameterized queries with your database API (e.g., sqlite3_bind_text for SQLite, PQexecParams for PostgreSQL). Never use sprintf to build SQL.", - ("CWE-89", "objective-c"): "Use parameterized queries with sqlite3_bind_text() or NSPredicate with substitution variables. Never concatenate user input into SQL strings.", - ("CWE-89", "swift"): "Use parameterized queries with sqlite3_bind_text() or a Swift ORM like GRDB with parameterized statements.", - + ( + "CWE-89", + "python", + ): "Use parameterized queries: cursor.execute('SELECT * FROM t WHERE id = %s', (user_id,)). For Django, use the ORM or QuerySet API. For SQLAlchemy, use bound parameters.", + ( + "CWE-89", + "java", + ): "Use PreparedStatement with parameter binding: ps = conn.prepareStatement('SELECT * FROM t WHERE id = ?'); ps.setString(1, userId);", + ( + "CWE-89", + "javascript_typescript", + ): "Use parameterized queries with your database driver, e.g., db.query('SELECT * FROM t WHERE id = $1', [userId]) for pg, or use an ORM like Prisma or Sequelize.", + ( + "CWE-89", + "go", + ): "Use parameterized queries: db.Query('SELECT * FROM t WHERE id = ?', userId). Never concatenate user input into SQL strings.", + ( + "CWE-89", + "php", + ): "Use PDO prepared statements: $stmt = $pdo->prepare('SELECT * FROM t WHERE id = ?'); $stmt->execute([$userId]);", + ( + "CWE-89", + "ruby", + ): "Use parameterized queries with ActiveRecord: User.where('id = ?', user_id) or use the ORM query interface.", + ( + "CWE-89", + "dotnet", + ): 'Use parameterized queries with SqlCommand: cmd.Parameters.AddWithValue("@id", userId); or use Entity Framework LINQ queries.', + ( + "CWE-89", + "kotlin", + ): "Use PreparedStatement with parameter binding or use an ORM like Exposed or Room with parameterized queries.", + ( + "CWE-89", + "scala", + ): "Use Slick's type-safe query DSL or JDBC PreparedStatement with parameter binding.", + ( + "CWE-89", + "elixir", + ): "Use Ecto parameterized queries: Repo.all(from u in User, where: u.id == ^user_id). Never interpolate user input into raw SQL.", + ( + "CWE-89", + "erlang", + ): 'Use parameterized queries with your database driver, e.g., epgsql:equery(C, "SELECT * FROM t WHERE id = $1", [UserId]).', + ( + "CWE-89", + "rust", + ): "Use parameterized queries with sqlx: sqlx::query('SELECT * FROM t WHERE id = $1').bind(user_id). Never format user input into SQL strings.", + ( + "CWE-89", + "c_cpp", + ): "Use parameterized queries with your database API (e.g., sqlite3_bind_text for SQLite, PQexecParams for PostgreSQL). Never use sprintf to build SQL.", + ( + "CWE-89", + "objective-c", + ): "Use parameterized queries with sqlite3_bind_text() or NSPredicate with substitution variables. Never concatenate user input into SQL strings.", + ( + "CWE-89", + "swift", + ): "Use parameterized queries with sqlite3_bind_text() or a Swift ORM like GRDB with parameterized statements.", # Command Injection (CWE-78) - ("CWE-78", "python"): "Use subprocess.run() with a list of arguments instead of shell=True: subprocess.run(['cmd', arg1, arg2]). Never pass user input to os.system() or shell commands.", - ("CWE-78", "java"): "Use ProcessBuilder with separate arguments: new ProcessBuilder('cmd', arg1, arg2). Never concatenate user input into Runtime.exec() strings.", - ("CWE-78", "javascript_typescript"): "Use child_process.execFile() or spawn() with argument arrays instead of exec(). Never interpolate user input into shell commands.", - ("CWE-78", "go"): "Use exec.Command() with separate arguments: exec.Command('cmd', arg1, arg2). Never pass user input to exec.Command('sh', '-c', userInput).", - ("CWE-78", "ruby"): "Use system() with separate arguments: system('cmd', arg1, arg2). Avoid backticks or %x{} with user input.", - ("CWE-78", "php"): "Use escapeshellarg() for arguments and escapeshellcmd() for commands. Prefer language-level APIs over shell execution.", - ("CWE-78", "dotnet"): "Use Process.Start() with Arguments set separately. Never concatenate user input into process arguments.", - ("CWE-78", "kotlin"): "Use ProcessBuilder with separate arguments: ProcessBuilder('cmd', arg1, arg2). Never concatenate user input into command strings.", - ("CWE-78", "scala"): "Use ProcessBuilder or scala.sys.process with separate arguments. Never interpolate user input into shell command strings.", - ("CWE-78", "elixir"): "Use System.cmd/2 with separate arguments: System.cmd(\"cmd\", [arg1, arg2]). Never pass user input to :os.cmd/1.", - ("CWE-78", "erlang"): "Use erlang:open_port with {spawn_executable, Cmd} and {args, Args} instead of os:cmd/1. Never interpolate user input into commands.", - ("CWE-78", "rust"): "Use std::process::Command with separate arguments: Command::new('cmd').arg(arg1).arg(arg2). Never pass user input to shell commands.", - ("CWE-78", "c_cpp"): "Use execve() or posix_spawn() with separate argument arrays. Never pass user input to system() or popen().", - ("CWE-78", "objective-c"): "Use NSTask with launchPath and arguments array. Never pass user input to system() or popen().", - ("CWE-78", "swift"): "Use Process (NSTask) with separate arguments array. Never pass user input to system() or shell commands.", - + ( + "CWE-78", + "python", + ): "Use subprocess.run() with a list of arguments instead of shell=True: subprocess.run(['cmd', arg1, arg2]). Never pass user input to os.system() or shell commands.", + ( + "CWE-78", + "java", + ): "Use ProcessBuilder with separate arguments: new ProcessBuilder('cmd', arg1, arg2). Never concatenate user input into Runtime.exec() strings.", + ( + "CWE-78", + "javascript_typescript", + ): "Use child_process.execFile() or spawn() with argument arrays instead of exec(). Never interpolate user input into shell commands.", + ( + "CWE-78", + "go", + ): "Use exec.Command() with separate arguments: exec.Command('cmd', arg1, arg2). Never pass user input to exec.Command('sh', '-c', userInput).", + ( + "CWE-78", + "ruby", + ): "Use system() with separate arguments: system('cmd', arg1, arg2). Avoid backticks or %x{} with user input.", + ( + "CWE-78", + "php", + ): "Use escapeshellarg() for arguments and escapeshellcmd() for commands. Prefer language-level APIs over shell execution.", + ( + "CWE-78", + "dotnet", + ): "Use Process.Start() with Arguments set separately. Never concatenate user input into process arguments.", + ( + "CWE-78", + "kotlin", + ): "Use ProcessBuilder with separate arguments: ProcessBuilder('cmd', arg1, arg2). Never concatenate user input into command strings.", + ( + "CWE-78", + "scala", + ): "Use ProcessBuilder or scala.sys.process with separate arguments. Never interpolate user input into shell command strings.", + ( + "CWE-78", + "elixir", + ): 'Use System.cmd/2 with separate arguments: System.cmd("cmd", [arg1, arg2]). Never pass user input to :os.cmd/1.', + ( + "CWE-78", + "erlang", + ): "Use erlang:open_port with {spawn_executable, Cmd} and {args, Args} instead of os:cmd/1. Never interpolate user input into commands.", + ( + "CWE-78", + "rust", + ): "Use std::process::Command with separate arguments: Command::new('cmd').arg(arg1).arg(arg2). Never pass user input to shell commands.", + ( + "CWE-78", + "c_cpp", + ): "Use execve() or posix_spawn() with separate argument arrays. Never pass user input to system() or popen().", + ( + "CWE-78", + "objective-c", + ): "Use NSTask with launchPath and arguments array. Never pass user input to system() or popen().", + ( + "CWE-78", + "swift", + ): "Use Process (NSTask) with separate arguments array. Never pass user input to system() or shell commands.", # XSS (CWE-79) - ("CWE-79", "python"): "Use template engine auto-escaping (Jinja2 autoescape=True, Django default). Use markupsafe.escape() for manual escaping. Never render raw user input with |safe or Markup().", - ("CWE-79", "java"): "Use context-aware output encoding (OWASP Java Encoder: Encode.forHtml()). Enable auto-escaping in templates (Thymeleaf, JSP with JSTL).", - ("CWE-79", "javascript_typescript"): "Use textContent instead of innerHTML. Use DOMPurify.sanitize() if HTML must be rendered. Enable auto-escaping in template engines (React JSX, Handlebars).", - ("CWE-79", "php"): "Use htmlspecialchars($input, ENT_QUOTES, 'UTF-8') for output. Enable auto-escaping in Twig/Blade templates. Never echo raw user input.", - ("CWE-79", "ruby"): "Use ERB auto-escaping (<%= %>) or sanitize helper. Never use raw() or html_safe on user input without sanitization.", - ("CWE-79", "dotnet"): "Use Razor auto-encoding or HtmlEncoder.Default.Encode(). Never use Html.Raw() with user input. Validate input on both client and server.", - ("CWE-79", "kotlin"): "Use context-aware output encoding. Enable auto-escaping in template engines. Use OWASP Java Encoder for manual escaping.", - ("CWE-79", "scala"): "Use Play framework's Twirl templates (auto-escaped by default). Use Html() only for trusted content. Sanitize user input before rendering.", - ("CWE-79", "elixir"): "Phoenix templates auto-escape by default. Never use raw/1 or {:safe, ...} with user input. Use Phoenix.HTML.html_escape/1 for manual escaping.", - ("CWE-79", "erlang"): "Escape all user input before inserting into HTML output. Use a templating library with auto-escaping enabled.", - ("CWE-79", "objective-c"): "Escape user input before rendering in web views. Use NSString methods to encode HTML entities. Avoid loading untrusted HTML in WKWebView.", - ("CWE-79", "swift"): "Escape user input before rendering in web views. Use String extension to encode HTML entities. Set WKWebView configuration to restrict JavaScript.", - + ( + "CWE-79", + "python", + ): "Use template engine auto-escaping (Jinja2 autoescape=True, Django default). Use markupsafe.escape() for manual escaping. Never render raw user input with |safe or Markup().", + ( + "CWE-79", + "java", + ): "Use context-aware output encoding (OWASP Java Encoder: Encode.forHtml()). Enable auto-escaping in templates (Thymeleaf, JSP with JSTL).", + ( + "CWE-79", + "javascript_typescript", + ): "Use textContent instead of innerHTML. Use DOMPurify.sanitize() if HTML must be rendered. Enable auto-escaping in template engines (React JSX, Handlebars).", + ( + "CWE-79", + "php", + ): "Use htmlspecialchars($input, ENT_QUOTES, 'UTF-8') for output. Enable auto-escaping in Twig/Blade templates. Never echo raw user input.", + ( + "CWE-79", + "ruby", + ): "Use ERB auto-escaping (<%= %>) or sanitize helper. Never use raw() or html_safe on user input without sanitization.", + ( + "CWE-79", + "dotnet", + ): "Use Razor auto-encoding or HtmlEncoder.Default.Encode(). Never use Html.Raw() with user input. Validate input on both client and server.", + ( + "CWE-79", + "kotlin", + ): "Use context-aware output encoding. Enable auto-escaping in template engines. Use OWASP Java Encoder for manual escaping.", + ( + "CWE-79", + "scala", + ): "Use Play framework's Twirl templates (auto-escaped by default). Use Html() only for trusted content. Sanitize user input before rendering.", + ( + "CWE-79", + "elixir", + ): "Phoenix templates auto-escape by default. Never use raw/1 or {:safe, ...} with user input. Use Phoenix.HTML.html_escape/1 for manual escaping.", + ( + "CWE-79", + "erlang", + ): "Escape all user input before inserting into HTML output. Use a templating library with auto-escaping enabled.", + ( + "CWE-79", + "objective-c", + ): "Escape user input before rendering in web views. Use NSString methods to encode HTML entities. Avoid loading untrusted HTML in WKWebView.", + ( + "CWE-79", + "swift", + ): "Escape user input before rendering in web views. Use String extension to encode HTML entities. Set WKWebView configuration to restrict JavaScript.", # Path Traversal (CWE-22) - ("CWE-22", "python"): "Use os.path.abspath() and verify the result starts with your allowed base directory. Use pathlib.Path.resolve() for canonicalization.", - ("CWE-22", "java"): "Use File.getCanonicalPath() and verify the result starts with the allowed base directory. Use java.nio.file.Path.normalize() and resolve().", - ("CWE-22", "javascript_typescript"): "Use path.resolve() and verify the result starts with the allowed base directory using path.relative() to check for '..' traversal.", - ("CWE-22", "go"): "Use filepath.Clean() and filepath.Abs(), then verify the result is within the allowed base directory with strings.HasPrefix().", - ("CWE-22", "php"): "Use realpath() and verify the result starts with the allowed base directory. Reject paths containing '..' sequences.", - ("CWE-22", "ruby"): "Use File.expand_path() and verify the result starts with the allowed base directory. Use Pathname#cleanpath for normalization.", - ("CWE-22", "dotnet"): "Use Path.GetFullPath() and verify the result starts with the allowed base directory. Use Path.Combine() instead of string concatenation.", - ("CWE-22", "kotlin"): "Use File.canonicalPath and verify it starts with the allowed base directory. Use java.nio.file.Path.normalize().", - ("CWE-22", "scala"): "Use java.io.File.getCanonicalPath() and verify it starts with the allowed base directory. Use java.nio.file.Path.normalize().", - ("CWE-22", "elixir"): "Use Path.expand/1 and verify the result starts with the allowed base directory. Reject paths containing '..' components.", - ("CWE-22", "erlang"): "Use filename:absname/1 and verify the result starts with the allowed base directory. Reject paths containing '..' components.", - ("CWE-22", "rust"): "Use std::fs::canonicalize() and verify the result starts with the allowed base directory. Use Path::starts_with() for validation.", - ("CWE-22", "objective-c"): "Use -[NSString stringByStandardizingPath] and verify the result starts with the allowed base directory. Reject '..' path components.", - ("CWE-22", "swift"): "Use URL.standardizedFileURL or (path as NSString).standardizingPath and verify the result is within the allowed base directory.", - + ( + "CWE-22", + "python", + ): "Use os.path.abspath() and verify the result starts with your allowed base directory. Use pathlib.Path.resolve() for canonicalization.", + ( + "CWE-22", + "java", + ): "Use File.getCanonicalPath() and verify the result starts with the allowed base directory. Use java.nio.file.Path.normalize() and resolve().", + ( + "CWE-22", + "javascript_typescript", + ): "Use path.resolve() and verify the result starts with the allowed base directory using path.relative() to check for '..' traversal.", + ( + "CWE-22", + "go", + ): "Use filepath.Clean() and filepath.Abs(), then verify the result is within the allowed base directory with strings.HasPrefix().", + ( + "CWE-22", + "php", + ): "Use realpath() and verify the result starts with the allowed base directory. Reject paths containing '..' sequences.", + ( + "CWE-22", + "ruby", + ): "Use File.expand_path() and verify the result starts with the allowed base directory. Use Pathname#cleanpath for normalization.", + ( + "CWE-22", + "dotnet", + ): "Use Path.GetFullPath() and verify the result starts with the allowed base directory. Use Path.Combine() instead of string concatenation.", + ( + "CWE-22", + "kotlin", + ): "Use File.canonicalPath and verify it starts with the allowed base directory. Use java.nio.file.Path.normalize().", + ( + "CWE-22", + "scala", + ): "Use java.io.File.getCanonicalPath() and verify it starts with the allowed base directory. Use java.nio.file.Path.normalize().", + ( + "CWE-22", + "elixir", + ): "Use Path.expand/1 and verify the result starts with the allowed base directory. Reject paths containing '..' components.", + ( + "CWE-22", + "erlang", + ): "Use filename:absname/1 and verify the result starts with the allowed base directory. Reject paths containing '..' components.", + ( + "CWE-22", + "rust", + ): "Use std::fs::canonicalize() and verify the result starts with the allowed base directory. Use Path::starts_with() for validation.", + ( + "CWE-22", + "objective-c", + ): "Use -[NSString stringByStandardizingPath] and verify the result starts with the allowed base directory. Reject '..' path components.", + ( + "CWE-22", + "swift", + ): "Use URL.standardizedFileURL or (path as NSString).standardizingPath and verify the result is within the allowed base directory.", # Unsafe Deserialization (CWE-502) - ("CWE-502", "python"): "Replace pickle/shelve with json.loads() for data interchange. If pickle is required, use hmac to verify data integrity before deserializing.", - ("CWE-502", "java"): "Use ObjectInputFilter (JEP 290) to restrict deserializable classes. Prefer JSON (Jackson/Gson) or Protocol Buffers for data interchange.", - ("CWE-502", "javascript_typescript"): "Avoid eval() or Function() for deserialization. Use JSON.parse() with a reviver function to validate types.", - ("CWE-502", "php"): "Replace unserialize() with json_decode(). If unserialize() is required, use the allowed_classes option to restrict types.", - ("CWE-502", "ruby"): "Replace Marshal.load/YAML.load with JSON.parse or YAML.safe_load. Never deserialize untrusted data with Marshal.", - ("CWE-502", "dotnet"): "Use System.Text.Json or Newtonsoft.Json instead of BinaryFormatter/SoapFormatter. Set TypeNameHandling.None in Newtonsoft.Json.", - ("CWE-502", "kotlin"): "Use kotlinx.serialization with JSON format. Avoid Java ObjectInputStream for untrusted data. Use ObjectInputFilter if needed.", - ("CWE-502", "scala"): "Use circe, play-json, or upickle for JSON deserialization. Avoid Java ObjectInputStream for untrusted data.", - ("CWE-502", "elixir"): "Use Jason.decode/1 or :erlang.binary_to_term/2 with [:safe] option. Never use :erlang.binary_to_term/1 on untrusted data.", - ("CWE-502", "erlang"): "Use binary_to_term/2 with [safe] option. Use jsx or jiffy for JSON deserialization. Never use binary_to_term/1 on untrusted data.", - ("CWE-502", "rust"): "Use serde with JSON/MessagePack instead of bincode for untrusted data. Validate deserialized data before use.", - ("CWE-502", "objective-c"): "Use NSJSONSerialization instead of NSKeyedUnarchiver for untrusted data. Use NSSecureCoding with allowedClasses for type validation.", - ("CWE-502", "swift"): "Use JSONDecoder with Codable instead of NSKeyedUnarchiver. Use NSSecureCoding with unarchivedObject(ofClass:from:) for type-safe unarchiving.", - + ( + "CWE-502", + "python", + ): "Replace pickle/shelve with json.loads() for data interchange. If pickle is required, use hmac to verify data integrity before deserializing.", + ( + "CWE-502", + "java", + ): "Use ObjectInputFilter (JEP 290) to restrict deserializable classes. Prefer JSON (Jackson/Gson) or Protocol Buffers for data interchange.", + ( + "CWE-502", + "javascript_typescript", + ): "Avoid eval() or Function() for deserialization. Use JSON.parse() with a reviver function to validate types.", + ( + "CWE-502", + "php", + ): "Replace unserialize() with json_decode(). If unserialize() is required, use the allowed_classes option to restrict types.", + ( + "CWE-502", + "ruby", + ): "Replace Marshal.load/YAML.load with JSON.parse or YAML.safe_load. Never deserialize untrusted data with Marshal.", + ( + "CWE-502", + "dotnet", + ): "Use System.Text.Json or Newtonsoft.Json instead of BinaryFormatter/SoapFormatter. Set TypeNameHandling.None in Newtonsoft.Json.", + ( + "CWE-502", + "kotlin", + ): "Use kotlinx.serialization with JSON format. Avoid Java ObjectInputStream for untrusted data. Use ObjectInputFilter if needed.", + ( + "CWE-502", + "scala", + ): "Use circe, play-json, or upickle for JSON deserialization. Avoid Java ObjectInputStream for untrusted data.", + ( + "CWE-502", + "elixir", + ): "Use Jason.decode/1 or :erlang.binary_to_term/2 with [:safe] option. Never use :erlang.binary_to_term/1 on untrusted data.", + ( + "CWE-502", + "erlang", + ): "Use binary_to_term/2 with [safe] option. Use jsx or jiffy for JSON deserialization. Never use binary_to_term/1 on untrusted data.", + ( + "CWE-502", + "rust", + ): "Use serde with JSON/MessagePack instead of bincode for untrusted data. Validate deserialized data before use.", + ( + "CWE-502", + "objective-c", + ): "Use NSJSONSerialization instead of NSKeyedUnarchiver for untrusted data. Use NSSecureCoding with allowedClasses for type validation.", + ( + "CWE-502", + "swift", + ): "Use JSONDecoder with Codable instead of NSKeyedUnarchiver. Use NSSecureCoding with unarchivedObject(ofClass:from:) for type-safe unarchiving.", # Hard-coded credentials (CWE-798) - ("CWE-798", "python"): "Store secrets in environment variables (os.environ['KEY']) or use a secrets manager (AWS Secrets Manager, HashiCorp Vault, python-dotenv).", - ("CWE-798", "java"): "Store secrets in environment variables (System.getenv('KEY')), a secrets manager, or externalized configuration (Spring Vault, AWS Secrets Manager).", - ("CWE-798", "javascript_typescript"): "Store secrets in environment variables (process.env.KEY) or use a secrets manager. Use dotenv for local development.", - ("CWE-798", "go"): "Store secrets in environment variables (os.Getenv('KEY')) or use a secrets manager (HashiCorp Vault, AWS Secrets Manager).", - ("CWE-798", "php"): "Store secrets in environment variables (getenv('KEY')) or use a secrets manager. Use vlucas/phpdotenv for local development.", - ("CWE-798", "ruby"): "Store secrets in environment variables (ENV['KEY']) or use Rails credentials (config/credentials.yml.enc) or a secrets manager.", - ("CWE-798", "dotnet"): "Store secrets in environment variables, Azure Key Vault, or user-secrets for development. Use IConfiguration to access secrets.", - ("CWE-798", "kotlin"): "Store secrets in environment variables (System.getenv('KEY')) or use Android Keystore / a secrets manager. Never commit secrets to source control.", - ("CWE-798", "scala"): "Store secrets in environment variables or use a secrets manager. Use Typesafe Config with environment variable substitution.", - ("CWE-798", "elixir"): "Store secrets in environment variables (System.get_env/1) or use runtime configuration. Use config/runtime.exs for production secrets.", - ("CWE-798", "erlang"): "Store secrets in environment variables (os:getenv/1) or use a secrets manager. Load secrets from configuration files excluded from version control.", - ("CWE-798", "rust"): "Store secrets in environment variables (std::env::var('KEY')) or use a secrets manager. Use dotenvy for local development.", - ("CWE-798", "c_cpp"): "Store secrets in environment variables (getenv('KEY')) or read from a protected configuration file. Never embed secrets in source code.", - ("CWE-798", "objective-c"): "Store secrets in the iOS Keychain or environment variables. Never embed secrets in source code or property lists.", - ("CWE-798", "swift"): "Store secrets in the iOS Keychain or environment variables. Use a secrets manager for server-side Swift. Never embed secrets in source code.", - + ( + "CWE-798", + "python", + ): "Store secrets in environment variables (os.environ['KEY']) or use a secrets manager (AWS Secrets Manager, HashiCorp Vault, python-dotenv).", + ( + "CWE-798", + "java", + ): "Store secrets in environment variables (System.getenv('KEY')), a secrets manager, or externalized configuration (Spring Vault, AWS Secrets Manager).", + ( + "CWE-798", + "javascript_typescript", + ): "Store secrets in environment variables (process.env.KEY) or use a secrets manager. Use dotenv for local development.", + ( + "CWE-798", + "go", + ): "Store secrets in environment variables (os.Getenv('KEY')) or use a secrets manager (HashiCorp Vault, AWS Secrets Manager).", + ( + "CWE-798", + "php", + ): "Store secrets in environment variables (getenv('KEY')) or use a secrets manager. Use vlucas/phpdotenv for local development.", + ( + "CWE-798", + "ruby", + ): "Store secrets in environment variables (ENV['KEY']) or use Rails credentials (config/credentials.yml.enc) or a secrets manager.", + ( + "CWE-798", + "dotnet", + ): "Store secrets in environment variables, Azure Key Vault, or user-secrets for development. Use IConfiguration to access secrets.", + ( + "CWE-798", + "kotlin", + ): "Store secrets in environment variables (System.getenv('KEY')) or use Android Keystore / a secrets manager. Never commit secrets to source control.", + ( + "CWE-798", + "scala", + ): "Store secrets in environment variables or use a secrets manager. Use Typesafe Config with environment variable substitution.", + ( + "CWE-798", + "elixir", + ): "Store secrets in environment variables (System.get_env/1) or use runtime configuration. Use config/runtime.exs for production secrets.", + ( + "CWE-798", + "erlang", + ): "Store secrets in environment variables (os:getenv/1) or use a secrets manager. Load secrets from configuration files excluded from version control.", + ( + "CWE-798", + "rust", + ): "Store secrets in environment variables (std::env::var('KEY')) or use a secrets manager. Use dotenvy for local development.", + ( + "CWE-798", + "c_cpp", + ): "Store secrets in environment variables (getenv('KEY')) or read from a protected configuration file. Never embed secrets in source code.", + ( + "CWE-798", + "objective-c", + ): "Store secrets in the iOS Keychain or environment variables. Never embed secrets in source code or property lists.", + ( + "CWE-798", + "swift", + ): "Store secrets in the iOS Keychain or environment variables. Use a secrets manager for server-side Swift. Never embed secrets in source code.", # Weak crypto (CWE-327) - ("CWE-327", "python"): "Use hashlib.sha256() or hashlib.sha3_256() instead of md5/sha1. Use cryptography library with AES-GCM or ChaCha20-Poly1305 for encryption.", - ("CWE-327", "java"): "Use MessageDigest.getInstance('SHA-256') instead of MD5/SHA1. Use AES/GCM/NoPadding for encryption. Use Cipher from javax.crypto with strong algorithms.", - ("CWE-327", "javascript_typescript"): "Use crypto.createHash('sha256') instead of md5/sha1. Use crypto.createCipheriv('aes-256-gcm', ...) for encryption.", - ("CWE-327", "go"): "Use crypto/sha256 instead of crypto/md5 or crypto/sha1. Use crypto/aes with GCM mode for encryption.", - ("CWE-327", "ruby"): "Use OpenSSL::Digest::SHA256 instead of MD5/SHA1. Use OpenSSL::Cipher.new('aes-256-gcm') for encryption.", - ("CWE-327", "dotnet"): "Use SHA256.Create() instead of MD5/SHA1. Use Aes.Create() with CipherMode.CBC or AesGcm for encryption.", - ("CWE-327", "kotlin"): "Use MessageDigest.getInstance('SHA-256') instead of MD5/SHA1. Use Cipher with 'AES/GCM/NoPadding' for encryption.", - ("CWE-327", "scala"): "Use MessageDigest.getInstance('SHA-256') instead of MD5/SHA1. Use javax.crypto.Cipher with AES-GCM for encryption.", - ("CWE-327", "elixir"): "Use :crypto.hash(:sha256, data) instead of :md5/:sha. Use :crypto.crypto_one_time_aead for AES-GCM encryption.", - ("CWE-327", "erlang"): "Use crypto:hash(sha256, Data) instead of md5/sha. Use crypto:crypto_one_time_aead for AES-GCM encryption.", - ("CWE-327", "rust"): "Use sha2 crate (Sha256::digest) instead of md5. Use aes-gcm crate for authenticated encryption.", - ("CWE-327", "c_cpp"): "Use SHA-256 (e.g., EVP_sha256() from OpenSSL) instead of MD5/SHA-1. Use AES-GCM for authenticated encryption.", - ("CWE-327", "objective-c"): "Use CC_SHA256 from CommonCrypto instead of CC_MD5/CC_SHA1. Use CCCrypt with kCCAlgorithmAES for encryption.", - ("CWE-327", "swift"): "Use SHA256 from CryptoKit instead of Insecure.MD5/SHA1. Use AES.GCM.seal() for authenticated encryption.", - + ( + "CWE-327", + "python", + ): "Use hashlib.sha256() or hashlib.sha3_256() instead of md5/sha1. Use cryptography library with AES-GCM or ChaCha20-Poly1305 for encryption.", + ( + "CWE-327", + "java", + ): "Use MessageDigest.getInstance('SHA-256') instead of MD5/SHA1. Use AES/GCM/NoPadding for encryption. Use Cipher from javax.crypto with strong algorithms.", + ( + "CWE-327", + "javascript_typescript", + ): "Use crypto.createHash('sha256') instead of md5/sha1. Use crypto.createCipheriv('aes-256-gcm', ...) for encryption.", + ( + "CWE-327", + "go", + ): "Use crypto/sha256 instead of crypto/md5 or crypto/sha1. Use crypto/aes with GCM mode for encryption.", + ( + "CWE-327", + "ruby", + ): "Use OpenSSL::Digest::SHA256 instead of MD5/SHA1. Use OpenSSL::Cipher.new('aes-256-gcm') for encryption.", + ( + "CWE-327", + "dotnet", + ): "Use SHA256.Create() instead of MD5/SHA1. Use Aes.Create() with CipherMode.CBC or AesGcm for encryption.", + ( + "CWE-327", + "kotlin", + ): "Use MessageDigest.getInstance('SHA-256') instead of MD5/SHA1. Use Cipher with 'AES/GCM/NoPadding' for encryption.", + ( + "CWE-327", + "scala", + ): "Use MessageDigest.getInstance('SHA-256') instead of MD5/SHA1. Use javax.crypto.Cipher with AES-GCM for encryption.", + ( + "CWE-327", + "elixir", + ): "Use :crypto.hash(:sha256, data) instead of :md5/:sha. Use :crypto.crypto_one_time_aead for AES-GCM encryption.", + ( + "CWE-327", + "erlang", + ): "Use crypto:hash(sha256, Data) instead of md5/sha. Use crypto:crypto_one_time_aead for AES-GCM encryption.", + ( + "CWE-327", + "rust", + ): "Use sha2 crate (Sha256::digest) instead of md5. Use aes-gcm crate for authenticated encryption.", + ( + "CWE-327", + "c_cpp", + ): "Use SHA-256 (e.g., EVP_sha256() from OpenSSL) instead of MD5/SHA-1. Use AES-GCM for authenticated encryption.", + ( + "CWE-327", + "objective-c", + ): "Use CC_SHA256 from CommonCrypto instead of CC_MD5/CC_SHA1. Use CCCrypt with kCCAlgorithmAES for encryption.", + ( + "CWE-327", + "swift", + ): "Use SHA256 from CryptoKit instead of Insecure.MD5/SHA1. Use AES.GCM.seal() for authenticated encryption.", # TLS/SSL bypass (CWE-295) - ("CWE-295", "python"): "Always use verify=True (the default) in requests. Set ssl_context properly. Never set CERT_NONE or disable hostname checking.", - ("CWE-295", "java"): "Never override TrustManager to accept all certificates. Use the default SSLContext or configure with trusted CA certificates only.", - ("CWE-295", "javascript_typescript"): "Never set rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Configure proper CA certificates for custom TLS needs.", - ("CWE-295", "go"): "Never set InsecureSkipVerify: true in tls.Config. Use the default TLS configuration which validates certificates properly.", - ("CWE-295", "ruby"): "Never set verify_mode = OpenSSL::SSL::VERIFY_NONE. Use the default certificate validation in Net::HTTP and OpenSSL.", - ("CWE-295", "dotnet"): "Never return true from ServerCertificateCustomValidationCallback. Use the default certificate validation from ServicePointManager.", - ("CWE-295", "kotlin"): "Never override TrustManager to accept all certificates. Use the default SSLContext or OkHttp's CertificatePinner for pinning.", - ("CWE-295", "scala"): "Never override TrustManager to accept all certificates. Use the default SSLContext or configure trusted CA certificates.", - ("CWE-295", "rust"): "Never use danger_accept_invalid_certs(true) in reqwest or rustls. Use the default TLS verification with proper CA certificates.", - ("CWE-295", "c_cpp"): "Always call SSL_CTX_set_verify with SSL_VERIFY_PEER. Never skip certificate or hostname verification in OpenSSL/BoringSSL.", - ("CWE-295", "objective-c"): "Never override NSURLSession delegate to accept invalid certificates. Use App Transport Security (ATS) defaults.", - ("CWE-295", "swift"): "Never override URLSession delegate to accept invalid certificates. Use App Transport Security (ATS) defaults.", - + ( + "CWE-295", + "python", + ): "Always use verify=True (the default) in requests. Set ssl_context properly. Never set CERT_NONE or disable hostname checking.", + ( + "CWE-295", + "java", + ): "Never override TrustManager to accept all certificates. Use the default SSLContext or configure with trusted CA certificates only.", + ( + "CWE-295", + "javascript_typescript", + ): "Never set rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. Configure proper CA certificates for custom TLS needs.", + ( + "CWE-295", + "go", + ): "Never set InsecureSkipVerify: true in tls.Config. Use the default TLS configuration which validates certificates properly.", + ( + "CWE-295", + "ruby", + ): "Never set verify_mode = OpenSSL::SSL::VERIFY_NONE. Use the default certificate validation in Net::HTTP and OpenSSL.", + ( + "CWE-295", + "dotnet", + ): "Never return true from ServerCertificateCustomValidationCallback. Use the default certificate validation from ServicePointManager.", + ( + "CWE-295", + "kotlin", + ): "Never override TrustManager to accept all certificates. Use the default SSLContext or OkHttp's CertificatePinner for pinning.", + ( + "CWE-295", + "scala", + ): "Never override TrustManager to accept all certificates. Use the default SSLContext or configure trusted CA certificates.", + ( + "CWE-295", + "rust", + ): "Never use danger_accept_invalid_certs(true) in reqwest or rustls. Use the default TLS verification with proper CA certificates.", + ( + "CWE-295", + "c_cpp", + ): "Always call SSL_CTX_set_verify with SSL_VERIFY_PEER. Never skip certificate or hostname verification in OpenSSL/BoringSSL.", + ( + "CWE-295", + "objective-c", + ): "Never override NSURLSession delegate to accept invalid certificates. Use App Transport Security (ATS) defaults.", + ( + "CWE-295", + "swift", + ): "Never override URLSession delegate to accept invalid certificates. Use App Transport Security (ATS) defaults.", # Code injection (CWE-94) - ("CWE-94", "python"): "Remove eval()/exec() calls with user input. Use ast.literal_eval() for safe parsing of Python literals. Use a sandboxed environment if dynamic execution is required.", - ("CWE-94", "java"): "Avoid ScriptEngine.eval() with user input. Use a sandboxed interpreter or template engine. Restrict class loading with a SecurityManager.", - ("CWE-94", "javascript_typescript"): "Remove eval(), Function(), and setTimeout/setInterval with string arguments. Use JSON.parse() for data parsing. Use a sandboxed environment (vm2) if dynamic execution is needed.", - ("CWE-94", "php"): "Remove eval(), assert(), and preg_replace with /e flag. Use proper parsing functions for data. Never include user-controlled file paths.", - ("CWE-94", "ruby"): "Remove eval(), instance_eval(), and send() with user input. Use safe parsing methods. Use a sandboxed environment if dynamic execution is required.", - ("CWE-94", "dotnet"): "Avoid CSharpScript.EvaluateAsync() or Roslyn compilation with user input. Use expression parsers or sandboxed environments.", - ("CWE-94", "kotlin"): "Avoid ScriptEngine.eval() or javax.tools.JavaCompiler with user input. Use a sandboxed interpreter or expression parser.", - ("CWE-94", "scala"): "Avoid scala.tools.reflect.ToolBox.eval() with user input. Use a sandboxed interpreter or safe expression parser.", - ("CWE-94", "elixir"): "Remove Code.eval_string/1 calls with user input. Use pattern matching and safe parsing instead. Never evaluate untrusted Elixir code.", - ("CWE-94", "erlang"): "Avoid erl_eval:expr/2 and erl_scan with user input. Use pattern matching and safe parsing instead. Never evaluate untrusted Erlang terms.", - + ( + "CWE-94", + "python", + ): "Remove eval()/exec() calls with user input. Use ast.literal_eval() for safe parsing of Python literals. Use a sandboxed environment if dynamic execution is required.", + ( + "CWE-94", + "java", + ): "Avoid ScriptEngine.eval() with user input. Use a sandboxed interpreter or template engine. Restrict class loading with a SecurityManager.", + ( + "CWE-94", + "javascript_typescript", + ): "Remove eval(), Function(), and setTimeout/setInterval with string arguments. Use JSON.parse() for data parsing. Use a sandboxed environment (vm2) if dynamic execution is needed.", + ( + "CWE-94", + "php", + ): "Remove eval(), assert(), and preg_replace with /e flag. Use proper parsing functions for data. Never include user-controlled file paths.", + ( + "CWE-94", + "ruby", + ): "Remove eval(), instance_eval(), and send() with user input. Use safe parsing methods. Use a sandboxed environment if dynamic execution is required.", + ( + "CWE-94", + "dotnet", + ): "Avoid CSharpScript.EvaluateAsync() or Roslyn compilation with user input. Use expression parsers or sandboxed environments.", + ( + "CWE-94", + "kotlin", + ): "Avoid ScriptEngine.eval() or javax.tools.JavaCompiler with user input. Use a sandboxed interpreter or expression parser.", + ( + "CWE-94", + "scala", + ): "Avoid scala.tools.reflect.ToolBox.eval() with user input. Use a sandboxed interpreter or safe expression parser.", + ( + "CWE-94", + "elixir", + ): "Remove Code.eval_string/1 calls with user input. Use pattern matching and safe parsing instead. Never evaluate untrusted Elixir code.", + ( + "CWE-94", + "erlang", + ): "Avoid erl_eval:expr/2 and erl_scan with user input. Use pattern matching and safe parsing instead. Never evaluate untrusted Erlang terms.", # XXE (CWE-611) - ("CWE-611", "python"): "Use defusedxml instead of xml.etree or lxml. Set resolve_entities=False and no_network=True in lxml parsers.", - ("CWE-611", "java"): "Set XMLConstants.FEATURE_SECURE_PROCESSING and disable DOCTYPE declarations: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true).", - ("CWE-611", "javascript_typescript"): "Use a safe XML parser. In libxmljs, set noent: false and nonet: true. In xml2js, external entities are disabled by default.", - ("CWE-611", "php"): "Call libxml_disable_entity_loader(true) before parsing XML. Use LIBXML_NOENT flag carefully. Use json_decode() if possible.", - ("CWE-611", "dotnet"): "Set XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit and XmlReaderSettings.XmlResolver = null.", - ("CWE-611", "kotlin"): "Set XMLConstants.FEATURE_SECURE_PROCESSING and disable DOCTYPE: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true).", - + ( + "CWE-611", + "python", + ): "Use defusedxml instead of xml.etree or lxml. Set resolve_entities=False and no_network=True in lxml parsers.", + ( + "CWE-611", + "java", + ): "Set XMLConstants.FEATURE_SECURE_PROCESSING and disable DOCTYPE declarations: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true).", + ( + "CWE-611", + "javascript_typescript", + ): "Use a safe XML parser. In libxmljs, set noent: false and nonet: true. In xml2js, external entities are disabled by default.", + ( + "CWE-611", + "php", + ): "Call libxml_disable_entity_loader(true) before parsing XML. Use LIBXML_NOENT flag carefully. Use json_decode() if possible.", + ( + "CWE-611", + "dotnet", + ): "Set XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit and XmlReaderSettings.XmlResolver = null.", + ( + "CWE-611", + "kotlin", + ): "Set XMLConstants.FEATURE_SECURE_PROCESSING and disable DOCTYPE: factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true).", # SSRF (CWE-918) - ("CWE-918", "python"): "Validate URLs against an allowlist of permitted hosts and schemes. Block private IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x). Use urllib.parse to validate before fetching.", - ("CWE-918", "javascript_typescript"): "Validate URLs against an allowlist of permitted hosts. Block private IP ranges and localhost. Use the URL constructor to parse and validate before fetching.", - ("CWE-918", "dotnet"): "Validate URLs against an allowlist of permitted hosts. Block private IP ranges and localhost. Resolve DNS and check the IP before making requests.", - + ( + "CWE-918", + "python", + ): "Validate URLs against an allowlist of permitted hosts and schemes. Block private IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x). Use urllib.parse to validate before fetching.", + ( + "CWE-918", + "javascript_typescript", + ): "Validate URLs against an allowlist of permitted hosts. Block private IP ranges and localhost. Use the URL constructor to parse and validate before fetching.", + ( + "CWE-918", + "dotnet", + ): "Validate URLs against an allowlist of permitted hosts. Block private IP ranges and localhost. Resolve DNS and check the IP before making requests.", # File upload (CWE-434) - ("CWE-434", "python"): "Validate file extension, MIME type, and content. Use werkzeug.utils.secure_filename(). Store uploads outside the web root with randomized names.", - ("CWE-434", "java"): "Validate file extension, MIME type, and content. Store uploads outside the web root. Use Apache Tika for content-type detection.", - ("CWE-434", "javascript_typescript"): "Validate file extension, MIME type, and content. Use multer with file filter. Store uploads outside the web root with randomized names.", - ("CWE-434", "php"): "Validate file extension, MIME type with finfo_file(), and content. Store uploads outside the web root. Never trust $_FILES['type'].", - ("CWE-434", "kotlin"): "Validate file extension, MIME type, and content. Store uploads outside the web root. Use Apache Tika for content-type detection.", - ("CWE-434", "scala"): "Validate file extension, MIME type, and content. Store uploads outside the web root with randomized names.", - ("CWE-434", "elixir"): "Validate file extension, MIME type, and content. Store uploads outside the web root. Use Plug.Upload metadata for validation.", - ("CWE-434", "erlang"): "Validate file extension, MIME type, and content. Store uploads outside the web root with randomized filenames.", - + ( + "CWE-434", + "python", + ): "Validate file extension, MIME type, and content. Use werkzeug.utils.secure_filename(). Store uploads outside the web root with randomized names.", + ( + "CWE-434", + "java", + ): "Validate file extension, MIME type, and content. Store uploads outside the web root. Use Apache Tika for content-type detection.", + ( + "CWE-434", + "javascript_typescript", + ): "Validate file extension, MIME type, and content. Use multer with file filter. Store uploads outside the web root with randomized names.", + ( + "CWE-434", + "php", + ): "Validate file extension, MIME type with finfo_file(), and content. Store uploads outside the web root. Never trust $_FILES['type'].", + ( + "CWE-434", + "kotlin", + ): "Validate file extension, MIME type, and content. Store uploads outside the web root. Use Apache Tika for content-type detection.", + ( + "CWE-434", + "scala", + ): "Validate file extension, MIME type, and content. Store uploads outside the web root with randomized names.", + ( + "CWE-434", + "elixir", + ): "Validate file extension, MIME type, and content. Store uploads outside the web root. Use Plug.Upload metadata for validation.", + ( + "CWE-434", + "erlang", + ): "Validate file extension, MIME type, and content. Store uploads outside the web root with randomized filenames.", # LDAP injection (CWE-90) - ("CWE-90", "python"): "Use ldap3 library with safe filter escaping: ldap3.utils.conv.escape_filter_chars(user_input). Never concatenate user input into LDAP filters.", - ("CWE-90", "java"): "Use javax.naming.ldap with properly escaped filter values. Use LdapEncoder.filterEncode() from Spring LDAP for escaping.", - ("CWE-90", "javascript_typescript"): "Use ldapjs with properly escaped filter values. Use ldapEscape.filter() to escape special characters in LDAP filters.", - ("CWE-90", "php"): "Use ldap_escape() to sanitize filter values: ldap_escape($input, '', LDAP_ESCAPE_FILTER). Never concatenate user input into LDAP filters.", - ("CWE-90", "dotnet"): "Use System.DirectoryServices with parameterized searches. Escape special LDAP characters before inserting into filters.", - ("CWE-90", "kotlin"): "Use javax.naming.ldap with properly escaped filter values. Use Spring LDAP's LdapEncoder.filterEncode() for escaping.", - + ( + "CWE-90", + "python", + ): "Use ldap3 library with safe filter escaping: ldap3.utils.conv.escape_filter_chars(user_input). Never concatenate user input into LDAP filters.", + ( + "CWE-90", + "java", + ): "Use javax.naming.ldap with properly escaped filter values. Use LdapEncoder.filterEncode() from Spring LDAP for escaping.", + ( + "CWE-90", + "javascript_typescript", + ): "Use ldapjs with properly escaped filter values. Use ldapEscape.filter() to escape special characters in LDAP filters.", + ( + "CWE-90", + "php", + ): "Use ldap_escape() to sanitize filter values: ldap_escape($input, '', LDAP_ESCAPE_FILTER). Never concatenate user input into LDAP filters.", + ( + "CWE-90", + "dotnet", + ): "Use System.DirectoryServices with parameterized searches. Escape special LDAP characters before inserting into filters.", + ( + "CWE-90", + "kotlin", + ): "Use javax.naming.ldap with properly escaped filter values. Use Spring LDAP's LdapEncoder.filterEncode() for escaping.", # Insecure random (CWE-338) - ("CWE-338", "python"): "Use secrets.token_bytes(), secrets.token_hex(), or secrets.choice() for security-sensitive operations.", - ("CWE-338", "java"): "Use java.security.SecureRandom instead of java.util.Random for security-sensitive operations.", - ("CWE-338", "javascript_typescript"): "Use crypto.randomBytes() or crypto.getRandomValues() for security-sensitive operations.", - ("CWE-338", "dotnet"): "Use System.Security.Cryptography.RandomNumberGenerator instead of System.Random for security-sensitive operations.", - ("CWE-338", "kotlin"): "Use java.security.SecureRandom instead of kotlin.random.Random or java.util.Random for security-sensitive operations.", - + ( + "CWE-338", + "python", + ): "Use secrets.token_bytes(), secrets.token_hex(), or secrets.choice() for security-sensitive operations.", + ( + "CWE-338", + "java", + ): "Use java.security.SecureRandom instead of java.util.Random for security-sensitive operations.", + ( + "CWE-338", + "javascript_typescript", + ): "Use crypto.randomBytes() or crypto.getRandomValues() for security-sensitive operations.", + ( + "CWE-338", + "dotnet", + ): "Use System.Security.Cryptography.RandomNumberGenerator instead of System.Random for security-sensitive operations.", + ( + "CWE-338", + "kotlin", + ): "Use java.security.SecureRandom instead of kotlin.random.Random or java.util.Random for security-sensitive operations.", # CSRF (CWE-352) - ("CWE-352", "java"): "Enable Spring Security CSRF protection or use a custom CSRF token filter. Include CSRF tokens in all HTML forms and AJAX requests.", - ("CWE-352", "kotlin"): "Enable Spring Security CSRF protection or use a framework-provided CSRF middleware. Include CSRF tokens in all state-changing requests.", - + ( + "CWE-352", + "java", + ): "Enable Spring Security CSRF protection or use a custom CSRF token filter. Include CSRF tokens in all HTML forms and AJAX requests.", + ( + "CWE-352", + "kotlin", + ): "Enable Spring Security CSRF protection or use a framework-provided CSRF middleware. Include CSRF tokens in all state-changing requests.", # JWT/signature verification (CWE-347) - ("CWE-347", "python"): "Always verify JWT signatures: jwt.decode(token, key, algorithms=['HS256']). Never use options={'verify_signature': False}.", - ("CWE-347", "javascript_typescript"): "Always verify JWT signatures with jsonwebtoken: jwt.verify(token, secret). Never use jwt.decode() without verification for authorization.", - ("CWE-347", "objective-c"): "Always verify cryptographic signatures before trusting signed data. Use Security.framework's SecKeyVerifySignature for RSA/EC verification.", - ("CWE-347", "swift"): "Always verify cryptographic signatures before trusting signed data. Use CryptoKit's isValidSignature or Security.framework's SecKeyVerifySignature.", - + ( + "CWE-347", + "python", + ): "Always verify JWT signatures: jwt.decode(token, key, algorithms=['HS256']). Never use options={'verify_signature': False}.", + ( + "CWE-347", + "javascript_typescript", + ): "Always verify JWT signatures with jsonwebtoken: jwt.verify(token, secret). Never use jwt.decode() without verification for authorization.", + ( + "CWE-347", + "objective-c", + ): "Always verify cryptographic signatures before trusting signed data. Use Security.framework's SecKeyVerifySignature for RSA/EC verification.", + ( + "CWE-347", + "swift", + ): "Always verify cryptographic signatures before trusting signed data. Use CryptoKit's isValidSignature or Security.framework's SecKeyVerifySignature.", # NoSQL injection (CWE-943) - ("CWE-943", "python"): "Use parameterized queries with PyMongo. Validate input types (reject dicts/lists where strings expected). Never pass raw request data to MongoDB queries.", - ("CWE-943", "javascript_typescript"): "Validate input types before MongoDB queries. Use mongoose schema validation. Replace $where with aggregation pipeline. Sanitize with mongo-sanitize.", - ("CWE-943", "objective-c"): "Validate all user input before including in NoSQL queries. Use parameterized queries and type-check inputs to reject injection payloads.", - ("CWE-943", "swift"): "Validate all user input before including in NoSQL queries. Use parameterized queries and type-check inputs to reject injection payloads.", - + ( + "CWE-943", + "python", + ): "Use parameterized queries with PyMongo. Validate input types (reject dicts/lists where strings expected). Never pass raw request data to MongoDB queries.", + ( + "CWE-943", + "javascript_typescript", + ): "Validate input types before MongoDB queries. Use mongoose schema validation. Replace $where with aggregation pipeline. Sanitize with mongo-sanitize.", + ( + "CWE-943", + "objective-c", + ): "Validate all user input before including in NoSQL queries. Use parameterized queries and type-check inputs to reject injection payloads.", + ( + "CWE-943", + "swift", + ): "Validate all user input before including in NoSQL queries. Use parameterized queries and type-check inputs to reject injection payloads.", # Prototype pollution (CWE-1321) - ("CWE-1321", "javascript_typescript"): "Use Object.create(null) for lookup objects. Validate keys against a denylist (__proto__, constructor, prototype). Use Map instead of plain objects.", - + ( + "CWE-1321", + "javascript_typescript", + ): "Use Object.create(null) for lookup objects. Validate keys against a denylist (__proto__, constructor, prototype). Use Map instead of plain objects.", # Template injection (CWE-1336) - ("CWE-1336", "python"): "Never pass user input to Jinja2 Template() or Mako Template(). Use render_template() with variables. Enable sandboxed environment for dynamic templates.", - ("CWE-1336", "javascript_typescript"): "Never pass user input to template compilation functions. Use pre-compiled templates with data binding. Enable strict mode in template engines.", - + ( + "CWE-1336", + "python", + ): "Never pass user input to Jinja2 Template() or Mako Template(). Use render_template() with variables. Enable sandboxed environment for dynamic templates.", + ( + "CWE-1336", + "javascript_typescript", + ): "Never pass user input to template compilation functions. Use pre-compiled templates with data binding. Enable strict mode in template engines.", # Debug mode (CWE-489) - ("CWE-489", "python"): "Set DEBUG=False in production. Use environment variables to control debug mode: DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'.", - ("CWE-489", "javascript_typescript"): "Set NODE_ENV=production in deployment. Remove console.log/debug statements. Use a logging library with configurable log levels.", - + ( + "CWE-489", + "python", + ): "Set DEBUG=False in production. Use environment variables to control debug mode: DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'.", + ( + "CWE-489", + "javascript_typescript", + ): "Set NODE_ENV=production in deployment. Remove console.log/debug statements. Use a logging library with configurable log levels.", # Code integrity (CWE-494) - ("CWE-494", "javascript_typescript"): "Use Subresource Integrity (SRI) for CDN scripts: