@lateos/npm-scan 1.0.0 → 1.1.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/README.de.md +3 -98
- package/README.fr.md +3 -98
- package/README.ja.md +3 -98
- package/README.md +2 -122
- package/README.zh.md +3 -98
- package/backend/cra.js +113 -21
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +48 -3
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +109 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +78 -19
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +12 -3
package/backend/cra.js
CHANGED
|
@@ -1,32 +1,124 @@
|
|
|
1
1
|
export function generateCRA(scans) {
|
|
2
2
|
const atkMap = {};
|
|
3
3
|
for (const s of scans) {
|
|
4
|
-
for (const f of
|
|
4
|
+
for (const f of s.findings || []) {
|
|
5
5
|
const key = f.atk_id || f.id;
|
|
6
|
-
if (!atkMap[key])
|
|
6
|
+
if (!atkMap[key]) {
|
|
7
|
+
atkMap[key] = [];
|
|
8
|
+
}
|
|
7
9
|
atkMap[key].push({ ...f, package_name: s.package_name, version: s.version });
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const CRA_ARTICLES = [
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
{
|
|
15
|
+
article: 'Art. 7',
|
|
16
|
+
title: 'Secure by default configuration',
|
|
17
|
+
atkId: 'ATK-001',
|
|
18
|
+
desc: 'Lifecycle hooks used for insecure defaults',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
article: 'Art. 7',
|
|
22
|
+
title: 'Secure by default configuration',
|
|
23
|
+
atkId: 'ATK-010',
|
|
24
|
+
desc: 'Anti-analysis in default state',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
article: 'Art. 10(1)',
|
|
28
|
+
title: 'Vulnerability disclosure',
|
|
29
|
+
atkId: 'ATK-008',
|
|
30
|
+
desc: 'Tarball integrity prevents disclosure accuracy',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
article: 'Art. 10(2)',
|
|
34
|
+
title: 'Known vulnerability reporting',
|
|
35
|
+
atkId: 'ATK-006',
|
|
36
|
+
desc: 'Dependency confusion undermines visibility',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
article: 'Art. 11',
|
|
40
|
+
title: 'Software Bill of Materials',
|
|
41
|
+
atkId: 'ATK-008',
|
|
42
|
+
desc: 'Integrity of SBOM entries must be verified',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
article: 'Art. 11',
|
|
46
|
+
title: 'Software Bill of Materials',
|
|
47
|
+
atkId: 'ATK-006',
|
|
48
|
+
desc: 'SBOM must reflect actual dependency graph',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
article: 'Annex I(1.1)',
|
|
52
|
+
title: 'No known exploitable vulnerabilities',
|
|
53
|
+
atkId: 'ATK-009',
|
|
54
|
+
desc: 'Conditional triggers may activate known vulns',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
article: 'Annex I(1.3)',
|
|
58
|
+
title: 'Least privilege',
|
|
59
|
+
atkId: 'ATK-003',
|
|
60
|
+
desc: 'Credential harvesting violates least privilege',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
article: 'Annex I(1.5)',
|
|
64
|
+
title: 'Limited attack surface',
|
|
65
|
+
atkId: 'ATK-002',
|
|
66
|
+
desc: 'Obfuscation increases attack surface',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
article: 'Annex I(1.5)',
|
|
70
|
+
title: 'Limited attack surface',
|
|
71
|
+
atkId: 'ATK-004',
|
|
72
|
+
desc: 'Persistence mechanisms expand attack surface',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
article: 'Annex I(1.5)',
|
|
76
|
+
title: 'Limited attack surface',
|
|
77
|
+
atkId: 'ATK-005',
|
|
78
|
+
desc: 'Network exfiltration expands attack surface',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
article: 'Annex I(2.1)',
|
|
82
|
+
title: 'Protection against unauthorized access',
|
|
83
|
+
atkId: 'ATK-003',
|
|
84
|
+
desc: 'Credential harvesting enables unauthorized access',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
article: 'Annex I(2.3)',
|
|
88
|
+
title: 'Data integrity',
|
|
89
|
+
atkId: 'ATK-008',
|
|
90
|
+
desc: 'Tarball tampering violates data integrity',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
article: 'Annex I(2.3)',
|
|
94
|
+
title: 'Data integrity',
|
|
95
|
+
atkId: 'ATK-011',
|
|
96
|
+
desc: 'Propagation attacks compromise data integrity',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
article: 'Annex I(3.2)',
|
|
100
|
+
title: 'Incident detection and reporting',
|
|
101
|
+
atkId: 'ATK-009',
|
|
102
|
+
desc: 'Conditional triggers evade incident detection',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
article: 'Annex I(3.2)',
|
|
106
|
+
title: 'Incident detection and reporting',
|
|
107
|
+
atkId: 'ATK-010',
|
|
108
|
+
desc: 'Sandbox evasion defeats incident detection',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
article: 'Annex I(3.3)',
|
|
112
|
+
title: 'Supply chain security monitoring',
|
|
113
|
+
atkId: 'ATK-011',
|
|
114
|
+
desc: 'Propagation requires SC monitoring',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
article: 'Annex I(3.3)',
|
|
118
|
+
title: 'Supply chain security monitoring',
|
|
119
|
+
atkId: 'ATK-007',
|
|
120
|
+
desc: 'Typosquatting undermines SC trust',
|
|
121
|
+
},
|
|
30
122
|
];
|
|
31
123
|
|
|
32
124
|
let rows = '';
|
|
@@ -66,4 +158,4 @@ th { background: #161b22; }
|
|
|
66
158
|
${body}
|
|
67
159
|
<p class="meta">EU Cyber Resilience Act (Regulation 2023/2841) mapped to ATK findings.</p>
|
|
68
160
|
</body></html>`;
|
|
69
|
-
}
|
|
161
|
+
}
|
package/backend/db.js
CHANGED
|
@@ -11,8 +11,12 @@ let db = null;
|
|
|
11
11
|
let initPromise = null;
|
|
12
12
|
|
|
13
13
|
async function ensureInit() {
|
|
14
|
-
if (db)
|
|
15
|
-
|
|
14
|
+
if (db) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (initPromise) {
|
|
18
|
+
return initPromise;
|
|
19
|
+
}
|
|
16
20
|
initPromise = (async () => {
|
|
17
21
|
const SQL = await initSqlJs();
|
|
18
22
|
if (fs.existsSync(DB_PATH)) {
|
|
@@ -29,7 +33,9 @@ async function ensureInit() {
|
|
|
29
33
|
|
|
30
34
|
function queryAll(sql, params = []) {
|
|
31
35
|
const stmt = db.prepare(sql);
|
|
32
|
-
if (params.length)
|
|
36
|
+
if (params.length) {
|
|
37
|
+
stmt.bind(params);
|
|
38
|
+
}
|
|
33
39
|
const rows = [];
|
|
34
40
|
while (stmt.step()) {
|
|
35
41
|
rows.push(stmt.getAsObject());
|
|
@@ -43,7 +49,7 @@ function queryOne(sql, params = []) {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
function lastId() {
|
|
46
|
-
const r = db.exec(
|
|
52
|
+
const r = db.exec('SELECT last_insert_rowid()');
|
|
47
53
|
return Number(r[0].values[0][0]);
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -53,9 +59,11 @@ function persist() {
|
|
|
53
59
|
|
|
54
60
|
export async function saveScan(pkgName, version = 'latest', findings = []) {
|
|
55
61
|
await ensureInit();
|
|
56
|
-
db.run(
|
|
62
|
+
db.run('INSERT INTO scans (package_name, version) VALUES (?, ?)', [pkgName, version]);
|
|
57
63
|
const scanId = lastId();
|
|
58
|
-
const stmt = db.prepare(
|
|
64
|
+
const stmt = db.prepare(
|
|
65
|
+
'INSERT INTO findings (scan_id, atk_id, severity, description, evidence) VALUES (?, ?, ?, ?, ?)'
|
|
66
|
+
);
|
|
59
67
|
for (const f of findings) {
|
|
60
68
|
stmt.run([scanId, f.id, f.severity, f.title || f.description, f.evidence || '']);
|
|
61
69
|
}
|
|
@@ -66,17 +74,17 @@ export async function saveScan(pkgName, version = 'latest', findings = []) {
|
|
|
66
74
|
|
|
67
75
|
export async function getRecentScans(limit = 10) {
|
|
68
76
|
await ensureInit();
|
|
69
|
-
return queryAll(
|
|
77
|
+
return queryAll('SELECT * FROM scans ORDER BY scanned_at DESC LIMIT ?', [limit]);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
export async function getFindings(scanId) {
|
|
73
81
|
await ensureInit();
|
|
74
|
-
return queryAll(
|
|
82
|
+
return queryAll('SELECT * FROM findings WHERE scan_id = ?', [scanId]);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
export async function getScan(scanId) {
|
|
78
86
|
await ensureInit();
|
|
79
|
-
return queryOne(
|
|
87
|
+
return queryOne('SELECT * FROM scans WHERE id = ?', [scanId]);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
export async function close() {
|
|
@@ -86,4 +94,4 @@ export async function close() {
|
|
|
86
94
|
db = null;
|
|
87
95
|
initPromise = null;
|
|
88
96
|
}
|
|
89
|
-
}
|
|
97
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export async function scan(pkgJson,
|
|
1
|
+
export async function scan(pkgJson, _files = []) {
|
|
2
2
|
const findings = [];
|
|
3
3
|
const scripts = pkgJson.scripts || {};
|
|
4
|
-
const suspicious = Object.keys(scripts).filter(s => /pre|post|install/i.test(s));
|
|
4
|
+
const suspicious = Object.keys(scripts).filter((s) => /pre|post|install/i.test(s));
|
|
5
5
|
if (suspicious.length) {
|
|
6
|
-
const content = suspicious.map(s => scripts[s]).join(' ');
|
|
6
|
+
const content = suspicious.map((s) => scripts[s]).join(' ');
|
|
7
7
|
if (/curl|wget|sh |bash |\.sh|exfil|steal|pwn|c2|pastebin/i.test(content)) {
|
|
8
8
|
findings.push({
|
|
9
9
|
id: 'ATK-001',
|
|
10
10
|
severity: 'high',
|
|
11
11
|
title: 'Malicious lifecycle scripts',
|
|
12
12
|
description: 'Suspicious install hooks',
|
|
13
|
-
evidence: suspicious.join(', ')
|
|
13
|
+
evidence: suspicious.join(', '),
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
return findings;
|
|
18
|
-
}
|
|
18
|
+
}
|
|
@@ -1,12 +1,43 @@
|
|
|
1
|
-
const DIST_BUILD_PATTERNS = [
|
|
2
|
-
|
|
1
|
+
const DIST_BUILD_PATTERNS = [
|
|
2
|
+
/\/dist\//,
|
|
3
|
+
/\/build\//,
|
|
4
|
+
/\/bundle/,
|
|
5
|
+
/\/min\//,
|
|
6
|
+
/\.min\.js$/,
|
|
7
|
+
/\.bundled?\.js$/,
|
|
8
|
+
];
|
|
9
|
+
const TEST_FIXTURE_PATTERNS = [
|
|
10
|
+
/\/test\//,
|
|
11
|
+
/\/tests\//,
|
|
12
|
+
/\/__tests__\//,
|
|
13
|
+
/\/spec\//,
|
|
14
|
+
/\.test\.js$/,
|
|
15
|
+
/\.spec\.js$/,
|
|
16
|
+
/fixtures?/,
|
|
17
|
+
];
|
|
3
18
|
const KNOWN_SAFE_DOMAINS = [
|
|
4
|
-
'registry.npmjs.org',
|
|
5
|
-
'
|
|
6
|
-
'
|
|
19
|
+
'registry.npmjs.org',
|
|
20
|
+
'cdn.jsdelivr.net',
|
|
21
|
+
'unpkg.com',
|
|
22
|
+
'cdn.skypack.dev',
|
|
23
|
+
'esm.sh',
|
|
24
|
+
'deno.land',
|
|
25
|
+
'raw.githubusercontent.com',
|
|
26
|
+
'github.com',
|
|
27
|
+
'npmjs.com',
|
|
28
|
+
'nodejs.org',
|
|
29
|
+
'v8.dev',
|
|
30
|
+
'typescriptlang.org',
|
|
7
31
|
];
|
|
8
32
|
|
|
9
|
-
const LIFECYCLE_SCRIPT_NAMES = [
|
|
33
|
+
const LIFECYCLE_SCRIPT_NAMES = [
|
|
34
|
+
'install',
|
|
35
|
+
'postinstall',
|
|
36
|
+
'preinstall',
|
|
37
|
+
'prepare',
|
|
38
|
+
'prepack',
|
|
39
|
+
'postpack',
|
|
40
|
+
];
|
|
10
41
|
|
|
11
42
|
function extractUrlDomain(code) {
|
|
12
43
|
const urlMatch = code.match(/https?:\/\/([^/'"\s]+)/);
|
|
@@ -14,22 +45,26 @@ function extractUrlDomain(code) {
|
|
|
14
45
|
}
|
|
15
46
|
|
|
16
47
|
function isDistOrBuild(filePath) {
|
|
17
|
-
return DIST_BUILD_PATTERNS.some(p => p.test(filePath));
|
|
48
|
+
return DIST_BUILD_PATTERNS.some((p) => p.test(filePath));
|
|
18
49
|
}
|
|
19
50
|
|
|
20
51
|
function isTestOrFixture(filePath) {
|
|
21
|
-
return TEST_FIXTURE_PATTERNS.some(p => p.test(filePath));
|
|
52
|
+
return TEST_FIXTURE_PATTERNS.some((p) => p.test(filePath));
|
|
22
53
|
}
|
|
23
54
|
|
|
24
55
|
function isKnownSafeDomain(domain) {
|
|
25
|
-
if (!domain)
|
|
26
|
-
|
|
56
|
+
if (!domain) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return KNOWN_SAFE_DOMAINS.some((safe) => domain === safe || domain.endsWith('.' + safe));
|
|
27
60
|
}
|
|
28
61
|
|
|
29
62
|
function locateLine(code, pattern) {
|
|
30
63
|
const lines = code.split('\n');
|
|
31
64
|
for (let i = 0; i < lines.length; i++) {
|
|
32
|
-
if (pattern.test(lines[i]))
|
|
65
|
+
if (pattern.test(lines[i])) {
|
|
66
|
+
return i + 1;
|
|
67
|
+
}
|
|
33
68
|
}
|
|
34
69
|
return null;
|
|
35
70
|
}
|
|
@@ -40,65 +75,96 @@ function decodePreview(code) {
|
|
|
40
75
|
try {
|
|
41
76
|
const decoded = atob(b64Match[1]);
|
|
42
77
|
return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
|
|
43
|
-
} catch {
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore decode errors */
|
|
80
|
+
}
|
|
44
81
|
}
|
|
45
|
-
|
|
82
|
+
|
|
46
83
|
const hexMatch = code.match(/Buffer\.from\(['"]([0-9a-fA-F]+)['"],\s*['"]hex['"]\)/);
|
|
47
84
|
if (hexMatch) {
|
|
48
85
|
try {
|
|
49
86
|
const decoded = Buffer.from(hexMatch[1], 'hex').toString();
|
|
50
87
|
return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
|
|
51
|
-
} catch {
|
|
88
|
+
} catch {
|
|
89
|
+
/* ignore decode errors */
|
|
90
|
+
}
|
|
52
91
|
}
|
|
53
|
-
|
|
92
|
+
|
|
54
93
|
const btoaMatch = code.match(/btoa\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
|
|
55
94
|
if (btoaMatch) {
|
|
56
95
|
try {
|
|
57
96
|
const decoded = atob(btoaMatch[1]);
|
|
58
97
|
return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
|
|
59
|
-
} catch {
|
|
98
|
+
} catch {
|
|
99
|
+
/* ignore decode errors */
|
|
100
|
+
}
|
|
60
101
|
}
|
|
61
|
-
|
|
102
|
+
|
|
62
103
|
return null;
|
|
63
104
|
}
|
|
64
105
|
|
|
65
106
|
function detectEncodingType(code) {
|
|
66
|
-
if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code))
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (/
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code)) {
|
|
108
|
+
return 'hex';
|
|
109
|
+
}
|
|
110
|
+
if (/atob\(/.test(code)) {
|
|
111
|
+
return 'base64';
|
|
112
|
+
}
|
|
113
|
+
if (/btoa\(/.test(code)) {
|
|
114
|
+
return 'base64';
|
|
115
|
+
}
|
|
116
|
+
if (/Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code)) {
|
|
117
|
+
return 'base64';
|
|
118
|
+
}
|
|
119
|
+
if (/String\.fromCharCode\(/.test(code)) {
|
|
120
|
+
return 'charcode';
|
|
121
|
+
}
|
|
122
|
+
if (/btoa\(.*btoa\(|atob\(.*atob\(/.test(code)) {
|
|
123
|
+
return 'double-base64';
|
|
124
|
+
}
|
|
72
125
|
return 'unknown';
|
|
73
126
|
}
|
|
74
127
|
|
|
75
128
|
function isFileInLifecycleScript(filePath, pkgJson) {
|
|
76
|
-
if (!pkgJson?.scripts)
|
|
77
|
-
|
|
129
|
+
if (!pkgJson?.scripts) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
const scripts = pkgJson.scripts;
|
|
79
134
|
const fileName = filePath.split('/').pop();
|
|
80
|
-
const normalizedPath = filePath
|
|
81
|
-
|
|
135
|
+
const normalizedPath = filePath
|
|
136
|
+
.replace(/^node_modules\//, '')
|
|
137
|
+
.replace(/^dist\//, '')
|
|
138
|
+
.replace(/^build\//, '');
|
|
139
|
+
|
|
82
140
|
for (const scriptName of LIFECYCLE_SCRIPT_NAMES) {
|
|
83
141
|
const scriptValue = scripts[scriptName];
|
|
84
|
-
if (!scriptValue)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (scriptValue.includes(
|
|
89
|
-
|
|
142
|
+
if (!scriptValue) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (scriptValue.includes(filePath)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (scriptValue.includes(fileName)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (scriptValue.includes(normalizedPath)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
90
156
|
const scriptFileMatch = scriptValue.match(/[^\s'"]+\.js$/);
|
|
91
|
-
if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0]))
|
|
157
|
+
if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
92
160
|
}
|
|
93
|
-
|
|
161
|
+
|
|
94
162
|
return false;
|
|
95
163
|
}
|
|
96
164
|
|
|
97
165
|
function isLikelyLifecycleFileName(filePath) {
|
|
98
166
|
const name = filePath.split('/').pop().replace(/\.js$/, '');
|
|
99
|
-
return LIFECYCLE_SCRIPT_NAMES.includes(name) ||
|
|
100
|
-
name === 'setup' ||
|
|
101
|
-
name === 'install-helper';
|
|
167
|
+
return LIFECYCLE_SCRIPT_NAMES.includes(name) || name === 'setup' || name === 'install-helper';
|
|
102
168
|
}
|
|
103
169
|
|
|
104
170
|
function createEvidence(code, filePath, pattern, pkgJson) {
|
|
@@ -106,8 +172,9 @@ function createEvidence(code, filePath, pattern, pkgJson) {
|
|
|
106
172
|
const line = locateLine(code, pattern);
|
|
107
173
|
const decodedPreview = decodePreview(code);
|
|
108
174
|
const destinationHost = extractUrlDomain(code);
|
|
109
|
-
const lifecycleHook =
|
|
110
|
-
|
|
175
|
+
const lifecycleHook =
|
|
176
|
+
isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
|
|
177
|
+
|
|
111
178
|
return {
|
|
112
179
|
file: filePath,
|
|
113
180
|
line: line,
|
|
@@ -121,7 +188,7 @@ function createEvidence(code, filePath, pattern, pkgJson) {
|
|
|
121
188
|
export async function scan(pkgJson, files = []) {
|
|
122
189
|
const findings = [];
|
|
123
190
|
const pkgName = pkgJson?.name || '';
|
|
124
|
-
const
|
|
191
|
+
const _selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
|
|
125
192
|
|
|
126
193
|
for (const f of files) {
|
|
127
194
|
const code = f.content;
|
|
@@ -137,15 +204,23 @@ export async function scan(pkgJson, files = []) {
|
|
|
137
204
|
if (hasEval) {
|
|
138
205
|
const hexDecode = /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"]/.test(code);
|
|
139
206
|
const b64Decode = /atob\(|Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code);
|
|
140
|
-
const b64UrlDecode =
|
|
207
|
+
const b64UrlDecode =
|
|
208
|
+
/try\s*\{[^}]*atob\s*\(/s.test(code) || /btoa\(.*\)\s*[^;]*\.replace\(/s.test(code);
|
|
141
209
|
|
|
142
210
|
if (hexDecode || b64Decode || b64UrlDecode) {
|
|
143
|
-
const evidence = createEvidence(
|
|
211
|
+
const evidence = createEvidence(
|
|
212
|
+
code,
|
|
213
|
+
filePath,
|
|
214
|
+
/eval\(|new Function\(|\bFunction\('/,
|
|
215
|
+
pkgJson
|
|
216
|
+
);
|
|
144
217
|
findings.push({
|
|
145
218
|
id: 'ATK-002',
|
|
146
219
|
severity: 'medium',
|
|
147
220
|
title: 'Obfuscated payload',
|
|
148
|
-
description: hexDecode
|
|
221
|
+
description: hexDecode
|
|
222
|
+
? 'Eval with hex-decoded payload'
|
|
223
|
+
: 'Eval with base64-decoded payload',
|
|
149
224
|
evidence: evidence,
|
|
150
225
|
context: {
|
|
151
226
|
file_path: filePath,
|
|
@@ -184,8 +259,12 @@ export async function scan(pkgJson, files = []) {
|
|
|
184
259
|
}
|
|
185
260
|
}
|
|
186
261
|
|
|
187
|
-
if (
|
|
188
|
-
|
|
262
|
+
if (
|
|
263
|
+
/atob\(|Buffer\.from/.test(code) &&
|
|
264
|
+
/url|fetch|curl|http\.request|https\.request/.test(code)
|
|
265
|
+
) {
|
|
266
|
+
const isNetworkObfusc =
|
|
267
|
+
/atob\(.*(https?:\/\/|\\x|http).*\)/s.test(code) ||
|
|
189
268
|
/Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"].*fetch\(|fetch\(.*atob\(/s.test(code);
|
|
190
269
|
if (isNetworkObfusc) {
|
|
191
270
|
const evidence = createEvidence(code, filePath, /atob\(|Buffer\.from/, pkgJson);
|
|
@@ -259,4 +338,4 @@ export async function scan(pkgJson, files = []) {
|
|
|
259
338
|
}
|
|
260
339
|
|
|
261
340
|
return findings;
|
|
262
|
-
}
|
|
341
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
4
|
-
if (
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
|
+
if (
|
|
5
|
+
/process\.env\.(NPM_TOKEN|GIT_TOKEN|AWS_SECRET|AWS_ACCESS|SSH_KEY)|\.npmrc|\.ssh\/id_rsa|readFile.*\.ssh/.test(
|
|
6
|
+
code
|
|
7
|
+
)
|
|
8
|
+
) {
|
|
5
9
|
findings.push({
|
|
6
10
|
id: 'ATK-003',
|
|
7
11
|
severity: 'high',
|
|
8
12
|
title: 'Credential harvesting',
|
|
9
13
|
description: 'Env vars or .npmrc/SSH key access',
|
|
10
|
-
evidence: 'credential pattern match'
|
|
14
|
+
evidence: 'credential pattern match',
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
return findings;
|
|
14
|
-
}
|
|
18
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
4
|
if (/mkdir.*(\.vscode|\.claude|\.cursor)/.test(code)) {
|
|
5
5
|
findings.push({
|
|
6
6
|
id: 'ATK-004',
|
|
7
7
|
severity: 'high',
|
|
8
8
|
title: 'Persistence via editor configs',
|
|
9
9
|
description: 'Creates .vscode/.claude/.cursor dirs',
|
|
10
|
-
evidence: 'mkdir pattern match'
|
|
10
|
+
evidence: 'mkdir pattern match',
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
13
|
return findings;
|
|
14
|
-
}
|
|
14
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export async function scan(pkgJson, files = []) {
|
|
2
2
|
const findings = [];
|
|
3
|
-
const code = files.map(f => f.content).join('\n');
|
|
4
|
-
if (
|
|
3
|
+
const code = files.map((f) => f.content).join('\n');
|
|
4
|
+
if (
|
|
5
|
+
/curl.*(-d|--data|--data-binary)|github\.com\/.*keys|pastebin|dns\.resolve.*\.com|exfil/.test(
|
|
6
|
+
code.toLowerCase()
|
|
7
|
+
)
|
|
8
|
+
) {
|
|
5
9
|
findings.push({
|
|
6
10
|
id: 'ATK-005',
|
|
7
11
|
severity: 'critical',
|
|
8
12
|
title: 'Network exfiltration',
|
|
9
13
|
description: 'Suspicious network calls: curl data exfil, pastebin, dns tunneling',
|
|
10
|
-
evidence: 'network exfil pattern'
|
|
14
|
+
evidence: 'network exfil pattern',
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
return findings;
|
|
14
|
-
}
|
|
18
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
export async function scan(pkgJson) {
|
|
2
2
|
const findings = [];
|
|
3
3
|
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
4
|
-
const squat = Object.keys(deps).filter(d => /squat|confus|typo/i.test(d.toLowerCase()));
|
|
4
|
+
const squat = Object.keys(deps).filter((d) => /squat|confus|typo/i.test(d.toLowerCase()));
|
|
5
5
|
if (squat.length) {
|
|
6
6
|
findings.push({
|
|
7
7
|
id: 'ATK-006',
|
|
8
8
|
severity: 'medium',
|
|
9
9
|
title: 'Dependency confusion',
|
|
10
10
|
description: 'Suspicious dependency names',
|
|
11
|
-
evidence: squat.join(', ')
|
|
11
|
+
evidence: squat.join(', '),
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
return findings;
|
|
15
|
-
}
|
|
15
|
+
}
|