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.
Files changed (111) hide show
  1. package/README.md +42 -2
  2. package/dist/ai/cache.js +2 -2
  3. package/dist/ai/cache.js.map +1 -1
  4. package/dist/ai/config.d.ts +1 -1
  5. package/dist/ai/config.js +1 -1
  6. package/dist/ai/config.js.map +1 -1
  7. package/dist/ai/rules/prompts/audit-logging.js +1 -1
  8. package/dist/ai/rules/rule-runner.d.ts.map +1 -1
  9. package/dist/ai/rules/rule-runner.js.map +1 -1
  10. package/dist/ai/rules/triage.d.ts.map +1 -1
  11. package/dist/ai/rules/triage.js +1 -1
  12. package/dist/ai/rules/triage.js.map +1 -1
  13. package/dist/ai/scanner.d.ts.map +1 -1
  14. package/dist/ai/scanner.js +1 -1
  15. package/dist/ai/scanner.js.map +1 -1
  16. package/dist/cli.js +77 -13
  17. package/dist/cli.js.map +1 -1
  18. package/dist/exclusions.d.ts +13 -0
  19. package/dist/exclusions.d.ts.map +1 -0
  20. package/dist/exclusions.js +27 -0
  21. package/dist/exclusions.js.map +1 -0
  22. package/dist/index.d.ts +0 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +0 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/marketplace/installer.d.ts.map +1 -1
  27. package/dist/marketplace/installer.js +3 -3
  28. package/dist/marketplace/installer.js.map +1 -1
  29. package/dist/marketplace/registry.d.ts.map +1 -1
  30. package/dist/marketplace/registry.js +3 -1
  31. package/dist/marketplace/registry.js.map +1 -1
  32. package/dist/reporters/auditor-report.d.ts +2 -1
  33. package/dist/reporters/auditor-report.d.ts.map +1 -1
  34. package/dist/reporters/auditor-report.js +203 -16
  35. package/dist/reporters/auditor-report.js.map +1 -1
  36. package/dist/reporters/branding.d.ts +39 -0
  37. package/dist/reporters/branding.d.ts.map +1 -0
  38. package/dist/reporters/branding.js +124 -0
  39. package/dist/reporters/branding.js.map +1 -0
  40. package/dist/reporters/finding-presentation.d.ts +74 -0
  41. package/dist/reporters/finding-presentation.d.ts.map +1 -0
  42. package/dist/reporters/finding-presentation.js +172 -0
  43. package/dist/reporters/finding-presentation.js.map +1 -0
  44. package/dist/reporters/index.d.ts.map +1 -1
  45. package/dist/reporters/index.js +50 -40
  46. package/dist/reporters/index.js.map +1 -1
  47. package/dist/reporters/scan-pdf-report.d.ts +23 -0
  48. package/dist/reporters/scan-pdf-report.d.ts.map +1 -0
  49. package/dist/reporters/scan-pdf-report.js +326 -0
  50. package/dist/reporters/scan-pdf-report.js.map +1 -0
  51. package/dist/scan.d.ts +11 -0
  52. package/dist/scan.d.ts.map +1 -1
  53. package/dist/scan.js +46 -1
  54. package/dist/scan.js.map +1 -1
  55. package/dist/scanners/api-security/index.js +2 -2
  56. package/dist/scanners/api-security/index.js.map +1 -1
  57. package/dist/scanners/authentication/index.d.ts.map +1 -1
  58. package/dist/scanners/authentication/index.js +32 -27
  59. package/dist/scanners/authentication/index.js.map +1 -1
  60. package/dist/scanners/configuration/index.js +2 -2
  61. package/dist/scanners/configuration/index.js.map +1 -1
  62. package/dist/scanners/credentials/index.d.ts.map +1 -1
  63. package/dist/scanners/credentials/index.js +9 -4
  64. package/dist/scanners/credentials/index.js.map +1 -1
  65. package/dist/scanners/credentials/index.test.js +3 -3
  66. package/dist/scanners/credentials/patterns.d.ts.map +1 -1
  67. package/dist/scanners/credentials/patterns.js +4 -4
  68. package/dist/scanners/credentials/patterns.js.map +1 -1
  69. package/dist/scanners/errors/index.js +2 -2
  70. package/dist/scanners/errors/index.js.map +1 -1
  71. package/dist/scanners/hipaa2026/index.d.ts.map +1 -1
  72. package/dist/scanners/hipaa2026/index.js +8 -20
  73. package/dist/scanners/hipaa2026/index.js.map +1 -1
  74. package/dist/scanners/hipaa2026/index.test.js +2 -2
  75. package/dist/scanners/hipaa2026/patterns.d.ts.map +1 -1
  76. package/dist/scanners/hipaa2026/patterns.js +18 -5
  77. package/dist/scanners/hipaa2026/patterns.js.map +1 -1
  78. package/dist/scanners/operational/index.d.ts.map +1 -1
  79. package/dist/scanners/operational/index.js +27 -27
  80. package/dist/scanners/operational/index.js.map +1 -1
  81. package/dist/scanners/rbac/index.js +2 -2
  82. package/dist/scanners/rbac/index.js.map +1 -1
  83. package/dist/scanners/rbac/index.test.js +3 -0
  84. package/dist/scanners/rbac/index.test.js.map +1 -1
  85. package/dist/scanners/rbac/patterns.d.ts.map +1 -1
  86. package/dist/scanners/rbac/patterns.js +10 -3
  87. package/dist/scanners/rbac/patterns.js.map +1 -1
  88. package/dist/scanners/revocation/index.js +2 -2
  89. package/dist/scanners/revocation/index.js.map +1 -1
  90. package/dist/scanners/sanitization/index.d.ts.map +1 -1
  91. package/dist/scanners/sanitization/index.js +2 -3
  92. package/dist/scanners/sanitization/index.js.map +1 -1
  93. package/dist/scanners/skills/index.js +1 -1
  94. package/dist/scanners/skills/index.js.map +1 -1
  95. package/dist/scanners/skills/patterns.js +3 -3
  96. package/dist/scanners/skills/patterns.js.map +1 -1
  97. package/dist/scanners/utils.d.ts +44 -0
  98. package/dist/scanners/utils.d.ts.map +1 -0
  99. package/dist/scanners/utils.js +77 -0
  100. package/dist/scanners/utils.js.map +1 -0
  101. package/dist/training/index.js +1 -1
  102. package/dist/training/index.js.map +1 -1
  103. package/dist/types.d.ts +38 -1
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/utils/scan-history.js +2 -2
  106. package/dist/utils/scan-history.js.map +1 -1
  107. package/package.json +2 -2
  108. package/dist/scan-code.d.ts +0 -12
  109. package/dist/scan-code.d.ts.map +0 -1
  110. package/dist/scan-code.js +0 -34
  111. 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,EAA4F,MAAM,aAAa,CAAC;AAitG/J,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' },
@@ -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 (error) {
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_\$(date +%Y%m%d).sql
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_\$(date +%Y%m%d).sql s3://your-backup-bucket/</code></pre>
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=\$(date +%Y%m%d_%H%M%S)
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 \$BACKUP_DIR
1896
+ mkdir -p $BACKUP_DIR
1891
1897
 
1892
1898
  # Perform backup
1893
- pg_dump \$DATABASE_URL > \$BACKUP_FILE
1899
+ pg_dump $DATABASE_URL > $BACKUP_FILE
1894
1900
 
1895
1901
  # Compress backup
1896
- gzip \$BACKUP_FILE
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 \$BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
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 \$TEST_DATABASE_URL < backup_YYYYMMDD.sql
1938
+ psql $TEST_DATABASE_URL < backup_YYYYMMDD.sql
1933
1939
 
1934
1940
  # Verify data integrity
1935
- psql \$TEST_DATABASE_URL -c "SELECT COUNT(*) FROM patients;"</code></pre>
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=\$(date +%Y%m%d_%H%M%S)
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="\$MONGODB_URI" --out=\${BACKUP_DIR}/\${BACKUP_NAME}
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 \$BACKUP_DIR -name "mongodb_backup_*.tar.gz" -mtime +7 -delete</code></pre>
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="\$TEST_MONGODB_URI" mongodb_backup_YYYYMMDD/
2024
+ mongorestore --uri="$TEST_MONGODB_URI" mongodb_backup_YYYYMMDD/
2020
2025
 
2021
2026
  # Verify collections
2022
- mongo \$TEST_MONGODB_URI --eval "db.getCollectionNames()"</code></pre>
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, highFindings) {
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) {