Skip to content

Instantly share code, notes, and snippets.

@jinnosux
Created September 9, 2025 13:26
Show Gist options
  • Save jinnosux/bec00c23fe4a67796e4cfa474a00fe52 to your computer and use it in GitHub Desktop.
Save jinnosux/bec00c23fe4a67796e4cfa474a00fe52 to your computer and use it in GitHub Desktop.
Simple NPM Supply Chain Attack (Sept 2025) Scanner
# Simple NPM Supply Chain Attack Scanner (ref https://www.bleepingcomputer.com/news/security/hackers-hijack-npm-packages-with-2-billion-weekly-downloads-in-supply-chain-attack/ )
# Scans package.json and package-lock.json in current directory for compromised packages
# Usage: .\ScanNPM.ps1 [optional-directory-path]
param(
[string]$Path = "."
)
# Compromised packages from September 2025 attack
$CompromisedPackages = @{
"ansi-regex" = "6.2.1"
"ansi-styles" = "6.2.2"
"backslash" = "0.2.1"
"chalk" = "5.6.1"
"chalk-template" = "1.1.1"
"color-convert" = "3.1.1"
"color-name" = "2.0.1"
"color-string" = "2.1.1"
"debug" = "4.4.2"
"error-ex" = "1.3.3"
"has-ansi" = "6.0.1"
"is-arrayish" = "0.3.3"
"simple-swizzle" = "0.2.3"
"slice-ansi" = "7.1.1"
"strip-ansi" = "7.1.1"
"supports-color" = "10.2.1"
"supports-hyperlinks" = "4.1.1"
"wrap-ansi" = "9.0.1"
"color" = "5.0.1"
}
# Additional compromised packages
$AdditionalCompromised = @{
"eslint-config-prettier" = @("8.10.1", "9.1.1", "10.1.6", "10.1.7")
"eslint-plugin-prettier" = @("4.2.2", "4.2.3")
"synckit" = @("0.11.9")
"@pkgr/core" = @("0.2.8")
"napi-postinstall" = @("0.3.1")
"got-fetch" = @("5.1.11", "5.1.12")
"is" = @("3.3.1", "5.0.0")
}
$Findings = @()
function Write-ColorOutput {
param([string]$Message, [string]$Color = "White")
switch ($Color) {
"Red" { Write-Host $Message -ForegroundColor Red }
"Green" { Write-Host $Message -ForegroundColor Green }
"Yellow" { Write-Host $Message -ForegroundColor Yellow }
"Blue" { Write-Host $Message -ForegroundColor Blue }
"Cyan" { Write-Host $Message -ForegroundColor Cyan }
default { Write-Host $Message }
}
}
function Test-VersionMatch {
param([string]$PackageVersion, [string]$CompromisedVersion)
# Remove semver prefixes (^, ~, >=, etc.)
$cleanVersion = $PackageVersion -replace '^[\^~>=<]+', ''
# Direct match
if ($cleanVersion -eq $CompromisedVersion) {
return $true
}
# Check ranges - for simplicity, if the exact version matches
if ($PackageVersion.StartsWith('^') -or $PackageVersion.StartsWith('~')) {
return $cleanVersion -eq $CompromisedVersion
}
return $false
}
function Add-Finding {
param(
[string]$Level,
[string]$Package,
[string]$Version,
[string]$Source
)
$finding = [PSCustomObject]@{
Level = $Level
Package = $Package
Version = $Version
Source = $Source
}
$script:Findings += $finding
$color = if ($Level -eq "CRITICAL") { "Red" } else { "Yellow" }
Write-ColorOutput "[WARNING] [$Level] $Package@$Version (found in $Source)" $color
}
function Test-PackageJson {
param([string]$FilePath)
if (!(Test-Path $FilePath)) {
Write-ColorOutput "[INFO] package.json not found" "Blue"
return
}
Write-ColorOutput "[SCAN] Scanning package.json..." "Blue"
try {
$content = Get-Content $FilePath -Raw -ErrorAction Stop
$packageData = $content | ConvertFrom-Json -ErrorAction Stop
# Check all dependency types
$depTypes = @('dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies')
foreach ($depType in $depTypes) {
if ($packageData.$depType) {
$packageData.$depType.PSObject.Properties | ForEach-Object {
$packageName = $_.Name
$versionSpec = $_.Value
# Check main compromised packages
if ($CompromisedPackages.ContainsKey($packageName)) {
$compromisedVersion = $CompromisedPackages[$packageName]
if (Test-VersionMatch $versionSpec $compromisedVersion) {
Add-Finding -Level "CRITICAL" -Package $packageName -Version $versionSpec -Source "package.json ($depType)"
}
}
# Check additional compromised packages
if ($AdditionalCompromised.ContainsKey($packageName)) {
$compromisedVersions = $AdditionalCompromised[$packageName]
foreach ($version in $compromisedVersions) {
if (Test-VersionMatch $versionSpec $version) {
Add-Finding -Level "HIGH" -Package $packageName -Version $versionSpec -Source "package.json ($depType)"
break
}
}
}
}
}
}
}
catch {
Write-ColorOutput "[ERROR] Error reading package.json: $($_.Exception.Message)" "Yellow"
}
}
function Test-PackageLockJson {
param([string]$FilePath)
if (!(Test-Path $FilePath)) {
Write-ColorOutput "[INFO] package-lock.json not found" "Blue"
return
}
Write-ColorOutput "[SCAN] Scanning package-lock.json..." "Blue"
try {
$content = Get-Content $FilePath -Raw -ErrorAction Stop
# Use regex for performance on large lock files
foreach ($package in $CompromisedPackages.GetEnumerator()) {
$packageName = $package.Key
$compromisedVersion = $package.Value
# Look for exact version matches in lock file
if ($content -match "`"$([regex]::Escape($packageName))`"[^}]+`"version`":\s*`"$([regex]::Escape($compromisedVersion))`"") {
Add-Finding -Level "CRITICAL" -Package $packageName -Version $compromisedVersion -Source "package-lock.json"
}
}
foreach ($package in $AdditionalCompromised.GetEnumerator()) {
$packageName = $package.Key
$compromisedVersions = $package.Value
foreach ($version in $compromisedVersions) {
if ($content -match "`"$([regex]::Escape($packageName))`"[^}]+`"version`":\s*`"$([regex]::Escape($version))`"") {
Add-Finding -Level "HIGH" -Package $packageName -Version $version -Source "package-lock.json"
}
}
}
}
catch {
Write-ColorOutput "[ERROR] Error reading package-lock.json: $($_.Exception.Message)" "Yellow"
}
}
function Test-YarnLock {
param([string]$FilePath)
if (!(Test-Path $FilePath)) {
Write-ColorOutput "[INFO] yarn.lock not found" "Blue"
return
}
Write-ColorOutput "[SCAN] Scanning yarn.lock..." "Blue"
try {
$content = Get-Content $FilePath -Raw -ErrorAction Stop
foreach ($package in $CompromisedPackages.GetEnumerator()) {
$packageName = $package.Key
$compromisedVersion = $package.Value
# Look for yarn.lock format: package@version:
if ($content -match "^$([regex]::Escape($packageName))@.*$([regex]::Escape($compromisedVersion))") {
Add-Finding -Level "CRITICAL" -Package $packageName -Version $compromisedVersion -Source "yarn.lock"
}
}
foreach ($package in $AdditionalCompromised.GetEnumerator()) {
$packageName = $package.Key
$compromisedVersions = $package.Value
foreach ($version in $compromisedVersions) {
if ($content -match "^$([regex]::Escape($packageName))@.*$([regex]::Escape($version))") {
Add-Finding -Level "HIGH" -Package $packageName -Version $version -Source "yarn.lock"
}
}
}
}
catch {
Write-ColorOutput "[ERROR] Error reading yarn.lock: $($_.Exception.Message)" "Yellow"
}
}
# Main execution
Write-ColorOutput "NPM Supply Chain Attack Scanner" "Cyan"
Write-ColorOutput "========================================" "Cyan"
Write-ColorOutput "[SCAN] Scanning directory: $((Get-Item $Path).FullName)" "Blue"
Write-ColorOutput ""
# Change to target directory
$originalLocation = Get-Location
Set-Location $Path
try {
# Scan files
Test-PackageJson "package.json"
Test-PackageLockJson "package-lock.json"
Test-YarnLock "yarn.lock"
Write-ColorOutput ""
Write-ColorOutput "[RESULTS] Scan Results" "Cyan"
Write-ColorOutput "===============" "Cyan"
if ($Findings.Count -gt 0) {
Write-ColorOutput "[ALERT] Found $($Findings.Count) compromised packages!" "Red"
Write-ColorOutput ""
$criticalCount = ($Findings | Where-Object Level -eq "CRITICAL").Count
$highCount = ($Findings | Where-Object Level -eq "HIGH").Count
if ($criticalCount -gt 0) {
Write-ColorOutput "[CRITICAL] $criticalCount packages" "Red"
}
if ($highCount -gt 0) {
Write-ColorOutput "[HIGH] $highCount packages" "Yellow"
}
Write-ColorOutput ""
Write-ColorOutput "Detailed Findings:" "Yellow"
$Findings | Sort-Object Level, Package | ForEach-Object {
$color = if ($_.Level -eq "CRITICAL") { "Red" } else { "Yellow" }
Write-ColorOutput " [$($_.Level)] $($_.Package)@$($_.Version) ($($_.Source))" $color
}
Write-ColorOutput ""
Write-ColorOutput "[ACTION] Immediate Actions Required:" "Yellow"
# Generate specific remediation commands for found packages
$affectedPackages = $Findings | Select-Object -ExpandProperty Package -Unique
if ($affectedPackages.Count -gt 0) {
Write-ColorOutput "1. Update these specific compromised packages to safe versions:" "White"
foreach ($pkg in $affectedPackages) {
# Suggest updating to latest version (remove the compromised version)
Write-ColorOutput " npm install $pkg@latest --save" "White"
}
Write-ColorOutput ""
Write-ColorOutput " Or check npmjs.com for each package's latest stable version" "White"
} else {
Write-ColorOutput "1. Update the compromised packages found above to their latest safe versions" "White"
}
Write-ColorOutput ""
Write-ColorOutput "2. Clear npm cache and regenerate lock files:" "White"
Write-ColorOutput " npm cache clean --force" "White"
Write-ColorOutput " Remove-Item package-lock.json -ErrorAction SilentlyContinue" "White"
Write-ColorOutput " npm install" "White"
Write-ColorOutput ""
Write-ColorOutput "3. If your app has crypto functionality - ROTATE WALLET KEYS IMMEDIATELY!" "Red"
Write-ColorOutput ""
Write-ColorOutput "[FAILED] SCAN FAILED - Security issues found" "Red"
# Save results to file
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$resultsFile = "npm_scan_results_$timestamp.csv"
$Findings | Export-Csv -Path $resultsFile -NoTypeInformation
Write-ColorOutput "[SAVED] Results saved to: $resultsFile" "Blue"
}
else {
Write-ColorOutput "[SUCCESS] No compromised packages detected!" "Green"
Write-ColorOutput "[SAFE] Your project appears to be safe from this attack" "Green"
}
}
finally {
Set-Location $originalLocation
}
Write-ColorOutput ""
Write-ColorOutput "[INFO] This scanner checks for the September 2025 npm supply chain attack" "Blue"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment