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