websec-audit
Version:
A universal security scanning and audit tool for websites
1,583 lines (1,576 loc) • 132 kB
JavaScript
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
});