@lateos/npm-scan 0.16.4 → 0.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +20 -20
- package/.husky/pre-commit +1 -1
- package/CHANGELOG.md +199 -199
- 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 +826 -826
- package/README.zh.md +708 -708
- package/SECURITY.md +72 -72
- package/backend/cra.js +68 -68
- 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/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 +75 -44
- 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 +219 -0
- package/backend/detectors/tier1-infostealer.js +280 -0
- package/backend/detectors/tier1-lifecycle-hook.js +176 -0
- package/backend/detectors/tier1-metadata-spoof.js +180 -0
- package/backend/detectors/tier1-typosquat.js +219 -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 -176
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- 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/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/deploy/helm/npm-scan/Chart.yaml +21 -21
- package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
- package/deploy/helm/npm-scan/templates/api.yaml +93 -93
- package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
- package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
- package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
- package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
- package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
- package/deploy/helm/npm-scan/values.yaml +102 -102
- package/package.json +57 -57
- package/scripts/download-corpus.js +30 -30
- package/scripts/gen-mal-corpus.js +34 -34
- package/scripts/generate-campaign-fixtures.js +170 -0
- package/src/config/top-5000.json +87 -0
- package/test/fixtures/lockfiles/npm-lock.json +68 -68
- package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
- package/test/fixtures/lockfiles/yarn.lock +103 -103
- package/test/fixtures/mock-data.js +69 -69
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
export async function checkBurstPublish(versionHistory, config = {}) {
|
|
2
|
-
const windowMinutes = config.burstWindowMinutes ?? 30;
|
|
3
|
-
const threshold = config.burstVersionThreshold ?? 2;
|
|
4
|
-
const hotPullMinutes = config.hotPullMinutes ?? 20;
|
|
5
|
-
|
|
6
|
-
const entries = versionHistory
|
|
7
|
-
.filter(v => v.publishedAt)
|
|
8
|
-
.map(v => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
|
|
9
|
-
.filter(e => !Number.isNaN(e.time))
|
|
10
|
-
.sort((a, b) => a.time - b.time);
|
|
11
|
-
|
|
12
|
-
if (entries.length < threshold) return { triggered: false };
|
|
13
|
-
|
|
14
|
-
const windowMs = windowMinutes * 60 * 1000;
|
|
15
|
-
let burstFound = false;
|
|
16
|
-
let burstWindowStart = null;
|
|
17
|
-
let burstWindowEnd = null;
|
|
18
|
-
let burstVersionCount = 0;
|
|
19
|
-
let burstVersions = [];
|
|
20
|
-
|
|
21
|
-
for (let i = 0; i < entries.length; i++) {
|
|
22
|
-
const start = entries[i].time;
|
|
23
|
-
const end = start + windowMs;
|
|
24
|
-
const inWindow = entries.filter(e => e.time >= start && e.time <= end);
|
|
25
|
-
|
|
26
|
-
if (inWindow.length >= threshold) {
|
|
27
|
-
burstFound = true;
|
|
28
|
-
burstWindowStart = new Date(start).toISOString();
|
|
29
|
-
burstWindowEnd = new Date(end).toISOString();
|
|
30
|
-
burstVersionCount = inWindow.length;
|
|
31
|
-
burstVersions = inWindow.map(e => e.version);
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let hotPullDetected = false;
|
|
37
|
-
for (let i = 1; i < entries.length; i++) {
|
|
38
|
-
const gapMinutes = (entries[i].time - entries[i - 1].time) / (1000 * 60);
|
|
39
|
-
if (gapMinutes > 0 && gapMinutes < hotPullMinutes) {
|
|
40
|
-
hotPullDetected = true;
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
triggered: burstFound || hotPullDetected,
|
|
47
|
-
burstWindow: burstFound
|
|
48
|
-
? { start: burstWindowStart, end: burstWindowEnd, versionCount: burstVersionCount, versions: burstVersions }
|
|
49
|
-
: null,
|
|
50
|
-
hotPullDetected,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
1
|
+
export async function checkBurstPublish(versionHistory, config = {}) {
|
|
2
|
+
const windowMinutes = config.burstWindowMinutes ?? 30;
|
|
3
|
+
const threshold = config.burstVersionThreshold ?? 2;
|
|
4
|
+
const hotPullMinutes = config.hotPullMinutes ?? 20;
|
|
5
|
+
|
|
6
|
+
const entries = versionHistory
|
|
7
|
+
.filter(v => v.publishedAt)
|
|
8
|
+
.map(v => ({ version: v.version, time: new Date(v.publishedAt).getTime() }))
|
|
9
|
+
.filter(e => !Number.isNaN(e.time))
|
|
10
|
+
.sort((a, b) => a.time - b.time);
|
|
11
|
+
|
|
12
|
+
if (entries.length < threshold) return { triggered: false };
|
|
13
|
+
|
|
14
|
+
const windowMs = windowMinutes * 60 * 1000;
|
|
15
|
+
let burstFound = false;
|
|
16
|
+
let burstWindowStart = null;
|
|
17
|
+
let burstWindowEnd = null;
|
|
18
|
+
let burstVersionCount = 0;
|
|
19
|
+
let burstVersions = [];
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < entries.length; i++) {
|
|
22
|
+
const start = entries[i].time;
|
|
23
|
+
const end = start + windowMs;
|
|
24
|
+
const inWindow = entries.filter(e => e.time >= start && e.time <= end);
|
|
25
|
+
|
|
26
|
+
if (inWindow.length >= threshold) {
|
|
27
|
+
burstFound = true;
|
|
28
|
+
burstWindowStart = new Date(start).toISOString();
|
|
29
|
+
burstWindowEnd = new Date(end).toISOString();
|
|
30
|
+
burstVersionCount = inWindow.length;
|
|
31
|
+
burstVersions = inWindow.map(e => e.version);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let hotPullDetected = false;
|
|
37
|
+
for (let i = 1; i < entries.length; i++) {
|
|
38
|
+
const gapMinutes = (entries[i].time - entries[i - 1].time) / (1000 * 60);
|
|
39
|
+
if (gapMinutes > 0 && gapMinutes < hotPullMinutes) {
|
|
40
|
+
hotPullDetected = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
triggered: burstFound || hotPullDetected,
|
|
47
|
+
burstWindow: burstFound
|
|
48
|
+
? { start: burstWindowStart, end: burstWindowEnd, versionCount: burstVersionCount, versions: burstVersions }
|
|
49
|
+
: null,
|
|
50
|
+
hotPullDetected,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
const CREDENTIAL_FILE_PATTERNS = [
|
|
2
|
-
/~\/\.npmrc/,
|
|
3
|
-
/~\/\.gitconfig/,
|
|
4
|
-
/~\/\.aws\/credentials/,
|
|
5
|
-
/~\/\.ssh\/id_\w+/,
|
|
6
|
-
/~\/\.vault-token/,
|
|
7
|
-
/~\/\.claude\/settings\.json/,
|
|
8
|
-
/~\/Library\/Application\s+Support\/1Password\//,
|
|
9
|
-
/\/etc\/vault\/token/,
|
|
10
|
-
/\/proc\/\*\/mem/,
|
|
11
|
-
/\$GITHUB_ENV/,
|
|
12
|
-
/\$GITHUB_TOKEN/,
|
|
13
|
-
/\$NPM_TOKEN/,
|
|
14
|
-
/\$NODE_AUTH_TOKEN/,
|
|
15
|
-
/GH_TOKEN/,
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const EXFIL_CHANNEL_PATTERNS = [
|
|
19
|
-
/(?:[a-z0-9_-]{40,})\.[a-z0-9_-]+\.(?:com|io|org|net|app|dev|xyz)(?:\/[^\s"')\]]{0,50})?/i,
|
|
20
|
-
/\/gists\b.*authorization/i,
|
|
21
|
-
/\/repos\/[^/]+\/[^/]+\/git\/refs/i,
|
|
22
|
-
/AES-256-GCM/,
|
|
23
|
-
/RSA\/(?:PKCS|OAEP)/,
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const ANTI_ANALYSIS_PATTERNS = [
|
|
27
|
-
{ pattern: /os\.cpus\(\)\.length\s*<\s*4/, label: 'CPU core count check (< 4)' },
|
|
28
|
-
{ pattern: /Intl\.DateTimeFormat.*(?:timeZone|locale)/, label: 'Timezone/locale check' },
|
|
29
|
-
{ pattern: /Intl\.DateTimeFormat.*\b(?:ru|rus|kz|by|cn|cns)\b/i, label: 'CIS/locale filtering' },
|
|
30
|
-
{ pattern: /\bspawn\(\s*[^,]+,\s*\{[^}]*detached:\s*true\s*\}/, label: 'Detached process spawn' },
|
|
31
|
-
{ pattern: /\bBUN_INSTALL\b/, label: 'BUN_INSTALL env reference' },
|
|
32
|
-
{ pattern: /~\/\.bun\/bin\/bun/, label: 'Bun binary path' },
|
|
33
|
-
{ pattern: /\bBun\.file\(/, label: 'Bun.file() API' },
|
|
34
|
-
{ pattern: /\bBun\.serve\(/, label: 'Bun.serve() API' },
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
function truncateSnippet(str, maxLen = 200) {
|
|
38
|
-
if (!str || str.length <= maxLen) return str || '';
|
|
39
|
-
return str.slice(0, maxLen) + '...';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function checkExfilPattern(extensionFiles = []) {
|
|
43
|
-
const signals = [];
|
|
44
|
-
const exfilPatterns = [];
|
|
45
|
-
const antiAnalysisTechniques = [];
|
|
46
|
-
|
|
47
|
-
for (const file of extensionFiles) {
|
|
48
|
-
const content = typeof file.content === 'string' ? file.content : '';
|
|
49
|
-
if (!content) continue;
|
|
50
|
-
const path = file.path || '';
|
|
51
|
-
|
|
52
|
-
for (const cp of CREDENTIAL_FILE_PATTERNS) {
|
|
53
|
-
const match = content.match(cp);
|
|
54
|
-
if (match) {
|
|
55
|
-
const snippet = truncateSnippet(match[0]);
|
|
56
|
-
if (!exfilPatterns.some(e => e.includes(snippet))) {
|
|
57
|
-
exfilPatterns.push(`${path}: ${snippet}`);
|
|
58
|
-
signals.push({ type: 'CREDENTIAL_FILE_TARGET', pattern: cp.source, file: path });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (const ep of EXFIL_CHANNEL_PATTERNS) {
|
|
64
|
-
const match = content.match(ep);
|
|
65
|
-
if (match) {
|
|
66
|
-
const snippet = truncateSnippet(match[0]);
|
|
67
|
-
exfilPatterns.push(`${path}: ${snippet}`);
|
|
68
|
-
signals.push({ type: 'EXFIL_CHANNEL', pattern: ep.source, file: path });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const ap of ANTI_ANALYSIS_PATTERNS) {
|
|
73
|
-
if (ap.pattern.test(content)) {
|
|
74
|
-
if (!antiAnalysisTechniques.includes(ap.label)) {
|
|
75
|
-
antiAnalysisTechniques.push(ap.label);
|
|
76
|
-
signals.push({ type: 'ANTI_ANALYSIS', technique: ap.label, file: path });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
triggered: signals.length > 0,
|
|
84
|
-
signals,
|
|
85
|
-
exfilPatterns,
|
|
86
|
-
antiAnalysisTechniques,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
1
|
+
const CREDENTIAL_FILE_PATTERNS = [
|
|
2
|
+
/~\/\.npmrc/,
|
|
3
|
+
/~\/\.gitconfig/,
|
|
4
|
+
/~\/\.aws\/credentials/,
|
|
5
|
+
/~\/\.ssh\/id_\w+/,
|
|
6
|
+
/~\/\.vault-token/,
|
|
7
|
+
/~\/\.claude\/settings\.json/,
|
|
8
|
+
/~\/Library\/Application\s+Support\/1Password\//,
|
|
9
|
+
/\/etc\/vault\/token/,
|
|
10
|
+
/\/proc\/\*\/mem/,
|
|
11
|
+
/\$GITHUB_ENV/,
|
|
12
|
+
/\$GITHUB_TOKEN/,
|
|
13
|
+
/\$NPM_TOKEN/,
|
|
14
|
+
/\$NODE_AUTH_TOKEN/,
|
|
15
|
+
/GH_TOKEN/,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const EXFIL_CHANNEL_PATTERNS = [
|
|
19
|
+
/(?:[a-z0-9_-]{40,})\.[a-z0-9_-]+\.(?:com|io|org|net|app|dev|xyz)(?:\/[^\s"')\]]{0,50})?/i,
|
|
20
|
+
/\/gists\b.*authorization/i,
|
|
21
|
+
/\/repos\/[^/]+\/[^/]+\/git\/refs/i,
|
|
22
|
+
/AES-256-GCM/,
|
|
23
|
+
/RSA\/(?:PKCS|OAEP)/,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const ANTI_ANALYSIS_PATTERNS = [
|
|
27
|
+
{ pattern: /os\.cpus\(\)\.length\s*<\s*4/, label: 'CPU core count check (< 4)' },
|
|
28
|
+
{ pattern: /Intl\.DateTimeFormat.*(?:timeZone|locale)/, label: 'Timezone/locale check' },
|
|
29
|
+
{ pattern: /Intl\.DateTimeFormat.*\b(?:ru|rus|kz|by|cn|cns)\b/i, label: 'CIS/locale filtering' },
|
|
30
|
+
{ pattern: /\bspawn\(\s*[^,]+,\s*\{[^}]*detached:\s*true\s*\}/, label: 'Detached process spawn' },
|
|
31
|
+
{ pattern: /\bBUN_INSTALL\b/, label: 'BUN_INSTALL env reference' },
|
|
32
|
+
{ pattern: /~\/\.bun\/bin\/bun/, label: 'Bun binary path' },
|
|
33
|
+
{ pattern: /\bBun\.file\(/, label: 'Bun.file() API' },
|
|
34
|
+
{ pattern: /\bBun\.serve\(/, label: 'Bun.serve() API' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function truncateSnippet(str, maxLen = 200) {
|
|
38
|
+
if (!str || str.length <= maxLen) return str || '';
|
|
39
|
+
return str.slice(0, maxLen) + '...';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function checkExfilPattern(extensionFiles = []) {
|
|
43
|
+
const signals = [];
|
|
44
|
+
const exfilPatterns = [];
|
|
45
|
+
const antiAnalysisTechniques = [];
|
|
46
|
+
|
|
47
|
+
for (const file of extensionFiles) {
|
|
48
|
+
const content = typeof file.content === 'string' ? file.content : '';
|
|
49
|
+
if (!content) continue;
|
|
50
|
+
const path = file.path || '';
|
|
51
|
+
|
|
52
|
+
for (const cp of CREDENTIAL_FILE_PATTERNS) {
|
|
53
|
+
const match = content.match(cp);
|
|
54
|
+
if (match) {
|
|
55
|
+
const snippet = truncateSnippet(match[0]);
|
|
56
|
+
if (!exfilPatterns.some(e => e.includes(snippet))) {
|
|
57
|
+
exfilPatterns.push(`${path}: ${snippet}`);
|
|
58
|
+
signals.push({ type: 'CREDENTIAL_FILE_TARGET', pattern: cp.source, file: path });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const ep of EXFIL_CHANNEL_PATTERNS) {
|
|
64
|
+
const match = content.match(ep);
|
|
65
|
+
if (match) {
|
|
66
|
+
const snippet = truncateSnippet(match[0]);
|
|
67
|
+
exfilPatterns.push(`${path}: ${snippet}`);
|
|
68
|
+
signals.push({ type: 'EXFIL_CHANNEL', pattern: ep.source, file: path });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const ap of ANTI_ANALYSIS_PATTERNS) {
|
|
73
|
+
if (ap.pattern.test(content)) {
|
|
74
|
+
if (!antiAnalysisTechniques.includes(ap.label)) {
|
|
75
|
+
antiAnalysisTechniques.push(ap.label);
|
|
76
|
+
signals.push({ type: 'ANTI_ANALYSIS', technique: ap.label, file: path });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
triggered: signals.length > 0,
|
|
84
|
+
signals,
|
|
85
|
+
exfilPatterns,
|
|
86
|
+
antiAnalysisTechniques,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
|
-
import { dirname, join } from 'path';
|
|
4
|
-
|
|
5
|
-
let iocsData = null;
|
|
6
|
-
let iocsLoaded = false;
|
|
7
|
-
let iocLoadError = null;
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = dirname(__filename);
|
|
11
|
-
const IOC_PATH = join(__dirname, '..', 'vsix-iocs.json');
|
|
12
|
-
|
|
13
|
-
function loadIOCData() {
|
|
14
|
-
if (iocsLoaded) return iocsData;
|
|
15
|
-
iocsLoaded = true;
|
|
16
|
-
try {
|
|
17
|
-
iocsData = JSON.parse(readFileSync(IOC_PATH, 'utf8'));
|
|
18
|
-
} catch (err) {
|
|
19
|
-
iocLoadError = err;
|
|
20
|
-
iocsData = null;
|
|
21
|
-
}
|
|
22
|
-
return iocsData;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getIOCLoadError() {
|
|
26
|
-
return iocLoadError;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function reloadIOCData() {
|
|
30
|
-
iocsLoaded = false;
|
|
31
|
-
iocLoadError = null;
|
|
32
|
-
return loadIOCData();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function checkKnownIOC(extensionId, version, publisherAccount, orphanCommits = [], versionHistory = []) {
|
|
36
|
-
const data = loadIOCData();
|
|
37
|
-
if (!data) return { triggered: false, matches: [] };
|
|
38
|
-
|
|
39
|
-
const matches = [];
|
|
40
|
-
const iocs = data.iocs || [];
|
|
41
|
-
|
|
42
|
-
for (const ioc of iocs) {
|
|
43
|
-
switch (ioc.type) {
|
|
44
|
-
case 'extensionId': {
|
|
45
|
-
if (ioc.value === extensionId) {
|
|
46
|
-
if (!ioc.maliciousVersions || ioc.maliciousVersions.length === 0 || ioc.maliciousVersions.includes(version)) {
|
|
47
|
-
matches.push({
|
|
48
|
-
type: 'extensionId',
|
|
49
|
-
value: extensionId,
|
|
50
|
-
maliciousVersion: version,
|
|
51
|
-
wave: ioc.wave,
|
|
52
|
-
cve: ioc.cve,
|
|
53
|
-
exposureWindowStart: ioc.exposureWindowStart,
|
|
54
|
-
exposureWindowEnd: ioc.exposureWindowEnd,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
case 'publisherAccount': {
|
|
62
|
-
if (ioc.value === publisherAccount) {
|
|
63
|
-
const pubTime = versionHistory.length > 0
|
|
64
|
-
? new Date(versionHistory[versionHistory.length - 1]?.publishedAt).getTime()
|
|
65
|
-
: null;
|
|
66
|
-
|
|
67
|
-
const windowStart = new Date(ioc.compromiseWindowStart).getTime();
|
|
68
|
-
const windowEnd = ioc.compromiseWindowEnd
|
|
69
|
-
? new Date(ioc.compromiseWindowEnd).getTime()
|
|
70
|
-
: Infinity;
|
|
71
|
-
|
|
72
|
-
if (pubTime && !Number.isNaN(pubTime) && pubTime >= windowStart && pubTime <= windowEnd) {
|
|
73
|
-
matches.push({
|
|
74
|
-
type: 'publisherAccount',
|
|
75
|
-
value: publisherAccount,
|
|
76
|
-
wave: ioc.wave,
|
|
77
|
-
compromiseWindowStart: ioc.compromiseWindowStart,
|
|
78
|
-
compromiseWindowEnd: ioc.compromiseWindowEnd,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
case 'orphanCommitHash': {
|
|
86
|
-
for (const commit of orphanCommits) {
|
|
87
|
-
if (ioc.value === commit || (ioc.value === 'PLACEHOLDER_UPDATE_FROM_THREAT_INTEL')) {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (ioc.value && commit && ioc.value.toLowerCase() === commit.toLowerCase()) {
|
|
91
|
-
matches.push({
|
|
92
|
-
type: 'orphanCommitHash',
|
|
93
|
-
value: commit,
|
|
94
|
-
repo: ioc.repo,
|
|
95
|
-
wave: ioc.wave,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { triggered: matches.length > 0, matches };
|
|
105
|
-
}
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
|
|
5
|
+
let iocsData = null;
|
|
6
|
+
let iocsLoaded = false;
|
|
7
|
+
let iocLoadError = null;
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const IOC_PATH = join(__dirname, '..', 'vsix-iocs.json');
|
|
12
|
+
|
|
13
|
+
function loadIOCData() {
|
|
14
|
+
if (iocsLoaded) return iocsData;
|
|
15
|
+
iocsLoaded = true;
|
|
16
|
+
try {
|
|
17
|
+
iocsData = JSON.parse(readFileSync(IOC_PATH, 'utf8'));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
iocLoadError = err;
|
|
20
|
+
iocsData = null;
|
|
21
|
+
}
|
|
22
|
+
return iocsData;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getIOCLoadError() {
|
|
26
|
+
return iocLoadError;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function reloadIOCData() {
|
|
30
|
+
iocsLoaded = false;
|
|
31
|
+
iocLoadError = null;
|
|
32
|
+
return loadIOCData();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function checkKnownIOC(extensionId, version, publisherAccount, orphanCommits = [], versionHistory = []) {
|
|
36
|
+
const data = loadIOCData();
|
|
37
|
+
if (!data) return { triggered: false, matches: [] };
|
|
38
|
+
|
|
39
|
+
const matches = [];
|
|
40
|
+
const iocs = data.iocs || [];
|
|
41
|
+
|
|
42
|
+
for (const ioc of iocs) {
|
|
43
|
+
switch (ioc.type) {
|
|
44
|
+
case 'extensionId': {
|
|
45
|
+
if (ioc.value === extensionId) {
|
|
46
|
+
if (!ioc.maliciousVersions || ioc.maliciousVersions.length === 0 || ioc.maliciousVersions.includes(version)) {
|
|
47
|
+
matches.push({
|
|
48
|
+
type: 'extensionId',
|
|
49
|
+
value: extensionId,
|
|
50
|
+
maliciousVersion: version,
|
|
51
|
+
wave: ioc.wave,
|
|
52
|
+
cve: ioc.cve,
|
|
53
|
+
exposureWindowStart: ioc.exposureWindowStart,
|
|
54
|
+
exposureWindowEnd: ioc.exposureWindowEnd,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'publisherAccount': {
|
|
62
|
+
if (ioc.value === publisherAccount) {
|
|
63
|
+
const pubTime = versionHistory.length > 0
|
|
64
|
+
? new Date(versionHistory[versionHistory.length - 1]?.publishedAt).getTime()
|
|
65
|
+
: null;
|
|
66
|
+
|
|
67
|
+
const windowStart = new Date(ioc.compromiseWindowStart).getTime();
|
|
68
|
+
const windowEnd = ioc.compromiseWindowEnd
|
|
69
|
+
? new Date(ioc.compromiseWindowEnd).getTime()
|
|
70
|
+
: Infinity;
|
|
71
|
+
|
|
72
|
+
if (pubTime && !Number.isNaN(pubTime) && pubTime >= windowStart && pubTime <= windowEnd) {
|
|
73
|
+
matches.push({
|
|
74
|
+
type: 'publisherAccount',
|
|
75
|
+
value: publisherAccount,
|
|
76
|
+
wave: ioc.wave,
|
|
77
|
+
compromiseWindowStart: ioc.compromiseWindowStart,
|
|
78
|
+
compromiseWindowEnd: ioc.compromiseWindowEnd,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'orphanCommitHash': {
|
|
86
|
+
for (const commit of orphanCommits) {
|
|
87
|
+
if (ioc.value === commit || (ioc.value === 'PLACEHOLDER_UPDATE_FROM_THREAT_INTEL')) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ioc.value && commit && ioc.value.toLowerCase() === commit.toLowerCase()) {
|
|
91
|
+
matches.push({
|
|
92
|
+
type: 'orphanCommitHash',
|
|
93
|
+
value: commit,
|
|
94
|
+
repo: ioc.repo,
|
|
95
|
+
wave: ioc.wave,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { triggered: matches.length > 0, matches };
|
|
105
|
+
}
|
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
const GITHUB_COMMIT_SHA_PATTERN = /api\.github\.com\/repos\/[^/]+\/[^/]+\/git\/commits\/[a-f0-9]{40}/;
|
|
2
|
-
const NPX_GIT_URL_PATTERN = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
|
|
3
|
-
const MCP_KEYWORDS = ['mcp', 'model-context-protocol', 'claude', 'setup', 'init'];
|
|
4
|
-
const EXTERNAL_FETCH_PATTERN = /(?:https?:\/\/)[^\s"')\]]+(?:\.com|\.io|\.org|\.dev|\.app|\.net)[^\s"')\]]*/;
|
|
5
|
-
const NON_NPMJS_FETCH = /(?:fetch|curl|wget)\s*\(?\s*["']https?:\/\/(?!(?:.*npmjs\.org|.*npm\.js\.org|.*github\.com))[^"']+/;
|
|
6
|
-
const BUN_PATTERNS = [/bun\s+install/, /install\s+.*bun/, /\bbunx\b/, /\.bun\/bin\//];
|
|
7
|
-
const NPX_GIT_SHORT = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
|
|
8
|
-
|
|
9
|
-
export async function checkOrphanCommitFetch(extensionFiles = []) {
|
|
10
|
-
const signals = [];
|
|
11
|
-
const indicators = [];
|
|
12
|
-
|
|
13
|
-
for (const file of extensionFiles) {
|
|
14
|
-
const content = typeof file.content === 'string' ? file.content : '';
|
|
15
|
-
if (!content) continue;
|
|
16
|
-
const path = file.path || '';
|
|
17
|
-
|
|
18
|
-
if (GITHUB_COMMIT_SHA_PATTERN.test(content)) {
|
|
19
|
-
const matches = content.match(GITHUB_COMMIT_SHA_PATTERN);
|
|
20
|
-
if (matches) {
|
|
21
|
-
indicators.push(`${path}: GitHub git commit SHA reference`);
|
|
22
|
-
signals.push({
|
|
23
|
-
type: 'ORPHAN_COMMIT_GITHUB_API',
|
|
24
|
-
indicator: 'GitHub API direct commit SHA resolution',
|
|
25
|
-
file: path,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (NPX_GIT_URL_PATTERN.test(content)) {
|
|
31
|
-
const matches = content.match(NPX_GIT_URL_PATTERN);
|
|
32
|
-
if (matches) {
|
|
33
|
-
indicators.push(`${path}: npx with git URL`);
|
|
34
|
-
signals.push({
|
|
35
|
-
type: 'NPX_GIT_URL',
|
|
36
|
-
indicator: 'npx resolves from git URL (non-registry)',
|
|
37
|
-
file: path,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const hasMCPKeywords = MCP_KEYWORDS.some(kw =>
|
|
43
|
-
new RegExp(`\\b${kw}\\b`, 'i').test(content));
|
|
44
|
-
const hasExternalFetch = NON_NPMJS_FETCH.test(content);
|
|
45
|
-
|
|
46
|
-
if (hasMCPKeywords && hasExternalFetch) {
|
|
47
|
-
indicators.push(`${path}: MCP-adjacent keywords + external fetch`);
|
|
48
|
-
signals.push({
|
|
49
|
-
type: 'MCP_DISGUISED_EXFIL',
|
|
50
|
-
indicator: 'Shell command disguised as MCP setup',
|
|
51
|
-
file: path,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const bp of BUN_PATTERNS) {
|
|
56
|
-
if (bp.test(content)) {
|
|
57
|
-
indicators.push(`${path}: Bun installation pattern`);
|
|
58
|
-
signals.push({
|
|
59
|
-
type: 'BUN_INSTALL',
|
|
60
|
-
indicator: `Bun runtime install pattern: ${bp.source}`,
|
|
61
|
-
file: path,
|
|
62
|
-
});
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { triggered: signals.length > 0, signals, indicators };
|
|
69
|
-
}
|
|
1
|
+
const GITHUB_COMMIT_SHA_PATTERN = /api\.github\.com\/repos\/[^/]+\/[^/]+\/git\/commits\/[a-f0-9]{40}/;
|
|
2
|
+
const NPX_GIT_URL_PATTERN = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
|
|
3
|
+
const MCP_KEYWORDS = ['mcp', 'model-context-protocol', 'claude', 'setup', 'init'];
|
|
4
|
+
const EXTERNAL_FETCH_PATTERN = /(?:https?:\/\/)[^\s"')\]]+(?:\.com|\.io|\.org|\.dev|\.app|\.net)[^\s"')\]]*/;
|
|
5
|
+
const NON_NPMJS_FETCH = /(?:fetch|curl|wget)\s*\(?\s*["']https?:\/\/(?!(?:.*npmjs\.org|.*npm\.js\.org|.*github\.com))[^"']+/;
|
|
6
|
+
const BUN_PATTERNS = [/bun\s+install/, /install\s+.*bun/, /\bbunx\b/, /\.bun\/bin\//];
|
|
7
|
+
const NPX_GIT_SHORT = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
|
|
8
|
+
|
|
9
|
+
export async function checkOrphanCommitFetch(extensionFiles = []) {
|
|
10
|
+
const signals = [];
|
|
11
|
+
const indicators = [];
|
|
12
|
+
|
|
13
|
+
for (const file of extensionFiles) {
|
|
14
|
+
const content = typeof file.content === 'string' ? file.content : '';
|
|
15
|
+
if (!content) continue;
|
|
16
|
+
const path = file.path || '';
|
|
17
|
+
|
|
18
|
+
if (GITHUB_COMMIT_SHA_PATTERN.test(content)) {
|
|
19
|
+
const matches = content.match(GITHUB_COMMIT_SHA_PATTERN);
|
|
20
|
+
if (matches) {
|
|
21
|
+
indicators.push(`${path}: GitHub git commit SHA reference`);
|
|
22
|
+
signals.push({
|
|
23
|
+
type: 'ORPHAN_COMMIT_GITHUB_API',
|
|
24
|
+
indicator: 'GitHub API direct commit SHA resolution',
|
|
25
|
+
file: path,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (NPX_GIT_URL_PATTERN.test(content)) {
|
|
31
|
+
const matches = content.match(NPX_GIT_URL_PATTERN);
|
|
32
|
+
if (matches) {
|
|
33
|
+
indicators.push(`${path}: npx with git URL`);
|
|
34
|
+
signals.push({
|
|
35
|
+
type: 'NPX_GIT_URL',
|
|
36
|
+
indicator: 'npx resolves from git URL (non-registry)',
|
|
37
|
+
file: path,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasMCPKeywords = MCP_KEYWORDS.some(kw =>
|
|
43
|
+
new RegExp(`\\b${kw}\\b`, 'i').test(content));
|
|
44
|
+
const hasExternalFetch = NON_NPMJS_FETCH.test(content);
|
|
45
|
+
|
|
46
|
+
if (hasMCPKeywords && hasExternalFetch) {
|
|
47
|
+
indicators.push(`${path}: MCP-adjacent keywords + external fetch`);
|
|
48
|
+
signals.push({
|
|
49
|
+
type: 'MCP_DISGUISED_EXFIL',
|
|
50
|
+
indicator: 'Shell command disguised as MCP setup',
|
|
51
|
+
file: path,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const bp of BUN_PATTERNS) {
|
|
56
|
+
if (bp.test(content)) {
|
|
57
|
+
indicators.push(`${path}: Bun installation pattern`);
|
|
58
|
+
signals.push({
|
|
59
|
+
type: 'BUN_INSTALL',
|
|
60
|
+
indicator: `Bun runtime install pattern: ${bp.source}`,
|
|
61
|
+
file: path,
|
|
62
|
+
});
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { triggered: signals.length > 0, signals, indicators };
|
|
69
|
+
}
|