From 2c4673a9f4558fe9c244a8fdc377ec0ff4890bab Mon Sep 17 00:00:00 2001 From: datarian Date: Sat, 13 Jun 2026 08:15:38 +0200 Subject: [PATCH] feat(ci): add GitHub security pipeline for PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 5-job security workflow triggered on PR open, synchronize, and reopen events — covering secret scanning (Gitleaks), npm dependency audit, TypeScript/React SAST via Semgrep, LaTeX shell-escape injection detection, and a personal data leak gate that hard-fails if gitignored profile files are accidentally tracked. Includes custom Semgrep rules targeting OWASP LLM Top 10 risks relevant to a Claude Code plugin: LLM01 prompt injection, LLM02 insecure output handling, LLM05 supply chain hooks, LLM06 credential disclosure, and LLM08 excessive agency (eval/exec). Findings upload as SARIF to the GitHub Security tab. Co-Authored-By: Claude Sonnet 4.6 --- .github/semgrep/llm-owasp.yaml | 181 +++++++++++++++++++++++++++++++++ .github/workflows/security.yml | 181 +++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 .github/semgrep/llm-owasp.yaml create mode 100644 .github/workflows/security.yml diff --git a/.github/semgrep/llm-owasp.yaml b/.github/semgrep/llm-owasp.yaml new file mode 100644 index 0000000..12672ad --- /dev/null +++ b/.github/semgrep/llm-owasp.yaml @@ -0,0 +1,181 @@ +rules: + # ───────────────────────────────────────────────────────────── + # OWASP LLM Top 10 — Custom Semgrep Rules + # Scope: Claude Code plugin that orchestrates LLM calls, + # renders LaTeX, and serves a React web resume. + # ───────────────────────────────────────────────────────────── + + # LLM02 — Insecure Output Handling + # Risk: LLM-generated text rendered as raw HTML enables stored XSS. + + - id: llm02-innerhtml-assignment + pattern: $EL.innerHTML = $VAL + message: > + LLM02 (Insecure Output Handling): Direct innerHTML assignment bypasses + React's sanitization. If $VAL ever holds AI-generated content, this enables + stored XSS. Use a sanitization library (e.g. DOMPurify) or render via React + JSX instead. + languages: [typescript, javascript] + severity: ERROR + metadata: + owasp-llm: LLM02 + cwe: CWE-79 + + - id: llm02-document-write + pattern: document.write($X) + message: > + LLM02 (Insecure Output Handling): document.write() renders raw HTML with + no escaping. Do not use with AI-generated or user-supplied content. + languages: [typescript, javascript] + severity: ERROR + metadata: + owasp-llm: LLM02 + cwe: CWE-79 + + - id: llm02-dangerous-set-inner-html + pattern: dangerouslySetInnerHTML={{ __html: $VAL }} + message: > + LLM02 (Insecure Output Handling): dangerouslySetInnerHTML renders raw HTML, + bypassing React's XSS protection. Sanitize AI-generated content with + DOMPurify before passing it here, or restructure to avoid raw HTML rendering. + languages: [typescript, javascript] + severity: WARNING + metadata: + owasp-llm: LLM02 + cwe: CWE-79 + + # LLM08 — Excessive Agency + # Risk: eval() / new Function() allow LLM-generated strings to execute as code. + + - id: llm08-eval + pattern: eval($X) + message: > + LLM08 (Excessive Agency): eval() executes arbitrary JavaScript. Never pass + AI-generated or user-controlled content to eval(). Refactor to use a safe + alternative (e.g. JSON.parse for data, explicit logic for control flow). + languages: [typescript, javascript] + severity: ERROR + metadata: + owasp-llm: LLM08 + cwe: CWE-95 + + - id: llm08-new-function + pattern: new Function($X) + message: > + LLM08 (Excessive Agency): new Function() is equivalent to eval() — it + compiles and runs arbitrary JavaScript. Do not construct Function objects + from AI-generated content. + languages: [typescript, javascript] + severity: ERROR + metadata: + owasp-llm: LLM08 + cwe: CWE-95 + + - id: llm08-child-process-exec-dynamic + patterns: + - pattern: | + require('child_process').exec($CMD, ...) + - pattern-not: | + require('child_process').exec("...", ...) + message: > + LLM08 (Excessive Agency): child_process.exec() with a dynamic command + string can enable OS command injection if $CMD contains AI-generated or + user-controlled content. Use execFile() with a fixed binary and argument + array instead. + languages: [typescript, javascript] + severity: ERROR + metadata: + owasp-llm: LLM08 + cwe: CWE-78 + + # LLM06 — Sensitive Information Disclosure + # Risk: API keys or credentials hardcoded in tracked source files. + + - id: llm06-anthropic-api-key + pattern-regex: 'sk-ant-[A-Za-z0-9_\-]{20,}' + message: > + LLM06 (Sensitive Information Disclosure): Anthropic API key detected in + source. Revoke and rotate immediately; store secrets in environment + variables or a secrets manager, never in code. + languages: [generic] + severity: ERROR + metadata: + owasp-llm: LLM06 + cwe: CWE-798 + + - id: llm06-github-pat + pattern-regex: 'ghp_[A-Za-z0-9]{36}' + message: > + LLM06 (Sensitive Information Disclosure): GitHub Personal Access Token + detected. Revoke and rotate immediately. + languages: [generic] + severity: ERROR + metadata: + owasp-llm: LLM06 + cwe: CWE-798 + + - id: llm06-openai-api-key + pattern-regex: 'sk-[A-Za-z0-9]{48}' + message: > + LLM06 (Sensitive Information Disclosure): Potential OpenAI API key detected. + Verify and rotate if confirmed. + languages: [generic] + severity: WARNING + metadata: + owasp-llm: LLM06 + cwe: CWE-798 + + # LLM01 — Prompt Injection (detectable surface: template literals in TS/JS) + # Risk: user-controlled strings interpolated directly into prompt strings sent + # to the Claude API without any demarcation or sanitization. + # Note: most prompt construction in this repo is in Markdown skill files + # (not statically analyzable); this rule catches TypeScript prompt builders. + + - id: llm01-prompt-string-concat + patterns: + - pattern: $PROMPT + $USER_INPUT + - metavariable-regex: + metavariable: $USER_INPUT + regex: '.*(user|input|content|query|message|prompt|text|data).*' + message: > + LLM01 (Prompt Injection): String concatenation may inject user-controlled + content directly into a prompt. Wrap untrusted content in explicit + delimiters (e.g. XML tags: ...) to prevent + an adversary from overriding system instructions. + languages: [typescript, javascript] + severity: WARNING + metadata: + owasp-llm: LLM01 + cwe: CWE-77 + + # LLM05 — Supply Chain + # Risk: install lifecycle scripts in package.json execute code during npm install. + + - id: llm05-npm-preinstall-script + pattern-regex: '"preinstall"\s*:\s*"[^"]+' + message: > + LLM05 (Supply Chain): A preinstall script runs automatically during + npm install. Verify this is intentional and audited; supply-chain + attacks often abuse lifecycle hooks. + languages: [generic] + paths: + include: + - "**/package.json" + severity: WARNING + metadata: + owasp-llm: LLM05 + cwe: CWE-494 + + - id: llm05-npm-postinstall-script + pattern-regex: '"postinstall"\s*:\s*"[^"]+' + message: > + LLM05 (Supply Chain): A postinstall script runs automatically after + npm install. Audit this script to ensure it has not been tampered with. + languages: [generic] + paths: + include: + - "**/package.json" + severity: WARNING + metadata: + owasp-llm: LLM05 + cwe: CWE-494 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..8e7823b --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,181 @@ +name: Security Pipeline + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + security-events: write # upload SARIF to GitHub Security tab + pull-requests: read + +jobs: + # ────────────────────────────────────────────────────────────── + # 1. Secret & credential scanning + # ────────────────────────────────────────────────────────────── + secret-scan: + name: Secret Scanning (Gitleaks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history so gitleaks can scan all commits in the PR + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # GITLEAKS_LICENSE is only required for GitHub org-level scanning. + # For a public personal repo this can be left unset. + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} + + # ────────────────────────────────────────────────────────────── + # 2. npm dependency audit (web-builder) + # ────────────────────────────────────────────────────────────── + npm-audit: + name: npm Dependency Audit + runs-on: ubuntu-latest + # Skip gracefully if the web-builder hasn't been committed yet + if: ${{ hashFiles('resumes/web-builder/package.json') != '' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: resumes/web-builder/package-lock.json + - name: Install dependencies + working-directory: resumes/web-builder + run: npm ci + - name: Audit for vulnerabilities (moderate+) + working-directory: resumes/web-builder + # --audit-level=moderate fails on moderate, high, and critical issues + run: npm audit --audit-level=moderate + + # ────────────────────────────────────────────────────────────── + # 3. SAST — TypeScript/React + OWASP LLM Top 10 (Semgrep) + # ────────────────────────────────────────────────────────────── + semgrep: + name: "SAST: Semgrep (TypeScript + OWASP LLM Top 10)" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Semgrep + run: pip install semgrep + + # --- TypeScript / React (standard auto rules) --- + - name: Semgrep – TypeScript/React auto rules + if: ${{ hashFiles('resumes/web-builder/src') != '' }} + # Exit 0 so we always upload SARIF; pipeline gating is done via SARIF review + run: | + semgrep scan \ + --config auto \ + --include "*.ts" --include "*.tsx" --include "*.js" \ + --sarif --output semgrep-ts.sarif \ + resumes/web-builder/src + continue-on-error: true + + - name: Upload TypeScript SARIF + if: always() && hashFiles('semgrep-ts.sarif') != '' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep-ts.sarif + category: semgrep-typescript + + # --- OWASP LLM Top 10 custom rules (whole repo) --- + - name: Semgrep – OWASP LLM Top 10 custom rules + run: | + semgrep scan \ + --config .github/semgrep/llm-owasp.yaml \ + --sarif --output semgrep-llm.sarif \ + . + # Fail the PR check when ERROR-severity LLM findings are detected + # (WARNING-severity still uploads but doesn't fail the build) + + - name: Upload LLM OWASP SARIF + if: always() && hashFiles('semgrep-llm.sarif') != '' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep-llm.sarif + category: semgrep-llm-owasp + + # ────────────────────────────────────────────────────────────── + # 4. LaTeX injection check + # \write18 / --shell-escape enables arbitrary OS command execution + # from within a .tex file — a real risk if resume content is + # ever rendered from untrusted input. + # ────────────────────────────────────────────────────────────── + latex-safety: + name: LaTeX Injection Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Scan for shell-escape primitives + run: | + echo "Checking LaTeX templates for dangerous commands..." + FAIL=0 + + # \write18{} and \immediate\write18{} execute arbitrary shell commands + if grep -rn --include="*.tex" -E '\\write18|\\immediate\\write18' resumes/templates/; then + echo "::error file=resumes/templates::Dangerous LaTeX \\write18 found — enables arbitrary shell execution during compilation" + FAIL=1 + fi + + # latexmkrc or texmf.cnf with shell_escape = t enables the same + if find . \( -name "*.latexmkrc" -o -name "texmf.cnf" \) \ + -not -path "./.git/*" \ + | xargs grep -l "shell_escape\s*=\s*t\|openout_any\s*=\s*a" 2>/dev/null; then + echo "::error::LaTeX configuration enables shell escape or unrestricted output — dangerous in a build pipeline" + FAIL=1 + fi + + # Dynamic \input / \include paths (warning only — valid in templates but worth auditing) + if grep -rn --include="*.tex" -E '\\(input|include)\{\\|\\(input|include)\{[^}]*\$' resumes/templates/; then + echo "::warning::Dynamic \\input/\\include path found — verify it cannot be controlled by user-supplied content" + fi + + [ $FAIL -eq 0 ] && echo "LaTeX safety check passed." + exit $FAIL + + # ────────────────────────────────────────────────────────────── + # 5. Personal data leak check + # PERSONAL_PROFILE.md and MISSING_INFORMATION.md are gitignored + # on purpose. This job fails if either file (or any .backup.) + # is accidentally tracked, preventing personal data from being + # pushed to the public repository. + # ────────────────────────────────────────────────────────────── + pii-check: + name: Personal Data Leak Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Ensure personal files are not tracked + run: | + echo "Verifying personal/private files are not committed..." + FAIL=0 + + # These files must never be tracked + FORBIDDEN_PATTERNS=( + "PERSONAL_PROFILE\\.md" + "MISSING_INFORMATION\\.md" + "\\.backup\\." + "resumes/compiled/" + "resumes/customized/" + ) + + for pattern in "${FORBIDDEN_PATTERNS[@]}"; do + matches=$(git ls-files | grep -E "$pattern" | grep -v "\.example\." || true) + if [ -n "$matches" ]; then + echo "::error::Private file tracked in git: $matches" + echo " These files must remain gitignored — they contain personal data." + FAIL=1 + fi + done + + [ $FAIL -eq 0 ] && echo "Personal data check passed — no private files tracked." + exit $FAIL