rav-xss 1.0.28

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/src/index.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const { exec } = require("child_process");
7
+ const { parseArgs, hasHelp, shouldConfigure, shouldOpenReports } = require("./cli/args");
8
+ const { showHelp } = require("./cli/help");
9
+ const { runWizard } = require("./cli/wizard");
10
+ const { loadConfig, validateConfig } = require("./config/manager");
11
+ const { XSSScanner } = require("./core/scanner");
12
+ const { colors } = require("./config/colors");
13
+
14
+ /**
15
+ * 📁 Abre a pasta de relatórios no explorador de arquivos
16
+ * @param {string} reportDir - Caminho do diretório de relatórios
17
+ */
18
+ const openReportsFolder = (reportDir) => {
19
+ const resolvedPath = path.resolve(reportDir);
20
+
21
+ console.log(`\n${colors.text("📂 Reports folder:")} ${colors.link(resolvedPath)}\n`);
22
+
23
+ if (!fs.existsSync(resolvedPath)) {
24
+ console.log(colors.warning("⚠️ Reports folder doesn't exist yet. Creating..."));
25
+ try {
26
+ fs.mkdirSync(resolvedPath, { recursive: true });
27
+ console.log(colors.success("✅ Reports folder created!"));
28
+ } catch (err) {
29
+ console.log(colors.error(`❌ Could not create folder: ${err.message}`));
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ const platform = process.platform;
35
+
36
+ if (platform === "win32") {
37
+ exec(`explorer "${resolvedPath}"`);
38
+ setTimeout(() => {
39
+ console.log(colors.success("✅ Reports folder opened!"));
40
+ process.exit(0);
41
+ }, 500);
42
+ } else if (platform === "darwin") {
43
+ exec(`open "${resolvedPath}"`, (error) => {
44
+ if (error) {
45
+ console.log(colors.error(`❌ Could not open folder: ${error.message}`));
46
+ console.log(colors.text(`📂 Location: ${colors.link(resolvedPath)}`));
47
+ } else {
48
+ console.log(colors.success("✅ Reports folder opened!"));
49
+ }
50
+ process.exit(0);
51
+ });
52
+ } else {
53
+ exec(`xdg-open "${resolvedPath}"`, (error) => {
54
+ if (error) {
55
+ console.log(colors.error(`❌ Could not open folder: ${error.message}`));
56
+ console.log(colors.text(`📂 Location: ${colors.link(resolvedPath)}`));
57
+ } else {
58
+ console.log(colors.success("✅ Reports folder opened!"));
59
+ }
60
+ process.exit(0);
61
+ });
62
+ }
63
+ };
64
+
65
+ /**
66
+ * 🚀 Função principal
67
+ * @returns {Promise<void>}
68
+ */
69
+ const main = async () => {
70
+ const args = parseArgs();
71
+
72
+ if (hasHelp(args)) {
73
+ showHelp();
74
+ process.exit(0);
75
+ }
76
+
77
+ if (shouldConfigure(args)) {
78
+ await runWizard();
79
+ process.exit(0);
80
+ }
81
+
82
+ if (shouldOpenReports(args)) {
83
+ const config = loadConfig();
84
+ openReportsFolder(config.scanner.report_dir);
85
+ return;
86
+ }
87
+
88
+ const config = loadConfig();
89
+ if (!validateConfig(config)) {
90
+ console.error("Invalid configuration. Run --configure to set up.");
91
+ process.exit(1);
92
+ }
93
+
94
+ if (args.mode) {
95
+ config.mode = args.mode;
96
+ }
97
+
98
+ if (args.headed) {
99
+ if (!config.scanner) config.scanner = {};
100
+ config.scanner.headless = false;
101
+ }
102
+
103
+ const scanner = new XSSScanner(config, args);
104
+ await scanner.run();
105
+ };
106
+
107
+ main().catch((err) => {
108
+ console.error(`\n[FATAL] ${err.message}\n`);
109
+ if (process.argv.includes("--verbose")) {
110
+ console.error(err.stack);
111
+ }
112
+ process.exit(1);
113
+ });
Binary file
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+
3
+ const boxen = require("boxen");
4
+ const { colors, theme } = require("../config/colors");
5
+ const packageInfo = require("./packageInfo");
6
+
7
+ class BoxManager {
8
+
9
+ get appInfo() {
10
+ return packageInfo.allInfo;
11
+ }
12
+
13
+ createBox(content, options = {}) {
14
+ const defaultOptions = {
15
+ padding: 1,
16
+ margin: 1,
17
+ borderStyle: "round",
18
+ borderColor: theme.border.primary,
19
+ textAlignment: "left"
20
+ };
21
+
22
+ try {
23
+ return boxen(content, { ...defaultOptions, ...options });
24
+ } catch (error) {
25
+ const width = options.width || 60;
26
+ const border = "─".repeat(width);
27
+ return `\n┌${border}┐\n${content}\n└${border}┘\n`;
28
+ }
29
+ }
30
+
31
+ createWelcomeBox(config, totalPayloads, category, targetUrl) {
32
+ const version = packageInfo.version || "1.0.0";
33
+ const contentLines = [];
34
+
35
+ contentLines.push(colors.info.bold("🛡️ Bug Bounty Edition"));
36
+ contentLines.push(colors.dim("─".repeat(45)));
37
+ contentLines.push("");
38
+
39
+ try {
40
+ const figlet = require("figlet");
41
+ const bannerText = figlet.textSync(packageInfo.name.toUpperCase(), {
42
+ font: "ANSI Shadow",
43
+ horizontalLayout: "default",
44
+ verticalLayout: "default"
45
+ });
46
+ contentLines.push(colors.action(bannerText));
47
+ } catch (e) {
48
+ contentLines.push(colors.action.bold(`⚡ ${packageInfo.name.toUpperCase()} ⚡`));
49
+ }
50
+
51
+ contentLines.push("");
52
+ contentLines.push(`${colors.icon.link} ${colors.link(packageInfo.site)}`);
53
+ contentLines.push("");
54
+
55
+ const infoItems = [
56
+ `${colors.icon.version} ${colors.muted("Version:")} ${colors.primary.bold("v" + version)}`,
57
+ `${colors.icon.payload} ${colors.muted("Payloads:")} ${colors.primary.bold(String(totalPayloads))}`,
58
+ `${colors.icon.category} ${colors.muted("Category:")} ${colors.highlight(category || "Not selected")}`,
59
+ `${colors.icon.target} ${colors.muted("Target:")} ${colors.url(this.truncateUrl(targetUrl))}`
60
+ ];
61
+
62
+ contentLines.push(infoItems.join(`\n`));
63
+
64
+ const content = contentLines.join("\n");
65
+
66
+ return this.createBox(content, {
67
+ borderStyle: "round",
68
+ borderColor: theme.border.primary,
69
+ padding: {
70
+ top: 1,
71
+ bottom: 2,
72
+ left: 3,
73
+ right: 3
74
+ },
75
+ margin: 1,
76
+ textAlignment: "center"
77
+ });
78
+ }
79
+
80
+ createTargetBox(target, index, total) {
81
+ const formattedContent = [
82
+ `${colors.primary.bold(`Target ${index + 1} of ${total}`)} ${colors.dim("•")} ${colors.title(target.name || "CLI Target")}`,
83
+ colors.text(target.url)
84
+ ].join("\n");
85
+
86
+ return this.createBox(formattedContent, {
87
+ borderStyle: "round",
88
+ borderColor: theme.border.info,
89
+ padding: 1,
90
+ margin: { top: 1, bottom: 1 }
91
+ });
92
+ }
93
+
94
+ /**
95
+ * 📊 Cria box de resultado do scan
96
+ * @param {Object} results - Resultados do scan
97
+ * @param {string} targetUrl - URL alvo
98
+ * @param {string} category - Categoria testada
99
+ * @param {string} duration - Duração do scan
100
+ * @param {string} reportPath - Caminho do relatório
101
+ * @param {string} reportDir - Diretório de relatórios
102
+ * @returns {string} Box formatado
103
+ */
104
+ createResultBox(results, targetUrl, category, duration, reportPath, reportDir) {
105
+ const hasVulns = results.vulns_found > 0;
106
+ const statusIcon = hasVulns ? colors.icon.error : colors.icon.success;
107
+ const statusColor = hasVulns ? colors.error : colors.success;
108
+ const statusText = hasVulns ? `${results.vulns_found} XSS FOUND` : "ALL CLEAN";
109
+
110
+ const isTermux = this.detectTermux();
111
+ const maxWidth = isTermux ? 80 : 80;
112
+ const boxWidth = isTermux ? 90 : 100;
113
+
114
+ const displayReportPath = this.wrapPath(reportPath, maxWidth, "Report");
115
+ const displayReportDir = this.wrapPath(reportDir, maxWidth, "Reports folder");
116
+
117
+ const contentLines = [
118
+ `${colors.action.bold(`${statusIcon} SCAN COMPLETE`)}`,
119
+ "",
120
+ `${colors.muted("Target")} ${colors.url(this.truncateUrl(targetUrl, maxWidth))}`,
121
+ `${colors.muted("Category")} ${colors.highlight(category)}`,
122
+ `${colors.muted("Tests")} ${colors.primary.bold(String(results.total_tests))}`,
123
+ `${colors.muted("Duration")} ${colors.highlight.bold(duration + "s")}`,
124
+ `${colors.muted("Result")} ${statusColor.bold(statusText)}`,
125
+ "",
126
+ `${colors.muted("📄 Report:")}`,
127
+ `${colors.link(displayReportPath)}`,
128
+ ];
129
+
130
+ if (reportDir) {
131
+ contentLines.push("");
132
+ contentLines.push(`${colors.muted("📂 Reports folder:")}`);
133
+ contentLines.push(`${colors.dim(displayReportDir)}`);
134
+ }
135
+
136
+ contentLines.push("");
137
+ contentLines.push(`${colors.dim("─".repeat(50))}`);
138
+ contentLines.push("");
139
+
140
+ if (isTermux && reportDir) {
141
+ contentLines.push(`${colors.muted("📋 List reports (Termux):")}`);
142
+ contentLines.push(`${colors.action.bold(` ls -la ${reportDir}/`)}`);
143
+ contentLines.push("");
144
+ }
145
+
146
+ contentLines.push(`${colors.text("📁 To open reports folder:")}`);
147
+ contentLines.push(`${colors.action.bold(" rav-xss --open-reports")}`);
148
+ contentLines.push(`${colors.muted(" or")}`);
149
+ contentLines.push(`${colors.action.bold(" rav-xss -r")}`);
150
+
151
+ return this.createBox(contentLines.join("\n"), {
152
+ borderStyle: "double",
153
+ padding: isTermux ? { top: 1, bottom: 1, left: 2, right: 2 } : 2,
154
+ margin: { top: 2, bottom: 1 },
155
+ borderColor: hasVulns ? theme.border.error : theme.border.success,
156
+ width: isTermux ? boxWidth : undefined
157
+ });
158
+ }
159
+
160
+ /**
161
+ * 📱 Detecta se está executando no Termux
162
+ * @returns {boolean} true se estiver no Termux
163
+ */
164
+ detectTermux() {
165
+ if (process.env.TERMUX_VERSION) return true;
166
+ if (process.env.PREFIX?.includes("com.termux")) return true;
167
+
168
+ try {
169
+ const os = require("os");
170
+ const hostname = os.hostname();
171
+ if (hostname && (
172
+ hostname.toLowerCase().includes("termux") ||
173
+ hostname.toLowerCase().includes("android")
174
+ )) return true;
175
+ } catch (e) { }
176
+
177
+ try {
178
+ const fs = require("fs");
179
+ if (fs.existsSync("/data/data/com.termux")) return true;
180
+ } catch (e) { }
181
+
182
+ return false;
183
+ }
184
+
185
+ /**
186
+ * 📏 Formata caminho com quebra de linha se necessário
187
+ * @param {string} filePath - Caminho completo
188
+ * @param {number} maxLength - Tamanho máximo por linha
189
+ * @param {string} label - Rótulo para indentação
190
+ * @returns {string} Caminho formatado
191
+ */
192
+ wrapPath(filePath, maxLength = 55, label = "") {
193
+ if (!filePath) return "N/A";
194
+
195
+ const indent = " ".repeat(14);
196
+
197
+ if (filePath.length <= maxLength) {
198
+ return filePath;
199
+ }
200
+
201
+ const parts = [];
202
+ let remaining = filePath;
203
+ let firstLine = true;
204
+
205
+ while (remaining.length > 0) {
206
+ if (remaining.length <= maxLength) {
207
+ parts.push(remaining);
208
+ break;
209
+ }
210
+
211
+ let splitPos = remaining.lastIndexOf("/", maxLength);
212
+ if (splitPos === -1 || splitPos < maxLength / 2) {
213
+ splitPos = remaining.lastIndexOf("\\", maxLength);
214
+ }
215
+ if (splitPos === -1 || splitPos < maxLength / 2) {
216
+ splitPos = maxLength;
217
+ }
218
+
219
+ parts.push(remaining.substring(0, splitPos + 1));
220
+ remaining = remaining.substring(splitPos + 1);
221
+ firstLine = false;
222
+ }
223
+
224
+ const formattedParts = parts.map((part, index) => {
225
+ if (index === 0) return part;
226
+ return indent + part;
227
+ });
228
+
229
+ return formattedParts.join("\n");
230
+ }
231
+
232
+ createExitBox() {
233
+ const content = [
234
+ colors.highlight.bold("👋 GOODBYE!"),
235
+ "",
236
+ colors.text(packageInfo.name),
237
+ colors.muted("Authorized testing only"),
238
+ "",
239
+ `${colors.text("Feito com")} ${colors.danger("💚")} ${colors.text("por")} ${colors.primary.bold(packageInfo.wuser)}`,
240
+ "",
241
+ `${colors.icon.link} ${colors.link(packageInfo.site)}`
242
+ ].join("\n");
243
+
244
+ return this.createBox(content, {
245
+ borderStyle: "round",
246
+ borderColor: theme.border.warning,
247
+ padding: 2,
248
+ margin: 1,
249
+ textAlignment: "center"
250
+ });
251
+ }
252
+
253
+ /**
254
+ * 🔗 Trunca a URL para exibição compacta
255
+ * @param {string} url - URL completa
256
+ * @param {number} maxLength - Comprimento máximo
257
+ * @returns {string} URL truncada
258
+ */
259
+ truncateUrl(url, maxLength = 55) {
260
+ if (!url) return "N/A";
261
+ if (url.length <= maxLength) return url;
262
+ return url.substring(0, maxLength - 3) + "...";
263
+ }
264
+ }
265
+
266
+ module.exports = new BoxManager();
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7
+
8
+ const ensureDir = (dirPath) => {
9
+ if (!fs.existsSync(dirPath)) {
10
+ fs.mkdirSync(dirPath, { recursive: true });
11
+ }
12
+ return true;
13
+ };
14
+
15
+ const timestamp = () => new Date().toISOString().replace(/[:.]/g, "-");
16
+
17
+ const loadPayloads = (filePath) => {
18
+ const content = fs.readFileSync(filePath, "utf8");
19
+ return content.split("\n").map(l => l.trim()).filter(Boolean);
20
+ };
21
+
22
+ module.exports = { sleep, ensureDir, timestamp, loadPayloads };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ const { colors } = require("../config/colors");
4
+ const boxManager = require("./box");
5
+ const packageInfo = require("./packageInfo");
6
+
7
+ class Logger {
8
+ static log(level, msg) {
9
+ const styles = {
10
+ info: `${colors.action.bold(" ℹ ")}${colors.text(msg)}`,
11
+ vuln: `${colors.error.bold(" ⚠ ")}${colors.danger(msg)}`,
12
+ safe: `${colors.success(" ✓ ")}${colors.muted(msg)}`,
13
+ warn: `${colors.warning.bold(" ⚡ ")}${colors.highlight2(msg)}`,
14
+ done: `${colors.success.bold(" ✔ ")}${colors.text(msg)}`,
15
+ error: `${colors.error.bold(" ✗ ")}${colors.danger(msg)}`
16
+ };
17
+ console.log(styles[level] || ` ${msg}`);
18
+ }
19
+
20
+ static showBanner(config, totalPayloads, category, targetUrl) {
21
+ console.clear();
22
+ const banner = boxManager.createWelcomeBox(config, totalPayloads, category, targetUrl);
23
+ console.log(banner);
24
+ console.log(colors.dim("\n" + "─".repeat(60) + "\n"));
25
+ }
26
+
27
+ static showTarget(target, index, total) {
28
+ const box = boxManager.createTargetBox(target, index, total);
29
+ console.log(box);
30
+ }
31
+
32
+ static showResults(results, targetUrl, category, duration, reportPath, reportDir) {
33
+ const box = boxManager.createResultBox(results, targetUrl, category, duration, reportPath, reportDir);
34
+ console.log(box);
35
+ }
36
+
37
+ static showExit() {
38
+ console.clear();
39
+ const box = boxManager.createExitBox();
40
+ console.log(box);
41
+ }
42
+ }
43
+
44
+ module.exports = { Logger };
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ /**
7
+ * 📄 Carrega informações do package.json
8
+ */
9
+ class PackageInfo {
10
+ constructor() {
11
+ this.packageData = this.loadPackageInfo();
12
+ }
13
+
14
+ /**
15
+ * Carrega informações do package.json
16
+ * @returns {Object} Dados do package.json
17
+ * @private
18
+ */
19
+ loadPackageInfo() {
20
+ try {
21
+ const possiblePaths = [
22
+ path.join(__dirname, "..", "..", "package.json"),
23
+ path.join(__dirname, "..", "package.json"),
24
+ path.join(process.cwd(), "package.json"),
25
+ path.join(__dirname, "package.json"),
26
+ ];
27
+
28
+ let packagePath = null;
29
+ let packageJson = null;
30
+
31
+ const triedPaths = [];
32
+
33
+ for (const testPath of possiblePaths) {
34
+ triedPaths.push(testPath);
35
+ if (fs.existsSync(testPath)) {
36
+ packagePath = testPath;
37
+ packageJson = fs.readFileSync(testPath, "utf8");
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (!packagePath || !packageJson) {
43
+ throw new Error(
44
+ `package.json not found!\n` +
45
+ `Tried paths:\n` +
46
+ triedPaths.map(p => ` - ${p} (${fs.existsSync(p) ? 'EXISTS' : 'NOT FOUND'})`).join("\n")
47
+ );
48
+ }
49
+
50
+ const data = JSON.parse(packageJson);
51
+
52
+ return {
53
+ name: data.name || "rav-xss",
54
+ version: data.version || "V1",
55
+ wuser: "RavenaStar",
56
+ site: "https://ravenastar.com",
57
+ description: data.description || "⚙️ CLI/NPM | RAV XSS | 🎯 Basic Reflected XSS scanner for bug bounty programs.",
58
+ author: data.author || "RavenaStar",
59
+ license: data.license || "MIT",
60
+ homepage: data.homepage || "https://github.com/ravenastar-js/rav-xss/"
61
+ };
62
+ } catch (error) {
63
+ if (process.argv.includes("--verbose") || process.env.NODE_ENV === "development") {
64
+ console.error("❌ Erro ao carregar package.json:", error.message);
65
+ }
66
+ return this.getFallbackInfo();
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Informações de fallback
72
+ * @returns {Object} Dados padrão
73
+ */
74
+ getFallbackInfo() {
75
+ return {
76
+ name: "rav-xss",
77
+ version: "V1",
78
+ wuser: "RavenaStar",
79
+ site: "https://ravenastar.com",
80
+ description: "⚙️ CLI/NPM | RAV XSS | 🎯 Basic Reflected XSS scanner for bug bounty programs.",
81
+ author: "ravenastar-js",
82
+ license: "MIT",
83
+ homepage: "https://github.com/ravenastar-js/rav-xss/"
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Força recarregar do disco (útil se o package.json foi atualizado)
89
+ */
90
+ reload() {
91
+ this.packageData = this.loadPackageInfo();
92
+ return this.packageData;
93
+ }
94
+
95
+ /**
96
+ * Retorna todas as informações
97
+ * @returns {Object} Todas as informações
98
+ */
99
+ get allInfo() {
100
+ return this.packageData;
101
+ }
102
+
103
+ /**
104
+ * Retorna o nome
105
+ * @returns {string} Nome do pacote
106
+ */
107
+ get name() {
108
+ return this.packageData.name;
109
+ }
110
+
111
+ /**
112
+ * Retorna a versão
113
+ * @returns {string} Versão
114
+ */
115
+ get version() {
116
+ return this.packageData.version;
117
+ }
118
+
119
+ /**
120
+ * Retorna a descrição
121
+ * @returns {string} Descrição
122
+ */
123
+ get description() {
124
+ return this.packageData.description;
125
+ }
126
+
127
+ /**
128
+ * Retorna o autor
129
+ * @returns {string} Autor
130
+ */
131
+ get author() {
132
+ return this.packageData.author;
133
+ }
134
+
135
+ /**
136
+ * Retorna o usuário/criador
137
+ * @returns {string} wuser
138
+ */
139
+ get wuser() {
140
+ return this.packageData.wuser;
141
+ }
142
+
143
+ /**
144
+ * Retorna o site
145
+ * @returns {string} Site
146
+ */
147
+ get site() {
148
+ return this.packageData.site;
149
+ }
150
+
151
+ /**
152
+ * Retorna a licença
153
+ * @returns {string} Licença
154
+ */
155
+ get license() {
156
+ return this.packageData.license;
157
+ }
158
+
159
+ /**
160
+ * Retorna a homepage
161
+ * @returns {string} Homepage
162
+ */
163
+ get homepage() {
164
+ return this.packageData.homepage;
165
+ }
166
+
167
+ /**
168
+ * Debug: Mostra qual caminho foi usado
169
+ * @returns {string} Caminho do package.json
170
+ */
171
+ get debugPath() {
172
+ try {
173
+ const possiblePaths = [
174
+ path.join(__dirname, "..", "..", "package.json"),
175
+ path.join(__dirname, "..", "package.json"),
176
+ path.join(process.cwd(), "package.json"),
177
+ path.join(__dirname, "package.json"),
178
+ ];
179
+
180
+ for (const testPath of possiblePaths) {
181
+ if (fs.existsSync(testPath)) {
182
+ return `FOUND: ${testPath}`;
183
+ }
184
+ }
185
+ return "NOT FOUND in any path";
186
+ } catch (e) {
187
+ return "ERROR: " + e.message;
188
+ }
189
+ }
190
+ }
191
+
192
+ module.exports = new PackageInfo();
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { timestamp } = require("./helpers");
6
+
7
+ class Reporter {
8
+ constructor(reportDir) {
9
+ this.reportDir = path.resolve(reportDir);
10
+ }
11
+
12
+ generateTextReport(results, targetUrl) {
13
+ const duration = ((new Date(results.scan_end) - new Date(results.scan_start)) / 1000).toFixed(1);
14
+ let report = "";
15
+ report += "═".repeat(60) + "\n";
16
+ report += " XSS REFLECTION SCANNER — REPORT\n";
17
+ report += "═".repeat(60) + "\n\n";
18
+ report += ` Date : ${new Date(results.scan_start).toLocaleString("en-US")}\n`;
19
+ report += ` Target : ${targetUrl}\n`;
20
+ report += ` Duration : ${duration}s\n`;
21
+ report += ` Tests : ${results.total_tests}\n`;
22
+ report += ` Vulnerable : ${results.vulns_found}\n`;
23
+ report += "\n" + "─".repeat(60) + "\n\n";
24
+
25
+ if (results.findings.length > 0) {
26
+ report += " FINDINGS:\n";
27
+ report += " " + "─".repeat(55) + "\n";
28
+ for (const f of results.findings) {
29
+ report += `\n [#${f.index}] ${f.payload}\n`;
30
+ report += ` URL: ${f.url}\n`;
31
+ }
32
+ } else {
33
+ report += " ✓ No reflected XSS detected.\n";
34
+ }
35
+
36
+ report += "\n" + "═".repeat(60) + "\n";
37
+ report += ` Completed: ${new Date(results.scan_end).toLocaleString("en-US")}\n`;
38
+ report += " Authorized testing only.\n";
39
+ report += "═".repeat(60) + "\n";
40
+ return report;
41
+ }
42
+
43
+ saveReport(results, targetUrl) {
44
+ if (!fs.existsSync(this.reportDir)) {
45
+ fs.mkdirSync(this.reportDir, { recursive: true });
46
+ }
47
+
48
+ const ts = timestamp();
49
+ const textReport = this.generateTextReport(results, targetUrl);
50
+ const textPath = path.join(this.reportDir, `xss_report_${ts}.txt`).replace(/\\/g, "/");
51
+ fs.writeFileSync(textPath, textReport);
52
+ return { textPath };
53
+ }
54
+ }
55
+
56
+ module.exports = { Reporter };