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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. 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
+ });