From 4134e0a521638a3c0ba374cf59f86b81f32f2418 Mon Sep 17 00:00:00 2001 From: Manon Goo Date: Mon, 23 Mar 2026 09:59:53 +0000 Subject: [PATCH 1/6] Add test data sources reference for manual testing Add tests/TEST_DATA_SOURCES.md documenting where to find real-world network configs online for each vendor format netconan supports (Cisco IOS, Arista EOS, Juniper JunOS, Fortinet FortiOS, AWS VPN, SNMP). Includes download instructions and usage tips. Add tests/test_data/ to .gitignore for downloaded configs. --- .gitignore | 3 + tests/TEST_DATA_SOURCES.md | 231 +++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 tests/TEST_DATA_SOURCES.md diff --git a/.gitignore b/.gitignore index 86944f7..2502366 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,6 @@ ENV/ # development artifacts working/ + +# downloaded test configs (see tests/TEST_DATA_SOURCES.md) +tests/test_data/ diff --git a/tests/TEST_DATA_SOURCES.md b/tests/TEST_DATA_SOURCES.md new file mode 100644 index 0000000..123205d --- /dev/null +++ b/tests/TEST_DATA_SOURCES.md @@ -0,0 +1,231 @@ +# Test Data Sources + +Where to find real-world network configuration files for testing netconan. + +Netconan's regex-based anonymization works across many vendor formats. Testing against +real configs (beyond the inline test strings in `tests/unit/`) helps validate coverage +and catch edge cases. + +## Directory Setup + +Downloaded configs should go in `tests/test_data/`, which is git-ignored to avoid +accidentally committing sensitive material. + +```bash +mkdir -p tests/test_data/{cisco,arista,juniper,fortinet,aws,snmp} +``` + +## Vendor Sources + +### Cisco IOS + +Cisco IOS has the broadest regex coverage in netconan (75+ password patterns). + +**GitHub repos:** + +- [Batfish Cisco test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/cisco/testconfigs) + — Hundreds of minimal IOS/IOS-XE configs covering edge cases. Clone the repo + and copy files from the path above. +- [NTC Templates](https://github.com/networktocode/ntc-templates) + — TextFSM templates with sample `show` command outputs for many Cisco platforms. + Look under `tests/` for fixture data. + +**Download example (Batfish):** + +```bash +git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/batfish/batfish.git /tmp/batfish +cd /tmp/batfish +git sparse-checkout set projects/batfish/src/test/resources/org/batfish/grammar/cisco/testconfigs +cp projects/batfish/src/test/resources/org/batfish/grammar/cisco/testconfigs/* \ + /path/to/netconan/tests/test_data/cisco/ +``` + +**Vendor docs:** + +- [Cisco IOS-XE Configuration Guides](https://www.cisco.com/c/en/us/support/ios-nx-os-software/ios-xe-17/products-installation-and-configuration-guides-list.html) + — Official config examples for every IOS-XE feature. + +### Arista EOS + +Arista EOS uses Cisco-like syntax. Netconan covers SHA-512 passwords and VRRP +authentication for EOS (see `default_pwd_regexes.py`, Issue #3 tracks expanding this). + +**GitHub repos:** + +- [Batfish Arista test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/arista/testconfigs) + — Arista-specific test configs in the Batfish grammar suite. +- [Arista NetDevOps Community](https://github.com/arista-netdevops-community) + — Multiple repos with EOS config examples and automation scripts. +- [Ansible Arista EOS Collection](https://github.com/ansible-collections/arista.eos) + — Ansible modules with EOS config snippets in the docs and test fixtures. + +**Download example (Batfish):** + +```bash +git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/batfish/batfish.git /tmp/batfish +cd /tmp/batfish +git sparse-checkout set projects/batfish/src/test/resources/org/batfish/grammar/arista/testconfigs +cp projects/batfish/src/test/resources/org/batfish/grammar/arista/testconfigs/* \ + /path/to/netconan/tests/test_data/arista/ +``` + +### Juniper JunOS (Set-Style) + +Set-style (`set system host-name ...`) is one of two JunOS config formats. Netconan +has strong Juniper support including Type 9 encryption handling. + +**GitHub repos:** + +- [Batfish Flat Juniper test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/flatjuniper/testconfigs) + — Set-style ("flat") Juniper configs used in Batfish parsing tests. + +**Download example:** + +```bash +git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/batfish/batfish.git /tmp/batfish +cd /tmp/batfish +git sparse-checkout set projects/batfish/src/test/resources/org/batfish/grammar/flatjuniper/testconfigs +cp projects/batfish/src/test/resources/org/batfish/grammar/flatjuniper/testconfigs/* \ + /path/to/netconan/tests/test_data/juniper/ +``` + +**Vendor docs:** + +- [Juniper TechLibrary — CLI Configuration](https://www.juniper.net/documentation/us/en/software/junos/cli/topics/topic-map/cli-configuration.html) + — Official reference covering both set and hierarchical formats. + +### Juniper JunOS (Hierarchical) + +Hierarchical format uses curly braces (`system { host-name router1; }`). Both formats +should be tested since netconan's regexes may behave differently with indented blocks. + +**GitHub repos:** + +- [Batfish Juniper test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/juniper/testconfigs) + — Hierarchical (brace-style) Juniper configs. + +**Download example:** + +```bash +git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/batfish/batfish.git /tmp/batfish +cd /tmp/batfish +git sparse-checkout set projects/batfish/src/test/resources/org/batfish/grammar/juniper/testconfigs +cp projects/batfish/src/test/resources/org/batfish/grammar/juniper/testconfigs/* \ + /path/to/netconan/tests/test_data/juniper/ +``` + +### Fortinet FortiOS + +FortiOS uses `config`/`set`/`end` block syntax. Netconan has basic support for ENC +passwords and pksecret fields (4 regex patterns). + +**GitHub repos:** + +- [Azure VPN Config Samples — FortiGate](https://github.com/Azure/Azure-vpn-config-samples/tree/master/Fortinet/Current) + — Full `show full-configuration` output from a FortiGate device. +- [Batfish Fortinet test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/fortios/testconfigs) + — FortiOS test configs in the Batfish grammar suite. + +**Download example (Azure VPN samples):** + +```bash +curl -o tests/test_data/fortinet/fortigate_full.txt \ + "https://raw.githubusercontent.com/Azure/Azure-vpn-config-samples/master/Fortinet/Current/fortigate_show%20full-configuration.txt" +``` + +**Vendor docs:** + +- [Fortinet Documentation Library](https://docs.fortinet.com/document/fortigate/7.6.0/administration-guide) + — Official FortiOS administration guide with config examples. + +### AWS VPN Configs + +AWS VPN configs use XML (``) and JSON (`PreSharedKey`) formats. +Netconan has 4 regex patterns for these. + +**GitHub repos:** + +- [AWS VPN Gateway strongSwan](https://github.com/aws-samples/vpn-gateway-strongswan) + — CloudFormation templates with VPN connection configs. +- [Terraform AWS VPN Gateway](https://github.com/terraform-aws-modules/terraform-aws-vpn-gateway) + — Terraform module with VPN config examples in `examples/`. + +**Vendor docs:** + +- [AWS Site-to-Site VPN User Guide](https://docs.aws.amazon.com/vpn/latest/s2svpn/VPNTunnels.html) + — Official documentation with XML/JSON config download examples. + +**Creating test data manually:** + +AWS VPN configs follow a predictable XML structure. A minimal test file: + +```xml + + + + + ExamplePreSharedKey123 + + + +``` + +### SNMP Configs + +SNMP community strings and SNMPv3 user/group definitions appear across all vendors. +Netconan covers `snmp-server community` and `snmp-server user` patterns (14+ regexes). + +**Vendor docs (Cisco — most comprehensive examples):** + +- [Cisco SNMPv3 Configuration Guide](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/snmp/configuration/xe-3se/3850/snmp-xe-3se-3850-book/nm-snmp-snmpv3.html) + — Complete SNMPv3 user/group/view config examples with all security levels. + +**Creating test data manually:** + +SNMP config lines follow standard patterns across vendors: + +``` +snmp-server community public RO +snmp-server community private RW +snmp-server group MYGROUP v3 priv read MYVIEW write MYVIEW +snmp-server user MYUSER MYGROUP v3 auth sha AuthPass123 priv aes 128 PrivPass456 +``` + +## Using Test Data for Development + +### Running netconan against downloaded configs + +```bash +# Anonymize a single file +netconan -i tests/test_data/cisco/example.cfg -o /tmp/anon_output.cfg -a -p + +# Anonymize a whole directory +netconan -i tests/test_data/cisco/ -o /tmp/anon_cisco/ -a -p + +# Check what gets anonymized (diff original vs output) +diff tests/test_data/cisco/example.cfg /tmp/anon_output.cfg +``` + +### Adding configs as test fixtures + +If a downloaded config exposes a bug or an unhandled pattern, add it as a test case: + +1. **Extract the relevant lines** — isolate just the config lines that need testing. +2. **Add to the appropriate test** — add inline test data to + `tests/unit/test_sensitive_item_removal.py` (for password/community patterns) or + create a new test file. +3. **Never commit raw downloaded configs** — they may contain real credentials or + proprietary content. Always sanitize first. + +### Tips for creating test cases from real configs + +- Look for lines that netconan **should** anonymize but doesn't (false negatives). +- Look for lines that netconan anonymizes **incorrectly** (false positives or mangled output). +- Juniper Type 9 encrypted passwords and Cisco Type 7 passwords are especially good + test targets since netconan has dedicated handling for them. +- Test both `show running-config` style output and startup-config file format — some + vendors include extra headers/timestamps that can affect regex matching. From de77dac585eb8007aecc9ae54a30cf41e803bb8c Mon Sep 17 00:00:00 2001 From: Manon Goo Date: Mon, 23 Mar 2026 10:08:45 +0000 Subject: [PATCH 2/6] Expand test data sources with intensive GitHub search Add many more real-world config repositories found through thorough GitHub searching: - Multi-vendor section: Batfish, CiscoConfParse, Azure VPN samples, Oxidized, containerlab topologies - Cisco: ciscoconfparse fixtures, tireland1985 lab configs, ccat, bbartik router configs - Arista: HPEIMCUtils sample, arista-network-ci, ceos_lab_demo, AVD EVPN webinar, multi-vendor-python - Juniper set-style: jcoeder/juniper-configurations (28 snippets), flightlesstux SRX config - Juniper hierarchical: Azure VPN SRX configs, jtkristoff templates, junoser parser fixtures - Fortinet: FortiLab-VPN-IPSEC backup files, fortigate-terraform-deploy, fgtconfig analysis tool - AWS VPN: PackeTsar IPv6 VPN, cloudposse terraform module, aws_vpn_config downloader, Azure Cisco ASA samples - SNMP: LibreNMS multi-vendor guide, JunOS SNMPv3 gist, net-snmp example conf, ansible SNMP playbooks --- tests/TEST_DATA_SOURCES.md | 129 ++++++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 16 deletions(-) diff --git a/tests/TEST_DATA_SOURCES.md b/tests/TEST_DATA_SOURCES.md index 123205d..6c482f2 100644 --- a/tests/TEST_DATA_SOURCES.md +++ b/tests/TEST_DATA_SOURCES.md @@ -15,6 +15,30 @@ accidentally committing sensitive material. mkdir -p tests/test_data/{cisco,arista,juniper,fortinet,aws,snmp} ``` +## Multi-Vendor Sources + +These repositories contain configs for multiple vendors in one place. + +- [Batfish](https://github.com/batfish/batfish) + — The single best source. Test configs for Cisco, Arista, Juniper (flat and + hierarchical), Fortinet, Palo Alto, and more under + `projects/batfish/src/test/resources/org/batfish/grammar/`. Hundreds of files. +- [CiscoConfParse test fixtures](https://github.com/mpenning/ciscoconfparse/tree/master/tests/fixtures/configs) + — Parser test fixtures covering Cisco IOS/NXOS/ASA/IOS-XR, Juniper JunOS, F5, + Arista EOS, Palo Alto, Brocade, HP, and more. +- [NTC Templates](https://github.com/networktocode/ntc-templates) + — TextFSM templates with sample `show` command outputs for many vendors. Look + under `tests/` for fixture data. +- [Azure VPN Config Samples](https://github.com/Azure/Azure-vpn-config-samples) + — Microsoft-maintained. Full running configs for Cisco ASA/ISR/ASR, Juniper SRX, + FortiGate, and others. Contains encrypted passwords and pre-shared keys. +- [Oxidized](https://github.com/ytti/oxidized) + — RANCID replacement supporting 90+ vendor OS types. The tool itself is useful for + collecting configs from lab devices. +- [Containerlab topologies (holo-routing)](https://github.com/holo-routing/containerlab-topologies) + — 25+ protocol lab topologies with configs for FRRouting, Nokia SR Linux, Arista + cEOS. Covers BGP, OSPF, IS-IS, MPLS-LDP, VRRP, and more. + ## Vendor Sources ### Cisco IOS @@ -24,11 +48,19 @@ Cisco IOS has the broadest regex coverage in netconan (75+ password patterns). **GitHub repos:** - [Batfish Cisco test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/cisco/testconfigs) - — Hundreds of minimal IOS/IOS-XE configs covering edge cases. Clone the repo - and copy files from the path above. + — Hundreds of minimal IOS/IOS-XE configs covering edge cases. +- [CiscoConfParse test fixtures](https://github.com/mpenning/ciscoconfparse/tree/master/tests/fixtures/configs) + — Multiple `sample_NN.ios` files with diverse IOS syntax (interfaces, routing, + ACLs, HSRP, etc.). Also includes ASA, NXOS, IOS-XR samples. +- [tireland1985/cisco-config-examples](https://github.com/tireland1985/cisco-config-examples) + — 5 sanitized lab configs: AP1141N, C2811-CUCME, C2911 router, C3560G L3 switch. + Contains enable secrets, TACACS+, SNMP community strings. - [NTC Templates](https://github.com/networktocode/ntc-templates) - — TextFSM templates with sample `show` command outputs for many Cisco platforms. - Look under `tests/` for fixture data. + — TextFSM templates with sample `show` command outputs under `tests/`. +- [frostbits-security/ccat](https://github.com/frostbits-security/ccat) + — Cisco Config Analysis Tool with test configs in `example/` directory. +- [bbartik/cisco-cfg](https://github.com/bbartik/cisco-cfg) + — 6 router config files including Jinja2 templates. **Download example (Batfish):** @@ -55,10 +87,20 @@ authentication for EOS (see `default_pwd_regexes.py`, Issue #3 tracks expanding - [Batfish Arista test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/arista/testconfigs) — Arista-specific test configs in the Batfish grammar suite. -- [Arista NetDevOps Community](https://github.com/arista-netdevops-community) - — Multiple repos with EOS config examples and automation scripts. +- [HPENetworking/HPEIMCUtils — Arista sample config](https://github.com/HPENetworking/HPEIMCUtils/blob/master/DeviceAdapters/Arista%20Networks/arista%20sample%20config.txt) + — Complete Arista config with enable secret, username admin secret, SNMP + community strings ("public"/"private"), MLAG, VLANs, NTP, AAA. +- [networkop/arista-network-ci](https://github.com/networkop/arista-network-ci) + — Generated configs for lab and production topologies. BGP, interfaces, VLANs, + SVIs, route-maps. Configs intentionally contain bugs for testing Batfish. +- [arista-netdevops-community/ceos_lab_demo](https://github.com/arista-netdevops-community/ceos_lab_demo) + — 3 cEOS startup configs for an EBGP triangle topology. +- [arista-netdevops-community/avd-evpn-webinar-june-11](https://github.com/arista-netdevops-community/avd-evpn-webinar-june-11) + — EVPN/VXLAN fabric configs in `intended/configs/` directory. - [Ansible Arista EOS Collection](https://github.com/ansible-collections/arista.eos) — Ansible modules with EOS config snippets in the docs and test fixtures. +- [JulioPDX/multi-vendor-python](https://github.com/JulioPDX/multi-vendor-python) + — Running/startup configs from Cisco vIOS, Arista vEOS, and Aruba CX. **Download example (Batfish):** @@ -80,16 +122,19 @@ has strong Juniper support including Type 9 encryption handling. - [Batfish Flat Juniper test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/flatjuniper/testconfigs) — Set-style ("flat") Juniper configs used in Batfish parsing tests. +- [jcoeder/juniper-configurations](https://github.com/jcoeder/juniper-configurations) + — 28 production-style set-style config snippets: BGP (communities, policies, bogon + filtering), OSPF, firewall rules (QFX5100 RE protection), HA (chassis redundancy, + MC-LAG), SRX dynamic VPN, SNMPv3, TACACS, EVPN/VXLAN, IPFIX. +- [flightlesstux/juniper-srx-config](https://github.com/flightlesstux/juniper-srx-config) + — SRX110H-VA set-style config for VDSL2 internet connectivity. System setup, + interfaces, security zones, NAT. -**Download example:** +**Download example (jcoeder):** ```bash -git clone --depth 1 --filter=blob:none --sparse \ - https://github.com/batfish/batfish.git /tmp/batfish -cd /tmp/batfish -git sparse-checkout set projects/batfish/src/test/resources/org/batfish/grammar/flatjuniper/testconfigs -cp projects/batfish/src/test/resources/org/batfish/grammar/flatjuniper/testconfigs/* \ - /path/to/netconan/tests/test_data/juniper/ +git clone https://github.com/jcoeder/juniper-configurations.git /tmp/juniper-configs +cp /tmp/juniper-configs/*.conf tests/test_data/juniper/ ``` **Vendor docs:** @@ -106,8 +151,17 @@ should be tested since netconan's regexes may behave differently with indented b - [Batfish Juniper test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/juniper/testconfigs) — Hierarchical (brace-style) Juniper configs. +- [Azure VPN Config Samples — Juniper SRX](https://github.com/Azure/Azure-vpn-config-samples/tree/master/Juniper/Current/SRX) + — Full hierarchical SRX configs for site-to-site VPNs with security zones, + policies, IPsec, IKE. Contains `encrypted-password "$1$..."` entries. +- [jtkristoff/junos](https://github.com/jtkristoff/junos) + — 10+ hierarchical config templates: BFD, BGP Monitoring Protocol, firewall + filters, iBGP, route origin validation, BGP route sanitization. +- [codeout/junoser](https://github.com/codeout/junoser) + — PEG parser for JunOS configs with test fixtures in both set and hierarchical + format. -**Download example:** +**Download example (Batfish):** ```bash git clone --depth 1 --filter=blob:none --sparse \ @@ -126,9 +180,19 @@ passwords and pksecret fields (4 regex patterns). **GitHub repos:** - [Azure VPN Config Samples — FortiGate](https://github.com/Azure/Azure-vpn-config-samples/tree/master/Fortinet/Current) - — Full `show full-configuration` output from a FortiGate device. + — Full `show full-configuration` output from a FortiGate device. Contains + `set password ENC SH2...` and `set psksecret ENC ...` entries. - [Batfish Fortinet test configs](https://github.com/batfish/batfish/tree/master/projects/batfish/src/test/resources/org/batfish/grammar/fortios/testconfigs) — FortiOS test configs in the Batfish grammar suite. +- [vansteenk/FortiLab-VPN-IPSEC](https://github.com/vansteenk/FortiLab-VPN-IPSEC) + — Lab environment with complete FortiGate `.conf` backup files. Contains VPN + IPsec phase1 configs with PSK, admin passwords (ENC format), user credentials, + certificate private keys. +- [fortinet/fortigate-terraform-deploy](https://github.com/fortinet/fortigate-terraform-deploy) + — Terraform deployment templates with FortiGate configs. See + `aws/6.2/ha/config-active.conf` for HA example. +- [cgustave/fgtconfig](https://github.com/cgustave/fgtconfig) + — FortiGate configuration analysis tool, may contain test fixture configs. **Download example (Azure VPN samples):** @@ -137,6 +201,13 @@ curl -o tests/test_data/fortinet/fortigate_full.txt \ "https://raw.githubusercontent.com/Azure/Azure-vpn-config-samples/master/Fortinet/Current/fortigate_show%20full-configuration.txt" ``` +**Download example (FortiLab):** + +```bash +git clone https://github.com/vansteenk/FortiLab-VPN-IPSEC.git /tmp/fortilab +cp /tmp/fortilab/*.conf tests/test_data/fortinet/ +``` + **Vendor docs:** - [Fortinet Documentation Library](https://docs.fortinet.com/document/fortigate/7.6.0/administration-guide) @@ -153,6 +224,17 @@ Netconan has 4 regex patterns for these. — CloudFormation templates with VPN connection configs. - [Terraform AWS VPN Gateway](https://github.com/terraform-aws-modules/terraform-aws-vpn-gateway) — Terraform module with VPN config examples in `examples/`. +- [PackeTsar/AWS_IPv6_VPN](https://github.com/PackeTsar/AWS_IPv6_VPN) + — Guide for building IPv6 site-to-site VPN to AWS with IKEv1 pre-shared-key + config examples. +- [cloudposse/terraform-aws-vpn-connection](https://github.com/cloudposse/terraform-aws-vpn-connection) + — Terraform module supporting `tunnel1_preshared_key` / `tunnel2_preshared_key`. +- [asantos2000/aws_vpn_config](https://github.com/asantos2000/aws_vpn_config) + — Tool to download VPN configs from AWS and convert to vendor-specific formats. + Uses `describe_vpn_connections` API to get the XML. +- [Azure VPN Config Samples — Cisco ASA](https://github.com/Azure/Azure-vpn-config-samples/tree/master/Cisco/Current/ASA) + — ASA running-config with `ikev1 pre-shared-key` entries; useful for testing + Cisco VPN pre-shared key regexes too. **Vendor docs:** @@ -179,7 +261,22 @@ AWS VPN configs follow a predictable XML structure. A minimal test file: SNMP community strings and SNMPv3 user/group definitions appear across all vendors. Netconan covers `snmp-server community` and `snmp-server user` patterns (14+ regexes). -**Vendor docs (Cisco — most comprehensive examples):** +**GitHub repos:** + +- [LibreNMS — SNMP Configuration Examples](https://github.com/librenms/librenms/blob/master/doc/Support/SNMP-Configuration-Examples.md) + — Comprehensive multi-vendor SNMP guide covering Cisco (ASA, IOS), Juniper, + Extreme, Linux, Windows. Uses placeholders like ``. +- [JunOS SNMPv3 example (Gist)](https://gist.github.com/rendoaw/541c41527d9c576305dd) + — Complete Juniper JunOS SNMPv3 config with `## SECRET-DATA` markers, MD5/SHA + auth, DES/AES128 privacy, community strings. +- [jcoeder/juniper-configurations](https://github.com/jcoeder/juniper-configurations) + — Includes SNMPv3 set-style config snippets among its 28 files. +- [net-snmp/net-snmp — EXAMPLE.conf](https://github.com/net-snmp/net-snmp/blob/master/EXAMPLE.conf.def) + — Example `snmpd.conf` with community string configuration. +- [colin-mccarthy/ansible-playbooks-for-cisco-ios](https://github.com/colin-mccarthy/ansible-playbooks-for-cisco-ios) + — SNMP configuration playbooks with `snmp-server` command examples. + +**Vendor docs:** - [Cisco SNMPv3 Configuration Guide](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/snmp/configuration/xe-3se/3850/snmp-xe-3se-3850-book/nm-snmp-snmpv3.html) — Complete SNMPv3 user/group/view config examples with all security levels. From 458794aa16b650ad23904c2c343d8e6dfa466d2f Mon Sep 17 00:00:00 2001 From: Manon Goo Date: Mon, 23 Mar 2026 10:28:11 +0000 Subject: [PATCH 3/6] Add automated config download script and integration test suite Add tools/download_test_configs.py to download real-world network configs from public GitHub repos (Batfish, CiscoConfParse, jcoeder, Azure VPN) into tests/test_data/ for 5 vendors (cisco, arista, juniper_flat, juniper_hierarchical, fortinet). Add tests/integration/test_real_configs.py with parametrized pytest tests that discover downloaded configs and verify netconan processes them without crashes, anonymizes password patterns, and changes IP addresses. The integration tests skip gracefully when test data is not downloaded. --- tests/TEST_DATA_SOURCES.md | 51 +++++ tests/integration/__init__.py | 0 tests/integration/test_real_configs.py | 237 ++++++++++++++++++++ tools/download_test_configs.py | 287 +++++++++++++++++++++++++ 4 files changed, 575 insertions(+) create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/test_real_configs.py create mode 100644 tools/download_test_configs.py diff --git a/tests/TEST_DATA_SOURCES.md b/tests/TEST_DATA_SOURCES.md index 6c482f2..b7b5997 100644 --- a/tests/TEST_DATA_SOURCES.md +++ b/tests/TEST_DATA_SOURCES.md @@ -292,6 +292,57 @@ snmp-server group MYGROUP v3 priv read MYVIEW write MYVIEW snmp-server user MYUSER MYGROUP v3 auth sha AuthPass123 priv aes 128 PrivPass456 ``` +## Automated Testing + +An automated download script and integration test suite are provided to streamline +testing against real-world configs. + +### Downloading configs + +```bash +# Download all vendors +python tools/download_test_configs.py + +# Download specific vendors only +python tools/download_test_configs.py --vendors cisco arista + +# Force re-download (overwrite existing) +python tools/download_test_configs.py --force +``` + +Configs are downloaded into `tests/test_data/{vendor}/{source}/`. The directory is +git-ignored so downloaded configs are never committed. + +**Current sources (7 across 5 vendors):** + +| Vendor | Source | Method | Files | +|--------|--------|--------|-------| +| cisco | batfish | git sparse | Cisco IOS/IOS-XE test configs | +| cisco | ciscoconfparse | git sparse | `*.ios` parser test fixtures | +| arista | batfish | git sparse | Arista EOS test configs | +| juniper_flat | jcoeder | git clone | 28 production-style set-style snippets | +| juniper_hierarchical | batfish | git sparse | Hierarchical (brace-style) Juniper configs | +| fortinet | batfish | git sparse | FortiOS test configs | +| fortinet | azure | curl | FortiGate `show full-configuration` | + +### Running integration tests + +```bash +# Run integration tests (skips if test_data/ not present) +python -m pytest tests/integration/test_real_configs.py -v + +# Run only crash tests (fastest) +python -m pytest tests/integration/test_real_configs.py -v -k test_no_crash + +# Run only password anonymization checks +python -m pytest tests/integration/test_real_configs.py -v -k test_passwords_anonymized +``` + +The integration tests verify three things per config file: +1. **No crash** — netconan processes the file without exceptions +2. **Passwords anonymized** — if the input contains password patterns, the output differs +3. **IPs anonymized** — if the input contains IP addresses, some change in the output + ## Using Test Data for Development ### Running netconan against downloaded configs diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_real_configs.py b/tests/integration/test_real_configs.py new file mode 100644 index 0000000..e8e1d69 --- /dev/null +++ b/tests/integration/test_real_configs.py @@ -0,0 +1,237 @@ +"""Integration tests that run netconan against real-world config files. + +Discovers configs downloaded into tests/test_data/{vendor}/{source}/ and +runs netconan against each, verifying: + - No crashes (no unhandled exceptions) + - Output file is produced and non-empty + - Password-like patterns are anonymized when present in input + - IP addresses are changed when present in input + +Requires test data to be downloaded first: + python tools/download_test_configs.py + +The entire suite is skipped if tests/test_data/ does not exist. +""" + +import os +import re +from pathlib import Path + +import pytest + +from netconan.netconan import main + +# Root test data directory +TEST_DATA_DIR = Path(__file__).parent.parent / "test_data" + +# Regex for detecting password-like patterns in input configs. +# Intentionally conservative — only match patterns that netconan's password +# regexes would actually anonymize, to avoid false positives from Juniper +# policy "community" statements or "authentication-order password" lines. +_PASSWORD_PATTERNS = re.compile( + r"(?i)(" + r"\bpassword\s+['\"\$\d]\S*" + r"|\bsecret\s+['\"\$\d]\S*" + r"|\bsnmp\S*\s+.*\bcommunity\s+\S" + r"|\bpre-shared-key\s+\S" + r"|\bset password ENC\s+\S" + r"|\bset psksecret\s+\S" + r"|\bmd5\s+\d" + r")" +) + +# Regex for extracting IPv4 addresses +_IP_PATTERN = re.compile(r"\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b") + +# Extensions to skip (binary or non-config files) +_SKIP_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".pdf", ".gz", ".tar", ".zip"} + +# Max file size to process (10 MB) +_MAX_FILE_SIZE = 10 * 1024 * 1024 + + +def discover_test_configs(): + """Scan tests/test_data/ and return (vendor, source, filepath) tuples.""" + if not TEST_DATA_DIR.is_dir(): + return [] + configs = [] + for vendor_dir in sorted(TEST_DATA_DIR.iterdir()): + if not vendor_dir.is_dir() or vendor_dir.name.startswith("."): + continue + vendor = vendor_dir.name + for source_dir in sorted(vendor_dir.iterdir()): + if not source_dir.is_dir() or source_dir.name.startswith("."): + continue + source = source_dir.name + for config_file in sorted(source_dir.iterdir()): + if not config_file.is_file(): + continue + if config_file.suffix.lower() in _SKIP_EXTENSIONS: + continue + if config_file.name.startswith("."): + continue + if config_file.stat().st_size > _MAX_FILE_SIZE: + continue + configs.append((vendor, source, config_file)) + return configs + + +def has_password_patterns(content): + """Check if content contains password-like patterns.""" + return bool(_PASSWORD_PATTERNS.search(content)) + + +def extract_ip_addresses(content): + """Extract all IPv4 addresses from content.""" + return set(_IP_PATTERN.findall(content)) + + +def _is_readable_text(filepath): + """Check if a file is readable as text.""" + try: + with open(filepath, "r", encoding="utf-8", errors="strict") as f: + f.read(1024) + return True + except (UnicodeDecodeError, PermissionError): + return False + + +# Collect test configs at import time for parametrize +_TEST_CONFIGS = discover_test_configs() + +# Skip the entire module if no test data exists +pytestmark = pytest.mark.skipif( + not _TEST_CONFIGS, + reason="No test configs found — run: python tools/download_test_configs.py", +) + + +def _config_id(param): + """Generate readable test ID from parametrize tuple.""" + vendor, source, filepath = param + return f"{vendor}/{source}/{filepath.name}" + + +@pytest.fixture +def output_dir(tmp_path): + """Provide a temporary output directory.""" + return tmp_path / "output" + + +@pytest.mark.parametrize( + "vendor,source,config_path", + _TEST_CONFIGS, + ids=[_config_id(c) for c in _TEST_CONFIGS], +) +def test_no_crash(vendor, source, config_path, output_dir): + """Netconan should process the file without raising exceptions.""" + if not _is_readable_text(config_path): + pytest.skip("Not a readable text file") + + output_file = output_dir / config_path.name + os.makedirs(output_dir, exist_ok=True) + + main( + [ + "-i", + str(config_path), + "-o", + str(output_file), + "-a", + "-p", + "-s", + "TESTSALT", + ] + ) + + assert output_file.exists(), f"Output file was not created: {output_file}" + assert output_file.stat().st_size > 0, f"Output file is empty: {output_file}" + + +@pytest.mark.parametrize( + "vendor,source,config_path", + _TEST_CONFIGS, + ids=[_config_id(c) for c in _TEST_CONFIGS], +) +def test_passwords_anonymized(vendor, source, config_path, output_dir): + """If input has password patterns, output should show anonymization markers.""" + if not _is_readable_text(config_path): + pytest.skip("Not a readable text file") + + try: + input_text = config_path.read_text(encoding="utf-8", errors="replace") + except Exception: + pytest.skip("Could not read input file") + + if not has_password_patterns(input_text): + pytest.skip("No password patterns detected in input") + + output_file = output_dir / config_path.name + os.makedirs(output_dir, exist_ok=True) + + main( + [ + "-i", + str(config_path), + "-o", + str(output_file), + "-a", + "-p", + "-s", + "TESTSALT", + ] + ) + + output_text = output_file.read_text(encoding="utf-8", errors="replace") + + # At minimum, the output should differ from input when passwords are present + assert ( + output_text != input_text + ), f"Output is identical to input despite password patterns in {config_path.name}" + + +@pytest.mark.parametrize( + "vendor,source,config_path", + _TEST_CONFIGS, + ids=[_config_id(c) for c in _TEST_CONFIGS], +) +def test_ips_anonymized(vendor, source, config_path, output_dir): + """If input has IP addresses, some should be changed in output.""" + if not _is_readable_text(config_path): + pytest.skip("Not a readable text file") + + try: + input_text = config_path.read_text(encoding="utf-8", errors="replace") + except Exception: + pytest.skip("Could not read input file") + + input_ips = extract_ip_addresses(input_text) + # Filter out common non-routable/mask IPs that netconan preserves by default + trivial_ips = {"0.0.0.0", "255.255.255.255", "255.255.255.0", "127.0.0.1"} + meaningful_ips = input_ips - trivial_ips + if len(meaningful_ips) < 2: + pytest.skip("Not enough non-trivial IP addresses in input") + + output_file = output_dir / config_path.name + os.makedirs(output_dir, exist_ok=True) + + main( + [ + "-i", + str(config_path), + "-o", + str(output_file), + "-a", + "-p", + "-s", + "TESTSALT", + ] + ) + + output_text = output_file.read_text(encoding="utf-8", errors="replace") + output_ips = extract_ip_addresses(output_text) + + # At least some IPs should have changed + assert ( + input_ips != output_ips + ), f"IP addresses unchanged in {config_path.name}: {meaningful_ips}" diff --git a/tools/download_test_configs.py b/tools/download_test_configs.py new file mode 100644 index 0000000..cc12ce8 --- /dev/null +++ b/tools/download_test_configs.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +"""Download real-world network configs from public GitHub repos for testing. + +Downloads configs into tests/test_data/{vendor}/{source}/ for use with +the integration test suite in tests/integration/test_real_configs.py. + +Usage: + python tools/download_test_configs.py # all vendors + python tools/download_test_configs.py --vendors cisco arista + python tools/download_test_configs.py --force # re-download all +""" + +import argparse +import json +import logging +import os +import shutil +import subprocess +import sys +import tempfile +import urllib.request + +logging.basicConfig( + format="%(levelname)s %(message)s", + level=logging.INFO, +) +logger = logging.getLogger(__name__) + +# Root of the netconan project +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TEST_DATA_DIR = os.path.join(PROJECT_ROOT, "tests", "test_data") +METADATA_FILE = os.path.join(TEST_DATA_DIR, ".download_metadata.json") + +# Batfish base paths inside the repo +_BF_GRAMMAR = "projects/batfish/src/test/resources/org/batfish/grammar" +_BF_VENDOR = "projects/batfish/src/test/resources/org/batfish/vendor" + +SOURCES = { + "cisco": [ + { + "name": "batfish", + "type": "git_sparse", + "repo": "https://github.com/batfish/batfish.git", + "paths": [f"{_BF_GRAMMAR}/cisco/testconfigs"], + "src_dir": f"{_BF_GRAMMAR}/cisco/testconfigs", + }, + { + "name": "ciscoconfparse", + "type": "git_sparse", + "repo": "https://github.com/mpenning/ciscoconfparse.git", + "paths": ["tests/fixtures/configs"], + "src_dir": "tests/fixtures/configs", + "glob": "*.ios", + }, + ], + "arista": [ + { + "name": "batfish", + "type": "git_sparse", + "repo": "https://github.com/batfish/batfish.git", + "paths": [f"{_BF_VENDOR}/arista/grammar/testconfigs"], + "src_dir": f"{_BF_VENDOR}/arista/grammar/testconfigs", + }, + ], + "juniper_flat": [ + { + "name": "jcoeder", + "type": "git_clone", + "repo": "https://github.com/jcoeder/juniper-configurations.git", + "glob": "*.conf", + }, + ], + "juniper_hierarchical": [ + { + "name": "batfish", + "type": "git_sparse", + "repo": "https://github.com/batfish/batfish.git", + "paths": [f"{_BF_GRAMMAR}/juniper/testconfigs"], + "src_dir": f"{_BF_GRAMMAR}/juniper/testconfigs", + }, + ], + "fortinet": [ + { + "name": "batfish", + "type": "git_sparse", + "repo": "https://github.com/batfish/batfish.git", + "paths": [f"{_BF_GRAMMAR}/fortios/testconfigs"], + "src_dir": f"{_BF_GRAMMAR}/fortios/testconfigs", + }, + { + "name": "azure", + "type": "url", + "url": ( + "https://raw.githubusercontent.com/Azure/" + "Azure-vpn-config-samples/master/Fortinet/Current/" + "fortigate_show%20full-configuration.txt" + ), + "filename": "fortigate_full.txt", + }, + ], +} + + +def load_metadata(): + """Load download metadata from disk.""" + if os.path.exists(METADATA_FILE): + with open(METADATA_FILE) as f: + return json.load(f) + return {} + + +def save_metadata(metadata): + """Save download metadata to disk.""" + os.makedirs(os.path.dirname(METADATA_FILE), exist_ok=True) + with open(METADATA_FILE, "w") as f: + json.dump(metadata, f, indent=2, sort_keys=True) + + +def source_key(vendor, source): + """Return a unique key for a vendor/source pair.""" + return f"{vendor}/{source['name']}" + + +def is_downloaded(metadata, vendor, source): + """Check if a source has already been downloaded.""" + key = source_key(vendor, source) + return key in metadata + + +def copy_files(src_dir, dest_dir, glob_pattern=None): + """Copy files from src_dir to dest_dir, optionally filtering by glob.""" + os.makedirs(dest_dir, exist_ok=True) + count = 0 + for entry in os.listdir(src_dir): + src_path = os.path.join(src_dir, entry) + if not os.path.isfile(src_path): + continue + if glob_pattern and not _matches_glob(entry, glob_pattern): + continue + shutil.copy2(src_path, os.path.join(dest_dir, entry)) + count += 1 + return count + + +def _matches_glob(filename, pattern): + """Simple glob matching for *.ext patterns.""" + if pattern.startswith("*."): + return filename.endswith(pattern[1:]) + return True + + +def download_git_sparse(source, tmp_dir, dest_dir): + """Download files using git sparse checkout.""" + repo_dir = os.path.join(tmp_dir, "repo") + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "--filter=blob:none", + "--sparse", + source["repo"], + repo_dir, + ], + check=True, + capture_output=True, + text=True, + ) + subprocess.run( + ["git", "sparse-checkout", "set"] + source["paths"], + cwd=repo_dir, + check=True, + capture_output=True, + text=True, + ) + src_dir = os.path.join(repo_dir, source["src_dir"]) + if not os.path.isdir(src_dir): + raise FileNotFoundError(f"Source directory not found: {src_dir}") + return copy_files(src_dir, dest_dir, source.get("glob")) + + +def download_git_clone(source, tmp_dir, dest_dir): + """Download files by cloning a full repo (shallow).""" + repo_dir = os.path.join(tmp_dir, "repo") + subprocess.run( + ["git", "clone", "--depth", "1", source["repo"], repo_dir], + check=True, + capture_output=True, + text=True, + ) + return copy_files(repo_dir, dest_dir, source.get("glob")) + + +def download_url(source, dest_dir): + """Download a single file from a URL.""" + os.makedirs(dest_dir, exist_ok=True) + dest_path = os.path.join(dest_dir, source["filename"]) + urllib.request.urlretrieve(source["url"], dest_path) + return 1 + + +def download_source(vendor, source, force=False): + """Download a single source. Returns (count, error_or_None).""" + key = source_key(vendor, source) + dest_dir = os.path.join(TEST_DATA_DIR, vendor, source["name"]) + + if not force and os.path.isdir(dest_dir) and os.listdir(dest_dir): + logger.info(" [skip] %s — already exists", key) + return 0, None + + logger.info(" [download] %s ...", key) + + if source["type"] == "url": + count = download_url(source, dest_dir) + return count, None + + with tempfile.TemporaryDirectory(prefix="netconan_dl_") as tmp_dir: + if source["type"] == "git_sparse": + count = download_git_sparse(source, tmp_dir, dest_dir) + elif source["type"] == "git_clone": + count = download_git_clone(source, tmp_dir, dest_dir) + else: + raise ValueError(f"Unknown source type: {source['type']}") + return count, None + + +def main(argv=None): + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Download real-world network configs for testing." + ) + parser.add_argument( + "--vendors", + nargs="+", + choices=sorted(SOURCES.keys()), + default=None, + help="Only download configs for these vendors (default: all)", + ) + parser.add_argument( + "--force", + action="store_true", + help="Re-download even if files already exist", + ) + args = parser.parse_args(argv) + + vendors = args.vendors or sorted(SOURCES.keys()) + metadata = load_metadata() + total_files = 0 + errors = [] + + for vendor in vendors: + sources = SOURCES.get(vendor, []) + if not sources: + logger.warning("No sources defined for vendor: %s", vendor) + continue + logger.info("Vendor: %s", vendor) + for src in sources: + try: + count, err = download_source(vendor, src, force=args.force) + if err: + errors.append((source_key(vendor, src), err)) + else: + total_files += count + metadata[source_key(vendor, src)] = { + "type": src["type"], + "repo": src.get("repo", src.get("url", "")), + } + except Exception as e: + key = source_key(vendor, src) + logger.error(" [error] %s: %s", key, e) + errors.append((key, str(e))) + + save_metadata(metadata) + + logger.info("") + logger.info("Done: %d files downloaded", total_files) + if errors: + logger.error("%d source(s) failed:", len(errors)) + for key, err in errors: + logger.error(" %s: %s", key, err) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 5bc5376d9a3fa9ddac45e6b8cc0f465ab5712b07 Mon Sep 17 00:00:00 2001 From: Manon Goo Date: Mon, 23 Mar 2026 10:45:46 +0000 Subject: [PATCH 4/6] Add tests README and support for local test configs Add tests/README.md with quick-start guide covering how to download test data, run integration tests, add your own local configs, and add/remove download sources. Add tests/test_data_local/ as a second scan directory so users can drop in their own config files without mixing with downloaded data. Both directories are git-ignored. --- .gitignore | 3 + tests/README.md | 111 +++++++++++++++++++++++++ tests/integration/test_real_configs.py | 30 ++++--- 3 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 tests/README.md diff --git a/.gitignore b/.gitignore index 2502366..46d4d3b 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,6 @@ working/ # downloaded test configs (see tests/TEST_DATA_SOURCES.md) tests/test_data/ + +# local test configs (your own configs, see tests/README.md) +tests/test_data_local/ diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..a603e50 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,111 @@ +# Tests + +## Test Structure + +``` +tests/ + unit/ # Fast unit tests (always run) + end_to_end/ # End-to-end tests with inline data + integration/ # Real-config integration tests (need downloaded data) + test_data/ # Downloaded configs (git-ignored) + test_data_local/ # Your own configs (git-ignored) + TEST_DATA_SOURCES.md # Detailed list of public config sources +``` + +## Running Tests + +```bash +# Unit + end-to-end tests only (no downloads needed) +python -m pytest --override-ini="addopts=" -x -q + +# Integration tests only +python -m pytest --override-ini="addopts=" tests/integration/ -v + +# Everything together +python -m pytest --override-ini="addopts=" -v +``` + +Integration tests skip automatically if no test data is present. + +## Downloading Test Data + +The download script fetches real-world configs from public GitHub repos: + +```bash +# Download all vendors (~600 files) +python tools/download_test_configs.py + +# Download specific vendors only +python tools/download_test_configs.py --vendors cisco juniper_flat + +# Force re-download (overwrite existing files) +python tools/download_test_configs.py --force +``` + +Files go into `tests/test_data/{vendor}/{source}/`. This directory is +git-ignored. + +## Using Your Own Local Configs + +Place your own config files in `tests/test_data_local/` using the same +`{vendor}/{source}/` structure: + +```bash +mkdir -p tests/test_data_local/cisco/my_lab +cp ~/my-router.cfg tests/test_data_local/cisco/my_lab/ + +mkdir -p tests/test_data_local/juniper_flat/office +cp ~/srx-config.txt tests/test_data_local/juniper_flat/office/ +``` + +The integration tests scan both `test_data/` and `test_data_local/` +automatically. The vendor and source names are free-form — use whatever +makes sense for your files. Both directories are git-ignored so your +configs are never committed. + +## Adding a Download Source + +Edit the `SOURCES` dict in `tools/download_test_configs.py`. Each vendor +maps to a list of source entries. Three types are supported: + +```python +# git sparse checkout (for large repos where you only need one directory) +{ + "name": "my_source", + "type": "git_sparse", + "repo": "https://github.com/org/repo.git", + "paths": ["path/to/configs"], # sparse-checkout paths + "src_dir": "path/to/configs", # directory to copy files from + "glob": "*.cfg", # optional file filter +}, + +# git clone (for small repos) +{ + "name": "my_source", + "type": "git_clone", + "repo": "https://github.com/org/repo.git", + "glob": "*.conf", # optional file filter +}, + +# single file URL +{ + "name": "my_source", + "type": "url", + "url": "https://example.com/config.txt", + "filename": "config.txt", # name for the saved file +}, +``` + +After adding, run `python tools/download_test_configs.py --force` to fetch. + +## Removing a Download Source + +Delete the source entry from the `SOURCES` dict in +`tools/download_test_configs.py`, then delete its local directory: + +```bash +rm -rf tests/test_data/{vendor}/{source} +``` + +The metadata file (`tests/test_data/.download_metadata.json`) will be +updated on the next download run. diff --git a/tests/integration/test_real_configs.py b/tests/integration/test_real_configs.py index e8e1d69..4c9fbe7 100644 --- a/tests/integration/test_real_configs.py +++ b/tests/integration/test_real_configs.py @@ -1,16 +1,20 @@ """Integration tests that run netconan against real-world config files. -Discovers configs downloaded into tests/test_data/{vendor}/{source}/ and -runs netconan against each, verifying: +Discovers configs in two directories: + - tests/test_data/{vendor}/{source}/ — downloaded by the download script + - tests/test_data_local/{vendor}/{source}/ — your own local configs + +Runs netconan against each file and verifies: - No crashes (no unhandled exceptions) - Output file is produced and non-empty - Password-like patterns are anonymized when present in input - IP addresses are changed when present in input -Requires test data to be downloaded first: - python tools/download_test_configs.py +Requires test data to be downloaded or placed manually: + python tools/download_test_configs.py # downloaded data + tests/test_data_local/{vendor}/{source}/ # your own configs -The entire suite is skipped if tests/test_data/ does not exist. +The entire suite is skipped if neither directory contains config files. """ import os @@ -21,8 +25,9 @@ from netconan.netconan import main -# Root test data directory +# Test data directories (both are scanned for configs) TEST_DATA_DIR = Path(__file__).parent.parent / "test_data" +TEST_DATA_LOCAL_DIR = Path(__file__).parent.parent / "test_data_local" # Regex for detecting password-like patterns in input configs. # Intentionally conservative — only match patterns that netconan's password @@ -50,12 +55,12 @@ _MAX_FILE_SIZE = 10 * 1024 * 1024 -def discover_test_configs(): - """Scan tests/test_data/ and return (vendor, source, filepath) tuples.""" - if not TEST_DATA_DIR.is_dir(): +def _scan_data_dir(data_dir): + """Scan a test data directory and return (vendor, source, filepath) tuples.""" + if not data_dir.is_dir(): return [] configs = [] - for vendor_dir in sorted(TEST_DATA_DIR.iterdir()): + for vendor_dir in sorted(data_dir.iterdir()): if not vendor_dir.is_dir() or vendor_dir.name.startswith("."): continue vendor = vendor_dir.name @@ -76,6 +81,11 @@ def discover_test_configs(): return configs +def discover_test_configs(): + """Scan test_data/ and test_data_local/ for config files.""" + return _scan_data_dir(TEST_DATA_DIR) + _scan_data_dir(TEST_DATA_LOCAL_DIR) + + def has_password_patterns(content): """Check if content contains password-like patterns.""" return bool(_PASSWORD_PATTERNS.search(content)) From 8b02ec5891756759e4f1706f2359c610f4021dec Mon Sep 17 00:00:00 2001 From: Manon Goo Date: Mon, 23 Mar 2026 10:55:51 +0000 Subject: [PATCH 5/6] Fix flake8 D104: add docstring to integration __init__.py --- tests/integration/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index e69de29..f92c4c0 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests using real-world network config files.""" From 76beada533a76a6055776660711f799b78ac532d Mon Sep 17 00:00:00 2001 From: Dan Halperin Date: Mon, 23 Mar 2026 12:51:13 -0700 Subject: [PATCH 6/6] Fix ruff format in test_real_configs.py Reformat assert message parenthesization to match ruff's preferred style after merging origin/master. --- tests/integration/test_real_configs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_real_configs.py b/tests/integration/test_real_configs.py index 4c9fbe7..14308b6 100644 --- a/tests/integration/test_real_configs.py +++ b/tests/integration/test_real_configs.py @@ -195,9 +195,9 @@ def test_passwords_anonymized(vendor, source, config_path, output_dir): output_text = output_file.read_text(encoding="utf-8", errors="replace") # At minimum, the output should differ from input when passwords are present - assert ( - output_text != input_text - ), f"Output is identical to input despite password patterns in {config_path.name}" + assert output_text != input_text, ( + f"Output is identical to input despite password patterns in {config_path.name}" + ) @pytest.mark.parametrize( @@ -242,6 +242,6 @@ def test_ips_anonymized(vendor, source, config_path, output_dir): output_ips = extract_ip_addresses(output_text) # At least some IPs should have changed - assert ( - input_ips != output_ips - ), f"IP addresses unchanged in {config_path.name}: {meaningful_ips}" + assert input_ips != output_ips, ( + f"IP addresses unchanged in {config_path.name}: {meaningful_ips}" + )