kramscan 0.1.1 → 0.3.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/LICENSE +1 -1
- package/README.md +419 -236
- package/dist/agent/confirmation.d.ts +5 -1
- package/dist/agent/confirmation.js +29 -9
- package/dist/agent/context.js +2 -3
- package/dist/agent/orchestrator.d.ts +2 -0
- package/dist/agent/orchestrator.js +50 -8
- package/dist/agent/prompts/system.d.ts +1 -1
- package/dist/agent/prompts/system.js +5 -7
- package/dist/agent/skills/health-check.js +22 -2
- package/dist/agent/skills/index.d.ts +1 -0
- package/dist/agent/skills/index.js +3 -1
- package/dist/agent/skills/verify-finding.d.ts +17 -0
- package/dist/agent/skills/verify-finding.js +91 -0
- package/dist/agent/skills/web-scan.js +46 -0
- package/dist/cli.js +156 -149
- package/dist/commands/agent.js +38 -38
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +112 -0
- package/dist/commands/analyze.js +103 -54
- package/dist/commands/config.js +55 -29
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +236 -0
- package/dist/commands/doctor.js +20 -15
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +109 -0
- package/dist/commands/onboard.js +188 -141
- package/dist/commands/report.js +68 -76
- package/dist/commands/scan.js +262 -81
- package/dist/commands/scans.d.ts +2 -0
- package/dist/commands/scans.js +55 -0
- package/dist/core/ai-client.d.ts +6 -1
- package/dist/core/ai-client.js +80 -12
- package/dist/core/ai-payloads.d.ts +17 -0
- package/dist/core/ai-payloads.js +54 -0
- package/dist/core/config-schema.d.ts +197 -0
- package/dist/core/config-schema.js +68 -0
- package/dist/core/config-schema.test.d.ts +1 -0
- package/dist/core/config-schema.test.js +151 -0
- package/dist/core/config.d.ts +8 -31
- package/dist/core/config.js +71 -14
- package/dist/core/diff-engine.d.ts +12 -0
- package/dist/core/diff-engine.js +47 -0
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +162 -0
- package/dist/core/scan-index.d.ts +20 -0
- package/dist/core/scan-index.js +52 -0
- package/dist/core/scan-storage.d.ts +11 -0
- package/dist/core/scan-storage.js +69 -0
- package/dist/core/scanner.d.ts +95 -13
- package/dist/core/scanner.js +342 -248
- package/dist/core/server-probe.d.ts +20 -0
- package/dist/core/server-probe.js +109 -0
- package/dist/core/vulnerability-detector.d.ts +9 -0
- package/dist/core/vulnerability-detector.js +46 -15
- package/dist/core/vulnerability-detector.test.d.ts +1 -0
- package/dist/core/vulnerability-detector.test.js +210 -0
- package/dist/index.js +3 -0
- package/dist/plugins/PluginManager.d.ts +27 -0
- package/dist/plugins/PluginManager.js +166 -0
- package/dist/plugins/index.d.ts +12 -0
- package/dist/plugins/index.js +29 -0
- package/dist/plugins/types.d.ts +55 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +67 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
- package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/CookieSecurityPlugin.js +91 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +222 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +13 -0
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +110 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +10 -0
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +69 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
- package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
- package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
- package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
- package/dist/reports/PdfGenerator.d.ts +36 -0
- package/dist/reports/PdfGenerator.js +404 -0
- package/dist/utils/logger.d.ts +33 -1
- package/dist/utils/logger.js +127 -8
- package/dist/utils/theme.d.ts +56 -0
- package/dist/utils/theme.js +201 -0
- package/package.json +6 -3
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseVulnerabilityPlugin, PluginContext } from "../types";
|
|
2
|
+
export declare class XSSPlugin extends BaseVulnerabilityPlugin {
|
|
3
|
+
readonly name = "XSS Detector";
|
|
4
|
+
readonly type: "xss";
|
|
5
|
+
readonly description = "Detects Cross-Site Scripting vulnerabilities";
|
|
6
|
+
private readonly payloads;
|
|
7
|
+
private getPayloads;
|
|
8
|
+
testParameter(context: PluginContext, param: string, _value: string): Promise<import("../types").VulnerabilityTestResult>;
|
|
9
|
+
testFormInput(context: PluginContext, formData: {
|
|
10
|
+
inputs: Array<{
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
}>;
|
|
14
|
+
}): Promise<import("../types").VulnerabilityTestResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.XSSPlugin = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
class XSSPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
6
|
+
name = "XSS Detector";
|
|
7
|
+
type = "xss";
|
|
8
|
+
description = "Detects Cross-Site Scripting vulnerabilities";
|
|
9
|
+
payloads = [
|
|
10
|
+
"<script>alert('XSS')</script>",
|
|
11
|
+
'"><script>alert(1)</script>',
|
|
12
|
+
"<img src=x onerror=alert(1)>",
|
|
13
|
+
"'-alert(1)-'",
|
|
14
|
+
"<svg/onload=alert(1)>",
|
|
15
|
+
];
|
|
16
|
+
async getPayloads(context, param) {
|
|
17
|
+
if (context.payloadGenerator) {
|
|
18
|
+
const aiPayloads = await context.payloadGenerator.generatePayloads("xss", {
|
|
19
|
+
parameterName: param,
|
|
20
|
+
url: context.url,
|
|
21
|
+
// We could extract more context from the page here if needed
|
|
22
|
+
});
|
|
23
|
+
if (aiPayloads.length > 0) {
|
|
24
|
+
return [...aiPayloads, ...this.payloads];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return this.payloads;
|
|
28
|
+
}
|
|
29
|
+
async testParameter(context, param, _value) {
|
|
30
|
+
const payloads = await this.getPayloads(context, param);
|
|
31
|
+
for (const payload of payloads) {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(context.url);
|
|
34
|
+
url.searchParams.set(param, payload);
|
|
35
|
+
await context.page.goto(url.toString(), {
|
|
36
|
+
waitUntil: "networkidle2",
|
|
37
|
+
timeout: context.timeout
|
|
38
|
+
});
|
|
39
|
+
const content = await context.page.content();
|
|
40
|
+
if (content.includes(payload)) {
|
|
41
|
+
return this.success(this.createVulnerability("Reflected Cross-Site Scripting (XSS)", `The parameter '${param}' reflects user input without proper encoding, allowing script injection.`, context.url, "high", `Payload: ${payload}`, "Implement input validation, output encoding, and Content Security Policy (CSP) headers.", "CWE-79"));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return this.failure(error.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.failure();
|
|
49
|
+
}
|
|
50
|
+
async testFormInput(context, formData) {
|
|
51
|
+
for (const input of formData.inputs) {
|
|
52
|
+
if (input.type === "hidden" || input.type === "submit")
|
|
53
|
+
continue;
|
|
54
|
+
const payloads = await this.getPayloads(context, input.name);
|
|
55
|
+
for (const payload of payloads) {
|
|
56
|
+
try {
|
|
57
|
+
await context.page.goto(context.url, {
|
|
58
|
+
waitUntil: "networkidle2",
|
|
59
|
+
timeout: context.timeout
|
|
60
|
+
});
|
|
61
|
+
const inputSelector = `input[name="${input.name}"], textarea[name="${input.name}"]`;
|
|
62
|
+
await context.page.type(inputSelector, payload);
|
|
63
|
+
const submitButton = await context.page.$("input[type=submit], button[type=submit]");
|
|
64
|
+
if (submitButton) {
|
|
65
|
+
await submitButton.click();
|
|
66
|
+
await context.page.waitForNavigation({ timeout: context.timeout }).catch(() => { });
|
|
67
|
+
}
|
|
68
|
+
const content = await context.page.content();
|
|
69
|
+
if (content.includes(payload)) {
|
|
70
|
+
return this.success(this.createVulnerability("Reflected Cross-Site Scripting (XSS)", `The form input '${input.name}' reflects user input without proper encoding.`, context.url, "high", `Payload: ${payload}`, "Implement input validation, output encoding, and Content Security Policy (CSP) headers.", "CWE-79"));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return this.failure(error.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return this.failure();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.XSSPlugin = XSSPlugin;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ScanResult } from "../core/vulnerability-detector";
|
|
2
|
+
import type { ScanError } from "../core/scanner";
|
|
3
|
+
export interface PdfGenerationOptions {
|
|
4
|
+
filename?: string;
|
|
5
|
+
format?: "A4" | "Letter" | "Legal";
|
|
6
|
+
margin?: {
|
|
7
|
+
top: string;
|
|
8
|
+
bottom: string;
|
|
9
|
+
left: string;
|
|
10
|
+
right: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface PdfReportData {
|
|
14
|
+
scanResult: ScanResult;
|
|
15
|
+
scanErrors?: ScanError[];
|
|
16
|
+
pluginErrors?: Map<string, Array<{
|
|
17
|
+
url: string;
|
|
18
|
+
error: string;
|
|
19
|
+
}>>;
|
|
20
|
+
}
|
|
21
|
+
export interface HtmlReportOptions {
|
|
22
|
+
filename?: string;
|
|
23
|
+
includeStyles?: boolean;
|
|
24
|
+
minify?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare class PdfGenerator {
|
|
27
|
+
generate(data: PdfReportData, options?: PdfGenerationOptions): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a standalone HTML report
|
|
30
|
+
* @param data - The scan result data
|
|
31
|
+
* @param options - Options for HTML generation
|
|
32
|
+
* @returns Path to the generated HTML file
|
|
33
|
+
*/
|
|
34
|
+
generateHtml(data: PdfReportData, options?: HtmlReportOptions): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
export declare const pdfGenerator: PdfGenerator;
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.pdfGenerator = exports.PdfGenerator = void 0;
|
|
40
|
+
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
41
|
+
const scan_storage_1 = require("../core/scan-storage");
|
|
42
|
+
const path_1 = __importDefault(require("path"));
|
|
43
|
+
function escapeHtml(text) {
|
|
44
|
+
if (!text)
|
|
45
|
+
return "";
|
|
46
|
+
return text
|
|
47
|
+
.toString()
|
|
48
|
+
.replace(/&/g, "&")
|
|
49
|
+
.replace(/</g, "<")
|
|
50
|
+
.replace(/>/g, ">")
|
|
51
|
+
.replace(/"/g, """)
|
|
52
|
+
.replace(/'/g, "'");
|
|
53
|
+
}
|
|
54
|
+
function sanitizeFilenamePart(value) {
|
|
55
|
+
return value
|
|
56
|
+
.replace(/[<>:"\/\\|?*\x00-\x1F]/g, "_")
|
|
57
|
+
.replace(/\s+/g, "_")
|
|
58
|
+
.replace(/_+/g, "_")
|
|
59
|
+
.replace(/^_+|_+$/g, "");
|
|
60
|
+
}
|
|
61
|
+
function severityBadge(severity) {
|
|
62
|
+
const s = severity.toLowerCase();
|
|
63
|
+
if (s === "critical")
|
|
64
|
+
return "badge badge-critical";
|
|
65
|
+
if (s === "high")
|
|
66
|
+
return "badge badge-high";
|
|
67
|
+
if (s === "medium")
|
|
68
|
+
return "badge badge-medium";
|
|
69
|
+
if (s === "low")
|
|
70
|
+
return "badge badge-low";
|
|
71
|
+
return "badge badge-info";
|
|
72
|
+
}
|
|
73
|
+
function buildPdfHtml(data) {
|
|
74
|
+
const { scanResult, scanErrors = [], pluginErrors = new Map() } = data;
|
|
75
|
+
const rows = scanResult.vulnerabilities
|
|
76
|
+
.map((v, i) => {
|
|
77
|
+
const sev = escapeHtml(v.severity.toUpperCase());
|
|
78
|
+
const title = escapeHtml(v.title);
|
|
79
|
+
const url = escapeHtml(v.url);
|
|
80
|
+
const type = escapeHtml(v.type);
|
|
81
|
+
const desc = escapeHtml(v.description);
|
|
82
|
+
const evidence = v.evidence ? escapeHtml(v.evidence) : "";
|
|
83
|
+
const remediation = v.remediation ? escapeHtml(v.remediation) : "";
|
|
84
|
+
const cwe = v.cwe ? escapeHtml(v.cwe) : "";
|
|
85
|
+
return `
|
|
86
|
+
<div class="card">
|
|
87
|
+
<div class="card-h">
|
|
88
|
+
<div class="idx">${i + 1}.</div>
|
|
89
|
+
<div class="title">${title}</div>
|
|
90
|
+
<div class="${severityBadge(v.severity)}">${sev}</div>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="meta">
|
|
93
|
+
<div><span class="k">URL:</span> <span class="v mono">${url}</span></div>
|
|
94
|
+
<div><span class="k">Type:</span> <span class="v mono">${type}</span></div>
|
|
95
|
+
${cwe ? `<div><span class="k">CWE:</span> <span class="v mono">${cwe}</span></div>` : ""}
|
|
96
|
+
</div>
|
|
97
|
+
<div class="section">
|
|
98
|
+
<div class="k">Description</div>
|
|
99
|
+
<div class="v">${desc}</div>
|
|
100
|
+
</div>
|
|
101
|
+
${evidence ? `<div class="section"><div class="k">Evidence</div><div class="v mono pre">${evidence}</div></div>` : ""}
|
|
102
|
+
${remediation ? `<div class="section"><div class="k">Remediation</div><div class="v">${remediation}</div></div>` : ""}
|
|
103
|
+
</div>`;
|
|
104
|
+
})
|
|
105
|
+
.join("\n");
|
|
106
|
+
const summary = scanResult.summary;
|
|
107
|
+
// Build errors section
|
|
108
|
+
let errorsHtml = "";
|
|
109
|
+
if (scanErrors.length > 0 || pluginErrors.size > 0) {
|
|
110
|
+
const errorRows = scanErrors.map((e) => `
|
|
111
|
+
<tr>
|
|
112
|
+
<td class="mono">${escapeHtml(e.url)}</td>
|
|
113
|
+
<td>Crawl Error</td>
|
|
114
|
+
<td>${escapeHtml(e.error)}</td>
|
|
115
|
+
</tr>
|
|
116
|
+
`).join("");
|
|
117
|
+
const pluginErrorRows = [];
|
|
118
|
+
pluginErrors.forEach((errors, pluginName) => {
|
|
119
|
+
errors.forEach((e) => {
|
|
120
|
+
pluginErrorRows.push(`
|
|
121
|
+
<tr>
|
|
122
|
+
<td class="mono">${escapeHtml(e.url)}</td>
|
|
123
|
+
<td>${escapeHtml(pluginName)}</td>
|
|
124
|
+
<td>${escapeHtml(e.error)}</td>
|
|
125
|
+
</tr>
|
|
126
|
+
`);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
const totalErrors = scanErrors.length + pluginErrorRows.length;
|
|
130
|
+
errorsHtml = `
|
|
131
|
+
<div class="section-header">
|
|
132
|
+
<h2>⚠️ Scan Errors & Skipped Items</h2>
|
|
133
|
+
<span class="badge badge-warning">${totalErrors} items</span>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="error-table-container">
|
|
136
|
+
<table class="error-table">
|
|
137
|
+
<thead>
|
|
138
|
+
<tr>
|
|
139
|
+
<th>URL</th>
|
|
140
|
+
<th>Source</th>
|
|
141
|
+
<th>Error</th>
|
|
142
|
+
</tr>
|
|
143
|
+
</thead>
|
|
144
|
+
<tbody>
|
|
145
|
+
${errorRows}
|
|
146
|
+
${pluginErrorRows.join("")}
|
|
147
|
+
</tbody>
|
|
148
|
+
</table>
|
|
149
|
+
</div>`;
|
|
150
|
+
}
|
|
151
|
+
return `<!doctype html>
|
|
152
|
+
<html>
|
|
153
|
+
<head>
|
|
154
|
+
<meta charset="utf-8" />
|
|
155
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
156
|
+
<title>KramScan Security Report</title>
|
|
157
|
+
<style>
|
|
158
|
+
:root {
|
|
159
|
+
--bg: #0b1020;
|
|
160
|
+
--panel: #111a33;
|
|
161
|
+
--panel2: #0e1630;
|
|
162
|
+
--text: #e9eefc;
|
|
163
|
+
--muted: #a9b6e5;
|
|
164
|
+
--line: rgba(233,238,252,0.14);
|
|
165
|
+
--critical: #ff4d4f;
|
|
166
|
+
--high: #ff7a45;
|
|
167
|
+
--medium: #fadb14;
|
|
168
|
+
--low: #40a9ff;
|
|
169
|
+
--info: #8c8c8c;
|
|
170
|
+
--warning: #faad14;
|
|
171
|
+
}
|
|
172
|
+
* { box-sizing: border-box; }
|
|
173
|
+
body {
|
|
174
|
+
margin: 0;
|
|
175
|
+
padding: 28px;
|
|
176
|
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, "Noto Sans", "Helvetica Neue", sans-serif;
|
|
177
|
+
color: var(--text);
|
|
178
|
+
background: radial-gradient(1000px 600px at 20% 10%, rgba(64,169,255,0.20), transparent 60%),
|
|
179
|
+
radial-gradient(900px 500px at 70% 0%, rgba(255,77,79,0.18), transparent 55%),
|
|
180
|
+
linear-gradient(180deg, var(--bg), #070a14 70%);
|
|
181
|
+
}
|
|
182
|
+
.top {
|
|
183
|
+
display: flex;
|
|
184
|
+
justify-content: space-between;
|
|
185
|
+
align-items: flex-end;
|
|
186
|
+
gap: 16px;
|
|
187
|
+
padding-bottom: 14px;
|
|
188
|
+
border-bottom: 1px solid var(--line);
|
|
189
|
+
}
|
|
190
|
+
.brand {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
gap: 6px;
|
|
194
|
+
}
|
|
195
|
+
.h1 { font-size: 22px; font-weight: 800; letter-spacing: 0.3px; }
|
|
196
|
+
.sub { color: var(--muted); font-size: 12px; }
|
|
197
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
198
|
+
.pill {
|
|
199
|
+
padding: 6px 10px;
|
|
200
|
+
border: 1px solid var(--line);
|
|
201
|
+
border-radius: 999px;
|
|
202
|
+
background: rgba(17,26,51,0.55);
|
|
203
|
+
color: var(--muted);
|
|
204
|
+
font-size: 12px;
|
|
205
|
+
white-space: nowrap;
|
|
206
|
+
}
|
|
207
|
+
.grid {
|
|
208
|
+
display: grid;
|
|
209
|
+
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
|
210
|
+
gap: 10px;
|
|
211
|
+
margin: 16px 0 10px;
|
|
212
|
+
}
|
|
213
|
+
.stat {
|
|
214
|
+
border: 1px solid var(--line);
|
|
215
|
+
background: rgba(17,26,51,0.55);
|
|
216
|
+
border-radius: 12px;
|
|
217
|
+
padding: 10px 12px;
|
|
218
|
+
}
|
|
219
|
+
.stat .k { color: var(--muted); font-size: 11px; }
|
|
220
|
+
.stat .v { font-size: 18px; font-weight: 800; margin-top: 6px; }
|
|
221
|
+
.cards { margin-top: 14px; display: flex; flex-direction: column; gap: 12px; }
|
|
222
|
+
.card {
|
|
223
|
+
border: 1px solid var(--line);
|
|
224
|
+
background: linear-gradient(180deg, rgba(17,26,51,0.70), rgba(14,22,48,0.70));
|
|
225
|
+
border-radius: 14px;
|
|
226
|
+
padding: 12px 12px 10px;
|
|
227
|
+
page-break-inside: avoid;
|
|
228
|
+
}
|
|
229
|
+
.card-h {
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: auto 1fr auto;
|
|
232
|
+
align-items: center;
|
|
233
|
+
gap: 10px;
|
|
234
|
+
}
|
|
235
|
+
.idx { color: var(--muted); font-weight: 700; }
|
|
236
|
+
.title { font-weight: 800; }
|
|
237
|
+
.badge {
|
|
238
|
+
padding: 4px 8px;
|
|
239
|
+
border-radius: 999px;
|
|
240
|
+
font-size: 11px;
|
|
241
|
+
font-weight: 800;
|
|
242
|
+
border: 1px solid var(--line);
|
|
243
|
+
}
|
|
244
|
+
.badge-critical { background: rgba(255,77,79,0.18); color: #ffd1d1; border-color: rgba(255,77,79,0.45); }
|
|
245
|
+
.badge-high { background: rgba(255,122,69,0.18); color: #ffe1d2; border-color: rgba(255,122,69,0.45); }
|
|
246
|
+
.badge-medium { background: rgba(250,219,20,0.16); color: #fff3bf; border-color: rgba(250,219,20,0.40); }
|
|
247
|
+
.badge-low { background: rgba(64,169,255,0.16); color: #d6ecff; border-color: rgba(64,169,255,0.40); }
|
|
248
|
+
.badge-info { background: rgba(140,140,140,0.16); color: #eee; border-color: rgba(140,140,140,0.35); }
|
|
249
|
+
.badge-warning { background: rgba(250,173,20,0.16); color: #fffbe6; border-color: rgba(250,173,20,0.40); }
|
|
250
|
+
.meta { margin-top: 10px; display: grid; gap: 6px; }
|
|
251
|
+
.section { margin-top: 10px; border-top: 1px dashed rgba(233,238,252,0.20); padding-top: 10px; }
|
|
252
|
+
.section-header {
|
|
253
|
+
display: flex;
|
|
254
|
+
justify-content: space-between;
|
|
255
|
+
align-items: center;
|
|
256
|
+
margin: 24px 0 12px;
|
|
257
|
+
padding-bottom: 8px;
|
|
258
|
+
border-bottom: 1px solid var(--line);
|
|
259
|
+
}
|
|
260
|
+
.section-header h2 {
|
|
261
|
+
margin: 0;
|
|
262
|
+
font-size: 16px;
|
|
263
|
+
color: var(--warning);
|
|
264
|
+
}
|
|
265
|
+
.error-table-container {
|
|
266
|
+
border: 1px solid var(--line);
|
|
267
|
+
border-radius: 12px;
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
background: rgba(17,26,51,0.55);
|
|
270
|
+
}
|
|
271
|
+
.error-table {
|
|
272
|
+
width: 100%;
|
|
273
|
+
border-collapse: collapse;
|
|
274
|
+
font-size: 12px;
|
|
275
|
+
}
|
|
276
|
+
.error-table th {
|
|
277
|
+
background: rgba(17,26,51,0.80);
|
|
278
|
+
padding: 10px 12px;
|
|
279
|
+
text-align: left;
|
|
280
|
+
color: var(--muted);
|
|
281
|
+
font-weight: 600;
|
|
282
|
+
border-bottom: 1px solid var(--line);
|
|
283
|
+
}
|
|
284
|
+
.error-table td {
|
|
285
|
+
padding: 8px 12px;
|
|
286
|
+
border-bottom: 1px solid rgba(233,238,252,0.10);
|
|
287
|
+
color: var(--text);
|
|
288
|
+
}
|
|
289
|
+
.error-table tr:last-child td {
|
|
290
|
+
border-bottom: none;
|
|
291
|
+
}
|
|
292
|
+
.error-table tr:hover td {
|
|
293
|
+
background: rgba(255,255,255,0.03);
|
|
294
|
+
}
|
|
295
|
+
.k { color: var(--muted); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; }
|
|
296
|
+
.v { margin-top: 6px; font-size: 12px; line-height: 1.45; }
|
|
297
|
+
.pre { white-space: pre-wrap; }
|
|
298
|
+
.footer { margin-top: 18px; color: var(--muted); font-size: 11px; border-top: 1px solid var(--line); padding-top: 12px; }
|
|
299
|
+
</style>
|
|
300
|
+
</head>
|
|
301
|
+
<body>
|
|
302
|
+
<div class="top">
|
|
303
|
+
<div class="brand">
|
|
304
|
+
<div class="h1">KramScan Security Report</div>
|
|
305
|
+
<div class="sub">Target: <span class="mono">${escapeHtml(scanResult.target)}</span></div>
|
|
306
|
+
<div class="sub">Timestamp: <span class="mono">${escapeHtml(scanResult.timestamp)}</span></div>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="pill">Automated PDF generated after scan</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div class="grid">
|
|
312
|
+
<div class="stat"><div class="k">Total</div><div class="v">${summary.total}</div></div>
|
|
313
|
+
<div class="stat"><div class="k">Critical</div><div class="v">${summary.critical}</div></div>
|
|
314
|
+
<div class="stat"><div class="k">High</div><div class="v">${summary.high}</div></div>
|
|
315
|
+
<div class="stat"><div class="k">Medium</div><div class="v">${summary.medium}</div></div>
|
|
316
|
+
<div class="stat"><div class="k">Low</div><div class="v">${summary.low}</div></div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<div class="sub">Crawled URLs: <span class="mono">${scanResult.metadata.crawledUrls}</span> | Forms tested: <span class="mono">${scanResult.metadata.testedForms}</span> | Requests: <span class="mono">${scanResult.metadata.requestsMade}</span> | Duration: <span class="mono">${(scanResult.duration / 1000).toFixed(2)}s</span></div>
|
|
320
|
+
|
|
321
|
+
<div style="margin-top: 20px; display: flex; align-items: center; gap: 15px; background: rgba(17,26,51,0.55); padding: 15px; border-radius: 12px; border: 1px solid var(--line);">
|
|
322
|
+
<div style="font-size: 24px; font-weight: 900; color: ${scanResult.score > 80 ? "#52c41a" : (scanResult.score > 50 ? "#faad14" : "#ff4d4f")};">
|
|
323
|
+
${scanResult.score}/100
|
|
324
|
+
</div>
|
|
325
|
+
<div style="flex-grow: 1;">
|
|
326
|
+
<div style="font-size: 11px; text-transform: uppercase; color: var(--muted); letter-spacing: 0.5px; margin-bottom: 5px;">Security Posture</div>
|
|
327
|
+
<div style="height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden;">
|
|
328
|
+
<div style="width: ${scanResult.score}%; height: 100%; background: ${scanResult.score > 80 ? "#52c41a" : (scanResult.score > 50 ? "#faad14" : "#ff4d4f")};"></div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div style="font-size: 12px; font-weight: 700; color: var(--muted);">
|
|
332
|
+
${scanResult.score > 80 ? "EXCELLENT" : (scanResult.score > 50 ? "FAIR" : "POOR")}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div class="cards">
|
|
337
|
+
${rows || `<div class="card"><div class="title">No vulnerabilities found</div><div class="v">The scanner did not detect issues in the tested scope.</div></div>`}
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
${errorsHtml}
|
|
341
|
+
|
|
342
|
+
<div class="footer">Generated by KramScan</div>
|
|
343
|
+
</body>
|
|
344
|
+
</html>`;
|
|
345
|
+
}
|
|
346
|
+
class PdfGenerator {
|
|
347
|
+
async generate(data, options = {}) {
|
|
348
|
+
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
349
|
+
const targetUrl = new URL(data.scanResult.target);
|
|
350
|
+
const host = sanitizeFilenamePart(targetUrl.hostname || "unknown");
|
|
351
|
+
const timestamp = new Date(data.scanResult.timestamp || new Date().toISOString())
|
|
352
|
+
.toISOString()
|
|
353
|
+
.replace(/[:.]/g, "-");
|
|
354
|
+
const pdfFilename = options.filename || `scanreport_${host}_${timestamp}.pdf`;
|
|
355
|
+
const pdfPath = path_1.default.join(reportsDir, pdfFilename);
|
|
356
|
+
const browser = await puppeteer_1.default.launch({
|
|
357
|
+
headless: true,
|
|
358
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
359
|
+
});
|
|
360
|
+
try {
|
|
361
|
+
const page = await browser.newPage();
|
|
362
|
+
const html = buildPdfHtml(data);
|
|
363
|
+
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
364
|
+
await page.pdf({
|
|
365
|
+
path: pdfPath,
|
|
366
|
+
format: options.format || "A4",
|
|
367
|
+
printBackground: true,
|
|
368
|
+
margin: options.margin || {
|
|
369
|
+
top: "12mm",
|
|
370
|
+
bottom: "12mm",
|
|
371
|
+
left: "10mm",
|
|
372
|
+
right: "10mm",
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
await browser.close();
|
|
378
|
+
}
|
|
379
|
+
return pdfPath;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Generate a standalone HTML report
|
|
383
|
+
* @param data - The scan result data
|
|
384
|
+
* @param options - Options for HTML generation
|
|
385
|
+
* @returns Path to the generated HTML file
|
|
386
|
+
*/
|
|
387
|
+
async generateHtml(data, options = {}) {
|
|
388
|
+
const reportsDir = await (0, scan_storage_1.ensureReportsDirectory)();
|
|
389
|
+
const targetUrl = new URL(data.scanResult.target);
|
|
390
|
+
const host = sanitizeFilenamePart(targetUrl.hostname || "unknown");
|
|
391
|
+
const timestamp = new Date(data.scanResult.timestamp || new Date().toISOString())
|
|
392
|
+
.toISOString()
|
|
393
|
+
.replace(/[:.]/g, "-");
|
|
394
|
+
const htmlFilename = options.filename || `scanreport_${host}_${timestamp}.html`;
|
|
395
|
+
const htmlPath = path_1.default.join(reportsDir, htmlFilename);
|
|
396
|
+
const html = buildPdfHtml(data);
|
|
397
|
+
// Write the HTML file
|
|
398
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs/promises")));
|
|
399
|
+
await fs.writeFile(htmlPath, html, "utf-8");
|
|
400
|
+
return htmlPath;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
exports.PdfGenerator = PdfGenerator;
|
|
404
|
+
exports.pdfGenerator = new PdfGenerator();
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
import { Ora } from "ora";
|
|
2
|
+
export declare enum LogLevel {
|
|
3
|
+
DEBUG = 0,
|
|
4
|
+
INFO = 1,
|
|
5
|
+
SUCCESS = 2,
|
|
6
|
+
WARN = 3,
|
|
7
|
+
ERROR = 4
|
|
8
|
+
}
|
|
9
|
+
export interface LogEntry {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
level: string;
|
|
12
|
+
message: string;
|
|
13
|
+
context?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface LoggerOptions {
|
|
16
|
+
level?: LogLevel;
|
|
17
|
+
jsonOutput?: boolean;
|
|
18
|
+
includeTimestamp?: boolean;
|
|
19
|
+
includeContext?: boolean;
|
|
20
|
+
}
|
|
2
21
|
export declare const logger: {
|
|
3
22
|
info: (message: string) => void;
|
|
4
23
|
success: (message: string) => void;
|
|
5
24
|
warn: (message: string) => void;
|
|
6
25
|
error: (message: string) => void;
|
|
7
|
-
debug: (message: string) => void;
|
|
26
|
+
debug: (message: string, context?: Record<string, unknown>) => void;
|
|
8
27
|
spinner: (text: string) => Ora;
|
|
9
28
|
};
|
|
29
|
+
export declare const structuredLogger: {
|
|
30
|
+
debug: (message: string, context?: Record<string, unknown>) => void;
|
|
31
|
+
info: (message: string, context?: Record<string, unknown>) => void;
|
|
32
|
+
warn: (message: string, context?: Record<string, unknown>) => void;
|
|
33
|
+
error: (message: string, context?: Record<string, unknown>) => void;
|
|
34
|
+
log: (level: LogLevel, message: string, context?: Record<string, unknown>) => void;
|
|
35
|
+
};
|
|
36
|
+
export declare function createChildLogger(defaultContext: Record<string, unknown>): {
|
|
37
|
+
debug: (message: string, context?: Record<string, unknown>) => void;
|
|
38
|
+
info: (message: string, context?: Record<string, unknown>) => void;
|
|
39
|
+
warn: (message: string, context?: Record<string, unknown>) => void;
|
|
40
|
+
error: (message: string, context?: Record<string, unknown>) => void;
|
|
41
|
+
};
|