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.
Files changed (70) hide show
  1. package/README.md +39 -1
  2. package/dist/cli.js +76 -12
  3. package/dist/cli.js.map +1 -1
  4. package/dist/compliance-score.d.ts +1 -0
  5. package/dist/compliance-score.d.ts.map +1 -1
  6. package/dist/compliance-score.js +1 -1
  7. package/dist/compliance-score.js.map +1 -1
  8. package/dist/exclusions.d.ts +13 -0
  9. package/dist/exclusions.d.ts.map +1 -0
  10. package/dist/exclusions.js +27 -0
  11. package/dist/exclusions.js.map +1 -0
  12. package/dist/reporters/auditor-report.d.ts +2 -1
  13. package/dist/reporters/auditor-report.d.ts.map +1 -1
  14. package/dist/reporters/auditor-report.js +239 -21
  15. package/dist/reporters/auditor-report.js.map +1 -1
  16. package/dist/reporters/branding.d.ts +39 -0
  17. package/dist/reporters/branding.d.ts.map +1 -0
  18. package/dist/reporters/branding.js +124 -0
  19. package/dist/reporters/branding.js.map +1 -0
  20. package/dist/reporters/finding-presentation.d.ts +84 -0
  21. package/dist/reporters/finding-presentation.d.ts.map +1 -0
  22. package/dist/reporters/finding-presentation.js +217 -0
  23. package/dist/reporters/finding-presentation.js.map +1 -0
  24. package/dist/reporters/index.d.ts.map +1 -1
  25. package/dist/reporters/index.js +34 -0
  26. package/dist/reporters/index.js.map +1 -1
  27. package/dist/reporters/scan-pdf-report.d.ts +23 -0
  28. package/dist/reporters/scan-pdf-report.d.ts.map +1 -0
  29. package/dist/reporters/scan-pdf-report.js +325 -0
  30. package/dist/reporters/scan-pdf-report.js.map +1 -0
  31. package/dist/scan.d.ts +11 -0
  32. package/dist/scan.d.ts.map +1 -1
  33. package/dist/scan.js +46 -1
  34. package/dist/scan.js.map +1 -1
  35. package/dist/scanners/authentication/index.d.ts.map +1 -1
  36. package/dist/scanners/authentication/index.js +30 -23
  37. package/dist/scanners/authentication/index.js.map +1 -1
  38. package/dist/scanners/credentials/index.d.ts.map +1 -1
  39. package/dist/scanners/credentials/index.js +7 -2
  40. package/dist/scanners/credentials/index.js.map +1 -1
  41. package/dist/scanners/credentials/index.test.js +3 -3
  42. package/dist/scanners/credentials/patterns.d.ts.map +1 -1
  43. package/dist/scanners/credentials/patterns.js +3 -3
  44. package/dist/scanners/credentials/patterns.js.map +1 -1
  45. package/dist/scanners/hipaa2026/index.d.ts.map +1 -1
  46. package/dist/scanners/hipaa2026/index.js +7 -19
  47. package/dist/scanners/hipaa2026/index.js.map +1 -1
  48. package/dist/scanners/hipaa2026/index.test.js +2 -2
  49. package/dist/scanners/hipaa2026/patterns.d.ts.map +1 -1
  50. package/dist/scanners/hipaa2026/patterns.js +18 -5
  51. package/dist/scanners/hipaa2026/patterns.js.map +1 -1
  52. package/dist/scanners/operational/index.d.ts.map +1 -1
  53. package/dist/scanners/operational/index.js +24 -24
  54. package/dist/scanners/operational/index.js.map +1 -1
  55. package/dist/scanners/rbac/index.test.js +3 -0
  56. package/dist/scanners/rbac/index.test.js.map +1 -1
  57. package/dist/scanners/rbac/patterns.d.ts.map +1 -1
  58. package/dist/scanners/rbac/patterns.js +10 -3
  59. package/dist/scanners/rbac/patterns.js.map +1 -1
  60. package/dist/scanners/utils.d.ts +44 -0
  61. package/dist/scanners/utils.d.ts.map +1 -0
  62. package/dist/scanners/utils.js +77 -0
  63. package/dist/scanners/utils.js.map +1 -0
  64. package/dist/types.d.ts +38 -1
  65. package/dist/types.d.ts.map +1 -1
  66. package/package.json +2 -2
  67. package/dist/scan-code.d.ts +0 -12
  68. package/dist/scan-code.d.ts.map +0 -1
  69. package/dist/scan-code.js +0 -34
  70. package/dist/scan-code.js.map +0 -1
@@ -0,0 +1,84 @@
1
+ import type { Finding, Severity } from '../types.js';
2
+ /**
3
+ * A finding is "proposed" when its HIPAA reference cites the NPRM (the proposed
4
+ * 2026 Security Rule), not a current obligation. Such findings are still shown,
5
+ * but they must not inflate a group's headline severity.
6
+ */
7
+ export declare function isProposedFinding(f: Finding): boolean;
8
+ /**
9
+ * Split findings into CURRENT obligations and PROPOSED (NPRM) ones. Proposed
10
+ * findings are rendered in their own "Upcoming Requirements" subsection and are
11
+ * excluded from the current-severity Scan Summary, so a proposed rule is never
12
+ * shown as a current red/critical violation.
13
+ */
14
+ export declare function partitionFindingsByStatus(findings: Finding[]): {
15
+ current: Finding[];
16
+ proposed: Finding[];
17
+ };
18
+ /** Stable ordering for the proposed-requirements list: by file, line, title. */
19
+ export declare function sortProposedFindings(findings: Finding[]): Finding[];
20
+ /** One screen entry: findings that share a (file, line, rule category). */
21
+ export interface LocationGroup {
22
+ key: string;
23
+ file: string;
24
+ line: number | undefined;
25
+ /** Rule category shared by all members (the "family"). */
26
+ category: string;
27
+ /** Headline severity — highest among CURRENT (non-proposed) members. */
28
+ severity: Severity;
29
+ /** Members, sorted highest-severity first then by title. */
30
+ members: Finding[];
31
+ }
32
+ /**
33
+ * Group findings by (file + line + rule category). Several rules that flag the
34
+ * same line for the SAME reason (e.g. all PHI-in-logs on route.ts:36) collapse
35
+ * into one entry; unrelated rules on the same line (e.g. MFA + backup) stay as
36
+ * separate rows. A single-member group renders as a normal row.
37
+ *
38
+ * The headline severity reflects only CURRENT requirements — proposed (NPRM)
39
+ * findings never raise it (they stay listed inside with their own badge). When
40
+ * every member is proposed, the highest proposed severity is used.
41
+ *
42
+ * Groups are ordered by group severity (critical first), then file, line, and
43
+ * category. The total member count always equals the input length — nothing is
44
+ * dropped.
45
+ */
46
+ export declare function groupFindingsByLocation(findings: Finding[]): LocationGroup[];
47
+ /** Count location-groups by their group severity (for summary/filter labels). */
48
+ export declare function countGroupsBySeverity(groups: LocationGroup[]): Record<Severity, number>;
49
+ /**
50
+ * Count findings by their OWN severity — one tally per finding. Unlike
51
+ * countGroupsBySeverity (which counts location groups, so collapsing inflates
52
+ * nothing but deflates the visible numbers), this matches the raw per-severity
53
+ * numbers the Executive Summary uses, so the report shows the SAME counts in the
54
+ * summary cards, the recommendations, and the filter chips.
55
+ *
56
+ * Proposed (NPRM) findings must be excluded by the caller — pass only the
57
+ * current findings so a proposed rule never lands in a current-severity bucket.
58
+ */
59
+ export declare function countFindingsBySeverity(findings: Finding[]): Record<Severity, number>;
60
+ /**
61
+ * Render a file path for display relative to the scan target root. A white-label
62
+ * report an agency hands to its client must never leak the developer's absolute
63
+ * machine path or username, so an absolute path is always reduced to a path
64
+ * relative to the scan root (e.g. "src/app/api/patients/route.ts"). Already-
65
+ * relative paths are returned unchanged; an absolute path that escapes the root
66
+ * falls back to its basename rather than leaking the full path.
67
+ */
68
+ export declare function toRelativeDisplayPath(file: string, root: string): string;
69
+ /**
70
+ * Normalize any hipaaReference string to one canonical style:
71
+ * "45 CFR §164.312(c) — Integrity Controls".
72
+ *
73
+ * Handles the three styles currently emitted by the scanners:
74
+ * - already-full "45 CFR §164.312(c) - Integrity Controls"
75
+ * - bare section "§164.502, §164.514"
76
+ * - NPRM-prefixed "NPRM §164.312(d) - Person or Entity Authentication"
77
+ * → kept distinct as a proposed rule:
78
+ * "45 CFR §164.312(d) — Person or Entity Authentication (NPRM — proposed rule)"
79
+ *
80
+ * Multi-section refs (comma-separated) are expanded into each canonical ref,
81
+ * joined with "; ". The original string is never mutated on the finding object.
82
+ */
83
+ export declare function formatHipaaRef(raw: string | undefined | null): string;
84
+ //# 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":"AAWA,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;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAIrF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMxE;AAyDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAOrE"}
@@ -0,0 +1,217 @@
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 { relative, isAbsolute, basename } from 'path';
12
+ const SEVERITY_RANK = {
13
+ critical: 0,
14
+ high: 1,
15
+ medium: 2,
16
+ low: 3,
17
+ info: 4,
18
+ };
19
+ /**
20
+ * A finding is "proposed" when its HIPAA reference cites the NPRM (the proposed
21
+ * 2026 Security Rule), not a current obligation. Such findings are still shown,
22
+ * but they must not inflate a group's headline severity.
23
+ */
24
+ export function isProposedFinding(f) {
25
+ return /\bNPRM\b/i.test(f.hipaaReference ?? '');
26
+ }
27
+ /**
28
+ * Split findings into CURRENT obligations and PROPOSED (NPRM) ones. Proposed
29
+ * findings are rendered in their own "Upcoming Requirements" subsection and are
30
+ * excluded from the current-severity Scan Summary, so a proposed rule is never
31
+ * shown as a current red/critical violation.
32
+ */
33
+ export function partitionFindingsByStatus(findings) {
34
+ const current = [];
35
+ const proposed = [];
36
+ for (const f of findings) {
37
+ (isProposedFinding(f) ? proposed : current).push(f);
38
+ }
39
+ return { current, proposed };
40
+ }
41
+ /** Stable ordering for the proposed-requirements list: by file, line, title. */
42
+ export function sortProposedFindings(findings) {
43
+ return [...findings].sort((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0) || a.title.localeCompare(b.title));
44
+ }
45
+ /** Highest severity among the given findings; null if the list is empty. */
46
+ function highestSeverity(findings) {
47
+ let best = null;
48
+ for (const f of findings) {
49
+ if (best === null || SEVERITY_RANK[f.severity] < SEVERITY_RANK[best])
50
+ best = f.severity;
51
+ }
52
+ return best;
53
+ }
54
+ /**
55
+ * Group findings by (file + line + rule category). Several rules that flag the
56
+ * same line for the SAME reason (e.g. all PHI-in-logs on route.ts:36) collapse
57
+ * into one entry; unrelated rules on the same line (e.g. MFA + backup) stay as
58
+ * separate rows. A single-member group renders as a normal row.
59
+ *
60
+ * The headline severity reflects only CURRENT requirements — proposed (NPRM)
61
+ * findings never raise it (they stay listed inside with their own badge). When
62
+ * every member is proposed, the highest proposed severity is used.
63
+ *
64
+ * Groups are ordered by group severity (critical first), then file, line, and
65
+ * category. The total member count always equals the input length — nothing is
66
+ * dropped.
67
+ */
68
+ export function groupFindingsByLocation(findings) {
69
+ const byKey = new Map();
70
+ for (const f of findings) {
71
+ const key = `${f.file}::${f.line ?? ''}::${f.category}`;
72
+ const existing = byKey.get(key);
73
+ if (existing)
74
+ existing.push(f);
75
+ else
76
+ byKey.set(key, [f]);
77
+ }
78
+ const groups = [];
79
+ for (const [key, members] of byKey) {
80
+ const sorted = [...members].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || a.title.localeCompare(b.title));
81
+ const current = sorted.filter(f => !isProposedFinding(f));
82
+ // Headline severity: highest among current findings; if the group is
83
+ // entirely proposed, fall back to the highest proposed severity.
84
+ const severity = highestSeverity(current.length > 0 ? current : sorted);
85
+ groups.push({
86
+ key,
87
+ file: sorted[0].file,
88
+ line: sorted[0].line,
89
+ category: sorted[0].category,
90
+ severity,
91
+ members: sorted,
92
+ });
93
+ }
94
+ groups.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||
95
+ a.file.localeCompare(b.file) ||
96
+ (a.line ?? 0) - (b.line ?? 0) ||
97
+ a.category.localeCompare(b.category));
98
+ return groups;
99
+ }
100
+ /** Count location-groups by their group severity (for summary/filter labels). */
101
+ export function countGroupsBySeverity(groups) {
102
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
103
+ for (const g of groups)
104
+ counts[g.severity]++;
105
+ return counts;
106
+ }
107
+ /**
108
+ * Count findings by their OWN severity — one tally per finding. Unlike
109
+ * countGroupsBySeverity (which counts location groups, so collapsing inflates
110
+ * nothing but deflates the visible numbers), this matches the raw per-severity
111
+ * numbers the Executive Summary uses, so the report shows the SAME counts in the
112
+ * summary cards, the recommendations, and the filter chips.
113
+ *
114
+ * Proposed (NPRM) findings must be excluded by the caller — pass only the
115
+ * current findings so a proposed rule never lands in a current-severity bucket.
116
+ */
117
+ export function countFindingsBySeverity(findings) {
118
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
119
+ for (const f of findings)
120
+ counts[f.severity]++;
121
+ return counts;
122
+ }
123
+ /**
124
+ * Render a file path for display relative to the scan target root. A white-label
125
+ * report an agency hands to its client must never leak the developer's absolute
126
+ * machine path or username, so an absolute path is always reduced to a path
127
+ * relative to the scan root (e.g. "src/app/api/patients/route.ts"). Already-
128
+ * relative paths are returned unchanged; an absolute path that escapes the root
129
+ * falls back to its basename rather than leaking the full path.
130
+ */
131
+ export function toRelativeDisplayPath(file, root) {
132
+ if (!file)
133
+ return '';
134
+ if (!isAbsolute(file))
135
+ return file;
136
+ const rel = relative(root, file);
137
+ if (!rel || rel.startsWith('..'))
138
+ return basename(file);
139
+ return rel;
140
+ }
141
+ /**
142
+ * Official 45 CFR Part 164 control names, keyed by section. Used only to fill in
143
+ * the control name for findings whose hipaaReference is a bare citation
144
+ * (e.g. "§164.502"). Findings that already carry a name keep their own wording.
145
+ */
146
+ const SECTION_NAMES = {
147
+ '164.308(a)(1)(ii)(A)': 'Risk Analysis',
148
+ '164.308(a)(7)(ii)(A)': 'Data Backup Plan',
149
+ '164.308(a)(8)': 'Evaluation',
150
+ '164.312(a)(1)': 'Access Control',
151
+ '164.312(a)(2)(i)': 'Unique User Identification',
152
+ '164.312(a)(2)(iii)': 'Automatic Logoff',
153
+ '164.312(a)(2)(iv)': 'Encryption and Decryption',
154
+ '164.312(b)': 'Audit Controls',
155
+ '164.312(c)': 'Integrity',
156
+ '164.312(d)': 'Person or Entity Authentication',
157
+ '164.312(e)(1)': 'Transmission Security',
158
+ '164.502': 'Uses and Disclosures of PHI: General Rules',
159
+ '164.502(b)': 'Minimum Necessary',
160
+ '164.514': 'De-identification and Minimum Necessary',
161
+ '164.530(j)': 'Documentation',
162
+ };
163
+ /** Progressively strip trailing "(...)" groups to find a fallback name. */
164
+ function nameForSection(section) {
165
+ let candidate = section;
166
+ while (candidate) {
167
+ if (SECTION_NAMES[candidate])
168
+ return SECTION_NAMES[candidate];
169
+ const stripped = candidate.replace(/\([^)]*\)$/, '');
170
+ if (stripped === candidate)
171
+ break;
172
+ candidate = stripped;
173
+ }
174
+ return undefined;
175
+ }
176
+ function normalizeOneRef(raw) {
177
+ const trimmed = raw.trim();
178
+ if (!trimmed || trimmed === '-' || trimmed === '—')
179
+ return null;
180
+ const sectionMatch = trimmed.match(/(\d{3}\.\d+(?:\s*\([^)]*\))*)/);
181
+ if (!sectionMatch)
182
+ return trimmed; // unrecognised shape — leave untouched
183
+ const section = sectionMatch[1].replace(/\s+/g, '');
184
+ // NPRM refs cite a PROPOSED rule, not a current obligation — keep that
185
+ // distinction explicit so the report never presents a proposed requirement
186
+ // as enforceable.
187
+ const isNprm = /\bNPRM\b/i.test(trimmed);
188
+ // Inline control name = text after a dash delimiter, if present.
189
+ const dashMatch = trimmed.match(/[-–—]\s*(.+)$/);
190
+ const name = (dashMatch ? dashMatch[1].trim() : undefined) ?? nameForSection(section);
191
+ const base = name ? `45 CFR §${section} — ${name}` : `45 CFR §${section}`;
192
+ return isNprm ? `${base} (NPRM — proposed rule)` : base;
193
+ }
194
+ /**
195
+ * Normalize any hipaaReference string to one canonical style:
196
+ * "45 CFR §164.312(c) — Integrity Controls".
197
+ *
198
+ * Handles the three styles currently emitted by the scanners:
199
+ * - already-full "45 CFR §164.312(c) - Integrity Controls"
200
+ * - bare section "§164.502, §164.514"
201
+ * - NPRM-prefixed "NPRM §164.312(d) - Person or Entity Authentication"
202
+ * → kept distinct as a proposed rule:
203
+ * "45 CFR §164.312(d) — Person or Entity Authentication (NPRM — proposed rule)"
204
+ *
205
+ * Multi-section refs (comma-separated) are expanded into each canonical ref,
206
+ * joined with "; ". The original string is never mutated on the finding object.
207
+ */
208
+ export function formatHipaaRef(raw) {
209
+ if (!raw)
210
+ return '—';
211
+ // Split only at commas that begin a new citation, so control names that
212
+ // happen to contain a comma are not broken apart.
213
+ const parts = raw.split(/,\s*(?=(?:45 CFR|NPRM|§|\d{3}\.))/);
214
+ const normalized = parts.map(normalizeOneRef).filter((p) => Boolean(p));
215
+ return normalized.length > 0 ? normalized.join('; ') : '—';
216
+ }
217
+ //# sourceMappingURL=finding-presentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finding-presentation.js","sourceRoot":"","sources":["../../src/reporters/finding-presentation.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/reporters/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAU,aAAa,EAA4E,MAAM,aAAa,CAAC;AAsrG/I,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,IAAI,CAAC,CA6Bf"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/reporters/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAU,aAAa,EAA4E,MAAM,aAAa,CAAC;AAitG/I,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,IAAI,CAAC,CAsCf"}
@@ -3,6 +3,8 @@ import * as path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { getRemediationGuide } from './remediation-guides.js';
5
5
  import { getStackSpecificGuides } from '../stack-detector/stack-guides.js';
6
+ import { brandFooterText, brandPreparedBy, logoDataUri } from './branding.js';
7
+ import { generateScanPdf } from './scan-pdf-report.js';
6
8
  const PACKAGE_CATEGORIES = {
7
9
  // Frameworks
8
10
  'next': { category: 'framework', provider: 'Next.js Framework' },
@@ -884,6 +886,11 @@ async function generateHtml(report, targetPath, options) {
884
886
  const complianceScoreHtml = await renderComplianceScoreHtml(report, targetPath);
885
887
  const assetInventoryHtml = await renderAssetInventoryHtml(targetPath);
886
888
  const dataFlowMapHtml = await renderDataFlowMapHtml(targetPath, report.findings);
889
+ // White-label branding (no-op when no brand is supplied → output unchanged).
890
+ const brandLogo = logoDataUri(options.branding);
891
+ const hasBrand = Boolean(options.branding?.name || brandLogo);
892
+ const brandPreparedByLabel = brandPreparedBy(options.branding);
893
+ const brandFooterLine = brandFooterText(options.branding);
887
894
  const severityColors = {
888
895
  critical: '#dc2626',
889
896
  high: '#ea580c',
@@ -914,6 +921,17 @@ async function generateHtml(report, targetPath, options) {
914
921
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; background: #f9fafb; padding: 2rem; }
915
922
  .container { max-width: 1400px; margin: 0 auto; }
916
923
 
924
+ /* White-label branding */
925
+ .brand-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e7eb; }
926
+ .brand-header img.brand-logo { max-height: 64px; max-width: 220px; object-fit: contain; }
927
+ .brand-header .brand-prepared-by { color: #6b7280; font-size: 0.95rem; }
928
+ .brand-header .brand-prepared-by strong { color: #111827; }
929
+ .brand-page-footer { display: none; }
930
+ @media print {
931
+ .brand-page-footer { display: block; position: fixed; bottom: 0; left: 0; right: 0; text-align: center; font-size: 0.7rem; color: #6b7280; padding: 0.4rem 0; }
932
+ @page { margin-bottom: 2.2cm; }
933
+ }
934
+
917
935
  /* Executive Summary Styles */
918
936
  .executive-summary-section { margin: 0 0 3rem 0; padding: 2.5rem; background: white; border-radius: 16px; box-shadow: 0 4px 6px rgba(0,0,0,0.07); }
919
937
  .exec-header h1 { color: #111827; font-size: 2.5rem; margin: 0 0 0.5rem 0; }
@@ -1299,6 +1317,12 @@ async function generateHtml(report, targetPath, options) {
1299
1317
  </head>
1300
1318
  <body>
1301
1319
  <div class="container">
1320
+ ${hasBrand ? `
1321
+ <div class="brand-header">
1322
+ ${brandLogo ? `<img src="${brandLogo}" alt="${escapeHtml(brandPreparedByLabel)} logo" class="brand-logo">` : ''}
1323
+ <div class="brand-prepared-by">Prepared by <strong>${escapeHtml(brandPreparedByLabel)}</strong></div>
1324
+ </div>` : ''}
1325
+
1302
1326
  ${renderExecutiveSummaryHtml(report)}
1303
1327
 
1304
1328
  <h1>HIPAA Compliance Report</h1>
@@ -1377,10 +1401,12 @@ async function generateHtml(report, targetPath, options) {
1377
1401
  </div>
1378
1402
 
1379
1403
  <footer style="margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 0.875rem;">
1404
+ ${hasBrand ? `<p><strong>${escapeHtml(brandFooterLine)}</strong></p>` : ''}
1380
1405
  <p>Generated by <strong>vlayer</strong> v0.2.0 - HIPAA Compliance Scanner for Healthcare Applications</p>
1381
1406
  <p>Run with <code>--fix</code> flag to automatically fix issues marked as "Auto-fixable"</p>
1382
1407
  </footer>
1383
1408
  </div>
1409
+ ${hasBrand ? `<div class="brand-page-footer">${escapeHtml(brandFooterLine)}</div>` : ''}
1384
1410
  </body>
1385
1411
  </html>`;
1386
1412
  }
@@ -3026,6 +3052,14 @@ function severityBadge(severity) {
3026
3052
  }
3027
3053
  export async function generateReport(result, targetPath, options) {
3028
3054
  const report = buildReport(result, targetPath, options.vulnerabilities);
3055
+ // PDF is binary: generate the buffer and write it directly.
3056
+ if (options.format === 'pdf') {
3057
+ const { buffer } = await generateScanPdf(result, targetPath, { branding: options.branding });
3058
+ const pdfPath = options.outputPath || 'vlayer-report.pdf';
3059
+ await writeFile(pdfPath, buffer);
3060
+ console.log(chalk.green(`\nReport saved to: ${pdfPath}`));
3061
+ return;
3062
+ }
3029
3063
  let content;
3030
3064
  let extension;
3031
3065
  switch (options.format) {