Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
09ac5c5
feat(constants): add Colors class for ANSI terminal formatting
aitestino Jan 26, 2026
75985c7
feat(output): add pattern-based output formatter for semantic validation
aitestino Jan 26, 2026
7162ead
refactor(validator): integrate output formatter and add checklist sum…
aitestino Jan 26, 2026
0ff5259
feat(cli): add --list-rules option to display available validation rules
aitestino Jan 26, 2026
5a8070a
feat(cli): add JSON output format option for validation results
ChristopherJHart Jan 26, 2026
d1d6857
fix(formatter): apply colorization to multiple rich content blocks
aitestino Jan 26, 2026
547d6f2
feat(models): add structured data models for rule results
aitestino Jan 26, 2026
d711d41
refactor(formatter): rewrite output formatter for structured data
aitestino Jan 26, 2026
29b87b9
refactor(exceptions): simplify SemanticErrorResult error type
aitestino Jan 26, 2026
eadea11
refactor(validator): update semantic validation for structured results
aitestino Jan 26, 2026
168c26d
test(formatter): add unit tests for format_json_result
aitestino Jan 26, 2026
06535a3
test(integration): verify error string format in JSON output
aitestino Jan 26, 2026
e057011
style: fix type annotations and code formatting
aitestino Jan 26, 2026
df15dfb
fix(cli): suppress exception chain on rule load failure
aitestino Jan 26, 2026
96f33c0
feat(constants): add ExitCode enum and consolidate shared constants
aitestino Jan 27, 2026
2d82015
refactor(cli): extract option types to dedicated module
aitestino Jan 27, 2026
ea9d839
refactor(cli): use ExitCode enum and add validation summaries
aitestino Jan 27, 2026
b602318
refactor(cli): remove unused defaults.py module
aitestino Jan 27, 2026
900e1fd
refactor(validator): add factory pattern and improve caching
aitestino Jan 27, 2026
794790e
feat(output): add validation summary and rules list formatters
aitestino Jan 27, 2026
6434bdc
refactor(models): remove unused all_violations property
aitestino Jan 27, 2026
a68979c
chore: remove unnecessary type ignore comment
aitestino Jan 27, 2026
4ff64eb
test(validator): add unit tests for Validator class
aitestino Jan 27, 2026
7599b98
test(cli): add unit tests for CLI error handling
aitestino Jan 27, 2026
1b9c57c
test(output): add tests for formatter public functions
aitestino Jan 27, 2026
2efee0f
test(integration): use ExitCode enum for assertions
aitestino Jan 27, 2026
4a74f90
docs: add structured rules documentation and exit codes
aitestino Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 149 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,42 @@ Options:
-v, --verbosity [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Verbosity level [env: NAC_VALIDATE_VERBOSITY] [default: WARNING]
-s, --schema FILE Path to schema file [env: NAC_VALIDATE_SCHEMA] [default: .schema.yaml]
-r, --rules DIRECTORY Path to directory with semantic validation rules
-r, --rules DIRECTORY Path to directory with semantic validation rules
[env: NAC_VALIDATE_RULES] [default: .rules]
-o, --output FILE Write merged content from YAML files to a new YAML file
[env: NAC_VALIDATE_OUTPUT]
--non-strict Accept unexpected elements in YAML files
[env: NAC_VALIDATE_NON_STRICT]
-f, --format [text|json]
Output format for validation results [default: text]
--version Display version number
--list-rules List all available validation rules and exit
--help Show this message and exit
```

## Exit Codes


The CLI uses specific exit codes to help automation distinguish between error types:

| Exit Code | Meaning |
|-----------|---------|
| 0 | Validation passed |
| 1 | Semantic validation failed (business rule violations) |
| 2 | Syntax validation failed (YAML syntax or schema errors) |
| 3 | Configuration error (missing schema, invalid rules, etc.) |

## How It Works

Syntactic validation is done by basic YAML syntax validation (e.g., indentation) and by providing a [Yamale](https://github.com/23andMe/Yamale) schema and validating all YAML files against that schema. Semantic validation is done by providing a set of rules (implemented in Python) which are then validated against the YAML data. Every rule is implemented as a Python class and should be placed in a `.py` file located in the `--rules` path.

Each `.py` file must have a single class named `Rule`. This class must have the following attributes: `id`, `description` and `severity`. It must implement a `classmethod()` named `match` that has a single function argument `data` which is the data read from all YAML files. It can optionally also have a second argument `schema` which would then provide the `Yamale` schema. It should return a list of strings, one for each rule violation with a descriptive message. A sample rule can be found below.
## Writing Validation Rules

Each `.py` file must have a single class named `Rule`. This class must have the following attributes: `id`, `description` and `severity`. It must implement a `classmethod()` named `match` that has a single function argument `data` which is the data read from all YAML files. It can optionally also have a second argument `schema` which would then provide the `Yamale` schema.

### Simple Rules (String List)

For simple validations, rules can return a list of strings describing each violation:

```python
class Rule:
Expand All @@ -51,6 +74,128 @@ class Rule:
return results
```

### Structured Rules (Recommended)

For richer output with context, explanations, and recommendations, use the structured data classes:

```python
from nac_validate import RuleContext, RuleResult, Violation


class Rule:
id = "301"
description = "Verify Infra VLAN Is Defined When Referenced by AAEPs"
severity = "HIGH"

# Rich context displayed in terminal output
CONTEXT = RuleContext(
title="INFRA VLAN CONFIGURATION WARNING",
affected_items_label="Affected AAEPs",
explanation="""\
The Infrastructure VLAN (Infra VLAN) is critical for APIC-to-leaf
communication. When infra_vlan is enabled on an AAEP, the global
infra_vlan value must be explicitly defined.""",
recommendation="""\
Define the Infra VLAN in your access_policies configuration:

apic:
access_policies:
infra_vlan: 3967
aaeps:
- name: INFRA-AAEP
infra_vlan: true""",
references=[
"https://www.cisco.com/c/en/us/td/docs/dcn/aci/apic/all/apic-fabric-access-policies.html"
],
)

@classmethod
def match(cls, inventory):
violations = []

aaeps = inventory.get("apic", {}).get("access_policies", {}).get("aaeps", [])
if aaeps is None:
aaeps = []

# Find AAEPs with infra_vlan enabled
affected_aaeps = [
aaep.get("name", "unnamed")
for aaep in aaeps
if aaep.get("infra_vlan", False)
]

if affected_aaeps:
infra_vlan = (
inventory.get("apic", {})
.get("access_policies", {})
.get("infra_vlan", 0)
)
if infra_vlan == 0:
for aaep_name in affected_aaeps:
violations.append(
Violation(
message=f"AAEP '{aaep_name}' has infra_vlan enabled but global infra_vlan is not defined",
path=f"apic.access_policies.aaeps[name={aaep_name}].infra_vlan",
details={
"aaep_name": aaep_name,
"infra_vlan_enabled": True,
"global_infra_vlan_defined": False,
},
)
)

if not violations:
return RuleResult()

return RuleResult(violations=violations, context=cls.CONTEXT)
```

### Structured Data Classes

**`Violation`** - Represents a single validation failure:

- `message` (str): Human-readable description of the issue
- `path` (str): Location in the YAML structure (e.g., `apic.tenants[name=PROD].vrfs[0]`)
- `details` (dict, optional): Machine-readable metadata for automation

**`RuleContext`** - Rich context for terminal output:

- `title` (str): Header displayed in violation output
- `affected_items_label` (str): Label for the violations list (e.g., "Affected AAEPs")
- `explanation` (str): Detailed explanation of why this matters
- `recommendation` (str): How to fix the issue, with examples
- `references` (list[str], optional): Links to documentation

**`RuleResult`** - Container for rule output:

- `violations` (list[Violation]): List of violations found
- `context` (RuleContext, optional): Rich context for terminal display

## JSON Output

Use `--format json` for machine-readable output suitable for CI/CD pipelines:

```bash
nac-validate data/ -s schema.yaml -r rules/ --format json
```

Output structure:

```json
{
"syntax_errors": [],
"semantic_errors": [
{
"rule_id": "301",
"description": "Verify Infra VLAN Is Defined When Referenced by AAEPs",
"errors": [
"apic.access_policies.aaeps[name=INFRA-AAEP].infra_vlan - AAEP 'INFRA-AAEP' has infra_vlan enabled but global infra_vlan is not defined"
]
}
]
}
```

## Installation

Python 3.10+ is required to install `nac-validate`. Don't have Python 3.10 or later? See [Python 3 Installation & Setup Guide](https://realpython.com/installing-python/).
Expand All @@ -69,7 +214,7 @@ uv tools install nac-validate

The tool can be integrated via a [pre-commit](https://pre-commit.com/) hook with the following config (`.pre-commit-config.yaml`), assuming the default values (`.schema.yaml`, `.rules/`) are appropriate:

```
```yaml
repos:
- repo: https://github.com/netascode/nac-validate
rev: v1.0.0
Expand All @@ -79,7 +224,7 @@ repos:

In case the schema or validation rules are located somewhere else the required CLI arguments can be added like this:

```
```yaml
repos:
- repo: https://github.com/netascode/nac-validate
rev: v1.0.0
Expand Down
13 changes: 12 additions & 1 deletion nac_validate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2025 Daniel Schmidt

from importlib.metadata import version # type: ignore
from importlib.metadata import version

from .models import GroupedRuleResult, RuleContext, RuleResult, Violation

__version__ = version(__name__)

# Public API for rule authors
__all__ = [
"Violation",
"RuleContext",
"RuleResult",
"GroupedRuleResult",
"__version__",
]
7 changes: 0 additions & 7 deletions nac_validate/cli/defaults.py

This file was deleted.

Loading