@lateos/npm-scan 0.18.3 → 1.1.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/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- 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 +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- 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 +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- 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 +138 -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 +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- 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 +147 -0
- 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 +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- 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 +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- 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 +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
|
@@ -10,7 +10,9 @@ export function scanCampaignMarker(allFiles) {
|
|
|
10
10
|
const ext = path.includes('.') ? '.' + path.split('.').pop() : '';
|
|
11
11
|
|
|
12
12
|
const isTarget = TARGET_FILENAMES.has(basename) || TARGET_EXTENSIONS.includes(ext);
|
|
13
|
-
if (!isTarget)
|
|
13
|
+
if (!isTarget) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
14
16
|
|
|
15
17
|
if (content.includes('P-2024-001')) {
|
|
16
18
|
matches.push({ file: path });
|
|
@@ -12,7 +12,7 @@ export function scanPayloadFingerprint(allFiles) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
if (byteSize === 48485) {
|
|
15
|
-
const alreadyMatched = matches.some(m => m.file === path);
|
|
15
|
+
const alreadyMatched = matches.some((m) => m.file === path);
|
|
16
16
|
if (!alreadyMatched) {
|
|
17
17
|
matches.push({ file: path, matchType: 'byteSize', byteSize });
|
|
18
18
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export function scanPublisherBlocklist(pkgJson, registryMeta) {
|
|
2
|
-
const publisherAccount =
|
|
3
|
-
|
|
4
|
-
||
|
|
2
|
+
const publisherAccount =
|
|
3
|
+
registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name ||
|
|
4
|
+
registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name ||
|
|
5
|
+
null;
|
|
5
6
|
|
|
6
7
|
if (publisherAccount === 'asdxzxc') {
|
|
7
8
|
return { triggered: true, publisher: publisherAccount };
|
|
@@ -3,8 +3,10 @@ const C2_PATTERNS = [/ddjidd564\.github\.io/i, /gist\.github\.com/i];
|
|
|
3
3
|
|
|
4
4
|
function scanContent(content, filePath) {
|
|
5
5
|
const matches = [];
|
|
6
|
-
const hasC2 = C2_PATTERNS.some(p => p.test(content));
|
|
7
|
-
if (!hasC2)
|
|
6
|
+
const hasC2 = C2_PATTERNS.some((p) => p.test(content));
|
|
7
|
+
if (!hasC2) {
|
|
8
|
+
return matches;
|
|
9
|
+
}
|
|
8
10
|
|
|
9
11
|
const hasCredPath = CRED_PATH_PATTERNS.test(content);
|
|
10
12
|
if (hasCredPath) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const ZERO_WIDTH_RANGES = [
|
|
2
|
-
[
|
|
3
|
-
[
|
|
2
|
+
[0x200b, 0x200d],
|
|
3
|
+
[0xfeff, 0xfeff],
|
|
4
4
|
];
|
|
5
5
|
|
|
6
6
|
function isZeroWidthChar(code) {
|
|
@@ -14,7 +14,9 @@ export function scanAIPoisoning(allFiles) {
|
|
|
14
14
|
|
|
15
15
|
for (const file of allFiles) {
|
|
16
16
|
const path = file.path?.replace(/\\/g, '/') || '';
|
|
17
|
-
if (!TARGET_FILES.test(path))
|
|
17
|
+
if (!TARGET_FILES.test(path)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
18
20
|
|
|
19
21
|
const content = file.content || '';
|
|
20
22
|
const found = [];
|
|
@@ -12,16 +12,21 @@ const LURE_PATTERNS = [
|
|
|
12
12
|
|
|
13
13
|
export function scanLureName(pkgJson, registryMeta) {
|
|
14
14
|
const pkgName = pkgJson?.name || '';
|
|
15
|
-
const matchedPattern = LURE_PATTERNS.find(p => p.test(pkgName));
|
|
16
|
-
if (!matchedPattern)
|
|
15
|
+
const matchedPattern = LURE_PATTERNS.find((p) => p.test(pkgName));
|
|
16
|
+
if (!matchedPattern) {
|
|
17
|
+
return { triggered: false };
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
const timeMap = registryMeta?.time || {};
|
|
19
|
-
const versions = Object.keys(timeMap).filter(v => v !== 'created' && v !== 'modified');
|
|
20
|
-
const firstVersion =
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const versions = Object.keys(timeMap).filter((v) => v !== 'created' && v !== 'modified');
|
|
22
|
+
const firstVersion =
|
|
23
|
+
versions.length > 0
|
|
24
|
+
? versions.sort((a, b) => new Date(timeMap[a]) - new Date(timeMap[b]))[0]
|
|
25
|
+
: null;
|
|
23
26
|
|
|
24
|
-
if (!firstVersion)
|
|
27
|
+
if (!firstVersion) {
|
|
28
|
+
return { triggered: false };
|
|
29
|
+
}
|
|
25
30
|
|
|
26
31
|
const firstPubDate = new Date(timeMap[firstVersion]);
|
|
27
32
|
const now = new Date();
|
|
@@ -7,8 +7,8 @@ export function scanCryptoPrimitives(allFiles, pkgJson) {
|
|
|
7
7
|
.map(([hook, content]) => ({ file: `script:${hook}`, content }));
|
|
8
8
|
|
|
9
9
|
const jsFiles = allFiles
|
|
10
|
-
.filter(f => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
|
|
11
|
-
.map(f => ({ file: f.path, content: f.content || '' }));
|
|
10
|
+
.filter((f) => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
|
|
11
|
+
.map((f) => ({ file: f.path, content: f.content || '' }));
|
|
12
12
|
|
|
13
13
|
for (const { file, content } of [...scriptEntries, ...jsFiles]) {
|
|
14
14
|
const hasFernet = /Fernet/i.test(content);
|
|
@@ -2,9 +2,14 @@ export function scanXorKey(allFiles) {
|
|
|
2
2
|
const matches = [];
|
|
3
3
|
for (const file of allFiles) {
|
|
4
4
|
const path = file.path?.replace(/\\/g, '/') || '';
|
|
5
|
-
const isLockFile =
|
|
5
|
+
const isLockFile =
|
|
6
|
+
/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|pnpm-lock\.yml|Cargo\.lock|Cargo\.toml)/i.test(
|
|
7
|
+
path
|
|
8
|
+
);
|
|
6
9
|
const isBundled = /\.node$|vendor|native/i.test(path);
|
|
7
|
-
if (!isLockFile && !isBundled)
|
|
10
|
+
if (!isLockFile && !isBundled) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
const content = file.content || '';
|
|
10
15
|
if (content.includes('cargo-build-helper-2026')) {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
const CRED_VALIDATION_PATTERNS = [
|
|
2
|
-
/sts\.amazonaws\.com/i,
|
|
3
|
-
/api\.github\.com\/user/i,
|
|
4
|
-
];
|
|
1
|
+
const CRED_VALIDATION_PATTERNS = [/sts\.amazonaws\.com/i, /api\.github\.com\/user/i];
|
|
5
2
|
|
|
6
3
|
export function scanCredValidation(allFiles, pkgJson) {
|
|
7
4
|
const matches = [];
|
|
@@ -19,7 +16,9 @@ export function scanCredValidation(allFiles, pkgJson) {
|
|
|
19
16
|
|
|
20
17
|
for (const file of allFiles) {
|
|
21
18
|
const path = file.path || '';
|
|
22
|
-
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs'))
|
|
19
|
+
if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
23
22
|
const content = file.content || '';
|
|
24
23
|
for (const pattern of CRED_VALIDATION_PATTERNS) {
|
|
25
24
|
if (pattern.test(content)) {
|
|
@@ -24,7 +24,9 @@ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
|
|
|
24
24
|
|
|
25
25
|
function highestSeverity(severities) {
|
|
26
26
|
for (const s of SEVERITY_ORDER) {
|
|
27
|
-
if (severities.includes(s))
|
|
27
|
+
if (severities.includes(s)) {
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
28
30
|
}
|
|
29
31
|
return 'none';
|
|
30
32
|
}
|
|
@@ -48,16 +50,16 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
48
50
|
.filter(([_, r]) => r.triggered)
|
|
49
51
|
.map(([id]) => id);
|
|
50
52
|
|
|
51
|
-
if (triggered.length === 0)
|
|
53
|
+
if (triggered.length === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
52
56
|
|
|
53
|
-
const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
|
|
57
|
+
const severity = highestSeverity(triggered.map((id) => RULE_SEVERITY[id]));
|
|
54
58
|
|
|
55
59
|
const evidence = {
|
|
56
60
|
campaign: 'TRAPDOOR',
|
|
57
61
|
triggeredRules: triggered,
|
|
58
|
-
details: Object.fromEntries(
|
|
59
|
-
Object.entries(results).filter(([_, r]) => r.triggered)
|
|
60
|
-
),
|
|
62
|
+
details: Object.fromEntries(Object.entries(results).filter(([_, r]) => r.triggered)),
|
|
61
63
|
iocSummary: {
|
|
62
64
|
publisher: 'asdxzxc',
|
|
63
65
|
c2Domain: 'ddjidd564.github.io',
|
|
@@ -66,12 +68,15 @@ export async function scan(pkgJson, files = [], registryMeta = null, allFiles =
|
|
|
66
68
|
},
|
|
67
69
|
};
|
|
68
70
|
|
|
69
|
-
return [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
id: 'TRAPDOOR',
|
|
74
|
+
severity,
|
|
75
|
+
title: 'TrapDoor cross-ecosystem supply chain attack campaign',
|
|
76
|
+
description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
|
|
77
|
+
evidence: JSON.stringify(evidence),
|
|
78
|
+
mitigation:
|
|
79
|
+
'Block install immediately. Revoke any npm tokens associated with this package. Rotate CI/CD secrets. Audit for postinstall scripts accessing credentials. Check for AI config poisoning (.cursorrules/CLAUDE.md). Verify all package versions from publisher asdxzxc. If confirmed compromise, follow incident response procedures per SECURITY.md.',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
77
82
|
}
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
const BLOCKED_MAINTAINERS = ['vpmdhaj'];
|
|
2
2
|
const VPMDHAJ_PREFIX_RE = /^vpmdhaj-/;
|
|
3
3
|
const TYPOSQUAT_TARGETS = [
|
|
4
|
-
'opensearch-setup',
|
|
5
|
-
'
|
|
6
|
-
'
|
|
4
|
+
'opensearch-setup',
|
|
5
|
+
'env-config-manager',
|
|
6
|
+
'express',
|
|
7
|
+
'lodash',
|
|
8
|
+
'axios',
|
|
9
|
+
'react',
|
|
10
|
+
'vue',
|
|
11
|
+
'angular',
|
|
12
|
+
'babel',
|
|
13
|
+
'webpack',
|
|
14
|
+
'typescript',
|
|
15
|
+
'moment',
|
|
16
|
+
'dotenv',
|
|
7
17
|
];
|
|
8
18
|
|
|
9
19
|
function levenshteinDistance(a, b) {
|
|
10
|
-
const m = a.length,
|
|
20
|
+
const m = a.length,
|
|
21
|
+
n = b.length;
|
|
11
22
|
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
12
|
-
for (let i = 0; i <= m; i++)
|
|
13
|
-
|
|
23
|
+
for (let i = 0; i <= m; i++) {
|
|
24
|
+
dp[i][0] = i;
|
|
25
|
+
}
|
|
26
|
+
for (let j = 0; j <= n; j++) {
|
|
27
|
+
dp[0][j] = j;
|
|
28
|
+
}
|
|
14
29
|
for (let i = 1; i <= m; i++) {
|
|
15
30
|
for (let j = 1; j <= n; j++) {
|
|
16
|
-
dp[i][j] =
|
|
31
|
+
dp[i][j] =
|
|
32
|
+
a[i - 1] === b[j - 1]
|
|
33
|
+
? dp[i - 1][j - 1]
|
|
34
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
17
35
|
}
|
|
18
36
|
}
|
|
19
37
|
return dp[m][n];
|
|
@@ -71,7 +89,13 @@ export function scanMaintainerAnomaly(pkgJson, registryMeta) {
|
|
|
71
89
|
};
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
triggered: false,
|
|
94
|
+
stopCondition: false,
|
|
95
|
+
maintainer: '',
|
|
96
|
+
suspiciousAliases: [],
|
|
97
|
+
reason: '',
|
|
98
|
+
};
|
|
75
99
|
}
|
|
76
100
|
|
|
77
101
|
export { BLOCKED_MAINTAINERS };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const SUSPICIOUS_HOOKS = ['preinstall'];
|
|
2
|
-
const
|
|
2
|
+
const _LOADER_SCRIPTS = ['setup.mjs', 'loader.js', 'stager.js', 'init.mjs'];
|
|
3
3
|
const BUN_RUN_RE = /\bbun\s+run\b/;
|
|
4
4
|
const NODE_SETUP_RE = /\bnode\s+(setup\.mjs|init\.mjs|loader\.js|stager\.js)\b/;
|
|
5
|
-
const
|
|
5
|
+
const _PREINSTALL_STAGER_RE = /preinstall\s*[:=]/;
|
|
6
6
|
|
|
7
7
|
export function scanPreinstallLoader(pkgJson) {
|
|
8
8
|
const scripts = pkgJson?.scripts || {};
|
|
@@ -10,7 +10,9 @@ export function scanPreinstallLoader(pkgJson) {
|
|
|
10
10
|
|
|
11
11
|
for (const hook of SUSPICIOUS_HOOKS) {
|
|
12
12
|
const cmd = scripts[hook];
|
|
13
|
-
if (!cmd)
|
|
13
|
+
if (!cmd) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
14
16
|
|
|
15
17
|
const details = { hookType: hook, hookCommand: cmd };
|
|
16
18
|
|
|
@@ -4,49 +4,71 @@ const VAULT_CRED_RE = /VAULT_ADDR|VAULT_TOKEN/;
|
|
|
4
4
|
const GITHUB_TOKEN_RE = /GITHUB_TOKEN|GH_TOKEN/;
|
|
5
5
|
const AWS_ACCESS_KEY_RE = /AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN/;
|
|
6
6
|
const BASE64_OBFUSCATION_RE = /Buffer\.from\([^)]+['"]base64['"]\)|btoa\(|atob\(/;
|
|
7
|
-
const HTTP_POST_EXFIL_RE =
|
|
8
|
-
|
|
7
|
+
const HTTP_POST_EXFIL_RE =
|
|
8
|
+
/(?:fetch|axios|request|got|curl)\s*\([^)]*(?:https?:\/\/[^'"\s)\]]+)[^)]*(?:method\s*[:=]\s*['"]POST['"]|\.post\s*\()/;
|
|
9
|
+
const DOMAIN_EXFIL_RE =
|
|
10
|
+
/(?:fetch|axios|request|got|curl)\s*\(['"](?:https?:\/\/)?[^'"\s)\]]*\.[^'"\s)\]]{2,}[^)]*\)/;
|
|
9
11
|
|
|
10
12
|
const TARGET_ENV_VARS = {
|
|
11
|
-
AWS: [
|
|
13
|
+
AWS: [
|
|
14
|
+
'AWS_CONTAINER_CREDENTIALS_FULL_URI',
|
|
15
|
+
'AWS_CONTAINER_AUTHORIZATION_TOKEN',
|
|
16
|
+
'AWS_ACCESS_KEY_ID',
|
|
17
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
18
|
+
'AWS_SESSION_TOKEN',
|
|
19
|
+
],
|
|
12
20
|
VAULT: ['VAULT_ADDR', 'VAULT_TOKEN'],
|
|
13
21
|
GITHUB: ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
14
22
|
};
|
|
15
23
|
|
|
16
|
-
export function scanCredExfil(files = [],
|
|
17
|
-
const code = files.map(f => f.content || '').join('\n');
|
|
18
|
-
if (!code)
|
|
24
|
+
export function scanCredExfil(files = [], _pkgJson) {
|
|
25
|
+
const code = files.map((f) => f.content || '').join('\n');
|
|
26
|
+
if (!code) {
|
|
27
|
+
return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
|
|
28
|
+
}
|
|
19
29
|
|
|
20
30
|
const targets = [];
|
|
21
31
|
const detectedEnvVars = [];
|
|
22
32
|
|
|
23
|
-
if (AWS_IMDS_RE.test(code))
|
|
33
|
+
if (AWS_IMDS_RE.test(code)) {
|
|
34
|
+
targets.push('AWS_IMDSv2');
|
|
35
|
+
}
|
|
24
36
|
if (ECS_CRED_RE.test(code)) {
|
|
25
37
|
targets.push('ECS_TASK_ROLE');
|
|
26
38
|
for (const v of TARGET_ENV_VARS.AWS) {
|
|
27
|
-
if (code.includes(v))
|
|
39
|
+
if (code.includes(v)) {
|
|
40
|
+
detectedEnvVars.push(v);
|
|
41
|
+
}
|
|
28
42
|
}
|
|
29
43
|
}
|
|
30
44
|
if (VAULT_CRED_RE.test(code)) {
|
|
31
45
|
targets.push('VAULT_CREDENTIALS');
|
|
32
46
|
for (const v of TARGET_ENV_VARS.VAULT) {
|
|
33
|
-
if (code.includes(v))
|
|
47
|
+
if (code.includes(v)) {
|
|
48
|
+
detectedEnvVars.push(v);
|
|
49
|
+
}
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
if (GITHUB_TOKEN_RE.test(code)) {
|
|
37
53
|
targets.push('GITHUB_TOKEN');
|
|
38
54
|
for (const v of TARGET_ENV_VARS.GITHUB) {
|
|
39
|
-
if (code.includes(v))
|
|
55
|
+
if (code.includes(v)) {
|
|
56
|
+
detectedEnvVars.push(v);
|
|
57
|
+
}
|
|
40
58
|
}
|
|
41
59
|
}
|
|
42
60
|
if (AWS_ACCESS_KEY_RE.test(code)) {
|
|
43
61
|
targets.push('AWS_ACCESS_KEYS');
|
|
44
62
|
for (const v of TARGET_ENV_VARS.AWS) {
|
|
45
|
-
if (code.includes(v) && !detectedEnvVars.includes(v))
|
|
63
|
+
if (code.includes(v) && !detectedEnvVars.includes(v)) {
|
|
64
|
+
detectedEnvVars.push(v);
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
if (targets.length === 0)
|
|
69
|
+
if (targets.length === 0) {
|
|
70
|
+
return { triggered: false, targets: [], exfilMethod: null, detectedEnvVars: [] };
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
let exfilMethod = null;
|
|
52
74
|
if (HTTP_POST_EXFIL_RE.test(code)) {
|
|
@@ -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
|
}
|