Multi-vendor network config translator with a verifiable cross-vendor audit
Project description
Netcanon
Multi-vendor network config translator with a verifiable cross-vendor audit.
Translates running-config between Cisco IOS-XE, Juniper Junos, Aruba AOS-S, Arista EOS, Fortinet FortiGate, MikroTik RouterOS, and OPNsense. You point Netcanon at a config from one vendor and it renders the equivalent config for another — through a shared canonical model, with every translatable field declared as supported, lossy, or unsupported.
What sets it apart is the audit underneath. Every supported vendor
pair × every field gets classified into one of eight variance classes
(ALIGNED / CODEC_BUG / EXPECTED_LOSSY / EXPECTED_UNSUPPORTED /
METHODOLOGY_ISSUE_under / METHODOLOGY_ISSUE_over / STRUCTURAL_ONLY
/ TRIVIAL_EMPTY). The cross-mesh audit catches silent translation
errors — the kind that produce output that looks valid but quietly
drops or transforms a field — before they ship.
See it in 10 seconds
docker run --rm ghcr.io/netcanon/netcanon:latest python tools/demo.py --pair cisco__junos
Paste this:
hostname leaf-01
!
vlan 10
name DATA
!
interface GigabitEthernet0/0/0
description Uplink to spine
switchport access vlan 10
!
ip route 0.0.0.0 0.0.0.0 192.168.1.1
Get this:
set system host-name leaf-01
set interfaces GigabitEthernet0/0/0 description "Uplink to spine"
set vlans DATA vlan-id 10
set routing-options static route 0.0.0.0/0 next-hop 192.168.1.1
Same canonical pipeline drives the HTTP API and the browser UI. Run
python tools/demo.py --list to see all four embedded scenarios
(Cisco→Junos, FortiGate→MikroTik, Aruba→Arista, OPNsense→Junos).
The trust signal — and the invitation
Across every supported vendor pair × every field declared as
supported, the cross-mesh audit holds zero CODEC_BUG cells.
That's not "we think it works"; that's every cell that should
translate, does, by automated test against vendor-doc-grounded
expectations.
The honest follow-up: the audit only covers cells we have fixtures
for. Real-world configs exercise paths the synthetic fixtures
haven't reached — and that's where you come in. If you have a
running-config that translates wrong (or doesn't translate at all),
that's the highest-impact bug report this project can receive. See
BUG_REPORTING.md for the workflow — Netcanon
ships its own sanitiser (the /sanitize browser page, the
netcanon sanitize CLI, and the POST /api/v1/sanitize HTTP
endpoint all share one library) so you never paste real WAN IPs,
hashes, hostnames, or usernames into a public issue.
For the full audit narrative + the variance-class taxonomy, see
docs/HOW_WE_TEST.md.
Install
Docker (recommended)
# Generate a Fernet key once and keep it somewhere safe — this is the
# encryption key for device credentials at rest. Loss = re-entering
# every saved device password; leak = decryptable backup state.
NETCANON_FERNET_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
docker run --rm -p 8000:8000 \
-v $(pwd)/configs:/app/configs \
-v $(pwd)/data:/app/data \
-e NETCANON_FERNET_KEY="$NETCANON_FERNET_KEY" \
ghcr.io/netcanon/netcanon:latest
# -> http://127.0.0.1:8000 (UI)
# -> http://127.0.0.1:8000/docs (Swagger)
# -> http://127.0.0.1:8000/health (health probe)
configs/ is where backed-up running-configs land; data/ holds
device profiles, schedules, and job state. Don't bind-mount
definitions/ — those YAMLs are baked into the image as tracked
content; mounting an empty host directory over them will crash
startup.
NETCANON_FERNET_KEY injects the credential-encryption key directly
(recommended for production / orchestrated deployments — the key
never touches disk). If you skip the -e flag, Netcanon auto-
generates a key on first run inside data/.fernet_key so the
container works zero-config; for the production deployment path see
SECURITY.md "Credential Storage".
The published image is signed via Sigstore (cosign verify ghcr.io/netcanon/netcanon ...) with an SBOM attestation.
Docker Hub mirror — same image, convenience-mirrored to Docker
Hub if your tooling defaults to docker.io:
docker run --rm -p 8000:8000 netcanon/netcanon:latest
The Docker Hub mirror has the same image bytes but no cosign
signature or SBOM attestation — operators in regulated environments
should pull from GHCR for the attested provenance chain. See
SECURITY.md for the supply-chain story.
Pip
pip install netcanon
uvicorn netcanon.main:app --host 127.0.0.1 --port 8000
netcanon also installs the netcanon CLI — netcanon sanitize -i my-config.txt --source-vendor cisco_iosxe_cli --dry-run is the
typical CLI entrypoint for the bug-reporting workflow. If the
server's running, the /sanitize browser page is the easier
path (paste or pick a stored config, click Sanitize, copy the
output — see BUG_REPORTING.md for the full
workflow including what gets redacted).
Desktop (Windows)
Download the MSI from Releases, or from source:
pip install -e ".[desktop]"
python -m netcanon_desktop
The desktop shell runs the same FastAPI app inside a PySide6 webview with a tray icon — same UI, no command-line.
Walkthroughs — "is this the right tool for my migration?"
Each walkthrough is paired 1:1 with a runnable demo scenario. Read
the narrative first, run python tools/demo.py --pair <key> to see
the actual translation.
| Walkthrough | Demo scenario | Frame |
|---|---|---|
| Cisco IOS-XE → Juniper Junos | cisco__junos |
DC leaf migration: VLANs + interfaces + routes |
| FortiGate → MikroTik RouterOS | fortigate__mikrotik |
Branch-firewall consolidation: DNS + interfaces + DHCP pools |
| Aruba AOS-S → Arista EOS | aruba__arista |
Switch refresh: VLAN-centric → port-centric grammar |
| OPNsense → Juniper Junos | opnsense__junos |
Edge-firewall migration with explicit Tier-3 boundary |
Each walkthrough ends in a manual-review checklist — what to verify on the device after the rendered config lands, before you apply it.
What translates, and what doesn't
The canonical model classifies every field by semantic stability
across vendors. Full per-codec matrix is in
docs/CAPABILITIES.md; the short version:
- Tier 1 — auto-translatable. hostname, interfaces (name / description / enabled state / IPv4 + IPv6 addresses / per-interface VRF binding), VLANs, static routes, DNS / NTP / syslog servers, timezone. Every shipped codec parses + renders these fully.
- Tier 2 — translatable with caveats. SNMP (incl. SNMPv3 USM),
LAGs, local users, RADIUS, DHCP server pools, VXLAN VNIs, EVPN
type-5 routes, routing instances / VRFs, Junos
apply-groups. Hashes that the target's CLI cannot consume surface as commented review lines, never as plaintext fallback. - Tier 3 — opaque carry / never auto-rendered. Firewall rules, NAT, IPsec / OpenVPN / WireGuard, QoS, route-maps, dynamic routing protocol stanzas, PKI. These are vendor-specific stateful policy that doesn't translate cross-vendor cleanly — Netcanon detects them, surfaces them via the migrate-page banner with a count and section names, and deliberately doesn't auto-render. Hand-build them natively on the target.
If your migration's primary need is firewall translation,
docs/COMPARISON.md names adjacent tools
(Capirca / Aerleon) that handle that scope. Netcanon is the right
tool for the router portion of a migration — and explicitly the
wrong tool to claim it does the firewall portion.
Two concerns, one app
Netcanon co-hosts:
- Backup — pulls
running-config(or vendor equivalent) from network devices over SSH / NETCONF / REST and stores it verbatim inconfigs/<hostname>.<ext>. Runs on a schedule or on demand. - Migration — translates a stored backup from one vendor's config grammar to another through the canonical intent tree.
Same FastAPI process; same UI; same Docker image. Use whichever
half (or both). See ARCHITECTURE.md for the
four-layer design.
Found a bug? Got a config that breaks it?
That's the contribution this project values most. Workflow:
- Sanitise your config — open the
/sanitizebrowser page (easiest if the server's running), or run thenetcanon sanitizeCLI (no server required). Both strip hostnames, usernames, IPs, hashes, certs, SNMP communities, etc., with a counter-per-session stable substitution table you can audit before submission. - Open a bug report or fixture submission.
- The fixture lands in
tests/fixtures/real/<vendor>/, the cross-mesh audit re-runs, and the variance class your fixture surfaces gets a row intests/fixtures/real/PHASE4_RECONCILIATION.md.
Full workflow is in BUG_REPORTING.md.
For contributors
| You want to… | Start here |
|---|---|
| Understand the architecture | ARCHITECTURE.md — four-layer model, canonical bridge, codec types |
| Follow the contributor rules | AGENTS.md — hard rules, parity checklist, gotchas |
| Read the slower-changing methodology | docs/METHODOLOGY.md — matrix-honesty discipline distilled, portable to other projects |
| Look up project jargon | docs/glossary.md — canonical, codec, mesh, ship-before-wire, target profile |
| Read the canonical model overview | netcanon/migration/canonical/README.md |
| Add or change an HTTP route | netcanon/api/routes/README.md — frozen pipeline-stage signatures, endpoint inventory |
| Add a new codec | netcanon/migration/codecs/README.md |
| Add a new device definition / target profile | definitions/README.md |
| Add a new canonical field | docs/adding-a-canonical-field.md |
| Ship a feature across web + desktop | docs/feature-parity-walkthrough.md |
| See what's shipped recently | CHANGELOG.md |
| Check codec certification tiers | tests/fixtures/real/RESULTS.md |
| Manually exercise recent changes | HUMAN_TESTING.md |
| Write tests | tests/README.md |
| Review the security model | SECURITY.md |
Run the test suite
pip install -e ".[dev]"
pytest # unit + integration + desktop (fast)
pytest -m e2e # Playwright browser tests (slower)
Tests run across four layers: unit (pure functions, no I/O — the
real-capture validation harness lives here as a unit subset),
integration (TestClient + mocked SSH at the get_collector
factory), e2e (Playwright against a live Uvicorn), and desktop
(PySide6 + pystray mocked). CI runs the full matrix on Python
3.11 / 3.12 / 3.13 against Ubuntu. CI output is the source of
truth for pass counts.
Layout
netcanon/ FastAPI application (shared by both platforms)
├── api/routes/ HTTP endpoints
├── collectors/ SSH/NETCONF/REST fetchers — one factory,
│ one mock-point (`get_collector`)
├── migration/ Cross-vendor translation pipeline
│ ├── canonical/ CanonicalIntent model + shared transforms
│ └── codecs/ Per-vendor parse/render implementations
├── services/ Plain-function orchestrators (pipeline, detect, …)
├── storage/ FileConfigStore
├── tools/ sanitize, etc.
└── templates/ Jinja2 templates (every interactive element
carries a data-testid — see AGENTS.md)
netcanon_desktop/ Windows tray/webview shell around the same server
definitions/ Device definition YAMLs
tools/demo.py One-command cross-vendor translation demo
docs/walkthroughs/ Narrative migration walkthroughs (paired with demo)
docs/vendors/ Per-vendor "what works for me?" pages
tests/unit/ Pure-function tests, no I/O
tests/integration/ FastAPI TestClient tests, SSH mocked
tests/e2e/ Playwright browser tests
tests/desktop/ PySide6/pystray-mocked desktop shell tests
tests/fixtures/real/ Real-capture validation corpus (see RESULTS.md)
License
MIT. See LICENSE. Third-party fixtures keep their
upstream licences — see tests/fixtures/real/NOTICE.md
for provenance.
For responsible disclosure of security issues, see
SECURITY.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file netcanon-0.1.0rc8.tar.gz.
File metadata
- Download URL: netcanon-0.1.0rc8.tar.gz
- Upload date:
- Size: 3.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9634c45bd9db4425dbc3391bb7b38b8761fab96927a53b5448257cbfcbe146e
|
|
| MD5 |
e4d8a23674144347e03f9d27217e468a
|
|
| BLAKE2b-256 |
383d90e82dd342ddc9ccd5bda8d638b0bddfef77b3e41afef779781333730ff9
|
Provenance
The following attestation bundles were made for netcanon-0.1.0rc8.tar.gz:
Publisher:
pypi-publish.yml on netcanon/netcanon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
netcanon-0.1.0rc8.tar.gz -
Subject digest:
d9634c45bd9db4425dbc3391bb7b38b8761fab96927a53b5448257cbfcbe146e - Sigstore transparency entry: 1523299584
- Sigstore integration time:
-
Permalink:
netcanon/netcanon@bb1a9420c65d41b58f060336731b210a4ed6eda7 -
Branch / Tag:
refs/tags/v0.1.0-rc8 - Owner: https://github.com/netcanon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@bb1a9420c65d41b58f060336731b210a4ed6eda7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file netcanon-0.1.0rc8-py3-none-any.whl.
File metadata
- Download URL: netcanon-0.1.0rc8-py3-none-any.whl
- Upload date:
- Size: 665.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9048b0af3ec4ca09f9c6cd733f9240dfdb7d336d9271526ea0c97495a811fbe0
|
|
| MD5 |
c2f1e3d19c8b5c33a08ebe3315afadf4
|
|
| BLAKE2b-256 |
11f56ab54c4c799132908a186c629a4e596890e70e2e34ec2b11db19e4fdacfa
|
Provenance
The following attestation bundles were made for netcanon-0.1.0rc8-py3-none-any.whl:
Publisher:
pypi-publish.yml on netcanon/netcanon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
netcanon-0.1.0rc8-py3-none-any.whl -
Subject digest:
9048b0af3ec4ca09f9c6cd733f9240dfdb7d336d9271526ea0c97495a811fbe0 - Sigstore transparency entry: 1523299592
- Sigstore integration time:
-
Permalink:
netcanon/netcanon@bb1a9420c65d41b58f060336731b210a4ed6eda7 -
Branch / Tag:
refs/tags/v0.1.0-rc8 - Owner: https://github.com/netcanon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@bb1a9420c65d41b58f060336731b210a4ed6eda7 -
Trigger Event:
push
-
Statement type: