Ctrl K
ComunidadGuías…

__tests__/recon.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use strict';
import { describe, it, expect } from 'vitest';
const { buildAdvisory, calculateGrade } = require('../src/recon/advisory');
const { normalizeTarget, auditCookies, identifyWaf } = require('../src/recon/perimeter');
const roles = require('../src/roles');
const tools = require('../src/tools');

describe('Ghost Browser Recon Engine — advisory & 4-pillar models', () => {
  it('builds 4-pillar advisory with Traefik, Nginx, Express, and DNS remediations for CSP', () => {
    const adv = buildAdvisory('csp', { evidence: 'Header missing' });
    expect(adv.key).toBe('csp');
    expect(adv.severity).toBe('HIGH');
    expect(adv.cwe).toMatch(/CWE-1021/);
    expect(adv.remediation.traefik).toContain('contentSecurityPolicy');
    expect(adv.remediation.nginx).toContain('add_header Content-Security-Policy');
    expect(adv.remediation.express).toContain('helmet.contentSecurityPolicy');
    expect(adv.evidence).toBe('Header missing');
  });

  it('builds 4-pillar advisory for session invalidation (CWE-613)', () => {
    const adv = buildAdvisory('session_invalidation');
    expect(adv.cwe).toMatch(/CWE-613/);
    expect(adv.remediation.express).toContain('session_version');
    expect(adv.threatScenario).toMatch(/compromised account/i);
  });

  it('calculates security grade and score based on severity weights', () => {
    const findings = [
      { severity: 'HIGH' },
      { severity: 'MEDIUM' }
    ];
    const { score, grade } = calculateGrade(findings);
    expect(score).toBe(77); // 100 - 15 - 8 = 77
    expect(grade).toBe('B');
  });

  it('drops to Grade F when multiple critical and high findings exist', () => {
    const findings = [
      { severity: 'CRITICAL' },
      { severity: 'CRITICAL' },
      { severity: 'HIGH' },
      { severity: 'HIGH' }
    ];
    const { score, grade } = calculateGrade(findings);
    expect(score).toBe(10); // 100 - 60 - 30 = 10
    expect(grade).toBe('F');
  });
});

describe('Ghost Browser Recon Engine — perimeter normalization and analysis', () => {
  it('normalizes bare hostnames into full HTTPS target objects', () => {
    const target = normalizeTarget('example.org');
    expect(target.hostname).toBe('example.org');
    expect(target.url).toBe('https://example.org/');
    expect(target.protocol).toBe('https:');
  });

  it('does not invent domain posture, CVEs or crawl results for an unreachable IP', async () => {
    const { auditTarget } = require('../src/recon/perimeter');
    const result = await auditTarget('http://127.0.0.254/');
    expect(result.cves).toEqual([]);
    expect(result.crawlEndpoints).toEqual([]);
    expect(result.score).toBeNull();
    expect(result.exploitabilityScore).toBeNull();
    expect(result.assessmentCoverage.dns).toBe(false);
    expect(result.findings.map((finding) => finding.key)).not.toContain('spf');
    expect(result.findings.map((finding) => finding.key)).not.toContain('dmarc');
    expect(result.findings.map((finding) => finding.key)).not.toContain('csp');
    expect(result.findings.map((finding) => finding.key)).not.toContain('hsts');
  }, 15000);

  it('audits cookie security flags', () => {
    const rawCookies = [
      'session=abc1234; Path=/',
      '__Host-secure=xyz; Path=/; Secure; HttpOnly; SameSite=Strict'
    ];
    const issues = auditCookies(rawCookies);
    expect(issues.length).toBe(1);
    expect(issues[0].cookie).toBe('session');
    expect(issues[0].httpOnly).toBe(false);
    expect(issues[0].secure).toBe(false);
    expect(issues[0].sameSite).toBe(false);
  });

  it('identifies WAF and reverse proxies from headers', () => {
    const headers = {
      server: 'cloudflare',
      'cf-ray': '81234-AMS'
    };
    const waf = identifyWaf(headers);
    expect(waf.fingerprints.some((f) => f.name === 'Cloudflare')).toBe(true);
    expect(waf.originBypassRisk).not.toBeNull();
  });

  it('categorizes header and DNS advisories as POSTURE_HARDENING', () => {
    const cspAdv = buildAdvisory('csp', { evidence: 'Missing CSP' });
    const hstsAdv = buildAdvisory('hsts', { evidence: 'Missing HSTS' });
    const xfoAdv = buildAdvisory('clickjacking', { evidence: 'Missing XFO' });
    expect(cspAdv.category).toBe('POSTURE_HARDENING');
    expect(hstsAdv.category).toBe('POSTURE_HARDENING');
    expect(xfoAdv.category).toBe('POSTURE_HARDENING');
  });

  it('calculates dual scores preserving 100/100 Grade A+ exploitability when only hardening headers are missing', () => {
    const postureOnlyFindings = [
      buildAdvisory('csp', { evidence: 'Missing CSP' }),
      buildAdvisory('hsts', { evidence: 'Missing HSTS' }),
      buildAdvisory('clickjacking', { evidence: 'Missing XFO' }),
      buildAdvisory('content_sniffing', { evidence: 'Missing XCTO' }),
      buildAdvisory('spf', { evidence: 'Missing SPF' }),
      buildAdvisory('dmarc', { evidence: 'Missing DMARC' })
    ];
    const { postureScore, postureGrade, exploitabilityScore, exploitabilityGrade } = calculateGrade(postureOnlyFindings);
    expect(exploitabilityScore).toBe(100);
    expect(exploitabilityGrade).toBe('A+');
    expect(postureScore).toBeLessThan(100);
  });

  it('penalizes exploitability score when active confirmed vulnerabilities exist', () => {
    const exploitFindings = [
      buildAdvisory('session_invalidation', { severity: 'CRITICAL', category: 'EXPLOITABLE_VULNERABILITY' }),
      buildAdvisory('open_ports', { severity: 'HIGH', category: 'EXPLOITABLE_VULNERABILITY' })
    ];
    const { exploitabilityScore, exploitabilityGrade } = calculateGrade(exploitFindings);
    expect(exploitabilityScore).toBe(55); // 100 - 30 - 15 = 55
    expect(exploitabilityGrade).toBe('D');
  });
});

describe('Ghost Browser Tool and Role Registration', () => {
  it('registers recon_scan and session_invalidation_probe in tools registry', () => {
    expect(tools.has('recon_scan')).toBe(true);
    expect(tools.has('dom_security_audit')).toBe(true);
    expect(tools.has('session_invalidation_probe')).toBe(true);
  });

  it('registers appsec.auditor role with security tools', () => {
    const roleList = roles.list();
    const appsecRole = roleList.find((r) => r.name === 'appsec.auditor');
    expect(appsecRole).toBeDefined();
    expect(appsecRole.tools).toContain('recon_scan');
    expect(appsecRole.tools).toContain('session_invalidation_probe');
    expect(appsecRole.tools).toContain('dom_security_audit');
  });
});

describe('Ghost Browser 6 State-of-the-Art Recon Modules', () => {
  it('requires evidence-backed scoring and does not call banners confirmed RCE', () => {
    const { reconPrompt } = require('../src/operator/reconPrompt');
    const prompt = reconPrompt();
    expect(prompt).toContain('A banner is only a claimed version');
    expect(prompt).toContain('Show Unassessed');
    expect(prompt).not.toContain('OpenSSH is < 9.8p1');
  });
  const { extractApexDomain } = require('../src/recon/subdomainDiscovery');
  const { analyzeHomeSignatures, toBaseUrl } = require('../src/recon/techFingerprint');
  const { SENSITIVE_PROBES } = require('../src/recon/sensitiveFiles');
  const { correlateCves, KNOWN_CVE_DATABASE } = require('../src/recon/bannerGrabber');
  const { captureVisualProof } = require('../src/recon/visualProof');
  const { inspectTlsDetails } = require('../src/recon/tlsAuditor');

  it('Point 1: extracts clean apex domain from URLs, subdomains, and ports', () => {
    expect(extractApexDomain('https://mail.soluti.nl:8443/login')).toBe('mail.soluti.nl');
    expect(extractApexDomain('www.soluti.nl')).toBe('soluti.nl');
    expect(extractApexDomain('http://soluti.nl/')).toBe('soluti.nl');
  });

  it('Point 2: fingerprints LiteSpeed, WordPress, and PHP stack correctly', () => {
    expect(toBaseUrl('https://soluti.nl/wp-login.php')).toBe('https://soluti.nl');
    const headers = {
      server: 'LiteSpeed',
      'x-powered-by': 'PHP/8.1.28',
      'x-litespeed-cache': 'hit'
    };
    const html = '<html><head><meta name="generator" content="WordPress 6.4.3" /></head><body><link rel="stylesheet" href="/wp-content/themes/custom/style.css"></body></html>';
    const techs = analyzeHomeSignatures(headers, html);
    expect(techs.some((t) => t.name === 'LiteSpeed Web Server')).toBe(true);
    expect(techs.some((t) => t.name === 'WordPress' && t.version === '6.4.3')).toBe(true);
    expect(techs.some((t) => t.name === 'PHP Runtime')).toBe(true);
    expect(techs.some((t) => t.name === 'LiteSpeed Cache')).toBe(true);
  });

  it('Point 3: sensitive files validator strictly rejects false-positive HTML 404 responses', () => {
    const gitProbe = SENSITIVE_PROBES.find((p) => p.path === '/.git/HEAD');
    expect(gitProbe).toBeDefined();
    // SPA returning 200 OK with HTML should be rejected
    expect(gitProbe.validator(200, '<!DOCTYPE html><html><body>Page Not Found</body></html>', { 'content-type': 'text/html' })).toBe(false);
    // Real git repository HEAD file should be accepted
    expect(gitProbe.validator(200, 'ref: refs/heads/main\n', { 'content-type': 'text/plain' })).toBe(true);

    const envProbe = SENSITIVE_PROBES.find((p) => p.path === '/.env');
    expect(envProbe.validator(200, '<!DOCTYPE html><html>...</html>', { 'content-type': 'text/html' })).toBe(false);
    expect(envProbe.validator(200, 'DB_PASSWORD=supersecret\nAPP_KEY=base64:123\n', { 'content-type': 'text/plain' })).toBe(true);
  });

  it('Point 4: correlates service daemon banners with official CVE database entries', () => {
    const banners = [
      { port: 22, service: 'SSH', banner: 'SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1' },
      { port: 21, service: 'FTP', banner: '220 ProFTPD 1.3.5 Server' },
      { port: 3306, service: 'MySQL/MariaDB', banner: 'MariaDB/MySQL Handshake Version: 10.6.15-MariaDB' }
    ];
    const cves = correlateCves(banners);
    expect(cves.length).toBeGreaterThanOrEqual(3);
    expect(cves.some((c) => c.cve.includes('CVE-2024-6387'))).toBe(true);
    expect(cves.some((c) => c.cve.includes('CVE-2019-12815'))).toBe(true);
    expect(cves.some((c) => c.cve.includes('CVE-2022-32082'))).toBe(true);
    expect(cves.find((c) => c.cve.includes('CVE-2024-6387')).status).toBe('version_range_candidate');
  });

  it('does not label OpenSSH 6.6.1p1 or patched versions as regreSSHion', () => {
    const banners = ['6.6.1p1', '8.4p1', '9.8p1', '10.0p2'].map((version) =>
      ({ port: 22, service: 'SSH', banner: `SSH-2.0-OpenSSH_${version}` }));
    expect(correlateCves(banners).filter((candidate) => candidate.cve.includes('CVE-2024-6387'))).toEqual([]);
    expect(correlateCves([{ port: 22, service: 'SSH', banner: 'SSH-2.0-OpenSSH_9.7p1' }])
      .some((candidate) => candidate.cve.includes('CVE-2024-6387'))).toBe(true);
  });

  it('Point 5: captures visual proof or safely falls back without crashing', async () => {
    const result = await captureVisualProof('https://example.com');
    expect(result).toBeDefined();
    expect(result.targetUrl).toBe('https://example.com');
    expect(result.finding).toBeDefined();
    expect(result.markdownEmbed).toBeDefined();
  });

  it('Point 6: exports TLS auditor functions and handles invalid host cleanly', async () => {
    const { auditTlsHealth } = require('../src/recon/tlsAuditor');
    const res = await auditTlsHealth('');
    expect(res.error).toBeDefined();
  });

  it('grabMysqlBanner returns null banner and does not false-flag closed ports', async () => {
    const { grabMysqlBanner } = require('../src/recon/bannerGrabber');
    const res = await grabMysqlBanner('127.0.0.1', 19876, 300);
    expect(res.version).toBeNull();
    expect(res.banner).toBeNull();
  });

  it('auditPassiveSubdomains isolates candidates to verified cert names when wildcard DNS is detected', async () => {
    const { auditPassiveSubdomains } = require('../src/recon/subdomainDiscovery');
    // Calling with empty or local domain doesn't crash and returns structured report
    const res = await auditPassiveSubdomains('127.0.0.1');
    expect(res).toBeDefined();
    expect(res.domain).toBe('127.0.0.1');
    expect(Array.isArray(res.findings)).toBe(true);
  });

  it('auditBusinessLogic detects negative quantity and client-side price tampering', async () => {
    const http = require('http');
    const { auditBusinessLogic, generateBusinessLogicPatch } = require('../src/recon/businessLogicAuditor');

    // Spin up an in-memory test server simulating a vulnerable vibe-coded checkout API
    const server = http.createServer((req, res) => {
      let body = '';
      req.on('data', chunk => { body += chunk; });
      req.on('end', () => {
        try {
          const data = JSON.parse(body);
          // Vulnerable logic: accepts negative quantities and client prices
          if (data.quantity === -1 || (data.items && data.items.some(i => i.quantity === -1))) {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            return res.end(JSON.stringify({ success: true, orderId: 1337, total: -10 }));
          }
          if (data.price === 0.01 || data.amount === 1) {
            res.writeHead(200, { 'Content-Type': 'application/json' });
            return res.end(JSON.stringify({ success: true, orderId: 1338, clientPriceAccepted: true, total: 0.01 }));
          }
        } catch (e) {}
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'invalid' }));
      });
    });

    await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
    const port = server.address().port;
    const testBaseUrl = `http://127.0.0.1:${port}`;

    try {
      const result = await auditBusinessLogic(testBaseUrl, {
        endpoints: ['/api/checkout']
      });

      expect(result).toBeDefined();
      expect(result.clean).toBe(false);
      expect(result.findings.length).toBeGreaterThanOrEqual(2);

      const negFinding = result.findings.find(f => f.key === 'negative_quantity_underflow');
      expect(negFinding).toBeDefined();
      expect(negFinding.severity).toBe('CRITICAL');
      expect(negFinding.cwe).toContain('CWE-840');
      expect(negFinding.pocCurl).toContain('-1');

      const priceFinding = result.findings.find(f => f.key === 'client_price_tampering');
      expect(priceFinding).toBeDefined();
      expect(priceFinding.severity).toBe('CRITICAL');
      expect(priceFinding.cwe).toContain('CWE-472');

      const patch = generateBusinessLogicPatch('negative_quantity_underflow');
      expect(patch).toContain('validateOrderQuantities');
    } finally {
      await new Promise(resolve => server.close(resolve));
    }
  });

  describe('Living App Map & Target Workspace Engine', () => {
    const {
      getTargetWorkspace,
      saveTargetWorkspace,
      ingestAuditData,
      listTargetWorkspaces,
      normalizeDomain
    } = require('../src/recon/targetWorkspace');

    it('normalizes target URLs into clean domain keys', () => {
      expect(normalizeDomain('https://app.example.com/login?ref=1')).toBe('app.example.com');
      expect(normalizeDomain('http://sub.domain.org:8080/')).toBe('sub.domain.org');
      expect(normalizeDomain('target.io')).toBe('target.io');
    });

    it('scaffolds a fresh target workspace dossier', () => {
      const ws = getTargetWorkspace('https://unit-test-app.com');
      expect(ws).toBeDefined();
      expect(ws.apexDomain).toBe('unit-test-app.com');
      expect(ws.scores.postureGrade).toBe('A+');
      expect(Array.isArray(ws.subdomains)).toBe(true);
      expect(Array.isArray(ws.spaRoutes)).toBe(true);
      expect(Array.isArray(ws.findings)).toBe(true);
      expect(Array.isArray(ws.history)).toBe(true);
    });

    it('ingests multi-phase audit findings, ports, routes, and updates attack surface map', () => {
      const target = 'https://unit-test-app.com';
      const ingested = ingestAuditData(target, {
        perimeter: { apexIp: '1.2.3.4', tlsGrade: 'A' },
        wildcardDns: false,
        liveSubdomains: ['api.unit-test-app.com', 'admin.unit-test-app.com'],
        ports: [{ port: 443, service: 'https', banner: 'nginx/1.24' }],
        spaRoutes: ['/dashboard', '/settings/billing'],
        apiEndpoints: ['/api/v1/auth/me', '/api/v1/checkout'],
        findings: [
          { key: 'test_cors', severity: 'HIGH', title: 'Permissive CORS' },
          { key: 'test_cve', severity: 'CRITICAL', title: 'RCE in Webpack Dev Server' }
        ]
      });

      expect(ingested.domain).toBe('unit-test-app.com');
      expect(ingested.perimeter.apexIp).toBe('1.2.3.4');
      expect(ingested.subdomains.length).toBe(2);
      expect(ingested.services.length).toBe(1);
      expect(ingested.services[0].port).toBe(443);
      expect(ingested.spaRoutes).toContain('/dashboard');
      expect(ingested.apiEndpoints).toContain('/api/v1/checkout');
      expect(ingested.findings.length).toBe(2);
      expect(ingested.history.length).toBeGreaterThanOrEqual(1);
      // Exploitability score should drop due to CRITICAL and HIGH findings
      expect(ingested.scores.exploitabilityScore).toBeLessThan(100);
      expect(['F', 'D', 'C']).toContain(ingested.scores.exploitabilityGrade);

      // Verify listTargetWorkspaces includes this domain
      const list = listTargetWorkspaces();
      expect(list.some(item => item.domain === 'unit-test-app.com')).toBe(true);
    });
  });
});