Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions .github/semgrep/llm-owasp.yaml
Original file line number Diff line number Diff line change
@@ -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: <user_input>...</user_input>) 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
181 changes: 181 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -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{<cmd>} and \immediate\write18{<cmd>} 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