@kernel.chat/kbot 3.57.0 → 3.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +4 -4
  2. package/dist/agents/replit.js +1 -1
  3. package/dist/bootstrap.js +1 -1
  4. package/dist/integrations/ableton-live.d.ts +52 -0
  5. package/dist/integrations/ableton-live.d.ts.map +1 -0
  6. package/dist/integrations/ableton-live.js +239 -0
  7. package/dist/integrations/ableton-live.js.map +1 -0
  8. package/dist/integrations/ableton-osc-installer.d.ts +13 -0
  9. package/dist/integrations/ableton-osc-installer.d.ts.map +1 -0
  10. package/dist/integrations/ableton-osc-installer.js +190 -0
  11. package/dist/integrations/ableton-osc-installer.js.map +1 -0
  12. package/dist/tools/ctf.d.ts +2 -0
  13. package/dist/tools/ctf.d.ts.map +1 -0
  14. package/dist/tools/ctf.js +2968 -0
  15. package/dist/tools/ctf.js.map +1 -0
  16. package/dist/tools/hacker-toolkit.d.ts +2 -0
  17. package/dist/tools/hacker-toolkit.d.ts.map +1 -0
  18. package/dist/tools/hacker-toolkit.js +3697 -0
  19. package/dist/tools/hacker-toolkit.js.map +1 -0
  20. package/dist/tools/index.d.ts.map +1 -1
  21. package/dist/tools/index.js +5 -0
  22. package/dist/tools/index.js.map +1 -1
  23. package/dist/tools/pentest.d.ts +2 -0
  24. package/dist/tools/pentest.d.ts.map +1 -0
  25. package/dist/tools/pentest.js +2225 -0
  26. package/dist/tools/pentest.js.map +1 -0
  27. package/dist/tools/redblue.d.ts +2 -0
  28. package/dist/tools/redblue.d.ts.map +1 -0
  29. package/dist/tools/redblue.js +3468 -0
  30. package/dist/tools/redblue.js.map +1 -0
  31. package/dist/tools/security-brain.d.ts +2 -0
  32. package/dist/tools/security-brain.d.ts.map +1 -0
  33. package/dist/tools/security-brain.js +2453 -0
  34. package/dist/tools/security-brain.js.map +1 -0
  35. package/dist/tools/serum2-preset.d.ts +11 -0
  36. package/dist/tools/serum2-preset.d.ts.map +1 -0
  37. package/dist/tools/serum2-preset.js +143 -0
  38. package/dist/tools/serum2-preset.js.map +1 -0
  39. package/package.json +2 -2
@@ -0,0 +1,2225 @@
1
+ // kbot Pentest Tools — Penetration testing workflow
2
+ // Self-contained using Node.js built-in modules: dns, https, http, tls, net.
3
+ // No external tools or shell-outs required.
4
+ // Findings stored in ~/.kbot/pentest/{session-id}/
5
+ import { registerTool } from './index.js';
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
9
+ import * as dns from 'node:dns';
10
+ import * as https from 'node:https';
11
+ import * as http from 'node:http';
12
+ import * as tls from 'node:tls';
13
+ import * as net from 'node:net';
14
+ import { URL } from 'node:url';
15
+ import { randomUUID } from 'node:crypto';
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // Constants
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ const PENTEST_DIR = join(homedir(), '.kbot', 'pentest');
20
+ const COMMON_PATHS = [
21
+ '/', '/admin', '/administrator', '/login', '/signin', '/signup',
22
+ '/register', '/api', '/api/v1', '/api/v2', '/api/docs',
23
+ '/api/swagger', '/api/graphql', '/graphql',
24
+ '/.env', '/.git', '/.git/HEAD', '/.git/config',
25
+ '/.gitignore', '/.svn', '/.hg',
26
+ '/wp-admin', '/wp-login.php', '/wp-content', '/wp-includes',
27
+ '/xmlrpc.php', '/wp-json',
28
+ '/phpmyadmin', '/pma', '/adminer',
29
+ '/robots.txt', '/sitemap.xml', '/sitemap_index.xml',
30
+ '/favicon.ico', '/crossdomain.xml',
31
+ '/.well-known/security.txt', '/.well-known/openid-configuration',
32
+ '/server-status', '/server-info',
33
+ '/.htaccess', '/.htpasswd',
34
+ '/config', '/config.json', '/config.yml', '/config.xml',
35
+ '/backup', '/backups', '/dump', '/db', '/database',
36
+ '/debug', '/trace', '/status', '/health', '/healthz',
37
+ '/metrics', '/prometheus', '/grafana',
38
+ '/console', '/shell', '/terminal',
39
+ '/test', '/testing', '/staging',
40
+ '/temp', '/tmp', '/upload', '/uploads',
41
+ '/files', '/documents', '/media', '/assets',
42
+ '/node_modules', '/vendor', '/composer.json', '/package.json',
43
+ '/Dockerfile', '/docker-compose.yml',
44
+ '/.dockerenv', '/Procfile',
45
+ '/info.php', '/phpinfo.php', '/info',
46
+ '/actuator', '/actuator/health', '/actuator/env',
47
+ '/elmah.axd', '/trace.axd',
48
+ '/cgi-bin', '/cgi-bin/test-cgi',
49
+ '/manager/html', '/jmx-console',
50
+ '/solr', '/jenkins', '/hudson',
51
+ '/_debug_toolbar/', '/__debug__/',
52
+ '/telescope', '/horizon',
53
+ '/swagger-ui.html', '/swagger.json', '/openapi.json',
54
+ '/v2/api-docs', '/v3/api-docs',
55
+ '/.DS_Store', '/Thumbs.db',
56
+ '/crossdomain.xml', '/clientaccesspolicy.xml',
57
+ '/error', '/errors', '/404', '/500',
58
+ '/ckeditor', '/tinymce', '/editor',
59
+ '/filemanager', '/file-manager',
60
+ '/webmail', '/mail', '/email',
61
+ '/cpanel', '/whm', '/plesk',
62
+ '/awstats', '/webalizer',
63
+ '/phppgadmin', '/pgadmin',
64
+ '/mongodb', '/mongo-express',
65
+ '/redis', '/memcached',
66
+ '/elasticsearch', '/_cat/indices',
67
+ '/_cluster/health',
68
+ ];
69
+ const COMMON_SUBDOMAINS = [
70
+ 'www', 'mail', 'ftp', 'smtp', 'pop', 'imap',
71
+ 'webmail', 'remote', 'vpn', 'gateway',
72
+ 'admin', 'administrator', 'panel',
73
+ 'api', 'api2', 'api3', 'rest',
74
+ 'dev', 'development', 'staging', 'stage', 'test', 'testing',
75
+ 'qa', 'uat', 'sandbox', 'demo', 'preview',
76
+ 'beta', 'alpha', 'canary', 'next',
77
+ 'app', 'application', 'portal', 'dashboard',
78
+ 'cdn', 'static', 'assets', 'media', 'img', 'images',
79
+ 'docs', 'doc', 'documentation', 'wiki', 'help', 'support',
80
+ 'blog', 'news', 'press',
81
+ 'shop', 'store', 'ecommerce', 'cart',
82
+ 'db', 'database', 'sql', 'mysql', 'postgres', 'mongo',
83
+ 'redis', 'cache', 'memcached',
84
+ 'search', 'elastic', 'elasticsearch', 'solr',
85
+ 'git', 'gitlab', 'github', 'bitbucket', 'svn',
86
+ 'ci', 'cd', 'jenkins', 'drone', 'build',
87
+ 'monitor', 'monitoring', 'grafana', 'prometheus', 'kibana',
88
+ 'log', 'logs', 'logging', 'syslog', 'elk',
89
+ 'backup', 'backups', 'bak',
90
+ 'ns1', 'ns2', 'ns3', 'dns',
91
+ 'mx', 'mx1', 'mx2',
92
+ 'proxy', 'reverse-proxy', 'lb', 'loadbalancer',
93
+ 'auth', 'sso', 'oauth', 'login', 'accounts', 'id',
94
+ 'internal', 'intranet', 'extranet', 'private',
95
+ 'secure', 'ssl', 'tls',
96
+ 'status', 'health', 'uptime',
97
+ 'metrics', 'analytics', 'stats',
98
+ 'chat', 'im', 'messaging', 'slack', 'teams',
99
+ 'crm', 'erp', 'hr',
100
+ 'jira', 'confluence', 'trello',
101
+ 'aws', 'gcp', 'azure', 'cloud',
102
+ 's3', 'storage', 'bucket',
103
+ 'queue', 'mq', 'rabbitmq', 'kafka',
104
+ 'ws', 'websocket', 'socket', 'realtime',
105
+ ];
106
+ const SECURITY_HEADERS = [
107
+ 'content-security-policy',
108
+ 'strict-transport-security',
109
+ 'x-frame-options',
110
+ 'x-content-type-options',
111
+ 'referrer-policy',
112
+ 'permissions-policy',
113
+ 'x-xss-protection',
114
+ 'cross-origin-opener-policy',
115
+ 'cross-origin-embedder-policy',
116
+ 'cross-origin-resource-policy',
117
+ ];
118
+ const SEVERITY_ORDER = {
119
+ CRITICAL: 0,
120
+ HIGH: 1,
121
+ MEDIUM: 2,
122
+ LOW: 3,
123
+ INFO: 4,
124
+ };
125
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE'];
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+ // Helpers
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+ /** Ensure the pentest directory exists */
130
+ function ensurePentestDir() {
131
+ if (!existsSync(PENTEST_DIR)) {
132
+ mkdirSync(PENTEST_DIR, { recursive: true });
133
+ }
134
+ }
135
+ /** Get session directory path */
136
+ function sessionDir(sessionId) {
137
+ return join(PENTEST_DIR, sessionId);
138
+ }
139
+ /** Create a new session directory */
140
+ function createSessionDir(sessionId) {
141
+ const dir = sessionDir(sessionId);
142
+ mkdirSync(dir, { recursive: true });
143
+ return dir;
144
+ }
145
+ /** Read session config */
146
+ function readSession(sessionId) {
147
+ const configPath = join(sessionDir(sessionId), 'config.json');
148
+ if (!existsSync(configPath))
149
+ return null;
150
+ try {
151
+ return JSON.parse(readFileSync(configPath, 'utf8'));
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ /** Write session config */
158
+ function writeSession(session) {
159
+ const configPath = join(sessionDir(session.id), 'config.json');
160
+ writeFileSync(configPath, JSON.stringify(session, null, 2));
161
+ }
162
+ /** Read recon data for a session */
163
+ function readRecon(sessionId) {
164
+ const p = join(sessionDir(sessionId), 'recon.json');
165
+ if (!existsSync(p))
166
+ return null;
167
+ try {
168
+ return JSON.parse(readFileSync(p, 'utf8'));
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ /** Write recon data */
175
+ function writeRecon(sessionId, data) {
176
+ writeFileSync(join(sessionDir(sessionId), 'recon.json'), JSON.stringify(data, null, 2));
177
+ }
178
+ /** Read vuln data for a session */
179
+ function readVulns(sessionId) {
180
+ const p = join(sessionDir(sessionId), 'vulns.json');
181
+ if (!existsSync(p))
182
+ return null;
183
+ try {
184
+ return JSON.parse(readFileSync(p, 'utf8'));
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ /** Write vuln data */
191
+ function writeVulns(sessionId, data) {
192
+ writeFileSync(join(sessionDir(sessionId), 'vulns.json'), JSON.stringify(data, null, 2));
193
+ }
194
+ /** Find the latest session ID */
195
+ function findLatestSession() {
196
+ ensurePentestDir();
197
+ const dirs = readdirSync(PENTEST_DIR).filter(d => {
198
+ const fp = join(PENTEST_DIR, d);
199
+ return statSync(fp).isDirectory() && existsSync(join(fp, 'config.json'));
200
+ });
201
+ if (dirs.length === 0)
202
+ return null;
203
+ // Sort by session start time
204
+ const sorted = dirs
205
+ .map(d => {
206
+ const session = readSession(d);
207
+ return { id: d, time: session?.started_at ?? '' };
208
+ })
209
+ .sort((a, b) => b.time.localeCompare(a.time));
210
+ return sorted[0]?.id ?? null;
211
+ }
212
+ /** Find session for a target, returning the latest one */
213
+ function findSessionForTarget(target) {
214
+ ensurePentestDir();
215
+ const dirs = readdirSync(PENTEST_DIR).filter(d => {
216
+ const fp = join(PENTEST_DIR, d);
217
+ return statSync(fp).isDirectory() && existsSync(join(fp, 'config.json'));
218
+ });
219
+ const matching = dirs
220
+ .map(d => readSession(d))
221
+ .filter((s) => s !== null && s.target === target)
222
+ .sort((a, b) => b.started_at.localeCompare(a.started_at));
223
+ return matching[0]?.id ?? null;
224
+ }
225
+ /** Create a finding object */
226
+ function createFinding(severity, category, title, description, evidence, remediation) {
227
+ return {
228
+ id: randomUUID().slice(0, 8),
229
+ severity,
230
+ category,
231
+ title,
232
+ description,
233
+ evidence,
234
+ remediation,
235
+ timestamp: new Date().toISOString(),
236
+ };
237
+ }
238
+ /** Parse a target into a URL object, handling bare IPs/domains */
239
+ function parseTarget(target) {
240
+ let urlStr = target.trim();
241
+ if (!urlStr.startsWith('http://') && !urlStr.startsWith('https://')) {
242
+ urlStr = `https://${urlStr}`;
243
+ }
244
+ return new URL(urlStr);
245
+ }
246
+ /** Promisified DNS resolve for a given record type */
247
+ function dnsResolve(hostname, rrtype) {
248
+ return new Promise((resolve) => {
249
+ dns.resolve(hostname, rrtype, (err, addresses) => {
250
+ if (err)
251
+ resolve([]);
252
+ else
253
+ resolve(Array.isArray(addresses) ? addresses.map(a => typeof a === 'string' ? a : JSON.stringify(a)) : []);
254
+ });
255
+ });
256
+ }
257
+ /** DNS reverse lookup */
258
+ function dnsReverse(ip) {
259
+ return new Promise((resolve) => {
260
+ dns.reverse(ip, (err, hostnames) => {
261
+ if (err)
262
+ resolve([]);
263
+ else
264
+ resolve(hostnames);
265
+ });
266
+ });
267
+ }
268
+ /** Make an HTTP(S) request and return response info */
269
+ function httpRequest(urlStr, options = {}) {
270
+ return new Promise((resolve, reject) => {
271
+ const method = options.method ?? 'GET';
272
+ const timeout = options.timeout ?? 10_000;
273
+ const parsed = new URL(urlStr);
274
+ const isHttps = parsed.protocol === 'https:';
275
+ const reqOptions = {
276
+ method,
277
+ hostname: parsed.hostname,
278
+ port: parsed.port || (isHttps ? 443 : 80),
279
+ path: parsed.pathname + parsed.search,
280
+ timeout,
281
+ headers: {
282
+ 'User-Agent': 'kbot-pentest/1.0 (security assessment tool)',
283
+ 'Accept': '*/*',
284
+ ...options.headers,
285
+ },
286
+ rejectUnauthorized: options.rejectUnauthorized ?? false,
287
+ };
288
+ const mod = isHttps ? https : http;
289
+ const req = mod.request(reqOptions, (res) => {
290
+ let body = '';
291
+ const maxBody = 256_000; // 256KB max
292
+ res.setEncoding('utf8');
293
+ res.on('data', (chunk) => {
294
+ if (body.length < maxBody) {
295
+ body += chunk;
296
+ }
297
+ });
298
+ res.on('end', () => {
299
+ const headers = {};
300
+ for (const [key, val] of Object.entries(res.headers)) {
301
+ headers[key.toLowerCase()] = val;
302
+ }
303
+ resolve({
304
+ statusCode: res.statusCode ?? 0,
305
+ headers,
306
+ body: body.slice(0, maxBody),
307
+ url: urlStr,
308
+ redirectUrl: res.headers.location,
309
+ });
310
+ });
311
+ });
312
+ req.on('error', (err) => reject(err));
313
+ req.on('timeout', () => {
314
+ req.destroy();
315
+ reject(new Error('Request timed out'));
316
+ });
317
+ req.end();
318
+ });
319
+ }
320
+ /** Make a request and catch errors, returning null on failure */
321
+ async function safeRequest(urlStr, options = {}) {
322
+ try {
323
+ return await httpRequest(urlStr, options);
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ }
329
+ /** Check if a TCP port is open */
330
+ function checkPort(host, port, timeout = 3000) {
331
+ return new Promise((resolve) => {
332
+ const socket = new net.Socket();
333
+ socket.setTimeout(timeout);
334
+ socket.on('connect', () => {
335
+ socket.destroy();
336
+ resolve(true);
337
+ });
338
+ socket.on('timeout', () => {
339
+ socket.destroy();
340
+ resolve(false);
341
+ });
342
+ socket.on('error', () => {
343
+ socket.destroy();
344
+ resolve(false);
345
+ });
346
+ socket.connect(port, host);
347
+ });
348
+ }
349
+ /** Get TLS certificate info */
350
+ function getTlsCertificate(hostname, port = 443, timeout = 10_000) {
351
+ return new Promise((resolve) => {
352
+ const socket = tls.connect({
353
+ host: hostname,
354
+ port,
355
+ timeout,
356
+ rejectUnauthorized: false,
357
+ servername: hostname,
358
+ }, () => {
359
+ const cert = socket.getPeerCertificate(true);
360
+ const cipher = socket.getCipher();
361
+ const protocol = socket.getProtocol() ?? 'unknown';
362
+ if (!cert || !cert.subject) {
363
+ socket.destroy();
364
+ resolve(null);
365
+ return;
366
+ }
367
+ const validFrom = new Date(cert.valid_from);
368
+ const validTo = new Date(cert.valid_to);
369
+ const now = new Date();
370
+ const daysUntilExpiry = Math.floor((validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
371
+ const expired = now > validTo || now < validFrom;
372
+ // Check if self-signed (issuer matches subject)
373
+ const selfSigned = cert.issuer &&
374
+ cert.subject &&
375
+ JSON.stringify(cert.issuer) === JSON.stringify(cert.subject);
376
+ // Parse Subject Alternative Names
377
+ const altNames = [];
378
+ if (cert.subjectaltname) {
379
+ const parts = cert.subjectaltname.split(',').map((s) => s.trim());
380
+ for (const part of parts) {
381
+ if (part.startsWith('DNS:')) {
382
+ altNames.push(part.slice(4));
383
+ }
384
+ else if (part.startsWith('IP Address:')) {
385
+ altNames.push(part.slice(11));
386
+ }
387
+ }
388
+ }
389
+ socket.destroy();
390
+ resolve({
391
+ valid: !expired && !selfSigned,
392
+ subject: cert.subject,
393
+ issuer: cert.issuer,
394
+ validFrom: cert.valid_from,
395
+ validTo: cert.valid_to,
396
+ serialNumber: cert.serialNumber ?? '',
397
+ fingerprint: cert.fingerprint ?? '',
398
+ fingerprint256: cert.fingerprint256 ?? '',
399
+ protocol,
400
+ cipher: cipher?.name ?? 'unknown',
401
+ bits: cipher?.version ? parseInt(String(cipher.version), 10) || 0 : 0,
402
+ altNames,
403
+ expired,
404
+ daysUntilExpiry,
405
+ selfSigned: !!selfSigned,
406
+ });
407
+ });
408
+ socket.on('error', () => {
409
+ socket.destroy();
410
+ resolve(null);
411
+ });
412
+ socket.on('timeout', () => {
413
+ socket.destroy();
414
+ resolve(null);
415
+ });
416
+ });
417
+ }
418
+ /** Extract technologies from HTTP response headers and body */
419
+ function fingerprintTechnologies(headers, body) {
420
+ const techs = [];
421
+ // Header-based detection
422
+ const serverHeader = String(headers['server'] ?? '');
423
+ if (serverHeader) {
424
+ techs.push(`Server: ${serverHeader}`);
425
+ if (/nginx/i.test(serverHeader))
426
+ techs.push('Nginx');
427
+ if (/apache/i.test(serverHeader))
428
+ techs.push('Apache');
429
+ if (/iis/i.test(serverHeader))
430
+ techs.push('IIS');
431
+ if (/cloudflare/i.test(serverHeader))
432
+ techs.push('Cloudflare');
433
+ if (/litespeed/i.test(serverHeader))
434
+ techs.push('LiteSpeed');
435
+ if (/openresty/i.test(serverHeader))
436
+ techs.push('OpenResty');
437
+ if (/caddy/i.test(serverHeader))
438
+ techs.push('Caddy');
439
+ if (/envoy/i.test(serverHeader))
440
+ techs.push('Envoy');
441
+ if (/gunicorn/i.test(serverHeader))
442
+ techs.push('Gunicorn');
443
+ if (/uvicorn/i.test(serverHeader))
444
+ techs.push('Uvicorn');
445
+ }
446
+ const poweredBy = String(headers['x-powered-by'] ?? '');
447
+ if (poweredBy) {
448
+ techs.push(`X-Powered-By: ${poweredBy}`);
449
+ if (/php/i.test(poweredBy))
450
+ techs.push('PHP');
451
+ if (/asp\.net/i.test(poweredBy))
452
+ techs.push('ASP.NET');
453
+ if (/express/i.test(poweredBy))
454
+ techs.push('Express.js');
455
+ if (/next\.js/i.test(poweredBy))
456
+ techs.push('Next.js');
457
+ if (/nuxt/i.test(poweredBy))
458
+ techs.push('Nuxt.js');
459
+ }
460
+ // Cookie-based detection
461
+ const setCookie = String(headers['set-cookie'] ?? '');
462
+ if (/PHPSESSID/i.test(setCookie))
463
+ techs.push('PHP (session cookie)');
464
+ if (/ASP\.NET_SessionId/i.test(setCookie))
465
+ techs.push('ASP.NET (session cookie)');
466
+ if (/JSESSIONID/i.test(setCookie))
467
+ techs.push('Java (session cookie)');
468
+ if (/laravel_session/i.test(setCookie))
469
+ techs.push('Laravel');
470
+ if (/rack\.session/i.test(setCookie))
471
+ techs.push('Ruby/Rack');
472
+ if (/connect\.sid/i.test(setCookie))
473
+ techs.push('Express.js (session cookie)');
474
+ if (/_rails/i.test(setCookie))
475
+ techs.push('Ruby on Rails');
476
+ if (/django/i.test(setCookie))
477
+ techs.push('Django');
478
+ if (/wordpress_/i.test(setCookie))
479
+ techs.push('WordPress');
480
+ // Header-based framework detection
481
+ if (headers['x-drupal-cache'])
482
+ techs.push('Drupal');
483
+ if (headers['x-generator'] && /drupal/i.test(String(headers['x-generator'])))
484
+ techs.push('Drupal');
485
+ if (headers['x-generator'] && /wordpress/i.test(String(headers['x-generator'])))
486
+ techs.push('WordPress');
487
+ if (headers['x-aspnet-version'])
488
+ techs.push(`ASP.NET ${headers['x-aspnet-version']}`);
489
+ if (headers['x-aspnetmvc-version'])
490
+ techs.push(`ASP.NET MVC ${headers['x-aspnetmvc-version']}`);
491
+ if (headers['x-runtime'])
492
+ techs.push('Ruby');
493
+ if (headers['x-request-id'] && headers['x-runtime'])
494
+ techs.push('Ruby on Rails');
495
+ if (headers['x-vercel-id'])
496
+ techs.push('Vercel');
497
+ if (headers['x-amz-request-id'] || headers['x-amz-id-2'])
498
+ techs.push('AWS');
499
+ if (headers['x-goog-generation'])
500
+ techs.push('Google Cloud');
501
+ if (headers['x-azure-ref'])
502
+ techs.push('Azure');
503
+ if (headers['cf-ray'])
504
+ techs.push('Cloudflare');
505
+ if (headers['fly-request-id'])
506
+ techs.push('Fly.io');
507
+ if (headers['x-render-origin-server'])
508
+ techs.push('Render');
509
+ if (headers['x-railway-request-id'])
510
+ techs.push('Railway');
511
+ if (headers['x-netlify-request-id'])
512
+ techs.push('Netlify');
513
+ // Body-based detection
514
+ if (/<meta[^>]+generator[^>]+wordpress/i.test(body))
515
+ techs.push('WordPress');
516
+ if (/<meta[^>]+generator[^>]+joomla/i.test(body))
517
+ techs.push('Joomla');
518
+ if (/<meta[^>]+generator[^>]+drupal/i.test(body))
519
+ techs.push('Drupal');
520
+ if (/<meta[^>]+generator[^>]+ghost/i.test(body))
521
+ techs.push('Ghost');
522
+ if (/<meta[^>]+generator[^>]+hugo/i.test(body))
523
+ techs.push('Hugo');
524
+ if (/<meta[^>]+generator[^>]+jekyll/i.test(body))
525
+ techs.push('Jekyll');
526
+ if (/<meta[^>]+generator[^>]+gatsby/i.test(body))
527
+ techs.push('Gatsby');
528
+ if (/<meta[^>]+generator[^>]+hexo/i.test(body))
529
+ techs.push('Hexo');
530
+ if (/<meta[^>]+generator[^>]+eleventy/i.test(body))
531
+ techs.push('Eleventy');
532
+ if (/wp-content|wp-includes/i.test(body))
533
+ techs.push('WordPress (paths)');
534
+ if (/__next/i.test(body) || /_next\/static/i.test(body))
535
+ techs.push('Next.js');
536
+ if (/__nuxt/i.test(body) || /_nuxt\//i.test(body))
537
+ techs.push('Nuxt.js');
538
+ if (/react/i.test(body) && /__REACT/i.test(body))
539
+ techs.push('React');
540
+ if (/ng-version/i.test(body) || /angular/i.test(body))
541
+ techs.push('Angular');
542
+ if (/vue/i.test(body) && /data-v-/i.test(body))
543
+ techs.push('Vue.js');
544
+ if (/svelte/i.test(body) || /svelte-/i.test(body))
545
+ techs.push('Svelte');
546
+ if (/ember/i.test(body) && /ember-cli/i.test(body))
547
+ techs.push('Ember.js');
548
+ if (/jquery/i.test(body))
549
+ techs.push('jQuery');
550
+ if (/bootstrap/i.test(body) && /bootstrap\.min/i.test(body))
551
+ techs.push('Bootstrap');
552
+ if (/tailwindcss/i.test(body) || /tailwind/i.test(body))
553
+ techs.push('Tailwind CSS');
554
+ if (/materialize/i.test(body))
555
+ techs.push('Materialize CSS');
556
+ if (/google-analytics|gtag|GA_TRACKING/i.test(body))
557
+ techs.push('Google Analytics');
558
+ if (/googletagmanager/i.test(body))
559
+ techs.push('Google Tag Manager');
560
+ if (/hotjar/i.test(body))
561
+ techs.push('Hotjar');
562
+ if (/segment\.com|analytics\.js/i.test(body))
563
+ techs.push('Segment');
564
+ if (/stripe/i.test(body) && /js\.stripe\.com/i.test(body))
565
+ techs.push('Stripe');
566
+ if (/recaptcha/i.test(body))
567
+ techs.push('Google reCAPTCHA');
568
+ if (/cloudflare/i.test(body) && /cdn-cgi/i.test(body))
569
+ techs.push('Cloudflare CDN');
570
+ if (/unpkg\.com|cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net/i.test(body))
571
+ techs.push('CDN-served libraries');
572
+ // Deduplicate
573
+ return [...new Set(techs)];
574
+ }
575
+ /** Extract info resembling WHOIS from DNS and other sources (no external whois binary) */
576
+ async function gatherWhoisInfo(hostname) {
577
+ const info = {};
578
+ info['hostname'] = hostname;
579
+ // Resolve IPs
580
+ const aRecords = await dnsResolve(hostname, 'A');
581
+ if (aRecords.length > 0) {
582
+ info['ip_addresses'] = aRecords.join(', ');
583
+ // Reverse DNS on first IP
584
+ const reverseNames = await dnsReverse(aRecords[0]);
585
+ if (reverseNames.length > 0) {
586
+ info['reverse_dns'] = reverseNames.join(', ');
587
+ }
588
+ }
589
+ const aaaaRecords = await dnsResolve(hostname, 'AAAA');
590
+ if (aaaaRecords.length > 0) {
591
+ info['ipv6_addresses'] = aaaaRecords.join(', ');
592
+ }
593
+ // Domain parts
594
+ const parts = hostname.split('.');
595
+ if (parts.length >= 2) {
596
+ info['tld'] = parts[parts.length - 1];
597
+ info['registered_domain'] = parts.slice(-2).join('.');
598
+ if (parts.length > 2) {
599
+ info['subdomain_prefix'] = parts.slice(0, -2).join('.');
600
+ }
601
+ }
602
+ return info;
603
+ }
604
+ /** Parse cookie attributes from a Set-Cookie header value */
605
+ function parseCookieAttributes(setCookieValue) {
606
+ const parts = setCookieValue.split(';').map(p => p.trim());
607
+ const nameValue = parts[0] ?? '';
608
+ const name = nameValue.split('=')[0] ?? '';
609
+ let httpOnly = false;
610
+ let secure = false;
611
+ let sameSite = '';
612
+ let path = '';
613
+ let domain = '';
614
+ let expires = '';
615
+ let maxAge = '';
616
+ for (const part of parts.slice(1)) {
617
+ const lower = part.toLowerCase();
618
+ if (lower === 'httponly')
619
+ httpOnly = true;
620
+ else if (lower === 'secure')
621
+ secure = true;
622
+ else if (lower.startsWith('samesite='))
623
+ sameSite = part.split('=')[1] ?? '';
624
+ else if (lower.startsWith('path='))
625
+ path = part.split('=')[1] ?? '';
626
+ else if (lower.startsWith('domain='))
627
+ domain = part.split('=')[1] ?? '';
628
+ else if (lower.startsWith('expires='))
629
+ expires = part.slice(8);
630
+ else if (lower.startsWith('max-age='))
631
+ maxAge = part.split('=')[1] ?? '';
632
+ }
633
+ return { name, httpOnly, secure, sameSite, path, domain, expires, maxAge };
634
+ }
635
+ /** Aggregate all findings from recon and vuln phases */
636
+ function getAllFindings(sessionId) {
637
+ const findings = [];
638
+ const vulns = readVulns(sessionId);
639
+ if (vulns)
640
+ findings.push(...vulns.findings);
641
+ return findings.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
642
+ }
643
+ /** Count findings by severity */
644
+ function countBySeverity(findings) {
645
+ const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, INFO: 0 };
646
+ for (const f of findings) {
647
+ counts[f.severity]++;
648
+ }
649
+ return counts;
650
+ }
651
+ /** Format a severity label with color-coding hints (for terminal markdown) */
652
+ function severityBadge(sev) {
653
+ switch (sev) {
654
+ case 'CRITICAL': return '🔴 CRITICAL';
655
+ case 'HIGH': return '🟠 HIGH';
656
+ case 'MEDIUM': return '🟡 MEDIUM';
657
+ case 'LOW': return '🔵 LOW';
658
+ case 'INFO': return 'ℹ️ INFO';
659
+ }
660
+ }
661
+ // ─────────────────────────────────────────────────────────────────────────────
662
+ // Recon Engine
663
+ // ─────────────────────────────────────────────────────────────────────────────
664
+ async function performRecon(target, depth, passiveOnly) {
665
+ const parsed = parseTarget(target);
666
+ const hostname = parsed.hostname;
667
+ const baseUrl = `${parsed.protocol}//${parsed.host}`;
668
+ const findings = [];
669
+ // ── DNS Resolution ────────────────────────────────────────────────────
670
+ const dnsResults = {};
671
+ const recordTypes = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA'];
672
+ for (const rtype of recordTypes) {
673
+ const records = await dnsResolve(hostname, rtype);
674
+ if (records.length > 0) {
675
+ dnsResults[rtype] = records;
676
+ }
677
+ }
678
+ // Check for DNS zone transfer potential (AXFR is not directly testable without a DNS library,
679
+ // but we can check for multiple NS records which indicate distributed DNS)
680
+ const nsRecords = dnsResults['NS'] ?? [];
681
+ if (nsRecords.length === 1) {
682
+ findings.push(createFinding('LOW', 'DNS', 'Single nameserver detected', `Only one NS record found: ${nsRecords[0]}. This is a single point of failure.`, `NS records: ${nsRecords.join(', ')}`, 'Configure at least two nameservers for redundancy.'));
683
+ }
684
+ // Check for SPF, DMARC, DKIM in TXT records
685
+ const txtRecords = dnsResults['TXT'] ?? [];
686
+ const hasSPF = txtRecords.some(r => r.includes('v=spf1'));
687
+ const hasDMARC = await dnsResolve(`_dmarc.${hostname}`, 'TXT');
688
+ const hasDKIM = await dnsResolve(`default._domainkey.${hostname}`, 'TXT');
689
+ if (!hasSPF) {
690
+ findings.push(createFinding('LOW', 'DNS', 'Missing SPF record', 'No SPF (Sender Policy Framework) record found. This allows email spoofing.', `TXT records: ${txtRecords.join('; ') || 'none'}`, 'Add an SPF record: v=spf1 include:_spf.google.com ~all (adjust for your email provider).'));
691
+ }
692
+ if (hasDMARC.length === 0) {
693
+ findings.push(createFinding('LOW', 'DNS', 'Missing DMARC record', 'No DMARC record found at _dmarc.' + hostname + '. Email from this domain can be easily spoofed.', 'No _dmarc TXT record present.', 'Add a DMARC record: _dmarc.' + hostname + ' IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc@' + hostname + '".'));
694
+ }
695
+ dnsResults['SPF'] = hasSPF;
696
+ dnsResults['DMARC'] = hasDMARC.length > 0 ? hasDMARC : false;
697
+ dnsResults['DKIM'] = hasDKIM.length > 0 ? hasDKIM : false;
698
+ // ── WHOIS-style info ──────────────────────────────────────────────────
699
+ const whoisInfo = await gatherWhoisInfo(hostname);
700
+ // ── HTTP Response Analysis ────────────────────────────────────────────
701
+ const mainResponse = await safeRequest(baseUrl, { timeout: 15_000 });
702
+ const headersMap = {};
703
+ let bodyContent = '';
704
+ let technologies = [];
705
+ if (mainResponse) {
706
+ for (const [key, val] of Object.entries(mainResponse.headers)) {
707
+ headersMap[key] = Array.isArray(val) ? val.join(', ') : String(val ?? '');
708
+ }
709
+ bodyContent = mainResponse.body;
710
+ technologies = fingerprintTechnologies(mainResponse.headers, bodyContent);
711
+ // Check for server version disclosure
712
+ const serverVal = headersMap['server'] ?? '';
713
+ if (/\d+\.\d+/.test(serverVal)) {
714
+ findings.push(createFinding('LOW', 'Information Disclosure', 'Server version disclosed in headers', `The Server header reveals version information: ${serverVal}`, `Server: ${serverVal}`, 'Configure the web server to hide version information. For Nginx: server_tokens off; For Apache: ServerTokens Prod.'));
715
+ }
716
+ const poweredBy = headersMap['x-powered-by'] ?? '';
717
+ if (poweredBy) {
718
+ findings.push(createFinding('LOW', 'Information Disclosure', 'X-Powered-By header reveals technology stack', `The X-Powered-By header discloses: ${poweredBy}`, `X-Powered-By: ${poweredBy}`, 'Remove the X-Powered-By header. For Express.js: app.disable("x-powered-by"). For PHP: expose_php = Off in php.ini.'));
719
+ }
720
+ // Check for redirect to HTTPS
721
+ if (parsed.protocol === 'https:') {
722
+ const httpResponse = await safeRequest(`http://${parsed.host}`, { timeout: 10_000 });
723
+ if (httpResponse) {
724
+ if (httpResponse.statusCode >= 300 && httpResponse.statusCode < 400) {
725
+ const redirectLoc = String(httpResponse.headers['location'] ?? '');
726
+ if (!redirectLoc.startsWith('https://')) {
727
+ findings.push(createFinding('MEDIUM', 'Transport Security', 'HTTP does not redirect to HTTPS', 'The HTTP version of the site redirects, but not to an HTTPS URL.', `HTTP redirect location: ${redirectLoc}`, 'Configure HTTP to redirect to the HTTPS version with a 301 redirect.'));
728
+ }
729
+ }
730
+ else if (httpResponse.statusCode === 200) {
731
+ findings.push(createFinding('MEDIUM', 'Transport Security', 'HTTP version serves content without redirecting to HTTPS', 'The site is accessible over plain HTTP without redirecting to HTTPS. Credentials and data may be transmitted in clear text.', `HTTP ${httpResponse.statusCode} response received on http://${parsed.host}`, 'Add a redirect from HTTP to HTTPS. For Nginx: return 301 https://$host$request_uri;'));
732
+ }
733
+ }
734
+ }
735
+ }
736
+ // ── SSL/TLS Certificate Analysis ──────────────────────────────────────
737
+ let sslInfo = {};
738
+ if (parsed.protocol === 'https:') {
739
+ const cert = await getTlsCertificate(hostname, parsed.port ? parseInt(parsed.port) : 443);
740
+ if (cert) {
741
+ sslInfo = {
742
+ valid: cert.valid,
743
+ subject: cert.subject,
744
+ issuer: cert.issuer,
745
+ validFrom: cert.validFrom,
746
+ validTo: cert.validTo,
747
+ serialNumber: cert.serialNumber,
748
+ fingerprint256: cert.fingerprint256,
749
+ protocol: cert.protocol,
750
+ cipher: cert.cipher,
751
+ altNames: cert.altNames,
752
+ expired: cert.expired,
753
+ daysUntilExpiry: cert.daysUntilExpiry,
754
+ selfSigned: cert.selfSigned,
755
+ };
756
+ if (cert.expired) {
757
+ findings.push(createFinding('CRITICAL', 'SSL/TLS', 'SSL certificate is expired', `The SSL certificate expired on ${cert.validTo}. Browsers will show security warnings.`, `Valid to: ${cert.validTo}`, 'Renew the SSL certificate immediately. Consider using Let\'s Encrypt for automatic renewal.'));
758
+ }
759
+ else if (cert.daysUntilExpiry < 30) {
760
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'SSL certificate expires within 30 days', `The SSL certificate expires in ${cert.daysUntilExpiry} days (${cert.validTo}).`, `Expires: ${cert.validTo} (${cert.daysUntilExpiry} days remaining)`, 'Renew the SSL certificate before it expires. Set up automated renewal with certbot or your certificate provider.'));
761
+ }
762
+ else if (cert.daysUntilExpiry < 90) {
763
+ findings.push(createFinding('MEDIUM', 'SSL/TLS', 'SSL certificate expires within 90 days', `The SSL certificate expires in ${cert.daysUntilExpiry} days (${cert.validTo}).`, `Expires: ${cert.validTo} (${cert.daysUntilExpiry} days remaining)`, 'Plan to renew the SSL certificate. Set up automated renewal if not already configured.'));
764
+ }
765
+ if (cert.selfSigned) {
766
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'Self-signed SSL certificate', 'The SSL certificate is self-signed. Browsers will show security warnings and users cannot trust the certificate.', `Issuer: ${JSON.stringify(cert.issuer)}, Subject: ${JSON.stringify(cert.subject)}`, 'Obtain a certificate from a trusted Certificate Authority. Let\'s Encrypt provides free certificates.'));
767
+ }
768
+ // Check for weak protocol versions
769
+ const weakProtocols = ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1.1'];
770
+ if (weakProtocols.includes(cert.protocol)) {
771
+ findings.push(createFinding('HIGH', 'SSL/TLS', `Weak TLS protocol version: ${cert.protocol}`, `The server negotiated ${cert.protocol}, which has known vulnerabilities.`, `Protocol: ${cert.protocol}`, 'Configure the server to only accept TLS 1.2 and TLS 1.3. Disable SSLv2, SSLv3, TLSv1, and TLSv1.1.'));
772
+ }
773
+ // Check for wildcard certificate over-usage
774
+ if (cert.altNames.some(n => n.startsWith('*.'))) {
775
+ findings.push(createFinding('INFO', 'SSL/TLS', 'Wildcard SSL certificate in use', 'A wildcard certificate is in use, which covers all subdomains. If the private key is compromised, all subdomains are affected.', `Alt names: ${cert.altNames.join(', ')}`, 'Consider using specific certificates for sensitive subdomains. Monitor for key compromise.'));
776
+ }
777
+ }
778
+ else {
779
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'Unable to establish TLS connection', 'Could not connect via TLS to retrieve the certificate. The server may not support HTTPS, or the connection timed out.', `Target: ${hostname}:${parsed.port || 443}`, 'Ensure the server is configured to accept TLS connections. Check firewall rules and server configuration.'));
780
+ }
781
+ }
782
+ // ── robots.txt and sitemap.xml ────────────────────────────────────────
783
+ let robotsTxt = null;
784
+ let sitemapXml = null;
785
+ const robotsResp = await safeRequest(`${baseUrl}/robots.txt`, { timeout: 8_000 });
786
+ if (robotsResp && robotsResp.statusCode === 200 && robotsResp.body.length > 0) {
787
+ robotsTxt = robotsResp.body.slice(0, 10_000);
788
+ // Check for sensitive paths in robots.txt Disallow rules
789
+ const disallowLines = robotsTxt.split('\n')
790
+ .filter(line => /^Disallow:/i.test(line.trim()))
791
+ .map(line => line.replace(/^Disallow:\s*/i, '').trim());
792
+ const sensitiveDisallows = disallowLines.filter(path => /admin|login|dashboard|config|backup|secret|private|internal|api|\.env|\.git/i.test(path));
793
+ if (sensitiveDisallows.length > 0) {
794
+ findings.push(createFinding('INFO', 'Information Disclosure', 'robots.txt reveals sensitive paths', `The robots.txt file discloses potentially sensitive paths via Disallow rules.`, `Sensitive disallows:\n${sensitiveDisallows.map(p => ` - ${p}`).join('\n')}`, 'While robots.txt is meant for search engines, attackers use it for reconnaissance. Ensure disallowed paths are also properly secured with authentication.'));
795
+ }
796
+ }
797
+ const sitemapResp = await safeRequest(`${baseUrl}/sitemap.xml`, { timeout: 8_000 });
798
+ if (sitemapResp && sitemapResp.statusCode === 200 && sitemapResp.body.includes('<urlset')) {
799
+ sitemapXml = sitemapResp.body.slice(0, 20_000);
800
+ }
801
+ // ── Common Path Enumeration ───────────────────────────────────────────
802
+ const pathResults = [];
803
+ if (!passiveOnly) {
804
+ // Determine how many paths to check based on depth
805
+ let pathsToCheck;
806
+ switch (depth) {
807
+ case 'quick':
808
+ pathsToCheck = COMMON_PATHS.slice(0, 25);
809
+ break;
810
+ case 'standard':
811
+ pathsToCheck = COMMON_PATHS.slice(0, 70);
812
+ break;
813
+ case 'deep':
814
+ pathsToCheck = COMMON_PATHS;
815
+ break;
816
+ }
817
+ // Check paths in batches of 10 for speed
818
+ const batchSize = 10;
819
+ for (let i = 0; i < pathsToCheck.length; i += batchSize) {
820
+ const batch = pathsToCheck.slice(i, i + batchSize);
821
+ const results = await Promise.allSettled(batch.map(async (path) => {
822
+ const resp = await safeRequest(`${baseUrl}${path}`, { timeout: 5_000 });
823
+ if (resp && resp.statusCode !== 404 && resp.statusCode !== 0) {
824
+ return { path, status: resp.statusCode, size: resp.body.length };
825
+ }
826
+ return null;
827
+ }));
828
+ for (const r of results) {
829
+ if (r.status === 'fulfilled' && r.value) {
830
+ pathResults.push(r.value);
831
+ }
832
+ }
833
+ }
834
+ // Flag sensitive discovered paths
835
+ const sensitivePathPatterns = [
836
+ { pattern: /\.env/i, severity: 'CRITICAL', title: '.env file accessible' },
837
+ { pattern: /\.git\/config/i, severity: 'CRITICAL', title: '.git/config exposed' },
838
+ { pattern: /\.git\/HEAD/i, severity: 'CRITICAL', title: '.git/HEAD exposed (git repository)' },
839
+ { pattern: /\.git$/i, severity: 'HIGH', title: '.git directory accessible' },
840
+ { pattern: /\.htpasswd/i, severity: 'CRITICAL', title: '.htpasswd file exposed' },
841
+ { pattern: /\.htaccess/i, severity: 'HIGH', title: '.htaccess file exposed' },
842
+ { pattern: /phpmyadmin|adminer|pma/i, severity: 'HIGH', title: 'Database admin panel exposed' },
843
+ { pattern: /server-status|server-info/i, severity: 'MEDIUM', title: 'Apache status/info page exposed' },
844
+ { pattern: /phpinfo|info\.php/i, severity: 'HIGH', title: 'PHP info page exposed' },
845
+ { pattern: /actuator/i, severity: 'HIGH', title: 'Spring Boot Actuator endpoint exposed' },
846
+ { pattern: /swagger|openapi|api-docs/i, severity: 'MEDIUM', title: 'API documentation exposed' },
847
+ { pattern: /wp-admin|wp-login/i, severity: 'INFO', title: 'WordPress admin login found' },
848
+ { pattern: /console|shell|terminal/i, severity: 'CRITICAL', title: 'Web shell/console potentially exposed' },
849
+ { pattern: /backup|dump|\.sql|\.bak/i, severity: 'HIGH', title: 'Backup files potentially accessible' },
850
+ { pattern: /config\.json|config\.yml|config\.xml/i, severity: 'HIGH', title: 'Configuration file exposed' },
851
+ { pattern: /node_modules/i, severity: 'MEDIUM', title: 'node_modules directory exposed' },
852
+ { pattern: /vendor|composer\.json/i, severity: 'MEDIUM', title: 'Vendor/dependency directory exposed' },
853
+ { pattern: /package\.json/i, severity: 'LOW', title: 'package.json exposed' },
854
+ { pattern: /Dockerfile|docker-compose/i, severity: 'MEDIUM', title: 'Docker configuration file exposed' },
855
+ { pattern: /\.DS_Store/i, severity: 'LOW', title: '.DS_Store file exposed' },
856
+ { pattern: /Thumbs\.db/i, severity: 'LOW', title: 'Thumbs.db file exposed' },
857
+ { pattern: /elmah|trace\.axd/i, severity: 'HIGH', title: 'Error logging/tracing endpoint exposed' },
858
+ { pattern: /graphql/i, severity: 'INFO', title: 'GraphQL endpoint discovered' },
859
+ { pattern: /jenkins|hudson|jmx-console/i, severity: 'HIGH', title: 'CI/CD or management console exposed' },
860
+ { pattern: /telescope|horizon/i, severity: 'MEDIUM', title: 'Laravel debug panel exposed' },
861
+ { pattern: /debug|__debug__/i, severity: 'HIGH', title: 'Debug interface exposed' },
862
+ { pattern: /elasticsearch|_cat|_cluster/i, severity: 'CRITICAL', title: 'Elasticsearch endpoint exposed' },
863
+ { pattern: /xmlrpc\.php/i, severity: 'MEDIUM', title: 'WordPress XML-RPC endpoint accessible' },
864
+ { pattern: /\.svn/i, severity: 'HIGH', title: '.svn directory exposed' },
865
+ { pattern: /\.hg/i, severity: 'HIGH', title: '.hg directory exposed' },
866
+ { pattern: /crossdomain\.xml|clientaccesspolicy\.xml/i, severity: 'INFO', title: 'Cross-domain policy file found' },
867
+ { pattern: /ckeditor|tinymce|editor/i, severity: 'INFO', title: 'Rich text editor path discovered' },
868
+ { pattern: /metrics|prometheus|grafana/i, severity: 'MEDIUM', title: 'Monitoring endpoint exposed' },
869
+ ];
870
+ for (const result of pathResults) {
871
+ if (result.status >= 200 && result.status < 400) {
872
+ for (const sp of sensitivePathPatterns) {
873
+ if (sp.pattern.test(result.path)) {
874
+ findings.push(createFinding(sp.severity, 'Path Discovery', sp.title, `The path ${result.path} returned HTTP ${result.status} (${result.size ?? 0} bytes).`, `GET ${baseUrl}${result.path} → ${result.status}`, 'Restrict access to this path. Use authentication, IP allowlisting, or remove the resource if unnecessary.'));
875
+ break;
876
+ }
877
+ }
878
+ }
879
+ // Directory listing detection
880
+ if (result.status === 200 && result.size && result.size > 0) {
881
+ const dirResp = await safeRequest(`${baseUrl}${result.path}`, { timeout: 5_000 });
882
+ if (dirResp && /Index of|<title>.*listing/i.test(dirResp.body)) {
883
+ findings.push(createFinding('MEDIUM', 'Path Discovery', `Directory listing enabled at ${result.path}`, 'Directory listing is enabled, allowing attackers to browse files.', `GET ${baseUrl}${result.path} → directory index page`, 'Disable directory listing. For Nginx: autoindex off; For Apache: Options -Indexes.'));
884
+ }
885
+ }
886
+ }
887
+ }
888
+ // ── Subdomain Enumeration ─────────────────────────────────────────────
889
+ const subdomainResults = [];
890
+ // Only enumerate subdomains for domain-like hostnames (not IPs)
891
+ const isIP = net.isIP(hostname);
892
+ if (!isIP) {
893
+ // Determine how many subdomains to check based on depth
894
+ let subdomainsToCheck;
895
+ switch (depth) {
896
+ case 'quick':
897
+ subdomainsToCheck = COMMON_SUBDOMAINS.slice(0, 15);
898
+ break;
899
+ case 'standard':
900
+ subdomainsToCheck = COMMON_SUBDOMAINS.slice(0, 50);
901
+ break;
902
+ case 'deep':
903
+ subdomainsToCheck = COMMON_SUBDOMAINS;
904
+ break;
905
+ }
906
+ // Get the base domain (registered domain, not the full hostname)
907
+ const baseDomain = hostname.split('.').slice(-2).join('.');
908
+ // Check subdomains in batches
909
+ const subBatchSize = 15;
910
+ for (let i = 0; i < subdomainsToCheck.length; i += subBatchSize) {
911
+ const batch = subdomainsToCheck.slice(i, i + subBatchSize);
912
+ const results = await Promise.allSettled(batch.map(async (sub) => {
913
+ const fqdn = `${sub}.${baseDomain}`;
914
+ // Skip if this is the same as the hostname
915
+ if (fqdn === hostname)
916
+ return null;
917
+ const ips = await dnsResolve(fqdn, 'A');
918
+ if (ips.length > 0) {
919
+ return { subdomain: fqdn, resolved: true, ip: ips[0] };
920
+ }
921
+ return { subdomain: fqdn, resolved: false };
922
+ }));
923
+ for (const r of results) {
924
+ if (r.status === 'fulfilled' && r.value) {
925
+ subdomainResults.push(r.value);
926
+ }
927
+ }
928
+ }
929
+ const resolvedSubs = subdomainResults.filter(s => s.resolved);
930
+ if (resolvedSubs.length > 0) {
931
+ findings.push(createFinding('INFO', 'Subdomain Enumeration', `${resolvedSubs.length} subdomains resolved`, `Found ${resolvedSubs.length} active subdomains for ${baseDomain}.`, resolvedSubs.map(s => ` ${s.subdomain} → ${s.ip}`).join('\n'), 'Review all subdomains for unnecessary exposure. Decommission unused subdomains. Ensure each subdomain has appropriate security controls.'));
932
+ }
933
+ }
934
+ const reconData = {
935
+ dns: dnsResults,
936
+ whois: whoisInfo,
937
+ headers: headersMap,
938
+ technologies,
939
+ ssl: sslInfo,
940
+ paths: pathResults,
941
+ subdomains: subdomainResults,
942
+ robots_txt: robotsTxt,
943
+ sitemap_xml: sitemapXml,
944
+ timestamp: new Date().toISOString(),
945
+ };
946
+ return { recon: reconData, findings };
947
+ }
948
+ // ─────────────────────────────────────────────────────────────────────────────
949
+ // Vulnerability Scanner Engine
950
+ // ─────────────────────────────────────────────────────────────────────────────
951
+ async function checkSecurityHeaders(target) {
952
+ const findings = [];
953
+ const parsed = parseTarget(target);
954
+ const baseUrl = `${parsed.protocol}//${parsed.host}`;
955
+ const resp = await safeRequest(baseUrl, { timeout: 10_000 });
956
+ if (!resp) {
957
+ findings.push(createFinding('INFO', 'Security Headers', 'Could not connect to target for header analysis', `Unable to connect to ${baseUrl} to analyze security headers.`, 'Connection failed or timed out.', 'Ensure the target is accessible and try again.'));
958
+ return findings;
959
+ }
960
+ const headers = resp.headers;
961
+ // Content-Security-Policy
962
+ const csp = headers['content-security-policy'];
963
+ if (!csp) {
964
+ findings.push(createFinding('MEDIUM', 'Security Headers', 'Missing Content-Security-Policy header', 'No Content-Security-Policy (CSP) header is set. This makes the site more vulnerable to XSS attacks.', 'Content-Security-Policy header not present in response.', 'Implement a Content-Security-Policy header. Start with: Content-Security-Policy: default-src \'self\'; script-src \'self\'; style-src \'self\' \'unsafe-inline\'.'));
965
+ }
966
+ else {
967
+ const cspStr = String(csp);
968
+ // Check for overly permissive CSP
969
+ if (cspStr.includes('unsafe-inline') && cspStr.includes('unsafe-eval')) {
970
+ findings.push(createFinding('MEDIUM', 'Security Headers', 'CSP allows unsafe-inline and unsafe-eval', 'The Content-Security-Policy allows both unsafe-inline and unsafe-eval, significantly reducing XSS protection.', `CSP: ${cspStr.slice(0, 200)}`, 'Remove unsafe-inline and unsafe-eval from the CSP. Use nonces or hashes for inline scripts.'));
971
+ }
972
+ else if (cspStr.includes('unsafe-inline')) {
973
+ findings.push(createFinding('LOW', 'Security Headers', 'CSP allows unsafe-inline', 'The Content-Security-Policy allows unsafe-inline, reducing XSS protection for inline scripts/styles.', `CSP: ${cspStr.slice(0, 200)}`, 'Consider using nonces or hashes instead of unsafe-inline for better XSS protection.'));
974
+ }
975
+ if (cspStr.includes('*') && /default-src[^;]*\*|script-src[^;]*\*/.test(cspStr)) {
976
+ findings.push(createFinding('HIGH', 'Security Headers', 'CSP uses wildcard source', 'The Content-Security-Policy uses a wildcard (*) in default-src or script-src, making it ineffective.', `CSP: ${cspStr.slice(0, 200)}`, 'Replace wildcard sources with specific domains. A wildcard CSP provides almost no protection.'));
977
+ }
978
+ }
979
+ // Strict-Transport-Security
980
+ const hsts = headers['strict-transport-security'];
981
+ if (!hsts && parsed.protocol === 'https:') {
982
+ findings.push(createFinding('MEDIUM', 'Security Headers', 'Missing Strict-Transport-Security (HSTS) header', 'No HSTS header is set. The site is vulnerable to SSL stripping attacks on first visit.', 'Strict-Transport-Security header not present in response.', 'Add: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload'));
983
+ }
984
+ else if (hsts) {
985
+ const hstsStr = String(hsts);
986
+ const maxAgeMatch = hstsStr.match(/max-age=(\d+)/);
987
+ if (maxAgeMatch) {
988
+ const maxAge = parseInt(maxAgeMatch[1], 10);
989
+ if (maxAge < 15_768_000) { // Less than 6 months
990
+ findings.push(createFinding('LOW', 'Security Headers', 'HSTS max-age is less than 6 months', `HSTS max-age is ${maxAge} seconds (${Math.floor(maxAge / 86400)} days). Recommended minimum is 6 months.`, `Strict-Transport-Security: ${hstsStr}`, 'Increase max-age to at least 31536000 (1 year). Add includeSubDomains and preload directives.'));
991
+ }
992
+ }
993
+ if (!hstsStr.includes('includeSubDomains')) {
994
+ findings.push(createFinding('LOW', 'Security Headers', 'HSTS does not include subdomains', 'HSTS is set but does not include the includeSubDomains directive. Subdomains are not protected.', `Strict-Transport-Security: ${hstsStr}`, 'Add includeSubDomains directive: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload'));
995
+ }
996
+ }
997
+ // X-Frame-Options
998
+ const xfo = headers['x-frame-options'];
999
+ if (!xfo) {
1000
+ // Check if CSP has frame-ancestors
1001
+ const cspVal = String(csp ?? '');
1002
+ if (!cspVal.includes('frame-ancestors')) {
1003
+ findings.push(createFinding('MEDIUM', 'Security Headers', 'Missing X-Frame-Options / CSP frame-ancestors', 'Neither X-Frame-Options nor CSP frame-ancestors is set. The site may be vulnerable to clickjacking.', 'No X-Frame-Options header and no frame-ancestors in CSP.', 'Add: X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors \'none\''));
1004
+ }
1005
+ }
1006
+ // X-Content-Type-Options
1007
+ const xcto = headers['x-content-type-options'];
1008
+ if (!xcto) {
1009
+ findings.push(createFinding('LOW', 'Security Headers', 'Missing X-Content-Type-Options header', 'No X-Content-Type-Options header. Browsers may MIME-sniff responses, leading to XSS via content type confusion.', 'X-Content-Type-Options header not present.', 'Add: X-Content-Type-Options: nosniff'));
1010
+ }
1011
+ // Referrer-Policy
1012
+ const refPol = headers['referrer-policy'];
1013
+ if (!refPol) {
1014
+ findings.push(createFinding('LOW', 'Security Headers', 'Missing Referrer-Policy header', 'No Referrer-Policy header. The browser may send full URLs in the Referer header, leaking sensitive paths and parameters.', 'Referrer-Policy header not present.', 'Add: Referrer-Policy: strict-origin-when-cross-origin (or no-referrer for maximum privacy).'));
1015
+ }
1016
+ // Permissions-Policy
1017
+ const permPol = headers['permissions-policy'];
1018
+ if (!permPol) {
1019
+ findings.push(createFinding('LOW', 'Security Headers', 'Missing Permissions-Policy header', 'No Permissions-Policy header. Browser features like camera, microphone, and geolocation are not explicitly restricted.', 'Permissions-Policy header not present.', 'Add: Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()'));
1020
+ }
1021
+ // Cross-Origin headers
1022
+ const coop = headers['cross-origin-opener-policy'];
1023
+ const coep = headers['cross-origin-embedder-policy'];
1024
+ const corp = headers['cross-origin-resource-policy'];
1025
+ if (!coop && !coep && !corp) {
1026
+ findings.push(createFinding('INFO', 'Security Headers', 'No cross-origin isolation headers', 'None of COOP, COEP, or CORP headers are set. The site is not cross-origin isolated.', 'No Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, or Cross-Origin-Resource-Policy headers found.', 'Consider adding cross-origin isolation headers for enhanced security. Start with: Cross-Origin-Opener-Policy: same-origin.'));
1027
+ }
1028
+ // X-XSS-Protection (legacy but still useful)
1029
+ const xss = headers['x-xss-protection'];
1030
+ if (xss && String(xss).includes('0')) {
1031
+ findings.push(createFinding('LOW', 'Security Headers', 'X-XSS-Protection explicitly disabled', 'The X-XSS-Protection header is set to 0, disabling the browser\'s built-in XSS filter. While this header is largely obsolete (superseded by CSP), explicitly disabling it with no CSP in place reduces protection.', `X-XSS-Protection: ${String(xss)}`, 'If no CSP is in place, set X-XSS-Protection: 1; mode=block. Prefer implementing a proper CSP instead.'));
1032
+ }
1033
+ return findings;
1034
+ }
1035
+ async function checkSSL(target) {
1036
+ const findings = [];
1037
+ const parsed = parseTarget(target);
1038
+ const hostname = parsed.hostname;
1039
+ const port = parsed.port ? parseInt(parsed.port) : 443;
1040
+ if (parsed.protocol !== 'https:') {
1041
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'Target is not using HTTPS', 'The target URL uses plain HTTP. All data is transmitted unencrypted.', `Protocol: ${parsed.protocol}`, 'Configure HTTPS with a valid TLS certificate. Use Let\'s Encrypt for free certificates.'));
1042
+ return findings;
1043
+ }
1044
+ const cert = await getTlsCertificate(hostname, port);
1045
+ if (!cert) {
1046
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'Cannot establish TLS connection', `Failed to connect via TLS to ${hostname}:${port}.`, `TLS connection to ${hostname}:${port} failed`, 'Check that the server supports TLS and the port is correct.'));
1047
+ return findings;
1048
+ }
1049
+ // Certificate expiry (already checked in recon, but standalone vuln scan needs it too)
1050
+ if (cert.expired) {
1051
+ findings.push(createFinding('CRITICAL', 'SSL/TLS', 'SSL certificate expired', `Certificate expired on ${cert.validTo}.`, `Valid to: ${cert.validTo}`, 'Renew the certificate immediately.'));
1052
+ }
1053
+ else if (cert.daysUntilExpiry < 14) {
1054
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'SSL certificate expires within 14 days', `Certificate expires in ${cert.daysUntilExpiry} days.`, `Expires: ${cert.validTo}`, 'Renew the certificate immediately.'));
1055
+ }
1056
+ if (cert.selfSigned) {
1057
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'Self-signed certificate', 'The certificate is self-signed and will not be trusted by browsers.', `Issuer matches subject: ${JSON.stringify(cert.issuer)}`, 'Obtain a certificate from a trusted CA.'));
1058
+ }
1059
+ // Protocol version check
1060
+ const weakProtocols = ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1.1'];
1061
+ if (weakProtocols.includes(cert.protocol)) {
1062
+ findings.push(createFinding('HIGH', 'SSL/TLS', `Weak protocol: ${cert.protocol}`, `Server negotiated ${cert.protocol} which is deprecated and has known vulnerabilities.`, `Negotiated protocol: ${cert.protocol}`, 'Configure the server to only support TLS 1.2 and TLS 1.3.'));
1063
+ }
1064
+ // Check common weak cipher suites (by name pattern)
1065
+ const weakCipherPatterns = [
1066
+ { pattern: /RC4/i, name: 'RC4', severity: 'HIGH' },
1067
+ { pattern: /DES(?!3)/i, name: 'DES', severity: 'HIGH' },
1068
+ { pattern: /3DES|DES-CBC3/i, name: '3DES', severity: 'MEDIUM' },
1069
+ { pattern: /NULL/i, name: 'NULL cipher', severity: 'CRITICAL' },
1070
+ { pattern: /EXPORT/i, name: 'Export-grade', severity: 'CRITICAL' },
1071
+ { pattern: /MD5/i, name: 'MD5-based', severity: 'MEDIUM' },
1072
+ { pattern: /anon/i, name: 'Anonymous', severity: 'CRITICAL' },
1073
+ ];
1074
+ for (const wc of weakCipherPatterns) {
1075
+ if (wc.pattern.test(cert.cipher)) {
1076
+ findings.push(createFinding(wc.severity, 'SSL/TLS', `Weak cipher suite: ${wc.name} (${cert.cipher})`, `The server negotiated a ${wc.name} cipher suite which is considered weak.`, `Cipher: ${cert.cipher}`, 'Configure the server to use strong cipher suites only: ECDHE+AESGCM:ECDHE+CHACHA20POLY1305.'));
1077
+ }
1078
+ }
1079
+ // Check certificate key size via the cipher info
1080
+ if (cert.cipher && /RSA/i.test(cert.cipher)) {
1081
+ // Note: We can't directly get key size from tls module cipher info,
1082
+ // but we can flag if RSA is used (recommend ECDHE)
1083
+ findings.push(createFinding('INFO', 'SSL/TLS', 'RSA key exchange in use', 'The negotiated cipher uses RSA key exchange instead of ECDHE. RSA key exchange does not provide forward secrecy.', `Cipher: ${cert.cipher}`, 'Prefer ECDHE key exchange for forward secrecy. Configure cipher order: ECDHE+AESGCM before RSA.'));
1084
+ }
1085
+ // Check if the cert covers the hostname
1086
+ const hostMatches = cert.altNames.some(name => {
1087
+ if (name.startsWith('*.')) {
1088
+ const wildcardBase = name.slice(2);
1089
+ return hostname.endsWith(wildcardBase) && hostname.split('.').length === name.split('.').length;
1090
+ }
1091
+ return name === hostname;
1092
+ });
1093
+ if (!hostMatches && cert.altNames.length > 0) {
1094
+ findings.push(createFinding('HIGH', 'SSL/TLS', 'Certificate hostname mismatch', `The certificate does not cover ${hostname}. Browsers will show warnings.`, `Certificate covers: ${cert.altNames.join(', ')}\nRequested hostname: ${hostname}`, 'Obtain a certificate that includes the correct hostname in the Subject Alternative Names.'));
1095
+ }
1096
+ return findings;
1097
+ }
1098
+ async function checkInjection(target) {
1099
+ const findings = [];
1100
+ const parsed = parseTarget(target);
1101
+ const baseUrl = `${parsed.protocol}//${parsed.host}`;
1102
+ // Test for reflected input in error pages (potential XSS)
1103
+ const xssPayloads = [
1104
+ { path: '/<script>alert(1)</script>', name: 'script tag in URL path' },
1105
+ { path: '/?q=<img+src=x+onerror=alert(1)>', name: 'img tag in query parameter' },
1106
+ { path: '/?search=%22onmouseover%3Dalert(1)%22', name: 'event handler in query parameter' },
1107
+ { path: '/?id=1%27+OR+1%3D1--', name: 'SQL injection in query parameter' },
1108
+ { path: '/?file=../../../etc/passwd', name: 'path traversal in query parameter' },
1109
+ { path: '/?url=http://evil.com', name: 'open redirect in query parameter' },
1110
+ { path: '/?redirect=http://evil.com', name: 'open redirect via redirect param' },
1111
+ { path: '/?next=http://evil.com', name: 'open redirect via next param' },
1112
+ { path: '/?return=http://evil.com', name: 'open redirect via return param' },
1113
+ { path: '/?callback=http://evil.com', name: 'open redirect via callback param' },
1114
+ ];
1115
+ for (const payload of xssPayloads) {
1116
+ const resp = await safeRequest(`${baseUrl}${payload.path}`, { timeout: 5_000 });
1117
+ if (!resp)
1118
+ continue;
1119
+ // Check for reflected XSS (the payload appears in the response body unescaped)
1120
+ if (payload.name.includes('script') && resp.body.includes('<script>alert(1)</script>')) {
1121
+ findings.push(createFinding('HIGH', 'Injection', 'Potential reflected XSS', `The server reflects the script tag back in the response body without encoding: ${payload.name}`, `URL: ${baseUrl}${payload.path}\nResponse contains unescaped script tag.`, 'Implement input validation and output encoding. Use a Content-Security-Policy to prevent inline script execution.'));
1122
+ }
1123
+ if (payload.name.includes('img') && resp.body.includes('onerror=alert(1)')) {
1124
+ findings.push(createFinding('HIGH', 'Injection', 'Potential reflected XSS via event handler', `The server reflects an event handler in the response body: ${payload.name}`, `URL: ${baseUrl}${payload.path}\nResponse contains onerror=alert(1).`, 'Implement strict output encoding for all user-controlled content in HTML responses.'));
1125
+ }
1126
+ // Check for SQL error disclosure
1127
+ if (payload.name.includes('SQL')) {
1128
+ const sqlErrors = [
1129
+ /SQL syntax/i, /mysql_/i, /pg_query/i, /ORA-\d{5}/i,
1130
+ /Microsoft OLE DB/i, /ODBC SQL Server/i, /unclosed quotation/i,
1131
+ /quoted string not properly terminated/i, /SQLite3::/i,
1132
+ /Warning.*mysql/i, /valid MySQL result/i,
1133
+ /PostgreSQL.*ERROR/i, /ERROR.*syntax error at/i,
1134
+ /com\.mysql/i, /java\.sql\.SQLException/i,
1135
+ /DB2 SQL error/i, /SQLSTATE/i,
1136
+ ];
1137
+ for (const pattern of sqlErrors) {
1138
+ if (pattern.test(resp.body)) {
1139
+ findings.push(createFinding('HIGH', 'Injection', 'SQL error message disclosed', 'The server returns SQL error messages that reveal database type and query structure.', `URL: ${baseUrl}${payload.path}\nMatched pattern: ${pattern.source}`, 'Never expose database error messages to users. Use generic error pages and log detailed errors server-side.'));
1140
+ break;
1141
+ }
1142
+ }
1143
+ }
1144
+ // Check for path traversal success
1145
+ if (payload.name.includes('path traversal')) {
1146
+ if (/root:|\/bin\/bash|\/bin\/sh|nobody:|daemon:/i.test(resp.body)) {
1147
+ findings.push(createFinding('CRITICAL', 'Injection', 'Path traversal vulnerability (LFI)', 'The server appears to be vulnerable to local file inclusion. /etc/passwd content was returned.', `URL: ${baseUrl}${payload.path}\nResponse contains system file content.`, 'Never use user input directly in file paths. Use allowlists for permitted files and sanitize all path components.'));
1148
+ }
1149
+ }
1150
+ // Check for open redirect
1151
+ if (payload.name.includes('redirect')) {
1152
+ if (resp.statusCode >= 300 && resp.statusCode < 400) {
1153
+ const location = String(resp.headers['location'] ?? '');
1154
+ if (location.includes('evil.com')) {
1155
+ findings.push(createFinding('MEDIUM', 'Injection', `Open redirect via ${payload.name.split('via ')[1] ?? 'parameter'}`, 'The server redirects to an arbitrary external URL based on user input. This can be used for phishing attacks.', `URL: ${baseUrl}${payload.path}\nRedirects to: ${location}`, 'Validate redirect URLs against an allowlist of permitted domains. Never redirect to user-controlled URLs.'));
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ // Check for error page information disclosure
1161
+ const errorPaths = [
1162
+ '/this-page-should-not-exist-404',
1163
+ '/api/this-endpoint-should-not-exist',
1164
+ '/%00',
1165
+ '/..%00',
1166
+ ];
1167
+ for (const errorPath of errorPaths) {
1168
+ const resp = await safeRequest(`${baseUrl}${errorPath}`, { timeout: 5_000 });
1169
+ if (!resp)
1170
+ continue;
1171
+ // Check for stack trace disclosure
1172
+ const stackTracePatterns = [
1173
+ /at\s+\S+\s+\([^)]+:\d+:\d+\)/, // JS stack trace
1174
+ /File ".*?", line \d+/, // Python traceback
1175
+ /\.java:\d+\)/, // Java stack trace
1176
+ /#\d+\s+\S+\s+\S+\.php/, // PHP stack trace
1177
+ /in\s+\S+\.rb:\d+/, // Ruby stack trace
1178
+ /^\s+at\s+\S+\.\S+\(/m, // .NET stack trace
1179
+ /Traceback \(most recent call last\)/, // Python traceback header
1180
+ /goroutine \d+ \[/, // Go panic
1181
+ /Exception in thread/, // Java exception
1182
+ /System\.(?:NullReferenceException|ArgumentException|InvalidOperationException)/, // .NET
1183
+ ];
1184
+ for (const pattern of stackTracePatterns) {
1185
+ if (pattern.test(resp.body)) {
1186
+ findings.push(createFinding('MEDIUM', 'Information Disclosure', 'Stack trace disclosed in error response', 'Error responses contain stack traces revealing internal code structure, file paths, and potentially sensitive information.', `URL: ${baseUrl}${errorPath}\nMatched: ${pattern.source}`, 'Configure the application to show generic error pages in production. Log detailed errors server-side only.'));
1187
+ break;
1188
+ }
1189
+ }
1190
+ // Check for debug mode indicators
1191
+ const debugPatterns = [
1192
+ /DEBUG\s*=\s*True/i,
1193
+ /DJANGO_SETTINGS_MODULE/i,
1194
+ /X-Debug-Token/i,
1195
+ /Whoops!/i, // Laravel debug page
1196
+ /Laravel.*Exception/i,
1197
+ /DebugBar/i,
1198
+ /symfony.*profiler/i,
1199
+ ];
1200
+ for (const pattern of debugPatterns) {
1201
+ if (pattern.test(resp.body)) {
1202
+ findings.push(createFinding('HIGH', 'Configuration', 'Debug mode appears to be enabled in production', 'The error response indicates the application is running in debug mode, exposing sensitive internal details.', `URL: ${baseUrl}${errorPath}\nMatched: ${pattern.source}`, 'Disable debug mode in production. Set DEBUG=False (Django), APP_DEBUG=false (Laravel), NODE_ENV=production (Node.js).'));
1203
+ break;
1204
+ }
1205
+ }
1206
+ }
1207
+ return findings;
1208
+ }
1209
+ async function checkAuth(target) {
1210
+ const findings = [];
1211
+ const parsed = parseTarget(target);
1212
+ const baseUrl = `${parsed.protocol}//${parsed.host}`;
1213
+ // Check for default credentials on common admin paths
1214
+ const defaultCredPaths = [
1215
+ { path: '/admin', title: 'Admin panel' },
1216
+ { path: '/administrator', title: 'Administrator panel' },
1217
+ { path: '/login', title: 'Login page' },
1218
+ { path: '/wp-login.php', title: 'WordPress login' },
1219
+ { path: '/phpmyadmin', title: 'phpMyAdmin' },
1220
+ { path: '/adminer', title: 'Adminer' },
1221
+ { path: '/manager/html', title: 'Tomcat Manager' },
1222
+ { path: '/jenkins', title: 'Jenkins' },
1223
+ ];
1224
+ for (const cred of defaultCredPaths) {
1225
+ const resp = await safeRequest(`${baseUrl}${cred.path}`, { timeout: 5_000 });
1226
+ if (!resp)
1227
+ continue;
1228
+ // Check if login page is accessible without authentication
1229
+ if (resp.statusCode === 200) {
1230
+ const hasLoginForm = /<form[^>]*>[\s\S]*?(?:password|login|signin)/i.test(resp.body);
1231
+ const hasBasicAuth = resp.statusCode === 401;
1232
+ if (hasLoginForm) {
1233
+ findings.push(createFinding('INFO', 'Authentication', `${cred.title} login page found at ${cred.path}`, `A login form is accessible at ${cred.path}. This is informational — verify brute-force protections are in place.`, `GET ${baseUrl}${cred.path} → 200 with login form`, 'Ensure rate limiting, account lockout, and CAPTCHA are configured on login forms. Use strong password policies.'));
1234
+ }
1235
+ // Check for no authentication required (direct access to admin)
1236
+ if (/dashboard|admin.*panel|control.*panel|management/i.test(resp.body) && !hasLoginForm) {
1237
+ findings.push(createFinding('CRITICAL', 'Authentication', `${cred.title} accessible without authentication`, `The admin interface at ${cred.path} appears accessible without requiring login credentials.`, `GET ${baseUrl}${cred.path} → 200 with admin content, no login form present.`, 'Implement authentication for all administrative interfaces. Use strong passwords and multi-factor authentication.'));
1238
+ }
1239
+ }
1240
+ }
1241
+ // Check for cookie security
1242
+ const mainResp = await safeRequest(baseUrl, { timeout: 10_000 });
1243
+ if (mainResp) {
1244
+ const setCookieHeaders = mainResp.headers['set-cookie'];
1245
+ const cookieValues = Array.isArray(setCookieHeaders)
1246
+ ? setCookieHeaders
1247
+ : setCookieHeaders
1248
+ ? [String(setCookieHeaders)]
1249
+ : [];
1250
+ for (const cookieStr of cookieValues) {
1251
+ const cookie = parseCookieAttributes(cookieStr);
1252
+ const isSessionCookie = /sess|token|auth|jwt|sid|login|user/i.test(cookie.name);
1253
+ if (isSessionCookie) {
1254
+ if (!cookie.httpOnly) {
1255
+ findings.push(createFinding('MEDIUM', 'Authentication', `Session cookie "${cookie.name}" missing HttpOnly flag`, 'The session cookie is accessible via JavaScript (document.cookie), making it vulnerable to XSS-based session theft.', `Set-Cookie: ${cookieStr.slice(0, 150)}...`, 'Add the HttpOnly flag to all session cookies to prevent JavaScript access.'));
1256
+ }
1257
+ if (!cookie.secure && parsed.protocol === 'https:') {
1258
+ findings.push(createFinding('MEDIUM', 'Authentication', `Session cookie "${cookie.name}" missing Secure flag`, 'The session cookie can be sent over unencrypted HTTP connections, enabling session hijacking via MITM.', `Set-Cookie: ${cookieStr.slice(0, 150)}...`, 'Add the Secure flag to all session cookies when using HTTPS.'));
1259
+ }
1260
+ if (!cookie.sameSite || cookie.sameSite.toLowerCase() === 'none') {
1261
+ findings.push(createFinding('LOW', 'Authentication', `Session cookie "${cookie.name}" has weak SameSite policy`, `The session cookie has SameSite=${cookie.sameSite || 'not set'}, which may allow CSRF attacks.`, `Set-Cookie: ${cookieStr.slice(0, 150)}...`, 'Set SameSite=Lax or SameSite=Strict on session cookies for CSRF protection.'));
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+ // Check for CORS misconfiguration
1267
+ const corsResp = await safeRequest(baseUrl, {
1268
+ timeout: 5_000,
1269
+ headers: { 'Origin': 'https://evil-attacker.com' },
1270
+ });
1271
+ if (corsResp) {
1272
+ const acao = String(corsResp.headers['access-control-allow-origin'] ?? '');
1273
+ const acac = String(corsResp.headers['access-control-allow-credentials'] ?? '');
1274
+ if (acao === '*') {
1275
+ if (acac.toLowerCase() === 'true') {
1276
+ findings.push(createFinding('CRITICAL', 'Authentication', 'CORS allows all origins with credentials', 'Access-Control-Allow-Origin is set to * with Access-Control-Allow-Credentials: true. Any website can make authenticated requests.', `Access-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true`, 'Never use * with credentials. Implement an allowlist of trusted origins.'));
1277
+ }
1278
+ else {
1279
+ findings.push(createFinding('MEDIUM', 'Authentication', 'CORS allows all origins', 'Access-Control-Allow-Origin is set to *, allowing any website to read responses. Evaluate if this is intended.', `Access-Control-Allow-Origin: *`, 'Restrict CORS to specific trusted origins unless the API is truly public.'));
1280
+ }
1281
+ }
1282
+ else if (acao === 'https://evil-attacker.com') {
1283
+ findings.push(createFinding('CRITICAL', 'Authentication', 'CORS reflects arbitrary origin', 'The server reflects the Origin header value in Access-Control-Allow-Origin, allowing any website to read responses.', `Origin: https://evil-attacker.com\nAccess-Control-Allow-Origin: ${acao}`, 'Do not reflect the Origin header. Implement a strict allowlist of permitted origins.'));
1284
+ }
1285
+ }
1286
+ return findings;
1287
+ }
1288
+ async function checkConfig(target) {
1289
+ const findings = [];
1290
+ const parsed = parseTarget(target);
1291
+ const baseUrl = `${parsed.protocol}//${parsed.host}`;
1292
+ // Check for rate limiting
1293
+ const rateLimitResults = [];
1294
+ for (let i = 0; i < 5; i++) {
1295
+ const resp = await safeRequest(baseUrl, { timeout: 5_000 });
1296
+ if (resp) {
1297
+ rateLimitResults.push(resp.statusCode);
1298
+ }
1299
+ }
1300
+ const has429 = rateLimitResults.includes(429);
1301
+ const mainResp = await safeRequest(baseUrl, { timeout: 5_000 });
1302
+ const hasRateLimitHeaders = mainResp && (mainResp.headers['x-ratelimit-limit'] ||
1303
+ mainResp.headers['x-rate-limit-limit'] ||
1304
+ mainResp.headers['ratelimit-limit'] ||
1305
+ mainResp.headers['retry-after']);
1306
+ if (!has429 && !hasRateLimitHeaders) {
1307
+ findings.push(createFinding('LOW', 'Configuration', 'No rate limiting detected', 'No rate limiting headers or 429 responses were observed after multiple rapid requests. The application may be vulnerable to brute-force attacks.', `5 rapid requests all returned: ${rateLimitResults.join(', ')}\nNo rate limit headers detected.`, 'Implement rate limiting on all endpoints, especially authentication endpoints. Use headers like X-RateLimit-Limit and X-RateLimit-Remaining.'));
1308
+ }
1309
+ // Check HTTP methods
1310
+ const optionsResp = await safeRequest(baseUrl, { method: 'OPTIONS', timeout: 5_000 });
1311
+ const allowedMethods = [];
1312
+ if (optionsResp) {
1313
+ const allow = String(optionsResp.headers['allow'] ?? '');
1314
+ if (allow) {
1315
+ allowedMethods.push(...allow.split(',').map(m => m.trim()));
1316
+ }
1317
+ const corsAllow = String(optionsResp.headers['access-control-allow-methods'] ?? '');
1318
+ if (corsAllow) {
1319
+ allowedMethods.push(...corsAllow.split(',').map(m => m.trim()));
1320
+ }
1321
+ }
1322
+ // Test individual methods
1323
+ const dangerousMethods = ['TRACE', 'PUT', 'DELETE'];
1324
+ for (const method of dangerousMethods) {
1325
+ const resp = await safeRequest(baseUrl, { method, timeout: 5_000 });
1326
+ if (resp && resp.statusCode !== 405 && resp.statusCode !== 501 && resp.statusCode !== 404) {
1327
+ if (method === 'TRACE' && resp.statusCode === 200) {
1328
+ findings.push(createFinding('MEDIUM', 'Configuration', 'TRACE method enabled', 'The TRACE HTTP method is enabled. This can be exploited for Cross-Site Tracing (XST) attacks to steal credentials.', `TRACE ${baseUrl} → ${resp.statusCode}`, 'Disable the TRACE method on the web server. For Nginx: if ($request_method = TRACE) { return 405; }'));
1329
+ }
1330
+ else if (method === 'PUT') {
1331
+ findings.push(createFinding('MEDIUM', 'Configuration', 'PUT method may be enabled', `The PUT method returned ${resp.statusCode} instead of 405. This could allow file upload or modification.`, `PUT ${baseUrl} → ${resp.statusCode}`, 'Disable PUT method if not needed. Ensure proper authorization checks on PUT endpoints.'));
1332
+ }
1333
+ else if (method === 'DELETE') {
1334
+ findings.push(createFinding('MEDIUM', 'Configuration', 'DELETE method may be enabled', `The DELETE method returned ${resp.statusCode} instead of 405. This could allow resource deletion.`, `DELETE ${baseUrl} → ${resp.statusCode}`, 'Disable DELETE method if not needed. Ensure proper authorization checks on DELETE endpoints.'));
1335
+ }
1336
+ }
1337
+ }
1338
+ // Check for directory listing on common directories
1339
+ const dirPaths = ['/images/', '/img/', '/uploads/', '/files/', '/css/', '/js/', '/assets/', '/static/', '/media/'];
1340
+ for (const dirPath of dirPaths) {
1341
+ const resp = await safeRequest(`${baseUrl}${dirPath}`, { timeout: 5_000 });
1342
+ if (resp && resp.statusCode === 200) {
1343
+ if (/Index of|<title>.*listing|Parent Directory|\[DIR\]|<pre>.*<a href/i.test(resp.body)) {
1344
+ findings.push(createFinding('MEDIUM', 'Configuration', `Directory listing enabled at ${dirPath}`, 'Directory listing allows attackers to browse and enumerate all files in the directory.', `GET ${baseUrl}${dirPath} → 200 with directory listing content.`, 'Disable directory listing. Nginx: autoindex off; Apache: Options -Indexes.'));
1345
+ }
1346
+ }
1347
+ }
1348
+ // Check for information in HTTP response
1349
+ if (mainResp) {
1350
+ // Check for ASP.NET version header
1351
+ const aspnetVersion = mainResp.headers['x-aspnet-version'] || mainResp.headers['x-aspnetmvc-version'];
1352
+ if (aspnetVersion) {
1353
+ findings.push(createFinding('LOW', 'Configuration', 'ASP.NET version disclosed', `The X-AspNet-Version or X-AspNetMvc-Version header reveals the framework version: ${aspnetVersion}`, `Header value: ${aspnetVersion}`, 'Remove version headers. In web.config: <httpRuntime enableVersionHeader="false" />'));
1354
+ }
1355
+ // Check for X-Debug-Token (Symfony)
1356
+ if (mainResp.headers['x-debug-token']) {
1357
+ findings.push(createFinding('HIGH', 'Configuration', 'Debug token header present (Symfony Profiler)', 'The X-Debug-Token header is present, indicating the Symfony Profiler is enabled in production.', `X-Debug-Token: ${mainResp.headers['x-debug-token']}`, 'Disable the Symfony Profiler in production by removing the web_profiler configuration.'));
1358
+ }
1359
+ // Check for ETag-based information leakage (Apache inode disclosure)
1360
+ const etag = String(mainResp.headers['etag'] ?? '');
1361
+ if (etag && /^"[0-9a-f]+-[0-9a-f]+-[0-9a-f]+"$/i.test(etag)) {
1362
+ findings.push(createFinding('LOW', 'Configuration', 'ETag reveals server inode information', 'The ETag header contains inode, size, and mtime information (Apache default), which can aid fingerprinting.', `ETag: ${etag}`, 'Configure Apache to not include inodes: FileETag MTime Size'));
1363
+ }
1364
+ }
1365
+ // Check for common security.txt
1366
+ const secTxtResp = await safeRequest(`${baseUrl}/.well-known/security.txt`, { timeout: 5_000 });
1367
+ if (!secTxtResp || secTxtResp.statusCode !== 200) {
1368
+ findings.push(createFinding('INFO', 'Configuration', 'No security.txt file found', 'No /.well-known/security.txt file was found. This file helps security researchers report vulnerabilities responsibly.', `GET ${baseUrl}/.well-known/security.txt → ${secTxtResp?.statusCode ?? 'connection failed'}`, 'Create a security.txt file at /.well-known/security.txt with contact information for security reports. See https://securitytxt.org/'));
1369
+ }
1370
+ // Check for X-Robots-Tag
1371
+ if (mainResp && !mainResp.headers['x-robots-tag']) {
1372
+ // This is just informational
1373
+ findings.push(createFinding('INFO', 'Configuration', 'No X-Robots-Tag header', 'The X-Robots-Tag header is not set. Consider using it to control indexing of sensitive pages.', 'X-Robots-Tag header not present.', 'Use X-Robots-Tag: noindex for pages that should not be indexed by search engines.'));
1374
+ }
1375
+ return findings;
1376
+ }
1377
+ async function performVulnScan(target, checks) {
1378
+ const allFindings = [];
1379
+ const checksRun = [];
1380
+ const runAll = checks === 'all';
1381
+ if (runAll || checks === 'headers') {
1382
+ checksRun.push('headers');
1383
+ const headerFindings = await checkSecurityHeaders(target);
1384
+ allFindings.push(...headerFindings);
1385
+ }
1386
+ if (runAll || checks === 'ssl') {
1387
+ checksRun.push('ssl');
1388
+ const sslFindings = await checkSSL(target);
1389
+ allFindings.push(...sslFindings);
1390
+ }
1391
+ if (runAll || checks === 'injection') {
1392
+ checksRun.push('injection');
1393
+ const injectionFindings = await checkInjection(target);
1394
+ allFindings.push(...injectionFindings);
1395
+ }
1396
+ if (runAll || checks === 'auth') {
1397
+ checksRun.push('auth');
1398
+ const authFindings = await checkAuth(target);
1399
+ allFindings.push(...authFindings);
1400
+ }
1401
+ if (runAll || checks === 'config') {
1402
+ checksRun.push('config');
1403
+ const configFindings = await checkConfig(target);
1404
+ allFindings.push(...configFindings);
1405
+ }
1406
+ // Sort by severity
1407
+ allFindings.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
1408
+ return {
1409
+ findings: allFindings,
1410
+ checks_run: checksRun,
1411
+ timestamp: new Date().toISOString(),
1412
+ };
1413
+ }
1414
+ // ─────────────────────────────────────────────────────────────────────────────
1415
+ // Report Generator
1416
+ // ─────────────────────────────────────────────────────────────────────────────
1417
+ function generateMarkdownReport(session, findings, recon) {
1418
+ const counts = countBySeverity(findings);
1419
+ const now = new Date().toISOString();
1420
+ let report = `# Penetration Test Report\n\n`;
1421
+ report += `**Target:** ${session.target}\n`;
1422
+ report += `**Scope:** ${session.scope}\n`;
1423
+ report += `**Mode:** ${session.passive_only ? 'Passive only' : 'Active + Passive'}\n`;
1424
+ report += `**Started:** ${session.started_at}\n`;
1425
+ report += `**Generated:** ${now}\n\n`;
1426
+ // Executive Summary
1427
+ report += `## Executive Summary\n\n`;
1428
+ const totalFindings = findings.length;
1429
+ const criticalAndHigh = counts.CRITICAL + counts.HIGH;
1430
+ if (criticalAndHigh === 0 && totalFindings === 0) {
1431
+ report += `No security findings were identified during this assessment. The target appears to have a strong security posture based on the checks performed.\n\n`;
1432
+ }
1433
+ else if (criticalAndHigh === 0) {
1434
+ report += `${totalFindings} finding(s) were identified, none of which are critical or high severity. The target has a reasonable security posture with room for improvement in lower-priority areas.\n\n`;
1435
+ }
1436
+ else if (counts.CRITICAL > 0) {
1437
+ report += `**${counts.CRITICAL} critical** and **${counts.HIGH} high** severity finding(s) require immediate attention. A total of ${totalFindings} finding(s) were identified. The target has significant security weaknesses that should be addressed urgently.\n\n`;
1438
+ }
1439
+ else {
1440
+ report += `**${counts.HIGH} high** severity finding(s) should be addressed promptly. A total of ${totalFindings} finding(s) were identified across all severity levels.\n\n`;
1441
+ }
1442
+ // Risk Matrix
1443
+ report += `## Risk Matrix\n\n`;
1444
+ report += `| Severity | Count |\n`;
1445
+ report += `|----------|-------|\n`;
1446
+ report += `| ${severityBadge('CRITICAL')} | ${counts.CRITICAL} |\n`;
1447
+ report += `| ${severityBadge('HIGH')} | ${counts.HIGH} |\n`;
1448
+ report += `| ${severityBadge('MEDIUM')} | ${counts.MEDIUM} |\n`;
1449
+ report += `| ${severityBadge('LOW')} | ${counts.LOW} |\n`;
1450
+ report += `| ${severityBadge('INFO')} | ${counts.INFO} |\n\n`;
1451
+ // Scope and Methodology
1452
+ report += `## Scope & Methodology\n\n`;
1453
+ report += `### Scope\n`;
1454
+ report += `- **Target:** ${session.target}\n`;
1455
+ report += `- **Type:** ${session.scope}\n`;
1456
+ report += `- **Passive only:** ${session.passive_only ? 'Yes' : 'No'}\n\n`;
1457
+ report += `### Methodology\n`;
1458
+ report += `The assessment was conducted using automated tools performing:\n`;
1459
+ report += `- DNS enumeration and analysis\n`;
1460
+ report += `- HTTP response header analysis\n`;
1461
+ report += `- SSL/TLS certificate and configuration analysis\n`;
1462
+ report += `- Common path and sensitive file discovery\n`;
1463
+ report += `- Subdomain enumeration\n`;
1464
+ report += `- Security header evaluation\n`;
1465
+ report += `- Injection vector testing (reflected XSS, SQLi, LFI, open redirect)\n`;
1466
+ report += `- Authentication and session management review\n`;
1467
+ report += `- Configuration and information disclosure checks\n\n`;
1468
+ // Recon Summary
1469
+ if (recon) {
1470
+ report += `## Reconnaissance Summary\n\n`;
1471
+ if (Object.keys(recon.dns).length > 0) {
1472
+ report += `### DNS Records\n`;
1473
+ for (const [rtype, records] of Object.entries(recon.dns)) {
1474
+ if (Array.isArray(records) && records.length > 0) {
1475
+ report += `- **${rtype}:** ${records.join(', ')}\n`;
1476
+ }
1477
+ else if (typeof records === 'boolean') {
1478
+ report += `- **${rtype}:** ${records ? 'Present' : 'Not found'}\n`;
1479
+ }
1480
+ }
1481
+ report += `\n`;
1482
+ }
1483
+ if (recon.technologies.length > 0) {
1484
+ report += `### Technologies Detected\n`;
1485
+ for (const tech of recon.technologies) {
1486
+ report += `- ${tech}\n`;
1487
+ }
1488
+ report += `\n`;
1489
+ }
1490
+ if (Object.keys(recon.ssl).length > 0) {
1491
+ report += `### SSL/TLS Certificate\n`;
1492
+ const ssl = recon.ssl;
1493
+ if (ssl.valid !== undefined)
1494
+ report += `- **Valid:** ${ssl.valid}\n`;
1495
+ if (ssl.protocol)
1496
+ report += `- **Protocol:** ${ssl.protocol}\n`;
1497
+ if (ssl.cipher)
1498
+ report += `- **Cipher:** ${ssl.cipher}\n`;
1499
+ if (ssl.validTo)
1500
+ report += `- **Expires:** ${ssl.validTo}\n`;
1501
+ if (ssl.daysUntilExpiry !== undefined)
1502
+ report += `- **Days until expiry:** ${ssl.daysUntilExpiry}\n`;
1503
+ if (ssl.selfSigned !== undefined)
1504
+ report += `- **Self-signed:** ${ssl.selfSigned}\n`;
1505
+ if (Array.isArray(ssl.altNames) && ssl.altNames.length > 0) {
1506
+ report += `- **Alt Names:** ${ssl.altNames.join(', ')}\n`;
1507
+ }
1508
+ report += `\n`;
1509
+ }
1510
+ const resolvedPaths = recon.paths.filter(p => p.status >= 200 && p.status < 400);
1511
+ if (resolvedPaths.length > 0) {
1512
+ report += `### Discovered Paths (${resolvedPaths.length})\n`;
1513
+ report += `| Path | Status | Size |\n`;
1514
+ report += `|------|--------|------|\n`;
1515
+ for (const p of resolvedPaths.slice(0, 50)) {
1516
+ report += `| ${p.path} | ${p.status} | ${p.size ?? '-'} |\n`;
1517
+ }
1518
+ if (resolvedPaths.length > 50) {
1519
+ report += `| ... | +${resolvedPaths.length - 50} more | |\n`;
1520
+ }
1521
+ report += `\n`;
1522
+ }
1523
+ const resolvedSubs = recon.subdomains.filter(s => s.resolved);
1524
+ if (resolvedSubs.length > 0) {
1525
+ report += `### Resolved Subdomains (${resolvedSubs.length})\n`;
1526
+ for (const s of resolvedSubs) {
1527
+ report += `- ${s.subdomain} → ${s.ip}\n`;
1528
+ }
1529
+ report += `\n`;
1530
+ }
1531
+ }
1532
+ // Findings
1533
+ report += `## Findings\n\n`;
1534
+ if (findings.length === 0) {
1535
+ report += `No findings to report.\n\n`;
1536
+ }
1537
+ else {
1538
+ // Findings table
1539
+ report += `| # | Severity | Category | Title |\n`;
1540
+ report += `|---|----------|----------|-------|\n`;
1541
+ findings.forEach((f, i) => {
1542
+ report += `| ${i + 1} | ${severityBadge(f.severity)} | ${f.category} | ${f.title} |\n`;
1543
+ });
1544
+ report += `\n`;
1545
+ // Detailed findings
1546
+ report += `### Detailed Findings\n\n`;
1547
+ findings.forEach((f, i) => {
1548
+ report += `#### ${i + 1}. ${f.title}\n\n`;
1549
+ report += `- **Severity:** ${severityBadge(f.severity)}\n`;
1550
+ report += `- **Category:** ${f.category}\n`;
1551
+ report += `- **Finding ID:** ${f.id}\n\n`;
1552
+ report += `**Description:**\n${f.description}\n\n`;
1553
+ report += `**Evidence:**\n\`\`\`\n${f.evidence}\n\`\`\`\n\n`;
1554
+ report += `**Remediation:**\n${f.remediation}\n\n`;
1555
+ report += `---\n\n`;
1556
+ });
1557
+ }
1558
+ // Remediation Priorities
1559
+ report += `## Remediation Priorities\n\n`;
1560
+ const criticalFindings = findings.filter(f => f.severity === 'CRITICAL');
1561
+ const highFindings = findings.filter(f => f.severity === 'HIGH');
1562
+ const mediumFindings = findings.filter(f => f.severity === 'MEDIUM');
1563
+ if (criticalFindings.length > 0) {
1564
+ report += `### Immediate (Critical)\n`;
1565
+ for (const f of criticalFindings) {
1566
+ report += `1. **${f.title}** — ${f.remediation}\n`;
1567
+ }
1568
+ report += `\n`;
1569
+ }
1570
+ if (highFindings.length > 0) {
1571
+ report += `### Short-term (High)\n`;
1572
+ for (const f of highFindings) {
1573
+ report += `1. **${f.title}** — ${f.remediation}\n`;
1574
+ }
1575
+ report += `\n`;
1576
+ }
1577
+ if (mediumFindings.length > 0) {
1578
+ report += `### Medium-term (Medium)\n`;
1579
+ for (const f of mediumFindings) {
1580
+ report += `1. **${f.title}** — ${f.remediation}\n`;
1581
+ }
1582
+ report += `\n`;
1583
+ }
1584
+ report += `---\n\n`;
1585
+ report += `*Report generated by kbot pentest tools. This is an automated assessment and may not cover all vulnerabilities. A manual review is recommended for comprehensive security evaluation.*\n`;
1586
+ return report;
1587
+ }
1588
+ function generateJsonReport(session, findings, recon) {
1589
+ return JSON.stringify({
1590
+ report: {
1591
+ target: session.target,
1592
+ scope: session.scope,
1593
+ passive_only: session.passive_only,
1594
+ started_at: session.started_at,
1595
+ generated_at: new Date().toISOString(),
1596
+ },
1597
+ summary: {
1598
+ total_findings: findings.length,
1599
+ severity_counts: countBySeverity(findings),
1600
+ },
1601
+ recon: recon ?? {},
1602
+ findings,
1603
+ }, null, 2);
1604
+ }
1605
+ function generateHtmlReport(session, findings, recon) {
1606
+ const counts = countBySeverity(findings);
1607
+ const now = new Date().toISOString();
1608
+ const severityColor = (sev) => {
1609
+ switch (sev) {
1610
+ case 'CRITICAL': return '#dc2626';
1611
+ case 'HIGH': return '#ea580c';
1612
+ case 'MEDIUM': return '#ca8a04';
1613
+ case 'LOW': return '#2563eb';
1614
+ case 'INFO': return '#6b7280';
1615
+ }
1616
+ };
1617
+ let html = `<!DOCTYPE html>
1618
+ <html lang="en">
1619
+ <head>
1620
+ <meta charset="UTF-8">
1621
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1622
+ <title>Pentest Report — ${session.target}</title>
1623
+ <style>
1624
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1625
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; max-width: 960px; margin: 0 auto; padding: 2rem; }
1626
+ h1 { font-size: 1.75rem; margin-bottom: 0.5rem; }
1627
+ h2 { font-size: 1.35rem; margin-top: 2rem; margin-bottom: 0.75rem; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.25rem; }
1628
+ h3 { font-size: 1.1rem; margin-top: 1.25rem; margin-bottom: 0.5rem; }
1629
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
1630
+ th, td { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; text-align: left; font-size: 0.9rem; }
1631
+ th { background: #f3f4f6; font-weight: 600; }
1632
+ .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; color: #fff; font-size: 0.75rem; font-weight: 600; }
1633
+ .meta { color: #6b7280; font-size: 0.9rem; margin-bottom: 1.5rem; }
1634
+ .finding { margin: 1.25rem 0; padding: 1rem; border: 1px solid #e5e7eb; border-radius: 8px; border-left: 4px solid; }
1635
+ .finding pre { background: #f9fafb; padding: 0.75rem; border-radius: 4px; overflow-x: auto; font-size: 0.8rem; margin: 0.5rem 0; }
1636
+ .summary-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.75rem; margin: 1rem 0; }
1637
+ .summary-card { text-align: center; padding: 0.75rem; border-radius: 8px; border: 1px solid #e5e7eb; }
1638
+ .summary-card .count { font-size: 1.5rem; font-weight: 700; }
1639
+ .summary-card .label { font-size: 0.75rem; text-transform: uppercase; }
1640
+ </style>
1641
+ </head>
1642
+ <body>
1643
+ <h1>Penetration Test Report</h1>
1644
+ <div class="meta">
1645
+ <p><strong>Target:</strong> ${session.target} | <strong>Scope:</strong> ${session.scope} | <strong>Mode:</strong> ${session.passive_only ? 'Passive' : 'Active'}</p>
1646
+ <p><strong>Started:</strong> ${session.started_at} | <strong>Generated:</strong> ${now}</p>
1647
+ </div>
1648
+
1649
+ <h2>Summary</h2>
1650
+ <div class="summary-grid">
1651
+ <div class="summary-card" style="border-color: ${severityColor('CRITICAL')}">
1652
+ <div class="count" style="color: ${severityColor('CRITICAL')}">${counts.CRITICAL}</div>
1653
+ <div class="label">Critical</div>
1654
+ </div>
1655
+ <div class="summary-card" style="border-color: ${severityColor('HIGH')}">
1656
+ <div class="count" style="color: ${severityColor('HIGH')}">${counts.HIGH}</div>
1657
+ <div class="label">High</div>
1658
+ </div>
1659
+ <div class="summary-card" style="border-color: ${severityColor('MEDIUM')}">
1660
+ <div class="count" style="color: ${severityColor('MEDIUM')}">${counts.MEDIUM}</div>
1661
+ <div class="label">Medium</div>
1662
+ </div>
1663
+ <div class="summary-card" style="border-color: ${severityColor('LOW')}">
1664
+ <div class="count" style="color: ${severityColor('LOW')}">${counts.LOW}</div>
1665
+ <div class="label">Low</div>
1666
+ </div>
1667
+ <div class="summary-card" style="border-color: ${severityColor('INFO')}">
1668
+ <div class="count" style="color: ${severityColor('INFO')}">${counts.INFO}</div>
1669
+ <div class="label">Info</div>
1670
+ </div>
1671
+ </div>
1672
+ `;
1673
+ if (findings.length > 0) {
1674
+ html += ` <h2>Findings</h2>
1675
+ <table>
1676
+ <thead><tr><th>#</th><th>Severity</th><th>Category</th><th>Title</th></tr></thead>
1677
+ <tbody>
1678
+ `;
1679
+ findings.forEach((f, i) => {
1680
+ html += ` <tr><td>${i + 1}</td><td><span class="badge" style="background:${severityColor(f.severity)}">${f.severity}</span></td><td>${f.category}</td><td>${f.title}</td></tr>\n`;
1681
+ });
1682
+ html += ` </tbody>
1683
+ </table>
1684
+
1685
+ <h2>Detailed Findings</h2>
1686
+ `;
1687
+ findings.forEach((f, i) => {
1688
+ html += ` <div class="finding" style="border-left-color: ${severityColor(f.severity)}">
1689
+ <h3>${i + 1}. ${f.title}</h3>
1690
+ <p><span class="badge" style="background:${severityColor(f.severity)}">${f.severity}</span> <strong>${f.category}</strong> (ID: ${f.id})</p>
1691
+ <p><strong>Description:</strong> ${f.description}</p>
1692
+ <pre>${f.evidence}</pre>
1693
+ <p><strong>Remediation:</strong> ${f.remediation}</p>
1694
+ </div>
1695
+ `;
1696
+ });
1697
+ }
1698
+ else {
1699
+ html += ` <h2>Findings</h2>\n <p>No findings to report.</p>\n`;
1700
+ }
1701
+ html += ` <hr style="margin-top:2rem">
1702
+ <p style="color:#9ca3af;font-size:0.8rem;margin-top:1rem"><em>Report generated by kbot pentest tools. Automated assessment — manual review recommended.</em></p>
1703
+ </body>
1704
+ </html>`;
1705
+ return html;
1706
+ }
1707
+ // ─────────────────────────────────────────────────────────────────────────────
1708
+ // Tool Registration
1709
+ // ─────────────────────────────────────────────────────────────────────────────
1710
+ export function registerPentestTools() {
1711
+ // ─── pentest_start ──────────────────────────────────────────────────────
1712
+ registerTool({
1713
+ name: 'pentest_start',
1714
+ description: 'Start a new penetration testing session against a target. Creates a session with findings storage in ~/.kbot/pentest/. Returns the session ID.',
1715
+ parameters: {
1716
+ target: { type: 'string', description: 'URL or IP address to test (e.g., "https://example.com" or "192.168.1.1")', required: true },
1717
+ scope: { type: 'string', description: 'Assessment scope: "full", "web", "network", or "api" (default: "web")' },
1718
+ passive_only: { type: 'boolean', description: 'Only perform passive reconnaissance — no active probing (default: false)' },
1719
+ },
1720
+ tier: 'free',
1721
+ timeout: 30_000,
1722
+ async execute(args) {
1723
+ const target = String(args.target ?? '').trim();
1724
+ if (!target) {
1725
+ return 'Error: target is required. Provide a URL or IP address.';
1726
+ }
1727
+ const validScopes = ['full', 'web', 'network', 'api'];
1728
+ const scope = validScopes.includes(String(args.scope ?? '')) ? String(args.scope) : 'web';
1729
+ const passiveOnly = args.passive_only === true;
1730
+ // Validate target
1731
+ try {
1732
+ parseTarget(target);
1733
+ }
1734
+ catch {
1735
+ return `Error: Invalid target "${target}". Provide a valid URL or IP address.`;
1736
+ }
1737
+ ensurePentestDir();
1738
+ const sessionId = `pentest-${Date.now()}-${randomUUID().slice(0, 6)}`;
1739
+ createSessionDir(sessionId);
1740
+ const session = {
1741
+ id: sessionId,
1742
+ target,
1743
+ scope,
1744
+ passive_only: passiveOnly,
1745
+ started_at: new Date().toISOString(),
1746
+ phases: {
1747
+ recon: 'pending',
1748
+ vuln_scan: 'pending',
1749
+ report: 'pending',
1750
+ },
1751
+ findings_count: 0,
1752
+ };
1753
+ writeSession(session);
1754
+ const lines = [
1755
+ `Pentest session started.`,
1756
+ ``,
1757
+ ` Session ID: ${sessionId}`,
1758
+ ` Target: ${target}`,
1759
+ ` Scope: ${scope}`,
1760
+ ` Passive only: ${passiveOnly}`,
1761
+ ` Storage: ~/.kbot/pentest/${sessionId}/`,
1762
+ ``,
1763
+ `Next steps:`,
1764
+ ` 1. Run pentest_recon for reconnaissance`,
1765
+ ` 2. Run pentest_vuln_scan for vulnerability assessment`,
1766
+ ` 3. Run pentest_report to generate the report`,
1767
+ ``,
1768
+ `Or use pentest_status to check progress.`,
1769
+ ];
1770
+ return lines.join('\n');
1771
+ },
1772
+ });
1773
+ // ─── pentest_recon ──────────────────────────────────────────────────────
1774
+ registerTool({
1775
+ name: 'pentest_recon',
1776
+ description: 'Perform reconnaissance on a target: DNS enumeration (A, AAAA, MX, NS, TXT, CNAME, SOA, SPF, DMARC), WHOIS-style info, HTTP header analysis, technology fingerprinting, SSL/TLS certificate analysis, robots.txt and sitemap.xml discovery, common path enumeration, and subdomain brute-forcing.',
1777
+ parameters: {
1778
+ target: { type: 'string', description: 'URL or IP address to scan (required)', required: true },
1779
+ depth: { type: 'string', description: 'Scan depth: "quick" (fast, fewer checks), "standard" (balanced), or "deep" (thorough, slow). Default: "standard"' },
1780
+ },
1781
+ tier: 'free',
1782
+ timeout: 300_000,
1783
+ async execute(args) {
1784
+ const target = String(args.target ?? '').trim();
1785
+ if (!target) {
1786
+ return 'Error: target is required.';
1787
+ }
1788
+ const validDepths = ['quick', 'standard', 'deep'];
1789
+ const depth = (validDepths.includes(String(args.depth ?? '')) ? String(args.depth) : 'standard');
1790
+ // Find or create a session for this target
1791
+ let sessionId = findSessionForTarget(target);
1792
+ if (!sessionId) {
1793
+ // Auto-create a session
1794
+ ensurePentestDir();
1795
+ sessionId = `pentest-${Date.now()}-${randomUUID().slice(0, 6)}`;
1796
+ createSessionDir(sessionId);
1797
+ const session = {
1798
+ id: sessionId,
1799
+ target,
1800
+ scope: 'web',
1801
+ passive_only: false,
1802
+ started_at: new Date().toISOString(),
1803
+ phases: { recon: 'pending', vuln_scan: 'pending', report: 'pending' },
1804
+ findings_count: 0,
1805
+ };
1806
+ writeSession(session);
1807
+ }
1808
+ const session = readSession(sessionId);
1809
+ if (!session) {
1810
+ return 'Error: Could not read session configuration.';
1811
+ }
1812
+ // Update phase status
1813
+ session.phases.recon = 'running';
1814
+ writeSession(session);
1815
+ try {
1816
+ const { recon, findings } = await performRecon(target, depth, session.passive_only);
1817
+ // Save recon data
1818
+ writeRecon(sessionId, recon);
1819
+ // Merge findings with existing vulns
1820
+ const existingVulns = readVulns(sessionId);
1821
+ const allFindings = [
1822
+ ...(existingVulns?.findings ?? []),
1823
+ ...findings,
1824
+ ];
1825
+ writeVulns(sessionId, {
1826
+ findings: allFindings,
1827
+ checks_run: [...(existingVulns?.checks_run ?? []), 'recon'],
1828
+ timestamp: new Date().toISOString(),
1829
+ });
1830
+ // Update session
1831
+ session.phases.recon = 'complete';
1832
+ session.findings_count = allFindings.length;
1833
+ writeSession(session);
1834
+ // Format output
1835
+ const lines = [
1836
+ `Reconnaissance complete for ${target} (depth: ${depth})`,
1837
+ ``,
1838
+ ];
1839
+ // DNS summary
1840
+ const dnsEntries = Object.entries(recon.dns).filter(([, v]) => {
1841
+ if (Array.isArray(v))
1842
+ return v.length > 0;
1843
+ return v !== false;
1844
+ });
1845
+ if (dnsEntries.length > 0) {
1846
+ lines.push(`DNS Records:`);
1847
+ for (const [rtype, records] of dnsEntries) {
1848
+ if (Array.isArray(records)) {
1849
+ lines.push(` ${rtype}: ${records.slice(0, 5).join(', ')}${records.length > 5 ? ` (+${records.length - 5} more)` : ''}`);
1850
+ }
1851
+ else if (typeof records === 'boolean') {
1852
+ lines.push(` ${rtype}: ${records ? 'present' : 'not found'}`);
1853
+ }
1854
+ }
1855
+ lines.push(``);
1856
+ }
1857
+ // Technologies
1858
+ if (recon.technologies.length > 0) {
1859
+ lines.push(`Technologies detected (${recon.technologies.length}):`);
1860
+ for (const tech of recon.technologies.slice(0, 15)) {
1861
+ lines.push(` - ${tech}`);
1862
+ }
1863
+ if (recon.technologies.length > 15) {
1864
+ lines.push(` ... +${recon.technologies.length - 15} more`);
1865
+ }
1866
+ lines.push(``);
1867
+ }
1868
+ // SSL
1869
+ if (Object.keys(recon.ssl).length > 0) {
1870
+ const ssl = recon.ssl;
1871
+ lines.push(`SSL/TLS:`);
1872
+ lines.push(` Valid: ${ssl.valid}, Protocol: ${ssl.protocol}, Cipher: ${ssl.cipher}`);
1873
+ lines.push(` Expires: ${ssl.validTo} (${ssl.daysUntilExpiry} days)`);
1874
+ lines.push(``);
1875
+ }
1876
+ // Paths
1877
+ const activePaths = recon.paths.filter(p => p.status >= 200 && p.status < 400);
1878
+ if (activePaths.length > 0) {
1879
+ lines.push(`Discovered paths (${activePaths.length} accessible):`);
1880
+ for (const p of activePaths.slice(0, 20)) {
1881
+ lines.push(` [${p.status}] ${p.path} (${p.size ?? 0} bytes)`);
1882
+ }
1883
+ if (activePaths.length > 20) {
1884
+ lines.push(` ... +${activePaths.length - 20} more`);
1885
+ }
1886
+ lines.push(``);
1887
+ }
1888
+ // Subdomains
1889
+ const resolvedSubs = recon.subdomains.filter(s => s.resolved);
1890
+ if (resolvedSubs.length > 0) {
1891
+ lines.push(`Resolved subdomains (${resolvedSubs.length}):`);
1892
+ for (const s of resolvedSubs.slice(0, 15)) {
1893
+ lines.push(` ${s.subdomain} -> ${s.ip}`);
1894
+ }
1895
+ if (resolvedSubs.length > 15) {
1896
+ lines.push(` ... +${resolvedSubs.length - 15} more`);
1897
+ }
1898
+ lines.push(``);
1899
+ }
1900
+ // Findings summary
1901
+ if (findings.length > 0) {
1902
+ const counts = countBySeverity(findings);
1903
+ lines.push(`Recon findings (${findings.length}):`);
1904
+ if (counts.CRITICAL > 0)
1905
+ lines.push(` CRITICAL: ${counts.CRITICAL}`);
1906
+ if (counts.HIGH > 0)
1907
+ lines.push(` HIGH: ${counts.HIGH}`);
1908
+ if (counts.MEDIUM > 0)
1909
+ lines.push(` MEDIUM: ${counts.MEDIUM}`);
1910
+ if (counts.LOW > 0)
1911
+ lines.push(` LOW: ${counts.LOW}`);
1912
+ if (counts.INFO > 0)
1913
+ lines.push(` INFO: ${counts.INFO}`);
1914
+ lines.push(``);
1915
+ for (const f of findings.slice(0, 10)) {
1916
+ lines.push(` [${f.severity}] ${f.title}`);
1917
+ }
1918
+ if (findings.length > 10) {
1919
+ lines.push(` ... +${findings.length - 10} more findings`);
1920
+ }
1921
+ }
1922
+ else {
1923
+ lines.push(`No findings from reconnaissance phase.`);
1924
+ }
1925
+ lines.push(``);
1926
+ lines.push(`Session: ${sessionId}`);
1927
+ lines.push(`Data saved to: ~/.kbot/pentest/${sessionId}/recon.json`);
1928
+ return lines.join('\n');
1929
+ }
1930
+ catch (err) {
1931
+ session.phases.recon = 'error';
1932
+ writeSession(session);
1933
+ return `Error during reconnaissance: ${err instanceof Error ? err.message : String(err)}`;
1934
+ }
1935
+ },
1936
+ });
1937
+ // ─── pentest_vuln_scan ──────────────────────────────────────────────────
1938
+ registerTool({
1939
+ name: 'pentest_vuln_scan',
1940
+ description: 'Perform a vulnerability assessment on a target. Checks: security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy), SSL/TLS issues (expired cert, weak cipher, protocol), injection vectors (reflected XSS, SQL injection, path traversal, open redirect), authentication issues (CORS misconfiguration, cookie security, default credentials), and configuration problems (rate limiting, HTTP methods, directory listing, information disclosure).',
1941
+ parameters: {
1942
+ target: { type: 'string', description: 'URL to scan (required)', required: true },
1943
+ checks: { type: 'string', description: 'Checks to run: "headers", "ssl", "injection", "auth", "config", or "all" (default: "all")' },
1944
+ },
1945
+ tier: 'free',
1946
+ timeout: 300_000,
1947
+ async execute(args) {
1948
+ const target = String(args.target ?? '').trim();
1949
+ if (!target) {
1950
+ return 'Error: target is required.';
1951
+ }
1952
+ const validChecks = ['headers', 'ssl', 'injection', 'auth', 'config', 'all'];
1953
+ const checks = validChecks.includes(String(args.checks ?? '')) ? String(args.checks) : 'all';
1954
+ // Find or create a session for this target
1955
+ let sessionId = findSessionForTarget(target);
1956
+ if (!sessionId) {
1957
+ ensurePentestDir();
1958
+ sessionId = `pentest-${Date.now()}-${randomUUID().slice(0, 6)}`;
1959
+ createSessionDir(sessionId);
1960
+ const session = {
1961
+ id: sessionId,
1962
+ target,
1963
+ scope: 'web',
1964
+ passive_only: false,
1965
+ started_at: new Date().toISOString(),
1966
+ phases: { recon: 'pending', vuln_scan: 'pending', report: 'pending' },
1967
+ findings_count: 0,
1968
+ };
1969
+ writeSession(session);
1970
+ }
1971
+ const session = readSession(sessionId);
1972
+ if (!session) {
1973
+ return 'Error: Could not read session configuration.';
1974
+ }
1975
+ session.phases.vuln_scan = 'running';
1976
+ writeSession(session);
1977
+ try {
1978
+ const vulnData = await performVulnScan(target, checks);
1979
+ // Merge with existing findings (avoid duplicates from recon)
1980
+ const existingVulns = readVulns(sessionId);
1981
+ const existingIds = new Set((existingVulns?.findings ?? []).map(f => f.id));
1982
+ const newFindings = vulnData.findings.filter(f => !existingIds.has(f.id));
1983
+ const allFindings = [
1984
+ ...(existingVulns?.findings ?? []),
1985
+ ...newFindings,
1986
+ ];
1987
+ const allChecks = [...new Set([
1988
+ ...(existingVulns?.checks_run ?? []),
1989
+ ...vulnData.checks_run,
1990
+ ])];
1991
+ writeVulns(sessionId, {
1992
+ findings: allFindings,
1993
+ checks_run: allChecks,
1994
+ timestamp: new Date().toISOString(),
1995
+ });
1996
+ session.phases.vuln_scan = 'complete';
1997
+ session.findings_count = allFindings.length;
1998
+ writeSession(session);
1999
+ // Format output
2000
+ const lines = [
2001
+ `Vulnerability scan complete for ${target}`,
2002
+ `Checks run: ${vulnData.checks_run.join(', ')}`,
2003
+ ``,
2004
+ ];
2005
+ if (vulnData.findings.length > 0) {
2006
+ const counts = countBySeverity(vulnData.findings);
2007
+ lines.push(`New findings (${vulnData.findings.length}):`);
2008
+ lines.push(` CRITICAL: ${counts.CRITICAL} | HIGH: ${counts.HIGH} | MEDIUM: ${counts.MEDIUM} | LOW: ${counts.LOW} | INFO: ${counts.INFO}`);
2009
+ lines.push(``);
2010
+ // Group by category
2011
+ const byCategory = new Map();
2012
+ for (const f of vulnData.findings) {
2013
+ const cat = byCategory.get(f.category) ?? [];
2014
+ cat.push(f);
2015
+ byCategory.set(f.category, cat);
2016
+ }
2017
+ for (const [category, catFindings] of byCategory) {
2018
+ lines.push(` ${category}:`);
2019
+ for (const f of catFindings) {
2020
+ lines.push(` [${f.severity}] ${f.title}`);
2021
+ }
2022
+ lines.push(``);
2023
+ }
2024
+ }
2025
+ else {
2026
+ lines.push(`No vulnerabilities found in the "${checks}" checks.`);
2027
+ lines.push(``);
2028
+ }
2029
+ lines.push(`Total findings in session: ${allFindings.length}`);
2030
+ lines.push(`Session: ${sessionId}`);
2031
+ lines.push(`Data saved to: ~/.kbot/pentest/${sessionId}/vulns.json`);
2032
+ return lines.join('\n');
2033
+ }
2034
+ catch (err) {
2035
+ session.phases.vuln_scan = 'error';
2036
+ writeSession(session);
2037
+ return `Error during vulnerability scan: ${err instanceof Error ? err.message : String(err)}`;
2038
+ }
2039
+ },
2040
+ });
2041
+ // ─── pentest_report ─────────────────────────────────────────────────────
2042
+ registerTool({
2043
+ name: 'pentest_report',
2044
+ description: 'Generate a structured penetration test report with executive summary, scope, methodology, findings table, risk matrix, and remediation priorities. Supports markdown, JSON, and HTML output formats.',
2045
+ parameters: {
2046
+ format: { type: 'string', description: 'Report format: "markdown", "json", or "html" (default: "markdown")' },
2047
+ session: { type: 'string', description: 'Session ID. Defaults to the latest session.' },
2048
+ },
2049
+ tier: 'free',
2050
+ timeout: 60_000,
2051
+ async execute(args) {
2052
+ const validFormats = ['markdown', 'json', 'html'];
2053
+ const format = validFormats.includes(String(args.format ?? '')) ? String(args.format) : 'markdown';
2054
+ // Find session
2055
+ let sessionId = String(args.session ?? '').trim();
2056
+ if (!sessionId) {
2057
+ const latest = findLatestSession();
2058
+ if (!latest) {
2059
+ return 'Error: No pentest sessions found. Run pentest_start first.';
2060
+ }
2061
+ sessionId = latest;
2062
+ }
2063
+ const session = readSession(sessionId);
2064
+ if (!session) {
2065
+ return `Error: Session "${sessionId}" not found.`;
2066
+ }
2067
+ const recon = readRecon(sessionId);
2068
+ const findings = getAllFindings(sessionId);
2069
+ // Update session
2070
+ session.phases.report = 'complete';
2071
+ writeSession(session);
2072
+ let report;
2073
+ let extension;
2074
+ switch (format) {
2075
+ case 'json':
2076
+ report = generateJsonReport(session, findings, recon);
2077
+ extension = 'json';
2078
+ break;
2079
+ case 'html':
2080
+ report = generateHtmlReport(session, findings, recon);
2081
+ extension = 'html';
2082
+ break;
2083
+ default:
2084
+ report = generateMarkdownReport(session, findings, recon);
2085
+ extension = 'md';
2086
+ break;
2087
+ }
2088
+ // Save report to session directory
2089
+ const reportPath = join(sessionDir(sessionId), `report.${extension}`);
2090
+ writeFileSync(reportPath, report);
2091
+ // Also save as report.md for the default case
2092
+ if (extension !== 'md') {
2093
+ const mdReport = generateMarkdownReport(session, findings, recon);
2094
+ writeFileSync(join(sessionDir(sessionId), 'report.md'), mdReport);
2095
+ }
2096
+ const counts = countBySeverity(findings);
2097
+ const summary = [
2098
+ `Report generated (${format}).`,
2099
+ ``,
2100
+ ` Session: ${sessionId}`,
2101
+ ` Target: ${session.target}`,
2102
+ ` Findings: ${findings.length} total`,
2103
+ ` CRITICAL: ${counts.CRITICAL}`,
2104
+ ` HIGH: ${counts.HIGH}`,
2105
+ ` MEDIUM: ${counts.MEDIUM}`,
2106
+ ` LOW: ${counts.LOW}`,
2107
+ ` INFO: ${counts.INFO}`,
2108
+ ``,
2109
+ ` Saved to: ~/.kbot/pentest/${sessionId}/report.${extension}`,
2110
+ ``,
2111
+ ];
2112
+ // For markdown and short reports, include the full report inline
2113
+ if (format === 'markdown' && report.length < 40_000) {
2114
+ summary.push(`--- Full Report ---\n`);
2115
+ summary.push(report);
2116
+ }
2117
+ else if (format === 'json' && report.length < 40_000) {
2118
+ summary.push(`--- Full Report ---\n`);
2119
+ summary.push(report);
2120
+ }
2121
+ else {
2122
+ summary.push(`Report is ${report.length} characters. View at: ~/.kbot/pentest/${sessionId}/report.${extension}`);
2123
+ }
2124
+ return summary.join('\n');
2125
+ },
2126
+ });
2127
+ // ─── pentest_status ─────────────────────────────────────────────────────
2128
+ registerTool({
2129
+ name: 'pentest_status',
2130
+ description: 'Check the status of the current or a specific penetration test session. Shows which phases are complete, total findings count, and severity breakdown.',
2131
+ parameters: {
2132
+ session: { type: 'string', description: 'Session ID. Defaults to the latest session.' },
2133
+ },
2134
+ tier: 'free',
2135
+ timeout: 10_000,
2136
+ async execute(args) {
2137
+ let sessionId = String(args.session ?? '').trim();
2138
+ if (!sessionId) {
2139
+ const latest = findLatestSession();
2140
+ if (!latest) {
2141
+ // List all sessions
2142
+ ensurePentestDir();
2143
+ const allDirs = readdirSync(PENTEST_DIR).filter(d => {
2144
+ const fp = join(PENTEST_DIR, d);
2145
+ return statSync(fp).isDirectory() && existsSync(join(fp, 'config.json'));
2146
+ });
2147
+ if (allDirs.length === 0) {
2148
+ return 'No pentest sessions found. Run pentest_start to begin a new assessment.';
2149
+ }
2150
+ const lines = [`Found ${allDirs.length} session(s):\n`];
2151
+ for (const d of allDirs) {
2152
+ const s = readSession(d);
2153
+ if (s) {
2154
+ lines.push(` ${s.id} — ${s.target} (${s.started_at})`);
2155
+ }
2156
+ }
2157
+ return lines.join('\n');
2158
+ }
2159
+ sessionId = latest;
2160
+ }
2161
+ const session = readSession(sessionId);
2162
+ if (!session) {
2163
+ return `Error: Session "${sessionId}" not found.`;
2164
+ }
2165
+ const findings = getAllFindings(sessionId);
2166
+ const counts = countBySeverity(findings);
2167
+ const recon = readRecon(sessionId);
2168
+ const phaseIcon = (status) => {
2169
+ switch (status) {
2170
+ case 'complete': return '[done]';
2171
+ case 'running': return '[running]';
2172
+ case 'error': return '[error]';
2173
+ default: return '[pending]';
2174
+ }
2175
+ };
2176
+ const lines = [
2177
+ `Pentest Session Status`,
2178
+ ``,
2179
+ ` Session: ${session.id}`,
2180
+ ` Target: ${session.target}`,
2181
+ ` Scope: ${session.scope}`,
2182
+ ` Passive only: ${session.passive_only}`,
2183
+ ` Started: ${session.started_at}`,
2184
+ ``,
2185
+ `Phases:`,
2186
+ ` ${phaseIcon(session.phases.recon)} Reconnaissance`,
2187
+ ` ${phaseIcon(session.phases.vuln_scan)} Vulnerability Scan`,
2188
+ ` ${phaseIcon(session.phases.report)} Report Generation`,
2189
+ ``,
2190
+ `Findings (${findings.length} total):`,
2191
+ ` CRITICAL: ${counts.CRITICAL}`,
2192
+ ` HIGH: ${counts.HIGH}`,
2193
+ ` MEDIUM: ${counts.MEDIUM}`,
2194
+ ` LOW: ${counts.LOW}`,
2195
+ ` INFO: ${counts.INFO}`,
2196
+ ``,
2197
+ ];
2198
+ // Show recent top findings
2199
+ if (findings.length > 0) {
2200
+ lines.push(`Top findings:`);
2201
+ for (const f of findings.slice(0, 8)) {
2202
+ lines.push(` [${f.severity}] ${f.title}`);
2203
+ }
2204
+ if (findings.length > 8) {
2205
+ lines.push(` ... +${findings.length - 8} more`);
2206
+ }
2207
+ lines.push(``);
2208
+ }
2209
+ // Show recon data summary if available
2210
+ if (recon) {
2211
+ const techCount = recon.technologies.length;
2212
+ const pathCount = recon.paths.filter(p => p.status >= 200 && p.status < 400).length;
2213
+ const subCount = recon.subdomains.filter(s => s.resolved).length;
2214
+ lines.push(`Recon data:`);
2215
+ lines.push(` Technologies: ${techCount} detected`);
2216
+ lines.push(` Paths found: ${pathCount} accessible`);
2217
+ lines.push(` Subdomains: ${subCount} resolved`);
2218
+ }
2219
+ lines.push(``);
2220
+ lines.push(`Storage: ~/.kbot/pentest/${sessionId}/`);
2221
+ return lines.join('\n');
2222
+ },
2223
+ });
2224
+ }
2225
+ //# sourceMappingURL=pentest.js.map