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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const SEVERITY_RANK = {
|
|
2
|
+
critical: 0,
|
|
3
|
+
high: 1,
|
|
4
|
+
medium: 2,
|
|
5
|
+
low: 3,
|
|
6
|
+
info: 4,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A finding is "proposed" when its HIPAA reference cites the NPRM (the proposed
|
|
10
|
+
* 2026 Security Rule), not a current obligation. Such findings are still shown,
|
|
11
|
+
* but they must not inflate a group's headline severity.
|
|
12
|
+
*/
|
|
13
|
+
export function isProposedFinding(f) {
|
|
14
|
+
return /\bNPRM\b/i.test(f.hipaaReference ?? '');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Split findings into CURRENT obligations and PROPOSED (NPRM) ones. Proposed
|
|
18
|
+
* findings are rendered in their own "Upcoming Requirements" subsection and are
|
|
19
|
+
* excluded from the current-severity Scan Summary, so a proposed rule is never
|
|
20
|
+
* shown as a current red/critical violation.
|
|
21
|
+
*/
|
|
22
|
+
export function partitionFindingsByStatus(findings) {
|
|
23
|
+
const current = [];
|
|
24
|
+
const proposed = [];
|
|
25
|
+
for (const f of findings) {
|
|
26
|
+
(isProposedFinding(f) ? proposed : current).push(f);
|
|
27
|
+
}
|
|
28
|
+
return { current, proposed };
|
|
29
|
+
}
|
|
30
|
+
/** Stable ordering for the proposed-requirements list: by file, line, title. */
|
|
31
|
+
export function sortProposedFindings(findings) {
|
|
32
|
+
return [...findings].sort((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0) || a.title.localeCompare(b.title));
|
|
33
|
+
}
|
|
34
|
+
/** Highest severity among the given findings; null if the list is empty. */
|
|
35
|
+
function highestSeverity(findings) {
|
|
36
|
+
let best = null;
|
|
37
|
+
for (const f of findings) {
|
|
38
|
+
if (best === null || SEVERITY_RANK[f.severity] < SEVERITY_RANK[best])
|
|
39
|
+
best = f.severity;
|
|
40
|
+
}
|
|
41
|
+
return best;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Group findings by (file + line + rule category). Several rules that flag the
|
|
45
|
+
* same line for the SAME reason (e.g. all PHI-in-logs on route.ts:36) collapse
|
|
46
|
+
* into one entry; unrelated rules on the same line (e.g. MFA + backup) stay as
|
|
47
|
+
* separate rows. A single-member group renders as a normal row.
|
|
48
|
+
*
|
|
49
|
+
* The headline severity reflects only CURRENT requirements — proposed (NPRM)
|
|
50
|
+
* findings never raise it (they stay listed inside with their own badge). When
|
|
51
|
+
* every member is proposed, the highest proposed severity is used.
|
|
52
|
+
*
|
|
53
|
+
* Groups are ordered by group severity (critical first), then file, line, and
|
|
54
|
+
* category. The total member count always equals the input length — nothing is
|
|
55
|
+
* dropped.
|
|
56
|
+
*/
|
|
57
|
+
export function groupFindingsByLocation(findings) {
|
|
58
|
+
const byKey = new Map();
|
|
59
|
+
for (const f of findings) {
|
|
60
|
+
const key = `${f.file}::${f.line ?? ''}::${f.category}`;
|
|
61
|
+
const existing = byKey.get(key);
|
|
62
|
+
if (existing)
|
|
63
|
+
existing.push(f);
|
|
64
|
+
else
|
|
65
|
+
byKey.set(key, [f]);
|
|
66
|
+
}
|
|
67
|
+
const groups = [];
|
|
68
|
+
for (const [key, members] of byKey) {
|
|
69
|
+
const sorted = [...members].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || a.title.localeCompare(b.title));
|
|
70
|
+
const current = sorted.filter(f => !isProposedFinding(f));
|
|
71
|
+
// Headline severity: highest among current findings; if the group is
|
|
72
|
+
// entirely proposed, fall back to the highest proposed severity.
|
|
73
|
+
const severity = highestSeverity(current.length > 0 ? current : sorted);
|
|
74
|
+
groups.push({
|
|
75
|
+
key,
|
|
76
|
+
file: sorted[0].file,
|
|
77
|
+
line: sorted[0].line,
|
|
78
|
+
category: sorted[0].category,
|
|
79
|
+
severity,
|
|
80
|
+
members: sorted,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
groups.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||
|
|
84
|
+
a.file.localeCompare(b.file) ||
|
|
85
|
+
(a.line ?? 0) - (b.line ?? 0) ||
|
|
86
|
+
a.category.localeCompare(b.category));
|
|
87
|
+
return groups;
|
|
88
|
+
}
|
|
89
|
+
/** Count location-groups by their group severity (for summary/filter labels). */
|
|
90
|
+
export function countGroupsBySeverity(groups) {
|
|
91
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
92
|
+
for (const g of groups)
|
|
93
|
+
counts[g.severity]++;
|
|
94
|
+
return counts;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Official 45 CFR Part 164 control names, keyed by section. Used only to fill in
|
|
98
|
+
* the control name for findings whose hipaaReference is a bare citation
|
|
99
|
+
* (e.g. "§164.502"). Findings that already carry a name keep their own wording.
|
|
100
|
+
*/
|
|
101
|
+
const SECTION_NAMES = {
|
|
102
|
+
'164.308(a)(1)(ii)(A)': 'Risk Analysis',
|
|
103
|
+
'164.308(a)(7)(ii)(A)': 'Data Backup Plan',
|
|
104
|
+
'164.308(a)(8)': 'Evaluation',
|
|
105
|
+
'164.312(a)(1)': 'Access Control',
|
|
106
|
+
'164.312(a)(2)(i)': 'Unique User Identification',
|
|
107
|
+
'164.312(a)(2)(iii)': 'Automatic Logoff',
|
|
108
|
+
'164.312(a)(2)(iv)': 'Encryption and Decryption',
|
|
109
|
+
'164.312(b)': 'Audit Controls',
|
|
110
|
+
'164.312(c)': 'Integrity',
|
|
111
|
+
'164.312(d)': 'Person or Entity Authentication',
|
|
112
|
+
'164.312(e)(1)': 'Transmission Security',
|
|
113
|
+
'164.502': 'Uses and Disclosures of PHI: General Rules',
|
|
114
|
+
'164.502(b)': 'Minimum Necessary',
|
|
115
|
+
'164.514': 'De-identification and Minimum Necessary',
|
|
116
|
+
'164.530(j)': 'Documentation',
|
|
117
|
+
};
|
|
118
|
+
/** Progressively strip trailing "(...)" groups to find a fallback name. */
|
|
119
|
+
function nameForSection(section) {
|
|
120
|
+
let candidate = section;
|
|
121
|
+
while (candidate) {
|
|
122
|
+
if (SECTION_NAMES[candidate])
|
|
123
|
+
return SECTION_NAMES[candidate];
|
|
124
|
+
const stripped = candidate.replace(/\([^)]*\)$/, '');
|
|
125
|
+
if (stripped === candidate)
|
|
126
|
+
break;
|
|
127
|
+
candidate = stripped;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function normalizeOneRef(raw) {
|
|
132
|
+
const trimmed = raw.trim();
|
|
133
|
+
if (!trimmed || trimmed === '-' || trimmed === '—')
|
|
134
|
+
return null;
|
|
135
|
+
const sectionMatch = trimmed.match(/(\d{3}\.\d+(?:\s*\([^)]*\))*)/);
|
|
136
|
+
if (!sectionMatch)
|
|
137
|
+
return trimmed; // unrecognised shape — leave untouched
|
|
138
|
+
const section = sectionMatch[1].replace(/\s+/g, '');
|
|
139
|
+
// NPRM refs cite a PROPOSED rule, not a current obligation — keep that
|
|
140
|
+
// distinction explicit so the report never presents a proposed requirement
|
|
141
|
+
// as enforceable.
|
|
142
|
+
const isNprm = /\bNPRM\b/i.test(trimmed);
|
|
143
|
+
// Inline control name = text after a dash delimiter, if present.
|
|
144
|
+
const dashMatch = trimmed.match(/[-–—]\s*(.+)$/);
|
|
145
|
+
const name = (dashMatch ? dashMatch[1].trim() : undefined) ?? nameForSection(section);
|
|
146
|
+
const base = name ? `45 CFR §${section} — ${name}` : `45 CFR §${section}`;
|
|
147
|
+
return isNprm ? `${base} (NPRM — proposed rule)` : base;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Normalize any hipaaReference string to one canonical style:
|
|
151
|
+
* "45 CFR §164.312(c) — Integrity Controls".
|
|
152
|
+
*
|
|
153
|
+
* Handles the three styles currently emitted by the scanners:
|
|
154
|
+
* - already-full "45 CFR §164.312(c) - Integrity Controls"
|
|
155
|
+
* - bare section "§164.502, §164.514"
|
|
156
|
+
* - NPRM-prefixed "NPRM §164.312(d) - Person or Entity Authentication"
|
|
157
|
+
* → kept distinct as a proposed rule:
|
|
158
|
+
* "45 CFR §164.312(d) — Person or Entity Authentication (NPRM — proposed rule)"
|
|
159
|
+
*
|
|
160
|
+
* Multi-section refs (comma-separated) are expanded into each canonical ref,
|
|
161
|
+
* joined with "; ". The original string is never mutated on the finding object.
|
|
162
|
+
*/
|
|
163
|
+
export function formatHipaaRef(raw) {
|
|
164
|
+
if (!raw)
|
|
165
|
+
return '—';
|
|
166
|
+
// Split only at commas that begin a new citation, so control names that
|
|
167
|
+
// happen to contain a comma are not broken apart.
|
|
168
|
+
const parts = raw.split(/,\s*(?=(?:45 CFR|NPRM|§|\d{3}\.))/);
|
|
169
|
+
const normalized = parts.map(normalizeOneRef).filter((p) => Boolean(p));
|
|
170
|
+
return normalized.length > 0 ? normalized.join('; ') : '—';
|
|
171
|
+
}
|
|
172
|
+
//# 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,
|
|
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' },
|
|
@@ -134,22 +136,6 @@ async function generateAssetInventory(targetPath) {
|
|
|
134
136
|
totalAssets: assets.length,
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
|
-
function generateAssetInventoryCsv(inventory) {
|
|
138
|
-
const header = 'ID,Name,Version,Type,Category,Provider,Responsible Person,Location\n';
|
|
139
|
-
const rows = inventory.assets.map(asset => {
|
|
140
|
-
return [
|
|
141
|
-
asset.id,
|
|
142
|
-
`"${asset.name}"`,
|
|
143
|
-
`"${asset.version}"`,
|
|
144
|
-
asset.type,
|
|
145
|
-
asset.category,
|
|
146
|
-
`"${asset.provider}"`,
|
|
147
|
-
`"${asset.responsiblePerson}"`,
|
|
148
|
-
`"${asset.location}"`,
|
|
149
|
-
].join(',');
|
|
150
|
-
}).join('\n');
|
|
151
|
-
return header + rows;
|
|
152
|
-
}
|
|
153
139
|
async function analyzeDataFlow(targetPath, findings) {
|
|
154
140
|
const entryPoints = [];
|
|
155
141
|
const dataStores = [];
|
|
@@ -618,7 +604,7 @@ async function getScoreTrending(targetPath, currentScore) {
|
|
|
618
604
|
}
|
|
619
605
|
return { direction, previousScore, change };
|
|
620
606
|
}
|
|
621
|
-
catch
|
|
607
|
+
catch {
|
|
622
608
|
// No history available
|
|
623
609
|
return null;
|
|
624
610
|
}
|
|
@@ -900,6 +886,11 @@ async function generateHtml(report, targetPath, options) {
|
|
|
900
886
|
const complianceScoreHtml = await renderComplianceScoreHtml(report, targetPath);
|
|
901
887
|
const assetInventoryHtml = await renderAssetInventoryHtml(targetPath);
|
|
902
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);
|
|
903
894
|
const severityColors = {
|
|
904
895
|
critical: '#dc2626',
|
|
905
896
|
high: '#ea580c',
|
|
@@ -930,6 +921,17 @@ async function generateHtml(report, targetPath, options) {
|
|
|
930
921
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; background: #f9fafb; padding: 2rem; }
|
|
931
922
|
.container { max-width: 1400px; margin: 0 auto; }
|
|
932
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
|
+
|
|
933
935
|
/* Executive Summary Styles */
|
|
934
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); }
|
|
935
937
|
.exec-header h1 { color: #111827; font-size: 2.5rem; margin: 0 0 0.5rem 0; }
|
|
@@ -1315,6 +1317,12 @@ async function generateHtml(report, targetPath, options) {
|
|
|
1315
1317
|
</head>
|
|
1316
1318
|
<body>
|
|
1317
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
|
+
|
|
1318
1326
|
${renderExecutiveSummaryHtml(report)}
|
|
1319
1327
|
|
|
1320
1328
|
<h1>HIPAA Compliance Report</h1>
|
|
@@ -1393,10 +1401,12 @@ async function generateHtml(report, targetPath, options) {
|
|
|
1393
1401
|
</div>
|
|
1394
1402
|
|
|
1395
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>` : ''}
|
|
1396
1405
|
<p>Generated by <strong>vlayer</strong> v0.2.0 - HIPAA Compliance Scanner for Healthcare Applications</p>
|
|
1397
1406
|
<p>Run with <code>--fix</code> flag to automatically fix issues marked as "Auto-fixable"</p>
|
|
1398
1407
|
</footer>
|
|
1399
1408
|
</div>
|
|
1409
|
+
${hasBrand ? `<div class="brand-page-footer">${escapeHtml(brandFooterLine)}</div>` : ''}
|
|
1400
1410
|
</body>
|
|
1401
1411
|
</html>`;
|
|
1402
1412
|
}
|
|
@@ -1797,12 +1807,9 @@ function renderExecutiveSummaryHtml(report) {
|
|
|
1797
1807
|
}
|
|
1798
1808
|
function renderBackupRecoveryGuideHtml(stack) {
|
|
1799
1809
|
let guideContent = '';
|
|
1800
|
-
let dbType = 'unknown';
|
|
1801
|
-
let dbDisplay = stack?.databaseDisplay || 'Unknown';
|
|
1802
1810
|
// Detect database type from stack
|
|
1803
1811
|
const database = stack?.database || 'unknown';
|
|
1804
1812
|
if (database.includes('supabase')) {
|
|
1805
|
-
dbType = 'supabase';
|
|
1806
1813
|
guideContent = `
|
|
1807
1814
|
<div class="backup-guide-card">
|
|
1808
1815
|
<div class="backup-guide-header">
|
|
@@ -1853,10 +1860,10 @@ function renderBackupRecoveryGuideHtml(stack) {
|
|
|
1853
1860
|
<strong>Additional Manual Backup (Optional)</strong>
|
|
1854
1861
|
<div class="backup-code-block">
|
|
1855
1862
|
<pre><code># Using pg_dump for additional backup
|
|
1856
|
-
pg_dump "postgresql://[user]:[password]@[host]:[port]/[database]" > backup_
|
|
1863
|
+
pg_dump "postgresql://[user]:[password]@[host]:[port]/[database]" > backup_$(date +%Y%m%d).sql
|
|
1857
1864
|
|
|
1858
1865
|
# Upload to secure offsite storage
|
|
1859
|
-
aws s3 cp backup_
|
|
1866
|
+
aws s3 cp backup_$(date +%Y%m%d).sql s3://your-backup-bucket/</code></pre>
|
|
1860
1867
|
</div>
|
|
1861
1868
|
</div>
|
|
1862
1869
|
</div>
|
|
@@ -1865,7 +1872,6 @@ aws s3 cp backup_\$(date +%Y%m%d).sql s3://your-backup-bucket/</code></pre>
|
|
|
1865
1872
|
`;
|
|
1866
1873
|
}
|
|
1867
1874
|
else if (database.includes('prisma') || database.includes('postgres')) {
|
|
1868
|
-
dbType = 'postgresql';
|
|
1869
1875
|
guideContent = `
|
|
1870
1876
|
<div class="backup-guide-card">
|
|
1871
1877
|
<div class="backup-guide-header">
|
|
@@ -1883,23 +1889,23 @@ aws s3 cp backup_\$(date +%Y%m%d).sql s3://your-backup-bucket/</code></pre>
|
|
|
1883
1889
|
# PostgreSQL Backup Script for HIPAA Compliance
|
|
1884
1890
|
|
|
1885
1891
|
BACKUP_DIR="/path/to/backups"
|
|
1886
|
-
TIMESTAMP
|
|
1892
|
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
1887
1893
|
BACKUP_FILE="\${BACKUP_DIR}/backup_\${TIMESTAMP}.sql"
|
|
1888
1894
|
|
|
1889
1895
|
# Create backup directory if not exists
|
|
1890
|
-
mkdir -p
|
|
1896
|
+
mkdir -p $BACKUP_DIR
|
|
1891
1897
|
|
|
1892
1898
|
# Perform backup
|
|
1893
|
-
pg_dump
|
|
1899
|
+
pg_dump $DATABASE_URL > $BACKUP_FILE
|
|
1894
1900
|
|
|
1895
1901
|
# Compress backup
|
|
1896
|
-
gzip
|
|
1902
|
+
gzip $BACKUP_FILE
|
|
1897
1903
|
|
|
1898
1904
|
# Upload to offsite storage (S3 example)
|
|
1899
1905
|
aws s3 cp \${BACKUP_FILE}.gz s3://your-backup-bucket/postgresql/
|
|
1900
1906
|
|
|
1901
1907
|
# Keep only last 30 days of local backups
|
|
1902
|
-
find
|
|
1908
|
+
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
|
|
1903
1909
|
|
|
1904
1910
|
echo "Backup completed: \${BACKUP_FILE}.gz"</code></pre>
|
|
1905
1911
|
</div>
|
|
@@ -1929,10 +1935,10 @@ aws s3 cp s3://your-backup-bucket/postgresql/backup_YYYYMMDD.sql.gz .
|
|
|
1929
1935
|
gunzip backup_YYYYMMDD.sql.gz
|
|
1930
1936
|
|
|
1931
1937
|
# Restore to test database
|
|
1932
|
-
psql
|
|
1938
|
+
psql $TEST_DATABASE_URL < backup_YYYYMMDD.sql
|
|
1933
1939
|
|
|
1934
1940
|
# Verify data integrity
|
|
1935
|
-
psql
|
|
1941
|
+
psql $TEST_DATABASE_URL -c "SELECT COUNT(*) FROM patients;"</code></pre>
|
|
1936
1942
|
</div>
|
|
1937
1943
|
</div>
|
|
1938
1944
|
</div>
|
|
@@ -1954,7 +1960,6 @@ psql \$TEST_DATABASE_URL -c "SELECT COUNT(*) FROM patients;"</code></pre>
|
|
|
1954
1960
|
`;
|
|
1955
1961
|
}
|
|
1956
1962
|
else if (database.includes('mongo')) {
|
|
1957
|
-
dbType = 'mongodb';
|
|
1958
1963
|
guideContent = `
|
|
1959
1964
|
<div class="backup-guide-card">
|
|
1960
1965
|
<div class="backup-guide-header">
|
|
@@ -1986,12 +1991,12 @@ psql \$TEST_DATABASE_URL -c "SELECT COUNT(*) FROM patients;"</code></pre>
|
|
|
1986
1991
|
<pre><code>#!/bin/bash
|
|
1987
1992
|
# MongoDB Backup Script
|
|
1988
1993
|
|
|
1989
|
-
TIMESTAMP
|
|
1994
|
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
1990
1995
|
BACKUP_DIR="/path/to/backups"
|
|
1991
1996
|
BACKUP_NAME="mongodb_backup_\${TIMESTAMP}"
|
|
1992
1997
|
|
|
1993
1998
|
# Perform backup
|
|
1994
|
-
mongodump --uri="
|
|
1999
|
+
mongodump --uri="$MONGODB_URI" --out=\${BACKUP_DIR}/\${BACKUP_NAME}
|
|
1995
2000
|
|
|
1996
2001
|
# Compress
|
|
1997
2002
|
tar -czf \${BACKUP_DIR}/\${BACKUP_NAME}.tar.gz -C \${BACKUP_DIR} \${BACKUP_NAME}
|
|
@@ -2001,7 +2006,7 @@ rm -rf \${BACKUP_DIR}/\${BACKUP_NAME}
|
|
|
2001
2006
|
aws s3 cp \${BACKUP_DIR}/\${BACKUP_NAME}.tar.gz s3://your-backup-bucket/mongodb/
|
|
2002
2007
|
|
|
2003
2008
|
# Cleanup old local backups (keep 7 days)
|
|
2004
|
-
find
|
|
2009
|
+
find $BACKUP_DIR -name "mongodb_backup_*.tar.gz" -mtime +7 -delete</code></pre>
|
|
2005
2010
|
</div>
|
|
2006
2011
|
</div>
|
|
2007
2012
|
</div>
|
|
@@ -2016,10 +2021,10 @@ aws s3 cp s3://your-backup-bucket/mongodb/mongodb_backup_YYYYMMDD.tar.gz .
|
|
|
2016
2021
|
tar -xzf mongodb_backup_YYYYMMDD.tar.gz
|
|
2017
2022
|
|
|
2018
2023
|
# Restore to test database
|
|
2019
|
-
mongorestore --uri="
|
|
2024
|
+
mongorestore --uri="$TEST_MONGODB_URI" mongodb_backup_YYYYMMDD/
|
|
2020
2025
|
|
|
2021
2026
|
# Verify collections
|
|
2022
|
-
mongo
|
|
2027
|
+
mongo $TEST_MONGODB_URI --eval "db.getCollectionNames()"</code></pre>
|
|
2023
2028
|
</div>
|
|
2024
2029
|
</div>
|
|
2025
2030
|
</div>
|
|
@@ -2028,7 +2033,6 @@ mongo \$TEST_MONGODB_URI --eval "db.getCollectionNames()"</code></pre>
|
|
|
2028
2033
|
`;
|
|
2029
2034
|
}
|
|
2030
2035
|
else {
|
|
2031
|
-
dbType = 'none';
|
|
2032
2036
|
guideContent = `
|
|
2033
2037
|
<div class="backup-guide-card backup-guide-warning">
|
|
2034
2038
|
<div class="backup-guide-header">
|
|
@@ -2171,9 +2175,8 @@ mongo \$TEST_MONGODB_URI --eval "db.getCollectionNames()"</code></pre>
|
|
|
2171
2175
|
</div>
|
|
2172
2176
|
`;
|
|
2173
2177
|
}
|
|
2174
|
-
function renderIncidentResponsePlanHtml(criticalFindings,
|
|
2178
|
+
function renderIncidentResponsePlanHtml(criticalFindings, _highFindings) {
|
|
2175
2179
|
const hasActiveIncident = criticalFindings > 0;
|
|
2176
|
-
const riskLevel = criticalFindings > 0 ? 'HIGH' : highFindings > 0 ? 'MEDIUM' : 'LOW';
|
|
2177
2180
|
return `
|
|
2178
2181
|
<div class="incident-response-section">
|
|
2179
2182
|
<div class="incident-header">
|
|
@@ -2598,7 +2601,6 @@ function renderScanComparisonHtml(comparison) {
|
|
|
2598
2601
|
const criticalChange = formatChange(severityChanges.critical, true);
|
|
2599
2602
|
const highChange = formatChange(severityChanges.high, true);
|
|
2600
2603
|
const mediumChange = formatChange(severityChanges.medium, true);
|
|
2601
|
-
const lowChange = formatChange(severityChanges.low, true);
|
|
2602
2604
|
// Format previous scan date
|
|
2603
2605
|
const prevDate = new Date(previousScan.timestamp);
|
|
2604
2606
|
const formattedDate = prevDate.toLocaleString('en-US', {
|
|
@@ -3050,6 +3052,14 @@ function severityBadge(severity) {
|
|
|
3050
3052
|
}
|
|
3051
3053
|
export async function generateReport(result, targetPath, options) {
|
|
3052
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
|
+
}
|
|
3053
3063
|
let content;
|
|
3054
3064
|
let extension;
|
|
3055
3065
|
switch (options.format) {
|