Automatically downloads, validates, normalizes, and publishes multiple DNS blocklists in a format suitable for Cato Networks FQDN Containers.
The project currently processes:
- HaGeZi Multi PRO — ads, trackers, telemetry, and related unwanted domains
- StevenBlack Unified — adware + malware
Each source is published as a separate output file so it can be assigned to its own Cato FQDN Container.
A single GitHub Actions workflow updates both lists automatically once per day.
This project provides network-wide domain blocking without requiring Pi-hole or another locally hosted DNS filtering service.
The primary goals are:
- Block web advertising
- Block advertising used by Android/mobile applications and games
- Block tracking and telemetry domains
- Provide an additional layer of adware/malware domain blocking
- Use Cato Networks as the enforcement point
- Automatically maintain the blocklists
- Require no dedicated server, VM, cron server, or other local infrastructure
Cato Networks remains the primary security platform. These third-party blocklists provide an additional domain-based advertising, privacy, and security layer.
GitHub Actions
|
| Daily at 03:17 Eastern
|
+----------+----------+
| |
v v
HaGeZi Multi PRO StevenBlack Unified
| (adware + malware)
| |
+----------+----------+
|
v
normalize.py
|
+----------+----------+
| |
v v
hagezi-pro.txt stevenblack-unified.txt
| |
v v
Cato FQDN Cato FQDN
Container Container
| |
+----------+----------+
|
v
Internet Firewall
|
v
BLOCK
GitHub provides the temporary Linux runner used to execute the Python script.
No local Python installation, Pi-hole server, cron server, or dedicated VM is required.
Source project:
https://github.com/hagezi/dns-blocklists
This project uses the Multi PRO Wildcard Domains / Domains Only feed.
Source:
https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/pro-onlydomains.txt
The PRO tier provides a balance between aggressive advertising/tracker blocking and minimizing disruption to legitimate applications.
It is particularly useful for:
- Web advertising
- Tracking
- Analytics
- Telemetry
- Mobile application advertising
- Mobile application tracking
The domains-oriented version is used because it is already close to the format required by a Cato FQDN Container.
Source project:
https://github.com/StevenBlack/hosts
This project uses:
Unified hosts = adware + malware
Source:
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
StevenBlack uses traditional hosts-file syntax:
0.0.0.0 ads.example.com
0.0.0.0 malware.example.net
The IP address:
0.0.0.0
is a hosts-file sinkhole address.
It is not the actual IP address of the destination and should not be imported into a Cato IP Container.
The normalizer therefore extracts only:
ads.example.com
malware.example.net
Although there is overlap between HaGeZi and StevenBlack, the generated files intentionally remain separate.
This provides two independent Cato Containers:
HaGeZi Multi PRO
|
v
Cato FQDN Container
and:
StevenBlack Unified
|
v
Cato FQDN Container
This provides better:
- Troubleshooting
- Visibility
- Policy control
- Testing
- Rollback capability
If a legitimate domain is blocked, it is easier to determine which source contains the domain.
The lists can also be enabled or disabled independently in Cato.
cato-blocklist/
|
+-- .github/
| +-- workflows/
| +-- update-blocklist.yml
|
+-- scripts/
| +-- normalize.py
|
+-- output/
| +-- hagezi-pro.txt
| +-- stevenblack-unified.txt
|
+-- README.md
The output directory and generated files are created automatically by normalize.py.
The Python script handles both upstream feeds.
It:
- Downloads HaGeZi Multi PRO.
- Parses its domains-only format.
- Downloads StevenBlack Unified.
- Parses its hosts-file format.
- Extracts FQDNs.
- Normalizes domains to lowercase.
- Validates FQDN syntax.
- Rejects IP addresses.
- Removes local/system hostnames.
- Removes duplicates.
- Sorts the output.
- Performs list-size sanity checks.
- Compares new lists against previous versions.
- Atomically replaces known-good output files.
The script uses only the Python standard library.
No:
pip install
or:
requirements.txt
is required.
output/hagezi-pro.txt
output/stevenblack-unified.txt
Both files contain exactly one FQDN per line:
ads.example.com
analytics.example.net
malware.example.org
tracker.example.com
They should not contain:
0.0.0.0 ads.example.com
or Adblock syntax such as:
||ads.example.com^
The project uses a single workflow:
.github/workflows/update-blocklist.yml
The workflow processes both blocklists during the same execution.
The normal workflow is:
GitHub Scheduler
|
v
Checkout repository
|
v
Configure Python
|
v
Run normalize.py
|
+---- HaGeZi
|
+---- StevenBlack
|
v
Validate both lists
|
v
Generate output files
|
v
Did anything change?
|
+---- No ----> Finish
|
+---- Yes ---> Commit
|
v
Push
The GitHub Action runs every day at:
03:17 AM Eastern Time
The workflow contains:
on:
schedule:
- cron: "17 3 * * *"
timezone: "America/New_York"
workflow_dispatch:Using:
America/New_York
allows the schedule to follow Eastern Standard Time and Eastern Daylight Time automatically.
The workflow_dispatch trigger also provides a Run workflow button for manual testing.
The workflow requires:
permissions:
contents: writeThis allows the GitHub Actions bot to commit updated output files back to the repository.
The workflow uses:
runs-on: ubuntu-latestGitHub supplies and maintains the temporary Linux runner.
The repository is checked out using:
uses: actions/checkout@v7Python is configured using:
uses: actions/setup-python@v7The workflow can be run manually from:
Repository
-> Actions
-> Update Cato Blocklists
-> Run workflow
This is useful when:
- Initially configuring the repository
- Testing changes
- Updating
normalize.py - Updating the GitHub workflow
- Forcing an immediate refresh
A manual run is identified by:
workflow_dispatch
A scheduled run is identified by:
schedule
A successful execution should show:
Checkout repository PASS
Set up Python PASS
Download and normalize blocklists PASS
Commit updated blocklists PASS
The normalizer should report statistics separately for each feed.
Example:
======================================================================
HaGeZi Multi PRO
======================================================================
Downloading HaGeZi Multi PRO:
...
Parser statistics:
Valid unique domains : ...
Invalid/skipped : ...
Comment lines : ...
Blank lines : ...
Published ... domains to:
output/hagezi-pro.txt
HaGeZi Multi PRO: SUCCESS
followed by:
======================================================================
StevenBlack Unified
======================================================================
Downloading StevenBlack Unified:
...
Parser statistics:
Valid unique domains : ...
Invalid/skipped : ...
Localhost/system : ...
Comment lines : ...
Blank lines : ...
Published ... domains to:
output/stevenblack-unified.txt
StevenBlack Unified: SUCCESS
Finally:
======================================================================
ALL FEEDS SUCCESSFUL
======================================================================
The two feeds are processed as one GitHub update operation.
Both must successfully pass validation before GitHub commits the generated files.
Conceptually:
HaGeZi PASS
StevenBlack PASS
|
v
Commit changes
If either feed fails:
HaGeZi PASS
StevenBlack FAIL
|
v
NO COMMIT
or:
HaGeZi FAIL
|
v
NO COMMIT
The GitHub Action is marked as failed.
The previously committed output files remain available to Cato.
This prevents an upstream failure from removing the existing blocklists.
The normalizer does not blindly publish whatever it downloads.
The expected minimum is configured as:
"minimum_domains": 100_000If HaGeZi unexpectedly produces fewer than 100,000 valid domains, the update fails.
The expected minimum is configured as:
"minimum_domains": 40_000If StevenBlack unexpectedly produces fewer than 40,000 valid domains, the update fails.
Both feeds also use:
MINIMUM_SIZE_RATIO = 0.70This means the new list must contain at least 70% as many entries as the previous version.
For example:
Previous:
220,000
New:
218,000
Result:
ACCEPT
But:
Previous:
220,000
New:
15,000
Result:
REJECT
The script writes to a temporary file before replacing the existing output.
Conceptually:
New blocklist
|
v
temporary file
|
| successful write
v
replace existing file
This prevents a partially written blocklist from replacing a working file.
Both generated files are staged:
git add output/hagezi-pro.txt
git add output/stevenblack-unified.txtGit then checks whether anything actually changed.
If neither list changed:
No blocklist changes.
No commit is created.
If one or both changed:
Blocklists changed. Committing updates.
GitHub creates:
Update Cato blocklists
and pushes the changes to the repository.
Cato should use the raw GitHub URLs, not the regular GitHub file-viewer URLs.
Format:
https://raw.githubusercontent.com/<USERNAME>/<REPOSITORY>/main/output/hagezi-pro.txt
Example:
https://raw.githubusercontent.com/mnn-github/cato-blocklist/main/output/hagezi-pro.txt
Format:
https://raw.githubusercontent.com/<USERNAME>/<REPOSITORY>/main/output/stevenblack-unified.txt
Example:
https://raw.githubusercontent.com/mnn-github/cato-blocklist/main/output/stevenblack-unified.txt
Before configuring Cato, open each raw URL in a private/incognito browser window.
The domain lists should load without authentication.
Create two independent FQDN Containers.
Example:
Name:
HaGeZi Multi PRO
Type:
FQDN
Source:
Sync file from URL
URL:
https://raw.githubusercontent.com/<USERNAME>/<REPOSITORY>/main/output/hagezi-pro.txt
Example:
Name:
StevenBlack Unified
Type:
FQDN
Source:
Sync file from URL
URL:
https://raw.githubusercontent.com/<USERNAME>/<REPOSITORY>/main/output/stevenblack-unified.txt
Use Test Container/Test Connection before saving each Container.
The Cato Container synchronization interval is independent of the GitHub Actions schedule.
For example:
03:17
GitHub generates latest lists
06:00
Cato synchronizes Containers
12:00
Cato synchronizes Containers
18:00
Cato synchronizes Containers
GitHub does not regenerate the files when Cato accesses them.
Cato simply retrieves the latest version already published to GitHub.
A Cato synchronization interval such as:
6 hours
provides a reasonable balance for these types of lists.
A 24-hour interval can also be used if once-daily synchronization is sufficient.
Each Container can be referenced independently by the Cato Internet Firewall.
Example:
1. Block HaGeZi Multi PRO
Destination: HaGeZi Multi PRO
Action: Block
2. Block StevenBlack Unified
Destination: StevenBlack Unified
Action: Block
An exception rule can be placed above these rules:
1. Blocklist Exceptions ALLOW
2. HaGeZi Multi PRO BLOCK
3. StevenBlack Unified BLOCK
4. Remaining policy...
This provides a convenient way to resolve false positives without modifying the generated files.
If an application stops working:
- Review Cato events.
- Identify the blocked destination FQDN.
- Determine which Container matched it.
- Confirm that the domain is legitimately required.
- Add the FQDN to an appropriate Allow/Exception rule above the blocklist rules.
Do not manually remove domains from:
output/hagezi-pro.txt
or:
output/stevenblack-unified.txt
because the files are regenerated during the next update.
Navigate to:
Repository
-> Actions
-> Update Cato Blocklists
A manually started run shows:
workflow_dispatch
An automatically scheduled run shows:
schedule
This provides confirmation that GitHub's scheduler triggered the workflow.
The schedule can be changed by editing:
.github/workflows/update-blocklist.yml
For example:
- cron: "30 20 * * *"
timezone: "America/New_York"would schedule the workflow for:
8:30 PM Eastern
Commit the YAML change to the default branch.
No manual workflow execution is required after changing the schedule.
GitHub automatically uses the new schedule after the updated workflow is committed.
To verify automatic scheduling without waiting until 3:17 AM:
- Determine a time approximately 10-15 minutes in the future.
- Temporarily change the cron schedule.
- Commit the change to
main. - Wait for the workflow to run automatically.
- Confirm the trigger is
schedule. - Restore the normal schedule.
Normal schedule:
- cron: "17 3 * * *"
timezone: "America/New_York"Scheduled workflows can occasionally start a few minutes later than the requested time.
Once configured, no manual intervention should normally be required.
DAILY AT 03:17 EASTERN
|
v
GitHub Actions
|
v
normalize.py
|
+----------+----------+
| |
v v
HaGeZi StevenBlack
| |
v v
Validate Validate
| |
v v
Normalize Normalize
| |
v v
hagezi-pro.txt stevenblack-unified.txt
| |
+----------+----------+
|
v
GitHub Commit
if changed
|
+----------+----------+
| |
v v
Cato Container Cato Container
| |
+----------+----------+
|
v
Internet Firewall
Open:
Repository
-> Actions
-> failed run
-> Download and normalize blocklists
Review the Python output.
The script identifies which feed failed.
Verify the URL starts with:
https://raw.githubusercontent.com/
Open the URL in an incognito/private browser.
It should work without GitHub authentication.
Verify that:
output/stevenblack-unified.txt
contains:
ads.example.com
malware.example.net
and not:
0.0.0.0 ads.example.com
This may be completely normal.
If the upstream source has not changed, the generated file remains identical and GitHub intentionally does not create a new commit.
The Action should still show a successful run.
Verify:
permissions:
contents: writeexists in the workflow.
The architecture deliberately places validation between third-party sources and Cato:
Third-party feed
|
v
Download
|
v
Format-specific parser
|
v
FQDN validation
|
v
Deduplication
|
v
Size sanity check
|
v
Known-good output
|
v
GitHub
|
v
Cato
Neither third-party source is treated as implicitly trusted input.
Periodically review:
GitHub
-> Actions
to verify that scheduled executions remain successful.
Also periodically review the Containers in Cato CMA to verify successful URL synchronization.
Git history provides an audit trail showing when either generated blocklist changed.
https://github.com/hagezi/dns-blocklists
https://github.com/StevenBlack/hosts
https://github.com/features/actions
This is an independent integration project and is not an official Cato Networks, HaGeZi, StevenBlack, or GitHub product.
Third-party blocklists can cause legitimate domains or applications to be blocked.
Review Cato events when troubleshooting connectivity and use explicit policy exceptions where appropriate.
Refer to each upstream project for its current licensing terms, documentation, list composition, and usage requirements.
This project maintains two independent Cato-compatible FQDN blocklists:
HaGeZi Multi PRO
|
v
output/hagezi-pro.txt
|
v
Cato FQDN Container
and:
StevenBlack Unified
|
v
output/stevenblack-unified.txt
|
v
Cato FQDN Container
A single GitHub Action runs:
Daily at 03:17 Eastern
and a single:
scripts/normalize.py
processes both sources.
The result is an automated, serverless blocklist pipeline:
HaGeZi -----------+
|
+--> GitHub Actions
| |
StevenBlack ------+ v
normalize.py
|
+-----+-----+
| |
v v
HaGeZi StevenBlack
| |
v v
Cato Cato
Container Container
| |
+-----+-----+
|
v
Internet Firewall
Since this replaces the previous README rather than creating a separate document, I kept the same writing-block ID so you can treat it as the revised version.