diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b2478d..2bbb202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,3 +99,18 @@ jobs: sudo mv /tmp/lychee /usr/local/bin/ - name: Check links run: lychee --no-progress --accept 429 rules/ hooks.md README.md + + semgrep: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: '3.12' + - name: Install semgrep + run: pip install semgrep==1.155.0 + - name: Validate rules against fixtures + run: bash tests/semgrep-validate.sh + - name: Scan repository + run: semgrep scan --config .semgrep/ . --exclude='tests/fixtures/**' --error diff --git a/.semgrep/dangerous-html.yaml b/.semgrep/dangerous-html.yaml new file mode 100644 index 0000000..5e1bb0a --- /dev/null +++ b/.semgrep/dangerous-html.yaml @@ -0,0 +1,12 @@ +rules: + - id: dangerous-inner-html + message: >- + dangerouslySetInnerHTML bypasses React's XSS protection. + All uses require manual review — sanitization alone does not suppress this rule. + Add a nosemgrep comment after review to suppress. + See: security.md — "Never use dangerouslySetInnerHTML with user-supplied content" + severity: WARNING + languages: [typescript, javascript] + pattern-either: + - pattern: <$EL dangerouslySetInnerHTML={...} /> + - pattern: <$EL dangerouslySetInnerHTML={...}>...$EL> diff --git a/.semgrep/fallback-secrets.yaml b/.semgrep/fallback-secrets.yaml new file mode 100644 index 0000000..7ee28ca --- /dev/null +++ b/.semgrep/fallback-secrets.yaml @@ -0,0 +1,48 @@ +rules: + - id: fallback-secret-js + message: >- + Hardcoded fallback for secret-like env var. + Secret variables should fail explicitly if not set, not fall back to defaults. + See: security.md — "Never hardcode secrets as fallback values" + severity: ERROR + languages: [javascript, typescript] + pattern-either: + - patterns: + - pattern: process.env.$VAR || "..." + - metavariable-regex: + metavariable: $VAR + regex: ".*(SECRET|PASSWORD|CREDENTIAL|PRIVATE|AUTH|API_KEY|TOKEN).*" + - patterns: + - pattern: process.env.$VAR ?? "..." + - metavariable-regex: + metavariable: $VAR + regex: ".*(SECRET|PASSWORD|CREDENTIAL|PRIVATE|AUTH|API_KEY|TOKEN).*" + - patterns: + - pattern: process.env["$VAR"] || "..." + - metavariable-regex: + metavariable: $VAR + regex: ".*(SECRET|PASSWORD|CREDENTIAL|PRIVATE|AUTH|API_KEY|TOKEN).*" + - patterns: + - pattern: process.env["$VAR"] ?? "..." + - metavariable-regex: + metavariable: $VAR + regex: ".*(SECRET|PASSWORD|CREDENTIAL|PRIVATE|AUTH|API_KEY|TOKEN).*" + + - id: fallback-secret-python + message: >- + Hardcoded fallback for secret-like env var. + Secret variables should fail explicitly if not set, not fall back to defaults. + See: security.md — "Never hardcode secrets as fallback values" + severity: ERROR + languages: [python] + pattern-either: + - patterns: + - pattern: os.environ.get($KEY, "...") + - metavariable-regex: + metavariable: $KEY + regex: ".*(SECRET|PASSWORD|CREDENTIAL|PRIVATE|AUTH|API_KEY|TOKEN).*" + - patterns: + - pattern: os.getenv($KEY, "...") + - metavariable-regex: + metavariable: $KEY + regex: ".*(SECRET|PASSWORD|CREDENTIAL|PRIVATE|AUTH|API_KEY|TOKEN).*" diff --git a/.semgrep/no-eval.yaml b/.semgrep/no-eval.yaml new file mode 100644 index 0000000..1050bfb --- /dev/null +++ b/.semgrep/no-eval.yaml @@ -0,0 +1,23 @@ +rules: + - id: no-eval-dynamic-exec + message: >- + Dynamic code execution detected. Use structured alternatives + (JSON.parse, ast.literal_eval, etc.) instead of executing arbitrary strings. + See: security.md — "Never use eval(), Function(), or dynamic code execution" + severity: ERROR + languages: [javascript, typescript] + pattern-either: + - pattern: eval(...) + - pattern: new Function(...) + - pattern: Function(...) + + - id: no-eval-dynamic-exec-python + message: >- + Dynamic code execution detected. Use structured alternatives + (json.loads, ast.literal_eval, etc.) instead of executing arbitrary strings. + See: security.md — "Never use eval(), Function(), or dynamic code execution" + severity: ERROR + languages: [python] + pattern-either: + - pattern: eval(...) + - pattern: exec(...) diff --git a/.semgrep/unsafe-yaml.yaml b/.semgrep/unsafe-yaml.yaml new file mode 100644 index 0000000..055cec8 --- /dev/null +++ b/.semgrep/unsafe-yaml.yaml @@ -0,0 +1,13 @@ +rules: + - id: unsafe-yaml-load + message: >- + yaml.load() without SafeLoader can execute arbitrary code. + Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader). + See: security.md — "Use yaml.safe_load not yaml.load" + severity: ERROR + languages: [python] + patterns: + - pattern: yaml.load(...) + - pattern-not: yaml.load(..., Loader=yaml.SafeLoader) + - pattern-not: yaml.load(..., Loader=yaml.BaseLoader) + - pattern-not: yaml.load(..., Loader=yaml.CSafeLoader) diff --git a/tests/fixtures/semgrep/dangerous-html.tsx b/tests/fixtures/semgrep/dangerous-html.tsx new file mode 100644 index 0000000..cac109a --- /dev/null +++ b/tests/fixtures/semgrep/dangerous-html.tsx @@ -0,0 +1,23 @@ +// Tests for dangerous-inner-html rule +import DOMPurify from "dompurify"; + +function Unsanitized({ content }: { content: string }) { + // ruleid: dangerous-inner-html + return
; +} + +function Sanitized({ content }: { content: string }) { + // ruleid: dangerous-inner-html + return ; +} + +function Safe({ content }: { content: string }) { + // ok: dangerous-inner-html + return