zephex 2.0.11 → 2.0.15
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.
- package/README.md +11 -40
- package/dist/cli.js +1 -1
- package/dist/index.js +78061 -141930
- package/dist/tools/architecture/index.js +870 -258
- package/dist/tools/audit_headers/index.js +705 -381
- package/dist/tools/context/index.js +1900 -368
- package/dist/tools/reader/readCode.js +1544 -525
- package/dist/tools/scope_task/index.js +24307 -19322
- package/dist/tools/search/findCode.js +17267 -13145
- package/dist/tools/server.js +23821 -97059
- package/dist/tools/thinking/index.js +909 -266
- package/package.json +3 -2
- package/dist/tools/inspect_url/index.js +0 -166175
|
@@ -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
|
-
|
|
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
|
|
542
|
+
const socket = tls.connect({
|
|
536
543
|
host: resolvedIp,
|
|
537
544
|
port: 443,
|
|
538
545
|
servername: hostname,
|
|
539
|
-
rejectUnauthorized: true
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
562
|
+
else if (daysLeft <= 7)
|
|
561
563
|
expiry_warning = `CRITICAL: Certificate expires in ${daysLeft} days — renew immediately`;
|
|
562
|
-
|
|
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
|
-
|
|
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
|
|
635
|
-
resolve(typeof
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
697
|
-
const
|
|
698
|
-
if (
|
|
699
|
-
|
|
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
|
-
|
|
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(
|
|
722
|
-
status:
|
|
723
|
-
issue:
|
|
724
|
-
fix:
|
|
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
|
|
793
|
-
|
|
794
|
-
"
|
|
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
|
|
892
|
-
if (!
|
|
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
|
-
|
|
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
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
979
|
-
issues.push("Server header
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
|
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 (
|
|
1023
|
-
issues.push("CRITICAL:
|
|
1024
|
-
|
|
1006
|
+
if (originReflected && allowsCredentials)
|
|
1007
|
+
issues.push("CRITICAL: Origin reflection with credentials — any 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:
|
|
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
|
|
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
|
-
|
|
1176
|
+
else if (h.status === "warn")
|
|
1060
1177
|
warnings.push(`WARNING: ${label} — ${h.issue}`);
|
|
1061
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1212
|
-
"
|
|
1213
|
-
"
|
|
1214
|
-
"
|
|
1215
|
-
"
|
|
1216
|
-
"
|
|
1217
|
-
"
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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: {
|