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,2714 @@
1
+ import healthcareRules from './healthcare.js';
2
+ import regionalEuRules from './regional-eu.js';
3
+ import regionalIntlRules from './regional-international.js';
4
+ import educationRules from './education.js';
5
+ import financialRules from './financial.js';
6
+ import frameworksRules from './frameworks.js';
7
+ import accessibilityExtRules from './accessibility-ext.js';
8
+
9
+ const rules = [
10
+ // COMP-001: No privacy policy
11
+ {
12
+ id: 'COMP-001',
13
+ category: 'compliance',
14
+ severity: 'high',
15
+ confidence: 'likely',
16
+ title: 'No Privacy Policy Page',
17
+ check({ files }) {
18
+ const findings = [];
19
+ const hasPrivacyPage = [...files.keys()].some(f =>
20
+ f.match(/privacy/i) || f.match(/privacypolicy/i) || f.match(/privacy-policy/i)
21
+ );
22
+ const hasPrivacyLink = [...files.values()].some(c =>
23
+ c.includes('/privacy') || c.includes('privacy-policy') || c.includes('privacy policy')
24
+ );
25
+
26
+ if (!hasPrivacyPage && !hasPrivacyLink) {
27
+ // Only flag if it looks like a web app with users
28
+ const hasUserFacing = [...files.values()].some(c =>
29
+ c.includes('signup') || c.includes('login') || c.includes('register') ||
30
+ c.includes('<form') || c.includes('email')
31
+ );
32
+ if (hasUserFacing) {
33
+ findings.push({
34
+ ruleId: 'COMP-001', category: 'compliance', severity: 'high',
35
+ title: 'No privacy policy page detected',
36
+ description: 'Required by GDPR, CCPA, and most app stores. Every app collecting user data needs one.',
37
+ fix: null,
38
+ });
39
+ }
40
+ }
41
+ return findings;
42
+ },
43
+ },
44
+
45
+ // COMP-002: No cookie consent
46
+ {
47
+ id: 'COMP-002',
48
+ category: 'compliance',
49
+ severity: 'high',
50
+ confidence: 'likely',
51
+ title: 'No Cookie Consent Banner',
52
+ check({ files }) {
53
+ const findings = [];
54
+ const hasCookieConsent = [...files.values()].some(c =>
55
+ c.includes('cookie-consent') || c.includes('cookieConsent') ||
56
+ c.includes('cookie banner') || c.includes('CookieBanner') ||
57
+ c.includes('cookie_consent')
58
+ );
59
+
60
+ // Check if cookies or analytics are used
61
+ const usesCookies = [...files.values()].some(c =>
62
+ c.includes('document.cookie') || c.includes('setCookie') ||
63
+ c.includes('analytics') || c.includes('gtag') || c.includes('GA_TRACKING') ||
64
+ c.includes('google-analytics') || c.includes('mixpanel') || c.includes('segment')
65
+ );
66
+
67
+ if (usesCookies && !hasCookieConsent) {
68
+ findings.push({
69
+ ruleId: 'COMP-002', category: 'compliance', severity: 'high',
70
+ title: 'Using cookies/analytics without consent banner',
71
+ description: 'GDPR and ePrivacy Directive require user consent before setting non-essential cookies.',
72
+ fix: null,
73
+ });
74
+ }
75
+ return findings;
76
+ },
77
+ },
78
+
79
+ // COMP-003: No data deletion endpoint
80
+ {
81
+ id: 'COMP-003',
82
+ category: 'compliance',
83
+ severity: 'medium',
84
+ confidence: 'likely',
85
+ title: 'No Data Deletion / Right to Erasure',
86
+ check({ files }) {
87
+ const findings = [];
88
+ const hasDeleteEndpoint = [...files.entries()].some(([filepath, content]) =>
89
+ (filepath.includes('api/') || filepath.includes('route')) &&
90
+ (content.includes('delete') || content.includes('DELETE')) &&
91
+ (content.includes('user') || content.includes('account'))
92
+ );
93
+
94
+ const hasUsers = [...files.values()].some(c =>
95
+ c.includes('signup') || c.includes('register') || c.includes('createUser')
96
+ );
97
+
98
+ if (hasUsers && !hasDeleteEndpoint) {
99
+ findings.push({
100
+ ruleId: 'COMP-003', category: 'compliance', severity: 'medium',
101
+ title: 'No user data deletion endpoint (GDPR right to erasure)',
102
+ description: 'GDPR requires you to delete user data on request. Add an account deletion feature.',
103
+ fix: null,
104
+ });
105
+ }
106
+ return findings;
107
+ },
108
+ },
109
+
110
+ // COMP-004: No accessibility basics
111
+ {
112
+ id: 'COMP-004',
113
+ category: 'compliance',
114
+ severity: 'medium',
115
+ confidence: 'likely',
116
+ title: 'Missing Accessibility Attributes',
117
+ check({ files }) {
118
+ const findings = [];
119
+ let imgWithoutAlt = 0;
120
+ let buttonWithoutLabel = 0;
121
+
122
+ for (const [filepath, content] of files) {
123
+ if (!filepath.match(/\.(jsx|tsx|html)$/)) continue;
124
+
125
+ // Check for images without alt
126
+ const imgMatches = content.match(/<img[^>]*>/g) || [];
127
+ for (const img of imgMatches) {
128
+ if (!img.includes('alt=') && !img.includes('alt =')) {
129
+ imgWithoutAlt++;
130
+ }
131
+ }
132
+
133
+ // Check for buttons without accessible labels
134
+ const btnMatches = content.match(/<button[^>]*>[^<]*<\/button>/g) || [];
135
+ for (const btn of btnMatches) {
136
+ if (btn.match(/<button[^>]*>\s*<\/button>/)) {
137
+ buttonWithoutLabel++;
138
+ }
139
+ }
140
+ }
141
+
142
+ if (imgWithoutAlt > 0) {
143
+ findings.push({
144
+ ruleId: 'COMP-004', category: 'compliance', severity: 'medium',
145
+ title: `${imgWithoutAlt} image(s) missing alt text (accessibility)`,
146
+ description: 'Screen readers need alt text to describe images. Required for WCAG compliance.',
147
+ fix: null,
148
+ });
149
+ }
150
+
151
+ if (buttonWithoutLabel > 0) {
152
+ findings.push({
153
+ ruleId: 'COMP-004b', category: 'compliance', severity: 'medium',
154
+ title: `${buttonWithoutLabel} button(s) without accessible labels`,
155
+ description: 'Buttons need text content or aria-label for screen reader accessibility.',
156
+ fix: null,
157
+ });
158
+ }
159
+ return findings;
160
+ },
161
+ },
162
+
163
+ // COMP-005: Terms of Service
164
+ {
165
+ id: 'COMP-005',
166
+ category: 'compliance',
167
+ severity: 'low',
168
+ confidence: 'suggestion',
169
+ title: 'No Terms of Service Page',
170
+ check({ files }) {
171
+ const findings = [];
172
+ const hasToS = [...files.keys()].some(f =>
173
+ f.match(/terms/i) || f.match(/tos/i)
174
+ );
175
+ const hasToSLink = [...files.values()].some(c =>
176
+ c.includes('/terms') || c.includes('terms-of-service') || c.includes('terms of service')
177
+ );
178
+
179
+ const hasUsers = [...files.values()].some(c =>
180
+ c.includes('signup') || c.includes('register')
181
+ );
182
+
183
+ if (hasUsers && !hasToS && !hasToSLink) {
184
+ findings.push({
185
+ ruleId: 'COMP-005', category: 'compliance', severity: 'low',
186
+ title: 'No terms of service page detected',
187
+ description: 'A ToS protects you legally. Important before accepting payments or user data.',
188
+ fix: null,
189
+ });
190
+ }
191
+ return findings;
192
+ },
193
+ },
194
+
195
+ // COMP-GDPR-001: Missing consent checkbox on signup
196
+ { id: 'COMP-GDPR-001', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Missing Consent Checkbox on Signup',
197
+ check({ files }) {
198
+ const findings = [];
199
+ for (const [fp, c] of files) {
200
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
201
+ if ((c.includes('signup') || c.includes('register') || c.includes('createAccount')) && c.includes('<form')) {
202
+ if (!c.match(/consent|agree|terms.*checkbox|checkbox.*terms|gdpr/i)) {
203
+ findings.push({ ruleId: 'COMP-GDPR-001', category: 'compliance', severity: 'high',
204
+ title: 'Signup form missing explicit consent checkbox (GDPR)', description: 'GDPR requires freely given, specific, informed consent. Add a checkbox for terms/privacy that users must check.', file: fp, fix: null });
205
+ }
206
+ }
207
+ }
208
+ return findings;
209
+ },
210
+ },
211
+
212
+ // COMP-GDPR-002: No data export endpoint (right to portability)
213
+ { id: 'COMP-GDPR-002', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Data Export Endpoint (GDPR Right to Portability)',
214
+ check({ files }) {
215
+ const findings = [];
216
+ const hasExport = [...files.values()].some(c => c.match(/export.*data|data.*export|download.*data|portability/i));
217
+ const hasUsers = [...files.values()].some(c => c.includes('signup') || c.includes('createUser'));
218
+ if (hasUsers && !hasExport) {
219
+ findings.push({ ruleId: 'COMP-GDPR-002', category: 'compliance', severity: 'medium',
220
+ title: 'No data export endpoint — GDPR right to data portability', description: 'GDPR Article 20 requires you to provide users their data in a machine-readable format on request.', fix: null });
221
+ }
222
+ return findings;
223
+ },
224
+ },
225
+
226
+ // COMP-GDPR-003: Analytics without consent
227
+ { id: 'COMP-GDPR-003', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Analytics Loaded Without Consent Check',
228
+ check({ files }) {
229
+ const findings = [];
230
+ for (const [fp, c] of files) {
231
+ if (!fp.match(/\.(jsx|tsx|html|js)$/)) continue;
232
+ if (c.match(/gtag|google-analytics|GA_MEASUREMENT|mixpanel|segment|amplitude|heap\.io/)) {
233
+ if (!c.match(/consent|cookie.*accept|cookieBanner|hasConsent|gdpr/i)) {
234
+ findings.push({ ruleId: 'COMP-GDPR-003', category: 'compliance', severity: 'high',
235
+ title: 'Analytics loaded without checking user consent', description: 'GDPR requires consent before loading tracking scripts. Check consent before initializing analytics.', file: fp, fix: null });
236
+ }
237
+ }
238
+ }
239
+ return findings;
240
+ },
241
+ },
242
+
243
+ // COMP-GDPR-004: IP addresses logged without anonymization
244
+ { id: 'COMP-GDPR-004', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'IP Addresses Logged Without Anonymization',
245
+ check({ files }) {
246
+ const findings = [];
247
+ for (const [fp, c] of files) {
248
+ if (!fp.match(/\.(js|ts)$/)) continue;
249
+ if (c.match(/req\.ip|req\.socket\.remoteAddress|x-forwarded-for/i)) {
250
+ if (c.match(/log|store|save|insert|record/i) && !c.match(/anonymi[sz]|mask|redact|truncate/i)) {
251
+ findings.push({ ruleId: 'COMP-GDPR-004', category: 'compliance', severity: 'medium',
252
+ title: 'IP addresses logged/stored without anonymization', description: 'IP addresses are personal data under GDPR. Anonymize (truncate last octet) before logging or storing.', file: fp, fix: null });
253
+ }
254
+ }
255
+ }
256
+ return findings;
257
+ },
258
+ },
259
+
260
+ // COMP-GDPR-005: No data retention policy
261
+ { id: 'COMP-GDPR-005', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Data Retention Policy',
262
+ check({ files }) {
263
+ const has = [...files.values()].some(c => c.match(/retention|purge|expire|ttl|deleteOld/i));
264
+ if (!has) {
265
+ return [{ ruleId: 'COMP-GDPR-005', category: 'compliance', severity: 'medium',
266
+ title: 'No data retention policy detected', description: 'GDPR requires data to be deleted when no longer needed. Implement scheduled deletion jobs and document retention periods.', fix: null }];
267
+ }
268
+ return [];
269
+ },
270
+ },
271
+
272
+ // COMP-A11Y-001: Form inputs without labels
273
+ { id: 'COMP-A11Y-001', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Form Inputs Without Labels',
274
+ check({ files }) {
275
+ const findings = [];
276
+ for (const [fp, c] of files) {
277
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
278
+ const inputs = (c.match(/<input[^>]*>/gi) || []);
279
+ for (const inp of inputs) {
280
+ if (!inp.match(/aria-label|aria-labelledby/i) && !inp.match(/type=["']hidden["']/i)) {
281
+ const surroundingCtx = c.substring(Math.max(0, c.indexOf(inp) - 200), c.indexOf(inp));
282
+ if (!surroundingCtx.match(/<label/i)) {
283
+ findings.push({ ruleId: 'COMP-A11Y-001', category: 'compliance', severity: 'high',
284
+ title: 'Input element without associated label (WCAG 1.3.1)', description: 'Every input needs a <label for="id"> or aria-label for screen reader accessibility.', file: fp, fix: null });
285
+ break;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ return findings;
291
+ },
292
+ },
293
+
294
+ // COMP-A11Y-002: Images without alt text
295
+ { id: 'COMP-A11Y-002', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Images Missing Alt Text',
296
+ check({ files }) {
297
+ const findings = [];
298
+ for (const [fp, c] of files) {
299
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
300
+ const imgs = (c.match(/<img[^>]*>/gi) || []);
301
+ let missing = 0;
302
+ for (const img of imgs) {
303
+ if (!img.match(/alt\s*=/i)) missing++;
304
+ }
305
+ if (missing > 0) {
306
+ findings.push({ ruleId: 'COMP-A11Y-002', category: 'compliance', severity: 'high',
307
+ title: `${missing} image(s) missing alt attribute (WCAG 1.1.1)`, description: 'Screen readers require alt text. Use alt="" for decorative images, descriptive text for informational images.', file: fp, fix: null });
308
+ }
309
+ }
310
+ return findings;
311
+ },
312
+ },
313
+
314
+ // COMP-A11Y-003: Focus outline removed
315
+ { id: 'COMP-A11Y-003', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Focus Outline Removed (Keyboard Navigation Broken)',
316
+ check({ files }) {
317
+ const findings = [];
318
+ for (const [fp, c] of files) {
319
+ if (!fp.match(/\.(css|scss|sass|less|jsx|tsx)$/)) continue;
320
+ if (c.match(/outline\s*:\s*0|outline\s*:\s*none/)) {
321
+ if (!c.match(/outline-offset|:focus-visible|focus-ring/)) {
322
+ findings.push({ ruleId: 'COMP-A11Y-003', category: 'compliance', severity: 'high',
323
+ title: 'outline: none removes keyboard focus indicator (WCAG 2.4.7)', description: 'Never remove focus outlines without providing an alternative. Use :focus-visible to show focus only for keyboard users.', file: fp, fix: null });
324
+ }
325
+ }
326
+ }
327
+ return findings;
328
+ },
329
+ },
330
+
331
+ // COMP-A11Y-004: Interactive div without role
332
+ { id: 'COMP-A11Y-004', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Clickable div Without ARIA Role',
333
+ check({ files }) {
334
+ const findings = [];
335
+ for (const [fp, c] of files) {
336
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
337
+ const lines = c.split('\n');
338
+ for (let i = 0; i < lines.length; i++) {
339
+ if (lines[i].match(/<div[^>]*onClick/i) && !lines[i].match(/role=["'](?:button|link|menuitem)/i)) {
340
+ findings.push({ ruleId: 'COMP-A11Y-004', category: 'compliance', severity: 'medium',
341
+ title: 'Clickable <div> without role="button" (WCAG 4.1.2)', description: 'Use <button> instead of <div onClick>. If div is required, add role="button" tabIndex={0} and keyboard event handlers.', file: fp, line: i + 1, fix: null });
342
+ }
343
+ }
344
+ }
345
+ return findings;
346
+ },
347
+ },
348
+
349
+ // COMP-A11Y-005: Video without captions
350
+ { id: 'COMP-A11Y-005', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Video Without Captions',
351
+ check({ files }) {
352
+ const findings = [];
353
+ for (const [fp, c] of files) {
354
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
355
+ if (c.includes('<video') && !c.includes('<track') && !c.match(/captions|subtitles/i)) {
356
+ findings.push({ ruleId: 'COMP-A11Y-005', category: 'compliance', severity: 'medium',
357
+ title: 'Video element without captions/subtitles (WCAG 1.2.2)', description: 'Add <track kind="captions"> for prerecorded video. Required for WCAG AA compliance.', file: fp, fix: null });
358
+ }
359
+ }
360
+ return findings;
361
+ },
362
+ },
363
+
364
+ // COMP-A11Y-006: Missing lang attribute on html
365
+ { id: 'COMP-A11Y-006', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Missing lang Attribute on HTML Element',
366
+ check({ files }) {
367
+ const findings = [];
368
+ for (const [fp, c] of files) {
369
+ if (!fp.match(/\.(html)$/) && !fp.match(/_document\.(jsx|tsx)$/)) continue;
370
+ if (c.includes('<html') && !c.match(/<html[^>]*lang=/i)) {
371
+ findings.push({ ruleId: 'COMP-A11Y-006', category: 'compliance', severity: 'medium',
372
+ title: 'HTML element missing lang attribute (WCAG 3.1.1)', description: 'Add lang="en" (or appropriate locale) to the <html> element for screen readers and search engines.', file: fp, fix: null });
373
+ }
374
+ }
375
+ return findings;
376
+ },
377
+ },
378
+
379
+ // COMP-A11Y-007: Non-descriptive link text
380
+ { id: 'COMP-A11Y-007', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Non-Descriptive Link Text',
381
+ check({ files }) {
382
+ const findings = [];
383
+ for (const [fp, c] of files) {
384
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
385
+ const matches = c.match(/<a[^>]*>\s*(?:click here|read more|here|more|link|this)\s*<\/a>/gi) || [];
386
+ if (matches.length > 0) {
387
+ findings.push({ ruleId: 'COMP-A11Y-007', category: 'compliance', severity: 'medium',
388
+ title: `${matches.length} link(s) with non-descriptive text ("click here", "read more")`, description: 'Screen reader users navigate by links. Use descriptive text like "Read the privacy policy" instead of "click here".', file: fp, fix: null });
389
+ }
390
+ }
391
+ return findings;
392
+ },
393
+ },
394
+
395
+ // COMP-EMAIL-001: No unsubscribe link in email templates
396
+ { id: 'COMP-EMAIL-001', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Email Template Missing Unsubscribe Link',
397
+ check({ files }) {
398
+ const findings = [];
399
+ for (const [fp, c] of files) {
400
+ if (!fp.match(/email|mail|template/i)) continue;
401
+ if (c.match(/<html|text\/html|sendMail|transporter\.send/i)) {
402
+ if (!c.match(/unsubscribe|opt.?out|manage.*preferences/i)) {
403
+ findings.push({ ruleId: 'COMP-EMAIL-001', category: 'compliance', severity: 'high',
404
+ title: 'Email template missing unsubscribe link (CAN-SPAM / GDPR)', description: 'All marketing emails must include an unsubscribe link. CAN-SPAM violations can result in $50k+ fines per email.', file: fp, fix: null });
405
+ }
406
+ }
407
+ }
408
+ return findings;
409
+ },
410
+ },
411
+
412
+ // COMP-EMAIL-002: No DMARC/SPF/DKIM reference
413
+ { id: 'COMP-EMAIL-002', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Email Authentication (SPF/DKIM/DMARC)',
414
+ check({ files }) {
415
+ const has = [...files.values()].some(c => c.match(/dmarc|spf|dkim|email.*auth/i));
416
+ if (!has && [...files.values()].some(c => c.match(/sendMail|sendgrid|nodemailer|smtp/i))) {
417
+ return [{ ruleId: 'COMP-EMAIL-002', category: 'compliance', severity: 'high',
418
+ title: 'No SPF/DKIM/DMARC email authentication configured', description: 'Without email authentication, your emails will be marked as spam and you are vulnerable to spoofing. Configure SPF, DKIM, and DMARC DNS records.', fix: null }];
419
+ }
420
+ return [];
421
+ },
422
+ },
423
+
424
+ // COMP-FIN-001: Payment card data stored
425
+ { id: 'COMP-FIN-001', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Payment Card Data Stored (PCI DSS Violation)',
426
+ check({ files }) {
427
+ const findings = [];
428
+ for (const [fp, c] of files) {
429
+ if (!isSourceFile(fp)) continue;
430
+ if (c.match(/cardNumber|card_number|creditCard|credit_card|cvv|cvc|expiry|expirationDate/i) &&
431
+ (c.match(/save|store|insert|create|log/i))) {
432
+ if (!c.match(/token|stripe|braintree|adyen/i)) {
433
+ findings.push({ ruleId: 'COMP-FIN-001', category: 'compliance', severity: 'critical',
434
+ title: 'Possible payment card data being stored (PCI DSS violation)', description: 'Never store raw card numbers, CVV, or expiry dates. Use Stripe/Braintree tokenization — they store card data, you store tokens.', file: fp, fix: null });
435
+ }
436
+ }
437
+ }
438
+ return findings;
439
+ },
440
+ },
441
+
442
+ // COMP-FIN-002: CVV stored after authorization
443
+ { id: 'COMP-FIN-002', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'CVV/CVC Stored After Authorization',
444
+ check({ files }) {
445
+ const findings = [];
446
+ for (const [fp, c] of files) {
447
+ if (!isSourceFile(fp)) continue;
448
+ if (c.match(/\bcvv\b|\bcvc\b|\bcvc2\b|\bcvv2\b/i) && c.match(/save|store|insert|db\.|redis\./i)) {
449
+ findings.push({ ruleId: 'COMP-FIN-002', category: 'compliance', severity: 'critical',
450
+ title: 'CVV/CVC value being stored — prohibited by PCI DSS', description: 'Storing CVV after authorization is a PCI DSS violation regardless of encryption. Remove all CVV storage.', file: fp, fix: null });
451
+ }
452
+ }
453
+ return findings;
454
+ },
455
+ },
456
+
457
+ // COMP-COPPA-001: No age verification — only fires on apps explicitly targeting children
458
+ { id: 'COMP-COPPA-001', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Age Verification on Registration',
459
+ check({ files }) {
460
+ const findings = [];
461
+ // Only fire if the app explicitly targets children (COPPA-specific keywords in code, not just any mention)
462
+ const hasChildrenTargeting = [...files.entries()].some(([fp, c]) =>
463
+ (fp.endsWith('package.json') || fp.endsWith('.json')) && /coppa|kids.*app|children.*platform|parental.*consent/i.test(c)
464
+ );
465
+ if (!hasChildrenTargeting) return findings;
466
+ const hasUsers = [...files.values()].some(c => c.includes('signup') || c.includes('register'));
467
+ const hasAgeGate = [...files.values()].some(c => c.match(/age.*gate|age.*verif|under.*13.*check|13.*under.*check/i));
468
+ if (hasUsers && !hasAgeGate) {
469
+ findings.push({ ruleId: 'COMP-COPPA-001', category: 'compliance', severity: 'high',
470
+ title: 'No age verification on user registration (COPPA risk)', description: "COPPA requires parental consent for users under 13. Add age verification or a 'I am over 13' confirmation to your signup flow.", fix: null });
471
+ }
472
+ return findings;
473
+ },
474
+ },
475
+
476
+ // COMP-INTL-001: No i18n framework
477
+ { id: 'COMP-INTL-001', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Internationalization Framework',
478
+ check({ files, stack }) {
479
+ const findings = [];
480
+ const allDeps = { ...stack.dependencies, ...stack.devDependencies };
481
+ const hasI18n = ['next-i18next', 'react-i18next', 'i18next', 'vue-i18n', 'lingui', 'formatjs', '@lingui/core'].some(d => d in allDeps);
482
+ if (!hasI18n && [...files.keys()].some(f => f.match(/\.(jsx|tsx)$/))) {
483
+ findings.push({ ruleId: 'COMP-INTL-001', category: 'compliance', severity: 'low',
484
+ title: 'No i18n framework detected — app cannot be easily localized', description: 'Add react-i18next or similar early. Retrofitting i18n into hardcoded strings is extremely costly.', fix: null });
485
+ }
486
+ return findings;
487
+ },
488
+ },
489
+
490
+ // COMP-INTL-002: Hardcoded currency symbol
491
+ { id: 'COMP-INTL-002', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Hardcoded Currency Symbol',
492
+ check({ files }) {
493
+ const findings = [];
494
+ for (const [fp, c] of files) {
495
+ if (!fp.match(/\.(jsx|tsx|html|js|ts)$/)) continue;
496
+ const lines = c.split('\n');
497
+ for (let i = 0; i < lines.length; i++) {
498
+ if (lines[i].match(/['"`]\$|['"`]€|['"`]£/) && lines[i].match(/price|amount|cost|total/i)) {
499
+ findings.push({ ruleId: 'COMP-INTL-002', category: 'compliance', severity: 'low',
500
+ title: 'Hardcoded currency symbol — not localization-friendly', description: 'Use Intl.NumberFormat with currency option for locale-aware currency formatting.', file: fp, line: i + 1, fix: null });
501
+ break;
502
+ }
503
+ }
504
+ }
505
+ return findings;
506
+ },
507
+ },
508
+
509
+ // COMP-SOC2-001: No security policy
510
+ { id: 'COMP-SOC2-001', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Security Policy Documentation',
511
+ check({ files }) {
512
+ const has = [...files.keys()].some(f => f.match(/security\.md|SECURITY\.md|security-policy/i));
513
+ if (!has) {
514
+ return [{ ruleId: 'COMP-SOC2-001', category: 'compliance', severity: 'low',
515
+ title: 'No SECURITY.md — missing vulnerability disclosure policy', description: 'Add a SECURITY.md with a responsible disclosure policy and contact information for reporting vulnerabilities.', fix: null }];
516
+ }
517
+ return [];
518
+ },
519
+ },
520
+
521
+ // COMP-SOC2-002: No penetration testing
522
+ { id: 'COMP-SOC2-002', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Penetration Testing Reference',
523
+ check({ files }) {
524
+ const has = [...files.values()].some(c => c.match(/pentest|penetration.test|security.audit|bug.bounty/i));
525
+ if (!has) {
526
+ return [{ ruleId: 'COMP-SOC2-002', category: 'compliance', severity: 'low',
527
+ title: 'No penetration testing or security audit documentation found', description: 'Schedule annual penetration tests. Document findings and remediations for SOC2/ISO27001 compliance.', fix: null }];
528
+ }
529
+ return [];
530
+ },
531
+ },
532
+
533
+ // COMP-A11Y-008: No axe-core testing
534
+ { id: 'COMP-A11Y-008', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Automated Accessibility Testing',
535
+ check({ files, stack }) {
536
+ const findings = [];
537
+ const allDeps = { ...stack.dependencies, ...stack.devDependencies };
538
+ const hasA11yTest = ['axe-core', '@axe-core/react', 'jest-axe', '@testing-library/jest-axe', 'cypress-axe', 'playwright-axe'].some(d => d in allDeps);
539
+ if (!hasA11yTest && [...files.keys()].some(f => f.match(/\.(jsx|tsx)$/))) {
540
+ findings.push({ ruleId: 'COMP-A11Y-008', category: 'compliance', severity: 'medium',
541
+ title: 'No automated accessibility testing (axe-core, jest-axe)',
542
+ description: 'Add jest-axe to catch accessibility violations in tests. Automated tools catch 30-40% of WCAG issues.', fix: null });
543
+ }
544
+ return findings;
545
+ },
546
+ },
547
+
548
+ // COMP-A11Y-009: Input with only placeholder (no label)
549
+ { id: 'COMP-A11Y-009', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Input Uses Only Placeholder as Label',
550
+ check({ files }) {
551
+ const findings = [];
552
+ for (const [fp, c] of files) {
553
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
554
+ const inputs = c.match(/<input[^>]*placeholder=[^>]*>/gi) || [];
555
+ for (const inp of inputs) {
556
+ if (!inp.match(/aria-label|aria-labelledby|id=/i)) {
557
+ const idx = c.indexOf(inp);
558
+ if (!c.substring(Math.max(0, idx - 200), idx).match(/<label/i)) {
559
+ findings.push({ ruleId: 'COMP-A11Y-009', category: 'compliance', severity: 'high',
560
+ title: 'Input relies on placeholder as the only label (WCAG 1.3.1)',
561
+ description: 'Placeholders disappear when typing and have low contrast. Use a visible <label> element instead.', file: fp, fix: null });
562
+ break;
563
+ }
564
+ }
565
+ }
566
+ }
567
+ return findings;
568
+ },
569
+ },
570
+
571
+ // COMP-A11Y-010: Modal without focus trap
572
+ { id: 'COMP-A11Y-010', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Modal Dialog Without Focus Trap',
573
+ check({ files, stack }) {
574
+ const findings = [];
575
+ if (!['react', 'nextjs', 'vue'].includes(stack.framework)) return findings;
576
+ const allDeps = { ...stack.dependencies, ...stack.devDependencies };
577
+ const hasFocusTrap = 'focus-trap-react' in allDeps || 'focus-trap' in allDeps || '@headlessui/react' in allDeps || 'radix-ui' in allDeps;
578
+ for (const [fp, c] of files) {
579
+ if (!fp.match(/\.(jsx|tsx)$/) || !c.match(/modal|dialog|overlay|drawer/i)) continue;
580
+ if (!hasFocusTrap && !c.match(/focusTrap|useFocusTrap|focus-trap|aria-modal/)) {
581
+ findings.push({ ruleId: 'COMP-A11Y-010', category: 'compliance', severity: 'high',
582
+ title: 'Modal component without focus trap — keyboard users can navigate behind modal',
583
+ description: 'Use focus-trap-react or @headlessui/react Dialog to trap focus within open modals (WCAG 2.4.3).', file: fp, fix: null });
584
+ break;
585
+ }
586
+ }
587
+ return findings;
588
+ },
589
+ },
590
+
591
+ // COMP-A11Y-011: No skip navigation link
592
+ { id: 'COMP-A11Y-011', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Skip Navigation Link',
593
+ check({ files, stack }) {
594
+ const findings = [];
595
+ if (!['react', 'nextjs', 'vue'].includes(stack.framework)) return findings;
596
+ const hasSkipLink = [...files.values()].some(c => c.match(/skip.*nav|skip.*content|skip.*main/i));
597
+ if (!hasSkipLink && [...files.keys()].some(f => f.match(/\.(jsx|tsx|html)$/))) {
598
+ findings.push({ ruleId: 'COMP-A11Y-011', category: 'compliance', severity: 'medium',
599
+ title: 'No "Skip to main content" link for keyboard navigation (WCAG 2.4.1)',
600
+ description: 'Add a visually-hidden skip link at the top of the page so keyboard users can bypass repeated navigation.', fix: null });
601
+ }
602
+ return findings;
603
+ },
604
+ },
605
+
606
+ // COMP-GDPR-006: No GDPR DPA reference
607
+ { id: 'COMP-GDPR-006', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Data Processing Agreement Reference',
608
+ check({ files }) {
609
+ const has = [...files.values()].some(c => c.match(/data.*processing.*agreement|DPA|processor.*agreement/i));
610
+ if (!has && [...files.values()].some(c => c.includes('signup') || c.includes('register'))) {
611
+ return [{ ruleId: 'COMP-GDPR-006', category: 'compliance', severity: 'low',
612
+ title: 'No Data Processing Agreement (DPA) documentation found',
613
+ description: 'If you process EU user data using third-party processors (AWS, Stripe, etc.), you need a DPA with each processor. Required under GDPR Article 28.', fix: null }];
614
+ }
615
+ return [];
616
+ },
617
+ },
618
+
619
+ // COMP-GDPR-007: Personal data in URL
620
+ { id: 'COMP-GDPR-007', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Personal Data Exposed in URL',
621
+ check({ files }) {
622
+ const findings = [];
623
+ for (const [fp, c] of files) {
624
+ if (!isSourceFile(fp)) continue;
625
+ if (c.match(/router\.\w+\s*\(\s*['"`][^'"]*:email|router\.\w+.*:name\b|router\.\w+.*:phone/)) {
626
+ findings.push({ ruleId: 'COMP-GDPR-007', category: 'compliance', severity: 'high',
627
+ title: 'Personal data (email/name/phone) as URL path parameter',
628
+ description: 'Personal data in URLs appears in browser history, server logs, and referrer headers. Use opaque IDs (UUIDs) in URLs instead.', file: fp, fix: null });
629
+ }
630
+ }
631
+ return findings;
632
+ },
633
+ },
634
+
635
+ // COMP-EMAIL-003: No double opt-in
636
+ { id: 'COMP-EMAIL-003', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Double Opt-In for Email Marketing',
637
+ check({ files }) {
638
+ const findings = [];
639
+ const hasEmailMarketing = [...files.values()].some(c =>
640
+ c.match(/subscribe|newsletter|mailing.?list|marketing.?email/i)
641
+ );
642
+ const hasDoubleOptIn = [...files.values()].some(c =>
643
+ c.match(/confirm.*email|verify.*email|double.*opt|confirmation.*link|email.*verify/i)
644
+ );
645
+ if (hasEmailMarketing && !hasDoubleOptIn) {
646
+ findings.push({ ruleId: 'COMP-EMAIL-003', category: 'compliance', severity: 'medium',
647
+ title: 'Email subscription without double opt-in confirmation',
648
+ description: 'GDPR best practice requires double opt-in for marketing emails. Send a confirmation email before adding users to marketing lists.', fix: null });
649
+ }
650
+ return findings;
651
+ },
652
+ },
653
+
654
+ // COMP-FIN-003: Payment page not on HTTPS
655
+ { id: 'COMP-FIN-003', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Payment Form Served Over HTTP',
656
+ check({ files }) {
657
+ const findings = [];
658
+ for (const [fp, c] of files) {
659
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
660
+ if (c.match(/credit.?card|card.*number|payment.*form|checkout/i)) {
661
+ const hasHTTPS = [...files.values()].some(content =>
662
+ content.match(/https:\/\/|HTTPS_ONLY|force.*https|http.*redirect.*https/i)
663
+ );
664
+ if (!hasHTTPS) {
665
+ findings.push({ ruleId: 'COMP-FIN-003', category: 'compliance', severity: 'critical',
666
+ title: 'Payment/checkout form without explicit HTTPS enforcement',
667
+ description: 'All payment pages must use HTTPS. Configure HSTS and redirect HTTP to HTTPS at the server/CDN level.', file: fp, fix: null });
668
+ }
669
+ }
670
+ }
671
+ return findings;
672
+ },
673
+ },
674
+
675
+ // COMP-SOC2-003: No incident response plan
676
+ { id: 'COMP-SOC2-003', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Incident Response Plan',
677
+ check({ files }) {
678
+ const has = [...files.keys()].some(f => f.match(/incident|runbook|playbook/i)) ||
679
+ [...files.values()].some(c => c.match(/incident.*response|on.?call|pagerduty|opsgenie/i));
680
+ if (!has) {
681
+ return [{ ruleId: 'COMP-SOC2-003', category: 'compliance', severity: 'low',
682
+ title: 'No incident response plan or runbook detected',
683
+ description: 'Document your incident response process: how to detect, escalate, communicate, and remediate incidents. Required for SOC2 Type II.', fix: null }];
684
+ }
685
+ return [];
686
+ },
687
+ },
688
+
689
+ // COMP-INTL-003: Hardcoded English error messages
690
+ { id: 'COMP-INTL-003', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Hardcoded English Error Messages',
691
+ check({ files }) {
692
+ const findings = [];
693
+ for (const [fp, c] of files) {
694
+ if (!fp.match(/\.(jsx|tsx)$/)) continue;
695
+ const hardcoded = (c.match(/>(?:[A-Z][^<>{}\n]{5,30})<\//g) || [])
696
+ .filter(m => !m.match(/^\s*{/) && m.match(/\s/) && !m.match(/^\d/)).length;
697
+ if (hardcoded > 10) {
698
+ findings.push({ ruleId: 'COMP-INTL-003', category: 'compliance', severity: 'low',
699
+ title: `${hardcoded} potentially hardcoded English strings in UI component`,
700
+ description: 'Extract user-visible strings to i18n translation keys. Hardcoded strings cannot be localized.', file: fp, fix: null });
701
+ }
702
+ }
703
+ return findings;
704
+ },
705
+ },
706
+
707
+ // COMP-INTL-004: No locale-aware number formatting
708
+ { id: 'COMP-INTL-004', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Locale-Aware Number Formatting',
709
+ check({ files }) {
710
+ const findings = [];
711
+ for (const [fp, c] of files) {
712
+ if (!isSourceFile(fp)) continue;
713
+ if (c.match(/\.toFixed\s*\(\s*2\s*\)/) && c.match(/price|amount|cost|total/i)) {
714
+ if (!c.match(/Intl\.NumberFormat|toLocaleString/)) {
715
+ findings.push({ ruleId: 'COMP-INTL-004', category: 'compliance', severity: 'low',
716
+ title: 'Using .toFixed(2) for currency display instead of Intl.NumberFormat',
717
+ description: 'Use new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }).format(amount) for locale-aware currency display.', file: fp, fix: null });
718
+ }
719
+ }
720
+ }
721
+ return findings;
722
+ },
723
+ },
724
+
725
+ // COMP-CCPA-001: No "Do Not Sell" link
726
+ { id: 'COMP-CCPA-001', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No "Do Not Sell My Personal Information" Link',
727
+ check({ files }) {
728
+ const findings = [];
729
+ const servesCalifornia = true; // assume any web app may serve California users
730
+ const hasDoNotSell = [...files.values()].some(c => c.match(/do not sell|doNotSell|opt.out.*sale|data.*sale/i));
731
+ const hasUsers = [...files.values()].some(c => c.includes('signup') || c.includes('register'));
732
+ if (hasUsers && !hasDoNotSell) {
733
+ findings.push({ ruleId: 'COMP-CCPA-001', category: 'compliance', severity: 'high',
734
+ title: 'No "Do Not Sell My Personal Information" link (CCPA requirement)',
735
+ description: 'California businesses that sell personal data must provide a "Do Not Sell" link. Add it to your privacy policy and footer.', fix: null });
736
+ }
737
+ return findings;
738
+ },
739
+ },
740
+
741
+ // COMP-COPPA-002: Age verification — only for children-targeting apps
742
+ { id: 'COMP-COPPA-002', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Minimum Age Validation',
743
+ check({ files }) {
744
+ const findings = [];
745
+ const hasChildrenTargeting = [...files.entries()].some(([fp, c]) =>
746
+ (fp.endsWith('package.json') || fp.endsWith('.json')) && /coppa|kids.*app|children.*platform/i.test(c)
747
+ );
748
+ if (!hasChildrenTargeting) return findings;
749
+ for (const [fp, c] of files) {
750
+ if (!isSourceFile(fp)) continue;
751
+ if (c.match(/birthdate|dateOfBirth|dob|age/i) && c.match(/register|signup|create.*user/i)) {
752
+ if (!c.match(/age.*>=?\s*13|13.*years|minimumAge|minAge/)) {
753
+ findings.push({ ruleId: 'COMP-COPPA-002', category: 'compliance', severity: 'high',
754
+ title: 'Age field collected but no minimum age validation (COPPA requires 13+)',
755
+ description: 'Validate that users are at least 13. For users under 13, COPPA requires verifiable parental consent before collecting any personal information.', file: fp, fix: null });
756
+ }
757
+ }
758
+ }
759
+ return findings;
760
+ },
761
+ },
762
+
763
+ // COMP-GDPR-008: No cookie consent management
764
+ { id: 'COMP-GDPR-008', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Cookie Consent Management',
765
+ check({ files }) {
766
+ const findings = [];
767
+ const allDeps = { ...Object.fromEntries([...files.keys()].filter(f => f.endsWith('package.json')).flatMap(f => { try { return Object.entries(JSON.parse([...files.values()][0])); } catch { return []; } })) };
768
+ const hasCookieConsent = [...files.values()].some(c => c.match(/cookieconsent|cookie.*consent|CookieBanner|gdpr.*cookie|OneTrust|Cookiebot|Consent.*Manager/i));
769
+ const hasCookies = [...files.values()].some(c => c.match(/res\.cookie\(|document\.cookie|cookie-parser|cookieSession/i));
770
+ if (hasCookies && !hasCookieConsent) {
771
+ findings.push({ ruleId: 'COMP-GDPR-008', category: 'compliance', severity: 'high', title: 'Cookies set without cookie consent management — GDPR ePrivacy Directive violation', description: 'Add a cookie consent banner. Non-essential cookies (analytics, advertising) require explicit consent under GDPR/ePrivacy. Use OneTrust, Cookiebot, or react-cookieconsent.', fix: null });
772
+ }
773
+ return findings;
774
+ },
775
+ },
776
+
777
+ // COMP-GDPR-009: No privacy policy link
778
+ { id: 'COMP-GDPR-009', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Privacy Policy Link',
779
+ check({ files }) {
780
+ const findings = [];
781
+ const hasPrivacyLink = [...files.values()].some(c => c.match(/privacy.*policy|privacy-policy|\/privacy/i));
782
+ const hasHTML = [...files.keys()].some(f => f.match(/\.(html|jsx|tsx|vue)$/));
783
+ if (hasHTML && !hasPrivacyLink) {
784
+ findings.push({ ruleId: 'COMP-GDPR-009', category: 'compliance', severity: 'high', title: 'No privacy policy link found — required under GDPR, CCPA, and most privacy laws', description: "Add a link to your privacy policy. GDPR Article 13 requires informing users about data collection. Include in footer, registration, and checkout.", fix: null });
785
+ }
786
+ return findings;
787
+ },
788
+ },
789
+
790
+ // COMP-GDPR-010: No terms of service
791
+ { id: 'COMP-GDPR-010', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Terms of Service',
792
+ check({ files }) {
793
+ const findings = [];
794
+ const hasTOS = [...files.values()].some(c => c.match(/terms.*of.*service|terms-of-service|\/tos|terms.*conditions|\/terms/i));
795
+ const hasHTML = [...files.keys()].some(f => f.match(/\.(html|jsx|tsx|vue)$/));
796
+ if (hasHTML && !hasTOS) {
797
+ findings.push({ ruleId: 'COMP-GDPR-010', category: 'compliance', severity: 'medium', title: 'No Terms of Service page linked', description: 'Add Terms of Service. Required for enforceability of your service terms and protection against abuse. Link from registration and footer.', fix: null });
798
+ }
799
+ return findings;
800
+ },
801
+ },
802
+
803
+ // COMP-A11Y-012: Color contrast — inline styles with low contrast colors
804
+ { id: 'COMP-A11Y-012', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Potential Low Color Contrast (WCAG 2.1)',
805
+ check({ files }) {
806
+ const findings = [];
807
+ for (const [fp, c] of files) {
808
+ if (!fp.match(/\.(css|scss|sass|less|styl)$/)) continue;
809
+ const lines = c.split('\n');
810
+ for (let i = 0; i < lines.length; i++) {
811
+ if (lines[i].match(/color:\s*#[a-fA-F0-9]{3,6}/) && lines[i].match(/#(?:ccc|ddd|eee|f0f0f0|e0e0e0|bbb|aaa|999)/i)) {
812
+ findings.push({ ruleId: 'COMP-A11Y-012', category: 'compliance', severity: 'medium', title: 'Potentially low contrast color detected — WCAG 2.1 AA requires 4.5:1 ratio', description: 'Use a contrast checker (WebAIM, axe). Text must have 4.5:1 contrast ratio (AA) or 7:1 (AAA). Failing contrast affects low-vision users.', file: fp, line: i + 1, fix: null });
813
+ }
814
+ }
815
+ }
816
+ return findings;
817
+ },
818
+ },
819
+
820
+ // COMP-A11Y-013: Interactive elements too small (touch target)
821
+ { id: 'COMP-A11Y-013', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Touch Target Too Small (WCAG 2.5.5)',
822
+ check({ files }) {
823
+ const findings = [];
824
+ for (const [fp, c] of files) {
825
+ if (!fp.match(/\.(css|scss|sass|less)$/)) continue;
826
+ const lines = c.split('\n');
827
+ for (let i = 0; i < lines.length; i++) {
828
+ if (lines[i].match(/(?:button|\.btn|a\s*\{)[^}]*/i)) {
829
+ const block = lines.slice(i, i + 10).join('\n');
830
+ if (block.match(/height:\s*(\d+)px/) && parseInt((block.match(/height:\s*(\d+)px/) || ['', '0'])[1]) < 44) {
831
+ findings.push({ ruleId: 'COMP-A11Y-013', category: 'compliance', severity: 'medium', title: 'Button/link height below 44px — WCAG 2.5.5 minimum touch target size', description: 'Make interactive elements at least 44x44px. Small touch targets cause mis-taps on mobile devices, especially for users with motor disabilities.', file: fp, line: i + 1, fix: null });
832
+ }
833
+ }
834
+ }
835
+ }
836
+ return findings;
837
+ },
838
+ },
839
+
840
+ // COMP-A11Y-014: No ARIA live regions for dynamic content
841
+ { id: 'COMP-A11Y-014', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Dynamic Content Without ARIA Live Region',
842
+ check({ files }) {
843
+ const findings = [];
844
+ for (const [fp, c] of files) {
845
+ if (!isSourceFile(fp)) continue;
846
+ if (c.match(/toast|notification|alert|snackbar|spinner|loading/i) && !c.match(/aria-live|role="alert"|role="status"|aria-atomic/i)) {
847
+ findings.push({ ruleId: 'COMP-A11Y-014', category: 'compliance', severity: 'medium', title: 'Dynamic content (notifications/alerts) without aria-live region', description: 'Add aria-live="polite" or role="alert" to notification containers. Screen readers cannot detect content changes without ARIA live regions.', file: fp, fix: null });
848
+ }
849
+ }
850
+ return findings;
851
+ },
852
+ },
853
+
854
+ // COMP-A11Y-015: Form without autocomplete attributes
855
+ { id: 'COMP-A11Y-015', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Form Inputs Without autocomplete Attribute',
856
+ check({ files }) {
857
+ const findings = [];
858
+ for (const [fp, c] of files) {
859
+ if (!fp.match(/\.(html|jsx|tsx|vue)$/)) continue;
860
+ const lines = c.split('\n');
861
+ for (let i = 0; i < lines.length; i++) {
862
+ if (lines[i].match(/<input[^>]+type=["'](email|tel|name|address)['"]/i) && !lines[i].match(/autocomplete=/i)) {
863
+ findings.push({ ruleId: 'COMP-A11Y-015', category: 'compliance', severity: 'low', title: 'Input missing autocomplete attribute — harder to use for assistive technology', description: 'Add autocomplete="email", autocomplete="name", etc. Autocomplete helps users with memory and motor disabilities fill forms faster.', file: fp, line: i + 1, fix: null });
864
+ }
865
+ }
866
+ }
867
+ return findings;
868
+ },
869
+ },
870
+
871
+ // COMP-PCI-001: Credit card number in logs
872
+ { id: 'COMP-PCI-001', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Credit Card Number Potentially Logged',
873
+ check({ files }) {
874
+ const findings = [];
875
+ for (const [fp, c] of files) {
876
+ if (!isSourceFile(fp)) continue;
877
+ const lines = c.split('\n');
878
+ for (let i = 0; i < lines.length; i++) {
879
+ if (lines[i].match(/console\.|logger\./i) && lines[i].match(/card|cardNumber|card_number|creditCard|pan\b/i)) {
880
+ findings.push({ ruleId: 'COMP-PCI-001', category: 'compliance', severity: 'critical', title: 'Payment card data potentially logged — PCI DSS Requirement 3 violation', description: 'PCI DSS forbids logging PANs. Remove card data from logs immediately. Violations trigger mandatory breach notification and fines.', file: fp, line: i + 1, fix: null });
881
+ }
882
+ }
883
+ }
884
+ return findings;
885
+ },
886
+ },
887
+
888
+ // COMP-PCI-002: Direct Stripe/Braintree keys in client-side code
889
+ { id: 'COMP-PCI-002', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Payment Provider Secret Key in Client-Side Code',
890
+ check({ files }) {
891
+ const findings = [];
892
+ for (const [fp, c] of files) {
893
+ if (!fp.match(/\.(js|jsx|ts|tsx)$/) || fp.includes('test') || fp.includes('spec')) continue;
894
+ if (fp.match(/public|client|frontend|browser/i) || c.match(/window\.|document\.|React/)) {
895
+ if (c.match(/sk_live_|rk_live_|STRIPE_SECRET|braintree.*privateKey/i)) {
896
+ findings.push({ ruleId: 'COMP-PCI-002', category: 'compliance', severity: 'critical', title: 'Payment secret key exposed in client-side code — PCI DSS violation', description: 'Never include sk_live_ or other secret keys in client-side code. Secret keys must only be used server-side. Rotate immediately if exposed.', file: fp, fix: null });
897
+ }
898
+ }
899
+ }
900
+ return findings;
901
+ },
902
+ },
903
+
904
+ // COMP-PCI-003: No TLS on payment endpoints
905
+ { id: 'COMP-PCI-003', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Payment Endpoint Without TLS Enforcement',
906
+ check({ files }) {
907
+ const findings = [];
908
+ for (const [fp, c] of files) {
909
+ if (!isSourceFile(fp)) continue;
910
+ if (c.match(/\/payment|\/checkout|\/charge|\/purchase|stripe\.|braintree\./i)) {
911
+ if (!c.match(/https|HTTPS|ssl|SSL|tls|TLS|secure.*true/i)) {
912
+ findings.push({ ruleId: 'COMP-PCI-003', category: 'compliance', severity: 'critical', title: 'Payment processing code without explicit TLS enforcement', description: 'PCI DSS requires TLS 1.2+ for all payment data transmission. Verify all payment endpoints use HTTPS and enforce it in code.', file: fp, fix: null });
913
+ }
914
+ }
915
+ }
916
+ return findings;
917
+ },
918
+ },
919
+
920
+ // COMP-SOC2-004: No access review process
921
+ { id: 'COMP-SOC2-004', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Access Review or Offboarding Process',
922
+ check({ files }) {
923
+ const findings = [];
924
+ const allPaths = [...files.keys()].join(' ');
925
+ const allCode = [...files.values()].join('\n');
926
+ const hasAccessReview = allCode.match(/offboard|access.*review|user.*deactivat|disable.*account|revoke.*access/i) || allPaths.match(/offboard|access-review/i);
927
+ if (!hasAccessReview) {
928
+ findings.push({ ruleId: 'COMP-SOC2-004', category: 'compliance', severity: 'medium', title: 'No access review or offboarding process detected — SOC 2 CC6.3 requirement', description: 'Implement user deactivation on employee offboarding. SOC 2 requires periodic access reviews and prompt revocation when employees leave.', fix: null });
929
+ }
930
+ return findings;
931
+ },
932
+ },
933
+
934
+ // COMP-SOC2-005: No multi-factor authentication for admin
935
+ { id: 'COMP-SOC2-005', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No MFA for Admin/Privileged Access',
936
+ check({ files }) {
937
+ const findings = [];
938
+ const allCode = [...files.values()].join('\n');
939
+ const hasAdminRoute = allCode.match(/\/admin|role.*admin|isAdmin|superuser/i);
940
+ const hasMFA = allCode.match(/totp|speakeasy|mfa|two.*factor|2fa|authenticator/i);
941
+ if (hasAdminRoute && !hasMFA) {
942
+ findings.push({ ruleId: 'COMP-SOC2-005', category: 'compliance', severity: 'high', title: 'Admin routes without MFA — SOC 2 CC6.1 and PCI DSS requirement', description: 'Require MFA (TOTP/hardware key) for admin accounts. SOC 2, PCI DSS, and most cyber insurance policies mandate MFA for privileged access.', fix: null });
943
+ }
944
+ return findings;
945
+ },
946
+ },
947
+
948
+ // COMP-HIPAA-001: Health data without encryption
949
+ { id: 'COMP-HIPAA-001', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Health Information Without Encryption (HIPAA)',
950
+ check({ files }) {
951
+ const findings = [];
952
+ for (const [fp, c] of files) {
953
+ if (!isSourceFile(fp)) continue;
954
+ const hasPHI = c.match(/diagnosis|medication|prescription|healthRecord|medicalHistory|patientData|ehr|phi\b/i);
955
+ const hasEncryption = c.match(/encrypt|AES|cipher|kms|vault/i);
956
+ if (hasPHI && !hasEncryption) {
957
+ findings.push({ ruleId: 'COMP-HIPAA-001', category: 'compliance', severity: 'critical', title: 'Protected Health Information (PHI) without encryption at rest — HIPAA violation', description: 'Encrypt PHI at rest and in transit. HIPAA requires technical safeguards for PHI. Violations carry fines up to $1.9M per violation category.', file: fp, fix: null });
958
+ }
959
+ }
960
+ return findings;
961
+ },
962
+ },
963
+
964
+ // COMP-HIPAA-002: PHI logged without redaction
965
+ { id: 'COMP-HIPAA-002', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Protected Health Information in Logs',
966
+ check({ files }) {
967
+ const findings = [];
968
+ for (const [fp, c] of files) {
969
+ if (!isSourceFile(fp)) continue;
970
+ const lines = c.split('\n');
971
+ for (let i = 0; i < lines.length; i++) {
972
+ if (lines[i].match(/console\.|logger\./i) && lines[i].match(/diagnosis|medication|prescription|patientId|healthRecord/i)) {
973
+ findings.push({ ruleId: 'COMP-HIPAA-002', category: 'compliance', severity: 'critical', title: 'PHI potentially in application logs — HIPAA violation', description: 'Redact health information from logs. HIPAA Security Rule prohibits storing PHI in log files without proper controls. Use patient IDs instead.', file: fp, line: i + 1, fix: null });
974
+ }
975
+ }
976
+ }
977
+ return findings;
978
+ },
979
+ },
980
+
981
+ // COMP-INTL-005: Hardcoded locale-specific phone format
982
+ { id: 'COMP-INTL-005', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Hardcoded US Phone Format',
983
+ check({ files }) {
984
+ const findings = [];
985
+ for (const [fp, c] of files) {
986
+ if (!isSourceFile(fp)) continue;
987
+ const lines = c.split('\n');
988
+ for (let i = 0; i < lines.length; i++) {
989
+ if (lines[i].match(/phone.*regex|\\d\{3\}.*\\d\{4\}|[0-9]{3}.*[0-9]{4}/)) {
990
+ findings.push({ ruleId: 'COMP-INTL-005', category: 'compliance', severity: 'low', title: 'US-format phone number regex — rejects international numbers', description: 'Use libphonenumber-js for phone validation. Hardcoded US format (###-###-####) rejects valid international numbers.', file: fp, line: i + 1, fix: null });
991
+ }
992
+ }
993
+ }
994
+ return findings;
995
+ },
996
+ },
997
+
998
+ // COMP-INTL-006: No RTL support
999
+ { id: 'COMP-INTL-006', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No RTL (Right-to-Left) Language Support',
1000
+ check({ files }) {
1001
+ const findings = [];
1002
+ const hasI18n = [...files.values()].some(c => c.match(/i18n|react-intl|i18next|vue-i18n|linguiJS/i));
1003
+ const hasRTL = [...files.values()].some(c => c.match(/dir=["']rtl|direction.*rtl|rtl-css|stylelint.*rtl/i));
1004
+ if (hasI18n && !hasRTL) {
1005
+ findings.push({ ruleId: 'COMP-INTL-006', category: 'compliance', severity: 'low', title: 'i18n configured but no RTL support — Arabic/Hebrew/Farsi users get broken layout', description: 'Add dir={dir} to root element and test with RTL languages. CSS logical properties (margin-inline-start) handle RTL automatically.', fix: null });
1006
+ }
1007
+ return findings;
1008
+ },
1009
+ },
1010
+
1011
+ // COMP-INTL-007: Date format without locale
1012
+ { id: 'COMP-INTL-007', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Date Formatted Without Locale Awareness',
1013
+ check({ files }) {
1014
+ const findings = [];
1015
+ for (const [fp, c] of files) {
1016
+ if (!isSourceFile(fp)) continue;
1017
+ const lines = c.split('\n');
1018
+ for (let i = 0; i < lines.length; i++) {
1019
+ if (lines[i].match(/\.toLocaleDateString\(\)|\.toLocaleString\(\)/) && !lines[i].match(/locale|language|Intl\./)) {
1020
+ findings.push({ ruleId: 'COMP-INTL-007', category: 'compliance', severity: 'low', title: 'toLocaleDateString() called without locale parameter — uses browser locale, inconsistent', description: "Pass explicit locale: date.toLocaleDateString('en-US', {...}). Without locale, output varies by user's browser settings.", file: fp, line: i + 1, fix: null });
1021
+ }
1022
+ }
1023
+ }
1024
+ return findings;
1025
+ },
1026
+ },
1027
+
1028
+ // COMP-INTL-008: No pluralization support
1029
+ { id: 'COMP-INTL-008', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Hardcoded Plural Forms Without i18n',
1030
+ check({ files }) {
1031
+ const findings = [];
1032
+ for (const [fp, c] of files) {
1033
+ if (!isSourceFile(fp)) continue;
1034
+ const lines = c.split('\n');
1035
+ for (let i = 0; i < lines.length; i++) {
1036
+ if (lines[i].match(/count === 1 \? ['"].*['"]\s*:\s*['"]/i) || lines[i].match(/n === 1 \? ['"].*['"]\s*:\s*['"]/i)) {
1037
+ findings.push({ ruleId: 'COMP-INTL-008', category: 'compliance', severity: 'low', title: 'Hardcoded English singular/plural — Arabic/Russian have 6 plural forms', description: "Use Intl.PluralRules or i18n library for pluralization. English has 2 plural forms; many languages have 3-6. Hardcoded English plurals break internationally.", file: fp, line: i + 1, fix: null });
1038
+ }
1039
+ }
1040
+ }
1041
+ return findings;
1042
+ },
1043
+ },
1044
+
1045
+ // COMP-GDPR-011: No data breach notification procedure
1046
+ { id: 'COMP-GDPR-011', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Data Breach Notification Procedure',
1047
+ check({ files }) {
1048
+ const findings = [];
1049
+ const allPaths = [...files.keys()].join(' ');
1050
+ const allCode = [...files.values()].join('\n');
1051
+ const hasBreachPlan = allCode.match(/breach.*notification|incident.*response|data.*breach|notify.*72.*hours|72h/i) || allPaths.match(/breach|incident/i);
1052
+ if (!hasBreachPlan) {
1053
+ findings.push({ ruleId: 'COMP-GDPR-011', category: 'compliance', severity: 'medium', title: 'No data breach notification procedure detected — GDPR requires 72-hour notification', description: 'Document your breach notification process. GDPR Article 33 requires notifying the supervisory authority within 72 hours of discovering a breach.', fix: null });
1054
+ }
1055
+ return findings;
1056
+ },
1057
+ },
1058
+
1059
+ // COMP-CCPA-002: No opt-out mechanism for data sale
1060
+ { id: 'COMP-CCPA-002', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Data Sale Opt-Out Mechanism',
1061
+ check({ files }) {
1062
+ const findings = [];
1063
+ const allCode = [...files.values()].join('\n');
1064
+ const hasAnalytics = allCode.match(/google.*analytics|mixpanel|segment|amplitude|facebook.*pixel|gtag/i);
1065
+ const hasOptOut = allCode.match(/opt.*out|doNotSell|do-not-sell|optOut.*sale/i);
1066
+ if (hasAnalytics && !hasOptOut) {
1067
+ findings.push({ ruleId: 'COMP-CCPA-002', category: 'compliance', severity: 'high', title: 'Analytics/tracking without CCPA opt-out mechanism', description: 'California residents have the right to opt-out of data selling. Add opt-out mechanism and honor Global Privacy Control (GPC) header.', fix: null });
1068
+ }
1069
+ return findings;
1070
+ },
1071
+ },
1072
+
1073
+ // COMP-GDPR-012: localStorage contains personal data
1074
+ { id: 'COMP-GDPR-012', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Personal Data Stored in localStorage',
1075
+ check({ files }) {
1076
+ const findings = [];
1077
+ for (const [fp, c] of files) {
1078
+ if (!isSourceFile(fp)) continue;
1079
+ const lines = c.split('\n');
1080
+ for (let i = 0; i < lines.length; i++) {
1081
+ if (lines[i].match(/localStorage\.setItem\(/) && lines[i].match(/email|name|phone|address|ssn|dob|passport/i)) {
1082
+ findings.push({ ruleId: 'COMP-GDPR-012', category: 'compliance', severity: 'high', title: 'Personal data stored in localStorage — accessible by any JS on the page', description: 'Avoid storing PII in localStorage. It persists indefinitely and is accessible to XSS. Store user IDs only; fetch profile data from API.', file: fp, line: i + 1, fix: null });
1083
+ }
1084
+ }
1085
+ }
1086
+ return findings;
1087
+ },
1088
+ },
1089
+
1090
+ // COMP-GDPR-013: Third-party tracking before consent
1091
+ { id: 'COMP-GDPR-013', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Third-Party Tracking Loaded Unconditionally',
1092
+ check({ files }) {
1093
+ const findings = [];
1094
+ for (const [fp, c] of files) {
1095
+ if (!fp.match(/\.(html|jsx|tsx|js)$/)) continue;
1096
+ const hasTracking = c.match(/facebook.*pixel|fbq\(|\_fbq|google.*analytics|gtag\(|ga\(|mixpanel\.init|amplitude\.init/i);
1097
+ const hasConsentCheck = c.match(/consent|hasConsented|cookieConsent|gdprConsent/i);
1098
+ if (hasTracking && !hasConsentCheck) {
1099
+ findings.push({ ruleId: 'COMP-GDPR-013', category: 'compliance', severity: 'high', title: 'Third-party tracking pixel/analytics loaded without consent check', description: 'Only load tracking after consent: if (hasConsented()) { loadAnalytics(); }. Loading before consent violates GDPR and ePrivacy.', file: fp, fix: null });
1100
+ }
1101
+ }
1102
+ return findings;
1103
+ },
1104
+ },
1105
+
1106
+ // COMP-A11Y-016: No error summary for form validation
1107
+ { id: 'COMP-A11Y-016', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Error Summary for Form Validation (WCAG 3.3.1)',
1108
+ check({ files }) {
1109
+ const findings = [];
1110
+ for (const [fp, c] of files) {
1111
+ if (!isSourceFile(fp)) continue;
1112
+ if (c.match(/onSubmit|handleSubmit/i) && c.match(/error|invalid|validation/i)) {
1113
+ if (!c.match(/aria-describedby|aria-invalid|role=["']alert|error.*summary/i)) {
1114
+ findings.push({ ruleId: 'COMP-A11Y-016', category: 'compliance', severity: 'medium', title: 'Form without accessible error announcements — WCAG 3.3.1', description: 'Use aria-invalid="true" and aria-describedby to link fields to error messages. Screen readers need programmatic error associations.', file: fp, fix: null });
1115
+ }
1116
+ }
1117
+ }
1118
+ return findings;
1119
+ },
1120
+ },
1121
+
1122
+ // COMP-A11Y-017: PDF/documents without accessible text
1123
+ { id: 'COMP-A11Y-017', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'PDF Generation Without Accessibility Tags',
1124
+ check({ files, stack }) {
1125
+ const findings = [];
1126
+ const allDeps = { ...stack.dependencies, ...stack.devDependencies };
1127
+ const hasPDF = ['puppeteer', 'playwright', 'pdfkit', 'jspdf', 'html2pdf.js', '@react-pdf/renderer'].some(d => d in allDeps);
1128
+ const hasA11y = [...files.values()].some(c => c.match(/tagged.*pdf|PDF\/UA|accessibility.*pdf|pdf.*alt/i));
1129
+ if (hasPDF && !hasA11y) {
1130
+ findings.push({ ruleId: 'COMP-A11Y-017', category: 'compliance', severity: 'medium', title: 'PDF generated without accessibility tags (PDF/UA)', description: 'Generate PDFs with tagged structure (headings, alt text, reading order). Required for ADA/Section 508 compliance for public-facing documents.', fix: null });
1131
+ }
1132
+ return findings;
1133
+ },
1134
+ },
1135
+
1136
+ // COMP-A11Y-018: No keyboard navigation for dropdown menus
1137
+ { id: 'COMP-A11Y-018', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Dropdown Menu Without Keyboard Navigation',
1138
+ check({ files }) {
1139
+ const findings = [];
1140
+ for (const [fp, c] of files) {
1141
+ if (!isSourceFile(fp)) continue;
1142
+ if (c.match(/dropdown|menu.*open|toggle.*menu/i) && !c.match(/onKeyDown|onKeyPress|keyCode|ArrowDown|ArrowUp|Escape/i)) {
1143
+ findings.push({ ruleId: 'COMP-A11Y-018', category: 'compliance', severity: 'medium', title: 'Dropdown menu without keyboard navigation — WCAG 2.1.1', description: 'Handle arrow keys, Enter, Escape for menus. WCAG 2.1.1 requires all functionality operable via keyboard. Use WAI-ARIA menu role.', file: fp, fix: null });
1144
+ }
1145
+ }
1146
+ return findings;
1147
+ },
1148
+ },
1149
+
1150
+ // COMP-SOC2-006: No change management process
1151
+ { id: 'COMP-SOC2-006', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Change Management Documentation',
1152
+ check({ files }) {
1153
+ const findings = [];
1154
+ const allPaths = [...files.keys()].join(' ');
1155
+ const hasChangeManagement = allPaths.match(/CHANGELOG|change.*log|CHANGES|CONTRIBUTING|change.*control/i) || [...files.values()].some(c => c.match(/change.*management|change.*control|change.*review/i));
1156
+ if (!hasChangeManagement) {
1157
+ findings.push({ ruleId: 'COMP-SOC2-006', category: 'compliance', severity: 'medium', title: 'No CHANGELOG or change management documentation — SOC 2 CC8.1 requirement', description: 'Maintain a CHANGELOG.md and document change management process. SOC 2 requires tracking changes to production systems.', fix: null });
1158
+ }
1159
+ return findings;
1160
+ },
1161
+ },
1162
+
1163
+ // COMP-SOC2-007: No vendor security assessment
1164
+ { id: 'COMP-SOC2-007', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Vendor Security Assessment',
1165
+ check({ files, stack }) {
1166
+ const findings = [];
1167
+ const allDeps = { ...stack.dependencies, ...stack.devDependencies };
1168
+ const criticalVendors = ['stripe', 'twilio', 'sendgrid', 'aws-sdk', '@aws-sdk/client-s3', 'firebase'];
1169
+ const hasVendor = criticalVendors.some(v => v in allDeps);
1170
+ const hasAssessment = [...files.values()].some(c => c.match(/vendor.*assessment|third.*party.*review|supplier.*security/i));
1171
+ if (hasVendor && !hasAssessment) {
1172
+ findings.push({ ruleId: 'COMP-SOC2-007', category: 'compliance', severity: 'low', title: 'Critical vendor integrations without documented security assessment', description: 'Document security assessments for vendors handling your data (Stripe, Twilio, AWS). SOC 2 Availability CC9.1 requires vendor risk management.', fix: null });
1173
+ }
1174
+ return findings;
1175
+ },
1176
+ },
1177
+
1178
+ // COMP-PCI-004: Weak TLS cipher suites for payment
1179
+ { id: 'COMP-PCI-004', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Weak TLS Ciphers for Payment Processing',
1180
+ check({ files }) {
1181
+ const findings = [];
1182
+ for (const [fp, c] of files) {
1183
+ if (!isSourceFile(fp)) continue;
1184
+ if (c.match(/stripe|braintree|payment|checkout/i) && c.match(/ciphers:/i)) {
1185
+ if (c.match(/RC4|DES|3DES|MD5|NULL|EXPORT|anon/i)) {
1186
+ findings.push({ ruleId: 'COMP-PCI-004', category: 'compliance', severity: 'high', title: 'Weak TLS cipher suite configured for payment endpoint — PCI DSS violation', description: 'PCI DSS 4.0 requires TLS 1.2+ with strong ciphers. Disable RC4, DES, 3DES, EXPORT ciphers. Use modern ECDHE ciphersuites.', file: fp, fix: null });
1187
+ }
1188
+ }
1189
+ }
1190
+ return findings;
1191
+ },
1192
+ },
1193
+
1194
+ // COMP-HIPAA-003: Audit log missing for PHI access
1195
+ { id: 'COMP-HIPAA-003', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Audit Log for PHI Access',
1196
+ check({ files }) {
1197
+ const findings = [];
1198
+ const allCode = [...files.values()].join('\n');
1199
+ const hasPHI = allCode.match(/diagnosis|medication|prescription|healthRecord|patientData/i);
1200
+ const hasAuditLog = allCode.match(/auditLog|audit_log|audit\.log|accessLog|hipaa.*log/i);
1201
+ if (hasPHI && !hasAuditLog) {
1202
+ findings.push({ ruleId: 'COMP-HIPAA-003', category: 'compliance', severity: 'high', title: 'PHI accessed without audit logging — HIPAA Security Rule 164.312(b)', description: 'Log all PHI access: who accessed, when, what data, from where. HIPAA requires audit controls for all ePHI access. Retain logs for 6 years.', fix: null });
1203
+ }
1204
+ return findings;
1205
+ },
1206
+ },
1207
+
1208
+ // COMP-HIPAA-004: PHI not de-identified before analytics
1209
+ { id: 'COMP-HIPAA-004', category: 'compliance', severity: 'high', confidence: 'likely', title: 'PHI Sent to Analytics Without De-identification',
1210
+ check({ files }) {
1211
+ const findings = [];
1212
+ for (const [fp, c] of files) {
1213
+ if (!isSourceFile(fp)) continue;
1214
+ if (c.match(/diagnosis|medication|prescription|healthRecord/i) && c.match(/analytics|mixpanel|segment|amplitude|gtag/i)) {
1215
+ if (!c.match(/deidentify|anonymize|hash\(|redact/i)) {
1216
+ findings.push({ ruleId: 'COMP-HIPAA-004', category: 'compliance', severity: 'high', title: 'PHI data potentially sent to third-party analytics without de-identification', description: 'De-identify or anonymize health data before sending to analytics platforms. Business associates must have a BAA with your organization.', file: fp, fix: null });
1217
+ }
1218
+ }
1219
+ }
1220
+ return findings;
1221
+ },
1222
+ },
1223
+
1224
+ // COMP-INTL-009: Currency displayed without formatting
1225
+ { id: 'COMP-INTL-009', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Currency Displayed Without Locale Formatting',
1226
+ check({ files }) {
1227
+ const findings = [];
1228
+ for (const [fp, c] of files) {
1229
+ if (!isSourceFile(fp)) continue;
1230
+ const lines = c.split('\n');
1231
+ for (let i = 0; i < lines.length; i++) {
1232
+ if (lines[i].match(/\$\{.*price|amount|total.*\}|\${.*cost.*}/i) && !lines[i].match(/Intl\.|toLocaleString|currency.*format/i)) {
1233
+ findings.push({ ruleId: 'COMP-INTL-009', category: 'compliance', severity: 'low', title: 'Currency interpolated directly — no locale-aware formatting', description: 'Use Intl.NumberFormat: new Intl.NumberFormat(locale, {style:"currency",currency:"USD"}).format(amount). Different locales use different decimal/thousand separators.', file: fp, line: i + 1, fix: null });
1234
+ }
1235
+ }
1236
+ }
1237
+ return findings;
1238
+ },
1239
+ },
1240
+
1241
+ // COMP-INTL-010: Missing timezone handling for global users
1242
+ { id: 'COMP-INTL-010', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Timezone Handling for User Dates',
1243
+ check({ files }) {
1244
+ const findings = [];
1245
+ for (const [fp, c] of files) {
1246
+ if (!isSourceFile(fp)) continue;
1247
+ if (c.match(/schedule|appointment|booking|event.*date|meeting/i) && !c.match(/timezone|timeZone|moment.*tz|date-fns-tz|luxon|dayjs.*timezone/i)) {
1248
+ findings.push({ ruleId: 'COMP-INTL-010', category: 'compliance', severity: 'low', title: 'Date/time scheduling without timezone handling — confusing for global users', description: 'Store dates in UTC, display in user timezone. Use date-fns-tz or Luxon. Ignoring timezones causes scheduling errors for international users.', file: fp, fix: null });
1249
+ }
1250
+ }
1251
+ return findings;
1252
+ },
1253
+ },
1254
+
1255
+ // COMP-CCPA-003: No data subject request process
1256
+ { id: 'COMP-CCPA-003', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Data Subject Request (DSR) Process',
1257
+ check({ files }) {
1258
+ const findings = [];
1259
+ const allCode = [...files.values()].join('\n');
1260
+ const hasDSR = allCode.match(/data.*request|subject.*request|dsr\b|data.*access.*request|right.*access|export.*data/i);
1261
+ const hasUserData = allCode.match(/user\.email|user\.name|personal.*data|userData/i);
1262
+ if (hasUserData && !hasDSR) {
1263
+ findings.push({ ruleId: 'COMP-CCPA-003', category: 'compliance', severity: 'medium', title: 'No data subject request process detected — CCPA and GDPR requirement', description: 'Implement data access/deletion request handling. CCPA and GDPR give users the right to know what data you hold and request deletion.', fix: null });
1264
+ }
1265
+ return findings;
1266
+ },
1267
+ },
1268
+
1269
+ // COMP-CHILDREN-001: Behavioral advertising targeting children
1270
+ { id: 'COMP-CHILDREN-001', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Behavioral Advertising on Platform Serving Minors',
1271
+ check({ files }) {
1272
+ const findings = [];
1273
+ const allCode = [...files.values()].join('\n');
1274
+ const hasKids = allCode.match(/children|kids|minors|under.*13|age.*13|family.*friendly/i);
1275
+ const hasBehavioral = allCode.match(/behavioral.*ad|targeted.*ad|retargeting|adtech|facebook.*pixel|google.*ads.*remarketing/i);
1276
+ if (hasKids && hasBehavioral) {
1277
+ findings.push({ ruleId: 'COMP-CHILDREN-001', category: 'compliance', severity: 'critical', title: 'Behavioral advertising on platform serving children — COPPA/GDPR-K violation', description: 'COPPA prohibits behavioral advertising to children under 13. The FTC actively enforces this. Remove behavioral advertising or implement age verification.', fix: null });
1278
+ }
1279
+ return findings;
1280
+ },
1281
+ },
1282
+
1283
+ // COMP-GDPR-014: No right to erasure implementation
1284
+ { id: 'COMP-GDPR-014', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Right to Erasure (GDPR Article 17)',
1285
+ check({ files }) {
1286
+ const findings = [];
1287
+ const allCode = [...files.values()].join('\n');
1288
+ const hasDelete = allCode.match(/deleteUser|delete.*account|removeUser|eraseUser|right.*erasure/i);
1289
+ const hasUserData = allCode.match(/user\.email|user\.name|userData|personal.*data/i);
1290
+ if (hasUserData && !hasDelete) {
1291
+ findings.push({ ruleId: 'COMP-GDPR-014', category: 'compliance', severity: 'high', title: 'No user account deletion/erasure endpoint — GDPR Article 17 violation', description: 'Implement DELETE /users/:id that removes all personal data. GDPR gives users the right to erasure. Include anonymization of audit logs.', fix: null });
1292
+ }
1293
+ return findings;
1294
+ },
1295
+ },
1296
+
1297
+ // COMP-GDPR-015: No data portability export
1298
+ { id: 'COMP-GDPR-015', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Data Portability Export (GDPR Article 20)',
1299
+ check({ files }) {
1300
+ const findings = [];
1301
+ const allCode = [...files.values()].join('\n');
1302
+ const hasExport = allCode.match(/exportData|dataExport|download.*data|portability|gdpr.*export/i);
1303
+ const hasUserData = allCode.match(/user\.email|user\.name|userData/i);
1304
+ if (hasUserData && !hasExport) {
1305
+ findings.push({ ruleId: 'COMP-GDPR-015', category: 'compliance', severity: 'medium', title: 'No data export/portability feature — GDPR Article 20 requirement', description: 'Add data export endpoint returning user data in machine-readable format (JSON/CSV). GDPR users have the right to receive their data for transfer.', fix: null });
1306
+ }
1307
+ return findings;
1308
+ },
1309
+ },
1310
+
1311
+ // COMP-A11Y-019: No skip to main content link
1312
+ { id: 'COMP-A11Y-019', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Skip Navigation Link (WCAG 2.4.1)',
1313
+ check({ files }) {
1314
+ const findings = [];
1315
+ for (const [fp, c] of files) {
1316
+ if (!fp.match(/\.(html|jsx|tsx|vue)$/)) continue;
1317
+ if (!fp.match(/component|partial|layout/i)) continue;
1318
+ const hasNav = c.match(/<nav|<header|navigation/i);
1319
+ const hasSkip = c.match(/skip.*(?:to|nav|main|content)|jump.*main|#main-content|#content/i);
1320
+ if (hasNav && !hasSkip) {
1321
+ findings.push({ ruleId: 'COMP-A11Y-019', category: 'compliance', severity: 'medium', title: 'Navigation without skip-to-main link — keyboard users must tab through all nav items', description: 'Add <a href="#main-content" class="sr-only focus:not-sr-only">Skip to main content</a>. WCAG 2.4.1 requires bypass blocks for keyboard users.', file: fp, fix: null });
1322
+ }
1323
+ }
1324
+ return findings;
1325
+ },
1326
+ },
1327
+
1328
+ // COMP-A11Y-020: Table without headers
1329
+ { id: 'COMP-A11Y-020', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'HTML Table Without Header Elements (WCAG 1.3.1)',
1330
+ check({ files }) {
1331
+ const findings = [];
1332
+ for (const [fp, c] of files) {
1333
+ if (!fp.match(/\.(html|jsx|tsx|vue)$/)) continue;
1334
+ const lines = c.split('\n');
1335
+ for (let i = 0; i < lines.length; i++) {
1336
+ if (lines[i].match(/<table/i)) {
1337
+ const tableContent = lines.slice(i, i + 20).join('\n');
1338
+ if (!tableContent.match(/<th|scope=|role=["']columnheader/i)) {
1339
+ findings.push({ ruleId: 'COMP-A11Y-020', category: 'compliance', severity: 'medium', title: 'HTML table without <th> header elements — screen readers cannot understand data structure', description: 'Add <th scope="col"> or <th scope="row"> headers. Without headers, screen readers read data as a meaningless list.', file: fp, line: i + 1, fix: null });
1340
+ }
1341
+ }
1342
+ }
1343
+ }
1344
+ return findings;
1345
+ },
1346
+ },
1347
+
1348
+ // COMP-A11Y-021: Icon button without accessible name
1349
+ { id: 'COMP-A11Y-021', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Icon-Only Button Without Accessible Name',
1350
+ check({ files }) {
1351
+ const findings = [];
1352
+ for (const [fp, c] of files) {
1353
+ if (!fp.match(/\.(html|jsx|tsx|vue)$/)) continue;
1354
+ const lines = c.split('\n');
1355
+ for (let i = 0; i < lines.length; i++) {
1356
+ if (lines[i].match(/<button[^>]*>[\s]*<(?:svg|i\s|span\s)/i) && !lines[i].match(/aria-label|aria-labelledby|title=/i)) {
1357
+ findings.push({ ruleId: 'COMP-A11Y-021', category: 'compliance', severity: 'high', title: 'Icon-only button without aria-label — screen readers announce "button" with no context', description: 'Add aria-label="Close dialog" to icon buttons. Without a text label, screen reader users cannot determine the button\'s purpose.', file: fp, line: i + 1, fix: null });
1358
+ }
1359
+ }
1360
+ }
1361
+ return findings;
1362
+ },
1363
+ },
1364
+
1365
+ // COMP-GDPR-016: Consent mechanism does not reject non-essential
1366
+ { id: 'COMP-GDPR-016', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Cookie Consent Without Reject Option',
1367
+ check({ files }) {
1368
+ const findings = [];
1369
+ for (const [fp, c] of files) {
1370
+ if (!isSourceFile(fp)) continue;
1371
+ if (c.match(/cookieconsent|cookie.*banner|CookieBanner/i)) {
1372
+ if (!c.match(/reject|decline|deny|refuse/i)) {
1373
+ findings.push({ ruleId: 'COMP-GDPR-016', category: 'compliance', severity: 'high', title: 'Cookie consent banner without reject/decline option', description: 'Add "Reject All" button. GDPR requires that consent be as easy to withdraw as to give. Banners with only "Accept" violate this requirement.', file: fp, fix: null });
1374
+ }
1375
+ }
1376
+ }
1377
+ return findings;
1378
+ },
1379
+ },
1380
+
1381
+ // COMP-PCI-005: Card number pattern in source code
1382
+ { id: 'COMP-PCI-005', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Payment Card Number Regex in Source',
1383
+ check({ files }) {
1384
+ const findings = [];
1385
+ for (const [fp, c] of files) {
1386
+ if (!isSourceFile(fp)) continue;
1387
+ const lines = c.split('\n');
1388
+ for (let i = 0; i < lines.length; i++) {
1389
+ if (lines[i].match(/(?:\d{4}[\s-]?){4}|4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}/)) {
1390
+ if (!lines[i].match(/\/\/|test|spec|mock|example|xxxx|XXXX/i)) {
1391
+ findings.push({ ruleId: 'COMP-PCI-005', category: 'compliance', severity: 'critical', title: 'Payment card number pattern found in source code', description: 'Never store or process raw card numbers. Use Stripe.js/Braintree.js to tokenize cards client-side so your server never sees the raw PAN.', file: fp, line: i + 1, fix: null });
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ return findings;
1397
+ },
1398
+ },
1399
+
1400
+ // COMP-SOC2-008: No system description document
1401
+ { id: 'COMP-SOC2-008', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No System Description for SOC 2',
1402
+ check({ files }) {
1403
+ const findings = [];
1404
+ const allPaths = [...files.keys()].join(' ');
1405
+ const hasSystemDesc = allPaths.match(/system.*description|trust.*criteria|soc2|soc-2|compliance.*doc/i) || [...files.values()].some(c => c.match(/trust.*service.*criteria|SOC.*Type/i));
1406
+ if (!hasSystemDesc) {
1407
+ findings.push({ ruleId: 'COMP-SOC2-008', category: 'compliance', severity: 'low', title: 'No SOC 2 system description documentation', description: 'Create system description covering: services, infrastructure, software, personnel, and risk management. Required for SOC 2 Type II audit.', fix: null });
1408
+ }
1409
+ return findings;
1410
+ },
1411
+ },
1412
+
1413
+ // COMP-INTL-011: No locale in Accept-Language header handling
1414
+ { id: 'COMP-INTL-011', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'API Does Not Respect Accept-Language Header',
1415
+ check({ files }) {
1416
+ const findings = [];
1417
+ const hasI18n = [...files.values()].some(c => c.match(/i18n|localization|translation/i));
1418
+ const hasLangHeader = [...files.values()].some(c => c.match(/Accept-Language|accept-language|req\.headers.*language/i));
1419
+ if (hasI18n && !hasLangHeader) {
1420
+ findings.push({ ruleId: 'COMP-INTL-011', category: 'compliance', severity: 'low', title: 'i18n configured but API does not read Accept-Language header', description: 'Read Accept-Language header: const lang = req.headers["accept-language"]?.split(",")[0]. Use it to return localized error messages and content.', fix: null });
1421
+ }
1422
+ return findings;
1423
+ },
1424
+ },
1425
+
1426
+ // COMP-INTL-012: Number formatted without locale
1427
+ { id: 'COMP-INTL-012', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Numbers Formatted Without Locale Awareness',
1428
+ check({ files }) {
1429
+ const findings = [];
1430
+ for (const [fp, c] of files) {
1431
+ if (!isSourceFile(fp)) continue;
1432
+ const lines = c.split('\n');
1433
+ for (let i = 0; i < lines.length; i++) {
1434
+ if (lines[i].match(/\.toFixed\(\d\)/) && lines[i].match(/price|amount|cost|total|balance/i)) {
1435
+ findings.push({ ruleId: 'COMP-INTL-012', category: 'compliance', severity: 'low', title: 'Number formatted with toFixed() — use Intl.NumberFormat for locale-aware formatting', description: 'Replace toFixed() with Intl.NumberFormat. In German, 1,234.56 is written 1.234,56 — toFixed always uses English decimal format.', file: fp, line: i + 1, fix: null });
1436
+ }
1437
+ }
1438
+ }
1439
+ return findings;
1440
+ },
1441
+ },
1442
+
1443
+ // COMP-GDPR-017: User password change without notification
1444
+ { id: 'COMP-GDPR-017', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Password Change Without Security Notification',
1445
+ check({ files }) {
1446
+ const findings = [];
1447
+ for (const [fp, c] of files) {
1448
+ if (!isSourceFile(fp)) continue;
1449
+ if (c.match(/password.*update|updatePassword|changePassword/i)) {
1450
+ if (!c.match(/sendEmail|notify|notification|email.*password.*changed/i)) {
1451
+ findings.push({ ruleId: 'COMP-GDPR-017', category: 'compliance', severity: 'medium', title: 'Password change without notifying user via email', description: 'Send an email when password changes: "Your password was changed. If this wasn\'t you, contact us." This is a security best practice and satisfies GDPR Article 34 breach notification requirement.', file: fp, fix: null });
1452
+ }
1453
+ }
1454
+ }
1455
+ return findings;
1456
+ },
1457
+ },
1458
+ // COMP-A11Y-022: No aria-expanded on toggle buttons
1459
+ { id: 'COMP-A11Y-022', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Toggle Button Without aria-expanded',
1460
+ check({ files }) {
1461
+ const findings = [];
1462
+ for (const [fp, c] of files) {
1463
+ if (!isSourceFile(fp)) continue;
1464
+ if (c.match(/toggle|accordion|collapse|expand/i) && c.match(/<button|onClick/i)) {
1465
+ if (!c.match(/aria-expanded|aria-controls/i)) {
1466
+ findings.push({ ruleId: 'COMP-A11Y-022', category: 'compliance', severity: 'medium', title: 'Toggle/accordion button without aria-expanded state', description: 'Add aria-expanded={isOpen}. Screen readers announce "button collapsed/expanded" to indicate state. Without it, users cannot determine if the panel is open.', file: fp, fix: null });
1467
+ }
1468
+ }
1469
+ }
1470
+ return findings;
1471
+ },
1472
+ },
1473
+ // COMP-A11Y-023: Use of tabindex > 0
1474
+ { id: 'COMP-A11Y-023', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Positive tabindex Disrupts Focus Order',
1475
+ check({ files }) {
1476
+ const findings = [];
1477
+ for (const [fp, c] of files) {
1478
+ if (!fp.match(/\.(html|jsx|tsx|vue)$/)) continue;
1479
+ const lines = c.split('\n');
1480
+ for (let i = 0; i < lines.length; i++) {
1481
+ if (lines[i].match(/tabIndex=["'][1-9]\d*["']|tabindex=["'][1-9]\d*["']/)) {
1482
+ findings.push({ ruleId: 'COMP-A11Y-023', category: 'compliance', severity: 'medium', title: 'Positive tabindex value — disrupts natural focus order (WCAG 2.4.3)', description: 'Use tabIndex={0} or tabIndex={-1}. Positive tabindex values override the natural DOM order, creating confusing keyboard navigation.', file: fp, line: i + 1, fix: null });
1483
+ }
1484
+ }
1485
+ }
1486
+ return findings;
1487
+ },
1488
+ },
1489
+ // COMP-SOC2-009: No background check process
1490
+ { id: 'COMP-SOC2-009', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No Personnel Security Process Documentation',
1491
+ check({ files }) {
1492
+ const findings = [];
1493
+ const allPaths = [...files.keys()].join(' ');
1494
+ const hasPersonnel = allPaths.match(/background.*check|personnel.*security|hr.*policy|employee.*security/i);
1495
+ if (!hasPersonnel) {
1496
+ findings.push({ ruleId: 'COMP-SOC2-009', category: 'compliance', severity: 'low', title: 'No personnel security policy documentation — SOC 2 CC1.4 requirement', description: 'Document background check and security onboarding procedures. SOC 2 requires evidence of personnel security controls for employees with system access.', fix: null });
1497
+ }
1498
+ return findings;
1499
+ },
1500
+ },
1501
+ // COMP-HIPAA-005: PHI transmitted over HTTP
1502
+ { id: 'COMP-HIPAA-005', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'Health Data Transmitted Over HTTP',
1503
+ check({ files }) {
1504
+ const findings = [];
1505
+ for (const [fp, c] of files) {
1506
+ if (!isSourceFile(fp)) continue;
1507
+ if (c.match(/health|medical|patient|phi\b/i) && c.match(/http:\/\/(?!localhost)/i)) {
1508
+ findings.push({ ruleId: 'COMP-HIPAA-005', category: 'compliance', severity: 'critical', title: 'Health data endpoint using HTTP — HIPAA requires TLS for PHI transmission', description: 'Use HTTPS for all endpoints handling health data. HIPAA Security Rule §164.312(e)(1) mandates encryption for ePHI in transit.', file: fp, fix: null });
1509
+ }
1510
+ }
1511
+ return findings;
1512
+ },
1513
+ },
1514
+ // COMP-PCI-006: No intrusion detection system
1515
+ { id: 'COMP-PCI-006', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Intrusion Detection System (PCI DSS 10.7)',
1516
+ check({ files }) {
1517
+ const findings = [];
1518
+ const allCode = [...files.values()].join('\n');
1519
+ const hasPayment = allCode.match(/stripe|braintree|payment|checkout|PCI/i);
1520
+ const hasIDS = allCode.match(/guardduty|ids\b|intrusion.*detection|snort|suricata|ossec|wazuh/i);
1521
+ if (hasPayment && !hasIDS) {
1522
+ findings.push({ ruleId: 'COMP-PCI-006', category: 'compliance', severity: 'medium', title: 'Payment processing without intrusion detection — PCI DSS Requirement 10.7', description: 'Implement file integrity monitoring and intrusion detection. PCI DSS requires detecting and responding to security system failures within 24 hours.', fix: null });
1523
+ }
1524
+ return findings;
1525
+ },
1526
+ },
1527
+ // COMP-CCPA-004: No privacy notice at data collection
1528
+ { id: 'COMP-CCPA-004', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Privacy Notice at Point of Collection (CCPA)',
1529
+ check({ files }) {
1530
+ const findings = [];
1531
+ for (const [fp, c] of files) {
1532
+ if (!fp.match(/\.(html|jsx|tsx|vue)$/)) continue;
1533
+ if (c.match(/\bform\b|signup|register|create.*account/i)) {
1534
+ if (!c.match(/privacy.*notice|privacy.*policy|we.*collect|personal.*information/i)) {
1535
+ findings.push({ ruleId: 'COMP-CCPA-004', category: 'compliance', severity: 'high', title: 'Registration/form without privacy notice at point of data collection', description: 'CCPA requires informing users of data collection at the time of collection. Add a link to privacy policy near data collection forms.', file: fp, fix: null });
1536
+ }
1537
+ }
1538
+ }
1539
+ return findings;
1540
+ },
1541
+ },
1542
+ // COMP-INTL-013: Hardcoded address format
1543
+ { id: 'COMP-INTL-013', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Hardcoded Address Format (US-Centric)',
1544
+ check({ files }) {
1545
+ const findings = [];
1546
+ for (const [fp, c] of files) {
1547
+ if (!fp.match(/\.(html|jsx|tsx|vue)$/)) continue;
1548
+ if (c.match(/zipCode|zip_code|postalCode/i) && c.match(/\d{5}(-\d{4})?/)) {
1549
+ if (!c.match(/country|international|locale/i)) {
1550
+ findings.push({ ruleId: 'COMP-INTL-013', category: 'compliance', severity: 'low', title: 'US ZIP code format without international postal code support', description: 'Support international postal codes (UK: SW1A 1AA, Canada: K1A 0A9). Add country selector and validate by country using a postal code library.', file: fp, fix: null });
1551
+ }
1552
+ }
1553
+ }
1554
+ return findings;
1555
+ },
1556
+ },
1557
+ // COMP-GDPR-018: Consent pre-checked
1558
+ { id: 'COMP-GDPR-018', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Pre-Checked Marketing Consent Checkbox',
1559
+ check({ files }) {
1560
+ const findings = [];
1561
+ for (const [fp, c] of files) {
1562
+ if (!fp.match(/\.(html|jsx|tsx|vue|js|ts)$/)) continue;
1563
+ const lines = c.split('\n');
1564
+ for (let i = 0; i < lines.length; i++) {
1565
+ if (lines[i].match(/checkbox|type=["']checkbox["']/i) && lines[i].match(/marketing|newsletter|consent|subscribe/i)) {
1566
+ if (lines[i].match(/checked|defaultChecked|value=["']true["']/i)) {
1567
+ findings.push({ ruleId: 'COMP-GDPR-018', category: 'compliance', severity: 'high', title: 'Marketing consent checkbox pre-checked — GDPR requires opt-in not opt-out', description: 'Consent must be freely given — do not pre-check consent boxes. GDPR Article 7 requires unambiguous, active consent for marketing communications.', file: fp, line: i + 1, fix: null });
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+ return findings;
1573
+ },
1574
+ },
1575
+ // COMP-GDPR-019: No age verification for age-restricted content
1576
+ { id: 'COMP-GDPR-019', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Age-Restricted Content Without Age Verification',
1577
+ check({ files }) {
1578
+ const findings = [];
1579
+ for (const [fp, c] of files) {
1580
+ if (!isSourceFile(fp)) continue;
1581
+ if (c.match(/adult|alcohol|gambling|tobacco|cannabis/i) && !c.match(/ageVerif|age_verif|dateOfBirth|dob.*verif|isAdult|verifyAge/i)) {
1582
+ findings.push({ ruleId: 'COMP-GDPR-019', category: 'compliance', severity: 'high', title: 'Age-restricted content served without age verification', description: 'Implement age verification before serving age-restricted content. GDPR and national laws require age gates for alcohol, gambling, and adult content to protect minors.', file: fp, fix: null });
1583
+ }
1584
+ }
1585
+ return findings;
1586
+ },
1587
+ },
1588
+ // COMP-A11Y-024: No keyboard navigation support
1589
+ { id: 'COMP-A11Y-024', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Interactive Elements Not Keyboard Accessible',
1590
+ check({ files }) {
1591
+ const findings = [];
1592
+ for (const [fp, c] of files) {
1593
+ if (!fp.match(/\.(jsx|tsx|html|vue)$/)) continue;
1594
+ const lines = c.split('\n');
1595
+ for (let i = 0; i < lines.length; i++) {
1596
+ if (lines[i].match(/onClick\s*=|@click\s*=/i) && lines[i].match(/<div|<span|<li/) && !lines[i].match(/role=|tabIndex|tabindex|onKeyDown|onKeyPress|onKeyUp/i)) {
1597
+ findings.push({ ruleId: 'COMP-A11Y-024', category: 'compliance', severity: 'medium', title: 'div/span with onClick but no keyboard handler — not keyboard accessible', description: 'Add tabIndex={0} and onKeyDown handler, or use a <button> element. WCAG 2.1 SC 2.1.1: All functionality must be available via keyboard.', file: fp, line: i + 1, fix: null });
1598
+ }
1599
+ }
1600
+ }
1601
+ return findings;
1602
+ },
1603
+ },
1604
+ // COMP-A11Y-025: Missing role attribute on custom interactive elements
1605
+ { id: 'COMP-A11Y-025', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Custom Interactive Widget Missing ARIA Role',
1606
+ check({ files }) {
1607
+ const findings = [];
1608
+ for (const [fp, c] of files) {
1609
+ if (!fp.match(/\.(jsx|tsx|html|vue)$/)) continue;
1610
+ const lines = c.split('\n');
1611
+ for (let i = 0; i < lines.length; i++) {
1612
+ if (lines[i].match(/<div[^>]*tabIndex|<div[^>]*tabindex/i) && !lines[i].match(/role=/)) {
1613
+ findings.push({ ruleId: 'COMP-A11Y-025', category: 'compliance', severity: 'medium', title: 'Focusable div without role attribute — assistive technology cannot identify its purpose', description: 'Add an appropriate role (button, tab, listitem, etc.) to focusable divs. WCAG 2.1 SC 4.1.2 requires interactive elements to have programmatically determinable roles.', file: fp, line: i + 1, fix: null });
1614
+ }
1615
+ }
1616
+ }
1617
+ return findings;
1618
+ },
1619
+ },
1620
+ // COMP-SOC2-010: No data encryption at rest documentation
1621
+ { id: 'COMP-SOC2-010', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Encryption at Rest Configuration Documented',
1622
+ check({ files }) {
1623
+ const findings = [];
1624
+ const hasSensitiveStorage = [...files.values()].some(c => c.match(/aws_rds|aws_s3|aws_dynamodb|aws_elasticache/i));
1625
+ const hasEncryption = [...files.values()].some(c => c.match(/storage_encrypted\s*=\s*true|encrypted\s*=\s*true|kms_key|sse_algorithm/i));
1626
+ if (hasSensitiveStorage && !hasEncryption) {
1627
+ findings.push({ ruleId: 'COMP-SOC2-010', category: 'compliance', severity: 'medium', title: 'Cloud storage resources without encryption at rest', description: 'Enable encryption at rest for all data storage (RDS, S3, DynamoDB). SOC 2 CC6.1 requires logical and physical access controls including encryption for sensitive data.', fix: null });
1628
+ }
1629
+ return findings;
1630
+ },
1631
+ },
1632
+ // COMP-CCPA-005: No opt-out mechanism for sale of personal data
1633
+ { id: 'COMP-CCPA-005', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No "Do Not Sell My Personal Information" Mechanism',
1634
+ check({ files }) {
1635
+ const findings = [];
1636
+ const hasSellData = [...files.values()].some(c => c.match(/segment|mixpanel|amplitude|facebook.*pixel|google.*analytics|third.*party.*data/i));
1637
+ const hasOptOut = [...files.values()].some(c => c.match(/doNotSell|do_not_sell|opt.?out.*sell|CCPA|data.*sale/i));
1638
+ if (hasSellData && !hasOptOut) {
1639
+ findings.push({ ruleId: 'COMP-CCPA-005', category: 'compliance', severity: 'high', title: 'Data shared with third parties without CCPA opt-out option', description: 'Add "Do Not Sell or Share My Personal Information" link as required by CCPA. California residents have the right to opt out of sale/sharing of personal data.', fix: null });
1640
+ }
1641
+ return findings;
1642
+ },
1643
+ },
1644
+ // COMP-HIPAA-006: PHI in URL parameters
1645
+ { id: 'COMP-HIPAA-006', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'PHI in URL Parameters',
1646
+ check({ files }) {
1647
+ const findings = [];
1648
+ for (const [fp, c] of files) {
1649
+ if (!isSourceFile(fp)) continue;
1650
+ const lines = c.split('\n');
1651
+ for (let i = 0; i < lines.length; i++) {
1652
+ if (lines[i].match(/[?&](patient|patient_id|mrn|ssn|diagnosis|medication|dob)=/i) && !lines[i].match(/\/\//)) {
1653
+ findings.push({ ruleId: 'COMP-HIPAA-006', category: 'compliance', severity: 'critical', title: 'PHI passed in URL parameters — logged in server logs and browser history', description: 'Use POST body or session for PHI. HIPAA requires safeguards to prevent unauthorized disclosure of PHI. URL parameters are logged in web server logs, proxy logs, and browser history.', file: fp, line: i + 1, fix: null });
1654
+ }
1655
+ }
1656
+ }
1657
+ return findings;
1658
+ },
1659
+ },
1660
+ // COMP-PCI-007: Storing CVV after authorization
1661
+ { id: 'COMP-PCI-007', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'CVV/CVV2 Value Stored in Database',
1662
+ check({ files }) {
1663
+ const findings = [];
1664
+ for (const [fp, c] of files) {
1665
+ if (!isSourceFile(fp)) continue;
1666
+ const lines = c.split('\n');
1667
+ for (let i = 0; i < lines.length; i++) {
1668
+ if (lines[i].match(/cvv|cvv2|cvc|security_code/i) && lines[i].match(/save\s*\(|insert|create|update|set\s*\(/i) && !lines[i].match(/\/\//)) {
1669
+ findings.push({ ruleId: 'COMP-PCI-007', category: 'compliance', severity: 'critical', title: 'CVV/CVV2 value being stored — PCI DSS Requirement 3 violation', description: 'Never store CVV after authorization. PCI DSS Requirement 3.3 strictly prohibits storing card validation codes. Use tokenization via your payment processor instead.', file: fp, line: i + 1, fix: null });
1670
+ }
1671
+ }
1672
+ }
1673
+ return findings;
1674
+ },
1675
+ },
1676
+ // COMP-INTL-014: No timezone handling for user dates
1677
+ { id: 'COMP-INTL-014', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'User-Facing Dates Without Timezone Consideration',
1678
+ check({ files }) {
1679
+ const findings = [];
1680
+ for (const [fp, c] of files) {
1681
+ if (!isSourceFile(fp)) continue;
1682
+ const lines = c.split('\n');
1683
+ for (let i = 0; i < lines.length; i++) {
1684
+ if (lines[i].match(/new Date\s*\(|Date\.now\s*\(\s*\)/) && !lines[i].match(/\/\//)) {
1685
+ const ctx = lines.slice(i, Math.min(lines.length, i + 5)).join('\n');
1686
+ if (ctx.match(/res\.json|res\.send|return.*date/i) && !ctx.match(/timezone|toISOString|UTC|Intl\.DateTimeFormat/i)) {
1687
+ findings.push({ ruleId: 'COMP-INTL-014', category: 'compliance', severity: 'medium', title: 'Date returned to client without timezone context', description: 'Always use ISO 8601 format with timezone (toISOString()) or include timezone info. Server-side dates without timezone context are interpreted in the user\'s local timezone, causing date display errors.', file: fp, line: i + 1, fix: null });
1688
+ }
1689
+ }
1690
+ }
1691
+ }
1692
+ return findings;
1693
+ },
1694
+ },
1695
+ // COMP-A11Y-026: Images in CSS (background-image) for content
1696
+ { id: 'COMP-A11Y-026', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Informational Content as CSS Background Image',
1697
+ check({ files }) {
1698
+ const findings = [];
1699
+ for (const [fp, c] of files) {
1700
+ if (!fp.match(/\.(css|scss|sass|less)$/)) continue;
1701
+ const lines = c.split('\n');
1702
+ for (let i = 0; i < lines.length; i++) {
1703
+ if (lines[i].match(/background-image:\s*url\s*\(/) && !lines[i].match(/decoration|ornament|pattern|gradient/i)) {
1704
+ findings.push({ ruleId: 'COMP-A11Y-026', category: 'compliance', severity: 'low', title: 'Informational image in CSS background — invisible to screen readers', description: 'Use <img> with alt text for informational images. CSS background images cannot have alt text and are invisible to screen readers (WCAG 2.1 SC 1.1.1).', file: fp, line: i + 1, fix: null });
1705
+ }
1706
+ }
1707
+ }
1708
+ return findings;
1709
+ },
1710
+ },
1711
+ // COMP-GDPR-020: Data exported without access control check
1712
+ { id: 'COMP-GDPR-020', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Data Export Endpoint Without Authorization Check',
1713
+ check({ files }) {
1714
+ const findings = [];
1715
+ for (const [fp, c] of files) {
1716
+ if (!isSourceFile(fp)) continue;
1717
+ const lines = c.split('\n');
1718
+ for (let i = 0; i < lines.length; i++) {
1719
+ if (lines[i].match(/export.*csv|export.*excel|download.*data|export.*report/i) && lines[i].match(/router\.(get|post)\s*\(/)) {
1720
+ const ctx = lines.slice(i, Math.min(lines.length, i + 15)).join('\n');
1721
+ if (!ctx.match(/auth|permission|role|admin|isAuthenticated|requireAuth/i)) {
1722
+ findings.push({ ruleId: 'COMP-GDPR-020', category: 'compliance', severity: 'high', title: 'Data export endpoint without authentication/authorization check', description: 'Require authentication and authorization for data export endpoints. GDPR Article 5 requires access controls on personal data. Unprotected export endpoints enable bulk PII theft.', file: fp, line: i + 1, fix: null });
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ return findings;
1728
+ },
1729
+ },
1730
+ // COMP-SOC2-011: No penetration testing evidence
1731
+ { id: 'COMP-SOC2-011', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'No Penetration Testing Process Documented',
1732
+ check({ files }) {
1733
+ const findings = [];
1734
+ const hasPentest = [...files.keys()].some(f => f.match(/pentest|penetration|security.?test|security.?audit/i)) || [...files.values()].some(c => c.match(/pentest|penetration.?test|security.?assessment/i));
1735
+ const hasCI = [...files.keys()].some(f => f.match(/\.github\/workflows|\.gitlab-ci/));
1736
+ if (hasCI && !hasPentest) {
1737
+ findings.push({ ruleId: 'COMP-SOC2-011', category: 'compliance', severity: 'medium', title: 'No penetration testing evidence in repository', description: 'Document penetration testing schedule and remediation process. SOC 2 CC7.1 requires that security testing be performed and findings remediated. Annual pen tests are standard.', fix: null });
1738
+ }
1739
+ return findings;
1740
+ },
1741
+ },
1742
+ // COMP-CHILDREN-002: Collecting children's location data — only for apps explicitly targeting children
1743
+ { id: 'COMP-CHILDREN-002', category: 'compliance', severity: 'critical', confidence: 'definite', title: "Location Data Collected That May Apply to Children's Platforms",
1744
+ check({ files }) {
1745
+ const findings = [];
1746
+ // Only fire if package.json or config explicitly mentions COPPA/children targeting
1747
+ const hasChildrenTargeting = [...files.entries()].some(([fp, c]) =>
1748
+ (fp.endsWith('package.json') || fp.endsWith('.json')) && /coppa|kids.*app|children.*platform/i.test(c)
1749
+ );
1750
+ if (!hasChildrenTargeting) return findings;
1751
+ const hasLocation = [...files.values()].some(c => c.match(/geolocation|navigator\.geolocation|latitude|longitude|location.*tracking/i));
1752
+ if (hasLocation) {
1753
+ findings.push({ ruleId: 'COMP-CHILDREN-002', category: 'compliance', severity: 'critical', title: 'Platform targeting children may be collecting location data', description: 'COPPA and GDPR-K prohibit collecting precise geolocation from children under 13 without verifiable parental consent. Remove location tracking or implement strict age verification with parental consent flow.', fix: null });
1754
+ }
1755
+ return findings;
1756
+ },
1757
+ },
1758
+ // COMP-INTL-015: Currency displayed without ISO code
1759
+ { id: 'COMP-INTL-015', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Currency Amount Displayed Without ISO Currency Code',
1760
+ check({ files }) {
1761
+ const findings = [];
1762
+ for (const [fp, c] of files) {
1763
+ if (!fp.match(/\.(jsx|tsx|html|vue|js|ts)$/)) continue;
1764
+ const lines = c.split('\n');
1765
+ for (let i = 0; i < lines.length; i++) {
1766
+ // Match literal $ currency display: $12.99 or "$" + amount, but NOT ${...} template literals
1767
+ if (lines[i].match(/\$\s*[0-9]|\"\$\"|'\$'/) && !lines[i].match(/\$\{/) && !lines[i].match(/currency.*USD|ISO|Intl\.NumberFormat.*currency/i)) {
1768
+ findings.push({ ruleId: 'COMP-INTL-015', category: 'compliance', severity: 'low', title: 'Currency displayed with $ symbol but no ISO code', description: 'Use Intl.NumberFormat with currency option. Display currency as "USD" or "EUR" in addition to symbol for international users and compliance with financial display regulations.', file: fp, line: i + 1, fix: null });
1769
+ }
1770
+ }
1771
+ }
1772
+ return findings;
1773
+ },
1774
+ },
1775
+ ];
1776
+
1777
+ // allComplianceRules is constructed at end of file after all rules.push() calls
1778
+
1779
+ function isSourceFile(f) { return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].some(e => f.endsWith(e)); }
1780
+
1781
+ // COMP-016: HIPAA - PHI in log statements
1782
+ rules.push({
1783
+ id: 'COMP-016', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'HIPAA: Protected Health Information (PHI) logged',
1784
+ check({ files }) {
1785
+ const findings = [];
1786
+ const phiFields = /(?:diagnosis|condition|medication|treatment|prescription|patient.*name|dob|date_of_birth|ssn|medical_record|insurance|health_plan)/i;
1787
+ const logPattern = /(?:console\.|logger\.|log\.)\w*\s*\([^)]*(?:patient|health|medical|diagnosis|medication)/i;
1788
+ for (const [fp, c] of files) {
1789
+ if (!isSourceFile(fp)) continue;
1790
+ const lines = c.split('\n');
1791
+ for (let i = 0; i < lines.length; i++) {
1792
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
1793
+ if (logPattern.test(lines[i]) || (phiFields.test(lines[i]) && /console\.|logger\./.test(lines[i]))) {
1794
+ findings.push({ ruleId: 'COMP-016', category: 'compliance', severity: 'critical', title: 'Potential PHI logged — HIPAA violation', description: 'Logging Protected Health Information violates HIPAA. Mask or exclude PHI from all log output. Use structured logging with field-level redaction.', file: fp, line: i + 1, fix: null });
1795
+ }
1796
+ }
1797
+ }
1798
+ return findings;
1799
+ },
1800
+ });
1801
+
1802
+ // COMP-017: HIPAA - Unencrypted health data storage
1803
+ rules.push({
1804
+ id: 'COMP-017', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'HIPAA: Health data field stored without encryption annotation',
1805
+ check({ files }) {
1806
+ const findings = [];
1807
+ const phiField = /(?:diagnosis|condition|medication|prescription|health_data|medical_history|lab_result)\s*[:=]/i;
1808
+ const encryptMarker = /encrypt|@Encrypted|@Column.*encrypted|cipher/i;
1809
+ for (const [fp, c] of files) {
1810
+ if (!isSourceFile(fp)) continue;
1811
+ const lines = c.split('\n');
1812
+ for (let i = 0; i < lines.length; i++) {
1813
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
1814
+ if (phiField.test(lines[i]) && /String|TEXT|VARCHAR|string:/i.test(lines[i]) && !encryptMarker.test(lines[i])) {
1815
+ findings.push({ ruleId: 'COMP-017', category: 'compliance', severity: 'critical', title: 'Health data field stored without encryption — HIPAA requires encryption at rest', description: 'PHI must be encrypted at rest under HIPAA. Use application-level encryption or database column encryption for health data fields.', file: fp, line: i + 1, fix: null });
1816
+ }
1817
+ }
1818
+ }
1819
+ return findings;
1820
+ },
1821
+ });
1822
+
1823
+ // COMP-018: PCI-DSS - Full card number stored
1824
+ rules.push({
1825
+ id: 'COMP-018', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'PCI-DSS: Full credit card number stored or logged',
1826
+ check({ files }) {
1827
+ const findings = [];
1828
+ const cardField = /(?:card_number|cardNumber|pan|credit_card|cc_number)\s*[:=]/i;
1829
+ const fullPAN = /(?:card_number|cardNumber|pan|full.*card)\s*[:=]\s*(?:req\.|body\.|input\.|String|TEXT|VARCHAR)/i;
1830
+ for (const [fp, c] of files) {
1831
+ if (!isSourceFile(fp)) continue;
1832
+ const lines = c.split('\n');
1833
+ for (let i = 0; i < lines.length; i++) {
1834
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
1835
+ if (fullPAN.test(lines[i]) && !/mask|truncat|last.*4|token|vault/i.test(lines[i])) {
1836
+ findings.push({ ruleId: 'COMP-018', category: 'compliance', severity: 'critical', title: 'Full PAN stored — PCI-DSS prohibits storing full card numbers', description: 'PCI-DSS prohibits storing full Primary Account Numbers. Store only the last 4 digits or use a payment tokenization service (Stripe, Braintree).', file: fp, line: i + 1, fix: null });
1837
+ }
1838
+ }
1839
+ }
1840
+ return findings;
1841
+ },
1842
+ });
1843
+
1844
+ // COMP-019: PCI-DSS - CVV stored
1845
+ rules.push({
1846
+ id: 'COMP-019', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'PCI-DSS: CVV/CVV2 security code stored',
1847
+ check({ files }) {
1848
+ const findings = [];
1849
+ const cvvField = /(?:cvv|cvv2|cvc|security_code|card_verification)\s*[:=]/i;
1850
+ for (const [fp, c] of files) {
1851
+ if (!isSourceFile(fp)) continue;
1852
+ const lines = c.split('\n');
1853
+ for (let i = 0; i < lines.length; i++) {
1854
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
1855
+ if (cvvField.test(lines[i]) && /save|store|insert|create|String|TEXT|VARCHAR/i.test(lines[i])) {
1856
+ findings.push({ ruleId: 'COMP-019', category: 'compliance', severity: 'critical', title: 'CVV stored — PCI-DSS strictly prohibits storing CVV after authorization', description: 'PCI-DSS Requirement 3.2 prohibits storing CVV/CVC after transaction authorization. Remove CVV storage immediately.', file: fp, line: i + 1, fix: null });
1857
+ }
1858
+ }
1859
+ }
1860
+ return findings;
1861
+ },
1862
+ });
1863
+
1864
+ // COMP-020: PCI-DSS - Unmasked PAN in logs
1865
+ rules.push({
1866
+ id: 'COMP-020', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'PCI-DSS: Card number or PAN in log output',
1867
+ check({ files }) {
1868
+ const findings = [];
1869
+ const pattern = /(?:console\.|logger\.|log\.)\w*\s*\([^)]*(?:cardNumber|card_number|pan|creditCard|credit_card)/i;
1870
+ for (const [fp, c] of files) {
1871
+ if (!isSourceFile(fp)) continue;
1872
+ const lines = c.split('\n');
1873
+ for (let i = 0; i < lines.length; i++) {
1874
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
1875
+ if (pattern.test(lines[i])) {
1876
+ findings.push({ ruleId: 'COMP-020', category: 'compliance', severity: 'critical', title: 'Card number logged — PCI-DSS violation', description: 'PCI-DSS requires masking card numbers in logs (show only last 4 digits). Logging full PANs is a reportable violation.', file: fp, line: i + 1, fix: null });
1877
+ }
1878
+ }
1879
+ }
1880
+ return findings;
1881
+ },
1882
+ });
1883
+
1884
+ // COMP-021: GDPR - No cookie consent mechanism
1885
+ rules.push({
1886
+ id: 'COMP-021', category: 'compliance', severity: 'high', confidence: 'likely', title: 'GDPR: No cookie consent mechanism found',
1887
+ check({ files }) {
1888
+ const findings = [];
1889
+ const hasCookies = [...files.values()].some(c => /document\.cookie|setCookie|cookie-parser|Set-Cookie/i.test(c));
1890
+ const hasConsent = [...files.values()].some(c => /cookie.*consent|cookieConsent|gdpr.*cookie|acceptCookies|cookieBanner|cookie.*policy|CookieYes|Cookiebot|OneTrust/i.test(c));
1891
+ if (hasCookies && !hasConsent) {
1892
+ findings.push({ ruleId: 'COMP-021', category: 'compliance', severity: 'high', title: 'Cookies set without consent mechanism — GDPR Article 7 violation', description: 'GDPR requires explicit consent before setting non-essential cookies. Implement a cookie consent banner with opt-in/opt-out controls.', fix: null });
1893
+ }
1894
+ return findings;
1895
+ },
1896
+ });
1897
+
1898
+ // COMP-022: GDPR - Third-party scripts without consent
1899
+ rules.push({
1900
+ id: 'COMP-022', category: 'compliance', severity: 'high', confidence: 'likely', title: 'GDPR: Third-party tracking scripts loaded without consent check',
1901
+ check({ files }) {
1902
+ const findings = [];
1903
+ const trackingScripts = /(?:google-analytics|gtag|ga\s*\(|fbq\s*\(|analytics\.js|segment\.com|mixpanel|hotjar|heap\.io)/i;
1904
+ const consentCheck = /cookie.*consent|hasConsent|gdprConsent|analyticsEnabled|trackingAllowed/i;
1905
+ for (const [fp, c] of files) {
1906
+ if (!fp.match(/\.(js|ts|jsx|tsx|html)$/)) continue;
1907
+ if (!trackingScripts.test(c)) continue;
1908
+ if (!consentCheck.test(c)) {
1909
+ findings.push({ ruleId: 'COMP-022', category: 'compliance', severity: 'high', title: 'Third-party analytics loaded without consent gate — GDPR violation', description: 'Load analytics and tracking scripts only after user consent. Wrap script loading in consent check: if (hasAnalyticsConsent()) { loadAnalytics(); }', file: fp, fix: null });
1910
+ }
1911
+ }
1912
+ return findings;
1913
+ },
1914
+ });
1915
+
1916
+ // COMP-023: GDPR - No data retention policy
1917
+ rules.push({
1918
+ id: 'COMP-023', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'GDPR: No data retention or deletion mechanism',
1919
+ check({ files }) {
1920
+ const findings = [];
1921
+ const storesPII = [...files.values()].some(c => /email.*save|user.*create|profile.*insert|personal.*data/i.test(c));
1922
+ const hasRetention = [...files.values()].some(c => /deleteAfter|retention|purge|ttl|expiresAt|deleteOldRecords|data.*cleanup/i.test(c));
1923
+ if (storesPII && !hasRetention) {
1924
+ findings.push({ ruleId: 'COMP-023', category: 'compliance', severity: 'medium', title: 'PII stored without data retention policy — GDPR requires data minimization', description: 'GDPR Article 5(1)(e) requires data not be kept longer than necessary. Implement automated deletion for expired user data.', fix: null });
1925
+ }
1926
+ return findings;
1927
+ },
1928
+ });
1929
+
1930
+ // COMP-024: Accessibility - Missing aria-label on interactive elements
1931
+ rules.push({
1932
+ id: 'COMP-024', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Accessibility: Interactive element without accessible label',
1933
+ check({ files }) {
1934
+ const findings = [];
1935
+ for (const [fp, c] of files) {
1936
+ if (!fp.match(/\.(jsx|tsx|html|vue)$/)) continue;
1937
+ const lines = c.split('\n');
1938
+ for (let i = 0; i < lines.length; i++) {
1939
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
1940
+ if (/<(?:button|a|input|select)\b/.test(lines[i]) && !/aria-label|aria-labelledby|title=|alt=/.test(lines[i])) {
1941
+ const nextLines = lines.slice(i, Math.min(lines.length, i + 3)).join(' ');
1942
+ if (!/aria-label|aria-labelledby/.test(nextLines)) {
1943
+ findings.push({ ruleId: 'COMP-024', category: 'compliance', severity: 'medium', title: 'Interactive element without accessible label — WCAG 2.1 violation', description: 'Buttons, links, and form inputs need accessible labels for screen readers. Add aria-label, aria-labelledby, or visible text content.', file: fp, line: i + 1, fix: null });
1944
+ }
1945
+ }
1946
+ }
1947
+ }
1948
+ return findings;
1949
+ },
1950
+ });
1951
+
1952
+ // COMP-025: Accessibility - Image without alt text
1953
+ rules.push({
1954
+ id: 'COMP-025', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Accessibility: <img> without alt attribute',
1955
+ check({ files }) {
1956
+ const findings = [];
1957
+ for (const [fp, c] of files) {
1958
+ if (!fp.match(/\.(jsx|tsx|html|vue)$/)) continue;
1959
+ const lines = c.split('\n');
1960
+ for (let i = 0; i < lines.length; i++) {
1961
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
1962
+ if (/<img\b/.test(lines[i]) && !/alt=/.test(lines[i])) {
1963
+ const ctx = lines.slice(i, Math.min(lines.length, i + 3)).join(' ');
1964
+ if (!/alt=/.test(ctx)) {
1965
+ findings.push({ ruleId: 'COMP-025', category: 'compliance', severity: 'medium', title: '<img> without alt attribute — WCAG 2.1 Level A failure', description: 'Images must have alt text for screen readers. Use alt="" for decorative images and a descriptive alt for informational images.', file: fp, line: i + 1, fix: null });
1966
+ }
1967
+ }
1968
+ }
1969
+ }
1970
+ return findings;
1971
+ },
1972
+ });
1973
+
1974
+ // COMP-026: COPPA - No age verification
1975
+ rules.push({
1976
+ id: 'COMP-026', category: 'compliance', severity: 'high', confidence: 'likely', title: 'COPPA: No age verification before collecting user data',
1977
+ check({ files }) {
1978
+ const findings = [];
1979
+ const collectsData = [...files.values()].some(c => /register|signup|createAccount|createUser/i.test(c));
1980
+ const hasAgeCheck = [...files.values()].some(c => /age.*verif|dateOfBirth|dob|ageGate|coppa|under.*13|age.*13|birthdate/i.test(c));
1981
+ if (collectsData && !hasAgeCheck) {
1982
+ findings.push({ ruleId: 'COMP-026', category: 'compliance', severity: 'high', title: 'No age verification — COPPA requires consent for users under 13', description: 'COPPA requires verifiable parental consent before collecting personal information from children under 13. Add age gate or date-of-birth check to registration.', fix: null });
1983
+ }
1984
+ return findings;
1985
+ },
1986
+ });
1987
+
1988
+ // COMP-027: Missing privacy policy link
1989
+ rules.push({
1990
+ id: 'COMP-027', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Registration/signup form without privacy policy link',
1991
+ check({ files }) {
1992
+ const findings = [];
1993
+ for (const [fp, c] of files) {
1994
+ if (!fp.match(/\.(jsx|tsx|html|vue)$/)) continue;
1995
+ if (!/register|signup|createAccount/i.test(c)) continue;
1996
+ if (!/privacy.*policy|privacyPolicy|privacy-policy/i.test(c)) {
1997
+ findings.push({ ruleId: 'COMP-027', category: 'compliance', severity: 'medium', title: 'Signup form without privacy policy link — required by GDPR, CCPA, COPPA', description: 'Registration forms must link to a privacy policy explaining what data is collected. GDPR, CCPA, and COPPA all require this disclosure at point of data collection.', file: fp, fix: null });
1998
+ }
1999
+ }
2000
+ return findings;
2001
+ },
2002
+ });
2003
+
2004
+ // COMP-028: SOC2 - Audit logging missing
2005
+ rules.push({
2006
+ id: 'COMP-028', category: 'compliance', severity: 'medium', confidence: 'definite', title: 'SOC2: No audit logging for sensitive operations',
2007
+ check({ files }) {
2008
+ const findings = [];
2009
+ const hasSensitiveOps = [...files.values()].some(c => /delete.*user|update.*role|admin.*action|permission.*change/i.test(c));
2010
+ const hasAuditLog = [...files.values()].some(c => /auditLog|audit_log|AuditTrail|audit\.log|writeAudit/i.test(c));
2011
+ if (hasSensitiveOps && !hasAuditLog) {
2012
+ findings.push({ ruleId: 'COMP-028', category: 'compliance', severity: 'medium', title: 'Sensitive operations without audit logging — SOC2 CC7.2 requires audit trail', description: 'SOC2 requires audit trails for access changes, privilege escalations, and data deletions. Implement an audit log that records who did what and when.', fix: null });
2013
+ }
2014
+ return findings;
2015
+ },
2016
+ });
2017
+
2018
+ // COMP-029: WCAG - Color-only information
2019
+ rules.push({
2020
+ id: 'COMP-029', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Accessibility: Information conveyed by color only',
2021
+ check({ files }) {
2022
+ const findings = [];
2023
+ for (const [fp, c] of files) {
2024
+ if (!fp.match(/\.(jsx|tsx|html|vue|css|scss)$/)) continue;
2025
+ const lines = c.split('\n');
2026
+ for (let i = 0; i < lines.length; i++) {
2027
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
2028
+ // Look for status/error indicators that only use color
2029
+ if (/color.*red|color.*green|color.*error|color.*success/i.test(lines[i]) && !/icon|text|label|aria/i.test(lines[i])) {
2030
+ const ctx = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 2)).join('\n');
2031
+ if (!/text|icon|label|aria-label/.test(ctx)) {
2032
+ findings.push({ ruleId: 'COMP-029', category: 'compliance', severity: 'medium', title: 'Status indicated by color only — inaccessible to color-blind users (WCAG 1.4.1)', description: 'Use icons, text, or patterns in addition to color to convey information. Color alone fails WCAG 1.4.1 (Use of Color).', file: fp, line: i + 1, fix: null });
2033
+ }
2034
+ }
2035
+ }
2036
+ }
2037
+ return findings;
2038
+ },
2039
+ });
2040
+
2041
+ // COMP-030: Missing HTTPS in production config
2042
+ rules.push({
2043
+ id: 'COMP-030', category: 'compliance', severity: 'high', confidence: 'likely', title: 'Production config using HTTP instead of HTTPS URLs',
2044
+ check({ files }) {
2045
+ const findings = [];
2046
+ const prodConfig = /(?:production|prod).*(?:url|endpoint|api|base)/i;
2047
+ for (const [fp, c] of files) {
2048
+ if (!fp.match(/\.(json|env|ya?ml|ts|js)$/)) continue;
2049
+ if (!fp.match(/prod|config|env/i)) continue;
2050
+ const lines = c.split('\n');
2051
+ for (let i = 0; i < lines.length; i++) {
2052
+ if (/http:\/\/(?!localhost|127\.|0\.0\.0)/.test(lines[i]) && !/^\s*(#|\/\/)/.test(lines[i])) {
2053
+ findings.push({ ruleId: 'COMP-030', category: 'compliance', severity: 'high', title: 'HTTP (not HTTPS) URL in production configuration', description: 'Production services must use HTTPS. Plain HTTP exposes data in transit. Change all production URLs to use https://.', file: fp, line: i + 1, fix: null });
2054
+ }
2055
+ }
2056
+ }
2057
+ return findings;
2058
+ },
2059
+ });
2060
+
2061
+ // COMP-031 through COMP-055: Additional compliance rules
2062
+
2063
+ // COMP-031: GDPR - Right to erasure not implemented
2064
+ rules.push({
2065
+ id: 'COMP-031', category: 'compliance', severity: 'high', confidence: 'likely', title: 'GDPR: No data deletion/erasure functionality',
2066
+ check({ files }) {
2067
+ const findings = [];
2068
+ const storesUserData = [...files.values()].some(c => /user\.create|createUser|registerUser/i.test(c));
2069
+ const hasDeleteUser = [...files.values()].some(c => /deleteUser|removeUser|user\.delete|account.*delete|gdpr.*delete|erasure|right.*forget/i.test(c));
2070
+ if (storesUserData && !hasDeleteUser) {
2071
+ findings.push({ ruleId: 'COMP-031', category: 'compliance', severity: 'high', title: 'User data stored without deletion capability — GDPR Right to Erasure (Article 17)', description: 'GDPR requires you to delete personal data upon user request. Implement a user data deletion flow that removes all PII from your systems.', fix: null });
2072
+ }
2073
+ return findings;
2074
+ },
2075
+ });
2076
+
2077
+ // COMP-032: PCI-DSS - Unencrypted transmission
2078
+ rules.push({
2079
+ id: 'COMP-032', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'PCI-DSS: Payment data transmitted over unencrypted connection',
2080
+ check({ files }) {
2081
+ const findings = [];
2082
+ for (const [fp, c] of files) {
2083
+ if (!isSourceFile(fp)) continue;
2084
+ const lines = c.split('\n');
2085
+ for (let i = 0; i < lines.length; i++) {
2086
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
2087
+ if (/http:\/\/(?!localhost)/.test(lines[i]) && /card|payment|pan|credit/i.test(lines[i])) {
2088
+ findings.push({ ruleId: 'COMP-032', category: 'compliance', severity: 'critical', title: 'Payment data sent over HTTP — PCI-DSS requires TLS for cardholder data', description: 'PCI-DSS Requirement 4.1 mandates strong cryptography for cardholder data in transit. Always use HTTPS/TLS for payment data transmission.', file: fp, line: i + 1, fix: null });
2089
+ }
2090
+ }
2091
+ }
2092
+ return findings;
2093
+ },
2094
+ });
2095
+
2096
+ // COMP-033: HIPAA - Audit log for PHI access
2097
+ rules.push({
2098
+ id: 'COMP-033', category: 'compliance', severity: 'high', confidence: 'likely', title: 'HIPAA: PHI accessed without audit log entry',
2099
+ check({ files }) {
2100
+ const findings = [];
2101
+ const hasPHIAccess = [...files.values()].some(c => /patient|diagnosis|medication|health_record/i.test(c) && /findOne|findById|SELECT/i.test(c));
2102
+ const hasAuditLog = [...files.values()].some(c => /auditLog|hipaa.*log|phi.*access|accessLog/i.test(c));
2103
+ if (hasPHIAccess && !hasAuditLog) {
2104
+ findings.push({ ruleId: 'COMP-033', category: 'compliance', severity: 'high', title: 'PHI accessed without HIPAA audit trail — HIPAA 164.312(b) violation', description: 'HIPAA requires audit controls that record and examine activity in systems containing PHI. Log who accessed what PHI data and when.', fix: null });
2105
+ }
2106
+ return findings;
2107
+ },
2108
+ });
2109
+
2110
+ // COMP-034: CCPA - No opt-out mechanism
2111
+ rules.push({
2112
+ id: 'COMP-034', category: 'compliance', severity: 'high', confidence: 'likely', title: 'CCPA: No opt-out of data sale mechanism',
2113
+ check({ files }) {
2114
+ const findings = [];
2115
+ const hasUserData = [...files.values()].some(c => /user.*data|personal.*info|analytics|tracking/i.test(c));
2116
+ const hasOptOut = [...files.values()].some(c => /opt-out|optOut|do.*not.*sell|ccpa|doNotSell/i.test(c));
2117
+ if (hasUserData && !hasOptOut) {
2118
+ findings.push({ ruleId: 'COMP-034', category: 'compliance', severity: 'high', title: 'No "Do Not Sell My Personal Information" opt-out — CCPA requirement for CA residents', description: 'CCPA requires businesses that sell personal information to provide a clear opt-out mechanism. Add a "Do Not Sell My Personal Information" link to your privacy page.', fix: null });
2119
+ }
2120
+ return findings;
2121
+ },
2122
+ });
2123
+
2124
+ // COMP-035: Accessibility - Form without label
2125
+ rules.push({
2126
+ id: 'COMP-035', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Accessibility: Form input without associated label',
2127
+ check({ files }) {
2128
+ const findings = [];
2129
+ for (const [fp, c] of files) {
2130
+ if (!fp.match(/\.(jsx|tsx|html|vue)$/)) continue;
2131
+ const lines = c.split('\n');
2132
+ for (let i = 0; i < lines.length; i++) {
2133
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
2134
+ if (/<input\b/.test(lines[i]) && !/type\s*=\s*["']hidden["']/.test(lines[i]) && !/aria-label|aria-labelledby/.test(lines[i])) {
2135
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join('\n');
2136
+ if (!/<label|aria-label|aria-labelledby/.test(ctx)) {
2137
+ findings.push({ ruleId: 'COMP-035', category: 'compliance', severity: 'medium', title: '<input> without label — screen readers cannot identify the field', description: 'Associate form inputs with labels using <label for="id">, aria-label, or aria-labelledby. WCAG 2.1 Success Criterion 1.3.1 (Info and Relationships).', file: fp, line: i + 1, fix: null });
2138
+ }
2139
+ }
2140
+ }
2141
+ }
2142
+ return findings;
2143
+ },
2144
+ });
2145
+
2146
+ // COMP-036: SOC2 - Plaintext passwords in configuration
2147
+ rules.push({
2148
+ id: 'COMP-036', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'SOC2: Plaintext credentials in configuration files',
2149
+ check({ files }) {
2150
+ const findings = [];
2151
+ for (const [fp, c] of files) {
2152
+ if (!fp.match(/\.(?:json|yaml|yml|toml|ini|env)$/)) continue;
2153
+ const lines = c.split('\n');
2154
+ for (let i = 0; i < lines.length; i++) {
2155
+ if (/^\s*[#;]/.test(lines[i])) continue;
2156
+ if (/(?:password|passwd|secret|api_key)\s*[:=]\s*["']?[A-Za-z0-9!@#$%^&*]{8,}["']?/i.test(lines[i]) && !/\$\{|\$\(|process\.env/i.test(lines[i])) {
2157
+ findings.push({ ruleId: 'COMP-036', category: 'compliance', severity: 'critical', title: 'Plaintext credential in config file — SOC2 CC6 control failure', description: 'SOC2 CC6 requires protecting logical access. Plaintext credentials in config files fail this control. Use environment variables or a secrets manager.', file: fp, line: i + 1, fix: null });
2158
+ }
2159
+ }
2160
+ }
2161
+ return findings;
2162
+ },
2163
+ });
2164
+
2165
+ // COMP-037: WCAG - Missing keyboard navigation support
2166
+ rules.push({
2167
+ id: 'COMP-037', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'Accessibility: onClick handler on non-interactive element without keyboard support',
2168
+ check({ files }) {
2169
+ const findings = [];
2170
+ for (const [fp, c] of files) {
2171
+ if (!fp.match(/\.(jsx|tsx)$/)) continue;
2172
+ const lines = c.split('\n');
2173
+ for (let i = 0; i < lines.length; i++) {
2174
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
2175
+ if (/<(?:div|span|p)\s[^>]*onClick\s*=/.test(lines[i]) && !/onKeyDown|onKeyPress|tabIndex|role\s*=/.test(lines[i])) {
2176
+ findings.push({ ruleId: 'COMP-037', category: 'compliance', severity: 'medium', title: 'Clickable div/span without keyboard handler — inaccessible to keyboard users', description: 'Non-interactive elements with onClick need onKeyDown/onKeyPress and tabIndex="0" for keyboard accessibility. Or use <button> which has built-in keyboard support.', file: fp, line: i + 1, fix: null });
2177
+ }
2178
+ }
2179
+ }
2180
+ return findings;
2181
+ },
2182
+ });
2183
+
2184
+ // COMP-038: GDPR - Data processed without legal basis
2185
+ rules.push({
2186
+ id: 'COMP-038', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'GDPR: No terms of service or consent check before data collection',
2187
+ check({ files }) {
2188
+ const findings = [];
2189
+ for (const [fp, c] of files) {
2190
+ if (!fp.match(/\.(jsx|tsx|html|vue)$/)) continue;
2191
+ if (!c.match(/register|signup|sign.?up/i)) continue;
2192
+ if (!c.match(/terms|consent|agree|privacy/i)) {
2193
+ findings.push({ ruleId: 'COMP-038', category: 'compliance', severity: 'medium', title: 'Registration form without terms/consent checkbox — GDPR Article 6 legal basis', description: 'GDPR requires a lawful basis for processing personal data. Include a consent checkbox linking to terms of service and privacy policy on registration forms.', file: fp, fix: null });
2194
+ }
2195
+ }
2196
+ return findings;
2197
+ },
2198
+ });
2199
+
2200
+ // COMP-039: PCI-DSS - Logging card data
2201
+ rules.push({
2202
+ id: 'COMP-039', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'PCI-DSS: Full card data logged — compliance violation',
2203
+ check({ files }) {
2204
+ const findings = [];
2205
+ const p = /(?:console\.|logger\.)\w*\s*\([^)]*(?:card|pan|cvv|expiry|expirationDate)/i;
2206
+ for (const [fp, c] of files) {
2207
+ if (!isSourceFile(fp)) continue;
2208
+ const lines = c.split('\n');
2209
+ for (let i = 0; i < lines.length; i++) {
2210
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
2211
+ if (p.test(lines[i])) {
2212
+ findings.push({ ruleId: 'COMP-039', category: 'compliance', severity: 'critical', title: 'Card data logged — PCI-DSS prohibits logging of card data', description: 'PCI-DSS requires that card data never be written to logs. Remove all logging of card numbers, CVVs, and expiration dates immediately.', file: fp, line: i + 1, fix: null });
2213
+ }
2214
+ }
2215
+ }
2216
+ return findings;
2217
+ },
2218
+ });
2219
+
2220
+ // COMP-040: ISO 27001 - No change management documentation
2221
+ rules.push({
2222
+ id: 'COMP-040', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No CHANGELOG or change management documentation',
2223
+ check({ files }) {
2224
+ const findings = [];
2225
+ const hasRelease = [...files.values()].some(c => /npm.*publish|release|deploy/i.test(c));
2226
+ const hasChangelog = [...files.keys()].some(f => f.match(/CHANGELOG|CHANGES|HISTORY/i));
2227
+ if (hasRelease && !hasChangelog) {
2228
+ findings.push({ ruleId: 'COMP-040', category: 'compliance', severity: 'low', title: 'No CHANGELOG — ISO 27001 A.12.1.2 recommends change management documentation', description: 'Maintain a CHANGELOG documenting changes between versions for compliance audit trails and user communication.', fix: null });
2229
+ }
2230
+ return findings;
2231
+ },
2232
+ });
2233
+
2234
+ // COMP-041 through COMP-060: More compliance rules
2235
+
2236
+ // COMP-041: WCAG - Insufficient color contrast indication
2237
+ rules.push({
2238
+ id: 'COMP-041', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Accessibility: Potential low color contrast ratio',
2239
+ check({ files }) {
2240
+ const findings = [];
2241
+ for (const [fp, c] of files) {
2242
+ if (!fp.match(/\.(css|scss|sass|less)$/)) continue;
2243
+ const lines = c.split('\n');
2244
+ for (let i = 0; i < lines.length; i++) {
2245
+ if (/^\s*\/\/|\/\*/.test(lines[i])) continue;
2246
+ if (/color\s*:\s*#(?:ccc|ddd|eee|aaa|bbb|999|888|777)\b/i.test(lines[i])) {
2247
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join('\n');
2248
+ if (!/background.*#(?:000|111|222|333)|aria|placeholder/i.test(ctx)) {
2249
+ findings.push({ ruleId: 'COMP-041', category: 'compliance', severity: 'low', title: 'Light gray text color — may fail WCAG 1.4.3 contrast ratio (4.5:1)', description: 'Light gray text (#ccc, #ddd) on white backgrounds typically fails WCAG AA contrast requirements. Use a contrast checker to verify 4.5:1 ratio for normal text.', file: fp, line: i + 1, fix: null });
2250
+ }
2251
+ }
2252
+ }
2253
+ }
2254
+ return findings;
2255
+ },
2256
+ });
2257
+
2258
+ // COMP-042: NIST - No multi-factor authentication
2259
+ rules.push({
2260
+ id: 'COMP-042', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No multi-factor authentication implementation',
2261
+ check({ files }) {
2262
+ const findings = [];
2263
+ const hasAuth = [...files.values()].some(c => /login|signin|authenticate/i.test(c));
2264
+ const hasMFA = [...files.values()].some(c => /mfa|2fa|totp|otp|authenticator|two.?factor|second.?factor|speakeasy|otpauth/i.test(c));
2265
+ if (hasAuth && !hasMFA) {
2266
+ findings.push({ ruleId: 'COMP-042', category: 'compliance', severity: 'high', title: 'No MFA implementation — NIST 800-63B recommends MFA for all accounts', description: 'NIST 800-63B recommends multi-factor authentication. Implement TOTP, SMS, or hardware key second factors for user authentication.', fix: null });
2267
+ }
2268
+ return findings;
2269
+ },
2270
+ });
2271
+
2272
+ // COMP-043: GDPR - Data export not implemented
2273
+ rules.push({
2274
+ id: 'COMP-043', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'GDPR: No data portability/export feature',
2275
+ check({ files }) {
2276
+ const findings = [];
2277
+ const storesUserData = [...files.values()].some(c => /user\.create|createUser/i.test(c));
2278
+ const hasExport = [...files.values()].some(c => /exportData|data.*export|portability|downloadMyData|gdpr.*export/i.test(c));
2279
+ if (storesUserData && !hasExport) {
2280
+ findings.push({ ruleId: 'COMP-043', category: 'compliance', severity: 'medium', title: 'No data portability feature — GDPR Article 20 (Right to Data Portability)', description: 'GDPR Article 20 gives users the right to receive their personal data in a machine-readable format. Implement a data export endpoint.', fix: null });
2281
+ }
2282
+ return findings;
2283
+ },
2284
+ });
2285
+
2286
+ // COMP-044: HIPAA - PHI sent via email without encryption
2287
+ rules.push({
2288
+ id: 'COMP-044', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'HIPAA: Health data potentially sent via email without encryption',
2289
+ check({ files }) {
2290
+ const findings = [];
2291
+ for (const [fp, c] of files) {
2292
+ if (!isSourceFile(fp)) continue;
2293
+ if (/sendMail|sendEmail|nodemailer|ses\.sendEmail/i.test(c)) {
2294
+ if (/patient|diagnosis|medication|health|medical/i.test(c) && !/encrypt|tls|starttls/i.test(c)) {
2295
+ findings.push({ ruleId: 'COMP-044', category: 'compliance', severity: 'critical', title: 'PHI potentially included in email without encryption', description: 'HIPAA prohibits transmitting PHI via unencrypted email. Use S/MIME encryption or a HIPAA-compliant messaging service. Never include PHI in email body.', file: fp, fix: null });
2296
+ }
2297
+ }
2298
+ }
2299
+ return findings;
2300
+ },
2301
+ });
2302
+
2303
+ // COMP-045: PCI-DSS - Cross-frame scripting protection missing
2304
+ rules.push({
2305
+ id: 'COMP-045', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'PCI-DSS: No Content-Security-Policy for payment pages',
2306
+ check({ files }) {
2307
+ const findings = [];
2308
+ const hasPaymentPage = [...files.values()].some(c => /checkout|payment|card.*form|stripe|braintree/i.test(c));
2309
+ const hasCSP = [...files.values()].some(c => /Content-Security-Policy|contentSecurityPolicy|helmetCsp|cspHeader/i.test(c));
2310
+ if (hasPaymentPage && !hasCSP) {
2311
+ findings.push({ ruleId: 'COMP-045', category: 'compliance', severity: 'medium', title: 'Payment page without Content-Security-Policy — PCI-DSS 6.4.3 requires strict CSP', description: 'PCI-DSS v4.0 Requirement 6.4.3 requires a CSP to be implemented on payment pages to prevent unauthorized scripts from stealing card data.', fix: null });
2312
+ }
2313
+ return findings;
2314
+ },
2315
+ });
2316
+
2317
+ // COMP-046: SOC2 - No incident response plan indicators
2318
+ rules.push({
2319
+ id: 'COMP-046', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'SOC2: No incident response contact or runbook found',
2320
+ check({ files }) {
2321
+ const findings = [];
2322
+ const hasProduction = [...files.values()].some(c => /NODE_ENV.*production|ENVIRONMENT.*prod/i.test(c));
2323
+ const hasIncidentResponse = [...files.keys()].some(f => f.match(/INCIDENT|RUNBOOK|ON_CALL|ESCALATION|incident-response|runbook/i));
2324
+ if (hasProduction && !hasIncidentResponse) {
2325
+ findings.push({ ruleId: 'COMP-046', category: 'compliance', severity: 'low', title: 'No incident response runbook — SOC2 CC7.3 requires defined incident response', description: 'SOC2 CC7.3 requires documented incident response procedures. Create an INCIDENT_RESPONSE.md or runbook documenting escalation paths and response steps.', fix: null });
2326
+ }
2327
+ return findings;
2328
+ },
2329
+ });
2330
+
2331
+ // COMP-047: GDPR - No data breach notification plan
2332
+ rules.push({
2333
+ id: 'COMP-047', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'GDPR: No data breach notification mechanism',
2334
+ check({ files }) {
2335
+ const findings = [];
2336
+ const storesPII = [...files.values()].some(c => /email.*save|createUser|userRecord/i.test(c));
2337
+ const hasBreachNotification = [...files.values()].some(c => /breach.*notif|dataBreachReport|security.*incident|GDPR.*72.*hour|notify.*dpa/i.test(c));
2338
+ if (storesPII && !hasBreachNotification) {
2339
+ findings.push({ ruleId: 'COMP-047', category: 'compliance', severity: 'medium', title: 'No data breach notification procedure — GDPR Article 33 requires 72-hour notification to DPA', description: 'GDPR requires notifying the supervisory authority within 72 hours of a data breach. Document and implement breach detection and notification procedures.', fix: null });
2340
+ }
2341
+ return findings;
2342
+ },
2343
+ });
2344
+
2345
+ // COMP-048: Accessibility - Missing skip navigation link
2346
+ rules.push({
2347
+ id: 'COMP-048', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Accessibility: No skip navigation link for keyboard users',
2348
+ check({ files }) {
2349
+ const findings = [];
2350
+ for (const [fp, c] of files) {
2351
+ if (!fp.match(/\.(jsx|tsx|html)$/)) continue;
2352
+ if (/<nav\b/.test(c) && !/<a[^>]*skip|skip.*navigation|skip.*main|skip.*content/i.test(c)) {
2353
+ findings.push({ ruleId: 'COMP-048', category: 'compliance', severity: 'low', title: 'Navigation without skip link — keyboard users must tab through all nav items', description: 'Add a "Skip to main content" link as the first focusable element. This allows keyboard users to bypass repetitive navigation. WCAG 2.4.1.', file: fp, fix: null });
2354
+ }
2355
+ }
2356
+ return findings;
2357
+ },
2358
+ });
2359
+
2360
+ // COMP-049: FERPA - Student records without access control
2361
+ rules.push({
2362
+ id: 'COMP-049', category: 'compliance', severity: 'high', confidence: 'likely', title: 'FERPA: Student data accessed without role-based access control',
2363
+ check({ files }) {
2364
+ const findings = [];
2365
+ const hasStudentData = [...files.values()].some(c => /student|enrollment|grade|transcript|course_record/i.test(c));
2366
+ const hasAccessControl = [...files.values()].some(c => /role.*admin|instructor.*permission|authorize|FERPA|educationRecord/i.test(c));
2367
+ if (hasStudentData && !hasAccessControl) {
2368
+ findings.push({ ruleId: 'COMP-049', category: 'compliance', severity: 'high', title: 'Student education records without FERPA access controls', description: 'FERPA requires restricting access to student education records. Implement role-based access control to ensure only authorized users (instructors, admins) can access student data.', fix: null });
2369
+ }
2370
+ return findings;
2371
+ },
2372
+ });
2373
+
2374
+ // COMP-050: Missing security.txt file
2375
+ rules.push({
2376
+ id: 'COMP-050', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No security.txt file — no responsible disclosure channel',
2377
+ check({ files }) {
2378
+ const findings = [];
2379
+ const hasWebServer = [...files.values()].some(c => /express|fastify|koa|hapi/i.test(c));
2380
+ const hasSecurityTxt = [...files.keys()].some(f => f.match(/security\.txt$/i));
2381
+ if (hasWebServer && !hasSecurityTxt) {
2382
+ findings.push({ ruleId: 'COMP-050', category: 'compliance', severity: 'low', title: 'No security.txt (RFC 9116) — no standard way for researchers to report vulnerabilities', description: 'Create /.well-known/security.txt following RFC 9116 to provide contact information for security researchers to report vulnerabilities responsibly.', fix: null });
2383
+ }
2384
+ return findings;
2385
+ },
2386
+ });
2387
+
2388
+ // COMP-051: Missing CCPA opt-out mechanism
2389
+ rules.push({
2390
+ id: 'COMP-051', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'CCPA: No "Do Not Sell" opt-out link found',
2391
+ check({ files }) {
2392
+ const findings = [];
2393
+ const hasCalifornia = [...files.keys()].some(f => /california|ccpa|donotsell|do-not-sell/i.test(f));
2394
+ const hasOptOut = [...files.values()].some(c => /do.not.sell|doNotSell|opt.out|optout/i.test(c));
2395
+ if (!hasCalifornia && !hasOptOut) {
2396
+ const jsFiles = [...files.keys()].filter(f => f.endsWith('.js') || f.endsWith('.jsx') || f.endsWith('.ts') || f.endsWith('.tsx'));
2397
+ if (jsFiles.length > 0) findings.push({ ruleId: 'COMP-051', category: 'compliance', severity: 'medium', title: 'CCPA: No "Do Not Sell My Personal Information" opt-out mechanism', description: 'California CCPA requires a clear "Do Not Sell My Personal Information" link for businesses that sell consumer data.', file: jsFiles[0], fix: null });
2398
+ }
2399
+ return findings;
2400
+ },
2401
+ });
2402
+
2403
+ // COMP-052: Missing accessibility for error messages
2404
+ rules.push({
2405
+ id: 'COMP-052', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'WCAG: Form error messages not associated with fields (aria-describedby)',
2406
+ check({ files }) {
2407
+ const findings = [];
2408
+ for (const [fp, c] of files) {
2409
+ if (!fp.match(/\.[jt]sx?$|\.html$/)) continue;
2410
+ const lines = c.split('\n');
2411
+ for (let i = 0; i < lines.length; i++) {
2412
+ if (/error|invalid|required/i.test(lines[i]) && /<(?:p|span|div)[^>]*class[^>]*error/i.test(lines[i]) && !/aria-describedby|role="alert"/.test(lines[i])) {
2413
+ findings.push({ ruleId: 'COMP-052', category: 'compliance', severity: 'medium', title: 'Error message without aria-describedby or role="alert" — screen readers may miss it', description: 'Add role="alert" to error messages or use aria-describedby on the input field to link it to the error.', file: fp, line: i + 1, fix: null });
2414
+ break;
2415
+ }
2416
+ }
2417
+ }
2418
+ return findings;
2419
+ },
2420
+ });
2421
+
2422
+ // COMP-053: Hardcoded strings in UI (i18n violations)
2423
+ rules.push({
2424
+ id: 'COMP-053', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'Hardcoded UI strings without i18n support',
2425
+ check({ files }) {
2426
+ const findings = [];
2427
+ for (const [fp, c] of files) {
2428
+ if (!fp.match(/\.[jt]sx$/) || fp.includes('test') || fp.includes('spec')) continue;
2429
+ if (/<[a-z]+[^>]*>[A-Z][a-zA-Z\s]{5,}<\//.test(c) && !/i18n|useTranslation|intl|t\(|formatMessage/.test(c)) {
2430
+ findings.push({ ruleId: 'COMP-053', category: 'compliance', severity: 'low', title: 'Hardcoded UI text without internationalization — accessibility/compliance issue for global apps', description: 'Use an i18n library (react-intl, i18next) to externalize user-facing strings for localization.', file: fp, fix: null });
2431
+ }
2432
+ }
2433
+ return findings;
2434
+ },
2435
+ });
2436
+
2437
+ // COMP-054: Missing data processing agreement indicator
2438
+ rules.push({
2439
+ id: 'COMP-054', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'GDPR: Third-party data processor used without DPA indication',
2440
+ check({ files }) {
2441
+ const findings = [];
2442
+ const thirdParties = ['segment', 'amplitude', 'mixpanel', 'intercom', 'hubspot', 'salesforce'];
2443
+ for (const [fp, c] of files) {
2444
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts')) continue;
2445
+ for (const tp of thirdParties) {
2446
+ if (new RegExp(`require.*${tp}|from.*${tp}|${tp}\\.track|${tp}\.identify`, 'i').test(c) && !/dpa|data.processing|gdpr/i.test(c)) {
2447
+ findings.push({ ruleId: 'COMP-054', category: 'compliance', severity: 'medium', title: `GDPR: ${tp} integration without DPA documentation`, description: `Ensure a Data Processing Agreement is in place with ${tp} and document data flows for GDPR compliance.`, file: fp, fix: null });
2448
+ break;
2449
+ }
2450
+ }
2451
+ }
2452
+ return findings;
2453
+ },
2454
+ });
2455
+
2456
+ // COMP-055: Accessibility - missing lang attribute
2457
+ rules.push({
2458
+ id: 'COMP-055', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'WCAG 3.1.1: HTML document without lang attribute',
2459
+ check({ files }) {
2460
+ const findings = [];
2461
+ for (const [fp, c] of files) {
2462
+ if (!fp.endsWith('.html')) continue;
2463
+ if (/<html[^>]*>/i.test(c) && !/<html[^>]*lang=/i.test(c)) findings.push({ ruleId: 'COMP-055', category: 'compliance', severity: 'medium', title: 'HTML document without lang attribute — screen readers use wrong language', description: 'Add lang attribute to the <html> element: <html lang="en">.', file: fp, fix: null });
2464
+ }
2465
+ return findings;
2466
+ },
2467
+ });
2468
+
2469
+ // COMP-056: HIPAA - medical data without audit trail
2470
+ rules.push({
2471
+ id: 'COMP-056', category: 'compliance', severity: 'high', confidence: 'likely', title: 'HIPAA: Medical record access without audit logging',
2472
+ check({ files }) {
2473
+ const findings = [];
2474
+ for (const [fp, c] of files) {
2475
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts')) continue;
2476
+ if (/(?:diagnosis|prescription|medical.record|patient.data|health.record)/i.test(c) && !/audit|log\.\w+|logger\.\w+/i.test(c)) {
2477
+ findings.push({ ruleId: 'COMP-056', category: 'compliance', severity: 'high', title: 'HIPAA: Medical data access without audit trail', description: 'Log all access to medical records with user identity, timestamp, and action for HIPAA audit requirements.', file: fp, fix: null });
2478
+ }
2479
+ }
2480
+ return findings;
2481
+ },
2482
+ });
2483
+
2484
+ // COMP-057: PCI-DSS - card data in memory longer than needed
2485
+ rules.push({
2486
+ id: 'COMP-057', category: 'compliance', severity: 'high', confidence: 'likely', title: 'PCI-DSS: Card data stored in variable after processing',
2487
+ check({ files }) {
2488
+ const findings = [];
2489
+ for (const [fp, c] of files) {
2490
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts')) continue;
2491
+ const lines = c.split('\n');
2492
+ for (let i = 0; i < lines.length; i++) {
2493
+ if (/(?:cardNumber|card_number|creditCard|credit_card)\s*=/.test(lines[i]) && !/null|undefined|delete|void/.test(lines[i])) {
2494
+ const block = lines.slice(i, Math.min(lines.length, i + 20)).join('\n');
2495
+ if (!/delete\s+\w+|= null|= undefined/.test(block)) findings.push({ ruleId: 'COMP-057', category: 'compliance', severity: 'high', title: 'PCI-DSS: Card number stored in variable without explicit clearing', description: 'Clear card data from memory immediately after use. Set variable to null and avoid storing in state.', file: fp, line: i + 1, fix: null });
2496
+ }
2497
+ }
2498
+ }
2499
+ return findings;
2500
+ },
2501
+ });
2502
+
2503
+ // COMP-058: Missing cookie notice
2504
+ rules.push({
2505
+ id: 'COMP-058', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'GDPR/ePrivacy: No cookie consent mechanism found',
2506
+ check({ files }) {
2507
+ const findings = [];
2508
+ const hasCookieConsent = [...files.values()].some(c => /cookie.consent|cookieConsent|cookie-consent|cookieBanner|cookie.notice|CookiePolicy/i.test(c));
2509
+ const hasCookieSet = [...files.values()].some(c => /document\.cookie\s*=|res\.cookie\s*\(|setCookie/i.test(c));
2510
+ if (hasCookieSet && !hasCookieConsent) {
2511
+ const cookieFile = [...files.keys()].find(fp => /document\.cookie\s*=|res\.cookie\s*\(/.test(files.get(fp) || ''));
2512
+ if (cookieFile) findings.push({ ruleId: 'COMP-058', category: 'compliance', severity: 'medium', title: 'Cookies set without consent mechanism — GDPR/ePrivacy compliance required', description: 'Implement a cookie consent banner and only set non-essential cookies after user consent.', file: cookieFile, fix: null });
2513
+ }
2514
+ return findings;
2515
+ },
2516
+ });
2517
+
2518
+ // COMP-059: Accessibility - interactive elements not keyboard accessible
2519
+ rules.push({
2520
+ id: 'COMP-059', category: 'compliance', severity: 'high', confidence: 'likely', title: 'WCAG 2.1.1: onClick handler on non-interactive element without keyboard support',
2521
+ check({ files }) {
2522
+ const findings = [];
2523
+ for (const [fp, c] of files) {
2524
+ if (!fp.match(/\.[jt]sx?$/)) continue;
2525
+ const lines = c.split('\n');
2526
+ for (let i = 0; i < lines.length; i++) {
2527
+ if (/<(?:div|span)[^>]*onClick/.test(lines[i]) && !/onKeyDown|onKeyPress|onKeyUp|role=|tabIndex/.test(lines[i])) {
2528
+ findings.push({ ruleId: 'COMP-059', category: 'compliance', severity: 'high', title: 'div/span with onClick but no keyboard handler — not keyboard accessible', description: 'Add onKeyDown handler, role="button", and tabIndex={0} to make clickable divs keyboard accessible.', file: fp, line: i + 1, fix: null });
2529
+ }
2530
+ }
2531
+ }
2532
+ return findings;
2533
+ },
2534
+ });
2535
+
2536
+ // COMP-060: SOC2 - missing data encryption in transit
2537
+ rules.push({
2538
+ id: 'COMP-060', category: 'compliance', severity: 'high', confidence: 'likely', title: 'SOC2: HTTP connection to external service without TLS',
2539
+ check({ files }) {
2540
+ const findings = [];
2541
+ for (const [fp, c] of files) {
2542
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts')) continue;
2543
+ const lines = c.split('\n');
2544
+ for (let i = 0; i < lines.length; i++) {
2545
+ if (/(?:fetch|axios\.get|axios\.post|http\.get|http\.request)\s*\(\s*['"]http:\/\//i.test(lines[i]) && !/localhost|127\.0\.0\.1|test|spec/.test(lines[i])) {
2546
+ findings.push({ ruleId: 'COMP-060', category: 'compliance', severity: 'high', title: 'HTTP (non-TLS) connection to external endpoint — data in transit unencrypted', description: 'Use HTTPS for all external service connections to ensure data in transit encryption.', file: fp, line: i + 1, fix: null });
2547
+ }
2548
+ }
2549
+ }
2550
+ return findings;
2551
+ },
2552
+ });
2553
+
2554
+ // COMP-061: COPPA - no age gate on sign-up
2555
+ rules.push({
2556
+ id: 'COMP-061', category: 'compliance', severity: 'high', confidence: 'likely', title: 'COPPA: User registration without age verification',
2557
+ check({ files }) {
2558
+ const findings = [];
2559
+ for (const [fp, c] of files) {
2560
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts') && !fp.match(/\.[jt]sx?$/)) continue;
2561
+ if (/register|signup|sign.up/i.test(c) && !/age|birthdate|birth.date|dob|dateOfBirth|coppa|13/i.test(c)) {
2562
+ findings.push({ ruleId: 'COMP-061', category: 'compliance', severity: 'high', title: 'COPPA: Registration form without age verification', description: 'If your service may be used by children under 13, implement COPPA-compliant age verification.', file: fp, fix: null });
2563
+ }
2564
+ }
2565
+ return findings;
2566
+ },
2567
+ });
2568
+
2569
+ // COMP-062: Missing Terms of Service acceptance tracking
2570
+ rules.push({
2571
+ id: 'COMP-062', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'User registration without Terms of Service acceptance',
2572
+ check({ files }) {
2573
+ const findings = [];
2574
+ for (const [fp, c] of files) {
2575
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts')) continue;
2576
+ if (/(?:router|app)\.post\s*\([^)]*(?:register|signup)/i.test(c) && !/tos|terms|termsAccepted|terms_accepted|acceptTerms/i.test(c)) {
2577
+ findings.push({ ruleId: 'COMP-062', category: 'compliance', severity: 'medium', title: 'Registration endpoint without ToS acceptance tracking', description: 'Record user acceptance of Terms of Service with timestamp for legal compliance.', file: fp, fix: null });
2578
+ }
2579
+ }
2580
+ return findings;
2581
+ },
2582
+ });
2583
+
2584
+ // COMP-063: GDPR - no data export endpoint
2585
+ rules.push({
2586
+ id: 'COMP-063', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'GDPR Article 20: No data portability/export endpoint',
2587
+ check({ files }) {
2588
+ const findings = [];
2589
+ const hasExport = [...files.keys()].some(f => /export|download|portab/i.test(f)) ||
2590
+ [...files.values()].some(c => /(?:router|app)\.get\s*\([^)]*(?:export|download|portab)/i.test(c));
2591
+ if (!hasExport) {
2592
+ const routerFiles = [...files.keys()].filter(f => f.endsWith('.js') || f.endsWith('.ts'));
2593
+ if (routerFiles.length > 0) findings.push({ ruleId: 'COMP-063', category: 'compliance', severity: 'medium', title: 'GDPR Article 20: No data export/portability endpoint found', description: 'Implement a data export endpoint allowing users to download their personal data in a portable format.', file: routerFiles[0], fix: null });
2594
+ }
2595
+ return findings;
2596
+ },
2597
+ });
2598
+
2599
+ // COMP-064: HIPAA - PHI in debug logs
2600
+ rules.push({
2601
+ id: 'COMP-064', category: 'compliance', severity: 'critical', confidence: 'definite', title: 'HIPAA: PHI fields may appear in debug logs',
2602
+ check({ files }) {
2603
+ const findings = [];
2604
+ for (const [fp, c] of files) {
2605
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts')) continue;
2606
+ const lines = c.split('\n');
2607
+ for (let i = 0; i < lines.length; i++) {
2608
+ if (/console\.(?:log|debug|info)\s*\([^)]*(?:patient|diagnosis|medication|ssn|dob|dateOfBirth)/i.test(lines[i])) {
2609
+ findings.push({ ruleId: 'COMP-064', category: 'compliance', severity: 'critical', title: 'HIPAA: PHI logged to console — PHI must not appear in logs', description: 'Remove PHI from log statements. Mask or omit health information from all log output.', file: fp, line: i + 1, fix: null });
2610
+ }
2611
+ }
2612
+ }
2613
+ return findings;
2614
+ },
2615
+ });
2616
+
2617
+ // COMP-065: ISO 27001 - no vulnerability disclosure policy
2618
+ rules.push({
2619
+ id: 'COMP-065', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'ISO 27001: No SECURITY.md or vulnerability disclosure policy',
2620
+ check({ files }) {
2621
+ const findings = [];
2622
+ const hasSecurity = [...files.keys()].some(f => /SECURITY\.md|security\.txt/i.test(f));
2623
+ if (!hasSecurity) {
2624
+ const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
2625
+ if (pkgJson) findings.push({ ruleId: 'COMP-065', category: 'compliance', severity: 'low', title: 'No SECURITY.md or security.txt — no vulnerability disclosure channel', description: 'Add a SECURITY.md file with instructions for reporting security vulnerabilities responsibly.', file: pkgJson, fix: null });
2626
+ }
2627
+ return findings;
2628
+ },
2629
+ });
2630
+
2631
+ // COMP-066: Missing content security policy
2632
+ rules.push({
2633
+ id: 'COMP-066', category: 'compliance', severity: 'high', confidence: 'likely', title: 'No Content-Security-Policy header configured',
2634
+ check({ files }) {
2635
+ const findings = [];
2636
+ const hasCSP = [...files.values()].some(c => /Content-Security-Policy|contentSecurityPolicy|helmet\s*\(/i.test(c));
2637
+ if (!hasCSP) {
2638
+ const serverFiles = [...files.keys()].filter(f => /server|app|index/.test(f) && (f.endsWith('.js') || f.endsWith('.ts')));
2639
+ if (serverFiles.length > 0) findings.push({ ruleId: 'COMP-066', category: 'compliance', severity: 'high', title: 'No Content-Security-Policy header — XSS attacks not mitigated', description: 'Configure CSP headers using helmet or manually setting Content-Security-Policy response header.', file: serverFiles[0], fix: null });
2640
+ }
2641
+ return findings;
2642
+ },
2643
+ });
2644
+
2645
+ // COMP-067: GDPR - sensitive data used for analytics without consent
2646
+ rules.push({
2647
+ id: 'COMP-067', category: 'compliance', severity: 'high', confidence: 'likely', title: 'GDPR: Email/name sent to analytics without consent check',
2648
+ check({ files }) {
2649
+ const findings = [];
2650
+ for (const [fp, c] of files) {
2651
+ if (!fp.endsWith('.js') && !fp.endsWith('.ts') && !fp.match(/\.[jt]sx?$/)) continue;
2652
+ if (/(?:analytics|gtag|ga|segment)\s*\.\s*(?:track|identify)\s*\([^)]*(?:email|name|phone)/i.test(c) && !/consent|hasConsent|gdpr|analyticsEnabled/i.test(c)) {
2653
+ findings.push({ ruleId: 'COMP-067', category: 'compliance', severity: 'high', title: 'PII sent to analytics without consent check — GDPR violation', description: 'Only send personal data to analytics services after obtaining explicit user consent.', file: fp, fix: null });
2654
+ }
2655
+ }
2656
+ return findings;
2657
+ },
2658
+ });
2659
+
2660
+ // COMP-068: Missing WCAG focus indicator
2661
+ rules.push({
2662
+ id: 'COMP-068', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'WCAG 2.4.7: CSS outline:none removes keyboard focus indicator',
2663
+ check({ files }) {
2664
+ const findings = [];
2665
+ for (const [fp, c] of files) {
2666
+ if (!fp.endsWith('.css') && !fp.endsWith('.scss') && !fp.endsWith('.sass')) continue;
2667
+ if (/outline\s*:\s*none|outline\s*:\s*0/.test(c) && !/focus-visible/.test(c)) findings.push({ ruleId: 'COMP-068', category: 'compliance', severity: 'medium', title: 'CSS outline:none removes focus indicator — keyboard users cannot see focus', description: 'Replace outline:none with :focus-visible styles to maintain keyboard navigation accessibility.', file: fp, fix: null });
2668
+ }
2669
+ return findings;
2670
+ },
2671
+ });
2672
+
2673
+ // COMP-069: SOC2 - no log retention policy
2674
+ rules.push({
2675
+ id: 'COMP-069', category: 'compliance', severity: 'medium', confidence: 'likely', title: 'SOC2: No log retention configuration found',
2676
+ check({ files }) {
2677
+ const findings = [];
2678
+ const hasRetention = [...files.values()].some(c => /retention|log.*ttl|logRetention|retentionDays/i.test(c)) ||
2679
+ [...files.keys()].some(f => /cloudwatch|logging\.ya?ml|log4j|logback/.test(f));
2680
+ if (!hasRetention) {
2681
+ const tfFiles = [...files.keys()].filter(f => f.endsWith('.tf'));
2682
+ if (tfFiles.length > 0) findings.push({ ruleId: 'COMP-069', category: 'compliance', severity: 'medium', title: 'SOC2: No log retention policy configured', description: 'Configure log retention periods (typically 90-365 days) to meet SOC2 audit evidence requirements.', file: tfFiles[0], fix: null });
2683
+ }
2684
+ return findings;
2685
+ },
2686
+ });
2687
+
2688
+ // COMP-070: Missing robots.txt to protect sensitive paths
2689
+ rules.push({
2690
+ id: 'COMP-070', category: 'compliance', severity: 'low', confidence: 'suggestion', title: 'No robots.txt to restrict search engine crawling of sensitive paths',
2691
+ check({ files }) {
2692
+ const findings = [];
2693
+ const hasRobots = [...files.keys()].some(f => /robots\.txt/.test(f));
2694
+ if (!hasRobots) {
2695
+ const pkgJson = [...files.keys()].find(f => f.endsWith('package.json'));
2696
+ if (pkgJson) findings.push({ ruleId: 'COMP-070', category: 'compliance', severity: 'low', title: 'No robots.txt file — admin/api paths may be indexed by search engines', description: 'Create a robots.txt file to prevent search engines from indexing sensitive application paths.', file: pkgJson, fix: null });
2697
+ }
2698
+ return findings;
2699
+ },
2700
+ });
2701
+
2702
+ // Aggregate all compliance rules (after all rules.push() calls above)
2703
+ const allComplianceRules = [
2704
+ ...rules,
2705
+ ...healthcareRules,
2706
+ ...regionalEuRules,
2707
+ ...regionalIntlRules,
2708
+ ...educationRules,
2709
+ ...financialRules,
2710
+ ...frameworksRules,
2711
+ ...accessibilityExtRules,
2712
+ ];
2713
+
2714
+ export default allComplianceRules;