Skip to content

Instantly share code, notes, and snippets.

@UrbanChrisy
Created April 1, 2026 11:19
Show Gist options
  • Select an option

  • Save UrbanChrisy/8edfff4893bc4c4e9cb030d0f8b716f6 to your computer and use it in GitHub Desktop.

Select an option

Save UrbanChrisy/8edfff4893bc4c4e9cb030d0f8b716f6 to your computer and use it in GitHub Desktop.
axios Supply Chain Compromise Scanner
#!/usr/bin/env bun
/**
* axios Supply Chain Compromise Scanner
* ======================================
* Incident: March 30-31, 2026
* Ref: https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan
*
* Scans all repos inside a parent folder for indicators of the
* compromised axios versions and the malicious plain-crypto-js dependency.
*
* Usage:
* bun run scan-axios-compromise.ts [path-to-parent-folder]
*
* The script discovers all subdirectories containing a package.json,
* scans each one independently, and produces a summary table.
*
* If no path is provided, scans the current directory.
*/
import { readdir, readFile, stat } from "node:fs/promises";
import { join, relative } from "node:path";
async function exists(p: string): Promise<boolean> {
try { await stat(p); return true; } catch { return false; }
}
// ─── Compromise Indicators ───────────────────────────────────────────────────
const COMPROMISED_AXIOS_VERSIONS = ["1.14.1", "0.30.4"];
const SAFE_AXIOS_VERSIONS = ["1.14.0", "0.30.3"];
const MALICIOUS_PACKAGES = [
"plain-crypto-js",
"@shadanai/openclaw",
"@qqbrowser/openclaw-qbot",
];
const C2_DOMAINS = ["sfrclak.com", "sfrclak.com:8000"];
const COMPROMISED_NPM_USER = "nrwise";
const SUSPICIOUS_EMAILS = ["ifstap@proton.me", "nrwise@proton.me"];
// ─── ANSI Colors ─────────────────────────────────────────────────────────────
const RED = "\x1b[31m";
const GREEN = "\x1b[32m";
const YELLOW = "\x1b[33m";
const CYAN = "\x1b[36m";
const BOLD = "\x1b[1m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";
const CRITICAL = `${RED}${BOLD}✗ CRITICAL${RESET}`;
const WARNING = `${YELLOW}⚠ WARNING${RESET}`;
const OK = `${GREEN}✓ OK${RESET}`;
const INFO = `${CYAN}ℹ INFO${RESET}`;
// ─── Types ───────────────────────────────────────────────────────────────────
interface Finding {
severity: "critical" | "warning" | "info";
file: string;
message: string;
detail?: string;
}
interface ScanResult {
repoName: string;
repoPath: string;
findings: Finding[];
stats: {
filesScanned: number;
lockfilesChecked: number;
nodeModulesChecked: number;
packageJsonsChecked: number;
};
usesAxios: boolean;
axiosVersions: string[];
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
async function* walk(
dir: string,
skipDirs = new Set([".git", ".cache", ".next", "dist", "build", ".turbo"])
): AsyncGenerator<string> {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (skipDirs.has(entry.name)) continue;
// We DO want to walk into node_modules to find plain-crypto-js
yield* walk(fullPath, skipDirs);
} else {
yield fullPath;
}
}
}
function matchVersion(version: string, targets: string[]): boolean {
// Strip semver prefixes like ^, ~, >=, etc
const cleaned = version.replace(/^[\^~>=<\s]+/, "");
return targets.some((t) => cleaned === t || cleaned.startsWith(t + "-"));
}
// ─── Scanners ────────────────────────────────────────────────────────────────
async function scanLockfile(
filePath: string,
findings: Finding[],
rel: (p: string) => string
) {
const content = await readFile(filePath, "utf-8");
const relPath = rel(filePath);
// Check for compromised axios versions
for (const ver of COMPROMISED_AXIOS_VERSIONS) {
// package-lock.json patterns
const patterns = [
`"axios": "${ver}"`,
`"version": "${ver}"`,
`axios@${ver}`,
`"axios@npm:${ver}"`,
`axios@^${ver}`,
`axios@~${ver}`,
`/axios/-/axios-${ver}.tgz`,
`"axios": "npm:axios@${ver}"`,
];
for (const pat of patterns) {
if (content.includes(pat)) {
findings.push({
severity: "critical",
file: relPath,
message: `Lockfile pins compromised axios@${ver}`,
detail: `Found pattern: ${pat}`,
});
break;
}
}
}
// Check for malicious dependency
for (const pkg of MALICIOUS_PACKAGES) {
if (content.includes(pkg)) {
findings.push({
severity: "critical",
file: relPath,
message: `Lockfile references malicious package "${pkg}"`,
detail:
pkg === "plain-crypto-js"
? "This package is the payload delivery vehicle for the axios compromise. If present, the dropper likely executed."
: `Known compromised package associated with the axios supply chain attack.`,
});
}
}
// Check for C2 domain references
for (const domain of C2_DOMAINS) {
if (content.includes(domain)) {
findings.push({
severity: "critical",
file: relPath,
message: `Lockfile contains C2 domain reference: ${domain}`,
detail: "This is the command-and-control server used by the RAT.",
});
}
}
}
async function scanPackageJson(
filePath: string,
findings: Finding[],
rel: (p: string) => string
) {
const content = await readFile(filePath, "utf-8");
const relPath = rel(filePath);
let pkg: any;
try {
pkg = JSON.parse(content);
} catch {
return;
}
const allDeps = {
...pkg.dependencies,
...pkg.devDependencies,
...pkg.peerDependencies,
...pkg.optionalDependencies,
};
// Check axios version
if (allDeps.axios) {
const ver = allDeps.axios;
for (const compromised of COMPROMISED_AXIOS_VERSIONS) {
if (matchVersion(ver, [compromised])) {
findings.push({
severity: "critical",
file: relPath,
message: `package.json specifies compromised axios version: ${ver}`,
detail: `Downgrade immediately to axios@${compromised.startsWith("1.") ? "1.14.0" : "0.30.3"}`,
});
}
}
// Info: report any axios usage for awareness
if (
!COMPROMISED_AXIOS_VERSIONS.some((v) => matchVersion(ver, [v]))
) {
findings.push({
severity: "info",
file: relPath,
message: `Uses axios@${ver}`,
detail: `Verify this resolves to a safe version (1.14.0 or earlier, 0.30.3 or earlier). Ranges like ^1.x could have resolved to 1.14.1 during the attack window.`,
});
}
}
// Check for malicious packages
for (const malPkg of MALICIOUS_PACKAGES) {
if (allDeps[malPkg]) {
findings.push({
severity: "critical",
file: relPath,
message: `package.json directly depends on malicious package: ${malPkg}`,
});
}
}
// Check overrides / resolutions (people might have pinned a bad version)
const overrides = {
...pkg.overrides,
...pkg.resolutions,
};
if (overrides?.axios) {
for (const compromised of COMPROMISED_AXIOS_VERSIONS) {
if (matchVersion(overrides.axios, [compromised])) {
findings.push({
severity: "critical",
file: relPath,
message: `overrides/resolutions pins axios to compromised version: ${overrides.axios}`,
});
}
}
}
}
async function scanNodeModules(
rootDir: string,
findings: Finding[],
rel: (p: string) => string
): Promise<number> {
let checked = 0;
// Recursively find all node_modules directories
async function findNodeModules(dir: string): Promise<string[]> {
const results: string[] = [];
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return results;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name === ".git") continue;
const full = join(dir, entry.name);
if (entry.name === "node_modules") {
results.push(full);
}
// Also check nested (hoisted) node_modules
if (entry.name !== "node_modules") {
results.push(...(await findNodeModules(full)));
}
}
return results;
}
const nmDirs = await findNodeModules(rootDir);
for (const nmDir of nmDirs) {
checked++;
// 1. Check if plain-crypto-js directory exists (smoking gun)
const plainCryptoDir = join(nmDir, "plain-crypto-js");
if (await exists(plainCryptoDir)) {
findings.push({
severity: "critical",
file: rel(plainCryptoDir),
message:
"plain-crypto-js found in node_modules — THE DROPPER LIKELY EXECUTED",
detail:
"This package is NEVER a legitimate dependency of axios. Its presence means the malicious postinstall hook ran. Rotate ALL credentials immediately.",
});
// Check the setup.js
const setupJs = join(plainCryptoDir, "setup.js");
if (await exists(setupJs)) {
findings.push({
severity: "critical",
file: rel(setupJs),
message: "Malicious setup.js dropper found",
detail:
"This is the obfuscated Node.js dropper that downloads and executes the RAT binary.",
});
}
}
// 2. Check for @shadanai/openclaw
const openclawDir = join(nmDir, "@shadanai", "openclaw");
if (await exists(openclawDir)) {
findings.push({
severity: "critical",
file: rel(openclawDir),
message: "@shadanai/openclaw found — known to vendor the malicious payload",
});
}
// 3. Check for @qqbrowser/openclaw-qbot
const qbotDir = join(nmDir, "@qqbrowser", "openclaw-qbot");
if (await exists(qbotDir)) {
findings.push({
severity: "critical",
file: rel(qbotDir),
message:
"@qqbrowser/openclaw-qbot found — ships tampered axios with plain-crypto-js",
});
}
// 4. Check installed axios version
const axiosPkg = join(nmDir, "axios", "package.json");
if (await exists(axiosPkg)) {
try {
const axiosData = JSON.parse(await readFile(axiosPkg, "utf-8"));
const installedVer = axiosData.version;
if (COMPROMISED_AXIOS_VERSIONS.includes(installedVer)) {
findings.push({
severity: "critical",
file: rel(axiosPkg),
message: `Installed axios version is ${installedVer} — COMPROMISED`,
detail: `This version contains the malicious plain-crypto-js dependency. Downgrade immediately.`,
});
} else {
findings.push({
severity: "info",
file: rel(axiosPkg),
message: `Installed axios version: ${installedVer}`,
});
}
// Check if installed axios has plain-crypto-js in its deps (tampered)
const axiosDeps = axiosData.dependencies || {};
if (axiosDeps["plain-crypto-js"]) {
findings.push({
severity: "critical",
file: rel(axiosPkg),
message:
"Installed axios has plain-crypto-js as a dependency — TAMPERED PACKAGE",
detail:
"Legitimate axios only depends on follow-redirects, form-data, and proxy-from-env. This is unambiguous tampering.",
});
}
} catch { }
}
}
return checked;
}
// ─── Bun-specific: Check bun.lockb ──────────────────────────────────────────
async function scanBunLockb(
filePath: string,
findings: Finding[],
rel: (p: string) => string
) {
// bun.lockb is a binary file — check for a text bun.lock first, then string-search the binary
const relPath = rel(filePath);
try {
// Prefer the text lockfile if it exists alongside bun.lockb
const textLockPath = filePath.replace("bun.lockb", "bun.lock");
if (await exists(textLockPath)) {
await scanLockfile(textLockPath, findings, rel);
return;
}
// For binary lockfile, do a raw string search
const buf = await readFile(filePath);
const asText = new TextDecoder("utf-8", { fatal: false }).decode(buf);
for (const ver of COMPROMISED_AXIOS_VERSIONS) {
if (asText.includes(`axios@${ver}`) || asText.includes(`axios-${ver}`)) {
findings.push({
severity: "critical",
file: relPath,
message: `bun.lockb references compromised axios@${ver}`,
});
}
}
for (const pkg of MALICIOUS_PACKAGES) {
if (asText.includes(pkg)) {
findings.push({
severity: "critical",
file: relPath,
message: `bun.lockb references malicious package: ${pkg}`,
});
}
}
} catch {
findings.push({
severity: "info",
file: relPath,
message:
"Could not fully parse bun.lockb — check manually or use `bun install --yarn` to generate a text lockfile",
});
}
}
// ─── Main Scanner (per-repo, quiet) ──────────────────────────────────────────
async function scan(rootDir: string): Promise<ScanResult> {
const findings: Finding[] = [];
const stats = {
filesScanned: 0,
lockfilesChecked: 0,
nodeModulesChecked: 0,
packageJsonsChecked: 0,
};
const rel = (p: string) => relative(rootDir, p) || ".";
// Phase 1: Scan lockfiles and package.json files
for await (const filePath of walk(rootDir)) {
stats.filesScanned++;
const name = filePath.split("/").pop() || "";
if (
name === "package-lock.json" ||
name === "yarn.lock" ||
name === "pnpm-lock.yaml" ||
name === "bun.lock"
) {
stats.lockfilesChecked++;
await scanLockfile(filePath, findings, rel);
}
if (name === "bun.lockb") {
stats.lockfilesChecked++;
await scanBunLockb(filePath, findings, rel);
}
if (name === "package.json" && !filePath.includes("node_modules")) {
stats.packageJsonsChecked++;
await scanPackageJson(filePath, findings, rel);
}
}
// Phase 2: Scan node_modules
stats.nodeModulesChecked = await scanNodeModules(rootDir, findings, rel);
// Derive axios info from findings
const axiosVersions: string[] = [];
let usesAxios = false;
for (const f of findings) {
const installedMatch = f.message.match(/Installed axios version: (.+)/);
const usesMatch = f.message.match(/Uses axios@(.+)/);
const compromisedMatch = f.message.match(/specifies compromised axios version: (.+)/);
if (installedMatch) {
axiosVersions.push(installedMatch[1]);
usesAxios = true;
}
if (usesMatch) {
if (!axiosVersions.includes(usesMatch[1])) axiosVersions.push(usesMatch[1]);
usesAxios = true;
}
if (compromisedMatch) {
if (!axiosVersions.includes(compromisedMatch[1])) axiosVersions.push(compromisedMatch[1]);
usesAxios = true;
}
}
// Get repo name from package.json
let repoName = rootDir.split("/").pop() || rootDir;
try {
const rootPkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf-8"));
if (rootPkg.name) repoName = rootPkg.name;
} catch { }
return { repoName, repoPath: rootDir, findings, stats, usesAxios, axiosVersions };
}
// ─── Repo Discovery ──────────────────────────────────────────────────────────
async function discoverRepos(parentDir: string): Promise<string[]> {
const repos: string[] = [];
let entries;
try {
entries = await readdir(parentDir, { withFileTypes: true });
} catch {
return repos;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith(".")) continue; // skip .git, .cache, etc.
if (entry.name === "node_modules") continue;
const dirPath = join(parentDir, entry.name);
// A "repo" is any directory with a package.json at its root
if (await exists(join(dirPath, "package.json"))) {
repos.push(dirPath);
}
}
return repos.sort();
}
// ─── Output ──────────────────────────────────────────────────────────────────
function severityIcon(s: string): string {
if (s === "critical") return `${RED}✗${RESET}`;
if (s === "warning") return `${YELLOW}⚠${RESET}`;
return `${GREEN}✓${RESET}`;
}
function statusLabel(result: ScanResult): string {
const crits = result.findings.filter((f) => f.severity === "critical").length;
if (crits > 0) return `${RED}${BOLD}COMPROMISED (${crits})${RESET}`;
if (!result.usesAxios) return `${DIM}no axios${RESET}`;
return `${GREEN}${BOLD}clean${RESET}`;
}
function printRepoDetails(result: ScanResult, parentDir: string) {
const relPath = relative(parentDir, result.repoPath) || ".";
const criticals = result.findings.filter((f) => f.severity === "critical");
const warnings = result.findings.filter((f) => f.severity === "warning");
const infos = result.findings.filter((f) => f.severity === "info");
const icon = criticals.length > 0 ? `${RED}✗${RESET}` : result.usesAxios ? `${GREEN}✓${RESET}` : `${DIM}─${RESET}`;
console.log(`\n ${icon} ${BOLD}${result.repoName}${RESET} ${DIM}(${relPath})${RESET}`);
if (!result.usesAxios && criticals.length === 0) {
console.log(` ${DIM}Does not use axios — skipped${RESET}`);
return;
}
if (result.axiosVersions.length > 0) {
console.log(` ${DIM}axios versions: ${result.axiosVersions.join(", ")}${RESET}`);
}
if (criticals.length > 0) {
for (const f of criticals) {
console.log(` ${CRITICAL} ${f.message}`);
console.log(` ${DIM} ${f.file}${RESET}`);
if (f.detail) console.log(` ${DIM} ${f.detail}${RESET}`);
}
}
if (warnings.length > 0) {
for (const f of warnings) {
console.log(` ${WARNING} ${f.message}`);
console.log(` ${DIM} ${f.file}${RESET}`);
}
}
// Only show info findings if there are no criticals (keep it concise)
if (criticals.length === 0 && infos.length > 0) {
for (const f of infos) {
console.log(` ${INFO} ${f.message}`);
if (f.detail) console.log(` ${DIM} ${f.detail}${RESET}`);
}
}
}
function printSummaryTable(results: ScanResult[], parentDir: string) {
const totalRepos = results.length;
const reposWithAxios = results.filter((r) => r.usesAxios);
const reposCompromised = results.filter(
(r) => r.findings.some((f) => f.severity === "critical")
);
const reposClean = reposWithAxios.filter(
(r) => !r.findings.some((f) => f.severity === "critical")
);
const reposWithoutAxios = results.filter((r) => !r.usesAxios);
// ─── Summary table ───
console.log(`\n${BOLD}─── Summary Table ──────────────────────────────────────────${RESET}\n`);
// Column widths
const nameWidth = Math.max(20, ...results.map((r) => r.repoName.length + 2));
const header = ` ${"Repo".padEnd(nameWidth)} ${"Status".padEnd(22)} ${"axios version(s)".padEnd(24)} ${"Findings"}`;
console.log(`${BOLD}${header}${RESET}`);
console.log(` ${"─".repeat(nameWidth)} ${"─".repeat(22)} ${"─".repeat(24)} ${"─".repeat(12)}`);
for (const r of results) {
const crits = r.findings.filter((f) => f.severity === "critical").length;
const warns = r.findings.filter((f) => f.severity === "warning").length;
const infs = r.findings.filter((f) => f.severity === "info").length;
let status: string;
if (crits > 0) status = `${RED}COMPROMISED${RESET}`;
else if (!r.usesAxios) status = `${DIM}no axios${RESET}`;
else status = `${GREEN}clean${RESET}`;
const versions = r.axiosVersions.length > 0 ? r.axiosVersions.join(", ") : "—";
const findingCounts: string[] = [];
if (crits > 0) findingCounts.push(`${RED}${crits}C${RESET}`);
if (warns > 0) findingCounts.push(`${YELLOW}${warns}W${RESET}`);
if (infs > 0) findingCounts.push(`${DIM}${infs}I${RESET}`);
const findingsStr = findingCounts.length > 0 ? findingCounts.join(" ") : `${DIM}—${RESET}`;
// We need to account for ANSI codes in padding — use raw length for display
const rawStatus = status.replace(/\x1b\[[0-9;]*m/g, "");
const rawVersions = versions.replace(/\x1b\[[0-9;]*m/g, "");
const statusPad = " ".repeat(Math.max(0, 22 - rawStatus.length));
const versionPad = " ".repeat(Math.max(0, 24 - rawVersions.length));
console.log(` ${r.repoName.padEnd(nameWidth)} ${status}${statusPad} ${versions}${versionPad} ${findingsStr}`);
}
// ─── Totals ───
console.log(`\n${BOLD}─── Totals ─────────────────────────────────────────────────${RESET}\n`);
console.log(` Repos scanned: ${totalRepos}`);
console.log(` Uses axios: ${reposWithAxios.length}`);
if (reposCompromised.length > 0) {
console.log(` ${RED}${BOLD}Compromised: ${reposCompromised.length}${RESET}`);
} else {
console.log(` Compromised: 0`);
}
console.log(` Clean (with axios):${reposClean.length > 0 ? " " + reposClean.length : " 0"}`);
console.log(` No axios: ${reposWithoutAxios.length}`);
const totalFiles = results.reduce((s, r) => s + r.stats.filesScanned, 0);
const totalLockfiles = results.reduce((s, r) => s + r.stats.lockfilesChecked, 0);
const totalPkgJsons = results.reduce((s, r) => s + r.stats.packageJsonsChecked, 0);
const totalNodeMods = results.reduce((s, r) => s + r.stats.nodeModulesChecked, 0);
console.log();
console.log(` ${DIM}Total files walked: ${totalFiles}${RESET}`);
console.log(` ${DIM}Total lockfiles checked: ${totalLockfiles}${RESET}`);
console.log(` ${DIM}Total package.json checked: ${totalPkgJsons}${RESET}`);
console.log(` ${DIM}Total node_modules checked: ${totalNodeMods}${RESET}`);
// ─── Remediation (only if compromised) ───
if (reposCompromised.length > 0) {
console.log(`\n${RED}${BOLD}═══════════════════════════════════════════════════════════${RESET}`);
console.log(`${RED}${BOLD} 🚨 COMPROMISE DETECTED — IMMEDIATE ACTION REQUIRED${RESET}`);
console.log(`${RED}${BOLD}═══════════════════════════════════════════════════════════${RESET}`);
console.log();
console.log(` Affected repos:`);
for (const r of reposCompromised) {
const relPath = relative(parentDir, r.repoPath);
console.log(` ${RED}✗${RESET} ${r.repoName} ${DIM}(${relPath})${RESET}`);
}
console.log();
console.log(` ${BOLD}Remediation steps:${RESET}`);
console.log(` 1. Downgrade axios to 1.14.0 (or 0.30.3 for legacy)`);
console.log(` 2. Delete node_modules/plain-crypto-js/ directories`);
console.log(` 3. Clean install: rm -rf node_modules && bun install`);
console.log(` 4. ${RED}${BOLD}ROTATE ALL SECRETS${RESET} — SSH keys, cloud tokens, API keys, .env files`);
console.log(` 5. Audit CI/CD pipeline secrets and deployment credentials`);
console.log(` 6. Check for connections to sfrclak.com (C2 server)`);
console.log(` 7. Review for persistence mechanisms (cron jobs, launch agents)`);
} else {
console.log(`\n ${GREEN}${BOLD}✓ No indicators of compromise across any repo.${RESET}`);
console.log(` ${DIM} If you ran \`npm/bun install\` between 00:21–03:15 UTC on March 31 2026,${RESET}`);
console.log(` ${DIM} consider auditing CI/CD logs and rotating secrets as a precaution.${RESET}`);
}
console.log(
`\n${DIM} Ref: https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan${RESET}`
);
console.log(
`${DIM} Ref: https://www.elastic.co/security-labs/axios-one-rat-to-rule-them-all${RESET}\n`
);
}
// ─── Entry ───────────────────────────────────────────────────────────────────
async function main() {
const targetDir = process.argv[2] || process.cwd();
try {
const s = await stat(targetDir);
if (!s.isDirectory()) {
console.error(`${RED}Error: ${targetDir} is not a directory${RESET}`);
process.exit(2);
}
} catch {
console.error(`${RED}Error: ${targetDir} does not exist${RESET}`);
process.exit(2);
}
console.log(`\n${BOLD}═══════════════════════════════════════════════════════════${RESET}`);
console.log(`${BOLD} axios Supply Chain Compromise Scanner${RESET}`);
console.log(`${DIM} Incident: March 30-31, 2026 | Multi-repo mode${RESET}`);
console.log(`${BOLD}═══════════════════════════════════════════════════════════${RESET}`);
// Discover repos
console.log(`\n${CYAN}Discovering repos in:${RESET} ${targetDir}\n`);
const repoPaths = await discoverRepos(targetDir);
if (repoPaths.length === 0) {
console.log(` ${YELLOW}No repos found.${RESET} Looking for directories containing a package.json.`);
console.log(` ${DIM}Make sure you're pointing at a parent folder that contains your repos.${RESET}\n`);
process.exit(0);
}
console.log(` Found ${BOLD}${repoPaths.length}${RESET} repo(s):\n`);
for (const p of repoPaths) {
console.log(` ${DIM}•${RESET} ${relative(targetDir, p)}`);
}
// Scan each repo
console.log(`\n${BOLD}─── Scanning ───────────────────────────────────────────────${RESET}`);
const results: ScanResult[] = [];
for (const repoPath of repoPaths) {
const relPath = relative(targetDir, repoPath);
process.stdout.write(` ${DIM}Scanning ${relPath}...${RESET}`);
const result = await scan(repoPath);
results.push(result);
const crits = result.findings.filter((f) => f.severity === "critical").length;
if (crits > 0) {
process.stdout.write(` ${RED}${crits} critical${RESET}\n`);
} else if (result.usesAxios) {
process.stdout.write(` ${GREEN}clean${RESET}\n`);
} else {
process.stdout.write(` ${DIM}no axios${RESET}\n`);
}
}
// Detailed findings for repos with issues
const reposWithFindings = results.filter(
(r) => r.findings.some((f) => f.severity === "critical" || f.severity === "warning") || r.usesAxios
);
if (reposWithFindings.length > 0) {
console.log(`\n${BOLD}─── Detailed Findings ──────────────────────────────────────${RESET}`);
for (const r of reposWithFindings) {
printRepoDetails(r, targetDir);
}
}
// Summary table
printSummaryTable(results, targetDir);
// Exit code
const anyCompromised = results.some((r) =>
r.findings.some((f) => f.severity === "critical")
);
process.exit(anyCompromised ? 1 : 0);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment