Skip to main content

shadowaitools

shadowaitools turns a DNS, proxy or firewall export into an inventory of the AI tools in use on your network. It runs on your own machine: the export is parsed locally, every hostname is reduced to its registrable domain, and each unique domain is looked up once against the AI blocklist for web filtering at aitoolsblocklist.com. What comes back is a list of tools with category, AI type, hit counts, the users or devices that reached them, and the vendor's position on training with your data, dated.

It is the command line and Python counterpart of the hosted service at shadowaitools.com, which lets you find the AI tools employees use from the same export in a browser and produces the per-user breakdown, the sanctioned versus unsanctioned split and a PDF evidence pack.

Only requests is required. Python 3.7 and newer.


Installation

pip install shadowaitools

This installs the shadowaitools command and the shadowaitools package. The API key is an AI Tools Blocklist key from the account area at aitoolsblocklist.com; pass it as --key or export it once:

export SHADOWAITOOLS_API_KEY=your_key

Quick start

shadowaitools scan nextdns-export.csv --csv inventory.csv --json inventory.json
Shadow AI inventory (csv export, 400 lines, 45 unique domains, 45 lookups)
AI tools found: 34   users involved: 10   train on your data by default: 16   terms silent: 15

domain                      hits   ai type     category                    trains on data   sanctioned  users
--------------------------  -----  ----------  --------------------------  ---------------  ----------  ----------------------
character.ai                10     ai_native   Text & Language             yes              no          laptop-marketing-02 +2
openai.com                  10     ai_native   Code & Development          opt_out_default  yes         laptop-eng-07 +4
otter.ai                    7      ai_native   Audio, Voice & Music        yes              no          laptop-sales-09 +2
midjourney.com              5      ai_native   Image & Visual              unstated         no          laptop-exec-01 +1
elevenlabs.io               4      ai_native   Text & Language             opt_out_default  no          laptop-eng-07 +1
fireflies.ai                3      ai_native   Audio, Voice & Music        no               no          desktop-support-03 +1

In Python:

from shadowaitools import scan, to_csv

inventory = scan(
    "zscaler-web.csv",
    api_key="your_key",
    sanctioned=["openai.com", "github.com"],
    cache_file="lookups.json",
)

print(inventory["summary"])
# {'lines': 400, 'records': 400, 'unique_domains': 45, 'lookups': 45, 'ai_tools_found': 34,
#  'sanctioned': 2, 'unsanctioned': 32, 'users_involved': 10, 'training_default_yes': 16,
#  'training_no': 3, 'unstated': 15, 'quota_remaining': 9999940}

for tool in inventory["tools"]:
    if not tool["sanctioned"] and tool["ai_type"] == "ai_native":
        print(tool["domain"], tool["hits"], [u["name"] for u in tool["users"]], tool["trains_on_data"])

with open("inventory.csv", "w") as fh:
    fh.write(to_csv(inventory))

How a scan works

  1. Parse. The first non-empty line decides the format. A header row with a known column name means CSV (comma, tab, semicolon or pipe). hostname= or dstname= tokens mean key=value syslog. query[A] means dnsmasq. The Squid access.log layout and Windows DNS Server debug packets have their own detectors. Anything else is read line by line for the first hostname and the first private IP address.
  2. Reduce. Each hostname becomes a registrable domain (chat.openai.com to openai.com, news.bbc.co.uk to bbc.co.uk). Names in reserved zones (.local, .internal, .lan, .corp, .home, .arpa, .test, .example) are dropped before anything is sent.
  3. Look up. Each unique domain is sent once to GET https://www.aitoolsblocklist.com/api/check?domain=<domain> with the key in the X-API-Key header. With cache_file, domains seen on a previous run are answered from the cache.
  4. Assemble. Domains with blocked: true become tools. Hits, hosts and users are attached from the parse, the sanctioned flag from your list, and the category, AI type and training fields from the lookup.

A first run on a 400-line export with 45 distinct domains makes 45 lookups. A month of resolver logs with two million lines usually collapses to a few thousand registrable domains, because most traffic goes to a small set of hosts, and the second run through the same cache pays only for domains that are new that day. The domains command prints the exact count before any lookup is made.

The user or device column is optional. When the export carries Identities, user, Source User, device_name, client_ip, src or a similar column, every tool lists who reached it; when it does not, the inventory still has the tools and the hit counts.

Accepted exports

Source Format Hostname column or field User column or field
Cisco Umbrella activity export csv Domain Identities, Internal IP
Cloudflare Gateway DNS log csv QueryName DeviceName, SourceIP, Email
DNSFilter query log csv domain client, device
NextDNS log export csv domain client_ip, device_name
Palo Alto URL filtering log csv URL Source User, Source address
Zscaler web log csv url user, cip
Fortinet FortiGate web filter syslog key-value hostname= user=, srcip=
SonicWall syslog key-value dstname= usr=, src=
Pi-hole and dnsmasq dnsmasq query[A] name from address
Squid squid CONNECT host:443 client IP, authenticated user
Windows DNS Server debug log windows-dns encoded question name client address
Plain hostname or URL list generic the line none
Any other text log generic first hostname on the line first private IPv4 on the line

Anything with a header row and a hostname column works, whatever produced it. The formats command prints this list from the installed version.

API

scan(source, api_key=None, **options)

source is a file path, a string of contents, bytes or an open file. Options:

Option Default Meaning
api_key env SHADOWAITOOLS_API_KEY or ATB_API_KEY AI Tools Blocklist key
concurrency 1 parallel lookups
pause 0.0 seconds to wait after each lookup
max_lines none stop parsing after this many lines
cache_file none JSON file of previous lookups, reused and extended
sanctioned [] domains to flag as sanctioned
on_progress none callback (done, total)
timeout, max_retries, base_url 30, 2, production passed to the client

Returns a dict with generated, format, summary, by_category and tools.

Tool fields

Field Values
domain, hosts, hits, users from the export
sanctioned True when the domain is in your list
primary_category one of 18 functional categories
categories [{"category", "subcategory"}], a tool can sit in several
ai_type ai_native or ai_enabled
trains_on_data yes, no, opt_out_default, unstated
opt_out_available, enterprise_no_training, api_no_training same value set
terms_checked ISO date the vendor terms were last read

Other functions

Function Purpose
parse_log(source, max_lines=None) {"format", "lines", "records": [{"host", "user"}]} with no network call
extract_domains(records) [{"domain", "hosts", "hits", "users"}] sorted by hits
registrable_domain(host) reduce a hostname
to_csv(inventory) one CSV row per tool
to_table(inventory) fixed-width text table
Client(api_key).lookup(domain) the raw lookup

Exceptions

AuthenticationError (401), QuotaError (403, inactive account or monthly quota used up), RateLimitError (429 after two retries) and the base ShadowAIToolsError (anything else, including 503 after retries). Each carries .status and .body.

Command line

shadowaitools scan <file> [--key KEY] [--json FILE] [--csv FILE] [--cache FILE]
                          [--sanctioned a.com,b.com] [--max-lines N] [--concurrency N]
                          [--pause SECONDS] [--quiet]
shadowaitools domains <file>     unique registrable domains and hit counts, no lookups
shadowaitools formats            the accepted export formats

domains is the dry run: it prints the format that was detected and the number of lookups a scan would need.

Worked examples

Weekly report for a security team

# weekly_shadow_ai.py
import datetime
import json
from shadowaitools import scan, to_csv

week = datetime.date.today().isocalendar()[1]
inv = scan(
    f"/exports/umbrella-week-{week}.csv",
    cache_file="/var/lib/shadowaitools/lookups.json",
    sanctioned=open("/etc/shadowaitools/approved.txt").read().split(),
    pause=0.05,
)

with open(f"/reports/shadow-ai-week-{week}.csv", "w") as fh:
    fh.write(to_csv(inv))

risky = [t for t in inv["tools"] if not t["sanctioned"] and t["trains_on_data"] in ("yes", "opt_out_default")]
print(f"week {week}: {inv['summary']['ai_tools_found']} tools, {len(risky)} unsanctioned tools that train on input")
for t in sorted(risky, key=lambda t: -t["hits"])[:10]:
    print(f"  {t['domain']:28} {t['hits']:5} hits  {len(t['users']):3} users  {t['primary_category']}")

Comparing two weeks

from shadowaitools import scan

before = scan("proxy-week-36.log", cache_file="lookups.json")
after = scan("proxy-week-37.log", cache_file="lookups.json")

seen_before = {t["domain"] for t in before["tools"]}
new_tools = [t for t in after["tools"] if t["domain"] not in seen_before]
print("new AI tools this week:", [t["domain"] for t in new_tools])

Because both scans share the cache, the second one only pays for domains that did not appear in the first.

Streaming a large export in chunks

Exports from a busy resolver run to millions of lines. parse_log accepts a string, so a file can be read in blocks and grouped before a single lookup is made.

from shadowaitools import Client, extract_domains, parse_log, registrable_domain

records = []
with open("dns-month.log", encoding="utf-8", errors="replace") as fh:
    block = []
    for line in fh:
        block.append(line)
        if len(block) == 200_000:
            records.extend(parse_log("".join(block))["records"])
            block = []
    if block:
        records.extend(parse_log("".join(block))["records"])

groups = extract_domains(records)
print(len(records), "records,", len(groups), "unique domains")

client = Client("your_key")
tools = []
for g in groups:
    r = client.lookup(g["domain"])
    if r.get("blocked"):
        tools.append((g["domain"], g["hits"], r.get("primary_category"), r.get("trains_on_data")))

Only the lookup

from shadowaitools import Client

c = Client("your_key")
print(c.lookup("chat.openai.com"))
# {'domain': 'openai.com', 'blocked': True, 'primary_category': 'Code & Development', 'ai_type': 'ai_native',
#  'categories': [...], 'trains_on_data': 'opt_out_default', 'opt_out_available': 'yes', ...}
print(c.lookup("example.com")["blocked"])
# False

Why the logs are the right starting point

Every governance framework begins with an inventory. The NIST AI Risk Management Framework puts "Map" before "Measure" and "Manage": an organisation has to know which AI systems are in use before any control can be applied. The ENISA work on AI cybersecurity makes the same point for European organisations, and the CISA guidance on secure AI deployment assumes that operators can enumerate the AI services their people reach. On the data-protection side, the ICO's guidance on AI and data protection treats the flow of personal data into third-party AI services as a processing activity that has to be documented.

The inventory you need already exists in the DNS filter, proxy or firewall. It is complete in a way a survey can never be, it costs nothing to export, and it names the hostnames rather than the products people remember. The missing step is classification, which is what the lookup adds: is this domain an AI tool, what kind, and what does the vendor do with the input. Alongside the tool inventory, the same organisation usually wants the opposite control for its own agents, which is where an allow list for AI agents comes in: the AI agent allow list tells a browsing agent which pages on a site it may open and which it must not, so the two datasets cover both directions of AI traffic.

Hosted audit

The package produces the inventory. The hosted shadow AI inventory at shadowaitools.com produces the report: upload the same export, get every tool with category and risk level, the per-user breakdown, dated training verdicts, the sanctioned split against your approved list, sector policy verdicts from the AI Policy Profiles, a CSV and a PDF evidence pack. The free preview names a fifth of the tools found and comes with a preview PDF; full reports are one-time purchases, and the subscription plans on aitoolsblocklist.com include one to ten audits a month.

Frequently asked questions

How do I detect shadow AI on my network? Export a week of DNS, proxy or firewall logs, run shadowaitools scan export.csv, and read the inventory. Every hostname is matched against 20,000+ known AI tool domains, so the result is the AI traffic that actually crossed your network. For a report with the per-user breakdown and a PDF, upload the same file at shadowaitools.com.

Is anything installed on endpoints or inspected in transit? No. The tool reads a log export that your DNS filter, proxy or firewall already produces. There is no agent, no TLS inspection and no change to the network.

Does the export leave my machine? No. It is parsed locally. Only unique registrable domains are looked up, one request each, and reserved internal zones are never sent.

Which exports are supported? Cisco Umbrella, Zscaler, Palo Alto, Fortinet, Cloudflare Gateway, DNSFilter, NextDNS, Pi-hole, SonicWall, Squid, Windows DNS Server debug logs, any CSV with a header row, key=value syslog lines and plain hostname lists. See the table above.

What is the difference between ai_native and ai_enabled? ai_native is a service whose product is the AI: a chatbot, a code assistant, an image generator. ai_enabled is an ordinary product that has added AI features, such as an office suite with a built-in assistant. Both appear in the inventory because both can receive pasted content; the flag lets you treat them differently.

How current is the database behind the lookup? It is rebuilt daily, with about 300,000 new domains checked every day against a 120-million-domain corpus, so new tools are caught close to launch. The training verdicts come from a review of 13,000+ vendor terms and carry the date they were checked.

Can I limit how many lookups a scan makes? Yes. shadowaitools domains file shows the count first; --max-lines caps parsing; --cache makes repeated runs free for domains already seen; --pause slows the run down.

Who is behind shadowaitools? Alpha Quantum, which also builds the AI tools blocklist, the AI agent allow list, the website categorization API and the web filtering database.

License

MIT

Release files for shadowaitools 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shadowaitools 1.0.0
File Size Uploaded
shadowaitools-1.0.0.tar.gz 25.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for shadowaitools 1.0.0
File Interpreter ABI Platform
shadowaitools-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 46.2 kB

Release files / shadowaitools-1.0.0.tar.gz

Download URL shadowaitools-1.0.0.tar.gz
Size 25.5 kB
Tags Source
SHA-256 checksum
How to use checksums
dbe39e8ad5d09de75c2c86219ab43ceb49f996a81635879b3522017187c37d7b
BLAKE2b-256 checksum
How to use checksums
49685b71e28e12a326e36b288b98494fbf11e987b34a1750f315f9f29667ae1a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.8.10

Release files / shadowaitools-1.0.0-py3-none-any.whl

Download URL shadowaitools-1.0.0-py3-none-any.whl
Size 20.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4a9f7b34aea079e608d7adc3327d2c1ddfb03d44112a1ae7c58fda2637b048a2
BLAKE2b-256 checksum
How to use checksums
a0cb9cfe526695ee3321da7410e4131c3a45528854c032b87102cbc1cb09fa41
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.8.10

Release history Release notifications | RSS feed

1.0.1

2 release files

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page