CI(security): catch Lightning, Shai-Hulud, npm hijack, design-flaw CVEs

Recent supply-chain incidents that scan_packages would have missed:
  - PyTorch Lightning 2.6.x: payload in _runtime/router_runtime.js
    (14.8 MB), persistence via .claude/settings.json SessionStart
    and .vscode/tasks.json folderOpen
  - npm chalk/debug + Shai-Hulud: hex-var obfuscation, window.ethereum
    Web3 hijack, .github/workflows/shai-hulud.yml repo takeover,
    trufflehog credential exfil
  - elementary-data 0.23.3: token harvesters with embedded gh{p,o,s}_
    and AKIA regexes
  - litellm 1.82.7: also covered by existing patterns, but anyone on
    `>=` got it during the 40-min exposure window
  - langchain-core CVE-2025-68664 / n8n CVE-2025-68668 / marimo
    CVE-2026-39987: first-party design flaws, not malicious-author

scan_packages.py:
  - Six new regexes: RE_DEV_TOOL_HIJACK, RE_TOKEN_REGEX,
    RE_JS_OBFUSCATION, RE_WEB3_HIJACK, RE_WORKFLOW_INJECT,
    RE_SHELL_DROPPER.
  - Three new checkers: check_js_file, check_shell_file,
    check_workflow_file. scan_archive now routes .js/.mjs/.cjs/.ts
    to the JS checker, .sh/.bash to the shell checker, and
    .github/workflows/*.yml to the workflow checker.
  - JS checker fires CRITICAL on hex-var obfuscation OR Web3 hijack
    OR (token regex + network) OR workflow-injection signature; HIGH
    on a >100 KB JS bundle inside a Python wheel (the Lightning tell).
  - Smoke-tested: every new pattern matches its canonical positive
    and rejects four legitimate-looking false-positive baits.

security-audit.yml:
  - OSV-Scanner step: cross-ecosystem advisory check (PyPI + npm
    + cargo) from one binary. OSV's feed is a superset of GitHub-
    Advisory; catches CVEs that haven't propagated yet (e.g.
    langchain-core was on OSV before GitHub Advisory).
  - Semgrep step: p/supply-chain + p/python + p/javascript +
    p/security-audit packs catch first-party logic bugs (CVEs 7/9/10
    above) that pattern scanning never sees.
  - Lockfile pin verifier: warns on every non-`==` spec in
    requirements/*.txt. Currently surfaces 104 unpinned specs as
    informational baseline; tighten to blocking once the baseline
    is curated.

All new steps continue-on-error initially; they surface findings to
the workflow summary + advisory-audit-logs artifact.
This commit is contained in:
Daniel Han 2026-05-06 23:54:04 +00:00
commit 1696a15cf8
2 changed files with 332 additions and 0 deletions

View file

@ -261,6 +261,126 @@ jobs:
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# OSV-Scanner: cross-ecosystem advisory DB (PyPI + npm + cargo)
# ─────────────────────────────────────────────────────────────
- name: OSV-Scanner (PyPI + npm + cargo, cross-ecosystem advisories)
# OSV's advisory feed is a superset of GitHub-Advisory + RustSec
# + npm advisories; running it alongside the per-ecosystem audit
# tools catches CVEs that haven't propagated to the per-ecosystem
# DBs yet (e.g. langchain-core CVE-2025-68664 was on OSV before
# GitHub Advisory). Single binary, one transitive resolver, all
# three lockfile types in one pass. Non-blocking until baselines
# close.
continue-on-error: true
run: |
set +e
curl -fsSL -o /tmp/osv-scanner.tar.gz \
https://github.com/google/osv-scanner/releases/download/v2.0.2/osv-scanner_linux_amd64.tar.gz
tar -xzf /tmp/osv-scanner.tar.gz -C /tmp osv-scanner
/tmp/osv-scanner --version
/tmp/osv-scanner scan source \
--lockfile=studio/frontend/package-lock.json \
--lockfile=studio/src-tauri/Cargo.lock \
--lockfile=requirements.txt:audit-reqs/unsloth-deps.txt \
--lockfile=requirements.txt:audit-reqs/studio.txt \
--lockfile=requirements.txt:audit-reqs/no-torch-runtime.txt \
--lockfile=requirements.txt:audit-reqs/overrides.txt \
--lockfile=requirements.txt:audit-reqs/extras.txt \
--lockfile=requirements.txt:audit-reqs/extras-no-deps.txt \
--format=table 2>&1 | tee logs-osv-scanner.txt
{
echo "## OSV-Scanner (cross-ecosystem)"
echo
echo '```'
tail -200 logs-osv-scanner.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Semgrep: design-flaw detection (catches what regex-pattern
# scanning of malicious authors cannot — first-party logic bugs
# like langchain-core CVE-2025-68664 dumps/dumpd injection,
# n8n CVE-2025-68668 _pyodide.eval_code sandbox escape, marimo
# CVE-2026-39987 unauth WebSocket).
# ─────────────────────────────────────────────────────────────
- name: Semgrep (supply-chain + python rule packs)
continue-on-error: true
run: |
set +e
python -m pip install --quiet 'semgrep>=1.95'
semgrep --version
semgrep scan \
--config p/supply-chain \
--config p/python \
--config p/javascript \
--config p/security-audit \
--severity ERROR --severity WARNING \
--metrics off \
--timeout 120 \
studio/backend unsloth scripts \
2>&1 | tee logs-semgrep.txt
{
echo "## Semgrep (supply-chain + python + javascript rules)"
echo
echo '```'
tail -200 logs-semgrep.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Lockfile pin verifier. The litellm 1.82.7 attack window was
# ~40 minutes; anyone resolving with `>=` got the malicious
# version automatically. Flag every spec in the requirements
# files that does not pin to an exact `==` (or `@` for git
# refs, or `===` for arbitrary equality). Warning-only for now;
# graduate to blocking once the baseline is clean.
# ─────────────────────────────────────────────────────────────
- name: Lockfile pin verifier (Python requirements)
continue-on-error: true
run: |
python <<'PY' | tee logs-pin-verifier.txt
import re
from pathlib import Path
# Specs that look like `pkg==1.2.3` or `pkg @ git+...` or
# bare comments / -r lines are pinned-or-not-applicable.
PINNED = re.compile(r"^\s*[A-Za-z0-9_.\-]+\s*(?:===|==)\s*[^,;]+\s*$")
GIT_OR_URL = re.compile(r"^\s*[A-Za-z0-9_.\-]+\s*@\s*(?:git\+|https?://)")
unpinned = []
for f in sorted(Path("studio/backend/requirements").glob("*.txt")):
for i, raw in enumerate(f.read_text().splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#") or line.startswith("-"):
continue
spec = line.split("#", 1)[0].strip().split(";", 1)[0].strip()
if not spec:
continue
if "git+" in spec or PINNED.match(spec) or GIT_OR_URL.match(spec):
continue
unpinned.append((str(f), i, line))
print(f"::group::Lockfile pin status")
if unpinned:
print(f"WARN: {len(unpinned)} non-`==` specs across requirements/*.txt")
print("(litellm 1.82.7 wave hit anyone on `>=`; tighten when feasible.)")
for f, i, line in unpinned[:80]:
print(f" {f}:{i}: {line}")
if len(unpinned) > 80:
print(f" ... and {len(unpinned) - 80} more")
else:
print("OK: every spec is exact-pinned.")
print("::endgroup::")
PY
{
echo "## Lockfile pin verifier"
echo
echo '```'
cat logs-pin-verifier.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v4
if: always()
with:
@ -270,6 +390,9 @@ jobs:
logs-npm-audit.txt
logs-npm-audit.json
logs-cargo-audit.txt
logs-osv-scanner.txt
logs-semgrep.txt
logs-pin-verifier.txt
audit-reqs/
retention-days: 30