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 +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +236 -127
- package/dist/index.js.map +1 -1
- package/dist/stdio.js +363 -167
- package/dist/stdio.js.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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=
|
|
391
|
+
// Core (sum=54)
|
|
392
392
|
spf: { importance: 10 },
|
|
393
393
|
dmarc: { importance: 16 },
|
|
394
394
|
dkim: { importance: 10 },
|
|
395
|
-
dnssec: { importance:
|
|
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=
|
|
418
|
+
// Core (sum=63)
|
|
419
419
|
spf: { importance: 10 },
|
|
420
|
-
dmarc: { importance:
|
|
420
|
+
dmarc: { importance: 20 },
|
|
421
421
|
dkim: { importance: 12 },
|
|
422
|
-
dnssec: { importance:
|
|
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=
|
|
445
|
+
// Core (sum=29)
|
|
446
446
|
spf: { importance: 2 },
|
|
447
|
-
dmarc: { importance:
|
|
448
|
-
dkim: { importance:
|
|
449
|
-
dnssec: { importance:
|
|
450
|
-
ssl: { importance:
|
|
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=
|
|
472
|
+
// Core (sum=28)
|
|
473
473
|
spf: { importance: 0 },
|
|
474
474
|
dmarc: { importance: 0 },
|
|
475
475
|
dkim: { importance: 0 },
|
|
476
|
-
dnssec: { importance:
|
|
477
|
-
ssl: { importance:
|
|
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=
|
|
499
|
+
// Core (sum=15)
|
|
500
500
|
spf: { importance: 1 },
|
|
501
501
|
dmarc: { importance: 1 },
|
|
502
502
|
dkim: { importance: 1 },
|
|
503
|
-
dnssec: { importance:
|
|
504
|
-
ssl: { importance:
|
|
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:
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
|
|
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
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
6128
|
-
return
|
|
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) => {
|