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,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;
|
|
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"}
|
package/dist/reporters/index.js
CHANGED
|
@@ -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) {
|