@codecot/pw-checker
Version:
A comprehensive CLI tool to audit passwords locally using HIBP and import from Chrome/Bitwarden
496 lines • 17.4 kB
JavaScript
// pdfSecurityReport.ts - Generate comprehensive PDF security audit reports
import { jsPDF } from "jspdf";
import sqlite3 from "sqlite3";
import { open } from "sqlite";
import chalk from "chalk";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { dbPath } from "./database.js";
// ESM replacement for __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Generate comprehensive security report data from database
*/
export async function generateSecurityReportData() {
const db = await open({
filename: dbPath,
driver: sqlite3.Database,
});
try {
// Basic statistics
const totalAccounts = await db.get("SELECT COUNT(*) as count FROM pw_entries");
const compromisedAccounts = await db.get("SELECT COUNT(*) as count FROM pw_entries WHERE compromised = 1");
const breachedEmails = await db.get(`
SELECT COUNT(*) as count FROM pw_entries
WHERE breach_info IS NOT NULL AND breach_info LIKE '%"breached":true%'
`);
// Risk breakdown
const riskBreakdown = {};
const riskResults = await db.all(`
SELECT risk_label, COUNT(*) as count FROM pw_entries
WHERE risk_label IS NOT NULL
GROUP BY risk_label
`);
riskResults.forEach(row => {
riskBreakdown[row.risk_label || 'Unknown'] = row.count;
});
// Sources breakdown
const sourcesBreakdown = {};
const sourcesResults = await db.all(`
SELECT source, COUNT(*) as count FROM pw_entries
GROUP BY source
`);
sourcesResults.forEach(row => {
sourcesBreakdown[row.source || 'Unknown'] = row.count;
});
// Categories breakdown
const categoriesBreakdown = {};
const categoriesResults = await db.all(`
SELECT category, COUNT(*) as count FROM pw_entries
WHERE category IS NOT NULL
GROUP BY category
`);
categoriesResults.forEach(row => {
categoriesBreakdown[row.category || 'other'] = row.count;
});
// Password strength analysis
const passwordStrengthBreakdown = {
'Very Weak': 0,
'Weak': 0,
'Medium': 0,
'Strong': 0,
'Very Strong': 0
};
// Age breakdown
const ageBreakdown = {
'Less than 3 months': 0,
'3-6 months': 0,
'6-12 months': 0,
'More than 1 year': 0,
'Unknown': 0
};
// Get all entries for detailed analysis
const allEntries = await db.all(`
SELECT password, date_created, risk_score, risk_factors
FROM pw_entries
`);
let duplicatePasswords = 0;
let weakPasswords = 0;
let totalAge = 0;
let ageCount = 0;
const passwordCounts = new Map();
allEntries.forEach(entry => {
// Count password duplicates
const count = passwordCounts.get(entry.password) || 0;
passwordCounts.set(entry.password, count + 1);
if (count > 0)
duplicatePasswords++;
// Analyze password strength
const strength = analyzePasswordStrength(entry.password);
passwordStrengthBreakdown[strength]++;
if (strength === 'Very Weak' || strength === 'Weak') {
weakPasswords++;
}
// Analyze password age
if (entry.date_created) {
const created = new Date(entry.date_created);
const now = new Date();
const ageMonths = (now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24 * 30.44);
totalAge += ageMonths;
ageCount++;
if (ageMonths < 3) {
ageBreakdown['Less than 3 months']++;
}
else if (ageMonths < 6) {
ageBreakdown['3-6 months']++;
}
else if (ageMonths < 12) {
ageBreakdown['6-12 months']++;
}
else {
ageBreakdown['More than 1 year']++;
}
}
else {
ageBreakdown['Unknown']++;
}
});
const averagePasswordAge = ageCount > 0 ? totalAge / ageCount : 0;
// Top risky accounts
const topRiskyAccounts = await db.all(`
SELECT url, username, risk_score, risk_label, risk_factors,
(CASE
WHEN LENGTH(url) > 50 THEN SUBSTR(url, 1, 47) || '...'
ELSE url
END) as name
FROM pw_entries
WHERE risk_score IS NOT NULL
ORDER BY risk_score DESC
LIMIT 20
`);
// Breach summary
const breachSummary = {};
const breachResults = await db.all(`
SELECT
CASE
WHEN breach_info IS NULL THEN 'No breaches'
WHEN breach_info LIKE '%"breached":false%' THEN 'Email not breached'
WHEN breach_info LIKE '%"breached":true%' THEN 'Email breached'
ELSE 'Unknown'
END as breach_status,
COUNT(*) as count
FROM pw_entries
GROUP BY breach_status
`);
breachResults.forEach(row => {
breachSummary[row.breach_status] = row.count;
});
// Top breached domains
const topBreachedDomains = await db.all(`
SELECT
CASE
WHEN url LIKE 'http%' THEN
SUBSTR(url, INSTR(url, '://') + 3,
CASE
WHEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') > 0
THEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') - 1
ELSE LENGTH(SUBSTR(url, INSTR(url, '://') + 3))
END)
ELSE url
END as domain,
COUNT(*) as count
FROM pw_entries
WHERE breach_info LIKE '%"breached":true%'
GROUP BY domain
ORDER BY count DESC
LIMIT 10
`);
// Generate recommendations
const recommendations = generateRecommendations({
totalAccounts: totalAccounts.count,
compromisedAccounts: compromisedAccounts.count,
breachedEmails: breachedEmails.count,
riskBreakdown,
duplicatePasswords,
weakPasswords
});
// Mock monthly trend data (in a real implementation, you'd track this over time)
const monthlyRiskTrend = generateMockTrend();
return {
totalAccounts: totalAccounts.count,
compromisedAccounts: compromisedAccounts.count,
breachedEmails: breachedEmails.count,
riskBreakdown,
topRiskyAccounts,
breachSummary,
recommendations,
generatedAt: new Date().toISOString(),
sourcesBreakdown,
categoriesBreakdown,
passwordStrengthBreakdown,
ageBreakdown,
duplicatePasswords,
weakPasswords,
averagePasswordAge,
topBreachedDomains,
monthlyRiskTrend
};
}
finally {
await db.close();
}
}
/**
* Analyze password strength
*/
function analyzePasswordStrength(password) {
if (!password)
return 'Very Weak';
const length = password.length;
const hasLower = /[a-z]/.test(password);
const hasUpper = /[A-Z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSymbols = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
let score = 0;
if (length >= 8)
score += 1;
if (length >= 12)
score += 1;
if (hasLower)
score += 1;
if (hasUpper)
score += 1;
if (hasNumbers)
score += 1;
if (hasSymbols)
score += 1;
if (score <= 2)
return 'Very Weak';
if (score <= 3)
return 'Weak';
if (score <= 4)
return 'Medium';
if (score <= 5)
return 'Strong';
return 'Very Strong';
}
/**
* Generate mock trend data for demonstration
*/
function generateMockTrend() {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
return months.map(month => ({
month,
risk: Math.floor(Math.random() * 40) + 30 // Random risk between 30-70
}));
}
/**
* Generate personalized security recommendations
*/
function generateRecommendations(data) {
const recommendations = [];
// Critical recommendations based on data
if (data.compromisedAccounts && data.compromisedAccounts > 0) {
recommendations.push(`🚨 URGENT: Change ${data.compromisedAccounts} compromised passwords immediately`);
}
if (data.riskBreakdown?.Critical && data.riskBreakdown.Critical > 0) {
recommendations.push(`⚠️ Address ${data.riskBreakdown.Critical} critical risk accounts`);
}
if (data.riskBreakdown?.High && data.riskBreakdown.High > 0) {
recommendations.push(`⚠️ Update ${data.riskBreakdown.High} high-risk passwords`);
}
if (data.duplicatePasswords && data.duplicatePasswords > 0) {
recommendations.push(`🔄 Replace ${data.duplicatePasswords} duplicate passwords with unique ones`);
}
if (data.weakPasswords && data.weakPasswords > 0) {
recommendations.push(`💪 Strengthen ${data.weakPasswords} weak passwords`);
}
// General security recommendations
if (data.breachedEmails && data.breachedEmails > 0) {
recommendations.push("🔐 Enable two-factor authentication on all breached accounts");
}
recommendations.push("🔑 Use a password manager to generate unique, strong passwords");
recommendations.push("📅 Set up regular password audits (quarterly recommended)");
recommendations.push("🔒 Enable two-factor authentication wherever possible");
recommendations.push("🚨 Monitor your accounts for unusual activity");
recommendations.push("📧 Check if your email appears in new data breaches regularly");
return recommendations;
}
/**
* Create color-coded risk indicator
*/
function getRiskColor(riskLabel) {
switch (riskLabel) {
case "Critical":
return "#8B0000";
case "High":
return "#FF0000";
case "Medium":
return "#FFA500";
case "Low":
return "#008000";
default:
return "#666666";
}
}
/**
* Generate PDF security audit report
*/
export async function generatePDFSecurityReport(outputPath) {
console.log(chalk.blue("📊 Generating security audit report..."));
const reportData = await generateSecurityReportData();
const pdf = new jsPDF("portrait", "mm", "a4");
// Page dimensions
const pageWidth = pdf.internal.pageSize.width;
const pageHeight = pdf.internal.pageSize.height;
const margin = 20;
const contentWidth = pageWidth - margin * 2;
// Colors and fonts
pdf.setFont("helvetica");
// Header
pdf.setFontSize(24);
pdf.setTextColor(51, 51, 51);
pdf.text("Security Audit Report", margin, 30);
pdf.setFontSize(12);
pdf.setTextColor(102, 102, 102);
const generatedDate = new Date(reportData.generatedAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
pdf.text(`Generated: ${generatedDate}`, margin, 40);
let yPos = 55;
// Executive Summary
pdf.setFontSize(16);
pdf.setTextColor(51, 51, 51);
pdf.text("Executive Summary", margin, yPos);
yPos += 10;
pdf.setFontSize(11);
pdf.setTextColor(68, 68, 68);
const summaryText = [
`Total Accounts Analyzed: ${reportData.totalAccounts}`,
`Compromised Passwords: ${reportData.compromisedAccounts}`,
`Emails Found in Breaches: ${reportData.breachedEmails}`,
`Overall Security Score: ${calculateOverallScore(reportData)}/100`,
];
summaryText.forEach((text) => {
pdf.text(text, margin, yPos);
yPos += 6;
});
yPos += 10;
// Risk Breakdown Chart (simplified for PDF)
pdf.setFontSize(16);
pdf.setTextColor(51, 51, 51);
pdf.text("Risk Level Distribution", margin, yPos);
yPos += 10;
// Draw risk level bars
const barHeight = 8;
const barSpacing = 12;
const maxBarWidth = contentWidth * 0.6;
Object.entries(reportData.riskBreakdown).forEach(([level, count]) => {
if (count === 0)
return;
const barWidth = Math.max(10, (count / reportData.totalAccounts) * maxBarWidth);
const color = getRiskColor(level);
// Convert hex to RGB
const rgb = hexToRgb(color);
if (rgb) {
pdf.setFillColor(rgb.r, rgb.g, rgb.b);
}
pdf.rect(margin, yPos, barWidth, barHeight, "F");
pdf.setFontSize(10);
pdf.setTextColor(51, 51, 51);
pdf.text(`${level}: ${count}`, margin + barWidth + 5, yPos + 6);
yPos += barSpacing;
});
yPos += 10;
// Top Risky Accounts
if (reportData.topRiskyAccounts.length > 0) {
pdf.setFontSize(16);
pdf.setTextColor(51, 51, 51);
pdf.text("Top 10 Accounts Requiring Attention", margin, yPos);
yPos += 10;
pdf.setFontSize(9);
// Table headers
pdf.setTextColor(51, 51, 51);
pdf.text("Risk", margin, yPos);
pdf.text("Account", margin + 25, yPos);
pdf.text("Score", margin + 100, yPos);
pdf.text("Issues", margin + 120, yPos);
yPos += 8;
// Draw line under headers
pdf.line(margin, yPos - 2, pageWidth - margin, yPos - 2);
yPos += 2;
reportData.topRiskyAccounts.slice(0, 10).forEach((account) => {
if (yPos > pageHeight - 30) {
pdf.addPage();
yPos = 30;
}
const color = getRiskColor(account.risk_label || "Low");
const rgb = hexToRgb(color);
if (rgb) {
pdf.setTextColor(rgb.r, rgb.g, rgb.b);
}
pdf.text(account.risk_label || "Low", margin, yPos);
pdf.setTextColor(51, 51, 51);
// Truncate long names
const accountName = account.name.length > 25
? account.name.substring(0, 22) + "..."
: account.name;
pdf.text(accountName, margin + 25, yPos);
pdf.text((account.risk_score || 0).toString(), margin + 100, yPos);
// Risk factors (truncated)
let factorsText = "None";
if (account.risk_factors) {
try {
const factors = account.risk_factors;
factorsText =
factors.length > 0 ? factors[0].substring(0, 30) + "..." : "None";
}
catch (e) {
factorsText = "Error parsing";
}
}
pdf.text(factorsText, margin + 120, yPos);
yPos += 6;
});
}
yPos += 15;
// Check if we need a new page for recommendations
if (yPos > pageHeight - 80) {
pdf.addPage();
yPos = 30;
}
// Recommendations
pdf.setFontSize(16);
pdf.setTextColor(51, 51, 51);
pdf.text("Security Recommendations", margin, yPos);
yPos += 10;
pdf.setFontSize(10);
pdf.setTextColor(68, 68, 68);
reportData.recommendations.forEach((rec) => {
if (yPos > pageHeight - 20) {
pdf.addPage();
yPos = 30;
}
// Word wrap for long recommendations
const lines = pdf.splitTextToSize(rec, contentWidth - 10);
pdf.text(lines, margin + 5, yPos);
yPos += lines.length * 5 + 3;
});
// Footer on last page
pdf.setFontSize(8);
pdf.setTextColor(153, 153, 153);
pdf.text("Generated by pw-checker security audit tool", margin, pageHeight - 10);
// Save PDF
const defaultPath = path.resolve(__dirname, "../reports/security-audit.pdf");
const finalPath = outputPath || defaultPath;
// Ensure reports directory exists
const reportsDir = path.dirname(finalPath);
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
// Save the PDF
pdf.save(finalPath);
console.log(chalk.green(`✅ PDF report generated: ${finalPath}`));
return finalPath;
}
/**
* Calculate overall security score
*/
function calculateOverallScore(data) {
if (data.totalAccounts === 0)
return 0;
const weights = {
Critical: 0,
High: 25,
Medium: 60,
Low: 100,
};
let totalWeightedScore = 0;
let totalAccounts = 0;
Object.entries(data.riskBreakdown).forEach(([level, count]) => {
const weight = weights[level] || 50;
totalWeightedScore += weight * count;
totalAccounts += count;
});
return totalAccounts > 0 ? Math.round(totalWeightedScore / totalAccounts) : 0;
}
/**
* Convert hex color to RGB
*/
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
//# sourceMappingURL=pdfSecurityReport.js.map