Cross-reference DNS, proxy, and traffic logs against 17,410+ classified AI domains. Surface every shadow AI tool in hours.
AI tools require no installation, skip OAuth, and run on domains unknown to legacy threat feeds. Your security stack was not built for this.
Flags Dropbox OAuth but misses employees pasting drafts into AI writing assistants on brand-new domains.
Alerts on unauthorized executables but ignores browser tabs sending source code to AI debugging services over HTTPS.
Inspects outbound email attachments but cannot inspect text pasted into web forms on unknown domains.
Operate at the network layer where all web traffic is visible.
Every connection starts with a DNS lookup. Correlate resolver logs against the AI tools feed to reveal every tool accessed.
HTTP-level detail: full URLs, methods, content types, and bytes transferred. Large POST payloads flag data submission.
Connection-level metadata: source, destination, duration, bytes. Detects long-lived interactive AI sessions.
DNS is the most valuable data source for shadow AI detection. Every browser navigation triggers a query your resolver already logs.
Export query logs from your DNS server or DNS filtering platform.
Load the AI Tools Blocklist as a lookup table. Every domain match confirms AI tool access.
Rank AI tools by frequency, identify heaviest users, and break down usage across 18 categories.
This Python script processes BIND-style query logs. Modify the parsing function to adapt to any DNS format.
#!/usr/bin/env python3 """Shadow AI detection via DNS query log analysis. Cross-references DNS resolver logs against the AI Tools Blocklist to identify unauthorized AI tool usage across the enterprise.""" import csv import re import sys import json from collections import Counter, defaultdict from datetime import datetime from pathlib import Path def load_blocklist(csv_path: str) -> dict: """Load AI Tools Blocklist CSV into a domain lookup dictionary.""" domains = {} with open(csv_path, "r") as f: reader = csv.DictReader(f) for row in reader: domains[row["domain"].strip().lower()] = { "category": row.get("primary_category", "Uncategorized"), "tool_name": row.get("tool_name", "Unknown"), "risk_level": row.get("risk_level", "medium"), } return domains def parse_bind_log(log_path: str): """Parse BIND/named query log lines. Expected format: DD-Mon-YYYY HH:MM:SS.mmm ... query: domain IN A ... (client_ip) Yields (timestamp_str, client_ip, queried_domain) tuples.""" pattern = re.compile( r"(\d{2}-\w{3}-\d{4}\s+\d{2}:\d{2}:\d{2})\.\d+\s+" r".*query:\s+([\w.\-]+)\s+IN\s+\w+.*\((\d+\.\d+\.\d+\.\d+)\)" ) with open(log_path, "r") as f: for line in f: m = pattern.search(line) if m: yield m.group(1), m.group(3), m.group(2).lower().rstrip(".") def extract_root_domain(fqdn: str) -> str: """Extract registrable domain from FQDN for matching.""" parts = fqdn.split(".") if len(parts) >= 2: return ".".join(parts[-2:]) return fqdn def detect_shadow_ai(log_path: str, blocklist_path: str): """Main detection: correlate DNS logs against AI tools blocklist.""" ai_domains = load_blocklist(blocklist_path) hits = defaultdict(lambda: { "clients": Counter(), "first_seen": None, "last_seen": None, "total_queries": 0, }) category_stats = Counter() total_queries = 0 matched_queries = 0 for ts, client_ip, domain in parse_bind_log(log_path): total_queries += 1 root = extract_root_domain(domain) match_domain = domain if domain in ai_domains else ( root if root in ai_domains else None ) if match_domain: matched_queries += 1 entry = hits[match_domain] entry["clients"][client_ip] += 1 entry["total_queries"] += 1 if not entry["first_seen"] or ts < entry["first_seen"]: entry["first_seen"] = ts if not entry["last_seen"] or ts > entry["last_seen"]: entry["last_seen"] = ts category_stats[ai_domains[match_domain]["category"]] += 1 # Generate report report = { "scan_date": datetime.now().isoformat(), "total_dns_queries": total_queries, "ai_tool_queries": matched_queries, "unique_ai_domains": len(hits), "unique_clients": len(set( ip for h in hits.values() for ip in h["clients"] )), "category_breakdown": dict(category_stats.most_common()), "top_domains": [], } for domain, data in sorted( hits.items(), key=lambda x: x[1]["total_queries"], reverse=True )[:50]: info = ai_domains[domain] report["top_domains"].append({ "domain": domain, "tool_name": info["tool_name"], "category": info["category"], "risk_level": info["risk_level"], "total_queries": data["total_queries"], "unique_clients": len(data["clients"]), "first_seen": data["first_seen"], "last_seen": data["last_seen"], }) return report if __name__ == "__main__": report = detect_shadow_ai(sys.argv[1], sys.argv[2]) print(json.dumps(report, indent=2)) print(f"\n{'='*60}") print(f"Shadow AI Detection Report — {report['scan_date'][:10]}") print(f"{'='*60}") print(f"Total DNS queries analyzed: {report['total_dns_queries']:,}") print(f"AI tool queries detected: {report['ai_tool_queries']:,}") print(f"Unique AI tool domains: {report['unique_ai_domains']}") print(f"Unique client IPs: {report['unique_clients']}") print(f"\nTop 20 Shadow AI Tools:") for i, d in enumerate(report["top_domains"][:20], 1): print(f" {i:2}. {d['domain']:<35} {d['category']:<25} " f"queries={d['total_queries']:<8} users={d['unique_clients']}")
extract_root_domain matches both FQDNs and root domainstldextract for public suffix handling in productionBatch processing works for audits. For continuous monitoring, stream DNS queries and alert in near-real-time.
#!/usr/bin/env python3 """Real-time shadow AI detection — tail DNS logs and alert on AI tool queries.""" import csv import time import json import re import logging from pathlib import Path from datetime import datetime, timedelta from collections import defaultdict logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("shadow_ai_monitor") class ShadowAIMonitor: def __init__(self, blocklist_path: str, alert_threshold: int = 5): self.ai_domains = self._load_blocklist(blocklist_path) self.alert_threshold = alert_threshold self.window = defaultdict(lambda: defaultdict(int)) self.window_start = datetime.now() self.alerted = set() logger.info(f"Loaded {len(self.ai_domains):,} AI tool domains") def _load_blocklist(self, path: str) -> dict: domains = {} with open(path) as f: for row in csv.DictReader(f): domains[row["domain"].strip().lower()] = row return domains def _resolve_domain(self, fqdn: str): """Match FQDN or root domain against blocklist.""" if fqdn in self.ai_domains: return fqdn root = ".".join(fqdn.split(".")[-2:]) return root if root in self.ai_domains else None def process_query(self, client_ip: str, domain: str): matched = self._resolve_domain(domain.lower().rstrip(".")) if not matched: return self.window[matched][client_ip] += 1 info = self.ai_domains[matched] total = sum(self.window[matched].values()) if total >= self.alert_threshold and matched not in self.alerted: self.alerted.add(matched) alert = { "alert_type": "shadow_ai_detected", "domain": matched, "tool_name": info.get("tool_name", "Unknown"), "category": info.get("primary_category", "Unknown"), "unique_users": len(self.window[matched]), "total_queries": total, "timestamp": datetime.now().isoformat(), } logger.warning(f"SHADOW AI ALERT: {json.dumps(alert)}") self._send_to_siem(alert) def _send_to_siem(self, alert: dict): """Forward alert to SIEM via syslog, webhook, or file.""" with open("/var/log/shadow_ai_alerts.json", "a") as f: f.write(json.dumps(alert) + "\n") def rotate_window(self, interval_minutes: int = 60): if datetime.now() - self.window_start > timedelta(minutes=interval_minutes): self.window.clear() self.alerted.clear() self.window_start = datetime.now() logger.info("Detection window rotated")
Groups queries by domain and client IP within configurable time intervals.
Fires structured JSON alerts when a domain crosses the configured query count.
Resets counters on a configurable interval to prevent alert fatigue.
1-hour window with threshold of 5 queries filters DNS prefetch noise from real usage.
Which AI tools are accessed. Could be a brief visit, curiosity click, or accidental navigation.
HTTP methods, content types, and bytes transferred in each direction reveal actual data submission.
This script processes standard proxy access logs. Adapts to any SWG platform.
#!/usr/bin/env python3 """Proxy log analysis for shadow AI detection with data exfiltration scoring. Parses SWG/proxy access logs, correlates against AI Tools Blocklist, and scores sessions by data leakage risk.""" import csv import re import sys from collections import defaultdict from dataclasses import dataclass, field from urllib.parse import urlparse @dataclass class AIToolSession: domain: str = "" category: str = "" tool_name: str = "" get_count: int = 0 post_count: int = 0 bytes_sent: int = 0 bytes_received: int = 0 client_ips: set = field(default_factory=set) timestamps: list = field(default_factory=list) @property def risk_score(self) -> int: """Score 0-100 based on data submission indicators.""" score = 0 if self.post_count > 0: score += 30 if self.bytes_sent > 10_000: score += 20 if self.bytes_sent > 100_000: score += 15 if self.post_count > 5: score += 15 if len(self.client_ips) > 3: score += 10 if self.bytes_sent > self.bytes_received: score += 10 return min(score, 100) def analyze_proxy_logs(log_path: str, blocklist_path: str): """Parse proxy logs and identify AI tool sessions with risk scoring.""" ai_domains = {} with open(blocklist_path) as f: for row in csv.DictReader(f): ai_domains[row["domain"].strip().lower()] = row sessions = defaultdict(AIToolSession) # Proxy log pattern: timestamp elapsed client action/code size method URL proxy_re = re.compile( r"(\d+\.\d+)\s+\d+\s+([\d.]+)\s+\w+/(\d+)\s+(\d+)\s+(\w+)\s+(https?://\S+)" ) with open(log_path) as f: for line in f: m = proxy_re.search(line) if not m: continue ts, client_ip, status, size, method, url = m.groups() domain = urlparse(url).hostname if not domain: continue root = ".".join(domain.split(".")[-2:]) matched = domain if domain in ai_domains else ( root if root in ai_domains else None ) if not matched: continue s = sessions[matched] s.domain = matched s.category = ai_domains[matched].get("primary_category", "") s.tool_name = ai_domains[matched].get("tool_name", "") s.client_ips.add(client_ip) s.timestamps.append(float(ts)) if method == "POST": s.post_count += 1 s.bytes_sent += int(size) elif method == "GET": s.get_count += 1 s.bytes_received += int(size) # Output ranked by risk score print(f"{'Domain':<35} {'Category':<22} {'Risk':>4} {'POSTs':>5} {'Sent':>10} {'Users':>5}") print("─" * 95) for s in sorted(sessions.values(), key=lambda x: x.risk_score, reverse=True): sent_kb = f"{s.bytes_sent / 1024:.1f} KB" print(f"{s.domain:<35} {s.category:<22} {s.risk_score:>4} {s.post_count:>5} {sent_kb:>10} {len(s.client_ips):>5}") analyze_proxy_logs(sys.argv[1], sys.argv[2])
| Indicator | Points | Interpretation |
|---|---|---|
| Any POST request | +30 | Content submission — text, files, or form data |
| Bytes sent > 10 KB | +20 | Substantial payload beyond simple prompts |
| Bytes sent > 100 KB | +15 | Document-sized uploads or code pastes |
| POST count > 5 | +15 | Sustained usage, not one-time curiosity |
| Multiple users (> 3) | +10 | Viral adoption across the organization |
| Bytes sent > received | +10 | Inverted ratio = uploading content, not browsing |
Active data submission to AI tools. Immediate investigation recommended.
Incidental browsing, marketing pages, or API health checks. Low priority.
AI tools exhibit distinctive network signatures. Behavioral heuristics catch usage even before domains appear in any blocklist.
Single large upload — the user's pasted text, code, or files.
Model inference delay. Standard web apps respond in under 500ms.
Persistent HTTP connection delivering tokens incrementally. Unlike standard page loads.
Domain in the blocklist means it is definitively an AI tool. Generate immediate alerts.
Queue for analyst review. Covers the 24-72 hour gap before blocklist inclusion.
Each detection layer compounds coverage of enterprise shadow AI usage — all keyed to the same 17,410+ domain feed.
Detection outputs integrate into your existing SIEM. The scripts output structured JSON for ingestion via any method.
Percentage of AI tool usage your program identifies. Compare detected domains against the full blocklist.
Rate of false positives in your detections. Report misclassified domains and they are corrected in a subsequent daily export.
No alerts generated.
Informational alerts feed dashboards. No incident response.
High-severity alerts with automated manager and SOC notification.
No new infrastructure, vendors, or procurement cycles required. The AI Tools Blocklist is the missing intelligence layer.
Most enterprises discover 50-200 shadow AI tools in their first scan. Request a trial to unlock the full 17,410+ domain feed.
Tell us about your environment and detection goals — we will provide a tailored trial of the full AI tools domain feed.