verification-layer 0.24.4 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -2
- package/dist/ai/cache.js +2 -2
- package/dist/ai/cache.js.map +1 -1
- package/dist/ai/config.d.ts +1 -1
- package/dist/ai/config.js +1 -1
- package/dist/ai/config.js.map +1 -1
- package/dist/ai/rules/prompts/audit-logging.js +1 -1
- package/dist/ai/rules/rule-runner.d.ts.map +1 -1
- package/dist/ai/rules/rule-runner.js.map +1 -1
- package/dist/ai/rules/triage.d.ts.map +1 -1
- package/dist/ai/rules/triage.js +1 -1
- package/dist/ai/rules/triage.js.map +1 -1
- package/dist/ai/scanner.d.ts.map +1 -1
- package/dist/ai/scanner.js +1 -1
- package/dist/ai/scanner.js.map +1 -1
- package/dist/cli.js +77 -13
- package/dist/cli.js.map +1 -1
- package/dist/exclusions.d.ts +13 -0
- package/dist/exclusions.d.ts.map +1 -0
- package/dist/exclusions.js +27 -0
- package/dist/exclusions.js.map +1 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/marketplace/installer.d.ts.map +1 -1
- package/dist/marketplace/installer.js +3 -3
- package/dist/marketplace/installer.js.map +1 -1
- package/dist/marketplace/registry.d.ts.map +1 -1
- package/dist/marketplace/registry.js +3 -1
- package/dist/marketplace/registry.js.map +1 -1
- package/dist/reporters/auditor-report.d.ts +2 -1
- package/dist/reporters/auditor-report.d.ts.map +1 -1
- package/dist/reporters/auditor-report.js +203 -16
- package/dist/reporters/auditor-report.js.map +1 -1
- package/dist/reporters/branding.d.ts +39 -0
- package/dist/reporters/branding.d.ts.map +1 -0
- package/dist/reporters/branding.js +124 -0
- package/dist/reporters/branding.js.map +1 -0
- package/dist/reporters/finding-presentation.d.ts +74 -0
- package/dist/reporters/finding-presentation.d.ts.map +1 -0
- package/dist/reporters/finding-presentation.js +172 -0
- package/dist/reporters/finding-presentation.js.map +1 -0
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +50 -40
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/scan-pdf-report.d.ts +23 -0
- package/dist/reporters/scan-pdf-report.d.ts.map +1 -0
- package/dist/reporters/scan-pdf-report.js +326 -0
- package/dist/reporters/scan-pdf-report.js.map +1 -0
- package/dist/scan.d.ts +11 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +46 -1
- package/dist/scan.js.map +1 -1
- package/dist/scanners/api-security/index.js +2 -2
- package/dist/scanners/api-security/index.js.map +1 -1
- package/dist/scanners/authentication/index.d.ts.map +1 -1
- package/dist/scanners/authentication/index.js +32 -27
- package/dist/scanners/authentication/index.js.map +1 -1
- package/dist/scanners/configuration/index.js +2 -2
- package/dist/scanners/configuration/index.js.map +1 -1
- package/dist/scanners/credentials/index.d.ts.map +1 -1
- package/dist/scanners/credentials/index.js +9 -4
- package/dist/scanners/credentials/index.js.map +1 -1
- package/dist/scanners/credentials/index.test.js +3 -3
- package/dist/scanners/credentials/patterns.d.ts.map +1 -1
- package/dist/scanners/credentials/patterns.js +4 -4
- package/dist/scanners/credentials/patterns.js.map +1 -1
- package/dist/scanners/errors/index.js +2 -2
- package/dist/scanners/errors/index.js.map +1 -1
- package/dist/scanners/hipaa2026/index.d.ts.map +1 -1
- package/dist/scanners/hipaa2026/index.js +8 -20
- package/dist/scanners/hipaa2026/index.js.map +1 -1
- package/dist/scanners/hipaa2026/index.test.js +2 -2
- package/dist/scanners/hipaa2026/patterns.d.ts.map +1 -1
- package/dist/scanners/hipaa2026/patterns.js +18 -5
- package/dist/scanners/hipaa2026/patterns.js.map +1 -1
- package/dist/scanners/operational/index.d.ts.map +1 -1
- package/dist/scanners/operational/index.js +27 -27
- package/dist/scanners/operational/index.js.map +1 -1
- package/dist/scanners/rbac/index.js +2 -2
- package/dist/scanners/rbac/index.js.map +1 -1
- package/dist/scanners/rbac/index.test.js +3 -0
- package/dist/scanners/rbac/index.test.js.map +1 -1
- package/dist/scanners/rbac/patterns.d.ts.map +1 -1
- package/dist/scanners/rbac/patterns.js +10 -3
- package/dist/scanners/rbac/patterns.js.map +1 -1
- package/dist/scanners/revocation/index.js +2 -2
- package/dist/scanners/revocation/index.js.map +1 -1
- package/dist/scanners/sanitization/index.d.ts.map +1 -1
- package/dist/scanners/sanitization/index.js +2 -3
- package/dist/scanners/sanitization/index.js.map +1 -1
- package/dist/scanners/skills/index.js +1 -1
- package/dist/scanners/skills/index.js.map +1 -1
- package/dist/scanners/skills/patterns.js +3 -3
- package/dist/scanners/skills/patterns.js.map +1 -1
- package/dist/scanners/utils.d.ts +44 -0
- package/dist/scanners/utils.d.ts.map +1 -0
- package/dist/scanners/utils.js +77 -0
- package/dist/scanners/utils.js.map +1 -0
- package/dist/training/index.js +1 -1
- package/dist/training/index.js.map +1 -1
- package/dist/types.d.ts +38 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/scan-history.js +2 -2
- package/dist/utils/scan-history.js.map +1 -1
- package/package.json +2 -2
- package/dist/scan-code.d.ts +0 -12
- package/dist/scan-code.d.ts.map +0 -1
- package/dist/scan-code.js +0 -34
- package/dist/scan-code.js.map +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ScanResult } from '../types.js';
|
|
1
|
+
import type { ScanResult, ResolvedBranding } from '../types.js';
|
|
2
2
|
interface AuditorReportOptions {
|
|
3
3
|
organizationName?: string;
|
|
4
4
|
reportPeriod?: string;
|
|
5
5
|
auditorName?: string;
|
|
6
6
|
includeBaseline?: boolean;
|
|
7
|
+
branding?: ResolvedBranding;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Generate auditor-ready HTML report with compliance score and SHA256 hash
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auditor-report.d.ts","sourceRoot":"","sources":["../../src/reporters/auditor-report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"auditor-report.d.ts","sourceRoot":"","sources":["../../src/reporters/auditor-report.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAWhE,UAAU,oBAAoB;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,oBAAyB,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAwkBhC"}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { generateComplianceScoreGauge, generateExecutiveSummary, generateEnhancedCSS } from './enhanced-html.js';
|
|
3
|
+
import { brandFooterText, brandPreparedBy, logoDataUri } from './branding.js';
|
|
4
|
+
import { groupFindingsByLocation, countGroupsBySeverity, formatHipaaRef, partitionFindingsByStatus, sortProposedFindings, } from './finding-presentation.js';
|
|
3
5
|
/**
|
|
4
6
|
* Generate auditor-ready HTML report with compliance score and SHA256 hash
|
|
5
7
|
*/
|
|
6
8
|
export function generateAuditorReport(result, targetPath, options = {}) {
|
|
7
|
-
const { organizationName = 'Organization', reportPeriod = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long' }), auditorName = 'VLayer Automated Scan', includeBaseline = false, } = options;
|
|
9
|
+
const { organizationName = 'Organization', reportPeriod = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long' }), auditorName = 'VLayer Automated Scan', includeBaseline = false, branding, } = options;
|
|
10
|
+
// White-label branding. When a brand is supplied the cover shows the brand
|
|
11
|
+
// logo + "Prepared by {brand}", and a footer line repeats on every printed
|
|
12
|
+
// page. With no branding the report renders exactly as before (default VLayer).
|
|
13
|
+
const brandLogo = logoDataUri(branding);
|
|
14
|
+
const hasBrand = Boolean(branding?.name || brandLogo);
|
|
15
|
+
const preparedBy = brandPreparedBy(branding);
|
|
16
|
+
const footerLine = brandFooterText(branding);
|
|
8
17
|
const timestamp = new Date().toISOString();
|
|
9
18
|
const score = result.complianceScore;
|
|
10
19
|
// Calculate findings
|
|
11
20
|
const activeFindings = result.findings.filter(f => !f.isBaseline && !f.suppressed);
|
|
21
|
+
// Presentation-only: separate proposed (NPRM) findings from current ones, then
|
|
22
|
+
// collapse multiple rules on the same file:line into one grouped entry. The
|
|
23
|
+
// findings themselves are untouched (count is preserved).
|
|
24
|
+
const { current: currentFindings, proposed: proposedRaw } = partitionFindingsByStatus(activeFindings);
|
|
25
|
+
const proposedFindings = sortProposedFindings(proposedRaw);
|
|
26
|
+
const locationGroups = groupFindingsByLocation(currentFindings);
|
|
27
|
+
const groupCounts = countGroupsBySeverity(locationGroups);
|
|
12
28
|
const acknowledgedFindings = result.findings.filter(f => f.acknowledged && !f.acknowledgment?.expired);
|
|
13
29
|
const suppressedFindings = result.findings.filter(f => f.suppressed);
|
|
14
30
|
const baselineFindings = result.findings.filter(f => f.isBaseline);
|
|
@@ -50,6 +66,20 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
50
66
|
font-weight: bold;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
.brand-logo {
|
|
70
|
+
max-height: 80px;
|
|
71
|
+
max-width: 240px;
|
|
72
|
+
margin: 0 auto 1rem;
|
|
73
|
+
display: block;
|
|
74
|
+
object-fit: contain;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.report-header .prepared-by {
|
|
78
|
+
opacity: 0.95;
|
|
79
|
+
font-size: 0.95rem;
|
|
80
|
+
margin-top: 0.5rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
.report-header h1 {
|
|
54
84
|
font-size: 2rem;
|
|
55
85
|
margin-bottom: 0.5rem;
|
|
@@ -151,6 +181,83 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
151
181
|
.severity-high { background: #ea580c; }
|
|
152
182
|
.severity-medium { background: #ca8a04; }
|
|
153
183
|
.severity-low { background: #2563eb; }
|
|
184
|
+
.severity-info { background: #6b7280; }
|
|
185
|
+
/* Proposed (NPRM) — deliberately neutral: not a current violation. */
|
|
186
|
+
.severity-proposed { background: #64748b; }
|
|
187
|
+
|
|
188
|
+
.upcoming-subtitle {
|
|
189
|
+
font-size: 0.85rem;
|
|
190
|
+
font-weight: 500;
|
|
191
|
+
color: #64748b;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.upcoming-note {
|
|
195
|
+
background: #f1f5f9;
|
|
196
|
+
border-left: 3px solid #64748b;
|
|
197
|
+
padding: 0.75rem 1rem;
|
|
198
|
+
margin: 0.5rem 0 1rem;
|
|
199
|
+
font-size: 0.9rem;
|
|
200
|
+
color: #475569;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.upcoming-inline {
|
|
204
|
+
color: #475569;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Consolidated multi-rule location entries */
|
|
209
|
+
.findings-count-note {
|
|
210
|
+
color: #4b5563;
|
|
211
|
+
font-size: 0.95rem;
|
|
212
|
+
margin: 0.25rem 0 0.75rem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.finding-group td { vertical-align: top; }
|
|
216
|
+
|
|
217
|
+
.group-location {
|
|
218
|
+
font-weight: 600;
|
|
219
|
+
color: #374151;
|
|
220
|
+
margin-bottom: 0.5rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.group-location-path {
|
|
224
|
+
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
225
|
+
font-size: 0.9rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.group-controls {
|
|
229
|
+
list-style: none;
|
|
230
|
+
margin: 0;
|
|
231
|
+
padding: 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.group-control {
|
|
235
|
+
display: grid;
|
|
236
|
+
grid-template-columns: 90px 1fr auto;
|
|
237
|
+
gap: 0.75rem;
|
|
238
|
+
align-items: baseline;
|
|
239
|
+
padding: 0.35rem 0;
|
|
240
|
+
border-top: 1px solid #f3f4f6;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.group-control:first-child { border-top: none; }
|
|
244
|
+
.group-control-sev { justify-self: start; }
|
|
245
|
+
.group-control-title { color: #1f2937; }
|
|
246
|
+
|
|
247
|
+
.group-control-ref {
|
|
248
|
+
color: #6b7280;
|
|
249
|
+
font-size: 0.85rem;
|
|
250
|
+
text-align: right;
|
|
251
|
+
white-space: normal;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@media (max-width: 640px) {
|
|
255
|
+
.group-control {
|
|
256
|
+
grid-template-columns: 1fr;
|
|
257
|
+
gap: 0.15rem;
|
|
258
|
+
}
|
|
259
|
+
.group-control-ref { text-align: left; }
|
|
260
|
+
}
|
|
154
261
|
|
|
155
262
|
.evidence-box {
|
|
156
263
|
background: #f9fafb;
|
|
@@ -176,6 +283,23 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
176
283
|
color: #6b7280;
|
|
177
284
|
}
|
|
178
285
|
|
|
286
|
+
/* Per-page footer: only visible when printing / exporting to PDF. */
|
|
287
|
+
.brand-page-footer { display: none; }
|
|
288
|
+
@media print {
|
|
289
|
+
.brand-page-footer {
|
|
290
|
+
display: block;
|
|
291
|
+
position: fixed;
|
|
292
|
+
bottom: 0;
|
|
293
|
+
left: 0;
|
|
294
|
+
right: 0;
|
|
295
|
+
text-align: center;
|
|
296
|
+
font-size: 0.7rem;
|
|
297
|
+
color: #6b7280;
|
|
298
|
+
padding: 0.4rem 0;
|
|
299
|
+
}
|
|
300
|
+
@page { margin-bottom: 2.2cm; }
|
|
301
|
+
}
|
|
302
|
+
|
|
179
303
|
.report-hash {
|
|
180
304
|
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
181
305
|
font-size: 0.75rem;
|
|
@@ -223,9 +347,12 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
223
347
|
<body>
|
|
224
348
|
<div class="container">
|
|
225
349
|
<div class="report-header">
|
|
226
|
-
|
|
350
|
+
${brandLogo
|
|
351
|
+
? `<img src="${brandLogo}" alt="${escapeHtml(preparedBy)} logo" class="brand-logo">`
|
|
352
|
+
: '<div class="logo-placeholder">VL</div>'}
|
|
227
353
|
<h1>HIPAA Compliance Audit Report</h1>
|
|
228
|
-
<div class="subtitle">${organizationName} - ${reportPeriod}</div>
|
|
354
|
+
<div class="subtitle">${escapeHtml(organizationName)} - ${escapeHtml(reportPeriod)}</div>
|
|
355
|
+
${hasBrand ? `<div class="prepared-by">Prepared by ${escapeHtml(preparedBy)}</div>` : ''}
|
|
229
356
|
</div>
|
|
230
357
|
|
|
231
358
|
<div class="report-meta">
|
|
@@ -255,13 +382,21 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
255
382
|
${generateComplianceScoreGauge(score)}
|
|
256
383
|
|
|
257
384
|
<h2>📋 Findings Summary</h2>
|
|
385
|
+
<p class="findings-count-note">
|
|
386
|
+
<strong>${currentFindings.length} current findings</strong> across
|
|
387
|
+
<strong>${locationGroups.length} entries</strong>
|
|
388
|
+
— grouped by file, line & control family. Filters count entries.
|
|
389
|
+
${proposedFindings.length > 0
|
|
390
|
+
? `<br><span class="upcoming-inline">+ ${proposedFindings.length} upcoming requirement${proposedFindings.length === 1 ? '' : 's'} (NPRM — proposed rule)</span> — listed separately below.`
|
|
391
|
+
: ''}
|
|
392
|
+
</p>
|
|
258
393
|
<div class="filters">
|
|
259
394
|
<div class="filter-buttons">
|
|
260
|
-
<button class="filter-btn active" onclick="filterFindings('all')">All (${
|
|
261
|
-
<button class="filter-btn" onclick="filterFindings('critical')">Critical (${
|
|
262
|
-
<button class="filter-btn" onclick="filterFindings('high')">High (${
|
|
263
|
-
<button class="filter-btn" onclick="filterFindings('medium')">Medium (${
|
|
264
|
-
<button class="filter-btn" onclick="filterFindings('low')">Low (${
|
|
395
|
+
<button class="filter-btn active" onclick="filterFindings('all')">All (${locationGroups.length})</button>
|
|
396
|
+
<button class="filter-btn" onclick="filterFindings('critical')">Critical (${groupCounts.critical})</button>
|
|
397
|
+
<button class="filter-btn" onclick="filterFindings('high')">High (${groupCounts.high})</button>
|
|
398
|
+
<button class="filter-btn" onclick="filterFindings('medium')">Medium (${groupCounts.medium})</button>
|
|
399
|
+
<button class="filter-btn" onclick="filterFindings('low')">Low (${groupCounts.low})</button>
|
|
265
400
|
</div>
|
|
266
401
|
</div>
|
|
267
402
|
|
|
@@ -275,17 +410,66 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
275
410
|
</tr>
|
|
276
411
|
</thead>
|
|
277
412
|
<tbody>
|
|
278
|
-
${
|
|
279
|
-
|
|
280
|
-
|
|
413
|
+
${locationGroups.map(g => {
|
|
414
|
+
const fileLine = `${escapeHtml(g.file)}:${g.line ?? 'N/A'}`;
|
|
415
|
+
if (g.members.length === 1) {
|
|
416
|
+
const f = g.members[0];
|
|
417
|
+
return `
|
|
418
|
+
<tr class="finding-row" data-severity="${g.severity}">
|
|
419
|
+
<td><span class="severity-badge severity-${g.severity}">${g.severity}</span></td>
|
|
281
420
|
<td>${escapeHtml(f.title)}</td>
|
|
282
|
-
<td style="font-family: monospace; font-size: 0.875rem;">${
|
|
283
|
-
<td>${f.hipaaReference
|
|
284
|
-
</tr
|
|
285
|
-
|
|
421
|
+
<td style="font-family: monospace; font-size: 0.875rem;">${fileLine}</td>
|
|
422
|
+
<td>${escapeHtml(formatHipaaRef(f.hipaaReference))}</td>
|
|
423
|
+
</tr>`;
|
|
424
|
+
}
|
|
425
|
+
const controls = g.members.map(f => `
|
|
426
|
+
<li class="group-control">
|
|
427
|
+
<span class="severity-badge severity-${f.severity} group-control-sev">${f.severity}</span>
|
|
428
|
+
<span class="group-control-title">${escapeHtml(f.title)}</span>
|
|
429
|
+
<span class="group-control-ref">${escapeHtml(formatHipaaRef(f.hipaaReference))}</span>
|
|
430
|
+
</li>`).join('');
|
|
431
|
+
return `
|
|
432
|
+
<tr class="finding-row finding-group" data-severity="${g.severity}">
|
|
433
|
+
<td><span class="severity-badge severity-${g.severity}">${g.severity}</span></td>
|
|
434
|
+
<td colspan="3">
|
|
435
|
+
<div class="group-location"><span class="group-location-path">${fileLine}</span> · ${g.members.length} controls flagged at this location</div>
|
|
436
|
+
<ul class="group-controls">${controls}
|
|
437
|
+
</ul>
|
|
438
|
+
</td>
|
|
439
|
+
</tr>`;
|
|
440
|
+
}).join('')}
|
|
286
441
|
</tbody>
|
|
287
442
|
</table>
|
|
288
443
|
|
|
444
|
+
${proposedFindings.length > 0 ? `
|
|
445
|
+
<h2>🕓 Upcoming Requirements <span class="upcoming-subtitle">(NPRM — proposed rule, not yet in effect)</span></h2>
|
|
446
|
+
<p class="upcoming-note">
|
|
447
|
+
These reference the proposed 2026 HIPAA Security Rule (NPRM). They are
|
|
448
|
+
<strong>not current obligations</strong> and are excluded from the severity counts above.
|
|
449
|
+
They will apply if and when the rule is finalized — treat them as forward-looking guidance.
|
|
450
|
+
</p>
|
|
451
|
+
<table class="findings-table">
|
|
452
|
+
<thead>
|
|
453
|
+
<tr>
|
|
454
|
+
<th>Status</th>
|
|
455
|
+
<th>Requirement</th>
|
|
456
|
+
<th>File</th>
|
|
457
|
+
<th>HIPAA Ref</th>
|
|
458
|
+
</tr>
|
|
459
|
+
</thead>
|
|
460
|
+
<tbody>
|
|
461
|
+
${proposedFindings.map(f => `
|
|
462
|
+
<tr>
|
|
463
|
+
<td><span class="severity-badge severity-proposed">Proposed</span></td>
|
|
464
|
+
<td>${escapeHtml(f.title)}</td>
|
|
465
|
+
<td style="font-family: monospace; font-size: 0.875rem;">${escapeHtml(f.file)}:${f.line ?? 'N/A'}</td>
|
|
466
|
+
<td>${escapeHtml(formatHipaaRef(f.hipaaReference))}</td>
|
|
467
|
+
</tr>
|
|
468
|
+
`).join('')}
|
|
469
|
+
</tbody>
|
|
470
|
+
</table>
|
|
471
|
+
` : ''}
|
|
472
|
+
|
|
289
473
|
${suppressedFindings.length > 0 ? `
|
|
290
474
|
<h2>🔕 Suppression Audit Trail</h2>
|
|
291
475
|
<p>The following findings have been suppressed with documented justifications:</p>
|
|
@@ -351,11 +535,14 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
351
535
|
</div>
|
|
352
536
|
|
|
353
537
|
<div class="report-footer">
|
|
354
|
-
|
|
538
|
+
${hasBrand
|
|
539
|
+
? `<p><strong>${escapeHtml(footerLine)}</strong></p>`
|
|
540
|
+
: '<p><strong>VLayer HIPAA Compliance Scanner</strong></p>'}
|
|
355
541
|
<p>Automated compliance scanning for healthcare applications</p>
|
|
356
542
|
<p style="margin-top: 1rem; font-size: 0.875rem;">Generated: ${new Date(timestamp).toLocaleString()}</p>
|
|
357
543
|
</div>
|
|
358
544
|
</div>
|
|
545
|
+
${hasBrand ? `<div class="brand-page-footer">${escapeHtml(footerLine)}</div>` : ''}
|
|
359
546
|
|
|
360
547
|
<script>
|
|
361
548
|
function filterFindings(severity) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auditor-report.js","sourceRoot":"","sources":["../../src/reporters/auditor-report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC,OAAO,EAAE,4BAA4B,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"auditor-report.js","sourceRoot":"","sources":["../../src/reporters/auditor-report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Branding, ResolvedBranding } from '../types.js';
|
|
2
|
+
/** Default author shown when no brand name is provided. */
|
|
3
|
+
export declare const DEFAULT_BRAND_NAME = "VLayer Automated Scan";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve branding from CLI flags (which take precedence) and config, validating
|
|
6
|
+
* the logo. This never throws: an invalid or missing logo becomes a warning and
|
|
7
|
+
* is dropped so the scan/report always completes.
|
|
8
|
+
*
|
|
9
|
+
* @param cli Branding from CLI flags (`--brand-name`, `--brand-logo`).
|
|
10
|
+
* @param config Branding from the `branding` block in config.
|
|
11
|
+
* @param baseDir Directory used to resolve a relative logo path (defaults to cwd).
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveBranding(cli: Branding | undefined, config: Branding | undefined, baseDir?: string): ResolvedBranding;
|
|
14
|
+
/**
|
|
15
|
+
* Footer line shown on every page. With branding it reads
|
|
16
|
+
* "Prepared by {name} · Powered by VLayer"; without, just "Powered by VLayer".
|
|
17
|
+
*/
|
|
18
|
+
export declare function brandFooterText(branding?: ResolvedBranding): string;
|
|
19
|
+
/** Author label for the cover ("Prepared by ..."), falling back to the default. */
|
|
20
|
+
export declare function brandPreparedBy(branding?: ResolvedBranding): string;
|
|
21
|
+
/**
|
|
22
|
+
* Read the logo and return a base64 data URI for embedding in HTML `<img src>`.
|
|
23
|
+
* Returns null if the logo can't be read (the report still renders).
|
|
24
|
+
*/
|
|
25
|
+
export declare function logoDataUri(branding?: ResolvedBranding): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the logo can be drawn into a PDF via pdfkit. pdfkit's `image()`
|
|
28
|
+
* supports PNG and JPEG but not SVG, so SVG logos are skipped in PDFs.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pdfLogoPath(branding?: ResolvedBranding): string | null;
|
|
31
|
+
/** Whether the brand logo is an SVG that PDF output cannot embed. */
|
|
32
|
+
export declare function isSvgLogo(branding?: ResolvedBranding): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Escape a string for safe interpolation into HTML text/attribute context.
|
|
35
|
+
* Mirrors the escaping used across the existing reporters so brand-supplied
|
|
36
|
+
* values (e.g. names with `<` or `"`) cannot inject markup.
|
|
37
|
+
*/
|
|
38
|
+
export declare function escapeHtml(text: string): string;
|
|
39
|
+
//# sourceMappingURL=branding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branding.d.ts","sourceRoot":"","sources":["../../src/reporters/branding.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAU9D,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,QAAQ,GAAG,SAAS,EACzB,MAAM,EAAE,QAAQ,GAAG,SAAS,EAC5B,OAAO,GAAE,MAAsB,GAC9B,gBAAgB,CAsBlB;AA4BD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAKnE;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAEnE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAAG,IAAI,CAWtE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAAG,IAAI,CAItE;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS/C"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/** Logo extensions we support, mapped to their resolved format + MIME type. */
|
|
4
|
+
const SUPPORTED_LOGO_FORMATS = {
|
|
5
|
+
'.png': { format: 'png', mime: 'image/png' },
|
|
6
|
+
'.jpg': { format: 'jpg', mime: 'image/jpeg' },
|
|
7
|
+
'.jpeg': { format: 'jpg', mime: 'image/jpeg' },
|
|
8
|
+
'.svg': { format: 'svg', mime: 'image/svg+xml' },
|
|
9
|
+
};
|
|
10
|
+
/** Default author shown when no brand name is provided. */
|
|
11
|
+
export const DEFAULT_BRAND_NAME = 'VLayer Automated Scan';
|
|
12
|
+
/**
|
|
13
|
+
* Resolve branding from CLI flags (which take precedence) and config, validating
|
|
14
|
+
* the logo. This never throws: an invalid or missing logo becomes a warning and
|
|
15
|
+
* is dropped so the scan/report always completes.
|
|
16
|
+
*
|
|
17
|
+
* @param cli Branding from CLI flags (`--brand-name`, `--brand-logo`).
|
|
18
|
+
* @param config Branding from the `branding` block in config.
|
|
19
|
+
* @param baseDir Directory used to resolve a relative logo path (defaults to cwd).
|
|
20
|
+
*/
|
|
21
|
+
export function resolveBranding(cli, config, baseDir = process.cwd()) {
|
|
22
|
+
const warnings = [];
|
|
23
|
+
// CLI flag wins over config for each field independently.
|
|
24
|
+
const rawName = firstNonEmpty(cli?.name, config?.name);
|
|
25
|
+
const rawLogo = firstNonEmpty(cli?.logo, config?.logo);
|
|
26
|
+
const result = { warnings };
|
|
27
|
+
if (rawName) {
|
|
28
|
+
result.name = rawName.trim();
|
|
29
|
+
}
|
|
30
|
+
if (rawLogo) {
|
|
31
|
+
const logo = validateLogo(rawLogo, baseDir, warnings);
|
|
32
|
+
if (logo) {
|
|
33
|
+
result.logoPath = logo.absolutePath;
|
|
34
|
+
result.logoFormat = logo.format;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function validateLogo(logo, baseDir, warnings) {
|
|
40
|
+
const absolutePath = path.isAbsolute(logo) ? logo : path.resolve(baseDir, logo);
|
|
41
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
42
|
+
const spec = SUPPORTED_LOGO_FORMATS[ext];
|
|
43
|
+
if (!spec) {
|
|
44
|
+
warnings.push(`Brand logo "${logo}" has an unsupported format (${ext || 'no extension'}). ` +
|
|
45
|
+
`Supported: .png, .jpg, .jpeg, .svg. Continuing without a logo.`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!existsSync(absolutePath)) {
|
|
49
|
+
warnings.push(`Brand logo not found at "${absolutePath}". Continuing without a logo.`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return { absolutePath, format: spec.format };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Footer line shown on every page. With branding it reads
|
|
56
|
+
* "Prepared by {name} · Powered by VLayer"; without, just "Powered by VLayer".
|
|
57
|
+
*/
|
|
58
|
+
export function brandFooterText(branding) {
|
|
59
|
+
if (branding?.name) {
|
|
60
|
+
return `Prepared by ${branding.name} · Powered by VLayer`;
|
|
61
|
+
}
|
|
62
|
+
return 'Powered by VLayer';
|
|
63
|
+
}
|
|
64
|
+
/** Author label for the cover ("Prepared by ..."), falling back to the default. */
|
|
65
|
+
export function brandPreparedBy(branding) {
|
|
66
|
+
return branding?.name || DEFAULT_BRAND_NAME;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the logo and return a base64 data URI for embedding in HTML `<img src>`.
|
|
70
|
+
* Returns null if the logo can't be read (the report still renders).
|
|
71
|
+
*/
|
|
72
|
+
export function logoDataUri(branding) {
|
|
73
|
+
if (!branding?.logoPath || !branding.logoFormat)
|
|
74
|
+
return null;
|
|
75
|
+
const ext = path.extname(branding.logoPath).toLowerCase();
|
|
76
|
+
const mime = SUPPORTED_LOGO_FORMATS[ext]?.mime;
|
|
77
|
+
if (!mime)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const data = readFileSync(branding.logoPath);
|
|
81
|
+
return `data:${mime};base64,${data.toString('base64')}`;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether the logo can be drawn into a PDF via pdfkit. pdfkit's `image()`
|
|
89
|
+
* supports PNG and JPEG but not SVG, so SVG logos are skipped in PDFs.
|
|
90
|
+
*/
|
|
91
|
+
export function pdfLogoPath(branding) {
|
|
92
|
+
if (!branding?.logoPath)
|
|
93
|
+
return null;
|
|
94
|
+
if (branding.logoFormat === 'svg')
|
|
95
|
+
return null;
|
|
96
|
+
return branding.logoPath;
|
|
97
|
+
}
|
|
98
|
+
/** Whether the brand logo is an SVG that PDF output cannot embed. */
|
|
99
|
+
export function isSvgLogo(branding) {
|
|
100
|
+
return branding?.logoFormat === 'svg';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Escape a string for safe interpolation into HTML text/attribute context.
|
|
104
|
+
* Mirrors the escaping used across the existing reporters so brand-supplied
|
|
105
|
+
* values (e.g. names with `<` or `"`) cannot inject markup.
|
|
106
|
+
*/
|
|
107
|
+
export function escapeHtml(text) {
|
|
108
|
+
const map = {
|
|
109
|
+
'&': '&',
|
|
110
|
+
'<': '<',
|
|
111
|
+
'>': '>',
|
|
112
|
+
'"': '"',
|
|
113
|
+
"'": ''',
|
|
114
|
+
};
|
|
115
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
116
|
+
}
|
|
117
|
+
function firstNonEmpty(...values) {
|
|
118
|
+
for (const v of values) {
|
|
119
|
+
if (typeof v === 'string' && v.trim().length > 0)
|
|
120
|
+
return v;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=branding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branding.js","sourceRoot":"","sources":["../../src/reporters/branding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation-layer helpers for the HTML/PDF reports.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: this module is purely cosmetic. It never mutates findings, never
|
|
5
|
+
* changes detection, and is NOT used to build the JSON output. It only decides
|
|
6
|
+
* how rows are *grouped and labelled* in the rendered reports. The underlying
|
|
7
|
+
* findings (and the JSON downstream tools depend on) are unchanged — every
|
|
8
|
+
* finding still exists, it is just shown once per file:line when several rules
|
|
9
|
+
* fire on the same location.
|
|
10
|
+
*/
|
|
11
|
+
import type { Finding, Severity } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* A finding is "proposed" when its HIPAA reference cites the NPRM (the proposed
|
|
14
|
+
* 2026 Security Rule), not a current obligation. Such findings are still shown,
|
|
15
|
+
* but they must not inflate a group's headline severity.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isProposedFinding(f: Finding): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Split findings into CURRENT obligations and PROPOSED (NPRM) ones. Proposed
|
|
20
|
+
* findings are rendered in their own "Upcoming Requirements" subsection and are
|
|
21
|
+
* excluded from the current-severity Scan Summary, so a proposed rule is never
|
|
22
|
+
* shown as a current red/critical violation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function partitionFindingsByStatus(findings: Finding[]): {
|
|
25
|
+
current: Finding[];
|
|
26
|
+
proposed: Finding[];
|
|
27
|
+
};
|
|
28
|
+
/** Stable ordering for the proposed-requirements list: by file, line, title. */
|
|
29
|
+
export declare function sortProposedFindings(findings: Finding[]): Finding[];
|
|
30
|
+
/** One screen entry: findings that share a (file, line, rule category). */
|
|
31
|
+
export interface LocationGroup {
|
|
32
|
+
key: string;
|
|
33
|
+
file: string;
|
|
34
|
+
line: number | undefined;
|
|
35
|
+
/** Rule category shared by all members (the "family"). */
|
|
36
|
+
category: string;
|
|
37
|
+
/** Headline severity — highest among CURRENT (non-proposed) members. */
|
|
38
|
+
severity: Severity;
|
|
39
|
+
/** Members, sorted highest-severity first then by title. */
|
|
40
|
+
members: Finding[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Group findings by (file + line + rule category). Several rules that flag the
|
|
44
|
+
* same line for the SAME reason (e.g. all PHI-in-logs on route.ts:36) collapse
|
|
45
|
+
* into one entry; unrelated rules on the same line (e.g. MFA + backup) stay as
|
|
46
|
+
* separate rows. A single-member group renders as a normal row.
|
|
47
|
+
*
|
|
48
|
+
* The headline severity reflects only CURRENT requirements — proposed (NPRM)
|
|
49
|
+
* findings never raise it (they stay listed inside with their own badge). When
|
|
50
|
+
* every member is proposed, the highest proposed severity is used.
|
|
51
|
+
*
|
|
52
|
+
* Groups are ordered by group severity (critical first), then file, line, and
|
|
53
|
+
* category. The total member count always equals the input length — nothing is
|
|
54
|
+
* dropped.
|
|
55
|
+
*/
|
|
56
|
+
export declare function groupFindingsByLocation(findings: Finding[]): LocationGroup[];
|
|
57
|
+
/** Count location-groups by their group severity (for summary/filter labels). */
|
|
58
|
+
export declare function countGroupsBySeverity(groups: LocationGroup[]): Record<Severity, number>;
|
|
59
|
+
/**
|
|
60
|
+
* Normalize any hipaaReference string to one canonical style:
|
|
61
|
+
* "45 CFR §164.312(c) — Integrity Controls".
|
|
62
|
+
*
|
|
63
|
+
* Handles the three styles currently emitted by the scanners:
|
|
64
|
+
* - already-full "45 CFR §164.312(c) - Integrity Controls"
|
|
65
|
+
* - bare section "§164.502, §164.514"
|
|
66
|
+
* - NPRM-prefixed "NPRM §164.312(d) - Person or Entity Authentication"
|
|
67
|
+
* → kept distinct as a proposed rule:
|
|
68
|
+
* "45 CFR §164.312(d) — Person or Entity Authentication (NPRM — proposed rule)"
|
|
69
|
+
*
|
|
70
|
+
* Multi-section refs (comma-separated) are expanded into each canonical ref,
|
|
71
|
+
* joined with "; ". The original string is never mutated on the finding object.
|
|
72
|
+
*/
|
|
73
|
+
export declare function formatHipaaRef(raw: string | undefined | null): string;
|
|
74
|
+
//# sourceMappingURL=finding-presentation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding-presentation.d.ts","sourceRoot":"","sources":["../../src/reporters/finding-presentation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAUrD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAErD;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG;IAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CAO1G;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAInE;AAED,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,QAAQ,EAAE,QAAQ,CAAC;IACnB,4DAA4D;IAC5D,OAAO,EAAE,OAAO,EAAE,CAAC;CACpB;AAWD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,aAAa,EAAE,CAoC5E;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAIvF;AAyDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAOrE"}
|