@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,116 +1,116 @@
|
|
|
1
|
-
const ACTIVATION_RISK_MATRIX = {
|
|
2
|
-
'*': { base: 'critical', label: 'Wildcard (all files)' },
|
|
3
|
-
'onStartupFinished': { base: 'high', label: 'Startup finished' },
|
|
4
|
-
'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
|
|
5
|
-
'workspaceContains': { base: 'high', label: 'Workspace contains' },
|
|
6
|
-
'onCommand:*': { base: 'low', label: 'Any command' },
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const DEFAULT_BASE_RISK = 'medium';
|
|
10
|
-
|
|
11
|
-
const ESCALATION_KEYWORDS = [
|
|
12
|
-
'npx', 'bun', 'curl', 'wget', 'fetch(',
|
|
13
|
-
'exec(', 'spawn(', 'execSync', 'spawnSync',
|
|
14
|
-
'child_process', 'shell: true', 'detached: true',
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const BUNDLED_BUN_PATTERN = /bun|runtime/;
|
|
18
|
-
|
|
19
|
-
const SIZE_DELTA_THRESHOLD = 400 * 1024;
|
|
20
|
-
|
|
21
|
-
const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
|
|
22
|
-
|
|
23
|
-
export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
|
|
24
|
-
const signals = [];
|
|
25
|
-
|
|
26
|
-
const activationEvents = extensionManifest?.activationEvents || [];
|
|
27
|
-
if (activationEvents.length === 0 && extensionManifest?.main) {
|
|
28
|
-
return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let maxBaseRisk = 0;
|
|
32
|
-
const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
|
|
33
|
-
const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
34
|
-
|
|
35
|
-
let worstEvent = null;
|
|
36
|
-
const why = [];
|
|
37
|
-
|
|
38
|
-
for (const event of activationEvents) {
|
|
39
|
-
const risk = ACTIVATION_RISK_MATRIX[event];
|
|
40
|
-
if (risk) {
|
|
41
|
-
const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
|
|
42
|
-
if (baseIdx > maxBaseRisk) {
|
|
43
|
-
maxBaseRisk = baseIdx;
|
|
44
|
-
worstEvent = event;
|
|
45
|
-
}
|
|
46
|
-
} else if (event.includes('*') && event !== 'onCommand:*') {
|
|
47
|
-
const baseIdx = riskValues['high'];
|
|
48
|
-
if (baseIdx > maxBaseRisk) {
|
|
49
|
-
maxBaseRisk = baseIdx;
|
|
50
|
-
worstEvent = event;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const contributes = extensionManifest?.contributes || {};
|
|
56
|
-
const commands = contributes?.commands || [];
|
|
57
|
-
const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
|
|
58
|
-
|
|
59
|
-
const bundledDeps = extensionManifest?.bundledDependencies || [];
|
|
60
|
-
const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
|
|
61
|
-
|
|
62
|
-
const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
|
|
63
|
-
const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
|
|
64
|
-
|
|
65
|
-
const activationEventsStr = activationEvents.join(' ');
|
|
66
|
-
const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
|
|
67
|
-
|
|
68
|
-
let escalateToCritical = false;
|
|
69
|
-
|
|
70
|
-
if (hasShellKeyword || hasBunBundled || hasShellInActivationContext) {
|
|
71
|
-
escalateToCritical = true;
|
|
72
|
-
why.push('HIGH activation event + shell/execution keywords');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (versionHistory.length >= 2) {
|
|
76
|
-
const sizes = versionHistory
|
|
77
|
-
.filter(v => v.assetSize)
|
|
78
|
-
.map(v => v.assetSize)
|
|
79
|
-
.sort((a, b) => b - a);
|
|
80
|
-
|
|
81
|
-
if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
|
|
82
|
-
escalateToCritical = true;
|
|
83
|
-
why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const priorActivationEvents = priorVersions
|
|
88
|
-
.filter(v => v.activationEvents)
|
|
89
|
-
.flatMap(v => v.activationEvents);
|
|
90
|
-
|
|
91
|
-
if (priorActivationEvents.length > 0) {
|
|
92
|
-
const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
|
|
93
|
-
if (newEvents.length > 0) {
|
|
94
|
-
why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
|
|
95
|
-
if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
|
|
96
|
-
escalateToCritical = true;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let riskLevel = maxBaseRisk > 0 ? riskLabels[maxBaseRisk] : null;
|
|
102
|
-
if (escalateToCritical && riskValues[riskLevel] <= riskValues['high']) {
|
|
103
|
-
riskLevel = 'critical';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
107
|
-
|
|
108
|
-
signals.push({
|
|
109
|
-
type: 'ACTIVATION_EVENT_RISK',
|
|
110
|
-
activationEvents,
|
|
111
|
-
riskLevel,
|
|
112
|
-
why,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
return { triggered: true, signals, riskLevel, why };
|
|
116
|
-
}
|
|
1
|
+
const ACTIVATION_RISK_MATRIX = {
|
|
2
|
+
'*': { base: 'critical', label: 'Wildcard (all files)' },
|
|
3
|
+
'onStartupFinished': { base: 'high', label: 'Startup finished' },
|
|
4
|
+
'workspaceContains:**/*': { base: 'high', label: 'Workspace contains wildcard' },
|
|
5
|
+
'workspaceContains': { base: 'high', label: 'Workspace contains' },
|
|
6
|
+
'onCommand:*': { base: 'low', label: 'Any command' },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DEFAULT_BASE_RISK = 'medium';
|
|
10
|
+
|
|
11
|
+
const ESCALATION_KEYWORDS = [
|
|
12
|
+
'npx', 'bun', 'curl', 'wget', 'fetch(',
|
|
13
|
+
'exec(', 'spawn(', 'execSync', 'spawnSync',
|
|
14
|
+
'child_process', 'shell: true', 'detached: true',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const BUNDLED_BUN_PATTERN = /bun|runtime/;
|
|
18
|
+
|
|
19
|
+
const SIZE_DELTA_THRESHOLD = 400 * 1024;
|
|
20
|
+
|
|
21
|
+
const SHELL_CMDS = ['npx', 'bun', 'curl', 'wget', 'exec', 'spawn', 'execSync'];
|
|
22
|
+
|
|
23
|
+
export async function checkActivationEventRisk(extensionManifest, versionHistory = [], priorVersions = []) {
|
|
24
|
+
const signals = [];
|
|
25
|
+
|
|
26
|
+
const activationEvents = extensionManifest?.activationEvents || [];
|
|
27
|
+
if (activationEvents.length === 0 && extensionManifest?.main) {
|
|
28
|
+
return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let maxBaseRisk = 0;
|
|
32
|
+
const riskLabels = ['none', 'low', 'medium', 'high', 'critical'];
|
|
33
|
+
const riskValues = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
34
|
+
|
|
35
|
+
let worstEvent = null;
|
|
36
|
+
const why = [];
|
|
37
|
+
|
|
38
|
+
for (const event of activationEvents) {
|
|
39
|
+
const risk = ACTIVATION_RISK_MATRIX[event];
|
|
40
|
+
if (risk) {
|
|
41
|
+
const baseIdx = riskValues[risk.base] || riskValues[DEFAULT_BASE_RISK];
|
|
42
|
+
if (baseIdx > maxBaseRisk) {
|
|
43
|
+
maxBaseRisk = baseIdx;
|
|
44
|
+
worstEvent = event;
|
|
45
|
+
}
|
|
46
|
+
} else if (event.includes('*') && event !== 'onCommand:*') {
|
|
47
|
+
const baseIdx = riskValues['high'];
|
|
48
|
+
if (baseIdx > maxBaseRisk) {
|
|
49
|
+
maxBaseRisk = baseIdx;
|
|
50
|
+
worstEvent = event;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const contributes = extensionManifest?.contributes || {};
|
|
56
|
+
const commands = contributes?.commands || [];
|
|
57
|
+
const cmdTitles = commands.map(c => (c.title || '').toLowerCase()).join(' ');
|
|
58
|
+
|
|
59
|
+
const bundledDeps = extensionManifest?.bundledDependencies || [];
|
|
60
|
+
const bundledStr = Array.isArray(bundledDeps) ? bundledDeps.join(' ') : '';
|
|
61
|
+
|
|
62
|
+
const hasShellKeyword = SHELL_CMDS.some(cmd => cmdTitles.includes(cmd));
|
|
63
|
+
const hasBunBundled = BUNDLED_BUN_PATTERN.test(bundledStr);
|
|
64
|
+
|
|
65
|
+
const activationEventsStr = activationEvents.join(' ');
|
|
66
|
+
const hasShellInActivationContext = ESCALATION_KEYWORDS.some(kw => activationEventsStr.toLowerCase().includes(kw.toLowerCase()));
|
|
67
|
+
|
|
68
|
+
let escalateToCritical = false;
|
|
69
|
+
|
|
70
|
+
if (hasShellKeyword || hasBunBundled || hasShellInActivationContext) {
|
|
71
|
+
escalateToCritical = true;
|
|
72
|
+
why.push('HIGH activation event + shell/execution keywords');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (versionHistory.length >= 2) {
|
|
76
|
+
const sizes = versionHistory
|
|
77
|
+
.filter(v => v.assetSize)
|
|
78
|
+
.map(v => v.assetSize)
|
|
79
|
+
.sort((a, b) => b - a);
|
|
80
|
+
|
|
81
|
+
if (sizes.length >= 2 && (sizes[0] - sizes[sizes.length - 1]) > SIZE_DELTA_THRESHOLD) {
|
|
82
|
+
escalateToCritical = true;
|
|
83
|
+
why.push(`HIGH activation event + version size delta > ${SIZE_DELTA_THRESHOLD} bytes`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const priorActivationEvents = priorVersions
|
|
88
|
+
.filter(v => v.activationEvents)
|
|
89
|
+
.flatMap(v => v.activationEvents);
|
|
90
|
+
|
|
91
|
+
if (priorActivationEvents.length > 0) {
|
|
92
|
+
const newEvents = activationEvents.filter(e => !priorActivationEvents.includes(e));
|
|
93
|
+
if (newEvents.length > 0) {
|
|
94
|
+
why.push(`First-time activation event(s) added: ${newEvents.join(', ')}`);
|
|
95
|
+
if (!escalateToCritical && maxBaseRisk >= riskValues['high']) {
|
|
96
|
+
escalateToCritical = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let riskLevel = maxBaseRisk > 0 ? riskLabels[maxBaseRisk] : null;
|
|
102
|
+
if (escalateToCritical && riskValues[riskLevel] <= riskValues['high']) {
|
|
103
|
+
riskLevel = 'critical';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!riskLevel) return { triggered: false, signals: [], riskLevel: null, why: [] };
|
|
107
|
+
|
|
108
|
+
signals.push({
|
|
109
|
+
type: 'ACTIVATION_EVENT_RISK',
|
|
110
|
+
activationEvents,
|
|
111
|
+
riskLevel,
|
|
112
|
+
why,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { triggered: true, signals, riskLevel, why };
|
|
116
|
+
}
|
|
@@ -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
|
+
}
|