Skip to main content

Network Security & Compliance Auditor — automated SSH baseline checks to detect and prevent configuration drift

Project description

Network Security & Compliance Auditor

License: MIT Python 3.12+ CI Release PyPI PyPI - Python Version

┌─────────────────────────────────────────────────────────────────┐
│                    AUDNET ARCHITECTURE                       │
│
│  ┌──────────┐    ┌──────────────┐    ┌────────────────────┐    │
│  │  YAML     │───▶│  Collector   │───▶│  TextFSM Parser    │    │
│  │  Inventory│    │  (Netmiko +  │    │  (CLI → JSON)      │    │
│  │  +Baseline│    │   ThreadPool)│    └────────┬───────────┘    │
│  └──────────┘    └──────┬───────┘             │                │
│                         │                      ▼                │
│                         │            ┌────────────────────┐    │
│                         │            │  Compliance Engine  │    │
│                         │            │  (4 Security Rules) │    │
│                         │                     │                │
│                         ▼                     ▼                │
│              ┌──────────────────┐   ┌────────────────────┐    │
│              │  DeviceSnapshot  │──▶│  Report Generator   │    │
│              │  (Pydantic)      │   │  (Jinja2 → MD/HTML) │    │
│              └──────────────────┘   └────────────────────┘    │
│                                              │                │
│                                              ▼                │
│                                    ┌────────────────────┐    │
│                                    │  audit_report.md   │    │
│                                    │  audit_report.html │    │
│                                    └────────────────────┘    │
│                                                                 │
│  Parallel SSH ──▶ 4 devices concurrently (configurable)        │
│  All layers independently testable — no real hardware needed   │
└─────────────────────────────────────────────────────────────────┘

Problem Statement

In production networks, configuration drift is inevitable. Engineers make manual changes that bypass security baselines — enabling SSHv1, leaving switchports on default VLANs, or pointing NTP/syslog to unauthorized servers. Traditional auditing is manual, error-prone, and doesn't scale.

audnet solves this by automating SSH-based compliance audits against security baselines. This detects drift in real-time and prevents future drift by enforcing hardened policies.

Solution

A Python CLI tool that:

  1. Connects in parallel to multiple routers/switches via SSH (Netmiko + ThreadPool, with retries)
  2. Pulls live stateshow ip interface brief, show version, show running-config
  3. Parses unstructured CLI into clean JSON using TextFSM templates
  4. Audits against baselines — flags SSHv1, unauthorized VLANs, rogue NTP/syslog servers
  5. Generates reports — professional Markdown and HTML with pass/fail summaries
  6. Supports filters & JSON for targeted runs and CI integration

Every layer is independently testable with mocked responses — no real network hardware required.

Installation

Quick install (end users)

# From PyPI (recommended for production)
pip install audnet

# Or with uv
uv tool install audnet

# From source (latest development version)
pip install git+https://github.com/islam666/Audnet.git

# Verify
audnet --version

Development setup (contributors)

Prerequisites

  • Python 3.12+
  • uv package manager
  • Linux/macOS environment

Step-by-step setup

# 1. Clone the repository
git clone https://github.com/islam666/Audnet.git
cd audnet

# 2. Install dependencies (uses uv.lock for reproducible installs)
uv venv
uv pip install -e ".[dev]"

# 3. Activate virtual environment
source .venv/bin/activate

# 4. Install pre-commit hooks
pre-commit install

# 5. Verify installation
python -c "import audnet; print(audnet.__version__)"
# Expected: 0.1.1

# 6. Run the test suite
pytest tests/ -v
# Expected: 200+ passed

uv pip install -e ".[dev]" reads the committed uv.lock to install the exact same dependency versions across all environments. Use uv lock (no args) to regenerate the lockfile after adding new dependencies.

Quick start

# Dry run against the sample inventory — no SSH connections made
audnet audit --dry-run

# Run a real audit against your devices
audnet audit --inventory inventories/devices.yaml --baseline baselines/security_baseline.yaml

Configure device inventory

Edit inventories/devices.yaml with your network devices:

defaults:
  device_type: cisco_ios
  port: 22

devices:
  - name: core-router-01
    host: 192.168.1.1
    username: admin
    password: "${AUDNET_PASSWORD}"  # resolved from environment

Set the password via environment variable:

export AUDNET_PASSWORD="your-secret-password"

SSH key-based authentication

Instead of password authentication, use SSH keys:

devices:
  - name: core-router-01
    host: 192.168.1.1
    username: admin
    use_keys: true
    key_file: ~/.ssh/id_ed25519
  • use_keys: true — enable SSH key authentication
  • key_file — path to the private key file (optional; uses SSH agent or default keys if omitted)

Customize security baseline

Edit baselines/security_baseline.yaml to match your organization's policies:

checks:
  ssh_version:
    severity: critical
    rule: ssh_v2_only

  inactive_ports:
    severity: high
    rule: no_open_ports
    allowed_vlans: [10, 20, 30]  # your secure VLANs

  ntp_config:
    severity: medium
    rule: ntp_approved
    approved_servers:
      - 10.0.0.50

  syslog_config:
    severity: medium
    rule: syslog_approved
    approved_servers:
      - 10.0.0.60

Usage

Run a full audit

source .venv/bin/activate
audnet audit \
  --inventory inventories/devices.yaml \
  --baseline baselines/security_baseline.yaml \
  --output audit_report \
  --format both \
  --workers 4

Advanced usage (new in this release)

Filter to one device or specific checks, output JSON for scripting:

audnet audit --device core-router-01 --check ssh_v2_only,ntp_config --json

Async mode (recommended for >20 devices)

The asyncio-based collector uses asyncssh for lower memory overhead and better scalability. It is recommended for audits involving more than 20 devices.

audnet audit --async

Trade-offs vs the default sync collector:

Sync (default) Async (--async)
Dependency Netmiko asyncssh
Concurrency ThreadPool asyncio Semaphore
Best for <20 devices >20 devices
Memory per connection Higher (thread stack) Lower (coroutine)

Usage Examples

All examples below use the default inventory and baseline paths. Adjust as needed.

Audit a single device

audnet audit --device core-router-01

Run specific checks only

Using comma-separated values in a single --check:

audnet audit --check ssh_v2_only,ntp_config

Or repeat the flag:

audnet audit --check ssh_v2_only --check ntp_config

JSON output for CI/CD pipelines

audnet audit --json

Example output:

[
  {
    "device_name": "core-router-01",
    "overall_pass": true,
    "checks": [
      {"check_name": "ssh_v2_only", "passed": true, "detail": "SSHv2 configured"},
      {"check_name": "ntp_approved", "passed": false, "detail": "unauthorized NTP: 10.0.0.99"}
    ]
  }
]

Pipe to jq for targeted queries:

audnet audit --json | jq '.[] | select(.overall_pass == false) | .device_name'

Dry-run mode

Validate your config without touching devices:

audnet audit --dry-run

Combine with filters to preview a targeted run:

audnet audit --dry-run --device core-router-01 --check ssh_v2_only

Strict mode for CI

Fail immediately if any device has a plaintext password (no ${ENV_VAR} reference). Checks password, secret, passwd, and token fields:

audnet audit --strict

Without --strict, a warning is logged instead of failing.

Verbose debug logging

audnet audit -v --dry-run

Combined: single device, specific check, JSON, strict

audnet audit --device core-router-01 --check ssh_v2_only --json --strict

Allow compliance failures without non-zero exit

By default, audnet exits with code 1 when compliance checks fail. Use --no-fail to always exit with code 0 (useful when you want the report but don't want CI to break):

audnet audit --no-fail

Show version

audnet --version

Sample Output

$ audnet audit --inventory inventories/devices.yaml
[INFO] Loaded 2 devices from inventory
[INFO] Connecting in parallel (workers=4)...
core-router-01: ✓ passed (4/4 checks)
dist-switch-02: ✗ failed (SSHv1 enabled, Gi0/3 on unauthorized VLAN 1)

Report: audit_report.md + audit_report.html generated.
Summary: 1 passed, 1 with issues.

CLI options

Option Default Description
--inventory inventories/devices.yaml Device inventory YAML path
--baseline baselines/security_baseline.yaml Security baseline YAML path
--output audit_report Output file prefix
--format both Output format: md, html, or both
--workers 4 Max parallel SSH connections
--device (all) Filter to single device by name
--check (all) Filter to specific checks (repeatable; comma-separated)
--json false Output JSON summary to stdout
--dry-run, -n false Validate config without connecting to devices
--strict false
--no-fail false
-v, --verbose false
--version
--async false
--connect-timeout 10

Dry-run mode

Use --dry-run (or -n) to validate your inventory and baseline and preview what would be audited — no SSH connections made:

audnet audit --inventory inventories/devices.yaml --dry-run

Output:

audnet v0.1.0 — Starting audit...
Loaded 2 devices, 4 checks
DRY RUN — no device connections will be made
Devices that would be audited:
  • core-router-01 (192.168.1.1) — cisco_ios
  • dist-switch-02 (192.168.1.2) — cisco_ios
Checks that would be run:
  • inactive_ports
  • ntp_config
  • ssh_v2_only
  • syslog_config
Dry run complete — config and baseline are valid

Combine with --device and --check to filter the preview:

audnet audit --dry-run --device core-router-01 --check ssh_v2_only

Output

The tool produces:

  • Terminal summary — Rich table with per-device pass/fail status
  • audit_report.md — Markdown report with detailed findings table
  • audit_report.html — Styled HTML report for sharing
  • JSON (with --json) — Machine-readable for CI/CD

Project Structure

audnet/
├── pyproject.toml              # Build config, dependencies, pytest/ruff settings
├── CHANGELOG.md                # Release history (Keep a Changelog format)
├── CONTRIBUTING.md             # Development guidelines, testing, PR workflow
├── LICENSE                     # MIT License
├── README.md                   # This file
├── SECURITY.md                 # Security policy, credential handling, disclosure
├── uv.lock                     # Reproducible dependency lockfile
├── .pre-commit-config.yaml     # Pre-commit hooks (ruff, mypy, bandit, etc.)
├── benchmarks/
│   └── bench_collectors.py     # Sync vs async collector performance benchmarks
├── inventories/
│   └── devices.yaml            # Sample device inventory
├── baselines/
│   └── security_baseline.yaml  # Compliance rules configuration
├── src/audnet/
│   ├── __init__.py             # Package init, version
│   ├── cli.py                  # Typer CLI entry point
│   ├── config.py               # YAML inventory/baseline loader with env resolution
│   ├── models.py               # Pydantic data models (incl. SecurityBaseline)
│   ├── exceptions.py           # Structured exception hierarchy
│   ├── vendor_registry.py      # Vendor registry for multi-vendor dispatch
│   ├── collector.py            # Parallel SSH collector (Netmiko + ThreadPool + retries)
│   ├── collector_async.py      # Asyncio collector (asyncssh + semaphore concurrency)
│   ├── parser.py               # TextFSM parser (CLI → structured JSON, vendor-aware)
│   ├── compliance.py           # Rule engine (4 security checks, vendor-pattern overrides)
│   ├── reporter.py             # Jinja2 report generator (Markdown + HTML)
│   ├── templates/
│   │   ├── __init__.py
│   │   ├── audit_report.md.j2  # Markdown report template
│   │   └── audit_report.html.j2 # HTML report template
│   └── textfsm_templates/
│       ├── __init__.py
│       ├── cisco_ios_show_ip_interface_brief.textfsm
│       ├── cisco_ios_show_version.textfsm
│       ├── cisco_ios_show_running_config.textfsm
│       ├── cisco_ios_show_interface_status.textfsm
│       └── cisco_ios_show_cdp_neighbors_detail.textfsm
└── tests/
    ├── __init__.py
    ├── conftest.py             # Shared pytest fixtures
    ├── test_models.py          # Device, ComplianceResult, AuditReport
    ├── test_config.py          # Inventory loading, env resolution
    ├── test_collector.py       # SSH collection, error handling, vendor dispatch
    ├── test_collector_async.py # Async collector: success, auth failure, timeout, mixed
    ├── test_parser.py          # TextFSM parsing, vendor-aware template selection
    ├── test_compliance.py      # All 4 rule types (pass/fail), case-insensitive
    ├── test_reporter.py        # Markdown/HTML rendering
    ├── test_vendor_registry.py # Vendor profiles, dispatch, registration
    ├── test_exceptions.py      # Exception hierarchy and inheritance
    ├── test_integration.py     # End-to-end: compliant, noncompliant, partial
    ├── test_logging.py         # Structlog configuration and secret redaction
    └── test_version.py         # Version string format and accessibility

Multi-Vendor Support

audnet uses a vendor registry/dispatch pattern (similar to NAPALM/Nornir driver architecture) for multi-vendor support. Device types are resolved automatically, with Cisco IOS as the fallback default.

Supported vendors

Vendor device_type Template prefix
Cisco IOS/IOS-XE cisco_ios cisco_ios
Cisco NX-OS cisco_nxos cisco_nxos
Arista EOS arista_eos arista_eos

Unknown device types fall back to cisco_ios commands and templates.

Configuring devices for different vendors

Set device_type per-device or as a default in your inventory YAML:

defaults:
  device_type: cisco_ios

devices:
  - name: core-router-01
    host: 192.168.1.1
    username: admin
    password: "${AUDNET_PASSWORD}"

  - name: nexus-switch-01
    host: 192.168.1.2
    device_type: cisco_nxos
    username: admin
    password: "${AUDNET_PASSWORD}"

  - name: arista-leaf-01
    host: 192.168.1.3
    device_type: arista_eos
    username: admin
    password: "${AUDNET_PASSWORD}"

Adding a new vendor

Adding support for a new network OS takes three steps. No changes to parser, collector, or compliance code are needed — the vendor registry pattern handles dispatch automatically.

Step 1: Add TextFSM templates

Create one template per data slot in textfsm_templates/. The naming convention is <prefix>_<slot_suffix>.textfsm, where the suffix matches the slot names used by the built-in vendors:

Slot Purpose Example suffix
show_ip_interface_brief Interface status show_ip_interface_brief
show_version Device version/info show_version
show_running_config Full running config show_running_config

For example, to add Juniper JunOS:

textfsm_templates/
├── juniper_junos_show_ip_interface_brief.textfsm
├── juniper_junos_show_version.textfsm
└── juniper_junos_show_running_config.textfsm

Each template should parse the vendor's equivalent CLI output into the same column names the compliance engine expects (e.g., INTERFACE, IP_ADDRESS, STATUS, PROTOCOL for interfaces).

Tip: Use the TextFSM CLI tool to interactively test templates against sample output before committing.

Step 2: Register the vendor

You have two options — static registration (recommended for built-in vendors) or runtime registration (for plugins or dynamic use).

Option A: Static registration — add to VENDOR_PROFILES in src/audnet/vendor_registry.py:

VENDOR_PROFILES["juniper_junos"] = _profile(
    commands=[
        "show interfaces terse",
        "show version",
        "show configuration",
    ],
    prefix="juniper_junos",
    description="Juniper JunOS",
)

The commands list must have exactly three entries matching the three slots above (interface brief, version, running config). The prefix must match the TextFSM template filename prefix.

Option B: Runtime registration — call register_vendor() from your code or a plugin:

from audnet.vendor_registry import register_vendor

register_vendor(
    device_type="juniper_junos",
    commands=["show interfaces terse", "show version", "show configuration"],
    template_prefix="juniper_junos",
)

Runtime registration is useful for plugins, tests, or adding vendors without modifying the audnet source.

Step 3: (Optional) Add vendor-specific compliance patterns

If the vendor uses different CLI syntax for the same security concepts, add vendor_patterns to your baseline YAML:

checks:
  ssh_version:
    severity: critical
    rule: ssh_v2_only
    vendor_patterns:
      juniper_junos:
        match: "set system ssh"
        ok_value: "set system ssh protocol-v2"

The key under vendor_patterns must match the device_type used in the inventory. If no vendor-specific pattern is defined, the default pattern is used.

Step 4: Configure devices in inventory

Set the device_type on your devices to match the registered key:

devices:
  - name: juniper-router-01
    host: 192.168.1.10
    device_type: juniper_junos
    username: admin
    password: "${AUDNET_PASSWORD}"

That's it. The collector will automatically send the correct commands, the parser will load the correct templates, and the compliance engine will use the correct patterns.

Verifying your vendor

Run a dry-run to confirm the vendor is recognized:

audnet audit --device juniper-router-01 --dry-run

Then run a full audit and check the output:

audnet audit --device juniper-router-01 --json

How it works

  • vendor_registry.py maps device_type to CLI commands and TextFSM template prefixes
  • collector.py calls get_commands(device_type) instead of a hardcoded dict
  • parser.py calls get_template_name(device_type, slot) for dynamic template loading
  • compliance.py uses pattern-based matching with optional per-vendor overrides
  • All vendor resolution falls back to cisco_ios for unknown device types

Performance & Scalability

Current architecture: ThreadPool + Netmiko

The default collector (collector.py) uses concurrent.futures.ThreadPoolExecutor with Netmiko for SSH. This works well for small-to-medium inventories (up to ~20 devices) but has limitations at scale:

  • Thread overhead: Each concurrent connection consumes a thread (~8MB stack)
  • GIL contention: Python's GIL limits true parallelism for CPU-bound parsing
  • Memory: 100 devices × 4 threads = significant memory for thread stacks

Async prototype: asyncio + asyncssh

An async collector prototype is available at collector_async.py. It replaces threads with coroutines and uses asyncssh for SSH transport:

Aspect Sync (ThreadPool) Async (asyncio)
Concurrency model OS threads Coroutines
Memory per connection ~8MB (thread stack) ~1KB (coroutine)
Default max_workers 4 50
Scales to ~20-50 devices 100+ devices
Dependency Netmiko asyncssh

Running the benchmark

uv run python benchmarks/bench_collectors.py

This compares sync vs async collection across 4/8/16/32 devices with mocked SSH responses. Results are written to benchmarks/results.json.

Migration path

The async collector is a prototype — it produces identical DeviceSnapshot output and shares the same parser, compliance, and vendor registry code.

To switch to async collection when scaling beyond ~50 devices:

  1. Install asyncssh: uv add asyncssh
  2. Change the import in cli.py:
    # from audnet.collector import collect_all
    from audnet.collector_async import collect_all_async as collect_all
    
  3. The --workers flag maps to asyncio.Semaphore limit (default: 50)
  4. Keep the sync collector as fallback for environments without asyncssh

Future: Scrapli

For production async deployments, consider migrating from asyncssh to Scrapli which provides:

  • Built-in multi-vendor support (replacing Netmiko's device-type abstraction)
  • Both sync and async transports
  • Structured parsing (replacing TextFSM for some platforms)
  • Active community and regular updates

The vendor registry pattern in vendor_registry.py is already compatible — Scrapli would replace only the SSH transport layer in the collector.

Compliance Checks

Check Rule Severity What it detects
SSH Version ssh_v2_only Critical SSHv1 enabled or SSHv2 not configured
Inactive Ports no_open_ports High Switchports in unauthorized VLANs
NTP Config ntp_approved Medium NTP servers not in approved list
Syslog Config syslog_approved Medium Syslog servers not in approved list

Adding a new compliance rule

  1. Write a _check_your_rule(snapshot, config) -> ComplianceResult function in compliance.py
  2. Add it to the _RULE_DISPATCH dict
  3. Add the rule config to baselines/security_baseline.yaml
  4. Write tests in test_compliance.py

Contributing

See CONTRIBUTING.md for development setup, adding rules, testing, and PR workflow.

Testing

# Run all tests
pytest tests/ -v

# Run with coverage
pytest tests/ --cov=audnet --cov-report=term-missing

# Run specific test file
pytest tests/test_compliance.py -v

# Lint
ruff check src/ tests/

All tests use mocked device responses — no real SSH connections or network hardware needed.

Security

audnet takes credential handling seriously. Passwords are stored as SecretStr (Pydantic) and are never rendered in logs or output.

Quick Start: Environment Variables

Use ${ENV_VAR} placeholders in inventory files:

devices:
  - name: core-switch-01
    host: 10.0.0.1
    password: "${AUDNET_PASSWORD}"
export AUDNET_PASSWORD="your-secret"
audnet audit

Production: External Secret Stores

For production, use a dedicated secret manager instead of environment variables:

Store Example
HashiCorp Vault export AUDNET_PASSWORD=$(vault kv get ...)
AWS Secrets Mgr export AUDNET_PASSWORD=$(aws secretsmanager ...)
1Password CLI export AUDNET_PASSWORD=$(op read ...)
Python keyring keyring.set_password("audnet", ...)

See SECURITY.md for detailed integration examples.

Strict Mode (CI/CD)

Use --strict in CI pipelines to enforce that no plaintext passwords exist in inventory files:

audnet audit --strict

This fails with a ConfigError if any device has a password that is not a ${ENV_VAR} reference. Without --strict, a warning is logged instead.

SSH Key Authentication

Prefer SSH keys over passwords:

devices:
  - name: core-switch-01
    host: 10.0.0.1
    use_keys: true
    key_file: ~/.ssh/id_ed25519

Checklist

  • Never commit inventory files with plaintext passwords
  • Add inventories/*.yaml to .gitignore (commit only inventories/example.yaml)
  • Use .env for local development (add .env to .gitignore)
  • Use --strict in CI/CD
  • Prefer SSH key authentication
  • Rotate credentials regularly

See SECURITY.md for the full security policy, vulnerability reporting, and responsible disclosure.

Changelog

See CHANGELOG.md for a detailed history of changes, new features, and bug fixes.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

audnet-0.1.2.tar.gz (138.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

audnet-0.1.2-py3-none-any.whl (30.5 kB view details)

Uploaded Python 3

File details

Details for the file audnet-0.1.2.tar.gz.

File metadata

  • Download URL: audnet-0.1.2.tar.gz
  • Upload date:
  • Size: 138.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for audnet-0.1.2.tar.gz
Algorithm Hash digest
SHA256 d92e9d6bc27be26dbf7432fc898a7802671265968537b5762bb0dde5316aed82
MD5 380655b9142d667b084beec12f0e1141
BLAKE2b-256 3cbc5d65bc2e8ce57dc216d3a1ba3d8f442f76e8fbac2df8914120eeb3372e3d

See more details on using hashes here.

Provenance

The following attestation bundles were made for audnet-0.1.2.tar.gz:

Publisher: publish.yml on islam666/Audnet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file audnet-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: audnet-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 30.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for audnet-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 684e3de09f573250f27e8bc148db00f93ba7a14d923e9cec05939051f39ac1ec
MD5 bce0c2249eec5d8c5fab1dff8f006e19
BLAKE2b-256 7948eccb4f54fb5f010080bac3065924b2bad5ef7ce5bd898da65f4c514a53a2

See more details on using hashes here.

Provenance

The following attestation bundles were made for audnet-0.1.2-py3-none-any.whl:

Publisher: publish.yml on islam666/Audnet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page