GitHub - genuinecode/sca-scan · GitHub
Skip to content

genuinecode/sca-scan

Folders and files

Repository files navigation

sca-scan

read-only triage for SCA (supply-chain attack) indicators across npm + rubygems + pypi. scans lockfiles + host indicators against a committed IOC baseline, plus GitHub-side persistence vectors.

context

active multi-ecosystem worm campaign (TeamPCP family). payload harvests creds from ~/.npmrc / ~/.aws/ / ~/.gem/credentials / etc, exfils via public GitHub repos under the victim's account, persists via GH Actions with on: discussion triggers.

dead-man switch wipes $HOME if it detects token revocation before the machine is cleaned. run this scan before rotating anything.

sources

primary research this tool tracks:

upstream IOC feeds (datadog, wiz) cited per-source in update_iocs.rb.

privacy

runs entirely on your machine. no project source, scan findings, IOC hits, or vulnerabilities sent anywhere. no vendor telemetry, no SaaS, no POSTs.

credential files are never opened or read. the tool checks ~/.npmrc, ~/.aws/credentials, ~/.gem/credentials, etc. for existence + size + mtime only -- nothing inside ever touched.

no LLM/AI calls. no openai/anthropic/google/etc; logic is deterministic ruby. you can audit every line.

network calls (all GETs, no POSTs):

  • recent_releases -- public registry lookups by package name (npm + rubygems URL path; pypi includes version per endpoint shape). same calls npm view / gem info make.
  • check_github.sh -- reads your OWN GitHub account (your existing auth token / relationship).
  • update_iocs.rb -- downloads public IOC corpus. nothing about your machine sent.

files

file what
scan.rb main scanner. yaml stdout.
update_iocs.rb fetches IOC CSVs from datadog/wiz, writes iocs/*.txt.
check_github.sh gh CLI checks: exfil repos, hulud branches, on: discussion workflows, recent workflow commits.
parsers.rb Gemfile / yarn / pnpm / package-lock / bun.lock parsers.
iocs.rb IOC baseline reader.
release_dates.rb per-package registry release-date lookup with on-disk caching.
iocs/*.txt committed IOC baseline. refresh via update_iocs.rb, commit the diff.
iocs/last_updated timestamp of the last refresh (committed).
iocs/release_dates.cache.json per-machine runtime cache, gitignored.

requirements

  • ruby 2.7+, stdlib only (rbenv users in legacy 2.6 projects: RBENV_VERSION=<ver> ruby scan.rb ...)
  • gh CLI (authed) + jq for check_github.sh
  • macOS host or docker container

run

clone once, anywhere you keep tools (eg. ~/tools/sca-scan):

mkdir -p ~/tools
git clone https://github.com/genuinecode/sca-scan.git ~/tools/sca-scan
cd ~/tools/sca-scan

scan one or more project dirs (on host):

ruby scan.rb ~/code/project [~/code/other_project]   # ~45s cold, ~10s warm
./check_github.sh ~/code/project                     # ~5s, needs gh CLI + jq

$HOME is always scanned regardless of which dirs you pass (droppers, shell rc, ~/.claude, token paths, launch agents). lockfiles + package.json under the passed dirs scan against the IOC baseline.

no network needed for scanning; IOC corpus ships in the repo. recent_releases hits npm/rubygems/pypi registries, cached per-machine in iocs/release_dates.cache.json.

host vs docker

run on host (default). covers project lockfiles + host persistence ($HOME, launchd, ssh, shell rc) + host-installed dev tooling + GH. most of the typical dev surface.

also run inside container only when it matters: long-lived containers, persistent volumes, or images that install non-lockfile deps at runtime. dev containers that get rebuilt: skip.

mount the host clone into the container so you don't need to install ruby/git inside:

# docker compose (replace `app` with your service name; check your compose file)
docker compose run --rm -v ~/tools/sca-scan:/sca-scan app ruby /sca-scan/scan.rb /app

# plain docker
docker run --rm \
  -v ~/tools/sca-scan:/sca-scan \
  -v ~/code/project:/app:ro \
  ruby:3-slim ruby /sca-scan/scan.rb /app

(/app = wherever the project sits inside the container.)

output

each check has a status::

status meaning
pass clean
flag investigate before touching tokens
review legit + malicious shapes look the same; needs human eye
info informational (eg. which credential files exist)
skipped not applicable in this env

flag is not confirmed compromise. read the evidence -- bundle.js is also webpack output, bun is also a legit runtime.

if anything flags

  1. do not rotate tokens yet. dead-man switch.
  2. read the evidence. false positives are normal.
  3. if real: disconnect network, image the disk, rotate from a clean machine.
  4. can't wait + uncertain? image the disk for forensics + selective data recovery, then rotate from a clean machine. do NOT restore the image in-place -- re-infects.

refreshing the corpus

ruby update_iocs.rb
git diff iocs/
git add iocs/ && git commit -m "ioc refresh $(date +%Y-%m-%d)"

each refresh = one reviewable commit. daily during active campaigns, weekly otherwise. scan.rb warns when baseline is >3d old.

don't auto-schedule this in CI; refresh PRs are reviewed. if you want scheduled refreshes, write a GitHub Action that opens a PR, not one that auto-commits.

before adding a source: curl -sw '%{http_code} %{size_download}\n' URL (verify 200 + sane size), curl -s URL | head (check schema). each entry must trace to a real advisory; don't sub aggregator repos for missing official ones.

what's NOT covered

  • waves that broke after the last refresh (baseline is point-in-time; check iocs/last_updated)
  • production deploys beyond docker (capistrano / fly / ECS) -- ask devops
  • other devs' machines; scans your local only
  • pypi has weak public GHSA coverage compared to npm
  • compromised secret managers outside the standard token paths
  • launch_agents lookback is 30d
  • lifecycle scripts in package.json listed not analysed; node bundle.js and husky install look the same here
  • bun.lockb (binary, pre-bun-v1.2 default) -- requires the bun CLI to parse. bun.lock (text, JSONC) is supported.

adding a new wave

constants at the top of update_iocs.rb:

  • CSV source with stable URL: add to SOURCES, write an extractor matching its schema
  • per-entry-cited small set: append to the QIX_SEPT_2025_NPM-style bundle

then ruby update_iocs.rb.

design

  • stdlib only: gem install during a campaign is the attack vector
  • read-only: no writes outside iocs/, no deletes, no POSTs, no interactive prompts
  • yaml stdout, evidence not verdicts
  • per-source attribution: every IOC entry cites its origin URL or bundle
  • update_iocs.rb refuses to overwrite if any network source failed -- a gutted baseline is worse than a stale one

contributing

PRs + issues welcome -- improvements, ecosystem additions, methodology fixes, all on the table.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

Contributors