@lateos/npm-scan 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -0
- package/.github/workflows/scan.yml +1 -0
- package/AGENTS.md +1 -0
- package/CONTRIBUTING.md +1 -0
- package/LICENSING.md +1 -0
- package/backend/db/schema.sql +1 -0
- package/backend/db.js +1 -0
- package/backend/detectors/atk-001-lifecycle.js +1 -0
- package/backend/detectors/atk-002-obfusc.js +1 -0
- package/backend/detectors/atk-003-creds.js +1 -0
- package/backend/detectors/atk-004-persist.js +1 -0
- package/backend/detectors/atk-005-exfil.js +1 -0
- package/backend/detectors/atk-006-depconf.js +1 -0
- package/backend/detectors/atk-007-typosquat.js +1 -0
- package/backend/detectors/index.js +1 -0
- package/backend/detectors.test.js +1 -0
- package/backend/fetch.js +1 -0
- package/backend/license.js +1 -0
- package/backend/sbom.js +1 -0
- package/cli/cli.js +1 -0
- package/docker/Dockerfile.cli +1 -0
- package/docker/docker-compose.yml +1 -0
- package/docs/attack-taxonomy.md +1 -0
- package/docs/project-plan.md +372 -0
- package/package.json +17 -4
- package/tests/corpus/malicious/shai-hulud.tgz +0 -0
- package/tests/corpus/run.js +27 -0
- package/cli.js +0 -4
- /package/{index.js → backend/index.js} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
name: CI\n\non:\n push:\n branches: [ main ]\n pull_request:\n branches: [ main ]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: '20'\n cache: 'npm'\n - run: npm ci\n - run: npm run lint\n - run: npm run test\n - run: npm run build\n # Self-scan stub\n - run: echo 'Self-scan: npm run scan package.json' # Phase 1+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
name: npm-scan\n\non: [pull_request]\n\njobs:\n scan:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n - run: npm ci\n - run: npx @lateos/npm-scan@latest scan-lockfile\n # Or: npm-scan scan-lockfile (if global)\n # Fail PR on high/critical
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# AGENTS.md\n\n## Project\nESM Node.js CLI monorepo for npm-scan supply chain scanner.\n\n## Verification\nNo lint/test deps yet. Scripts stubbed in package.json.\nRun `npm run lint test build`.\n\n## Architecture\n- `cli/`: Commander.js entrypoints (Phase 1)\n- `backend/`: Core logic, license.js, db/schema.sql\n- `docker/`: Multi-arch images (cli, pipeline)\n- `docs/`: project-plan.md, attack-taxonomy.md\n\nFollow project-plan.md phases/ATK.\n\n## Conventions\n- No deps—verify package.json before libs.\n- License: Apache-2.0 + Commons Clause (LICENSING.md).\n- Local git (no remote).\n- Phase 0 complete: foundation stubs ready for Phase 1 detectors.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# CONTRIBUTING.md\n\nThank you for contributing to npm-scan!\n\n## Development Workflow\n\n1. Fork repo, create feature branch `feat/atk-xxx-description`.\n2. Run `npm run lint test`.\n3. Update CHANGELOG.md.\n4. PR with self-review.\n\n## New ATK Entry / Detector\n\nATK changes require:\n- PoC malicious package sample.\n- Detection rule/code.\n- False positive analysis (test corpus).\n- NIST 800-161 mapping.\n\nUpdate docs/attack-taxonomy.md; bump version.\n\n## Licensing\n\nSee [LICENSING.md](LICENSING.md). Core Apache-2.0; premium Commons Clause.\n\n## No-Go\n- ML/ telemetry until Phase 4.\n- Secrets/keys in code/PR.\n- Sandbox changes without threat model update.\n\n## Test Corpus\n\nAdd to `tests/corpus/clean/` and `malicious/` for CI.\n\nPRs reviewed in 48h.
|
package/LICENSING.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# LICENSING.md\n\n## Model: Apache-2.0 core + Commons Clause premium\n\n### Core (Apache-2.0):\n- Static analysis engine, ATK-001–007 detectors, CLI, lockfile scanner, SBOM output (CycloneDX), GitHub Action, Docker images, JSON output, SQLite-backed local storage, basic HTML report.\n\n### Premium (Apache-2.0 + Commons Clause):\n- Dynamic sandbox (ATK-008+), advanced compliance reports (PDF, regulatory templates), SIEM connectors, reachability analysis, team dashboard, SSO, audit logs, API/webhooks, on-prem/air-gapped licenses, priority support.\n\n## Commons Clause\nThe Commons Clause prohibits selling our open core software as a service. See https://commonsclause.com/ for details.\n\n## Feature Flags\nPremium features gated by license key validated at runtime. Keys issued per-seat CLI, per-org hosted.\n\nFull Apache-2.0 license in LICENSE file (TBD).\n\nLicensing boundaries defined before external contributions. Changes require PR updating this file.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
-- SQLite schema for local CLI mode (free tier)\n-- Tables: scans, findings (ATK-linked)\n\nCREATE TABLE IF NOT EXISTS scans (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n package_name TEXT NOT NULL,\n version TEXT,\n scanned_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n status TEXT DEFAULT 'completed',\n sbom_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS findings (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scan_id INTEGER NOT NULL,\n atk_id TEXT NOT NULL REFERENCES attack_taxonomy(id),\n severity TEXT CHECK (severity IN ('info', 'low', 'medium', 'high', 'critical')),\n description TEXT,\n evidence TEXT,\n mitigation TEXT,\n FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE CASCADE\n);\n\n-- View for reports\nCREATE VIEW scan_findings AS\nSELECT s.*, f.* FROM scans s\nJOIN findings f ON s.id = f.scan_id;\n\n-- Indexes\nCREATE INDEX idx_scans_package ON scans(package_name);\nCREATE INDEX idx_findings_atk ON findings(atk_id);\nCREATE INDEX idx_findings_severity ON findings(severity);
|
package/backend/db.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';\nimport fs from 'fs';\nimport path from 'path';\n\nconst DB_PATH = 'npm-scan.db';\n\nlet db;\n\nfunction init() {\n db = new Database(DB_PATH);\n const schemaPath = path.join(process.cwd(), 'backend', 'db', 'schema.sql');\n const schema = fs.readFileSync(schemaPath, 'utf8');\n db.exec(schema);\n}\n\ninit();\n\nexport function saveScan(pkgName, version = 'latest', findings = []) {\n const scanStmt = db.prepare('INSERT INTO scans (package_name, version) VALUES (?, ?)');\n const scanId = scanStmt.run(pkgName, version).lastInsertRowid;\n\n const findStmt = db.prepare('INSERT INTO findings (scan_id, atk_id, severity, description, evidence) VALUES (?, ?, ?, ?, ?)');\n for (const f of findings) {\n findStmt.run(scanId, f.id, f.severity, f.title || f.description, f.evidence || '');\n }\n\n return scanId;\n}\n\nexport function getRecentScans(limit = 10) {\n return db.prepare('SELECT * FROM scans ORDER BY scanned_at DESC LIMIT ?').all(limit);\n}\n\nexport function getFindings(scanId) {\n return db.prepare('SELECT * FROM findings WHERE scan_id = ?').all(scanId);\n}\n\nexport function close() {\n db.close();\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function scan(pkgJson, files = []) {\n const findings = [];\n const scripts = pkgJson.scripts || {};\n const suspicious = Object.keys(scripts).filter(s => /pre|post|install/i.test(s));\n if (suspicious.length) {\n findings.push({\n id: 'ATK-001',\n severity: 'high',\n title: 'Malicious lifecycle scripts',\n description: 'Suspicious install hooks',\n evidence: suspicious.join(', ')\n });\n }\n return findings;\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function scan(pkgJson, files = []) {\n const findings = [];\n const code = files.map(f => f.content).join('\\n');\n if (/eval\\(|atob\\(|Buffer.from\\(/g.test(code)) {\n findings.push({\n id: 'ATK-002',\n severity: 'medium',\n title: 'Obfuscated payload',\n description: 'Eval/base64/hex patterns',\n evidence: 'eval/atob detected'\n });\n }\n return findings;\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function scan(pkgJson, files = []) {\n const findings = [];\n const code = files.map(f => f.content).join('\\n');\n if (/process.env.(NPM|GIT|AWS|SSH)|\\.npmrc/g.test(code)) {\n findings.push({\n id: 'ATK-003',\n severity: 'high',\n title: 'Credential harvesting',\n description: 'Env/ .npmrc access',\n evidence: 'NPM_TOKEN/.npmrc match'\n });\n }\n return findings;\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function scan(pkgJson, files = []) {\n const findings = [];\n const code = files.map(f => f.content).join('\\n');\n if (/mkdir.*(\\.vscode|\\.claude|\\.cursor)/g.test(code)) {\n findings.push({\n id: 'ATK-004',\n severity: 'high',\n title: 'Persistence via editor configs',\n evidence: '.vscode mkdir match'\n });\n }\n return findings;\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function scan(pkgJson, files = []) {\n const findings = [];\n const code = files.map(f => f.content).join('\\n');\n if (/fetch|curl.*(github|pastebin|c2)|post.*data/g.test(code)) {\n findings.push({\n id: 'ATK-005',\n severity: 'critical',\n title: 'Network exfiltration',\n evidence: 'curl/fetch C2'\n });\n }\n return findings;\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function scan(pkgJson) {\n const findings = [];\n const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };\n const squat = Object.keys(deps).filter(d => /squat|confuse|typo/i.test(d.toLowerCase()));\n if (squat.length) {\n findings.push({\n id: 'ATK-006',\n severity: 'medium',\n title: 'Dependency confusion',\n evidence: squat.join(', ')\n });\n }\n return findings;\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function scan(pkgJson) {\n const findings = [];\n const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };\n // Stub edit-distance (e.g. lodash → lodashh)\n const suspects = Object.keys(deps).filter(d => d.length > 4 && Math.random() < 0.1); // stub\n if (suspects.length) {\n findings.push({\n id: 'ATK-007',\n severity: 'low',\n title: 'Typosquatting suspects',\n evidence: suspects.join(', ')\n });\n }\n return findings;\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// backend/detectors/index.js\n\nimport * as atk001 from './atk-001-lifecycle.js';\nimport * as atk002 from './atk-002-obfusc.js';\nimport * as atk003 from './atk-003-creds.js';\nimport * as atk004 from './atk-004-persist.js';\nimport * as atk005 from './atk-005-exfil.js';\nimport * as atk006 from './atk-006-depconf.js';\nimport * as atk007 from './atk-007-typosquat.js';\n\nexport async function runAll(pkgJson, files = []) {\n const findings = [];\n findings.push(...await atk001.scan(pkgJson, files));\n findings.push(...await atk002.scan(pkgJson, files));\n findings.push(...await atk003.scan(pkgJson, files));\n findings.push(...await atk004.scan(pkgJson, files));\n findings.push(...await atk005.scan(pkgJson, files));\n findings.push(...await atk006.scan(pkgJson, files));\n findings.push(...await atk007.scan(pkgJson, files));\n return findings.sort((a, b) => b.severity.localeCompare(a.severity));\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { test, mock } from 'node:test';\nimport assert from 'assert/strict';\n\nimport * as detectors from '../detectors/index.js';\n\ntest('detectors runAll empty', async () => {\n const findings = await detectors.runAll({});\n assert.equal(findings.length, 0);\n});\n\ntest('ATK-001 detects preinstall', async () => {\n const pkg = { scripts: { preinstall: 'malicious' } };\n const findings = await detectors.runAll(pkg);\n assert(findings.some(f => f.atk === 'ATK-001'));\n});
|
package/backend/fetch.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import fetch from 'node-fetch';\nimport AdmZip from 'adm-zip';\nimport { globSync } from 'glob';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nexport async function fetchPackage(target) {\n const metaRes = await fetch(`https://registry.npmjs.org/${target}/latest`);\n const meta = await metaRes.json();\n const tarUrl = meta.dist.tarball;\n const tarRes = await fetch(tarUrl);\n const buffer = Buffer.from(await tarRes.arrayBuffer());\n if (buffer.length > 500 * 1024 * 1024) throw new Error('Tarball too large');\n const tmpDir = os.tmpdir() + '/npm-scan-' + Date.now();\n fs.mkdirSync(tmpDir, { recursive: true });\n const zip = new AdmZip(buffer);\n zip.extractAllTo(tmpDir, true);\n const pkgPath = path.join(tmpDir, 'package', 'package.json');\n const pkgJsonStr = fs.readFileSync(pkgPath, 'utf8');\n const pkgJson = JSON.parse(pkgJsonStr);\n const jsFiles = globSync(path.join(tmpDir, 'package', '**/*.js')).map(p => ({\n path: p,\n content: fs.readFileSync(p, 'utf8')\n }));\n return { pkgJson, jsFiles, tmpDir };\n}\n\nexport function cleanup(tmpDir) {\n require('fs').rmSync(tmpDir, { recursive: true, force: true });\n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/** @module license */\nexport function validateLicense(key, feature = '*') {\n // Stub: runtime validation (env var or file)\n if (!key || !key.startsWith('npm-scan-premium-')) {\n throw new Error(`Invalid license for feature: ${feature}`);\n }\n // TODO: crypto verify signature, expiry, seats\n return true;\n}\n\nexport function isFeatureEnabled(feature, licenseKey = process.env.NPM_SCAN_LICENSE_KEY) {\n try {\n return validateLicense(licenseKey, feature);\n } catch {\n return false;\n }\n}\n\n// Usage: if (!isFeatureEnabled('dynamic-sandbox')) { console.warn('Upgrade for sandbox'); }
|
package/backend/sbom.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { CycloneDX } from 'cyclonedx-node';\n\nexport function generateSBOM(pkgJson, findings, format = 'json') {\n const sbom = new CycloneDX({specVersion: '1.5'});\n // Components\n sbom.addComponent({\n name: pkgJson.name,\n version: pkgJson.version || 'unknown',\n type: 'library',\n purl: `pkg:npm/${pkgJson.name}@${pkgJson.version}`\n });\n // Vulnerabilities from findings\n for (const f of findings) {\n sbom.addVulnerability({\n id: f.id,\n title: f.title,\n severity: f.severity.toUpperCase(),\n description: f.description,\n recommendation: f.mitigation || 'Review evidence'\n });\n }\n return format === 'xml' ? sbom.toJsonXml() : sbom.toJson();\n}
|
package/cli/cli.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst program = new Command()\n .name('npm-scan')\n .description('npm supply chain security scanner')\n .version('0.1.1');\n\nprogram\n .command('scan')\n .description('Scan package')\n .argument('<target>', 'package name')\n .option('-l, --license-key <key>', 'Premium license')\n .action(async (target, options) => {\n try {\n const { pkgJson, jsFiles, tmpDir } = await import('../backend/fetch.js').then(m => m.fetchPackage(target));\n const findings = await import('../backend/detectors/index.js').then(m => m.runAll(pkgJson, jsFiles));\n const { saveScan } = await import('../backend/db.js');\n const scanId = saveScan(target, 'latest', findings);\n console.log(JSON.stringify({scanId, findings}, null, 2));\n import('../backend/fetch.js').then(m => m.cleanup(tmpDir));\n } catch (e) {\n console.error(e.message);\n }\n });\n\nprogram\n .command('scan-lockfile')\n .description('Scan package-lock.json')\n .action(() => {\n console.log('Scanning lockfile...');\n });\n\nprogram\n .command('report')\n .description('Generate report')\n .option('-i, --id <id>', 'Scan ID')\n .option('--sbom [format]', 'CycloneDX SBOM (json/xml)', 'json')\n .action(async (options) => {\n const { getRecentScans, getFindings } = await import('../backend/db.js');\n if (options.id) {\n const findings = getFindings(options.id);\n if (options.sbom) {\n const pkg = { name: 'scanned-pkg', version: 'unknown' }; // from scan\n const { generateSBOM } = await import('../backend/sbom.js');\n const sbom = generateSBOM(pkg, findings, options.sbom);\n console.log(sbom);\n } else {\n console.log(JSON.stringify(findings, null, 2));\n }\n } else {\n const scans = getRecentScans();\n console.log('Recent scans:', JSON.stringify(scans, null, 2));\n }\n });\n\nprogram.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FROM node:20-alpine AS cli\n\nWORKDIR /app\nCOPY package.json .\nRUN npm ci --only=production\nCOPY . .\n\nENTRYPOINT [\"node\", \"cli/cli.js\"]\n\n# Multi-arch build: docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/lateos/npm-scan:cli .
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version: '3.8'\nservices:\n cli:\n build:\n context: ..\n dockerfile: docker/Dockerfile.cli\n image: ghcr.io/lateos/npm-scan:cli\n\n # Full pipeline stubs (Phase 1+)\n enumerator:\n image: ghcr.io/lateos/npm-scan:enumerator\n fetcher:\n image: ghcr.io/lateos/npm-scan:fetcher\n depends_on: [redis]\n analyzer-static:\n image: ghcr.io/lateos/npm-scan:analyzer\n # ...\n\n redis:\n image: redis:alpine\n\n# Usage: docker compose up cli
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# npm Attack Taxonomy (ATK)\n\nVersioned anchor for detectors, PRs, reports. Each entry: attack class, detection surface, evasion surface, NIST 800-161 mapping.\n\n## ATK Table\n\n| ID | Class | Detection Surface | Evasion Surface | NIST 800-161 | Status |\n|---------|--------------------------------------------|-------------------|--------------------------|------------------|--------|\n| ATK-001 | Malicious lifecycle scripts (pre/postinstall) | Static | Obfuscation | SR-3.1 | Phase 1 |\n| ATK-002 | Obfuscated payload (hex/base64/eval) | Static | Polyglots | SR-4.2 | Phase 1 |\n| ATK-003 | Credential harvesting (.npmrc/SSH/env) | Static+Dynamic | Conditional triggers | SR-5.3 | Phase 1 |\n| ATK-004 | Persistence (.vscode/.claude/.cursor) | Static | Hidden files | SR-6.4 | Phase 1 |\n| ATK-005 | Network exfiltration (GitHub/DNS/HTTP C2) | Static+Dynamic | Encrypted payloads | SR-7.5 | Phase 1 |\n| ATK-006 | Dependency confusion/namespace squatting | Static (lock) | Typosquatting | SR-2.2 | Phase 1 |\n| ATK-007 | Typosquatting (edit-distance top-N) | Static | Homoglyphs | SR-2.1 | Phase 1 |\n| ATK-008 | Tarball tampering (tarball ≠ repo) | Static (diff) | Mirror repos | SR-8.1 | Phase 2 |\n| ATK-009 | Conditional triggers (CI/time) | Dynamic | Env probes | SR-9.2 | Phase 2 |\n| ATK-010 | Sandbox evasion | Dynamic | Anti-analysis | SR-10.3 | Phase 2 |\n| ATK-011 | Transitive propagation (worm) | Dynamic | Peer deps | SR-11.4 | Phase 3 |\n\n## Governance\nNew ATK requires PR with: PoC sample, detection rule, FP analysis, NIST map. Published here; referenced in reports.\n\nChanges version this file.
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# Project Plan: npm-scan
|
|
2
|
+
## Enhanced Open-Core npm Supply Chain Security Scanner
|
|
3
|
+
### (Successor to / Evolution of Package-Inferno)
|
|
4
|
+
|
|
5
|
+
**Date:** May 2026
|
|
6
|
+
**Version:** 1.1
|
|
7
|
+
**Author:** Lateos (lateos.ai)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Project Vision & Objectives
|
|
12
|
+
|
|
13
|
+
Build a best-in-class, developer-friendly npm supply chain security tool that detects sophisticated attacks like Mini Shai-Hulud (and future variants) through behavioral, static, and hybrid analysis.
|
|
14
|
+
|
|
15
|
+
### Core Goals
|
|
16
|
+
|
|
17
|
+
- Detect malicious patterns: preinstall hooks, obfuscation, credential harvesting, persistence via `.claude`/`.vscode`, GitHub exfiltration, and emerging variants
|
|
18
|
+
- Provide enterprise-grade compliance reporting and SIEM integrations
|
|
19
|
+
- Follow open-core model: generous free tier + gated premium features
|
|
20
|
+
- Distribute as both npm CLI and Docker images
|
|
21
|
+
- Be easy to run locally, in CI, and at scale
|
|
22
|
+
|
|
23
|
+
### Differentiation
|
|
24
|
+
|
|
25
|
+
Hybrid analysis + a formal, versioned attack taxonomy + a strong compliance/SIEM story + excellent UX.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Attack Taxonomy (ATK Series) — The Moat
|
|
30
|
+
|
|
31
|
+
Before writing detection code, publish and maintain a versioned **npm Attack Taxonomy (ATK)**. This is modeled on the IPI taxonomy pattern and serves as the anchor for all detector development, contributor PRs, and marketing claims.
|
|
32
|
+
|
|
33
|
+
Each entry defines: attack class, detection surface (static/dynamic/both), evasion surface, and mapping to NIST 800-161 controls.
|
|
34
|
+
|
|
35
|
+
| ID | Class | Detection Surface | Status |
|
|
36
|
+
|----|-------|-------------------|--------|
|
|
37
|
+
| ATK-001 | Malicious lifecycle scripts (`preinstall`, `postinstall`, `install`) | Static | Phase 1 |
|
|
38
|
+
| ATK-002 | Obfuscated payload delivery (hex encoding, base64, `eval`) | Static | Phase 1 |
|
|
39
|
+
| ATK-003 | Credential harvesting (env var scraping, `.npmrc`, SSH key access) | Static + Dynamic | Phase 1 |
|
|
40
|
+
| ATK-004 | Persistence via editor/tool config dirs (`.vscode`, `.claude`, `.cursor`) | Static | Phase 1 |
|
|
41
|
+
| ATK-005 | Network exfiltration (GitHub API, DNS tunneling, HTTP POST to C2) | Static + Dynamic | Phase 1 |
|
|
42
|
+
| ATK-006 | Dependency confusion / namespace squatting | Static (lockfile) | Phase 1 |
|
|
43
|
+
| ATK-007 | Typosquatting (edit-distance matching against top-N packages) | Static | Phase 1 |
|
|
44
|
+
| ATK-008 | Tarball tampering (published tarball ≠ source repo) | Static (diff) | Phase 2 |
|
|
45
|
+
| ATK-009 | Conditional/dormant triggers (CI env detection, time-based activation) | Dynamic | Phase 2 |
|
|
46
|
+
| ATK-010 | Sandbox evasion detection (anti-analysis behaviors) | Dynamic | Phase 2 |
|
|
47
|
+
| ATK-011 | Transitive supply chain propagation (worm-style lateral spread) | Dynamic | Phase 3 |
|
|
48
|
+
|
|
49
|
+
> **Governance:** ATK entries are versioned. New entries require a PR with: proof-of-concept sample, detection rule, false positive analysis, and NIST 800-161 control mapping. The taxonomy is published at `docs/attack-taxonomy.md` and referenced in all scan reports.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 3. Licensing — Decided Before the First PR
|
|
54
|
+
|
|
55
|
+
Licensing boundaries must be defined in `LICENSING.md` before accepting any external contributions.
|
|
56
|
+
|
|
57
|
+
### Model: Apache-2.0 core + Commons Clause premium
|
|
58
|
+
|
|
59
|
+
- **Core (Apache-2.0):** Static analysis engine, ATK-001–007 detectors, CLI, lockfile scanner, SBOM output (CycloneDX), GitHub Action, Docker images, JSON output, SQLite-backed local storage, basic HTML report.
|
|
60
|
+
- **Premium (Apache-2.0 + Commons Clause):** Dynamic sandbox (ATK-008+), advanced compliance reports (PDF, regulatory templates), SIEM connectors, reachability analysis, team dashboard, SSO, audit logs, API/webhooks, on-prem/air-gapped licenses, priority support.
|
|
61
|
+
|
|
62
|
+
> **Why Commons Clause over BSL:** Commons Clause is lighter-weight, avoids the community friction HashiCorp and Elasticsearch created with BSL transitions, and the boundary ("you may not sell this software as a service") is unambiguous. BSL is a fallback only if legal counsel recommends it.
|
|
63
|
+
|
|
64
|
+
### Feature Flags
|
|
65
|
+
|
|
66
|
+
Premium features are gated by a license key validated at runtime. The key system skeleton ships in Phase 0. Keys are issued per-seat for CLI, per-org for hosted.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4. Core Requirements
|
|
71
|
+
|
|
72
|
+
### Free / Open-Source Tier
|
|
73
|
+
|
|
74
|
+
- Static analysis (ATK-001–007): obfuscation, credential patterns, lifecycle scripts, YARA
|
|
75
|
+
- CLI: `scan`, `scan-lockfile`, `report` commands
|
|
76
|
+
- Docker-based full pipeline (Enumerator → Fetcher → Analyzer → Dashboard)
|
|
77
|
+
- SBOM output: CycloneDX (Phase 1), SPDX (Phase 2)
|
|
78
|
+
- Basic HTML report (Phase 1); PDF report is premium (Phase 2)
|
|
79
|
+
- GitHub Action
|
|
80
|
+
- Policy-as-code engine (YAML config, free)
|
|
81
|
+
- SQLite for local/CLI mode — zero external dependencies
|
|
82
|
+
|
|
83
|
+
### Premium Features (license key or hosted SaaS)
|
|
84
|
+
|
|
85
|
+
- Dynamic sandbox / hybrid analysis (ATK-008–011, safe hook execution with syscall monitoring)
|
|
86
|
+
- Advanced compliance reports (PDF/HTML, regulatory mapping: NIST 800-161, EU CRA, SOC 2, ISO 27001, DORA)
|
|
87
|
+
- Reachability analysis (parse call graphs, surface only reachable findings)
|
|
88
|
+
- Full SIEM connectors (Splunk TA, Microsoft Sentinel Solution, Elastic integration, QRadar)
|
|
89
|
+
- Team dashboard, SSO, audit logs, high-scale orchestration
|
|
90
|
+
- Priority support and on-prem/air-gapped licenses
|
|
91
|
+
- OPA/Rego policy engine (YAML for free tier)
|
|
92
|
+
- PostgreSQL backend (team/hosted tier)
|
|
93
|
+
|
|
94
|
+
> **Note on ML-assisted false-positive reduction:** Explicitly deferred until real scan telemetry exists. Will not appear in roadmap until Phase 4+ with data to justify it.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 5. Tech Stack
|
|
99
|
+
|
|
100
|
+
| Layer | Technology | Notes |
|
|
101
|
+
|-------|------------|-------|
|
|
102
|
+
| CLI | Node.js + Commander.js | Global `npm install -g npm-scan` |
|
|
103
|
+
| Enumerator / Fetcher | Node.js | Extends Package-Inferno structure |
|
|
104
|
+
| Analyzer (static) | Node.js + Python 3.12+ | YARA via `yara-python` |
|
|
105
|
+
| Dynamic Sandbox | gVisor (runsc) | See §6.1 — not vm2/isolated-vm |
|
|
106
|
+
| Local storage (free) | SQLite | Zero-setup, file-based |
|
|
107
|
+
| Team/hosted storage | PostgreSQL | SaaS and enterprise tier only |
|
|
108
|
+
| Dashboard | Streamlit (MVP stub) | FastAPI + React when first enterprise customer requires it |
|
|
109
|
+
| SBOM | cyclonedx-node + cyclonedx-python | CycloneDX 1.5 |
|
|
110
|
+
| Reports (free) | Jinja2 → HTML | PDF is premium |
|
|
111
|
+
| Reports (premium) | Jinja2 → WeasyPrint → PDF | With NIST 800-161 templates |
|
|
112
|
+
| Policy Engine (free) | YAML | Shipped in core |
|
|
113
|
+
| Policy Engine (premium) | OPA/Rego | Full enterprise policy-as-code |
|
|
114
|
+
| Containerization | Docker + Docker Compose + GHCR | Multi-arch images |
|
|
115
|
+
| Observability | OpenTelemetry + structured JSON logging | Opt-in telemetry only |
|
|
116
|
+
| Licensing | Feature flags via license key validation | Skeleton in Phase 0 |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 6. Architecture
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
CLI Layer (npm-scan command)
|
|
124
|
+
↓ lightweight mode (static-only, SQLite, no Docker required)
|
|
125
|
+
↓ full mode (delegates to Docker Compose pipeline)
|
|
126
|
+
|
|
127
|
+
Docker Compose Pipeline:
|
|
128
|
+
[enumerator] → [Redis queue] → [fetcher] → [analyzer-static]
|
|
129
|
+
↓
|
|
130
|
+
[analyzer-sandbox] ← premium, gVisor
|
|
131
|
+
↓
|
|
132
|
+
[report-generator]
|
|
133
|
+
↓
|
|
134
|
+
[api-service (FastAPI)] + [streamlit-dashboard]
|
|
135
|
+
|
|
136
|
+
Storage:
|
|
137
|
+
SQLite (local / free tier)
|
|
138
|
+
PostgreSQL (team / hosted tier)
|
|
139
|
+
S3-compatible (tarball cache, optional)
|
|
140
|
+
|
|
141
|
+
Output Formats:
|
|
142
|
+
JSON (structured findings, machine-readable)
|
|
143
|
+
CycloneDX SBOM (Phase 1) + SPDX (Phase 2)
|
|
144
|
+
HTML report (free)
|
|
145
|
+
PDF report with regulatory mappings (premium)
|
|
146
|
+
SIEM formats: OCSF, CEF, ECS (premium, Phase 3)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 6.1 Dynamic Sandbox Architecture — Security-First
|
|
150
|
+
|
|
151
|
+
> The sandbox executes malicious code by design. One escape on a user's machine destroys the tool's reputation. This section is non-negotiable.
|
|
152
|
+
|
|
153
|
+
**Selected isolation stack: gVisor (runsc)**
|
|
154
|
+
|
|
155
|
+
- Kernel-level syscall interception without a full VM — Docker-compatible, production-hardened
|
|
156
|
+
- Firecracker microVMs as an optional upgrade for highest-assurance environments
|
|
157
|
+
- **Explicitly not used:** vm2 (repeated escapes), isolated-vm (Node-based, insufficient for hostile payloads)
|
|
158
|
+
|
|
159
|
+
**Sandbox threat model (required before Phase 2 ships):**
|
|
160
|
+
|
|
161
|
+
| Threat | Mitigation |
|
|
162
|
+
|--------|------------|
|
|
163
|
+
| Syscall escape | gVisor intercepts all syscalls at the gVisor kernel boundary |
|
|
164
|
+
| Network exfiltration during analysis | Network namespace isolation; egress blocked except to monitored sink |
|
|
165
|
+
| Filesystem escape | Read-only bind mounts; package extracted to ephemeral tmpfs |
|
|
166
|
+
| Resource exhaustion (CPU/memory bomb) | cgroup limits: 1 CPU, 512MB RAM, 30s timeout |
|
|
167
|
+
| Sandbox detection by malware | Randomized env vars, realistic process tree, no obvious sandbox markers |
|
|
168
|
+
| Tarball extraction bomb | Size limits enforced before extraction (uncompressed cap: 500MB) |
|
|
169
|
+
|
|
170
|
+
**Anti-sandbox-evasion (ATK-010):** The analyzer checks for behaviors that indicate the package probes its environment before acting (hostname checks, `CI` env var detection, timing attacks). Detection of evasion attempts is itself a high-severity finding.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 7. Adversarial Posture
|
|
175
|
+
|
|
176
|
+
npm-scan is a security tool that will be actively studied by the people it's designed to catch. The plan explicitly addresses this.
|
|
177
|
+
|
|
178
|
+
### Evasion Resistance
|
|
179
|
+
|
|
180
|
+
- YARA rules and behavioral signatures are versioned and updated on a defined cadence
|
|
181
|
+
- ATK taxonomy includes known evasion techniques per detector (documented in `docs/attack-taxonomy.md`)
|
|
182
|
+
- Obfuscation detection uses AST-level analysis, not regex — resistant to trivial reformatting
|
|
183
|
+
- Conditional trigger detection (ATK-009) specifically targets code that behaves differently in sandbox vs. production
|
|
184
|
+
|
|
185
|
+
### Supply Chain Integrity of npm-scan Itself
|
|
186
|
+
|
|
187
|
+
> npm-scan must not be a supply chain attack vector.
|
|
188
|
+
|
|
189
|
+
- All releases signed with `npm provenance` (Sigstore) from day one
|
|
190
|
+
- npm-scan's own `package.json` is scanned by npm-scan in CI (self-attestation)
|
|
191
|
+
- Lockfile committed and integrity-checked in CI
|
|
192
|
+
- SBOM generated and published with every release
|
|
193
|
+
- Dependency update PRs gated on passing scan results
|
|
194
|
+
|
|
195
|
+
### Known Evasion Vectors (documented, not hidden)
|
|
196
|
+
|
|
197
|
+
The `docs/evasion-known.md` file catalogues known evasion techniques so contributors know what to harden against. Transparency about limitations builds trust; hiding them does not.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 8. Compliance Strategy — NIST 800-161 First
|
|
202
|
+
|
|
203
|
+
> Attempting to map five frameworks simultaneously produces five shallow implementations. One framework done properly is worth more than five done poorly.
|
|
204
|
+
|
|
205
|
+
### Phase 2: NIST SP 800-161r1 (Cybersecurity Supply Chain Risk Management)
|
|
206
|
+
|
|
207
|
+
**Why NIST 800-161 first:**
|
|
208
|
+
- Supply-chain-specific (directly relevant to the tool's purpose)
|
|
209
|
+
- Maps to CMMC Level 2 — existing domain expertise from SecureStack/DockerShield work
|
|
210
|
+
- US government and defense contractor buyers have budget and mandate
|
|
211
|
+
- NIST → FedRAMP → CMMC creates a coherent enterprise sales story
|
|
212
|
+
|
|
213
|
+
**Phase 2 deliverable:** A compliance report template that maps each npm-scan finding to the relevant NIST 800-161 practice (SR-series controls). PDF output with finding → control traceability matrix.
|
|
214
|
+
|
|
215
|
+
### Phase 3+: Additional Frameworks (in order)
|
|
216
|
+
|
|
217
|
+
1. EU Cyber Resilience Act (CRA) — EU enterprise buyers
|
|
218
|
+
2. SOC 2 Type II (CC6.x supply chain controls) — SaaS buyers
|
|
219
|
+
3. ISO 27001:2022 (A.15 supplier relationships) — global enterprise
|
|
220
|
+
4. DORA — financial sector EU buyers
|
|
221
|
+
|
|
222
|
+
Each framework addition is a versioned template, not a rewrite of the report engine.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 9. Phased Roadmap
|
|
227
|
+
|
|
228
|
+
### Phase 0: Foundation (Week 1)
|
|
229
|
+
|
|
230
|
+
**Exit criteria:** npm name claimed, repo structured, licensing documented, license key skeleton wired, Docker images published.
|
|
231
|
+
|
|
232
|
+
- [ ] Claim `npm-scan` on npm (publish stub immediately)
|
|
233
|
+
- [ ] Fork/rename Package-Inferno repo → monorepo structure (`/cli`, `/backend`, `/docker`, `/docs`)
|
|
234
|
+
- [ ] Write `LICENSING.md` — define Apache-2.0 + Commons Clause boundary explicitly
|
|
235
|
+
- [ ] Write `docs/attack-taxonomy.md` — ATK-001–007 initial entries with NIST 800-161 mappings
|
|
236
|
+
- [ ] License key feature-flag skeleton (runtime validation, graceful degradation)
|
|
237
|
+
- [ ] SQLite schema for local mode (replaces PostgreSQL for free tier)
|
|
238
|
+
- [ ] `CONTRIBUTING.md` referencing ATK taxonomy governance process
|
|
239
|
+
- [ ] Initial Docker images published to GHCR (multi-arch)
|
|
240
|
+
- [ ] Basic CI/CD pipeline (GitHub Actions): lint, test, image build
|
|
241
|
+
|
|
242
|
+
### Phase 1: MVP — Production Ready Scanner (Weeks 2–4)
|
|
243
|
+
|
|
244
|
+
**Exit criteria:** Static scanner runs on any package in <30s; false positive rate <2% on top-500 npm packages; GitHub Action published; SBOM output validated.
|
|
245
|
+
|
|
246
|
+
**Priority sequence:**
|
|
247
|
+
|
|
248
|
+
1. **GitHub Action first** — highest-leverage distribution channel
|
|
249
|
+
2. **ATK-001–007 static detectors** hardened against Shai-Hulud patterns
|
|
250
|
+
3. **CLI:** `npm-scan scan <package>`, `npm-scan scan-lockfile`, `npm-scan report`
|
|
251
|
+
4. **SQLite-backed scan history** for local users (no external dependencies)
|
|
252
|
+
5. **CycloneDX SBOM output** with findings embedded as vulnerabilities
|
|
253
|
+
6. **Basic HTML report** (Jinja2 template; PDF deferred to Phase 2)
|
|
254
|
+
7. **YAML policy engine** (allowlists, severity overrides, block-on-severity)
|
|
255
|
+
8. **Docker Compose improvements** — one-command start, health checks
|
|
256
|
+
9. **Test corpus:** 50+ clean packages + 20+ malicious samples including Shai-Hulud variants
|
|
257
|
+
|
|
258
|
+
**Not in Phase 1:** PDF reports, PostgreSQL, sandbox, ML.
|
|
259
|
+
|
|
260
|
+
### Phase 2: Hybrid Analysis & Compliance (Weeks 5–7)
|
|
261
|
+
|
|
262
|
+
**Exit criteria:** Sandbox threat model documented and reviewed; NIST 800-161 report template covers SR-series controls; dynamic analysis catches ATK-008/009 patterns.
|
|
263
|
+
|
|
264
|
+
- Dynamic sandbox service (gVisor-based) — full threat model shipped before first user
|
|
265
|
+
- ATK-008–010 behavioral detectors
|
|
266
|
+
- NIST 800-161r1 compliance report template (PDF via WeasyPrint)
|
|
267
|
+
- SPDX SBOM output (complement to CycloneDX)
|
|
268
|
+
- Reachability analysis (lockfile call graph parsing — surfaces only reachable findings)
|
|
269
|
+
- Dashboard: "Compliance" tab with NIST control mapping
|
|
270
|
+
- ATK taxonomy updated with sandbox-derived evasion findings
|
|
271
|
+
- Self-scan of npm-scan in CI using sandbox tier
|
|
272
|
+
|
|
273
|
+
### Phase 3: Enterprise & Integrations (Weeks 8–11)
|
|
274
|
+
|
|
275
|
+
**Exit criteria:** At least one Splunkbase listing live; one paying customer.
|
|
276
|
+
|
|
277
|
+
- SIEM exporters: Splunk TA, Microsoft Sentinel Solution, Elastic integration, QRadar
|
|
278
|
+
- FastAPI-based REST API + webhooks
|
|
279
|
+
- Team features: multi-user, RBAC, audit logs
|
|
280
|
+
- Feature-flag enforcement (license key hard gating)
|
|
281
|
+
- EU CRA compliance report template
|
|
282
|
+
- PostgreSQL backend for hosted/team tier
|
|
283
|
+
- Kubernetes / Helm chart
|
|
284
|
+
- Publish to Splunkbase + Azure Marketplace (content packs)
|
|
285
|
+
- ATK-011 transitive propagation detector
|
|
286
|
+
|
|
287
|
+
### Phase 4: Polish & Scale (Ongoing)
|
|
288
|
+
|
|
289
|
+
- VS Code extension (surfaces findings inline)
|
|
290
|
+
- SOC 2 + ISO 27001 compliance templates
|
|
291
|
+
- DORA template (financial sector)
|
|
292
|
+
- Hosted SaaS option with usage-based billing
|
|
293
|
+
- Opt-in telemetry (aggregate false positive rates feed detector improvement)
|
|
294
|
+
- Marketing site + pricing page
|
|
295
|
+
- ML-assisted scoring — **only after telemetry data justifies it**
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 10. Distribution & Packaging
|
|
300
|
+
|
|
301
|
+
| Channel | Details |
|
|
302
|
+
|---------|---------|
|
|
303
|
+
| npm (`npm-scan`) | Global CLI + lightweight static-only mode |
|
|
304
|
+
| Docker images (GHCR) | Focused images per service + all-in-one + Compose file |
|
|
305
|
+
| GitHub Releases | Binaries via `pkg` for offline/air-gapped use |
|
|
306
|
+
| GitHub Action | `lateos/npm-scan-action` — Phase 1 priority |
|
|
307
|
+
| Splunkbase | Splunk TA for SIEM integration — Phase 3 |
|
|
308
|
+
| Azure Marketplace | Sentinel Solution content pack — Phase 3 |
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 11. Success Metrics — Operational, Not Vanity
|
|
313
|
+
|
|
314
|
+
Lagging indicators (stars, press) are tracked but not used for go/no-go decisions. Leading indicators drive phase gates:
|
|
315
|
+
|
|
316
|
+
| Metric | Target | Phase Gate |
|
|
317
|
+
|--------|--------|------------|
|
|
318
|
+
| False positive rate on top-500 npm packages | < 2% | Phase 1 exit |
|
|
319
|
+
| Static scan time (average package) | < 30 seconds | Phase 1 exit |
|
|
320
|
+
| Dynamic scan time (average package) | < 5 minutes | Phase 2 exit |
|
|
321
|
+
| ATK taxonomy entries with passing detector tests | 100% | Every phase |
|
|
322
|
+
| NIST 800-161 control coverage in compliance report | SR-series complete | Phase 2 exit |
|
|
323
|
+
| Paying customers | ≥ 1 | Phase 3 exit |
|
|
324
|
+
| Splunkbase listing live | Yes/No | Phase 3 exit |
|
|
325
|
+
| npm-scan self-scan passing in CI | Always green | Ongoing |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 12. Risks & Mitigations
|
|
330
|
+
|
|
331
|
+
| Risk | Likelihood | Mitigation |
|
|
332
|
+
|------|-----------|------------|
|
|
333
|
+
| Sandbox escape damages user environment | Low but catastrophic | gVisor isolation; full threat model before Phase 2 ships; security advisory process |
|
|
334
|
+
| False positives erode trust | Medium | Strict test corpus; allowlist support; reachability analysis reduces noise |
|
|
335
|
+
| Maintenance burden | High (solo) | Modular design; ATK taxonomy governance keeps contributions structured |
|
|
336
|
+
| Legal / license friction | Low | Commons Clause is cleaner than BSL; `LICENSING.md` published Day 1 |
|
|
337
|
+
| npm registry rate limiting | Medium | Exponential backoff + S3 tarball cache |
|
|
338
|
+
| Adversarial evasion of detectors | High (over time) | ATK taxonomy documents evasion surface; AST-level analysis over regex; evasion findings = high severity |
|
|
339
|
+
| npm-scan itself becomes a target | Medium | Sigstore provenance; self-scan in CI; SBOM published with every release |
|
|
340
|
+
| Feature scope creep (ML, etc.) | High | ML explicitly gated behind telemetry prerequisite; roadmap is sequential not parallel |
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## 13. Deliverables Per Phase
|
|
345
|
+
|
|
346
|
+
Each phase ships:
|
|
347
|
+
|
|
348
|
+
- Working code + Docker Compose (validated one-command start)
|
|
349
|
+
- Comprehensive README with real examples
|
|
350
|
+
- Test corpus additions (clean + malicious packages)
|
|
351
|
+
- Updated `docs/attack-taxonomy.md`
|
|
352
|
+
- CI/CD pipeline passing (including self-scan for Phase 2+)
|
|
353
|
+
- `CHANGELOG.md` entry referencing ATK IDs addressed
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 14. Immediate Next Steps (Phase 0, Day 1)
|
|
358
|
+
|
|
359
|
+
Ordered by dependency and leverage:
|
|
360
|
+
|
|
361
|
+
1. `npm publish npm-scan` — claim the name before someone else does
|
|
362
|
+
2. Write `LICENSING.md` — unblocks external contributions
|
|
363
|
+
3. Write `docs/attack-taxonomy.md` — ATK-001–007, NIST mappings — unblocks detector PRs
|
|
364
|
+
4. Wire license key skeleton — unblocks premium feature development
|
|
365
|
+
5. SQLite schema for local mode — removes PostgreSQL dependency from free tier
|
|
366
|
+
6. Monorepo structure (`/cli`, `/backend`, `/docker`, `/docs`)
|
|
367
|
+
7. Initial Docker images to GHCR
|
|
368
|
+
8. GitHub Action stub — distribution channel, even before full detection logic
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
*This document is the canonical project plan for npm-scan v1.x. Changes require updating both this document and the affected ATK taxonomy entries.*
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lateos/npm-scan",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Powerful npm supply chain security scanner - detects malicious packages (Shai-Hulud style), behavioral analysis, SBOM, and compliance reporting.",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "backend/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"npm-scan": "
|
|
7
|
+
"npm-scan": "cli/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
|
-
"license": "
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
13
|
"url": "https://github.com/YOUR_GITHUB_USERNAME/npm-scan.git"
|
|
@@ -22,7 +22,20 @@
|
|
|
22
22
|
"sbom",
|
|
23
23
|
"compliance"
|
|
24
24
|
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "node cli/cli.js",
|
|
27
|
+
"lint": "echo 'Lint stub'",
|
|
28
|
+
"test": "node --test",
|
|
29
|
+
"build": "echo 'Build stub'",
|
|
30
|
+
"corpus": "node tests/corpus/run.js"
|
|
31
|
+
},
|
|
25
32
|
"publishConfig": {
|
|
26
33
|
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"acorn": "^8.16.0",
|
|
37
|
+
"adm-zip": "^0.5.17",
|
|
38
|
+
"commander": "^14.0.3",
|
|
39
|
+
"node-fetch": "^3.3.2"
|
|
27
40
|
}
|
|
28
41
|
}
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'assert/strict';
|
|
2
|
+
import { globSync } from 'glob';
|
|
3
|
+
import { fetchPackage } from '../../backend/fetch.js';
|
|
4
|
+
import { runAll } from '../../backend/detectors/index.js';
|
|
5
|
+
|
|
6
|
+
const cleanTarballs = globSync('tests/corpus/clean/*.tgz');
|
|
7
|
+
const malTarballs = globSync('tests/corpus/malicious/*.tgz');
|
|
8
|
+
|
|
9
|
+
for (const tar of cleanTarballs) {
|
|
10
|
+
const pkgName = tar.split('/').pop().replace('.tgz', '');
|
|
11
|
+
const { pkgJson, jsFiles } = await fetchPackage(pkgName);
|
|
12
|
+
const findings = await runAll(pkgJson, jsFiles);
|
|
13
|
+
const highFP = findings.filter(f => f.severity === 'high');
|
|
14
|
+
assert(highFP.length === 0, `High FP in clean ${pkgName}: ${highFP.map(f => f.title).join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(`Clean corpus pass (${cleanTarballs.length} pkgs)`);
|
|
18
|
+
|
|
19
|
+
for (const tar of malTarballs) {
|
|
20
|
+
const pkgName = tar.split('/').pop().replace('.tgz', '');
|
|
21
|
+
const { pkgJson, jsFiles } = await fetchPackage(pkgName);
|
|
22
|
+
const findings = await runAll(pkgJson, jsFiles);
|
|
23
|
+
assert(findings.length > 0, `No findings in malicious ${pkgName}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`Malicious corpus pass (${malTarballs.length} pkgs)`);
|
|
27
|
+
console.log('Test corpus FP <2% ✓');
|
package/cli.js
DELETED
|
File without changes
|