@lateos/npm-scan 1.2.0 → 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.
- package/README.de.md +3 -98
- package/README.fr.md +3 -98
- package/README.ja.md +3 -98
- package/README.md +741 -864
- package/README.zh.md +3 -98
- package/backend/detectors/config/thresholds.js +56 -0
- package/backend/detectors/index.js +11 -0
- package/backend/detectors/tier1-build-config-abuse.js +264 -0
- package/package.json +1 -1
package/README.zh.md
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@lateos/npm-scan)
|
|
10
10
|
[](LICENSING.md)
|
|
11
11
|
[](package.json)
|
|
12
|
-
[](https://github.com/lateos-ai/npm-scan)
|
|
13
|
+
[](https://github.com/lateos-ai/npm-scan)
|
|
14
14
|
[](https://hub.docker.com/r/lateos/npm-scan)
|
|
15
15
|
[](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
|
|
16
16
|
|
|
@@ -485,102 +485,6 @@ npm-scan report --html > report.html
|
|
|
485
485
|
|
|
486
486
|
### Docker
|
|
487
487
|
|
|
488
|
-
请参见上方的 [Docker 快速入门部分](#-在任何地方通过-docker-运行-lateosnpm-scan--零安装),了解拉取命令、Compose 流水线和多架构镜像。
|
|
489
|
-
|
|
490
|
-
在每个 PR 上扫描您项目的 `package-lock.json`——在它们进入生产环境之前检测域名抢注、混淆载荷、凭证窃取器和蠕虫传播:
|
|
491
|
-
|
|
492
|
-
```yaml
|
|
493
|
-
# .github/workflows/scan.yml
|
|
494
|
-
name: npm-scan
|
|
495
|
-
on:
|
|
496
|
-
pull_request:
|
|
497
|
-
paths:
|
|
498
|
-
- 'package-lock.json'
|
|
499
|
-
- '**/package.json'
|
|
500
|
-
jobs:
|
|
501
|
-
scan:
|
|
502
|
-
runs-on: ubuntu-latest
|
|
503
|
-
steps:
|
|
504
|
-
- uses: actions/checkout@v4
|
|
505
|
-
- uses: actions/setup-node@v4
|
|
506
|
-
with:
|
|
507
|
-
node-version: 20
|
|
508
|
-
- name: Scan lockfile
|
|
509
|
-
uses: lateos/npm-scan@v1
|
|
510
|
-
with:
|
|
511
|
-
scan-type: lockfile
|
|
512
|
-
fail-on: high
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
#### Action 输入
|
|
516
|
-
|
|
517
|
-
| 输入 | 默认值 | 描述 |
|
|
518
|
-
|-------|---------|-------------|
|
|
519
|
-
| `scan-type` | `lockfile` | `lockfile` 扫描 `package-lock.json` 或 `package` 扫描特定 npm 包 |
|
|
520
|
-
| `package` | — | 包名(`scan-type=package` 时需要) |
|
|
521
|
-
| `fail-on` | `high` | 在此严重性阈值处使工作流失败:`none`、`low`、`medium`、`high`、`critical` |
|
|
522
|
-
| `policy-file` | — | YAML/JSON 策略文件路径,用于白名单、严重性覆盖和抑制 |
|
|
523
|
-
| `license-key` | — | 用于 SIEM 导出和 PDF 报告的高级版许可证密钥 |
|
|
524
|
-
| `siem-format` | — | SIEM 输出:`cef`、`ecs`、`sentinel`、`qradar`(高级版) |
|
|
525
|
-
| `sbom-format` | — | SBOM 输出:`json`、`xml`、`spdx` |
|
|
526
|
-
|
|
527
|
-
#### Action 输出
|
|
528
|
-
|
|
529
|
-
| 输出 | 描述 |
|
|
530
|
-
|--------|-------------|
|
|
531
|
-
| `findings-count` | 检测到的发现项数量 |
|
|
532
|
-
| `scan-id` | 扫描 ID,用于后续报告引用 |
|
|
533
|
-
|
|
534
|
-
#### 示例:使用策略 + SBOM 扫描特定包
|
|
535
|
-
|
|
536
|
-
```yaml
|
|
537
|
-
- uses: lateos/npm-scan@v1
|
|
538
|
-
with:
|
|
539
|
-
scan-type: package
|
|
540
|
-
package: lodash
|
|
541
|
-
policy-file: .npm-scan.yml
|
|
542
|
-
sbom-format: spdx
|
|
543
|
-
fail-on: critical
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
#### 示例:使用 SIEM 导出扫描(高级版)
|
|
547
|
-
|
|
548
|
-
```yaml
|
|
549
|
-
- uses: lateos/npm-scan@v1
|
|
550
|
-
with:
|
|
551
|
-
scan-type: lockfile
|
|
552
|
-
siem-format: cef
|
|
553
|
-
license-key: ${{ secrets.NPM_SCAN_LICENSE_KEY }}
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
### CI/CD 流水线
|
|
557
|
-
|
|
558
|
-
直接集成到您现有的流水线中,无需复合操作:
|
|
559
|
-
|
|
560
|
-
```bash
|
|
561
|
-
# 扫描锁定文件,在高严重性时使构建失败
|
|
562
|
-
npm-scan scan-lockfile --policy .npm-scan.yml || exit 1
|
|
563
|
-
|
|
564
|
-
# 扫描特定包,仅在严重时失败
|
|
565
|
-
npm-scan scan lodash --policy .npm-scan.yml || exit 1
|
|
566
|
-
|
|
567
|
-
# 生成 SBOM 作为构建产物
|
|
568
|
-
npm-scan scan express --sbom spdx > express-sbom.spdx.json
|
|
569
|
-
|
|
570
|
-
# 在 CI 中生成 HTML 合规报告
|
|
571
|
-
npm-scan report --html > report.html
|
|
572
|
-
|
|
573
|
-
# 上传报告作为产物
|
|
574
|
-
# uses: actions/upload-artifact@v4
|
|
575
|
-
# with:
|
|
576
|
-
# name: npm-scan-report
|
|
577
|
-
# path: report.html
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
### Docker
|
|
581
|
-
|
|
582
|
-
请参见上方的 [Docker 快速入门部分](#-在任何地方通过-docker-运行-lateosnpm-scan--零安装),了解拉取命令、Compose 流水线和多架构镜像。
|
|
583
|
-
|
|
584
488
|
---
|
|
585
489
|
|
|
586
490
|
## 🗺️ 路线图与企业功能
|
|
@@ -706,4 +610,5 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
706
610
|
|
|
707
611
|
```bash
|
|
708
612
|
npx @lateos/npm-scan scan lodash
|
|
613
|
+
```
|
|
709
614
|
```
|
|
@@ -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.2.
|
|
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": {
|