zephex 2.0.11 → 2.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -148,6 +148,25 @@ function filePolyfill(path) {
148
148
  text: async () => readFile(path, "utf8")
149
149
  };
150
150
  }
151
+
152
+ class GlobPolyfill {
153
+ pattern;
154
+ constructor(pattern) {
155
+ this.pattern = pattern;
156
+ }
157
+ async* scan(opts) {
158
+ const { glob } = await import("node:fs/promises");
159
+ const cwd = opts?.cwd ?? process.cwd();
160
+ for await (const entry of glob(this.pattern, { cwd })) {
161
+ if (opts?.absolute) {
162
+ const { resolve } = await import("node:path");
163
+ yield resolve(cwd, entry);
164
+ } else {
165
+ yield entry;
166
+ }
167
+ }
168
+ }
169
+ }
151
170
  function ensureBunPolyfill() {
152
171
  const g = globalThis;
153
172
  if (typeof g.Bun !== "undefined")
@@ -155,7 +174,8 @@ function ensureBunPolyfill() {
155
174
  g.Bun = {
156
175
  file: filePolyfill,
157
176
  spawn: spawnPolyfill,
158
- JSONL: { parse: jsonlParsePolyfill }
177
+ JSONL: { parse: jsonlParsePolyfill },
178
+ Glob: GlobPolyfill
159
179
  };
160
180
  }
161
181
  var init_bun_polyfill = __esm(() => {
@@ -403,19 +423,16 @@ function isPrivateIpv4(ip) {
403
423
  }
404
424
  function isPrivateIpv6(ip) {
405
425
  const lower = ip.toLowerCase();
406
- if (lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80")) {
426
+ if (lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80"))
407
427
  return true;
408
- }
409
428
  const v4MappedDotted = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
410
- if (v4MappedDotted?.[1]) {
429
+ if (v4MappedDotted?.[1])
411
430
  return isPrivateIpv4(v4MappedDotted[1]);
412
- }
413
431
  const v4MappedHex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
414
432
  if (v4MappedHex?.[1] && v4MappedHex[2]) {
415
433
  const hi = parseInt(v4MappedHex[1], 16);
416
434
  const lo = parseInt(v4MappedHex[2], 16);
417
- const dottedQuad = `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
418
- return isPrivateIpv4(dottedQuad);
435
+ return isPrivateIpv4(`${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`);
419
436
  }
420
437
  return false;
421
438
  }
@@ -436,25 +453,25 @@ async function resolveAndValidateIp(hostname) {
436
453
  throw new AuditHeadersError(`No IPv4 address found for ${hostname}`, -32603, "The hostname may not have a public IPv4 record");
437
454
  }
438
455
  const ip = addresses[0];
439
- if (isPrivateIpv4(ip)) {
456
+ if (isPrivateIpv4(ip))
440
457
  throw new AuditHeadersError(`Blocked: ${hostname} resolves to a private/reserved IP address`, -32602, "Only public HTTPS endpoints are supported");
441
- }
442
- if (isPrivateIpv6(ip)) {
458
+ if (isPrivateIpv6(ip))
443
459
  throw new AuditHeadersError(`Blocked: ${hostname} resolves to a private IPv6 address`, -32602, "Only public HTTPS endpoints are supported");
444
- }
445
460
  return ip;
446
461
  }
447
- function makeRawRequest(resolvedIp, originalHostname, port, path, method, isHttps) {
462
+ function makeRawRequest(resolvedIp, originalHostname, port, path, method, isHttps, extraHeaders) {
448
463
  return new Promise((resolve, reject) => {
464
+ const reqHeaders = {
465
+ Host: originalHostname,
466
+ "User-Agent": "audit-headers-mcp/1.0",
467
+ ...extraHeaders
468
+ };
449
469
  const options = {
450
470
  hostname: resolvedIp,
451
471
  port,
452
472
  path: path || "/",
453
473
  method,
454
- headers: {
455
- Host: originalHostname,
456
- "User-Agent": "audit-headers-mcp/1.0"
457
- },
474
+ headers: reqHeaders,
458
475
  servername: originalHostname,
459
476
  rejectUnauthorized: true,
460
477
  timeout: 1e4
@@ -470,12 +487,7 @@ function makeRawRequest(resolvedIp, originalHostname, port, path, method, isHttp
470
487
  const h = res.headers;
471
488
  const locationRaw = h["location"];
472
489
  const location = Array.isArray(locationRaw) ? locationRaw[0] ?? null : locationRaw ?? null;
473
- resolve({
474
- statusCode: res.statusCode ?? 0,
475
- headers: h,
476
- httpVersion: res.httpVersion,
477
- location
478
- });
490
+ resolve({ statusCode: res.statusCode ?? 0, headers: h, httpVersion: res.httpVersion, location });
479
491
  });
480
492
  res.on("error", reject);
481
493
  });
@@ -493,9 +505,8 @@ async function fetchWithPinnedDns(startUrl) {
493
505
  let httpVersion = "1.1";
494
506
  let statusCode = 0;
495
507
  for (let hop = 0;hop <= 5; hop++) {
496
- if (hop === 5) {
508
+ if (hop === 5)
497
509
  throw new AuditHeadersError("Too many redirects (max 5)", -32603);
498
- }
499
510
  const parsed = new URL2(currentUrl);
500
511
  const hostname = parsed.hostname;
501
512
  const isHttps = parsed.protocol === "https:";
@@ -507,11 +518,7 @@ async function fetchWithPinnedDns(startUrl) {
507
518
  httpVersion = response.httpVersion;
508
519
  if (response.statusCode >= 300 && response.statusCode < 400 && response.location) {
509
520
  const nextUrl = new URL2(response.location, currentUrl).href;
510
- redirectChain.push({
511
- url: currentUrl,
512
- status: response.statusCode,
513
- location: response.location
514
- });
521
+ redirectChain.push({ url: currentUrl, status: response.statusCode, location: response.location });
515
522
  currentUrl = nextUrl;
516
523
  } else {
517
524
  break;
@@ -532,107 +539,65 @@ function safeStr(s, max = 200) {
532
539
  async function inspectSsl(hostname, resolvedIp) {
533
540
  return new Promise((resolve) => {
534
541
  try {
535
- const tlsOptions = {
542
+ const socket = tls.connect({
536
543
  host: resolvedIp,
537
544
  port: 443,
538
545
  servername: hostname,
539
- rejectUnauthorized: true
540
- };
541
- const socket = tls.connect(tlsOptions, () => {
546
+ rejectUnauthorized: true,
547
+ ALPNProtocols: ["h2", "http/1.1"]
548
+ }, () => {
542
549
  try {
543
550
  const cert = socket.getPeerCertificate();
544
551
  const protocol = socket.getProtocol() ?? null;
552
+ const alpn = socket.alpnProtocol;
545
553
  socket.destroy();
546
554
  if (!cert || !cert.valid_to) {
547
- resolve({
548
- valid: true,
549
- days_until_expiry: null,
550
- issuer: null,
551
- protocol,
552
- expiry_warning: null
553
- });
555
+ resolve({ ssl: { valid: true, days_until_expiry: null, issuer: null, protocol, expiry_warning: null }, http2: alpn === "h2" });
554
556
  return;
555
557
  }
556
558
  const daysLeft = Math.round((new Date(cert.valid_to).getTime() - Date.now()) / 86400000);
557
559
  let expiry_warning = null;
558
- if (daysLeft < 0) {
560
+ if (daysLeft < 0)
559
561
  expiry_warning = "CRITICAL: Certificate has expired";
560
- } else if (daysLeft <= 7) {
562
+ else if (daysLeft <= 7)
561
563
  expiry_warning = `CRITICAL: Certificate expires in ${daysLeft} days — renew immediately`;
562
- } else if (daysLeft <= 30) {
564
+ else if (daysLeft <= 30)
563
565
  expiry_warning = `WARNING: Certificate expires in ${daysLeft} days — schedule renewal`;
564
- }
565
566
  const issuerRaw = cert.issuer?.O ?? cert.issuer?.CN ?? null;
566
567
  const issuerStr = Array.isArray(issuerRaw) ? issuerRaw[0] ?? null : issuerRaw;
567
568
  resolve({
568
- valid: true,
569
- days_until_expiry: daysLeft,
570
- issuer: typeof issuerStr === "string" ? issuerStr.slice(0, 100) : null,
571
- protocol,
572
- expiry_warning
569
+ ssl: { valid: true, days_until_expiry: daysLeft, issuer: typeof issuerStr === "string" ? issuerStr.slice(0, 100) : null, protocol, expiry_warning },
570
+ http2: alpn === "h2"
573
571
  });
574
572
  } catch (e) {
575
573
  socket.destroy();
576
- resolve({
577
- valid: false,
578
- days_until_expiry: null,
579
- issuer: null,
580
- protocol: null,
581
- expiry_warning: null,
582
- error: e.message
583
- });
574
+ resolve({ ssl: { valid: false, days_until_expiry: null, issuer: null, protocol: null, expiry_warning: null, error: e.message }, http2: false });
584
575
  }
585
576
  });
586
577
  socket.setTimeout(1e4, () => {
587
578
  socket.destroy();
588
- resolve({
589
- valid: false,
590
- days_until_expiry: null,
591
- issuer: null,
592
- protocol: null,
593
- expiry_warning: null,
594
- error: "TLS connection timed out"
595
- });
579
+ resolve({ ssl: { valid: false, days_until_expiry: null, issuer: null, protocol: null, expiry_warning: null, error: "TLS connection timed out" }, http2: false });
596
580
  });
597
581
  socket.on("error", (err) => {
598
- resolve({
599
- valid: false,
600
- days_until_expiry: null,
601
- issuer: null,
602
- protocol: null,
603
- expiry_warning: null,
604
- error: err.message
605
- });
582
+ resolve({ ssl: { valid: false, days_until_expiry: null, issuer: null, protocol: null, expiry_warning: null, error: err.message }, http2: false });
606
583
  });
607
584
  } catch (e) {
608
- resolve({
609
- valid: false,
610
- days_until_expiry: null,
611
- issuer: null,
612
- protocol: null,
613
- expiry_warning: null,
614
- error: e.message
615
- });
585
+ resolve({ ssl: { valid: false, days_until_expiry: null, issuer: null, protocol: null, expiry_warning: null, error: e.message }, http2: false });
616
586
  }
617
587
  });
618
588
  }
619
589
  async function checkHstsPreload(domain) {
620
590
  return new Promise((resolve) => {
621
591
  const path = `/api/v2/status?domain=${encodeURIComponent(domain)}`;
622
- const req = https.get({
623
- hostname: "hstspreload.org",
624
- path,
625
- headers: { "User-Agent": "audit-headers-mcp/1.0" },
626
- timeout: 3000
627
- }, (res) => {
592
+ const req = https.get({ hostname: "hstspreload.org", path, headers: { "User-Agent": "audit-headers-mcp/1.0" }, timeout: 3000 }, (res) => {
628
593
  let body = "";
629
594
  res.on("data", (chunk) => {
630
595
  body += chunk.toString();
631
596
  });
632
597
  res.on("end", () => {
633
598
  try {
634
- const parsed = JSON.parse(body);
635
- resolve(typeof parsed.status === "string" ? parsed.status : "unknown");
599
+ const p = JSON.parse(body);
600
+ resolve(typeof p.status === "string" ? p.status : "unknown");
636
601
  } catch {
637
602
  resolve("check_failed");
638
603
  }
@@ -646,240 +611,206 @@ async function checkHstsPreload(domain) {
646
611
  req.on("error", () => resolve("check_failed"));
647
612
  });
648
613
  }
614
+ function analyzeCspQuality(cspValue, isReportOnly) {
615
+ const lower = cspValue.toLowerCase();
616
+ const issues = [];
617
+ let score = 100;
618
+ const hasUnsafeInline = /script-src[^;]*'unsafe-inline'/.test(lower) || !lower.includes("script-src") && /default-src[^;]*'unsafe-inline'/.test(lower);
619
+ const hasUnsafeEval = /script-src[^;]*'unsafe-eval'/.test(lower) || !lower.includes("script-src") && /default-src[^;]*'unsafe-eval'/.test(lower);
620
+ const hasWildcard = /(default-src|script-src)\s+[^;]*(?<!')(?<!=)\*(?!\.)/.test(lower);
621
+ const hasDataSrc = /script-src[^;]*\bdata:/.test(lower) || !lower.includes("script-src") && /default-src[^;]*\bdata:/.test(lower);
622
+ const hasBlobSrc = /script-src[^;]*\bblob:/.test(lower) || !lower.includes("script-src") && /default-src[^;]*\bblob:/.test(lower);
623
+ const hasHttpsSrc = /script-src[^;]*(?<!')\bhttps:(?!\/)/.test(lower) || !lower.includes("script-src") && /default-src[^;]*(?<!')\bhttps:(?!\/)/.test(lower);
624
+ const missingFrameAncestors = !lower.includes("frame-ancestors");
625
+ const missingBaseUri = !lower.includes("base-uri");
626
+ const missingObjectSrc = !lower.includes("object-src");
627
+ const hasNonceOrHash = /'nonce-[^']+'/i.test(cspValue) || /'sha(256|384|512)-[^']+'/i.test(cspValue);
628
+ const hasStrictDynamic = lower.includes("'strict-dynamic'");
629
+ if (isReportOnly) {
630
+ issues.push("Report-Only mode — no enforcement, violations only logged");
631
+ score = 10;
632
+ }
633
+ if (hasWildcard) {
634
+ issues.push("Wildcard * in script-src/default-src — allows scripts from any origin");
635
+ score = Math.min(score, 5);
636
+ }
637
+ if (hasUnsafeInline && !hasNonceOrHash) {
638
+ issues.push("'unsafe-inline' in script-src without nonce/hash — XSS protection defeated");
639
+ score -= 40;
640
+ }
641
+ if (hasUnsafeEval) {
642
+ issues.push("'unsafe-eval' — allows eval()/Function()/setTimeout(string)");
643
+ score -= 20;
644
+ }
645
+ if (hasDataSrc) {
646
+ issues.push("data: in script-src — allows data:text/javascript injection");
647
+ score -= 25;
648
+ }
649
+ if (hasBlobSrc) {
650
+ issues.push("blob: in script-src — allows blob URL script execution");
651
+ score -= 15;
652
+ }
653
+ if (hasHttpsSrc) {
654
+ issues.push("https: as source — allows scripts from ANY HTTPS origin");
655
+ score -= 20;
656
+ }
657
+ if (missingObjectSrc) {
658
+ issues.push("Missing object-src — Flash/plugin injection possible");
659
+ score -= 10;
660
+ }
661
+ if (missingBaseUri) {
662
+ issues.push("Missing base-uri — <base> tag injection possible");
663
+ score -= 10;
664
+ }
665
+ if (missingFrameAncestors) {
666
+ issues.push("Missing frame-ancestors — no clickjacking protection via CSP");
667
+ score -= 10;
668
+ }
669
+ if (hasNonceOrHash) {
670
+ issues.push("✓ Uses nonce/hash — strong script allowlisting");
671
+ score += 15;
672
+ }
673
+ if (hasStrictDynamic) {
674
+ issues.push("✓ Uses strict-dynamic — modern CSP Level 3");
675
+ score += 10;
676
+ }
677
+ if (lower.includes("object-src 'none'"))
678
+ score += 5;
679
+ if (lower.includes("base-uri 'none'") || lower.includes("base-uri 'self'"))
680
+ score += 5;
681
+ score = Math.max(0, Math.min(100, score));
682
+ return {
683
+ csp_score: score,
684
+ has_unsafe_inline: hasUnsafeInline,
685
+ has_unsafe_eval: hasUnsafeEval,
686
+ has_wildcard_src: hasWildcard,
687
+ has_data_src: hasDataSrc,
688
+ has_blob_src: hasBlobSrc,
689
+ has_https_src: hasHttpsSrc,
690
+ missing_frame_ancestors: missingFrameAncestors,
691
+ missing_base_uri: missingBaseUri,
692
+ missing_object_src: missingObjectSrc,
693
+ report_only_mode: isReportOnly,
694
+ has_nonce_or_hash: hasNonceOrHash,
695
+ has_strict_dynamic: hasStrictDynamic,
696
+ issues
697
+ };
698
+ }
649
699
  function analyzeSecurityHeaders(headers) {
650
700
  const result = {};
651
701
  let score = 100;
702
+ let cspQuality;
652
703
  {
653
704
  const val = getHeaderString(headers, "strict-transport-security");
654
705
  if (!val) {
655
- result.strict_transport_security = {
656
- present: false,
657
- status: "fail",
658
- issue: "Missing — site can be accessed over HTTP, vulnerable to SSL stripping",
659
- fix: "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"
660
- };
706
+ result.strict_transport_security = { present: false, status: "fail", issue: "Missing — vulnerable to SSL stripping", fix: "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload", fixes: FIX_SNIPPETS.hsts };
661
707
  score -= 25;
662
708
  } else {
663
709
  const maxAgeMatch = val.match(/max-age=(\d+)/i);
664
710
  const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : 0;
665
711
  if (maxAge < 31536000) {
666
- result.strict_transport_security = {
667
- present: true,
668
- value_summary: safeStr(val),
669
- status: "warn",
670
- issue: `max-age is only ${maxAge} seconds — should be at least 31536000 (1 year)`,
671
- fix: "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"
672
- };
712
+ result.strict_transport_security = { present: true, value_summary: safeStr(val), status: "warn", issue: `max-age=${maxAge} — should be ≥31536000 (1 year)`, fix: "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload", fixes: FIX_SNIPPETS.hsts };
673
713
  score -= 10;
674
714
  } else {
675
- result.strict_transport_security = {
676
- present: true,
677
- value_summary: safeStr(val),
678
- status: "pass",
679
- issue: null,
680
- fix: null
681
- };
715
+ result.strict_transport_security = { present: true, value_summary: safeStr(val), status: "pass", issue: null, fix: null };
682
716
  }
683
717
  }
684
718
  }
685
719
  {
686
720
  const val = getHeaderString(headers, "content-security-policy");
687
- if (!val) {
688
- result.content_security_policy = {
689
- present: false,
690
- status: "fail",
691
- issue: "Missing — no protection against XSS attacks",
692
- fix: "Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'"
693
- };
721
+ const reportOnlyVal = getHeaderString(headers, "content-security-policy-report-only");
722
+ const effectiveVal = val ?? reportOnlyVal;
723
+ const isReportOnly = !val && !!reportOnlyVal;
724
+ if (!effectiveVal) {
725
+ result.content_security_policy = { present: false, status: "fail", issue: "Missing — no XSS protection", fix: "Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'", fixes: FIX_SNIPPETS.csp };
694
726
  score -= 25;
695
727
  } else {
696
- const issues = [];
697
- const lower = val.toLowerCase();
698
- if (lower.includes("'unsafe-inline'")) {
699
- issues.push("allows 'unsafe-inline' — XSS protection weakened");
728
+ cspQuality = analyzeCspQuality(effectiveVal, isReportOnly);
729
+ const cspIssues = cspQuality.issues.filter((i) => !i.startsWith("✓"));
730
+ if (isReportOnly)
731
+ score -= 15;
732
+ if (cspQuality.has_unsafe_inline && !cspQuality.has_nonce_or_hash)
700
733
  score -= 10;
701
- }
702
- if (lower.includes("'unsafe-eval'")) {
703
- issues.push("allows 'unsafe-eval' — eval() execution enabled, potential XSS vector");
734
+ if (cspQuality.has_unsafe_eval)
704
735
  score -= 5;
705
- }
706
- if (/(?:default-src|script-src|style-src|img-src|connect-src|font-src|media-src|frame-src)\s+[^;]*\*/.test(lower)) {
707
- issues.push("wildcard (*) in src directive — overly permissive");
708
- }
709
- if (!lower.includes("default-src")) {
710
- issues.push("no default-src fallback directive");
711
- }
712
- if (lower.includes("frame-ancestors")) {
713
- issues.push("frame-ancestors present (supersedes X-Frame-Options)");
714
- }
715
- const hasReporting = lower.includes("report-uri") || lower.includes("report-to");
716
- if (!hasReporting) {
717
- issues.push("no reporting endpoint (report-uri/report-to) — violations are silently dropped");
718
- }
736
+ if (cspQuality.has_wildcard_src)
737
+ score -= 15;
719
738
  result.content_security_policy = {
720
739
  present: true,
721
- value_summary: safeStr(val),
722
- status: issues.length > 0 ? "warn" : "pass",
723
- issue: issues.length > 0 ? issues.join("; ") : null,
724
- fix: issues.some((i) => i.includes("unsafe")) ? "Remove 'unsafe-inline' and 'unsafe-eval'. Use nonces or hashes for inline scripts." : null
740
+ value_summary: safeStr(effectiveVal),
741
+ status: cspQuality.csp_score < 50 ? "fail" : cspIssues.length > 0 ? "warn" : "pass",
742
+ issue: cspIssues.length > 0 ? cspIssues.join("; ") : null,
743
+ fix: cspIssues.length > 0 ? "Remove unsafe-inline/unsafe-eval. Use nonces or hashes. Add object-src 'none'; base-uri 'self'; frame-ancestors 'self'" : null,
744
+ fixes: cspIssues.length > 0 ? FIX_SNIPPETS.csp : undefined
725
745
  };
726
746
  }
727
747
  }
728
748
  {
729
749
  const val = getHeaderString(headers, "x-frame-options");
730
750
  if (!val) {
731
- result.x_frame_options = {
732
- present: false,
733
- status: "fail",
734
- issue: "Missing — page can be embedded in iframes, vulnerable to clickjacking",
735
- fix: "X-Frame-Options: SAMEORIGIN"
736
- };
751
+ result.x_frame_options = { present: false, status: "fail", issue: "Missing — vulnerable to clickjacking", fix: "X-Frame-Options: SAMEORIGIN", fixes: FIX_SNIPPETS.xfo };
737
752
  score -= 15;
738
753
  } else {
739
754
  const upper = val.trim().toUpperCase();
740
755
  if (upper.startsWith("ALLOW-FROM")) {
741
- result.x_frame_options = {
742
- present: true,
743
- value_summary: safeStr(val),
744
- status: "warn",
745
- issue: "ALLOW-FROM is deprecated and not supported by modern browsers",
746
- fix: "Use Content-Security-Policy frame-ancestors instead"
747
- };
756
+ result.x_frame_options = { present: true, value_summary: safeStr(val), status: "warn", issue: "ALLOW-FROM is deprecated — use CSP frame-ancestors", fix: "Use Content-Security-Policy frame-ancestors instead" };
748
757
  } else {
749
- result.x_frame_options = {
750
- present: true,
751
- value_summary: safeStr(val),
752
- status: upper === "DENY" || upper === "SAMEORIGIN" ? "pass" : "pass",
753
- issue: null,
754
- fix: null
755
- };
758
+ result.x_frame_options = { present: true, value_summary: safeStr(val), status: "pass", issue: null, fix: null };
756
759
  }
757
760
  }
758
761
  }
759
762
  {
760
763
  const val = getHeaderString(headers, "x-content-type-options");
761
764
  if (!val) {
762
- result.x_content_type_options = {
763
- present: false,
764
- status: "fail",
765
- issue: "Missing — browsers may MIME-sniff responses, enabling content injection",
766
- fix: "X-Content-Type-Options: nosniff"
767
- };
765
+ result.x_content_type_options = { present: false, status: "fail", issue: "Missing — MIME-sniffing enabled", fix: "X-Content-Type-Options: nosniff", fixes: FIX_SNIPPETS.xcto };
768
766
  score -= 15;
769
767
  } else {
770
768
  const isNosniff = val.toLowerCase().includes("nosniff");
771
- result.x_content_type_options = {
772
- present: true,
773
- value_summary: safeStr(val),
774
- status: isNosniff ? "pass" : "warn",
775
- issue: isNosniff ? null : "Value should be 'nosniff'",
776
- fix: isNosniff ? null : "X-Content-Type-Options: nosniff"
777
- };
769
+ result.x_content_type_options = { present: true, value_summary: safeStr(val), status: isNosniff ? "pass" : "warn", issue: isNosniff ? null : "Value should be 'nosniff'", fix: isNosniff ? null : "X-Content-Type-Options: nosniff", fixes: isNosniff ? undefined : FIX_SNIPPETS.xcto };
778
770
  }
779
771
  }
780
772
  {
781
773
  const val = getHeaderString(headers, "referrer-policy");
782
774
  if (!val) {
783
- result.referrer_policy = {
784
- present: false,
785
- status: "fail",
786
- issue: "Missing — full URLs including query parameters may leak to third parties",
787
- fix: "Referrer-Policy: strict-origin-when-cross-origin"
788
- };
775
+ result.referrer_policy = { present: false, status: "fail", issue: "Missing — URLs may leak to third parties", fix: "Referrer-Policy: strict-origin-when-cross-origin", fixes: FIX_SNIPPETS.referrer };
789
776
  score -= 10;
790
777
  } else {
791
778
  const lower = val.toLowerCase().trim();
792
- const strictPolicies = new Set([
793
- "no-referrer",
794
- "same-origin",
795
- "strict-origin",
796
- "strict-origin-when-cross-origin"
797
- ]);
798
- if (strictPolicies.has(lower)) {
799
- result.referrer_policy = {
800
- present: true,
801
- value_summary: safeStr(val),
802
- status: "pass",
803
- issue: null,
804
- fix: null
805
- };
779
+ const strict = new Set(["no-referrer", "same-origin", "strict-origin", "strict-origin-when-cross-origin"]);
780
+ if (strict.has(lower)) {
781
+ result.referrer_policy = { present: true, value_summary: safeStr(val), status: "pass", issue: null, fix: null };
806
782
  } else if (lower === "unsafe-url") {
807
- result.referrer_policy = {
808
- present: true,
809
- value_summary: safeStr(val),
810
- status: "warn",
811
- issue: "'unsafe-url' leaks full URL to all destinations",
812
- fix: "Referrer-Policy: strict-origin-when-cross-origin"
813
- };
783
+ result.referrer_policy = { present: true, value_summary: safeStr(val), status: "warn", issue: "'unsafe-url' leaks full URL to all destinations", fix: "Referrer-Policy: strict-origin-when-cross-origin", fixes: FIX_SNIPPETS.referrer };
814
784
  } else {
815
- result.referrer_policy = {
816
- present: true,
817
- value_summary: safeStr(val),
818
- status: "info",
819
- issue: `'${lower}' is acceptable but consider a stricter policy`,
820
- fix: "Referrer-Policy: strict-origin-when-cross-origin"
821
- };
785
+ result.referrer_policy = { present: true, value_summary: safeStr(val), status: "info", issue: `'${lower}' — consider a stricter policy`, fix: "Referrer-Policy: strict-origin-when-cross-origin", fixes: FIX_SNIPPETS.referrer };
822
786
  }
823
787
  }
824
788
  }
825
789
  {
826
790
  const val = getHeaderString(headers, "permissions-policy");
827
791
  if (!val) {
828
- result.permissions_policy = {
829
- present: false,
830
- status: "warn",
831
- issue: "Missing — browser features like camera and microphone are unrestricted",
832
- fix: "Permissions-Policy: camera=(), microphone=(), geolocation=()"
833
- };
792
+ result.permissions_policy = { present: false, status: "warn", issue: "Missing — browser features unrestricted", fix: "Permissions-Policy: camera=(), microphone=(), geolocation=()", fixes: FIX_SNIPPETS.permissions };
834
793
  score -= 5;
835
794
  } else {
836
- result.permissions_policy = {
837
- present: true,
838
- value_summary: safeStr(val),
839
- status: "pass",
840
- issue: null,
841
- fix: null
842
- };
795
+ result.permissions_policy = { present: true, value_summary: safeStr(val), status: "pass", issue: null, fix: null };
843
796
  }
844
797
  }
845
798
  {
846
799
  const val = getHeaderString(headers, "cross-origin-opener-policy");
847
800
  if (!val) {
848
- result.cross_origin_opener_policy = {
849
- present: false,
850
- status: "info",
851
- issue: "Missing — page may be vulnerable to cross-origin attacks via window.opener",
852
- fix: "Cross-Origin-Opener-Policy: same-origin"
853
- };
801
+ result.cross_origin_opener_policy = { present: false, status: "info", issue: "Missing — cross-origin window.opener attacks possible", fix: "Cross-Origin-Opener-Policy: same-origin", fixes: FIX_SNIPPETS.coop };
854
802
  score -= 3;
855
803
  } else {
856
- result.cross_origin_opener_policy = {
857
- present: true,
858
- value_summary: safeStr(val),
859
- status: "pass",
860
- issue: null,
861
- fix: null
862
- };
804
+ result.cross_origin_opener_policy = { present: true, value_summary: safeStr(val), status: "pass", issue: null, fix: null };
863
805
  }
864
806
  }
865
807
  {
866
808
  const val = getHeaderString(headers, "cross-origin-resource-policy");
867
809
  if (!val) {
868
- result.cross_origin_resource_policy = {
869
- present: false,
870
- status: "info",
871
- issue: "Missing — resources can be loaded by any origin",
872
- fix: "Cross-Origin-Resource-Policy: same-origin"
873
- };
810
+ result.cross_origin_resource_policy = { present: false, status: "info", issue: "Missing — resources loadable by any origin", fix: "Cross-Origin-Resource-Policy: same-origin", fixes: FIX_SNIPPETS.corp };
874
811
  score -= 2;
875
812
  } else {
876
- result.cross_origin_resource_policy = {
877
- present: true,
878
- value_summary: safeStr(val),
879
- status: "pass",
880
- issue: null,
881
- fix: null
882
- };
813
+ result.cross_origin_resource_policy = { present: true, value_summary: safeStr(val), status: "pass", issue: null, fix: null };
883
814
  }
884
815
  }
885
816
  if (score >= 90) {
@@ -888,13 +819,11 @@ function analyzeSecurityHeaders(headers) {
888
819
  score += 5;
889
820
  const cspVal = getHeaderString(headers, "content-security-policy");
890
821
  if (cspVal) {
891
- const lower = cspVal.toLowerCase();
892
- if (!lower.includes("'unsafe-inline'") && !lower.includes("'unsafe-eval'") && lower.includes("default-src")) {
822
+ const l = cspVal.toLowerCase();
823
+ if (!l.includes("'unsafe-inline'") && !l.includes("'unsafe-eval'") && l.includes("default-src"))
893
824
  score += 5;
894
- }
895
825
  }
896
- const allPresent = Object.values(result).every((h) => h.present);
897
- if (allPresent)
826
+ if (Object.values(result).every((h) => h.present))
898
827
  score += 5;
899
828
  }
900
829
  score = Math.max(0, score);
@@ -917,18 +846,15 @@ function analyzeSecurityHeaders(headers) {
917
846
  result[key] = { present: entry.present, status: "pass" };
918
847
  }
919
848
  }
920
- return {
921
- grade,
922
- score,
923
- headers: result
924
- };
849
+ return { grade, score, headers: result, csp_quality: cspQuality };
925
850
  }
926
851
  function parseCookieFlags(headers) {
927
852
  const raw = headers["set-cookie"];
928
853
  if (!raw)
929
- return [];
854
+ return { cookies: [], frameworkFromCookies: null };
930
855
  const cookies = Array.isArray(raw) ? raw : [raw];
931
856
  const result = [];
857
+ let frameworkFromCookies = null;
932
858
  for (const cookieStr of cookies) {
933
859
  const parts = cookieStr.split(";").map((p) => p.trim());
934
860
  const nameVal = parts[0] ?? "";
@@ -939,32 +865,52 @@ function parseCookieFlags(headers) {
939
865
  const lower = cookieStr.toLowerCase();
940
866
  const secure = lower.includes("secure");
941
867
  const httponly = lower.includes("httponly");
868
+ const hasPartitioned = lower.includes("partitioned");
869
+ const hasDomain = /;\s*domain\s*=/i.test(cookieStr);
870
+ const pathMatch = cookieStr.match(/;\s*path\s*=\s*([^;]*)/i);
871
+ const pathVal = pathMatch?.[1]?.trim() ?? null;
942
872
  let samesite = null;
943
873
  const ssMatch = lower.match(/samesite=([a-z]+)/);
944
- if (ssMatch?.[1]) {
874
+ if (ssMatch?.[1])
945
875
  samesite = ssMatch[1].charAt(0).toUpperCase() + ssMatch[1].slice(1);
946
- }
947
876
  const issues = [];
877
+ const prefixIssues = [];
948
878
  if (!secure)
949
879
  issues.push("Missing Secure flag");
950
- if (!httponly && SESSION_COOKIE_PATTERN.test(name)) {
951
- issues.push("Missing HttpOnly flag on session-like cookie");
952
- }
953
- if (samesite?.toLowerCase() === "none" && !secure) {
954
- issues.push("SameSite=None requires Secure flag");
955
- }
956
- const flag = {
957
- name: name.slice(0, 100),
958
- secure,
959
- httponly,
960
- samesite,
961
- status: issues.length === 0 ? "pass" : "fail"
962
- };
963
- if (issues.length > 0)
964
- flag.issue = issues.join("; ");
880
+ if (!httponly && SESSION_COOKIE_PATTERN.test(name))
881
+ issues.push("Missing HttpOnly on session-like cookie");
882
+ if (samesite?.toLowerCase() === "none" && !secure)
883
+ issues.push("SameSite=None requires Secure flag — browsers reject this cookie");
884
+ if (name.startsWith("__Host-") || name.toLowerCase().startsWith("__host-")) {
885
+ if (!secure)
886
+ prefixIssues.push("__Host- prefix requires Secure attribute");
887
+ if (hasDomain)
888
+ prefixIssues.push("__Host- prefix must NOT have Domain attribute");
889
+ if (pathVal !== "/")
890
+ prefixIssues.push("__Host- prefix requires Path=/");
891
+ }
892
+ if ((name.startsWith("__Secure-") || name.toLowerCase().startsWith("__secure-")) && !secure) {
893
+ prefixIssues.push("__Secure- prefix requires Secure attribute");
894
+ }
895
+ if (hasPartitioned && !secure)
896
+ prefixIssues.push("Partitioned cookies require Secure attribute");
897
+ if (!frameworkFromCookies) {
898
+ for (const [pattern, fw] of COOKIE_FRAMEWORK_MAP) {
899
+ if (pattern.test(name)) {
900
+ frameworkFromCookies = fw;
901
+ break;
902
+ }
903
+ }
904
+ }
905
+ const allIssues = [...issues, ...prefixIssues];
906
+ const flag = { name: name.slice(0, 100), secure, httponly, samesite, status: allIssues.length === 0 ? "pass" : "fail" };
907
+ if (allIssues.length > 0)
908
+ flag.issue = allIssues.join("; ");
909
+ if (prefixIssues.length > 0)
910
+ flag.prefix_issues = prefixIssues;
965
911
  result.push(flag);
966
912
  }
967
- return result;
913
+ return { cookies: result, frameworkFromCookies };
968
914
  }
969
915
  function analyzeServerFingerprint(headers) {
970
916
  const serverVal = getHeaderString(headers, "server");
@@ -973,77 +919,248 @@ function analyzeServerFingerprint(headers) {
973
919
  const serverLeaksVersion = serverVal !== null && /\/\d/.test(serverVal);
974
920
  const poweredByPresent = poweredBy !== null;
975
921
  const issues = [];
976
- if (serverLeaksVersion) {
922
+ if (serverLeaksVersion)
977
923
  issues.push("Server header reveals software version — remove or genericize");
978
- } else if (serverPresent) {
979
- issues.push("Server header is present — consider removing to reduce attack surface");
980
- }
981
- if (poweredByPresent) {
982
- issues.push("X-Powered-By header reveals framework remove entirely");
983
- }
984
- return {
985
- server_header_present: serverPresent,
986
- server_leaks_version: serverLeaksVersion,
987
- x_powered_by_present: poweredByPresent,
988
- issues
989
- };
990
- }
991
- function analyzeCacheHeaders(headers, urlPath) {
924
+ else if (serverPresent)
925
+ issues.push("Server header present — consider removing");
926
+ if (poweredByPresent)
927
+ issues.push("X-Powered-By reveals framework — remove entirely");
928
+ let framework = null;
929
+ const sv = (serverVal ?? "").toLowerCase();
930
+ const pb = (poweredBy ?? "").toLowerCase();
931
+ if (sv.includes("vercel") || sv === "vercel")
932
+ framework = "Vercel";
933
+ else if (sv.includes("netlify"))
934
+ framework = "Netlify";
935
+ else if (sv.includes("cloudflare"))
936
+ framework = "Cloudflare";
937
+ else if (sv.includes("nginx"))
938
+ framework = "Nginx";
939
+ else if (sv.includes("apache"))
940
+ framework = "Apache";
941
+ else if (sv.includes("caddy"))
942
+ framework = "Caddy";
943
+ else if (pb.includes("express"))
944
+ framework = "Express";
945
+ else if (pb.includes("next.js"))
946
+ framework = "Next.js";
947
+ else if (pb.includes("nuxt"))
948
+ framework = "Nuxt";
949
+ else if (pb.includes("php"))
950
+ framework = "PHP";
951
+ else if (pb.includes("asp.net"))
952
+ framework = "ASP.NET";
953
+ else if (pb.includes("django"))
954
+ framework = "Django";
955
+ else if (pb.includes("flask"))
956
+ framework = "Flask";
957
+ else if (pb.includes("rails") || pb.includes("ruby"))
958
+ framework = "Rails";
959
+ return { server_header_present: serverPresent, server_leaks_version: serverLeaksVersion, x_powered_by_present: poweredByPresent, issues, framework_detected: framework };
960
+ }
961
+ function analyzeCacheHeaders(headers, urlPath, hasCookies) {
992
962
  const cc = getHeaderString(headers, "cache-control");
993
963
  const cacheControlPresent = cc !== null;
994
964
  const hasNoStore = cc !== null && cc.toLowerCase().includes("no-store");
965
+ const hasPublic = cc !== null && cc.toLowerCase().includes("public");
966
+ const hasNoCache = cc !== null && cc.toLowerCase().includes("no-cache");
995
967
  const etagPresent = getHeaderString(headers, "etag") !== null;
996
968
  const lastModifiedPresent = getHeaderString(headers, "last-modified") !== null;
997
969
  const vary = getHeaderString(headers, "vary");
970
+ const contentType = getHeaderString(headers, "content-type") ?? "";
971
+ const isHtml = contentType.includes("text/html");
972
+ const isApi = urlPath.includes("/api/") || contentType.includes("application/json");
998
973
  const issues = [];
999
974
  const lowerPath = urlPath.toLowerCase();
1000
975
  const isSensitivePath = SENSITIVE_PATH_FRAGMENTS.some((p) => lowerPath.includes(p));
1001
- if (isSensitivePath && !hasNoStore) {
1002
- issues.push(`WARNING: no-store not set on ${urlPath} — sensitive page may be cached by browser`);
1003
- }
1004
- return {
1005
- cache_control_present: cacheControlPresent,
1006
- has_no_store: hasNoStore,
1007
- etag_present: etagPresent,
1008
- last_modified_present: lastModifiedPresent,
1009
- vary_header: vary ? vary.slice(0, 200) : null,
1010
- issues
1011
- };
976
+ if (isSensitivePath && !hasNoStore)
977
+ issues.push(`WARNING: no-store not set on ${urlPath} — sensitive page may be cached`);
978
+ if (hasCookies && !hasNoStore && !cc?.toLowerCase().includes("private"))
979
+ issues.push("WARNING: Response sets cookies but missing no-store/private — may be cached by CDN");
980
+ if (hasPublic && isApi)
981
+ issues.push("WARNING: Cache-Control: public on API response — user-specific data may be cached by shared caches");
982
+ if (isHtml && !cacheControlPresent)
983
+ issues.push("WARNING: No Cache-Control on HTML page — browsers use heuristic caching");
984
+ if (isHtml && !hasNoCache && !hasNoStore && cc && !cc.toLowerCase().includes("must-revalidate"))
985
+ issues.push("INFO: HTML page cached without revalidation directive — stale content after deploys");
986
+ const fixes = issues.length > 0 ? FIX_SNIPPETS.cache_nostore : undefined;
987
+ return { cache_control_present: cacheControlPresent, has_no_store: hasNoStore, etag_present: etagPresent, last_modified_present: lastModifiedPresent, vary_header: vary ? vary.slice(0, 200) : null, issues, fixes };
1012
988
  }
1013
989
  async function checkCorsPreflight(resolvedIp, hostname, port, path, isHttps) {
1014
990
  try {
1015
- const response = await makeRawRequest(resolvedIp, hostname, port, path, "OPTIONS", isHttps);
991
+ const response = await makeRawRequest(resolvedIp, hostname, port, path, "OPTIONS", isHttps, {
992
+ Origin: "https://evil.example.com",
993
+ "Access-Control-Request-Method": "GET"
994
+ });
1016
995
  const origin = getHeaderString(response.headers, "access-control-allow-origin");
1017
996
  const credentials = getHeaderString(response.headers, "access-control-allow-credentials");
997
+ const methods = getHeaderString(response.headers, "access-control-allow-methods");
998
+ const maxAge = getHeaderString(response.headers, "access-control-max-age");
999
+ const varyHeader = getHeaderString(response.headers, "vary");
1018
1000
  const isWildcard = origin === "*";
1019
1001
  const allowsCredentials = credentials?.toLowerCase() === "true";
1020
- const allowsCredentialsWithWildcard = isWildcard && allowsCredentials;
1002
+ const originReflected = origin === "https://evil.example.com";
1003
+ const nullAllowed = origin === "null";
1004
+ const varyOriginPresent = !!varyHeader && varyHeader.toLowerCase().includes("origin");
1021
1005
  const issues = [];
1022
- if (allowsCredentialsWithWildcard) {
1023
- issues.push("CRITICAL: Access-Control-Allow-Credentials with wildcard origin browsers will reject this combination");
1024
- }
1006
+ if (originReflected && allowsCredentials)
1007
+ issues.push("CRITICAL: Origin reflection with credentialsany site can make authenticated cross-origin requests");
1008
+ else if (originReflected)
1009
+ issues.push("WARNING: Server reflects arbitrary Origin header — potential CORS misconfiguration");
1010
+ if (nullAllowed && allowsCredentials)
1011
+ issues.push("CRITICAL: null origin allowed with credentials — exploitable via sandboxed iframes");
1012
+ if (isWildcard && allowsCredentials)
1013
+ issues.push("CRITICAL: Wildcard origin with credentials — browsers block this but server is misconfigured");
1014
+ if (origin && !isWildcard && !varyOriginPresent)
1015
+ issues.push("WARNING: Dynamic ACAO without Vary: Origin — cache poisoning risk");
1016
+ if (!maxAge)
1017
+ issues.push("INFO: No Access-Control-Max-Age — preflight repeated every request (performance)");
1025
1018
  return {
1026
1019
  checked: true,
1027
- allowed_origins: origin === null ? "none" : isWildcard ? "wildcard" : "specific",
1028
- allows_credentials_with_wildcard: allowsCredentialsWithWildcard,
1020
+ allowed_origins: origin === null ? "none" : isWildcard ? "wildcard" : originReflected ? "reflects_origin" : "specific",
1021
+ allows_credentials_with_wildcard: isWildcard && allowsCredentials,
1022
+ origin_reflected: originReflected,
1023
+ null_origin_allowed: nullAllowed,
1024
+ vary_origin_present: varyOriginPresent,
1025
+ methods: methods ? methods.slice(0, 200) : null,
1026
+ max_age: maxAge,
1029
1027
  issues
1030
1028
  };
1031
1029
  } catch {
1032
- return {
1033
- checked: true,
1034
- allowed_origins: "unknown",
1035
- allows_credentials_with_wildcard: false,
1036
- issues: []
1037
- };
1030
+ return { checked: true, allowed_origins: "unknown", allows_credentials_with_wildcard: false, issues: [] };
1038
1031
  }
1039
1032
  }
1040
- function aggregateIssues(ssl, secHeaders, cookies, fingerprint, cache, httpVersion, cors) {
1033
+ function detectCdnWaf(headers) {
1034
+ let cdnName = null;
1035
+ let wafName = null;
1036
+ for (const sig of CDN_SIGNATURES) {
1037
+ if (sig.detect(headers)) {
1038
+ if (sig.type === "cdn" || sig.type === "both")
1039
+ cdnName = cdnName ?? sig.name;
1040
+ if (sig.type === "waf" || sig.type === "both")
1041
+ wafName = wafName ?? sig.name;
1042
+ }
1043
+ }
1044
+ return { cdnName, wafName };
1045
+ }
1046
+ function detectInfoLeakage(headers) {
1047
+ const entries = [];
1048
+ const serverVal = getHeaderString(headers, "server");
1049
+ if (serverVal && /\/\d/.test(serverVal)) {
1050
+ entries.push({ header: "Server", severity: "high", issue: "Server header exposes software version — enables targeted CVE exploitation", fixes: FIX_SNIPPETS.remove_server });
1051
+ }
1052
+ for (const check of INFO_LEAK_HEADERS) {
1053
+ if (getHeaderString(headers, check.header) !== null) {
1054
+ entries.push({ header: check.header, severity: check.severity, issue: check.message, fixes: check.fixKey ? FIX_SNIPPETS[check.fixKey] : undefined });
1055
+ }
1056
+ }
1057
+ return { leaks_found: entries.length, entries };
1058
+ }
1059
+ function detectDeprecatedHeaders(headers) {
1060
+ const entries = [];
1061
+ const xxp = getHeaderString(headers, "x-xss-protection");
1062
+ if (xxp !== null) {
1063
+ const trimmed = xxp.trim();
1064
+ if (trimmed !== "0") {
1065
+ entries.push({ header: "X-XSS-Protection", value_summary: safeStr(xxp), severity: "error", issue: "Non-zero X-XSS-Protection is dangerous — XSS Auditor removed from all browsers (Chrome 78). Can introduce XSS via false positives. Set to 0 or remove." });
1066
+ }
1067
+ }
1068
+ if (getHeaderString(headers, "public-key-pins") !== null) {
1069
+ entries.push({ header: "Public-Key-Pins", severity: "error", issue: "HPKP is deprecated and dangerous — can permanently brick your site. Removed from Chrome 72, Firefox 72. Remove immediately." });
1070
+ }
1071
+ if (getHeaderString(headers, "public-key-pins-report-only") !== null) {
1072
+ entries.push({ header: "Public-Key-Pins-Report-Only", severity: "warning", issue: "HPKP-Report-Only is deprecated — no browser processes it. Remove." });
1073
+ }
1074
+ if (getHeaderString(headers, "expect-ct") !== null) {
1075
+ entries.push({ header: "Expect-CT", severity: "warning", issue: "Expect-CT deprecated since Chrome 107. Certificate Transparency is enforced by default. Remove." });
1076
+ }
1077
+ const fp = getHeaderString(headers, "feature-policy");
1078
+ const pp = getHeaderString(headers, "permissions-policy");
1079
+ if (fp !== null && pp === null) {
1080
+ entries.push({ header: "Feature-Policy", severity: "warning", issue: "Feature-Policy is deprecated — rename to Permissions-Policy with updated syntax", fixes: FIX_SNIPPETS.permissions });
1081
+ } else if (fp !== null && pp !== null) {
1082
+ entries.push({ header: "Feature-Policy", severity: "info", issue: "Feature-Policy is redundant alongside Permissions-Policy — consider removing" });
1083
+ }
1084
+ return { found: entries.length, entries };
1085
+ }
1086
+ async function checkDnsSecurity(domain) {
1087
+ const dnsP = dns.promises;
1088
+ let spf = { found: false, policy: null, issues: [] };
1089
+ try {
1090
+ const records = await dnsP.resolveTxt(domain);
1091
+ const spfRecords = records.map((c) => c.join("")).filter((r) => r.startsWith("v=spf1"));
1092
+ if (spfRecords.length === 0) {
1093
+ spf = { found: false, policy: null, issues: ["No SPF record — anyone can send email as this domain"] };
1094
+ } else if (spfRecords.length > 1) {
1095
+ spf = { found: true, policy: null, issues: ["Multiple SPF records — RFC 7208 violation (PermError)"] };
1096
+ } else {
1097
+ const rec = spfRecords[0];
1098
+ let policy = null;
1099
+ if (rec.includes("-all"))
1100
+ policy = "hardfail";
1101
+ else if (rec.includes("~all"))
1102
+ policy = "softfail";
1103
+ else if (rec.includes("?all"))
1104
+ policy = "neutral";
1105
+ else if (rec.includes("+all"))
1106
+ policy = "pass_all";
1107
+ const issues = [];
1108
+ if (policy === "pass_all")
1109
+ issues.push("+all allows ANY server to send email — effectively no SPF");
1110
+ else if (policy === "softfail")
1111
+ issues.push("~all (softfail) — consider upgrading to -all (hardfail)");
1112
+ else if (policy === "neutral")
1113
+ issues.push("?all (neutral) — provides no protection");
1114
+ spf = { found: true, policy, issues };
1115
+ }
1116
+ } catch {}
1117
+ let dmarc = { found: false, policy: null, issues: [] };
1118
+ try {
1119
+ const records = await dnsP.resolveTxt(`_dmarc.${domain}`);
1120
+ const dmarcRecords = records.map((c) => c.join("")).filter((r) => r.startsWith("v=DMARC1"));
1121
+ if (dmarcRecords.length === 0) {
1122
+ dmarc = { found: false, policy: null, issues: ["No DMARC record — email spoofing not prevented"] };
1123
+ } else {
1124
+ const rec = dmarcRecords[0];
1125
+ const pMatch = rec.match(/;\s*p=([^;\s]+)/);
1126
+ const policy = pMatch?.[1] ?? null;
1127
+ const issues = [];
1128
+ if (policy === "none")
1129
+ issues.push("p=none — monitoring only, no enforcement");
1130
+ dmarc = { found: true, policy, issues };
1131
+ }
1132
+ } catch {}
1133
+ let dkim = { found: false, selectors_found: [] };
1134
+ const selectorResults = await Promise.allSettled(COMMON_DKIM_SELECTORS.map(async (sel) => {
1135
+ try {
1136
+ const records = await dnsP.resolveTxt(`${sel}._domainkey.${domain}`);
1137
+ const rec = records.map((c) => c.join("")).find((r) => r.includes("p="));
1138
+ if (rec)
1139
+ return sel;
1140
+ } catch {}
1141
+ return null;
1142
+ }));
1143
+ const foundSelectors = selectorResults.filter((r) => r.status === "fulfilled" && r.value !== null).map((r) => r.value);
1144
+ if (foundSelectors.length > 0)
1145
+ dkim = { found: true, selectors_found: foundSelectors };
1146
+ return { spf, dmarc, dkim };
1147
+ }
1148
+ function detectHttp3(headers) {
1149
+ const altSvc = getHeaderString(headers, "alt-svc");
1150
+ if (!altSvc)
1151
+ return false;
1152
+ return /\bh3\b/.test(altSvc);
1153
+ }
1154
+ function detectCompression(headers) {
1155
+ const encoding = (getHeaderString(headers, "content-encoding") ?? "").toLowerCase();
1156
+ return { brotli: encoding.includes("br"), gzip: encoding.includes("gzip") };
1157
+ }
1158
+ function aggregateIssues(ssl, secHeaders, cookies, fingerprint, cache, httpVersion, cors, infoLeak, deprecated, dnsSec) {
1041
1159
  const criticals = [];
1042
1160
  const warnings = [];
1043
1161
  const infos = [];
1044
- if (!ssl.valid) {
1162
+ if (!ssl.valid)
1045
1163
  criticals.push(`CRITICAL: SSL/TLS connection failed — ${ssl.error ?? "unknown error"}`);
1046
- }
1047
1164
  if (ssl.expiry_warning) {
1048
1165
  if (ssl.expiry_warning.startsWith("CRITICAL"))
1049
1166
  criticals.push(ssl.expiry_warning);
@@ -1054,39 +1171,74 @@ function aggregateIssues(ssl, secHeaders, cookies, fingerprint, cache, httpVersi
1054
1171
  if (!h.issue)
1055
1172
  continue;
1056
1173
  const label = key.replace(/_/g, "-").toUpperCase();
1057
- if (h.status === "fail") {
1174
+ if (h.status === "fail")
1058
1175
  criticals.push(`CRITICAL: ${label} — ${h.issue}`);
1059
- } else if (h.status === "warn") {
1176
+ else if (h.status === "warn")
1060
1177
  warnings.push(`WARNING: ${label} — ${h.issue}`);
1061
- } else if (h.status === "info") {
1178
+ else if (h.status === "info")
1062
1179
  infos.push(`INFO: ${label} — ${h.issue}`);
1063
- }
1180
+ }
1181
+ if (secHeaders.csp_quality) {
1182
+ const cq = secHeaders.csp_quality;
1183
+ if (cq.csp_score < 30)
1184
+ criticals.push(`CRITICAL: CSP quality score ${cq.csp_score}/100 — policy is effectively useless`);
1185
+ else if (cq.csp_score < 50)
1186
+ warnings.push(`WARNING: CSP quality score ${cq.csp_score}/100 — significant weaknesses`);
1064
1187
  }
1065
1188
  for (const c of cookies) {
1066
- if (c.status === "fail" && c.issue) {
1189
+ if (c.status === "fail" && c.issue)
1067
1190
  warnings.push(`WARNING: Cookie '${c.name}' — ${c.issue}`);
1068
- }
1069
1191
  }
1070
- for (const i of fingerprint.issues) {
1192
+ for (const i of fingerprint.issues)
1071
1193
  warnings.push(`WARNING: ${i}`);
1072
- }
1073
1194
  for (const i of cache.issues) {
1074
- if (i.startsWith("WARNING:"))
1195
+ if (i.startsWith("WARNING"))
1075
1196
  warnings.push(i);
1076
1197
  else
1077
1198
  infos.push(`INFO: ${i}`);
1078
1199
  }
1079
- if (httpVersion === "1.1") {
1200
+ if (httpVersion === "1.1")
1080
1201
  infos.push("INFO: HTTP/1.1 detected — consider HTTP/2 for improved performance");
1081
- }
1082
1202
  if (cors.checked && cors.issues) {
1083
1203
  for (const i of cors.issues) {
1084
1204
  if (i.startsWith("CRITICAL"))
1085
1205
  criticals.push(i);
1206
+ else if (i.startsWith("WARNING"))
1207
+ warnings.push(i);
1086
1208
  else
1087
- warnings.push(`WARNING: ${i}`);
1209
+ infos.push(i);
1088
1210
  }
1089
1211
  }
1212
+ for (const e of infoLeak.entries) {
1213
+ if (e.severity === "critical")
1214
+ criticals.push(`CRITICAL: ${e.header} — ${e.issue}`);
1215
+ else if (e.severity === "high")
1216
+ warnings.push(`WARNING: ${e.header} — ${e.issue}`);
1217
+ else
1218
+ infos.push(`INFO: ${e.header} — ${e.issue}`);
1219
+ }
1220
+ for (const e of deprecated.entries) {
1221
+ if (e.severity === "error")
1222
+ criticals.push(`CRITICAL: ${e.header} — ${e.issue}`);
1223
+ else if (e.severity === "warning")
1224
+ warnings.push(`WARNING: ${e.header} — ${e.issue}`);
1225
+ else
1226
+ infos.push(`INFO: ${e.header} — ${e.issue}`);
1227
+ }
1228
+ for (const i of dnsSec.spf.issues) {
1229
+ if (i.includes("+all") || i.includes("No SPF"))
1230
+ warnings.push(`WARNING: SPF — ${i}`);
1231
+ else
1232
+ infos.push(`INFO: SPF — ${i}`);
1233
+ }
1234
+ for (const i of dnsSec.dmarc.issues) {
1235
+ if (i.includes("No DMARC"))
1236
+ warnings.push(`WARNING: DMARC — ${i}`);
1237
+ else
1238
+ infos.push(`INFO: DMARC — ${i}`);
1239
+ }
1240
+ if (!dnsSec.dkim.found)
1241
+ infos.push("INFO: DKIM — no DKIM records found for common selectors");
1090
1242
  return [...criticals, ...warnings, ...infos];
1091
1243
  }
1092
1244
  async function runAudit(url, options) {
@@ -1111,46 +1263,64 @@ async function runAudit(url, options) {
1111
1263
  } catch {
1112
1264
  finalResolvedIp = finalHostname;
1113
1265
  }
1114
- const ssl = isHttps && opts.checkSsl ? await inspectSsl(finalHostname, finalResolvedIp) : {
1115
- valid: isHttps,
1116
- days_until_expiry: null,
1117
- issuer: null,
1118
- protocol: null,
1119
- expiry_warning: opts.checkSsl ? "WARNING: Connection is not HTTPS — no TLS certificate" : "SSL check disabled"
1120
- };
1121
- const securityHeaders = opts.checkHeaders ? analyzeSecurityHeaders(headers) : {
1122
- grade: "skip",
1123
- score: null,
1124
- headers: {
1125
- strict_transport_security: { present: false, status: "info", issue: null, fix: null },
1126
- content_security_policy: { present: false, status: "info", issue: null, fix: null },
1127
- x_frame_options: { present: false, status: "info", issue: null, fix: null },
1128
- x_content_type_options: { present: false, status: "info", issue: null, fix: null },
1129
- referrer_policy: { present: false, status: "info", issue: null, fix: null },
1130
- permissions_policy: { present: false, status: "info", issue: null, fix: null },
1131
- cross_origin_opener_policy: { present: false, status: "info", issue: null, fix: null },
1132
- cross_origin_resource_policy: { present: false, status: "info", issue: null, fix: null }
1133
- },
1134
- summary: "Check disabled"
1135
- };
1266
+ let ssl;
1267
+ let http2 = false;
1268
+ if (isHttps && opts.checkSsl) {
1269
+ const sslResult = await inspectSsl(finalHostname, finalResolvedIp);
1270
+ ssl = sslResult.ssl;
1271
+ http2 = sslResult.http2;
1272
+ } else {
1273
+ ssl = { valid: isHttps, days_until_expiry: null, issuer: null, protocol: null, expiry_warning: opts.checkSsl ? "WARNING: Connection is not HTTPS" : "SSL check disabled" };
1274
+ }
1275
+ const securityHeaders = opts.checkHeaders ? analyzeSecurityHeaders(headers) : { grade: "skip", score: null, headers: { strict_transport_security: { present: false, status: "info", issue: null, fix: null }, content_security_policy: { present: false, status: "info", issue: null, fix: null }, x_frame_options: { present: false, status: "info", issue: null, fix: null }, x_content_type_options: { present: false, status: "info", issue: null, fix: null }, referrer_policy: { present: false, status: "info", issue: null, fix: null }, permissions_policy: { present: false, status: "info", issue: null, fix: null }, cross_origin_opener_policy: { present: false, status: "info", issue: null, fix: null }, cross_origin_resource_policy: { present: false, status: "info", issue: null, fix: null } }, summary: "Check disabled" };
1136
1276
  if (opts.checkHeaders && opts.checkSsl) {
1137
1277
  const hstsVal = getHeaderString(headers, "strict-transport-security");
1138
1278
  if (hstsVal?.toLowerCase().includes("preload")) {
1139
1279
  const preloadStatus = await checkHstsPreload(finalHostname);
1140
1280
  const stsEntry = securityHeaders.headers.strict_transport_security;
1141
- if (stsEntry) {
1281
+ if (stsEntry)
1142
1282
  stsEntry.hsts_preload_status = preloadStatus;
1143
- }
1144
1283
  }
1145
1284
  }
1146
- const cookieFlags = opts.checkCookies ? parseCookieFlags(headers) : [];
1285
+ const { cookies: cookieFlags, frameworkFromCookies } = opts.checkCookies ? parseCookieFlags(headers) : { cookies: [], frameworkFromCookies: null };
1147
1286
  const serverFingerprint = analyzeServerFingerprint(headers);
1148
- const cacheAnalysis = analyzeCacheHeaders(headers, finalPath);
1287
+ const hasCookies = cookieFlags.length > 0;
1288
+ const cacheAnalysis = analyzeCacheHeaders(headers, finalPath, hasCookies);
1149
1289
  const httpVersionStr = httpVersion === "2.0" || httpVersion === "2" ? "2.0" : httpVersion ?? "1.1";
1150
1290
  const contentType = getHeaderString(headers, "content-type") ?? "";
1151
1291
  const shouldCheckCors = finalPath.includes("/api/") || contentType.includes("application/json");
1152
1292
  const corsPreflight = shouldCheckCors ? await checkCorsPreflight(finalResolvedIp, finalHostname, finalPort, finalPath, isHttps) : { checked: false };
1153
- const issues = aggregateIssues(ssl, securityHeaders, cookieFlags, serverFingerprint, cacheAnalysis, httpVersionStr, corsPreflight);
1293
+ const { cdnName, wafName } = detectCdnWaf(headers);
1294
+ const http3 = detectHttp3(headers);
1295
+ const { brotli, gzip } = detectCompression(headers);
1296
+ const infoLeakage = detectInfoLeakage(headers);
1297
+ const deprecatedHeaders = detectDeprecatedHeaders(headers);
1298
+ let dnsSecurity;
1299
+ try {
1300
+ dnsSecurity = await Promise.race([
1301
+ checkDnsSecurity(finalHostname),
1302
+ new Promise((resolve) => setTimeout(() => resolve({
1303
+ spf: { found: false, policy: null, issues: ["DNS lookup timed out"] },
1304
+ dmarc: { found: false, policy: null, issues: ["DNS lookup timed out"] },
1305
+ dkim: { found: false, selectors_found: [] }
1306
+ }), 5000))
1307
+ ]);
1308
+ } catch {
1309
+ dnsSecurity = { spf: { found: false, policy: null, issues: [] }, dmarc: { found: false, policy: null, issues: [] }, dkim: { found: false, selectors_found: [] } };
1310
+ }
1311
+ const frameworkDetected = serverFingerprint.framework_detected ?? frameworkFromCookies ?? null;
1312
+ const infra = {
1313
+ cdn_detected: cdnName !== null,
1314
+ cdn_name: cdnName,
1315
+ waf_detected: wafName !== null,
1316
+ waf_name: wafName,
1317
+ http2,
1318
+ http3,
1319
+ brotli,
1320
+ gzip,
1321
+ framework_detected: frameworkDetected
1322
+ };
1323
+ const issues = aggregateIssues(ssl, securityHeaders, cookieFlags, serverFingerprint, cacheAnalysis, httpVersionStr, corsPreflight, infoLeakage, deprecatedHeaders, dnsSecurity);
1154
1324
  {
1155
1325
  const stsEntry = securityHeaders.headers.strict_transport_security;
1156
1326
  const preloadStatus = stsEntry?.hsts_preload_status;
@@ -1158,6 +1328,8 @@ async function runAudit(url, options) {
1158
1328
  issues.push(`INFO: HSTS header claims preload but domain is not on the preload list (status: ${preloadStatus}) — submit at hstspreload.org`);
1159
1329
  }
1160
1330
  }
1331
+ if (!cdnName)
1332
+ infos_push(issues, "INFO: No CDN detected — origin server is directly exposed");
1161
1333
  const criticalCount = issues.filter((i) => i.startsWith("CRITICAL")).length;
1162
1334
  const warningCount = issues.filter((i) => i.startsWith("WARNING")).length;
1163
1335
  const infoCount = issues.filter((i) => i.startsWith("INFO")).length;
@@ -1183,10 +1355,18 @@ async function runAudit(url, options) {
1183
1355
  server_fingerprint: serverFingerprint,
1184
1356
  cache_analysis: cacheAnalysis,
1185
1357
  http_version: httpVersionStr,
1186
- cors_preflight: corsPreflight
1358
+ cors_preflight: corsPreflight,
1359
+ infra,
1360
+ info_leakage: infoLeakage,
1361
+ deprecated_headers: deprecatedHeaders,
1362
+ dns: dnsSecurity
1187
1363
  };
1188
1364
  }
1189
- var rateLimitMap, PRIVATE_RANGES, BLOCKED_AUDIT_HOSTNAMES, SESSION_COOKIE_PATTERN, SENSITIVE_PATH_FRAGMENTS;
1365
+ function infos_push(issues, msg) {
1366
+ if (!issues.includes(msg))
1367
+ issues.push(msg);
1368
+ }
1369
+ var rateLimitMap, PRIVATE_RANGES, BLOCKED_AUDIT_HOSTNAMES, FIX_SNIPPETS, SESSION_COOKIE_PATTERN, COOKIE_FRAMEWORK_MAP, SENSITIVE_PATH_FRAGMENTS, CDN_SIGNATURES, INFO_LEAK_HEADERS, COMMON_DKIM_SELECTORS;
1190
1370
  var init_handlers = __esm(() => {
1191
1371
  init_sanitize();
1192
1372
  init_types();
@@ -1207,15 +1387,156 @@ var init_handlers = __esm(() => {
1207
1387
  /^metadata\.google\.internal$/i,
1208
1388
  /^metadata\.amazonaws\.com$/i
1209
1389
  ];
1390
+ FIX_SNIPPETS = {
1391
+ hsts: {
1392
+ nginx: 'add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;',
1393
+ express: "app.use(helmet({ strictTransportSecurity: { maxAge: 63072000, includeSubDomains: true, preload: true } }));",
1394
+ vercel: '{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" }',
1395
+ next_config: "{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }",
1396
+ cloudflare: "newHeaders.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');",
1397
+ caddy: 'header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"',
1398
+ apache: 'Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"'
1399
+ },
1400
+ csp: {
1401
+ nginx: `add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'" always;`,
1402
+ express: `app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], baseUri: ["'self'"], frameAncestors: ["'none'"], objectSrc: ["'none'"] } } }));`,
1403
+ vercel: `{ "key": "Content-Security-Policy", "value": "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'" }`,
1404
+ next_config: `{ key: 'Content-Security-Policy', value: "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'" }`,
1405
+ cloudflare: `newHeaders.set('Content-Security-Policy', "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'");`,
1406
+ caddy: `header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'"`,
1407
+ apache: `Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'"`
1408
+ },
1409
+ xfo: {
1410
+ nginx: 'add_header X-Frame-Options "SAMEORIGIN" always;',
1411
+ express: "app.use(helmet({ xFrameOptions: { action: 'sameorigin' } }));",
1412
+ vercel: '{ "key": "X-Frame-Options", "value": "SAMEORIGIN" }',
1413
+ next_config: "{ key: 'X-Frame-Options', value: 'SAMEORIGIN' }",
1414
+ cloudflare: "newHeaders.set('X-Frame-Options', 'SAMEORIGIN');",
1415
+ caddy: 'header X-Frame-Options "SAMEORIGIN"',
1416
+ apache: 'Header always set X-Frame-Options "SAMEORIGIN"'
1417
+ },
1418
+ xcto: {
1419
+ nginx: 'add_header X-Content-Type-Options "nosniff" always;',
1420
+ express: "app.use(helmet()); // sets nosniff by default",
1421
+ vercel: '{ "key": "X-Content-Type-Options", "value": "nosniff" }',
1422
+ next_config: "{ key: 'X-Content-Type-Options', value: 'nosniff' }",
1423
+ cloudflare: "newHeaders.set('X-Content-Type-Options', 'nosniff');",
1424
+ caddy: 'header X-Content-Type-Options "nosniff"',
1425
+ apache: 'Header always set X-Content-Type-Options "nosniff"'
1426
+ },
1427
+ referrer: {
1428
+ nginx: 'add_header Referrer-Policy "strict-origin-when-cross-origin" always;',
1429
+ express: "app.use(helmet({ referrerPolicy: { policy: 'strict-origin-when-cross-origin' } }));",
1430
+ vercel: '{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }',
1431
+ next_config: "{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }",
1432
+ cloudflare: "newHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin');",
1433
+ caddy: 'header Referrer-Policy "strict-origin-when-cross-origin"',
1434
+ apache: 'Header always set Referrer-Policy "strict-origin-when-cross-origin"'
1435
+ },
1436
+ permissions: {
1437
+ nginx: 'add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;',
1438
+ express: `res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');`,
1439
+ vercel: '{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }',
1440
+ next_config: "{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }",
1441
+ cloudflare: "newHeaders.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');",
1442
+ caddy: 'header Permissions-Policy "camera=(), microphone=(), geolocation=()"',
1443
+ apache: 'Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"'
1444
+ },
1445
+ coop: {
1446
+ nginx: 'add_header Cross-Origin-Opener-Policy "same-origin" always;',
1447
+ express: `res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');`,
1448
+ vercel: '{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }',
1449
+ next_config: "{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' }",
1450
+ cloudflare: "newHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');",
1451
+ caddy: 'header Cross-Origin-Opener-Policy "same-origin"',
1452
+ apache: 'Header always set Cross-Origin-Opener-Policy "same-origin"'
1453
+ },
1454
+ corp: {
1455
+ nginx: 'add_header Cross-Origin-Resource-Policy "same-origin" always;',
1456
+ express: `res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');`,
1457
+ vercel: '{ "key": "Cross-Origin-Resource-Policy", "value": "same-origin" }',
1458
+ next_config: "{ key: 'Cross-Origin-Resource-Policy', value: 'same-origin' }",
1459
+ cloudflare: "newHeaders.set('Cross-Origin-Resource-Policy', 'same-origin');",
1460
+ caddy: 'header Cross-Origin-Resource-Policy "same-origin"',
1461
+ apache: 'Header always set Cross-Origin-Resource-Policy "same-origin"'
1462
+ },
1463
+ cache_nostore: {
1464
+ nginx: 'add_header Cache-Control "no-store, max-age=0" always;',
1465
+ express: `res.setHeader('Cache-Control', 'no-store, max-age=0');`,
1466
+ vercel: '{ "key": "Cache-Control", "value": "no-store, max-age=0" }',
1467
+ next_config: "{ key: 'Cache-Control', value: 'no-store, max-age=0' }",
1468
+ cloudflare: "newHeaders.set('Cache-Control', 'no-store, max-age=0');",
1469
+ caddy: 'header Cache-Control "no-store, max-age=0"',
1470
+ apache: 'Header always set Cache-Control "no-store, max-age=0"'
1471
+ },
1472
+ remove_server: {
1473
+ nginx: "server_tokens off;",
1474
+ express: "app.use((req, res, next) => { res.removeHeader('Server'); next(); });",
1475
+ cloudflare: "newHeaders.delete('Server');",
1476
+ caddy: "header -Server",
1477
+ apache: `ServerTokens Prod
1478
+ ServerSignature Off`
1479
+ },
1480
+ remove_powered_by: {
1481
+ nginx: "proxy_hide_header X-Powered-By;",
1482
+ express: "app.disable('x-powered-by');",
1483
+ next_config: "module.exports = { poweredByHeader: false };",
1484
+ cloudflare: "newHeaders.delete('X-Powered-By');",
1485
+ caddy: "header -X-Powered-By",
1486
+ apache: "Header always unset X-Powered-By"
1487
+ }
1488
+ };
1210
1489
  SESSION_COOKIE_PATTERN = /session|sid|token|auth/i;
1211
- SENSITIVE_PATH_FRAGMENTS = [
1212
- "/login",
1213
- "/dashboard",
1214
- "/admin",
1215
- "/auth",
1216
- "/account",
1217
- "/settings"
1490
+ COOKIE_FRAMEWORK_MAP = [
1491
+ [/^PHPSESSID$/i, "PHP"],
1492
+ [/^JSESSIONID$/i, "Java"],
1493
+ [/^ASP\.NET_SessionId$/i, "ASP.NET"],
1494
+ [/^connect\.sid$/i, "Express"],
1495
+ [/^laravel_session$/i, "Laravel"],
1496
+ [/^_rails_session$/i, "Rails"],
1497
+ [/^django_session$/i, "Django"],
1498
+ [/^PLAY_SESSION$/i, "Play Framework"],
1499
+ [/^rack\.session$/i, "Ruby Rack"],
1500
+ [/^wp-settings-/i, "WordPress"]
1501
+ ];
1502
+ SENSITIVE_PATH_FRAGMENTS = ["/login", "/dashboard", "/admin", "/auth", "/account", "/settings"];
1503
+ CDN_SIGNATURES = [
1504
+ { name: "Cloudflare", type: "both", detect: (h) => !!h["cf-ray"] || getHeaderString(h, "server")?.toLowerCase() === "cloudflare" },
1505
+ { name: "AWS CloudFront", type: "cdn", detect: (h) => !!h["x-amz-cf-id"] || (getHeaderString(h, "via") ?? "").toLowerCase().includes("cloudfront") },
1506
+ { name: "Akamai", type: "cdn", detect: (h) => !!h["x-akamai-transformed"] || /AkamaiGHost/i.test(getHeaderString(h, "server") ?? "") },
1507
+ { name: "Fastly", type: "cdn", detect: (h) => !!h["x-served-by"] && /^cache-/i.test(getHeaderString(h, "x-served-by") ?? "") || !!h["x-timer"] },
1508
+ { name: "Vercel", type: "cdn", detect: (h) => !!h["x-vercel-id"] || getHeaderString(h, "server")?.toLowerCase() === "vercel" },
1509
+ { name: "Netlify", type: "cdn", detect: (h) => !!h["x-nf-request-id"] || getHeaderString(h, "server")?.toLowerCase() === "netlify" },
1510
+ { name: "Google Cloud CDN", type: "cdn", detect: (h) => (getHeaderString(h, "via") ?? "").includes("google") || getHeaderString(h, "server") === "Google Frontend" },
1511
+ { name: "Azure Front Door", type: "cdn", detect: (h) => !!h["x-azure-ref"] },
1512
+ { name: "BunnyCDN", type: "cdn", detect: (h) => /BunnyCDN/i.test(getHeaderString(h, "server") ?? "") || !!h["cdn-pullzone"] },
1513
+ { name: "KeyCDN", type: "cdn", detect: (h) => /keycdn-engine/i.test(getHeaderString(h, "server") ?? "") || !!h["x-edge-location"] },
1514
+ { name: "Sucuri", type: "waf", detect: (h) => /Sucuri/i.test(getHeaderString(h, "server") ?? "") || !!h["x-sucuri-id"] },
1515
+ { name: "Imperva", type: "waf", detect: (h) => !!h["x-iinfo"] || /Incapsula|Imperva/i.test(getHeaderString(h, "x-cdn") ?? "") }
1516
+ ];
1517
+ INFO_LEAK_HEADERS = [
1518
+ { header: "x-powered-by", severity: "high", message: "Leaks server technology — remove entirely", fixKey: "remove_powered_by" },
1519
+ { header: "x-aspnet-version", severity: "high", message: "Leaks ASP.NET version — disable via web.config: enableVersionHeader=false" },
1520
+ { header: "x-aspnetmvc-version", severity: "high", message: "Leaks ASP.NET MVC version" },
1521
+ { header: "x-generator", severity: "medium", message: "Leaks CMS/generator information" },
1522
+ { header: "x-drupal-cache", severity: "medium", message: "Reveals Drupal CMS in use" },
1523
+ { header: "x-drupal-dynamic-cache", severity: "medium", message: "Reveals Drupal 8+ with dynamic page cache" },
1524
+ { header: "x-pingback", severity: "medium", message: "Reveals WordPress with XML-RPC enabled (attack surface)" },
1525
+ { header: "x-runtime", severity: "medium", message: "Leaks request processing time — enables timing attacks" },
1526
+ { header: "x-debug-token", severity: "critical", message: "Symfony debug mode enabled in production" },
1527
+ { header: "x-debug-token-link", severity: "critical", message: "Symfony profiler exposed in production" },
1528
+ { header: "x-powered-cms", severity: "medium", message: "Leaks CMS platform information" },
1529
+ { header: "x-backend-server", severity: "critical", message: "Leaks internal server hostname" },
1530
+ { header: "x-server-name", severity: "critical", message: "Leaks internal server hostname" },
1531
+ { header: "x-upstream", severity: "critical", message: "Leaks internal upstream server info" },
1532
+ { header: "x-host", severity: "high", message: "Leaks internal hostname" },
1533
+ { header: "x-mod-pagespeed", severity: "low", message: "Reveals Google PageSpeed module version" },
1534
+ { header: "x-page-speed", severity: "low", message: "Reveals Google PageSpeed module version" },
1535
+ { header: "x-litespeed-cache", severity: "low", message: "Reveals LiteSpeed server" },
1536
+ { header: "x-turbo-charged-by", severity: "low", message: "Reveals LiteSpeed server" },
1537
+ { header: "x-redirect-by", severity: "low", message: "Reveals redirect handler (e.g., WordPress)" }
1218
1538
  ];
1539
+ COMMON_DKIM_SELECTORS = ["google", "google1", "selector1", "selector2", "k1", "s1", "default", "dkim", "mail"];
1219
1540
  });
1220
1541
 
1221
1542
  // node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
@@ -15176,7 +15497,7 @@ var init_v4_mini = __esm(() => {
15176
15497
  init_external2();
15177
15498
  });
15178
15499
 
15179
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
15500
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
15180
15501
  function isZ4Schema(s) {
15181
15502
  const schema = s;
15182
15503
  return !!schema._zod;
@@ -15252,7 +15573,7 @@ var init_v4 = __esm(() => {
15252
15573
  init_classic();
15253
15574
  });
15254
15575
 
15255
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
15576
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
15256
15577
  var exports_types = {};
15257
15578
  __export(exports_types, {
15258
15579
  isTaskAugmentedRequestParams: () => isTaskAugmentedRequestParams,
@@ -15445,7 +15766,7 @@ var init_types2 = __esm(() => {
15445
15766
  ProgressTokenSchema = union([string2(), number2().int()]);
15446
15767
  CursorSchema = string2();
15447
15768
  TaskCreationParamsSchema = looseObject({
15448
- ttl: union([number2(), _null3()]).optional(),
15769
+ ttl: number2().optional(),
15449
15770
  pollInterval: number2().optional()
15450
15771
  });
15451
15772
  TaskMetadataSchema = object({
@@ -15596,7 +15917,8 @@ var init_types2 = __esm(() => {
15596
15917
  roots: object({
15597
15918
  listChanged: boolean2().optional()
15598
15919
  }).optional(),
15599
- tasks: ClientTasksCapabilitySchema.optional()
15920
+ tasks: ClientTasksCapabilitySchema.optional(),
15921
+ extensions: record(string2(), AssertObjectSchema).optional()
15600
15922
  });
15601
15923
  InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({
15602
15924
  protocolVersion: string2(),
@@ -15621,7 +15943,8 @@ var init_types2 = __esm(() => {
15621
15943
  tools: object({
15622
15944
  listChanged: boolean2().optional()
15623
15945
  }).optional(),
15624
- tasks: ServerTasksCapabilitySchema.optional()
15946
+ tasks: ServerTasksCapabilitySchema.optional(),
15947
+ extensions: record(string2(), AssertObjectSchema).optional()
15625
15948
  });
15626
15949
  InitializeResultSchema = ResultSchema.extend({
15627
15950
  protocolVersion: string2(),
@@ -15736,6 +16059,7 @@ var init_types2 = __esm(() => {
15736
16059
  uri: string2(),
15737
16060
  description: optional(string2()),
15738
16061
  mimeType: optional(string2()),
16062
+ size: optional(number2()),
15739
16063
  annotations: AnnotationsSchema.optional(),
15740
16064
  _meta: optional(looseObject({}))
15741
16065
  });
@@ -16265,7 +16589,7 @@ var init_types2 = __esm(() => {
16265
16589
  };
16266
16590
  });
16267
16591
 
16268
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
16592
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
16269
16593
  function isTerminal(status) {
16270
16594
  return status === "completed" || status === "failed" || status === "cancelled";
16271
16595
  }
@@ -16483,7 +16807,7 @@ var init_esm = __esm(() => {
16483
16807
  init_zodToJsonSchema();
16484
16808
  });
16485
16809
 
16486
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
16810
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
16487
16811
  function getMethodLiteral(schema) {
16488
16812
  const shape = getObjectShape(schema);
16489
16813
  const methodSchema = shape?.method;
@@ -16508,7 +16832,7 @@ var init_zod_json_schema_compat = __esm(() => {
16508
16832
  init_esm();
16509
16833
  });
16510
16834
 
16511
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
16835
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
16512
16836
  class Protocol {
16513
16837
  constructor(_options) {
16514
16838
  this._options = _options;
@@ -23816,7 +24140,7 @@ var require_dist = __commonJS((exports, module) => {
23816
24140
  exports.default = formatsPlugin;
23817
24141
  });
23818
24142
 
23819
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js
24143
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js
23820
24144
  function createDefaultAjvInstance() {
23821
24145
  const ajv = new import_ajv.default({
23822
24146
  strict: false,
@@ -23859,7 +24183,7 @@ var init_ajv_provider = __esm(() => {
23859
24183
  import_ajv_formats = __toESM(require_dist(), 1);
23860
24184
  });
23861
24185
 
23862
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
24186
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
23863
24187
  class ExperimentalServerTasks {
23864
24188
  constructor(_server) {
23865
24189
  this._server = _server;
@@ -23940,7 +24264,7 @@ var init_server = __esm(() => {
23940
24264
  init_types2();
23941
24265
  });
23942
24266
 
23943
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
24267
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
23944
24268
  function assertToolsCallTaskCapability(requests, method, entityName) {
23945
24269
  if (!requests) {
23946
24270
  throw new Error(`${entityName} does not support task creation (required for ${method})`);
@@ -23975,7 +24299,7 @@ function assertClientRequestTaskCapability(requests, method, entityName) {
23975
24299
  }
23976
24300
  }
23977
24301
 
23978
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
24302
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
23979
24303
  var exports_server = {};
23980
24304
  __export(exports_server, {
23981
24305
  Server: () => Server
@@ -24320,7 +24644,7 @@ var init_server2 = __esm(() => {
24320
24644
  };
24321
24645
  });
24322
24646
 
24323
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
24647
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
24324
24648
  class ReadBuffer {
24325
24649
  append(chunk) {
24326
24650
  this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
@@ -24353,7 +24677,7 @@ var init_stdio = __esm(() => {
24353
24677
  init_types2();
24354
24678
  });
24355
24679
 
24356
- // node_modules/.bun/@modelcontextprotocol+sdk@1.28.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
24680
+ // node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
24357
24681
  var exports_stdio = {};
24358
24682
  __export(exports_stdio, {
24359
24683
  StdioServerTransport: () => StdioServerTransport
@@ -24459,7 +24783,7 @@ var init_audit_headers = __esm(async () => {
24459
24783
  ` + `• When a user shares a URL from Vercel, Netlify, Cloudflare Pages, AWS CloudFront, Azure Front Door, GCP Load Balancer / Cloud CDN, Fastly, Akamai, BunnyCDN, KeyCDN, jsDelivr / unpkg, custom Nginx / Apache / Caddy / HAProxy / Traefik — the fix snippets are tailored to common origins
24460
24784
  ` + `• When the user asks to compare headers across staging vs production, or across regions / edge locations
24461
24785
  ` + `
24462
- ` + `Does NOT render JavaScript or measure Core Web Vitals — for those, use inspect_url. audit_headers is the right tool for anything at the HTTP / TLS / cookie / redirect layer.
24786
+ ` + `Does NOT render JavaScript or measure Core Web Vitals — for those, use Zephex_dev_info. audit_headers is the right tool for anything at the HTTP / TLS / cookie / redirect layer.
24463
24787
  ` + `
24464
24788
  ` + "Params: url (https recommended), check_redirects / check_ssl / check_headers / check_cookies (all default true; disable to speed up if you only need one aspect).",
24465
24789
  inputSchema: {