
Analyzing Dns Logs For Exfiltration
- 434 installs
- 27.3k repo stars
- Updated August 2, 2026
- mukul975/anthropic-cybersecurity-skills
Run structured DNS log reviews to spot tunneling, C2 beaconing, and exfiltration when you operate your own apps or small-team production DNS telemetry.
About
Analyzing DNS Logs for Exfiltration is an agent skill that walks solo builders and small teams through hunting command-and-control and data theft hidden in DNS queries. It is aimed at anyone who can access Zeek, Splunk ES, or resolver logs—not only enterprise SOCs—and need repeatable heuristics instead of ad-hoc grep. The workflow ties DNS tunneling indicators (entropy, label length, record types), C2 behaviors (DGA, beaconing, fast-flux), DoH anomalies, and zone-transfer probes to concrete checks and example scripts. You get guidance on baselines, alert thresholds, and enrichment with threat intelligence and domain permutation tools. Use it when incidents, suspicious egress, or routine operate-phase reviews suggest DNS might be the covert channel. It matters because DNS is often logged everywhere while exfiltration patterns are easy to miss without a structured rubric.
- Maps DNS tunneling signals: high-entropy subdomains, long labels (>50 chars), TXT/NULL abuse, and resolver volume spikes
- Covers C2 patterns: DGA-like names, regular beacon intervals, fast-flux style IP churn, and rare query types with ready
- Includes DoH provider baselines, unauthorized zone-transfer attempts, and exfiltration heuristics tied to oversized or h
- References Sigma-style DNS exfiltration rules and dnstwist for permutation and typosquat context
- Operational playbook: log aggregation, entropy scoring, volume baselines, alert thresholds, and threat-intel enrichment
Analyzing Dns Logs For Exfiltration by the numbers
- 434 all-time installs (skills.sh)
- +23 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #532 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill analyzing-dns-logs-for-exfiltrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 434 |
|---|---|
| repo stars | ★ 27.3k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 2, 2026 |
| Repository | mukul975/anthropic-cybersecurity-skills ↗ |
What it does
Run structured DNS log reviews to spot tunneling, C2 beaconing, and exfiltration when you operate your own apps or small-team production DNS telemetry.
Files
Analyzing DNS Logs for Exfiltration
When to Use
Use this skill when:
- SOC teams suspect data exfiltration through DNS tunneling to bypass firewall/proxy controls
- Threat intelligence indicates adversaries using DNS-based C2 channels (e.g., Cobalt Strike DNS beacon)
- UEBA detects anomalous DNS query volumes from specific hosts
- Malware analysis reveals DNS-over-HTTPS (DoH) or DNS tunneling capabilities
Do not use for standard DNS troubleshooting or availability monitoring — this skill focuses on security-relevant DNS abuse detection.
Prerequisites
- DNS query logging enabled (Windows DNS Server, Bind, Infoblox, or Cisco Umbrella)
- DNS logs ingested into SIEM (Splunk with
Stream:DNS,dnssourcetype, or Zeek DNS logs) - Passive DNS data for historical domain resolution analysis
- Baseline of normal DNS behavior (query volume, domain distribution, TXT record frequency)
- Python with
mathandcollectionslibraries for entropy calculation
Workflow
Step 1: Detect DNS Tunneling via Subdomain Length Analysis
DNS tunneling encodes data in subdomain labels, creating unusually long queries:
index=dns sourcetype="stream:dns" query_type IN ("A", "AAAA", "TXT", "CNAME", "MX")
| eval domain_parts = split(query, ".")
| eval subdomain = mvindex(domain_parts, 0, mvcount(domain_parts)-3)
| eval subdomain_str = mvjoin(subdomain, ".")
| eval subdomain_len = len(subdomain_str)
| eval tld = mvindex(domain_parts, -1)
| eval registered_domain = mvindex(domain_parts, -2).".".tld
| where subdomain_len > 50
| stats count AS queries, dc(query) AS unique_queries,
avg(subdomain_len) AS avg_subdomain_len,
max(subdomain_len) AS max_subdomain_len,
values(src_ip) AS sources
by registered_domain
| where queries > 20
| sort - avg_subdomain_len
| table registered_domain, queries, unique_queries, avg_subdomain_len, max_subdomain_len, sourcesStep 2: Detect High-Entropy Domain Queries (DGA Detection)
Domain Generation Algorithms produce random-looking domains:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval sld = mvindex(domain_parts, -2)
| eval sld_len = len(sld)
| eval char_count = sld_len
| eval vowels = len(replace(sld, "[^aeiou]", ""))
| eval consonants = len(replace(sld, "[^bcdfghjklmnpqrstvwxyz]", ""))
| eval digits = len(replace(sld, "[^0-9]", ""))
| eval vowel_ratio = if(char_count > 0, vowels / char_count, 0)
| eval digit_ratio = if(char_count > 0, digits / char_count, 0)
| where sld_len > 12 AND (vowel_ratio < 0.2 OR digit_ratio > 0.3)
| stats count AS queries, dc(query) AS unique_domains, values(src_ip) AS sources
by query
| where unique_domains > 10
| sort - queriesPython-based Shannon Entropy Calculation for DNS queries:
import math
from collections import Counter
def shannon_entropy(text):
"""Calculate Shannon entropy of a string"""
if not text:
return 0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
# Test with examples
normal_domain = "google" # Low entropy
dga_domain = "x8kj2m9p4qw7n" # High entropy
tunnel_subdomain = "aGVsbG8gd29ybGQ.evil.com" # Base64 encoded data
print(f"Normal: {shannon_entropy(normal_domain)}") # ~2.25
print(f"DGA: {shannon_entropy(dga_domain)}") # ~3.70
print(f"Tunnel: {shannon_entropy(tunnel_subdomain)}") # ~3.50
# Threshold: entropy > 3.5 for subdomain = likely tunneling/DGASplunk implementation of entropy scoring:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval check_string = mvindex(domain_parts, 0)
| eval check_len = len(check_string)
| where check_len > 8
| eval chars = split(check_string, "")
| stats count AS total_chars, dc(chars) AS unique_chars by query, src_ip, check_string, check_len
| eval entropy_estimate = log(unique_chars, 2) * (unique_chars / check_len)
| where entropy_estimate > 3.5
| stats count AS high_entropy_queries, dc(query) AS unique_queries by src_ip
| where high_entropy_queries > 50
| sort - high_entropy_queriesStep 3: Detect Anomalous DNS Query Volume
Identify hosts generating abnormal DNS traffic:
index=dns sourcetype="stream:dns" earliest=-24h
| bin _time span=1h
| stats count AS queries, dc(query) AS unique_domains by src_ip, _time
| eventstats avg(queries) AS avg_queries, stdev(queries) AS stdev_queries by src_ip
| eval z_score = (queries - avg_queries) / stdev_queries
| where z_score > 3 OR queries > 5000
| sort - z_score
| table _time, src_ip, queries, unique_domains, avg_queries, z_scoreDetect TXT record abuse (common tunneling method):
index=dns sourcetype="stream:dns" query_type="TXT"
| stats count AS txt_queries, dc(query) AS unique_txt_domains,
values(query) AS domains by src_ip
| where txt_queries > 100
| eval suspicion = case(
txt_queries > 1000, "CRITICAL — Likely DNS tunneling",
txt_queries > 500, "HIGH — Possible DNS tunneling",
txt_queries > 100, "MEDIUM — Unusual TXT volume"
)
| sort - txt_queries
| table src_ip, txt_queries, unique_txt_domains, suspicionStep 4: Detect Known DNS Tunneling Tools
Search for signatures of common DNS tunneling tools:
index=dns sourcetype="stream:dns"
| eval query_lower = lower(query)
| where (
match(query_lower, "\.dnscat\.") OR
match(query_lower, "\.dns2tcp\.") OR
match(query_lower, "\.iodine\.") OR
match(query_lower, "\.dnscapy\.") OR
match(query_lower, "\.cobalt.*\.beacon") OR
query_type="NULL" OR
(query_type="TXT" AND len(query) > 100)
)
| stats count by src_ip, query, query_type
| sort - countDetect DNS over HTTPS (DoH) bypassing local DNS:
index=proxy OR index=firewall
dest IN ("1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4",
"9.9.9.9", "149.112.112.112", "208.67.222.222")
dest_port=443
| stats sum(bytes_out) AS total_bytes, count AS connections by src_ip, dest
| where connections > 100 OR total_bytes > 10485760
| eval alert = "Possible DoH bypass — DNS queries sent over HTTPS to public resolver"
| sort - total_bytesStep 5: Correlate DNS Findings with Endpoint Data
Cross-reference suspicious DNS with process data:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| stats count AS dns_queries, earliest(_time) AS first_query, latest(_time) AS last_query
by src_ip, query
| join src_ip [
search index=sysmon EventCode=3 DestinationPort=53 Computer="WORKSTATION-042"
| stats count AS connections, values(Image) AS processes by SourceIp
| rename SourceIp AS src_ip
]
| table src_ip, query, dns_queries, first_query, last_query, processesStep 6: Calculate Data Exfiltration Volume Estimate
Estimate data volume encoded in DNS queries:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| eval domain_parts = split(query, ".")
| eval encoded_data = mvindex(domain_parts, 0)
| eval encoded_bytes = len(encoded_data)
| eval decoded_bytes = encoded_bytes * 0.75 -- Base64 decoding factor
| stats sum(decoded_bytes) AS total_bytes_estimated, count AS total_queries,
earliest(_time) AS first_seen, latest(_time) AS last_seen
| eval estimated_kb = round(total_bytes_estimated / 1024, 1)
| eval estimated_mb = round(total_bytes_estimated / 1048576, 2)
| eval duration_hours = round((last_seen - first_seen) / 3600, 1)
| eval rate_kbps = round(estimated_kb / (duration_hours * 3600) * 8, 2)
| table total_queries, estimated_mb, duration_hours, rate_kbps, first_seen, last_seenKey Concepts
| Term | Definition |
|---|---|
| DNS Tunneling | Technique encoding data within DNS queries/responses to exfiltrate data or establish C2 channels through DNS |
| DGA | Domain Generation Algorithm — malware technique generating pseudo-random domain names for C2 resilience |
| Shannon Entropy | Mathematical measure of randomness in a string — high entropy (>3.5) in domain names indicates DGA or tunneling |
| TXT Record Abuse | Using DNS TXT records (designed for text data) as a high-bandwidth channel for data tunneling |
| DNS over HTTPS (DoH) | DNS queries encrypted over HTTPS (port 443), bypassing traditional DNS monitoring |
| Passive DNS | Historical record of DNS resolutions showing which IPs a domain resolved to over time |
Tools & Systems
- Splunk Stream: Network traffic capture add-on providing parsed DNS query data for SIEM analysis
- Zeek (Bro): Network security monitor generating detailed DNS transaction logs for analysis
- Cisco Umbrella (OpenDNS): Cloud DNS security platform blocking malicious domains and logging query data
- Infoblox DNS Firewall: DNS-layer security providing RPZ-based blocking and detailed query logging
- Farsight DNSDB: Passive DNS database for historical domain resolution lookups and infrastructure mapping
Common Scenarios
- Cobalt Strike DNS Beacon: Detect periodic TXT queries with encoded payloads to C2 domain
- Data Exfiltration: Large volumes of unique subdomain queries encoding stolen data in Base64/hex
- DGA Malware: Detect DNS queries to algorithmically generated domains (high entropy, no web content)
- DNS-over-HTTPS Bypass: Employee using DoH to bypass corporate DNS filtering and monitoring
- Slow Drip Exfiltration: Low-volume DNS tunneling staying below threshold alerts (requires baseline comparison)
Output Format
DNS EXFILTRATION ANALYSIS — WORKSTATION-042
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Period: 2024-03-14 to 2024-03-15
Source: 192.168.1.105 (WORKSTATION-042, Finance Dept)
Findings:
[CRITICAL] DNS tunneling detected to evil-tunnel[.]com
Query Volume: 12,847 queries in 18 hours
Avg Subdomain Len: 63 characters (normal: <20)
Avg Entropy: 3.82 (threshold: 3.5)
Query Types: TXT (89%), A (11%)
Estimated Data: ~4.7 MB exfiltrated via DNS
Rate: 0.58 kbps (slow drip pattern)
[HIGH] DGA-like domains resolved
Unique DGA Domains: 247 domains resolved
Pattern: 15-char random alphanumeric.xyz TLD
Entropy Range: 3.6 - 4.1
Process Attribution:
Process: svchost_update.exe (masquerading — not legitimate svchost)
PID: 4892
Parent: explorer.exe
Hash: SHA256: a1b2c3d4... (VT: 34/72 malicious — Cobalt Strike beacon)
Containment:
[DONE] Host isolated via EDR
[DONE] Domain evil-tunnel[.]com added to DNS sinkhole
[DONE] Incident IR-2024-0448 created
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please do not remove or change
the license header comment from a contributed file except when
necessary.
Copyright 2026 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
API Reference: DNS Exfiltration Detection Tools
Shannon Entropy Calculation
Python Implementation
import math
from collections import Counter
def shannon_entropy(text):
counter = Counter(text.lower())
length = len(text)
return -sum((c/length) * math.log2(c/length) for c in counter.values())Threshold Values
| Entropy | Classification |
|---|---|
| < 2.5 | Normal domain (e.g., "google") |
| 2.5 - 3.5 | Borderline (monitor) |
| > 3.5 | Suspicious (likely DGA/tunneling) |
| > 4.0 | High confidence malicious |
Splunk DNS Queries
Tunneling Detection
index=dns sourcetype="stream:dns"
| eval subdomain_len=len(mvindex(split(query,"."),0))
| where subdomain_len > 50
| stats count by registered_domain, src_ipDGA Detection
index=dns
| eval sld=mvindex(split(query,"."), -2)
| where len(sld) > 12
| stats count, dc(query) AS unique by src_ipVolume Anomaly
index=dns earliest=-24h
| bin _time span=1h
| stats count AS queries by src_ip, _time
| eventstats avg(queries) AS avg_q, stdev(queries) AS stdev_q by src_ip
| eval z_score=(queries - avg_q) / stdev_q
| where z_score > 3TXT Record Abuse
index=dns query_type="TXT"
| stats count AS txt_queries by src_ip
| where txt_queries > 100Zeek DNS Log Format
Log Fields (dns.log)
| Column | Field | Description |
|---|---|---|
| 0 | ts | Timestamp |
| 2 | id.orig_h | Source IP |
| 4 | id.resp_h | DNS server IP |
| 9 | query | Query domain name |
| 13 | qtype_name | Query type (A, TXT, CNAME) |
| 15 | rcode_name | Response code |
| 21 | answers | Response answers |
Zeek CLI Analysis
cat dns.log | zeek-cut query qtype_name id.orig_h | sort | uniq -c | sort -rnDNS Tunneling Tools (Detection Signatures)
| Tool | DNS Pattern |
|---|---|
| iodine | *.pirate.sea (TXT/NULL records) |
| dnscat2 | *.dnscat. prefix in queries |
| dns2tcp | *.dns2tcp. pattern |
| Cobalt Strike DNS | Periodic TXT queries with encoded payloads |
Passive DNS Lookup APIs
Farsight DNSDB
curl -H "X-API-Key: $KEY" \
"https://api.dnsdb.info/dnsdb/v2/lookup/rrset/name/evil.com/A"VirusTotal Domain Resolutions
curl -H "x-apikey: $KEY" \
"https://www.virustotal.com/api/v3/domains/evil.com/resolutions"Cisco Umbrella (OpenDNS) Investigate API
Domain Categorization
curl -H "Authorization: Bearer $TOKEN" \
"https://investigate.api.umbrella.com/domains/categorization/evil.com"Security Information
curl -H "Authorization: Bearer $TOKEN" \
"https://investigate.api.umbrella.com/security/name/evil.com"#!/usr/bin/env python3
"""DNS exfiltration detection agent using entropy analysis and query pattern detection."""
import math
from collections import Counter, defaultdict
def shannon_entropy(text):
"""Calculate Shannon entropy of a string."""
if not text:
return 0.0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
def extract_subdomain(fqdn):
"""Extract the subdomain portion from a fully qualified domain name."""
parts = fqdn.rstrip(".").split(".")
if len(parts) > 2:
return ".".join(parts[:-2])
return ""
def extract_registered_domain(fqdn):
"""Extract the registered domain (SLD + TLD) from an FQDN."""
parts = fqdn.rstrip(".").split(".")
if len(parts) >= 2:
return ".".join(parts[-2:])
return fqdn
def detect_tunneling(dns_records, subdomain_len_threshold=50, min_queries=20):
"""Detect DNS tunneling based on subdomain length anomalies."""
domain_stats = defaultdict(lambda: {"queries": 0, "unique_queries": set(),
"subdomain_lengths": [], "sources": set()})
for record in dns_records:
query = record.get("query", "")
src = record.get("src_ip", "unknown")
subdomain = extract_subdomain(query)
reg_domain = extract_registered_domain(query)
if len(subdomain) > subdomain_len_threshold:
stats = domain_stats[reg_domain]
stats["queries"] += 1
stats["unique_queries"].add(query)
stats["subdomain_lengths"].append(len(subdomain))
stats["sources"].add(src)
alerts = []
for domain, stats in domain_stats.items():
if stats["queries"] >= min_queries:
avg_len = sum(stats["subdomain_lengths"]) / len(stats["subdomain_lengths"])
max_len = max(stats["subdomain_lengths"])
alerts.append({
"domain": domain,
"queries": stats["queries"],
"unique_queries": len(stats["unique_queries"]),
"avg_subdomain_length": round(avg_len, 1),
"max_subdomain_length": max_len,
"sources": list(stats["sources"]),
"verdict": "CRITICAL - Likely DNS tunneling",
})
return sorted(alerts, key=lambda x: x["avg_subdomain_length"], reverse=True)
def detect_dga(dns_records, entropy_threshold=3.5, min_sld_length=12):
"""Detect Domain Generation Algorithm queries using entropy scoring."""
suspicious = defaultdict(lambda: {"count": 0, "sources": set(), "entropies": []})
for record in dns_records:
query = record.get("query", "").rstrip(".")
src = record.get("src_ip", "unknown")
parts = query.split(".")
if len(parts) < 2:
continue
sld = parts[-2]
if len(sld) < min_sld_length:
continue
ent = shannon_entropy(sld)
if ent > entropy_threshold:
suspicious[query]["count"] += 1
suspicious[query]["sources"].add(src)
suspicious[query]["entropies"].append(ent)
alerts = []
for domain, data in suspicious.items():
avg_entropy = sum(data["entropies"]) / len(data["entropies"])
alerts.append({
"domain": domain,
"queries": data["count"],
"avg_entropy": round(avg_entropy, 4),
"sources": list(data["sources"]),
"verdict": "HIGH - Possible DGA domain",
})
return sorted(alerts, key=lambda x: x["avg_entropy"], reverse=True)
def detect_volume_anomaly(dns_records, z_score_threshold=3.0):
"""Detect hosts with anomalously high DNS query volumes."""
host_counts = defaultdict(int)
for record in dns_records:
src = record.get("src_ip", "unknown")
host_counts[src] += 1
if not host_counts:
return []
values = list(host_counts.values())
mean_q = sum(values) / len(values)
if len(values) < 2:
return []
variance = sum((x - mean_q) ** 2 for x in values) / (len(values) - 1)
stdev_q = variance ** 0.5
if stdev_q == 0:
return []
anomalies = []
for host, count in host_counts.items():
z = (count - mean_q) / stdev_q
if z > z_score_threshold:
anomalies.append({
"src_ip": host,
"queries": count,
"z_score": round(z, 2),
"mean": round(mean_q, 1),
"verdict": "HIGH - Anomalous query volume",
})
return sorted(anomalies, key=lambda x: x["z_score"], reverse=True)
def detect_txt_abuse(dns_records, threshold=100):
"""Detect excessive TXT record queries (common tunneling method)."""
txt_counts = defaultdict(lambda: {"count": 0, "unique_domains": set()})
for record in dns_records:
qtype = str(record.get("query_type", "")).upper()
if qtype in ("TXT", "16"):
src = record.get("src_ip", "unknown")
txt_counts[src]["count"] += 1
txt_counts[src]["unique_domains"].add(record.get("query", ""))
alerts = []
for src, data in txt_counts.items():
if data["count"] > threshold:
level = "CRITICAL" if data["count"] > 1000 else "HIGH" if data["count"] > 500 else "MEDIUM"
alerts.append({
"src_ip": src,
"txt_queries": data["count"],
"unique_domains": len(data["unique_domains"]),
"verdict": f"{level} - Possible DNS tunneling via TXT records",
})
return sorted(alerts, key=lambda x: x["txt_queries"], reverse=True)
def estimate_exfil_volume(dns_records, target_domain):
"""Estimate data volume encoded in DNS queries to a specific domain."""
total_encoded_bytes = 0
query_count = 0
for record in dns_records:
query = record.get("query", "")
if target_domain in query:
subdomain = extract_subdomain(query)
total_encoded_bytes += len(subdomain)
query_count += 1
decoded_bytes = int(total_encoded_bytes * 0.75) # Base64 decode factor
return {
"target_domain": target_domain,
"total_queries": query_count,
"encoded_bytes": total_encoded_bytes,
"estimated_decoded_bytes": decoded_bytes,
"estimated_kb": round(decoded_bytes / 1024, 1),
"estimated_mb": round(decoded_bytes / (1024 * 1024), 3),
}
def parse_zeek_dns_log(log_path):
"""Parse a Zeek dns.log file into structured records."""
records = []
with open(log_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
parts = line.strip().split("\t")
if len(parts) >= 10:
records.append({
"timestamp": parts[0],
"src_ip": parts[2],
"src_port": parts[3],
"dst_ip": parts[4],
"query": parts[9] if len(parts) > 9 else "",
"query_type": parts[13] if len(parts) > 13 else "",
})
return records
if __name__ == "__main__":
print("=" * 60)
print("DNS Exfiltration Detection Agent")
print("Tunneling, DGA, volume anomaly, and TXT abuse detection")
print("=" * 60)
# Demo with synthetic DNS records
demo_records = [
{"query": f"{'a' * 60}.evil-tunnel.com", "src_ip": "192.168.1.105",
"query_type": "TXT"} for _ in range(50)
] + [
{"query": "x8kj2m9p4qw7nz3.xyz", "src_ip": "192.168.1.110",
"query_type": "A"} for _ in range(5)
] + [
{"query": "google.com", "src_ip": "192.168.1.50", "query_type": "A"}
for _ in range(10)
]
print("\n--- DNS Tunneling Detection ---")
tunneling = detect_tunneling(demo_records, subdomain_len_threshold=30, min_queries=10)
for t in tunneling:
print(f"[!] {t['domain']}: {t['queries']} queries, "
f"avg subdomain len={t['avg_subdomain_length']}")
print("\n--- DGA Detection ---")
dga = detect_dga(demo_records, entropy_threshold=3.0, min_sld_length=10)
for d in dga[:5]:
print(f"[!] {d['domain']}: entropy={d['avg_entropy']}")
print("\n--- TXT Record Abuse ---")
txt = detect_txt_abuse(demo_records, threshold=10)
for t in txt:
print(f"[!] {t['src_ip']}: {t['txt_queries']} TXT queries")
print("\n--- Entropy Examples ---")
examples = ["google", "x8kj2m9p4qw7n", "aGVsbG8gd29ybGQ"]
for ex in examples:
print(f" '{ex}' -> entropy={shannon_entropy(ex)}")
Related skills
FAQ
Is Analyzing Dns Logs For Exfiltration safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.