Skip to content

Instantly share code, notes, and snippets.

@anton-yurchenko
Created March 31, 2026 14:09
Show Gist options
  • Select an option

  • Save anton-yurchenko/e50a47f6831867cc3c83bfbfcbc931a4 to your computer and use it in GitHub Desktop.

Select an option

Save anton-yurchenko/e50a47f6831867cc3c83bfbfcbc931a4 to your computer and use it in GitHub Desktop.
Scan GitHub repositories for specific NPM package usage
#!/usr/bin/env bash
set -euo pipefail
# Scan a GitHub organization for npm packages
# Usage: ./scan.sh <org> <github-token> <pkg[@version]> [<pkg[@version]> ...]
#
# Examples:
# ./scan.sh MyOrg ghp_xxx axios@1.14.1 axios@0.30.4 plain-crypto-js@4.2.1
# ./scan.sh MyOrg ghp_xxx plain-crypto-js # any version
usage() {
echo "Usage: $0 <org> <github-token> <pkg[@version]> [<pkg[@version]> ...]"
echo ""
echo " pkg@version — match exact version (e.g. axios@1.14.1)"
echo " pkg — flag any occurrence regardless of version (e.g. plain-crypto-js)"
exit 1
}
[[ $# -ge 3 ]] || usage
ORG="$1"; shift
TOKEN="$1"; shift
PACKAGES=("$@")
FOUND=()
gh_api() {
curl -s -H "Authorization: token ${TOKEN}" \
-H "Accept: application/vnd.github+json" \
"$@"
}
code_search() {
local query="$1"
local page=1
local per_page=100
while :; do
local response
response=$(gh_api "https://api.github.com/search/code?q=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$query'))")&per_page=${per_page}&page=${page}")
local count
count=$(echo "$response" | jq '.items | length')
if [[ "$count" == "null" || "$count" == "0" ]]; then
break
fi
echo "$response" | jq -r '.items[] | "\(.repository.full_name) \(.path) \(.url)"'
if [[ "$count" -lt "$per_page" ]]; then
break
fi
((page++))
sleep 2
done
}
# Check a file for an exact version match. Returns 0 if found.
check_file_for_version() {
local contents_url="$1"
local pkg_name="$2"
local pkg_version="$3"
local file_path="$4"
local content
content=$(gh_api -H "Accept: application/vnd.github.raw+json" "$contents_url" 2>/dev/null || true)
if [[ -z "$content" ]]; then
return 1
fi
if echo "$file_path" | grep -q "package-lock"; then
if echo "$content" | python3 -c "
import sys, json
try:
lock = json.load(sys.stdin)
except:
sys.exit(1)
pkg = '${pkg_name}'
ver = '${pkg_version}'
for key, val in lock.get('packages', {}).items():
if key.endswith('/' + pkg) or key == pkg:
if val.get('version') == ver:
print(f'MATCH in packages: {key} -> {ver}')
sys.exit(0)
def check_deps(deps, path=''):
for name, info in deps.items():
if name == pkg and info.get('version') == ver:
print(f'MATCH in dependencies: {path}{name} -> {ver}')
sys.exit(0)
if 'dependencies' in info:
check_deps(info['dependencies'], path + name + '/')
check_deps(lock.get('dependencies', {}))
sys.exit(1)
" 2>/dev/null; then
return 0
fi
else
if echo "$content" | python3 -c "
import sys, json
try:
pkg = json.load(sys.stdin)
except:
sys.exit(1)
name = '${pkg_name}'
ver = '${pkg_version}'
for section in ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']:
deps = pkg.get(section, {})
if name in deps:
declared = deps[name].lstrip('^~>=<! ')
if declared == ver:
print(f'MATCH in {section}: {name} -> {deps[name]}')
sys.exit(0)
sys.exit(1)
" 2>/dev/null; then
return 0
fi
fi
return 1
}
# Check whether a file simply contains the package name (any version).
check_file_for_presence() {
local contents_url="$1"
local pkg_name="$2"
local file_path="$3"
local content
content=$(gh_api -H "Accept: application/vnd.github.raw+json" "$contents_url" 2>/dev/null || true)
if [[ -z "$content" ]]; then
return 1
fi
if echo "$file_path" | grep -q "package-lock"; then
if echo "$content" | python3 -c "
import sys, json
try:
lock = json.load(sys.stdin)
except:
sys.exit(1)
pkg = '${pkg_name}'
for key, val in lock.get('packages', {}).items():
if key.endswith('/' + pkg) or key == pkg:
print(f'FOUND in packages: {key} -> {val.get(\"version\", \"?\")}')
sys.exit(0)
def check_deps(deps, path=''):
for name, info in deps.items():
if name == pkg:
print(f'FOUND in dependencies: {path}{name} -> {info.get(\"version\", \"?\")}')
sys.exit(0)
if 'dependencies' in info:
check_deps(info['dependencies'], path + name + '/')
check_deps(lock.get('dependencies', {}))
sys.exit(1)
" 2>/dev/null; then
return 0
fi
else
if echo "$content" | python3 -c "
import sys, json
try:
pkg = json.load(sys.stdin)
except:
sys.exit(1)
name = '${pkg_name}'
for section in ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']:
deps = pkg.get(section, {})
if name in deps:
print(f'FOUND in {section}: {name} -> {deps[name]}')
sys.exit(0)
sys.exit(1)
" 2>/dev/null; then
return 0
fi
fi
return 1
}
echo "============================================"
echo " npm package scanner"
echo " Org: ${ORG}"
echo " Date: $(date -u '+%Y-%m-%d %H:%M UTC')"
echo "============================================"
echo ""
echo "Scanning for:"
for p in "${PACKAGES[@]}"; do
if [[ "$p" == *@* ]]; then
echo " - ${p} (exact version)"
else
echo " - ${p} (any version)"
fi
done
echo ""
declare -A CHECKED_FILES=()
for spec in "${PACKAGES[@]}"; do
if [[ "$spec" == *@* ]]; then
pkg_name="${spec%@*}"
pkg_version="${spec#*@}"
mode="version"
else
pkg_name="$spec"
pkg_version=""
mode="any"
fi
if [[ "$mode" == "version" ]]; then
echo "--- Searching for ${pkg_name} (target version: ${pkg_version}) ---"
else
echo "--- Searching for ${pkg_name} (any version) ---"
fi
for filename in "package.json" "package-lock.json"; do
echo " Searching in ${filename}..."
results=$(code_search "org:${ORG} filename:${filename} \"${pkg_name}\"" 2>/dev/null || true)
if [[ -z "$results" ]]; then
echo " No results from code search."
continue
fi
while IFS= read -r line; do
repo=$(echo "$line" | awk '{print $1}')
fpath=$(echo "$line" | awk '{print $2}')
file_key="${repo}:${fpath}:${spec}"
if [[ -n "${CHECKED_FILES[$file_key]+x}" ]]; then
continue
fi
CHECKED_FILES[$file_key]=1
contents_url="https://api.github.com/repos/${repo}/contents/${fpath}"
echo -n " Checking ${repo} / ${fpath} ... "
if [[ "$mode" == "version" ]]; then
if check_file_for_version "$contents_url" "$pkg_name" "$pkg_version" "$fpath"; then
echo "VULNERABLE!"
FOUND+=("${repo} | ${fpath} | ${spec}")
else
echo "ok (different version)"
fi
else
if check_file_for_presence "$contents_url" "$pkg_name" "$fpath"; then
echo "FOUND!"
FOUND+=("${repo} | ${fpath} | ${pkg_name} (any version)")
else
echo "not present"
fi
fi
sleep 0.5
done <<< "$results"
done
# For "any version" mode, also do a broad search outside package*.json
if [[ "$mode" == "any" ]]; then
echo " Broad search (all files)..."
results=$(code_search "org:${ORG} \"${pkg_name}\" NOT filename:package.json NOT filename:package-lock.json" 2>/dev/null || true)
if [[ -n "$results" ]]; then
while IFS= read -r line; do
repo=$(echo "$line" | awk '{print $1}')
fpath=$(echo "$line" | awk '{print $2}')
file_key="${repo}:${fpath}:${spec}"
if [[ -n "${CHECKED_FILES[$file_key]+x}" ]]; then
continue
fi
CHECKED_FILES[$file_key]=1
echo " WARNING: ${pkg_name} referenced in ${repo} / ${fpath}"
FOUND+=("${repo} | ${fpath} | ${pkg_name} (ref in non-package file)")
done <<< "$results"
else
echo " No additional references found."
fi
fi
echo ""
done
echo "============================================"
echo " RESULTS"
echo "============================================"
if [[ ${#FOUND[@]} -eq 0 ]]; then
echo ""
echo " No affected repositories found in ${ORG}."
echo ""
else
echo ""
echo " AFFECTED REPOSITORIES:"
echo ""
printf " %-40s | %-30s | %s\n" "REPOSITORY" "FILE" "PACKAGE"
printf " %-40s-+-%-30s-+-%s\n" "----------------------------------------" "------------------------------" "-------------------------"
for entry in "${FOUND[@]}"; do
printf " %s\n" "$entry"
done
echo ""
echo " Total: ${#FOUND[@]} finding(s)"
echo ""
fi
echo "Scan complete."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment