@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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
|
+
|
|
3
|
+
const THRESHOLDS = {
|
|
4
|
+
flag_threshold: 70,
|
|
5
|
+
warn_threshold: 50,
|
|
6
|
+
known_c2_endpoints: [
|
|
7
|
+
'filev2.getsession.org',
|
|
8
|
+
'api.signal.org',
|
|
9
|
+
'*.briarproject.org',
|
|
10
|
+
'api.ricochet.im',
|
|
11
|
+
],
|
|
12
|
+
onion_pattern_weight: 30,
|
|
13
|
+
encoded_url_weight: 35,
|
|
14
|
+
env_var_c2_weight: 40,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const KNOWN_C2_RE =
|
|
18
|
+
/(?:filev2\.getsession\.org|api\.signal\.org|(?:[\w-]+\.)?briarproject\.org|api\.ricochet\.im|signal-cli|signal-desktop|tor\s*(?:proxy|socks|connect|bridge))/gi;
|
|
19
|
+
const ONION_RE = /(?:[a-z2-7]{16,56}\.onion|\.onion|tor\s*(?:proxy|socks|connect|bridge))/gi;
|
|
20
|
+
const ENCODED_URL_RE =
|
|
21
|
+
/(?:atob|Buffer\.from|decodeURIComponent)\s*\((?:['"`][A-Za-z0-9+/=]{20,}['"`]|['"`](?:[0-9a-fA-F]{2,})['"`])/gi;
|
|
22
|
+
const HEX_DOMAIN_RE = /(?:0x[0-9a-fA-F]{2,}){4,}/g;
|
|
23
|
+
const SESSION_RE = /session|oxen|filev2|getsession/i;
|
|
24
|
+
const SIGNAL_RE = /signal|signal-cli|signal-desktop/i;
|
|
25
|
+
const BRIAR_RE = /briar|briarproject/i;
|
|
26
|
+
|
|
27
|
+
function detectC2InContent(content) {
|
|
28
|
+
const findings = [];
|
|
29
|
+
|
|
30
|
+
let match;
|
|
31
|
+
KNOWN_C2_RE.lastIndex = 0;
|
|
32
|
+
while ((match = KNOWN_C2_RE.exec(content)) !== null) {
|
|
33
|
+
findings.push({
|
|
34
|
+
type: 'known_endpoint',
|
|
35
|
+
endpoint: match[0],
|
|
36
|
+
weight: 80,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ONION_RE.lastIndex = 0;
|
|
41
|
+
while ((match = ONION_RE.exec(content)) !== null) {
|
|
42
|
+
findings.push({
|
|
43
|
+
type: 'onion_service',
|
|
44
|
+
pattern: match[0],
|
|
45
|
+
weight: THRESHOLDS.onion_pattern_weight,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ENCODED_URL_RE.lastIndex = 0;
|
|
50
|
+
while ((match = ENCODED_URL_RE.exec(content)) !== null) {
|
|
51
|
+
findings.push({
|
|
52
|
+
type: 'encoded_url',
|
|
53
|
+
snippet: match[0].substring(0, 60),
|
|
54
|
+
weight: THRESHOLDS.encoded_url_weight,
|
|
55
|
+
encoding: match[0].includes('atob')
|
|
56
|
+
? 'base64'
|
|
57
|
+
: match[0].includes('Buffer.from')
|
|
58
|
+
? 'hex_or_other'
|
|
59
|
+
: 'other',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
HEX_DOMAIN_RE.lastIndex = 0;
|
|
64
|
+
while ((match = HEX_DOMAIN_RE.exec(content)) !== null) {
|
|
65
|
+
findings.push({
|
|
66
|
+
type: 'hex_encoded_domain',
|
|
67
|
+
snippet: match[0].substring(0, 40),
|
|
68
|
+
weight: THRESHOLDS.encoded_url_weight,
|
|
69
|
+
encoding: 'hex',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return findings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function computeConfidence(c2Findings, hasSession, hasSignal, hasBriar) {
|
|
77
|
+
let base = 0;
|
|
78
|
+
|
|
79
|
+
const totalWeight = c2Findings.reduce((s, f) => s + f.weight, 0);
|
|
80
|
+
base += Math.min(totalWeight, 80);
|
|
81
|
+
|
|
82
|
+
if (c2Findings.length === 0) {
|
|
83
|
+
base = 20;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (hasSession) base += 20;
|
|
87
|
+
if (hasSignal) base += 20;
|
|
88
|
+
if (hasBriar) base += 15;
|
|
89
|
+
|
|
90
|
+
if (c2Findings.length > 0) {
|
|
91
|
+
base += 20;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (c2Findings.length > 1) {
|
|
95
|
+
base += Math.min(c2Findings.length * 5, 15);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Math.min(100, Math.max(0, base));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function severityLabel(score) {
|
|
102
|
+
if (score >= 80) return 'critical';
|
|
103
|
+
if (score >= 60) return 'high';
|
|
104
|
+
return 'medium';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function confidenceLabel(score) {
|
|
108
|
+
if (score >= 80) return 'CRITICAL';
|
|
109
|
+
if (score >= 60) return 'HIGH';
|
|
110
|
+
return 'MEDIUM';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const name = 'tier1-encrypted-c2';
|
|
114
|
+
|
|
115
|
+
export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
|
|
116
|
+
const pkgName = pkgJson?.name;
|
|
117
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
118
|
+
|
|
119
|
+
const allContents = [];
|
|
120
|
+
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
121
|
+
for (const val of Object.values(pkgJson.scripts)) {
|
|
122
|
+
if (typeof val === 'string') {
|
|
123
|
+
allContents.push({ source: 'package.json scripts', content: val });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const files = jsFiles || [];
|
|
129
|
+
for (const f of files) {
|
|
130
|
+
if (f?.content) {
|
|
131
|
+
allContents.push({ source: f.path || f.name || 'unknown', content: f.content });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (allContents.length === 0) return [];
|
|
136
|
+
|
|
137
|
+
const hasSession = SESSION_RE.test(JSON.stringify(allContents.map((c) => c.content)));
|
|
138
|
+
const hasSignal = SIGNAL_RE.test(JSON.stringify(allContents.map((c) => c.content)));
|
|
139
|
+
const hasBriar = BRIAR_RE.test(JSON.stringify(allContents.map((c) => c.content)));
|
|
140
|
+
|
|
141
|
+
const allFindings = [];
|
|
142
|
+
for (const { source, content } of allContents) {
|
|
143
|
+
const c2Findings = detectC2InContent(content);
|
|
144
|
+
if (c2Findings.length > 0) {
|
|
145
|
+
allFindings.push({ source, c2Findings });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const flatC2 = allFindings.flatMap((f) => f.c2Findings);
|
|
150
|
+
const totalSignals =
|
|
151
|
+
flatC2.length + (hasSession ? 1 : 0) + (hasSignal ? 1 : 0) + (hasBriar ? 1 : 0);
|
|
152
|
+
if (totalSignals === 0) return [];
|
|
153
|
+
|
|
154
|
+
const confidenceScore = computeConfidence(flatC2, hasSession, hasSignal, hasBriar);
|
|
155
|
+
if (confidenceScore < THRESHOLDS.warn_threshold) return [];
|
|
156
|
+
|
|
157
|
+
const endpointTypes = [...new Set(flatC2.map((f) => f.type))];
|
|
158
|
+
const primaryType = endpointTypes.includes('known_endpoint')
|
|
159
|
+
? 'known_endpoint'
|
|
160
|
+
: endpointTypes.includes('onion_service')
|
|
161
|
+
? 'onion_service'
|
|
162
|
+
: endpointTypes.includes('encoded_url')
|
|
163
|
+
? 'encoded_url'
|
|
164
|
+
: endpointTypes.includes('hex_encoded_domain')
|
|
165
|
+
? 'hex_encoded_domain'
|
|
166
|
+
: 'protocol_signal';
|
|
167
|
+
|
|
168
|
+
const evidence = flatC2.map((f) => {
|
|
169
|
+
if (f.type === 'known_endpoint') return `c2_endpoint: ${f.endpoint}`;
|
|
170
|
+
if (f.type === 'onion_service') return `onion_pattern: ${f.pattern}`;
|
|
171
|
+
return `encoded: ${f.snippet}`;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (hasSession) evidence.push('protocol: Session/Oxen messenger');
|
|
175
|
+
if (hasSignal) evidence.push('protocol: Signal messenger');
|
|
176
|
+
if (hasBriar) evidence.push('protocol: Briar project');
|
|
177
|
+
|
|
178
|
+
const locations =
|
|
179
|
+
allFindings.length > 0
|
|
180
|
+
? allFindings.map((f) => ({ file: f.source, line: 1, column: 1 })).slice(0, 5)
|
|
181
|
+
: [{ file: 'package.json', line: 1, column: 1 }];
|
|
182
|
+
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
detector: 'tier1-encrypted-c2',
|
|
186
|
+
id: 'TIER1-ENCRYPTED-C2',
|
|
187
|
+
severity: severityLabel(confidenceScore),
|
|
188
|
+
confidence: confidenceLabel(confidenceScore),
|
|
189
|
+
confidenceScore,
|
|
190
|
+
subtype: primaryType,
|
|
191
|
+
message: `${flatC2.length > 0 ? flatC2.length + ' encrypted C2 signal(s)' : 'Encrypted C2 protocol(s) detected'}${hasSession || hasSignal || hasBriar ? ' (' + [hasSession ? 'Session' : '', hasSignal ? 'Signal' : '', hasBriar ? 'Briar' : ''].filter(Boolean).join(', ') + ')' : ''}`,
|
|
192
|
+
evidence: evidence.slice(0, 8),
|
|
193
|
+
locations,
|
|
194
|
+
crossFiles: [],
|
|
195
|
+
reference: 'D11: TanStack Mini Shai-Hulud encrypted C2',
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
}
|
|
@@ -12,25 +12,29 @@ const NPMJS_DOMAIN_RE = /npmjs\.(?:com|org)/i;
|
|
|
12
12
|
const AWS_KEY_RE = /AKIA[0-9A-Z]{16}/g;
|
|
13
13
|
const NPM_TOKEN_RE = /npm_[a-zA-Z0-9]{36}/g;
|
|
14
14
|
const GH_TOKEN_RE = /ghp_[a-zA-Z0-9]{30,40}/g;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
const _GH_OLD_TOKEN_RE = /gho_[a-zA-Z0-9]{36}/g;
|
|
16
|
+
const _GITLAB_TOKEN_RE = /glpat-[a-zA-Z0-9_-]{20,}/g;
|
|
17
17
|
|
|
18
18
|
const ENV_DUMP_RE = /process\.env\.(?:AWS_[A-Z_]+|NPM_TOKEN|NPM_AUTH_TOKEN|GIT_TOKEN|SSH_KEY)/g;
|
|
19
19
|
|
|
20
20
|
const EVAL_RE = /\beval\s*\(/g;
|
|
21
21
|
const FUNCTION_CTOR_RE = /\bFunction\s*\(/g;
|
|
22
|
-
const
|
|
22
|
+
const _B64_STRING_RE = /['"`]([A-Za-z0-9+/]{40,}={0,2})['"`]/g;
|
|
23
23
|
|
|
24
24
|
// Named malware signatures — zero-FP string literals for confirmed campaigns
|
|
25
25
|
const NAMED_SIGNATURES = [
|
|
26
|
-
'Miasma: The Spreading Blight',
|
|
26
|
+
'Miasma: The Spreading Blight', // Miasma campaign, June 2026, @redhat-cloud-services compromise
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
function shannonEntropy(s) {
|
|
30
30
|
const len = s.length;
|
|
31
|
-
if (len === 0)
|
|
31
|
+
if (len === 0) {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
32
34
|
const freq = {};
|
|
33
|
-
for (const ch of s)
|
|
35
|
+
for (const ch of s) {
|
|
36
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
37
|
+
}
|
|
34
38
|
let entropy = 0;
|
|
35
39
|
for (const count of Object.values(freq)) {
|
|
36
40
|
const p = count / len;
|
|
@@ -43,7 +47,9 @@ function isMinified(content) {
|
|
|
43
47
|
const identifiers = content.match(/\b[a-zA-Z_$][\w$]*\b/g);
|
|
44
48
|
if (identifiers && identifiers.length > 0) {
|
|
45
49
|
const avgLen = identifiers.reduce((s, id) => s + id.length, 0) / identifiers.length;
|
|
46
|
-
if (avgLen < 3)
|
|
50
|
+
if (avgLen < 3) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
return shannonEntropy(content) > 5.5;
|
|
49
55
|
}
|
|
@@ -94,9 +100,12 @@ function patternMatcher(f, content) {
|
|
|
94
100
|
isObfuscated: false,
|
|
95
101
|
};
|
|
96
102
|
|
|
97
|
-
if (!content)
|
|
103
|
+
if (!content) {
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
98
106
|
|
|
99
|
-
result.isObfuscated =
|
|
107
|
+
result.isObfuscated =
|
|
108
|
+
isMinified(content) || EVAL_RE.test(content) || FUNCTION_CTOR_RE.test(content);
|
|
100
109
|
|
|
101
110
|
FS_READ_RE.lastIndex = 0;
|
|
102
111
|
HTTP_FETCH_RE.lastIndex = 0;
|
|
@@ -109,13 +118,17 @@ function patternMatcher(f, content) {
|
|
|
109
118
|
const hasCurlWget = CURL_WGET_RE.test(content);
|
|
110
119
|
|
|
111
120
|
const domains = extractDomains(content);
|
|
112
|
-
const externalDomains = domains.filter(d => !NPMJS_DOMAIN_RE.test(d));
|
|
113
|
-
const gitHubDomains = domains.filter(d => GITHUB_DOMAIN_RE.test(d) && !NPMJS_DOMAIN_RE.test(d));
|
|
121
|
+
const externalDomains = domains.filter((d) => !NPMJS_DOMAIN_RE.test(d));
|
|
122
|
+
const gitHubDomains = domains.filter((d) => GITHUB_DOMAIN_RE.test(d) && !NPMJS_DOMAIN_RE.test(d));
|
|
114
123
|
|
|
115
124
|
if (hasFsRead && hasHttpFetch) {
|
|
116
|
-
const isGithubOnly =
|
|
125
|
+
const isGithubOnly =
|
|
126
|
+
gitHubDomains.length > 0 && externalDomains.length === gitHubDomains.length;
|
|
117
127
|
result.hasPattern = true;
|
|
118
|
-
result.patterns.push({
|
|
128
|
+
result.patterns.push({
|
|
129
|
+
subtype: isGithubOnly ? 'nw_exfil_to_github' : 'fs_exfil',
|
|
130
|
+
baseScore: 80,
|
|
131
|
+
});
|
|
119
132
|
result.domainsFound.push(...domains);
|
|
120
133
|
FS_READ_RE.lastIndex = 0;
|
|
121
134
|
const fsMatch = FS_READ_RE.exec(content);
|
|
@@ -123,15 +136,21 @@ function patternMatcher(f, content) {
|
|
|
123
136
|
const lc = getLineColumn(content, fsMatch.index);
|
|
124
137
|
result.locations.push({ file, line: lc.line, column: lc.column });
|
|
125
138
|
}
|
|
126
|
-
result.evidence.push(
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
result.evidence.push(
|
|
140
|
+
isGithubOnly
|
|
141
|
+
? 'pattern: fs.readFile + network to GitHub'
|
|
142
|
+
: 'pattern: fs.readFile + external fetch'
|
|
143
|
+
);
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
if (hasFsRead && (hasChildProc || hasCurlWget)) {
|
|
132
|
-
const isGithubOnly =
|
|
147
|
+
const isGithubOnly =
|
|
148
|
+
gitHubDomains.length > 0 && externalDomains.length === gitHubDomains.length;
|
|
133
149
|
result.hasPattern = true;
|
|
134
|
-
result.patterns.push({
|
|
150
|
+
result.patterns.push({
|
|
151
|
+
subtype: isGithubOnly ? 'nw_exfil_to_github' : 'fs_exfil',
|
|
152
|
+
baseScore: 80,
|
|
153
|
+
});
|
|
135
154
|
result.domainsFound.push(...domains);
|
|
136
155
|
FS_READ_RE.lastIndex = 0;
|
|
137
156
|
const fsMatch = FS_READ_RE.exec(content);
|
|
@@ -139,9 +158,11 @@ function patternMatcher(f, content) {
|
|
|
139
158
|
const lc = getLineColumn(content, fsMatch.index);
|
|
140
159
|
result.locations.push({ file, line: lc.line, column: lc.column });
|
|
141
160
|
}
|
|
142
|
-
result.evidence.push(
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
result.evidence.push(
|
|
162
|
+
isGithubOnly
|
|
163
|
+
? 'pattern: fs.readFile + child_process to GitHub'
|
|
164
|
+
: 'pattern: fs.readFile + child_process network'
|
|
165
|
+
);
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
const creds = extractCredentials(content);
|
|
@@ -152,7 +173,7 @@ function patternMatcher(f, content) {
|
|
|
152
173
|
result.patterns.push({ subtype: primaryType, baseScore: 85 });
|
|
153
174
|
const lc = getLineColumn(content, creds[0].index);
|
|
154
175
|
result.locations.push({ file, line: lc.line, column: lc.column });
|
|
155
|
-
const typeNames = [...new Set(creds.map(c => c.type))];
|
|
176
|
+
const typeNames = [...new Set(creds.map((c) => c.type))];
|
|
156
177
|
result.evidence.push(`hardcoded_credentials: ${creds.length} (${typeNames.join(', ')})`);
|
|
157
178
|
}
|
|
158
179
|
|
|
@@ -171,9 +192,11 @@ function patternMatcher(f, content) {
|
|
|
171
192
|
|
|
172
193
|
export const name = 'tier1-infostealer';
|
|
173
194
|
|
|
174
|
-
export async function scan(pkgJson, jsFiles,
|
|
195
|
+
export async function scan(pkgJson, jsFiles, _registryMeta, _allFiles) {
|
|
175
196
|
const pkgName = pkgJson?.name;
|
|
176
|
-
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName))
|
|
197
|
+
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
177
200
|
|
|
178
201
|
const files = jsFiles || [];
|
|
179
202
|
|
|
@@ -181,39 +204,49 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
181
204
|
const sigTexts = [];
|
|
182
205
|
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
183
206
|
for (const value of Object.values(pkgJson.scripts)) {
|
|
184
|
-
if (typeof value === 'string')
|
|
207
|
+
if (typeof value === 'string') {
|
|
208
|
+
sigTexts.push(value);
|
|
209
|
+
}
|
|
185
210
|
}
|
|
186
211
|
}
|
|
187
212
|
for (const f of files) {
|
|
188
|
-
if (f?.content)
|
|
213
|
+
if (f?.content) {
|
|
214
|
+
sigTexts.push(f.content);
|
|
215
|
+
}
|
|
189
216
|
}
|
|
190
217
|
for (const sig of NAMED_SIGNATURES) {
|
|
191
218
|
for (const text of sigTexts) {
|
|
192
219
|
if (text.includes(sig)) {
|
|
193
|
-
return [
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
detector: 'tier1-infostealer',
|
|
223
|
+
id: 'TIER1-INFOSTEALER',
|
|
224
|
+
severity: 'critical',
|
|
225
|
+
confidence: 'CRITICAL',
|
|
226
|
+
confidenceScore: 98,
|
|
227
|
+
subtype: 'named_signature_miasma',
|
|
228
|
+
message: `Named malware signature detected: "${sig}"`,
|
|
229
|
+
evidence: [sig],
|
|
230
|
+
locations: [{ file: '', line: 0 }],
|
|
231
|
+
crossFiles: [],
|
|
232
|
+
reference: 'Campaign 2 & 3',
|
|
233
|
+
},
|
|
234
|
+
];
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
237
|
}
|
|
209
238
|
|
|
210
|
-
if (files.length === 0)
|
|
239
|
+
if (files.length === 0) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
211
242
|
|
|
212
243
|
let parseFailCount = 0;
|
|
213
244
|
|
|
214
245
|
for (const f of files) {
|
|
215
246
|
const content = f.content || '';
|
|
216
|
-
if (!content)
|
|
247
|
+
if (!content) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
217
250
|
try {
|
|
218
251
|
acorn.parse(content, { ecmaVersion: 'latest' });
|
|
219
252
|
} catch {
|
|
@@ -221,12 +254,16 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
221
254
|
}
|
|
222
255
|
}
|
|
223
256
|
|
|
224
|
-
if (files.length >= 20 && parseFailCount / files.length >= 0.1)
|
|
257
|
+
if (files.length >= 20 && parseFailCount / files.length >= 0.1) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
225
260
|
|
|
226
|
-
const perFile = files.map(f => patternMatcher(f, f.content || ''));
|
|
227
|
-
const filesWithPatterns = perFile.filter(p => p.hasPattern);
|
|
261
|
+
const perFile = files.map((f) => patternMatcher(f, f.content || ''));
|
|
262
|
+
const filesWithPatterns = perFile.filter((p) => p.hasPattern);
|
|
228
263
|
|
|
229
|
-
if (filesWithPatterns.length === 0)
|
|
264
|
+
if (filesWithPatterns.length === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
230
267
|
|
|
231
268
|
let highestBase = 0;
|
|
232
269
|
let mainSubtype = '';
|
|
@@ -234,13 +271,17 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
234
271
|
const allEvidence = [];
|
|
235
272
|
const allLocations = [];
|
|
236
273
|
const involvedFiles = [];
|
|
237
|
-
const
|
|
274
|
+
const _hasCreds = false;
|
|
238
275
|
|
|
239
276
|
for (const f of filesWithPatterns) {
|
|
240
|
-
if (!involvedFiles.includes(f.file))
|
|
277
|
+
if (!involvedFiles.includes(f.file)) {
|
|
278
|
+
involvedFiles.push(f.file);
|
|
279
|
+
}
|
|
241
280
|
allLocations.push(...f.locations);
|
|
242
281
|
allEvidence.push(...f.evidence);
|
|
243
|
-
if (f.isObfuscated)
|
|
282
|
+
if (f.isObfuscated) {
|
|
283
|
+
isObfuscated = true;
|
|
284
|
+
}
|
|
244
285
|
for (const p of f.patterns) {
|
|
245
286
|
if (p.baseScore > highestBase) {
|
|
246
287
|
highestBase = p.baseScore;
|
|
@@ -251,12 +292,16 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
251
292
|
|
|
252
293
|
let baseScore = highestBase;
|
|
253
294
|
|
|
254
|
-
const anyCredPattern = filesWithPatterns.some(f =>
|
|
295
|
+
const anyCredPattern = filesWithPatterns.some((f) =>
|
|
296
|
+
f.patterns.some((p) => p.subtype.startsWith('cred_'))
|
|
297
|
+
);
|
|
255
298
|
if (anyCredPattern) {
|
|
256
299
|
baseScore = Math.min(100, Math.round(baseScore * 2.5));
|
|
257
300
|
}
|
|
258
301
|
|
|
259
|
-
if (isObfuscated)
|
|
302
|
+
if (isObfuscated) {
|
|
303
|
+
baseScore += 15;
|
|
304
|
+
}
|
|
260
305
|
|
|
261
306
|
if (involvedFiles.length > 1) {
|
|
262
307
|
baseScore = Math.min(100, Math.round(baseScore * 1.3));
|
|
@@ -265,8 +310,12 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
265
310
|
const confidenceScore = Math.max(50, Math.min(100, baseScore));
|
|
266
311
|
|
|
267
312
|
function confidenceLabel(score) {
|
|
268
|
-
if (score >= 95)
|
|
269
|
-
|
|
313
|
+
if (score >= 95) {
|
|
314
|
+
return 'CRITICAL';
|
|
315
|
+
}
|
|
316
|
+
if (score >= 80) {
|
|
317
|
+
return 'HIGH';
|
|
318
|
+
}
|
|
270
319
|
return 'MEDIUM';
|
|
271
320
|
}
|
|
272
321
|
|
|
@@ -276,14 +325,16 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
276
325
|
const locationMap = new Map();
|
|
277
326
|
for (const loc of allLocations) {
|
|
278
327
|
const key = `${loc.file}:${loc.line}:${loc.column}`;
|
|
279
|
-
if (!locationMap.has(key))
|
|
328
|
+
if (!locationMap.has(key)) {
|
|
329
|
+
locationMap.set(key, loc);
|
|
330
|
+
}
|
|
280
331
|
}
|
|
281
332
|
|
|
282
333
|
const isCritical = anyCredPattern;
|
|
283
334
|
const severity = isCritical ? 'critical' : confidenceScore >= 80 ? 'high' : 'medium';
|
|
284
335
|
|
|
285
|
-
const
|
|
286
|
-
.flatMap(f => f.domainsFound)
|
|
336
|
+
const _domainSummary = filesWithPatterns
|
|
337
|
+
.flatMap((f) => f.domainsFound)
|
|
287
338
|
.filter(Boolean)
|
|
288
339
|
.slice(0, 3);
|
|
289
340
|
|
|
@@ -300,17 +351,19 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
300
351
|
message = 'Filesystem exfiltration to external domain detected';
|
|
301
352
|
}
|
|
302
353
|
|
|
303
|
-
return [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
354
|
+
return [
|
|
355
|
+
{
|
|
356
|
+
detector: 'tier1-infostealer',
|
|
357
|
+
id: 'TIER1-INFOSTEALER',
|
|
358
|
+
severity,
|
|
359
|
+
confidence: confidenceLabel(confidenceScore),
|
|
360
|
+
confidenceScore,
|
|
361
|
+
subtype: mainSubtype || 'fs_exfil',
|
|
362
|
+
message,
|
|
363
|
+
evidence,
|
|
364
|
+
locations: [...locationMap.values()],
|
|
365
|
+
crossFiles: [...new Set(involvedFiles)],
|
|
366
|
+
reference: 'Campaign 2 & 3',
|
|
367
|
+
},
|
|
368
|
+
];
|
|
316
369
|
}
|