dns-security-mcp 0.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +723 -0
  3. package/dist/blocklist/index.d.ts +3 -0
  4. package/dist/blocklist/index.d.ts.map +1 -0
  5. package/dist/blocklist/index.js +596 -0
  6. package/dist/blocklist/index.js.map +1 -0
  7. package/dist/ct/index.d.ts +3 -0
  8. package/dist/ct/index.d.ts.map +1 -0
  9. package/dist/ct/index.js +534 -0
  10. package/dist/ct/index.js.map +1 -0
  11. package/dist/data/dkim-selectors.d.ts +2 -0
  12. package/dist/data/dkim-selectors.d.ts.map +1 -0
  13. package/dist/data/dkim-selectors.js +60 -0
  14. package/dist/data/dkim-selectors.js.map +1 -0
  15. package/dist/data/dnsbl-lists.d.ts +8 -0
  16. package/dist/data/dnsbl-lists.d.ts.map +1 -0
  17. package/dist/data/dnsbl-lists.js +54 -0
  18. package/dist/data/dnsbl-lists.js.map +1 -0
  19. package/dist/data/takeover-fingerprints.d.ts +8 -0
  20. package/dist/data/takeover-fingerprints.d.ts.map +1 -0
  21. package/dist/data/takeover-fingerprints.js +84 -0
  22. package/dist/data/takeover-fingerprints.js.map +1 -0
  23. package/dist/data/tunneling-signatures.d.ts +17 -0
  24. package/dist/data/tunneling-signatures.d.ts.map +1 -0
  25. package/dist/data/tunneling-signatures.js +85 -0
  26. package/dist/data/tunneling-signatures.js.map +1 -0
  27. package/dist/dns/index.d.ts +3 -0
  28. package/dist/dns/index.d.ts.map +1 -0
  29. package/dist/dns/index.js +1211 -0
  30. package/dist/dns/index.js.map +1 -0
  31. package/dist/dnssec/index.d.ts +3 -0
  32. package/dist/dnssec/index.d.ts.map +1 -0
  33. package/dist/dnssec/index.js +1377 -0
  34. package/dist/dnssec/index.js.map +1 -0
  35. package/dist/domain/index.d.ts +3 -0
  36. package/dist/domain/index.d.ts.map +1 -0
  37. package/dist/domain/index.js +938 -0
  38. package/dist/domain/index.js.map +1 -0
  39. package/dist/email/index.d.ts +3 -0
  40. package/dist/email/index.d.ts.map +1 -0
  41. package/dist/email/index.js +1188 -0
  42. package/dist/email/index.js.map +1 -0
  43. package/dist/hijack/index.d.ts +3 -0
  44. package/dist/hijack/index.d.ts.map +1 -0
  45. package/dist/hijack/index.js +1117 -0
  46. package/dist/hijack/index.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +151 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/infra/index.d.ts +3 -0
  52. package/dist/infra/index.d.ts.map +1 -0
  53. package/dist/infra/index.js +797 -0
  54. package/dist/infra/index.js.map +1 -0
  55. package/dist/privacy/index.d.ts +3 -0
  56. package/dist/privacy/index.d.ts.map +1 -0
  57. package/dist/privacy/index.js +772 -0
  58. package/dist/privacy/index.js.map +1 -0
  59. package/dist/protocol/mcp-server.d.ts +4 -0
  60. package/dist/protocol/mcp-server.d.ts.map +1 -0
  61. package/dist/protocol/mcp-server.js +32 -0
  62. package/dist/protocol/mcp-server.js.map +1 -0
  63. package/dist/protocol/tools.d.ts +3 -0
  64. package/dist/protocol/tools.d.ts.map +1 -0
  65. package/dist/protocol/tools.js +29 -0
  66. package/dist/protocol/tools.js.map +1 -0
  67. package/dist/report/index.d.ts +3 -0
  68. package/dist/report/index.d.ts.map +1 -0
  69. package/dist/report/index.js +1167 -0
  70. package/dist/report/index.js.map +1 -0
  71. package/dist/threat/index.d.ts +3 -0
  72. package/dist/threat/index.d.ts.map +1 -0
  73. package/dist/threat/index.js +999 -0
  74. package/dist/threat/index.js.map +1 -0
  75. package/dist/tunnel/index.d.ts +3 -0
  76. package/dist/tunnel/index.d.ts.map +1 -0
  77. package/dist/tunnel/index.js +688 -0
  78. package/dist/tunnel/index.js.map +1 -0
  79. package/dist/types/index.d.ts +52 -0
  80. package/dist/types/index.d.ts.map +1 -0
  81. package/dist/types/index.js +8 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/typo/index.d.ts +3 -0
  84. package/dist/typo/index.d.ts.map +1 -0
  85. package/dist/typo/index.js +625 -0
  86. package/dist/typo/index.js.map +1 -0
  87. package/dist/utils/cache.d.ts +11 -0
  88. package/dist/utils/cache.d.ts.map +1 -0
  89. package/dist/utils/cache.js +35 -0
  90. package/dist/utils/cache.js.map +1 -0
  91. package/dist/utils/dns-client.d.ts +37 -0
  92. package/dist/utils/dns-client.d.ts.map +1 -0
  93. package/dist/utils/dns-client.js +359 -0
  94. package/dist/utils/dns-client.js.map +1 -0
  95. package/dist/utils/rate-limiter.d.ts +10 -0
  96. package/dist/utils/rate-limiter.d.ts.map +1 -0
  97. package/dist/utils/rate-limiter.js +35 -0
  98. package/dist/utils/rate-limiter.js.map +1 -0
  99. package/package.json +63 -0
@@ -0,0 +1,688 @@
1
+ import { z } from "zod";
2
+ import { text, json } from "../types/index.js";
3
+ import { shannonEntropy, createResolver } from "../utils/dns-client.js";
4
+ import { TUNNEL_SIGNATURES, TUNNEL_THRESHOLDS } from "../data/tunneling-signatures.js";
5
+ // ─── Helpers ───
6
+ /**
7
+ * Extract subdomain labels from a fully-qualified domain name.
8
+ * Given "abc.def.example.com", returns ["abc", "def"] (everything before the base domain).
9
+ * The base domain is assumed to be the last two labels (e.g. "example.com").
10
+ */
11
+ function extractSubdomainLabels(query) {
12
+ const labels = query.replace(/\.$/, "").split(".");
13
+ if (labels.length <= 2)
14
+ return [];
15
+ return labels.slice(0, labels.length - 2);
16
+ }
17
+ /**
18
+ * Classify entropy value against thresholds.
19
+ */
20
+ function classifyEntropy(entropy) {
21
+ if (entropy >= TUNNEL_THRESHOLDS.highEntropy)
22
+ return "likely-tunnel";
23
+ if (entropy >= TUNNEL_THRESHOLDS.normalEntropy)
24
+ return "suspicious";
25
+ return "normal";
26
+ }
27
+ // ─── Tool 1: Entropy Analysis ───
28
+ const tunnelEntropyAnalysis = {
29
+ name: "tunnel_entropy_analysis",
30
+ description: "Calculates Shannon entropy per subdomain label to detect DNS tunneling. Normal DNS labels have entropy ~3.0-3.5, while encoded/encrypted data used in tunneling has entropy >4.0.",
31
+ schema: {
32
+ queries: z
33
+ .array(z.string())
34
+ .describe("List of DNS query names (FQDNs) to analyze for tunneling entropy patterns"),
35
+ },
36
+ execute: async (args) => {
37
+ const queries = args.queries;
38
+ if (!queries.length) {
39
+ return text("No queries provided for entropy analysis.");
40
+ }
41
+ const results = queries.map((query) => {
42
+ const subLabels = extractSubdomainLabels(query);
43
+ if (subLabels.length === 0) {
44
+ return {
45
+ query,
46
+ subdomainLabels: [],
47
+ labelEntropies: [],
48
+ combinedEntropy: 0,
49
+ classification: "normal",
50
+ note: "No subdomain labels found — base domain only",
51
+ };
52
+ }
53
+ const combined = subLabels.join("");
54
+ const combinedEntropy = shannonEntropy(combined);
55
+ const labelEntropies = subLabels.map((label) => ({
56
+ label,
57
+ length: label.length,
58
+ entropy: Math.round(shannonEntropy(label) * 1000) / 1000,
59
+ }));
60
+ const classification = classifyEntropy(combinedEntropy);
61
+ return {
62
+ query,
63
+ subdomainLabels: subLabels,
64
+ labelEntropies,
65
+ combinedEntropy: Math.round(combinedEntropy * 1000) / 1000,
66
+ classification,
67
+ };
68
+ });
69
+ const suspicious = results.filter((r) => r.classification === "suspicious" || r.classification === "likely-tunnel");
70
+ return json({
71
+ summary: {
72
+ totalQueries: queries.length,
73
+ normalCount: results.filter((r) => r.classification === "normal").length,
74
+ suspiciousCount: results.filter((r) => r.classification === "suspicious").length,
75
+ likelyTunnelCount: results.filter((r) => r.classification === "likely-tunnel").length,
76
+ thresholds: {
77
+ normalMax: TUNNEL_THRESHOLDS.normalEntropy,
78
+ highEntropy: TUNNEL_THRESHOLDS.highEntropy,
79
+ },
80
+ },
81
+ results,
82
+ flagged: suspicious,
83
+ });
84
+ },
85
+ };
86
+ // ─── Tool 2: Query Length Analysis ───
87
+ const tunnelQueryLength = {
88
+ name: "tunnel_query_length",
89
+ description: "Measures subdomain label lengths and total query length to detect DNS tunneling. Normal browsing rarely exceeds 3 labels or 60 total characters. Tunneling often uses labels >40 chars and totals >200 chars.",
90
+ schema: {
91
+ queries: z
92
+ .array(z.string())
93
+ .describe("List of DNS query names (FQDNs) to analyze for abnormal length patterns"),
94
+ },
95
+ execute: async (args) => {
96
+ const queries = args.queries;
97
+ if (!queries.length) {
98
+ return text("No queries provided for length analysis.");
99
+ }
100
+ const results = queries.map((query) => {
101
+ const labels = query.replace(/\.$/, "").split(".");
102
+ const subLabels = extractSubdomainLabels(query);
103
+ const totalLength = query.replace(/\.$/, "").length;
104
+ const flags = [];
105
+ const longLabels = subLabels.filter((l) => l.length > TUNNEL_THRESHOLDS.maxLabelLength);
106
+ if (longLabels.length > 0) {
107
+ flags.push(`${longLabels.length} label(s) exceed ${TUNNEL_THRESHOLDS.maxLabelLength} chars: [${longLabels.map((l) => `"${l.substring(0, 20)}..." (${l.length})`).join(", ")}]`);
108
+ }
109
+ if (totalLength > TUNNEL_THRESHOLDS.maxTotalLength) {
110
+ flags.push(`Total query length ${totalLength} exceeds threshold ${TUNNEL_THRESHOLDS.maxTotalLength}`);
111
+ }
112
+ if (labels.length > TUNNEL_THRESHOLDS.maxLabelCount) {
113
+ flags.push(`Label count ${labels.length} exceeds threshold ${TUNNEL_THRESHOLDS.maxLabelCount}`);
114
+ }
115
+ return {
116
+ query: query.length > 80 ? query.substring(0, 80) + "..." : query,
117
+ fullLength: query.length,
118
+ totalLength,
119
+ labelCount: labels.length,
120
+ subdomainLabelCount: subLabels.length,
121
+ labelLengths: labels.map((l) => ({ label: l.substring(0, 30), length: l.length })),
122
+ maxLabelLength: Math.max(0, ...labels.map((l) => l.length)),
123
+ isSuspicious: flags.length > 0,
124
+ flags,
125
+ };
126
+ });
127
+ const flagged = results.filter((r) => r.isSuspicious);
128
+ return json({
129
+ summary: {
130
+ totalQueries: queries.length,
131
+ flaggedCount: flagged.length,
132
+ thresholds: {
133
+ maxLabelLength: TUNNEL_THRESHOLDS.maxLabelLength,
134
+ maxTotalLength: TUNNEL_THRESHOLDS.maxTotalLength,
135
+ maxLabelCount: TUNNEL_THRESHOLDS.maxLabelCount,
136
+ },
137
+ },
138
+ results,
139
+ flagged,
140
+ });
141
+ },
142
+ };
143
+ // ─── Tool 3: TXT Payload Detection ───
144
+ const tunnelTxtPayload = {
145
+ name: "tunnel_txt_payload",
146
+ description: "Resolves TXT records for a domain and optional subdomains, then detects encoded payloads commonly used in DNS tunneling: base64, hex-encoded data, binary markers, and high-entropy content.",
147
+ schema: {
148
+ domain: z.string().describe("Base domain to resolve TXT records for"),
149
+ subdomains: z
150
+ .array(z.string())
151
+ .optional()
152
+ .describe("Optional list of subdomains to also check for TXT record payloads"),
153
+ },
154
+ execute: async (args, ctx) => {
155
+ const domain = args.domain;
156
+ const subdomains = args.subdomains ?? [];
157
+ const resolver = createResolver(ctx.config.customResolver);
158
+ const targets = [domain, ...subdomains.map((s) => `${s}.${domain}`)];
159
+ const base64Pattern = /^[A-Za-z0-9+/]{40,}={0,2}$/;
160
+ const hexPattern = /^[a-fA-F0-9]{32,}$/;
161
+ const binaryMarkers = ["\x00", "\x01", "\xff", "\\x00", "\\x01", "\\xff"];
162
+ const results = await Promise.all(targets.map(async (target) => {
163
+ try {
164
+ const txtRecords = await resolver.resolveTxt(target);
165
+ const flatRecords = txtRecords.map((chunks) => chunks.join(""));
166
+ const analysis = flatRecords.map((value) => {
167
+ const entropy = shannonEntropy(value);
168
+ const flags = [];
169
+ if (base64Pattern.test(value.trim())) {
170
+ flags.push("Matches base64-encoded payload pattern");
171
+ }
172
+ if (hexPattern.test(value.trim())) {
173
+ flags.push("Matches hex-encoded data pattern");
174
+ }
175
+ if (binaryMarkers.some((marker) => value.includes(marker))) {
176
+ flags.push("Contains binary data markers");
177
+ }
178
+ if (entropy >= TUNNEL_THRESHOLDS.highEntropy) {
179
+ flags.push(`High entropy (${Math.round(entropy * 1000) / 1000}) suggests encoded/encrypted data`);
180
+ }
181
+ return {
182
+ value: value.length > 120 ? value.substring(0, 120) + "..." : value,
183
+ length: value.length,
184
+ entropy: Math.round(entropy * 1000) / 1000,
185
+ entropyClassification: classifyEntropy(entropy),
186
+ isSuspicious: flags.length > 0,
187
+ flags,
188
+ };
189
+ });
190
+ return {
191
+ target,
192
+ recordCount: flatRecords.length,
193
+ analysis,
194
+ hasSuspiciousPayloads: analysis.some((a) => a.isSuspicious),
195
+ };
196
+ }
197
+ catch {
198
+ return {
199
+ target,
200
+ recordCount: 0,
201
+ analysis: [],
202
+ hasSuspiciousPayloads: false,
203
+ error: "Failed to resolve TXT records (NXDOMAIN or timeout)",
204
+ };
205
+ }
206
+ }));
207
+ const suspicious = results.filter((r) => r.hasSuspiciousPayloads);
208
+ return json({
209
+ summary: {
210
+ targetsChecked: targets.length,
211
+ targetsWithTxt: results.filter((r) => r.recordCount > 0).length,
212
+ suspiciousTargets: suspicious.length,
213
+ },
214
+ results,
215
+ flagged: suspicious,
216
+ });
217
+ },
218
+ };
219
+ // ─── Tool 4: Record Type Anomaly Detection ───
220
+ const tunnelRecordAnomaly = {
221
+ name: "tunnel_record_anomaly",
222
+ description: "Analyzes DNS queries for record type abuse patterns commonly used in tunneling. Detects indicators of NULL, TXT, CNAME, and MX record abuse, plus anomalous query patterns consistent with data exfiltration.",
223
+ schema: {
224
+ queries: z
225
+ .array(z.string())
226
+ .describe("List of DNS query names (FQDNs) to analyze for record type anomaly patterns"),
227
+ },
228
+ execute: async (args) => {
229
+ const queries = args.queries;
230
+ if (!queries.length) {
231
+ return text("No queries provided for record anomaly analysis.");
232
+ }
233
+ // Analyze structural patterns that imply record type abuse
234
+ const patterns = {
235
+ txtAbuse: 0,
236
+ cnameAbuse: 0,
237
+ nullAbuse: 0,
238
+ mxAbuse: 0,
239
+ aRecordAbuse: 0,
240
+ };
241
+ const perQuery = queries.map((query) => {
242
+ const subLabels = extractSubdomainLabels(query);
243
+ const combined = subLabels.join("");
244
+ const flags = [];
245
+ // Long base64-like labels => likely TXT record tunnel
246
+ if (/^[A-Za-z0-9+/=]{30,}\./.test(query)) {
247
+ flags.push("Base64-encoded label pattern (TXT record abuse indicator)");
248
+ patterns.txtAbuse++;
249
+ }
250
+ // Very long hex-only labels => likely NULL record or TXT abuse
251
+ if (/^[a-f0-9]{40,}\./.test(query)) {
252
+ flags.push("Long hex-encoded label (NULL/TXT record abuse indicator)");
253
+ patterns.nullAbuse++;
254
+ }
255
+ // Multiple encoded subdomains chained => CNAME abuse
256
+ if (subLabels.length >= 3 &&
257
+ subLabels.every((l) => l.length >= 10 && shannonEntropy(l) >= TUNNEL_THRESHOLDS.normalEntropy)) {
258
+ flags.push("Multiple high-entropy labels chained (CNAME tunneling indicator)");
259
+ patterns.cnameAbuse++;
260
+ }
261
+ // Short hex labels with consistent length => A/AAAA record beacon
262
+ if (subLabels.length >= 1 &&
263
+ subLabels[0].length >= 8 &&
264
+ subLabels[0].length <= 32 &&
265
+ /^[a-f0-9]+$/.test(subLabels[0])) {
266
+ flags.push("Short hex-encoded label (A/AAAA beacon indicator)");
267
+ patterns.aRecordAbuse++;
268
+ }
269
+ // High entropy combined with unusual domain depth => MX abuse pattern
270
+ if (combined.length > 0 && shannonEntropy(combined) >= TUNNEL_THRESHOLDS.highEntropy && subLabels.length >= 2) {
271
+ flags.push("High entropy with deep subdomain nesting (MX/record abuse indicator)");
272
+ patterns.mxAbuse++;
273
+ }
274
+ return {
275
+ query: query.length > 80 ? query.substring(0, 80) + "..." : query,
276
+ subdomainLabelCount: subLabels.length,
277
+ isSuspicious: flags.length > 0,
278
+ flags,
279
+ };
280
+ });
281
+ const totalFlagged = perQuery.filter((r) => r.isSuspicious).length;
282
+ // Determine dominant abuse pattern
283
+ const dominantPattern = Object.entries(patterns).sort((a, b) => b[1] - a[1])[0];
284
+ return json({
285
+ summary: {
286
+ totalQueries: queries.length,
287
+ flaggedQueries: totalFlagged,
288
+ flagRate: Math.round((totalFlagged / queries.length) * 100 * 10) / 10 + "%",
289
+ dominantPattern: dominantPattern[1] > 0
290
+ ? { type: dominantPattern[0], count: dominantPattern[1] }
291
+ : null,
292
+ patternCounts: patterns,
293
+ },
294
+ results: perQuery,
295
+ flagged: perQuery.filter((r) => r.isSuspicious),
296
+ });
297
+ },
298
+ };
299
+ // ─── Tool 5: Tool Signature Matching ───
300
+ const tunnelToolSignatures = {
301
+ name: "tunnel_tool_signatures",
302
+ description: "Matches DNS query patterns against known tunneling tool signatures (iodine, dns2tcp, dnscat2, Cobalt Strike, Sliver C2, DNSStager, etc.). Returns matched tools with descriptions and indicators.",
303
+ schema: {
304
+ queries: z
305
+ .array(z.string())
306
+ .describe("List of DNS query names (FQDNs) to match against tunneling tool signatures"),
307
+ },
308
+ execute: async (args) => {
309
+ const queries = args.queries;
310
+ if (!queries.length) {
311
+ return text("No queries provided for signature matching.");
312
+ }
313
+ const matchedTools = new Map();
314
+ const perQuery = queries.map((query) => {
315
+ const matches = [];
316
+ for (const sig of TUNNEL_SIGNATURES) {
317
+ if (sig.pattern.test(query)) {
318
+ matches.push({
319
+ tool: sig.tool,
320
+ description: sig.description,
321
+ indicators: sig.indicators,
322
+ });
323
+ const existing = matchedTools.get(sig.tool);
324
+ if (existing) {
325
+ existing.count++;
326
+ if (existing.examples.length < 3) {
327
+ existing.examples.push(query.length > 60 ? query.substring(0, 60) + "..." : query);
328
+ }
329
+ }
330
+ else {
331
+ matchedTools.set(sig.tool, {
332
+ count: 1,
333
+ description: sig.description,
334
+ indicators: sig.indicators,
335
+ examples: [query.length > 60 ? query.substring(0, 60) + "..." : query],
336
+ });
337
+ }
338
+ }
339
+ }
340
+ return {
341
+ query: query.length > 80 ? query.substring(0, 80) + "..." : query,
342
+ matchCount: matches.length,
343
+ matchedTools: matches.map((m) => m.tool),
344
+ matches,
345
+ };
346
+ });
347
+ const toolSummary = Array.from(matchedTools.entries())
348
+ .map(([tool, data]) => ({
349
+ tool,
350
+ matchCount: data.count,
351
+ description: data.description,
352
+ indicators: data.indicators,
353
+ examples: data.examples,
354
+ }))
355
+ .sort((a, b) => b.matchCount - a.matchCount);
356
+ const queriesWithMatches = perQuery.filter((r) => r.matchCount > 0);
357
+ return json({
358
+ summary: {
359
+ totalQueries: queries.length,
360
+ queriesMatched: queriesWithMatches.length,
361
+ uniqueToolsDetected: toolSummary.length,
362
+ signaturesChecked: TUNNEL_SIGNATURES.length,
363
+ },
364
+ detectedTools: toolSummary,
365
+ results: perQuery,
366
+ flagged: queriesWithMatches,
367
+ });
368
+ },
369
+ };
370
+ // ─── Tool 6: Covert Channel Detection ───
371
+ const tunnelCovertChannel = {
372
+ name: "tunnel_covert_channel",
373
+ description: "Detects covert DNS channels through timing analysis (beaconing detection when timestamps are provided) and label pattern analysis (incrementing counters, session IDs, sequential encoding).",
374
+ schema: {
375
+ queries: z
376
+ .array(z.string())
377
+ .describe("List of DNS query names (FQDNs) to analyze for covert channel patterns"),
378
+ timestamps: z
379
+ .array(z.number())
380
+ .optional()
381
+ .describe("Optional array of Unix timestamps (milliseconds) corresponding to each query for timing/beaconing analysis"),
382
+ },
383
+ execute: async (args) => {
384
+ const queries = args.queries;
385
+ const timestamps = args.timestamps;
386
+ if (!queries.length) {
387
+ return text("No queries provided for covert channel analysis.");
388
+ }
389
+ // ── Timing Analysis ──
390
+ let timingAnalysis = { hasTimestamps: false };
391
+ if (timestamps && timestamps.length >= 2 && timestamps.length === queries.length) {
392
+ const sorted = [...timestamps].sort((a, b) => a - b);
393
+ const intervals = [];
394
+ for (let i = 1; i < sorted.length; i++) {
395
+ intervals.push(sorted[i] - sorted[i - 1]);
396
+ }
397
+ const mean = intervals.reduce((sum, v) => sum + v, 0) / intervals.length;
398
+ const variance = intervals.reduce((sum, v) => sum + (v - mean) ** 2, 0) / intervals.length;
399
+ const stdDev = Math.sqrt(variance);
400
+ const cv = mean > 0 ? stdDev / mean : 0;
401
+ const isBeaconing = cv < TUNNEL_THRESHOLDS.beaconVariance;
402
+ timingAnalysis = {
403
+ hasTimestamps: true,
404
+ intervals: intervals.length <= 20 ? intervals : intervals.slice(0, 20),
405
+ meanInterval: Math.round(mean),
406
+ stdDeviation: Math.round(stdDev * 100) / 100,
407
+ coefficientOfVariation: Math.round(cv * 10000) / 10000,
408
+ isBeaconing,
409
+ beaconIntervalMs: isBeaconing ? Math.round(mean) : undefined,
410
+ };
411
+ }
412
+ // ── Label Pattern Analysis ──
413
+ const labelPatterns = queries.map((query) => {
414
+ const subLabels = extractSubdomainLabels(query);
415
+ const flags = [];
416
+ if (subLabels.length === 0) {
417
+ return { query, subLabels: [], flags, isSuspicious: false };
418
+ }
419
+ const firstLabel = subLabels[0];
420
+ // Check for incrementing counters (e.g., "001", "002", ... or "a1", "a2")
421
+ const counterMatch = firstLabel.match(/^(.+?)(\d+)$/);
422
+ if (counterMatch) {
423
+ flags.push(`Possible counter/sequence ID: prefix="${counterMatch[1]}", seq=${counterMatch[2]}`);
424
+ }
425
+ // Check for hex-encoded session ID patterns
426
+ if (/^[a-f0-9]{8,16}$/.test(firstLabel)) {
427
+ flags.push("Hex-encoded session ID pattern in first label");
428
+ }
429
+ // Check for base32-encoded patterns (often used for session management)
430
+ if (/^[a-z2-7]{16,}$/.test(firstLabel)) {
431
+ flags.push("Base32-encoded pattern detected (common in tunnel session management)");
432
+ }
433
+ // Check for consistent label structure across subdomains (encoding pattern)
434
+ if (subLabels.length >= 2) {
435
+ const lengths = subLabels.map((l) => l.length);
436
+ const allSameLength = lengths.every((l) => l === lengths[0]);
437
+ if (allSameLength && lengths[0] >= 10) {
438
+ flags.push(`All ${subLabels.length} subdomain labels have uniform length (${lengths[0]}) — encoding indicator`);
439
+ }
440
+ }
441
+ return {
442
+ query: query.length > 80 ? query.substring(0, 80) + "..." : query,
443
+ subLabels,
444
+ flags,
445
+ isSuspicious: flags.length > 0,
446
+ };
447
+ });
448
+ // ── Sequential Label Detection ──
449
+ // Check if first labels across queries are incrementing
450
+ let sequentialAnalysis = { isSequential: false };
451
+ if (queries.length >= 3) {
452
+ const firstLabels = queries
453
+ .map((q) => extractSubdomainLabels(q))
454
+ .filter((labels) => labels.length > 0)
455
+ .map((labels) => labels[0]);
456
+ if (firstLabels.length >= 3) {
457
+ // Check for numeric incrementing
458
+ const numericParts = firstLabels
459
+ .map((l) => {
460
+ const match = l.match(/(\d+)$/);
461
+ return match ? parseInt(match[1], 10) : null;
462
+ })
463
+ .filter((n) => n !== null);
464
+ if (numericParts.length >= 3) {
465
+ let incrementing = 0;
466
+ for (let i = 1; i < numericParts.length; i++) {
467
+ if (numericParts[i] === numericParts[i - 1] + 1)
468
+ incrementing++;
469
+ }
470
+ const seqRatio = incrementing / (numericParts.length - 1);
471
+ if (seqRatio >= 0.7) {
472
+ sequentialAnalysis = {
473
+ isSequential: true,
474
+ detail: `${Math.round(seqRatio * 100)}% of labels show sequential incrementing (${incrementing}/${numericParts.length - 1} pairs)`,
475
+ };
476
+ }
477
+ }
478
+ }
479
+ }
480
+ const flaggedQueries = labelPatterns.filter((r) => r.isSuspicious);
481
+ return json({
482
+ summary: {
483
+ totalQueries: queries.length,
484
+ flaggedByPattern: flaggedQueries.length,
485
+ isBeaconing: timingAnalysis.isBeaconing ?? false,
486
+ hasSequentialLabels: sequentialAnalysis.isSequential,
487
+ },
488
+ timingAnalysis,
489
+ sequentialAnalysis,
490
+ labelPatterns,
491
+ flagged: flaggedQueries,
492
+ });
493
+ },
494
+ };
495
+ // ─── Tool 7: Full Tunnel Scan ───
496
+ const tunnelFullScan = {
497
+ name: "tunnel_full_scan",
498
+ description: "Comprehensive DNS tunneling detection that runs all 6 individual tunnel checks (entropy, length, TXT payload, record anomaly, tool signatures, covert channel), aggregates findings, and returns an overall tunnel probability score (0-100).",
499
+ schema: {
500
+ queries: z
501
+ .array(z.string())
502
+ .describe("List of DNS query names (FQDNs) to run the complete tunneling detection suite against"),
503
+ timestamps: z
504
+ .array(z.number())
505
+ .optional()
506
+ .describe("Optional array of Unix timestamps (milliseconds) for each query, used in beaconing/covert channel detection"),
507
+ },
508
+ execute: async (args, ctx) => {
509
+ const queries = args.queries;
510
+ const timestamps = args.timestamps;
511
+ if (!queries.length) {
512
+ return text("No queries provided for full tunnel scan.");
513
+ }
514
+ // Run all checks internally
515
+ const entropyResult = await tunnelEntropyAnalysis.execute({ queries }, ctx);
516
+ const lengthResult = await tunnelQueryLength.execute({ queries }, ctx);
517
+ const anomalyResult = await tunnelRecordAnomaly.execute({ queries }, ctx);
518
+ const sigResult = await tunnelToolSignatures.execute({ queries }, ctx);
519
+ const covertResult = await tunnelCovertChannel.execute({ queries, ...(timestamps ? { timestamps } : {}) }, ctx);
520
+ // Parse results from JSON text output
521
+ const parseResult = (result) => {
522
+ try {
523
+ return JSON.parse(result.content[0].text);
524
+ }
525
+ catch {
526
+ return {};
527
+ }
528
+ };
529
+ const entropy = parseResult(entropyResult);
530
+ const length = parseResult(lengthResult);
531
+ const anomaly = parseResult(anomalyResult);
532
+ const signatures = parseResult(sigResult);
533
+ const covert = parseResult(covertResult);
534
+ // ── Scoring ──
535
+ let score = 0;
536
+ const detectedTechniques = [];
537
+ const matchedTools = [];
538
+ // Entropy scoring (0-25 points)
539
+ const entropySummary = entropy.summary;
540
+ if (entropySummary) {
541
+ const tunnelRatio = (entropySummary.likelyTunnelCount ?? 0) / queries.length;
542
+ const suspiciousRatio = (entropySummary.suspiciousCount ?? 0) / queries.length;
543
+ score += Math.min(25, Math.round(tunnelRatio * 25 + suspiciousRatio * 10));
544
+ if (tunnelRatio > 0.3)
545
+ detectedTechniques.push("High-entropy encoded subdomains");
546
+ }
547
+ // Length scoring (0-20 points)
548
+ const lengthSummary = length.summary;
549
+ if (lengthSummary) {
550
+ const flaggedRatio = (lengthSummary.flaggedCount ?? 0) / queries.length;
551
+ score += Math.min(20, Math.round(flaggedRatio * 20));
552
+ if (flaggedRatio > 0.3)
553
+ detectedTechniques.push("Abnormal query/label lengths");
554
+ }
555
+ // Record anomaly scoring (0-15 points)
556
+ const anomalySummary = anomaly.summary;
557
+ if (anomalySummary) {
558
+ const flaggedQueries = anomalySummary.flaggedQueries ?? 0;
559
+ const flaggedRatio = flaggedQueries / queries.length;
560
+ score += Math.min(15, Math.round(flaggedRatio * 15));
561
+ if (flaggedRatio > 0.3)
562
+ detectedTechniques.push("Record type abuse patterns");
563
+ }
564
+ // Signature scoring (0-20 points)
565
+ const sigSummary = signatures.summary;
566
+ const detectedToolsList = signatures.detectedTools;
567
+ if (sigSummary) {
568
+ const matchedRatio = (sigSummary.queriesMatched ?? 0) / queries.length;
569
+ const toolCount = sigSummary.uniqueToolsDetected ?? 0;
570
+ score += Math.min(20, Math.round(matchedRatio * 15 + Math.min(toolCount, 3) * 2));
571
+ if (detectedToolsList) {
572
+ for (const t of detectedToolsList) {
573
+ matchedTools.push(t.tool);
574
+ }
575
+ }
576
+ if (matchedRatio > 0.2)
577
+ detectedTechniques.push("Known tunneling tool signatures matched");
578
+ }
579
+ // Covert channel scoring (0-20 points)
580
+ const covertSummary = covert.summary;
581
+ if (covertSummary) {
582
+ const isBeaconing = covertSummary.isBeaconing ?? false;
583
+ const hasSequential = covertSummary.hasSequentialLabels ?? false;
584
+ const patternFlagged = covertSummary.flaggedByPattern ?? 0;
585
+ const patternRatio = patternFlagged / queries.length;
586
+ if (isBeaconing) {
587
+ score += 12;
588
+ detectedTechniques.push("Beaconing behavior detected (regular timing intervals)");
589
+ }
590
+ if (hasSequential) {
591
+ score += 5;
592
+ detectedTechniques.push("Sequential label incrementing detected");
593
+ }
594
+ score += Math.min(3, Math.round(patternRatio * 3));
595
+ }
596
+ // Clamp score to 0-100
597
+ score = Math.max(0, Math.min(100, score));
598
+ // Risk classification
599
+ let riskLevel;
600
+ if (score >= 80)
601
+ riskLevel = "critical";
602
+ else if (score >= 60)
603
+ riskLevel = "high";
604
+ else if (score >= 35)
605
+ riskLevel = "medium";
606
+ else if (score >= 15)
607
+ riskLevel = "low";
608
+ else
609
+ riskLevel = "none";
610
+ // Per-query breakdown
611
+ const perQuery = queries.map((query, i) => {
612
+ const subLabels = extractSubdomainLabels(query);
613
+ const combined = subLabels.join("");
614
+ const combinedEntropy = combined.length > 0 ? shannonEntropy(combined) : 0;
615
+ const totalLength = query.replace(/\.$/, "").length;
616
+ const labels = query.replace(/\.$/, "").split(".");
617
+ const sigMatches = TUNNEL_SIGNATURES
618
+ .filter((sig) => sig.pattern.test(query))
619
+ .map((sig) => sig.tool);
620
+ const queryFlags = [];
621
+ if (combinedEntropy >= TUNNEL_THRESHOLDS.highEntropy)
622
+ queryFlags.push("high-entropy");
623
+ if (subLabels.some((l) => l.length > TUNNEL_THRESHOLDS.maxLabelLength))
624
+ queryFlags.push("long-labels");
625
+ if (totalLength > TUNNEL_THRESHOLDS.maxTotalLength)
626
+ queryFlags.push("long-query");
627
+ if (labels.length > TUNNEL_THRESHOLDS.maxLabelCount)
628
+ queryFlags.push("many-labels");
629
+ if (sigMatches.length > 0)
630
+ queryFlags.push("signature-match");
631
+ return {
632
+ query: query.length > 80 ? query.substring(0, 80) + "..." : query,
633
+ entropy: Math.round(combinedEntropy * 1000) / 1000,
634
+ totalLength,
635
+ labelCount: labels.length,
636
+ signatureMatches: sigMatches,
637
+ flags: queryFlags,
638
+ };
639
+ });
640
+ return json({
641
+ tunnelProbability: score,
642
+ riskLevel,
643
+ detectedTechniques,
644
+ matchedTools,
645
+ summary: {
646
+ totalQueries: queries.length,
647
+ entropy: entropySummary
648
+ ? {
649
+ normal: entropySummary.normalCount ?? 0,
650
+ suspicious: entropySummary.suspiciousCount ?? 0,
651
+ likelyTunnel: entropySummary.likelyTunnelCount ?? 0,
652
+ }
653
+ : null,
654
+ length: lengthSummary
655
+ ? { flagged: lengthSummary.flaggedCount ?? 0 }
656
+ : null,
657
+ anomaly: anomalySummary
658
+ ? { flagged: anomalySummary.flaggedQueries ?? 0 }
659
+ : null,
660
+ signatures: sigSummary
661
+ ? {
662
+ matched: sigSummary.queriesMatched ?? 0,
663
+ uniqueTools: sigSummary.uniqueToolsDetected ?? 0,
664
+ }
665
+ : null,
666
+ covert: covertSummary
667
+ ? {
668
+ beaconing: covertSummary.isBeaconing ?? false,
669
+ sequential: covertSummary.hasSequentialLabels ?? false,
670
+ flaggedByPattern: covertSummary.flaggedByPattern ?? 0,
671
+ }
672
+ : null,
673
+ },
674
+ perQueryBreakdown: perQuery,
675
+ });
676
+ },
677
+ };
678
+ // ─── Export ───
679
+ export const tunnelTools = [
680
+ tunnelEntropyAnalysis,
681
+ tunnelQueryLength,
682
+ tunnelTxtPayload,
683
+ tunnelRecordAnomaly,
684
+ tunnelToolSignatures,
685
+ tunnelCovertChannel,
686
+ tunnelFullScan,
687
+ ];
688
+ //# sourceMappingURL=index.js.map