getdoorman 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/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,1684 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
function isSourceFile(f) { return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].some(e => f.endsWith(e)); }
|
|
6
|
+
function isCIFile(f) { return f.match(/\.github\/workflows\/.*\.ya?ml$|\.gitlab-ci\.ya?ml$|Jenkinsfile$|\.circleci\/config\.ya?ml$|\.travis\.yml$/) !== null; }
|
|
7
|
+
|
|
8
|
+
const rules = [
|
|
9
|
+
// DEPS-001: Known vulnerabilities
|
|
10
|
+
{
|
|
11
|
+
id: 'DEPS-001',
|
|
12
|
+
category: 'dependencies',
|
|
13
|
+
severity: 'high',
|
|
14
|
+
confidence: 'likely',
|
|
15
|
+
title: 'Dependencies with Known Vulnerabilities',
|
|
16
|
+
check({ files, stack }) {
|
|
17
|
+
const findings = [];
|
|
18
|
+
if (stack.runtime !== 'node') return findings;
|
|
19
|
+
if (!files.has('package.json')) return findings;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const result = execSync('npm audit --json 2>/dev/null', {
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
timeout: 30000,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
});
|
|
27
|
+
const audit = JSON.parse(result);
|
|
28
|
+
const vulns = audit.vulnerabilities || {};
|
|
29
|
+
|
|
30
|
+
let criticalCount = 0;
|
|
31
|
+
let highCount = 0;
|
|
32
|
+
|
|
33
|
+
for (const [name, info] of Object.entries(vulns)) {
|
|
34
|
+
if (info.severity === 'critical') criticalCount++;
|
|
35
|
+
if (info.severity === 'high') highCount++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (criticalCount > 0) {
|
|
39
|
+
findings.push({
|
|
40
|
+
ruleId: 'DEPS-001', category: 'dependencies', severity: 'critical',
|
|
41
|
+
title: `${criticalCount} critical vulnerabilit${criticalCount === 1 ? 'y' : 'ies'} in dependencies`,
|
|
42
|
+
description: 'Run `npm audit fix` or update the affected packages.',
|
|
43
|
+
fix: null,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (highCount > 0) {
|
|
47
|
+
findings.push({
|
|
48
|
+
ruleId: 'DEPS-001b', category: 'dependencies', severity: 'high',
|
|
49
|
+
title: `${highCount} high-severity vulnerabilit${highCount === 1 ? 'y' : 'ies'} in dependencies`,
|
|
50
|
+
description: 'Run `npm audit fix` or update the affected packages.',
|
|
51
|
+
fix: null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// npm audit can fail for many reasons (no package-lock, network, etc.)
|
|
56
|
+
// Log at debug level so users can diagnose if needed
|
|
57
|
+
if (typeof context !== 'undefined' && context.debug) console.warn(`[DEPS-001] npm audit failed: ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
return findings;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// DEPS-002: No lock file
|
|
64
|
+
{
|
|
65
|
+
id: 'DEPS-002',
|
|
66
|
+
category: 'dependencies',
|
|
67
|
+
severity: 'medium',
|
|
68
|
+
confidence: 'likely',
|
|
69
|
+
title: 'No Package Lock File',
|
|
70
|
+
check({ files, stack }) {
|
|
71
|
+
const findings = [];
|
|
72
|
+
if (stack.runtime !== 'node') return findings;
|
|
73
|
+
if (!files.has('package.json')) return findings;
|
|
74
|
+
|
|
75
|
+
const hasLock = files.has('package-lock.json') ||
|
|
76
|
+
[...files.keys()].some(f => f === 'yarn.lock' || f === 'pnpm-lock.yaml' || f === 'bun.lockb');
|
|
77
|
+
|
|
78
|
+
if (!hasLock) {
|
|
79
|
+
findings.push({
|
|
80
|
+
ruleId: 'DEPS-002', category: 'dependencies', severity: 'medium',
|
|
81
|
+
title: 'No lock file committed — builds may be non-deterministic',
|
|
82
|
+
description: 'Commit your lock file (package-lock.json, yarn.lock, etc.) for reproducible builds.',
|
|
83
|
+
fix: null,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return findings;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// DEPS-003: Using deprecated packages
|
|
91
|
+
{
|
|
92
|
+
id: 'DEPS-003',
|
|
93
|
+
category: 'dependencies',
|
|
94
|
+
severity: 'medium',
|
|
95
|
+
confidence: 'likely',
|
|
96
|
+
title: 'Deprecated Dependencies',
|
|
97
|
+
check({ stack }) {
|
|
98
|
+
const findings = [];
|
|
99
|
+
const deprecated = {
|
|
100
|
+
'request': 'Use `node-fetch`, `axios`, or built-in `fetch` instead',
|
|
101
|
+
'moment': 'Use `date-fns`, `dayjs`, or `luxon` instead',
|
|
102
|
+
'uuid': null, // not deprecated but check version
|
|
103
|
+
'csurf': 'Deprecated due to issues. Use csrf-csrf or custom implementation',
|
|
104
|
+
'express-validator': null,
|
|
105
|
+
'body-parser': 'Built into Express 4.16+, remove it',
|
|
106
|
+
'querystring': 'Use URLSearchParams (built-in) instead',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
for (const [dep, message] of Object.entries(deprecated)) {
|
|
110
|
+
if (dep in (stack.dependencies || {})) {
|
|
111
|
+
findings.push({
|
|
112
|
+
ruleId: 'DEPS-003', category: 'dependencies', severity: 'medium',
|
|
113
|
+
title: `Deprecated package: ${dep}`,
|
|
114
|
+
description: message || `${dep} is deprecated. Find a maintained alternative.`,
|
|
115
|
+
fix: null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// DEPS-004: No .gitignore for node_modules
|
|
124
|
+
{
|
|
125
|
+
id: 'DEPS-004',
|
|
126
|
+
category: 'dependencies',
|
|
127
|
+
severity: 'high',
|
|
128
|
+
confidence: 'likely',
|
|
129
|
+
title: 'node_modules Not in .gitignore',
|
|
130
|
+
check({ files, stack }) {
|
|
131
|
+
const findings = [];
|
|
132
|
+
if (stack.runtime !== 'node') return findings;
|
|
133
|
+
|
|
134
|
+
const gitignore = files.get('.gitignore') || '';
|
|
135
|
+
if (!gitignore.includes('node_modules')) {
|
|
136
|
+
findings.push({
|
|
137
|
+
ruleId: 'DEPS-004', category: 'dependencies', severity: 'high',
|
|
138
|
+
title: 'node_modules not in .gitignore — may be committed to git',
|
|
139
|
+
description: 'Add node_modules to .gitignore. It should never be in version control.',
|
|
140
|
+
fix: {
|
|
141
|
+
type: 'insert',
|
|
142
|
+
position: 0,
|
|
143
|
+
content: 'node_modules/',
|
|
144
|
+
targetFile: '.gitignore',
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return findings;
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// DEPS-005: Wildcard version on security-critical package
|
|
153
|
+
{ id: 'DEPS-005', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'Wildcard Version on Security-Critical Package',
|
|
154
|
+
check({ stack }) {
|
|
155
|
+
const findings = [];
|
|
156
|
+
const securityPkgs = ['express', 'jsonwebtoken', 'bcrypt', 'bcryptjs', 'helmet', 'passport', 'crypto-js', 'node-forge', 'axios'];
|
|
157
|
+
for (const [pkg, version] of Object.entries(stack.dependencies || {})) {
|
|
158
|
+
if (securityPkgs.includes(pkg) && (version === '*' || version === 'latest' || version === 'x')) {
|
|
159
|
+
findings.push({ ruleId: 'DEPS-005', category: 'dependencies', severity: 'high',
|
|
160
|
+
title: `Security-critical package "${pkg}" uses wildcard version "${version}"`,
|
|
161
|
+
description: 'Pin security-critical packages to exact versions. Wildcards can silently pull in compromised versions.', fix: null });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return findings;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// DEPS-006: No automated dependency updates
|
|
169
|
+
{ id: 'DEPS-006', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'No Automated Dependency Updates',
|
|
170
|
+
check({ files }) {
|
|
171
|
+
const hasDependabot = [...files.keys()].some(f => f.includes('dependabot.yml') || f.includes('.dependabot'));
|
|
172
|
+
const hasRenovate = [...files.keys()].some(f => f.includes('renovate.json') || f.includes('.renovaterc'));
|
|
173
|
+
if (!hasDependabot && !hasRenovate) {
|
|
174
|
+
return [{ ruleId: 'DEPS-006', category: 'dependencies', severity: 'medium',
|
|
175
|
+
title: 'No automated dependency update tool (Dependabot/Renovate) configured',
|
|
176
|
+
description: 'Add Dependabot or Renovate to automatically receive security patches. Without automation, outdated deps accumulate.', fix: null }];
|
|
177
|
+
}
|
|
178
|
+
return [];
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// DEPS-007: GPL license in commercial project
|
|
183
|
+
{ id: 'DEPS-007', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'GPL-Licensed Dependency',
|
|
184
|
+
check({ stack }) {
|
|
185
|
+
const findings = [];
|
|
186
|
+
const gplPackages = ['gpl-', 'gnu-', 'ffmpeg', 'gstreamer'];
|
|
187
|
+
for (const pkg of Object.keys(stack.dependencies || {})) {
|
|
188
|
+
if (gplPackages.some(g => pkg.toLowerCase().startsWith(g))) {
|
|
189
|
+
findings.push({ ruleId: 'DEPS-007', category: 'dependencies', severity: 'high',
|
|
190
|
+
title: `Potentially GPL-licensed package: ${pkg}`,
|
|
191
|
+
description: 'GPL requires your entire application to be open-sourced under GPL. Verify the license and consult legal if building commercial software.', fix: null });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return findings;
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// DEPS-008: Dependency installed from git URL
|
|
199
|
+
{ id: 'DEPS-008', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'Dependency from Git URL',
|
|
200
|
+
check({ stack }) {
|
|
201
|
+
const findings = [];
|
|
202
|
+
for (const [pkg, version] of Object.entries({ ...stack.dependencies, ...stack.devDependencies })) {
|
|
203
|
+
if (typeof version === 'string' && (version.startsWith('github:') || version.startsWith('git+') ||
|
|
204
|
+
version.startsWith('git://') || version.startsWith('bitbucket:'))) {
|
|
205
|
+
findings.push({ ruleId: 'DEPS-008', category: 'dependencies', severity: 'high',
|
|
206
|
+
title: `Package "${pkg}" installed from git URL — no integrity verification`,
|
|
207
|
+
description: 'Git URL dependencies bypass npm integrity checks and can change silently. Use published npm versions.', fix: null });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return findings;
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// DEPS-009: Full lodash imported
|
|
215
|
+
{ id: 'DEPS-009', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Full Lodash Imported',
|
|
216
|
+
check({ stack }) {
|
|
217
|
+
const findings = [];
|
|
218
|
+
if ('lodash' in (stack.dependencies || {})) {
|
|
219
|
+
findings.push({ ruleId: 'DEPS-009', category: 'dependencies', severity: 'medium',
|
|
220
|
+
title: 'Full lodash in production dependencies adds ~70KB to bundle',
|
|
221
|
+
description: "Import individual functions (import debounce from 'lodash/debounce') or use lodash-es with tree shaking.", fix: null });
|
|
222
|
+
}
|
|
223
|
+
return findings;
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// DEPS-010: moment.js in dependencies
|
|
228
|
+
{ id: 'DEPS-010', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'moment.js — Heavy Deprecated Library',
|
|
229
|
+
check({ stack }) {
|
|
230
|
+
const findings = [];
|
|
231
|
+
if ('moment' in (stack.dependencies || {})) {
|
|
232
|
+
findings.push({ ruleId: 'DEPS-010', category: 'dependencies', severity: 'medium',
|
|
233
|
+
title: 'moment.js adds 67KB+ and is in maintenance-only mode',
|
|
234
|
+
description: "Replace with date-fns (tree-shakeable) or dayjs (2KB). moment.js is no longer actively developed.", fix: null });
|
|
235
|
+
}
|
|
236
|
+
return findings;
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
// DEPS-011: request package (deprecated)
|
|
241
|
+
{ id: 'DEPS-011', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Deprecated request Package',
|
|
242
|
+
check({ stack }) {
|
|
243
|
+
if ('request' in (stack.dependencies || {})) {
|
|
244
|
+
return [{ ruleId: 'DEPS-011', category: 'dependencies', severity: 'medium',
|
|
245
|
+
title: 'request package is deprecated and unmaintained',
|
|
246
|
+
description: "Replace with built-in fetch, axios, or got. The request package has been deprecated since 2020.", fix: null }];
|
|
247
|
+
}
|
|
248
|
+
return [];
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// DEPS-012: devDependency accidentally in dependencies
|
|
253
|
+
{ id: 'DEPS-012', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Testing Library in Production Dependencies',
|
|
254
|
+
check({ stack }) {
|
|
255
|
+
const findings = [];
|
|
256
|
+
const testOnlyPkgs = ['jest', 'mocha', 'chai', 'sinon', 'jasmine', 'vitest', 'cypress', 'playwright',
|
|
257
|
+
'supertest', 'nock', 'faker', '@faker-js/faker', 'ts-jest', 'babel-jest'];
|
|
258
|
+
for (const pkg of Object.keys(stack.dependencies || {})) {
|
|
259
|
+
if (testOnlyPkgs.includes(pkg) || testOnlyPkgs.some(t => pkg.startsWith(`@${t}/`) || pkg.startsWith(`${t}-`))) {
|
|
260
|
+
findings.push({ ruleId: 'DEPS-012', category: 'dependencies', severity: 'medium',
|
|
261
|
+
title: `Test-only package "${pkg}" in production dependencies`,
|
|
262
|
+
description: 'Move test frameworks to devDependencies. They bloat production installs and may expose test infrastructure.', fix: null });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return findings;
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
// DEPS-013: No bundle size budget
|
|
270
|
+
{ id: 'DEPS-013', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'No Bundle Size Budget',
|
|
271
|
+
check({ files, stack }) {
|
|
272
|
+
const findings = [];
|
|
273
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
274
|
+
const hasBudget = 'bundlesize' in allDeps || 'size-limit' in allDeps || '@size-limit/preset-app' in allDeps ||
|
|
275
|
+
[...files.keys()].some(f => f.includes('.bundlesize') || f.includes('size-limit'));
|
|
276
|
+
if (!hasBudget && [...files.keys()].some(f => f.match(/\.(jsx|tsx)$/))) {
|
|
277
|
+
findings.push({ ruleId: 'DEPS-013', category: 'dependencies', severity: 'low',
|
|
278
|
+
title: 'No bundle size budget configured',
|
|
279
|
+
description: 'Add size-limit to fail CI when bundle size exceeds budget. Unmonitored bundles grow silently.', fix: null });
|
|
280
|
+
}
|
|
281
|
+
return findings;
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// DEPS-014: Multiple HTTP client libraries
|
|
286
|
+
{ id: 'DEPS-014', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Multiple HTTP Client Libraries',
|
|
287
|
+
check({ stack }) {
|
|
288
|
+
const findings = [];
|
|
289
|
+
const httpClients = ['axios', 'got', 'node-fetch', 'superagent', 'ky', 'wretch'];
|
|
290
|
+
const used = httpClients.filter(c => c in (stack.dependencies || {}) || c in (stack.devDependencies || {}));
|
|
291
|
+
if (used.length >= 2) {
|
|
292
|
+
findings.push({ ruleId: 'DEPS-014', category: 'dependencies', severity: 'low',
|
|
293
|
+
title: `Multiple HTTP client libraries: ${used.join(', ')}`,
|
|
294
|
+
description: 'Standardize on one HTTP client. Multiple clients bloat the bundle and confuse contributors.', fix: null });
|
|
295
|
+
}
|
|
296
|
+
return findings;
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// DEPS-015: No SRI for CDN scripts
|
|
301
|
+
{ id: 'DEPS-015', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'CDN Scripts Without Subresource Integrity',
|
|
302
|
+
check({ files }) {
|
|
303
|
+
const findings = [];
|
|
304
|
+
for (const [fp, c] of files) {
|
|
305
|
+
if (!fp.match(/\.(html)$/) && !fp.match(/_document\.(jsx|tsx)$/)) continue;
|
|
306
|
+
const lines = c.split('\n');
|
|
307
|
+
for (let i = 0; i < lines.length; i++) {
|
|
308
|
+
if (lines[i].match(/<script[^>]+src=["']https?:\/\//i) && !lines[i].match(/integrity=/i)) {
|
|
309
|
+
findings.push({ ruleId: 'DEPS-015', category: 'dependencies', severity: 'high',
|
|
310
|
+
title: 'CDN script without Subresource Integrity (SRI) hash',
|
|
311
|
+
description: 'Add integrity="sha384-..." to CDN <script> tags. Without SRI, a compromised CDN can serve malicious code.', file: fp, line: i + 1, fix: null });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return findings;
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
// DEPS-016: Package with known typosquatting name
|
|
320
|
+
{ id: 'DEPS-016', category: 'dependencies', severity: 'critical', confidence: 'definite', title: 'Suspected Typosquatting Package',
|
|
321
|
+
check({ stack }) {
|
|
322
|
+
const findings = [];
|
|
323
|
+
const typosquats = { 'crossenv': 'cross-env', 'lodash-express': 'lodash', 'expres': 'express', 'requets': 'request', 'coloers': 'colors', 'mongoos': 'mongoose', 'exress': 'express', 'node-uuid': 'uuid', 'nodemon-alternative': 'nodemon', 'react-devtools-core2': 'react-devtools-core' };
|
|
324
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
325
|
+
for (const [dep, intended] of Object.entries(typosquats)) {
|
|
326
|
+
if (dep in allDeps) {
|
|
327
|
+
findings.push({ ruleId: 'DEPS-016', category: 'dependencies', severity: 'critical', title: `Suspected typosquatting: '${dep}' (intended: '${intended}')`, description: `Remove '${dep}' and install '${intended}'. Typosquatted packages can contain malware. Run npm audit and verify package before use.`, fix: null });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return findings;
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
// DEPS-017: Using deprecated crypto package
|
|
335
|
+
{ id: 'DEPS-017', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'Deprecated Cryptography Package',
|
|
336
|
+
check({ stack }) {
|
|
337
|
+
const findings = [];
|
|
338
|
+
const deprecated = { 'node-forge': 'Use Node.js built-in crypto or @noble/* libraries', 'crypto-js': 'Use Web Crypto API or Node.js built-in crypto', 'sjcl': 'Use Web Crypto API', 'jsencrypt': 'Use node:crypto or @noble/rsa', 'bcryptjs': 'Consider bcrypt (native) or argon2 for better security' };
|
|
339
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
340
|
+
for (const [dep, msg] of Object.entries(deprecated)) {
|
|
341
|
+
if (dep in allDeps) {
|
|
342
|
+
findings.push({ ruleId: 'DEPS-017', category: 'dependencies', severity: 'high', title: `Deprecated crypto package: ${dep}`, description: msg, fix: null });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return findings;
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// DEPS-018: No package-lock.json integrity in CI
|
|
350
|
+
{ id: 'DEPS-018', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'CI Uses npm install Instead of npm ci',
|
|
351
|
+
check({ files }) {
|
|
352
|
+
const findings = [];
|
|
353
|
+
for (const [fp, c] of files) {
|
|
354
|
+
if (!fp.match(/\.ya?ml$|Makefile|Dockerfile/i)) continue;
|
|
355
|
+
if (c.match(/npm\s+install(?!\s+-g|\s+--global)/) && !c.match(/npm\s+ci\b/)) {
|
|
356
|
+
findings.push({ ruleId: 'DEPS-018', category: 'dependencies', severity: 'high', title: 'CI uses npm install instead of npm ci — lockfile not enforced', description: 'Use npm ci in CI. It installs exact versions from package-lock.json and fails if lockfile is out of sync.', file: fp, fix: null });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return findings;
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
// DEPS-019: Duplicate packages with different major versions
|
|
364
|
+
{ id: 'DEPS-019', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Peer Dependency Conflicts',
|
|
365
|
+
check({ stack }) {
|
|
366
|
+
const findings = [];
|
|
367
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
368
|
+
const reactVersion = allDeps['react'];
|
|
369
|
+
const reactDomVersion = allDeps['react-dom'];
|
|
370
|
+
if (reactVersion && reactDomVersion && reactVersion !== reactDomVersion) {
|
|
371
|
+
findings.push({ ruleId: 'DEPS-019', category: 'dependencies', severity: 'low', title: `react@${reactVersion} and react-dom@${reactDomVersion} version mismatch`, description: 'react and react-dom must be the same version. Mismatches cause cryptic runtime errors.', fix: null });
|
|
372
|
+
}
|
|
373
|
+
return findings;
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// DEPS-020: Test libraries in production dependencies
|
|
378
|
+
{ id: 'DEPS-020', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Testing Library in Production Dependencies',
|
|
379
|
+
check({ stack }) {
|
|
380
|
+
const findings = [];
|
|
381
|
+
const testLibs = ['jest', 'mocha', 'chai', 'sinon', 'supertest', 'nock', 'vitest', '@testing-library/react', 'cypress', 'playwright', 'ava', 'tape'];
|
|
382
|
+
for (const lib of testLibs) {
|
|
383
|
+
if (lib in (stack.dependencies || {})) {
|
|
384
|
+
findings.push({ ruleId: 'DEPS-020', category: 'dependencies', severity: 'medium', title: `Test library '${lib}' in production dependencies`, description: `Move '${lib}' to devDependencies. Test libraries in production bloat the bundle and increase attack surface.`, fix: null });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return findings;
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
// DEPS-021: No license check in CI
|
|
392
|
+
{ id: 'DEPS-021', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'No Automated License Compliance Check',
|
|
393
|
+
check({ files, stack }) {
|
|
394
|
+
const findings = [];
|
|
395
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
396
|
+
const hasLicenseCheck = [...files.values()].some(c => c.match(/license-checker|licensee|fossa|snyk.*license|license.*audit/i));
|
|
397
|
+
if (Object.keys(allDeps).length > 20 && !hasLicenseCheck) {
|
|
398
|
+
findings.push({ ruleId: 'DEPS-021', category: 'dependencies', severity: 'medium', title: 'No license compliance scanning — GPL/AGPL dependencies may create legal obligations', description: 'Add license-checker to CI: npx license-checker --failOn GPL. GPL/AGPL in production code requires open-sourcing your application.', fix: null });
|
|
399
|
+
}
|
|
400
|
+
return findings;
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// DEPS-022: node_modules committed to git
|
|
405
|
+
{ id: 'DEPS-022', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'node_modules May Be Committed to Git',
|
|
406
|
+
check({ files }) {
|
|
407
|
+
const findings = [];
|
|
408
|
+
const hasNodeModules = [...files.keys()].some(f => f.includes('node_modules/'));
|
|
409
|
+
const hasGitignore = [...files.keys()].some(f => f.endsWith('.gitignore'));
|
|
410
|
+
if (hasNodeModules) {
|
|
411
|
+
findings.push({ ruleId: 'DEPS-022', category: 'dependencies', severity: 'high', title: 'node_modules directory committed to git repository', description: 'Add node_modules to .gitignore immediately. Committed node_modules bloat repo size and prevent automatic updates via Dependabot.', fix: null });
|
|
412
|
+
} else if (!hasGitignore) {
|
|
413
|
+
findings.push({ ruleId: 'DEPS-022', category: 'dependencies', severity: 'medium', title: 'No .gitignore file — node_modules may be accidentally committed', description: 'Add .gitignore with node_modules/ entry. Without it, npm install output could be committed.', fix: null });
|
|
414
|
+
}
|
|
415
|
+
return findings;
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
// DEPS-023: Requiring entire package instead of submodule
|
|
420
|
+
{ id: 'DEPS-023', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Importing Entire Library Instead of Submodule',
|
|
421
|
+
check({ files }) {
|
|
422
|
+
const findings = [];
|
|
423
|
+
for (const [fp, c] of files) {
|
|
424
|
+
if (!isSourceFile(fp)) continue;
|
|
425
|
+
const heavyImports = [
|
|
426
|
+
{ pattern: /require\(['"]lodash['"]\)|import\s+\w+\s+from\s+['"]lodash['"]/, fix: "import get from 'lodash/get'" },
|
|
427
|
+
{ pattern: /require\(['"]rxjs['"]\)|import\s+\*\s+from\s+['"]rxjs['"]/, fix: "import { map } from 'rxjs/operators'" },
|
|
428
|
+
{ pattern: /require\(['"]date-fns['"]\)|import\s+\*\s+from\s+['"]date-fns['"]/, fix: "import { format } from 'date-fns'" },
|
|
429
|
+
];
|
|
430
|
+
for (const { pattern, fix } of heavyImports) {
|
|
431
|
+
if (c.match(pattern)) {
|
|
432
|
+
findings.push({ ruleId: 'DEPS-023', category: 'dependencies', severity: 'medium', title: 'Full library imported instead of specific function', description: `Import only what you need: ${fix}. Full imports include unused code, increasing bundle size.`, file: fp, fix: null });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return findings;
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
// DEPS-024: No automated vulnerability scanning in CI
|
|
441
|
+
{ id: 'DEPS-024', category: 'dependencies', severity: 'high', confidence: 'definite', title: 'No Vulnerability Scanning in CI Pipeline',
|
|
442
|
+
check({ files }) {
|
|
443
|
+
const findings = [];
|
|
444
|
+
const hasScan = [...files.values()].some(c => c.match(/npm\s+audit|snyk\s+test|yarn\s+audit|trivy\s+fs|grype/i));
|
|
445
|
+
if (!hasScan) {
|
|
446
|
+
findings.push({ ruleId: 'DEPS-024', category: 'dependencies', severity: 'high', title: 'No dependency vulnerability scan in CI', description: 'Add npm audit --audit-level=high or snyk test to CI pipeline. Unscanned dependencies may have known CVEs.', fix: null });
|
|
447
|
+
}
|
|
448
|
+
return findings;
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
// DEPS-025: Using node: protocol missing for built-in modules
|
|
453
|
+
{ id: 'DEPS-025', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Node Built-ins Without node: Protocol',
|
|
454
|
+
check({ files }) {
|
|
455
|
+
const findings = [];
|
|
456
|
+
const builtins = ['fs', 'path', 'crypto', 'http', 'https', 'os', 'util', 'stream', 'buffer', 'child_process', 'events', 'net', 'readline'];
|
|
457
|
+
for (const [fp, c] of files) {
|
|
458
|
+
if (!isSourceFile(fp)) continue;
|
|
459
|
+
for (const mod of builtins) {
|
|
460
|
+
if (c.match(new RegExp(`require\\(['"]${mod}['"]\\)|from\\s+['"]${mod}['"]`)) && !c.match(new RegExp(`require\\(['"]node:${mod}['"]\\)|from\\s+['"]node:${mod}['"]`))) {
|
|
461
|
+
findings.push({ ruleId: 'DEPS-025', category: 'dependencies', severity: 'low', title: `Importing '${mod}' without node: protocol prefix`, description: `Use 'node:${mod}' to unambiguously identify built-in modules. Prevents shadowing by npm packages with the same name.`, file: fp, fix: null });
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return findings;
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
// DEPS-026: AGPL licensed package in commercial code
|
|
471
|
+
{ id: 'DEPS-026', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'AGPL Package in Commercial Project',
|
|
472
|
+
check({ stack }) {
|
|
473
|
+
const findings = [];
|
|
474
|
+
const agplPackages = { 'uplot': 'LGPL/GPL', 'ghostscript': 'AGPL', 'mongodb': 'SSPL', 'elasticsearch': 'SSPL/Elastic License' };
|
|
475
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
476
|
+
for (const [dep, license] of Object.entries(agplPackages)) {
|
|
477
|
+
if (dep in allDeps) {
|
|
478
|
+
findings.push({ ruleId: 'DEPS-026', category: 'dependencies', severity: 'high', title: `${dep} uses ${license} — may require open-sourcing your application`, description: `Review ${dep} license terms. AGPL/SSPL may require publishing your source code if you offer the software as a service.`, fix: null });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return findings;
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
// DEPS-027: engines field missing in package.json
|
|
486
|
+
{ id: 'DEPS-027', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'No Node.js Engine Version Specified',
|
|
487
|
+
check({ files }) {
|
|
488
|
+
const findings = [];
|
|
489
|
+
for (const [fp, c] of files) {
|
|
490
|
+
if (!fp.endsWith('package.json') || fp.includes('node_modules')) continue;
|
|
491
|
+
try {
|
|
492
|
+
const pkg = JSON.parse(c);
|
|
493
|
+
if (!pkg.engines || !pkg.engines.node) {
|
|
494
|
+
findings.push({ ruleId: 'DEPS-027', category: 'dependencies', severity: 'low', title: 'package.json missing "engines" field — Node.js version not specified', description: 'Add "engines": { "node": ">=20.0.0" } to package.json. This prevents accidental deployment to incompatible Node versions.', file: fp, fix: null });
|
|
495
|
+
}
|
|
496
|
+
} catch {}
|
|
497
|
+
}
|
|
498
|
+
return findings;
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
// DEPS-028: Peer dependency not installed
|
|
503
|
+
{ id: 'DEPS-028', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Missing Peer Dependencies',
|
|
504
|
+
check({ stack }) {
|
|
505
|
+
const findings = [];
|
|
506
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
507
|
+
const peerMap = { '@testing-library/react': 'react', '@apollo/client': 'graphql', 'react-redux': 'redux', 'formik': 'react', 'react-hook-form': 'react', 'styled-components': 'react' };
|
|
508
|
+
for (const [lib, peer] of Object.entries(peerMap)) {
|
|
509
|
+
if (lib in allDeps && !(peer in allDeps)) {
|
|
510
|
+
findings.push({ ruleId: 'DEPS-028', category: 'dependencies', severity: 'medium', title: `${lib} installed but peer dependency '${peer}' is missing`, description: `Install '${peer}': it is a required peer dependency for ${lib}.`, fix: null });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return findings;
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// DEPS-029: package.json with no description or author
|
|
518
|
+
{ id: 'DEPS-029', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'package.json Missing Metadata',
|
|
519
|
+
check({ files }) {
|
|
520
|
+
const findings = [];
|
|
521
|
+
for (const [fp, c] of files) {
|
|
522
|
+
if (!fp.endsWith('package.json') || fp.includes('node_modules')) continue;
|
|
523
|
+
try {
|
|
524
|
+
const pkg = JSON.parse(c);
|
|
525
|
+
if (!pkg.description || !pkg.author) {
|
|
526
|
+
findings.push({ ruleId: 'DEPS-029', category: 'dependencies', severity: 'low', title: 'package.json missing description or author fields', description: 'Add description and author. These are required for npm publishing and help developers understand the package purpose.', file: fp, fix: null });
|
|
527
|
+
}
|
|
528
|
+
} catch {}
|
|
529
|
+
}
|
|
530
|
+
return findings;
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
// DEPS-030: No .nvmrc or .node-version file
|
|
535
|
+
{ id: 'DEPS-030', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'No Node.js Version File (.nvmrc)',
|
|
536
|
+
check({ files }) {
|
|
537
|
+
const findings = [];
|
|
538
|
+
const hasNvmrc = [...files.keys()].some(f => f.endsWith('.nvmrc') || f.endsWith('.node-version'));
|
|
539
|
+
if (!hasNvmrc) {
|
|
540
|
+
findings.push({ ruleId: 'DEPS-030', category: 'dependencies', severity: 'low', title: 'No .nvmrc or .node-version file — Node.js version not pinned for local development', description: 'Add .nvmrc with exact Node.js version (e.g., 20.11.0). Ensures all developers and CI use the same Node version.', fix: null });
|
|
541
|
+
}
|
|
542
|
+
return findings;
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
// DEPS-031: Inconsistent package manager lockfiles
|
|
547
|
+
{ id: 'DEPS-031', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Multiple Package Manager Lockfiles',
|
|
548
|
+
check({ files }) {
|
|
549
|
+
const findings = [];
|
|
550
|
+
const lockfiles = [...files.keys()].filter(f => f.match(/package-lock\.json|yarn\.lock|pnpm-lock\.yaml/));
|
|
551
|
+
if (lockfiles.length > 1) {
|
|
552
|
+
findings.push({ ruleId: 'DEPS-031', category: 'dependencies', severity: 'medium', title: `Multiple lockfiles found: ${lockfiles.join(', ')} — use only one package manager`, description: 'Commit to one package manager. Multiple lockfiles cause inconsistent installs. Add .npmrc with engine-strict=true to enforce.', fix: null });
|
|
553
|
+
}
|
|
554
|
+
return findings;
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
// DEPS-032: Using require() in ES module context
|
|
559
|
+
{ id: 'DEPS-032', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Mixed CommonJS and ES Modules',
|
|
560
|
+
check({ files }) {
|
|
561
|
+
const findings = [];
|
|
562
|
+
for (const [fp, c] of files) {
|
|
563
|
+
if (!fp.match(/\.mjs$|\.cjs$/) && !fp.includes('.')) continue;
|
|
564
|
+
if (fp.endsWith('.mjs') && c.match(/require\(/)) {
|
|
565
|
+
findings.push({ ruleId: 'DEPS-032', category: 'dependencies', severity: 'medium', title: 'require() used in .mjs ES module file', description: 'Use import/export in .mjs files. require() is not available in ES modules — this causes a ReferenceError at runtime.', file: fp, fix: null });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return findings;
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
// DEPS-033: package.json with no main/exports field
|
|
573
|
+
{ id: 'DEPS-033', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Package Missing "exports" Field',
|
|
574
|
+
check({ files }) {
|
|
575
|
+
const findings = [];
|
|
576
|
+
for (const [fp, c] of files) {
|
|
577
|
+
if (!fp.endsWith('package.json') || fp.includes('node_modules')) continue;
|
|
578
|
+
try {
|
|
579
|
+
const pkg = JSON.parse(c);
|
|
580
|
+
if (pkg.name && !pkg.private && !pkg.exports && !pkg.main) {
|
|
581
|
+
findings.push({ ruleId: 'DEPS-033', category: 'dependencies', severity: 'low', title: 'Published package missing "exports" field — consumers access internals', description: 'Add "exports" to package.json to define public API. Without it, consumers can import internal modules you may refactor.', file: fp, fix: null });
|
|
582
|
+
}
|
|
583
|
+
} catch {}
|
|
584
|
+
}
|
|
585
|
+
return findings;
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
// DEPS-034: Nested node_modules risk (phantom dependencies)
|
|
590
|
+
{ id: 'DEPS-034', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Using Phantom (Implicit) Dependency',
|
|
591
|
+
check({ files, stack }) {
|
|
592
|
+
const findings = [];
|
|
593
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
594
|
+
for (const [fp, c] of files) {
|
|
595
|
+
if (!isSourceFile(fp)) continue;
|
|
596
|
+
const matches = c.matchAll(/require\(['"]([a-z@][^'"./]+)['"]\)|from\s+['"]([a-z@][^'"./]+)['"]/g);
|
|
597
|
+
for (const m of matches) {
|
|
598
|
+
const pkg = m[1] || m[2];
|
|
599
|
+
if (pkg && !pkg.startsWith('node:') && !(pkg in allDeps)) {
|
|
600
|
+
const isBuiltin = ['fs', 'path', 'http', 'https', 'crypto', 'os', 'util', 'stream', 'buffer', 'events', 'net', 'url', 'child_process', 'readline'].includes(pkg);
|
|
601
|
+
if (!isBuiltin) {
|
|
602
|
+
findings.push({ ruleId: 'DEPS-034', category: 'dependencies', severity: 'medium', title: `'${pkg}' imported but not in package.json — phantom dependency`, description: `Add '${pkg}' to dependencies. Phantom dependencies work accidentally via hoisting but break on clean installs or in monorepos.`, file: fp, fix: null });
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return findings;
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
// DEPS-035: Security package pinned to exact version (no patches)
|
|
613
|
+
{ id: 'DEPS-035', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Security Package Pinned to Exact Version',
|
|
614
|
+
check({ stack }) {
|
|
615
|
+
const findings = [];
|
|
616
|
+
const securityPackages = ['helmet', 'express-rate-limit', 'bcrypt', 'argon2', 'jsonwebtoken', 'passport', 'csrf'];
|
|
617
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
618
|
+
for (const pkg of securityPackages) {
|
|
619
|
+
if (pkg in allDeps && /^\d/.test(allDeps[pkg])) {
|
|
620
|
+
findings.push({ ruleId: 'DEPS-035', category: 'dependencies', severity: 'medium', title: `Security package '${pkg}' pinned to exact version — won't receive patches`, description: `Use '~${allDeps[pkg]}' to allow patch updates. Security packages should receive patch updates automatically via Dependabot.`, fix: null });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return findings;
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
// DEPS-036: Development dependency used in production code
|
|
628
|
+
{ id: 'DEPS-036', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Development Dependency Used in Production Code',
|
|
629
|
+
check({ files, stack }) {
|
|
630
|
+
const findings = [];
|
|
631
|
+
const devOnlyPkgs = Object.keys(stack.devDependencies || {}).filter(d => !['typescript', '@types', 'ts-node'].some(p => d.startsWith(p)));
|
|
632
|
+
for (const [fp, c] of files) {
|
|
633
|
+
if (!isSourceFile(fp) || fp.includes('test') || fp.includes('spec')) continue;
|
|
634
|
+
for (const pkg of devOnlyPkgs) {
|
|
635
|
+
if (pkg.length > 3 && c.match(new RegExp(`require\\(['"]${pkg.replace(/[-@/]/g, '.')}['"]\\)|from\\s+['"]${pkg.replace(/[-@/]/g, '.')}['"]`))) {
|
|
636
|
+
findings.push({ ruleId: 'DEPS-036', category: 'dependencies', severity: 'medium', title: `devDependency '${pkg}' imported in production file`, description: `Move '${pkg}' to dependencies if used in production. devDependencies are not installed in production (npm install --production).`, file: fp, fix: null });
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return findings;
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
// DEPS-037: Missing peerDependencies in published package
|
|
646
|
+
{ id: 'DEPS-037', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Published Package Missing peerDependencies',
|
|
647
|
+
check({ files }) {
|
|
648
|
+
const findings = [];
|
|
649
|
+
for (const [fp, c] of files) {
|
|
650
|
+
if (!fp.endsWith('package.json') || fp.includes('node_modules')) continue;
|
|
651
|
+
try {
|
|
652
|
+
const pkg = JSON.parse(c);
|
|
653
|
+
if (!pkg.private && pkg.dependencies) {
|
|
654
|
+
const peers = ['react', 'vue', 'angular', 'svelte'];
|
|
655
|
+
for (const peer of peers) {
|
|
656
|
+
if (peer in pkg.dependencies && !pkg.peerDependencies?.[peer]) {
|
|
657
|
+
findings.push({ ruleId: 'DEPS-037', category: 'dependencies', severity: 'low', title: `Published package includes '${peer}' in dependencies instead of peerDependencies`, description: `Move '${peer}' to peerDependencies. Bundling framework dependencies causes duplicate instances and version conflicts.`, file: fp, fix: null });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
} catch {}
|
|
662
|
+
}
|
|
663
|
+
return findings;
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
// DEPS-038: Source maps published to npm
|
|
668
|
+
{ id: 'DEPS-038', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Source Maps Published to npm',
|
|
669
|
+
check({ files }) {
|
|
670
|
+
const findings = [];
|
|
671
|
+
for (const [fp, c] of files) {
|
|
672
|
+
if (!fp.endsWith('package.json') || fp.includes('node_modules')) continue;
|
|
673
|
+
try {
|
|
674
|
+
const pkg = JSON.parse(c);
|
|
675
|
+
const files_list = pkg.files || [];
|
|
676
|
+
if (files_list.some(f => f.match(/\.map$|maps\//))) {
|
|
677
|
+
findings.push({ ruleId: 'DEPS-038', category: 'dependencies', severity: 'medium', title: 'Source maps included in npm publish — exposes original source code', description: 'Remove .map files from package.json "files" array. Published source maps expose your original TypeScript/minified source to anyone.', file: fp, fix: null });
|
|
678
|
+
}
|
|
679
|
+
} catch {}
|
|
680
|
+
}
|
|
681
|
+
return findings;
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
// DEPS-039: Using forked/patched package without audit
|
|
686
|
+
{ id: 'DEPS-039', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Forked Package Without Security Note',
|
|
687
|
+
check({ files }) {
|
|
688
|
+
const findings = [];
|
|
689
|
+
for (const [fp, c] of files) {
|
|
690
|
+
if (!fp.endsWith('package.json') || fp.includes('node_modules')) continue;
|
|
691
|
+
try {
|
|
692
|
+
const pkg = JSON.parse(c);
|
|
693
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
694
|
+
for (const [dep, version] of Object.entries(allDeps)) {
|
|
695
|
+
if (typeof version === 'string' && version.match(/github:|bitbucket:|gitlab:/)) {
|
|
696
|
+
findings.push({ ruleId: 'DEPS-039', category: 'dependencies', severity: 'medium', title: `Package '${dep}' installed from git source — no integrity guarantee`, description: `Pin to a specific commit hash: "${dep}": "github:owner/repo#abc1234". Floating git refs can change without notice.`, file: fp, fix: null });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch {}
|
|
700
|
+
}
|
|
701
|
+
return findings;
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
// DEPS-040: High severity npm audit findings
|
|
706
|
+
{ id: 'DEPS-040', category: 'dependencies', severity: 'critical', confidence: 'definite', title: 'Known High Severity CVE in Dependency',
|
|
707
|
+
check({ stack }) {
|
|
708
|
+
const findings = [];
|
|
709
|
+
const knownVulnerable = {
|
|
710
|
+
'node-serialize': { versions: '*', cve: 'CVE-2017-5941', desc: 'Remote code execution via serialized object' },
|
|
711
|
+
'serialize-javascript': { versions: '<3.1.0', cve: 'CVE-2020-7660', desc: 'XSS via serialized regex' },
|
|
712
|
+
'lodash': { versions: '<4.17.21', cve: 'CVE-2021-23337', desc: 'Command injection and prototype pollution' },
|
|
713
|
+
'axios': { versions: '<0.21.1', cve: 'CVE-2020-28168', desc: 'SSRF via redirects' },
|
|
714
|
+
'jsonwebtoken': { versions: '<9.0.0', cve: 'CVE-2022-23529', desc: 'Arbitrary file write via crafted JWT' },
|
|
715
|
+
'qs': { versions: '<6.7.3', cve: 'CVE-2022-24999', desc: 'Prototype pollution' },
|
|
716
|
+
'semver': { versions: '<7.5.2', cve: 'CVE-2022-25883', desc: 'Regular expression denial of service' },
|
|
717
|
+
};
|
|
718
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
719
|
+
for (const [pkg, info] of Object.entries(knownVulnerable)) {
|
|
720
|
+
if (pkg in allDeps) {
|
|
721
|
+
findings.push({ ruleId: 'DEPS-040', category: 'dependencies', severity: 'critical', title: `${pkg} has known CVE: ${info.cve} — ${info.desc}`, description: `Update ${pkg} immediately. Run: npm audit fix. This vulnerability is actively exploited.`, fix: null });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return findings;
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
// DEPS-041: Using old Node crypto instead of webcrypto
|
|
729
|
+
{ id: 'DEPS-041', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Using Legacy Node.js crypto API',
|
|
730
|
+
check({ files }) {
|
|
731
|
+
const findings = [];
|
|
732
|
+
for (const [fp, c] of files) {
|
|
733
|
+
if (!isSourceFile(fp)) continue;
|
|
734
|
+
if (c.match(/require\(['"]crypto['"]\)|from\s+['"]crypto['"]/) && c.match(/createCipher\b|createDecipher\b/)) {
|
|
735
|
+
findings.push({ ruleId: 'DEPS-041', category: 'dependencies', severity: 'low', title: 'Using deprecated crypto.createCipher/createDecipher', description: 'Use crypto.createCipheriv/createDecipheriv with explicit IV. createCipher is deprecated and uses a weak key derivation. Migrate to the WebCrypto API or crypto.subtle.', file: fp, fix: null });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return findings;
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
// DEPS-042: Production app without lockfile
|
|
743
|
+
{ id: 'DEPS-042', category: 'dependencies', severity: 'critical', confidence: 'definite', title: 'No Lockfile in Repository',
|
|
744
|
+
check({ files }) {
|
|
745
|
+
const findings = [];
|
|
746
|
+
const hasPackageJson = [...files.keys()].some(f => f.endsWith('package.json') && !f.includes('node_modules'));
|
|
747
|
+
const hasLockfile = [...files.keys()].some(f => f.match(/package-lock\.json|yarn\.lock|pnpm-lock\.yaml/));
|
|
748
|
+
if (hasPackageJson && !hasLockfile) {
|
|
749
|
+
findings.push({ ruleId: 'DEPS-042', category: 'dependencies', severity: 'critical', title: 'No lockfile committed to repository — non-deterministic installs', description: 'Commit package-lock.json (or yarn.lock/pnpm-lock.yaml). Without a lockfile, npm install can install different versions on different machines and in CI.', fix: null });
|
|
750
|
+
}
|
|
751
|
+
return findings;
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
// DEPS-043: Using http instead of https for package registry
|
|
756
|
+
{ id: 'DEPS-043', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'Package Registry Using HTTP (Not HTTPS)',
|
|
757
|
+
check({ files }) {
|
|
758
|
+
const findings = [];
|
|
759
|
+
for (const [fp, c] of files) {
|
|
760
|
+
if (!fp.endsWith('.npmrc') && !fp.endsWith('.yarnrc') && !fp.endsWith('.yarnrc.yml')) continue;
|
|
761
|
+
if (c.match(/registry=http:\/\/(?!localhost)/i)) {
|
|
762
|
+
findings.push({ ruleId: 'DEPS-043', category: 'dependencies', severity: 'high', title: 'Package registry configured with HTTP — packages served without TLS', description: 'Change registry URL to HTTPS. HTTP registries allow MITM attacks to serve malicious packages. Use https://registry.npmjs.org.', file: fp, fix: null });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return findings;
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
// DEPS-044: Using deprecated xmlhttprequest in modern code
|
|
770
|
+
{ id: 'DEPS-044', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Using XMLHttpRequest Instead of Fetch',
|
|
771
|
+
check({ files }) {
|
|
772
|
+
const findings = [];
|
|
773
|
+
for (const [fp, c] of files) {
|
|
774
|
+
if (!isSourceFile(fp)) continue;
|
|
775
|
+
if (c.match(/new XMLHttpRequest\(\)|\.XMLHttpRequest/)) {
|
|
776
|
+
findings.push({ ruleId: 'DEPS-044', category: 'dependencies', severity: 'low', title: 'Using XMLHttpRequest — migrate to fetch() or axios', description: 'Replace XHR with fetch() or axios. XMLHttpRequest has a verbose API and lacks async/await support. fetch is available in all modern browsers and Node 18+.', file: fp, fix: null });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return findings;
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
|
|
783
|
+
// DEPS-045: Deprecated Node.js version in Dockerfile
|
|
784
|
+
{ id: 'DEPS-045', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'End-of-Life Node.js Version in Docker Image',
|
|
785
|
+
check({ files }) {
|
|
786
|
+
const findings = [];
|
|
787
|
+
for (const [fp, c] of files) {
|
|
788
|
+
if (!fp.endsWith('Dockerfile') && !fp.match(/Dockerfile\./)) continue;
|
|
789
|
+
const match = c.match(/FROM\s+node:(\d+)/i);
|
|
790
|
+
if (match) {
|
|
791
|
+
const version = parseInt(match[1]);
|
|
792
|
+
const eol = [10, 12, 14, 16, 17, 19, 21];
|
|
793
|
+
if (eol.includes(version)) {
|
|
794
|
+
findings.push({ ruleId: 'DEPS-045', category: 'dependencies', severity: 'high', title: `Node.js ${version} is end-of-life — no longer receives security patches`, description: `Upgrade to Node.js 20 LTS (latest LTS) or Node.js 22. EOL versions have known unpatched CVEs.`, file: fp, fix: null });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return findings;
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
|
|
802
|
+
// DEPS-046: Package score too low (maintenance risk)
|
|
803
|
+
{ id: 'DEPS-046', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Abandoned Package Dependency',
|
|
804
|
+
check({ stack }) {
|
|
805
|
+
const findings = [];
|
|
806
|
+
const abandonedPackages = ['request', 'request-promise', 'node-uuid', 'jade', 'grunt', 'bower', 'left-pad', 'coffee-script', 'gulp-cli'];
|
|
807
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
808
|
+
for (const pkg of abandonedPackages) {
|
|
809
|
+
if (pkg in allDeps) {
|
|
810
|
+
findings.push({ ruleId: 'DEPS-046', category: 'dependencies', severity: 'medium', title: `Abandoned package '${pkg}' — no longer maintained`, description: `Replace '${pkg}' with a maintained alternative. Unmaintained packages never receive security patches.`, fix: null });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return findings;
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
// DEPS-047: Multiple similar packages for same purpose
|
|
817
|
+
{ id: 'DEPS-047', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Duplicate Functionality Packages',
|
|
818
|
+
check({ stack }) {
|
|
819
|
+
const findings = [];
|
|
820
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
821
|
+
const httpClients = ['axios', 'got', 'node-fetch', 'superagent', 'request', 'undici', 'ky'].filter(d => d in allDeps);
|
|
822
|
+
const dateLibs = ['moment', 'dayjs', 'date-fns', 'luxon'].filter(d => d in allDeps);
|
|
823
|
+
const testRunners = ['jest', 'mocha', 'vitest', 'jasmine', 'ava', 'tape'].filter(d => d in allDeps);
|
|
824
|
+
if (httpClients.length > 1) findings.push({ ruleId: 'DEPS-047', category: 'dependencies', severity: 'low', title: `Multiple HTTP client libraries: ${httpClients.join(', ')} — choose one`, description: 'Standardize on one HTTP client library. Multiple clients with overlapping functionality increase bundle size and maintenance burden.', fix: null });
|
|
825
|
+
if (dateLibs.length > 1) findings.push({ ruleId: 'DEPS-047', category: 'dependencies', severity: 'low', title: `Multiple date libraries: ${dateLibs.join(', ')} — choose one`, description: 'Standardize on one date library. Date-fns is preferred for tree-shaking. Moment.js adds 230KB and is in maintenance mode.', fix: null });
|
|
826
|
+
if (testRunners.length > 1) findings.push({ ruleId: 'DEPS-047', category: 'dependencies', severity: 'low', title: `Multiple test runners: ${testRunners.join(', ')} — choose one`, description: 'Use a single test runner to avoid configuration complexity and incompatible mocking systems.', fix: null });
|
|
827
|
+
return findings;
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
// DEPS-048: No audit of new dependencies
|
|
831
|
+
{ id: 'DEPS-048', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'No Automated Dependency Audit in CI',
|
|
832
|
+
check({ files }) {
|
|
833
|
+
const findings = [];
|
|
834
|
+
const hasAudit = [...files.values()].some(c => c.match(/npm\s+audit|yarn\s+audit|pnpm\s+audit|snyk\s+test/i));
|
|
835
|
+
if (!hasAudit) {
|
|
836
|
+
findings.push({ ruleId: 'DEPS-048', category: 'dependencies', severity: 'medium', title: 'No automated dependency audit in CI pipeline', description: 'Add npm audit --audit-level=high to CI. Catches known vulnerabilities in dependencies before deployment.', fix: null });
|
|
837
|
+
}
|
|
838
|
+
return findings;
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
// DEPS-049: Webpack without externals for large packages
|
|
842
|
+
{ id: 'DEPS-049', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Webpack Bundle Includes Large Peer Dependencies',
|
|
843
|
+
check({ files, stack }) {
|
|
844
|
+
const findings = [];
|
|
845
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
846
|
+
const hasWebpack = 'webpack' in allDeps;
|
|
847
|
+
const largeDeps = ['lodash', 'moment', 'rxjs', 'jquery'].filter(d => d in allDeps);
|
|
848
|
+
if (hasWebpack && largeDeps.length > 0) {
|
|
849
|
+
for (const [fp, c] of files) {
|
|
850
|
+
if (fp.match(/webpack\.config\./)) {
|
|
851
|
+
if (!c.match(/externals:/i)) {
|
|
852
|
+
findings.push({ ruleId: 'DEPS-049', category: 'dependencies', severity: 'low', title: `Large packages bundled: ${largeDeps.join(', ')} — consider externals`, description: 'Mark large packages as webpack externals if served via CDN. Reduces bundle size for library authors by not duplicating large deps.', file: fp, fix: null });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return findings;
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
// DEPS-050: workspace package version drift
|
|
861
|
+
{ id: 'DEPS-050', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Monorepo Package Version Inconsistencies',
|
|
862
|
+
check({ files }) {
|
|
863
|
+
const findings = [];
|
|
864
|
+
const packageFiles = [...files.entries()].filter(([fp]) => fp.endsWith('package.json') && !fp.includes('node_modules'));
|
|
865
|
+
if (packageFiles.length < 2) return findings;
|
|
866
|
+
const versions = new Map();
|
|
867
|
+
for (const [fp, c] of packageFiles) {
|
|
868
|
+
try {
|
|
869
|
+
const pkg = JSON.parse(c);
|
|
870
|
+
for (const [dep, ver] of Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
871
|
+
if (!versions.has(dep)) versions.set(dep, []);
|
|
872
|
+
versions.get(dep).push({ file: fp, version: ver });
|
|
873
|
+
}
|
|
874
|
+
} catch {}
|
|
875
|
+
}
|
|
876
|
+
for (const [dep, entries] of versions) {
|
|
877
|
+
const uniqueVersions = new Set(entries.map(e => e.version));
|
|
878
|
+
if (uniqueVersions.size > 1 && ['react', 'typescript', 'webpack', '@nestjs/core'].includes(dep)) {
|
|
879
|
+
findings.push({ ruleId: 'DEPS-050', category: 'dependencies', severity: 'medium', title: `Monorepo: '${dep}' has ${uniqueVersions.size} different versions`, description: `Align ${dep} version across all packages. Version drift causes subtle compatibility bugs and duplicated bundle code.`, fix: null });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return findings;
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
// DEPS-051: Using require() for ES module packages
|
|
886
|
+
{ id: 'DEPS-051', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'require() Used to Import ESM-Only Package',
|
|
887
|
+
check({ files, stack }) {
|
|
888
|
+
const findings = [];
|
|
889
|
+
const esmOnlyPackages = ['chalk', 'ora', 'p-limit', 'execa', 'node-fetch', 'got', 'sindresorhus'];
|
|
890
|
+
for (const [fp, c] of files) {
|
|
891
|
+
if (!isSourceFile(fp)) continue;
|
|
892
|
+
const lines = c.split('\n');
|
|
893
|
+
for (let i = 0; i < lines.length; i++) {
|
|
894
|
+
for (const pkg of esmOnlyPackages) {
|
|
895
|
+
if (lines[i].match(new RegExp(`require\\s*\\(['"\`]${pkg}['"\`]\\)`))) {
|
|
896
|
+
if (pkg in (stack.dependencies || {}) || pkg in (stack.devDependencies || {})) {
|
|
897
|
+
findings.push({ ruleId: 'DEPS-051', category: 'dependencies', severity: 'medium', title: `require('${pkg}') — this package is ESM-only from v5+`, description: `Use dynamic import() instead of require() for ${pkg}. ESM-only packages cannot be loaded via CommonJS require().`, file: fp, line: i + 1, fix: null });
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return findings;
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
// DEPS-052: No .npmrc with security settings
|
|
907
|
+
{ id: 'DEPS-052', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'No .npmrc With Security Hardening',
|
|
908
|
+
check({ files }) {
|
|
909
|
+
const findings = [];
|
|
910
|
+
const npmrc = [...files.entries()].find(([f]) => f.match(/\.npmrc$/));
|
|
911
|
+
if (!npmrc) {
|
|
912
|
+
findings.push({ ruleId: 'DEPS-052', category: 'dependencies', severity: 'medium', title: 'No .npmrc file — missing npm security hardening', description: 'Create .npmrc with: ignore-scripts=false, audit=true, fund=false, save-exact=true. These settings prevent malicious postinstall scripts and enforce exact version pinning.', fix: null });
|
|
913
|
+
} else {
|
|
914
|
+
const content = npmrc[1];
|
|
915
|
+
if (!content.match(/save-exact\s*=\s*true/)) {
|
|
916
|
+
findings.push({ ruleId: 'DEPS-052', category: 'dependencies', severity: 'medium', title: '.npmrc without save-exact=true — installed versions may differ across environments', description: 'Add save-exact=true to .npmrc to pin all newly installed packages to exact versions. This prevents accidental minor version upgrades that introduce breaking changes.', file: npmrc[0], fix: null });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return findings;
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
// DEPS-053: Installing packages with --unsafe-perm
|
|
923
|
+
{ id: 'DEPS-053', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'npm Install with --unsafe-perm Flag',
|
|
924
|
+
check({ files }) {
|
|
925
|
+
const findings = [];
|
|
926
|
+
for (const [fp, c] of files) {
|
|
927
|
+
if (!fp.match(/Dockerfile[^/]*$/) && !isCIFile(fp) && !fp.match(/Makefile|\.sh$/)) continue;
|
|
928
|
+
const lines = c.split('\n');
|
|
929
|
+
for (let i = 0; i < lines.length; i++) {
|
|
930
|
+
if (lines[i].match(/npm.*install.*--unsafe-perm|--unsafe-perm.*npm.*install/i)) {
|
|
931
|
+
findings.push({ ruleId: 'DEPS-053', category: 'dependencies', severity: 'high', title: 'npm install with --unsafe-perm — allows scripts to run as root', description: 'Remove --unsafe-perm. This flag allows npm lifecycle scripts to run with root privileges, enabling malicious packages to take full system control.', file: fp, line: i + 1, fix: null });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return findings;
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
// DEPS-054: Dependency with known malicious version
|
|
939
|
+
{ id: 'DEPS-054', category: 'dependencies', severity: 'critical', confidence: 'definite', title: 'Known Malicious Package Version in Dependencies',
|
|
940
|
+
check({ stack }) {
|
|
941
|
+
const findings = [];
|
|
942
|
+
const malicious = { 'event-stream': '3.3.6', 'bootstrap-sass': '3.4.1', 'eslint-scope': '3.7.2', 'getcookies': '1.0.0', 'crossenv': '*' };
|
|
943
|
+
for (const [pkg, ver] of Object.entries(malicious)) {
|
|
944
|
+
const installedVer = stack.dependencies?.[pkg] || stack.devDependencies?.[pkg];
|
|
945
|
+
if (installedVer) {
|
|
946
|
+
findings.push({ ruleId: 'DEPS-054', category: 'dependencies', severity: 'critical', title: `'${pkg}' was involved in a supply chain attack`, description: `${pkg} (${installedVer}) has a version history involving supply chain compromise. Audit your dependency tree and consider using a different package.`, fix: null });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return findings;
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
// DEPS-055: No package integrity verification
|
|
953
|
+
{ id: 'DEPS-055', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Missing Package Integrity Verification (No Lockfile or Integrity Checks)',
|
|
954
|
+
check({ files }) {
|
|
955
|
+
const findings = [];
|
|
956
|
+
const hasLockfile = [...files.keys()].some(f => f.match(/package-lock\.json|yarn\.lock|pnpm-lock\.yaml$/));
|
|
957
|
+
const hasPackageJson = [...files.keys()].some(f => f.match(/^package\.json$/));
|
|
958
|
+
if (hasPackageJson && !hasLockfile) {
|
|
959
|
+
findings.push({ ruleId: 'DEPS-055', category: 'dependencies', severity: 'medium', title: 'No lockfile committed — package integrity cannot be verified', description: 'Commit your lockfile (package-lock.json or yarn.lock). Without a lockfile, npm install resolves different versions each time, breaking reproducibility and making supply chain attacks easier.', fix: null });
|
|
960
|
+
}
|
|
961
|
+
return findings;
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
// DEPS-056: Package with install hooks and no verification
|
|
965
|
+
{ id: 'DEPS-056', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'Package with postinstall Script Not Audited',
|
|
966
|
+
check({ files, stack }) {
|
|
967
|
+
const findings = [];
|
|
968
|
+
const riskyPkgsWithHooks = ['node-gyp', 'fsevents', 'canvas', 'sharp', 'bcrypt', 'puppeteer', 'cypress', 'electron'];
|
|
969
|
+
for (const pkg of riskyPkgsWithHooks) {
|
|
970
|
+
if (pkg in (stack.dependencies || {})) {
|
|
971
|
+
const npmrcHasIgnoreScripts = [...files.values()].some(c => c.match(/ignore-scripts\s*=\s*true/));
|
|
972
|
+
if (!npmrcHasIgnoreScripts) {
|
|
973
|
+
findings.push({ ruleId: 'DEPS-056', category: 'dependencies', severity: 'high', title: `'${pkg}' runs install scripts — review postinstall hooks`, description: `${pkg} runs native build scripts during installation. Ensure you trust this package's integrity. Consider adding ignore-scripts=true and only enabling per-package overrides.`, fix: null });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return findings;
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
// DEPS-057: No Software Bill of Materials (SBOM) generation
|
|
981
|
+
{ id: 'DEPS-057', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'No SBOM Generation in Build Process',
|
|
982
|
+
check({ files }) {
|
|
983
|
+
const findings = [];
|
|
984
|
+
const hasSBOM = [...files.values()].some(c => c.match(/cyclonedx|sbom|syft|spdx|software.*bill.*materials/i));
|
|
985
|
+
const hasCICD = [...files.keys()].some(f => f.match(/\.github\/workflows|\.gitlab-ci/));
|
|
986
|
+
if (hasCICD && !hasSBOM) {
|
|
987
|
+
findings.push({ ruleId: 'DEPS-057', category: 'dependencies', severity: 'low', title: 'No Software Bill of Materials (SBOM) generated in CI', description: 'Generate an SBOM using @cyclonedx/cyclonedx-npm or syft. Executive Order 14028 and many enterprise customers require an SBOM for security transparency.', fix: null });
|
|
988
|
+
}
|
|
989
|
+
return findings;
|
|
990
|
+
},
|
|
991
|
+
},
|
|
992
|
+
// DEPS-058: Direct import of sub-path without checking exports map
|
|
993
|
+
{ id: 'DEPS-058', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Deep Package Imports Without Checking Exports Map',
|
|
994
|
+
check({ files }) {
|
|
995
|
+
const findings = [];
|
|
996
|
+
for (const [fp, c] of files) {
|
|
997
|
+
if (!isSourceFile(fp)) continue;
|
|
998
|
+
const lines = c.split('\n');
|
|
999
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1000
|
+
if (lines[i].match(/from\s+['"`]lodash\/|require\s*\(\s*['"`]lodash\//)) {
|
|
1001
|
+
findings.push({ ruleId: 'DEPS-058', category: 'dependencies', severity: 'low', title: 'Deep import from lodash — consider lodash-es or per-method packages', description: 'Use lodash-es for tree-shaking or import specific methods (import cloneDeep from "lodash/cloneDeep"). Importing all of lodash bundles ~70KB unnecessarily.', file: fp, line: i + 1, fix: null });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return findings;
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
// DEPS-059: Missing resolutions for vulnerable transitive dependency
|
|
1009
|
+
{ id: 'DEPS-059', category: 'dependencies', severity: 'medium', confidence: 'definite', title: 'No resolutions/overrides for Known Vulnerable Transitive Dependency',
|
|
1010
|
+
check({ files, stack }) {
|
|
1011
|
+
const findings = [];
|
|
1012
|
+
const packageJsonFile = [...files.entries()].find(([f]) => f.match(/^package\.json$/));
|
|
1013
|
+
if (!packageJsonFile) return findings;
|
|
1014
|
+
try {
|
|
1015
|
+
const pkg = JSON.parse(packageJsonFile[1]);
|
|
1016
|
+
if (!pkg.resolutions && !pkg.overrides) {
|
|
1017
|
+
const vulnerableTransitive = ['minimist', 'glob-parent', 'nth-check', 'loader-utils'];
|
|
1018
|
+
for (const dep of vulnerableTransitive) {
|
|
1019
|
+
if (dep in (stack.devDependencies || {})) {
|
|
1020
|
+
findings.push({ ruleId: 'DEPS-059', category: 'dependencies', severity: 'medium', title: `No overrides/resolutions for '${dep}' — vulnerable transitive versions may be installed`, description: 'Use "overrides" (npm) or "resolutions" (yarn) to force a patched version of vulnerable transitive dependencies when direct update is not possible.', fix: null });
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch {}
|
|
1026
|
+
return findings;
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
// DEPS-060: Using pre-release versions in production
|
|
1030
|
+
{ id: 'DEPS-060', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Pre-Release Package Versions in Production Dependencies',
|
|
1031
|
+
check({ stack }) {
|
|
1032
|
+
const findings = [];
|
|
1033
|
+
for (const [pkg, ver] of Object.entries(stack.dependencies || {})) {
|
|
1034
|
+
if (ver.match(/alpha|beta|rc\.|canary|next|preview|-0\.\d/) && !pkg.match(/^@types\//)) {
|
|
1035
|
+
findings.push({ ruleId: 'DEPS-060', category: 'dependencies', severity: 'medium', title: `Production dependency '${pkg}' is a pre-release version (${ver})`, description: `Replace ${pkg}@${ver} with a stable release. Pre-release packages may have breaking changes, incomplete documentation, and are not supported by package maintainers.`, fix: null });
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return findings;
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
];
|
|
1042
|
+
|
|
1043
|
+
export default rules;
|
|
1044
|
+
|
|
1045
|
+
// DEPS-061: Using deprecated moment.js
|
|
1046
|
+
rules.push({
|
|
1047
|
+
id: 'DEPS-061', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'moment.js dependency — deprecated, use date-fns or dayjs',
|
|
1048
|
+
check({ stack }) {
|
|
1049
|
+
const findings = [];
|
|
1050
|
+
if (stack.dependencies?.moment || stack.devDependencies?.moment) {
|
|
1051
|
+
findings.push({ ruleId: 'DEPS-061', category: 'dependencies', severity: 'medium', title: 'moment.js is deprecated and should be replaced with date-fns or dayjs', description: 'moment.js is in maintenance mode and marked as a legacy project. Replace with date-fns (tree-shakeable) or dayjs (2kB, same API) for new code.', fix: null });
|
|
1052
|
+
}
|
|
1053
|
+
return findings;
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// DEPS-062: Using deprecated request package
|
|
1058
|
+
rules.push({
|
|
1059
|
+
id: 'DEPS-062', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'request package deprecated — use node-fetch, got, or axios',
|
|
1060
|
+
check({ stack }) {
|
|
1061
|
+
const findings = [];
|
|
1062
|
+
if (stack.dependencies?.request || stack.devDependencies?.request) {
|
|
1063
|
+
findings.push({ ruleId: 'DEPS-062', category: 'dependencies', severity: 'medium', title: '"request" package is deprecated and unmaintained', description: 'The "request" package was deprecated in 2020. Replace with got (Node.js), node-fetch, or axios which are actively maintained and support modern features.', fix: null });
|
|
1064
|
+
}
|
|
1065
|
+
return findings;
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// DEPS-063: Using vulnerable version pattern of lodash
|
|
1070
|
+
rules.push({
|
|
1071
|
+
id: 'DEPS-063', category: 'dependencies', severity: 'medium', confidence: 'definite', title: 'lodash < 4.17.21 — prototype pollution vulnerability',
|
|
1072
|
+
check({ stack }) {
|
|
1073
|
+
const findings = [];
|
|
1074
|
+
const lodashVer = stack.dependencies?.lodash || stack.devDependencies?.lodash;
|
|
1075
|
+
if (lodashVer) {
|
|
1076
|
+
const match = lodashVer.match(/^[\^~]?(\d+)\.(\d+)\.(\d+)/);
|
|
1077
|
+
if (match) {
|
|
1078
|
+
const [, major, minor, patch] = match.map(Number);
|
|
1079
|
+
if (major < 4 || (major === 4 && minor < 17) || (major === 4 && minor === 17 && patch < 21)) {
|
|
1080
|
+
findings.push({ ruleId: 'DEPS-063', category: 'dependencies', severity: 'medium', title: `lodash@${lodashVer} has known prototype pollution vulnerabilities`, description: 'Upgrade lodash to >= 4.17.21. Older versions are vulnerable to prototype pollution (CVE-2020-8203, CVE-2019-10744).', fix: null });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return findings;
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// DEPS-064: Using log4j or vulnerable log libraries
|
|
1089
|
+
rules.push({
|
|
1090
|
+
id: 'DEPS-064', category: 'dependencies', severity: 'high', confidence: 'definite', title: 'Vulnerable logging library version detected',
|
|
1091
|
+
check({ stack }) {
|
|
1092
|
+
const findings = [];
|
|
1093
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1094
|
+
// Check for log4j (Java via node build tools is unusual, but check)
|
|
1095
|
+
if (allDeps['log4js']) {
|
|
1096
|
+
const ver = allDeps['log4js'];
|
|
1097
|
+
const m = ver.match(/^[\^~]?(\d+)/);
|
|
1098
|
+
if (m && parseInt(m[1]) < 6) {
|
|
1099
|
+
findings.push({ ruleId: 'DEPS-064', category: 'dependencies', severity: 'high', title: `log4js@${ver} — upgrade to >= 6.4.1 for security fixes`, description: 'Older versions of log4js have known vulnerabilities. Upgrade to the latest stable version.', fix: null });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return findings;
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// DEPS-065: Too many dependencies (bundle bloat)
|
|
1107
|
+
rules.push({
|
|
1108
|
+
id: 'DEPS-065', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Very large number of direct production dependencies',
|
|
1109
|
+
check({ stack }) {
|
|
1110
|
+
const findings = [];
|
|
1111
|
+
const depCount = Object.keys(stack.dependencies || {}).length;
|
|
1112
|
+
if (depCount > 100) {
|
|
1113
|
+
findings.push({ ruleId: 'DEPS-065', category: 'dependencies', severity: 'low', title: `${depCount} direct production dependencies — consider consolidation`, description: 'A large number of direct dependencies increases supply chain risk, bundle size, and maintenance burden. Audit with npm ls --depth=0 and remove unused packages.', fix: null });
|
|
1114
|
+
}
|
|
1115
|
+
return findings;
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// DEPS-066: Missing package-lock.json or yarn.lock
|
|
1120
|
+
rules.push({
|
|
1121
|
+
id: 'DEPS-066', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'No lockfile found — non-deterministic installs',
|
|
1122
|
+
check({ files, stack }) {
|
|
1123
|
+
const findings = [];
|
|
1124
|
+
const hasPackageJson = [...files.keys()].some(f => f.match(/^package\.json$|\/package\.json$/));
|
|
1125
|
+
const hasLockfile = [...files.keys()].some(f => f.match(/package-lock\.json$|yarn\.lock$|pnpm-lock\.yaml$/));
|
|
1126
|
+
if (hasPackageJson && !hasLockfile) {
|
|
1127
|
+
findings.push({ ruleId: 'DEPS-066', category: 'dependencies', severity: 'high', title: 'No lockfile (package-lock.json/yarn.lock) — installs are non-deterministic', description: 'Without a lockfile, npm install may resolve different package versions across environments. Commit your lockfile to ensure reproducible builds.', fix: null });
|
|
1128
|
+
}
|
|
1129
|
+
return findings;
|
|
1130
|
+
},
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
// DEPS-067: Using node_modules in Dockerfile COPY
|
|
1134
|
+
rules.push({
|
|
1135
|
+
id: 'DEPS-067', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'node_modules copied into Docker image — not rebuilt from lockfile',
|
|
1136
|
+
check({ files }) {
|
|
1137
|
+
const findings = [];
|
|
1138
|
+
for (const [fp, c] of files) {
|
|
1139
|
+
if (!fp.match(/Dockerfile/i)) continue;
|
|
1140
|
+
const lines = c.split('\n');
|
|
1141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1142
|
+
if (/^\s*#/.test(lines[i])) continue;
|
|
1143
|
+
if (/^COPY\s+node_modules/.test(lines[i])) {
|
|
1144
|
+
findings.push({ ruleId: 'DEPS-067', category: 'dependencies', severity: 'medium', title: 'Dockerfile copies node_modules from host — use npm ci instead', description: 'COPY node_modules copies your local dev dependencies into the image. Use COPY package*.json ./ && RUN npm ci --only=production for clean production installs.', file: fp, line: i + 1, fix: null });
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return findings;
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// DEPS-068: Using npm install instead of npm ci in CI
|
|
1153
|
+
rules.push({
|
|
1154
|
+
id: 'DEPS-068', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'CI pipeline uses npm install instead of npm ci',
|
|
1155
|
+
check({ files }) {
|
|
1156
|
+
const findings = [];
|
|
1157
|
+
for (const [fp, c] of files) {
|
|
1158
|
+
if (!fp.match(/\.ya?ml$|Makefile|Jenkinsfile/)) continue;
|
|
1159
|
+
if (!fp.match(/\.github|\.gitlab|circleci|jenkins|travis|ci\//i) && !fp.match(/Makefile|Jenkinsfile/)) continue;
|
|
1160
|
+
const lines = c.split('\n');
|
|
1161
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1162
|
+
if (/^\s*#/.test(lines[i])) continue;
|
|
1163
|
+
if (/\bnpm\s+install\b/.test(lines[i]) && !/npm\s+install\s+\w+/.test(lines[i])) {
|
|
1164
|
+
findings.push({ ruleId: 'DEPS-068', category: 'dependencies', severity: 'medium', title: 'CI uses "npm install" — use "npm ci" for reproducible installs', description: 'npm ci installs exact versions from package-lock.json and is faster in CI. npm install may upgrade packages and modify the lockfile.', file: fp, line: i + 1, fix: null });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return findings;
|
|
1169
|
+
},
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// DEPS-069: Peer dependency version mismatch risk
|
|
1173
|
+
rules.push({
|
|
1174
|
+
id: 'DEPS-069', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'peerDependencies not pinned — silent version mismatch risk',
|
|
1175
|
+
check({ stack }) {
|
|
1176
|
+
const findings = [];
|
|
1177
|
+
const peers = stack.peerDependencies || {};
|
|
1178
|
+
for (const [pkg, ver] of Object.entries(peers)) {
|
|
1179
|
+
if (/^\*$|^>=\d|^>/.test(ver)) {
|
|
1180
|
+
findings.push({ ruleId: 'DEPS-069', category: 'dependencies', severity: 'low', title: `peerDependency "${pkg}": "${ver}" — overly permissive range`, description: 'Overly permissive peer dependency ranges may result in incompatible versions being installed silently. Specify a precise range like "^18.0.0" instead of ">=14".', fix: null });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return findings;
|
|
1184
|
+
},
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// DEPS-070: Using rimraf < 4 (security update)
|
|
1188
|
+
rules.push({
|
|
1189
|
+
id: 'DEPS-070', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Using outdated build tool with known issues',
|
|
1190
|
+
check({ stack }) {
|
|
1191
|
+
const findings = [];
|
|
1192
|
+
const outdatedTools = {
|
|
1193
|
+
'node-uuid': 'Use the "uuid" package instead — node-uuid is deprecated',
|
|
1194
|
+
'jade': 'jade was renamed to pug — replace with pug package',
|
|
1195
|
+
'grunt': 'Consider migrating from grunt to npm scripts or a modern bundler',
|
|
1196
|
+
};
|
|
1197
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1198
|
+
for (const [pkg, msg] of Object.entries(outdatedTools)) {
|
|
1199
|
+
if (allDeps[pkg]) {
|
|
1200
|
+
findings.push({ ruleId: 'DEPS-070', category: 'dependencies', severity: 'low', title: `Deprecated package "${pkg}" found`, description: msg, fix: null });
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return findings;
|
|
1204
|
+
},
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// DEPS-071: Using known-vulnerable serialize-javascript version
|
|
1208
|
+
rules.push({
|
|
1209
|
+
id: 'DEPS-071', category: 'dependencies', severity: 'high', confidence: 'definite', title: 'serialize-javascript < 3.1.0 — XSS vulnerability (CVE-2020-7660)',
|
|
1210
|
+
check({ stack }) {
|
|
1211
|
+
const findings = [];
|
|
1212
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1213
|
+
const ver = allDeps['serialize-javascript'];
|
|
1214
|
+
if (ver) {
|
|
1215
|
+
const m = ver.match(/^[\^~]?(\d+)\.(\d+)/);
|
|
1216
|
+
if (m && (parseInt(m[1]) < 3 || (parseInt(m[1]) === 3 && parseInt(m[2]) < 1))) {
|
|
1217
|
+
findings.push({ ruleId: 'DEPS-071', category: 'dependencies', severity: 'high', title: `serialize-javascript@${ver} has XSS vulnerability (CVE-2020-7660)`, description: 'Upgrade serialize-javascript to >= 3.1.0 to fix a vulnerability where malicious HTML characters were not escaped.', fix: null });
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return findings;
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// DEPS-072: No dependency vulnerability scanning in CI
|
|
1225
|
+
rules.push({
|
|
1226
|
+
id: 'DEPS-072', category: 'dependencies', severity: 'high', confidence: 'definite', title: 'No dependency vulnerability scan in CI pipeline',
|
|
1227
|
+
check({ files }) {
|
|
1228
|
+
const findings = [];
|
|
1229
|
+
const hasCIFile = [...files.keys()].some(f => f.match(/\.github\/workflows|\.gitlab-ci|\.circleci|Jenkinsfile/i));
|
|
1230
|
+
const hasAudit = [...files.values()].some(c => /npm\s+audit|yarn\s+audit|snyk\s+test|grype|trivy|ossindex/i.test(c));
|
|
1231
|
+
if (hasCIFile && !hasAudit) {
|
|
1232
|
+
findings.push({ ruleId: 'DEPS-072', category: 'dependencies', severity: 'high', title: 'CI pipeline without dependency vulnerability scanning', description: 'Add npm audit, Snyk, or Trivy to your CI pipeline to automatically detect known vulnerabilities in dependencies before deployment.', fix: null });
|
|
1233
|
+
}
|
|
1234
|
+
return findings;
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// DEPS-073 through DEPS-100: Additional dependency rules
|
|
1239
|
+
|
|
1240
|
+
// DEPS-073: Using deprecated crypto module APIs
|
|
1241
|
+
rules.push({
|
|
1242
|
+
id: 'DEPS-073', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Dependency using deprecated Node.js crypto APIs',
|
|
1243
|
+
check({ stack }) {
|
|
1244
|
+
const findings = [];
|
|
1245
|
+
const deprecatedPackages = ['node-uuid', 'csprng', 'secure-random'];
|
|
1246
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1247
|
+
for (const pkg of deprecatedPackages) {
|
|
1248
|
+
if (allDeps[pkg]) {
|
|
1249
|
+
findings.push({ ruleId: 'DEPS-073', category: 'dependencies', severity: 'medium', title: `Package "${pkg}" is deprecated`, description: `${pkg} is deprecated. Use crypto.randomBytes() or crypto.randomUUID() from Node.js built-in crypto module instead.`, fix: null });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return findings;
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// DEPS-074: Transitive dependency on insecure crypto
|
|
1257
|
+
rules.push({
|
|
1258
|
+
id: 'DEPS-074', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Package known to depend on deprecated or weak crypto',
|
|
1259
|
+
check({ stack }) {
|
|
1260
|
+
const findings = [];
|
|
1261
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1262
|
+
const weakCryptoDeps = ['md5', 'sha1', 'sha.js', 'des', 'rc4'];
|
|
1263
|
+
for (const pkg of weakCryptoDeps) {
|
|
1264
|
+
if (allDeps[pkg]) {
|
|
1265
|
+
findings.push({ ruleId: 'DEPS-074', category: 'dependencies', severity: 'medium', title: `Direct dependency on weak crypto package "${pkg}"`, description: `${pkg} implements a cryptographically broken algorithm. Remove this dependency and use Node.js built-in crypto module with AES-256-GCM or SHA-256+.`, fix: null });
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return findings;
|
|
1269
|
+
},
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
// DEPS-075: Using eval-based templating engines
|
|
1273
|
+
rules.push({
|
|
1274
|
+
id: 'DEPS-075', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Template engine that uses eval() — code injection risk',
|
|
1275
|
+
check({ stack }) {
|
|
1276
|
+
const findings = [];
|
|
1277
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1278
|
+
const evalEngines = ['jade', 'doT', 'underscore.template'];
|
|
1279
|
+
for (const pkg of evalEngines) {
|
|
1280
|
+
if (allDeps[pkg]) {
|
|
1281
|
+
findings.push({ ruleId: 'DEPS-075', category: 'dependencies', severity: 'medium', title: `Template engine "${pkg}" uses eval() — template injection risk`, description: `${pkg} uses eval() which can execute arbitrary code if template strings contain user input. Consider safer alternatives or ensure templates are never constructed from user input.`, fix: null });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return findings;
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// DEPS-076: Using known-vulnerable version of minimist
|
|
1289
|
+
rules.push({
|
|
1290
|
+
id: 'DEPS-076', category: 'dependencies', severity: 'medium', confidence: 'definite', title: 'minimist < 1.2.6 — prototype pollution vulnerability',
|
|
1291
|
+
check({ stack }) {
|
|
1292
|
+
const findings = [];
|
|
1293
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1294
|
+
const ver = allDeps['minimist'];
|
|
1295
|
+
if (ver) {
|
|
1296
|
+
const m = ver.match(/^[\^~]?(\d+)\.(\d+)\.(\d+)/);
|
|
1297
|
+
if (m) {
|
|
1298
|
+
const [, major, minor, patch] = m.map(Number);
|
|
1299
|
+
if (major < 1 || (major === 1 && minor < 2) || (major === 1 && minor === 2 && patch < 6)) {
|
|
1300
|
+
findings.push({ ruleId: 'DEPS-076', category: 'dependencies', severity: 'medium', title: `minimist@${ver} has prototype pollution vulnerability`, description: 'Upgrade minimist to >= 1.2.6 to fix CVE-2021-44906 (prototype pollution).', fix: null });
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return findings;
|
|
1305
|
+
},
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
// DEPS-077: Package.json engines field missing
|
|
1309
|
+
rules.push({
|
|
1310
|
+
id: 'DEPS-077', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'No engines field in package.json — any Node version may be used',
|
|
1311
|
+
check({ files, stack }) {
|
|
1312
|
+
const findings = [];
|
|
1313
|
+
if (!stack.engines?.node) {
|
|
1314
|
+
const hasPackageJson = [...files.keys()].some(f => f.match(/^package\.json$|\/package\.json$/));
|
|
1315
|
+
if (hasPackageJson) {
|
|
1316
|
+
findings.push({ ruleId: 'DEPS-077', category: 'dependencies', severity: 'low', title: 'package.json without engines.node — no Node.js version constraint', description: 'Specify the required Node.js version in package.json "engines": { "node": ">=18.0.0" } to prevent running on incompatible versions.', fix: null });
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return findings;
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
// DEPS-078: Using sync-only library in async context
|
|
1324
|
+
rules.push({
|
|
1325
|
+
id: 'DEPS-078', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Synchronous-only library used in async/server context',
|
|
1326
|
+
check({ stack, files }) {
|
|
1327
|
+
const findings = [];
|
|
1328
|
+
const syncLibs = ['sync-request', 'xmlhttprequest'];
|
|
1329
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1330
|
+
const hasServer = [...files.values()].some(c => /express|fastify|koa|hapi/i.test(c));
|
|
1331
|
+
for (const pkg of syncLibs) {
|
|
1332
|
+
if (allDeps[pkg] && hasServer) {
|
|
1333
|
+
findings.push({ ruleId: 'DEPS-078', category: 'dependencies', severity: 'medium', title: `"${pkg}" makes synchronous HTTP requests — blocks Node.js event loop`, description: `${pkg} performs synchronous I/O which blocks the event loop. Use an async HTTP client (fetch, axios, got) instead.`, fix: null });
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return findings;
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// DEPS-079: Using test framework in production dependencies
|
|
1341
|
+
rules.push({
|
|
1342
|
+
id: 'DEPS-079', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Test framework installed as production dependency',
|
|
1343
|
+
check({ stack }) {
|
|
1344
|
+
const findings = [];
|
|
1345
|
+
const testLibs = ['jest', 'mocha', 'jasmine', 'chai', 'sinon', 'supertest', 'nock', 'enzyme'];
|
|
1346
|
+
for (const pkg of testLibs) {
|
|
1347
|
+
if (stack.dependencies?.[pkg]) {
|
|
1348
|
+
findings.push({ ruleId: 'DEPS-079', category: 'dependencies', severity: 'medium', title: `Test framework "${pkg}" in production dependencies`, description: `Move ${pkg} to devDependencies. Test frameworks should not be included in production builds as they increase bundle size and attack surface.`, fix: null });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return findings;
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// DEPS-080: Using multiple HTTP client libraries
|
|
1356
|
+
rules.push({
|
|
1357
|
+
id: 'DEPS-080', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Multiple HTTP client libraries installed — unnecessary duplication',
|
|
1358
|
+
check({ stack }) {
|
|
1359
|
+
const findings = [];
|
|
1360
|
+
const httpClients = ['axios', 'node-fetch', 'got', 'superagent', 'request', 'ky', 'undici'];
|
|
1361
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1362
|
+
const usedClients = httpClients.filter(c => allDeps[c]);
|
|
1363
|
+
if (usedClients.length > 2) {
|
|
1364
|
+
findings.push({ ruleId: 'DEPS-080', category: 'dependencies', severity: 'low', title: `${usedClients.length} HTTP clients installed: ${usedClients.join(', ')} — standardize on one`, description: 'Multiple HTTP client libraries increase bundle size and maintenance overhead. Choose one (axios or node-fetch) and remove the rest.', fix: null });
|
|
1365
|
+
}
|
|
1366
|
+
return findings;
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// DEPS-081: Using deprecated helmet middleware version
|
|
1371
|
+
rules.push({
|
|
1372
|
+
id: 'DEPS-081', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'helmet.js < 4.0.0 — legacy version with different defaults',
|
|
1373
|
+
check({ stack }) {
|
|
1374
|
+
const findings = [];
|
|
1375
|
+
const ver = stack.dependencies?.helmet || stack.devDependencies?.helmet;
|
|
1376
|
+
if (ver) {
|
|
1377
|
+
const m = ver.match(/^[\^~]?(\d+)/);
|
|
1378
|
+
if (m && parseInt(m[1]) < 4) {
|
|
1379
|
+
findings.push({ ruleId: 'DEPS-081', category: 'dependencies', severity: 'medium', title: `helmet@${ver} is a legacy version — upgrade to helmet@7+`, description: 'helmet 4+ has much stricter security defaults including Content-Security-Policy. Upgrade and review the new defaults for your application.', fix: null });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return findings;
|
|
1383
|
+
},
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
// DEPS-082: Using body-parser instead of express built-in
|
|
1387
|
+
rules.push({
|
|
1388
|
+
id: 'DEPS-082', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'body-parser installed — use express.json() built-in instead',
|
|
1389
|
+
check({ stack }) {
|
|
1390
|
+
const findings = [];
|
|
1391
|
+
if (stack.dependencies?.['body-parser'] && stack.dependencies?.express) {
|
|
1392
|
+
const expressVer = stack.dependencies.express;
|
|
1393
|
+
const m = expressVer.match(/^[\^~]?(\d+)/);
|
|
1394
|
+
if (m && parseInt(m[1]) >= 4) {
|
|
1395
|
+
findings.push({ ruleId: 'DEPS-082', category: 'dependencies', severity: 'low', title: 'body-parser is a separate dependency — express.json() is built in since Express 4.16+', description: 'Remove the body-parser package and use app.use(express.json()) and app.use(express.urlencoded()) directly. body-parser is now built into Express.', fix: null });
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return findings;
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// DEPS-083 through DEPS-100
|
|
1403
|
+
|
|
1404
|
+
// DEPS-083: Using unsafe-regex in code
|
|
1405
|
+
rules.push({
|
|
1406
|
+
id: 'DEPS-083', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'No safe-regex or validator for user-provided patterns',
|
|
1407
|
+
check({ stack }) {
|
|
1408
|
+
const findings = [];
|
|
1409
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1410
|
+
const hasUserRegex = Object.keys(allDeps).some(d => d.match(/validator|express-validator|joi|zod/));
|
|
1411
|
+
const hasSafeRegex = Object.keys(allDeps).some(d => d.match(/safe-regex|re2|regexp-tree/));
|
|
1412
|
+
// Only flag if there's a web framework (meaning user input) without safe regex tools
|
|
1413
|
+
const hasWebFramework = Object.keys(allDeps).some(d => d.match(/^express$|^fastify$|^koa$/));
|
|
1414
|
+
if (hasWebFramework && !hasSafeRegex) {
|
|
1415
|
+
findings.push({ ruleId: 'DEPS-083', category: 'dependencies', severity: 'medium', title: 'No safe-regex or RE2 library — ReDoS attacks possible with user-provided patterns', description: 'If users can provide regex patterns, use the "safe-regex" package to check patterns or "re2" for linear-time regex evaluation.', fix: null });
|
|
1416
|
+
}
|
|
1417
|
+
return findings;
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
// DEPS-084: Using nodemon in production
|
|
1422
|
+
rules.push({
|
|
1423
|
+
id: 'DEPS-084', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'nodemon installed as production dependency',
|
|
1424
|
+
check({ stack }) {
|
|
1425
|
+
const findings = [];
|
|
1426
|
+
if (stack.dependencies?.nodemon) {
|
|
1427
|
+
findings.push({ ruleId: 'DEPS-084', category: 'dependencies', severity: 'high', title: 'nodemon in production dependencies — restarts server on any file change', description: 'nodemon is a development tool. Move it to devDependencies and use pm2, forever, or systemd for production process management.', fix: null });
|
|
1428
|
+
}
|
|
1429
|
+
return findings;
|
|
1430
|
+
},
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
// DEPS-085: Using outdated major version of Express
|
|
1434
|
+
rules.push({
|
|
1435
|
+
id: 'DEPS-085', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Express.js version 3.x — end of life',
|
|
1436
|
+
check({ stack }) {
|
|
1437
|
+
const findings = [];
|
|
1438
|
+
const ver = stack.dependencies?.express;
|
|
1439
|
+
if (ver) {
|
|
1440
|
+
const m = ver.match(/^[\^~]?(\d+)/);
|
|
1441
|
+
if (m && parseInt(m[1]) < 4) {
|
|
1442
|
+
findings.push({ ruleId: 'DEPS-085', category: 'dependencies', severity: 'medium', title: `express@${ver} is end-of-life — upgrade to Express 4.x or 5.x`, description: 'Express 3.x is end-of-life and receives no security patches. Upgrade to Express 4.x (stable) or 5.x (latest).', fix: null });
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return findings;
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// DEPS-086: Using passport.js without session store
|
|
1450
|
+
rules.push({
|
|
1451
|
+
id: 'DEPS-086', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'passport.js without explicit session store — using insecure MemoryStore',
|
|
1452
|
+
check({ stack, files }) {
|
|
1453
|
+
const findings = [];
|
|
1454
|
+
if ((stack.dependencies?.passport || stack.dependencies?.['passport-local']) &&
|
|
1455
|
+
!Object.keys({ ...stack.dependencies, ...stack.devDependencies }).some(d => d.match(/connect-redis|connect-mongo|express-session.*store|session-store/i))) {
|
|
1456
|
+
const hasMemoryStoreWarning = [...files.values()].some(c => /MemoryStore|connect-redis|connect-mongo|session.*store/i.test(c));
|
|
1457
|
+
if (!hasMemoryStoreWarning) {
|
|
1458
|
+
findings.push({ ruleId: 'DEPS-086', category: 'dependencies', severity: 'medium', title: 'passport.js without persistent session store — MemoryStore leaks on restart', description: 'The default MemoryStore is not suitable for production — it leaks memory and sessions are lost on restart. Use connect-redis or connect-mongo.', fix: null });
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return findings;
|
|
1462
|
+
},
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
// DEPS-087: Multiple versions of same package type
|
|
1466
|
+
rules.push({
|
|
1467
|
+
id: 'DEPS-087', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'Duplicate utility libraries of same category',
|
|
1468
|
+
check({ stack }) {
|
|
1469
|
+
const findings = [];
|
|
1470
|
+
const allDeps = { ...stack.dependencies, ...stack.devDependencies };
|
|
1471
|
+
const loggers = ['winston', 'pino', 'bunyan', 'morgan', 'log4js'].filter(d => allDeps[d]);
|
|
1472
|
+
if (loggers.length > 2) {
|
|
1473
|
+
findings.push({ ruleId: 'DEPS-087', category: 'dependencies', severity: 'low', title: `Multiple logging libraries: ${loggers.join(', ')} — standardize on one`, description: 'Using multiple logging libraries creates inconsistent log formatting and increases bundle size. Standardize on one: pino (fastest) or winston (most features).', fix: null });
|
|
1474
|
+
}
|
|
1475
|
+
return findings;
|
|
1476
|
+
},
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// DEPS-088: Missing integrity subresource check
|
|
1480
|
+
rules.push({
|
|
1481
|
+
id: 'DEPS-088', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'External CDN script without Subresource Integrity (SRI) hash',
|
|
1482
|
+
check({ files }) {
|
|
1483
|
+
const findings = [];
|
|
1484
|
+
for (const [fp, c] of files) {
|
|
1485
|
+
if (!fp.match(/\.html$/)) continue;
|
|
1486
|
+
const lines = c.split('\n');
|
|
1487
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1488
|
+
if (/^\s*<!--/.test(lines[i])) continue;
|
|
1489
|
+
if (/<script[^>]+src\s*=\s*["']https?:\/\//.test(lines[i]) && !/integrity\s*=/.test(lines[i])) {
|
|
1490
|
+
findings.push({ ruleId: 'DEPS-088', category: 'dependencies', severity: 'medium', title: 'CDN script without integrity attribute — supply chain attack possible', description: 'Add integrity and crossorigin attributes to CDN scripts: <script integrity="sha384-..." crossorigin="anonymous">. This prevents execution of tampered files.', file: fp, line: i + 1, fix: null });
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return findings;
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// DEPS-089: Using dev dependencies in Docker build
|
|
1499
|
+
rules.push({
|
|
1500
|
+
id: 'DEPS-089', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Dockerfile installs all dependencies including devDependencies',
|
|
1501
|
+
check({ files }) {
|
|
1502
|
+
const findings = [];
|
|
1503
|
+
for (const [fp, c] of files) {
|
|
1504
|
+
if (!fp.match(/Dockerfile/i)) continue;
|
|
1505
|
+
if (c.match(/npm\s+(?:ci|install)\b/) && !c.match(/npm\s+ci.*--omit=dev|npm\s+ci.*--production|npm\s+install.*--production/)) {
|
|
1506
|
+
findings.push({ ruleId: 'DEPS-089', category: 'dependencies', severity: 'medium', title: 'Dockerfile installs devDependencies — increases image size and attack surface', description: 'Use npm ci --omit=dev (or NODE_ENV=production npm ci) in your production Dockerfile to exclude development dependencies.', file: fp, fix: null });
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return findings;
|
|
1510
|
+
},
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
// DEPS-090: Missing license compliance check
|
|
1514
|
+
rules.push({
|
|
1515
|
+
id: 'DEPS-090', category: 'dependencies', severity: 'low', confidence: 'likely', title: 'No license compliance check in CI/CD',
|
|
1516
|
+
check({ files }) {
|
|
1517
|
+
const findings = [];
|
|
1518
|
+
const hasCIFile = [...files.keys()].some(f => f.match(/\.github\/workflows|\.gitlab-ci|\.circleci/i));
|
|
1519
|
+
const hasLicenseCheck = [...files.values()].some(c => /license-checker|licensee|fossa|snyk.*license|license.*audit/i.test(c));
|
|
1520
|
+
if (hasCIFile && !hasLicenseCheck) {
|
|
1521
|
+
findings.push({ ruleId: 'DEPS-090', category: 'dependencies', severity: 'low', title: 'No license compliance check — copyleft licenses may affect IP', description: 'Add license-checker to CI to detect dependencies with restrictive licenses (GPL, AGPL) that could affect your IP or distribution rights.', fix: null });
|
|
1522
|
+
}
|
|
1523
|
+
return findings;
|
|
1524
|
+
},
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
// DEPS-091: Using npm shrinkwrap instead of package-lock.json
|
|
1528
|
+
rules.push({
|
|
1529
|
+
id: 'DEPS-091', category: 'dependencies', severity: 'low', confidence: 'suggestion', title: 'npm-shrinkwrap.json used instead of package-lock.json',
|
|
1530
|
+
check({ files }) {
|
|
1531
|
+
const findings = [];
|
|
1532
|
+
const hasShrinkwrap = [...files.keys()].some(f => f.endsWith('npm-shrinkwrap.json'));
|
|
1533
|
+
if (hasShrinkwrap) findings.push({ ruleId: 'DEPS-091', category: 'dependencies', severity: 'low', title: 'npm-shrinkwrap.json is obsolete — use package-lock.json instead', description: 'package-lock.json is the modern standard for lock files. npm-shrinkwrap.json is only needed for published packages.', file: 'npm-shrinkwrap.json', fix: null });
|
|
1534
|
+
return findings;
|
|
1535
|
+
},
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
// DEPS-092: Sharp/canvas without pre-built binaries
|
|
1539
|
+
rules.push({
|
|
1540
|
+
id: 'DEPS-092', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'sharp or canvas dependency without native binary considerations',
|
|
1541
|
+
check({ files, stack }) {
|
|
1542
|
+
const findings = [];
|
|
1543
|
+
if (stack.dependencies?.['sharp'] || stack.dependencies?.['canvas']) {
|
|
1544
|
+
const hasBuildStep = [...files.keys()].some(f => f.endsWith('Dockerfile') || f.endsWith('.dockerfile'));
|
|
1545
|
+
if (hasBuildStep) {
|
|
1546
|
+
const pkg = stack.dependencies?.['sharp'] ? 'sharp' : 'canvas';
|
|
1547
|
+
const dockerFile = [...files.keys()].find(f => f.endsWith('Dockerfile'));
|
|
1548
|
+
if (dockerFile) {
|
|
1549
|
+
const dc = files.get(dockerFile) || '';
|
|
1550
|
+
if (!/build-essential|python3|node-gyp/.test(dc)) findings.push({ ruleId: 'DEPS-092', category: 'dependencies', severity: 'medium', title: `${pkg} requires native build tools in Docker image`, description: `Add build-essential, python3, and node-gyp to your Dockerfile when building ${pkg} from source.`, file: dockerFile, fix: null });
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return findings;
|
|
1555
|
+
},
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// DEPS-093: express-validator not used with express
|
|
1559
|
+
rules.push({
|
|
1560
|
+
id: 'DEPS-093', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'Express used without input validation library',
|
|
1561
|
+
check({ files, stack }) {
|
|
1562
|
+
const findings = [];
|
|
1563
|
+
if (!stack.dependencies?.['express']) return findings;
|
|
1564
|
+
const hasValidator = stack.dependencies?.['express-validator'] || stack.dependencies?.['joi'] || stack.dependencies?.['zod'] || stack.dependencies?.['yup'];
|
|
1565
|
+
if (!hasValidator) {
|
|
1566
|
+
const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
|
|
1567
|
+
if (pkgJson) findings.push({ ruleId: 'DEPS-093', category: 'dependencies', severity: 'medium', title: 'Express application without a validation library', description: 'Add express-validator, joi, or zod to validate and sanitize request data.', file: pkgJson, fix: null });
|
|
1568
|
+
}
|
|
1569
|
+
return findings;
|
|
1570
|
+
},
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
// DEPS-094: Vulnerable version of axios
|
|
1574
|
+
rules.push({
|
|
1575
|
+
id: 'DEPS-094', category: 'dependencies', severity: 'high', confidence: 'definite', title: 'axios version below 0.28.0 — SSRF vulnerability (CVE-2023-45857)',
|
|
1576
|
+
check({ files, stack }) {
|
|
1577
|
+
const findings = [];
|
|
1578
|
+
const ver = stack.dependencies?.['axios'];
|
|
1579
|
+
if (ver) {
|
|
1580
|
+
const match = ver.match(/^[~^]?(\d+)\.(\d+)\.(\d+)/);
|
|
1581
|
+
if (match) {
|
|
1582
|
+
const [, major, minor] = match.map(Number);
|
|
1583
|
+
if (major === 0 && minor < 28) {
|
|
1584
|
+
const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
|
|
1585
|
+
if (pkgJson) findings.push({ ruleId: 'DEPS-094', category: 'dependencies', severity: 'high', title: 'axios < 0.28.0 has SSRF vulnerability (CVE-2023-45857)', description: 'Upgrade axios to 0.28.0 or 1.x to fix CSRF header exposure vulnerability.', file: pkgJson, fix: null });
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return findings;
|
|
1590
|
+
},
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
// DEPS-095: vm2 package (abandoned, RCE vulnerabilities)
|
|
1594
|
+
rules.push({
|
|
1595
|
+
id: 'DEPS-095', category: 'dependencies', severity: 'critical', confidence: 'definite', title: 'vm2 package is abandoned and has multiple RCE vulnerabilities',
|
|
1596
|
+
check({ files, stack }) {
|
|
1597
|
+
const findings = [];
|
|
1598
|
+
if (stack.dependencies?.['vm2']) {
|
|
1599
|
+
const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
|
|
1600
|
+
if (pkgJson) findings.push({ ruleId: 'DEPS-095', category: 'dependencies', severity: 'critical', title: 'vm2 is abandoned (CVE-2023-29017, CVE-2023-37466) — multiple sandbox escape RCEs', description: 'Replace vm2 with isolated-vm or a separate process for sandboxing. vm2 is no longer maintained.', file: pkgJson, fix: null });
|
|
1601
|
+
}
|
|
1602
|
+
return findings;
|
|
1603
|
+
},
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
// DEPS-096: jsonwebtoken < 9.0.0
|
|
1607
|
+
rules.push({
|
|
1608
|
+
id: 'DEPS-096', category: 'dependencies', severity: 'critical', confidence: 'definite', title: 'jsonwebtoken < 9.0.0 — CVE-2022-23529 vulnerability',
|
|
1609
|
+
check({ files, stack }) {
|
|
1610
|
+
const findings = [];
|
|
1611
|
+
const ver = stack.dependencies?.['jsonwebtoken'];
|
|
1612
|
+
if (ver) {
|
|
1613
|
+
const match = ver.match(/^[~^]?(\d+)/);
|
|
1614
|
+
if (match && parseInt(match[1]) < 9) {
|
|
1615
|
+
const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
|
|
1616
|
+
if (pkgJson) findings.push({ ruleId: 'DEPS-096', category: 'dependencies', severity: 'critical', title: 'jsonwebtoken < 9.0.0 has critical vulnerability (CVE-2022-23529)', description: 'Upgrade jsonwebtoken to >= 9.0.0 to fix the vulnerability.', file: pkgJson, fix: null });
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return findings;
|
|
1620
|
+
},
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// DEPS-097: @actions/core before 1.10.0 (log injection)
|
|
1624
|
+
rules.push({
|
|
1625
|
+
id: 'DEPS-097', category: 'dependencies', severity: 'high', confidence: 'definite', title: '@actions/core < 1.10.0 — GitHub Actions log injection vulnerability',
|
|
1626
|
+
check({ files, stack }) {
|
|
1627
|
+
const findings = [];
|
|
1628
|
+
const ver = stack.dependencies?.['@actions/core'] || stack.devDependencies?.['@actions/core'];
|
|
1629
|
+
if (ver) {
|
|
1630
|
+
const match = ver.match(/^[~^]?(\d+)\.(\d+)/);
|
|
1631
|
+
if (match) {
|
|
1632
|
+
const [, major, minor] = match.map(Number);
|
|
1633
|
+
if (major === 1 && minor < 10) {
|
|
1634
|
+
const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
|
|
1635
|
+
if (pkgJson) findings.push({ ruleId: 'DEPS-097', category: 'dependencies', severity: 'high', title: '@actions/core < 1.10.0 — log injection vulnerability', description: 'Upgrade @actions/core to >= 1.10.0 to fix log injection vulnerability.', file: pkgJson, fix: null });
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return findings;
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
// DEPS-098: Using deprecated xmldom package
|
|
1644
|
+
rules.push({
|
|
1645
|
+
id: 'DEPS-098', category: 'dependencies', severity: 'high', confidence: 'likely', title: 'xmldom package has multiple XXE vulnerabilities',
|
|
1646
|
+
check({ files, stack }) {
|
|
1647
|
+
const findings = [];
|
|
1648
|
+
if (stack.dependencies?.['xmldom']) {
|
|
1649
|
+
const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
|
|
1650
|
+
if (pkgJson) findings.push({ ruleId: 'DEPS-098', category: 'dependencies', severity: 'high', title: 'xmldom has known XXE vulnerabilities — use @xmldom/xmldom instead', description: 'Replace xmldom with @xmldom/xmldom which has security fixes applied.', file: pkgJson, fix: null });
|
|
1651
|
+
}
|
|
1652
|
+
return findings;
|
|
1653
|
+
},
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
// DEPS-099: No .npmrc with package integrity settings
|
|
1657
|
+
rules.push({
|
|
1658
|
+
id: 'DEPS-099', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'No .npmrc with audit settings configured',
|
|
1659
|
+
check({ files }) {
|
|
1660
|
+
const findings = [];
|
|
1661
|
+
const hasNpmrc = [...files.keys()].some(f => f.endsWith('.npmrc'));
|
|
1662
|
+
if (!hasNpmrc) {
|
|
1663
|
+
const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
|
|
1664
|
+
if (pkgJson) findings.push({ ruleId: 'DEPS-099', category: 'dependencies', severity: 'medium', title: 'No .npmrc file — audit-level and save-exact not configured', description: 'Create .npmrc with: audit-level=moderate and save-exact=true to enforce security standards.', file: pkgJson, fix: null });
|
|
1665
|
+
}
|
|
1666
|
+
return findings;
|
|
1667
|
+
},
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
// DEPS-100: Cookie library without secure defaults
|
|
1671
|
+
rules.push({
|
|
1672
|
+
id: 'DEPS-100', category: 'dependencies', severity: 'medium', confidence: 'likely', title: 'cookie package used without Secure/HttpOnly defaults',
|
|
1673
|
+
check({ files, stack }) {
|
|
1674
|
+
const findings = [];
|
|
1675
|
+
if (!stack.dependencies?.['cookie'] && !stack.dependencies?.['cookies']) return findings;
|
|
1676
|
+
for (const [fp, c] of files) {
|
|
1677
|
+
if (!fp.endsWith('.js') && !fp.endsWith('.ts')) continue;
|
|
1678
|
+
if (/cookie\.serialize\s*\(|cookies\.set\s*\(/.test(c) && !/secure\s*:\s*true|httpOnly\s*:\s*true/.test(c)) {
|
|
1679
|
+
findings.push({ ruleId: 'DEPS-100', category: 'dependencies', severity: 'medium', title: 'Cookie set without secure/httpOnly options', description: 'Always set secure: true and httpOnly: true when setting cookies to prevent MITM and XSS theft.', file: fp, fix: null });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return findings;
|
|
1683
|
+
},
|
|
1684
|
+
});
|