verification-layer 0.24.5 → 0.25.1
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 +39 -1
- package/dist/cli.js +76 -12
- package/dist/cli.js.map +1 -1
- package/dist/compliance-score.d.ts +1 -0
- package/dist/compliance-score.d.ts.map +1 -1
- package/dist/compliance-score.js +1 -1
- package/dist/compliance-score.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/reporters/auditor-report.d.ts +2 -1
- package/dist/reporters/auditor-report.d.ts.map +1 -1
- package/dist/reporters/auditor-report.js +239 -21
- 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 +84 -0
- package/dist/reporters/finding-presentation.d.ts.map +1 -0
- package/dist/reporters/finding-presentation.js +217 -0
- package/dist/reporters/finding-presentation.js.map +1 -0
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +34 -0
- 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 +325 -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/authentication/index.d.ts.map +1 -1
- package/dist/scanners/authentication/index.js +30 -23
- package/dist/scanners/authentication/index.js.map +1 -1
- package/dist/scanners/credentials/index.d.ts.map +1 -1
- package/dist/scanners/credentials/index.js +7 -2
- 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 +3 -3
- package/dist/scanners/credentials/patterns.js.map +1 -1
- package/dist/scanners/hipaa2026/index.d.ts.map +1 -1
- package/dist/scanners/hipaa2026/index.js +7 -19
- 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 +24 -24
- package/dist/scanners/operational/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/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/types.d.ts +38 -1
- package/dist/types.d.ts.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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default exclusion globs for vlayer's OWN generated output artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Without these, a second scan re-reads the report/baseline files vlayer wrote
|
|
5
|
+
* on a previous run and flags their text as if it were source code (e.g. the
|
|
6
|
+
* literal "Weak cryptography: DES encryption" inside a JSON report). On the demo
|
|
7
|
+
* this produced 62/95 false positives. These globs are added to the file-discovery
|
|
8
|
+
* ignore list by default and can be disabled with `--include-own-artifacts`.
|
|
9
|
+
*
|
|
10
|
+
* Patterns are prefixed with a globstar so they match at any depth in the tree.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_VLAYER_OUTPUT_EXCLUDES: readonly string[];
|
|
13
|
+
//# sourceMappingURL=exclusions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclusions.d.ts","sourceRoot":"","sources":["../src/exclusions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAc3D,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default exclusion globs for vlayer's OWN generated output artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Without these, a second scan re-reads the report/baseline files vlayer wrote
|
|
5
|
+
* on a previous run and flags their text as if it were source code (e.g. the
|
|
6
|
+
* literal "Weak cryptography: DES encryption" inside a JSON report). On the demo
|
|
7
|
+
* this produced 62/95 false positives. These globs are added to the file-discovery
|
|
8
|
+
* ignore list by default and can be disabled with `--include-own-artifacts`.
|
|
9
|
+
*
|
|
10
|
+
* Patterns are prefixed with a globstar so they match at any depth in the tree.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_VLAYER_OUTPUT_EXCLUDES = [
|
|
13
|
+
// Standard report outputs (`vlayer scan/report -o`)
|
|
14
|
+
'**/vlayer-report.json',
|
|
15
|
+
'**/vlayer-report.html',
|
|
16
|
+
'**/vlayer-report.md',
|
|
17
|
+
'**/vlayer-report.pdf',
|
|
18
|
+
// Auditor report outputs (`vlayer report` / `vlayer audit --generate-report`)
|
|
19
|
+
'**/vlayer-audit-report.html',
|
|
20
|
+
'**/vlayer-audit-report.pdf',
|
|
21
|
+
// Baseline + metadata vlayer writes into the project
|
|
22
|
+
'**/.vlayer-baseline.json',
|
|
23
|
+
'**/.vlayer/**',
|
|
24
|
+
// Curated sample reports directory (publishable report artifacts)
|
|
25
|
+
'**/samples/**',
|
|
26
|
+
];
|
|
27
|
+
//# sourceMappingURL=exclusions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclusions.js","sourceRoot":"","sources":["../src/exclusions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAsB;IAC/D,oDAAoD;IACpD,uBAAuB;IACvB,uBAAuB;IACvB,qBAAqB;IACrB,sBAAsB;IACtB,8EAA8E;IAC9E,6BAA6B;IAC7B,4BAA4B;IAC5B,qDAAqD;IACrD,0BAA0B;IAC1B,eAAe;IACf,kEAAkE;IAClE,eAAe;CAChB,CAAC"}
|
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"auditor-report.d.ts","sourceRoot":"","sources":["../../src/reporters/auditor-report.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAahE,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,CAsmBhC"}
|
|
@@ -1,17 +1,59 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { resolve, basename } from 'path';
|
|
2
3
|
import { generateComplianceScoreGauge, generateExecutiveSummary, generateEnhancedCSS } from './enhanced-html.js';
|
|
4
|
+
import { generateRecommendations } from '../compliance-score.js';
|
|
5
|
+
import { brandFooterText, brandPreparedBy, logoDataUri } from './branding.js';
|
|
6
|
+
import { groupFindingsByLocation, countFindingsBySeverity, formatHipaaRef, partitionFindingsByStatus, sortProposedFindings, toRelativeDisplayPath, } from './finding-presentation.js';
|
|
3
7
|
/**
|
|
4
8
|
* Generate auditor-ready HTML report with compliance score and SHA256 hash
|
|
5
9
|
*/
|
|
6
10
|
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;
|
|
11
|
+
const { organizationName = 'Organization', reportPeriod = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long' }), auditorName = 'VLayer Automated Scan', includeBaseline = false, branding, } = options;
|
|
12
|
+
// White-label branding. When a brand is supplied the cover shows the brand
|
|
13
|
+
// logo + "Prepared by {brand}", and a footer line repeats on every printed
|
|
14
|
+
// page. With no branding the report renders exactly as before (default VLayer).
|
|
15
|
+
const brandLogo = logoDataUri(branding);
|
|
16
|
+
const hasBrand = Boolean(branding?.name || brandLogo);
|
|
17
|
+
const preparedBy = brandPreparedBy(branding);
|
|
18
|
+
const footerLine = brandFooterText(branding);
|
|
8
19
|
const timestamp = new Date().toISOString();
|
|
9
20
|
const score = result.complianceScore;
|
|
10
21
|
// Calculate findings
|
|
11
22
|
const activeFindings = result.findings.filter(f => !f.isBaseline && !f.suppressed);
|
|
23
|
+
// Presentation-only: separate proposed (NPRM) findings from current ones, then
|
|
24
|
+
// collapse multiple rules on the same file:line into one grouped entry. The
|
|
25
|
+
// findings themselves are untouched (count is preserved).
|
|
26
|
+
const { current: currentFindings, proposed: proposedRaw } = partitionFindingsByStatus(activeFindings);
|
|
27
|
+
const proposedFindings = sortProposedFindings(proposedRaw);
|
|
28
|
+
const locationGroups = groupFindingsByLocation(currentFindings);
|
|
29
|
+
// Per-severity counts come from the RAW current findings (proposed excluded),
|
|
30
|
+
// so the summary cards, recommendations, and filter chips all show the SAME
|
|
31
|
+
// numbers. Location-grouping below is purely a display device for the list.
|
|
32
|
+
const severityCounts = countFindingsBySeverity(currentFindings);
|
|
12
33
|
const acknowledgedFindings = result.findings.filter(f => f.acknowledged && !f.acknowledgment?.expired);
|
|
13
34
|
const suppressedFindings = result.findings.filter(f => f.suppressed);
|
|
14
35
|
const baselineFindings = result.findings.filter(f => f.isBaseline);
|
|
36
|
+
// Display-corrected score: the stored breakdown counts ALL active findings
|
|
37
|
+
// (it includes proposed/NPRM), which would show e.g. "8 critical" up top while
|
|
38
|
+
// the current-obligation list shows 7. Re-key the breakdown to the current
|
|
39
|
+
// findings so every rendered number reconciles, and regenerate the
|
|
40
|
+
// recommendation text from those same numbers. The compliance score value,
|
|
41
|
+
// grade and penalties are left untouched — only the displayed counts change.
|
|
42
|
+
const absTargetRoot = resolve(targetPath);
|
|
43
|
+
const displayTarget = basename(absTargetRoot) || targetPath;
|
|
44
|
+
const displayBreakdown = {
|
|
45
|
+
...score.breakdown,
|
|
46
|
+
critical: severityCounts.critical,
|
|
47
|
+
high: severityCounts.high,
|
|
48
|
+
medium: severityCounts.medium,
|
|
49
|
+
low: severityCounts.low,
|
|
50
|
+
total: currentFindings.length,
|
|
51
|
+
};
|
|
52
|
+
const displayScore = {
|
|
53
|
+
...score,
|
|
54
|
+
breakdown: displayBreakdown,
|
|
55
|
+
recommendations: generateRecommendations(displayBreakdown, score.score),
|
|
56
|
+
};
|
|
15
57
|
const html = `<!DOCTYPE html>
|
|
16
58
|
<html lang="en">
|
|
17
59
|
<head>
|
|
@@ -50,6 +92,20 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
50
92
|
font-weight: bold;
|
|
51
93
|
}
|
|
52
94
|
|
|
95
|
+
.brand-logo {
|
|
96
|
+
max-height: 80px;
|
|
97
|
+
max-width: 240px;
|
|
98
|
+
margin: 0 auto 1rem;
|
|
99
|
+
display: block;
|
|
100
|
+
object-fit: contain;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.report-header .prepared-by {
|
|
104
|
+
opacity: 0.95;
|
|
105
|
+
font-size: 0.95rem;
|
|
106
|
+
margin-top: 0.5rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
53
109
|
.report-header h1 {
|
|
54
110
|
font-size: 2rem;
|
|
55
111
|
margin-bottom: 0.5rem;
|
|
@@ -151,6 +207,83 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
151
207
|
.severity-high { background: #ea580c; }
|
|
152
208
|
.severity-medium { background: #ca8a04; }
|
|
153
209
|
.severity-low { background: #2563eb; }
|
|
210
|
+
.severity-info { background: #6b7280; }
|
|
211
|
+
/* Proposed (NPRM) — deliberately neutral: not a current violation. */
|
|
212
|
+
.severity-proposed { background: #64748b; }
|
|
213
|
+
|
|
214
|
+
.upcoming-subtitle {
|
|
215
|
+
font-size: 0.85rem;
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
color: #64748b;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.upcoming-note {
|
|
221
|
+
background: #f1f5f9;
|
|
222
|
+
border-left: 3px solid #64748b;
|
|
223
|
+
padding: 0.75rem 1rem;
|
|
224
|
+
margin: 0.5rem 0 1rem;
|
|
225
|
+
font-size: 0.9rem;
|
|
226
|
+
color: #475569;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.upcoming-inline {
|
|
230
|
+
color: #475569;
|
|
231
|
+
font-weight: 600;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Consolidated multi-rule location entries */
|
|
235
|
+
.findings-count-note {
|
|
236
|
+
color: #4b5563;
|
|
237
|
+
font-size: 0.95rem;
|
|
238
|
+
margin: 0.25rem 0 0.75rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.finding-group td { vertical-align: top; }
|
|
242
|
+
|
|
243
|
+
.group-location {
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
color: #374151;
|
|
246
|
+
margin-bottom: 0.5rem;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.group-location-path {
|
|
250
|
+
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
251
|
+
font-size: 0.9rem;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.group-controls {
|
|
255
|
+
list-style: none;
|
|
256
|
+
margin: 0;
|
|
257
|
+
padding: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.group-control {
|
|
261
|
+
display: grid;
|
|
262
|
+
grid-template-columns: 90px 1fr auto;
|
|
263
|
+
gap: 0.75rem;
|
|
264
|
+
align-items: baseline;
|
|
265
|
+
padding: 0.35rem 0;
|
|
266
|
+
border-top: 1px solid #f3f4f6;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.group-control:first-child { border-top: none; }
|
|
270
|
+
.group-control-sev { justify-self: start; }
|
|
271
|
+
.group-control-title { color: #1f2937; }
|
|
272
|
+
|
|
273
|
+
.group-control-ref {
|
|
274
|
+
color: #6b7280;
|
|
275
|
+
font-size: 0.85rem;
|
|
276
|
+
text-align: right;
|
|
277
|
+
white-space: normal;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@media (max-width: 640px) {
|
|
281
|
+
.group-control {
|
|
282
|
+
grid-template-columns: 1fr;
|
|
283
|
+
gap: 0.15rem;
|
|
284
|
+
}
|
|
285
|
+
.group-control-ref { text-align: left; }
|
|
286
|
+
}
|
|
154
287
|
|
|
155
288
|
.evidence-box {
|
|
156
289
|
background: #f9fafb;
|
|
@@ -176,6 +309,23 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
176
309
|
color: #6b7280;
|
|
177
310
|
}
|
|
178
311
|
|
|
312
|
+
/* Per-page footer: only visible when printing / exporting to PDF. */
|
|
313
|
+
.brand-page-footer { display: none; }
|
|
314
|
+
@media print {
|
|
315
|
+
.brand-page-footer {
|
|
316
|
+
display: block;
|
|
317
|
+
position: fixed;
|
|
318
|
+
bottom: 0;
|
|
319
|
+
left: 0;
|
|
320
|
+
right: 0;
|
|
321
|
+
text-align: center;
|
|
322
|
+
font-size: 0.7rem;
|
|
323
|
+
color: #6b7280;
|
|
324
|
+
padding: 0.4rem 0;
|
|
325
|
+
}
|
|
326
|
+
@page { margin-bottom: 2.2cm; }
|
|
327
|
+
}
|
|
328
|
+
|
|
179
329
|
.report-hash {
|
|
180
330
|
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
181
331
|
font-size: 0.75rem;
|
|
@@ -223,9 +373,12 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
223
373
|
<body>
|
|
224
374
|
<div class="container">
|
|
225
375
|
<div class="report-header">
|
|
226
|
-
|
|
376
|
+
${brandLogo
|
|
377
|
+
? `<img src="${brandLogo}" alt="${escapeHtml(preparedBy)} logo" class="brand-logo">`
|
|
378
|
+
: '<div class="logo-placeholder">VL</div>'}
|
|
227
379
|
<h1>HIPAA Compliance Audit Report</h1>
|
|
228
|
-
<div class="subtitle">${organizationName} - ${reportPeriod}</div>
|
|
380
|
+
<div class="subtitle">${escapeHtml(organizationName)} - ${escapeHtml(reportPeriod)}</div>
|
|
381
|
+
${hasBrand ? `<div class="prepared-by">Prepared by ${escapeHtml(preparedBy)}</div>` : ''}
|
|
229
382
|
</div>
|
|
230
383
|
|
|
231
384
|
<div class="report-meta">
|
|
@@ -240,7 +393,7 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
240
393
|
</div>
|
|
241
394
|
<div class="meta-item">
|
|
242
395
|
<div class="meta-label">Target Path</div>
|
|
243
|
-
<div class="meta-value">${
|
|
396
|
+
<div class="meta-value">${escapeHtml(displayTarget)}</div>
|
|
244
397
|
</div>
|
|
245
398
|
<div class="meta-item">
|
|
246
399
|
<div class="meta-label">Files Scanned</div>
|
|
@@ -250,18 +403,27 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
250
403
|
</div>
|
|
251
404
|
|
|
252
405
|
<div class="content">
|
|
253
|
-
${generateExecutiveSummary(
|
|
406
|
+
${generateExecutiveSummary(displayScore, result.scannedFiles, result.scanDuration)}
|
|
254
407
|
|
|
255
|
-
${generateComplianceScoreGauge(
|
|
408
|
+
${generateComplianceScoreGauge(displayScore)}
|
|
256
409
|
|
|
257
410
|
<h2>📋 Findings Summary</h2>
|
|
411
|
+
<p class="findings-count-note">
|
|
412
|
+
<strong>${currentFindings.length} findings grouped into ${locationGroups.length} location${locationGroups.length === 1 ? '' : 's'}</strong>
|
|
413
|
+
— multiple controls flagged at the same file & line are shown together,
|
|
414
|
+
but each is counted individually. The chip counts below match the
|
|
415
|
+
Executive Summary.
|
|
416
|
+
${proposedFindings.length > 0
|
|
417
|
+
? `<br><span class="upcoming-inline">+ ${proposedFindings.length} upcoming requirement${proposedFindings.length === 1 ? '' : 's'} (NPRM — proposed rule)</span> — listed separately below, excluded from the counts above.`
|
|
418
|
+
: ''}
|
|
419
|
+
</p>
|
|
258
420
|
<div class="filters">
|
|
259
421
|
<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 (${
|
|
422
|
+
<button class="filter-btn active" onclick="filterFindings('all')">All (${currentFindings.length})</button>
|
|
423
|
+
<button class="filter-btn" onclick="filterFindings('critical')">Critical (${severityCounts.critical})</button>
|
|
424
|
+
<button class="filter-btn" onclick="filterFindings('high')">High (${severityCounts.high})</button>
|
|
425
|
+
<button class="filter-btn" onclick="filterFindings('medium')">Medium (${severityCounts.medium})</button>
|
|
426
|
+
<button class="filter-btn" onclick="filterFindings('low')">Low (${severityCounts.low})</button>
|
|
265
427
|
</div>
|
|
266
428
|
</div>
|
|
267
429
|
|
|
@@ -275,17 +437,69 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
275
437
|
</tr>
|
|
276
438
|
</thead>
|
|
277
439
|
<tbody>
|
|
278
|
-
${
|
|
279
|
-
|
|
280
|
-
|
|
440
|
+
${locationGroups.map(g => {
|
|
441
|
+
const fileLine = `${escapeHtml(toRelativeDisplayPath(g.file, absTargetRoot))}:${g.line ?? 'N/A'}`;
|
|
442
|
+
// A row matches a filter chip if ANY member has that severity, so the
|
|
443
|
+
// chip count (raw findings) and the rows it reveals stay consistent.
|
|
444
|
+
const memberSeverities = [...new Set(g.members.map(m => m.severity))].join(' ');
|
|
445
|
+
if (g.members.length === 1) {
|
|
446
|
+
const f = g.members[0];
|
|
447
|
+
return `
|
|
448
|
+
<tr class="finding-row" data-severity="${g.severity}" data-severities="${memberSeverities}">
|
|
449
|
+
<td><span class="severity-badge severity-${g.severity}">${g.severity}</span></td>
|
|
281
450
|
<td>${escapeHtml(f.title)}</td>
|
|
282
|
-
<td style="font-family: monospace; font-size: 0.875rem;">${
|
|
283
|
-
<td>${f.hipaaReference
|
|
284
|
-
</tr
|
|
285
|
-
|
|
451
|
+
<td style="font-family: monospace; font-size: 0.875rem;">${fileLine}</td>
|
|
452
|
+
<td>${escapeHtml(formatHipaaRef(f.hipaaReference))}</td>
|
|
453
|
+
</tr>`;
|
|
454
|
+
}
|
|
455
|
+
const controls = g.members.map(f => `
|
|
456
|
+
<li class="group-control">
|
|
457
|
+
<span class="severity-badge severity-${f.severity} group-control-sev">${f.severity}</span>
|
|
458
|
+
<span class="group-control-title">${escapeHtml(f.title)}</span>
|
|
459
|
+
<span class="group-control-ref">${escapeHtml(formatHipaaRef(f.hipaaReference))}</span>
|
|
460
|
+
</li>`).join('');
|
|
461
|
+
return `
|
|
462
|
+
<tr class="finding-row finding-group" data-severity="${g.severity}" data-severities="${memberSeverities}">
|
|
463
|
+
<td><span class="severity-badge severity-${g.severity}">${g.severity}</span></td>
|
|
464
|
+
<td colspan="3">
|
|
465
|
+
<div class="group-location"><span class="group-location-path">${fileLine}</span> · ${g.members.length} controls flagged at this location</div>
|
|
466
|
+
<ul class="group-controls">${controls}
|
|
467
|
+
</ul>
|
|
468
|
+
</td>
|
|
469
|
+
</tr>`;
|
|
470
|
+
}).join('')}
|
|
286
471
|
</tbody>
|
|
287
472
|
</table>
|
|
288
473
|
|
|
474
|
+
${proposedFindings.length > 0 ? `
|
|
475
|
+
<h2>🕓 Upcoming Requirements <span class="upcoming-subtitle">(NPRM — proposed rule, not yet in effect)</span></h2>
|
|
476
|
+
<p class="upcoming-note">
|
|
477
|
+
These reference the proposed 2026 HIPAA Security Rule (NPRM). They are
|
|
478
|
+
<strong>not current obligations</strong> and are excluded from the severity counts above.
|
|
479
|
+
They will apply if and when the rule is finalized — treat them as forward-looking guidance.
|
|
480
|
+
</p>
|
|
481
|
+
<table class="findings-table">
|
|
482
|
+
<thead>
|
|
483
|
+
<tr>
|
|
484
|
+
<th>Status</th>
|
|
485
|
+
<th>Requirement</th>
|
|
486
|
+
<th>File</th>
|
|
487
|
+
<th>HIPAA Ref</th>
|
|
488
|
+
</tr>
|
|
489
|
+
</thead>
|
|
490
|
+
<tbody>
|
|
491
|
+
${proposedFindings.map(f => `
|
|
492
|
+
<tr>
|
|
493
|
+
<td><span class="severity-badge severity-proposed">Proposed</span></td>
|
|
494
|
+
<td>${escapeHtml(f.title)}</td>
|
|
495
|
+
<td style="font-family: monospace; font-size: 0.875rem;">${escapeHtml(toRelativeDisplayPath(f.file, absTargetRoot))}:${f.line ?? 'N/A'}</td>
|
|
496
|
+
<td>${escapeHtml(formatHipaaRef(f.hipaaReference))}</td>
|
|
497
|
+
</tr>
|
|
498
|
+
`).join('')}
|
|
499
|
+
</tbody>
|
|
500
|
+
</table>
|
|
501
|
+
` : ''}
|
|
502
|
+
|
|
289
503
|
${suppressedFindings.length > 0 ? `
|
|
290
504
|
<h2>🔕 Suppression Audit Trail</h2>
|
|
291
505
|
<p>The following findings have been suppressed with documented justifications:</p>
|
|
@@ -302,7 +516,7 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
302
516
|
<tr>
|
|
303
517
|
<td>${escapeHtml(f.title)}</td>
|
|
304
518
|
<td>${f.suppression?.reason || 'No reason provided'}</td>
|
|
305
|
-
<td style="font-family: monospace; font-size: 0.875rem;">${escapeHtml(f.file)}:${f.line || 'N/A'}</td>
|
|
519
|
+
<td style="font-family: monospace; font-size: 0.875rem;">${escapeHtml(toRelativeDisplayPath(f.file, absTargetRoot))}:${f.line || 'N/A'}</td>
|
|
306
520
|
</tr>
|
|
307
521
|
`).join('')}
|
|
308
522
|
</tbody>
|
|
@@ -351,11 +565,14 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
351
565
|
</div>
|
|
352
566
|
|
|
353
567
|
<div class="report-footer">
|
|
354
|
-
|
|
568
|
+
${hasBrand
|
|
569
|
+
? `<p><strong>${escapeHtml(footerLine)}</strong></p>`
|
|
570
|
+
: '<p><strong>VLayer HIPAA Compliance Scanner</strong></p>'}
|
|
355
571
|
<p>Automated compliance scanning for healthcare applications</p>
|
|
356
572
|
<p style="margin-top: 1rem; font-size: 0.875rem;">Generated: ${new Date(timestamp).toLocaleString()}</p>
|
|
357
573
|
</div>
|
|
358
574
|
</div>
|
|
575
|
+
${hasBrand ? `<div class="brand-page-footer">${escapeHtml(footerLine)}</div>` : ''}
|
|
359
576
|
|
|
360
577
|
<script>
|
|
361
578
|
function filterFindings(severity) {
|
|
@@ -366,7 +583,8 @@ export function generateAuditorReport(result, targetPath, options = {}) {
|
|
|
366
583
|
event.target.classList.add('active');
|
|
367
584
|
|
|
368
585
|
rows.forEach(row => {
|
|
369
|
-
|
|
586
|
+
const severities = (row.dataset.severities || row.dataset.severity || '').split(' ');
|
|
587
|
+
if (severity === 'all' || severities.indexOf(severity) !== -1) {
|
|
370
588
|
row.style.display = '';
|
|
371
589
|
} else {
|
|
372
590
|
row.style.display = 'none';
|
|
@@ -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;
|
|
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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"}
|