@lateos/npm-scan 0.18.2 → 1.0.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 +265 -233
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +861 -826
- package/README.zh.md +708 -708
- package/VALIDATION.md +92 -0
- package/backend/cra.js +68 -68
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/config/thresholds.js +66 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +87 -81
- package/backend/detectors/lib/ast-patterns.js +21 -0
- package/backend/detectors/lib/entropy-analyzer.js +24 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-binary-embed.js +34 -5
- package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-version-anomaly.js +187 -0
- package/backend/detectors.test.js +88 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -193
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/scripts/analyze-false-positives.js +146 -0
- package/backend/scripts/analyze-validation.js +151 -0
- package/backend/scripts/detect-false-positives.js +93 -0
- package/backend/scripts/fetch-top-packages.js +129 -0
- package/backend/scripts/validate-detectors.js +142 -0
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/tests-d5-enhanced.test.js +46 -0
- package/backend/tests-d6-version-anomaly.test.js +58 -0
- package/backend/tests-d6.test.js +116 -0
- package/backend/tests-d6c.test.js +106 -0
- package/backend/tests-d7-obfuscation.test.js +91 -0
- package/backend/tests.test.js +898 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/package.json +74 -57
- 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
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { MegalodonSignal } from './types.js';
|
|
2
|
-
|
|
3
|
-
export async function scan(registryMeta, velocityResult) {
|
|
4
|
-
const evidence = [];
|
|
5
|
-
const versions = registryMeta?.versions || {};
|
|
6
|
-
const timeMap = registryMeta?.time || {};
|
|
7
|
-
|
|
8
|
-
const filteredTimes = {};
|
|
9
|
-
for (const [v, t] of Object.entries(timeMap)) {
|
|
10
|
-
if (v === 'created' || v === 'modified') continue;
|
|
11
|
-
if (t) filteredTimes[v] = t;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const sortedVersions = Object.entries(filteredTimes)
|
|
15
|
-
.filter(([, t]) => t && !Number.isNaN(new Date(t).getTime()))
|
|
16
|
-
.sort((a, b) => new Date(a[1]).getTime() - new Date(b[1]).getTime())
|
|
17
|
-
.map(([v]) => v);
|
|
18
|
-
|
|
19
|
-
if (sortedVersions.length === 0) return [];
|
|
20
|
-
|
|
21
|
-
if (velocityResult?.triggered) {
|
|
22
|
-
const windowStartISO = velocityResult.windowStartISO;
|
|
23
|
-
const allInWindow = velocityResult._allVersions || [];
|
|
24
|
-
|
|
25
|
-
const priorPublishers = new Set();
|
|
26
|
-
for (const v of sortedVersions) {
|
|
27
|
-
if (new Date(filteredTimes[v]).getTime() >= new Date(windowStartISO).getTime()) break;
|
|
28
|
-
const user = versions[v]?._npmUser?.name;
|
|
29
|
-
if (user) priorPublishers.add(user);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (priorPublishers.size === 0 && allInWindow.length > 0) {
|
|
33
|
-
const firstUser = versions[allInWindow[0]]?._npmUser?.name;
|
|
34
|
-
if (firstUser) priorPublishers.add(firstUser);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const suspiciousPublishers = [];
|
|
38
|
-
const affectedVersions = [];
|
|
39
|
-
for (const v of allInWindow) {
|
|
40
|
-
const user = versions[v]?._npmUser?.name;
|
|
41
|
-
if (user && !priorPublishers.has(user)) {
|
|
42
|
-
if (!suspiciousPublishers.includes(user)) suspiciousPublishers.push(user);
|
|
43
|
-
if (!affectedVersions.includes(v)) affectedVersions.push(v);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (suspiciousPublishers.length > 0) {
|
|
48
|
-
const detail = `Drift detected: known publishers [${[...priorPublishers].join(', ')}], new publisher(s) [${suspiciousPublishers.join(', ')}] in versions [${affectedVersions.join(', ')}]`;
|
|
49
|
-
|
|
50
|
-
const firstSuspiciousVer = allInWindow.find(v => affectedVersions.includes(v));
|
|
51
|
-
let ageNote = '';
|
|
52
|
-
if (firstSuspiciousVer && suspiciousPublishers[0]) {
|
|
53
|
-
ageNote = await checkAccountAge(suspiciousPublishers[0], filteredTimes[firstSuspiciousVer]);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
evidence.push({
|
|
57
|
-
signal: MegalodonSignal.PUBLISHER_DRIFT,
|
|
58
|
-
file: 'registry.npmjs.org',
|
|
59
|
-
excerpt: `publisher drift: ${suspiciousPublishers.join(', ')}`,
|
|
60
|
-
detail: detail + (ageNote ? ' | ' + ageNote : ''),
|
|
61
|
-
_severityHint: 'HIGH',
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
if (sortedVersions.length < 4) return [];
|
|
66
|
-
|
|
67
|
-
const last3 = sortedVersions.slice(-3);
|
|
68
|
-
const prior = sortedVersions.slice(0, -3);
|
|
69
|
-
|
|
70
|
-
const priorPublishers = new Set();
|
|
71
|
-
for (const v of prior) {
|
|
72
|
-
const user = versions[v]?._npmUser?.name;
|
|
73
|
-
if (user) priorPublishers.add(user);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const suspiciousPublishers = [];
|
|
77
|
-
const affectedVersions = [];
|
|
78
|
-
for (const v of last3) {
|
|
79
|
-
const user = versions[v]?._npmUser?.name;
|
|
80
|
-
if (user && !priorPublishers.has(user)) {
|
|
81
|
-
if (!suspiciousPublishers.includes(user)) suspiciousPublishers.push(user);
|
|
82
|
-
if (!affectedVersions.includes(v)) affectedVersions.push(v);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (suspiciousPublishers.length > 0) {
|
|
87
|
-
const detail = `Drift (fallback): known publishers [${[...priorPublishers].join(', ')}], new publisher(s) [${suspiciousPublishers.join(', ')}] in last 3 versions [${affectedVersions.join(', ')}]`;
|
|
88
|
-
|
|
89
|
-
let ageNote = '';
|
|
90
|
-
if (suspiciousPublishers[0] && affectedVersions[0]) {
|
|
91
|
-
ageNote = await checkAccountAge(suspiciousPublishers[0], filteredTimes[affectedVersions[0]]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
evidence.push({
|
|
95
|
-
signal: MegalodonSignal.PUBLISHER_DRIFT,
|
|
96
|
-
file: 'registry.npmjs.org',
|
|
97
|
-
excerpt: `publisher drift: ${suspiciousPublishers.join(', ')}`,
|
|
98
|
-
detail: detail + (ageNote ? ' | ' + ageNote : ''),
|
|
99
|
-
_severityHint: 'MEDIUM',
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return evidence;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function checkAccountAge(npmUser, firstSuspiciousTime) {
|
|
108
|
-
try {
|
|
109
|
-
const url = `https://registry.npmjs.org/-/user/org.couchdb.user/${encodeURIComponent(npmUser)}`;
|
|
110
|
-
const res = await fetch(url);
|
|
111
|
-
if (!res.ok) return '';
|
|
112
|
-
const data = await res.json();
|
|
113
|
-
const created = data?.date;
|
|
114
|
-
if (!created) return '';
|
|
115
|
-
const createdDate = new Date(created).getTime();
|
|
116
|
-
const firstPub = new Date(firstSuspiciousTime).getTime();
|
|
117
|
-
const daysDiff = (firstPub - createdDate) / (1000 * 60 * 60 * 24);
|
|
118
|
-
if (!Number.isNaN(daysDiff) && daysDiff >= 0 && daysDiff <= 30) {
|
|
119
|
-
return `Publisher account created ${Math.round(daysDiff)} days before first suspicious publish`;
|
|
120
|
-
}
|
|
121
|
-
} catch {
|
|
122
|
-
}
|
|
123
|
-
return '';
|
|
124
|
-
}
|
|
1
|
+
import { MegalodonSignal } from './types.js';
|
|
2
|
+
|
|
3
|
+
export async function scan(registryMeta, velocityResult) {
|
|
4
|
+
const evidence = [];
|
|
5
|
+
const versions = registryMeta?.versions || {};
|
|
6
|
+
const timeMap = registryMeta?.time || {};
|
|
7
|
+
|
|
8
|
+
const filteredTimes = {};
|
|
9
|
+
for (const [v, t] of Object.entries(timeMap)) {
|
|
10
|
+
if (v === 'created' || v === 'modified') continue;
|
|
11
|
+
if (t) filteredTimes[v] = t;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sortedVersions = Object.entries(filteredTimes)
|
|
15
|
+
.filter(([, t]) => t && !Number.isNaN(new Date(t).getTime()))
|
|
16
|
+
.sort((a, b) => new Date(a[1]).getTime() - new Date(b[1]).getTime())
|
|
17
|
+
.map(([v]) => v);
|
|
18
|
+
|
|
19
|
+
if (sortedVersions.length === 0) return [];
|
|
20
|
+
|
|
21
|
+
if (velocityResult?.triggered) {
|
|
22
|
+
const windowStartISO = velocityResult.windowStartISO;
|
|
23
|
+
const allInWindow = velocityResult._allVersions || [];
|
|
24
|
+
|
|
25
|
+
const priorPublishers = new Set();
|
|
26
|
+
for (const v of sortedVersions) {
|
|
27
|
+
if (new Date(filteredTimes[v]).getTime() >= new Date(windowStartISO).getTime()) break;
|
|
28
|
+
const user = versions[v]?._npmUser?.name;
|
|
29
|
+
if (user) priorPublishers.add(user);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (priorPublishers.size === 0 && allInWindow.length > 0) {
|
|
33
|
+
const firstUser = versions[allInWindow[0]]?._npmUser?.name;
|
|
34
|
+
if (firstUser) priorPublishers.add(firstUser);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const suspiciousPublishers = [];
|
|
38
|
+
const affectedVersions = [];
|
|
39
|
+
for (const v of allInWindow) {
|
|
40
|
+
const user = versions[v]?._npmUser?.name;
|
|
41
|
+
if (user && !priorPublishers.has(user)) {
|
|
42
|
+
if (!suspiciousPublishers.includes(user)) suspiciousPublishers.push(user);
|
|
43
|
+
if (!affectedVersions.includes(v)) affectedVersions.push(v);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (suspiciousPublishers.length > 0) {
|
|
48
|
+
const detail = `Drift detected: known publishers [${[...priorPublishers].join(', ')}], new publisher(s) [${suspiciousPublishers.join(', ')}] in versions [${affectedVersions.join(', ')}]`;
|
|
49
|
+
|
|
50
|
+
const firstSuspiciousVer = allInWindow.find(v => affectedVersions.includes(v));
|
|
51
|
+
let ageNote = '';
|
|
52
|
+
if (firstSuspiciousVer && suspiciousPublishers[0]) {
|
|
53
|
+
ageNote = await checkAccountAge(suspiciousPublishers[0], filteredTimes[firstSuspiciousVer]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
evidence.push({
|
|
57
|
+
signal: MegalodonSignal.PUBLISHER_DRIFT,
|
|
58
|
+
file: 'registry.npmjs.org',
|
|
59
|
+
excerpt: `publisher drift: ${suspiciousPublishers.join(', ')}`,
|
|
60
|
+
detail: detail + (ageNote ? ' | ' + ageNote : ''),
|
|
61
|
+
_severityHint: 'HIGH',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
if (sortedVersions.length < 4) return [];
|
|
66
|
+
|
|
67
|
+
const last3 = sortedVersions.slice(-3);
|
|
68
|
+
const prior = sortedVersions.slice(0, -3);
|
|
69
|
+
|
|
70
|
+
const priorPublishers = new Set();
|
|
71
|
+
for (const v of prior) {
|
|
72
|
+
const user = versions[v]?._npmUser?.name;
|
|
73
|
+
if (user) priorPublishers.add(user);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const suspiciousPublishers = [];
|
|
77
|
+
const affectedVersions = [];
|
|
78
|
+
for (const v of last3) {
|
|
79
|
+
const user = versions[v]?._npmUser?.name;
|
|
80
|
+
if (user && !priorPublishers.has(user)) {
|
|
81
|
+
if (!suspiciousPublishers.includes(user)) suspiciousPublishers.push(user);
|
|
82
|
+
if (!affectedVersions.includes(v)) affectedVersions.push(v);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (suspiciousPublishers.length > 0) {
|
|
87
|
+
const detail = `Drift (fallback): known publishers [${[...priorPublishers].join(', ')}], new publisher(s) [${suspiciousPublishers.join(', ')}] in last 3 versions [${affectedVersions.join(', ')}]`;
|
|
88
|
+
|
|
89
|
+
let ageNote = '';
|
|
90
|
+
if (suspiciousPublishers[0] && affectedVersions[0]) {
|
|
91
|
+
ageNote = await checkAccountAge(suspiciousPublishers[0], filteredTimes[affectedVersions[0]]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
evidence.push({
|
|
95
|
+
signal: MegalodonSignal.PUBLISHER_DRIFT,
|
|
96
|
+
file: 'registry.npmjs.org',
|
|
97
|
+
excerpt: `publisher drift: ${suspiciousPublishers.join(', ')}`,
|
|
98
|
+
detail: detail + (ageNote ? ' | ' + ageNote : ''),
|
|
99
|
+
_severityHint: 'MEDIUM',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return evidence;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function checkAccountAge(npmUser, firstSuspiciousTime) {
|
|
108
|
+
try {
|
|
109
|
+
const url = `https://registry.npmjs.org/-/user/org.couchdb.user/${encodeURIComponent(npmUser)}`;
|
|
110
|
+
const res = await fetch(url);
|
|
111
|
+
if (!res.ok) return '';
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
const created = data?.date;
|
|
114
|
+
if (!created) return '';
|
|
115
|
+
const createdDate = new Date(created).getTime();
|
|
116
|
+
const firstPub = new Date(firstSuspiciousTime).getTime();
|
|
117
|
+
const daysDiff = (firstPub - createdDate) / (1000 * 60 * 60 * 24);
|
|
118
|
+
if (!Number.isNaN(daysDiff) && daysDiff >= 0 && daysDiff <= 30) {
|
|
119
|
+
return `Publisher account created ${Math.round(daysDiff)} days before first suspicious publish`;
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export async function scan(registryMeta) {
|
|
2
|
-
return [];
|
|
3
|
-
}
|
|
1
|
+
export async function scan(registryMeta) {
|
|
2
|
+
return [];
|
|
3
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export async function scan(pkgJson, registryMeta) {
|
|
2
|
-
return [];
|
|
3
|
-
}
|
|
1
|
+
export async function scan(pkgJson, registryMeta) {
|
|
2
|
+
return [];
|
|
3
|
+
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { MegalodonSignal } from './types.js';
|
|
2
|
-
import { scan as scanD1 } from './d1-workflow-scan.js';
|
|
3
|
-
import { scan as scanD2 } from './d2-credential-harvest.js';
|
|
4
|
-
import { scan as scanD3 } from './d3-publish-velocity.js';
|
|
5
|
-
import { scan as scanD4 } from './d4-publisher-drift.js';
|
|
6
|
-
import { scan as scanD5 } from './d5-bot-commit-identity.js';
|
|
7
|
-
import { scan as scanD6 } from './d6-date-anachronism.js';
|
|
8
|
-
|
|
9
|
-
const SIGNAL_SEVERITY = {
|
|
10
|
-
[MegalodonSignal.WORKFLOW_C2_EXFIL]: 5,
|
|
11
|
-
[MegalodonSignal.WORKFLOW_DECODE_CHAIN]: 4,
|
|
12
|
-
[MegalodonSignal.PUBLISH_VELOCITY]: 4,
|
|
13
|
-
[MegalodonSignal.PUBLISHER_DRIFT]: 4,
|
|
14
|
-
[MegalodonSignal.CREDENTIAL_HARVEST]: 3,
|
|
15
|
-
[MegalodonSignal.BOT_COMMIT_IDENTITY]: 2,
|
|
16
|
-
[MegalodonSignal.DATE_ANACHRONISM]: 2,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical', 'critical'];
|
|
20
|
-
|
|
21
|
-
function resolveSeverity(signals, d4Evidence) {
|
|
22
|
-
let maxScore = 0;
|
|
23
|
-
for (const s of signals) {
|
|
24
|
-
maxScore = Math.max(maxScore, SIGNAL_SEVERITY[s] || 0);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const d4Hint = d4Evidence.find(e => e._severityHint);
|
|
28
|
-
if (d4Hint) {
|
|
29
|
-
const hintScore = d4Hint._severityHint === 'HIGH' ? 4 : d4Hint._severityHint === 'MEDIUM' ? 3 : 0;
|
|
30
|
-
maxScore = Math.max(maxScore, hintScore);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return SEVERITY_LABELS[maxScore] || 'none';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function scanAll(pkgJson, allFiles = [], registryMeta = {}) {
|
|
37
|
-
const allEvidence = [];
|
|
38
|
-
|
|
39
|
-
const d1Ev = await scanD1(allFiles);
|
|
40
|
-
allEvidence.push(...d1Ev);
|
|
41
|
-
|
|
42
|
-
const d2Ev = await scanD2(allFiles);
|
|
43
|
-
allEvidence.push(...d2Ev);
|
|
44
|
-
|
|
45
|
-
const d3Ev = await scanD3(registryMeta);
|
|
46
|
-
allEvidence.push(...d3Ev);
|
|
47
|
-
|
|
48
|
-
const velocityResult = d3Ev.length > 0 ? {
|
|
49
|
-
triggered: true,
|
|
50
|
-
windowStartISO: d3Ev[0]._windowStartISO || null,
|
|
51
|
-
versionsInWindow: d3Ev[0].excerpt || '',
|
|
52
|
-
_allVersions: d3Ev[0]._allVersions || [],
|
|
53
|
-
} : { triggered: false, versionsInWindow: [], windowStartISO: null };
|
|
54
|
-
|
|
55
|
-
const d4Ev = await scanD4(registryMeta, velocityResult);
|
|
56
|
-
allEvidence.push(...d4Ev);
|
|
57
|
-
|
|
58
|
-
allEvidence.push(...await scanD5(registryMeta));
|
|
59
|
-
allEvidence.push(...await scanD6(pkgJson, registryMeta));
|
|
60
|
-
|
|
61
|
-
const signals = [...new Set(allEvidence.map(e => e.signal).filter(Boolean))];
|
|
62
|
-
|
|
63
|
-
if (signals.length === 0) return [];
|
|
64
|
-
|
|
65
|
-
const severity = resolveSeverity(signals, d4Ev);
|
|
66
|
-
|
|
67
|
-
const cleaned = allEvidence.map(({ _windowStartISO, _allVersions, _severityHint, ...rest }) => rest);
|
|
68
|
-
|
|
69
|
-
return [{
|
|
70
|
-
id: 'MEGALODON',
|
|
71
|
-
severity,
|
|
72
|
-
title: 'Megalodon CI/CD attack campaign',
|
|
73
|
-
description: `${signals.length} signal(s): ${signals.join(', ')}`,
|
|
74
|
-
evidence: JSON.stringify({
|
|
75
|
-
campaign: 'MEGALODON',
|
|
76
|
-
signals,
|
|
77
|
-
evidence: cleaned,
|
|
78
|
-
}),
|
|
79
|
-
}];
|
|
80
|
-
}
|
|
1
|
+
import { MegalodonSignal } from './types.js';
|
|
2
|
+
import { scan as scanD1 } from './d1-workflow-scan.js';
|
|
3
|
+
import { scan as scanD2 } from './d2-credential-harvest.js';
|
|
4
|
+
import { scan as scanD3 } from './d3-publish-velocity.js';
|
|
5
|
+
import { scan as scanD4 } from './d4-publisher-drift.js';
|
|
6
|
+
import { scan as scanD5 } from './d5-bot-commit-identity.js';
|
|
7
|
+
import { scan as scanD6 } from './d6-date-anachronism.js';
|
|
8
|
+
|
|
9
|
+
const SIGNAL_SEVERITY = {
|
|
10
|
+
[MegalodonSignal.WORKFLOW_C2_EXFIL]: 5,
|
|
11
|
+
[MegalodonSignal.WORKFLOW_DECODE_CHAIN]: 4,
|
|
12
|
+
[MegalodonSignal.PUBLISH_VELOCITY]: 4,
|
|
13
|
+
[MegalodonSignal.PUBLISHER_DRIFT]: 4,
|
|
14
|
+
[MegalodonSignal.CREDENTIAL_HARVEST]: 3,
|
|
15
|
+
[MegalodonSignal.BOT_COMMIT_IDENTITY]: 2,
|
|
16
|
+
[MegalodonSignal.DATE_ANACHRONISM]: 2,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical', 'critical'];
|
|
20
|
+
|
|
21
|
+
function resolveSeverity(signals, d4Evidence) {
|
|
22
|
+
let maxScore = 0;
|
|
23
|
+
for (const s of signals) {
|
|
24
|
+
maxScore = Math.max(maxScore, SIGNAL_SEVERITY[s] || 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const d4Hint = d4Evidence.find(e => e._severityHint);
|
|
28
|
+
if (d4Hint) {
|
|
29
|
+
const hintScore = d4Hint._severityHint === 'HIGH' ? 4 : d4Hint._severityHint === 'MEDIUM' ? 3 : 0;
|
|
30
|
+
maxScore = Math.max(maxScore, hintScore);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return SEVERITY_LABELS[maxScore] || 'none';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function scanAll(pkgJson, allFiles = [], registryMeta = {}) {
|
|
37
|
+
const allEvidence = [];
|
|
38
|
+
|
|
39
|
+
const d1Ev = await scanD1(allFiles);
|
|
40
|
+
allEvidence.push(...d1Ev);
|
|
41
|
+
|
|
42
|
+
const d2Ev = await scanD2(allFiles);
|
|
43
|
+
allEvidence.push(...d2Ev);
|
|
44
|
+
|
|
45
|
+
const d3Ev = await scanD3(registryMeta);
|
|
46
|
+
allEvidence.push(...d3Ev);
|
|
47
|
+
|
|
48
|
+
const velocityResult = d3Ev.length > 0 ? {
|
|
49
|
+
triggered: true,
|
|
50
|
+
windowStartISO: d3Ev[0]._windowStartISO || null,
|
|
51
|
+
versionsInWindow: d3Ev[0].excerpt || '',
|
|
52
|
+
_allVersions: d3Ev[0]._allVersions || [],
|
|
53
|
+
} : { triggered: false, versionsInWindow: [], windowStartISO: null };
|
|
54
|
+
|
|
55
|
+
const d4Ev = await scanD4(registryMeta, velocityResult);
|
|
56
|
+
allEvidence.push(...d4Ev);
|
|
57
|
+
|
|
58
|
+
allEvidence.push(...await scanD5(registryMeta));
|
|
59
|
+
allEvidence.push(...await scanD6(pkgJson, registryMeta));
|
|
60
|
+
|
|
61
|
+
const signals = [...new Set(allEvidence.map(e => e.signal).filter(Boolean))];
|
|
62
|
+
|
|
63
|
+
if (signals.length === 0) return [];
|
|
64
|
+
|
|
65
|
+
const severity = resolveSeverity(signals, d4Ev);
|
|
66
|
+
|
|
67
|
+
const cleaned = allEvidence.map(({ _windowStartISO, _allVersions, _severityHint, ...rest }) => rest);
|
|
68
|
+
|
|
69
|
+
return [{
|
|
70
|
+
id: 'MEGALODON',
|
|
71
|
+
severity,
|
|
72
|
+
title: 'Megalodon CI/CD attack campaign',
|
|
73
|
+
description: `${signals.length} signal(s): ${signals.join(', ')}`,
|
|
74
|
+
evidence: JSON.stringify({
|
|
75
|
+
campaign: 'MEGALODON',
|
|
76
|
+
signals,
|
|
77
|
+
evidence: cleaned,
|
|
78
|
+
}),
|
|
79
|
+
}];
|
|
80
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export const MegalodonSignal = Object.freeze({
|
|
2
|
-
WORKFLOW_C2_EXFIL: 'D1_WORKFLOW_C2_EXFIL',
|
|
3
|
-
WORKFLOW_DECODE_CHAIN: 'D1_WORKFLOW_DECODE_CHAIN',
|
|
4
|
-
CREDENTIAL_HARVEST: 'D2_CREDENTIAL_HARVEST',
|
|
5
|
-
PUBLISH_VELOCITY: 'D3_PUBLISH_VELOCITY',
|
|
6
|
-
PUBLISHER_DRIFT: 'D4_PUBLISHER_DRIFT',
|
|
7
|
-
BOT_COMMIT_IDENTITY: 'D5_BOT_COMMIT_IDENTITY',
|
|
8
|
-
DATE_ANACHRONISM: 'D6_DATE_ANACHRONISM',
|
|
9
|
-
});
|
|
1
|
+
export const MegalodonSignal = Object.freeze({
|
|
2
|
+
WORKFLOW_C2_EXFIL: 'D1_WORKFLOW_C2_EXFIL',
|
|
3
|
+
WORKFLOW_DECODE_CHAIN: 'D1_WORKFLOW_DECODE_CHAIN',
|
|
4
|
+
CREDENTIAL_HARVEST: 'D2_CREDENTIAL_HARVEST',
|
|
5
|
+
PUBLISH_VELOCITY: 'D3_PUBLISH_VELOCITY',
|
|
6
|
+
PUBLISHER_DRIFT: 'D4_PUBLISHER_DRIFT',
|
|
7
|
+
BOT_COMMIT_IDENTITY: 'D5_BOT_COMMIT_IDENTITY',
|
|
8
|
+
DATE_ANACHRONISM: 'D6_DATE_ANACHRONISM',
|
|
9
|
+
});
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
export async function checkBurstPublish(registryMeta, config = {}) {
|
|
2
|
-
const windowMinutes = config.burstWindowMinutes ?? 30;
|
|
3
|
-
const threshold = config.burstVersionThreshold ?? 3;
|
|
4
|
-
|
|
5
|
-
const times = registryMeta?.time || {};
|
|
6
|
-
const entries = Object.entries(times)
|
|
7
|
-
.filter(([v]) => v !== 'created' && v !== 'modified')
|
|
8
|
-
.filter(([, t]) => t)
|
|
9
|
-
.map(([v, t]) => [v, new Date(t).getTime()])
|
|
10
|
-
.filter(([, ts]) => !Number.isNaN(ts))
|
|
11
|
-
.sort((a, b) => a[1] - b[1]);
|
|
12
|
-
|
|
13
|
-
if (entries.length === 0) return { triggered: false };
|
|
14
|
-
|
|
15
|
-
const windowMs = windowMinutes * 60 * 1000;
|
|
16
|
-
|
|
17
|
-
for (let i = 0; i < entries.length; i++) {
|
|
18
|
-
const windowStart = entries[i][1];
|
|
19
|
-
const windowEnd = windowStart + windowMs;
|
|
20
|
-
const inWindow = [];
|
|
21
|
-
|
|
22
|
-
for (let j = i; j < entries.length; j++) {
|
|
23
|
-
if (entries[j][1] <= windowEnd) {
|
|
24
|
-
inWindow.push(entries[j][0]);
|
|
25
|
-
} else {
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (inWindow.length >= threshold) {
|
|
31
|
-
return {
|
|
32
|
-
triggered: true,
|
|
33
|
-
windowStart: new Date(windowStart).toISOString(),
|
|
34
|
-
windowEnd: new Date(windowEnd).toISOString(),
|
|
35
|
-
versionCount: inWindow.length,
|
|
36
|
-
versions: inWindow,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { triggered: false };
|
|
42
|
-
}
|
|
1
|
+
export async function checkBurstPublish(registryMeta, config = {}) {
|
|
2
|
+
const windowMinutes = config.burstWindowMinutes ?? 30;
|
|
3
|
+
const threshold = config.burstVersionThreshold ?? 3;
|
|
4
|
+
|
|
5
|
+
const times = registryMeta?.time || {};
|
|
6
|
+
const entries = Object.entries(times)
|
|
7
|
+
.filter(([v]) => v !== 'created' && v !== 'modified')
|
|
8
|
+
.filter(([, t]) => t)
|
|
9
|
+
.map(([v, t]) => [v, new Date(t).getTime()])
|
|
10
|
+
.filter(([, ts]) => !Number.isNaN(ts))
|
|
11
|
+
.sort((a, b) => a[1] - b[1]);
|
|
12
|
+
|
|
13
|
+
if (entries.length === 0) return { triggered: false };
|
|
14
|
+
|
|
15
|
+
const windowMs = windowMinutes * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < entries.length; i++) {
|
|
18
|
+
const windowStart = entries[i][1];
|
|
19
|
+
const windowEnd = windowStart + windowMs;
|
|
20
|
+
const inWindow = [];
|
|
21
|
+
|
|
22
|
+
for (let j = i; j < entries.length; j++) {
|
|
23
|
+
if (entries[j][1] <= windowEnd) {
|
|
24
|
+
inWindow.push(entries[j][0]);
|
|
25
|
+
} else {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (inWindow.length >= threshold) {
|
|
31
|
+
return {
|
|
32
|
+
triggered: true,
|
|
33
|
+
windowStart: new Date(windowStart).toISOString(),
|
|
34
|
+
windowEnd: new Date(windowEnd).toISOString(),
|
|
35
|
+
versionCount: inWindow.length,
|
|
36
|
+
versions: inWindow,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { triggered: false };
|
|
42
|
+
}
|