blackveil-dns 1.4.0

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.
@@ -0,0 +1,506 @@
1
+ /** Standard DNS record type codes */
2
+ declare const RecordType: {
3
+ readonly A: 1;
4
+ readonly AAAA: 28;
5
+ readonly CNAME: 5;
6
+ readonly MX: 15;
7
+ readonly TXT: 16;
8
+ readonly NS: 2;
9
+ readonly SOA: 6;
10
+ readonly CAA: 257;
11
+ readonly TLSA: 52;
12
+ readonly DNSKEY: 48;
13
+ readonly DS: 43;
14
+ readonly RRSIG: 46;
15
+ readonly PTR: 12;
16
+ readonly SRV: 33;
17
+ };
18
+ type RecordTypeName = keyof typeof RecordType;
19
+ /** A single DNS answer record from the DoH JSON response */
20
+ interface DnsAnswer {
21
+ name: string;
22
+ type: number;
23
+ TTL: number;
24
+ data: string;
25
+ }
26
+ /** A single DNS authority record */
27
+ interface DnsAuthority {
28
+ name: string;
29
+ type: number;
30
+ TTL: number;
31
+ data: string;
32
+ }
33
+ /** Cloudflare DoH JSON wire-format response */
34
+ interface DohResponse {
35
+ Status: number;
36
+ TC: boolean;
37
+ RD: boolean;
38
+ RA: boolean;
39
+ AD: boolean;
40
+ CD: boolean;
41
+ Question: Array<{
42
+ name: string;
43
+ type: number;
44
+ }>;
45
+ Answer?: DnsAnswer[];
46
+ Authority?: DnsAuthority[];
47
+ }
48
+ /** Configuration for a custom secondary DoH resolver (e.g., bv-dns on Oracle Cloud). */
49
+ interface SecondaryDohConfig {
50
+ /** DoH endpoint URL (e.g. https://harlan.blackveilsecurity.com/dns-query) */
51
+ endpoint: string;
52
+ /** Optional auth token sent as X-BV-Token header */
53
+ token?: string;
54
+ }
55
+ interface QueryDnsOptions {
56
+ timeoutMs?: number;
57
+ retries?: number;
58
+ confirmWithSecondaryOnEmpty?: boolean;
59
+ /** When true, skip secondary resolver confirmation on empty results. Used in scan context for speed. */
60
+ skipSecondaryConfirmation?: boolean;
61
+ /** Scan-scoped DNS query cache. Stores Promises keyed by `domain:type:dnssecCheck` to deduplicate concurrent and sequential identical queries within a single scan. */
62
+ queryCache?: Map<string, Promise<DohResponse>>;
63
+ /** Custom secondary DoH resolver. When set, used instead of Google DoH for empty-result confirmation. Falls back to Google if this resolver fails. */
64
+ secondaryDoh?: SecondaryDohConfig;
65
+ }
66
+
67
+ /** Error thrown when a DNS query fails */
68
+ declare class DnsQueryError extends Error {
69
+ readonly domain: string;
70
+ readonly recordType: string;
71
+ readonly status?: number | undefined;
72
+ constructor(message: string, domain: string, recordType: string, status?: number | undefined);
73
+ }
74
+ /**
75
+ * Query Cloudflare DoH for DNS records.
76
+ *
77
+ * When `opts.queryCache` is provided, deduplicates concurrent and sequential
78
+ * identical queries within a single scan by caching the Promise keyed by
79
+ * `domain:type:dnssecCheck`. Failed queries are evicted so retries can re-attempt.
80
+ *
81
+ * @param domain - The domain name to query
82
+ * @param type - DNS record type name (e.g. "TXT", "MX", "A")
83
+ * @param dnssecCheck - If true, sets the CD=0 flag to request DNSSEC validation
84
+ * @returns The full DoH JSON response
85
+ */
86
+ declare function queryDns(domain: string, type: RecordTypeName, dnssecCheck?: boolean, opts?: QueryDnsOptions): Promise<DohResponse>;
87
+
88
+ /** Parsed CAA record with flags, tag, and value */
89
+ interface CaaRecord {
90
+ flags: number;
91
+ tag: string;
92
+ value: string;
93
+ }
94
+ /**
95
+ * Query DNS and return just the answer data strings.
96
+ * Returns an empty array if no answers are found.
97
+ */
98
+ declare function queryDnsRecords(domain: string, type: RecordTypeName, opts?: QueryDnsOptions): Promise<string[]>;
99
+ /**
100
+ * Query TXT records, concatenate multi-string values, and unescape DNS
101
+ * presentation-format backslash sequences.
102
+ *
103
+ * Cloudflare DoH returns TXT data with surrounding quotes and multiple
104
+ * strings separated by `" "`. Per RFC 7208 §3.3, multi-string TXT records
105
+ * MUST be concatenated without adding spaces. Some nameservers also emit
106
+ * RFC 1035 §5.1 backslash escapes (e.g. `\;` for a literal semicolon);
107
+ * we unescape both `\X` (single-char) and `\DDD` (decimal octet) forms.
108
+ */
109
+ declare function queryTxtRecords(domain: string, opts?: QueryDnsOptions): Promise<string[]>;
110
+ /**
111
+ * Parse a single CAA record data string.
112
+ * Handles both human-readable format (e.g. `0 issue "letsencrypt.org"`)
113
+ * and Cloudflare DoH hex wire format (e.g. `\# 19 00 05 69 73 73 75 65...`).
114
+ *
115
+ * Wire format bytes: flags(1) + tag_length(1) + tag(tag_length) + value(rest)
116
+ */
117
+ declare function parseCaaRecord(data: string): CaaRecord | null;
118
+ /**
119
+ * Query CAA records and parse them into structured objects.
120
+ * Handles both human-readable and hex wire format from DoH.
121
+ */
122
+ declare function queryCaaRecords(domain: string, opts?: QueryDnsOptions): Promise<CaaRecord[]>;
123
+ /**
124
+ * Query MX records and parse them into priority + exchange pairs.
125
+ */
126
+ declare function queryMxRecords(domain: string, opts?: QueryDnsOptions): Promise<Array<{
127
+ priority: number;
128
+ exchange: string;
129
+ }>>;
130
+
131
+ type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
132
+ type FindingConfidence = 'deterministic' | 'heuristic' | 'verified';
133
+ type CheckCategory = 'spf' | 'dmarc' | 'dkim' | 'dnssec' | 'ssl' | 'mta_sts' | 'ns' | 'caa' | 'subdomain_takeover' | 'mx' | 'bimi' | 'tlsrpt' | 'lookalikes' | 'shadow_domains' | 'txt_hygiene' | 'http_security' | 'dane' | 'mx_reputation' | 'srv' | 'zone_hygiene';
134
+ interface Finding {
135
+ category: CheckCategory;
136
+ title: string;
137
+ severity: Severity;
138
+ detail: string;
139
+ metadata?: Record<string, unknown>;
140
+ }
141
+ interface CheckResult {
142
+ category: CheckCategory;
143
+ passed: boolean;
144
+ score: number;
145
+ findings: Finding[];
146
+ }
147
+ interface ScanScore {
148
+ overall: number;
149
+ grade: string;
150
+ categoryScores: Record<CheckCategory, number>;
151
+ findings: Finding[];
152
+ summary: string;
153
+ }
154
+ /** Display/UI weight distribution for categories. NOT used in scoring — see IMPORTANCE_WEIGHTS for actual scoring weights. Exists for category registry and display purposes only. */
155
+ declare const CATEGORY_DISPLAY_WEIGHTS: Record<CheckCategory, number>;
156
+ /** Severity penalty multipliers applied to the category score */
157
+ declare const SEVERITY_PENALTIES: Record<Severity, number>;
158
+ /**
159
+ * Infer how strongly a finding can be trusted based on available evidence.
160
+ * - verified: explicit proof (currently only supported on takeover checks)
161
+ * - heuristic: signal-based or partial-evidence checks
162
+ * - deterministic: direct record/protocol validation
163
+ */
164
+ declare function inferFindingConfidence(finding: Finding): FindingConfidence;
165
+ /**
166
+ * Compute the score for a single check category based on its findings.
167
+ * Starts at 100 and deducts points based on finding severities.
168
+ */
169
+ declare function computeCategoryScore(findings: Finding[]): number;
170
+ /**
171
+ * Build a CheckResult from a category and its findings.
172
+ */
173
+ declare function buildCheckResult(category: CheckCategory, findings: Finding[]): CheckResult;
174
+ /**
175
+ * Create a finding object with the given parameters.
176
+ */
177
+ declare function createFinding(category: CheckCategory, title: string, severity: Severity, detail: string, metadata?: Record<string, unknown>): Finding;
178
+
179
+ /**
180
+ * Runtime scoring configuration.
181
+ *
182
+ * All scoring weights, thresholds, and tuning constants are configurable
183
+ * via the `SCORING_CONFIG` environment variable (JSON string). The open-source
184
+ * codebase ships with reasonable defaults; production deployments can override
185
+ * any subset of values.
186
+ *
187
+ * Parse once at request entry and thread through the call chain — never
188
+ * re-parse per tool call.
189
+ */
190
+
191
+ /** All tunable scoring parameters. */
192
+ interface ScoringConfig {
193
+ /** Base importance weights per check category (used when no profile context). */
194
+ weights: Record<CheckCategory, number>;
195
+ /** Per-profile importance weights. */
196
+ profileWeights: Record<DomainProfile, Record<CheckCategory, number>>;
197
+ /** Scoring thresholds and constants. */
198
+ thresholds: {
199
+ emailBonusImportance: number;
200
+ spfStrongThreshold: number;
201
+ criticalOverallPenalty: number;
202
+ criticalGapCeiling: number;
203
+ };
204
+ /** Grade boundaries (minimum score for each grade). */
205
+ grades: {
206
+ aPlus: number;
207
+ a: number;
208
+ bPlus: number;
209
+ b: number;
210
+ cPlus: number;
211
+ c: number;
212
+ dPlus: number;
213
+ d: number;
214
+ e: number;
215
+ };
216
+ /** Baseline failure rates for adaptive weight computation. */
217
+ baselineFailureRates: Record<string, number>;
218
+ }
219
+
220
+ type DomainProfile = 'mail_enabled' | 'enterprise_mail' | 'non_mail' | 'web_only' | 'minimal';
221
+ interface DomainContext {
222
+ profile: DomainProfile;
223
+ signals: string[];
224
+ weights: Record<CheckCategory, ImportanceProfile>;
225
+ detectedProvider: string | null;
226
+ }
227
+ interface ImportanceProfile {
228
+ importance: number;
229
+ }
230
+ /**
231
+ * Detect domain context from completed check results.
232
+ * Pure function — reads findings metadata only, no DNS queries.
233
+ */
234
+ declare function detectDomainContext(results: CheckResult[]): DomainContext;
235
+ /** Look up the weight table for a given profile, optionally from runtime config. */
236
+ declare function getProfileWeights(profile: DomainProfile, config?: ScoringConfig): Record<CheckCategory, ImportanceProfile>;
237
+
238
+ /** Map numeric score to letter grade */
239
+ declare function scoreToGrade(score: number, config?: ScoringConfig): string;
240
+ /**
241
+ * Compute the overall scan score from individual check results.
242
+ * Uses weighted average of category scores.
243
+ *
244
+ * When a `DomainContext` is provided, uses profile-specific weights,
245
+ * critical gap categories, and email bonus eligibility instead of defaults.
246
+ */
247
+ declare function computeScanScore(results: CheckResult[], context?: DomainContext, config?: ScoringConfig): ScanScore;
248
+
249
+ /**
250
+ * Input sanitization and validation utilities for the DNS Security MCP Server.
251
+ * Handles domain validation, input cleaning, and MCP error response helpers.
252
+ * Compatible with Cloudflare Workers runtime (no Node.js APIs).
253
+ */
254
+ interface ValidationResult {
255
+ valid: boolean;
256
+ error?: string;
257
+ }
258
+ /**
259
+ * Validate and sanitize a domain name for DNS queries.
260
+ * Rejects localhost, private/reserved TLDs, IP addresses, and malformed domains.
261
+ */
262
+ declare function validateDomain(input: string): ValidationResult;
263
+ /**
264
+ * Sanitize a domain string: trim, lowercase, remove trailing dot.
265
+ * Call validateDomain first to ensure the domain is valid.
266
+ */
267
+ declare function sanitizeDomain(input: string): string;
268
+ /**
269
+ * Sanitize arbitrary text input for safe logging/display.
270
+ * Removes control characters except newlines and tabs, and truncates length.
271
+ */
272
+ declare function sanitizeInput(input: string, maxLength?: number): string;
273
+
274
+ /** Server version — keep in sync with package.json */
275
+ declare const SERVER_VERSION = "1.4.0";
276
+
277
+ /**
278
+ * Check BIMI records for a domain.
279
+ * Validates the presence and configuration of BIMI TXT records,
280
+ * including logo URL format and VMC authority evidence.
281
+ */
282
+ declare function checkBimi(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
283
+
284
+ /**
285
+ * Check CAA records for a domain.
286
+ * Validates that CAA records exist and are properly configured.
287
+ */
288
+ declare function checkCaa(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
289
+
290
+ /**
291
+ * Check DKIM records for a domain.
292
+ * Probes common selectors at <selector>._domainkey.<domain>.
293
+ * Optionally accepts a specific selector to check.
294
+ */
295
+ declare function checkDkim(domain: string, selector?: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
296
+
297
+ /** Parse DMARC tag-value pairs from a DMARC record string. */
298
+ declare function parseDmarcTags(record: string): Map<string, string>;
299
+
300
+ /**
301
+ * Check DMARC records for a domain.
302
+ * Queries _dmarc.<domain> TXT records and validates policy configuration.
303
+ */
304
+ declare function checkDmarc(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
305
+
306
+ /**
307
+ * Check DNSSEC configuration for a domain.
308
+ * Verifies the AD (Authenticated Data) flag, checks for DNSKEY/DS records,
309
+ * and audits algorithm and digest type security.
310
+ */
311
+ declare function checkDnssec(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
312
+
313
+ /**
314
+ * Detect registered lookalike/typosquat domains with DNS or mail infrastructure.
315
+ * Generates domain permutations and checks for active registrations using adaptive batching.
316
+ * Filters out false positives from wildcard DNS on parent domains and null MX records.
317
+ */
318
+ declare function checkLookalikes(domain: string): Promise<CheckResult>;
319
+
320
+ /**
321
+ * Check MTA-STS configuration for a domain.
322
+ * Queries _mta-sts.<domain> TXT records and optionally fetches the policy file.
323
+ */
324
+ declare function checkMtaSts(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
325
+
326
+ /**
327
+ * MX record check tool for MCP server.
328
+ * Validates presence and quality of MX records for a domain.
329
+ * Returns CheckResult with findings including RFC compliance, redundancy, and provider detection.
330
+ */
331
+
332
+ interface CheckMxOptions {
333
+ providerSignaturesUrl?: string;
334
+ providerSignaturesAllowedHosts?: string[];
335
+ providerSignaturesSha256?: string;
336
+ }
337
+ /** Check MX record configuration for a domain */
338
+ declare function checkMx(domain: string, options?: CheckMxOptions, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
339
+
340
+ /**
341
+ * Check nameserver configuration for a domain.
342
+ * Validates NS records exist, checks for diversity, and verifies responsiveness.
343
+ */
344
+ declare function checkNs(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
345
+
346
+ /**
347
+ * Check SPF records for a domain.
348
+ * Looks for v=spf1 TXT records and validates their configuration.
349
+ * Recursively expands include chains to compute true DNS lookup count.
350
+ */
351
+ declare function checkSpf(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
352
+
353
+ /**
354
+ * SSL/TLS certificate check tool.
355
+ * Validates SSL certificate by attempting HTTPS connection,
356
+ * checks HSTS configuration,
357
+ * and verifies HTTP→HTTPS redirect.
358
+ * Workers-compatible: uses fetch API only (cert expiry/chain require external APIs).
359
+ */
360
+
361
+ /**
362
+ * Check SSL/TLS configuration for a domain.
363
+ * Validates HTTPS connectivity, HSTS headers, and HTTP→HTTPS redirect.
364
+ */
365
+ declare function checkSsl(domain: string): Promise<CheckResult>;
366
+
367
+ /**
368
+ * Subdomain Takeover / Dangling CNAME Detection Tool
369
+ * Scans known/active subdomains for orphaned CNAME records pointing to deleted/unresolved third-party services.
370
+ */
371
+
372
+ /**
373
+ * Check for dangling CNAME records on known/active subdomains.
374
+ * Flags orphaned records and potential takeover vectors.
375
+ */
376
+ declare function checkSubdomainTakeover(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
377
+
378
+ /**
379
+ * Check TLS-RPT records for a domain.
380
+ * Validates the presence and configuration of SMTP TLS Reporting records.
381
+ */
382
+ declare function checkTlsrpt(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
383
+
384
+ interface ImpactNarrative {
385
+ impact?: string;
386
+ adverseConsequences?: string;
387
+ }
388
+
389
+ /**
390
+ * Explain Finding tool.
391
+ * Provides static explanations for DNS security findings.
392
+ * No AI binding required - uses a built-in knowledge base.
393
+ */
394
+
395
+ interface ExplanationResult {
396
+ checkType: string;
397
+ status: string;
398
+ details?: string;
399
+ title: string;
400
+ severity: string;
401
+ explanation: string;
402
+ impact?: string;
403
+ adverseConsequences?: string;
404
+ recommendation: string;
405
+ references: string[];
406
+ }
407
+ /**
408
+ * Resolve impact/adverse-consequence narrative for a finding context.
409
+ * Uses explicit explanation entries first, then category, then severity fallback.
410
+ */
411
+ declare function resolveImpactNarrative(params: {
412
+ checkType?: string;
413
+ category?: string;
414
+ status?: string;
415
+ severity?: string;
416
+ title?: string;
417
+ detail?: string;
418
+ }): ImpactNarrative;
419
+ declare function explainFinding(checkType: string, status: string, details?: string): ExplanationResult;
420
+ declare function formatExplanation(result: ExplanationResult): string;
421
+
422
+ interface ScanRuntimeOptions {
423
+ providerSignaturesUrl?: string;
424
+ providerSignaturesAllowedHosts?: string[];
425
+ providerSignaturesSha256?: string;
426
+ profile?: 'mail_enabled' | 'enterprise_mail' | 'non_mail' | 'web_only' | 'minimal' | 'auto';
427
+ profileAccumulator?: DurableObjectNamespace;
428
+ waitUntil?: (promise: Promise<unknown>) => void;
429
+ scoringConfig?: ScoringConfig;
430
+ /** Override cache TTL in seconds (default: 300). Clamped to [60, 3600]. */
431
+ cacheTtlSeconds?: number;
432
+ /** Custom secondary DoH resolver config (bv-dns). Threaded to scanDns but only active when skipSecondaryConfirmation is false. */
433
+ secondaryDoh?: SecondaryDohConfig;
434
+ }
435
+
436
+ /**
437
+ * Email Security Maturity Staging.
438
+ * Classifies a domain's email security posture into a maturity stage (0-4)
439
+ * based on the results of individual DNS security checks.
440
+ */
441
+
442
+ interface MaturityStage {
443
+ stage: number;
444
+ label: string;
445
+ description: string;
446
+ nextStep: string;
447
+ }
448
+
449
+ /** Structured scan result for machine-readable consumption (e.g., CI/CD actions). */
450
+ interface StructuredScanResult {
451
+ domain: string;
452
+ score: number;
453
+ grade: string;
454
+ passed: boolean;
455
+ maturityStage: number | null;
456
+ maturityLabel: string | null;
457
+ categoryScores: Record<string, number>;
458
+ findingCounts: {
459
+ critical: number;
460
+ high: number;
461
+ medium: number;
462
+ low: number;
463
+ };
464
+ scoringProfile: string;
465
+ scoringSignals: string[];
466
+ scoringNote: string | null;
467
+ adaptiveWeightDeltas: Record<string, number> | null;
468
+ timestamp: string;
469
+ cached: boolean;
470
+ }
471
+ /** Build a machine-readable structured result from a scan. */
472
+ declare function buildStructuredScanResult(result: ScanDomainResult): StructuredScanResult;
473
+ declare function formatScanReport(result: ScanDomainResult): string;
474
+
475
+ /**
476
+ * scan-domain orchestrator tool.
477
+ * Runs all DNS security checks in parallel via Promise.all
478
+ * and computes an overall security score.
479
+ *
480
+ * Uses KV-backed cache with 5-minute TTL for scan results when available,
481
+ * with in-memory fallback when KV is not configured.
482
+ * Compatible with Cloudflare Workers runtime (no Node.js APIs).
483
+ */
484
+
485
+ interface ScanDomainResult {
486
+ domain: string;
487
+ score: ScanScore;
488
+ checks: CheckResult[];
489
+ maturity: MaturityStage;
490
+ context: DomainContext;
491
+ cached: boolean;
492
+ timestamp: string;
493
+ scoringNote: string | null;
494
+ adaptiveWeightDeltas: Record<string, number> | null;
495
+ }
496
+ /**
497
+ * Run a full DNS security scan on a domain.
498
+ * Executes all checks in parallel and computes an overall score.
499
+ *
500
+ * @param domain - The domain to scan (must already be validated and sanitized by the caller)
501
+ * @param kv - Optional KV namespace for persistent scan result caching
502
+ * @returns Full scan result with score, individual check results, and metadata
503
+ */
504
+ declare function scanDomain(domain: string, kv?: KVNamespace, runtimeOptions?: ScanRuntimeOptions): Promise<ScanDomainResult>;
505
+
506
+ export { CATEGORY_DISPLAY_WEIGHTS, type CaaRecord, type CheckCategory, type CheckMxOptions, type CheckResult, type DnsAnswer, type DnsAuthority, DnsQueryError, type DohResponse, type DomainContext, type DomainProfile, type ExplanationResult, type Finding, type FindingConfidence, type MaturityStage, type QueryDnsOptions, RecordType, type RecordTypeName, SERVER_VERSION, SEVERITY_PENALTIES, type ScanDomainResult, type ScanRuntimeOptions, type ScanScore, type Severity, type StructuredScanResult, buildCheckResult, buildStructuredScanResult, checkBimi, checkCaa, checkDkim, checkDmarc, checkDnssec, checkLookalikes, checkMtaSts, checkMx, checkNs, checkSpf, checkSsl, checkSubdomainTakeover, checkTlsrpt, computeCategoryScore, computeScanScore, createFinding, detectDomainContext, explainFinding, formatExplanation, formatScanReport, getProfileWeights, inferFindingConfidence, parseCaaRecord, parseDmarcTags, queryCaaRecords, queryDns, queryDnsRecords, queryMxRecords, queryTxtRecords, resolveImpactNarrative, sanitizeDomain, sanitizeInput, scanDomain, scoreToGrade, validateDomain };