Files

142 lines
8.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { DIAGNOSTIC_RULES, evaluateDiagnostics } from "./diagnostics";
const okMeta: Record<string, unknown> = { status: "ok", errors: [] };
const baseDns: Record<string, unknown> = { mx: [], spf: [], dmarc: [], _meta: okMeta };
const baseTls: Record<string, unknown> = { authorized: true, hostnameValid: true, daysRemaining: 90, _meta: okMeta };
const secureHeaders: Record<string, string> = {
"strict-transport-security": "max-age=31536000; includeSubDomains",
"content-security-policy": "default-src 'self'; object-src 'none'; base-uri 'self'",
"x-content-type-options": "nosniff",
"referrer-policy": "strict-origin-when-cross-origin",
"x-frame-options": "DENY",
};
const baseHttp: Record<string, unknown> = { http: { status: 301 }, httpRedirectsToHttps: true, httpsChain: [{ status: 200, headers: secureHeaders }], _meta: okMeta };
function ids(dns: Record<string, unknown> = baseDns, tls: Record<string, unknown> = baseTls, http: Record<string, unknown> = baseHttp) {
return evaluateDiagnostics(dns, tls, http).map((finding) => finding.id);
}
describe("diagnostics", () => {
it("has unique stable rule identifiers", () => {
const ids = DIAGNOSTIC_RULES.map((rule) => rule.id);
expect(new Set(ids).size).toBe(ids.length);
});
it("finds mail and HTTP posture gaps deterministically", () => {
const found = ids(
{ mx: [{ exchange: "mail.example.com" }], spf: [], dmarc: [], _meta: okMeta },
baseTls,
{ http: { status: 200 }, httpRedirectsToHttps: false, httpsChain: [{ status: 200, headers: { "strict-transport-security": null, "content-security-policy": null, "x-content-type-options": null } }], _meta: okMeta },
);
expect(found).toEqual([
"http.hsts_missing",
"http.csp_missing",
"http.redirect",
"dns.dmarc_missing",
"dns.spf_missing",
"http.frame_protection",
"http.nosniff_missing",
"http.referrer_policy_missing",
]);
});
it("keeps partial collectors informational and does not deduct missing evidence", () => {
const findings = evaluateDiagnostics(
{ mx: [{ exchange: "mail.example.com" }], spf: [], dmarc: [], _meta: { status: "partial", errors: [{ probe: "TXT", message: "timeout" }, { probe: "DMARC", message: "timeout" }] } },
{ authorized: true, hostnameValid: true, daysRemaining: 90, _meta: okMeta },
{ http: { error: "timeout" }, httpRedirectsToHttps: false, httpsChain: [{ error: "timeout" }], _meta: { status: "partial", errors: [{ probe: "HTTPS", message: "timeout" }] } },
);
expect(findings.map((finding) => finding.id)).toEqual(["collector.dns_partial", "collector.http_partial"]);
expect(findings.every((finding) => finding.scoreImpact === 0)).toBe(true);
});
it("does not score headers when the HTTPS chain has no terminal response", () => {
expect(ids(baseDns, baseTls, { http: { status: 301 }, httpRedirectsToHttps: true, httpsChain: [{ status: 302, location: "/next", headers: {} }, { url: "/next", error: "redirect failed" }], _meta: { status: "partial", errors: [{ probe: "HTTPS", message: "redirect failed" }] } })).toEqual(["collector.http_partial"]);
});
it("deduplicates the generic TLS verification penalty when specific causes exist", () => {
const found = ids(baseDns, { authorized: false, authorizationError: "CERT_HAS_EXPIRED", hostnameValid: false, hostnameError: "hostname mismatch", daysRemaining: -2, _meta: okMeta }, baseHttp);
expect(found).toContain("tls.expired");
expect(found).toContain("tls.hostname");
expect(found).not.toContain("tls.verification");
});
it("reports an untrusted issuer when no more specific TLS cause is present", () => {
expect(ids(baseDns, { authorized: false, authorizationError: "UNABLE_TO_VERIFY_LEAF_SIGNATURE", hostnameValid: true, daysRemaining: 90, _meta: okMeta }, baseHttp)).toContain("tls.verification");
});
it("reports malformed certificate validity dates without adding generic trust noise", () => {
const found = ids(baseDns, { authorized: false, authorizationError: "CERT_INVALID", hostnameValid: true, validityDatesValid: false, _meta: okMeta }, baseHttp);
expect(found).toContain("tls.validity_dates");
expect(found).not.toContain("tls.verification");
});
it("does not score expiry states from an incoherent certificate window", () => {
const found = ids(baseDns, {
authorized: true,
hostnameValid: true,
validFrom: "Jul 02 00:00:00 2026 GMT",
validTo: "Jul 01 00:00:00 2026 GMT",
validityDatesValid: false,
notYetValid: true,
daysRemaining: -1,
_meta: okMeta,
}, baseHttp);
expect(found).toContain("tls.validity_dates");
expect(found).not.toContain("tls.expired");
expect(found).not.toContain("tls.not_yet_valid");
expect(found).not.toContain("tls.expiry_critical");
expect(found).not.toContain("tls.expiry_warning");
});
it("reports HTTPS downgrades and loops as findings", () => {
const downgrade = ids(baseDns, baseTls, { ...baseHttp, httpsDowngrade: true, httpsChain: [{ status: 302, location: "http://example.com/", headers: secureHeaders }, { error: "HTTPS redirect downgrade is not allowed.", code: "HTTPS_DOWNGRADE" }], _meta: { status: "partial", errors: [{ probe: "HTTPS", message: "downgrade" }] } });
expect(downgrade).toContain("http.https_downgrade");
const loop = ids(baseDns, baseTls, { ...baseHttp, httpsChain: [{ status: 302, location: "/", headers: secureHeaders }, { error: "Redirect loop detected.", code: "REDIRECT_LOOP" }], _meta: { status: "partial", errors: [{ probe: "HTTPS", message: "loop" }] } });
expect(loop).toContain("http.redirect_loop");
});
it("surfaces redirects rejected by the target policy", () => {
expect(ids(baseDns, baseTls, {
...baseHttp,
httpsChain: [{ status: 302, location: "http://127.0.0.1/", headers: secureHeaders }, { code: "REDIRECT_TARGET_BLOCKED", error: "Target rejected" }],
_meta: { status: "partial", errors: [{ probe: "HTTPS", message: "target blocked" }] },
})).toContain("http.redirect_blocked");
});
it("classifies terminal HTTP status without using a green status", () => {
expect(ids(baseDns, baseTls, { ...baseHttp, httpsChain: [{ status: 404, headers: secureHeaders }] })).toContain("http.terminal_client_error");
expect(ids(baseDns, baseTls, { ...baseHttp, httpsChain: [{ status: 503, headers: secureHeaders }] })).toContain("http.terminal_server_error");
});
it("evaluates HSTS, CSP and nosniff values rather than presence alone", () => {
const findings = ids(baseDns, baseTls, { ...baseHttp, httpsChain: [{ status: 200, headers: { ...secureHeaders, "strict-transport-security": "max-age=0", "content-security-policy": "default-src * 'unsafe-eval'", "x-content-type-options": "text/plain" } }] });
expect(findings).toContain("http.hsts_disabled");
expect(findings).toContain("http.csp_weak");
expect(findings).toContain("http.nosniff_invalid");
});
it("does not require mail authentication for an explicit null MX", () => {
const findings = ids({ mx: [{ exchange: ".", priority: 0 }], spf: [], dmarc: [], _meta: okMeta }, baseTls, baseHttp);
expect(findings).not.toContain("dns.spf_missing");
expect(findings).not.toContain("dns.dmarc_missing");
});
it("reports multiple SPF records as a distinct configuration error", () => {
expect(ids({ mx: [{ exchange: "mail.example.com" }], spf: ["v=spf1 -all", "v=spf1 ~all"], dmarc: [], _meta: okMeta }, baseTls, baseHttp)).toContain("dns.spf_multiple");
});
it("reports malformed MX answers while preserving null-MX semantics", () => {
expect(ids({ mx: [{ exchange: "", priority: 10 }], spf: [], dmarc: [], _meta: okMeta }, baseTls, baseHttp)).toContain("dns.mx_invalid");
expect(ids({ mx: [{ exchange: "", priority: 0 }], spf: [], dmarc: [], _meta: okMeta }, baseTls, baseHttp)).not.toContain("dns.mx_invalid");
expect(ids({ mx: [{ exchange: ".", priority: 0 }], spf: [], dmarc: [], _meta: okMeta }, baseTls, baseHttp)).not.toContain("dns.mx_invalid");
});
it("recognizes DMARC pct=0 as non-enforcing without mislabeling the record invalid", () => {
const findings = ids({ mx: [{ exchange: "mail.example.com" }], spf: ["v=spf1 -all"], dmarc: ["v=DMARC1; p=reject; pct=0"], _meta: okMeta }, baseTls, baseHttp);
expect(findings).toContain("dns.dmarc_monitor");
expect(findings).not.toContain("dns.dmarc_invalid");
});
});