@lateos/npm-scan 0.18.0 → 0.18.2
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 +34 -0
- package/backend/detectors/index.js +6 -0
- package/backend/detectors/tier1-cloud-imds.js +124 -0
- package/backend/detectors/tier1-infostealer.js +36 -0
- package/backend/detectors/tier1-multistage-postinstall.js +81 -0
- package/backend/detectors/tier1-version-confusion.js +107 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
## v0.18.2 — June 2, 2026
|
|
12
|
+
|
|
13
|
+
### New Detectors
|
|
14
|
+
- **D6a** `tier1-version-confusion.js` — Detects dependency confusion via sentinel
|
|
15
|
+
versions (99.99.99 family → HIGH) and high-version heuristic (major≥9 → MEDIUM).
|
|
16
|
+
Covers Sonatype-2026-003429 and Microsoft scope confusion campaigns.
|
|
17
|
+
- **D6b** `tier1-multistage-postinstall.js` — Detects two-stage remote download +
|
|
18
|
+
binary execution and detached background persistence in lifecycle scripts.
|
|
19
|
+
Covers Gen-2 stager patterns from the OpenSearch/ES typosquatting wave.
|
|
20
|
+
- **D6c** `tier1-cloud-imds.js` — Detects GCP metadata server and Azure IMDS endpoint
|
|
21
|
+
targeting in scripts and JS files. Covers the Miasma @redhat-cloud-services campaign.
|
|
22
|
+
|
|
23
|
+
### Detector Enhancements
|
|
24
|
+
- **D2** `tier1-infostealer.js` — Added NAMED_SIGNATURES array with early-return
|
|
25
|
+
CRITICAL detection for confirmed malware campaign strings. First entry: Miasma
|
|
26
|
+
campaign identifier (June 2026).
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
- **D6b** `tier1-multistage-postinstall.js`
|
|
30
|
+
- Removed /g flag from REMOTE_FETCH_RE, BINARY_EXEC_RE, DETACHED_RE —
|
|
31
|
+
eliminated fragile lastIndex state between hook iterations
|
|
32
|
+
- Added critical severity tier to severityLabel — Signal A+B findings
|
|
33
|
+
now consistently report severity: critical / confidence: CRITICAL
|
|
34
|
+
- Fixed hardcoded "postinstall" in finding message — now reflects
|
|
35
|
+
whichever hook fired and the subtype string
|
|
36
|
+
|
|
37
|
+
### Infrastructure
|
|
38
|
+
- Added Detector Registry section to AGENTS.md with calibration notes.
|
|
39
|
+
|
|
40
|
+
### Test Suite
|
|
41
|
+
- 656 passing, 0 failing, 19 skipping.
|
|
42
|
+
|
|
9
43
|
### Added
|
|
10
44
|
- `scan --file <path>` flag to analyze local `.tgz` tarballs without fetching from npm registry
|
|
11
45
|
- `scan --fail-on <level>` flag to exit with code 1 when findings >= severity (CI/CD integration)
|
|
@@ -23,6 +23,9 @@ import { scan as tier1InfostealerScan } from './tier1-infostealer.js';
|
|
|
23
23
|
import { scan as tier1LifecycleHookScan } from './tier1-lifecycle-hook.js';
|
|
24
24
|
import { scan as tier1BinaryEmbedScan } from './tier1-binary-embed.js';
|
|
25
25
|
import { scan as tier1MetadataSpoofScan } from './tier1-metadata-spoof.js';
|
|
26
|
+
import { scan as tier1VersionConfusionScan } from './tier1-version-confusion.js';
|
|
27
|
+
import { scan as tier1CloudImdsScan } from './tier1-cloud-imds.js';
|
|
28
|
+
import { scan as tier1MultistagePostinstallScan } from './tier1-multistage-postinstall.js';
|
|
26
29
|
|
|
27
30
|
function timeout(ms) {
|
|
28
31
|
return new Promise((_, reject) => setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms));
|
|
@@ -72,5 +75,8 @@ export async function runAll(pkgJson, files = [], registryMeta = null, allFiles
|
|
|
72
75
|
findings.push(...await runTier1('tier1-lifecycle-hook', tier1LifecycleHookScan, pkgJson, files, registryMeta, allFiles || files));
|
|
73
76
|
findings.push(...await runTier1('tier1-binary-embed', tier1BinaryEmbedScan, pkgJson, files, registryMeta, allFiles || files));
|
|
74
77
|
findings.push(...await runTier1('tier1-metadata-spoof', tier1MetadataSpoofScan, pkgJson, files, registryMeta, allFiles || files));
|
|
78
|
+
findings.push(...await runTier1('tier1-version-confusion', tier1VersionConfusionScan, pkgJson, files, registryMeta, allFiles || files));
|
|
79
|
+
findings.push(...await runTier1('tier1-cloud-imds', tier1CloudImdsScan, pkgJson, files, registryMeta, allFiles || files));
|
|
80
|
+
findings.push(...await runTier1('tier1-multistage-postinstall', tier1MultistagePostinstallScan, pkgJson, files, registryMeta, allFiles || files));
|
|
75
81
|
return findings.sort((a, b) => b.severity.localeCompare(a.severity));
|
|
76
82
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const GCP_PATTERNS = [
|
|
2
|
+
'metadata.google.internal',
|
|
3
|
+
'computeMetadata/v1',
|
|
4
|
+
'metadata.google.internal/computeMetadata',
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const AZURE_PATTERNS = [
|
|
8
|
+
'169.254.169.254/metadata/instance',
|
|
9
|
+
'169.254.169.254/metadata/identity',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const AZURE_IP = '169.254.169.254';
|
|
13
|
+
const METADATA_HEADER_RE = /Metadata\s*:\s*true/i;
|
|
14
|
+
|
|
15
|
+
function severityLabel(score) {
|
|
16
|
+
if (score >= 80) return 'high';
|
|
17
|
+
return 'medium';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function confidenceLabel(score) {
|
|
21
|
+
if (score >= 80) return 'HIGH';
|
|
22
|
+
if (score >= 60) return 'MEDIUM';
|
|
23
|
+
return 'LOW';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasGcpPattern(text) {
|
|
27
|
+
return GCP_PATTERNS.some(p => text.includes(p));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasAzurePath(text) {
|
|
31
|
+
return AZURE_PATTERNS.some(p => text.includes(p));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasAzureHeaderPattern(text) {
|
|
35
|
+
const lines = text.split('\n');
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
if (!lines[i].includes(AZURE_IP)) continue;
|
|
38
|
+
const start = Math.max(0, i - 5);
|
|
39
|
+
const end = Math.min(lines.length, i + 6);
|
|
40
|
+
for (let j = start; j < end; j++) {
|
|
41
|
+
if (METADATA_HEADER_RE.test(lines[j])) return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasAzurePattern(text) {
|
|
48
|
+
return hasAzurePath(text) || hasAzureHeaderPattern(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectTexts(pkgJson, jsFiles) {
|
|
52
|
+
const texts = [];
|
|
53
|
+
|
|
54
|
+
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
55
|
+
for (const value of Object.values(pkgJson.scripts)) {
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
texts.push(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (jsFiles && Array.isArray(jsFiles)) {
|
|
63
|
+
for (const file of jsFiles) {
|
|
64
|
+
if (file?.content && typeof file.content === 'string') {
|
|
65
|
+
texts.push(file.content);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return texts;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const name = 'tier1-cloud-imds';
|
|
74
|
+
|
|
75
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
76
|
+
const texts = collectTexts(pkgJson, jsFiles);
|
|
77
|
+
if (texts.length === 0) return [];
|
|
78
|
+
|
|
79
|
+
let hasGcp = false;
|
|
80
|
+
let hasAzure = false;
|
|
81
|
+
|
|
82
|
+
for (const text of texts) {
|
|
83
|
+
if (!hasGcp && hasGcpPattern(text)) hasGcp = true;
|
|
84
|
+
if (!hasAzure && hasAzurePattern(text)) hasAzure = true;
|
|
85
|
+
if (hasGcp && hasAzure) break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!hasGcp && !hasAzure) return [];
|
|
89
|
+
|
|
90
|
+
let confidenceScore;
|
|
91
|
+
let subtype;
|
|
92
|
+
|
|
93
|
+
if (hasGcp && hasAzure) {
|
|
94
|
+
confidenceScore = 92;
|
|
95
|
+
subtype = 'multi_cloud_imds';
|
|
96
|
+
} else if (hasGcp) {
|
|
97
|
+
confidenceScore = 82;
|
|
98
|
+
subtype = 'gcp_metadata';
|
|
99
|
+
} else {
|
|
100
|
+
confidenceScore = 82;
|
|
101
|
+
subtype = 'azure_imds';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [{
|
|
105
|
+
detector: 'tier1-cloud-imds',
|
|
106
|
+
id: 'TIER1-CLOUD-IMDS',
|
|
107
|
+
severity: severityLabel(confidenceScore),
|
|
108
|
+
confidence: confidenceLabel(confidenceScore),
|
|
109
|
+
confidenceScore,
|
|
110
|
+
subtype,
|
|
111
|
+
message: hasGcp && hasAzure
|
|
112
|
+
? `Package references both GCP metadata and Azure IMDS endpoints — cloud credential harvesting`
|
|
113
|
+
: hasGcp
|
|
114
|
+
? `Package references GCP metadata server endpoint — cloud credential harvesting`
|
|
115
|
+
: `Package references Azure IMDS endpoint — cloud credential harvesting`,
|
|
116
|
+
evidence: [
|
|
117
|
+
...(hasGcp ? ['gcp: metadata.google.internal / computeMetadata/v1 pattern detected'] : []),
|
|
118
|
+
...(hasAzure ? ['azure: 169.254.169.254/metadata pattern detected'] : []),
|
|
119
|
+
],
|
|
120
|
+
crossFiles: [],
|
|
121
|
+
locations: [{ file: '', line: 0 }],
|
|
122
|
+
reference: 'Miasma Cloud IMDS',
|
|
123
|
+
}];
|
|
124
|
+
}
|
|
@@ -21,6 +21,11 @@ const EVAL_RE = /\beval\s*\(/g;
|
|
|
21
21
|
const FUNCTION_CTOR_RE = /\bFunction\s*\(/g;
|
|
22
22
|
const B64_STRING_RE = /['"`]([A-Za-z0-9+/]{40,}={0,2})['"`]/g;
|
|
23
23
|
|
|
24
|
+
// Named malware signatures — zero-FP string literals for confirmed campaigns
|
|
25
|
+
const NAMED_SIGNATURES = [
|
|
26
|
+
'Miasma: The Spreading Blight', // Miasma campaign, June 2026, @redhat-cloud-services compromise
|
|
27
|
+
];
|
|
28
|
+
|
|
24
29
|
function shannonEntropy(s) {
|
|
25
30
|
const len = s.length;
|
|
26
31
|
if (len === 0) return 0;
|
|
@@ -171,6 +176,37 @@ export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
|
171
176
|
if (pkgName && KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
172
177
|
|
|
173
178
|
const files = jsFiles || [];
|
|
179
|
+
|
|
180
|
+
// Named malware signature check — zero-FP string literals, early return
|
|
181
|
+
const sigTexts = [];
|
|
182
|
+
if (pkgJson?.scripts && typeof pkgJson.scripts === 'object') {
|
|
183
|
+
for (const value of Object.values(pkgJson.scripts)) {
|
|
184
|
+
if (typeof value === 'string') sigTexts.push(value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const f of files) {
|
|
188
|
+
if (f?.content) sigTexts.push(f.content);
|
|
189
|
+
}
|
|
190
|
+
for (const sig of NAMED_SIGNATURES) {
|
|
191
|
+
for (const text of sigTexts) {
|
|
192
|
+
if (text.includes(sig)) {
|
|
193
|
+
return [{
|
|
194
|
+
detector: 'tier1-infostealer',
|
|
195
|
+
id: 'TIER1-INFOSTEALER',
|
|
196
|
+
severity: 'critical',
|
|
197
|
+
confidence: 'CRITICAL',
|
|
198
|
+
confidenceScore: 98,
|
|
199
|
+
subtype: 'named_signature_miasma',
|
|
200
|
+
message: `Named malware signature detected: "${sig}"`,
|
|
201
|
+
evidence: [sig],
|
|
202
|
+
locations: [{ file: '', line: 0 }],
|
|
203
|
+
crossFiles: [],
|
|
204
|
+
reference: 'Campaign 2 & 3',
|
|
205
|
+
}];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
174
210
|
if (files.length === 0) return [];
|
|
175
211
|
|
|
176
212
|
let parseFailCount = 0;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const SCAN_HOOKS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
2
|
+
|
|
3
|
+
const REMOTE_FETCH_RE = /\b(?:fetch|axios\.get|axios\.post|http\.get|https\.get)\(|\b(?:curl|wget)\s/;
|
|
4
|
+
const BINARY_EXEC_RE = /\b(?:execFile|execFileSync|execSync|exec|spawnSync|spawn)\s*\(/;
|
|
5
|
+
const DETACHED_RE = /detached\s*:\s*true/;
|
|
6
|
+
|
|
7
|
+
function severityLabel(score) {
|
|
8
|
+
if (score >= 95) return 'critical';
|
|
9
|
+
if (score >= 80) return 'high';
|
|
10
|
+
if (score >= 60) return 'medium';
|
|
11
|
+
return 'low';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function confidenceLabel(score) {
|
|
15
|
+
if (score >= 95) return 'CRITICAL';
|
|
16
|
+
if (score >= 80) return 'HIGH';
|
|
17
|
+
if (score >= 60) return 'MEDIUM';
|
|
18
|
+
return 'LOW';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const name = 'tier1-multistage-postinstall';
|
|
22
|
+
|
|
23
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
24
|
+
const scripts = pkgJson?.scripts;
|
|
25
|
+
if (!scripts || typeof scripts !== 'object') return [];
|
|
26
|
+
|
|
27
|
+
const findings = [];
|
|
28
|
+
|
|
29
|
+
for (const hookName of SCAN_HOOKS) {
|
|
30
|
+
const content = scripts[hookName];
|
|
31
|
+
if (!content || typeof content !== 'string') continue;
|
|
32
|
+
|
|
33
|
+
const hasRemoteFetch = REMOTE_FETCH_RE.test(content);
|
|
34
|
+
const hasBinaryExec = BINARY_EXEC_RE.test(content);
|
|
35
|
+
const hasDetached = DETACHED_RE.test(content);
|
|
36
|
+
|
|
37
|
+
const signalA = hasRemoteFetch && hasBinaryExec;
|
|
38
|
+
const signalB = hasDetached;
|
|
39
|
+
|
|
40
|
+
if (!signalA && !signalB) continue;
|
|
41
|
+
|
|
42
|
+
let confidenceScore;
|
|
43
|
+
let subtype;
|
|
44
|
+
|
|
45
|
+
if (signalA && signalB) {
|
|
46
|
+
confidenceScore = 95;
|
|
47
|
+
subtype = 'two_stage_plus_detached';
|
|
48
|
+
} else if (signalA) {
|
|
49
|
+
confidenceScore = 82;
|
|
50
|
+
subtype = 'two_stage_download_exec';
|
|
51
|
+
} else {
|
|
52
|
+
confidenceScore = 78;
|
|
53
|
+
subtype = 'detached_background_process';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const evidence = [`hook: ${hookName}`];
|
|
57
|
+
if (hasRemoteFetch) evidence.push('pattern: remote fetch call');
|
|
58
|
+
if (hasBinaryExec) evidence.push('pattern: binary execution call');
|
|
59
|
+
if (hasDetached) evidence.push('pattern: detached background process');
|
|
60
|
+
|
|
61
|
+
findings.push({
|
|
62
|
+
detector: 'tier1-multistage-postinstall',
|
|
63
|
+
id: 'TIER1-MULTISTAGE-POSTINSTALL',
|
|
64
|
+
severity: severityLabel(confidenceScore),
|
|
65
|
+
confidence: confidenceLabel(confidenceScore),
|
|
66
|
+
confidenceScore,
|
|
67
|
+
subtype,
|
|
68
|
+
message: `Multi-stage install hook detected in "${hookName}" — ${subtype}`,
|
|
69
|
+
evidence,
|
|
70
|
+
locations: [{
|
|
71
|
+
file: 'package.json',
|
|
72
|
+
field: `scripts.${hookName}`,
|
|
73
|
+
value: content.length > 200 ? `${content.slice(0, 200)}...` : content,
|
|
74
|
+
}],
|
|
75
|
+
crossFiles: [],
|
|
76
|
+
reference: 'Sonatype-2026-003429',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return findings;
|
|
81
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { KNOWN_REPUTABLE_PACKAGES } from '../policy.js';
|
|
2
|
+
|
|
3
|
+
const SENTINEL_EXACT = ['99.99.99'];
|
|
4
|
+
const SENTINEL_FAMILY = ['9.9.9', '9.9.10', '10.10.10', '11.11.11'];
|
|
5
|
+
|
|
6
|
+
function severityLabel(score) {
|
|
7
|
+
if (score >= 80) return 'high';
|
|
8
|
+
if (score >= 60) return 'medium';
|
|
9
|
+
return 'low';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function confidenceLabel(score) {
|
|
13
|
+
if (score >= 80) return 'HIGH';
|
|
14
|
+
if (score >= 60) return 'MEDIUM';
|
|
15
|
+
return 'LOW';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseVersion(version) {
|
|
19
|
+
if (!version || typeof version !== 'string') return null;
|
|
20
|
+
const parts = version.split('.');
|
|
21
|
+
if (parts.length !== 3) return null;
|
|
22
|
+
const [major, minor, patch] = parts.map(Number);
|
|
23
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) return null;
|
|
24
|
+
return { major, minor, patch };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function matchesHeuristic(parsed) {
|
|
28
|
+
return parsed.major >= 9 && parsed.minor >= 5 && parsed.patch >= 5 && parsed.major !== 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const name = 'tier1-version-confusion';
|
|
32
|
+
|
|
33
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
34
|
+
const pkgName = pkgJson?.name;
|
|
35
|
+
const version = pkgJson?.version;
|
|
36
|
+
|
|
37
|
+
if (!pkgName || !version) return [];
|
|
38
|
+
if (KNOWN_REPUTABLE_PACKAGES.has(pkgName)) return [];
|
|
39
|
+
|
|
40
|
+
const parsed = parseVersion(version);
|
|
41
|
+
if (!parsed) return [];
|
|
42
|
+
|
|
43
|
+
const vStr = version;
|
|
44
|
+
|
|
45
|
+
// Priority: SENTINEL_EXACT > SENTINEL_FAMILY > HEURISTIC
|
|
46
|
+
if (SENTINEL_EXACT.includes(vStr)) {
|
|
47
|
+
const score = 85;
|
|
48
|
+
return [{
|
|
49
|
+
detector: 'tier1-version-confusion',
|
|
50
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
51
|
+
severity: severityLabel(score),
|
|
52
|
+
confidence: confidenceLabel(score),
|
|
53
|
+
confidenceScore: score,
|
|
54
|
+
subtype: 'sentinel_exact',
|
|
55
|
+
message: `Package "${pkgName}" uses exact sentinel version ${vStr} — dependency confusion indicator`,
|
|
56
|
+
evidence: [
|
|
57
|
+
`version: ${vStr}`,
|
|
58
|
+
`sentinel: exact match`,
|
|
59
|
+
],
|
|
60
|
+
crossFiles: [],
|
|
61
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
62
|
+
reference: 'Sonatype-2026-003429',
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (SENTINEL_FAMILY.includes(vStr)) {
|
|
67
|
+
const score = 65;
|
|
68
|
+
return [{
|
|
69
|
+
detector: 'tier1-version-confusion',
|
|
70
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
71
|
+
severity: severityLabel(score),
|
|
72
|
+
confidence: confidenceLabel(score),
|
|
73
|
+
confidenceScore: score,
|
|
74
|
+
subtype: 'sentinel_family',
|
|
75
|
+
message: `Package "${pkgName}" uses sentinel family version ${vStr} — dependency confusion indicator`,
|
|
76
|
+
evidence: [
|
|
77
|
+
`version: ${vStr}`,
|
|
78
|
+
`sentinel: family match`,
|
|
79
|
+
],
|
|
80
|
+
crossFiles: [],
|
|
81
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
82
|
+
reference: 'Sonatype-2026-003429',
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (matchesHeuristic(parsed)) {
|
|
87
|
+
const score = 62;
|
|
88
|
+
return [{
|
|
89
|
+
detector: 'tier1-version-confusion',
|
|
90
|
+
id: 'TIER1-VERSION-CONFUSION',
|
|
91
|
+
severity: severityLabel(score),
|
|
92
|
+
confidence: confidenceLabel(score),
|
|
93
|
+
confidenceScore: score,
|
|
94
|
+
subtype: 'high_version_heuristic',
|
|
95
|
+
message: `Package "${pkgName}" version ${vStr} matches high-version heuristic — possible dependency confusion`,
|
|
96
|
+
evidence: [
|
|
97
|
+
`version: ${vStr}`,
|
|
98
|
+
`major: ${parsed.major}, minor: ${parsed.minor}, patch: ${parsed.patch}`,
|
|
99
|
+
],
|
|
100
|
+
crossFiles: [],
|
|
101
|
+
locations: [{ file: 'package.json', line: 3, column: 10 }],
|
|
102
|
+
reference: 'Microsoft Scope Confusion',
|
|
103
|
+
}];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [];
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lateos/npm-scan",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Modern npm supply chain security scanner — detects obfuscated payloads, credential stealers, conditional triggers, sandbox evasion, and worm-like propagation. 11 attack types, SBOM, NIST/EU CRA compliance reporting.",
|
|
5
5
|
"main": "backend/index.js",
|
|
6
6
|
"bin": {
|