UNPKG

websec-audit

Version:

A universal security scanning and audit tool for websites

1,583 lines (1,576 loc) 132 kB
import axios from 'axios'; import * as cheerio from 'cheerio'; import * as emailValidator from 'email-validator'; import * as dns3 from 'dns'; import { promisify } from 'util'; import * as tls from 'tls'; import * as net from 'net'; import * as crypto from 'crypto'; var __getOwnPropNames = Object.getOwnPropertyNames; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; // node_modules/wappalyzer-core/wappalyzer.js var require_wappalyzer = __commonJS({ "node_modules/wappalyzer-core/wappalyzer.js"(exports, module) { function toArray(value) { return Array.isArray(value) ? value : [value]; } var benchmarkEnabled = typeof process !== "undefined" ? !!process.env.WAPPALYZER_BENCHMARK : false; var benchmarks = []; function benchmark(duration, pattern, value = "", technology) { if (!benchmarkEnabled) { return; } benchmarks.push({ duration, pattern: String(pattern.regex), value: String(value).slice(0, 100), valueLength: value.length, technology: technology.name }); } function benchmarkSummary() { if (!benchmarkEnabled) { return; } const totalPatterns = Object.values(benchmarks).length; const totalDuration = Object.values(benchmarks).reduce( (sum, { duration }) => sum + duration, 0 ); console.log({ totalPatterns, totalDuration, averageDuration: Math.round(totalDuration / totalPatterns), slowestTechnologies: Object.values( benchmarks.reduce((benchmarks2, { duration, technology }) => { if (benchmarks2[technology]) { benchmarks2[technology].duration += duration; } else { benchmarks2[technology] = { technology, duration }; } return benchmarks2; }, {}) ).sort(({ duration: a }, { duration: b }) => a > b ? -1 : 1).filter(({ duration }) => duration).slice(0, 5).reduce( (technologies, { technology, duration }) => ({ ...technologies, [technology]: duration }), {} ), slowestPatterns: Object.values(benchmarks).sort(({ duration: a }, { duration: b }) => a > b ? -1 : 1).filter(({ duration }) => duration).slice(0, 5) }); } var Wappalyzer = { technologies: [], categories: [], requires: [], categoryRequires: [], slugify: (string) => string.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/--+/g, "-").replace(/(?:^-|-$)/g, ""), getTechnology: (name) => [ ...Wappalyzer.technologies, ...Wappalyzer.requires.map(({ technologies }) => technologies).flat(), ...Wappalyzer.categoryRequires.map(({ technologies }) => technologies).flat() ].find(({ name: _name }) => name === _name), getCategory: (id) => Wappalyzer.categories.find(({ id: _id }) => id === _id), /** * Resolve promises for implied technology. * @param {Array} detections */ resolve(detections = []) { const resolved = detections.reduce((resolved2, { technology, lastUrl }) => { if (resolved2.findIndex( ({ technology: { name } }) => name === technology?.name ) === -1) { let version = ""; let confidence = 0; let rootPath; detections.filter( ({ technology: _technology }) => _technology && _technology.name === technology.name ).forEach( ({ technology: { name }, pattern, version: _version = "", rootPath: _rootPath }) => { confidence = Math.min(100, confidence + pattern.confidence); version = _version.length > version.length && _version.length <= 15 && (parseInt(_version, 10) || 0) < 1e4 ? _version : version; rootPath = rootPath || _rootPath || void 0; } ); resolved2.push({ technology, confidence, version, rootPath, lastUrl }); } return resolved2; }, []); Wappalyzer.resolveExcludes(resolved); Wappalyzer.resolveImplies(resolved); const priority = ({ technology: { categories } }) => categories.reduce( (max, id) => Math.max(max, Wappalyzer.getCategory(id).priority), 0 ); return resolved.sort((a, b) => priority(a) > priority(b) ? 1 : -1).map( ({ technology: { name, description, slug, categories, icon, website, pricing, cpe }, confidence, version, rootPath, lastUrl }) => ({ name, description, slug, categories: categories.map((id) => Wappalyzer.getCategory(id)), confidence, version, icon, website, pricing, cpe, rootPath, lastUrl }) ); }, /** * Resolve promises for version of technology. * @param {Promise} resolved * @param match */ resolveVersion({ version, regex }, match) { let resolved = version; if (version) { const matches = regex.exec(match); if (matches) { matches.forEach((match2, index) => { if (String(match2).length > 10) { return; } const ternary = new RegExp(`\\\\${index}\\?([^:]+):(.*)$`).exec( version ); if (ternary && ternary.length === 3) { resolved = version.replace( ternary[0], match2 ? ternary[1] : ternary[2] ); } resolved = resolved.trim().replace(new RegExp(`\\\\${index}`, "g"), match2 || ""); }); resolved = resolved.replace(/\\\d/, ""); } } return resolved; }, /** * Resolve promises for excluded technology. * @param {Promise} resolved */ resolveExcludes(resolved) { resolved.forEach(({ technology }) => { technology.excludes.forEach(({ name }) => { const excluded = Wappalyzer.getTechnology(name); if (!excluded) { throw new Error(`Excluded technology does not exist: ${name}`); } let index; do { index = resolved.findIndex( ({ technology: { name: name2 } }) => name2 === excluded.name ); if (index !== -1) { resolved.splice(index, 1); } } while (index !== -1); }); }); }, /** * Resolve promises for implied technology. * @param {Promise} resolved */ resolveImplies(resolved) { let done = false; do { done = true; resolved.forEach(({ technology, confidence, lastUrl }) => { technology.implies.forEach( ({ name, confidence: _confidence, version }) => { const implied = Wappalyzer.getTechnology(name); if (!implied) { throw new Error(`Implied technology does not exist: ${name}`); } if (resolved.findIndex( ({ technology: { name: name2 } }) => name2 === implied.name ) === -1) { resolved.push({ technology: implied, confidence: Math.min(confidence, _confidence), version: version || "", lastUrl }); done = false; } } ); }); } while (resolved.length && !done); }, /** * Initialize analyzation. * @param {*} param0 */ analyze(items, technologies = Wappalyzer.technologies) { benchmarks = []; const oo = Wappalyzer.analyzeOneToOne; const om = Wappalyzer.analyzeOneToMany; const mm = Wappalyzer.analyzeManyToMany; const relations = { certIssuer: oo, cookies: mm, css: oo, dns: mm, headers: mm, html: oo, meta: mm, probe: mm, robots: oo, scriptSrc: om, scripts: oo, text: oo, url: oo, xhr: oo }; try { const detections = technologies.map( (technology) => Object.keys(relations).map( (type) => items[type] && relations[type](technology, type, items[type]) ).flat() ).flat().filter((technology) => technology); benchmarkSummary(); return detections; } catch (error) { throw new Error(error.message || error.toString()); } }, /** * Extract technologies from data collected. * @param {object} data */ setTechnologies(data) { const transform = Wappalyzer.transformPatterns; Wappalyzer.technologies = Object.keys(data).reduce((technologies, name) => { const { cats, certIssuer, cookies, cpe, css, description, dns: dns5, dom, excludes, headers, html, icon, implies, js, meta, pricing, probe, requires, requiresCategory, robots, scriptSrc, scripts, text, url, website, xhr } = data[name]; technologies.push({ categories: cats || [], certIssuer: transform(certIssuer), cookies: transform(cookies), cpe: cpe || null, css: transform(css), description: description || null, dns: transform(dns5), dom: transform( typeof dom === "string" || Array.isArray(dom) ? toArray(dom).reduce( (dom2, selector) => ({ ...dom2, [selector]: { exists: "" } }), {} ) : dom, true, false ), excludes: transform(excludes).map(({ value }) => ({ name: value })), headers: transform(headers), html: transform(html), icon: icon || "default.svg", implies: transform(implies).map(({ value, confidence, version }) => ({ name: value, confidence, version })), js: transform(js, true), meta: transform(meta), name, pricing: pricing || [], probe: transform(probe, true), requires: transform(requires).map(({ value }) => ({ name: value })), requiresCategory: transform(requiresCategory).map(({ value }) => ({ id: value })), robots: transform(robots), scriptSrc: transform(scriptSrc), scripts: transform(scripts), slug: Wappalyzer.slugify(name), text: transform(text), url: transform(url), website: website || null, xhr: transform(xhr) }); return technologies; }, []); Wappalyzer.technologies.filter(({ requires }) => requires.length).forEach( (technology) => technology.requires.forEach(({ name }) => { if (!Wappalyzer.getTechnology(name)) { throw new Error(`Required technology does not exist: ${name}`); } Wappalyzer.requires[name] = Wappalyzer.requires[name] || []; Wappalyzer.requires[name].push(technology); }) ); Wappalyzer.requires = Object.keys(Wappalyzer.requires).map((name) => ({ name, technologies: Wappalyzer.requires[name] })); Wappalyzer.technologies.filter(({ requiresCategory }) => requiresCategory.length).forEach( (technology) => technology.requiresCategory.forEach(({ id }) => { Wappalyzer.categoryRequires[id] = Wappalyzer.categoryRequires[id] || []; Wappalyzer.categoryRequires[id].push(technology); }) ); Wappalyzer.categoryRequires = Object.keys(Wappalyzer.categoryRequires).map( (id) => ({ categoryId: parseInt(id, 10), technologies: Wappalyzer.categoryRequires[id] }) ); Wappalyzer.technologies = Wappalyzer.technologies.filter( ({ requires, requiresCategory }) => !requires.length && !requiresCategory.length ); }, /** * Assign categories for data. * @param {Object} data */ setCategories(data) { Wappalyzer.categories = Object.keys(data).reduce((categories, id) => { const category = data[id]; categories.push({ id: parseInt(id, 10), slug: Wappalyzer.slugify(category.name), ...category }); return categories; }, []).sort(({ priority: a }, { priority: b }) => a > b ? -1 : 0); }, /** * Transform patterns for internal use. * @param {string|array} patterns * @param {boolean} caseSensitive */ transformPatterns(patterns, caseSensitive = false, isRegex = true) { if (!patterns) { return []; } if (typeof patterns === "string" || typeof patterns === "number" || Array.isArray(patterns)) { patterns = { main: patterns }; } const parsed = Object.keys(patterns).reduce((parsed2, key) => { parsed2[caseSensitive ? key : key.toLowerCase()] = toArray( patterns[key] ).map((pattern) => Wappalyzer.parsePattern(pattern, isRegex)); return parsed2; }, {}); return "main" in parsed ? parsed.main : parsed; }, /** * Extract information from regex pattern. * @param {string|object} pattern */ parsePattern(pattern, isRegex = true) { if (typeof pattern === "object") { return Object.keys(pattern).reduce( (parsed, key) => ({ ...parsed, [key]: Wappalyzer.parsePattern(pattern[key]) }), {} ); } else { const { value, regex, confidence, version } = pattern.toString().split("\\;").reduce((attrs, attr, i) => { if (i) { attr = attr.split(":"); if (attr.length > 1) { attrs[attr.shift()] = attr.join(":"); } } else { attrs.value = typeof pattern === "number" ? pattern : attr; attrs.regex = new RegExp( isRegex ? attr.replace(/\//g, "\\/").replace(/\\\+/g, "__escapedPlus__").replace(/\+/g, "{1,250}").replace(/\*/g, "{0,250}").replace(/__escapedPlus__/g, "\\+") : "", "i" ); } return attrs; }, {}); return { value, regex, confidence: parseInt(confidence || 100, 10), version: version || "" }; } }, /** * @todo describe * @param {Object} technology * @param {String} type * @param {String} value */ analyzeOneToOne(technology, type, value) { return technology[type].reduce((technologies, pattern) => { const startTime = Date.now(); const matches = pattern.regex.exec(value); if (matches) { technologies.push({ technology, pattern: { ...pattern, type, value, match: matches[0] }, version: Wappalyzer.resolveVersion(pattern, value) }); } benchmark(Date.now() - startTime, pattern, value, technology); return technologies; }, []); }, /** * @todo update * @param {Object} technology * @param {String} type * @param {Array} items */ analyzeOneToMany(technology, type, items = []) { return items.reduce((technologies, value) => { const patterns = technology[type] || []; patterns.forEach((pattern) => { const startTime = Date.now(); const matches = pattern.regex.exec(value); if (matches) { technologies.push({ technology, pattern: { ...pattern, type, value, match: matches[0] }, version: Wappalyzer.resolveVersion(pattern, value) }); } benchmark(Date.now() - startTime, pattern, value, technology); }); return technologies; }, []); }, /** * * @param {Object} technology * @param {string} types * @param {Array} items */ analyzeManyToMany(technology, types, items = {}) { const [type, ...subtypes] = types.split("."); return Object.keys(technology[type]).reduce((technologies, key) => { const patterns = technology[type][key] || []; const values = items[key] || []; patterns.forEach((_pattern) => { const pattern = (subtypes || []).reduce( (pattern2, subtype) => pattern2[subtype] || {}, _pattern ); values.forEach((value) => { const startTime = Date.now(); const matches = pattern.regex.exec(value); if (matches) { technologies.push({ technology, pattern: { ...pattern, type, value, match: matches[0] }, version: Wappalyzer.resolveVersion(pattern, value) }); } benchmark(Date.now() - startTime, pattern, value, technology); }); }); return technologies; }, []); } }; if (typeof module !== "undefined") { module.exports = Wappalyzer; } } }); var makeRequest = async (url, options) => { try { const config = { url, method: options?.method || "GET", headers: { "User-Agent": "Mozilla/5.0 (compatible; SecurityScanner/1.0)", ...options?.headers }, timeout: options?.timeout || 1e4, // Default 10s timeout data: options?.data, validateStatus: () => true // Don't throw on any HTTP status code }; const response = await axios(config); return { status: response.status, headers: response.headers, data: response.data, error: null }; } catch (error) { return { status: 0, headers: {}, data: null, error: error.message || "Request failed" }; } }; var normalizeUrl = (input) => { if (!input) return ""; let url = input; if (!url.startsWith("http://") && !url.startsWith("https://")) { url = "https://" + url; } try { const parsed = new URL(url); return parsed.origin; } catch (e) { return url; } }; var extractDomain = (url) => { try { const parsed = new URL(normalizeUrl(url)); let domain = parsed.hostname; if (domain.startsWith("www.")) { domain = domain.substring(4); } return domain; } catch (e) { return url.replace(/^(https?:\/\/)?(www\.)?/, "").split("/")[0]; } }; var safeJsonParse = (text) => { try { return JSON.parse(text); } catch (e) { return null; } }; var createScannerInput = (target) => { if (typeof target === "string") { return { target: normalizeUrl(target), timeout: 1e4 }; } return { ...target, target: normalizeUrl(target.target) }; }; // src/modules/securityHeaders.ts var SECURITY_HEADERS = { "strict-transport-security": { description: "HTTP Strict Transport Security (HSTS) enforces secure (HTTPS) connections", severity: "high" }, "content-security-policy": { description: "Content Security Policy prevents XSS and data injection attacks", severity: "high" }, "x-content-type-options": { description: "X-Content-Type-Options prevents MIME-sniffing", severity: "medium" }, "x-frame-options": { description: "X-Frame-Options protects against clickjacking", severity: "medium" }, "x-xss-protection": { description: "X-XSS-Protection enables the cross-site scripting filter", severity: "medium" }, "referrer-policy": { description: "Referrer Policy controls how much information is sent in the Referer header", severity: "low" }, "permissions-policy": { description: "Permissions Policy controls which browser features can be used", severity: "low" }, "cross-origin-embedder-policy": { description: "Cross-Origin Embedder Policy prevents loading cross-origin resources", severity: "low" }, "cross-origin-opener-policy": { description: "Cross-Origin Opener Policy prevents opening cross-origin windows", severity: "low" }, "cross-origin-resource-policy": { description: "Cross-Origin Resource Policy prevents cross-origin loading", severity: "low" } }; var scanSecurityHeaders = async (input) => { const startTime = Date.now(); const normalizedInput = createScannerInput(input); try { const response = await makeRequest(normalizedInput.target, { method: "HEAD", timeout: normalizedInput.timeout, headers: normalizedInput.headers }); if (response.error || !response.headers) { return { status: "failure", scanner: "securityHeaders", error: response.error || "Failed to retrieve headers", data: { headers: {}, missing: Object.keys(SECURITY_HEADERS), issues: [], score: 0 }, timeTaken: Date.now() - startTime }; } const headers = {}; const headerNames = Object.keys(response.headers); headerNames.forEach((name) => { headers[name.toLowerCase()] = response.headers[name]; }); const missing = []; const issues = []; Object.keys(SECURITY_HEADERS).forEach((header) => { if (!headers[header]) { missing.push(header); issues.push({ severity: SECURITY_HEADERS[header].severity, header, description: `Missing ${header} header. ${SECURITY_HEADERS[header].description}` }); } }); const totalHeaders = Object.keys(SECURITY_HEADERS).length; const presentHeaders = totalHeaders - missing.length; const score = Math.round(presentHeaders / totalHeaders * 100); if (headers["strict-transport-security"] && !headers["strict-transport-security"].includes("max-age=")) { issues.push({ severity: "medium", header: "strict-transport-security", description: "HSTS header does not include max-age directive" }); } if (headers["x-frame-options"] && !["DENY", "SAMEORIGIN"].includes(headers["x-frame-options"].toUpperCase())) { issues.push({ severity: "medium", header: "x-frame-options", description: "X-Frame-Options should be set to DENY or SAMEORIGIN" }); } return { status: "success", scanner: "securityHeaders", data: { headers, missing, issues, score }, timeTaken: Date.now() - startTime }; } catch (error) { return { status: "failure", scanner: "securityHeaders", error: error.message || "Unknown error", data: { headers: {}, missing: Object.keys(SECURITY_HEADERS), issues: [], score: 0 }, timeTaken: Date.now() - startTime }; } }; var detectForms = async (input) => { const startTime = Date.now(); const normalizedInput = createScannerInput(input); try { let html; if (normalizedInput.options?.html) { html = normalizedInput.options.html; } else { const response = await makeRequest(normalizedInput.target, { method: "GET", timeout: normalizedInput.timeout, headers: normalizedInput.headers }); if (response.error || !response.data) { return { status: "failure", scanner: "formDetection", error: response.error || "Failed to retrieve HTML content", data: { forms: [], total: 0 }, timeTaken: Date.now() - startTime }; } html = typeof response.data === "string" ? response.data : String(response.data); } const $ = cheerio.load(html); const forms = $("form"); const formResults = []; forms.each((_i, formElement) => { const form = $(formElement); const action = form.attr("action") || ""; const method = (form.attr("method") || "get").toLowerCase(); const inputs = []; const formInputs = form.find('input, select, textarea, button[type="submit"]'); formInputs.each((_j, inputElement) => { const input2 = $(inputElement); const type = input2.attr("type") || "text"; inputs.push({ name: input2.attr("name"), type, id: input2.attr("id"), required: input2.attr("required") !== void 0, autocomplete: input2.attr("autocomplete") }); }); const hasPassword = inputs.some((input2) => input2.type === "password"); const hasCSRF = inputs.some((input2) => { const name = (input2.name || "").toLowerCase(); return name.includes("csrf") || name.includes("token") || name.includes("nonce") || name === "_token"; }); const issues = []; if (hasPassword) { if (method !== "post") { issues.push({ severity: "high", description: "Login form uses insecure method (GET). Should use POST to prevent credentials in URL." }); } if (!hasCSRF) { issues.push({ severity: "high", description: "Form appears to be missing CSRF protection token." }); } if (action && action.startsWith("http:")) { issues.push({ severity: "high", description: "Form submits to insecure (HTTP) endpoint." }); } const passwordInputs = inputs.filter((input2) => input2.type === "password"); if (passwordInputs.some((input2) => input2.autocomplete !== "off" && input2.autocomplete !== "new-password")) { issues.push({ severity: "medium", description: `Password field doesn't have autocomplete="off" or autocomplete="new-password".` }); } } formResults.push({ action, method, inputs, hasPassword, hasCSRF, issues }); }); return { status: "success", scanner: "formDetection", data: { forms: formResults, total: formResults.length }, timeTaken: Date.now() - startTime }; } catch (error) { return { status: "failure", scanner: "formDetection", error: error.message || "Unknown error", data: { forms: [], total: 0 }, timeTaken: Date.now() - startTime }; } }; // src/modules/sensitiveFiles.ts var SENSITIVE_PATHS = [ "/.git/config", "/.env", "/.env.local", "/.env.development", "/.env.production", "/config.json", "/config.js", "/config.php", "/wp-config.php", "/config.xml", "/credentials.json", "/secrets.json", "/settings.json", "/database.yml", "/db.sqlite", "/backup.zip", "/backup.sql", "/backup.tar.gz", "/dump.sql", "/users.sql", "/users.csv", "/phpinfo.php", "/info.php", "/.htpasswd", "/server-status", "/server-info", "/readme.md", "/README.md", "/api/swagger", "/api/docs", "/swagger.json", "/swagger-ui.html", "/robots.txt", "/sitemap.xml", "/.well-known/security.txt" ]; var scanSensitiveFiles = async (input) => { const startTime = Date.now(); const normalizedInput = createScannerInput(input); const baseUrl = normalizeUrl(normalizedInput.target); const timeout = normalizedInput.timeout || 5e3; const exposedFiles = []; const issues = []; try { let pathsToTest = [...SENSITIVE_PATHS]; if (normalizedInput.options?.additionalPaths) { pathsToTest = pathsToTest.concat(normalizedInput.options.additionalPaths); } const concurrentLimit = normalizedInput.options?.concurrentLimit || 5; const chunks = []; for (let i = 0; i < pathsToTest.length; i += concurrentLimit) { chunks.push(pathsToTest.slice(i, i + concurrentLimit)); } for (const chunk of chunks) { const promises = chunk.map((path) => { const url = baseUrl + path; return makeRequest(url, { method: "GET", timeout, headers: normalizedInput.headers }).then((response) => { if (response.error) { return; } if (response.status >= 200 && response.status < 400) { let contentTypeHeader = response.headers["content-type"]; const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader.join(", ") : contentTypeHeader || void 0; const contentLength = response.headers["content-length"] ? parseInt( Array.isArray(response.headers["content-length"]) ? response.headers["content-length"][0] : response.headers["content-length"], 10 ) : void 0; const hasContent = response.data && (typeof response.data === "string" ? response.data.length > 50 : true); const isDefaultPage = typeof response.data === "string" && (response.data.includes("<html") && response.data.includes("</html>") && response.data.includes("<title>Index of") === false); if (isDefaultPage && !contentLength) { return; } exposedFiles.push({ path, status: response.status, contentType, size: contentLength }); let severity = "medium"; let description = `Exposed file: ${path}`; if (path.includes(".env") || path.includes("config") || path.includes("credential") || path.includes("secret") || path.includes("password") || path.includes(".git/") || path.includes("backup") || path.includes("dump")) { severity = "high"; description = `Critical file exposed: ${path}. May contain sensitive information or credentials.`; } else if (path.includes("readme") || path.includes("robots.txt") || path.includes("sitemap.xml") || path.includes(".well-known")) { severity = "low"; description = `Information disclosure: ${path}. May reveal system information.`; } issues.push({ severity, path, description }); } }).catch((error) => { if (normalizedInput.options?.debug) { console.error(`Error scanning ${url}: ${error.message}`); } }); }); await Promise.all(promises); } return { status: "success", scanner: "sensitiveFiles", data: { exposedFiles, issues }, timeTaken: Date.now() - startTime }; } catch (error) { return { status: "failure", scanner: "sensitiveFiles", error: error.message || "Unknown error", data: { exposedFiles: [], issues: [] }, timeTaken: Date.now() - startTime }; } }; // src/modules/subdomains.ts var scanSubdomains = async (input) => { const startTime = Date.now(); const normalizedInput = createScannerInput(input); const domain = extractDomain(normalizedInput.target); try { const crtShUrl = `https://crt.sh/?q=%.${domain}&output=json`; const response = await makeRequest(crtShUrl, { method: "GET", timeout: normalizedInput.timeout || 15e3, // Increase default timeout for crt.sh headers: { "Accept": "application/json", "User-Agent": "Mozilla/5.0 (compatible; SecurityScanner/1.0)" } }); if (response.error) { return { status: "failure", scanner: "subdomains", error: `Failed to retrieve certificate data: ${response.error}`, data: { subdomains: [], total: 0 }, timeTaken: Date.now() - startTime }; } let crtData = []; if (typeof response.data === "string") { try { const cleanJson = response.data.trim().replace(/\n/g, ""); crtData = JSON.parse(cleanJson); } catch (e) { if (response.data.includes("<HTML>") || response.data.includes("<html>")) { return { status: "failure", scanner: "subdomains", error: "crt.sh returned HTML instead of JSON. Try again later.", data: { subdomains: [], total: 0 }, timeTaken: Date.now() - startTime }; } console.error("JSON Parse Error:", e); console.error("Response data sample:", response.data.substring(0, 200)); return { status: "failure", scanner: "subdomains", error: `Failed to parse certificate data: ${e.message}`, data: { subdomains: [], total: 0 }, timeTaken: Date.now() - startTime }; } } else if (Array.isArray(response.data)) { crtData = response.data; } else if (response.data && typeof response.data === "object") { crtData = [response.data]; } if (!crtData || crtData.length === 0) { return { status: "failure", scanner: "subdomains", error: "No certificate data found for this domain", data: { subdomains: [], total: 0 }, timeTaken: Date.now() - startTime }; } const allDomains = /* @__PURE__ */ new Set(); crtData.forEach((cert) => { if (cert && cert.name_value) { const name = cert.name_value.toLowerCase(); const domains = name.split(/[,\s]+/); domains.forEach((d) => { const cleanDomain = d.trim(); if (cleanDomain.endsWith("." + domain) && cleanDomain !== domain) { allDomains.add(cleanDomain); } }); } }); const subdomains = Array.from(allDomains).sort(); if (normalizedInput.options?.checkLive === true) { const liveSubdomains = []; const concurrentLimit = normalizedInput.options?.concurrentLimit || 5; const chunks = []; for (let i = 0; i < subdomains.length; i += concurrentLimit) { chunks.push(subdomains.slice(i, i + concurrentLimit)); } for (const chunk of chunks) { const promises = chunk.map((subdomain) => { return makeRequest(`https://${subdomain}`, { method: "HEAD", timeout: 5e3 // Short timeout for live checks }).then((resp) => { if (!resp.error) { liveSubdomains.push({ domain: subdomain, status: resp.status }); } }).catch(() => { }); }); await Promise.all(promises); } return { status: "success", scanner: "subdomains", data: { subdomains, total: subdomains.length, live: liveSubdomains }, timeTaken: Date.now() - startTime }; } return { status: "success", scanner: "subdomains", data: { subdomains, total: subdomains.length }, timeTaken: Date.now() - startTime }; } catch (error) { return { status: "failure", scanner: "subdomains", error: error.message || "Unknown error", data: { subdomains: [], total: 0 }, timeTaken: Date.now() - startTime }; } }; // src/modules/techStack.ts var TECH_PATTERNS = { // Front-end Frameworks "React": { patterns: ["react.js", "react-dom", "reactjs", '"react"', "_reactjs_", "react.production.min.js", "react.development.js"], category: "Web Frameworks", language: "JavaScript" }, "Vue.js": { patterns: ["vue.js", "vue@", "vue.min.js", "vue.runtime", "vue.common", "vue.esm"], category: "Web Frameworks", language: "JavaScript" }, "Angular": { patterns: ["angular.js", "ng-app", "ng-controller", "angular.min.js", "angular/core", "@angular"], category: "Web Frameworks", language: "JavaScript" }, "jQuery": { patterns: ["jquery.js", "jquery.min.js", "/jquery-", "jquery/jquery", "code.jquery"], category: "JavaScript Libraries", language: "JavaScript" }, "Bootstrap": { patterns: ["bootstrap.css", "bootstrap.min.css", "bootstrap.bundle", "bootstrap/dist", 'class="container"', 'class="row"', 'class="col-'], category: "Web Frameworks", language: "CSS" }, "Tailwind CSS": { patterns: [ "tailwind.css", "tailwindcss", "tailwind.min.css", 'class="tw-', 'class="bg-', 'class="text-', 'class="flex', "/tailwind/", "tailwind.config.js", "@tailwind base", "tailwindcss/dist" ], category: "CSS Frameworks", language: "CSS" }, "Nuxt.js": { patterns: [ "__NUXT__", "/_nuxt/", "<nuxt-link", '"nuxt":', "@nuxtjs", "Nuxt.js", "window.$nuxt", "nuxt.config.js", "/_nuxt/commons.", "data-n-head", '<div id="__nuxt"' ], category: "Web Frameworks", language: "JavaScript" }, "Next.js": { patterns: ["next.js", "__NEXT_DATA__", "/_next/", '"next":', "next/link"], category: "Web Frameworks", language: "JavaScript" }, // CMS "WordPress": { patterns: ["wp-content", "wp-includes", "wordpress", "wp-json"], category: "CMS", language: "PHP" }, "Drupal": { patterns: ["Drupal.settings", "/sites/default/files", "drupal.js"], category: "CMS", language: "PHP" }, "Joomla": { patterns: ["/administrator/index.php", "joomla", "com_content"], category: "CMS", language: "PHP" }, "Shopify": { patterns: ["Shopify.", "shopify", ".myshopify.com"], category: "Ecommerce", language: "Ruby" }, "Magento": { patterns: ["magento", "Mage.", "/skin/frontend/"], category: "Ecommerce", language: "PHP" }, "WooCommerce": { patterns: ["woocommerce", "wc-api", "wc_add_to_cart"], category: "Ecommerce", language: "PHP" }, // Back-end Technologies "Laravel": { patterns: [ // More specific Laravel patterns "laravel_session=", "XSRF-TOKEN", "X-XSRF-TOKEN", "Laravel Framework", "laravel.js", "/laravel/", "app/Http/Controllers", "Illuminate\\", "laravel.mix" ], category: "Web Frameworks", language: "PHP" }, "Express.js": { patterns: ["express", "express.js", "expressjs"], category: "Web Frameworks", language: "JavaScript" }, "Django": { patterns: ["django", "csrftoken", "csrfmiddlewaretoken"], category: "Web Frameworks", language: "Python" }, "Ruby on Rails": { patterns: ["rails", "ruby on rails", "csrf-token"], category: "Web Frameworks", language: "Ruby" }, "ASP.NET": { patterns: [ // More specific ASP.NET patterns "__VIEWSTATE", "__EVENTVALIDATION", ".aspx", ".ashx", ".asmx", "ASP.NET_SessionId", "X-AspNet-Version", "X-AspNetMvc-Version" ], category: "Web Frameworks", language: "C#" }, "Spring": { patterns: ["spring", "spring.js", "org.springframework"], category: "Web Frameworks", language: "Java" }, // Web Servers "Apache": { patterns: ["apache", "apache/"], category: "Web Servers", language: "" }, "Nginx": { patterns: ["nginx"], category: "Web Servers", language: "" }, "IIS": { patterns: ["iis", "microsoft-iis", "ms-iis"], category: "Web Servers", language: "" }, "Cloudflare": { patterns: ["cloudflare", "cf-ray", "__cfduid"], category: "CDN", language: "" }, "Litespeed": { patterns: ["litespeed"], category: "Web Servers", language: "" }, // Languages "PHP": { patterns: [ // More specific PHP patterns to reduce false positives "/index.php", "phpinfo()", "php_version", "PHPSESSID=", 'content="php"', "Powered by PHP", ".php?" ], category: "Programming Languages", language: "PHP" }, "Node.js": { patterns: ["node.js", "nodejs", "node_modules"], category: "Programming Languages", language: "JavaScript" }, "Python": { patterns: [ // More specific Python patterns "python-requests", "wsgi.py", "django.contrib", ".py?", "PYTHONPATH", 'content="python"', "Powered by Python" ], category: "Programming Languages", language: "Python" }, "Ruby": { patterns: ["ruby", ".rb", "ruby on"], category: "Programming Languages", language: "Ruby" }, "Java": { patterns: ["java", ".jsp", ".jar"], category: "Programming Languages", language: "Java" } }; function detectTechByPatterns(html, headers) { const technologies = []; const frameworks = /* @__PURE__ */ new Set(); const languages = /* @__PURE__ */ new Set(); const servers = /* @__PURE__ */ new Set(); const headersString = JSON.stringify(headers).toLowerCase(); const MIN_CONFIDENCE_THRESHOLD = 40; const STRONG_PATTERN_INDICATORS = [ "__VIEWSTATE", // ASP.NET "PHPSESSID", // PHP "laravel_session=", // Laravel "wp-content", // WordPress 'class="container"', // Bootstrap "/tailwind" // Tailwind ]; Object.entries(TECH_PATTERNS).forEach(([techName, techInfo]) => { let matchCount = 0; let strongMatchFound = false; let headerMatchFound = false; const htmlLower = html.toLowerCase(); for (const pattern of techInfo.patterns) { const patternLower = pattern.toLowerCase(); if (htmlLower.includes(patternLower)) { matchCount++; if (STRONG_PATTERN_INDICATORS.some((indicator) => patternLower.includes(indicator.toLowerCase()))) { strongMatchFound = true; matchCount += 2; } } if (headersString.includes(patternLower)) { headerMatchFound = true; matchCount += 2; } } let confidence = 0; if (matchCount > 0) { confidence = Math.min(100, matchCount * 20); if (headerMatchFound) { confidence = Math.min(100, confidence + 30); } if (strongMatchFound) { confidence = Math.min(100, confidence + 20); } if (confidence >= MIN_CONFIDENCE_THRESHOLD) { technologies.push({ name: techName, categories: [techInfo.category], confidence }); if (techInfo.category === "Web Frameworks") { frameworks.add(techName); } if (techInfo.category === "Web Servers") { servers.add(techName); } if (techInfo.language && confidence >= 60) { languages.add(techInfo.language); } } } }); if (headers["server"]) { const serverHeader = Array.isArray(headers["server"]) ? headers["server"][0] : headers["server"]; servers.add(serverHeader); if (!technologies.some((t) => t.name === serverHeader)) { technologies.push({ name: serverHeader, categories: ["Web Servers"], confidence: 100 }); } } if (headers["x-powered-by"]) { const poweredBy = Array.isArray(headers["x-powered-by"]) ? headers["x-powered-by"][0] : headers["x-powered-by"]; const poweredByParts = poweredBy.split(", "); poweredByParts.forEach((tech) => { if (!technologies.some((t) => t.name === tech)) { technologies.push({ name: tech, categories: ["Web Frameworks"], confidence: 90 // High but not absolute confidence }); } const techLower = tech.toLowerCase(); if (techLower.includes("php/") || techLower === "php") { languages.add("PHP"); const phpVersionMatch = techLower.match(/php\/([0-9.]+)/i); if (phpVersionMatch && !technologies.some((t) => t.name === "PHP")) { technologies.push({ name: "PHP", version: phpVersionMatch[1], categories: ["Programming Languages"], confidence: 95 }); } } if (techLower === "asp.net" || techLower.includes("asp.net/")) { frameworks.add("ASP.NET"); languages.add("C#"); const aspVersionMatch = techLower.match(/asp\.net[/\s]+([0-9.]+)/i); if (aspVersionMatch && !technologies.some((t) => t.name === "ASP.NET")) { technologies.push({ name: "ASP.NET", version: aspVersionMatch[1], categories: ["Web Frameworks"], confidence: 95 }); } } if (techLower.includes("express/")) frameworks.add("Express.js"); if (techLower.includes("node/")) languages.add("JavaScript"); if (techLower.includes("nuxt/")) frameworks.add("Nuxt.js"); }); } const generatorMatch = html.match(/<meta\s+name=["']generator["']\s+content=["']([^"']+)["']/i); if (generatorMatch && generatorMatch[1]) { const generator = generatorMatch[1]; if (!technologies.some((t) => t.name === generator)) { technologies.push({ name: generator, categories: ["CMS"], confidence: 100 }); } const generatorLower = generator.toLowerCase(); if (generatorLower.includes("wordpress")) { frameworks.add("WordPress"); languages.add("PHP"); } if (generatorLower.includes("drupal")) { frameworks.add("Drupal"); languages.add("PHP"); } if (generatorLower.includes("joomla")) { frameworks.add("Joomla"); languages.add("PHP"); } } return { technologies, frameworks: Array.from(frameworks), languages: Array.from(languages), servers: Array.from(servers) }; } function validateAndCleanResults(results) { results.technologies = results.technologies.filter((tech) => tech.confidence >= 50); const techMap = /* @__PURE__ */ new Map(); results.technologies.forEach((tech) => { const existingTech = techMap.get(tech.name); if (!existingTech || existingTech.confidence < tech.confidence) { techMap.set(tech.name, tech); } }); results.technologies = Array.from(techMap.values()); results.frameworks = [...new Set(results.frameworks)].filter( (framework) => results.technologies.some((tech) => tech.name === framework || // Handle Nuxt/Nuxt.js variation tech.name === "Nuxt.js" && framework === "Nuxt" || tech.name === "Nuxt" && framework === "Nuxt.js") ); const languagesWithTech = results.languages.filter((lang) => { return results.technologies.some((tech) => tech.categories.includes("Programming Languages") && tech.name === lang); }); const languagesFromFrameworks = results.technologies.filter((tech) => tech.categories.includes("Web Frameworks") && tech.confidence >= 75).map((tech) => { switch (tech.name) { case "ASP.NET": return "C#"; case "Laravel": return "PHP"; case "Django": return "Python"; case "Nuxt.js": case "Nuxt": case "Vue.js": case "React": case "Angular": return "JavaScript"; case "Ruby on Rails": return "Ruby"; case "Spring": return "Java"; default: return null; } }).filter(Boolean); results.languages = [.../* @__PURE__ */ new Set([...languagesWithTech, ...languagesFromFrameworks])]; results.servers = [...new Set(results.servers)].filter( (server) => results.technologies.some((tech) => tech.name === server || // Handle IIS/Microsoft-IIS variation tech.name.toLowerCase().includes("iis") && server.toLowerCase().includes("iis")) ); return results; } var detectTechStack = async (input) => { const startTime = Date.now(); const normalizedInput = createScannerInput(input); try { const Wappalyzer = { analyze: async () => [] }; let usingFallbackDetection = true; try { if (typeof window === "undefined") { try { const wappalyzerCore = require_wappalyzer(); if (wappalyzerCore && typeof wappalyzerCore.analyze === "function") { Object.assign(Wappalyzer, wappalyzerCore); usingFallbackDetection = false; } } catch (err) { usingFallbackDetection = true; } } else { usingFallbackDetection = true; } } catch (e) { usingFallbackDetection = true; } const mainPageRequest = makeRequest(normalizedInput.target, { method: "GET", timeout: normalizedInput.timeout, headers: normalizedInput.headers });