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,468 @@
1
+ function isSourceFile(f) { return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].some(e => f.endsWith(e)); }
2
+ function isWebFile(f) { return ['.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte'].some(e => f.endsWith(e)); }
3
+
4
+ const rules = [
5
+ // --- Section 508 Rules ---
6
+
7
+ // COMP-SEC508-001: Video without captions
8
+ {
9
+ id: 'COMP-SEC508-001',
10
+ category: 'compliance',
11
+ severity: 'high',
12
+ confidence: 'likely',
13
+ title: 'Video Without Captions',
14
+ check({ files }) {
15
+ const findings = [];
16
+ for (const [filepath, content] of files.entries()) {
17
+ if (!isWebFile(filepath)) continue;
18
+ const lines = content.split('\n');
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+
22
+ // Detect <video> tags without an associated <track> element
23
+ if (/<video[\s>]/i.test(line)) {
24
+ // Look ahead up to 15 lines for a closing </video> and a <track element
25
+ const block = lines.slice(i, i + 15).join('\n');
26
+ const hasTrack = /<track\s/i.test(block);
27
+ const hasCaptionAttr = /captions|subtitles/i.test(block);
28
+ if (!hasTrack && !hasCaptionAttr) {
29
+ findings.push({
30
+ ruleId: 'COMP-SEC508-001', category: 'compliance', severity: 'high',
31
+ title: 'Video element without captions or subtitles track',
32
+ description: 'Section 508 requires synchronized captions for video content. Add a <track kind="captions"> element inside the <video> tag.',
33
+ file: filepath, line: i + 1, fix: null,
34
+ });
35
+ }
36
+ }
37
+
38
+ // Detect iframe/embed pointing to video services without caption params
39
+ if (/<(?:iframe|embed)\s[^>]*(?:youtube|vimeo|dailymotion|wistia|vidyard)/i.test(line)) {
40
+ const hasCaption = /cc_load_policy|texttrack|subtitles|captions|cc=1/i.test(line);
41
+ if (!hasCaption) {
42
+ findings.push({
43
+ ruleId: 'COMP-SEC508-001', category: 'compliance', severity: 'high',
44
+ title: 'Embedded video player without captions enabled',
45
+ description: 'Section 508 requires captions for video. Enable captions via the embed URL parameters (e.g., cc_load_policy=1 for YouTube).',
46
+ file: filepath, line: i + 1, fix: null,
47
+ });
48
+ }
49
+ }
50
+ }
51
+
52
+ // Detect video player libraries in source files without caption configuration
53
+ if (isSourceFile(filepath)) {
54
+ const usesVideoLib = /(?:video\.js|videojs|ReactPlayer|plyr|hls\.js|dash\.js|JWPlayer|jwplayer)/i.test(content);
55
+ if (usesVideoLib) {
56
+ const hasCaptionConfig = /tracks|captions|subtitles|textTracks|addTextTrack|cc|closedCaptions/i.test(content);
57
+ if (!hasCaptionConfig) {
58
+ findings.push({
59
+ ruleId: 'COMP-SEC508-001', category: 'compliance', severity: 'high',
60
+ title: 'Video player library used without caption configuration',
61
+ description: 'Section 508 requires synchronized captions. Configure caption/subtitle tracks in your video player setup.',
62
+ file: filepath, line: 1, fix: null,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return findings;
69
+ },
70
+ },
71
+
72
+ // COMP-SEC508-002: Audio without text alternative
73
+ {
74
+ id: 'COMP-SEC508-002',
75
+ category: 'compliance',
76
+ severity: 'high',
77
+ confidence: 'likely',
78
+ title: 'Audio Without Text Alternative',
79
+ check({ files }) {
80
+ const findings = [];
81
+ for (const [filepath, content] of files.entries()) {
82
+ if (!isWebFile(filepath)) continue;
83
+ const lines = content.split('\n');
84
+ for (let i = 0; i < lines.length; i++) {
85
+ const line = lines[i];
86
+
87
+ if (/<audio[\s>]/i.test(line)) {
88
+ // Look for transcript references in the surrounding area (30 lines after)
89
+ const surroundingBlock = lines.slice(i, i + 30).join('\n');
90
+ const hasTranscript = /transcript|aria-describedby|aria-description|text.?alternative|\.vtt|\.srt/i.test(surroundingBlock);
91
+ if (!hasTranscript) {
92
+ // Also check the whole file for a transcript section
93
+ const fileHasTranscript = /transcript|text.?alternative|aria-describedby/i.test(content);
94
+ if (!fileHasTranscript) {
95
+ findings.push({
96
+ ruleId: 'COMP-SEC508-002', category: 'compliance', severity: 'high',
97
+ title: 'Audio element without transcript or text alternative',
98
+ description: 'Section 508 requires a text transcript for audio-only content. Provide a transcript link or aria-describedby reference near the audio element.',
99
+ file: filepath, line: i + 1, fix: null,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return findings;
107
+ },
108
+ },
109
+
110
+ // COMP-SEC508-003: Time-based content without pause/stop
111
+ {
112
+ id: 'COMP-SEC508-003',
113
+ category: 'compliance',
114
+ severity: 'medium',
115
+ confidence: 'likely',
116
+ title: 'Auto-Playing Content Without Pause/Stop Control',
117
+ check({ files }) {
118
+ const findings = [];
119
+ for (const [filepath, content] of files.entries()) {
120
+ if (!isWebFile(filepath) && !isSourceFile(filepath)) continue;
121
+ const lines = content.split('\n');
122
+ for (let i = 0; i < lines.length; i++) {
123
+ const line = lines[i];
124
+
125
+ // Detect autoplay on video/audio elements
126
+ if (/<(?:video|audio)\s[^>]*autoplay/i.test(line)) {
127
+ const surroundingBlock = lines.slice(Math.max(0, i - 10), i + 20).join('\n');
128
+ const hasPauseControl = /pause|stop|controls[\s>=]/i.test(surroundingBlock);
129
+ if (!hasPauseControl) {
130
+ findings.push({
131
+ ruleId: 'COMP-SEC508-003', category: 'compliance', severity: 'medium',
132
+ title: 'Auto-playing media without pause/stop controls',
133
+ description: 'Section 508 requires a mechanism to pause, stop, or hide auto-playing content. Add the "controls" attribute or provide custom pause/stop buttons.',
134
+ file: filepath, line: i + 1, fix: null,
135
+ });
136
+ }
137
+ }
138
+ }
139
+
140
+ // Detect auto-playing carousels/slideshows/animations in source files
141
+ if (isSourceFile(filepath)) {
142
+ const hasAutoCarousel = /autoplay|autoPlay|auto.?slide|auto.?scroll|auto.?rotate|setInterval\s*\([^)]*(?:slide|carousel|next|rotate)/i.test(content);
143
+ if (hasAutoCarousel) {
144
+ const hasPause = /pause|stop|clearInterval|isPaused|isPlaying|togglePlay|onMouseEnter/i.test(content);
145
+ if (!hasPause) {
146
+ findings.push({
147
+ ruleId: 'COMP-SEC508-003', category: 'compliance', severity: 'medium',
148
+ title: 'Auto-advancing content without pause mechanism',
149
+ description: 'Section 508 requires users be able to pause, stop, or hide moving/auto-updating content. Add pause/stop controls to carousels, slideshows, or animations.',
150
+ file: filepath, line: 1, fix: null,
151
+ });
152
+ }
153
+ }
154
+ }
155
+ }
156
+ return findings;
157
+ },
158
+ },
159
+
160
+ // COMP-SEC508-004: Missing skip navigation
161
+ {
162
+ id: 'COMP-SEC508-004',
163
+ category: 'compliance',
164
+ severity: 'high',
165
+ confidence: 'likely',
166
+ title: 'Missing Skip Navigation Link',
167
+ check({ files }) {
168
+ const findings = [];
169
+ const webFiles = [...files.entries()].filter(([f]) => isWebFile(f));
170
+ if (webFiles.length === 0) return findings;
171
+
172
+ // Only check files that look like full pages or layout components
173
+ for (const [filepath, content] of webFiles) {
174
+ const isPageLayout = /<(?:html|head|body|header|nav|Header|Navigation|Layout|AppLayout)/i.test(content) ||
175
+ /function\s+(?:Layout|App|Header|Page)\b/i.test(content) ||
176
+ /(?:export\s+default\s+)?(?:class|const)\s+(?:Layout|App|Header|Page)\b/i.test(content);
177
+
178
+ if (!isPageLayout) continue;
179
+
180
+ const hasNav = /<nav[\s>]|role=["']navigation/i.test(content);
181
+ if (!hasNav) continue;
182
+
183
+ const hasSkipLink = /skip.?(?:to|nav|content|main)|skiplink|skip.?link|#main.?content|#content|href=["']#main/i.test(content);
184
+ if (!hasSkipLink) {
185
+ findings.push({
186
+ ruleId: 'COMP-SEC508-004', category: 'compliance', severity: 'high',
187
+ title: 'Page layout with navigation but no skip navigation link',
188
+ description: 'Section 508 requires a mechanism to bypass repeated blocks of navigation. Add a "Skip to main content" link as the first focusable element.',
189
+ file: filepath, line: 1, fix: null,
190
+ });
191
+ }
192
+ }
193
+ return findings;
194
+ },
195
+ },
196
+
197
+ // COMP-SEC508-005: Documents without accessibility
198
+ {
199
+ id: 'COMP-SEC508-005',
200
+ category: 'compliance',
201
+ severity: 'medium',
202
+ confidence: 'likely',
203
+ title: 'PDF Generation Without Accessibility Options',
204
+ check({ files }) {
205
+ const findings = [];
206
+ for (const [filepath, content] of files.entries()) {
207
+ if (!isSourceFile(filepath)) continue;
208
+
209
+ // jsPDF without accessibility
210
+ if (/jsPDF|new\s+jspdf/i.test(content)) {
211
+ const hasA11y = /setDocumentProperties|setLanguage|tagged|structTree|markContent|addMetadata|accessibility/i.test(content);
212
+ if (!hasA11y) {
213
+ findings.push({
214
+ ruleId: 'COMP-SEC508-005', category: 'compliance', severity: 'medium',
215
+ title: 'jsPDF used without accessibility/tagging options',
216
+ description: 'Section 508 requires accessible (tagged) PDFs. Use jsPDF accessibility plugins or set document properties (title, language, structure tags).',
217
+ file: filepath, line: 1, fix: null,
218
+ });
219
+ }
220
+ }
221
+
222
+ // PDFKit without accessibility
223
+ if (/require\s*\(\s*['"]pdfkit['"]\)|from\s+['"]pdfkit['"]/i.test(content)) {
224
+ const hasA11y = /tagged|markContent|structParentTree|addStructure|\.info\s*=|pdfVersion.*['"]1\.[5-9]/i.test(content);
225
+ if (!hasA11y) {
226
+ findings.push({
227
+ ruleId: 'COMP-SEC508-005', category: 'compliance', severity: 'medium',
228
+ title: 'PDFKit used without tagged PDF or accessibility metadata',
229
+ description: 'Section 508 requires accessible PDFs. Set document metadata (title, language) and consider generating tagged PDF structure.',
230
+ file: filepath, line: 1, fix: null,
231
+ });
232
+ }
233
+ }
234
+
235
+ // Puppeteer PDF generation without accessibility
236
+ if (/\.pdf\s*\(|page\.pdf\s*\(/i.test(content) && /puppeteer|playwright/i.test(content)) {
237
+ const hasA11y = /tagged|accessib|displayHeaderFooter|aria|--enable-tagged-pdf/i.test(content);
238
+ if (!hasA11y) {
239
+ findings.push({
240
+ ruleId: 'COMP-SEC508-005', category: 'compliance', severity: 'medium',
241
+ title: 'Browser-based PDF generation without accessibility options',
242
+ description: 'Section 508 requires accessible PDFs. When generating PDFs with Puppeteer/Playwright, use the tagged option and ensure the source HTML is accessible.',
243
+ file: filepath, line: 1, fix: null,
244
+ });
245
+ }
246
+ }
247
+ }
248
+ return findings;
249
+ },
250
+ },
251
+
252
+ // --- ADA Digital Compliance Rules ---
253
+
254
+ // COMP-ADA-001: No accessibility statement
255
+ {
256
+ id: 'COMP-ADA-001',
257
+ category: 'compliance',
258
+ severity: 'medium',
259
+ confidence: 'likely',
260
+ title: 'No Accessibility Statement',
261
+ check({ files }) {
262
+ const findings = [];
263
+ const fileNames = [...files.keys()];
264
+ const fileContents = [...files.values()];
265
+
266
+ // Only flag for web apps (has HTML or component files)
267
+ const isWebApp = fileNames.some(f => isWebFile(f)) ||
268
+ fileContents.some(c => /<html|<head|ReactDOM|createApp|createRoot/i.test(c));
269
+ if (!isWebApp) return findings;
270
+
271
+ const hasA11yPage = fileNames.some(f =>
272
+ /accessib/i.test(f) || /a11y/i.test(f)
273
+ );
274
+ const hasA11yLink = fileContents.some(c =>
275
+ /accessibility.?statement|accessibility.?policy|\/accessibility|\/a11y/i.test(c)
276
+ );
277
+
278
+ if (!hasA11yPage && !hasA11yLink) {
279
+ findings.push({
280
+ ruleId: 'COMP-ADA-001', category: 'compliance', severity: 'medium',
281
+ title: 'No accessibility statement page detected',
282
+ description: 'ADA best practice requires an accessibility statement describing your commitment to accessibility, conformance level, and contact information for feedback.',
283
+ fix: null,
284
+ });
285
+ }
286
+ return findings;
287
+ },
288
+ },
289
+
290
+ // COMP-ADA-002: Contact forms without accessible alternatives
291
+ {
292
+ id: 'COMP-ADA-002',
293
+ category: 'compliance',
294
+ severity: 'medium',
295
+ confidence: 'likely',
296
+ title: 'Contact Form Without Accessible Alternatives',
297
+ check({ files }) {
298
+ const findings = [];
299
+ for (const [filepath, content] of files.entries()) {
300
+ if (!isWebFile(filepath)) continue;
301
+
302
+ // Detect contact forms
303
+ const isContactForm = /contact.?form|contact.?us|<form[^>]*(?:contact|inquiry|feedback)/i.test(content);
304
+ if (!isContactForm) continue;
305
+
306
+ const hasAlternative = /(?:phone|telephone|tel:|call\s+us|tty|tdd|relay\s+service|email\s+us|mail\s+to|mailto:)/i.test(content);
307
+ if (!hasAlternative) {
308
+ findings.push({
309
+ ruleId: 'COMP-ADA-002', category: 'compliance', severity: 'medium',
310
+ title: 'Contact form without alternative contact methods',
311
+ description: 'ADA compliance recommends providing alternative contact methods (phone, email, TTY/TDD) alongside web forms so users with disabilities have multiple ways to reach you.',
312
+ file: filepath, line: 1, fix: null,
313
+ });
314
+ }
315
+ }
316
+ return findings;
317
+ },
318
+ },
319
+
320
+ // COMP-ADA-003: CAPTCHA without accessible alternative
321
+ {
322
+ id: 'COMP-ADA-003',
323
+ category: 'compliance',
324
+ severity: 'high',
325
+ confidence: 'likely',
326
+ title: 'CAPTCHA Without Accessible Alternative',
327
+ check({ files }) {
328
+ const findings = [];
329
+ for (const [filepath, content] of files.entries()) {
330
+ if (!isWebFile(filepath) && !isSourceFile(filepath)) continue;
331
+
332
+ const lines = content.split('\n');
333
+ for (let i = 0; i < lines.length; i++) {
334
+ const line = lines[i];
335
+
336
+ // Detect reCAPTCHA, hCaptcha, or generic CAPTCHA usage
337
+ const hasCaptcha = /recaptcha|hcaptcha|g-recaptcha|h-captcha|captcha/i.test(line);
338
+ if (!hasCaptcha) continue;
339
+
340
+ // Skip comment-only lines
341
+ if (/^\s*(?:\/\/|\/\*|\*|<!--)/.test(line)) continue;
342
+
343
+ // Check surrounding context for accessible alternatives
344
+ const block = lines.slice(Math.max(0, i - 5), i + 15).join('\n');
345
+ const hasAccessibleAlt = /audio|accessible|a11y|aria-|data-sitekey[^>]*invisible|data-size=["']invisible|challenge.?type.*audio|fallback|alternative/i.test(block);
346
+
347
+ // reCAPTCHA v3 and invisible reCAPTCHA are more accessible by design
348
+ const isAccessibleVersion = /recaptcha\/api.*render=|grecaptcha\.execute\(|captcha_invisible|invisible.?recaptcha|v3/i.test(block);
349
+
350
+ if (!hasAccessibleAlt && !isAccessibleVersion) {
351
+ findings.push({
352
+ ruleId: 'COMP-ADA-003', category: 'compliance', severity: 'high',
353
+ title: 'CAPTCHA without accessible alternative',
354
+ description: 'Visual CAPTCHAs create barriers for users with disabilities. Use reCAPTCHA v3 (invisible), provide an audio alternative, or offer an accessible fallback like email verification.',
355
+ file: filepath, line: i + 1, fix: null,
356
+ });
357
+ break; // One finding per file is sufficient
358
+ }
359
+ }
360
+ }
361
+ return findings;
362
+ },
363
+ },
364
+
365
+ // COMP-ADA-004: Dynamic content without ARIA live regions
366
+ {
367
+ id: 'COMP-ADA-004',
368
+ category: 'compliance',
369
+ severity: 'medium',
370
+ confidence: 'likely',
371
+ title: 'Dynamic Content Without ARIA Live Region',
372
+ check({ files }) {
373
+ const findings = [];
374
+ for (const [filepath, content] of files.entries()) {
375
+ if (!isSourceFile(filepath) && !isWebFile(filepath)) continue;
376
+
377
+ // Detect dynamic DOM updates in source files
378
+ if (isSourceFile(filepath)) {
379
+ const hasDynamicUpdate = /innerHTML\s*[=+]|\.textContent\s*=|\.insertAdjacentHTML|\.append\(|setState\(|\.value\s*=.*(?:message|error|success|notification|toast|alert|status|result)/i.test(content);
380
+ if (!hasDynamicUpdate) continue;
381
+
382
+ // Check for notification/toast/alert patterns without ARIA
383
+ const hasNotificationPattern = /toast|notification|snackbar|alert.?message|flash.?message|status.?message|error.?message|success.?message/i.test(content);
384
+ if (!hasNotificationPattern) continue;
385
+
386
+ const hasAriaLive = /aria-live|role=["'](?:alert|status|log|timer|marquee)["']|ariaLive|role:\s*["'](?:alert|status)/i.test(content);
387
+ if (!hasAriaLive) {
388
+ findings.push({
389
+ ruleId: 'COMP-ADA-004', category: 'compliance', severity: 'medium',
390
+ title: 'Dynamic notification/status content without ARIA live region',
391
+ description: 'ADA compliance requires dynamic content updates (toasts, alerts, status messages) to be announced to screen readers. Use aria-live="polite" or role="alert" on the container.',
392
+ file: filepath, line: 1, fix: null,
393
+ });
394
+ }
395
+ }
396
+
397
+ // Check web template files for dynamic areas lacking aria-live
398
+ if (isWebFile(filepath)) {
399
+ const hasNotificationArea = /class=["'][^"']*(?:toast|notification|snackbar|alert|flash|message|status)[^"']*["']/i.test(content);
400
+ if (!hasNotificationArea) continue;
401
+
402
+ const hasAriaLive = /aria-live|role=["'](?:alert|status|log)["']/i.test(content);
403
+ if (!hasAriaLive) {
404
+ findings.push({
405
+ ruleId: 'COMP-ADA-004', category: 'compliance', severity: 'medium',
406
+ title: 'Notification/alert container without ARIA live region',
407
+ description: 'Screen readers cannot detect visual-only dynamic updates. Add aria-live="polite" for non-urgent updates or role="alert" for important notifications.',
408
+ file: filepath, line: 1, fix: null,
409
+ });
410
+ }
411
+ }
412
+ }
413
+ return findings;
414
+ },
415
+ },
416
+
417
+ // COMP-ADA-005: Mobile app without screen reader support
418
+ {
419
+ id: 'COMP-ADA-005',
420
+ category: 'compliance',
421
+ severity: 'high',
422
+ confidence: 'likely',
423
+ title: 'Mobile App Without Screen Reader Support',
424
+ check({ files }) {
425
+ const findings = [];
426
+ for (const [filepath, content] of files.entries()) {
427
+ if (!isSourceFile(filepath) && !filepath.endsWith('.dart')) continue;
428
+
429
+ // React Native detection
430
+ const isReactNative = /from\s+['"]react-native['"]/i.test(content);
431
+ if (isReactNative) {
432
+ const hasInteractiveElements = /TouchableOpacity|TouchableHighlight|Pressable|Button|TextInput|Switch|<Image/i.test(content);
433
+ if (!hasInteractiveElements) continue;
434
+
435
+ const hasA11yProps = /accessibilityLabel|accessibilityHint|accessibilityRole|accessibilityState|accessible=|accessibilityValue|importantForAccessibility|aria-label/i.test(content);
436
+ if (!hasA11yProps) {
437
+ findings.push({
438
+ ruleId: 'COMP-ADA-005', category: 'compliance', severity: 'high',
439
+ title: 'React Native component without accessibility props',
440
+ description: 'ADA requires mobile apps to be usable with screen readers. Add accessibilityLabel, accessibilityHint, and accessibilityRole props to interactive React Native components.',
441
+ file: filepath, line: 1, fix: null,
442
+ });
443
+ }
444
+ }
445
+
446
+ // Flutter detection
447
+ const isFlutter = /import\s+['"]package:flutter/i.test(content);
448
+ if (isFlutter) {
449
+ const hasInteractiveWidgets = /ElevatedButton|TextButton|IconButton|GestureDetector|InkWell|TextField|Switch|Checkbox|Image\./i.test(content);
450
+ if (!hasInteractiveWidgets) continue;
451
+
452
+ const hasSemantics = /Semantics\(|semanticsLabel|excludeFromSemantics|tooltip:/i.test(content);
453
+ if (!hasSemantics) {
454
+ findings.push({
455
+ ruleId: 'COMP-ADA-005', category: 'compliance', severity: 'high',
456
+ title: 'Flutter widget without Semantics wrapper',
457
+ description: 'ADA requires mobile apps to be usable with screen readers. Wrap interactive widgets with Semantics() or add semanticsLabel to provide screen reader descriptions.',
458
+ file: filepath, line: 1, fix: null,
459
+ });
460
+ }
461
+ }
462
+ }
463
+ return findings;
464
+ },
465
+ },
466
+ ];
467
+
468
+ export default rules;