{"id":51140921,"url":"https://github.com/netcanon/netcanon","last_synced_at":"2026-06-25T23:00:42.970Z","repository":{"id":359006256,"uuid":"1237388284","full_name":"netcanon/netcanon","owner":"netcanon","description":"Multi-vendor network config translator — Cisco / Juniper / Fortinet / Aruba / Arista / MikroTik / OPNsense. Cross-mesh audit catches silent translation errors before they ship.","archived":false,"fork":false,"pushed_at":"2026-06-20T07:01:02.000Z","size":22531,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-20T07:13:52.176Z","etag":null,"topics":["arista","aruba","cisco","config-migration","fastapi","fortinet","juniper","mikrotik","network-automation","network-configuration","opnsense","python","vendor-translation"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/netcanon/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/netcanon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-13T06:20:23.000Z","updated_at":"2026-06-20T07:01:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/netcanon/netcanon","commit_stats":null,"previous_names":["netcanon/netcanon"],"tags_count":25,"template":false,"template_full_name":null,"purl":"pkg:github/netcanon/netcanon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netcanon%2Fnetcanon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netcanon%2Fnetcanon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netcanon%2Fnetcanon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netcanon%2Fnetcanon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/netcanon","download_url":"https://codeload.github.com/netcanon/netcanon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netcanon%2Fnetcanon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34795436,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arista","aruba","cisco","config-migration","fastapi","fortinet","juniper","mikrotik","network-automation","network-configuration","opnsense","python","vendor-translation"],"created_at":"2026-06-25T23:00:27.286Z","updated_at":"2026-06-25T23:00:42.958Z","avatar_url":"https://github.com/netcanon.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Netcanon\n\n[![CI](https://github.com/netcanon/netcanon/actions/workflows/ci.yml/badge.svg)](https://github.com/netcanon/netcanon/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/netcanon)](https://pypi.org/project/netcanon/)\n[![Python versions](https://img.shields.io/pypi/pyversions/netcanon)](https://pypi.org/project/netcanon/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Container: GHCR](https://img.shields.io/badge/ghcr.io-netcanon%2Fnetcanon-2496ED?logo=docker\u0026logoColor=white)](https://github.com/netcanon/netcanon/pkgs/container/netcanon)\n\n**Multi-vendor network config translator with a verifiable cross-vendor audit.**\n\nTranslates running-config across twelve codecs spanning Cisco (IOS-XE,\nNX-OS, IOS-XR), Juniper Junos, Arista EOS, Aruba (AOS-S, AOS-CX),\nFortinet FortiGate, MikroTik RouterOS, OPNsense, and VyOS — see\n[`docs/CAPABILITIES.md`](docs/CAPABILITIES.md) for the full per-codec list.\nYou point Netcanon at a config from one vendor and it renders the\nequivalent config for another — through a shared canonical model, with\nevery translatable field declared as supported, lossy, or unsupported.\n\nWhat sets it apart is the audit underneath.  Every supported vendor\npair × every field gets classified into one of eight variance classes\n(`ALIGNED` / `CODEC_BUG` / `EXPECTED_LOSSY` / `EXPECTED_UNSUPPORTED` /\n`METHODOLOGY_ISSUE_under` / `METHODOLOGY_ISSUE_over` / `STRUCTURAL_ONLY`\n/ `TRIVIAL_EMPTY`).  The cross-mesh audit catches silent translation\nerrors — the kind that produce output that *looks* valid but quietly\ndrops or transforms a field — before they ship.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/assets/migrate.png\" width=\"860\" alt=\"Netcanon migrate page: a Cisco IOS-XE config translated to Junos, showing auto-detection, a green “Validation OK” banner, an amber “Tier-3 sections detected” banner listing the ACL and NAT lines that don’t translate, and the rendered Junos `set` output.\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003csub\u003eThe browser UI translating Cisco IOS-XE → Junos — interface names and L2 membership mapped across vendors, every field declared supported/lossy/unsupported, and Tier-3 sections (ACLs, NAT, …) surfaced rather than silently dropped.\u003c/sub\u003e\u003c/p\u003e\n\n---\n\n## See it in 10 seconds\n\n```bash\ndocker run --rm --entrypoint netcanon ghcr.io/netcanon/netcanon:latest demo --pair cisco__junos\n```\n\n(Installed via pip instead? Just `netcanon demo --pair cisco__junos`.)\n\nThe `demo` command above translates a built-in sample. To translate your\nown, start the server (see [Install](#install)) and paste a config like\nthis into the browser migrate page:\n\n```\nhostname access-sw-01\n!\nvlan 10\n name DATA\n!\ninterface GigabitEthernet1/0/1\n description Server-A\n switchport mode access\n switchport access vlan 10\n!\nsnmp-server community public RO\n!\nip route 0.0.0.0 0.0.0.0 192.168.1.1\n```\n\nGet this:\n\n```\nset system host-name access-sw-01\nset interfaces ge-1/0/1 description \"Server-A\"\nset interfaces ge-1/0/1 unit 0 family ethernet-switching interface-mode access\nset interfaces ge-1/0/1 unit 0 family ethernet-switching vlan members DATA\nset vlans DATA vlan-id 10\nset routing-options static route 0.0.0.0/0 next-hop 192.168.1.1\nset snmp community public authorization read-only\n```\n\nNotice ``GigabitEthernet1/0/1`` became ``ge-1/0/1`` and the access-VLAN\nmembership rendered into Junos `ethernet-switching` form — Netcanon\ntranslates interface names and L2 membership across vendor conventions,\nnot just the surrounding scalar config.\n\nSame canonical pipeline drives the HTTP API and the browser UI.  Run\n`netcanon demo --list` (or `python tools/demo.py --list` from a source\ncheckout) to see all four embedded scenarios (Cisco→Junos,\nFortiGate→MikroTik, Aruba→Arista, OPNsense→Junos).\n\n---\n\n## The trust signal — and the invitation\n\nAcross every vendor pair we ship a fixture **and** a cross-vendor\nexpectation for — 8 of the 12 codecs today; `aruba_aoscx`, `cisco_iosxr`,\n`cisco_nxos`, and `vyos` have fixtures but not yet cross-vendor\nexpectations — the cross-mesh audit tracks `CODEC_BUG` drift cell by\ncell.  The live reconciliation (`tests/fixtures/real/PHASE4_RECONCILIATION.md`\nfor the roll-up, `tests/fixtures/real/phase4_findings_residuals.md` for\nthe per-cell triage) currently reports a small number of residual\nhigh-severity cells (5 at last run) — each triaged as a benign\nmodelling/structural artifact (4 on real fixtures, 1 synthetic) rather\nthan a translation error, and every one is enumerated.  That's not \"we\nthink it works\"; that's every covered cell, checked by automated test\nagainst vendor-doc-grounded expectations, with nothing swept under the rug.\n\nThe honest follow-up: **the audit only covers cells we have fixtures\nfor.**  Real-world configs exercise paths the synthetic fixtures\nhaven't reached — and that's where you come in.  If you have a\nrunning-config that translates wrong (or doesn't translate at all),\nthat's the highest-impact bug report this project can receive.  See\n[`BUG_REPORTING.md`](BUG_REPORTING.md) for the workflow — Netcanon\nships its own sanitiser (the `/sanitize` browser page, the\n`netcanon sanitize` CLI, and the `POST /api/v1/sanitize` HTTP\nendpoint all share one library) so you never paste real WAN IPs,\nhashes, hostnames, or usernames into a public issue.\n\nFor the full audit narrative + the variance-class taxonomy, see\n[`docs/HOW_WE_TEST.md`](docs/HOW_WE_TEST.md).\n\n---\n\n## How it compares\n\nArriving from \"I need a Batfish / NAPALM / Capirca alternative\"?  Most\nadjacent tools occupy a different slot — Netcanon's niche is\n**bidirectional translation between vendors' native running-config\nformats**, with a per-field capability matrix and a cross-mesh audit.\n\n| Tool | What it does | Translates native config? |\n|---|---|---|\n| **Netcanon** | Multi-vendor config translation (parse + render) | ✅ bidirectional, 12 codecs |\n| [Batfish](https://github.com/batfish/batfish) | Config analysis + routing simulation | ❌ parse-only — *complements* Netcanon |\n| [Capirca](https://github.com/google/capirca) / [Aerleon](https://github.com/aerleon/aerleon) | Firewall ACL DSL → vendor syntax | ❌ render-only from a DSL — *competes* (firewall scope) |\n| [NAPALM](https://github.com/napalm-automation/napalm) / [Netmiko](https://github.com/ktbyers/netmiko) | Device get/set + SSH transport | ❌ no translation — *complements* (deploy side) |\n| [Oxidized](https://github.com/ytti/oxidized) / [RANCID](https://shrubbery.net/rancid/) | Multi-vendor config **backup** + diff | ❌ backup-only — overlaps Netcanon's backup half |\n\nNetcanon **competes** with Capirca / Aerleon (but defers firewall / NAT\n/ VPN / QoS to Tier-3) and **complements** Batfish (analyse what you\ntranslated), NAPALM / Netmiko (deploy it), and NetBox / Nautobot\n(desired-state vs existing-state).  Full breakdown — including the\nbackup + sanitiser landscape — in\n[`docs/COMPARISON.md`](docs/COMPARISON.md).\n\n---\n\n## Install\n\n### Docker (recommended)\n\n\u003e [!IMPORTANT]\n\u003e **As of 0.4.0 the container fails closed on a public bind.** The\n\u003e default entrypoint (`netcanon serve`) binds `0.0.0.0`, so it now\n\u003e **refuses to start** unless you either set an API key\n\u003e (`-e NETCANON_API_KEY=...`, which gates `/api/v1`) **or** explicitly\n\u003e opt out with `-e NETCANON_ALLOW_INSECURE_BIND=1`. This is deliberate\n\u003e — it stops an accidental `docker run -p` from exposing an\n\u003e unauthenticated API to your network. Just kicking the tyres? Add\n\u003e `-e NETCANON_ALLOW_INSECURE_BIND=1`; for anything reachable by other\n\u003e hosts, set `NETCANON_API_KEY` (the full command below does). A pure\n\u003e loopback bind (`-e NETCANON_HOST=127.0.0.1`) needs neither.\n\n```bash\n# Optional but recommended for production: setting the key explicitly keeps\n# it in your secret store, separate from the data volume.  Skip this line\n# AND the `-e` flag below and Netcanon auto-generates + persists a key in\n# data/.fernet_key on first run (zero-config).  This key encrypts device\n# credentials at rest: loss = re-entering every saved device password;\n# leak = decryptable stored credentials.\nNETCANON_FERNET_KEY=$(python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\")\n\n# Required for a network-exposed (0.0.0.0) deployment: an API key gates\n# /api/v1.  Without it (or NETCANON_ALLOW_INSECURE_BIND=1) the container\n# refuses to start on a non-loopback bind (SEC-01 fail-closed).\nNETCANON_API_KEY=$(python -c \"import secrets; print(secrets.token_urlsafe(32))\")\n\ndocker run --rm -p 8000:8000 \\\n    -v $(pwd)/configs:/app/configs \\\n    -v $(pwd)/data:/app/data \\\n    -e NETCANON_FERNET_KEY=\"$NETCANON_FERNET_KEY\" \\\n    -e NETCANON_API_KEY=\"$NETCANON_API_KEY\" \\\n    ghcr.io/netcanon/netcanon:latest\n# -\u003e http://127.0.0.1:8000        (UI)\n# -\u003e http://127.0.0.1:8000/docs   (Swagger)\n# -\u003e http://127.0.0.1:8000/health (health probe)\n```\n\n`configs/` is where backed-up running-configs land; `data/` holds\ndevice profiles, schedules, and job state.  The device-definition\nlibrary ships *inside the package* (there is no `definitions/` mount\ntarget) — set `NETCANON_DEFINITIONS_DIR` to a bind-mounted directory\nonly if you maintain a custom definition set.\n\n`NETCANON_FERNET_KEY` injects the credential-encryption key directly\n(recommended for production / orchestrated deployments — the key\nnever touches disk).  If you skip the `-e` flag, Netcanon auto-\ngenerates a key on first run inside `data/.fernet_key` so the\ncontainer works zero-config; for the production deployment path see\n[`SECURITY.md`](SECURITY.md) \"Credential Storage\".\n\n`NETCANON_API_KEY` turns on a built-in bearer-token gate: when set,\nevery `/api/v1` request must carry `Authorization: Bearer\n$NETCANON_API_KEY`.  **The key gates the `/api/v1` surface only — it does\nNOT cover the HTML UI.**  Several UI pages are server-rendered and read\ndata server-side, *not* through `/api/v1`: the diff view\n(`/configs/{a}/vs/{b}`) emits full config text (secrets included), and\n`/configs` / `/devices` list the config + device inventory.  The API key\ndoes **not** protect those pages, so for any non-loopback exposure you\n**must** front the app with a reverse proxy that authenticates the whole\nsurface — do not rely on the key alone to secure the UI.  Because the\nimage binds `0.0.0.0`, `netcanon serve` refuses to start without a key\n(or `NETCANON_ALLOW_INSECURE_BIND=1`), so an unauthenticated public bind\nis a deliberate choice — see [`SECURITY.md`](SECURITY.md) \"Threat Model\".\n\nThe published image is signed via Sigstore with an SBOM attestation.\nVerify against the immutable digest (`ghcr.io/netcanon/netcanon@sha256:\u003cdigest\u003e`),\nnot a mutable tag — see [`SECURITY.md`](SECURITY.md) \"Supply-Chain\nIntegrity\" for the exact `cosign verify` invocation.\n\n**Docker Hub mirror** — same image, convenience-mirrored to Docker\nHub if your tooling defaults to `docker.io`:\n\n```bash\ndocker run --rm -p 8000:8000 -e NETCANON_ALLOW_INSECURE_BIND=1 netcanon/netcanon:latest\n```\n\nThe Docker Hub mirror has the same image bytes but no cosign\nsignature or SBOM attestation — operators in regulated environments\nshould pull from GHCR for the attested provenance chain.  See\n[`SECURITY.md`](SECURITY.md) for the supply-chain story.\n\n### Pip\n\n```bash\npip install netcanon\nuvicorn netcanon.main:app --host 127.0.0.1 --port 8000\n```\n\n`netcanon` also installs the `netcanon` CLI — `netcanon sanitize -i\nmy-config.txt --source-vendor cisco_iosxe_cli --dry-run` is the\ntypical CLI entrypoint for the bug-reporting workflow.  If the\nserver's running, the **`/sanitize` browser page** is the easier\npath (paste or pick a stored config, click Sanitize, copy the\noutput — see [`BUG_REPORTING.md`](BUG_REPORTING.md) for the full\nworkflow including what gets redacted).\n\n### Desktop (Windows)\n\nDownload the MSI from\n[Releases](https://github.com/netcanon/netcanon/releases), or from\nsource:\n\n```bash\npip install -e \".[desktop]\"\npython -m netcanon_desktop\n```\n\nThe desktop shell runs the same FastAPI app inside a PySide6 webview\nwith a tray icon — same UI, no command-line.  See\n[`netcanon_desktop/README.md`](netcanon_desktop/README.md) for the\nthreading model, settings, and MSI build instructions.\n\n---\n\n## Walkthroughs — \"is this the right tool for my migration?\"\n\nEach walkthrough is paired 1:1 with a runnable demo scenario.  Read\nthe narrative first, run `python tools/demo.py --pair \u003ckey\u003e` to see\nthe actual translation.\n\n| Walkthrough | Demo scenario | Frame |\n|---|---|---|\n| [Cisco IOS-XE → Juniper Junos](docs/walkthroughs/cisco_iosxe_to_junos.md) | `cisco__junos` | DC leaf migration: VLANs + interfaces + routes |\n| [FortiGate → MikroTik RouterOS](docs/walkthroughs/fortigate_to_mikrotik.md) | `fortigate__mikrotik` | Branch-firewall consolidation: DNS + interfaces + DHCP pools |\n| [Aruba AOS-S → Arista EOS](docs/walkthroughs/aruba_to_arista.md) | `aruba__arista` | Switch refresh: VLAN-centric → port-centric grammar |\n| [OPNsense → Juniper Junos](docs/walkthroughs/opnsense_to_junos.md) | `opnsense__junos` | Edge-firewall migration with explicit Tier-3 boundary |\n\nEach walkthrough ends in a manual-review checklist — what to verify\non the device after the rendered config lands, before you apply it.\n\n---\n\n## What translates, and what doesn't\n\nThe canonical model classifies every field by semantic stability\nacross vendors.  Full per-codec matrix is in\n[`docs/CAPABILITIES.md`](docs/CAPABILITIES.md); the short version:\n\n* **Tier 1 — auto-translatable.**  hostname, interfaces (name /\n  description / enabled state / IPv4 + IPv6 addresses / per-interface\n  VRF binding), VLANs, static routes, DNS / NTP / syslog servers,\n  timezone.  Every shipped codec parses + renders these fully (the\n  experimental `cisco_iosxe` NETCONF stub excepted — it renders\n  interfaces only).\n* **Tier 2 — translatable with caveats.**  SNMP (incl. SNMPv3 USM),\n  LAGs, local users, RADIUS, DHCP server pools, VXLAN VNIs, EVPN\n  type-5 routes, routing instances / VRFs, Junos `apply-groups`.\n  Hashes that the target's CLI cannot consume surface as commented\n  review lines, never as plaintext fallback.\n* **Tier 3 — opaque carry / never auto-rendered.**  Firewall rules,\n  NAT, IPsec / OpenVPN / WireGuard, QoS, route-maps, dynamic routing\n  protocol stanzas, PKI.  These are vendor-specific stateful policy\n  that doesn't translate cross-vendor cleanly — Netcanon **detects**\n  them, surfaces them via the migrate-page banner with a count and\n  section names, and deliberately doesn't auto-render.  Hand-build\n  them natively on the target.\n\nIf your migration's primary need is firewall translation,\n[`docs/COMPARISON.md`](docs/COMPARISON.md) names adjacent tools\n(Capirca / Aerleon) that handle that scope.  Netcanon is the right\ntool for the *router* portion of a migration — and explicitly the\nwrong tool to claim it does the firewall portion.\n\n---\n\n## Two concerns, one app\n\nNetcanon co-hosts:\n\n1. **Backup** — pulls `running-config` (or vendor equivalent) from\n   network devices over SSH / NETCONF / REST and stores it verbatim\n   in `configs/\u003chostname\u003e.\u003cext\u003e`.  Runs on a schedule or on demand.\n2. **Migration** — translates a stored backup from one vendor's\n   config grammar to another through the canonical intent tree.\n\nSame FastAPI process; same UI; same Docker image.  Use whichever\nhalf (or both).  See [`ARCHITECTURE.md`](ARCHITECTURE.md) for the\nfour-layer design.\n\n---\n\n## Found a bug?  Got a config that breaks it?\n\nThat's the contribution this project values most.  Workflow:\n\n1. Sanitise your config — open the `/sanitize` browser page (easiest\n   if the server's running), or run the `netcanon sanitize` CLI\n   (no server required).  Both strip hostnames, usernames, IPs,\n   hashes, SNMP communities, etc., with a counter-per-session\n   stable substitution table you can audit before submission.\n2. Open a [bug report](https://github.com/netcanon/netcanon/issues/new?template=bug_report.yml)\n   or [fixture submission](https://github.com/netcanon/netcanon/issues/new?template=fixture_submission.yml).\n3. The fixture lands in `tests/fixtures/real/\u003cvendor\u003e/`, the\n   cross-mesh audit re-runs, and the variance class your fixture\n   surfaces gets a row in `tests/fixtures/real/PHASE4_RECONCILIATION.md`.\n\nFull workflow is in [`BUG_REPORTING.md`](BUG_REPORTING.md).\n\n---\n\n## For contributors\n\n| You want to… | Start here |\n|---|---|\n| Understand the architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) — four-layer model, canonical bridge, codec types |\n| Follow the contributor rules | [`AGENTS.md`](AGENTS.md) — hard rules, parity checklist, gotchas |\n| Read the slower-changing methodology | [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md) — matrix-honesty discipline distilled, portable to other projects |\n| Look up project jargon | [`docs/glossary.md`](docs/glossary.md) — canonical, codec, mesh, ship-before-wire, target profile |\n| Read the canonical model overview | [`netcanon/migration/canonical/README.md`](netcanon/migration/canonical/README.md) |\n| Add or change an HTTP route | [`netcanon/api/routes/README.md`](netcanon/api/routes/README.md) — frozen pipeline-stage signatures, endpoint inventory |\n| Add a new codec | [`netcanon/migration/codecs/README.md`](netcanon/migration/codecs/README.md) |\n| Add a new device definition / target profile | [`netcanon/definitions/library/README.md`](netcanon/definitions/library/README.md) |\n| Add a new canonical field | [`docs/adding-a-canonical-field.md`](docs/adding-a-canonical-field.md) |\n| Ship a feature across web + desktop | [`docs/feature-parity-walkthrough.md`](docs/feature-parity-walkthrough.md) |\n| See what's shipped recently | [`CHANGELOG.md`](CHANGELOG.md) |\n| Check codec certification tiers | [`tests/fixtures/real/RESULTS.md`](tests/fixtures/real/RESULTS.md) |\n| Write tests | [`tests/README.md`](tests/README.md) |\n| Review the security model | [`SECURITY.md`](SECURITY.md) |\n| Community / participation norms | [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) — Contributor Covenant + enforcement contact |\n\n### Run the test suite\n\n```bash\npip install -e \".[dev]\"\npytest                       # all tiers; desktop tier needs the [desktop] extra\npytest -m e2e                # Playwright browser tests (slower)\n```\n\nTests run across four layers: unit (pure functions, no I/O — the\nreal-capture validation harness lives here as a unit subset),\nintegration (TestClient + mocked SSH at the `get_collector`\nfactory), e2e (Playwright against a live Uvicorn), and desktop\n(PySide6 + pystray mocked).  CI runs the **unit + integration** tiers\non Python 3.11 / 3.12 / 3.13 / 3.14 against Ubuntu; the e2e + desktop\ntiers run locally, not in CI.  The CI pytest invocation uses `-x`\n(stop at first failure), so a green run reports the authoritative\npass count while a red run stops early.\n\n### Layout\n\n```\nnetcanon/              FastAPI application (shared by both platforms)\n ├── api/routes/          HTTP endpoints\n ├── collectors/          SSH/NETCONF/REST fetchers — one factory,\n │                        one mock-point (`get_collector`)\n ├── definitions/         Device-definition loader + shipped YAML\n │                        library (`library/`, baked into the wheel)\n ├── migration/           Cross-vendor translation pipeline\n │   ├── canonical/         CanonicalIntent model + shared transforms\n │   └── codecs/            Per-vendor parse/render implementations\n ├── services/            Plain-function orchestrators (pipeline, detect, …)\n ├── storage/             FileConfigStore\n ├── tools/               sanitize, etc.\n └── templates/           Jinja2 templates (most interactive elements\n                          carry a data-testid — see AGENTS.md)\n\nnetcanon_desktop/      Windows tray/webview shell around the same server\ntools/demo.py           One-command cross-vendor translation demo\ndocs/walkthroughs/      Narrative migration walkthroughs (paired with demo)\ndocs/vendors/           Per-vendor \"what works for me?\" pages\ntests/unit/             Pure-function tests, no I/O\ntests/integration/      FastAPI TestClient tests, SSH mocked\ntests/e2e/              Playwright browser tests\ntests/desktop/          PySide6/pystray-mocked desktop shell tests\ntests/fixtures/real/    Real-capture validation corpus (see RESULTS.md)\n```\n\n---\n\n## See also\n\n* [`ARCHITECTURE.md`](ARCHITECTURE.md) — the four-layer model + canonical\n  bridge + codec types\n* [`AGENTS.md`](AGENTS.md) — contributor directives, hard rules, doc-sync\n  checklist\n* [`tests/README.md`](tests/README.md) — test-tier layout + how to run\n* [`docs/CAPABILITIES.md`](docs/CAPABILITIES.md) — per-codec capability matrix\n* [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) — operator-facing\n  diagnostic flowchart\n* [`SECURITY.md`](SECURITY.md) — security model, sanitiser, supply-chain\n  integrity controls\n* [`CHANGELOG.md`](CHANGELOG.md) — release log\n* [`TRADEMARKS.md`](TRADEMARKS.md) — third-party trademark / nominative-use notice\n\n---\n\n## License\n\nMIT.  See [`LICENSE`](LICENSE).  Third-party fixtures keep their\nupstream licences — see [`tests/fixtures/real/NOTICE.md`](tests/fixtures/real/NOTICE.md)\nfor provenance.\n\nFor responsible disclosure of security issues, see\n[`SECURITY.md`](SECURITY.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetcanon%2Fnetcanon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnetcanon%2Fnetcanon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetcanon%2Fnetcanon/lists"}