blackveil-dns 2.0.6 → 2.0.9

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 CHANGED
@@ -59,7 +59,7 @@ Transport support:
59
59
  ## What you get
60
60
 
61
61
  - **57+ checks across 20 categories** — SPF, DMARC, DKIM, DNSSEC, SSL/TLS, MTA-STS, NS, CAA, MX, BIMI, TLS-RPT, subdomain takeover, lookalike domains, HTTP security headers, DANE, shadow domains, TXT hygiene, MX reputation, SRV, zone hygiene
62
- - **Maturity staging** — Stage 0-4 classification (Unprotected to Hardened) with next steps
62
+ - **Maturity staging** — Stage 0-4 classification (Unprotected to Hardened) with score-based capping to prevent inflated labels
63
63
  - **Trust surface analysis** — detects shared SaaS platforms (Google, M365, SendGrid) and cross-references DMARC enforcement to determine real exposure
64
64
  - **Guided remediation** — `generate_fix_plan` produces prioritized actions; record generators output ready-to-publish SPF, DMARC, DKIM, and MTA-STS records
65
65
  - **Spoofability scoring** — `assess_spoofability` computes a composite 0-100 email spoofability score from SPF trust surface, DMARC enforcement, and DKIM coverage with interaction multipliers
@@ -209,8 +209,9 @@ claude mcp add --transport http blackveil-dns https://dns-mcp.blackveilsecurity.
209
209
  {
210
210
  "mcpServers": {
211
211
  "blackveil-dns": {
212
- "command": "npx",
212
+ "command": "/opt/homebrew/bin/npx",
213
213
  "args": [
214
+ "-y",
214
215
  "mcp-remote",
215
216
  "https://dns-mcp.blackveilsecurity.com/mcp",
216
217
  "--header",
@@ -270,6 +271,13 @@ claude mcp add-json blackveil-dns \
270
271
 
271
272
  > On macOS GUI apps, `npx` may not resolve from `PATH`; if Homebrew is installed elsewhere, replace `/opt/homebrew/bin/npx` with your actual `npx` path. After editing the config, fully restart Claude Desktop. If you already have other servers, merge `"blackveil-dns"` into your existing `"mcpServers"` object — don't paste a second `{ }` wrapper.
272
273
 
274
+ If you operate this Worker with bearer auth enabled, also register the same value as the production Worker secret:
275
+
276
+ ```bash
277
+ npx wrangler versions secret put BV_API_KEY -c .dev/wrangler.deploy.jsonc
278
+ npx wrangler versions deploy -c .dev/wrangler.deploy.jsonc --yes
279
+ ```
280
+
273
281
  </details>
274
282
 
275
283
  <details>
package/dist/index.d.ts CHANGED
@@ -159,7 +159,7 @@ declare function sanitizeDomain(input: string): string;
159
159
  declare function sanitizeInput(input: string, maxLength?: number): string;
160
160
 
161
161
  /** Server version — keep in sync with package.json */
162
- declare const SERVER_VERSION = "2.0.6";
162
+ declare const SERVER_VERSION = "2.0.9";
163
163
 
164
164
  /**
165
165
  * Check BIMI records for a domain.
package/dist/index.js CHANGED
@@ -388,11 +388,11 @@ function createFinding(category, title, severity, detail, metadata) {
388
388
  }
389
389
  var PROFILE_WEIGHTS = {
390
390
  mail_enabled: {
391
- // Core (sum=52)
391
+ // Core (sum=54)
392
392
  spf: { importance: 10 },
393
393
  dmarc: { importance: 16 },
394
394
  dkim: { importance: 10 },
395
- dnssec: { importance: 8 },
395
+ dnssec: { importance: 10 },
396
396
  ssl: { importance: 8 },
397
397
  // Protective (sum=20)
398
398
  subdomain_takeover: { importance: 4 },
@@ -415,11 +415,11 @@ var PROFILE_WEIGHTS = {
415
415
  zone_hygiene: { importance: 0 }
416
416
  },
417
417
  enterprise_mail: {
418
- // Core (sum=58)
418
+ // Core (sum=63)
419
419
  spf: { importance: 10 },
420
- dmarc: { importance: 18 },
420
+ dmarc: { importance: 20 },
421
421
  dkim: { importance: 12 },
422
- dnssec: { importance: 10 },
422
+ dnssec: { importance: 13 },
423
423
  ssl: { importance: 8 },
424
424
  // Protective (sum=22)
425
425
  subdomain_takeover: { importance: 5 },
@@ -442,12 +442,12 @@ var PROFILE_WEIGHTS = {
442
442
  zone_hygiene: { importance: 0 }
443
443
  },
444
444
  non_mail: {
445
- // Core (sum=19)
445
+ // Core (sum=29)
446
446
  spf: { importance: 2 },
447
- dmarc: { importance: 2 },
448
- dkim: { importance: 1 },
449
- dnssec: { importance: 8 },
450
- ssl: { importance: 8 },
447
+ dmarc: { importance: 3 },
448
+ dkim: { importance: 2 },
449
+ dnssec: { importance: 12 },
450
+ ssl: { importance: 10 },
451
451
  // Protective (sum=24)
452
452
  subdomain_takeover: { importance: 6 },
453
453
  http_security: { importance: 6 },
@@ -469,12 +469,12 @@ var PROFILE_WEIGHTS = {
469
469
  zone_hygiene: { importance: 0 }
470
470
  },
471
471
  web_only: {
472
- // Core (sum=20)
472
+ // Core (sum=28)
473
473
  spf: { importance: 0 },
474
474
  dmarc: { importance: 0 },
475
475
  dkim: { importance: 0 },
476
- dnssec: { importance: 8 },
477
- ssl: { importance: 12 },
476
+ dnssec: { importance: 14 },
477
+ ssl: { importance: 14 },
478
478
  // Protective (sum=24)
479
479
  subdomain_takeover: { importance: 6 },
480
480
  http_security: { importance: 8 },
@@ -496,12 +496,12 @@ var PROFILE_WEIGHTS = {
496
496
  zone_hygiene: { importance: 0 }
497
497
  },
498
498
  minimal: {
499
- // Core (sum=10)
499
+ // Core (sum=15)
500
500
  spf: { importance: 1 },
501
501
  dmarc: { importance: 1 },
502
502
  dkim: { importance: 1 },
503
- dnssec: { importance: 3 },
504
- ssl: { importance: 4 },
503
+ dnssec: { importance: 5 },
504
+ ssl: { importance: 7 },
505
505
  // Protective (sum=10)
506
506
  subdomain_takeover: { importance: 2 },
507
507
  http_security: { importance: 2 },
@@ -524,11 +524,11 @@ var PROFILE_WEIGHTS = {
524
524
  }
525
525
  };
526
526
  var PROFILE_CRITICAL_CATEGORIES = {
527
- mail_enabled: ["spf", "dmarc", "dkim", "ssl", "dnssec"],
528
- enterprise_mail: ["spf", "dmarc", "dkim", "ssl", "dnssec"],
527
+ mail_enabled: ["spf", "dmarc", "dkim", "ssl", "dnssec", "subdomain_takeover"],
528
+ enterprise_mail: ["spf", "dmarc", "dkim", "ssl", "dnssec", "subdomain_takeover"],
529
529
  non_mail: ["ssl", "dnssec", "http_security", "subdomain_takeover", "dane_https"],
530
530
  web_only: ["ssl", "dnssec", "http_security", "subdomain_takeover", "dane_https"],
531
- minimal: ["ssl", "dnssec"]
531
+ minimal: ["ssl", "dnssec", "subdomain_takeover"]
532
532
  };
533
533
  var PROFILE_EMAIL_BONUS_ELIGIBLE = {
534
534
  mail_enabled: true,
@@ -634,7 +634,7 @@ function getProfileWeights(profile, config) {
634
634
  }
635
635
  var DEFAULT_SCORING_CONFIG = {
636
636
  tierSplit: { core: 70, protective: 20, hardening: 10 },
637
- coreWeights: { dmarc: 16, dkim: 10, spf: 10, dnssec: 8, ssl: 8 },
637
+ coreWeights: { dmarc: 16, dkim: 10, spf: 10, dnssec: 10, ssl: 8 },
638
638
  protectiveWeights: {
639
639
  subdomain_takeover: 4,
640
640
  http_security: 3,
@@ -663,21 +663,142 @@ var DEFAULT_SCORING_CONFIG = {
663
663
  dPlus: 56,
664
664
  d: 50
665
665
  }};
666
+ var DEFAULT_EMAIL_BONUS_KEYS = {
667
+ spf: "spf",
668
+ dkim: "dkim",
669
+ dmarc: "dmarc"
670
+ };
666
671
  function clampPercent(score) {
667
672
  return Math.max(0, Math.min(100, score));
668
673
  }
669
- function computeProviderConfidenceModifier(findings) {
670
- const confidences = [];
671
- for (const finding of findings) {
672
- const confidence = finding.metadata?.providerConfidence;
673
- if (typeof confidence === "number" && Number.isFinite(confidence)) {
674
- confidences.push(Math.max(0, Math.min(1, confidence)));
674
+ function computeProviderModifier(providerConfidence) {
675
+ if (!providerConfidence) return 0;
676
+ const values = Object.values(providerConfidence).filter(
677
+ (v) => typeof v === "number" && Number.isFinite(v)
678
+ );
679
+ if (values.length === 0) return 0;
680
+ const clamped = values.map((v) => Math.max(0, Math.min(1, v)));
681
+ const avg = clamped.reduce((sum, v) => sum + v, 0) / clamped.length;
682
+ const centered = avg - 0.5;
683
+ return Math.round(centered * 10);
684
+ }
685
+ function computeGenericScore(input, config) {
686
+ const cfg = config ?? DEFAULT_SCORING_CONFIG;
687
+ const tierSplit = cfg.tierSplit;
688
+ const thresholds = cfg.thresholds;
689
+ const transient = input.transientFailures ?? {};
690
+ const emailKeys = input.emailBonusKeys ?? DEFAULT_EMAIL_BONUS_KEYS;
691
+ const coreWeights = {};
692
+ const protectiveWeights = {};
693
+ const hardeningKeys = [];
694
+ for (const [key, tier] of Object.entries(input.tierMap)) {
695
+ if (transient[key]) continue;
696
+ const weight = input.weights[key] ?? 0;
697
+ if (tier === "core") {
698
+ coreWeights[key] = weight;
699
+ } else if (tier === "protective") {
700
+ protectiveWeights[key] = weight;
701
+ } else if (tier === "hardening") {
702
+ hardeningKeys.push(key);
703
+ }
704
+ }
705
+ const coreMax = Object.values(coreWeights).reduce((sum, w) => sum + w, 0);
706
+ let coreEarned = 0;
707
+ for (const [key, weight] of Object.entries(coreWeights)) {
708
+ if (weight === 0) continue;
709
+ const rawScore = clampPercent(input.categoryScores[key] ?? 100);
710
+ const effectiveScore = input.missingControls[key] ? 0 : rawScore;
711
+ coreEarned += effectiveScore / 100 * weight;
712
+ }
713
+ const corePct = coreMax > 0 ? coreEarned / coreMax : 1;
714
+ const corePoints = corePct * tierSplit.core;
715
+ const protectiveMax = Object.values(protectiveWeights).reduce((sum, w) => sum + w, 0);
716
+ let protectiveEarned = 0;
717
+ for (const [key, weight] of Object.entries(protectiveWeights)) {
718
+ if (weight === 0) continue;
719
+ const rawScore = clampPercent(input.categoryScores[key] ?? 100);
720
+ protectiveEarned += rawScore / 100 * weight;
721
+ }
722
+ const protectivePct = protectiveMax > 0 ? protectiveEarned / protectiveMax : 1;
723
+ const protectivePoints = protectivePct * tierSplit.protective;
724
+ const submittedHardeningKeys = hardeningKeys.filter((key) => key in input.hardeningPassed);
725
+ const passedCount = submittedHardeningKeys.filter((key) => input.hardeningPassed[key]).length;
726
+ const hardeningPoints = submittedHardeningKeys.length > 0 ? passedCount / submittedHardeningKeys.length * tierSplit.hardening : 0;
727
+ const base = corePoints + protectivePoints + hardeningPoints;
728
+ let emailBonus = 0;
729
+ if (input.emailBonusEligible) {
730
+ const spfKey = emailKeys.spf;
731
+ const dkimKey = emailKeys.dkim;
732
+ const dmarcKey = emailKeys.dmarc;
733
+ const spfScore = input.categoryScores[spfKey] ?? 0;
734
+ const spfStrong = !input.missingControls[spfKey] && spfScore >= thresholds.spfStrongThreshold;
735
+ const dkimNotMissing = !input.missingControls[dkimKey];
736
+ const dmarcScore = input.categoryScores[dmarcKey];
737
+ const dmarcPresent = dmarcScore !== void 0 && !input.missingControls[dmarcKey];
738
+ if (spfStrong && dkimNotMissing && dmarcPresent) {
739
+ if (dmarcScore >= 90) {
740
+ emailBonus = thresholds.emailBonusFull;
741
+ } else if (dmarcScore >= 70) {
742
+ emailBonus = thresholds.emailBonusMid;
743
+ } else {
744
+ emailBonus = thresholds.emailBonusPartial;
745
+ }
675
746
  }
676
747
  }
677
- if (confidences.length === 0) return 0;
678
- const avgConfidence = confidences.reduce((sum, value) => sum + value, 0) / confidences.length;
679
- const centered = avgConfidence - 0.5;
680
- return Math.round(centered * 10);
748
+ const providerModifier = computeProviderModifier(input.providerConfidence);
749
+ const counts = input.findingSeverityCounts;
750
+ const criticalPenalty = counts && counts.critical > 0 ? thresholds.criticalOverallPenalty : 0;
751
+ const preCeiling = clampPercent(Math.round(base) + emailBonus + providerModifier - criticalPenalty);
752
+ const criticalGaps = [];
753
+ for (const key of input.criticalCategories) {
754
+ if (input.missingControls[key]) {
755
+ criticalGaps.push(key);
756
+ }
757
+ }
758
+ const overall = criticalGaps.length > 0 ? Math.min(preCeiling, thresholds.criticalGapCeiling) : preCeiling;
759
+ const grade = scoreToGrade(overall, config);
760
+ const summary = buildSummary(counts, grade);
761
+ const filledScores = {};
762
+ for (const key of Object.keys(input.tierMap)) {
763
+ filledScores[key] = input.categoryScores[key] ?? 100;
764
+ }
765
+ for (const key of Object.keys(input.categoryScores)) {
766
+ if (!(key in filledScores)) {
767
+ filledScores[key] = input.categoryScores[key];
768
+ }
769
+ }
770
+ return {
771
+ overall,
772
+ grade,
773
+ categoryScores: filledScores,
774
+ summary,
775
+ tierBreakdown: {
776
+ core: corePoints,
777
+ protective: protectivePoints,
778
+ hardening: hardeningPoints
779
+ },
780
+ emailBonus,
781
+ criticalGaps,
782
+ providerModifier,
783
+ criticalPenalty
784
+ };
785
+ }
786
+ function buildSummary(counts, grade) {
787
+ if (!counts) {
788
+ return `Excellent! No security issues found. Grade: ${grade}`;
789
+ }
790
+ const { critical, high, medium, low } = counts;
791
+ const totalNonInfo = critical + high + medium + low;
792
+ if (totalNonInfo === 0) {
793
+ return `Excellent! No security issues found. Grade: ${grade}`;
794
+ }
795
+ if (critical > 0) {
796
+ return `${critical} critical issue(s) found requiring immediate attention. Grade: ${grade}`;
797
+ }
798
+ if (high > 0) {
799
+ return `${high} high-severity issue(s) found. Grade: ${grade}`;
800
+ }
801
+ return `${totalNonInfo} issue(s) found. Grade: ${grade}`;
681
802
  }
682
803
  function scoreToGrade(score, config) {
683
804
  const g = config?.grades ?? DEFAULT_SCORING_CONFIG.grades;
@@ -692,6 +813,75 @@ function scoreToGrade(score, config) {
692
813
  return "F";
693
814
  }
694
815
  var DEFAULT_CRITICAL_CATEGORIES = ["spf", "dmarc", "dkim", "ssl"];
816
+ function buildGenericContext(results, categoryScores, allFindings, context, config) {
817
+ const weights = {};
818
+ if (context) {
819
+ for (const category of Object.keys(context.weights)) {
820
+ weights[category] = context.weights[category].importance;
821
+ }
822
+ } else {
823
+ for (const [key, value] of Object.entries(config.coreWeights)) {
824
+ weights[key] = value;
825
+ }
826
+ for (const [key, value] of Object.entries(config.protectiveWeights)) {
827
+ weights[key] = value;
828
+ }
829
+ for (const cat of Object.keys(CATEGORY_TIERS)) {
830
+ if (CATEGORY_TIERS[cat] === "hardening" && !(cat in weights)) {
831
+ weights[cat] = 0;
832
+ }
833
+ }
834
+ }
835
+ const missingControls = {};
836
+ const resultMap = /* @__PURE__ */ new Map();
837
+ for (const result of results) {
838
+ resultMap.set(result.category, result);
839
+ if (scoreIndicatesMissingControl(result.findings)) {
840
+ missingControls[result.category] = true;
841
+ }
842
+ }
843
+ const hardeningPassed = {};
844
+ for (const cat of Object.keys(CATEGORY_TIERS)) {
845
+ if (CATEGORY_TIERS[cat] === "hardening") {
846
+ const result = resultMap.get(cat);
847
+ hardeningPassed[cat] = !!(result && result.passed);
848
+ }
849
+ }
850
+ const providerConfidence = {};
851
+ for (const finding of allFindings) {
852
+ const confidence = finding.metadata?.providerConfidence;
853
+ if (typeof confidence === "number" && Number.isFinite(confidence)) {
854
+ const key = `_finding_${Object.keys(providerConfidence).length}`;
855
+ providerConfidence[key] = confidence;
856
+ }
857
+ }
858
+ const verifiedCriticalCount = allFindings.filter(
859
+ (f) => f.severity === "critical" && inferFindingConfidence(f) === "verified"
860
+ ).length;
861
+ const findingSeverityCounts = {
862
+ critical: verifiedCriticalCount,
863
+ high: allFindings.filter((f) => f.severity === "high").length,
864
+ medium: allFindings.filter((f) => f.severity === "medium").length,
865
+ low: allFindings.filter((f) => f.severity === "low").length,
866
+ info: allFindings.filter((f) => f.severity === "info").length
867
+ };
868
+ const criticalCategories = context ? PROFILE_CRITICAL_CATEGORIES[context.profile] : DEFAULT_CRITICAL_CATEGORIES;
869
+ let emailBonusEligible = context ? PROFILE_EMAIL_BONUS_ELIGIBLE[context.profile] : true;
870
+ if (!resultMap.has("spf") || !resultMap.has("dmarc")) {
871
+ emailBonusEligible = false;
872
+ }
873
+ return {
874
+ categoryScores: { ...categoryScores },
875
+ tierMap: { ...CATEGORY_TIERS },
876
+ weights,
877
+ missingControls,
878
+ hardeningPassed,
879
+ criticalCategories: [...criticalCategories],
880
+ emailBonusEligible,
881
+ providerConfidence: Object.keys(providerConfidence).length > 0 ? providerConfidence : void 0,
882
+ findingSeverityCounts
883
+ };
884
+ }
695
885
  function computeScanScore(results, context, config) {
696
886
  const partialScores = {};
697
887
  const allFindings = [];
@@ -700,10 +890,6 @@ function computeScanScore(results, context, config) {
700
890
  }
701
891
  const categoryScores = partialScores;
702
892
  const cfg = config ?? DEFAULT_SCORING_CONFIG;
703
- const tierSplit = cfg.tierSplit;
704
- const spfStrongThreshold = cfg.thresholds.spfStrongThreshold;
705
- const criticalOverallPenalty = cfg.thresholds.criticalOverallPenalty;
706
- const criticalGapCeiling = cfg.thresholds.criticalGapCeiling;
707
893
  if (results.length === 0) {
708
894
  return {
709
895
  overall: 100,
@@ -717,102 +903,24 @@ function computeScanScore(results, context, config) {
717
903
  categoryScores[result.category] = result.score;
718
904
  allFindings.push(...result.findings);
719
905
  }
720
- const activeCoreWeights = {};
721
- const activeProtectiveWeights = {};
722
- if (context) {
723
- for (const category of Object.keys(context.weights)) {
724
- const tier = CATEGORY_TIERS[category];
725
- const weight = context.weights[category].importance;
726
- if (tier === "core") {
727
- activeCoreWeights[category] = weight;
728
- } else if (tier === "protective") {
729
- activeProtectiveWeights[category] = weight;
730
- }
731
- }
732
- } else {
733
- Object.assign(activeCoreWeights, cfg.coreWeights);
734
- Object.assign(activeProtectiveWeights, cfg.protectiveWeights);
735
- }
736
- const coreMax = Object.values(activeCoreWeights).reduce((sum, w) => sum + w, 0);
737
- let coreEarned = 0;
738
- for (const [category, weight] of Object.entries(activeCoreWeights)) {
739
- if (weight === 0) continue;
740
- const cat = category;
741
- const result = results.find((r) => r.category === cat);
742
- const rawScore = result ? clampPercent(result.score) : 100;
743
- const effectiveScore = result && scoreIndicatesMissingControl(result.findings) ? 0 : rawScore;
744
- coreEarned += effectiveScore / 100 * weight;
745
- }
746
- const corePct = coreMax > 0 ? coreEarned / coreMax : 1;
747
- const protectiveMax = Object.values(activeProtectiveWeights).reduce((sum, w) => sum + w, 0);
748
- let protectiveEarned = 0;
749
- for (const [category, weight] of Object.entries(activeProtectiveWeights)) {
750
- if (weight === 0) continue;
751
- const cat = category;
752
- const result = results.find((r) => r.category === cat);
753
- const rawScore = result ? clampPercent(result.score) : 100;
754
- protectiveEarned += rawScore / 100 * weight;
755
- }
756
- const protectivePct = protectiveMax > 0 ? protectiveEarned / protectiveMax : 1;
757
- const hardeningCategories = Object.keys(CATEGORY_TIERS).filter(
758
- (cat) => CATEGORY_TIERS[cat] === "hardening"
759
- );
760
- const hardeningCount = hardeningCategories.length;
761
- let passedHardeningCount = 0;
762
- for (const cat of hardeningCategories) {
763
- const result = results.find((r) => r.category === cat);
764
- if (result && result.passed) {
765
- passedHardeningCount++;
766
- }
767
- }
768
- const hardeningPts = hardeningCount > 0 ? passedHardeningCount / hardeningCount * tierSplit.hardening : 0;
769
- const base = corePct * tierSplit.core + protectivePct * tierSplit.protective + hardeningPts;
770
- const emailBonusEligible = context ? PROFILE_EMAIL_BONUS_ELIGIBLE[context.profile] : true;
771
- const spfResult = results.find((result) => result.category === "spf");
772
- const dkimResult = results.find((result) => result.category === "dkim");
773
- const dmarcResult = results.find((result) => result.category === "dmarc");
774
- const spfStrong = !!spfResult && !scoreIndicatesMissingControl(spfResult.findings) && spfResult.score >= spfStrongThreshold;
775
- const dkimNotDeterministicallyMissing = !dkimResult || !scoreIndicatesMissingControl(dkimResult.findings);
776
- const dmarcPresent = !!dmarcResult && !scoreIndicatesMissingControl(dmarcResult.findings);
777
- let emailBonus = 0;
778
- if (emailBonusEligible && spfStrong && dkimNotDeterministicallyMissing && dmarcPresent && dmarcResult) {
779
- if (dmarcResult.score >= 90) {
780
- emailBonus = cfg.thresholds.emailBonusFull;
781
- } else if (dmarcResult.score >= 70) {
782
- emailBonus = cfg.thresholds.emailBonusMid;
783
- } else {
784
- emailBonus = cfg.thresholds.emailBonusPartial;
785
- }
786
- }
787
- const providerModifier = computeProviderConfidenceModifier(allFindings);
788
- const verifiedCriticalCount = allFindings.filter(
789
- (finding) => finding.severity === "critical" && inferFindingConfidence(finding) === "verified"
790
- ).length;
791
- const criticalPenalty = verifiedCriticalCount > 0 ? criticalOverallPenalty : 0;
792
- const preCeiling = clampPercent(Math.round(base) + emailBonus + providerModifier - criticalPenalty);
793
- const criticalCategories = context ? PROFILE_CRITICAL_CATEGORIES[context.profile] : DEFAULT_CRITICAL_CATEGORIES;
794
- const hasCriticalGap = criticalCategories.some((cat) => {
795
- const result = results.find((r) => r.category === cat);
796
- return result && scoreIndicatesMissingControl(result.findings);
797
- });
798
- const overall = hasCriticalGap ? Math.min(preCeiling, criticalGapCeiling) : preCeiling;
799
- const grade = scoreToGrade(overall, config);
800
- const criticalCount = allFindings.filter((finding) => finding.severity === "critical").length;
801
- const highCount = allFindings.filter((finding) => finding.severity === "high").length;
802
- const totalIssues = allFindings.filter((finding) => finding.severity !== "info").length;
906
+ const genericContext = buildGenericContext(results, categoryScores, allFindings, context, cfg);
907
+ const genericResult = computeGenericScore(genericContext, config);
908
+ const criticalCount = allFindings.filter((f) => f.severity === "critical").length;
909
+ const highCount = allFindings.filter((f) => f.severity === "high").length;
910
+ const totalIssues = allFindings.filter((f) => f.severity !== "info").length;
803
911
  let summary;
804
912
  if (totalIssues === 0) {
805
- summary = `Excellent! No security issues found. Grade: ${grade}`;
913
+ summary = `Excellent! No security issues found. Grade: ${genericResult.grade}`;
806
914
  } else if (criticalCount > 0) {
807
- summary = `${criticalCount} critical issue(s) found requiring immediate attention. Grade: ${grade}`;
915
+ summary = `${criticalCount} critical issue(s) found requiring immediate attention. Grade: ${genericResult.grade}`;
808
916
  } else if (highCount > 0) {
809
- summary = `${highCount} high-severity issue(s) found. Grade: ${grade}`;
917
+ summary = `${highCount} high-severity issue(s) found. Grade: ${genericResult.grade}`;
810
918
  } else {
811
- summary = `${totalIssues} issue(s) found. Grade: ${grade}`;
919
+ summary = `${totalIssues} issue(s) found. Grade: ${genericResult.grade}`;
812
920
  }
813
921
  return {
814
- overall,
815
- grade,
922
+ overall: genericResult.overall,
923
+ grade: genericResult.grade,
816
924
  categoryScores,
817
925
  findings: allFindings,
818
926
  summary
@@ -1017,7 +1125,7 @@ function sanitizeInput(input, maxLength = 500) {
1017
1125
  }
1018
1126
 
1019
1127
  // src/lib/server-version.ts
1020
- var SERVER_VERSION = "2.0.6";
1128
+ var SERVER_VERSION = "2.0.9";
1021
1129
 
1022
1130
  // packages/dns-checks/dist/index.js
1023
1131
  var SEVERITY_PENALTIES2 = {
@@ -5757,7 +5865,8 @@ function isPlainObject(value) {
5757
5865
  return typeof value === "object" && value !== null && !Array.isArray(value);
5758
5866
  }
5759
5867
  function sanitizeString(value) {
5760
- return value.length > MAX_LOG_STRING_LENGTH ? `${value.slice(0, MAX_LOG_STRING_LENGTH)}...` : value;
5868
+ const stripped = value.replace(/[\x00-\x08\x0a-\x1f\x7f]/g, " ");
5869
+ return stripped.length > MAX_LOG_STRING_LENGTH ? `${stripped.slice(0, MAX_LOG_STRING_LENGTH)}...` : stripped;
5761
5870
  }
5762
5871
  function sanitizeLogValue(value, key) {
5763
5872
  if (key && isSensitiveKey(key)) {
@@ -6124,8 +6233,8 @@ async function checkApexDmarcPolicy(domain) {
6124
6233
  }
6125
6234
  }
6126
6235
  function isMissingRecordFinding(finding) {
6127
- const text = `${finding.title} ${finding.detail}`.toLowerCase();
6128
- return /(no\s+.+\s+record|missing|not\s+found|no\s+mta-sts|no\s+dkim)/.test(text);
6236
+ const title = finding.title.toLowerCase();
6237
+ return title.includes("missing") || title.includes("not found") || title.includes("no mta-sts") || title.includes("no dkim") || /^no\s+\S+\s+record/.test(title);
6129
6238
  }
6130
6239
  function clarifyMtaStsForMailDomain(domain, results) {
6131
6240
  return results.map((result) => {