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,539 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * SOC2 Trust Service Criteria mapped to Doorman rule prefixes.
6
+ */
7
+ const SOC2_CONTROLS = [
8
+ { id: 'CC6.1', name: 'Logical and Physical Access Controls', prefixes: ['SEC-AUTH'], description: 'Controls that limit logical access to systems and data to authorized users.' },
9
+ { id: 'CC6.6', name: 'System Boundary Security', prefixes: ['SEC-CORS', 'SEC-HDR'], description: 'Controls that restrict access at system boundaries, including network and application layers.' },
10
+ { id: 'CC6.7', name: 'Transmission Integrity and Security', prefixes: ['SEC-CRY'], description: 'Controls that protect data in transit using encryption and integrity mechanisms.' },
11
+ { id: 'CC7.1', name: 'Detection and Monitoring', prefixes: ['SEC-RL'], description: 'Controls for detecting anomalies, rate limiting, and monitoring access patterns.' },
12
+ { id: 'CC7.2', name: 'Change Management', prefixes: ['DEP-'], description: 'Controls that manage changes to infrastructure and software, including dependency management.' },
13
+ { id: 'CC8.1', name: 'Software Development and Testing', prefixes: ['QUAL-', 'REL-'], description: 'Controls that ensure code quality through testing, reviews, and secure development practices.' },
14
+ ];
15
+
16
+ /**
17
+ * GDPR Articles mapped to Doorman rule prefixes.
18
+ */
19
+ const GDPR_CONTROLS = [
20
+ { id: 'Art. 25', name: 'Data Protection by Design and Default', prefixes: ['DATA-'], description: 'Technical and organizational measures to implement data protection principles.' },
21
+ { id: 'Art. 32', name: 'Security of Processing', prefixes: ['SEC-CRY', 'SEC-AUTH'], description: 'Appropriate technical measures including encryption and access control.' },
22
+ { id: 'Art. 33', name: 'Breach Notification', prefixes: ['SEC-RL', 'SEC-HDR'], description: 'Logging and monitoring capabilities for timely breach detection and notification.' },
23
+ { id: 'Art. 5(1)(c)', name: 'Data Minimization', prefixes: ['DATA-PII', 'DATA-EXP'], description: 'Personal data must be adequate, relevant, and limited to what is necessary.' },
24
+ ];
25
+
26
+ /**
27
+ * HIPAA Security Rule sections mapped to Doorman rule prefixes.
28
+ */
29
+ const HIPAA_CONTROLS = [
30
+ { id: '164.312(a)', name: 'Access Control', prefixes: ['SEC-AUTH'], description: 'Technical policies to allow access only to authorized persons or software programs.' },
31
+ { id: '164.312(c)', name: 'Integrity Controls', prefixes: ['SEC-INJ', 'SEC-XSS', 'SEC-CSRF'], description: 'Policies to protect ePHI from improper alteration or destruction.' },
32
+ { id: '164.312(d)', name: 'Person or Entity Authentication', prefixes: ['SEC-AUTH', 'SEC-MISC'], description: 'Procedures to verify that a person seeking access is who they claim to be.' },
33
+ { id: '164.312(e)', name: 'Transmission Security', prefixes: ['SEC-CRY', 'SEC-HDR'], description: 'Technical security measures to guard against unauthorized access during transmission.' },
34
+ ];
35
+
36
+ /**
37
+ * PCI-DSS Requirements mapped to Doorman rule prefixes.
38
+ */
39
+ const PCI_CONTROLS = [
40
+ { id: 'Req. 2', name: 'Do Not Use Vendor-Supplied Defaults', prefixes: ['SEC-MISC'], description: 'Change all vendor-supplied defaults and remove unnecessary default accounts.' },
41
+ { id: 'Req. 3', name: 'Protect Stored Cardholder Data', prefixes: ['SEC-CRY', 'DATA-'], description: 'Protect stored cardholder data with encryption and access restrictions.' },
42
+ { id: 'Req. 6', name: 'Develop and Maintain Secure Systems', prefixes: ['SEC-'], description: 'Develop and maintain secure systems and applications throughout the SDLC.' },
43
+ { id: 'Req. 8', name: 'Identify and Authenticate Access', prefixes: ['SEC-AUTH'], description: 'Identify and authenticate access to system components.' },
44
+ ];
45
+
46
+ const FRAMEWORK_MAP = {
47
+ soc2: { name: 'SOC 2 Type II', controls: SOC2_CONTROLS },
48
+ gdpr: { name: 'GDPR', controls: GDPR_CONTROLS },
49
+ hipaa: { name: 'HIPAA Security Rule', controls: HIPAA_CONTROLS },
50
+ pci: { name: 'PCI-DSS v4.0', controls: PCI_CONTROLS },
51
+ };
52
+
53
+ /**
54
+ * Check if a finding's ruleId matches any of the given prefixes.
55
+ */
56
+ function matchesPrefix(ruleId, prefixes) {
57
+ if (!ruleId) return false;
58
+ return prefixes.some(prefix => ruleId.startsWith(prefix));
59
+ }
60
+
61
+ /**
62
+ * Determine control status from associated findings.
63
+ */
64
+ function evaluateControl(control, findings) {
65
+ const matched = findings.filter(f => matchesPrefix(f.ruleId, control.prefixes));
66
+ const criticalOrHigh = matched.filter(f => f.severity === 'critical' || f.severity === 'high');
67
+ const medium = matched.filter(f => f.severity === 'medium');
68
+
69
+ let status;
70
+ if (criticalOrHigh.length > 0) {
71
+ status = 'fail';
72
+ } else if (medium.length > 0) {
73
+ status = 'warning';
74
+ } else {
75
+ status = 'pass';
76
+ }
77
+
78
+ let remediation = null;
79
+ if (status === 'fail') {
80
+ remediation = `Resolve ${criticalOrHigh.length} critical/high finding(s): ${criticalOrHigh.map(f => f.ruleId).join(', ')}`;
81
+ } else if (status === 'warning') {
82
+ remediation = `Address ${medium.length} medium finding(s) to achieve full compliance.`;
83
+ }
84
+
85
+ return {
86
+ id: control.id,
87
+ name: control.name,
88
+ description: control.description,
89
+ status,
90
+ findings: matched,
91
+ remediation,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Map scan findings to compliance frameworks and return compliance status for each.
97
+ *
98
+ * @param {Array} findings - Array of finding objects from a Doorman scan.
99
+ * @returns {{ soc2: object, gdpr: object, hipaa: object, pci: object }}
100
+ */
101
+ export function getComplianceStatus(findings) {
102
+ const result = {};
103
+
104
+ for (const [key, framework] of Object.entries(FRAMEWORK_MAP)) {
105
+ const controls = framework.controls.map(ctrl => evaluateControl(ctrl, findings));
106
+ const passing = controls.filter(c => c.status === 'pass').length;
107
+ const score = controls.length > 0 ? Math.round((passing / controls.length) * 100) : 100;
108
+
109
+ let status;
110
+ if (score === 100) status = 'compliant';
111
+ else if (score >= 50) status = 'partial';
112
+ else status = 'non-compliant';
113
+
114
+ result[key] = {
115
+ name: framework.name,
116
+ status,
117
+ controls,
118
+ score,
119
+ };
120
+ }
121
+
122
+ return result;
123
+ }
124
+
125
+ /**
126
+ * Map findings to their associated compliance controls across all frameworks.
127
+ *
128
+ * @param {Array} findings - Array of finding objects.
129
+ * @returns {Map<string, Array>} Map of control ID to array of matched findings.
130
+ */
131
+ export function mapFindingsToControls(findings) {
132
+ const controlMap = new Map();
133
+
134
+ for (const [, framework] of Object.entries(FRAMEWORK_MAP)) {
135
+ for (const control of framework.controls) {
136
+ const matched = findings.filter(f => matchesPrefix(f.ruleId, control.prefixes));
137
+ if (matched.length > 0) {
138
+ controlMap.set(control.id, matched);
139
+ }
140
+ }
141
+ }
142
+
143
+ return controlMap;
144
+ }
145
+
146
+ /**
147
+ * Generate a professional HTML compliance report from scan results.
148
+ *
149
+ * @param {{ findings: Array, score: number, stack?: object }} scanResult
150
+ * @param {{ framework?: string, projectName?: string, outputPath?: string }} options
151
+ * @returns {{ html: string, summary: object }}
152
+ */
153
+ export function generateReport(scanResult, options = {}) {
154
+ const { findings = [], score = 0, stack = {} } = scanResult;
155
+ const frameworkFilter = (options.framework || 'all').toLowerCase();
156
+ const projectName = options.projectName || 'Untitled Project';
157
+ const reportDate = new Date().toISOString().split('T')[0];
158
+ const reportTime = new Date().toISOString();
159
+
160
+ const compliance = getComplianceStatus(findings);
161
+
162
+ // Filter frameworks if requested
163
+ const frameworks = frameworkFilter === 'all'
164
+ ? compliance
165
+ : { [frameworkFilter]: compliance[frameworkFilter] };
166
+
167
+ // Severity counts
168
+ const severityCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
169
+ for (const f of findings) {
170
+ if (severityCounts[f.severity] !== undefined) severityCounts[f.severity]++;
171
+ }
172
+
173
+ // Build remediation roadmap sorted by priority
174
+ const remediationItems = [];
175
+ for (const [fwKey, fw] of Object.entries(frameworks)) {
176
+ if (!fw) continue;
177
+ for (const ctrl of fw.controls) {
178
+ if (ctrl.status !== 'pass' && ctrl.remediation) {
179
+ remediationItems.push({
180
+ framework: fw.name,
181
+ control: `${ctrl.id} — ${ctrl.name}`,
182
+ priority: ctrl.status === 'fail' ? 'High' : 'Medium',
183
+ remediation: ctrl.remediation,
184
+ findingCount: ctrl.findings.length,
185
+ });
186
+ }
187
+ }
188
+ }
189
+ remediationItems.sort((a, b) => (a.priority === 'High' ? -1 : 1) - (b.priority === 'High' ? -1 : 1));
190
+
191
+ // Score styling
192
+ let scoreClass, scoreLabel;
193
+ if (score >= 80) { scoreClass = 'score-good'; scoreLabel = 'SAFE TO LAUNCH'; }
194
+ else if (score >= 60) { scoreClass = 'score-warn'; scoreLabel = 'NEEDS ATTENTION'; }
195
+ else if (score >= 40) { scoreClass = 'score-warn'; scoreLabel = 'AT RISK'; }
196
+ else { scoreClass = 'score-bad'; scoreLabel = 'NOT SAFE TO LAUNCH'; }
197
+
198
+ // Overall compliance summary
199
+ const frameworkSummaryRows = Object.entries(frameworks)
200
+ .filter(([, fw]) => fw)
201
+ .map(([, fw]) => {
202
+ const statusClass = fw.status === 'compliant' ? 'pass' : fw.status === 'partial' ? 'warning' : 'fail';
203
+ const statusLabel = fw.status === 'compliant' ? 'Compliant' : fw.status === 'partial' ? 'Partial' : 'Non-Compliant';
204
+ return `<tr>
205
+ <td><strong>${escapeHtml(fw.name)}</strong></td>
206
+ <td class="${statusClass}">${statusLabel}</td>
207
+ <td>${fw.score}%</td>
208
+ <td>${fw.controls.filter(c => c.status === 'pass').length} / ${fw.controls.length}</td>
209
+ </tr>`;
210
+ }).join('\n');
211
+
212
+ // Findings table rows
213
+ const findingsRows = findings.map(f => {
214
+ const sevClass = f.severity === 'critical' || f.severity === 'high' ? 'fail' : f.severity === 'medium' ? 'warning' : 'pass';
215
+ return `<tr>
216
+ <td class="${sevClass}">${escapeHtml((f.severity || '').toUpperCase())}</td>
217
+ <td>${escapeHtml(f.ruleId || '')}</td>
218
+ <td>${escapeHtml(f.title || '')}</td>
219
+ <td>${escapeHtml(f.file || 'N/A')}${f.line ? ':' + f.line : ''}</td>
220
+ <td>${escapeHtml(f.category || '')}</td>
221
+ </tr>`;
222
+ }).join('\n');
223
+
224
+ // Per-framework compliance detail sections
225
+ const frameworkDetailSections = Object.entries(frameworks)
226
+ .filter(([, fw]) => fw)
227
+ .map(([, fw]) => {
228
+ const controlRows = fw.controls.map(ctrl => {
229
+ const statusIcon = ctrl.status === 'pass' ? '\u2705' : ctrl.status === 'warning' ? '\u26A0\uFE0F' : '\u274C';
230
+ const statusClass = ctrl.status === 'pass' ? 'pass' : ctrl.status === 'warning' ? 'warning' : 'fail';
231
+ return `<tr>
232
+ <td>${statusIcon}</td>
233
+ <td><strong>${escapeHtml(ctrl.id)}</strong></td>
234
+ <td>${escapeHtml(ctrl.name)}</td>
235
+ <td class="${statusClass}">${ctrl.status.toUpperCase()}</td>
236
+ <td>${ctrl.findings.length}</td>
237
+ <td>${escapeHtml(ctrl.remediation || 'No action required.')}</td>
238
+ </tr>`;
239
+ }).join('\n');
240
+
241
+ const statusClass = fw.status === 'compliant' ? 'pass' : fw.status === 'partial' ? 'warning' : 'fail';
242
+
243
+ return `
244
+ <div class="section">
245
+ <h2>${escapeHtml(fw.name)}</h2>
246
+ <p>Overall Status: <span class="${statusClass}" style="font-weight: bold;">${fw.status.toUpperCase()}</span> &mdash; ${fw.score}% of controls passing</p>
247
+ <table>
248
+ <thead>
249
+ <tr>
250
+ <th style="width:40px;"></th>
251
+ <th>Control</th>
252
+ <th>Name</th>
253
+ <th>Status</th>
254
+ <th>Findings</th>
255
+ <th>Remediation</th>
256
+ </tr>
257
+ </thead>
258
+ <tbody>
259
+ ${controlRows}
260
+ </tbody>
261
+ </table>
262
+ </div>`;
263
+ }).join('\n');
264
+
265
+ // Remediation roadmap
266
+ const remediationRows = remediationItems.map((item, idx) => {
267
+ const prioClass = item.priority === 'High' ? 'fail' : 'warning';
268
+ return `<tr>
269
+ <td>${idx + 1}</td>
270
+ <td class="${prioClass}">${item.priority}</td>
271
+ <td>${escapeHtml(item.framework)}</td>
272
+ <td>${escapeHtml(item.control)}</td>
273
+ <td>${item.findingCount}</td>
274
+ <td>${escapeHtml(item.remediation)}</td>
275
+ </tr>`;
276
+ }).join('\n');
277
+
278
+ // Stack detection label
279
+ const stackLabel = [stack.framework || stack.language || 'Unknown', stack.orm, stack.database]
280
+ .filter(Boolean).join(' + ');
281
+
282
+ const html = `<!DOCTYPE html>
283
+ <html lang="en">
284
+ <head>
285
+ <meta charset="UTF-8">
286
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
287
+ <title>Doorman Compliance Report — ${escapeHtml(projectName)}</title>
288
+ <style>
289
+ *, *::before, *::after { box-sizing: border-box; }
290
+ body {
291
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, sans-serif;
292
+ max-width: 960px;
293
+ margin: 0 auto;
294
+ padding: 40px 24px;
295
+ color: #1f2937;
296
+ line-height: 1.6;
297
+ background: #ffffff;
298
+ }
299
+ .header {
300
+ border-bottom: 3px solid #2563eb;
301
+ padding-bottom: 24px;
302
+ margin-bottom: 32px;
303
+ }
304
+ .header h1 {
305
+ margin: 0 0 4px 0;
306
+ font-size: 28px;
307
+ color: #111827;
308
+ }
309
+ .header .subtitle {
310
+ color: #6b7280;
311
+ font-size: 14px;
312
+ margin: 0;
313
+ }
314
+ .header .logo-text {
315
+ font-size: 14px;
316
+ font-weight: 600;
317
+ color: #2563eb;
318
+ letter-spacing: 0.5px;
319
+ text-transform: uppercase;
320
+ margin-bottom: 8px;
321
+ }
322
+ .executive-summary {
323
+ display: flex;
324
+ gap: 32px;
325
+ align-items: flex-start;
326
+ margin: 32px 0;
327
+ padding: 24px;
328
+ background: #f9fafb;
329
+ border-radius: 8px;
330
+ border: 1px solid #e5e7eb;
331
+ }
332
+ .score-block { text-align: center; min-width: 140px; }
333
+ .score-badge {
334
+ font-size: 56px;
335
+ font-weight: 800;
336
+ line-height: 1;
337
+ }
338
+ .score-label {
339
+ font-size: 13px;
340
+ font-weight: 700;
341
+ letter-spacing: 0.5px;
342
+ margin-top: 4px;
343
+ }
344
+ .score-good { color: #16a34a; }
345
+ .score-warn { color: #ca8a04; }
346
+ .score-bad { color: #dc2626; }
347
+ .summary-stats { flex: 1; }
348
+ .summary-stats h3 { margin: 0 0 12px 0; font-size: 16px; }
349
+ .stat-grid {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
352
+ gap: 8px;
353
+ }
354
+ .stat-item {
355
+ text-align: center;
356
+ padding: 8px;
357
+ border-radius: 6px;
358
+ background: #fff;
359
+ border: 1px solid #e5e7eb;
360
+ }
361
+ .stat-item .stat-value { font-size: 24px; font-weight: 700; }
362
+ .stat-item .stat-label { font-size: 11px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.3px; }
363
+ .stat-critical .stat-value { color: #dc2626; }
364
+ .stat-high .stat-value { color: #ea580c; }
365
+ .stat-medium .stat-value { color: #ca8a04; }
366
+ .stat-low .stat-value { color: #2563eb; }
367
+ .stat-info .stat-value { color: #6b7280; }
368
+ .section { margin: 36px 0; }
369
+ .section h2 {
370
+ font-size: 20px;
371
+ color: #111827;
372
+ border-bottom: 2px solid #e5e7eb;
373
+ padding-bottom: 8px;
374
+ margin-bottom: 16px;
375
+ }
376
+ table { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 12px; }
377
+ th, td { border: 1px solid #e5e7eb; padding: 10px 14px; text-align: left; }
378
+ th { background: #f9fafb; font-weight: 600; color: #374151; font-size: 12px; text-transform: uppercase; letter-spacing: 0.3px; }
379
+ tr:nth-child(even) { background: #fafbfc; }
380
+ .pass { color: #16a34a; font-weight: 600; }
381
+ .fail { color: #dc2626; font-weight: 600; }
382
+ .warning { color: #ca8a04; font-weight: 600; }
383
+ .meta-table { margin: 16px 0; }
384
+ .meta-table td { border: none; padding: 4px 16px 4px 0; }
385
+ .meta-table td:first-child { font-weight: 600; color: #6b7280; white-space: nowrap; }
386
+ .attestation {
387
+ margin-top: 48px;
388
+ padding: 20px;
389
+ background: #f9fafb;
390
+ border: 1px solid #e5e7eb;
391
+ border-radius: 8px;
392
+ font-size: 13px;
393
+ color: #6b7280;
394
+ }
395
+ .attestation strong { color: #374151; }
396
+ .disclaimer {
397
+ margin-top: 24px;
398
+ padding-top: 16px;
399
+ border-top: 1px solid #e5e7eb;
400
+ font-size: 11px;
401
+ color: #9ca3af;
402
+ }
403
+ .no-findings { padding: 24px; text-align: center; color: #6b7280; font-style: italic; }
404
+ @media print {
405
+ body { padding: 20px; }
406
+ .no-print { display: none; }
407
+ .executive-summary { break-inside: avoid; }
408
+ .section { break-inside: avoid; }
409
+ }
410
+ </style>
411
+ </head>
412
+ <body>
413
+ <div class="header">
414
+ <div class="logo-text">Doorman Compliance Report</div>
415
+ <h1>${escapeHtml(projectName)}</h1>
416
+ <p class="subtitle">Generated on ${escapeHtml(reportDate)} &bull; Framework${frameworkFilter === 'all' ? 's' : ''}: ${frameworkFilter === 'all' ? 'SOC 2, GDPR, HIPAA, PCI-DSS' : escapeHtml(frameworks[frameworkFilter]?.name || frameworkFilter.toUpperCase())}</p>
417
+ </div>
418
+
419
+ <table class="meta-table">
420
+ <tr><td>Report Date</td><td>${escapeHtml(reportDate)}</td></tr>
421
+ <tr><td>Project</td><td>${escapeHtml(projectName)}</td></tr>
422
+ <tr><td>Detected Stack</td><td>${escapeHtml(stackLabel)}</td></tr>
423
+ <tr><td>Total Findings</td><td>${findings.length}</td></tr>
424
+ <tr><td>Doorman Version</td><td>v1.0.0</td></tr>
425
+ </table>
426
+
427
+ <div class="section">
428
+ <h2>Executive Summary</h2>
429
+ <div class="executive-summary">
430
+ <div class="score-block">
431
+ <div class="score-badge ${scoreClass}">${score}</div>
432
+ <div class="score-label ${scoreClass}">${scoreLabel}</div>
433
+ <div style="font-size: 12px; color: #9ca3af; margin-top: 2px;">out of 100</div>
434
+ </div>
435
+ <div class="summary-stats">
436
+ <h3>Findings by Severity</h3>
437
+ <div class="stat-grid">
438
+ <div class="stat-item stat-critical"><div class="stat-value">${severityCounts.critical}</div><div class="stat-label">Critical</div></div>
439
+ <div class="stat-item stat-high"><div class="stat-value">${severityCounts.high}</div><div class="stat-label">High</div></div>
440
+ <div class="stat-item stat-medium"><div class="stat-value">${severityCounts.medium}</div><div class="stat-label">Medium</div></div>
441
+ <div class="stat-item stat-low"><div class="stat-value">${severityCounts.low}</div><div class="stat-label">Low</div></div>
442
+ <div class="stat-item stat-info"><div class="stat-value">${severityCounts.info}</div><div class="stat-label">Info</div></div>
443
+ </div>
444
+ </div>
445
+ </div>
446
+
447
+ <h3>Compliance Status by Framework</h3>
448
+ <table>
449
+ <thead>
450
+ <tr><th>Framework</th><th>Status</th><th>Score</th><th>Controls Passing</th></tr>
451
+ </thead>
452
+ <tbody>
453
+ ${frameworkSummaryRows}
454
+ </tbody>
455
+ </table>
456
+ </div>
457
+
458
+ <div class="section">
459
+ <h2>Findings Detail</h2>
460
+ ${findings.length > 0 ? `
461
+ <table>
462
+ <thead>
463
+ <tr><th>Severity</th><th>Rule</th><th>Title</th><th>Location</th><th>Category</th></tr>
464
+ </thead>
465
+ <tbody>
466
+ ${findingsRows}
467
+ </tbody>
468
+ </table>` : '<div class="no-findings">No findings detected. All checks passed.</div>'}
469
+ </div>
470
+
471
+ ${frameworkDetailSections}
472
+
473
+ <div class="section">
474
+ <h2>Remediation Roadmap</h2>
475
+ ${remediationItems.length > 0 ? `
476
+ <p>The following items are prioritized by severity. Address high-priority items first to achieve compliance.</p>
477
+ <table>
478
+ <thead>
479
+ <tr><th>#</th><th>Priority</th><th>Framework</th><th>Control</th><th>Findings</th><th>Action Required</th></tr>
480
+ </thead>
481
+ <tbody>
482
+ ${remediationRows}
483
+ </tbody>
484
+ </table>` : '<div class="no-findings">No remediation actions required. All controls are passing.</div>'}
485
+ </div>
486
+
487
+ <div class="attestation">
488
+ <strong>Attestation</strong><br>
489
+ This report was generated by Doorman v1.0.0 on ${escapeHtml(reportTime)}. The findings
490
+ reflect an automated analysis of the project source code at the time of the scan. This report
491
+ is intended to support compliance efforts and should be reviewed by a qualified auditor before
492
+ being used as evidence for certification or regulatory purposes.
493
+ </div>
494
+
495
+ <div class="disclaimer">
496
+ <strong>Disclaimer:</strong> This automated compliance report maps technical findings to regulatory
497
+ framework controls as a convenience. It does not constitute legal advice or a formal compliance
498
+ assessment. Compliance with SOC 2, GDPR, HIPAA, PCI-DSS, and other frameworks requires a
499
+ comprehensive review by qualified professionals that includes organizational policies, procedures,
500
+ and technical controls beyond the scope of static code analysis. Doorman makes no representations
501
+ or warranties regarding the completeness or accuracy of compliance mappings.
502
+ </div>
503
+ </body>
504
+ </html>`;
505
+
506
+ // Write to disk if outputPath is provided
507
+ if (options.outputPath) {
508
+ writeFileSync(resolve(options.outputPath), html, 'utf-8');
509
+ }
510
+
511
+ const summary = {
512
+ projectName,
513
+ date: reportDate,
514
+ score,
515
+ totalFindings: findings.length,
516
+ severityCounts,
517
+ frameworks: Object.fromEntries(
518
+ Object.entries(frameworks)
519
+ .filter(([, fw]) => fw)
520
+ .map(([key, fw]) => [key, { name: fw.name, status: fw.status, score: fw.score }])
521
+ ),
522
+ remediationCount: remediationItems.length,
523
+ };
524
+
525
+ return { html, summary };
526
+ }
527
+
528
+ /**
529
+ * Escape HTML special characters to prevent XSS in generated reports.
530
+ */
531
+ function escapeHtml(str) {
532
+ if (typeof str !== 'string') return '';
533
+ return str
534
+ .replace(/&/g, '&amp;')
535
+ .replace(/</g, '&lt;')
536
+ .replace(/>/g, '&gt;')
537
+ .replace(/"/g, '&quot;')
538
+ .replace(/'/g, '&#039;');
539
+ }