@lateos/npm-scan 1.1.1 → 1.2.1
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.
|
@@ -108,4 +108,60 @@ export default {
|
|
|
108
108
|
cross_package_burst_weight: 50,
|
|
109
109
|
notes: 'D13: Version velocity anomaly and maintainer compromise detection',
|
|
110
110
|
},
|
|
111
|
+
'D14-BUILD-CONFIG-ABUSE': {
|
|
112
|
+
flag_threshold: 70,
|
|
113
|
+
warn_threshold: 50,
|
|
114
|
+
pattern_weights: {
|
|
115
|
+
shell_exec: 50,
|
|
116
|
+
process_spawn: 45,
|
|
117
|
+
env_access: 35,
|
|
118
|
+
fs_access: 40,
|
|
119
|
+
http_request: 50,
|
|
120
|
+
path_traversal: 55,
|
|
121
|
+
hardcoded_key: 60,
|
|
122
|
+
getenv_call: 35,
|
|
123
|
+
curl_call: 45,
|
|
124
|
+
execve_call: 55,
|
|
125
|
+
credential_scan: 50,
|
|
126
|
+
socket_call: 40,
|
|
127
|
+
environ_access: 35,
|
|
128
|
+
},
|
|
129
|
+
legitimate_native_addons: [
|
|
130
|
+
'node-sass',
|
|
131
|
+
'sqlite3',
|
|
132
|
+
'bcrypt',
|
|
133
|
+
'@mapbox/node-pre-gyp',
|
|
134
|
+
'better-sqlite3',
|
|
135
|
+
'sharp',
|
|
136
|
+
'canvas',
|
|
137
|
+
'node-gyp',
|
|
138
|
+
'prebuild-install',
|
|
139
|
+
],
|
|
140
|
+
pattern_confidence: {
|
|
141
|
+
hardcoded_key: 0.95,
|
|
142
|
+
execve_call: 0.9,
|
|
143
|
+
socket_call: 0.75,
|
|
144
|
+
env_access: 0.6,
|
|
145
|
+
getenv_call: 0.7,
|
|
146
|
+
curl_call: 0.85,
|
|
147
|
+
},
|
|
148
|
+
max_binary_size_bytes: 10 * 1024 * 1024,
|
|
149
|
+
binary_size_weight: 30,
|
|
150
|
+
binary_age_mismatch_days: 30,
|
|
151
|
+
undeclared_gyp_weight: 45,
|
|
152
|
+
known_reputable_packages: [
|
|
153
|
+
'electron',
|
|
154
|
+
'puppeteer',
|
|
155
|
+
'playwright',
|
|
156
|
+
'sharp',
|
|
157
|
+
'esbuild',
|
|
158
|
+
'node-gyp',
|
|
159
|
+
'@mapbox/node-pre-gyp',
|
|
160
|
+
'node-pre-gyp',
|
|
161
|
+
'webpack',
|
|
162
|
+
'vite',
|
|
163
|
+
'rollup',
|
|
164
|
+
],
|
|
165
|
+
notes: 'D14: Build Configuration Abuse (Phantom Gyp / Miasma variant)',
|
|
166
|
+
},
|
|
111
167
|
};
|
|
@@ -33,6 +33,7 @@ import { scan as tier1SelfPropagationScan } from './tier1-self-propagation.js';
|
|
|
33
33
|
import { scan as tier1EncryptedC2Scan } from './tier1-encrypted-c2.js';
|
|
34
34
|
import { scan as tier1TransitiveDepsScan } from './tier1-transitive-deps.js';
|
|
35
35
|
import { scan as tier1MaintainerCompromiseScan } from './tier1-maintainer-compromise.js';
|
|
36
|
+
import { scan as tier1BuildConfigAbuseScan } from './tier1-build-config-abuse.js';
|
|
36
37
|
|
|
37
38
|
function timeout(ms) {
|
|
38
39
|
return new Promise((_, reject) =>
|
|
@@ -231,5 +232,15 @@ export async function runAll(pkgJson, files = [], registryMeta = null, allFiles
|
|
|
231
232
|
allFiles || files
|
|
232
233
|
))
|
|
233
234
|
);
|
|
235
|
+
findings.push(
|
|
236
|
+
...(await runTier1(
|
|
237
|
+
'tier1-build-config-abuse',
|
|
238
|
+
tier1BuildConfigAbuseScan,
|
|
239
|
+
pkgJson,
|
|
240
|
+
files,
|
|
241
|
+
registryMeta,
|
|
242
|
+
allFiles || files
|
|
243
|
+
))
|
|
244
|
+
);
|
|
234
245
|
return findings.sort((a, b) => b.severity.localeCompare(a.severity));
|
|
235
246
|
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import thresholds from './config/thresholds.js';
|
|
2
|
+
|
|
3
|
+
const cfg = thresholds['D14-BUILD-CONFIG-ABUSE'];
|
|
4
|
+
const PATTERN_WEIGHTS = cfg.pattern_weights;
|
|
5
|
+
const PATTERN_CONFIDENCE = cfg.pattern_confidence;
|
|
6
|
+
const LEGITIMATE_ADDONS = new Set(cfg.legitimate_native_addons);
|
|
7
|
+
|
|
8
|
+
function fileByName(files, name) {
|
|
9
|
+
if (!files) return null;
|
|
10
|
+
const target = name.replace(/\\/g, '/').toLowerCase();
|
|
11
|
+
return (
|
|
12
|
+
files.find((f) => {
|
|
13
|
+
const fp = (f.path || f.name || '').replace(/\\/g, '/').toLowerCase();
|
|
14
|
+
return fp === target || fp.endsWith('/' + target);
|
|
15
|
+
}) || null
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function filesByExt(files, exts) {
|
|
20
|
+
if (!files) return [];
|
|
21
|
+
const lower = exts.map((e) => e.toLowerCase());
|
|
22
|
+
return files.filter((f) => {
|
|
23
|
+
const fp = (f.path || f.name || '').toLowerCase();
|
|
24
|
+
return lower.some((e) => fp.endsWith(e));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractLines(content, matchIndex) {
|
|
29
|
+
if (!content) return 1;
|
|
30
|
+
const before = content.slice(0, matchIndex);
|
|
31
|
+
return (before.match(/\n/g) || []).length + 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const name = 'tier1-build-config-abuse';
|
|
35
|
+
|
|
36
|
+
export async function scan(pkgJson, jsFiles, registryMeta, allFiles) {
|
|
37
|
+
const pkgName = pkgJson?.name;
|
|
38
|
+
if (
|
|
39
|
+
pkgName &&
|
|
40
|
+
cfg.known_reputable_packages?.some((r) => pkgName === r || pkgName.startsWith(r + '/'))
|
|
41
|
+
) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const files = allFiles || jsFiles || [];
|
|
46
|
+
if (files.length === 0) return [];
|
|
47
|
+
|
|
48
|
+
const findings = [];
|
|
49
|
+
let aggregatedRisk = 0;
|
|
50
|
+
|
|
51
|
+
const hasGypFile = !!fileByName(files, 'binding.gyp');
|
|
52
|
+
const isLegitimateAddon = pkgName && LEGITIMATE_ADDONS.has(pkgName);
|
|
53
|
+
|
|
54
|
+
// Step 1: binding.gyp presence on non-legitimate addon
|
|
55
|
+
const hasGypDeclared =
|
|
56
|
+
pkgJson &&
|
|
57
|
+
(pkgJson.gypfile === true ||
|
|
58
|
+
!!pkgJson.binary ||
|
|
59
|
+
(pkgJson.scripts &&
|
|
60
|
+
typeof pkgJson.scripts.install === 'string' &&
|
|
61
|
+
pkgJson.scripts.install.includes('node-gyp')) ||
|
|
62
|
+
(pkgJson.scripts &&
|
|
63
|
+
typeof pkgJson.scripts.install === 'string' &&
|
|
64
|
+
pkgJson.scripts.install.includes('node-pre-gyp')));
|
|
65
|
+
|
|
66
|
+
if (hasGypFile && !isLegitimateAddon && !hasGypDeclared) {
|
|
67
|
+
findings.push({
|
|
68
|
+
detector: 'tier1-build-config-abuse',
|
|
69
|
+
id: 'D14-BUILD-CONFIG-ABUSE',
|
|
70
|
+
severity: 'medium',
|
|
71
|
+
confidence: 'MEDIUM',
|
|
72
|
+
confidenceScore: 40,
|
|
73
|
+
message: `Unexpected binding.gyp in non-native-addon package${pkgName ? ': ' + pkgName : ''}`,
|
|
74
|
+
evidence: ['binding.gyp present but package is not a known native addon'],
|
|
75
|
+
locations: [{ file: 'binding.gyp', line: 1 }],
|
|
76
|
+
});
|
|
77
|
+
aggregatedRisk += 20;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 2: Parse binding.gyp for suspicious patterns
|
|
81
|
+
if (hasGypFile) {
|
|
82
|
+
const gypFile = fileByName(files, 'binding.gyp');
|
|
83
|
+
const gypContent = gypFile?.content || '';
|
|
84
|
+
|
|
85
|
+
if (gypContent) {
|
|
86
|
+
const gypPatterns = {
|
|
87
|
+
shell_exec: /<!?\(.*\)/g,
|
|
88
|
+
process_spawn: /\b(spawn|exec|execSync|spawnSync|fork)\s*\(/g,
|
|
89
|
+
env_access: /process\.env\./g,
|
|
90
|
+
fs_access: /\bfs\.(read|write|readFile|writeFile|readdir|exists|stat|mkdir|rm|unlink)/g,
|
|
91
|
+
http_request: /\b(http|https|curl|wget|fetch)\b/gi,
|
|
92
|
+
path_traversal: /\.\.\//g,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const [patternName, regex] of Object.entries(gypPatterns)) {
|
|
96
|
+
regex.lastIndex = 0;
|
|
97
|
+
let match;
|
|
98
|
+
while ((match = regex.exec(gypContent)) !== null) {
|
|
99
|
+
const line = extractLines(gypContent, match.index);
|
|
100
|
+
findings.push({
|
|
101
|
+
detector: 'tier1-build-config-abuse',
|
|
102
|
+
id: 'D14-BUILD-CONFIG-ABUSE',
|
|
103
|
+
severity: PATTERN_WEIGHTS[patternName] >= 50 ? 'high' : 'medium',
|
|
104
|
+
confidence: 'HIGH',
|
|
105
|
+
confidenceScore: PATTERN_WEIGHTS[patternName],
|
|
106
|
+
message: `binding.gyp contains ${patternName.replace(/_/g, ' ')} pattern`,
|
|
107
|
+
evidence: [`pattern: ${patternName}`, `match: ${match[0].slice(0, 120)}`],
|
|
108
|
+
locations: [{ file: 'binding.gyp', line }],
|
|
109
|
+
});
|
|
110
|
+
aggregatedRisk += PATTERN_WEIGHTS[patternName] || 30;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Step 3: Analyze C/C++ Source Code
|
|
117
|
+
const cppFiles = filesByExt(files, ['.cc', '.cpp', '.c', '.cxx', '.h', '.hpp']);
|
|
118
|
+
for (const cppFile of cppFiles) {
|
|
119
|
+
const content = cppFile.content || '';
|
|
120
|
+
if (!content) continue;
|
|
121
|
+
|
|
122
|
+
const cPatterns = {
|
|
123
|
+
hardcoded_key:
|
|
124
|
+
/(?:AWS|GITHUB|SLACK|STRIPE|TOKEN|SECRET|API_KEY|PASSWORD)\s*[:=]\s*['"][A-Za-z0-9_-]{20,}['"]|['"](?:sk_live_|sk_test_|ghp_|gho_|ghs_|ghu_)[A-Za-z0-9_-]{20,}['"]/g,
|
|
125
|
+
getenv_call: /\b(getenv|secure_getenv|getenv_s)\s*\(/g,
|
|
126
|
+
curl_call: /\b(curl_easy_perform|curl_slist_append|CURLOPT_URL|curl_easy_setopt)\s*\(/g,
|
|
127
|
+
execve_call: /\b(execve|execvp|execl|execlp|system|popen|pclose)\s*\(/g,
|
|
128
|
+
credential_scan: /(~\/\.aws|~\/\.ssh|\.env|credentials\.json|\/etc\/passwd)/g,
|
|
129
|
+
socket_call: /\b(socket\s*\(|listen\s*\(|accept\s*\(|connect\s*\(AF_)/g,
|
|
130
|
+
environ_access: /\b(environ|__environ)\s*\[/g,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const [patternName, regex] of Object.entries(cPatterns)) {
|
|
134
|
+
regex.lastIndex = 0;
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = regex.exec(content)) !== null) {
|
|
137
|
+
const line = extractLines(content, match.index);
|
|
138
|
+
const confidence = PATTERN_CONFIDENCE[patternName] || 0.7;
|
|
139
|
+
const risk = PATTERN_WEIGHTS[patternName] || 30;
|
|
140
|
+
|
|
141
|
+
findings.push({
|
|
142
|
+
detector: 'tier1-build-config-abuse',
|
|
143
|
+
id: 'D14-BUILD-CONFIG-ABUSE',
|
|
144
|
+
severity: risk >= 50 ? 'high' : 'medium',
|
|
145
|
+
confidence: confidence >= 0.85 ? 'HIGH' : confidence >= 0.7 ? 'MEDIUM' : 'LOW',
|
|
146
|
+
confidenceScore: risk,
|
|
147
|
+
message: `C/C++ code contains ${patternName.replace(/_/g, ' ')} pattern`,
|
|
148
|
+
evidence: [
|
|
149
|
+
`pattern: ${patternName}`,
|
|
150
|
+
`match: ${match[0].slice(0, 120)}`,
|
|
151
|
+
`confidence: ${confidence}`,
|
|
152
|
+
],
|
|
153
|
+
locations: [{ file: cppFile.path || cppFile.name || 'unknown.cc', line }],
|
|
154
|
+
});
|
|
155
|
+
aggregatedRisk += risk;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Step 4: Check .node file legitimacy
|
|
161
|
+
const nodeFiles = filesByExt(files, ['.node']);
|
|
162
|
+
for (const nodeFile of nodeFiles) {
|
|
163
|
+
const content = nodeFile.content || '';
|
|
164
|
+
const fileSize = content.length;
|
|
165
|
+
|
|
166
|
+
if (fileSize > cfg.max_binary_size_bytes) {
|
|
167
|
+
findings.push({
|
|
168
|
+
detector: 'tier1-build-config-abuse',
|
|
169
|
+
id: 'D14-BUILD-CONFIG-ABUSE',
|
|
170
|
+
severity: 'medium',
|
|
171
|
+
confidence: 'MEDIUM',
|
|
172
|
+
confidenceScore: cfg.binary_size_weight,
|
|
173
|
+
message: 'Large prebuilt .node binary',
|
|
174
|
+
evidence: [
|
|
175
|
+
`file: ${nodeFile.path || nodeFile.name}`,
|
|
176
|
+
`size: ${(fileSize / (1024 * 1024)).toFixed(1)} MB`,
|
|
177
|
+
`max allowed: ${(cfg.max_binary_size_bytes / (1024 * 1024)).toFixed(1)} MB`,
|
|
178
|
+
],
|
|
179
|
+
locations: [{ file: nodeFile.path || nodeFile.name || 'unknown.node' }],
|
|
180
|
+
});
|
|
181
|
+
aggregatedRisk += cfg.binary_size_weight;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Step 5: Cross-reference — undeclared binding.gyp
|
|
186
|
+
if (hasGypFile && pkgJson && !hasGypDeclared) {
|
|
187
|
+
findings.push({
|
|
188
|
+
detector: 'tier1-build-config-abuse',
|
|
189
|
+
id: 'D14-BUILD-CONFIG-ABUSE',
|
|
190
|
+
severity: 'high',
|
|
191
|
+
confidence: 'HIGH',
|
|
192
|
+
confidenceScore: cfg.undeclared_gyp_weight,
|
|
193
|
+
message: 'Undeclared binding.gyp — package.json does not advertise native build',
|
|
194
|
+
evidence: ['binding.gyp exists but no gypfile/binary/install-script in package.json'],
|
|
195
|
+
locations: [{ file: 'binding.gyp', line: 1 }],
|
|
196
|
+
});
|
|
197
|
+
aggregatedRisk += cfg.undeclared_gyp_weight;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (findings.length === 0) return [];
|
|
201
|
+
|
|
202
|
+
const overallScore = Math.min(100, Math.max(0, aggregatedRisk));
|
|
203
|
+
let severity;
|
|
204
|
+
if (overallScore >= cfg.flag_threshold) {
|
|
205
|
+
severity = 'critical';
|
|
206
|
+
} else if (overallScore >= cfg.warn_threshold) {
|
|
207
|
+
severity = 'high';
|
|
208
|
+
} else if (overallScore >= 30) {
|
|
209
|
+
severity = 'medium';
|
|
210
|
+
} else {
|
|
211
|
+
severity = 'low';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function confidenceLabel(sc) {
|
|
215
|
+
if (sc >= 80) return 'HIGH';
|
|
216
|
+
if (sc >= 50) return 'MEDIUM';
|
|
217
|
+
return 'LOW';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const hasShellExec = findings.some((f) => f.evidence?.some((e) => e.includes('shell_exec')));
|
|
221
|
+
const hasCreds = findings.some((f) => f.evidence?.some((e) => e.includes('hardcoded_key')));
|
|
222
|
+
const hasNetwork = findings.some((f) =>
|
|
223
|
+
f.evidence?.some((e) => e.includes('curl_call') || e.includes('http_request'))
|
|
224
|
+
);
|
|
225
|
+
const hasExec = findings.some((f) => f.evidence?.some((e) => e.includes('execve_call')));
|
|
226
|
+
|
|
227
|
+
let recommendation = 'PASS';
|
|
228
|
+
if (hasShellExec || hasCreds || hasExec) {
|
|
229
|
+
recommendation = 'BLOCK - Native addon build contains malicious patterns';
|
|
230
|
+
} else if (hasNetwork) {
|
|
231
|
+
recommendation = 'WARN - Native addon build makes network calls';
|
|
232
|
+
} else if (aggregatedRisk > cfg.warn_threshold) {
|
|
233
|
+
recommendation = 'REVIEW - Suspicious build configuration detected';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
detector: 'tier1-build-config-abuse',
|
|
239
|
+
id: 'D14-BUILD-CONFIG-ABUSE',
|
|
240
|
+
severity,
|
|
241
|
+
confidence: confidenceLabel(overallScore),
|
|
242
|
+
confidenceScore: overallScore,
|
|
243
|
+
message: `Build Configuration Abuse detected (aggregated risk: ${aggregatedRisk})`,
|
|
244
|
+
evidence: [
|
|
245
|
+
`total_findings: ${findings.length}`,
|
|
246
|
+
`aggregated_risk: ${aggregatedRisk}`,
|
|
247
|
+
...findings.map((f) => {
|
|
248
|
+
const loc = f.locations?.[0];
|
|
249
|
+
return `${f.message}${loc ? ' @ ' + (loc.file || '') + (loc.line ? ':' + loc.line : '') : ''}`;
|
|
250
|
+
}),
|
|
251
|
+
],
|
|
252
|
+
locations: findings.flatMap((f) => f.locations || []),
|
|
253
|
+
recommendation,
|
|
254
|
+
detail: findings.map((f) => ({
|
|
255
|
+
type:
|
|
256
|
+
f.evidence?.find((e) => e.startsWith('pattern:'))?.replace('pattern: ', '') || 'unknown',
|
|
257
|
+
pattern: f.evidence?.find((e) => e.startsWith('pattern:'))?.replace('pattern: ', ''),
|
|
258
|
+
confidence: f.confidenceScore,
|
|
259
|
+
risk: f.confidenceScore,
|
|
260
|
+
location: f.locations?.[0] || null,
|
|
261
|
+
})),
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lateos/npm-scan",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Production-grade npm supply chain vulnerability scanner. Detects 100% of 3 real May 2026 supply chain campaigns (dependency confusion, obfuscation, impersonation) with 0% false positive rate on top 1,000 npm packages.",
|
|
5
5
|
"main": "backend/index.js",
|
|
6
6
|
"bin": {
|