🗺️ Borges
Borges (Better ORGanizations Entities mappingS) maps Autonomous Systems (ASes) to the organizations that operate them. It finds sibling ASes run by the same company, even when WHOIS lists them under different organizations. It combines CAIDA's WHOIS-based AS2Org and PeeringDB with two new signals: LLM extraction of sibling ASNs from PeeringDB's free-text fields, and website inference from redirect chains, domain similarity and favicons. It is the code of the ACM IMC 2025 paper Learning AS-to-Organization Mappings with Borges.
✨ Features
- 🏢 Organization keys: groups ASes by CAIDA AS2Org (WHOIS) organization IDs and PeeringDB organization IDs
- 🤖 LLM sibling extraction: few-shot prompting reads PeeringDB
notesandakafields and keeps only real sibling ASNs, not phone numbers, years or prefix limits - 🔀 Redirect analysis: networks whose websites resolve to the same final URL are grouped together
- 🖼️ Favicon analysis: shared favicons plus similar domains reveal common branding; a vision LLM rejects framework defaults (WordPress, Bootstrap, …)
- 🧩 Consolidation: merges every signal into network groups, with blocklists for known false bridges
- 💾 Robust pipeline: nine stages with checkpoint/resume, parallel scraping with caching, and Parquet/JSON/CSV exports
- 💸 Free to run: tests and CI need no key. The LLM stages can be skipped or pointed at a local model
🚀 Quick Start
pip install borges # or: uv pip install borges
borges init # writes config.yaml (prompts, blocklists), .env and data/
borges download # latest PeeringDB + AS2Org snapshots into data/input/
borges pipeline run # needs an LLM key, or see below to skip it
From a clone (for development):
git clone https://github.com/NU-AquaLab/borges.git
cd borges
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
📖 Usage
Pipeline stages
| # | Stage | What it does | LLM |
|---|---|---|---|
| 1 | load_data |
Load PeeringDB and WHOIS (AS2Org) data | |
| 2 | redirect_scraping |
Follow each PeeringDB website to its final URL (no HTML stored) | |
| 3 | as_detection |
Extract sibling ASNs from notes and aka |
🤖 |
| 4 | redirect_analysis |
Group networks that share a final URL | |
| 5 | favicon_download |
Download website favicons | |
| 6 | favicon_analysis |
Decide whether shared favicons mean a shared company | 🤖 |
| 7 | whois_processing |
Group ASes by AS2Org organization | |
| 8 | network_consolidation |
Merge groups from all sources | |
| 9 | export_results |
Write Parquet/JSON/CSV and reports to data/output/ |
Commands
borges pipeline list # stages and their dependencies
borges pipeline run # full run
borges pipeline run --stage redirect_scraping --stage as_detection # selected stages
borges pipeline run --skip favicon_analysis # skip stages
borges pipeline run --resume # continue from checkpoints
borges pipeline run --dry-run # show what would run
borges --config my-config.yaml pipeline run # or: export BORGES_CONFIG=...
borges download --peeringdb-date 2025-07-16 --as2org-date 2025-07-01
borges --help lists the rest (report generate, favicon download, config, version).
💸 Running without a paid API key
Only stages 3 and 6 call an LLM. Pick one option:
| Option | How | Trade-off |
|---|---|---|
| Skip the LLM stages | borges pipeline run --skip as_detection --skip favicon_analysis |
No sibling extraction from notes/aka, no favicon grouping |
| Free local model | Any OpenAI-compatible server: Ollama, LM Studio, vLLM. Set base_url (below) |
Results differ from the paper, which used gpt-4o-mini |
| OpenAI | OPENAI_API_KEY=... in .env |
About US$3 per full run |
With Ollama (ollama pull llama3.2 and ollama pull llama3.2-vision), set this in config.yaml:
api:
openai:
api_key: unused # local servers ignore it, but it must be set
base_url: http://localhost:11434/v1
model: llama3.2
vision_model: llama3.2-vision
Programmatic use
from borges.config import load_config, set_config
from borges.pipeline import Pipeline
config = load_config("config.yaml")
set_config(config)
pipeline = Pipeline(config)
results = pipeline.run(skip_stages=["as_detection", "favicon_analysis"])
🗃️ Output
export_results writes timestamped files to data/output/:
| File | Contents |
|---|---|
autonomous_systems_*.parquet |
One row per AS |
organizations_*.parquet |
Organization groupings |
relationships_*.parquet |
Detected sibling relationships and their source |
network_groups_*.parquet |
Consolidated groups of sibling ASes |
redirect_analysis_*.parquet |
Final URL of each network's website |
network_report_*.json |
Full analysis report |
export_summary_*.json |
Export metadata and statistics |
The main settings in config.yaml:
| Key | Default | Purpose |
|---|---|---|
api.openai.model / vision_model |
gpt-4o-mini |
Text and vision models |
api.openai.base_url |
(unset: OpenAI) | Any OpenAI-compatible endpoint |
api.openai.request_delay / retry_delay |
0.5 / 30 s |
Rate limiting |
scraping.html.max_workers / timeout |
100 / 30 s |
Redirect scraping parallelism |
scraping.favicon.max_workers |
50 |
Favicon download parallelism |
peeringdb_asn_exclusions, blocklists |
see file | Known false organizational bridges |
processing.merge_guard.enabled |
false |
Guard against false merges (brand check, bridge guard); see evaluation |
🔬 Methodology (brief)
The full method is in the paper (PDF). In short:
- Organization keys. Start from organization IDs in CAIDA's AS2Org (WHOIS) and in PeeringDB.
- Information extraction. Few-shot prompting of
gpt-4o-mini(temperature 0) recovers sibling ASNs written in PeeringDB'snotesandakafields. Earlier work used regular expressions, which confuse phone numbers, years and prefix limits with ASNs. - Website inference.
- Redirects: networks whose PeeringDB websites end at the same final URL are siblings. For example, Limelight (AS22822) and Edgecast (AS15133) both redirect to
www.edg.io. - Favicons: websites with the same favicon and the same subdomain are grouped. Remaining shared favicons go to a vision-LLM classifier that separates company logos (Claro Chile and Claro Puerto Rico) from framework defaults (Bootstrap, WordPress).
- Redirects: networks whose PeeringDB websites end at the same final URL are siblings. For example, Limelight (AS22822) and Edgecast (AS15133) both redirect to
- Consolidation. All signals are merged into organization groups.
Results on the July 2024 PeeringDB and AS2Org snapshots:
| Measure | Result |
|---|---|
Sibling extraction from notes/aka (320 records checked by hand) |
accuracy 0.947 · precision 0.974 · recall 0.94 |
| Favicon company classifier (449 favicons checked by hand) | accuracy 0.986 · precision 0.997 · recall 0.984 |
| Organization Factor (new metric, 0 to 1) | 0.3576: +7% over AS2Org, +3.3% over as2org+ |
| Users newly attributed to large conglomerates | ≈192 million (≈5% of the Internet population) |
⚠️ Known limitations
- No website history. There is no longitudinal archive of the websites listed in PeeringDB, so past runs cannot be reproduced exactly from the code alone. Keep your inputs and outputs.
- PeeringDB coverage is partial. Registration is voluntary, and entries can be incomplete or outdated. For example, some Microsoft ASNs are missing.
- Errors in the input propagate. If a PeeringDB record lists the wrong sibling, Borges faithfully extracts the wrong sibling (e.g. AS10026 listing AS2706).
- Layered ownership is out of scope. Groups spanning separate brands and regions are not linked, such as América Móvil's Claro and A1.
- False merges through weak signals. One favicon or website shared by unrelated networks can chain them into one group. The optional merge guard prevents most of these. It is off by default until its results are reviewed.
- Known false bridges are handled by configuration:
- AS4004 appears under Orange in PeeringDB but under Sprint in WHOIS. It is listed in
peeringdb_asn_exclusions. - Shared PeeringDB listings and default favicons can wrongly join small networks to large operators. They are covered by blocklists in
config.yaml.
- AS4004 appears under Orange in PeeringDB but under Sprint in WHOIS. It is listed in
- LLM output varies by model. The published numbers are for
gpt-4o-mini. Local or newer models need their own validation.
🏗️ Architecture
src/borges/
├── cli.py # Click CLI: init | pipeline | report | favicon | config | version
├── config.py # Pydantic config, ${ENV} interpolation, global get_config()/set_config()
├── pipeline/
│ ├── runner.py # Pipeline: stage ordering, dependencies, checkpoints
│ └── stages.py # the nine stages
├── analyzers/ # LLM sibling extraction, redirects, WHOIS, number validation, consolidation
├── scrapers/ # redirect, HTML and favicon scrapers
├── data/ # loaders, processors, exporters
├── models/ # AS network model and Pydantic schemas
└── utils/ # LLM client (OpenAI-compatible), HTTP client, logging
scripts/ # download_data.py (= borges download), evaluate_merge_guard.py, check_wheel.py
config.yaml # default configuration, prompts and blocklists (shipped in the wheel)
data/reference/ # negative favicon samples (framework defaults)
tests/ # unit tests; no network, no API key
🧪 Development
uv pip install -e ".[dev]"
ruff check src tests scripts --select E9,F63,F7,F82 # syntax errors and undefined names
black --check src tests # formatting (black 26)
mypy src/borges # informational for now
pytest # no network, no API key
git config blame.ignoreRevsFile .git-blame-ignore-revs # hide the bulk reformat from blame
See CONTRIBUTING.md for the full workflow.
📊 Example Output
$ borges pipeline run --skip as_detection --skip favicon_analysis --dry-run
Pipeline dry run - stages that would be executed:
✓ load_data: Load initial data from PeeringDB and WHOIS.
✓ redirect_scraping: Scrape redirect information from websites (no HTML content).
✓ redirect_analysis: Analyze URL redirects.
✓ favicon_download: Scrape favicons from websites.
✓ whois_processing: Process WHOIS data.
✓ network_consolidation: Consolidate network groups from different analysis sources.
✓ export_results: Export final results.
🤝 Contributing
Bug fixes, documentation, tests and new signals are welcome. Please open an issue before large changes. See CONTRIBUTING.md, the Code of Conduct, and SECURITY.md to report a vulnerability privately.
- Fork the repository
- Create a feature branch (
git checkout -b fix/my-fix) - Commit your changes (
git commit -m 'Fix …') - Push to the branch (
git push origin fix/my-fix) - Open a Pull Request
📄 License
MIT.
📝 Citation
If you use Borges, please cite the paper. It is also in CITATION.cff, which feeds GitHub's Cite this repository button:
@inproceedings{borges:imc,
author = {Selmo, Carlos and Carisimo, Esteban and Bustamante, Fabi{\'a}n E. and Alvarez-Hamelin, J. Ignacio},
title = {Learning AS-to-Organization Mappings with Borges},
booktitle = {Proceedings of the 2025 ACM Internet Measurement Conference},
series = {IMC '25},
year = {2025},
month = {10},
location = {Madison, WI, USA},
publisher = {Association for Computing Machinery},
doi = {10.1145/3730567.3732918},
url = {https://doi.org/10.1145/3730567.3732918}
}
🔗 Related Resources
- Paper: ACM Digital Library · PDF
- Website and artifacts: nu-aqualab.github.io/borges-website (September 2025 mappings)
- Press: Mapping Who Really Runs the Internet: Introducing Borges (Internet Society Pulse)
- CAIDA AS2Org: AS Organizations dataset and the PeeringDB archive
- as2org+: Arturi, Carisimo and Bustamante, as2org+: Enriching AS-to-Organization Mappings with PeeringDB, PAM 2023
- State-Owned ASes: estcarisimo/state-owned-ases, a hand-built dataset where Borges-style automation could help
🙏 Acknowledgements
- Paper: Learning AS-to-Organization Mappings with Borges
- Authors: Carlos Selmo, Esteban Carisimo, Fabián E. Bustamante, J. Ignacio Alvarez-Hamelin
- Conference: ACM Internet Measurement Conference (IMC) 2025, Madison, WI, USA
- Funding: NSF grant CNS-2107392 and UBACyT 20020220100053BA
Thanks to CAIDA for archiving PeeringDB snapshots and publishing AS2Org, to PeeringDB, and to @zhiyichenGT for the consolidator and WHOIS fixes.
Metadata
Release files for borges 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| borges-1.1.0.tar.gz | 127.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| borges-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 260.4 kB
Release files / borges-1.1.0.tar.gz
| Download URL | borges-1.1.0.tar.gz |
|---|---|
| Size | 127.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9c8decd7b0b57bb6aa481ca55b008b3a8a4488b49197e247923e0451f9fc40ed
|
|
BLAKE2b-256 checksum How to use checksums |
46129440796b7322e8a3e1fbfc597f1fec4d8a2d561ce36fc6a043966a373a79
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / borges-1.1.0-py3-none-any.whl
| Download URL | borges-1.1.0-py3-none-any.whl |
|---|---|
| Size | 132.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e6c1d2f3586949d83fdcab307858fedf72d422183f726af679f03f6262f645fe
|
|
BLAKE2b-256 checksum How to use checksums |
4f82ae1a7524d173448650d03e8edb12dc70879935ffba435eb214c3ade37576
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log