Each client’s monthly AI report,
built from its own firewall.
The code reads the client’s firewall or DNS filter with that device’s own API key, keeps the logs on your machine, and writes a usage report under your name. Only domain names are looked up with us.
12 firewalls and filters · 4 languages · no client logs sent to us
Sample
Preview the monthly report
12 pages, opens in your browser · PDF
Choose the device and the language
One program covers every device and all your clients. Each guide lists what you need, where to create the API key, your settings file and the commands to schedule.
SonicWall
Receives syslogSonicWall firewalls do not offer their web log through the API, so the program receives the firewall’s syslog messages. SonicOS records each web site visit (“Web site hit”) and each blocked site (“Web site access denied”) with the site name, the user and the device.
What you need
- Any SonicOS version that sends syslog
- Content Filtering or App Control logging web site events
- A machine that runs the program and can receive UDP from the firewall
Set it up
- In SonicOS, open the syslog settings (in SonicOS 7: Device → Log → Syslog; the menu can differ by version) and add a syslog server: the IP of the machine running the program, UDP port 5514.
- Make sure web site events are logged (Content Filter / web site hit events at priority Inform or lower).
- Add the client to the settings file with
"source": "syslog"and the IP address the firewall sends from. - Run
syslogon a machine the firewall can reach (at the client or at your office). It listens on UDP port 5514; use--port 514if you prefer the standard port (needs admin rights). Keep it running, for example as a Windows service or a systemd unit.
Your settings file
{
"api_key": "YOUR_AI_TOOLS_BLOCKLIST_API_KEY",
"msp": {
"name": "Your IT Company",
"email": "[email protected]",
"phone": "",
"website": "",
"logo_url": "",
"color": "#0e7490"
},
"data_dir": "./atb-data",
"output_dir": "./reports",
"clients": [
{
"name": "Example Dental Inc.",
"profile": "healthcare",
"source": "syslog",
"syslog": {
"device_ip": "203.0.113.10"
}
}
]
}One entry per client in clients. profile is the client’s sector policy (for example healthcare, legal, financial, accounting or msp) and decides which tools the report marks as blocked. The program only sees messages that arrive while it runs, so keep it running all month.
Run it
$ ./atb-report syslog --config atb-report.json
$ ./atb-report report --config atb-report.json --month 2026-09./atb-report collect --config atb-report.json every day.1//! AI Tools Blocklist report code, Rust version 1.0 (2026-09-28)
2//! https://www.aitoolsblocklist.com/ai-governance-for-msps.php#report-code
3//!
4//! Builds a monthly AI report for each client from the client's own firewall or DNS-filter logs. The logs stay on
5//! your machine: only the list of domain names is checked against the AI Tools Blocklist API (your plan's API key).
6//!
7//! atb-report collect --config atb-report.json pull the latest logs (run daily)
8//! atb-report syslog --config atb-report.json --port 5514 receive syslog (SonicWall, Cisco ASA / Firepower)
9//! atb-report import --config atb-report.json --client "Example Dental" --file export.csv
10//! atb-report report --config atb-report.json --month 2026-09 [--client "Example Dental"]
11//!
12//! Sources: umbrella, meraki, fortigate, paloalto, dnsfilter, cloudflare, defender, watchguard, syslog, file.
13//! One self-contained program; settings: see atb-report.example.json. It behaves exactly like the Python version
14//! (same settings file, same data folder, same reports), so either can be used, or both on the same data.
15//! Build: cargo build --release -> target/release/atb-report
16
17mod common;
18mod http;
19mod parse;
20mod platform;
21mod sources;
22mod store;
23
24use common::{
25 day_of, dumps, err, get, log, py_str, py_strip, req, slug, text, truthy, utcnow, DateTime, Error, Map, Result, MONTH_NAMES,
26};
27use http::{Http, Payload};
28use serde_json::{json, Value};
29use std::collections::{BTreeMap, BTreeSet};
30use std::fs;
31use std::path::PathBuf;
32use std::time::{Duration, Instant};
33use store::Store;
34
35pub const VERSION: &str = "1.0";
36
37/// The report page. The "template" setting can point to another copy.
38const DEFAULT_TEMPLATE: &str = include_str!("../../shared/report-template.html");
39
40// ------------------------------------------------------------------ command line
41
42/// Command-line options (the same as the Python version's).
43#[derive(Default)]
44struct Args {
45 cmd: String,
46 config: String,
47 client: Option<String>,
48 days: i64,
49 port: i64,
50 bind: String,
51 seconds: i64,
52 file: String,
53 month: String,
54}
55
56fn usage(cmd: &str) -> String {
57 let extra = match cmd {
58 "collect" => " [--days DAYS]",
59 "syslog" => " [--port PORT] [--bind BIND] [--seconds SECONDS]",
60 "import" => " --file FILE",
61 "report" => " [--month MONTH]",
62 _ => return "usage: atb-report [-h] {collect,syslog,import,report} ...".into(),
63 };
64 format!("usage: atb-report {} [-h] --config CONFIG [--client CLIENT]{}", cmd, extra)
65}
66
67/// Prints an argument error the way argparse does and exits with code 2.
68fn usage_error(cmd: &str, msg: &str) -> ! {
69 eprintln!("{}", usage(cmd));
70 let prog = if cmd.is_empty() { "atb-report".to_string() } else { format!("atb-report {}", cmd) };
71 eprintln!("{}: error: {}", prog, msg);
72 std::process::exit(2);
73}
74
75fn parse_args(argv: &[String]) -> Args {
76 let cmd = match argv.first().map(String::as_str) {
77 Some(c @ ("collect" | "syslog" | "import" | "report")) => c.to_string(),
78 Some("-h" | "--help") => {
79 println!("{}\n\nAI Tools Blocklist monthly report code", usage(""));
80 std::process::exit(0);
81 }
82 Some(other) => usage_error("", &format!("argument cmd: invalid choice: '{}' (choose from 'collect', 'syslog', 'import', 'report')", other)),
83 None => usage_error("", "the following arguments are required: cmd"),
84 };
85 let mut names = vec!["config", "client"];
86 names.extend(match cmd.as_str() {
87 "collect" => vec!["days"],
88 "syslog" => vec!["port", "bind", "seconds"],
89 "import" => vec!["file"],
90 _ => vec!["month"],
91 });
92 let mut vals: BTreeMap<&str, String> = BTreeMap::new();
93 let mut i = 1;
94 while i < argv.len() {
95 let tok = &argv[i];
96 if tok == "-h" || tok == "--help" {
97 println!("{}", usage(&cmd));
98 std::process::exit(0);
99 }
100 let Some(opt) = tok.strip_prefix("--") else {
101 usage_error(&cmd, &format!("unrecognized arguments: {}", tok));
102 };
103 let (name, inline) = match opt.split_once('=') {
104 Some((n, v)) => (n, Some(v.to_string())),
105 None => (opt, None),
106 };
107 // like argparse, accept an unambiguous abbreviation such as --conf
108 let matches: Vec<&str> = names.iter().copied().filter(|n| *n == name || n.starts_with(name)).collect();
109 let full = match matches.as_slice() {
110 _ if names.contains(&name) => name,
111 [one] if !name.is_empty() => *one,
112 [] | [_] => usage_error(&cmd, &format!("unrecognized arguments: {}", tok)),
113 many => usage_error(&cmd, &format!("ambiguous option: --{} could match {}", name, many.iter().map(|m| format!("--{}", m)).collect::<Vec<_>>().join(", "))),
114 };
115 let value = match inline {
116 Some(v) => v,
117 None => {
118 i += 1;
119 match argv.get(i) {
120 Some(v) if !(v.starts_with('-') && v.parse::<f64>().is_err()) => v.clone(),
121 _ => usage_error(&cmd, &format!("argument --{}: expected one argument", full)),
122 }
123 }
124 };
125 vals.insert(full, value);
126 i += 1;
127 }
128 let int = |name: &str, default: i64| -> i64 {
129 match vals.get(name) {
130 None => default,
131 Some(v) => py_strip(v).parse().unwrap_or_else(|_| usage_error(&cmd, &format!("argument --{}: invalid int value: '{}'", name, v))),
132 }
133 };
134 let mut required = vec!["config"];
135 if cmd == "import" {
136 required.push("file");
137 }
138 let missing: Vec<String> = required.iter().filter(|r| !vals.contains_key(**r)).map(|r| format!("--{}", r)).collect();
139 if !missing.is_empty() {
140 usage_error(&cmd, &format!("the following arguments are required: {}", missing.join(", ")));
141 }
142 Args {
143 days: int("days", 0),
144 port: int("port", 5514),
145 seconds: int("seconds", 0),
146 bind: vals.get("bind").cloned().unwrap_or_else(|| "0.0.0.0".into()),
147 month: vals.get("month").cloned().unwrap_or_else(|| prev_month(&utcnow().date()[..7]).unwrap_or_default()),
148 config: vals["config"].clone(),
149 client: vals.get("client").cloned(),
150 file: vals.get("file").cloned().unwrap_or_default(),
151 cmd,
152 }
153}
154
155fn main() {
156 if let Err(e) = common::init_clock() {
157 eprintln!("ERROR {}", e);
158 std::process::exit(1);
159 }
160 let argv: Vec<String> = std::env::args().skip(1).collect();
161 let args = parse_args(&argv);
162 let result = load_config(&args.config).and_then(|cfg| match args.cmd.as_str() {
163 "collect" => cmd_collect(&cfg, &args),
164 "syslog" => cmd_syslog(&cfg, &args),
165 "import" => cmd_import(&cfg, &args),
166 _ => cmd_report(&cfg, &args),
167 });
168 match result {
169 Ok(code) => std::process::exit(code),
170 Err(e) => {
171 log(&format!("ERROR {}", e));
172 std::process::exit(1);
173 }
174 }
175}
Every language and every device, with a download page and your report key already filled in.
I have the MSP planSee the MSP planThe program runs on your own machine with your own access to each device, under your agreement with its vendor; we never connect to your clients’ devices. Only domain names are sent to the AI Tools Blocklist API, never who visited them. SonicWall, Fortinet, FortiGate, Palo Alto Networks, Cisco, Meraki, Umbrella, DNSFilter, Cloudflare, Microsoft, Defender and WatchGuard are trademarks of their owners; AI Tools Blocklist is not affiliated with or endorsed by them.
The full code is part of the MSP plan
MSP plan customers download the complete program for every language and filter, with the report layout and a settings file that already holds their API key.
Without the plan you can download the preview: the first 30% of the code, to see how it works.
I have the MSP plan: log in for the full code →Block the risky AI services, then show each client the proof.
The MSP plan includes the block lists for up to 25 clients, the report code and the lookups it needs.