blackveil-dns 2.0.10 → 2.1.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.
package/README.md CHANGED
@@ -24,6 +24,10 @@ Open-source DNS & email security scanner for Claude, Cursor, VS Code, and MCP cl
24
24
 
25
25
  ## Try it in 30 seconds
26
26
 
27
+ **Claude Desktop** (one-click install):
28
+
29
+ Download the [Blackveil DNS extension](https://github.com/MadaBurns/bv-claude-dns/releases/latest/download/bv-claude-dns.mcpb) and open it — all 33 tools available instantly.
30
+
27
31
  **Claude Code** (one command):
28
32
 
29
33
  ```bash
@@ -361,6 +365,20 @@ Enforce DNS security grades in your pipeline with the [Blackveil DNS GitHub Acti
361
365
 
362
366
  The action outputs `score`, `grade`, `maturity`, `scoring-profile`, and `passed` for downstream steps.
363
367
 
368
+ ### Package Release Controls
369
+
370
+ `blackveil-dns` npm publishing is gated by `.github/workflows/publish.yml`.
371
+
372
+ On `v*` tags, the workflow enforces:
373
+ - `npm run validate:internal-deps`
374
+ - Typecheck (`npx tsc --noEmit`)
375
+ - Lint (`npm run lint`)
376
+ - Test (`npm test`)
377
+ - Security audit (`npm audit --audit-level=high`)
378
+ - Changelog presence for the target version
379
+
380
+ Publish only proceeds after all gates pass.
381
+
364
382
  ## Monitoring
365
383
 
366
384
  Get weekly DNS security reports in Slack or Discord. See [`examples/slack-discord-webhook/`](examples/slack-discord-webhook/) for a ready-to-deploy Cloudflare Cron Trigger recipe.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CheckResult, ScoringConfig, ScanScore, DomainContext } from '@blackveil/dns-checks/scoring';
2
2
  export { CATEGORY_DISPLAY_WEIGHTS, CheckCategory, CheckResult, DomainContext, DomainProfile, Finding, FindingConfidence, SEVERITY_PENALTIES, ScanScore, Severity, buildCheckResult, computeCategoryScore, computeScanScore, createFinding, detectDomainContext, getProfileWeights, inferFindingConfidence, scoreToGrade } from '@blackveil/dns-checks/scoring';
3
+ import { z } from 'zod';
3
4
  export { parseDmarcTags } from '@blackveil/dns-checks';
4
5
 
5
6
  /** Standard DNS record type codes */
@@ -52,7 +53,7 @@ interface DohResponse {
52
53
  }
53
54
  /** Configuration for a custom secondary DoH resolver (e.g., bv-dns on Oracle Cloud). */
54
55
  interface SecondaryDohConfig {
55
- /** DoH endpoint URL (e.g. https://harlan.blackveilsecurity.com/dns-query) */
56
+ /** DoH endpoint URL (e.g. https://doh.example.com/dns-query) */
56
57
  endpoint: string;
57
58
  /** Optional auth token sent as X-BV-Token header */
58
59
  token?: string;
@@ -159,7 +160,7 @@ declare function sanitizeDomain(input: string): string;
159
160
  declare function sanitizeInput(input: string, maxLength?: number): string;
160
161
 
161
162
  /** Server version — keep in sync with package.json */
162
- declare const SERVER_VERSION = "2.0.10";
163
+ declare const SERVER_VERSION = "2.1.0";
163
164
 
164
165
  /**
165
166
  * Check BIMI records for a domain.
@@ -246,7 +247,12 @@ declare function checkSubdomainTakeover(domain: string, dnsOptions?: QueryDnsOpt
246
247
  */
247
248
  declare function checkTlsrpt(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
248
249
 
249
- type OutputFormat = 'full' | 'compact';
250
+ /** Output format. Trims and lowercases input before validation. */
251
+ declare const FormatSchema: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodEnum<{
252
+ full: "full";
253
+ compact: "compact";
254
+ }>>;
255
+ type OutputFormat = z.infer<typeof FormatSchema>;
250
256
 
251
257
  interface ImpactNarrative {
252
258
  impact?: string;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod';
1
2
  import * as punycode from 'punycode/';
2
3
 
3
4
  // src/lib/dns-types.ts
@@ -57,6 +58,40 @@ var DOH_EDGE_CACHE_TTL = 300;
57
58
  var INFLIGHT_CLEANUP_MS = 3e4;
58
59
  var DNS_RETRY_BASE_DELAY_MS = 75;
59
60
  var DNS_CONFIRM_WITH_SECONDARY_ON_EMPTY = true;
61
+ var DnsAnswerSchema = z.object({
62
+ name: z.string(),
63
+ type: z.number(),
64
+ TTL: z.number().optional(),
65
+ data: z.string()
66
+ });
67
+ var DnsAuthoritySchema = z.object({
68
+ name: z.string(),
69
+ type: z.number(),
70
+ TTL: z.number().optional(),
71
+ data: z.string()
72
+ });
73
+ var DohResponseSchema = z.object({
74
+ Status: z.number().finite(),
75
+ TC: z.boolean().optional(),
76
+ RD: z.boolean().optional(),
77
+ RA: z.boolean().optional(),
78
+ AD: z.boolean().optional(),
79
+ CD: z.boolean().optional(),
80
+ Question: z.array(z.object({ name: z.string(), type: z.unknown() })).optional(),
81
+ Answer: z.array(DnsAnswerSchema).optional(),
82
+ Authority: z.array(DnsAuthoritySchema).optional()
83
+ }).passthrough();
84
+ var CaaRecordSchema = z.object({
85
+ flags: z.number(),
86
+ tag: z.string(),
87
+ value: z.string()
88
+ });
89
+ z.object({
90
+ usage: z.number(),
91
+ selector: z.number(),
92
+ matchingType: z.number(),
93
+ certData: z.string()
94
+ });
60
95
 
61
96
  // src/lib/dns-transport.ts
62
97
  var DOH_ENDPOINT = "https://cloudflare-dns.com/dns-query";
@@ -84,8 +119,9 @@ async function fetchDohResponse(url, timeoutMs) {
84
119
  });
85
120
  if (!response.ok) return null;
86
121
  const data = await response.json();
87
- if (typeof data !== "object" || data === null || typeof data.Status !== "number" || !Number.isFinite(data.Status)) return null;
88
- return data;
122
+ const parsed = DohResponseSchema.safeParse(data);
123
+ if (!parsed.success) return null;
124
+ return parsed.data;
89
125
  } catch {
90
126
  return null;
91
127
  } finally {
@@ -108,8 +144,9 @@ async function fetchDohWithAuth(url, timeoutMs, token) {
108
144
  });
109
145
  if (!response.ok) return null;
110
146
  const data = await response.json();
111
- if (typeof data !== "object" || data === null || typeof data.Status !== "number" || !Number.isFinite(data.Status)) return null;
112
- return data;
147
+ const parsed = DohResponseSchema.safeParse(data);
148
+ if (!parsed.success) return null;
149
+ return parsed.data;
113
150
  } catch {
114
151
  return null;
115
152
  } finally {
@@ -180,10 +217,11 @@ async function queryDnsUncached(domain, type, dnssecCheck = false, opts) {
180
217
  throw new DnsQueryError(`DoH returned HTTP ${response.status}`, domain, type, response.status);
181
218
  }
182
219
  const raw = await response.json();
183
- if (typeof raw !== "object" || raw === null || typeof raw.Status !== "number" || !Number.isFinite(raw.Status)) {
220
+ const validated = DohResponseSchema.safeParse(raw);
221
+ if (!validated.success) {
184
222
  throw new DnsQueryError("Invalid DoH response format", domain, type);
185
223
  }
186
- const data = raw;
224
+ const data = validated.data;
187
225
  if (confirmWithSecondaryOnEmpty && !opts?.skipSecondaryConfirmation && !hasTypedAnswers(data, type)) {
188
226
  if (opts?.secondaryDoh?.endpoint) {
189
227
  const bvDns = await fetchDohWithAuth(
@@ -245,15 +283,17 @@ function parseCaaRecord(data) {
245
283
  if (isNaN(flags) || isNaN(tagLen) || hexBytes.length < 2 + tagLen) return null;
246
284
  const tag = hexBytes.slice(2, 2 + tagLen).map((hexByte) => String.fromCharCode(parseInt(hexByte, 16))).join("");
247
285
  const value = hexBytes.slice(2 + tagLen).map((hexByte) => String.fromCharCode(parseInt(hexByte, 16))).join("");
248
- return { flags, tag: tag.toLowerCase(), value };
286
+ const record = { flags, tag: tag.toLowerCase(), value };
287
+ return CaaRecordSchema.safeParse(record).success ? record : null;
249
288
  }
250
289
  const match = data.match(/^(\d+)\s+(\S+)\s+"?([^"]*)"?\s*$/);
251
290
  if (match) {
252
- return {
291
+ const record = {
253
292
  flags: parseInt(match[1], 10),
254
293
  tag: match[2].toLowerCase(),
255
294
  value: match[3]
256
295
  };
296
+ return CaaRecordSchema.safeParse(record).success ? record : null;
257
297
  }
258
298
  return null;
259
299
  }
@@ -1125,9 +1165,7 @@ function sanitizeInput(input, maxLength = 500) {
1125
1165
  }
1126
1166
 
1127
1167
  // src/lib/server-version.ts
1128
- var SERVER_VERSION = "2.0.10";
1129
-
1130
- // packages/dns-checks/dist/index.js
1168
+ var SERVER_VERSION = "2.1.0";
1131
1169
  var SEVERITY_PENALTIES2 = {
1132
1170
  critical: 40,
1133
1171
  high: 25,
@@ -4214,6 +4252,53 @@ async function checkHTTPSecurity(domain, fetchFn, options) {
4214
4252
  }
4215
4253
  return buildCheckResult2("http_security", findings);
4216
4254
  }
4255
+ var CheckCategorySchema = z.enum([
4256
+ "spf",
4257
+ "dmarc",
4258
+ "dkim",
4259
+ "dnssec",
4260
+ "ssl",
4261
+ "mta_sts",
4262
+ "ns",
4263
+ "caa",
4264
+ "subdomain_takeover",
4265
+ "mx",
4266
+ "bimi",
4267
+ "tlsrpt",
4268
+ "lookalikes",
4269
+ "shadow_domains",
4270
+ "txt_hygiene",
4271
+ "http_security",
4272
+ "dane",
4273
+ "mx_reputation",
4274
+ "srv",
4275
+ "zone_hygiene",
4276
+ "dane_https",
4277
+ "svcb_https"
4278
+ ]);
4279
+ var SeveritySchema = z.enum(["critical", "high", "medium", "low", "info"]);
4280
+ z.enum(["deterministic", "heuristic", "verified"]);
4281
+ z.enum(["core", "protective", "hardening"]);
4282
+ var FindingSchema = z.object({
4283
+ category: CheckCategorySchema,
4284
+ title: z.string(),
4285
+ severity: SeveritySchema,
4286
+ detail: z.string(),
4287
+ metadata: z.record(z.string(), z.unknown()).optional()
4288
+ });
4289
+ z.object({
4290
+ category: CheckCategorySchema,
4291
+ passed: z.boolean(),
4292
+ score: z.number(),
4293
+ findings: z.array(FindingSchema)
4294
+ });
4295
+ z.object({
4296
+ overall: z.number(),
4297
+ grade: z.string(),
4298
+ categoryScores: z.record(z.string(), z.number()),
4299
+ findings: z.array(FindingSchema),
4300
+ summary: z.string()
4301
+ });
4217
4302
 
4218
4303
  // src/tools/check-bimi.ts
4219
4304
  function makeQueryDNS(dnsOptions) {