ulert
Version:
Open source website guardian — audit uptime, broken links, and security headers from the command line.
530 lines (464 loc) • 18.6 kB
JavaScript
// /labs/cosmos/fingerprint.js
const dns = require('dns').promises;
const axios = require('axios');
const tls = require('tls');
const crypto = require('crypto');
const cheerio = require('cheerio');
const whois = require('whois-json');
const dnsPromises = require('dns').promises;
const sslChecker = require('ssl-checker');
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
// Limpieza única del dominio para todo el módulo
function cleanDomain(domain) {
return domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
}
module.exports.run = async (domain, osintData) => {
domain = cleanDomain(domain);
console.log('🧠 Ejecutando fingerprinting para:', domain);
console.log('--------------------------------------------');
let ip = null;
try {
console.log('🌐 Resolviendo IP...');
ip = (await dns.lookup(domain)).address;
console.log(`✔️ Dirección IP encontrada: ${ip}`);
} catch (err) {
console.error(`❌ No se pudo resolver el dominio ${domain}: ${err.code}`);
}
console.log('\n🌍 Buscando ASN...');
const asn = await getASN(ip);
if (asn) console.log('✔️ ASN:', asn.trim());
else console.log('⚠️ No se pudo obtener ASN.');
console.log('\n🔐 Obteniendo certificado TLS...');
const cert = await getCert(domain);
if (cert) {
console.log('✔️ Certificado obtenido:');
console.log(` CN: ${cert.subject.CN}`);
console.log(` Emitido por: ${cert.issuer.CN}`);
console.log(` Vigencia: ${cert.valid_from} - ${cert.valid_to}`);
console.log(` Fingerprint: ${cert.fingerprint}`);
} else {
console.log('⚠️ No se pudo obtener el certificado.');
}
console.log('\n🌐 Obteniendo registros DNS...');
const dnsRecords = await getDNSRecords(domain);
if (dnsRecords) {
console.log('✔️ Registros DNS obtenidos:');
Object.entries(dnsRecords).forEach(([type, records]) => {
console.log(` ${type}: ${records.length}`);
});
} else {
console.log('⚠️ No se pudieron obtener registros DNS.');
}
console.log('\n📊 Buscando ID de Google Analytics...');
const ga = await googleAnalyticsId(domain);
if (ga) console.log(`✔️ Google Analytics ID encontrado: ${ga}`);
else console.log('⚠️ No se encontró Google Analytics ID.');
console.log('\n🧬 Analizando HTML para fingerprint...');
const htmlSig = await htmlFingerprint(domain);
if (htmlSig) {
console.log('✔️ HTML fingerprint generado');
console.log(` 🔖 Título: ${htmlSig.title || 'N/A'}`);
console.log(` 📄 Descripción: ${htmlSig.description || 'N/A'}`);
console.log(` 🔐 Cookies: ${htmlSig.cookies.length}`);
console.log(` 💠 Hash SHA-256: ${htmlSig.hash}`);
console.log(` 📄 Tamaño HTML: ${htmlSig.html.length} bytes`);
if (htmlSig.technologies.length > 0) {
console.log(' 🛠️ Tecnologías detectadas:');
htmlSig.technologies.forEach(technologies => {
const tecName = technologies.split('=')[0];
console.log(` - ${tecName}`);
});
}
// Recursos externos
console.log(` 📦 Recursos externos: scripts(${htmlSig.scripts.length}), css(${htmlSig.styles.length}), imgs(${htmlSig.images.length})`);
if (htmlSig.cookies.length > 0) {
console.log(' 🍪 Cookies detalladas:');
htmlSig.cookies.forEach(cookie => {
const name = cookie.split('=')[0];
console.log(` - ${name}`);
});
}
// Encabezados HTTP relevantes
console.log(' 📰 Encabezados HTTP relevantes:');
const relevantHeaders = ['server', 'x-powered-by', 'strict-transport-security', 'content-security-policy', 'x-frame-options'];
relevantHeaders.forEach(hdr => {
if (htmlSig.headers[hdr]) {
console.log(` - ${hdr}: ${htmlSig.headers[hdr]}`);
}
});
} else {
console.log('⚠️ No se pudo generar fingerprint HTML.');
}
console.log('\n🔍 Verificando getwhois...');
const whoisData = await getWhois(domain);
if (whoisData) {
console.log('✔️ WHOIS data:');
console.log(` Registrant: ${whoisData.registrant || 'N/A'}`);
console.log(` Registrar: ${whoisData.registrar || 'N/A'}`);
console.log(` Creation Date: ${whoisData.creationDate || 'N/A'}`);
console.log(` Expiry Date: ${whoisData.expiryDate || 'N/A'}`);
console.log(` Source: ${whoisData.source || 'N/A'}`);
console.log(` Timestamp: ${whoisData.timestamp || 'N/A'}`);
} else {
console.log('⚠️ No se pudo obtener información WHOIS.');
}
console.log('\n🔗 Verificando puntaje subdominios...');
const subdomainTakeovers = await Promise.all(
osintData.subdomains.map(subdomain => checkSubdomainTakeover(subdomain))
);
subdomainTakeovers.forEach(result => {
if (result.vulnerable) {
console.log(`⚠️ Subdominio vulnerable: ${result.subdomain} (CNAME: ${result.cname})`);
} else {
console.log(`✔️ Subdominio seguro: ${result.subdomain}`);
}
});
console.log('\n🔐 Verificando encabezados de seguridad...');
const securityHeaders = await checkSecurityHeaders(`https://${domain}`);
console.log('✔️ Encabezados de seguridad:');
console.log(` CSP: ${securityHeaders.CSP}`);
console.log(` HSTS: ${securityHeaders.HSTS}`);
console.log(` X-Frame-Options: ${securityHeaders.XFrameOptions}`);
console.log(` X-Content-Type-Options: ${securityHeaders.XContentTypeOptions}`);
console.log(` Source: ${securityHeaders.source}`);
console.log('\n📧 Verificando autenticación de correo electrónico...');
const emailAuth = await checkEmailAuth(domain);
console.log('✔️ Autenticación de correo electrónico:');
console.log(` DMARC: ${emailAuth.DMARC}`);
console.log(` SPF: ${emailAuth.SPF}`);
console.log(` DKIM: ${emailAuth.DKIM}`);
console.log(` Source: ${emailAuth.source}`);
console.log(` Timestamp: ${emailAuth.timestamp}`);
console.log('\n📸 Tomando captura de pantalla...');
const screenshot = await takeScreenshot(`https://${domain}`, `screenshot-${domain}.png`);
if (screenshot) {
console.log(`✔️ Captura de pantalla guardada: ${screenshot.file}`);
} else {
console.log('⚠️ No se pudo tomar captura de pantalla.');
}
console.log('\n✅ Fingerprinting finalizado.\n');
return { ip, asn, cert, ga, htmlSig, dnsRecords, whoisData, subdomainTakeovers, securityHeaders, emailAuth, screenshot };
};
// === Funciones auxiliares ===
async function getASN(ip) {
if (!ip) {
console.warn('⚠️ IP no disponible para búsqueda ASN.');
return null;
}
// 1. Intentar hackertarget
try {
const res = await axios.get(`https://api.hackertarget.com/aslookup/?q=${ip}`);
if (
typeof res.data === 'string' &&
res.data.includes('API count exceeded')
) {
console.warn('⚠️ Límite de hackertarget.com alcanzado, probando ipinfo.io...');
// 2. Intentar ipinfo.io
try {
const infoRes = await axios.get(`https://ipinfo.io/${ip}/json`);
if (infoRes.data && infoRes.data.org) {
return infoRes.data.org; // Ejemplo: "AS12345 ExampleOrg"
} else {
// 3. Intentar ip-api.com
console.warn('⚠️ ipinfo.io no devolvió ASN, probando ip-api.com...');
try {
const apiRes = await axios.get(`http://ip-api.com/json/${ip}`);
if (apiRes.data && apiRes.data.as) {
return apiRes.data.as; // Ejemplo: "AS12345 ExampleOrg"
}
return null;
} catch (e3) {
console.error('❌ Error obteniendo ASN en ip-api.com:', e3.message);
return null;
}
}
} catch (e2) {
console.error('❌ Error obteniendo ASN en ipinfo.io:', e2.message);
// 3. Intentar ip-api.com si ipinfo.io falla
try {
const apiRes = await axios.get(`http://ip-api.com/json/${ip}`);
if (apiRes.data && apiRes.data.as) {
return apiRes.data.as;
}
return null;
} catch (e3) {
console.error('❌ Error obteniendo ASN en ip-api.com:', e3.message);
return null;
}
}
}
return res.data;
} catch (e) {
console.error('❌ Error obteniendo ASN:', e.message);
// Si hackertarget falla, intenta ipinfo.io y luego ip-api.com
try {
const infoRes = await axios.get(`https://ipinfo.io/${ip}/json`);
if (infoRes.data && infoRes.data.org) {
return infoRes.data.org;
} else {
try {
const apiRes = await axios.get(`http://ip-api.com/json/${ip}`);
if (apiRes.data && apiRes.data.as) {
return apiRes.data.as;
}
return null;
} catch (e3) {
console.error('❌ Error obteniendo ASN en ip-api.com:', e3.message);
return null;
}
}
} catch (e2) {
console.error('❌ Error obteniendo ASN en ipinfo.io:', e2.message);
try {
const apiRes = await axios.get(`http://ip-api.com/json/${ip}`);
if (apiRes.data && apiRes.data.as) {
return apiRes.data.as;
}
return null;
} catch (e3) {
console.error('❌ Error obteniendo ASN en ip-api.com:', e3.message);
return null;
}
}
}
}
async function getCert(domain) {
domain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
return new Promise((resolve) => {
const socket = tls.connect(443, domain, { servername: domain, timeout: 7000 }, () => {
const cert = socket.getPeerCertificate();
socket.end();
if (cert && cert.subject) {
resolve({
subject: cert.subject,
issuer: cert.issuer,
valid_from: cert.valid_from,
valid_to: cert.valid_to,
fingerprint: cert.fingerprint,
});
} else {
resolve(null);
}
});
socket.on('error', (err) => {
console.warn(`⚠️ Error TLS para ${domain}: ${err.message}`);
resolve(null);
});
socket.on('timeout', () => {
console.warn(`⚠️ Timeout TLS para ${domain}`);
socket.destroy();
resolve(null);
});
});
}
async function googleAnalyticsId(domain) {
domain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
try {
const res = await axios.get(`https://${domain}`, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
'Connection': 'keep-alive',
// Puedes agregar más headers si lo deseas
}
});
const html = res.data;
// Buscar UA-XXXXXX-X (Universal Analytics)
const uaMatch = html.match(/UA-\d{4,10}-\d{1,4}/i);
// Buscar G-XXXXXXXXXX (GA4)
const gMatch = html.match(/G-[A-Z0-9]{8,12}/i);
// Buscar GTM-XXXXXXX (Google Tag Manager)
const gtmMatch = html.match(/GTM-[A-Z0-9]{6,8}/i);
// Devolver todos los IDs encontrados, separados por coma
const ids = [uaMatch, gMatch, gtmMatch].filter(Boolean).map(m => m[0]);
return ids.length > 0 ? ids.join(', ') : null;
} catch (error) {
console.error(`❌ Error obteniendo GA ID para ${domain}:`, error.code || error.message);
return null;
}
}
async function htmlFingerprint(domain) {
domain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
try {
const res = await axios.get(`https://${domain}`, { timeout: 10000 });
const html = res.data.slice(0, 10000);
const $ = cheerio.load(html);
// Extraer recursos externos
const scripts = $('script[src]').map((i, el) => $(el).attr('src')).get();
const styles = $('link[rel="stylesheet"]').map((i, el) => $(el).attr('href')).get();
const images = $('img[src]').map((i, el) => $(el).attr('src')).get();
const title = $('title').text();
const description = $('meta[name="description"]').attr('content') || '';
const hash = crypto.createHash('sha256').update(html).digest('hex');
const cookies = res.headers['set-cookie'] || [];
// ✅ Esperar la detección de tecnologías
const techs = await detectTechnologiesFromHTML($, scripts, styles, html);
return {
html,
hash,
headers: res.headers,
title,
description,
cookies,
technologies: techs,
scripts,
styles,
images
};
} catch (error) {
console.error(`❌ Error generando HTML fingerprint:`, error.code || error.message);
return null;
}
}
async function getDNSRecords(domain) {
const resolver = dns;
const types = ['MX', 'TXT', 'NS', 'CNAME'];
const records = {};
try {
for (const type of types) {
try {
const res = await resolver.resolve(domain, type);
records[type] = res;
} catch (e) {
records[type] = [];
}
}
return records;
} catch {
return null;
}
}
async function detectTechnologiesFromHTML($, scripts, styles, html) {
const techs = new Set();
const lowerHTML = html.toLowerCase();
// === Heurísticas por scripts ===
scripts.forEach(src => {
if (!src) return;
const lowerSrc = src.toLowerCase();
if (lowerSrc.includes('jquery')) techs.add('jQuery');
if (lowerSrc.includes('react')) techs.add('React');
if (lowerSrc.includes('vue')) techs.add('Vue.js');
if (lowerSrc.includes('angular')) techs.add('Angular');
if (lowerSrc.includes('bootstrap')) techs.add('Bootstrap');
if (lowerSrc.includes('tailwind')) techs.add('Tailwind CSS');
if (lowerSrc.includes('google-analytics')) techs.add('Google Analytics');
if (lowerSrc.includes('gtm.js')) techs.add('Google Tag Manager');
if (lowerSrc.includes('hotjar')) techs.add('Hotjar');
});
// === Heurísticas por estilos ===
styles.forEach(href => {
if (!href) return;
const lowerHref = href.toLowerCase();
if (lowerHref.includes('bootstrap')) techs.add('Bootstrap');
if (lowerHref.includes('tailwind')) techs.add('Tailwind CSS');
if (lowerHref.includes('font-awesome')) techs.add('Font Awesome');
});
// === Meta generator (WordPress, Drupal, Joomla...) ===
const generator = ($('meta[name="generator"]').attr('content') || '').toLowerCase();
if (generator.includes('wordpress')) techs.add('WordPress');
if (generator.includes('drupal')) techs.add('Drupal');
if (generator.includes('joomla')) techs.add('Joomla');
// === Patrones en el HTML ===
if (lowerHTML.includes('wp-content')) techs.add('WordPress');
if (lowerHTML.includes('woocommerce')) techs.add('WooCommerce');
if (lowerHTML.includes('wp-admin')) techs.add('WordPress');
if (lowerHTML.includes('shopify')) techs.add('Shopify');
if (lowerHTML.includes('magento')) techs.add('Magento');
if (lowerHTML.includes('angular')) techs.add('Angular');
if (lowerHTML.includes('gatsby')) techs.add('Gatsby');
if (lowerHTML.includes('next.js')) techs.add('Next.js');
if (lowerHTML.includes('nuxt.js')) techs.add('Nuxt.js');
// === Detección por comentarios HTML ===
const commentMatches = html.match(/<!--(.*?)-->/gs) || [];
commentMatches.forEach(comment => {
const lowerComment = comment.toLowerCase();
if (lowerComment.includes('wordpress')) techs.add('WordPress');
if (lowerComment.includes('drupal')) techs.add('Drupal');
if (lowerComment.includes('joomla')) techs.add('Joomla');
});
const techArray = Array.from(techs);
return techArray;
}
async function getWhois(domain) {
try {
const data = await whois(domain);
return {
registrant: data.registrant || data['Registrant Name'] || null,
registrar: data.registrar || null,
creationDate: data.creationDate || null,
expiryDate: data.registryExpiryDate || null,
source: 'WHOIS',
timestamp: new Date().toISOString(),
confidence: 'high'
};
} catch (err) {
return { error: 'WHOIS lookup failed', domain };
}
}
async function checkSubdomainTakeover(subdomain) {
try {
const cname = await dns.resolveCname(subdomain).catch(() => null);
if (cname) {
const res = await axios.get(`http://${subdomain}`, { timeout: 5000 }).catch(e => e.response || {});
const vulnerablePatterns = [
'There isn’t a GitHub Pages site here',
'NoSuchBucket',
'heroku | no such app'
];
const vulnerable = vulnerablePatterns.some(p =>
(res.data || '').toLowerCase().includes(p.toLowerCase())
);
return { subdomain, cname, vulnerable, source: 'HTTP+CNAME', timestamp: new Date().toISOString() };
}
} catch {}
return { subdomain, vulnerable: false };
}
async function checkSecurityHeaders(url) {
const res = await axios.get(url, { timeout: 5000 });
const headers = res.headers;
return {
CSP: headers['content-security-policy'] ? 'OK' : 'MISSING',
HSTS: headers['strict-transport-security'] ? 'OK' : 'MISSING',
XFrameOptions: headers['x-frame-options'] || 'MISSING',
XContentTypeOptions: headers['x-content-type-options'] || 'MISSING',
source: 'HTTP Headers',
timestamp: new Date().toISOString()
};
}
async function checkEmailAuth(domain) {
const dmarc = await dnsPromises.resolveTxt(`_dmarc.${domain}`).catch(() => []);
const spf = await dnsPromises.resolveTxt(domain).catch(() => []);
const dkim = await dnsPromises.resolveTxt(`default._domainkey.${domain}`).catch(() => []);
return {
DMARC: dmarc.length ? dmarc.flat().join(' ') : 'MISSING',
SPF: spf.length ? spf.flat().join(' ') : 'MISSING',
DKIM: dkim.length ? dkim.flat().join(' ') : 'MISSING',
source: 'DNS TXT',
timestamp: new Date().toISOString()
};
}
const OUTPUT_DIR = path.join(__dirname, 'output');
function ensureOutputDir() {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
}
async function takeScreenshot(url, filename) {
ensureOutputDir(); // Nos aseguramos de que exista la carpeta
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
const filePath = path.join(OUTPUT_DIR, filename); // ruta completa
await page.screenshot({ path: filePath, fullPage: true });
await browser.close();
return {
url,
file: filePath,
source: 'Screenshot',
timestamp: new Date().toISOString()
};
}