verification-layer 0.21.0 → 0.22.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 +251 -615
- package/dist/cli.js +283 -0
- package/dist/cli.js.map +1 -1
- package/dist/reporters/audit-report.d.ts.map +1 -1
- package/dist/reporters/audit-report.js +180 -0
- package/dist/reporters/audit-report.js.map +1 -1
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +2612 -5
- package/dist/reporters/index.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +14 -1
- package/dist/scan.js.map +1 -1
- package/dist/scanners/api-security/index.d.ts +7 -0
- package/dist/scanners/api-security/index.d.ts.map +1 -0
- package/dist/scanners/api-security/index.js +139 -0
- package/dist/scanners/api-security/index.js.map +1 -0
- package/dist/scanners/api-security/index.test.d.ts +5 -0
- package/dist/scanners/api-security/index.test.d.ts.map +1 -0
- package/dist/scanners/api-security/index.test.js +360 -0
- package/dist/scanners/api-security/index.test.js.map +1 -0
- package/dist/scanners/api-security/patterns.d.ts +32 -0
- package/dist/scanners/api-security/patterns.d.ts.map +1 -0
- package/dist/scanners/api-security/patterns.js +159 -0
- package/dist/scanners/api-security/patterns.js.map +1 -0
- package/dist/scanners/authentication/index.d.ts +7 -0
- package/dist/scanners/authentication/index.d.ts.map +1 -0
- package/dist/scanners/authentication/index.js +107 -0
- package/dist/scanners/authentication/index.js.map +1 -0
- package/dist/scanners/authentication/index.test.d.ts +5 -0
- package/dist/scanners/authentication/index.test.d.ts.map +1 -0
- package/dist/scanners/authentication/index.test.js +379 -0
- package/dist/scanners/authentication/index.test.js.map +1 -0
- package/dist/scanners/authentication/patterns.d.ts +32 -0
- package/dist/scanners/authentication/patterns.d.ts.map +1 -0
- package/dist/scanners/authentication/patterns.js +133 -0
- package/dist/scanners/authentication/patterns.js.map +1 -0
- package/dist/scanners/configuration/index.d.ts +8 -0
- package/dist/scanners/configuration/index.d.ts.map +1 -0
- package/dist/scanners/configuration/index.js +87 -0
- package/dist/scanners/configuration/index.js.map +1 -0
- package/dist/scanners/configuration/index.test.d.ts +5 -0
- package/dist/scanners/configuration/index.test.d.ts.map +1 -0
- package/dist/scanners/configuration/index.test.js +344 -0
- package/dist/scanners/configuration/index.test.js.map +1 -0
- package/dist/scanners/configuration/patterns.d.ts +32 -0
- package/dist/scanners/configuration/patterns.d.ts.map +1 -0
- package/dist/scanners/configuration/patterns.js +146 -0
- package/dist/scanners/configuration/patterns.js.map +1 -0
- package/dist/scanners/credentials/index.d.ts +7 -0
- package/dist/scanners/credentials/index.d.ts.map +1 -0
- package/dist/scanners/credentials/index.js +129 -0
- package/dist/scanners/credentials/index.js.map +1 -0
- package/dist/scanners/credentials/index.test.d.ts +5 -0
- package/dist/scanners/credentials/index.test.d.ts.map +1 -0
- package/dist/scanners/credentials/index.test.js +395 -0
- package/dist/scanners/credentials/index.test.js.map +1 -0
- package/dist/scanners/credentials/patterns.d.ts +32 -0
- package/dist/scanners/credentials/patterns.d.ts.map +1 -0
- package/dist/scanners/credentials/patterns.js +140 -0
- package/dist/scanners/credentials/patterns.js.map +1 -0
- package/dist/scanners/errors/index.d.ts +8 -0
- package/dist/scanners/errors/index.d.ts.map +1 -0
- package/dist/scanners/errors/index.js +78 -0
- package/dist/scanners/errors/index.js.map +1 -0
- package/dist/scanners/errors/index.test.d.ts +5 -0
- package/dist/scanners/errors/index.test.d.ts.map +1 -0
- package/dist/scanners/errors/index.test.js +330 -0
- package/dist/scanners/errors/index.test.js.map +1 -0
- package/dist/scanners/errors/patterns.d.ts +27 -0
- package/dist/scanners/errors/patterns.d.ts.map +1 -0
- package/dist/scanners/errors/patterns.js +97 -0
- package/dist/scanners/errors/patterns.js.map +1 -0
- package/dist/scanners/hipaa2026/index.d.ts.map +1 -1
- package/dist/scanners/hipaa2026/index.js +49 -17
- package/dist/scanners/hipaa2026/index.js.map +1 -1
- package/dist/scanners/hipaa2026/index.test.js +26 -15
- package/dist/scanners/hipaa2026/index.test.js.map +1 -1
- package/dist/scanners/operational/index.d.ts +7 -0
- package/dist/scanners/operational/index.d.ts.map +1 -0
- package/dist/scanners/operational/index.js +171 -0
- package/dist/scanners/operational/index.js.map +1 -0
- package/dist/scanners/operational/index.test.d.ts +5 -0
- package/dist/scanners/operational/index.test.d.ts.map +1 -0
- package/dist/scanners/operational/index.test.js +406 -0
- package/dist/scanners/operational/index.test.js.map +1 -0
- package/dist/scanners/operational/patterns.d.ts +33 -0
- package/dist/scanners/operational/patterns.d.ts.map +1 -0
- package/dist/scanners/operational/patterns.js +151 -0
- package/dist/scanners/operational/patterns.js.map +1 -0
- package/dist/scanners/rbac/index.d.ts +7 -0
- package/dist/scanners/rbac/index.d.ts.map +1 -0
- package/dist/scanners/rbac/index.js +145 -0
- package/dist/scanners/rbac/index.js.map +1 -0
- package/dist/scanners/rbac/index.test.d.ts +5 -0
- package/dist/scanners/rbac/index.test.d.ts.map +1 -0
- package/dist/scanners/rbac/index.test.js +422 -0
- package/dist/scanners/rbac/index.test.js.map +1 -0
- package/dist/scanners/rbac/patterns.d.ts +32 -0
- package/dist/scanners/rbac/patterns.d.ts.map +1 -0
- package/dist/scanners/rbac/patterns.js +124 -0
- package/dist/scanners/rbac/patterns.js.map +1 -0
- package/dist/scanners/revocation/index.d.ts +8 -0
- package/dist/scanners/revocation/index.d.ts.map +1 -0
- package/dist/scanners/revocation/index.js +83 -0
- package/dist/scanners/revocation/index.js.map +1 -0
- package/dist/scanners/revocation/index.test.d.ts +5 -0
- package/dist/scanners/revocation/index.test.d.ts.map +1 -0
- package/dist/scanners/revocation/index.test.js +332 -0
- package/dist/scanners/revocation/index.test.js.map +1 -0
- package/dist/scanners/revocation/patterns.d.ts +27 -0
- package/dist/scanners/revocation/patterns.d.ts.map +1 -0
- package/dist/scanners/revocation/patterns.js +109 -0
- package/dist/scanners/revocation/patterns.js.map +1 -0
- package/dist/scanners/sanitization/index.d.ts +8 -0
- package/dist/scanners/sanitization/index.d.ts.map +1 -0
- package/dist/scanners/sanitization/index.js +98 -0
- package/dist/scanners/sanitization/index.js.map +1 -0
- package/dist/scanners/sanitization/index.test.d.ts +5 -0
- package/dist/scanners/sanitization/index.test.d.ts.map +1 -0
- package/dist/scanners/sanitization/index.test.js +370 -0
- package/dist/scanners/sanitization/index.test.js.map +1 -0
- package/dist/scanners/sanitization/patterns.d.ts +27 -0
- package/dist/scanners/sanitization/patterns.d.ts.map +1 -0
- package/dist/scanners/sanitization/patterns.js +117 -0
- package/dist/scanners/sanitization/patterns.js.map +1 -0
- package/dist/training/certificate.d.ts +26 -0
- package/dist/training/certificate.d.ts.map +1 -0
- package/dist/training/certificate.js +92 -0
- package/dist/training/certificate.js.map +1 -0
- package/dist/training/index.d.ts +3 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/index.js +243 -0
- package/dist/training/index.js.map +1 -0
- package/dist/training/modules.d.ts +13 -0
- package/dist/training/modules.d.ts.map +1 -0
- package/dist/training/modules.js +608 -0
- package/dist/training/modules.js.map +1 -0
- package/dist/training/questions.d.ts +9 -0
- package/dist/training/questions.d.ts.map +1 -0
- package/dist/training/questions.js +505 -0
- package/dist/training/questions.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/npm-audit.d.ts +6 -0
- package/dist/utils/npm-audit.d.ts.map +1 -0
- package/dist/utils/npm-audit.js +95 -0
- package/dist/utils/npm-audit.js.map +1 -0
- package/dist/utils/scan-history.d.ts +59 -0
- package/dist/utils/scan-history.d.ts.map +1 -0
- package/dist/utils/scan-history.js +170 -0
- package/dist/utils/scan-history.js.map +1 -0
- package/package.json +4 -1
- package/templates/baa-verification-letter.md +105 -0
- package/templates/irp.md +545 -0
- package/templates/notice-of-privacy-practices.md +491 -0
- package/templates/physical-safeguards-checklist.md +247 -0
- package/templates/security-officer-designation.md +237 -0
package/dist/reporters/index.js
CHANGED
|
@@ -1,8 +1,629 @@
|
|
|
1
|
-
import { writeFile } from 'fs/promises';
|
|
1
|
+
import { writeFile, readFile, readdir } from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { getRemediationGuide } from './remediation-guides.js';
|
|
4
5
|
import { getStackSpecificGuides } from '../stack-detector/stack-guides.js';
|
|
5
|
-
|
|
6
|
+
const PACKAGE_CATEGORIES = {
|
|
7
|
+
// Frameworks
|
|
8
|
+
'next': { category: 'framework', provider: 'Next.js Framework' },
|
|
9
|
+
'react': { category: 'framework', provider: 'React Library' },
|
|
10
|
+
'vue': { category: 'framework', provider: 'Vue.js Framework' },
|
|
11
|
+
'nuxt': { category: 'framework', provider: 'Nuxt Framework' },
|
|
12
|
+
'svelte': { category: 'framework', provider: 'Svelte Framework' },
|
|
13
|
+
'express': { category: 'framework', provider: 'Express.js Framework' },
|
|
14
|
+
'fastify': { category: 'framework', provider: 'Fastify Framework' },
|
|
15
|
+
'hono': { category: 'framework', provider: 'Hono Framework' },
|
|
16
|
+
'elysia': { category: 'framework', provider: 'Elysia Framework' },
|
|
17
|
+
'nestjs': { category: 'framework', provider: 'NestJS Framework' },
|
|
18
|
+
// Databases & ORMs
|
|
19
|
+
'@prisma/client': { category: 'database', provider: 'Prisma ORM' },
|
|
20
|
+
'prisma': { category: 'database', provider: 'Prisma ORM' },
|
|
21
|
+
'mongoose': { category: 'database', provider: 'Mongoose ODM (MongoDB)' },
|
|
22
|
+
'drizzle-orm': { category: 'database', provider: 'Drizzle ORM' },
|
|
23
|
+
'typeorm': { category: 'database', provider: 'TypeORM' },
|
|
24
|
+
'sequelize': { category: 'database', provider: 'Sequelize ORM' },
|
|
25
|
+
'knex': { category: 'database', provider: 'Knex.js Query Builder' },
|
|
26
|
+
'pg': { category: 'database', provider: 'PostgreSQL Driver' },
|
|
27
|
+
'mysql2': { category: 'database', provider: 'MySQL Driver' },
|
|
28
|
+
'mongodb': { category: 'database', provider: 'MongoDB Driver' },
|
|
29
|
+
'redis': { category: 'database', provider: 'Redis Client' },
|
|
30
|
+
// Authentication
|
|
31
|
+
'next-auth': { category: 'auth', provider: 'NextAuth.js' },
|
|
32
|
+
'@auth/core': { category: 'auth', provider: 'Auth.js' },
|
|
33
|
+
'passport': { category: 'auth', provider: 'Passport.js' },
|
|
34
|
+
'jsonwebtoken': { category: 'auth', provider: 'JWT (JSON Web Tokens)' },
|
|
35
|
+
'bcrypt': { category: 'auth', provider: 'bcrypt Password Hashing' },
|
|
36
|
+
'argon2': { category: 'auth', provider: 'Argon2 Password Hashing' },
|
|
37
|
+
'@clerk/nextjs': { category: 'auth', provider: 'Clerk Authentication' },
|
|
38
|
+
'@supabase/auth-helpers': { category: 'auth', provider: 'Supabase Auth' },
|
|
39
|
+
'lucia': { category: 'auth', provider: 'Lucia Auth' },
|
|
40
|
+
// Cloud / BaaS
|
|
41
|
+
'@supabase/supabase-js': { category: 'cloud', provider: 'Supabase BaaS' },
|
|
42
|
+
'@aws-sdk': { category: 'cloud', provider: 'AWS SDK' },
|
|
43
|
+
'aws-sdk': { category: 'cloud', provider: 'AWS SDK' },
|
|
44
|
+
'@google-cloud': { category: 'cloud', provider: 'Google Cloud SDK' },
|
|
45
|
+
'@azure': { category: 'cloud', provider: 'Azure SDK' },
|
|
46
|
+
'firebase': { category: 'cloud', provider: 'Firebase' },
|
|
47
|
+
'firebase-admin': { category: 'cloud', provider: 'Firebase Admin' },
|
|
48
|
+
'@vercel/analytics': { category: 'cloud', provider: 'Vercel Analytics' },
|
|
49
|
+
'@vercel/edge': { category: 'cloud', provider: 'Vercel Edge Functions' },
|
|
50
|
+
// Payment
|
|
51
|
+
'stripe': { category: 'payment', provider: 'Stripe Payments' },
|
|
52
|
+
'@stripe/stripe-js': { category: 'payment', provider: 'Stripe.js' },
|
|
53
|
+
'paypal': { category: 'payment', provider: 'PayPal SDK' },
|
|
54
|
+
// Communication
|
|
55
|
+
'@sendgrid/mail': { category: 'communication', provider: 'SendGrid Email' },
|
|
56
|
+
'nodemailer': { category: 'communication', provider: 'Nodemailer Email' },
|
|
57
|
+
'twilio': { category: 'communication', provider: 'Twilio SMS/Voice' },
|
|
58
|
+
'@twilio/conversations': { category: 'communication', provider: 'Twilio Conversations' },
|
|
59
|
+
'resend': { category: 'communication', provider: 'Resend Email' },
|
|
60
|
+
// Utility (commonly used)
|
|
61
|
+
'axios': { category: 'utility', provider: 'Axios HTTP Client' },
|
|
62
|
+
'lodash': { category: 'utility', provider: 'Lodash Utility Library' },
|
|
63
|
+
'date-fns': { category: 'utility', provider: 'date-fns Date Utility' },
|
|
64
|
+
'dayjs': { category: 'utility', provider: 'Day.js Date Library' },
|
|
65
|
+
'zod': { category: 'utility', provider: 'Zod Validation' },
|
|
66
|
+
'joi': { category: 'utility', provider: 'Joi Validation' },
|
|
67
|
+
'yup': { category: 'utility', provider: 'Yup Validation' },
|
|
68
|
+
};
|
|
69
|
+
function categorizePackage(packageName) {
|
|
70
|
+
// Check exact match
|
|
71
|
+
if (PACKAGE_CATEGORIES[packageName]) {
|
|
72
|
+
return PACKAGE_CATEGORIES[packageName];
|
|
73
|
+
}
|
|
74
|
+
// Check prefix match (for scoped packages)
|
|
75
|
+
for (const [key, value] of Object.entries(PACKAGE_CATEGORIES)) {
|
|
76
|
+
if (packageName.startsWith(key)) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Default to utility/other
|
|
81
|
+
return { category: 'other', provider: packageName };
|
|
82
|
+
}
|
|
83
|
+
async function generateAssetInventory(targetPath) {
|
|
84
|
+
const assets = [];
|
|
85
|
+
try {
|
|
86
|
+
// Read package.json
|
|
87
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
88
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
|
89
|
+
const dependencies = {
|
|
90
|
+
...packageJson.dependencies,
|
|
91
|
+
...packageJson.devDependencies,
|
|
92
|
+
};
|
|
93
|
+
let assetId = 1;
|
|
94
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
95
|
+
const { category, provider } = categorizePackage(name);
|
|
96
|
+
// Skip dev-only packages that aren't relevant for asset inventory
|
|
97
|
+
if (name.startsWith('@types/') ||
|
|
98
|
+
name === 'typescript' ||
|
|
99
|
+
name === 'eslint' ||
|
|
100
|
+
name.startsWith('eslint-') ||
|
|
101
|
+
name === 'prettier' ||
|
|
102
|
+
name.startsWith('@testing-library/') ||
|
|
103
|
+
name === 'vitest' ||
|
|
104
|
+
name === 'jest') {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
assets.push({
|
|
108
|
+
id: `TECH-${String(assetId).padStart(3, '0')}`,
|
|
109
|
+
name,
|
|
110
|
+
version: String(version).replace(/[\^~]/g, ''),
|
|
111
|
+
type: 'software',
|
|
112
|
+
category: category,
|
|
113
|
+
provider,
|
|
114
|
+
responsiblePerson: '', // To be filled by client
|
|
115
|
+
location: '', // To be filled by client
|
|
116
|
+
});
|
|
117
|
+
assetId++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// If package.json doesn't exist or can't be read, return empty inventory
|
|
122
|
+
console.warn('Could not read package.json for asset inventory:', error);
|
|
123
|
+
}
|
|
124
|
+
// Sort by category, then by name
|
|
125
|
+
assets.sort((a, b) => {
|
|
126
|
+
if (a.category !== b.category) {
|
|
127
|
+
return a.category.localeCompare(b.category);
|
|
128
|
+
}
|
|
129
|
+
return a.name.localeCompare(b.name);
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
assets,
|
|
133
|
+
detectedAt: new Date().toISOString(),
|
|
134
|
+
totalAssets: assets.length,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
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
|
+
async function analyzeDataFlow(targetPath, findings) {
|
|
154
|
+
const entryPoints = [];
|
|
155
|
+
const dataStores = [];
|
|
156
|
+
const externalServices = [];
|
|
157
|
+
const authProviders = [];
|
|
158
|
+
try {
|
|
159
|
+
// Read package.json to detect dependencies
|
|
160
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
161
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
|
162
|
+
const dependencies = {
|
|
163
|
+
...packageJson.dependencies,
|
|
164
|
+
...packageJson.devDependencies,
|
|
165
|
+
};
|
|
166
|
+
// Detect data stores
|
|
167
|
+
const dbPackages = [
|
|
168
|
+
{ pattern: '@prisma/client', name: 'Prisma ORM', id: 'prisma' },
|
|
169
|
+
{ pattern: 'prisma', name: 'Prisma', id: 'prisma' },
|
|
170
|
+
{ pattern: '@supabase/supabase-js', name: 'Supabase', id: 'supabase' },
|
|
171
|
+
{ pattern: 'mongoose', name: 'MongoDB (Mongoose)', id: 'mongodb' },
|
|
172
|
+
{ pattern: 'mongodb', name: 'MongoDB', id: 'mongodb' },
|
|
173
|
+
{ pattern: 'pg', name: 'PostgreSQL', id: 'postgresql' },
|
|
174
|
+
{ pattern: 'mysql2', name: 'MySQL', id: 'mysql' },
|
|
175
|
+
{ pattern: 'redis', name: 'Redis', id: 'redis' },
|
|
176
|
+
{ pattern: 'drizzle-orm', name: 'Drizzle ORM', id: 'drizzle' },
|
|
177
|
+
];
|
|
178
|
+
for (const pkg of dbPackages) {
|
|
179
|
+
if (dependencies[pkg.pattern]) {
|
|
180
|
+
const hasIssues = findings.some(f => f.file.toLowerCase().includes(pkg.id) ||
|
|
181
|
+
f.category === 'encryption' ||
|
|
182
|
+
f.category === 'data-retention');
|
|
183
|
+
dataStores.push({
|
|
184
|
+
id: pkg.id,
|
|
185
|
+
name: pkg.name,
|
|
186
|
+
type: 'data-store',
|
|
187
|
+
hasIssues,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Detect auth providers
|
|
192
|
+
const authPackages = [
|
|
193
|
+
{ pattern: 'next-auth', name: 'NextAuth.js', id: 'nextauth' },
|
|
194
|
+
{ pattern: '@clerk/nextjs', name: 'Clerk', id: 'clerk' },
|
|
195
|
+
{ pattern: '@auth0/nextjs-auth0', name: 'Auth0', id: 'auth0' },
|
|
196
|
+
{ pattern: 'passport', name: 'Passport.js', id: 'passport' },
|
|
197
|
+
{ pattern: '@supabase/auth-helpers', name: 'Supabase Auth', id: 'supabase-auth' },
|
|
198
|
+
{ pattern: 'lucia', name: 'Lucia Auth', id: 'lucia' },
|
|
199
|
+
];
|
|
200
|
+
for (const pkg of authPackages) {
|
|
201
|
+
if (dependencies[pkg.pattern]) {
|
|
202
|
+
const hasIssues = findings.some(f => f.category === 'access-control' ||
|
|
203
|
+
f.category === 'audit-logging');
|
|
204
|
+
authProviders.push({
|
|
205
|
+
id: pkg.id,
|
|
206
|
+
name: pkg.name,
|
|
207
|
+
type: 'auth-provider',
|
|
208
|
+
hasIssues,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Detect external services
|
|
213
|
+
const externalPackages = [
|
|
214
|
+
{ pattern: 'stripe', name: 'Stripe (Payments)', id: 'stripe' },
|
|
215
|
+
{ pattern: '@sendgrid/mail', name: 'SendGrid (Email)', id: 'sendgrid' },
|
|
216
|
+
{ pattern: 'nodemailer', name: 'Nodemailer (Email)', id: 'nodemailer' },
|
|
217
|
+
{ pattern: 'twilio', name: 'Twilio (SMS)', id: 'twilio' },
|
|
218
|
+
{ pattern: 'resend', name: 'Resend (Email)', id: 'resend' },
|
|
219
|
+
{ pattern: '@aws-sdk', name: 'AWS Services', id: 'aws' },
|
|
220
|
+
{ pattern: 'aws-sdk', name: 'AWS Services', id: 'aws' },
|
|
221
|
+
{ pattern: '@google-cloud', name: 'Google Cloud', id: 'gcp' },
|
|
222
|
+
{ pattern: 'firebase', name: 'Firebase', id: 'firebase' },
|
|
223
|
+
];
|
|
224
|
+
for (const pkg of externalPackages) {
|
|
225
|
+
if (Object.keys(dependencies).some(dep => dep.includes(pkg.pattern))) {
|
|
226
|
+
const hasIssues = findings.some(f => f.category === 'encryption' ||
|
|
227
|
+
f.title.toLowerCase().includes('api') ||
|
|
228
|
+
f.title.toLowerCase().includes('external'));
|
|
229
|
+
externalServices.push({
|
|
230
|
+
id: pkg.id,
|
|
231
|
+
name: pkg.name,
|
|
232
|
+
type: 'external-service',
|
|
233
|
+
hasIssues,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Detect entry points (API routes)
|
|
238
|
+
// Look for common API route patterns
|
|
239
|
+
const apiPatterns = [
|
|
240
|
+
'pages/api',
|
|
241
|
+
'app/api',
|
|
242
|
+
'src/pages/api',
|
|
243
|
+
'src/app/api',
|
|
244
|
+
];
|
|
245
|
+
let apiRouteCount = 0;
|
|
246
|
+
for (const pattern of apiPatterns) {
|
|
247
|
+
try {
|
|
248
|
+
const apiPath = path.join(targetPath, pattern);
|
|
249
|
+
const files = await readdir(apiPath, { recursive: true });
|
|
250
|
+
apiRouteCount += files.filter(f => typeof f === 'string' && (f.endsWith('.ts') || f.endsWith('.js'))).length;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Directory doesn't exist, continue
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (apiRouteCount > 0) {
|
|
257
|
+
const hasIssues = findings.some(f => f.file.includes('/api/') ||
|
|
258
|
+
f.category === 'access-control' ||
|
|
259
|
+
f.category === 'phi-exposure');
|
|
260
|
+
entryPoints.push({
|
|
261
|
+
id: 'api-routes',
|
|
262
|
+
name: `API Routes (${apiRouteCount} detected)`,
|
|
263
|
+
type: 'entry-point',
|
|
264
|
+
hasIssues,
|
|
265
|
+
details: `${apiRouteCount} API route files found`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Generic API endpoint if framework detected
|
|
270
|
+
if (dependencies['express'] || dependencies['fastify'] || dependencies['next']) {
|
|
271
|
+
const hasIssues = findings.some(f => f.category === 'access-control' || f.category === 'phi-exposure');
|
|
272
|
+
entryPoints.push({
|
|
273
|
+
id: 'api-endpoints',
|
|
274
|
+
name: 'API Endpoints',
|
|
275
|
+
type: 'entry-point',
|
|
276
|
+
hasIssues,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.warn('Could not analyze data flow:', error);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
entryPoints,
|
|
286
|
+
dataStores,
|
|
287
|
+
externalServices,
|
|
288
|
+
authProviders,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function generateMermaidDiagram(dataFlow) {
|
|
292
|
+
const lines = [];
|
|
293
|
+
lines.push('flowchart TD');
|
|
294
|
+
// Define nodes
|
|
295
|
+
lines.push(' User[👤 User/Patient]');
|
|
296
|
+
lines.push(' Frontend[🖥️ Frontend Application]');
|
|
297
|
+
// Entry points
|
|
298
|
+
if (dataFlow.entryPoints.length > 0) {
|
|
299
|
+
dataFlow.entryPoints.forEach(ep => {
|
|
300
|
+
const icon = '🔌';
|
|
301
|
+
lines.push(` ${ep.id}[${icon} ${ep.name}]`);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
lines.push(' api[🔌 API Layer]');
|
|
306
|
+
}
|
|
307
|
+
// Auth providers
|
|
308
|
+
if (dataFlow.authProviders.length > 0) {
|
|
309
|
+
dataFlow.authProviders.forEach(auth => {
|
|
310
|
+
const icon = '🔐';
|
|
311
|
+
lines.push(` ${auth.id}[${icon} ${auth.name}]`);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// Data stores
|
|
315
|
+
if (dataFlow.dataStores.length > 0) {
|
|
316
|
+
dataFlow.dataStores.forEach(ds => {
|
|
317
|
+
const icon = '🗄️';
|
|
318
|
+
lines.push(` ${ds.id}[(${icon} ${ds.name})]`);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
lines.push(' db[(🗄️ Database)]');
|
|
323
|
+
}
|
|
324
|
+
// External services
|
|
325
|
+
dataFlow.externalServices.forEach(svc => {
|
|
326
|
+
const icon = svc.name.includes('Payment') || svc.name.includes('Stripe') ? '💳' :
|
|
327
|
+
svc.name.includes('Email') || svc.name.includes('Mail') ? '📧' :
|
|
328
|
+
svc.name.includes('SMS') || svc.name.includes('Twilio') ? '📱' :
|
|
329
|
+
'☁️';
|
|
330
|
+
lines.push(` ${svc.id}[${icon} ${svc.name}]`);
|
|
331
|
+
});
|
|
332
|
+
// Define connections
|
|
333
|
+
lines.push('');
|
|
334
|
+
lines.push(' %% Main data flow');
|
|
335
|
+
lines.push(' User --> Frontend');
|
|
336
|
+
if (dataFlow.entryPoints.length > 0) {
|
|
337
|
+
lines.push(' Frontend --> ' + dataFlow.entryPoints[0].id);
|
|
338
|
+
// Auth flow
|
|
339
|
+
if (dataFlow.authProviders.length > 0) {
|
|
340
|
+
lines.push(' ' + dataFlow.entryPoints[0].id + ' --> ' + dataFlow.authProviders[0].id);
|
|
341
|
+
}
|
|
342
|
+
// Database flow
|
|
343
|
+
if (dataFlow.dataStores.length > 0) {
|
|
344
|
+
lines.push(' ' + dataFlow.entryPoints[0].id + ' --> ' + dataFlow.dataStores[0].id);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
lines.push(' ' + dataFlow.entryPoints[0].id + ' --> db');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
lines.push(' Frontend --> api');
|
|
352
|
+
lines.push(' api --> db');
|
|
353
|
+
}
|
|
354
|
+
// External services connections
|
|
355
|
+
dataFlow.externalServices.forEach(svc => {
|
|
356
|
+
const source = dataFlow.entryPoints.length > 0 ? dataFlow.entryPoints[0].id : 'api';
|
|
357
|
+
lines.push(' ' + source + ' --> ' + svc.id);
|
|
358
|
+
});
|
|
359
|
+
// Apply styling for components with issues
|
|
360
|
+
lines.push('');
|
|
361
|
+
lines.push(' %% Styling');
|
|
362
|
+
lines.push(' classDef issueNode fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#991b1b');
|
|
363
|
+
lines.push(' classDef normalNode fill:#dbeafe,stroke:#3b82f6,stroke-width:2px');
|
|
364
|
+
lines.push(' classDef externalNode fill:#fef3c7,stroke:#f59e0b,stroke-width:2px');
|
|
365
|
+
// Apply classes
|
|
366
|
+
const issueNodes = [];
|
|
367
|
+
const normalNodes = [];
|
|
368
|
+
const externalNodes = [];
|
|
369
|
+
[...dataFlow.entryPoints, ...dataFlow.authProviders, ...dataFlow.dataStores].forEach(component => {
|
|
370
|
+
if (component.hasIssues) {
|
|
371
|
+
issueNodes.push(component.id);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
normalNodes.push(component.id);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
dataFlow.externalServices.forEach(svc => {
|
|
378
|
+
if (svc.hasIssues) {
|
|
379
|
+
issueNodes.push(svc.id);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
externalNodes.push(svc.id);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
if (issueNodes.length > 0) {
|
|
386
|
+
lines.push(' class ' + issueNodes.join(',') + ' issueNode');
|
|
387
|
+
}
|
|
388
|
+
if (normalNodes.length > 0) {
|
|
389
|
+
lines.push(' class ' + normalNodes.join(',') + ' normalNode');
|
|
390
|
+
}
|
|
391
|
+
if (externalNodes.length > 0) {
|
|
392
|
+
lines.push(' class ' + externalNodes.join(',') + ' externalNode');
|
|
393
|
+
}
|
|
394
|
+
return lines.join('\n');
|
|
395
|
+
}
|
|
396
|
+
async function renderDataFlowMapHtml(targetPath, findings) {
|
|
397
|
+
const dataFlow = await analyzeDataFlow(targetPath, findings);
|
|
398
|
+
const mermaidCode = generateMermaidDiagram(dataFlow);
|
|
399
|
+
const totalComponents = dataFlow.entryPoints.length + dataFlow.dataStores.length +
|
|
400
|
+
dataFlow.externalServices.length + dataFlow.authProviders.length;
|
|
401
|
+
const componentsWithIssues = [
|
|
402
|
+
...dataFlow.entryPoints,
|
|
403
|
+
...dataFlow.dataStores,
|
|
404
|
+
...dataFlow.externalServices,
|
|
405
|
+
...dataFlow.authProviders,
|
|
406
|
+
].filter(c => c.hasIssues).length;
|
|
407
|
+
return `
|
|
408
|
+
<div class="data-flow-map-section">
|
|
409
|
+
<div class="flow-header">
|
|
410
|
+
<h2>🔄 ePHI Data Flow Map</h2>
|
|
411
|
+
<p class="flow-subtitle">
|
|
412
|
+
Visual representation of Protected Health Information (ePHI) data flow through your application
|
|
413
|
+
</p>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div class="flow-stats">
|
|
417
|
+
<div class="flow-stat-box">
|
|
418
|
+
<div class="flow-stat-value">${totalComponents}</div>
|
|
419
|
+
<div class="flow-stat-label">Components</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="flow-stat-box">
|
|
422
|
+
<div class="flow-stat-value">${dataFlow.entryPoints.length}</div>
|
|
423
|
+
<div class="flow-stat-label">Entry Points</div>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="flow-stat-box">
|
|
426
|
+
<div class="flow-stat-value">${dataFlow.dataStores.length}</div>
|
|
427
|
+
<div class="flow-stat-label">Data Stores</div>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="flow-stat-box ${componentsWithIssues > 0 ? 'flow-stat-warning' : ''}">
|
|
430
|
+
<div class="flow-stat-value">${componentsWithIssues}</div>
|
|
431
|
+
<div class="flow-stat-label">With Issues</div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div class="mermaid-container">
|
|
436
|
+
<pre class="mermaid">
|
|
437
|
+
${mermaidCode}
|
|
438
|
+
</pre>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div class="flow-legend">
|
|
442
|
+
<h3>Legend</h3>
|
|
443
|
+
<div class="legend-items">
|
|
444
|
+
<div class="legend-item">
|
|
445
|
+
<span class="legend-box legend-normal"></span>
|
|
446
|
+
<span>Secure Component</span>
|
|
447
|
+
</div>
|
|
448
|
+
<div class="legend-item">
|
|
449
|
+
<span class="legend-box legend-issue"></span>
|
|
450
|
+
<span>Component with Security Issues</span>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="legend-item">
|
|
453
|
+
<span class="legend-box legend-external"></span>
|
|
454
|
+
<span>External Service (requires BAA)</span>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div class="flow-notice">
|
|
460
|
+
<strong>⚠️ Important:</strong> Este diagrama muestra el flujo de datos detectado en el código fuente.
|
|
461
|
+
Debe complementarse con documentación de infraestructura de red, VPNs, firewalls, segmentación de red,
|
|
462
|
+
y otros controles de seguridad no visibles en el análisis de código estático.
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
<div class="flow-components">
|
|
466
|
+
<h3>Detected Components</h3>
|
|
467
|
+
|
|
468
|
+
${dataFlow.entryPoints.length > 0 ? `
|
|
469
|
+
<div class="component-group">
|
|
470
|
+
<h4>🔌 Entry Points</h4>
|
|
471
|
+
<ul>
|
|
472
|
+
${dataFlow.entryPoints.map(ep => `
|
|
473
|
+
<li class="${ep.hasIssues ? 'component-with-issues' : ''}">
|
|
474
|
+
${escapeHtml(ep.name)}
|
|
475
|
+
${ep.hasIssues ? '<span class="issue-badge">⚠️ Has Issues</span>' : ''}
|
|
476
|
+
</li>
|
|
477
|
+
`).join('')}
|
|
478
|
+
</ul>
|
|
479
|
+
</div>
|
|
480
|
+
` : ''}
|
|
481
|
+
|
|
482
|
+
${dataFlow.authProviders.length > 0 ? `
|
|
483
|
+
<div class="component-group">
|
|
484
|
+
<h4>🔐 Authentication Providers</h4>
|
|
485
|
+
<ul>
|
|
486
|
+
${dataFlow.authProviders.map(auth => `
|
|
487
|
+
<li class="${auth.hasIssues ? 'component-with-issues' : ''}">
|
|
488
|
+
${escapeHtml(auth.name)}
|
|
489
|
+
${auth.hasIssues ? '<span class="issue-badge">⚠️ Has Issues</span>' : ''}
|
|
490
|
+
</li>
|
|
491
|
+
`).join('')}
|
|
492
|
+
</ul>
|
|
493
|
+
</div>
|
|
494
|
+
` : ''}
|
|
495
|
+
|
|
496
|
+
${dataFlow.dataStores.length > 0 ? `
|
|
497
|
+
<div class="component-group">
|
|
498
|
+
<h4>🗄️ Data Stores</h4>
|
|
499
|
+
<ul>
|
|
500
|
+
${dataFlow.dataStores.map(ds => `
|
|
501
|
+
<li class="${ds.hasIssues ? 'component-with-issues' : ''}">
|
|
502
|
+
${escapeHtml(ds.name)}
|
|
503
|
+
${ds.hasIssues ? '<span class="issue-badge">⚠️ Has Issues</span>' : ''}
|
|
504
|
+
</li>
|
|
505
|
+
`).join('')}
|
|
506
|
+
</ul>
|
|
507
|
+
</div>
|
|
508
|
+
` : ''}
|
|
509
|
+
|
|
510
|
+
${dataFlow.externalServices.length > 0 ? `
|
|
511
|
+
<div class="component-group">
|
|
512
|
+
<h4>☁️ External Services (BAA Required)</h4>
|
|
513
|
+
<ul>
|
|
514
|
+
${dataFlow.externalServices.map(svc => `
|
|
515
|
+
<li class="${svc.hasIssues ? 'component-with-issues' : ''}">
|
|
516
|
+
${escapeHtml(svc.name)}
|
|
517
|
+
<span class="baa-badge">BAA</span>
|
|
518
|
+
${svc.hasIssues ? '<span class="issue-badge">⚠️ Has Issues</span>' : ''}
|
|
519
|
+
</li>
|
|
520
|
+
`).join('')}
|
|
521
|
+
</ul>
|
|
522
|
+
</div>
|
|
523
|
+
` : ''}
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
function calculateComplianceScore(findings) {
|
|
529
|
+
// Count by severity
|
|
530
|
+
const criticals = findings.filter(f => f.severity === 'critical').length;
|
|
531
|
+
const highs = findings.filter(f => f.severity === 'high').length;
|
|
532
|
+
const mediums = findings.filter(f => f.severity === 'medium').length;
|
|
533
|
+
const lows = findings.filter(f => f.severity === 'low').length;
|
|
534
|
+
// Calculate overall score
|
|
535
|
+
const deductions = (criticals * 15) + (highs * 8) + (mediums * 3) + (lows * 1);
|
|
536
|
+
const overall = Math.max(0, Math.min(100, 100 - deductions));
|
|
537
|
+
// Determine grade and color
|
|
538
|
+
let grade;
|
|
539
|
+
let color;
|
|
540
|
+
if (overall >= 95) {
|
|
541
|
+
grade = 'A+';
|
|
542
|
+
color = '#10b981'; // green
|
|
543
|
+
}
|
|
544
|
+
else if (overall >= 80) {
|
|
545
|
+
grade = 'A';
|
|
546
|
+
color = '#10b981'; // green
|
|
547
|
+
}
|
|
548
|
+
else if (overall >= 60) {
|
|
549
|
+
grade = 'B';
|
|
550
|
+
color = '#eab308'; // yellow
|
|
551
|
+
}
|
|
552
|
+
else if (overall >= 40) {
|
|
553
|
+
grade = 'C';
|
|
554
|
+
color = '#f97316'; // orange
|
|
555
|
+
}
|
|
556
|
+
else if (overall >= 20) {
|
|
557
|
+
grade = 'D';
|
|
558
|
+
color = '#ef4444'; // red
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
grade = 'F';
|
|
562
|
+
color = '#dc2626'; // dark red
|
|
563
|
+
}
|
|
564
|
+
// Calculate by category
|
|
565
|
+
const categoryMap = {
|
|
566
|
+
'phi-exposure': 'PHI Protection',
|
|
567
|
+
'encryption': 'Encryption',
|
|
568
|
+
'audit-logging': 'Audit & Logging',
|
|
569
|
+
'access-control': 'Access Control',
|
|
570
|
+
'data-retention': 'Data Retention',
|
|
571
|
+
};
|
|
572
|
+
const byCategory = {};
|
|
573
|
+
for (const [key, label] of Object.entries(categoryMap)) {
|
|
574
|
+
const categoryFindings = findings.filter(f => f.category === key);
|
|
575
|
+
const catCriticals = categoryFindings.filter(f => f.severity === 'critical').length;
|
|
576
|
+
const catHighs = categoryFindings.filter(f => f.severity === 'high').length;
|
|
577
|
+
const catMediums = categoryFindings.filter(f => f.severity === 'medium').length;
|
|
578
|
+
const catLows = categoryFindings.filter(f => f.severity === 'low').length;
|
|
579
|
+
const catDeductions = (catCriticals * 15) + (catHighs * 8) + (catMediums * 3) + (catLows * 1);
|
|
580
|
+
const catScore = Math.max(0, Math.min(100, 100 - catDeductions));
|
|
581
|
+
byCategory[label] = {
|
|
582
|
+
score: catScore,
|
|
583
|
+
findings: categoryFindings.length,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
return { overall, grade, color, byCategory };
|
|
587
|
+
}
|
|
588
|
+
async function getScoreTrending(targetPath, currentScore) {
|
|
589
|
+
try {
|
|
590
|
+
const historyDir = path.join(targetPath, '.vlayer', 'history');
|
|
591
|
+
const files = await readdir(historyDir);
|
|
592
|
+
// Find the most recent history file (exclude current)
|
|
593
|
+
const historyFiles = files
|
|
594
|
+
.filter(f => f.endsWith('.json'))
|
|
595
|
+
.sort()
|
|
596
|
+
.reverse();
|
|
597
|
+
if (historyFiles.length === 0) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
// Read the most recent previous scan
|
|
601
|
+
const previousFile = path.join(historyDir, historyFiles[0]);
|
|
602
|
+
const content = await readFile(previousFile, 'utf-8');
|
|
603
|
+
const previousReport = JSON.parse(content);
|
|
604
|
+
if (!previousReport.complianceScore) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const previousScore = previousReport.complianceScore.overall;
|
|
608
|
+
const change = currentScore - previousScore;
|
|
609
|
+
let direction;
|
|
610
|
+
if (Math.abs(change) < 1) {
|
|
611
|
+
direction = 'same';
|
|
612
|
+
}
|
|
613
|
+
else if (change > 0) {
|
|
614
|
+
direction = 'up';
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
direction = 'down';
|
|
618
|
+
}
|
|
619
|
+
return { direction, previousScore, change };
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
// No history available
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function buildReport(result, targetPath, vulnerabilities) {
|
|
6
627
|
const acknowledged = result.findings.filter(f => f.acknowledged && !f.acknowledgment?.expired);
|
|
7
628
|
const suppressed = result.findings.filter(f => f.suppressed);
|
|
8
629
|
const baseline = result.findings.filter(f => f.isBaseline);
|
|
@@ -20,6 +641,16 @@ function buildReport(result, targetPath) {
|
|
|
20
641
|
low: newFindings.filter(f => f.severity === 'low').length,
|
|
21
642
|
info: newFindings.filter(f => f.severity === 'info').length,
|
|
22
643
|
};
|
|
644
|
+
// Add vulnerability summary if present
|
|
645
|
+
if (vulnerabilities && vulnerabilities.length > 0) {
|
|
646
|
+
summary.vulnerabilities = {
|
|
647
|
+
total: vulnerabilities.length,
|
|
648
|
+
critical: vulnerabilities.filter(v => v.severity === 'critical').length,
|
|
649
|
+
high: vulnerabilities.filter(v => v.severity === 'high').length,
|
|
650
|
+
moderate: vulnerabilities.filter(v => v.severity === 'moderate').length,
|
|
651
|
+
low: vulnerabilities.filter(v => v.severity === 'low').length,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
23
654
|
return {
|
|
24
655
|
timestamp: new Date().toISOString(),
|
|
25
656
|
targetPath,
|
|
@@ -28,6 +659,7 @@ function buildReport(result, targetPath) {
|
|
|
28
659
|
scannedFiles: result.scannedFiles,
|
|
29
660
|
scanDuration: result.scanDuration,
|
|
30
661
|
stack: result.stack,
|
|
662
|
+
vulnerabilities,
|
|
31
663
|
};
|
|
32
664
|
}
|
|
33
665
|
function generateJson(report) {
|
|
@@ -64,6 +696,19 @@ function generateMarkdown(report) {
|
|
|
64
696
|
`| **Total** | **${report.summary.total}** |`,
|
|
65
697
|
'',
|
|
66
698
|
];
|
|
699
|
+
// Add vulnerability summary if present
|
|
700
|
+
if (report.vulnerabilities && report.vulnerabilities.length > 0) {
|
|
701
|
+
lines.push('## Dependency Vulnerabilities', '', `> Security vulnerabilities detected in project dependencies via \`npm audit\``, '', '### Summary', '', '| Severity | Count |', '|----------|-------|', `| Critical | ${report.summary.vulnerabilities?.critical || 0} |`, `| High | ${report.summary.vulnerabilities?.high || 0} |`, `| Moderate | ${report.summary.vulnerabilities?.moderate || 0} |`, `| Low | ${report.summary.vulnerabilities?.low || 0} |`, `| **Total** | **${report.vulnerabilities.length}** |`, '', '### Affected Packages', '');
|
|
702
|
+
for (const vuln of report.vulnerabilities) {
|
|
703
|
+
const fixInfo = vuln.fixAvailable
|
|
704
|
+
? typeof vuln.fixAvailable === 'object'
|
|
705
|
+
? `✅ Fix: ${vuln.fixAvailable.name}@${vuln.fixAvailable.version}`
|
|
706
|
+
: '✅ Fix Available'
|
|
707
|
+
: '⚠️ No Fix Yet';
|
|
708
|
+
lines.push(`#### ${vuln.severity.toUpperCase()}: \`${vuln.name}\``, '', `- **Vulnerability:** ${vuln.via}`, `- **Affected Range:** \`${vuln.range}\``, `- **Status:** ${fixInfo}`, vuln.url ? `- **Advisory:** ${vuln.url}` : '', '');
|
|
709
|
+
}
|
|
710
|
+
lines.push('---', '');
|
|
711
|
+
}
|
|
67
712
|
if (report.findings.length > 0) {
|
|
68
713
|
lines.push('## Findings', '');
|
|
69
714
|
const groupedByCategory = report.findings.reduce((acc, f) => {
|
|
@@ -213,7 +858,10 @@ function renderStackSection(stack) {
|
|
|
213
858
|
</div>
|
|
214
859
|
`;
|
|
215
860
|
}
|
|
216
|
-
function generateHtml(report) {
|
|
861
|
+
async function generateHtml(report, targetPath, options) {
|
|
862
|
+
const complianceScoreHtml = await renderComplianceScoreHtml(report, targetPath);
|
|
863
|
+
const assetInventoryHtml = await renderAssetInventoryHtml(targetPath);
|
|
864
|
+
const dataFlowMapHtml = await renderDataFlowMapHtml(targetPath, report.findings);
|
|
217
865
|
const severityColors = {
|
|
218
866
|
critical: '#dc2626',
|
|
219
867
|
high: '#ea580c',
|
|
@@ -227,10 +875,73 @@ function generateHtml(report) {
|
|
|
227
875
|
<meta charset="UTF-8">
|
|
228
876
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
229
877
|
<title>HIPAA Compliance Report - vlayer</title>
|
|
878
|
+
<script type="module">
|
|
879
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
880
|
+
mermaid.initialize({
|
|
881
|
+
startOnLoad: true,
|
|
882
|
+
theme: 'default',
|
|
883
|
+
flowchart: {
|
|
884
|
+
useMaxWidth: true,
|
|
885
|
+
htmlLabels: true,
|
|
886
|
+
curve: 'basis'
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
</script>
|
|
230
890
|
<style>
|
|
231
891
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
232
892
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; background: #f9fafb; padding: 2rem; }
|
|
233
893
|
.container { max-width: 1400px; margin: 0 auto; }
|
|
894
|
+
|
|
895
|
+
/* Executive Summary Styles */
|
|
896
|
+
.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); }
|
|
897
|
+
.exec-header h1 { color: #111827; font-size: 2.5rem; margin: 0 0 0.5rem 0; }
|
|
898
|
+
.exec-subtitle { color: #6b7280; font-size: 1.1rem; margin: 0 0 1rem 0; }
|
|
899
|
+
.exec-meta { color: #9ca3af; font-size: 0.9rem; }
|
|
900
|
+
.exec-meta span { margin: 0 0.5rem; }
|
|
901
|
+
.exec-meta span:first-child { margin-left: 0; }
|
|
902
|
+
.exec-status-card { display: flex; align-items: center; gap: 1.5rem; padding: 2rem; margin: 2rem 0; background: #f9fafb; border-radius: 12px; border-left: 6px solid; }
|
|
903
|
+
.exec-status-icon { font-size: 3rem; line-height: 1; }
|
|
904
|
+
.exec-status-content h2 { margin: 0 0 0.5rem 0; font-size: 1.5rem; }
|
|
905
|
+
.exec-status-content p { margin: 0; color: #4b5563; font-size: 1rem; }
|
|
906
|
+
.exec-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem; margin: 2rem 0; }
|
|
907
|
+
.exec-metric-card { background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #e5e7eb; box-shadow: 0 2px 4px rgba(0,0,0,0.04); }
|
|
908
|
+
.exec-metric-label { color: #6b7280; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem; }
|
|
909
|
+
.exec-metric-value { color: #111827; font-size: 2rem; font-weight: bold; margin-bottom: 0.5rem; line-height: 1; }
|
|
910
|
+
.exec-metric-desc { color: #6b7280; font-size: 0.85rem; }
|
|
911
|
+
.exec-priority-section { margin: 2.5rem 0; padding: 2rem; background: #fef2f2; border-radius: 12px; border-left: 4px solid #dc2626; }
|
|
912
|
+
.exec-priority-section h3 { color: #111827; margin: 0 0 0.75rem 0; font-size: 1.3rem; }
|
|
913
|
+
.exec-priority-intro { color: #4b5563; margin: 0 0 1.5rem 0; }
|
|
914
|
+
.exec-priority-list { display: flex; flex-direction: column; gap: 1rem; }
|
|
915
|
+
.exec-priority-item { display: flex; gap: 1rem; background: white; padding: 1.5rem; border-radius: 8px; border-left: 4px solid; }
|
|
916
|
+
.exec-priority-number { width: 32px; height: 32px; background: #111827; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.9rem; flex-shrink: 0; }
|
|
917
|
+
.exec-priority-content { flex: 1; }
|
|
918
|
+
.exec-priority-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
|
|
919
|
+
.exec-priority-badge { padding: 0.25rem 0.6rem; border-radius: 4px; color: white; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
920
|
+
.exec-priority-title { font-weight: 600; color: #111827; font-size: 1.05rem; }
|
|
921
|
+
.exec-priority-desc { color: #4b5563; margin-bottom: 0.75rem; font-size: 0.95rem; }
|
|
922
|
+
.exec-priority-action { background: #f0fdf4; padding: 0.75rem; border-radius: 6px; border-left: 3px solid #10b981; margin-bottom: 0.75rem; }
|
|
923
|
+
.exec-priority-action strong { color: #065f46; }
|
|
924
|
+
.exec-priority-ref { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
|
925
|
+
.exec-file-ref { font-family: 'SF Mono', Monaco, monospace; font-size: 0.8rem; color: #6b7280; background: #f3f4f6; padding: 0.25rem 0.5rem; border-radius: 4px; }
|
|
926
|
+
.exec-hipaa-ref { font-family: 'SF Mono', Monaco, monospace; font-size: 0.8rem; color: #3730a3; background: #e0e7ff; padding: 0.25rem 0.5rem; border-radius: 4px; }
|
|
927
|
+
.exec-categories-section { margin: 2.5rem 0; }
|
|
928
|
+
.exec-categories-section h3 { color: #111827; margin: 0 0 1.5rem 0; font-size: 1.3rem; }
|
|
929
|
+
.exec-category-bars { display: flex; flex-direction: column; gap: 1rem; }
|
|
930
|
+
.exec-category-item { background: white; padding: 1.25rem; border-radius: 8px; border: 1px solid #e5e7eb; }
|
|
931
|
+
.exec-category-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
|
|
932
|
+
.exec-category-name { font-weight: 600; color: #111827; }
|
|
933
|
+
.exec-category-count { color: #6b7280; font-size: 0.9rem; }
|
|
934
|
+
.exec-category-bar-bg { height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }
|
|
935
|
+
.exec-category-bar { height: 100%; background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%); border-radius: 4px; transition: width 0.3s ease; }
|
|
936
|
+
.exec-recommendations { margin: 2.5rem 0; padding: 2rem; background: #eff6ff; border-radius: 12px; border-left: 4px solid #3b82f6; }
|
|
937
|
+
.exec-recommendations h3 { color: #111827; margin: 0 0 1rem 0; font-size: 1.3rem; }
|
|
938
|
+
.exec-recommendations-list { margin: 0; padding-left: 1.5rem; }
|
|
939
|
+
.exec-recommendations-list li { margin: 0.75rem 0; color: #374151; line-height: 1.7; }
|
|
940
|
+
.exec-recommendations-list li strong { color: #1e40af; }
|
|
941
|
+
.exec-congrats { display: flex; align-items: center; gap: 1.5rem; padding: 2rem; margin: 2rem 0; background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); border-radius: 12px; border-left: 4px solid #10b981; }
|
|
942
|
+
.exec-congrats-icon { font-size: 3rem; line-height: 1; }
|
|
943
|
+
.exec-congrats-content h3 { color: #065f46; margin: 0 0 0.5rem 0; }
|
|
944
|
+
.exec-congrats-content p { color: #047857; margin: 0; }
|
|
234
945
|
h1 { color: #111827; margin-bottom: 0.5rem; }
|
|
235
946
|
h2 { color: #374151; margin: 2rem 0 1rem; }
|
|
236
947
|
.meta { color: #6b7280; margin-bottom: 2rem; }
|
|
@@ -304,15 +1015,270 @@ function generateHtml(report) {
|
|
|
304
1015
|
.guide-category:last-child { margin-bottom: 0; }
|
|
305
1016
|
.stack-guide { margin: 0.75rem 0; }
|
|
306
1017
|
|
|
1018
|
+
/* Compliance Score Styles */
|
|
1019
|
+
.compliance-score-section { margin: 2rem 0; padding: 2rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); color: white; }
|
|
1020
|
+
.score-header h2 { color: white; margin: 0 0 0.5rem 0; font-size: 1.75rem; }
|
|
1021
|
+
.score-subtitle { color: rgba(255,255,255,0.9); margin: 0; font-size: 0.95rem; }
|
|
1022
|
+
.score-main { display: grid; grid-template-columns: auto 1fr; gap: 2rem; margin: 2rem 0; align-items: center; }
|
|
1023
|
+
.score-circle { width: 180px; height: 180px; border-radius: 50%; border: 8px solid; display: flex; flex-direction: column; align-items: center; justify-content: center; background: white; position: relative; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
|
1024
|
+
.score-value { font-size: 3.5rem; font-weight: bold; line-height: 1; }
|
|
1025
|
+
.score-max { font-size: 1rem; color: #6b7280; margin-top: -0.5rem; }
|
|
1026
|
+
.score-grade { position: absolute; bottom: -15px; padding: 0.35rem 1rem; border-radius: 20px; color: white; font-weight: bold; font-size: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
|
|
1027
|
+
.score-info { color: white; }
|
|
1028
|
+
.score-description { font-size: 1.1rem; margin-bottom: 1rem; line-height: 1.6; }
|
|
1029
|
+
.score-trending { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; background: rgba(255,255,255,0.2); border-radius: 8px; margin-bottom: 1rem; }
|
|
1030
|
+
.trending-icon { font-size: 1.5rem; font-weight: bold; }
|
|
1031
|
+
.trending-up { border-left: 4px solid #10b981; }
|
|
1032
|
+
.trending-up .trending-icon { color: #10b981; }
|
|
1033
|
+
.trending-down { border-left: 4px solid #ef4444; }
|
|
1034
|
+
.trending-down .trending-icon { color: #ef4444; }
|
|
1035
|
+
.trending-same { border-left: 4px solid #94a3b8; }
|
|
1036
|
+
.trending-same .trending-icon { color: #94a3b8; }
|
|
1037
|
+
.trending-text { font-size: 0.9rem; }
|
|
1038
|
+
.score-formula { font-size: 0.85rem; color: rgba(255,255,255,0.8); margin-top: 0.75rem; font-family: 'SF Mono', Monaco, monospace; }
|
|
1039
|
+
.score-categories { margin-top: 2rem; padding-top: 2rem; border-top: 1px solid rgba(255,255,255,0.2); }
|
|
1040
|
+
.score-categories h3 { color: white; margin: 0 0 1.5rem 0; font-size: 1.3rem; }
|
|
1041
|
+
.category-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; }
|
|
1042
|
+
.category-card { background: rgba(255,255,255,0.15); padding: 1.25rem; border-radius: 10px; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); }
|
|
1043
|
+
.category-name { font-weight: 600; margin-bottom: 0.75rem; font-size: 0.95rem; }
|
|
1044
|
+
.category-score-container { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
|
|
1045
|
+
.category-bar-bg { flex: 1; height: 8px; background: rgba(255,255,255,0.2); border-radius: 4px; overflow: hidden; }
|
|
1046
|
+
.category-bar { height: 100%; border-radius: 4px; transition: width 0.3s ease; }
|
|
1047
|
+
.category-score { font-weight: bold; font-size: 1.25rem; min-width: 40px; text-align: right; }
|
|
1048
|
+
.category-findings { font-size: 0.8rem; color: rgba(255,255,255,0.7); }
|
|
1049
|
+
|
|
1050
|
+
/* Risk Analysis Styles */
|
|
1051
|
+
.risk-analysis-section { margin: 2rem 0; padding: 1.5rem; background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
1052
|
+
.risk-analysis-section h2 { color: #111827; margin-bottom: 0.5rem; }
|
|
1053
|
+
.risk-analysis-section h3 { color: #374151; margin: 1.5rem 0 1rem; font-size: 1.1rem; }
|
|
1054
|
+
|
|
1055
|
+
/* Asset Inventory Styles */
|
|
1056
|
+
.asset-inventory-section { margin: 2rem 0; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
1057
|
+
.inventory-header h2 { color: #111827; margin: 0 0 0.5rem 0; }
|
|
1058
|
+
.inventory-subtitle { color: #6b7280; margin: 0 0 1.5rem 0; font-size: 0.95rem; }
|
|
1059
|
+
.inventory-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
|
1060
|
+
.stat-box { background: #f9fafb; padding: 1.25rem; border-radius: 8px; text-align: center; border: 1px solid #e5e7eb; }
|
|
1061
|
+
.stat-value { font-size: 1.75rem; font-weight: bold; color: #1f2937; margin-bottom: 0.25rem; }
|
|
1062
|
+
.stat-label { color: #6b7280; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1063
|
+
.inventory-notice { background: #eff6ff; padding: 1rem 1.25rem; border-radius: 8px; border-left: 4px solid #3b82f6; margin-bottom: 1.5rem; color: #1e40af; font-size: 0.9rem; }
|
|
1064
|
+
.inventory-notice strong { color: #1e3a8a; }
|
|
1065
|
+
.export-csv-link { color: #2563eb; font-weight: 600; text-decoration: none; margin-left: 0.5rem; }
|
|
1066
|
+
.export-csv-link:hover { text-decoration: underline; }
|
|
1067
|
+
.inventory-table-container { overflow-x: auto; }
|
|
1068
|
+
.inventory-table { width: 100%; border-collapse: collapse; }
|
|
1069
|
+
.inventory-table th { background: #f9fafb; padding: 0.75rem 1rem; text-align: left; font-weight: 600; color: #374151; border-bottom: 2px solid #e5e7eb; font-size: 0.85rem; white-space: nowrap; }
|
|
1070
|
+
.inventory-table td { padding: 0.75rem 1rem; border-bottom: 1px solid #e5e7eb; vertical-align: middle; }
|
|
1071
|
+
.inventory-table tr:hover { background: #f9fafb; }
|
|
1072
|
+
.inventory-table .category-row { background: #f3f4f6; }
|
|
1073
|
+
.inventory-table .category-row td { padding: 0.5rem 1rem; font-weight: 600; color: #1f2937; border-bottom: 1px solid #d1d5db; }
|
|
1074
|
+
.asset-id { font-family: 'SF Mono', Monaco, monospace; color: #6b7280; font-size: 0.85rem; }
|
|
1075
|
+
.asset-name code { background: #f3f4f6; padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.85rem; color: #1f2937; }
|
|
1076
|
+
.asset-version { color: #6b7280; font-size: 0.85rem; }
|
|
1077
|
+
.asset-provider { color: #374151; font-size: 0.9rem; }
|
|
1078
|
+
.category-badge { display: inline-block; padding: 0.2rem 0.6rem; background: #e0e7ff; color: #3730a3; border-radius: 4px; font-size: 0.75rem; font-weight: 500; text-transform: capitalize; }
|
|
1079
|
+
.editable-field input { width: 100%; padding: 0.4rem 0.6rem; border: 1px solid #d1d5db; border-radius: 4px; font-size: 0.85rem; color: #1f2937; }
|
|
1080
|
+
.editable-field input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
|
|
1081
|
+
.editable-field input::placeholder { color: #9ca3af; }
|
|
1082
|
+
|
|
1083
|
+
/* Data Flow Map Styles */
|
|
1084
|
+
.data-flow-map-section { margin: 2rem 0; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
1085
|
+
.flow-header h2 { color: #111827; margin: 0 0 0.5rem 0; }
|
|
1086
|
+
.flow-subtitle { color: #6b7280; margin: 0 0 1.5rem 0; font-size: 0.95rem; }
|
|
1087
|
+
.flow-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
1088
|
+
.flow-stat-box { background: #f9fafb; padding: 1.25rem; border-radius: 8px; text-align: center; border: 1px solid #e5e7eb; }
|
|
1089
|
+
.flow-stat-box.flow-stat-warning { background: #fef2f2; border-color: #fecaca; }
|
|
1090
|
+
.flow-stat-value { font-size: 1.75rem; font-weight: bold; color: #1f2937; margin-bottom: 0.25rem; }
|
|
1091
|
+
.flow-stat-warning .flow-stat-value { color: #dc2626; }
|
|
1092
|
+
.flow-stat-label { color: #6b7280; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1093
|
+
.mermaid-container { background: #fafafa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 2rem; margin: 2rem 0; overflow-x: auto; }
|
|
1094
|
+
.mermaid-container pre.mermaid { background: transparent; margin: 0; padding: 0; text-align: center; }
|
|
1095
|
+
.flow-legend { margin: 2rem 0; padding: 1.5rem; background: #f9fafb; border-radius: 8px; border-left: 4px solid #3b82f6; }
|
|
1096
|
+
.flow-legend h3 { margin: 0 0 1rem 0; color: #374151; font-size: 1rem; }
|
|
1097
|
+
.legend-items { display: flex; flex-wrap: wrap; gap: 1.5rem; }
|
|
1098
|
+
.legend-item { display: flex; align-items: center; gap: 0.5rem; }
|
|
1099
|
+
.legend-box { width: 40px; height: 20px; border-radius: 4px; border: 2px solid; }
|
|
1100
|
+
.legend-normal { background: #dbeafe; border-color: #3b82f6; }
|
|
1101
|
+
.legend-issue { background: #fee2e2; border-color: #dc2626; }
|
|
1102
|
+
.legend-external { background: #fef3c7; border-color: #f59e0b; }
|
|
1103
|
+
.flow-notice { background: #fef3c7; padding: 1rem 1.25rem; border-radius: 8px; border-left: 4px solid #f59e0b; margin: 2rem 0; color: #92400e; font-size: 0.9rem; }
|
|
1104
|
+
.flow-notice strong { color: #78350f; }
|
|
1105
|
+
.flow-components { margin-top: 2rem; }
|
|
1106
|
+
.flow-components h3 { color: #374151; margin: 0 0 1.5rem 0; font-size: 1.1rem; }
|
|
1107
|
+
.component-group { margin-bottom: 1.5rem; }
|
|
1108
|
+
.component-group h4 { color: #4b5563; margin: 0 0 0.75rem 0; font-size: 0.95rem; }
|
|
1109
|
+
.component-group ul { list-style: none; margin: 0; padding: 0; }
|
|
1110
|
+
.component-group li { padding: 0.5rem 0.75rem; margin: 0.25rem 0; background: #f9fafb; border-radius: 6px; display: flex; align-items: center; justify-content: space-between; }
|
|
1111
|
+
.component-with-issues { background: #fef2f2 !important; border-left: 3px solid #dc2626; }
|
|
1112
|
+
.issue-badge { background: #dc2626; color: white; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
|
|
1113
|
+
.baa-badge { background: #f59e0b; color: white; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; margin-left: 0.5rem; }
|
|
1114
|
+
|
|
1115
|
+
.risk-summary-table table, .risk-detail-table table { width: 100%; border-collapse: collapse; background: white; }
|
|
1116
|
+
.risk-summary-table { margin: 1rem 0; overflow-x: auto; }
|
|
1117
|
+
.risk-detail-table { margin: 1rem 0; overflow-x: auto; }
|
|
1118
|
+
|
|
1119
|
+
.risk-summary-table th, .risk-detail-table th { background: #f9fafb; padding: 0.75rem 1rem; text-align: left; font-weight: 600; color: #374151; border-bottom: 2px solid #e5e7eb; }
|
|
1120
|
+
.risk-summary-table td, .risk-detail-table td { padding: 0.75rem 1rem; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
|
|
1121
|
+
.risk-summary-table tr:hover, .risk-detail-table tr:hover { background: #f9fafb; }
|
|
1122
|
+
|
|
1123
|
+
.threat-cell { font-weight: 500; color: #1f2937; max-width: 200px; }
|
|
1124
|
+
.vulnerability-cell { max-width: 250px; }
|
|
1125
|
+
.vulnerability-cell strong { display: block; margin-bottom: 0.25rem; color: #111827; }
|
|
1126
|
+
.file-ref { font-family: 'SF Mono', Monaco, monospace; font-size: 0.75rem; color: #6b7280; word-break: break-all; }
|
|
1127
|
+
.risk-level-cell { text-align: center; }
|
|
1128
|
+
.risk-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; color: white; text-transform: uppercase; }
|
|
1129
|
+
.status-cell { text-align: center; }
|
|
1130
|
+
.status-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.75rem; font-weight: 500; }
|
|
1131
|
+
.status-open { background: #fee2e2; color: #991b1b; }
|
|
1132
|
+
.status-available { background: #d1fae5; color: #065f46; }
|
|
1133
|
+
.remediation-cell { color: #4b5563; font-size: 0.875rem; max-width: 300px; }
|
|
1134
|
+
.hipaa-cell { color: #6b7280; font-size: 0.8rem; font-family: 'SF Mono', Monaco, monospace; white-space: nowrap; }
|
|
1135
|
+
|
|
1136
|
+
/* Backup & Recovery Styles */
|
|
1137
|
+
.backup-recovery-section { margin: 2rem 0; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
1138
|
+
.backup-header h2 { color: #111827; margin: 0 0 0.5rem 0; }
|
|
1139
|
+
.backup-subtitle { color: #6b7280; margin: 0 0 1.5rem 0; font-size: 0.95rem; }
|
|
1140
|
+
.backup-hipaa-notice { background: #fef3c7; padding: 1.25rem; border-radius: 8px; border-left: 4px solid #f59e0b; margin-bottom: 2rem; color: #92400e; font-size: 0.9rem; line-height: 1.6; }
|
|
1141
|
+
.backup-hipaa-notice strong { color: #78350f; }
|
|
1142
|
+
.backup-guide-card { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 2rem; margin: 2rem 0; }
|
|
1143
|
+
.backup-guide-card.backup-guide-warning { background: #fffbeb; border-color: #fcd34d; }
|
|
1144
|
+
.backup-guide-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 2px solid #e5e7eb; }
|
|
1145
|
+
.backup-guide-icon { font-size: 2rem; line-height: 1; }
|
|
1146
|
+
.backup-guide-header h4 { margin: 0; color: #111827; font-size: 1.2rem; }
|
|
1147
|
+
.backup-guide-content { }
|
|
1148
|
+
.backup-step { display: flex; gap: 1rem; margin: 1.5rem 0; }
|
|
1149
|
+
.backup-step-number { width: 36px; height: 36px; background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1rem; flex-shrink: 0; box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); }
|
|
1150
|
+
.backup-step-content { flex: 1; }
|
|
1151
|
+
.backup-step-content strong { display: block; color: #111827; margin-bottom: 0.5rem; font-size: 1.05rem; }
|
|
1152
|
+
.backup-step-content p { color: #4b5563; margin: 0.5rem 0; font-size: 0.95rem; }
|
|
1153
|
+
.backup-code-block { background: #1e1e1e; border-radius: 6px; padding: 1rem; margin: 1rem 0; overflow-x: auto; }
|
|
1154
|
+
.backup-code-block pre { margin: 0; }
|
|
1155
|
+
.backup-code-block code { font-family: 'SF Mono', Monaco, 'Courier New', monospace; font-size: 0.85rem; color: #d4d4d4; white-space: pre; line-height: 1.5; }
|
|
1156
|
+
.backup-checklist { list-style: none; margin: 0.75rem 0 0 0; padding: 0; }
|
|
1157
|
+
.backup-checklist li { padding: 0.5rem 0; color: #374151; font-size: 0.9rem; }
|
|
1158
|
+
.backup-verification-checklist { margin: 2.5rem 0; padding: 2rem; background: #f0fdf4; border-radius: 10px; border-left: 4px solid #10b981; }
|
|
1159
|
+
.backup-verification-checklist h3 { color: #065f46; margin: 0 0 1.5rem 0; font-size: 1.2rem; }
|
|
1160
|
+
.backup-checklist-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
|
1161
|
+
.backup-checklist-item { background: white; padding: 1rem; border-radius: 8px; border: 1px solid #d1fae5; display: flex; align-items: start; gap: 0.75rem; }
|
|
1162
|
+
.backup-checklist-item input[type="checkbox"] { width: 20px; height: 20px; margin-top: 0.25rem; cursor: pointer; flex-shrink: 0; accent-color: #10b981; }
|
|
1163
|
+
.backup-checklist-item label { flex: 1; cursor: pointer; }
|
|
1164
|
+
.backup-checklist-item label strong { display: block; color: #065f46; margin-bottom: 0.25rem; font-size: 0.95rem; }
|
|
1165
|
+
.backup-checklist-item label span { display: block; color: #6b7280; font-size: 0.85rem; }
|
|
1166
|
+
.backup-recovery-timeline { margin: 2.5rem 0; padding: 2rem; background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); border-radius: 10px; border-left: 4px solid #3b82f6; }
|
|
1167
|
+
.backup-recovery-timeline h3 { color: #1e40af; margin: 0 0 1.5rem 0; font-size: 1.2rem; }
|
|
1168
|
+
.rto-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; }
|
|
1169
|
+
.rto-item { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border-left: 4px solid; text-align: center; }
|
|
1170
|
+
.rto-item.rto-critical { border-left-color: #dc2626; }
|
|
1171
|
+
.rto-item.rto-important { border-left-color: #f59e0b; }
|
|
1172
|
+
.rto-item.rto-standard { border-left-color: #10b981; }
|
|
1173
|
+
.rto-label { color: #6b7280; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem; }
|
|
1174
|
+
.rto-time { color: #111827; font-size: 1.75rem; font-weight: bold; margin-bottom: 0.5rem; }
|
|
1175
|
+
.rto-desc { color: #4b5563; font-size: 0.85rem; }
|
|
1176
|
+
|
|
1177
|
+
/* Incident Response Styles */
|
|
1178
|
+
.incident-response-section { margin: 2rem 0; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
1179
|
+
.incident-header h2 { color: #111827; margin: 0 0 0.5rem 0; }
|
|
1180
|
+
.incident-subtitle { color: #6b7280; margin: 0 0 1.5rem 0; font-size: 0.95rem; }
|
|
1181
|
+
.incident-alert { display: flex; align-items: start; gap: 1rem; padding: 1.5rem; margin-bottom: 2rem; background: #fef2f2; border-radius: 10px; border-left: 4px solid #dc2626; }
|
|
1182
|
+
.incident-alert-icon { font-size: 2rem; line-height: 1; }
|
|
1183
|
+
.incident-alert-content strong { display: block; color: #991b1b; margin-bottom: 0.5rem; font-size: 1.1rem; }
|
|
1184
|
+
.incident-alert-content p { color: #7f1d1d; margin: 0; }
|
|
1185
|
+
.incident-hipaa-notice { background: #eff6ff; padding: 1.25rem; border-radius: 8px; border-left: 4px solid #3b82f6; margin-bottom: 2rem; color: #1e40af; font-size: 0.9rem; line-height: 1.6; }
|
|
1186
|
+
.incident-hipaa-notice strong { color: #1e3a8a; }
|
|
1187
|
+
.incident-team-section { margin: 2.5rem 0; }
|
|
1188
|
+
.incident-team-section h3 { color: #111827; margin: 0 0 1.5rem 0; font-size: 1.2rem; }
|
|
1189
|
+
.incident-team-table { overflow-x: auto; }
|
|
1190
|
+
.incident-team-table table { width: 100%; border-collapse: collapse; background: #f9fafb; border-radius: 8px; overflow: hidden; }
|
|
1191
|
+
.incident-team-table th { background: #374151; color: white; padding: 0.75rem 1rem; text-align: left; font-weight: 600; font-size: 0.85rem; }
|
|
1192
|
+
.incident-team-table td { padding: 0.75rem 1rem; border-bottom: 1px solid #e5e7eb; vertical-align: middle; }
|
|
1193
|
+
.incident-team-table tr:last-child td { border-bottom: none; }
|
|
1194
|
+
.incident-team-table tr:hover { background: white; }
|
|
1195
|
+
.editable-cell { background: white; }
|
|
1196
|
+
.contact-input { width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 4px; font-size: 0.85rem; }
|
|
1197
|
+
.contact-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
|
|
1198
|
+
.severity-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin: 1.5rem 0; }
|
|
1199
|
+
.severity-card { background: #f9fafb; border-radius: 10px; padding: 1.5rem; border-left: 4px solid; }
|
|
1200
|
+
.severity-card.severity-critical { border-left-color: #dc2626; background: linear-gradient(135deg, #fef2f2 0%, #f9fafb 100%); }
|
|
1201
|
+
.severity-card.severity-high { border-left-color: #ea580c; background: linear-gradient(135deg, #fff7ed 0%, #f9fafb 100%); }
|
|
1202
|
+
.severity-card.severity-medium { border-left-color: #f59e0b; background: linear-gradient(135deg, #fffbeb 0%, #f9fafb 100%); }
|
|
1203
|
+
.severity-card.severity-low { border-left-color: #3b82f6; background: linear-gradient(135deg, #eff6ff 0%, #f9fafb 100%); }
|
|
1204
|
+
.severity-badge { display: inline-block; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 700; font-size: 0.85rem; margin-bottom: 1rem; }
|
|
1205
|
+
.severity-critical .severity-badge { background: #dc2626; color: white; }
|
|
1206
|
+
.severity-high .severity-badge { background: #ea580c; color: white; }
|
|
1207
|
+
.severity-medium .severity-badge { background: #f59e0b; color: white; }
|
|
1208
|
+
.severity-low .severity-badge { background: #3b82f6; color: white; }
|
|
1209
|
+
.severity-examples { margin-bottom: 1rem; }
|
|
1210
|
+
.severity-examples strong { display: block; color: #374151; margin-bottom: 0.5rem; }
|
|
1211
|
+
.severity-examples ul { margin: 0.5rem 0 0 1.5rem; padding: 0; }
|
|
1212
|
+
.severity-examples li { color: #4b5563; font-size: 0.9rem; margin: 0.25rem 0; }
|
|
1213
|
+
.severity-response { padding-top: 1rem; border-top: 1px solid #e5e7eb; }
|
|
1214
|
+
.severity-response strong { color: #111827; font-size: 0.9rem; }
|
|
1215
|
+
.phase-card { display: flex; gap: 1.5rem; margin: 1.5rem 0; padding: 1.5rem; background: #f9fafb; border-radius: 10px; border-left: 4px solid #3b82f6; }
|
|
1216
|
+
.phase-number { width: 48px; height: 48px; background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.5rem; flex-shrink: 0; box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3); }
|
|
1217
|
+
.phase-content { flex: 1; }
|
|
1218
|
+
.phase-content h4 { margin: 0 0 1rem 0; color: #111827; font-size: 1.1rem; }
|
|
1219
|
+
.phase-checklist { }
|
|
1220
|
+
.phase-checklist ul { margin: 0.5rem 0 0 1.5rem; padding: 0; }
|
|
1221
|
+
.phase-checklist li { color: #374151; font-size: 0.9rem; margin: 0.5rem 0; }
|
|
1222
|
+
.phase-item { margin-bottom: 1rem; }
|
|
1223
|
+
.phase-item:last-child { margin-bottom: 0; }
|
|
1224
|
+
.phase-item strong { display: block; color: #1f2937; margin-bottom: 0.5rem; }
|
|
1225
|
+
.incident-breach-timeline { margin: 2.5rem 0; padding: 2rem; background: linear-gradient(135deg, #fef3c7 0%, #fef9c3 100%); border-radius: 10px; border-left: 4px solid #f59e0b; }
|
|
1226
|
+
.incident-breach-timeline h3 { color: #78350f; margin: 0 0 1.5rem 0; font-size: 1.2rem; }
|
|
1227
|
+
.timeline-container { position: relative; padding-left: 2rem; }
|
|
1228
|
+
.timeline-container::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: #f59e0b; }
|
|
1229
|
+
.timeline-item { position: relative; margin-bottom: 2rem; }
|
|
1230
|
+
.timeline-item:last-child { margin-bottom: 0; }
|
|
1231
|
+
.timeline-marker { position: absolute; left: -2.5rem; width: 20px; height: 20px; border-radius: 50%; border: 3px solid #f59e0b; }
|
|
1232
|
+
.timeline-marker.timeline-discovery { background: #dc2626; }
|
|
1233
|
+
.timeline-marker.timeline-assessment { background: #f59e0b; }
|
|
1234
|
+
.timeline-marker.timeline-notification { background: #3b82f6; }
|
|
1235
|
+
.timeline-marker.timeline-documentation { background: #10b981; }
|
|
1236
|
+
.timeline-content { background: white; padding: 1rem 1.25rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
|
|
1237
|
+
.timeline-content strong { display: block; color: #111827; margin-bottom: 0.5rem; font-size: 1rem; }
|
|
1238
|
+
.timeline-content p { color: #4b5563; margin: 0; font-size: 0.9rem; line-height: 1.6; }
|
|
1239
|
+
.contacts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin: 1.5rem 0; }
|
|
1240
|
+
.contact-card { background: #f9fafb; padding: 1.25rem; border-radius: 8px; border: 1px solid #e5e7eb; }
|
|
1241
|
+
.contact-title { font-weight: 700; color: #111827; margin-bottom: 0.75rem; font-size: 0.95rem; }
|
|
1242
|
+
.contact-info { color: #4b5563; font-size: 0.85rem; }
|
|
1243
|
+
.contact-info div { margin: 0.25rem 0; }
|
|
1244
|
+
.contact-info a { color: #3b82f6; text-decoration: none; }
|
|
1245
|
+
.contact-info a:hover { text-decoration: underline; }
|
|
1246
|
+
.editable-contact input { width: 100%; margin: 0.25rem 0; }
|
|
1247
|
+
.contact-input-wide { width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 4px; font-size: 0.85rem; margin: 0.25rem 0; }
|
|
1248
|
+
.contact-input-wide:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
|
|
1249
|
+
.incident-log-template { margin: 2.5rem 0; }
|
|
1250
|
+
.incident-log-template h3 { color: #111827; margin: 0 0 0.75rem 0; font-size: 1.2rem; }
|
|
1251
|
+
.template-note { color: #6b7280; margin-bottom: 1rem; font-size: 0.9rem; }
|
|
1252
|
+
.log-template-box { background: #f9fafb; border: 2px dashed #d1d5db; border-radius: 8px; padding: 1.5rem; font-family: 'SF Mono', Monaco, monospace; font-size: 0.85rem; }
|
|
1253
|
+
.log-field { margin: 0.75rem 0; padding: 0.5rem 0; border-bottom: 1px solid #e5e7eb; }
|
|
1254
|
+
.log-field:last-child { border-bottom: none; }
|
|
1255
|
+
.log-field strong { color: #374151; display: inline-block; min-width: 180px; }
|
|
1256
|
+
.log-placeholder { color: #6b7280; font-style: italic; }
|
|
1257
|
+
.incident-testing-section { margin: 2.5rem 0; padding: 2rem; background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); border-radius: 10px; border-left: 4px solid #10b981; }
|
|
1258
|
+
.incident-testing-section h3 { color: #065f46; margin: 0 0 1.5rem 0; font-size: 1.2rem; }
|
|
1259
|
+
.testing-recommendations { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; }
|
|
1260
|
+
.testing-item { background: white; padding: 1.25rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
|
|
1261
|
+
.testing-frequency { display: inline-block; background: #10b981; color: white; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; margin-bottom: 0.75rem; }
|
|
1262
|
+
.testing-activity strong { display: block; color: #111827; margin-bottom: 0.5rem; }
|
|
1263
|
+
.testing-activity p { color: #4b5563; margin: 0; font-size: 0.9rem; }
|
|
1264
|
+
|
|
307
1265
|
@media (max-width: 768px) {
|
|
308
1266
|
body { padding: 1rem; }
|
|
309
1267
|
.summary { grid-template-columns: repeat(2, 1fr); }
|
|
310
1268
|
.stack-cards { grid-template-columns: 1fr; }
|
|
1269
|
+
.risk-detail-table table { font-size: 0.8rem; }
|
|
1270
|
+
.risk-detail-table th, .risk-detail-table td { padding: 0.5rem; }
|
|
1271
|
+
.score-main { grid-template-columns: 1fr; justify-items: center; }
|
|
1272
|
+
.score-circle { width: 150px; height: 150px; }
|
|
1273
|
+
.score-value { font-size: 2.5rem; }
|
|
1274
|
+
.category-grid { grid-template-columns: 1fr; }
|
|
311
1275
|
}
|
|
312
1276
|
</style>
|
|
313
1277
|
</head>
|
|
314
1278
|
<body>
|
|
315
1279
|
<div class="container">
|
|
1280
|
+
${renderExecutiveSummaryHtml(report)}
|
|
1281
|
+
|
|
316
1282
|
<h1>HIPAA Compliance Report</h1>
|
|
317
1283
|
<p style="color: #6b7280; margin-bottom: 1rem;">Generated by <strong>vlayer</strong> - HIPAA Compliance Scanner</p>
|
|
318
1284
|
<div class="meta">
|
|
@@ -340,8 +1306,24 @@ function generateHtml(report) {
|
|
|
340
1306
|
</div>
|
|
341
1307
|
</div>
|
|
342
1308
|
|
|
1309
|
+
${complianceScoreHtml}
|
|
1310
|
+
|
|
1311
|
+
${renderScanComparisonHtml(options.scanComparison)}
|
|
1312
|
+
|
|
343
1313
|
${report.stack && report.stack.framework !== 'unknown' ? renderStackSection(report.stack) : ''}
|
|
344
1314
|
|
|
1315
|
+
${renderRiskAnalysisHtml(report)}
|
|
1316
|
+
|
|
1317
|
+
${assetInventoryHtml}
|
|
1318
|
+
|
|
1319
|
+
${dataFlowMapHtml}
|
|
1320
|
+
|
|
1321
|
+
${report.stack ? renderBackupRecoveryGuideHtml(report.stack) : ''}
|
|
1322
|
+
|
|
1323
|
+
${renderIncidentResponsePlanHtml(report.summary.critical, report.summary.high)}
|
|
1324
|
+
|
|
1325
|
+
${report.vulnerabilities ? renderDependencyVulnerabilitiesHtml(report.vulnerabilities) : ''}
|
|
1326
|
+
|
|
345
1327
|
<h2>Findings</h2>
|
|
346
1328
|
<div class="findings">
|
|
347
1329
|
${report.findings.map(f => {
|
|
@@ -393,6 +1375,1631 @@ function formatCategory(category) {
|
|
|
393
1375
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
394
1376
|
.join(' ');
|
|
395
1377
|
}
|
|
1378
|
+
function getCategoryThreat(category) {
|
|
1379
|
+
const threats = {
|
|
1380
|
+
'phi-exposure': 'Unauthorized PHI Disclosure',
|
|
1381
|
+
'encryption': 'Data Breach / Interception',
|
|
1382
|
+
'audit-logging': 'Lack of Accountability / Forensics',
|
|
1383
|
+
'access-control': 'Unauthorized Access / Privilege Escalation',
|
|
1384
|
+
'data-retention': 'Non-Compliance with Retention Requirements',
|
|
1385
|
+
};
|
|
1386
|
+
return threats[category] || 'Security Vulnerability';
|
|
1387
|
+
}
|
|
1388
|
+
function getRiskLevel(severity) {
|
|
1389
|
+
const levels = {
|
|
1390
|
+
critical: 'CRITICAL',
|
|
1391
|
+
high: 'HIGH',
|
|
1392
|
+
medium: 'MEDIUM',
|
|
1393
|
+
low: 'LOW',
|
|
1394
|
+
info: 'INFO',
|
|
1395
|
+
};
|
|
1396
|
+
return levels[severity] || 'MEDIUM';
|
|
1397
|
+
}
|
|
1398
|
+
function getMitigationStatus(finding) {
|
|
1399
|
+
if (finding.fixType) {
|
|
1400
|
+
return 'Remediation Available (Auto-fix)';
|
|
1401
|
+
}
|
|
1402
|
+
return 'Open';
|
|
1403
|
+
}
|
|
1404
|
+
async function renderAssetInventoryHtml(targetPath) {
|
|
1405
|
+
const inventory = await generateAssetInventory(targetPath);
|
|
1406
|
+
if (inventory.totalAssets === 0) {
|
|
1407
|
+
return `
|
|
1408
|
+
<div class="asset-inventory-section">
|
|
1409
|
+
<h2>📦 Technology Asset Inventory</h2>
|
|
1410
|
+
<p style="color: #6b7280;">No package.json found or no dependencies detected.</p>
|
|
1411
|
+
</div>
|
|
1412
|
+
`;
|
|
1413
|
+
}
|
|
1414
|
+
const categoryIcons = {
|
|
1415
|
+
framework: '⚡',
|
|
1416
|
+
database: '🗄️',
|
|
1417
|
+
auth: '🔐',
|
|
1418
|
+
cloud: '☁️',
|
|
1419
|
+
payment: '💳',
|
|
1420
|
+
communication: '📧',
|
|
1421
|
+
utility: '🔧',
|
|
1422
|
+
other: '📦',
|
|
1423
|
+
};
|
|
1424
|
+
const categoryLabels = {
|
|
1425
|
+
framework: 'Frameworks',
|
|
1426
|
+
database: 'Databases & ORMs',
|
|
1427
|
+
auth: 'Authentication',
|
|
1428
|
+
cloud: 'Cloud & BaaS',
|
|
1429
|
+
payment: 'Payment Services',
|
|
1430
|
+
communication: 'Communication',
|
|
1431
|
+
utility: 'Utilities',
|
|
1432
|
+
other: 'Other',
|
|
1433
|
+
};
|
|
1434
|
+
// Group by category
|
|
1435
|
+
const byCategory = inventory.assets.reduce((acc, asset) => {
|
|
1436
|
+
if (!acc[asset.category]) {
|
|
1437
|
+
acc[asset.category] = [];
|
|
1438
|
+
}
|
|
1439
|
+
acc[asset.category].push(asset);
|
|
1440
|
+
return acc;
|
|
1441
|
+
}, {});
|
|
1442
|
+
return `
|
|
1443
|
+
<div class="asset-inventory-section">
|
|
1444
|
+
<div class="inventory-header">
|
|
1445
|
+
<h2>📦 Technology Asset Inventory</h2>
|
|
1446
|
+
<p class="inventory-subtitle">
|
|
1447
|
+
Software assets detected from package.json analysis
|
|
1448
|
+
</p>
|
|
1449
|
+
</div>
|
|
1450
|
+
|
|
1451
|
+
<div class="inventory-stats">
|
|
1452
|
+
<div class="stat-box">
|
|
1453
|
+
<div class="stat-value">${inventory.totalAssets}</div>
|
|
1454
|
+
<div class="stat-label">Total Assets</div>
|
|
1455
|
+
</div>
|
|
1456
|
+
<div class="stat-box">
|
|
1457
|
+
<div class="stat-value">${Object.keys(byCategory).length}</div>
|
|
1458
|
+
<div class="stat-label">Categories</div>
|
|
1459
|
+
</div>
|
|
1460
|
+
<div class="stat-box">
|
|
1461
|
+
<div class="stat-value">${new Date(inventory.detectedAt).toLocaleDateString()}</div>
|
|
1462
|
+
<div class="stat-label">Detected</div>
|
|
1463
|
+
</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
|
|
1466
|
+
<div class="inventory-notice">
|
|
1467
|
+
<strong>📋 Note:</strong> This inventory covers software assets detected in code (package.json).
|
|
1468
|
+
Please complete the "Responsible Person" and "Location" fields, and add any hardware,
|
|
1469
|
+
infrastructure, or third-party services not detected automatically.
|
|
1470
|
+
<a href="#" class="export-csv-link" onclick="event.preventDefault(); exportInventoryCsv();">
|
|
1471
|
+
Export as CSV →
|
|
1472
|
+
</a>
|
|
1473
|
+
</div>
|
|
1474
|
+
|
|
1475
|
+
<div class="inventory-table-container">
|
|
1476
|
+
<table class="inventory-table">
|
|
1477
|
+
<thead>
|
|
1478
|
+
<tr>
|
|
1479
|
+
<th>ID</th>
|
|
1480
|
+
<th>Name</th>
|
|
1481
|
+
<th>Version</th>
|
|
1482
|
+
<th>Type</th>
|
|
1483
|
+
<th>Category</th>
|
|
1484
|
+
<th>Provider</th>
|
|
1485
|
+
<th>Responsible Person</th>
|
|
1486
|
+
<th>Location</th>
|
|
1487
|
+
</tr>
|
|
1488
|
+
</thead>
|
|
1489
|
+
<tbody>
|
|
1490
|
+
${Object.entries(byCategory)
|
|
1491
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
1492
|
+
.map(([category, assets]) => `
|
|
1493
|
+
<tr class="category-row">
|
|
1494
|
+
<td colspan="8">
|
|
1495
|
+
<strong>${categoryIcons[category]} ${categoryLabels[category]}</strong>
|
|
1496
|
+
</td>
|
|
1497
|
+
</tr>
|
|
1498
|
+
${assets.map(asset => `
|
|
1499
|
+
<tr>
|
|
1500
|
+
<td class="asset-id">${escapeHtml(asset.id)}</td>
|
|
1501
|
+
<td class="asset-name"><code>${escapeHtml(asset.name)}</code></td>
|
|
1502
|
+
<td class="asset-version">${escapeHtml(asset.version)}</td>
|
|
1503
|
+
<td>${escapeHtml(asset.type)}</td>
|
|
1504
|
+
<td><span class="category-badge">${escapeHtml(asset.category)}</span></td>
|
|
1505
|
+
<td class="asset-provider">${escapeHtml(asset.provider)}</td>
|
|
1506
|
+
<td class="editable-field">
|
|
1507
|
+
<input type="text" placeholder="Enter name..." class="person-input" />
|
|
1508
|
+
</td>
|
|
1509
|
+
<td class="editable-field">
|
|
1510
|
+
<input type="text" placeholder="e.g., AWS us-east-1" class="location-input" />
|
|
1511
|
+
</td>
|
|
1512
|
+
</tr>
|
|
1513
|
+
`).join('')}
|
|
1514
|
+
`).join('')}
|
|
1515
|
+
</tbody>
|
|
1516
|
+
</table>
|
|
1517
|
+
</div>
|
|
1518
|
+
|
|
1519
|
+
<script>
|
|
1520
|
+
const inventoryData = ${JSON.stringify(inventory.assets)};
|
|
1521
|
+
|
|
1522
|
+
function exportInventoryCsv() {
|
|
1523
|
+
const header = 'ID,Name,Version,Type,Category,Provider,Responsible Person,Location\\n';
|
|
1524
|
+
|
|
1525
|
+
const rows = inventoryData.map((asset, idx) => {
|
|
1526
|
+
const personInput = document.querySelectorAll('.person-input')[idx];
|
|
1527
|
+
const locationInput = document.querySelectorAll('.location-input')[idx];
|
|
1528
|
+
|
|
1529
|
+
return [
|
|
1530
|
+
asset.id,
|
|
1531
|
+
'"' + asset.name + '"',
|
|
1532
|
+
'"' + asset.version + '"',
|
|
1533
|
+
asset.type,
|
|
1534
|
+
asset.category,
|
|
1535
|
+
'"' + asset.provider + '"',
|
|
1536
|
+
'"' + (personInput?.value || '') + '"',
|
|
1537
|
+
'"' + (locationInput?.value || '') + '"',
|
|
1538
|
+
].join(',');
|
|
1539
|
+
}).join('\\n');
|
|
1540
|
+
|
|
1541
|
+
const csv = header + rows;
|
|
1542
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
1543
|
+
const url = URL.createObjectURL(blob);
|
|
1544
|
+
const a = document.createElement('a');
|
|
1545
|
+
a.href = url;
|
|
1546
|
+
a.download = 'technology-asset-inventory.csv';
|
|
1547
|
+
a.click();
|
|
1548
|
+
URL.revokeObjectURL(url);
|
|
1549
|
+
}
|
|
1550
|
+
</script>
|
|
1551
|
+
</div>
|
|
1552
|
+
`;
|
|
1553
|
+
}
|
|
1554
|
+
function renderExecutiveSummaryHtml(report) {
|
|
1555
|
+
const criticalCount = report.summary.critical;
|
|
1556
|
+
const highCount = report.summary.high;
|
|
1557
|
+
const totalFindings = report.summary.total;
|
|
1558
|
+
// Determine overall status
|
|
1559
|
+
const overallStatus = criticalCount === 0 && highCount === 0 ? 'compliant' :
|
|
1560
|
+
criticalCount === 0 && highCount <= 3 ? 'needs-attention' :
|
|
1561
|
+
'non-compliant';
|
|
1562
|
+
const statusConfig = {
|
|
1563
|
+
'compliant': {
|
|
1564
|
+
icon: '✅',
|
|
1565
|
+
label: 'HIPAA Compliant',
|
|
1566
|
+
color: '#10b981',
|
|
1567
|
+
message: 'Your application demonstrates strong HIPAA compliance practices with no critical vulnerabilities detected.',
|
|
1568
|
+
},
|
|
1569
|
+
'needs-attention': {
|
|
1570
|
+
icon: '⚠️',
|
|
1571
|
+
label: 'Requires Attention',
|
|
1572
|
+
color: '#f59e0b',
|
|
1573
|
+
message: 'Your application has areas requiring attention to achieve full HIPAA compliance. Address high-severity findings promptly.',
|
|
1574
|
+
},
|
|
1575
|
+
'non-compliant': {
|
|
1576
|
+
icon: '❌',
|
|
1577
|
+
label: 'Non-Compliant',
|
|
1578
|
+
color: '#dc2626',
|
|
1579
|
+
message: 'Critical HIPAA compliance issues detected. Immediate action required to protect PHI and avoid regulatory penalties.',
|
|
1580
|
+
},
|
|
1581
|
+
};
|
|
1582
|
+
const status = statusConfig[overallStatus];
|
|
1583
|
+
// Get top priority findings (critical and high, max 5)
|
|
1584
|
+
const priorityFindings = report.findings
|
|
1585
|
+
.filter(f => f.severity === 'critical' || f.severity === 'high')
|
|
1586
|
+
.slice(0, 5);
|
|
1587
|
+
// Calculate risk exposure
|
|
1588
|
+
const riskLevel = criticalCount > 5 ? 'Critical' :
|
|
1589
|
+
criticalCount > 0 ? 'High' :
|
|
1590
|
+
highCount > 10 ? 'High' :
|
|
1591
|
+
highCount > 0 ? 'Medium' : 'Low';
|
|
1592
|
+
const riskColor = riskLevel === 'Critical' ? '#dc2626' :
|
|
1593
|
+
riskLevel === 'High' ? '#ea580c' :
|
|
1594
|
+
riskLevel === 'Medium' ? '#f59e0b' : '#10b981';
|
|
1595
|
+
// Categorize findings
|
|
1596
|
+
const findingsByCategory = report.findings.reduce((acc, f) => {
|
|
1597
|
+
acc[f.category] = (acc[f.category] || 0) + 1;
|
|
1598
|
+
return acc;
|
|
1599
|
+
}, {});
|
|
1600
|
+
const topCategories = Object.entries(findingsByCategory)
|
|
1601
|
+
.sort((a, b) => b[1] - a[1])
|
|
1602
|
+
.slice(0, 3);
|
|
1603
|
+
return `
|
|
1604
|
+
<div class="executive-summary-section">
|
|
1605
|
+
<div class="exec-header">
|
|
1606
|
+
<h1>Executive Summary</h1>
|
|
1607
|
+
<p class="exec-subtitle">HIPAA Compliance Assessment Report</p>
|
|
1608
|
+
<div class="exec-meta">
|
|
1609
|
+
<span>Generated: ${new Date(report.timestamp).toLocaleDateString('en-US', {
|
|
1610
|
+
year: 'numeric',
|
|
1611
|
+
month: 'long',
|
|
1612
|
+
day: 'numeric'
|
|
1613
|
+
})}</span>
|
|
1614
|
+
<span>•</span>
|
|
1615
|
+
<span>Scanned: ${report.scannedFiles} files</span>
|
|
1616
|
+
<span>•</span>
|
|
1617
|
+
<span>Duration: ${(report.scanDuration / 1000).toFixed(2)}s</span>
|
|
1618
|
+
</div>
|
|
1619
|
+
</div>
|
|
1620
|
+
|
|
1621
|
+
<div class="exec-status-card" style="border-left-color: ${status.color}">
|
|
1622
|
+
<div class="exec-status-icon" style="color: ${status.color}">${status.icon}</div>
|
|
1623
|
+
<div class="exec-status-content">
|
|
1624
|
+
<h2 style="color: ${status.color}">${status.label}</h2>
|
|
1625
|
+
<p>${status.message}</p>
|
|
1626
|
+
</div>
|
|
1627
|
+
</div>
|
|
1628
|
+
|
|
1629
|
+
<div class="exec-grid">
|
|
1630
|
+
<div class="exec-metric-card">
|
|
1631
|
+
<div class="exec-metric-label">Risk Exposure</div>
|
|
1632
|
+
<div class="exec-metric-value" style="color: ${riskColor}">${riskLevel}</div>
|
|
1633
|
+
<div class="exec-metric-desc">Overall risk level based on findings severity</div>
|
|
1634
|
+
</div>
|
|
1635
|
+
|
|
1636
|
+
<div class="exec-metric-card">
|
|
1637
|
+
<div class="exec-metric-label">Total Findings</div>
|
|
1638
|
+
<div class="exec-metric-value">${totalFindings}</div>
|
|
1639
|
+
<div class="exec-metric-desc">
|
|
1640
|
+
<span style="color: #dc2626; font-weight: 600;">${criticalCount} critical</span>
|
|
1641
|
+
${highCount > 0 ? `, <span style="color: #ea580c; font-weight: 600;">${highCount} high</span>` : ''}
|
|
1642
|
+
</div>
|
|
1643
|
+
</div>
|
|
1644
|
+
|
|
1645
|
+
<div class="exec-metric-card">
|
|
1646
|
+
<div class="exec-metric-label">Files Analyzed</div>
|
|
1647
|
+
<div class="exec-metric-value">${report.scannedFiles}</div>
|
|
1648
|
+
<div class="exec-metric-desc">Source code files scanned for HIPAA compliance</div>
|
|
1649
|
+
</div>
|
|
1650
|
+
|
|
1651
|
+
<div class="exec-metric-card">
|
|
1652
|
+
<div class="exec-metric-label">Technology Stack</div>
|
|
1653
|
+
<div class="exec-metric-value" style="font-size: 1.25rem;">
|
|
1654
|
+
${report.stack?.frameworkDisplay || 'N/A'}
|
|
1655
|
+
</div>
|
|
1656
|
+
<div class="exec-metric-desc">
|
|
1657
|
+
${report.stack?.databaseDisplay || 'Unknown DB'} + ${report.stack?.authDisplay || 'Unknown Auth'}
|
|
1658
|
+
</div>
|
|
1659
|
+
</div>
|
|
1660
|
+
</div>
|
|
1661
|
+
|
|
1662
|
+
${priorityFindings.length > 0 ? `
|
|
1663
|
+
<div class="exec-priority-section">
|
|
1664
|
+
<h3>🚨 Priority Action Items</h3>
|
|
1665
|
+
<p class="exec-priority-intro">
|
|
1666
|
+
The following issues require immediate attention to ensure HIPAA compliance:
|
|
1667
|
+
</p>
|
|
1668
|
+
<div class="exec-priority-list">
|
|
1669
|
+
${priorityFindings.map((f, idx) => `
|
|
1670
|
+
<div class="exec-priority-item" style="border-left-color: ${f.severity === 'critical' ? '#dc2626' : '#ea580c'}">
|
|
1671
|
+
<div class="exec-priority-number">${idx + 1}</div>
|
|
1672
|
+
<div class="exec-priority-content">
|
|
1673
|
+
<div class="exec-priority-header">
|
|
1674
|
+
<span class="exec-priority-badge" style="background: ${f.severity === 'critical' ? '#dc2626' : '#ea580c'}">
|
|
1675
|
+
${f.severity.toUpperCase()}
|
|
1676
|
+
</span>
|
|
1677
|
+
<span class="exec-priority-title">${escapeHtml(f.title)}</span>
|
|
1678
|
+
</div>
|
|
1679
|
+
<div class="exec-priority-desc">${escapeHtml(f.description)}</div>
|
|
1680
|
+
<div class="exec-priority-action">
|
|
1681
|
+
<strong>Action:</strong> ${escapeHtml(f.recommendation)}
|
|
1682
|
+
</div>
|
|
1683
|
+
<div class="exec-priority-ref">
|
|
1684
|
+
<span class="exec-file-ref">${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</span>
|
|
1685
|
+
${f.hipaaReference ? `<span class="exec-hipaa-ref">${escapeHtml(f.hipaaReference)}</span>` : ''}
|
|
1686
|
+
</div>
|
|
1687
|
+
</div>
|
|
1688
|
+
</div>
|
|
1689
|
+
`).join('')}
|
|
1690
|
+
</div>
|
|
1691
|
+
</div>
|
|
1692
|
+
` : ''}
|
|
1693
|
+
|
|
1694
|
+
${topCategories.length > 0 ? `
|
|
1695
|
+
<div class="exec-categories-section">
|
|
1696
|
+
<h3>📊 Top Affected Areas</h3>
|
|
1697
|
+
<div class="exec-category-bars">
|
|
1698
|
+
${topCategories.map(([category, count]) => {
|
|
1699
|
+
const categoryLabel = formatCategory(category);
|
|
1700
|
+
const percentage = Math.round((count / totalFindings) * 100);
|
|
1701
|
+
return `
|
|
1702
|
+
<div class="exec-category-item">
|
|
1703
|
+
<div class="exec-category-header">
|
|
1704
|
+
<span class="exec-category-name">${escapeHtml(categoryLabel)}</span>
|
|
1705
|
+
<span class="exec-category-count">${count} finding${count !== 1 ? 's' : ''} (${percentage}%)</span>
|
|
1706
|
+
</div>
|
|
1707
|
+
<div class="exec-category-bar-bg">
|
|
1708
|
+
<div class="exec-category-bar" style="width: ${percentage}%"></div>
|
|
1709
|
+
</div>
|
|
1710
|
+
</div>
|
|
1711
|
+
`;
|
|
1712
|
+
}).join('')}
|
|
1713
|
+
</div>
|
|
1714
|
+
</div>
|
|
1715
|
+
` : ''}
|
|
1716
|
+
|
|
1717
|
+
<div class="exec-recommendations">
|
|
1718
|
+
<h3>💡 Recommended Next Steps</h3>
|
|
1719
|
+
<ol class="exec-recommendations-list">
|
|
1720
|
+
${criticalCount > 0 ? `
|
|
1721
|
+
<li>
|
|
1722
|
+
<strong>Immediate (24-48 hours):</strong> Address all ${criticalCount} critical finding${criticalCount !== 1 ? 's' : ''}
|
|
1723
|
+
that could lead to PHI exposure or regulatory violations.
|
|
1724
|
+
</li>
|
|
1725
|
+
` : ''}
|
|
1726
|
+
${highCount > 0 ? `
|
|
1727
|
+
<li>
|
|
1728
|
+
<strong>Short-term (1-2 weeks):</strong> Resolve ${highCount} high-severity issue${highCount !== 1 ? 's' : ''}
|
|
1729
|
+
to strengthen security posture and reduce audit risk.
|
|
1730
|
+
</li>
|
|
1731
|
+
` : ''}
|
|
1732
|
+
<li>
|
|
1733
|
+
<strong>Review BAAs:</strong> Ensure Business Associate Agreements are in place for all external service providers
|
|
1734
|
+
identified in the ePHI Data Flow Map section.
|
|
1735
|
+
</li>
|
|
1736
|
+
<li>
|
|
1737
|
+
<strong>Document remediation:</strong> Track all fixes and maintain audit logs for compliance documentation.
|
|
1738
|
+
</li>
|
|
1739
|
+
<li>
|
|
1740
|
+
<strong>Schedule regular scans:</strong> Run vlayer scans weekly to monitor ongoing compliance and catch new issues early.
|
|
1741
|
+
</li>
|
|
1742
|
+
</ol>
|
|
1743
|
+
</div>
|
|
1744
|
+
|
|
1745
|
+
${criticalCount === 0 && highCount === 0 ? `
|
|
1746
|
+
<div class="exec-congrats">
|
|
1747
|
+
<div class="exec-congrats-icon">🎉</div>
|
|
1748
|
+
<div class="exec-congrats-content">
|
|
1749
|
+
<h3>Excellent Work!</h3>
|
|
1750
|
+
<p>
|
|
1751
|
+
No critical or high-severity issues detected. Continue monitoring with regular scans
|
|
1752
|
+
and stay current with HIPAA regulation updates.
|
|
1753
|
+
</p>
|
|
1754
|
+
</div>
|
|
1755
|
+
</div>
|
|
1756
|
+
` : ''}
|
|
1757
|
+
</div>
|
|
1758
|
+
`;
|
|
1759
|
+
}
|
|
1760
|
+
function renderBackupRecoveryGuideHtml(stack) {
|
|
1761
|
+
let guideContent = '';
|
|
1762
|
+
let dbType = 'unknown';
|
|
1763
|
+
let dbDisplay = stack?.databaseDisplay || 'Unknown';
|
|
1764
|
+
// Detect database type from stack
|
|
1765
|
+
const database = stack?.database || 'unknown';
|
|
1766
|
+
if (database.includes('supabase')) {
|
|
1767
|
+
dbType = 'supabase';
|
|
1768
|
+
guideContent = `
|
|
1769
|
+
<div class="backup-guide-card">
|
|
1770
|
+
<div class="backup-guide-header">
|
|
1771
|
+
<span class="backup-guide-icon">🗄️</span>
|
|
1772
|
+
<h4>Supabase Database Backup Configuration</h4>
|
|
1773
|
+
</div>
|
|
1774
|
+
<div class="backup-guide-content">
|
|
1775
|
+
<div class="backup-step">
|
|
1776
|
+
<div class="backup-step-number">1</div>
|
|
1777
|
+
<div class="backup-step-content">
|
|
1778
|
+
<strong>Enable Point-in-Time Recovery (PITR)</strong>
|
|
1779
|
+
<p>Navigate to Dashboard → Settings → Database → Enable PITR</p>
|
|
1780
|
+
<div class="backup-code-block">
|
|
1781
|
+
<code>PITR allows you to restore your database to any point within the last 7 days</code>
|
|
1782
|
+
</div>
|
|
1783
|
+
</div>
|
|
1784
|
+
</div>
|
|
1785
|
+
|
|
1786
|
+
<div class="backup-step">
|
|
1787
|
+
<div class="backup-step-number">2</div>
|
|
1788
|
+
<div class="backup-step-content">
|
|
1789
|
+
<strong>Configure Daily Automated Backups</strong>
|
|
1790
|
+
<p>Supabase Pro+ plans include daily backups. Verify in your project settings.</p>
|
|
1791
|
+
<ul class="backup-checklist">
|
|
1792
|
+
<li>✓ Backup retention: 7-30 days (depending on plan)</li>
|
|
1793
|
+
<li>✓ Automated daily snapshots</li>
|
|
1794
|
+
<li>✓ Geographic redundancy enabled</li>
|
|
1795
|
+
</ul>
|
|
1796
|
+
</div>
|
|
1797
|
+
</div>
|
|
1798
|
+
|
|
1799
|
+
<div class="backup-step">
|
|
1800
|
+
<div class="backup-step-number">3</div>
|
|
1801
|
+
<div class="backup-step-content">
|
|
1802
|
+
<strong>Test Restore Procedure (Quarterly)</strong>
|
|
1803
|
+
<p>Regularly verify backup integrity by performing test restores:</p>
|
|
1804
|
+
<div class="backup-code-block">
|
|
1805
|
+
<pre><code># Create test restore
|
|
1806
|
+
# Dashboard → Database → Backups → Restore to new project
|
|
1807
|
+
# Verify data integrity and application functionality</code></pre>
|
|
1808
|
+
</div>
|
|
1809
|
+
</div>
|
|
1810
|
+
</div>
|
|
1811
|
+
|
|
1812
|
+
<div class="backup-step">
|
|
1813
|
+
<div class="backup-step-number">4</div>
|
|
1814
|
+
<div class="backup-step-content">
|
|
1815
|
+
<strong>Additional Manual Backup (Optional)</strong>
|
|
1816
|
+
<div class="backup-code-block">
|
|
1817
|
+
<pre><code># Using pg_dump for additional backup
|
|
1818
|
+
pg_dump "postgresql://[user]:[password]@[host]:[port]/[database]" > backup_\$(date +%Y%m%d).sql
|
|
1819
|
+
|
|
1820
|
+
# Upload to secure offsite storage
|
|
1821
|
+
aws s3 cp backup_\$(date +%Y%m%d).sql s3://your-backup-bucket/</code></pre>
|
|
1822
|
+
</div>
|
|
1823
|
+
</div>
|
|
1824
|
+
</div>
|
|
1825
|
+
</div>
|
|
1826
|
+
</div>
|
|
1827
|
+
`;
|
|
1828
|
+
}
|
|
1829
|
+
else if (database.includes('prisma') || database.includes('postgres')) {
|
|
1830
|
+
dbType = 'postgresql';
|
|
1831
|
+
guideContent = `
|
|
1832
|
+
<div class="backup-guide-card">
|
|
1833
|
+
<div class="backup-guide-header">
|
|
1834
|
+
<span class="backup-guide-icon">🐘</span>
|
|
1835
|
+
<h4>PostgreSQL + Prisma Backup Configuration</h4>
|
|
1836
|
+
</div>
|
|
1837
|
+
<div class="backup-guide-content">
|
|
1838
|
+
<div class="backup-step">
|
|
1839
|
+
<div class="backup-step-number">1</div>
|
|
1840
|
+
<div class="backup-step-content">
|
|
1841
|
+
<strong>Create Backup Script</strong>
|
|
1842
|
+
<p>Save this script as <code>backup-db.sh</code> in your project root:</p>
|
|
1843
|
+
<div class="backup-code-block">
|
|
1844
|
+
<pre><code>#!/bin/bash
|
|
1845
|
+
# PostgreSQL Backup Script for HIPAA Compliance
|
|
1846
|
+
|
|
1847
|
+
BACKUP_DIR="/path/to/backups"
|
|
1848
|
+
TIMESTAMP=\$(date +%Y%m%d_%H%M%S)
|
|
1849
|
+
BACKUP_FILE="\${BACKUP_DIR}/backup_\${TIMESTAMP}.sql"
|
|
1850
|
+
|
|
1851
|
+
# Create backup directory if not exists
|
|
1852
|
+
mkdir -p \$BACKUP_DIR
|
|
1853
|
+
|
|
1854
|
+
# Perform backup
|
|
1855
|
+
pg_dump \$DATABASE_URL > \$BACKUP_FILE
|
|
1856
|
+
|
|
1857
|
+
# Compress backup
|
|
1858
|
+
gzip \$BACKUP_FILE
|
|
1859
|
+
|
|
1860
|
+
# Upload to offsite storage (S3 example)
|
|
1861
|
+
aws s3 cp \${BACKUP_FILE}.gz s3://your-backup-bucket/postgresql/
|
|
1862
|
+
|
|
1863
|
+
# Keep only last 30 days of local backups
|
|
1864
|
+
find \$BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete
|
|
1865
|
+
|
|
1866
|
+
echo "Backup completed: \${BACKUP_FILE}.gz"</code></pre>
|
|
1867
|
+
</div>
|
|
1868
|
+
</div>
|
|
1869
|
+
</div>
|
|
1870
|
+
|
|
1871
|
+
<div class="backup-step">
|
|
1872
|
+
<div class="backup-step-number">2</div>
|
|
1873
|
+
<div class="backup-step-content">
|
|
1874
|
+
<strong>Schedule via Cron (Daily 2 AM)</strong>
|
|
1875
|
+
<div class="backup-code-block">
|
|
1876
|
+
<pre><code># Add to crontab: crontab -e
|
|
1877
|
+
0 2 * * * /path/to/backup-db.sh >> /var/log/backup.log 2>&1</code></pre>
|
|
1878
|
+
</div>
|
|
1879
|
+
</div>
|
|
1880
|
+
</div>
|
|
1881
|
+
|
|
1882
|
+
<div class="backup-step">
|
|
1883
|
+
<div class="backup-step-number">3</div>
|
|
1884
|
+
<div class="backup-step-content">
|
|
1885
|
+
<strong>Test Restore Monthly</strong>
|
|
1886
|
+
<div class="backup-code-block">
|
|
1887
|
+
<pre><code># Download backup from S3
|
|
1888
|
+
aws s3 cp s3://your-backup-bucket/postgresql/backup_YYYYMMDD.sql.gz .
|
|
1889
|
+
|
|
1890
|
+
# Decompress
|
|
1891
|
+
gunzip backup_YYYYMMDD.sql.gz
|
|
1892
|
+
|
|
1893
|
+
# Restore to test database
|
|
1894
|
+
psql \$TEST_DATABASE_URL < backup_YYYYMMDD.sql
|
|
1895
|
+
|
|
1896
|
+
# Verify data integrity
|
|
1897
|
+
psql \$TEST_DATABASE_URL -c "SELECT COUNT(*) FROM patients;"</code></pre>
|
|
1898
|
+
</div>
|
|
1899
|
+
</div>
|
|
1900
|
+
</div>
|
|
1901
|
+
|
|
1902
|
+
<div class="backup-step">
|
|
1903
|
+
<div class="backup-step-number">4</div>
|
|
1904
|
+
<div class="backup-step-content">
|
|
1905
|
+
<strong>Offsite Storage Options</strong>
|
|
1906
|
+
<ul class="backup-checklist">
|
|
1907
|
+
<li>✓ AWS S3 with versioning and encryption</li>
|
|
1908
|
+
<li>✓ Google Cloud Storage with lifecycle policies</li>
|
|
1909
|
+
<li>✓ Azure Blob Storage with geo-redundancy</li>
|
|
1910
|
+
<li>✓ Ensure BAA in place with storage provider</li>
|
|
1911
|
+
</ul>
|
|
1912
|
+
</div>
|
|
1913
|
+
</div>
|
|
1914
|
+
</div>
|
|
1915
|
+
</div>
|
|
1916
|
+
`;
|
|
1917
|
+
}
|
|
1918
|
+
else if (database.includes('mongo')) {
|
|
1919
|
+
dbType = 'mongodb';
|
|
1920
|
+
guideContent = `
|
|
1921
|
+
<div class="backup-guide-card">
|
|
1922
|
+
<div class="backup-guide-header">
|
|
1923
|
+
<span class="backup-guide-icon">🍃</span>
|
|
1924
|
+
<h4>MongoDB + Mongoose Backup Configuration</h4>
|
|
1925
|
+
</div>
|
|
1926
|
+
<div class="backup-guide-content">
|
|
1927
|
+
<div class="backup-step">
|
|
1928
|
+
<div class="backup-step-number">1</div>
|
|
1929
|
+
<div class="backup-step-content">
|
|
1930
|
+
<strong>Using MongoDB Atlas (Recommended)</strong>
|
|
1931
|
+
<p>Enable automated backups in Atlas dashboard:</p>
|
|
1932
|
+
<ul class="backup-checklist">
|
|
1933
|
+
<li>✓ Navigate to Project → Backup tab</li>
|
|
1934
|
+
<li>✓ Enable Continuous Cloud Backup</li>
|
|
1935
|
+
<li>✓ Configure snapshot schedule (daily recommended)</li>
|
|
1936
|
+
<li>✓ Set retention policy (30 days minimum for HIPAA)</li>
|
|
1937
|
+
<li>✓ Enable Point-in-Time Restore</li>
|
|
1938
|
+
</ul>
|
|
1939
|
+
</div>
|
|
1940
|
+
</div>
|
|
1941
|
+
|
|
1942
|
+
<div class="backup-step">
|
|
1943
|
+
<div class="backup-step-number">2</div>
|
|
1944
|
+
<div class="backup-step-content">
|
|
1945
|
+
<strong>Manual Backup with mongodump</strong>
|
|
1946
|
+
<p>Create backup script for self-hosted MongoDB:</p>
|
|
1947
|
+
<div class="backup-code-block">
|
|
1948
|
+
<pre><code>#!/bin/bash
|
|
1949
|
+
# MongoDB Backup Script
|
|
1950
|
+
|
|
1951
|
+
TIMESTAMP=\$(date +%Y%m%d_%H%M%S)
|
|
1952
|
+
BACKUP_DIR="/path/to/backups"
|
|
1953
|
+
BACKUP_NAME="mongodb_backup_\${TIMESTAMP}"
|
|
1954
|
+
|
|
1955
|
+
# Perform backup
|
|
1956
|
+
mongodump --uri="\$MONGODB_URI" --out=\${BACKUP_DIR}/\${BACKUP_NAME}
|
|
1957
|
+
|
|
1958
|
+
# Compress
|
|
1959
|
+
tar -czf \${BACKUP_DIR}/\${BACKUP_NAME}.tar.gz -C \${BACKUP_DIR} \${BACKUP_NAME}
|
|
1960
|
+
rm -rf \${BACKUP_DIR}/\${BACKUP_NAME}
|
|
1961
|
+
|
|
1962
|
+
# Upload to S3
|
|
1963
|
+
aws s3 cp \${BACKUP_DIR}/\${BACKUP_NAME}.tar.gz s3://your-backup-bucket/mongodb/
|
|
1964
|
+
|
|
1965
|
+
# Cleanup old local backups (keep 7 days)
|
|
1966
|
+
find \$BACKUP_DIR -name "mongodb_backup_*.tar.gz" -mtime +7 -delete</code></pre>
|
|
1967
|
+
</div>
|
|
1968
|
+
</div>
|
|
1969
|
+
</div>
|
|
1970
|
+
|
|
1971
|
+
<div class="backup-step">
|
|
1972
|
+
<div class="backup-step-number">3</div>
|
|
1973
|
+
<div class="backup-step-content">
|
|
1974
|
+
<strong>Test Restore Quarterly</strong>
|
|
1975
|
+
<div class="backup-code-block">
|
|
1976
|
+
<pre><code># Download and extract backup
|
|
1977
|
+
aws s3 cp s3://your-backup-bucket/mongodb/mongodb_backup_YYYYMMDD.tar.gz .
|
|
1978
|
+
tar -xzf mongodb_backup_YYYYMMDD.tar.gz
|
|
1979
|
+
|
|
1980
|
+
# Restore to test database
|
|
1981
|
+
mongorestore --uri="\$TEST_MONGODB_URI" mongodb_backup_YYYYMMDD/
|
|
1982
|
+
|
|
1983
|
+
# Verify collections
|
|
1984
|
+
mongo \$TEST_MONGODB_URI --eval "db.getCollectionNames()"</code></pre>
|
|
1985
|
+
</div>
|
|
1986
|
+
</div>
|
|
1987
|
+
</div>
|
|
1988
|
+
</div>
|
|
1989
|
+
</div>
|
|
1990
|
+
`;
|
|
1991
|
+
}
|
|
1992
|
+
else {
|
|
1993
|
+
dbType = 'none';
|
|
1994
|
+
guideContent = `
|
|
1995
|
+
<div class="backup-guide-card backup-guide-warning">
|
|
1996
|
+
<div class="backup-guide-header">
|
|
1997
|
+
<span class="backup-guide-icon">⚠️</span>
|
|
1998
|
+
<h4>No Database Detected</h4>
|
|
1999
|
+
</div>
|
|
2000
|
+
<div class="backup-guide-content">
|
|
2001
|
+
<p style="margin-bottom: 1rem;">
|
|
2002
|
+
No database connection was detected in your codebase during the scan.
|
|
2003
|
+
</p>
|
|
2004
|
+
<div class="backup-step">
|
|
2005
|
+
<div class="backup-step-content">
|
|
2006
|
+
<strong>If you are using an external database service:</strong>
|
|
2007
|
+
<ul class="backup-checklist">
|
|
2008
|
+
<li>✓ Configure backup per your database provider's documentation</li>
|
|
2009
|
+
<li>✓ Ensure automated daily backups are enabled</li>
|
|
2010
|
+
<li>✓ Verify backup retention meets HIPAA requirements (30+ days recommended)</li>
|
|
2011
|
+
<li>✓ Test restore procedures quarterly</li>
|
|
2012
|
+
<li>✓ Document backup and restore procedures</li>
|
|
2013
|
+
<li>✓ Ensure Business Associate Agreement (BAA) with provider</li>
|
|
2014
|
+
</ul>
|
|
2015
|
+
</div>
|
|
2016
|
+
</div>
|
|
2017
|
+
<div class="backup-step">
|
|
2018
|
+
<div class="backup-step-content">
|
|
2019
|
+
<strong>Common managed database providers:</strong>
|
|
2020
|
+
<ul style="margin-top: 0.5rem;">
|
|
2021
|
+
<li><strong>AWS RDS:</strong> Automated backups, point-in-time recovery</li>
|
|
2022
|
+
<li><strong>Google Cloud SQL:</strong> Automated backups, on-demand snapshots</li>
|
|
2023
|
+
<li><strong>Azure SQL:</strong> Automated backups with geo-redundancy</li>
|
|
2024
|
+
<li><strong>MongoDB Atlas:</strong> Continuous cloud backup</li>
|
|
2025
|
+
<li><strong>PlanetScale:</strong> Daily automated backups</li>
|
|
2026
|
+
</ul>
|
|
2027
|
+
</div>
|
|
2028
|
+
</div>
|
|
2029
|
+
</div>
|
|
2030
|
+
</div>
|
|
2031
|
+
`;
|
|
2032
|
+
}
|
|
2033
|
+
return `
|
|
2034
|
+
<div class="backup-recovery-section">
|
|
2035
|
+
<div class="backup-header">
|
|
2036
|
+
<h2>💾 Backup & Recovery Guide</h2>
|
|
2037
|
+
<p class="backup-subtitle">
|
|
2038
|
+
Database backup and disaster recovery procedures for HIPAA compliance
|
|
2039
|
+
</p>
|
|
2040
|
+
</div>
|
|
2041
|
+
|
|
2042
|
+
<div class="backup-hipaa-notice">
|
|
2043
|
+
<strong>⚖️ HIPAA NPRM Requirement:</strong> The HIPAA Notice of Proposed Rulemaking requires
|
|
2044
|
+
organizations to maintain the ability to restore ePHI within <strong>72 hours</strong> of a disaster.
|
|
2045
|
+
This guide covers code-level configuration. You must verify actual backup execution,
|
|
2046
|
+
offsite storage, and restore testing independently through operational procedures.
|
|
2047
|
+
</div>
|
|
2048
|
+
|
|
2049
|
+
${guideContent}
|
|
2050
|
+
|
|
2051
|
+
<div class="backup-verification-checklist">
|
|
2052
|
+
<h3>✅ Backup Verification Checklist</h3>
|
|
2053
|
+
<div class="backup-checklist-grid">
|
|
2054
|
+
<div class="backup-checklist-item">
|
|
2055
|
+
<input type="checkbox" id="backup-enabled" />
|
|
2056
|
+
<label for="backup-enabled">
|
|
2057
|
+
<strong>Automated backups enabled</strong>
|
|
2058
|
+
<span>Verify backups run daily without manual intervention</span>
|
|
2059
|
+
</label>
|
|
2060
|
+
</div>
|
|
2061
|
+
<div class="backup-checklist-item">
|
|
2062
|
+
<input type="checkbox" id="backup-offsite" />
|
|
2063
|
+
<label for="backup-offsite">
|
|
2064
|
+
<strong>Offsite storage configured</strong>
|
|
2065
|
+
<span>Backups stored in geographically separate location</span>
|
|
2066
|
+
</label>
|
|
2067
|
+
</div>
|
|
2068
|
+
<div class="backup-checklist-item">
|
|
2069
|
+
<input type="checkbox" id="backup-encrypted" />
|
|
2070
|
+
<label for="backup-encrypted">
|
|
2071
|
+
<strong>Backup encryption enabled</strong>
|
|
2072
|
+
<span>At-rest encryption for all backup files</span>
|
|
2073
|
+
</label>
|
|
2074
|
+
</div>
|
|
2075
|
+
<div class="backup-checklist-item">
|
|
2076
|
+
<input type="checkbox" id="backup-tested" />
|
|
2077
|
+
<label for="backup-tested">
|
|
2078
|
+
<strong>Restore procedure tested</strong>
|
|
2079
|
+
<span>Last restore test: [Document date]</span>
|
|
2080
|
+
</label>
|
|
2081
|
+
</div>
|
|
2082
|
+
<div class="backup-checklist-item">
|
|
2083
|
+
<input type="checkbox" id="backup-retention" />
|
|
2084
|
+
<label for="backup-retention">
|
|
2085
|
+
<strong>Retention policy configured</strong>
|
|
2086
|
+
<span>Minimum 30 days, aligned with business requirements</span>
|
|
2087
|
+
</label>
|
|
2088
|
+
</div>
|
|
2089
|
+
<div class="backup-checklist-item">
|
|
2090
|
+
<input type="checkbox" id="backup-monitoring" />
|
|
2091
|
+
<label for="backup-monitoring">
|
|
2092
|
+
<strong>Backup monitoring/alerts</strong>
|
|
2093
|
+
<span>Notifications for backup failures</span>
|
|
2094
|
+
</label>
|
|
2095
|
+
</div>
|
|
2096
|
+
<div class="backup-checklist-item">
|
|
2097
|
+
<input type="checkbox" id="backup-baa" />
|
|
2098
|
+
<label for="backup-baa">
|
|
2099
|
+
<strong>BAA with storage provider</strong>
|
|
2100
|
+
<span>Business Associate Agreement signed and current</span>
|
|
2101
|
+
</label>
|
|
2102
|
+
</div>
|
|
2103
|
+
<div class="backup-checklist-item">
|
|
2104
|
+
<input type="checkbox" id="backup-documented" />
|
|
2105
|
+
<label for="backup-documented">
|
|
2106
|
+
<strong>Procedures documented</strong>
|
|
2107
|
+
<span>Backup and restore procedures in runbook</span>
|
|
2108
|
+
</label>
|
|
2109
|
+
</div>
|
|
2110
|
+
</div>
|
|
2111
|
+
</div>
|
|
2112
|
+
|
|
2113
|
+
<div class="backup-recovery-timeline">
|
|
2114
|
+
<h3>⏱️ Recovery Time Objectives (RTO)</h3>
|
|
2115
|
+
<div class="rto-grid">
|
|
2116
|
+
<div class="rto-item rto-critical">
|
|
2117
|
+
<div class="rto-label">Critical Data</div>
|
|
2118
|
+
<div class="rto-time">< 4 hours</div>
|
|
2119
|
+
<div class="rto-desc">Patient records, active appointments</div>
|
|
2120
|
+
</div>
|
|
2121
|
+
<div class="rto-item rto-important">
|
|
2122
|
+
<div class="rto-label">Important Data</div>
|
|
2123
|
+
<div class="rto-time">< 24 hours</div>
|
|
2124
|
+
<div class="rto-desc">Billing, historical records</div>
|
|
2125
|
+
</div>
|
|
2126
|
+
<div class="rto-item rto-standard">
|
|
2127
|
+
<div class="rto-label">Standard Data</div>
|
|
2128
|
+
<div class="rto-time">< 72 hours</div>
|
|
2129
|
+
<div class="rto-desc">Analytics, logs, reports (HIPAA max)</div>
|
|
2130
|
+
</div>
|
|
2131
|
+
</div>
|
|
2132
|
+
</div>
|
|
2133
|
+
</div>
|
|
2134
|
+
`;
|
|
2135
|
+
}
|
|
2136
|
+
function renderIncidentResponsePlanHtml(criticalFindings, highFindings) {
|
|
2137
|
+
const hasActiveIncident = criticalFindings > 0;
|
|
2138
|
+
const riskLevel = criticalFindings > 0 ? 'HIGH' : highFindings > 0 ? 'MEDIUM' : 'LOW';
|
|
2139
|
+
return `
|
|
2140
|
+
<div class="incident-response-section">
|
|
2141
|
+
<div class="incident-header">
|
|
2142
|
+
<h2>🚨 Incident Response Plan</h2>
|
|
2143
|
+
<p class="incident-subtitle">
|
|
2144
|
+
HIPAA-compliant incident response and breach notification procedures
|
|
2145
|
+
</p>
|
|
2146
|
+
</div>
|
|
2147
|
+
|
|
2148
|
+
${hasActiveIncident ? `
|
|
2149
|
+
<div class="incident-alert">
|
|
2150
|
+
<div class="incident-alert-icon">⚠️</div>
|
|
2151
|
+
<div class="incident-alert-content">
|
|
2152
|
+
<strong>Active Security Issues Detected</strong>
|
|
2153
|
+
<p>
|
|
2154
|
+
${criticalFindings} critical issue${criticalFindings !== 1 ? 's' : ''} detected that may constitute a security incident.
|
|
2155
|
+
Review findings immediately and follow incident response procedures below.
|
|
2156
|
+
</p>
|
|
2157
|
+
</div>
|
|
2158
|
+
</div>
|
|
2159
|
+
` : ''}
|
|
2160
|
+
|
|
2161
|
+
<div class="incident-hipaa-notice">
|
|
2162
|
+
<strong>📋 HIPAA Breach Notification Rule:</strong> If a breach affects 500 or more individuals,
|
|
2163
|
+
you must notify HHS within <strong>60 days</strong>. For breaches affecting fewer than 500 individuals,
|
|
2164
|
+
notification must be made within 60 days of discovery. Maintain detailed documentation of all incidents.
|
|
2165
|
+
</div>
|
|
2166
|
+
|
|
2167
|
+
<div class="incident-team-section">
|
|
2168
|
+
<h3>👥 Incident Response Team (IRT)</h3>
|
|
2169
|
+
<div class="incident-team-table">
|
|
2170
|
+
<table>
|
|
2171
|
+
<thead>
|
|
2172
|
+
<tr>
|
|
2173
|
+
<th>Role</th>
|
|
2174
|
+
<th>Responsibilities</th>
|
|
2175
|
+
<th>Contact Person</th>
|
|
2176
|
+
<th>Phone/Email</th>
|
|
2177
|
+
</tr>
|
|
2178
|
+
</thead>
|
|
2179
|
+
<tbody>
|
|
2180
|
+
<tr>
|
|
2181
|
+
<td><strong>Incident Commander</strong></td>
|
|
2182
|
+
<td>Overall incident coordination, decision-making authority, external communication</td>
|
|
2183
|
+
<td class="editable-cell"><input type="text" placeholder="Name" class="contact-input" /></td>
|
|
2184
|
+
<td class="editable-cell"><input type="text" placeholder="Contact" class="contact-input" /></td>
|
|
2185
|
+
</tr>
|
|
2186
|
+
<tr>
|
|
2187
|
+
<td><strong>Security Lead</strong></td>
|
|
2188
|
+
<td>Technical investigation, containment actions, forensic analysis</td>
|
|
2189
|
+
<td class="editable-cell"><input type="text" placeholder="Name" class="contact-input" /></td>
|
|
2190
|
+
<td class="editable-cell"><input type="text" placeholder="Contact" class="contact-input" /></td>
|
|
2191
|
+
</tr>
|
|
2192
|
+
<tr>
|
|
2193
|
+
<td><strong>Compliance Officer</strong></td>
|
|
2194
|
+
<td>HIPAA breach assessment, regulatory notifications, documentation</td>
|
|
2195
|
+
<td class="editable-cell"><input type="text" placeholder="Name" class="contact-input" /></td>
|
|
2196
|
+
<td class="editable-cell"><input type="text" placeholder="Contact" class="contact-input" /></td>
|
|
2197
|
+
</tr>
|
|
2198
|
+
<tr>
|
|
2199
|
+
<td><strong>Legal Counsel</strong></td>
|
|
2200
|
+
<td>Legal guidance, law enforcement coordination, liability assessment</td>
|
|
2201
|
+
<td class="editable-cell"><input type="text" placeholder="Name" class="contact-input" /></td>
|
|
2202
|
+
<td class="editable-cell"><input type="text" placeholder="Contact" class="contact-input" /></td>
|
|
2203
|
+
</tr>
|
|
2204
|
+
<tr>
|
|
2205
|
+
<td><strong>Communications Lead</strong></td>
|
|
2206
|
+
<td>Patient notifications, media relations, stakeholder communication</td>
|
|
2207
|
+
<td class="editable-cell"><input type="text" placeholder="Name" class="contact-input" /></td>
|
|
2208
|
+
<td class="editable-cell"><input type="text" placeholder="Contact" class="contact-input" /></td>
|
|
2209
|
+
</tr>
|
|
2210
|
+
</tbody>
|
|
2211
|
+
</table>
|
|
2212
|
+
</div>
|
|
2213
|
+
</div>
|
|
2214
|
+
|
|
2215
|
+
<div class="incident-severity-section">
|
|
2216
|
+
<h3>📊 Incident Severity Levels</h3>
|
|
2217
|
+
<div class="severity-grid">
|
|
2218
|
+
<div class="severity-card severity-critical">
|
|
2219
|
+
<div class="severity-badge">P0 - CRITICAL</div>
|
|
2220
|
+
<div class="severity-examples">
|
|
2221
|
+
<strong>Examples:</strong>
|
|
2222
|
+
<ul>
|
|
2223
|
+
<li>PHI exposed to unauthorized parties</li>
|
|
2224
|
+
<li>Ransomware affecting production systems</li>
|
|
2225
|
+
<li>Active data breach in progress</li>
|
|
2226
|
+
<li>Complete system compromise</li>
|
|
2227
|
+
</ul>
|
|
2228
|
+
</div>
|
|
2229
|
+
<div class="severity-response">
|
|
2230
|
+
<strong>Response Time:</strong> Immediate (within 15 minutes)
|
|
2231
|
+
</div>
|
|
2232
|
+
</div>
|
|
2233
|
+
|
|
2234
|
+
<div class="severity-card severity-high">
|
|
2235
|
+
<div class="severity-badge">P1 - HIGH</div>
|
|
2236
|
+
<div class="severity-examples">
|
|
2237
|
+
<strong>Examples:</strong>
|
|
2238
|
+
<ul>
|
|
2239
|
+
<li>Unauthorized access attempt detected</li>
|
|
2240
|
+
<li>Malware on non-production systems</li>
|
|
2241
|
+
<li>Potential PHI exposure (unconfirmed)</li>
|
|
2242
|
+
<li>DDoS attack affecting availability</li>
|
|
2243
|
+
</ul>
|
|
2244
|
+
</div>
|
|
2245
|
+
<div class="severity-response">
|
|
2246
|
+
<strong>Response Time:</strong> Within 1 hour
|
|
2247
|
+
</div>
|
|
2248
|
+
</div>
|
|
2249
|
+
|
|
2250
|
+
<div class="severity-card severity-medium">
|
|
2251
|
+
<div class="severity-badge">P2 - MEDIUM</div>
|
|
2252
|
+
<div class="severity-examples">
|
|
2253
|
+
<strong>Examples:</strong>
|
|
2254
|
+
<ul>
|
|
2255
|
+
<li>Suspicious login activity</li>
|
|
2256
|
+
<li>Policy violation detected</li>
|
|
2257
|
+
<li>Non-PHI data anomaly</li>
|
|
2258
|
+
<li>Failed security control</li>
|
|
2259
|
+
</ul>
|
|
2260
|
+
</div>
|
|
2261
|
+
<div class="severity-response">
|
|
2262
|
+
<strong>Response Time:</strong> Within 4 hours
|
|
2263
|
+
</div>
|
|
2264
|
+
</div>
|
|
2265
|
+
|
|
2266
|
+
<div class="severity-card severity-low">
|
|
2267
|
+
<div class="severity-badge">P3 - LOW</div>
|
|
2268
|
+
<div class="severity-examples">
|
|
2269
|
+
<strong>Examples:</strong>
|
|
2270
|
+
<ul>
|
|
2271
|
+
<li>Minor configuration issue</li>
|
|
2272
|
+
<li>Informational security alert</li>
|
|
2273
|
+
<li>Routine security event</li>
|
|
2274
|
+
<li>Non-urgent vulnerability</li>
|
|
2275
|
+
</ul>
|
|
2276
|
+
</div>
|
|
2277
|
+
<div class="severity-response">
|
|
2278
|
+
<strong>Response Time:</strong> Within 24 hours
|
|
2279
|
+
</div>
|
|
2280
|
+
</div>
|
|
2281
|
+
</div>
|
|
2282
|
+
</div>
|
|
2283
|
+
|
|
2284
|
+
<div class="incident-phases-section">
|
|
2285
|
+
<h3>🔄 Incident Response Phases</h3>
|
|
2286
|
+
|
|
2287
|
+
<div class="phase-card">
|
|
2288
|
+
<div class="phase-number">1</div>
|
|
2289
|
+
<div class="phase-content">
|
|
2290
|
+
<h4>Detection & Analysis</h4>
|
|
2291
|
+
<div class="phase-checklist">
|
|
2292
|
+
<div class="phase-item">
|
|
2293
|
+
<strong>⚡ Immediate Actions (0-15 min):</strong>
|
|
2294
|
+
<ul>
|
|
2295
|
+
<li>Document initial detection time and source</li>
|
|
2296
|
+
<li>Assess severity using criteria above</li>
|
|
2297
|
+
<li>Alert Incident Commander and Security Lead</li>
|
|
2298
|
+
<li>Begin incident log documentation</li>
|
|
2299
|
+
</ul>
|
|
2300
|
+
</div>
|
|
2301
|
+
<div class="phase-item">
|
|
2302
|
+
<strong>🔍 Investigation (15 min - 2 hours):</strong>
|
|
2303
|
+
<ul>
|
|
2304
|
+
<li>Determine scope: systems affected, data involved</li>
|
|
2305
|
+
<li>Identify if PHI is involved or at risk</li>
|
|
2306
|
+
<li>Collect initial evidence (logs, screenshots, network captures)</li>
|
|
2307
|
+
<li>Determine root cause (preliminary)</li>
|
|
2308
|
+
</ul>
|
|
2309
|
+
</div>
|
|
2310
|
+
</div>
|
|
2311
|
+
</div>
|
|
2312
|
+
</div>
|
|
2313
|
+
|
|
2314
|
+
<div class="phase-card">
|
|
2315
|
+
<div class="phase-number">2</div>
|
|
2316
|
+
<div class="phase-content">
|
|
2317
|
+
<h4>Containment</h4>
|
|
2318
|
+
<div class="phase-checklist">
|
|
2319
|
+
<div class="phase-item">
|
|
2320
|
+
<strong>🛡️ Short-term Containment:</strong>
|
|
2321
|
+
<ul>
|
|
2322
|
+
<li>Isolate affected systems from network</li>
|
|
2323
|
+
<li>Disable compromised user accounts</li>
|
|
2324
|
+
<li>Block malicious IPs/domains at firewall</li>
|
|
2325
|
+
<li>Preserve evidence before taking systems offline</li>
|
|
2326
|
+
</ul>
|
|
2327
|
+
</div>
|
|
2328
|
+
<div class="phase-item">
|
|
2329
|
+
<strong>🔒 Long-term Containment:</strong>
|
|
2330
|
+
<ul>
|
|
2331
|
+
<li>Apply temporary security patches</li>
|
|
2332
|
+
<li>Implement additional monitoring</li>
|
|
2333
|
+
<li>Establish secure backup systems</li>
|
|
2334
|
+
<li>Document all containment actions</li>
|
|
2335
|
+
</ul>
|
|
2336
|
+
</div>
|
|
2337
|
+
</div>
|
|
2338
|
+
</div>
|
|
2339
|
+
</div>
|
|
2340
|
+
|
|
2341
|
+
<div class="phase-card">
|
|
2342
|
+
<div class="phase-number">3</div>
|
|
2343
|
+
<div class="phase-content">
|
|
2344
|
+
<h4>Eradication</h4>
|
|
2345
|
+
<div class="phase-checklist">
|
|
2346
|
+
<ul>
|
|
2347
|
+
<li>Remove malware from all affected systems</li>
|
|
2348
|
+
<li>Close security vulnerabilities exploited</li>
|
|
2349
|
+
<li>Reset all compromised credentials</li>
|
|
2350
|
+
<li>Apply permanent security patches</li>
|
|
2351
|
+
<li>Verify threat has been completely removed</li>
|
|
2352
|
+
</ul>
|
|
2353
|
+
</div>
|
|
2354
|
+
</div>
|
|
2355
|
+
</div>
|
|
2356
|
+
|
|
2357
|
+
<div class="phase-card">
|
|
2358
|
+
<div class="phase-number">4</div>
|
|
2359
|
+
<div class="phase-content">
|
|
2360
|
+
<h4>Recovery</h4>
|
|
2361
|
+
<div class="phase-checklist">
|
|
2362
|
+
<ul>
|
|
2363
|
+
<li>Restore systems from clean backups</li>
|
|
2364
|
+
<li>Verify system integrity before reconnecting</li>
|
|
2365
|
+
<li>Gradually restore services to production</li>
|
|
2366
|
+
<li>Monitor closely for signs of re-infection</li>
|
|
2367
|
+
<li>Validate business operations are normal</li>
|
|
2368
|
+
</ul>
|
|
2369
|
+
</div>
|
|
2370
|
+
</div>
|
|
2371
|
+
</div>
|
|
2372
|
+
|
|
2373
|
+
<div class="phase-card">
|
|
2374
|
+
<div class="phase-number">5</div>
|
|
2375
|
+
<div class="phase-content">
|
|
2376
|
+
<h4>Post-Incident Activity</h4>
|
|
2377
|
+
<div class="phase-checklist">
|
|
2378
|
+
<ul>
|
|
2379
|
+
<li>Conduct post-incident review meeting</li>
|
|
2380
|
+
<li>Document lessons learned</li>
|
|
2381
|
+
<li>Update incident response procedures</li>
|
|
2382
|
+
<li>Implement preventive measures</li>
|
|
2383
|
+
<li>Complete HIPAA breach determination</li>
|
|
2384
|
+
<li>File required regulatory notifications</li>
|
|
2385
|
+
</ul>
|
|
2386
|
+
</div>
|
|
2387
|
+
</div>
|
|
2388
|
+
</div>
|
|
2389
|
+
</div>
|
|
2390
|
+
|
|
2391
|
+
<div class="incident-breach-timeline">
|
|
2392
|
+
<h3>⏰ HIPAA Breach Notification Timeline</h3>
|
|
2393
|
+
<div class="timeline-container">
|
|
2394
|
+
<div class="timeline-item">
|
|
2395
|
+
<div class="timeline-marker timeline-discovery"></div>
|
|
2396
|
+
<div class="timeline-content">
|
|
2397
|
+
<strong>Day 0: Discovery</strong>
|
|
2398
|
+
<p>Incident discovered. Begin investigation and documentation.</p>
|
|
2399
|
+
</div>
|
|
2400
|
+
</div>
|
|
2401
|
+
<div class="timeline-item">
|
|
2402
|
+
<div class="timeline-marker timeline-assessment"></div>
|
|
2403
|
+
<div class="timeline-content">
|
|
2404
|
+
<strong>Days 1-5: Assessment</strong>
|
|
2405
|
+
<p>Complete breach risk assessment. Determine if PHI compromised.</p>
|
|
2406
|
+
</div>
|
|
2407
|
+
</div>
|
|
2408
|
+
<div class="timeline-item">
|
|
2409
|
+
<div class="timeline-marker timeline-notification"></div>
|
|
2410
|
+
<div class="timeline-content">
|
|
2411
|
+
<strong>Within 60 Days: Notifications</strong>
|
|
2412
|
+
<p>
|
|
2413
|
+
• Notify affected individuals (written notice)<br/>
|
|
2414
|
+
• Notify HHS if 500+ individuals affected<br/>
|
|
2415
|
+
• Notify media if 500+ individuals in same state
|
|
2416
|
+
</p>
|
|
2417
|
+
</div>
|
|
2418
|
+
</div>
|
|
2419
|
+
<div class="timeline-item">
|
|
2420
|
+
<div class="timeline-marker timeline-documentation"></div>
|
|
2421
|
+
<div class="timeline-content">
|
|
2422
|
+
<strong>Ongoing: Documentation</strong>
|
|
2423
|
+
<p>Maintain records for 6 years minimum per HIPAA requirements.</p>
|
|
2424
|
+
</div>
|
|
2425
|
+
</div>
|
|
2426
|
+
</div>
|
|
2427
|
+
</div>
|
|
2428
|
+
|
|
2429
|
+
<div class="incident-contacts-section">
|
|
2430
|
+
<h3>📞 Critical Contacts</h3>
|
|
2431
|
+
<div class="contacts-grid">
|
|
2432
|
+
<div class="contact-card">
|
|
2433
|
+
<div class="contact-title">HHS Office for Civil Rights</div>
|
|
2434
|
+
<div class="contact-info">
|
|
2435
|
+
<div>Website: <a href="https://ocrportal.hhs.gov/ocr/breach/wizard_breach.jsf" target="_blank">ocrportal.hhs.gov</a></div>
|
|
2436
|
+
<div>Phone: 1-800-368-1019</div>
|
|
2437
|
+
</div>
|
|
2438
|
+
</div>
|
|
2439
|
+
<div class="contact-card">
|
|
2440
|
+
<div class="contact-title">FBI Cyber Division</div>
|
|
2441
|
+
<div class="contact-info">
|
|
2442
|
+
<div>Website: <a href="https://www.fbi.gov/contact-us" target="_blank">fbi.gov/contact-us</a></div>
|
|
2443
|
+
<div>IC3: <a href="https://www.ic3.gov" target="_blank">ic3.gov</a></div>
|
|
2444
|
+
</div>
|
|
2445
|
+
</div>
|
|
2446
|
+
<div class="contact-card">
|
|
2447
|
+
<div class="contact-title">Business Associates</div>
|
|
2448
|
+
<div class="contact-info editable-contact">
|
|
2449
|
+
<input type="text" placeholder="Primary BA Contact" class="contact-input-wide" />
|
|
2450
|
+
<input type="text" placeholder="Contact Info" class="contact-input-wide" />
|
|
2451
|
+
</div>
|
|
2452
|
+
</div>
|
|
2453
|
+
<div class="contact-card">
|
|
2454
|
+
<div class="contact-title">Cyber Insurance Provider</div>
|
|
2455
|
+
<div class="contact-info editable-contact">
|
|
2456
|
+
<input type="text" placeholder="Insurance Company" class="contact-input-wide" />
|
|
2457
|
+
<input type="text" placeholder="Policy # / Phone" class="contact-input-wide" />
|
|
2458
|
+
</div>
|
|
2459
|
+
</div>
|
|
2460
|
+
</div>
|
|
2461
|
+
</div>
|
|
2462
|
+
|
|
2463
|
+
<div class="incident-log-template">
|
|
2464
|
+
<h3>📝 Incident Log Template</h3>
|
|
2465
|
+
<p class="template-note">Use this template to document security incidents. Maintain logs for 6 years minimum.</p>
|
|
2466
|
+
<div class="log-template-box">
|
|
2467
|
+
<div class="log-field">
|
|
2468
|
+
<strong>Incident ID:</strong> <span class="log-placeholder">[AUTO-GENERATED or YYYY-MM-DD-###]</span>
|
|
2469
|
+
</div>
|
|
2470
|
+
<div class="log-field">
|
|
2471
|
+
<strong>Date/Time Discovered:</strong> <span class="log-placeholder">[YYYY-MM-DD HH:MM UTC]</span>
|
|
2472
|
+
</div>
|
|
2473
|
+
<div class="log-field">
|
|
2474
|
+
<strong>Discovered By:</strong> <span class="log-placeholder">[Name, Role]</span>
|
|
2475
|
+
</div>
|
|
2476
|
+
<div class="log-field">
|
|
2477
|
+
<strong>Severity Level:</strong> <span class="log-placeholder">[P0/P1/P2/P3]</span>
|
|
2478
|
+
</div>
|
|
2479
|
+
<div class="log-field">
|
|
2480
|
+
<strong>Systems Affected:</strong> <span class="log-placeholder">[List all systems, applications, databases]</span>
|
|
2481
|
+
</div>
|
|
2482
|
+
<div class="log-field">
|
|
2483
|
+
<strong>PHI Involved:</strong> <span class="log-placeholder">[YES/NO/UNKNOWN] - If yes, describe data types and # of individuals</span>
|
|
2484
|
+
</div>
|
|
2485
|
+
<div class="log-field">
|
|
2486
|
+
<strong>Initial Description:</strong> <span class="log-placeholder">[What happened? How was it detected?]</span>
|
|
2487
|
+
</div>
|
|
2488
|
+
<div class="log-field">
|
|
2489
|
+
<strong>Containment Actions:</strong> <span class="log-placeholder">[Actions taken and timestamps]</span>
|
|
2490
|
+
</div>
|
|
2491
|
+
<div class="log-field">
|
|
2492
|
+
<strong>Root Cause:</strong> <span class="log-placeholder">[Technical cause, vulnerability exploited]</span>
|
|
2493
|
+
</div>
|
|
2494
|
+
<div class="log-field">
|
|
2495
|
+
<strong>Resolution:</strong> <span class="log-placeholder">[How was incident resolved?]</span>
|
|
2496
|
+
</div>
|
|
2497
|
+
<div class="log-field">
|
|
2498
|
+
<strong>Breach Determination:</strong> <span class="log-placeholder">[BREACH / NOT A BREACH - Justification]</span>
|
|
2499
|
+
</div>
|
|
2500
|
+
<div class="log-field">
|
|
2501
|
+
<strong>Notifications Sent:</strong> <span class="log-placeholder">[HHS, Individuals, Media - with dates]</span>
|
|
2502
|
+
</div>
|
|
2503
|
+
</div>
|
|
2504
|
+
</div>
|
|
2505
|
+
|
|
2506
|
+
<div class="incident-testing-section">
|
|
2507
|
+
<h3>🧪 Plan Testing & Drills</h3>
|
|
2508
|
+
<div class="testing-recommendations">
|
|
2509
|
+
<div class="testing-item">
|
|
2510
|
+
<div class="testing-frequency">Quarterly</div>
|
|
2511
|
+
<div class="testing-activity">
|
|
2512
|
+
<strong>Tabletop Exercises</strong>
|
|
2513
|
+
<p>Simulate breach scenarios with IRT. Review and update procedures.</p>
|
|
2514
|
+
</div>
|
|
2515
|
+
</div>
|
|
2516
|
+
<div class="testing-item">
|
|
2517
|
+
<div class="testing-frequency">Bi-Annual</div>
|
|
2518
|
+
<div class="testing-activity">
|
|
2519
|
+
<strong>Technical Drills</strong>
|
|
2520
|
+
<p>Test actual incident response tools, backup restores, and communication channels.</p>
|
|
2521
|
+
</div>
|
|
2522
|
+
</div>
|
|
2523
|
+
<div class="testing-item">
|
|
2524
|
+
<div class="testing-frequency">Annual</div>
|
|
2525
|
+
<div class="testing-activity">
|
|
2526
|
+
<strong>Full-Scale Simulation</strong>
|
|
2527
|
+
<p>End-to-end breach simulation with all stakeholders including executives and legal.</p>
|
|
2528
|
+
</div>
|
|
2529
|
+
</div>
|
|
2530
|
+
<div class="testing-item">
|
|
2531
|
+
<div class="testing-frequency">Annual</div>
|
|
2532
|
+
<div class="testing-activity">
|
|
2533
|
+
<strong>Plan Review & Update</strong>
|
|
2534
|
+
<p>Review incident response plan against current threats and regulations.</p>
|
|
2535
|
+
</div>
|
|
2536
|
+
</div>
|
|
2537
|
+
</div>
|
|
2538
|
+
</div>
|
|
2539
|
+
</div>
|
|
2540
|
+
`;
|
|
2541
|
+
}
|
|
2542
|
+
function renderScanComparisonHtml(comparison) {
|
|
2543
|
+
if (!comparison || !comparison.previousScan) {
|
|
2544
|
+
return '';
|
|
2545
|
+
}
|
|
2546
|
+
const { previousScan, scoreChange, severityChanges, newIssues, resolvedIssues } = comparison;
|
|
2547
|
+
const scoreArrow = scoreChange > 0 ? '↑' : scoreChange < 0 ? '↓' : '→';
|
|
2548
|
+
const scoreColor = scoreChange > 0 ? '#10b981' : scoreChange < 0 ? '#dc2626' : '#6b7280';
|
|
2549
|
+
const scoreSign = scoreChange > 0 ? '+' : '';
|
|
2550
|
+
const formatChange = (change, inverted = false) => {
|
|
2551
|
+
// For severity counts, a decrease is good (inverted = true)
|
|
2552
|
+
const isPositive = inverted ? change < 0 : change > 0;
|
|
2553
|
+
const isNegative = inverted ? change > 0 : change < 0;
|
|
2554
|
+
return {
|
|
2555
|
+
arrow: isPositive ? '↑' : isNegative ? '↓' : '→',
|
|
2556
|
+
color: isPositive ? '#10b981' : isNegative ? '#dc2626' : '#6b7280',
|
|
2557
|
+
sign: change > 0 ? '+' : '',
|
|
2558
|
+
};
|
|
2559
|
+
};
|
|
2560
|
+
const criticalChange = formatChange(severityChanges.critical, true);
|
|
2561
|
+
const highChange = formatChange(severityChanges.high, true);
|
|
2562
|
+
const mediumChange = formatChange(severityChanges.medium, true);
|
|
2563
|
+
const lowChange = formatChange(severityChanges.low, true);
|
|
2564
|
+
// Format previous scan date
|
|
2565
|
+
const prevDate = new Date(previousScan.timestamp);
|
|
2566
|
+
const formattedDate = prevDate.toLocaleString('en-US', {
|
|
2567
|
+
year: 'numeric',
|
|
2568
|
+
month: 'short',
|
|
2569
|
+
day: 'numeric',
|
|
2570
|
+
hour: '2-digit',
|
|
2571
|
+
minute: '2-digit'
|
|
2572
|
+
});
|
|
2573
|
+
return `
|
|
2574
|
+
<div class="comparison-section" style="margin: 3rem 0; padding: 2.5rem; background: white; border-radius: 16px; box-shadow: 0 4px 6px rgba(0,0,0,0.07);">
|
|
2575
|
+
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem;">
|
|
2576
|
+
<span style="font-size: 2.5rem;">📊</span>
|
|
2577
|
+
<div>
|
|
2578
|
+
<h2 style="margin: 0; color: #111827; font-size: 1.8rem;">Comparison with Previous Scan</h2>
|
|
2579
|
+
<p style="margin: 0.25rem 0 0 0; color: #6b7280; font-size: 0.95rem;">
|
|
2580
|
+
Previous scan: ${escapeHtml(formattedDate)}
|
|
2581
|
+
</p>
|
|
2582
|
+
</div>
|
|
2583
|
+
</div>
|
|
2584
|
+
|
|
2585
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2.5rem;">
|
|
2586
|
+
<!-- Score Comparison -->
|
|
2587
|
+
<div style="background: linear-gradient(135deg, ${scoreChange >= 0 ? '#f0fdf4' : '#fef2f2'} 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid ${scoreChange >= 0 ? '#bbf7d0' : '#fecaca'}; box-shadow: 0 2px 4px rgba(0,0,0,0.04);">
|
|
2588
|
+
<div style="color: #6b7280; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">Compliance Score</div>
|
|
2589
|
+
<div style="display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
2590
|
+
<span style="color: #9ca3af; font-size: 1.2rem; font-weight: 600;">${previousScan.complianceScore}</span>
|
|
2591
|
+
<span style="color: #6b7280; font-size: 1rem;">→</span>
|
|
2592
|
+
<span style="color: ${scoreColor}; font-size: 2rem; font-weight: bold;">${previousScan.complianceScore + scoreChange}</span>
|
|
2593
|
+
</div>
|
|
2594
|
+
<div style="color: ${scoreColor}; font-size: 1rem; font-weight: 600;">
|
|
2595
|
+
(${scoreSign}${scoreChange}) ${scoreArrow}
|
|
2596
|
+
</div>
|
|
2597
|
+
</div>
|
|
2598
|
+
|
|
2599
|
+
<!-- Critical Comparison -->
|
|
2600
|
+
<div style="background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #fee2e2; box-shadow: 0 2px 4px rgba(0,0,0,0.04);">
|
|
2601
|
+
<div style="color: #dc2626; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">Critical Issues</div>
|
|
2602
|
+
<div style="display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
2603
|
+
<span style="color: #9ca3af; font-size: 1.2rem; font-weight: 600;">${previousScan.severity.critical}</span>
|
|
2604
|
+
<span style="color: #6b7280; font-size: 1rem;">→</span>
|
|
2605
|
+
<span style="color: #dc2626; font-size: 2rem; font-weight: bold;">${previousScan.severity.critical + severityChanges.critical}</span>
|
|
2606
|
+
</div>
|
|
2607
|
+
<div style="color: ${criticalChange.color}; font-size: 1rem; font-weight: 600;">
|
|
2608
|
+
(${criticalChange.sign}${severityChanges.critical}) ${criticalChange.arrow}
|
|
2609
|
+
</div>
|
|
2610
|
+
</div>
|
|
2611
|
+
|
|
2612
|
+
<!-- High Comparison -->
|
|
2613
|
+
<div style="background: linear-gradient(135deg, #fff7ed 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #fed7aa; box-shadow: 0 2px 4px rgba(0,0,0,0.04);">
|
|
2614
|
+
<div style="color: #ea580c; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">High Issues</div>
|
|
2615
|
+
<div style="display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
2616
|
+
<span style="color: #9ca3af; font-size: 1.2rem; font-weight: 600;">${previousScan.severity.high}</span>
|
|
2617
|
+
<span style="color: #6b7280; font-size: 1rem;">→</span>
|
|
2618
|
+
<span style="color: #ea580c; font-size: 2rem; font-weight: bold;">${previousScan.severity.high + severityChanges.high}</span>
|
|
2619
|
+
</div>
|
|
2620
|
+
<div style="color: ${highChange.color}; font-size: 1rem; font-weight: 600;">
|
|
2621
|
+
(${highChange.sign}${severityChanges.high}) ${highChange.arrow}
|
|
2622
|
+
</div>
|
|
2623
|
+
</div>
|
|
2624
|
+
|
|
2625
|
+
<!-- Medium Comparison -->
|
|
2626
|
+
<div style="background: linear-gradient(135deg, #fefce8 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #fef08a; box-shadow: 0 2px 4px rgba(0,0,0,0.04);">
|
|
2627
|
+
<div style="color: #ca8a04; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">Medium Issues</div>
|
|
2628
|
+
<div style="display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
2629
|
+
<span style="color: #9ca3af; font-size: 1.2rem; font-weight: 600;">${previousScan.severity.medium}</span>
|
|
2630
|
+
<span style="color: #6b7280; font-size: 1rem;">→</span>
|
|
2631
|
+
<span style="color: #ca8a04; font-size: 2rem; font-weight: bold;">${previousScan.severity.medium + severityChanges.medium}</span>
|
|
2632
|
+
</div>
|
|
2633
|
+
<div style="color: ${mediumChange.color}; font-size: 1rem; font-weight: 600;">
|
|
2634
|
+
(${mediumChange.sign}${severityChanges.medium}) ${mediumChange.arrow}
|
|
2635
|
+
</div>
|
|
2636
|
+
</div>
|
|
2637
|
+
</div>
|
|
2638
|
+
|
|
2639
|
+
${newIssues.length > 0 || resolvedIssues.length > 0 ? `
|
|
2640
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
|
2641
|
+
${newIssues.length > 0 ? `
|
|
2642
|
+
<div style="background: #fef2f2; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #dc2626;">
|
|
2643
|
+
<h3 style="margin: 0 0 1rem 0; color: #991b1b; font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
2644
|
+
<span>⚠️</span> New Issues (${newIssues.length})
|
|
2645
|
+
</h3>
|
|
2646
|
+
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
|
2647
|
+
${newIssues.slice(0, 10).map(id => `
|
|
2648
|
+
<code style="background: white; color: #991b1b; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.85rem; font-family: 'SF Mono', Monaco, monospace;">
|
|
2649
|
+
${escapeHtml(id)}
|
|
2650
|
+
</code>
|
|
2651
|
+
`).join('')}
|
|
2652
|
+
${newIssues.length > 10 ? `<span style="color: #7f1d1d; font-size: 0.9rem;">... and ${newIssues.length - 10} more</span>` : ''}
|
|
2653
|
+
</div>
|
|
2654
|
+
</div>
|
|
2655
|
+
` : ''}
|
|
2656
|
+
|
|
2657
|
+
${resolvedIssues.length > 0 ? `
|
|
2658
|
+
<div style="background: #f0fdf4; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #10b981;">
|
|
2659
|
+
<h3 style="margin: 0 0 1rem 0; color: #065f46; font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
2660
|
+
<span>✅</span> Resolved Issues (${resolvedIssues.length})
|
|
2661
|
+
</h3>
|
|
2662
|
+
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
|
2663
|
+
${resolvedIssues.slice(0, 10).map(id => `
|
|
2664
|
+
<code style="background: white; color: #065f46; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.85rem; font-family: 'SF Mono', Monaco, monospace;">
|
|
2665
|
+
${escapeHtml(id)}
|
|
2666
|
+
</code>
|
|
2667
|
+
`).join('')}
|
|
2668
|
+
${resolvedIssues.length > 10 ? `<span style="color: #047857; font-size: 0.9rem;">... and ${resolvedIssues.length - 10} more</span>` : ''}
|
|
2669
|
+
</div>
|
|
2670
|
+
</div>
|
|
2671
|
+
` : ''}
|
|
2672
|
+
</div>
|
|
2673
|
+
` : `
|
|
2674
|
+
<div style="background: #eff6ff; padding: 1.5rem; border-radius: 8px; text-align: center;">
|
|
2675
|
+
<p style="margin: 0; color: #1e40af; font-weight: 500;">
|
|
2676
|
+
No new issues appeared and no issues were resolved since the last scan.
|
|
2677
|
+
</p>
|
|
2678
|
+
</div>
|
|
2679
|
+
`}
|
|
2680
|
+
</div>
|
|
2681
|
+
`;
|
|
2682
|
+
}
|
|
2683
|
+
function renderDependencyVulnerabilitiesHtml(vulnerabilities) {
|
|
2684
|
+
if (!vulnerabilities || vulnerabilities.length === 0) {
|
|
2685
|
+
return '';
|
|
2686
|
+
}
|
|
2687
|
+
const vulnCounts = {
|
|
2688
|
+
critical: vulnerabilities.filter(v => v.severity === 'critical').length,
|
|
2689
|
+
high: vulnerabilities.filter(v => v.severity === 'high').length,
|
|
2690
|
+
moderate: vulnerabilities.filter(v => v.severity === 'moderate').length,
|
|
2691
|
+
low: vulnerabilities.filter(v => v.severity === 'low').length,
|
|
2692
|
+
};
|
|
2693
|
+
const severityColors = {
|
|
2694
|
+
critical: '#dc2626',
|
|
2695
|
+
high: '#ea580c',
|
|
2696
|
+
moderate: '#ca8a04',
|
|
2697
|
+
low: '#2563eb',
|
|
2698
|
+
info: '#6b7280',
|
|
2699
|
+
};
|
|
2700
|
+
return `
|
|
2701
|
+
<div class="vulnerability-section" style="margin: 3rem 0; padding: 2.5rem; background: white; border-radius: 16px; box-shadow: 0 4px 6px rgba(0,0,0,0.07);">
|
|
2702
|
+
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem;">
|
|
2703
|
+
<span style="font-size: 2.5rem;">📦</span>
|
|
2704
|
+
<div>
|
|
2705
|
+
<h2 style="margin: 0; color: #111827; font-size: 1.8rem;">Dependency Vulnerabilities</h2>
|
|
2706
|
+
<p style="margin: 0.25rem 0 0 0; color: #6b7280; font-size: 0.95rem;">
|
|
2707
|
+
Security vulnerabilities detected in project dependencies via <code>npm audit</code>
|
|
2708
|
+
</p>
|
|
2709
|
+
</div>
|
|
2710
|
+
</div>
|
|
2711
|
+
|
|
2712
|
+
${vulnCounts.critical > 0 || vulnCounts.high > 0 ? `
|
|
2713
|
+
<div style="background: #fef2f2; border-left: 4px solid #dc2626; padding: 1.5rem; margin-bottom: 2rem; border-radius: 8px;">
|
|
2714
|
+
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;">
|
|
2715
|
+
<span style="font-size: 1.5rem;">⚠️</span>
|
|
2716
|
+
<strong style="color: #991b1b; font-size: 1.1rem;">Action Required</strong>
|
|
2717
|
+
</div>
|
|
2718
|
+
<p style="color: #7f1d1d; margin: 0;">
|
|
2719
|
+
${vulnCounts.critical > 0 ? `${vulnCounts.critical} critical` : ''}${vulnCounts.critical > 0 && vulnCounts.high > 0 ? ' and ' : ''}${vulnCounts.high > 0 ? `${vulnCounts.high} high` : ''}
|
|
2720
|
+
severity vulnerabilities detected. Update affected packages immediately.
|
|
2721
|
+
</p>
|
|
2722
|
+
</div>
|
|
2723
|
+
` : ''}
|
|
2724
|
+
|
|
2725
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-bottom: 2.5rem;">
|
|
2726
|
+
<div style="background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #fee2e2; box-shadow: 0 2px 4px rgba(220, 38, 38, 0.1);">
|
|
2727
|
+
<div style="color: #dc2626; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">Critical</div>
|
|
2728
|
+
<div style="color: #dc2626; font-size: 2.5rem; font-weight: bold; line-height: 1;">${vulnCounts.critical}</div>
|
|
2729
|
+
</div>
|
|
2730
|
+
<div style="background: linear-gradient(135deg, #fff7ed 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #fed7aa; box-shadow: 0 2px 4px rgba(234, 88, 12, 0.1);">
|
|
2731
|
+
<div style="color: #ea580c; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">High</div>
|
|
2732
|
+
<div style="color: #ea580c; font-size: 2.5rem; font-weight: bold; line-height: 1;">${vulnCounts.high}</div>
|
|
2733
|
+
</div>
|
|
2734
|
+
<div style="background: linear-gradient(135deg, #fefce8 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #fef08a; box-shadow: 0 2px 4px rgba(202, 138, 4, 0.1);">
|
|
2735
|
+
<div style="color: #ca8a04; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">Moderate</div>
|
|
2736
|
+
<div style="color: #ca8a04; font-size: 2.5rem; font-weight: bold; line-height: 1;">${vulnCounts.moderate}</div>
|
|
2737
|
+
</div>
|
|
2738
|
+
<div style="background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%); padding: 1.5rem; border-radius: 12px; border: 1px solid #dbeafe; box-shadow: 0 2px 4px rgba(37, 99, 235, 0.1);">
|
|
2739
|
+
<div style="color: #2563eb; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; margin-bottom: 0.5rem;">Low</div>
|
|
2740
|
+
<div style="color: #2563eb; font-size: 2.5rem; font-weight: bold; line-height: 1;">${vulnCounts.low}</div>
|
|
2741
|
+
</div>
|
|
2742
|
+
</div>
|
|
2743
|
+
|
|
2744
|
+
<h3 style="color: #111827; margin: 2rem 0 1.5rem 0; font-size: 1.3rem;">Affected Packages</h3>
|
|
2745
|
+
|
|
2746
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
2747
|
+
${vulnerabilities.map(vuln => `
|
|
2748
|
+
<div style="background: #f9fafb; padding: 1.5rem; border-radius: 8px; border-left: 4px solid ${severityColors[vuln.severity]};">
|
|
2749
|
+
<div style="display: flex; justify-content: between; align-items: start; gap: 1rem; margin-bottom: 0.75rem; flex-wrap: wrap;">
|
|
2750
|
+
<div style="flex: 1; min-width: 200px;">
|
|
2751
|
+
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;">
|
|
2752
|
+
<span style="padding: 0.25rem 0.6rem; border-radius: 4px; color: white; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; background: ${severityColors[vuln.severity]};">
|
|
2753
|
+
${vuln.severity}
|
|
2754
|
+
</span>
|
|
2755
|
+
<code style="font-family: 'SF Mono', Monaco, monospace; font-size: 0.95rem; font-weight: 600; color: #111827;">${escapeHtml(vuln.name)}</code>
|
|
2756
|
+
</div>
|
|
2757
|
+
<div style="color: #4b5563; font-size: 0.9rem; margin-bottom: 0.5rem;">
|
|
2758
|
+
${escapeHtml(vuln.via)}
|
|
2759
|
+
</div>
|
|
2760
|
+
<div style="font-family: 'SF Mono', Monaco, monospace; font-size: 0.8rem; color: #6b7280;">
|
|
2761
|
+
Vulnerable range: <code style="background: #e5e7eb; padding: 0.125rem 0.375rem; border-radius: 3px;">${escapeHtml(vuln.range)}</code>
|
|
2762
|
+
</div>
|
|
2763
|
+
</div>
|
|
2764
|
+
<div style="text-align: right;">
|
|
2765
|
+
${vuln.fixAvailable
|
|
2766
|
+
? typeof vuln.fixAvailable === 'object'
|
|
2767
|
+
? `<div style="background: #10b981; color: white; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; font-weight: 600;">
|
|
2768
|
+
Fix Available
|
|
2769
|
+
<div style="font-size: 0.75rem; font-weight: 400; margin-top: 0.25rem; opacity: 0.9;">
|
|
2770
|
+
${escapeHtml(vuln.fixAvailable.name)}@${escapeHtml(vuln.fixAvailable.version)}
|
|
2771
|
+
</div>
|
|
2772
|
+
</div>`
|
|
2773
|
+
: `<div style="background: #10b981; color: white; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; font-weight: 600;">
|
|
2774
|
+
Fix Available
|
|
2775
|
+
</div>`
|
|
2776
|
+
: `<div style="background: #6b7280; color: white; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; font-weight: 600;">
|
|
2777
|
+
No Fix Yet
|
|
2778
|
+
</div>`}
|
|
2779
|
+
</div>
|
|
2780
|
+
</div>
|
|
2781
|
+
${vuln.url ? `
|
|
2782
|
+
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid #e5e7eb;">
|
|
2783
|
+
<a href="${escapeHtml(vuln.url)}" target="_blank" rel="noopener" style="color: #3b82f6; text-decoration: none; font-size: 0.85rem; display: flex; align-items: center; gap: 0.375rem;">
|
|
2784
|
+
📄 Advisory Details
|
|
2785
|
+
<span style="font-size: 0.7rem;">↗</span>
|
|
2786
|
+
</a>
|
|
2787
|
+
</div>
|
|
2788
|
+
` : ''}
|
|
2789
|
+
</div>
|
|
2790
|
+
`).join('')}
|
|
2791
|
+
</div>
|
|
2792
|
+
|
|
2793
|
+
<div style="background: #eff6ff; padding: 1.5rem; margin-top: 2rem; border-radius: 8px; border-left: 4px solid #3b82f6;">
|
|
2794
|
+
<div style="display: flex; align-items: start; gap: 0.75rem;">
|
|
2795
|
+
<span style="font-size: 1.5rem;">💡</span>
|
|
2796
|
+
<div>
|
|
2797
|
+
<strong style="color: #1e40af; font-size: 1rem;">Remediation Steps</strong>
|
|
2798
|
+
<ol style="margin: 0.75rem 0 0 0; padding-left: 1.5rem; color: #1e3a8a;">
|
|
2799
|
+
<li style="margin: 0.5rem 0;">Run <code style="background: white; padding: 0.125rem 0.375rem; border-radius: 3px; font-family: 'SF Mono', Monaco, monospace;">npm audit fix</code> to automatically install compatible updates</li>
|
|
2800
|
+
<li style="margin: 0.5rem 0;">For breaking changes, use <code style="background: white; padding: 0.125rem 0.375rem; border-radius: 3px; font-family: 'SF Mono', Monaco, monospace;">npm audit fix --force</code> (test thoroughly after)</li>
|
|
2801
|
+
<li style="margin: 0.5rem 0;">Review advisory details for packages without automated fixes</li>
|
|
2802
|
+
<li style="margin: 0.5rem 0;">Consider alternative packages if vulnerabilities cannot be resolved</li>
|
|
2803
|
+
</ol>
|
|
2804
|
+
</div>
|
|
2805
|
+
</div>
|
|
2806
|
+
</div>
|
|
2807
|
+
</div>
|
|
2808
|
+
`;
|
|
2809
|
+
}
|
|
2810
|
+
async function renderComplianceScoreHtml(report, targetPath) {
|
|
2811
|
+
const score = calculateComplianceScore(report.findings.filter(f => !f.acknowledged && !f.suppressed && !f.isBaseline));
|
|
2812
|
+
const trending = await getScoreTrending(targetPath, score.overall);
|
|
2813
|
+
const trendingIcon = trending
|
|
2814
|
+
? trending.direction === 'up'
|
|
2815
|
+
? '↑'
|
|
2816
|
+
: trending.direction === 'down'
|
|
2817
|
+
? '↓'
|
|
2818
|
+
: '→'
|
|
2819
|
+
: '';
|
|
2820
|
+
const trendingClass = trending
|
|
2821
|
+
? trending.direction === 'up'
|
|
2822
|
+
? 'trending-up'
|
|
2823
|
+
: trending.direction === 'down'
|
|
2824
|
+
? 'trending-down'
|
|
2825
|
+
: 'trending-same'
|
|
2826
|
+
: '';
|
|
2827
|
+
const trendingText = trending
|
|
2828
|
+
? trending.direction === 'up'
|
|
2829
|
+
? `+${trending.change.toFixed(1)} from previous scan`
|
|
2830
|
+
: trending.direction === 'down'
|
|
2831
|
+
? `${trending.change.toFixed(1)} from previous scan`
|
|
2832
|
+
: 'No change from previous scan'
|
|
2833
|
+
: '';
|
|
2834
|
+
return `
|
|
2835
|
+
<div class="compliance-score-section">
|
|
2836
|
+
<div class="score-header">
|
|
2837
|
+
<h2>HIPAA Compliance Score</h2>
|
|
2838
|
+
<p class="score-subtitle">Overall security posture based on identified findings</p>
|
|
2839
|
+
</div>
|
|
2840
|
+
|
|
2841
|
+
<div class="score-main">
|
|
2842
|
+
<div class="score-circle" style="border-color: ${score.color}">
|
|
2843
|
+
<div class="score-value" style="color: ${score.color}">${Math.round(score.overall)}</div>
|
|
2844
|
+
<div class="score-max">/100</div>
|
|
2845
|
+
<div class="score-grade" style="background: ${score.color}">${score.grade}</div>
|
|
2846
|
+
</div>
|
|
2847
|
+
|
|
2848
|
+
<div class="score-info">
|
|
2849
|
+
<div class="score-description">
|
|
2850
|
+
${score.overall >= 80
|
|
2851
|
+
? '<strong>Excellent!</strong> Your codebase demonstrates strong HIPAA compliance practices.'
|
|
2852
|
+
: score.overall >= 60
|
|
2853
|
+
? '<strong>Good progress.</strong> Address remaining issues to strengthen compliance.'
|
|
2854
|
+
: score.overall >= 40
|
|
2855
|
+
? '<strong>Needs improvement.</strong> Several compliance gaps require attention.'
|
|
2856
|
+
: '<strong>Critical issues detected.</strong> Immediate action required for HIPAA compliance.'}
|
|
2857
|
+
</div>
|
|
2858
|
+
|
|
2859
|
+
${trending ? `
|
|
2860
|
+
<div class="score-trending ${trendingClass}">
|
|
2861
|
+
<span class="trending-icon">${trendingIcon}</span>
|
|
2862
|
+
<span class="trending-text">${trendingText}</span>
|
|
2863
|
+
</div>
|
|
2864
|
+
` : ''}
|
|
2865
|
+
|
|
2866
|
+
<div class="score-formula">
|
|
2867
|
+
<strong>Score Calculation:</strong> 100 - (Critical×15 + High×8 + Medium×3 + Low×1)
|
|
2868
|
+
</div>
|
|
2869
|
+
</div>
|
|
2870
|
+
</div>
|
|
2871
|
+
|
|
2872
|
+
<div class="score-categories">
|
|
2873
|
+
<h3>Compliance by Category</h3>
|
|
2874
|
+
<div class="category-grid">
|
|
2875
|
+
${Object.entries(score.byCategory)
|
|
2876
|
+
.sort((a, b) => a[1].score - b[1].score)
|
|
2877
|
+
.map(([category, data]) => {
|
|
2878
|
+
const catColor = data.score >= 80 ? '#10b981' : data.score >= 60 ? '#eab308' : data.score >= 40 ? '#f97316' : '#ef4444';
|
|
2879
|
+
const catWidth = data.score;
|
|
2880
|
+
return `
|
|
2881
|
+
<div class="category-card">
|
|
2882
|
+
<div class="category-name">${escapeHtml(category)}</div>
|
|
2883
|
+
<div class="category-score-container">
|
|
2884
|
+
<div class="category-bar-bg">
|
|
2885
|
+
<div class="category-bar" style="width: ${catWidth}%; background: ${catColor}"></div>
|
|
2886
|
+
</div>
|
|
2887
|
+
<div class="category-score" style="color: ${catColor}">${Math.round(data.score)}</div>
|
|
2888
|
+
</div>
|
|
2889
|
+
<div class="category-findings">${data.findings} finding${data.findings !== 1 ? 's' : ''}</div>
|
|
2890
|
+
</div>
|
|
2891
|
+
`;
|
|
2892
|
+
})
|
|
2893
|
+
.join('')}
|
|
2894
|
+
</div>
|
|
2895
|
+
</div>
|
|
2896
|
+
</div>
|
|
2897
|
+
`;
|
|
2898
|
+
}
|
|
2899
|
+
function renderRiskAnalysisHtml(report) {
|
|
2900
|
+
const severityColors = {
|
|
2901
|
+
critical: '#dc2626',
|
|
2902
|
+
high: '#ea580c',
|
|
2903
|
+
medium: '#ca8a04',
|
|
2904
|
+
low: '#2563eb',
|
|
2905
|
+
info: '#6b7280',
|
|
2906
|
+
};
|
|
2907
|
+
// Summary table
|
|
2908
|
+
const summaryHtml = `
|
|
2909
|
+
<div class="risk-summary-table">
|
|
2910
|
+
<table>
|
|
2911
|
+
<thead>
|
|
2912
|
+
<tr>
|
|
2913
|
+
<th>Risk Level</th>
|
|
2914
|
+
<th>Count</th>
|
|
2915
|
+
<th>Percentage</th>
|
|
2916
|
+
</tr>
|
|
2917
|
+
</thead>
|
|
2918
|
+
<tbody>
|
|
2919
|
+
<tr style="border-left: 4px solid ${severityColors.critical}">
|
|
2920
|
+
<td><strong>Critical</strong></td>
|
|
2921
|
+
<td>${report.summary.critical}</td>
|
|
2922
|
+
<td>${report.summary.total > 0 ? Math.round((report.summary.critical / report.summary.total) * 100) : 0}%</td>
|
|
2923
|
+
</tr>
|
|
2924
|
+
<tr style="border-left: 4px solid ${severityColors.high}">
|
|
2925
|
+
<td><strong>High</strong></td>
|
|
2926
|
+
<td>${report.summary.high}</td>
|
|
2927
|
+
<td>${report.summary.total > 0 ? Math.round((report.summary.high / report.summary.total) * 100) : 0}%</td>
|
|
2928
|
+
</tr>
|
|
2929
|
+
<tr style="border-left: 4px solid ${severityColors.medium}">
|
|
2930
|
+
<td><strong>Medium</strong></td>
|
|
2931
|
+
<td>${report.summary.medium}</td>
|
|
2932
|
+
<td>${report.summary.total > 0 ? Math.round((report.summary.medium / report.summary.total) * 100) : 0}%</td>
|
|
2933
|
+
</tr>
|
|
2934
|
+
<tr style="border-left: 4px solid ${severityColors.low}">
|
|
2935
|
+
<td><strong>Low</strong></td>
|
|
2936
|
+
<td>${report.summary.low}</td>
|
|
2937
|
+
<td>${report.summary.total > 0 ? Math.round((report.summary.low / report.summary.total) * 100) : 0}%</td>
|
|
2938
|
+
</tr>
|
|
2939
|
+
<tr style="border-top: 2px solid #e5e7eb; font-weight: 600;">
|
|
2940
|
+
<td><strong>Total</strong></td>
|
|
2941
|
+
<td>${report.summary.total}</td>
|
|
2942
|
+
<td>100%</td>
|
|
2943
|
+
</tr>
|
|
2944
|
+
</tbody>
|
|
2945
|
+
</table>
|
|
2946
|
+
</div>
|
|
2947
|
+
`;
|
|
2948
|
+
// Detailed risk table
|
|
2949
|
+
const detailedHtml = report.findings.length > 0 ? `
|
|
2950
|
+
<div class="risk-detail-table">
|
|
2951
|
+
<table>
|
|
2952
|
+
<thead>
|
|
2953
|
+
<tr>
|
|
2954
|
+
<th>Threat</th>
|
|
2955
|
+
<th>Vulnerability</th>
|
|
2956
|
+
<th>Risk Level</th>
|
|
2957
|
+
<th>Mitigation Status</th>
|
|
2958
|
+
<th>Remediation Plan</th>
|
|
2959
|
+
<th>HIPAA Reference</th>
|
|
2960
|
+
</tr>
|
|
2961
|
+
</thead>
|
|
2962
|
+
<tbody>
|
|
2963
|
+
${report.findings.map(f => `
|
|
2964
|
+
<tr style="border-left: 4px solid ${severityColors[f.severity]}">
|
|
2965
|
+
<td class="threat-cell">${escapeHtml(getCategoryThreat(f.category))}</td>
|
|
2966
|
+
<td class="vulnerability-cell">
|
|
2967
|
+
<strong>${escapeHtml(f.title)}</strong>
|
|
2968
|
+
<div class="file-ref">${escapeHtml(f.file)}${f.line ? `:${f.line}` : ''}</div>
|
|
2969
|
+
</td>
|
|
2970
|
+
<td class="risk-level-cell">
|
|
2971
|
+
<span class="risk-badge" style="background: ${severityColors[f.severity]}">
|
|
2972
|
+
${getRiskLevel(f.severity)}
|
|
2973
|
+
</span>
|
|
2974
|
+
</td>
|
|
2975
|
+
<td class="status-cell">
|
|
2976
|
+
<span class="status-badge ${f.fixType ? 'status-available' : 'status-open'}">
|
|
2977
|
+
${escapeHtml(getMitigationStatus(f))}
|
|
2978
|
+
</span>
|
|
2979
|
+
</td>
|
|
2980
|
+
<td class="remediation-cell">${escapeHtml(f.recommendation)}</td>
|
|
2981
|
+
<td class="hipaa-cell">${escapeHtml(f.hipaaReference || 'N/A')}</td>
|
|
2982
|
+
</tr>
|
|
2983
|
+
`).join('')}
|
|
2984
|
+
</tbody>
|
|
2985
|
+
</table>
|
|
2986
|
+
</div>
|
|
2987
|
+
` : '<p style="text-align: center; color: #6b7280; padding: 2rem;">No risks identified.</p>';
|
|
2988
|
+
return `
|
|
2989
|
+
<div class="risk-analysis-section">
|
|
2990
|
+
<h2>📊 Risk Analysis</h2>
|
|
2991
|
+
<p style="color: #6b7280; margin-bottom: 1.5rem;">
|
|
2992
|
+
Comprehensive risk assessment of identified HIPAA compliance findings with threat categorization and remediation tracking.
|
|
2993
|
+
</p>
|
|
2994
|
+
|
|
2995
|
+
<h3>Risk Summary</h3>
|
|
2996
|
+
${summaryHtml}
|
|
2997
|
+
|
|
2998
|
+
<h3 style="margin-top: 2rem;">Detailed Risk Assessment</h3>
|
|
2999
|
+
${detailedHtml}
|
|
3000
|
+
</div>
|
|
3001
|
+
`;
|
|
3002
|
+
}
|
|
396
3003
|
function severityBadge(severity) {
|
|
397
3004
|
const badges = {
|
|
398
3005
|
critical: '🔴',
|
|
@@ -404,12 +3011,12 @@ function severityBadge(severity) {
|
|
|
404
3011
|
return badges[severity] || '⚪';
|
|
405
3012
|
}
|
|
406
3013
|
export async function generateReport(result, targetPath, options) {
|
|
407
|
-
const report = buildReport(result, targetPath);
|
|
3014
|
+
const report = buildReport(result, targetPath, options.vulnerabilities);
|
|
408
3015
|
let content;
|
|
409
3016
|
let extension;
|
|
410
3017
|
switch (options.format) {
|
|
411
3018
|
case 'html':
|
|
412
|
-
content = generateHtml(report);
|
|
3019
|
+
content = await generateHtml(report, targetPath, options);
|
|
413
3020
|
extension = 'html';
|
|
414
3021
|
break;
|
|
415
3022
|
case 'markdown':
|