From 9db69d4ce4e80e7aaeef71e92240c1f2fb87d723 Mon Sep 17 00:00:00 2001 From: Oskar Gorczowski Date: Mon, 8 Dec 2025 12:04:13 +0100 Subject: [PATCH 1/6] added yamllint option --- nac_validate/cli/main.py | 14 +++++++++++- nac_validate/validator.py | 45 +++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/nac_validate/cli/main.py b/nac_validate/cli/main.py index 198c79e..0d2002e 100644 --- a/nac_validate/cli/main.py +++ b/nac_validate/cli/main.py @@ -9,6 +9,7 @@ import typer +import nac_validate import nac_validate.validator from ..exceptions import ( @@ -123,6 +124,16 @@ def version_callback(value: bool) -> None: ] +YamllintOn = Annotated[ + bool, + typer.Option( + "--yamllint-on", + help="Enable yamllint validation.", + envvar="NAC_VALIDATE_YAMLLINT_ON", + ), +] + + Version = Annotated[ bool, typer.Option( @@ -142,13 +153,14 @@ def main( rules: Rules = DEFAULT_RULES, output: Output = None, non_strict: NonStrict = False, + yamllint_on: YamllintOn = False, version: Version = False, ) -> None: """A CLI tool to perform syntactic and semantic validation of YAML files.""" configure_logging(verbosity) try: - validator = nac_validate.validator.Validator(schema, rules) + validator = nac_validate.validator.Validator(schema, rules, enable_yamllint=yamllint_on) validator.validate_syntax(paths, not non_strict) validator.validate_semantics(paths) if output: diff --git a/nac_validate/validator.py b/nac_validate/validator.py index 4bac414..7735fc3 100644 --- a/nac_validate/validator.py +++ b/nac_validate/validator.py @@ -5,6 +5,7 @@ import importlib.util import logging import os +import subprocess import sys import warnings from inspect import signature @@ -29,7 +30,8 @@ class Validator: - def __init__(self, schema_path: Path, rules_path: Path): + def __init__(self, schema_path: Path, rules_path: Path, enable_yamllint: bool = False): + self.enable_yamllint = enable_yamllint self.data: dict[str, Any] | None = None self.schema = None if os.path.exists(schema_path): @@ -71,12 +73,47 @@ def __init__(self, schema_path: Path, rules_path: Path): f"Rules directory not found: {rules_path}" ) + def _run_yamllint(self, file_path: Path) -> None: + """Run yamllint validation on a file""" + if not file_path.suffix in [".yaml", ".yml"]: + return + + logger.debug(f"Running yamllint on {file_path}") + + try: + # NAC-specific yamllint configuration - only new-lines rule enabled + config = "{extends: relaxed, rules: {new-lines: enable}}" + + result = subprocess.run( + ["yamllint", "-d", config, str(file_path)], + capture_output=True, + text=True + ) + + logger.debug(f"Yamllint exit code: {result.returncode}") + + if result.returncode != 0: + # Parse yamllint output - log errors but don't block other validations + for line in result.stdout.strip().split('\n'): + if line.strip() and ':' in line: + # Extract filename and error details + if file_path.name in line: + continue # Skip filename line + msg = f"Yamllint error: {line.strip()}" + logger.error(msg) + # Note: NOT adding to self.errors to allow other validations to continue + + except FileNotFoundError: + logger.warning("yamllint not found - skipping yamllint validation") + except Exception as e: + logger.warning(f"yamllint validation failed for {file_path}: {e}") + def _validate_syntax_file(self, file_path: Path, strict: bool = True) -> None: """Run syntactic validation for a single file""" if os.path.isfile(file_path) and file_path.suffix in [".yaml", ".yml"]: logger.info("Validate file: %s", file_path) - # YAML syntax validation + # YAML syntax validation first data = None try: data = load_yaml_files([file_path]) @@ -107,6 +144,10 @@ def _validate_syntax_file(self, file_path: Path, strict: bool = True) -> None: logger.error(msg) self.errors.append(msg) + # Run yamllint after other validations (if enabled) + if self.enable_yamllint: + self._run_yamllint(file_path) + def _get_named_path(self, data: dict[str, Any], path: str) -> str: """Convert a numeric path to a named path for better error messages.""" path_segments = path.split(".") diff --git a/pyproject.toml b/pyproject.toml index b12ac57..02ce8c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "typer>=0.17.4", "yamale>=6.0.0", "jmespath>=1.0.0", + "yamllint>=1.35.1", ] [project.urls] From bc13b2cfb52adbec13cd7a32744f6f13b88718ac Mon Sep 17 00:00:00 2001 From: Oskar Gorczowski Date: Mon, 8 Dec 2025 12:07:15 +0100 Subject: [PATCH 2/6] update comments for yamllint --- nac_validate/validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nac_validate/validator.py b/nac_validate/validator.py index 7735fc3..98066c1 100644 --- a/nac_validate/validator.py +++ b/nac_validate/validator.py @@ -81,7 +81,7 @@ def _run_yamllint(self, file_path: Path) -> None: logger.debug(f"Running yamllint on {file_path}") try: - # NAC-specific yamllint configuration - only new-lines rule enabled + # NAC-specific yamllint configuration - only minimum checks and new-lines rule enabled config = "{extends: relaxed, rules: {new-lines: enable}}" result = subprocess.run( @@ -113,7 +113,7 @@ def _validate_syntax_file(self, file_path: Path, strict: bool = True) -> None: if os.path.isfile(file_path) and file_path.suffix in [".yaml", ".yml"]: logger.info("Validate file: %s", file_path) - # YAML syntax validation first + # YAML syntax validation data = None try: data = load_yaml_files([file_path]) From b9566ad823c97084324d751fb78448e1c4707446 Mon Sep 17 00:00:00 2001 From: Oskar Gorczowski Date: Mon, 8 Dec 2025 12:19:30 +0100 Subject: [PATCH 3/6] adjust yamllint rules --- nac_validate/validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nac_validate/validator.py b/nac_validate/validator.py index 98066c1..821c04f 100644 --- a/nac_validate/validator.py +++ b/nac_validate/validator.py @@ -81,8 +81,8 @@ def _run_yamllint(self, file_path: Path) -> None: logger.debug(f"Running yamllint on {file_path}") try: - # NAC-specific yamllint configuration - only minimum checks and new-lines rule enabled - config = "{extends: relaxed, rules: {new-lines: enable}}" + # NAC-specific yamllint configuration - minimal validation with only new-lines and anchors + config = "{extends: relaxed, rules: {key-duplicates: disable, new-line-at-end-of-file: disable, hyphens: disable, indentation: disable, colons: disable, commas: disable, empty-lines: disable, line-length: disable, trailing-spaces: {level: warning}, new-lines: enable}}" result = subprocess.run( ["yamllint", "-d", config, str(file_path)], From bdb5c934e219aba1af49abfccd9b62569f93fa40 Mon Sep 17 00:00:00 2001 From: Oskar Gorczowski Date: Mon, 8 Dec 2025 13:24:26 +0100 Subject: [PATCH 4/6] pre-commit run --- nac_validate/cli/main.py | 4 +++- nac_validate/validator.py | 24 +++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/nac_validate/cli/main.py b/nac_validate/cli/main.py index 0d2002e..cf3931b 100644 --- a/nac_validate/cli/main.py +++ b/nac_validate/cli/main.py @@ -160,7 +160,9 @@ def main( configure_logging(verbosity) try: - validator = nac_validate.validator.Validator(schema, rules, enable_yamllint=yamllint_on) + validator = nac_validate.validator.Validator( + schema, rules, enable_yamllint=yamllint_on + ) validator.validate_syntax(paths, not non_strict) validator.validate_semantics(paths) if output: diff --git a/nac_validate/validator.py b/nac_validate/validator.py index 821c04f..0b3bede 100644 --- a/nac_validate/validator.py +++ b/nac_validate/validator.py @@ -30,7 +30,9 @@ class Validator: - def __init__(self, schema_path: Path, rules_path: Path, enable_yamllint: bool = False): + def __init__( + self, schema_path: Path, rules_path: Path, enable_yamllint: bool = False + ): self.enable_yamllint = enable_yamllint self.data: dict[str, Any] | None = None self.schema = None @@ -75,34 +77,34 @@ def __init__(self, schema_path: Path, rules_path: Path, enable_yamllint: bool = def _run_yamllint(self, file_path: Path) -> None: """Run yamllint validation on a file""" - if not file_path.suffix in [".yaml", ".yml"]: + if file_path.suffix not in [".yaml", ".yml"]: return - + logger.debug(f"Running yamllint on {file_path}") - + try: # NAC-specific yamllint configuration - minimal validation with only new-lines and anchors config = "{extends: relaxed, rules: {key-duplicates: disable, new-line-at-end-of-file: disable, hyphens: disable, indentation: disable, colons: disable, commas: disable, empty-lines: disable, line-length: disable, trailing-spaces: {level: warning}, new-lines: enable}}" - + result = subprocess.run( ["yamllint", "-d", config, str(file_path)], capture_output=True, - text=True + text=True, ) - + logger.debug(f"Yamllint exit code: {result.returncode}") - + if result.returncode != 0: # Parse yamllint output - log errors but don't block other validations - for line in result.stdout.strip().split('\n'): - if line.strip() and ':' in line: + for line in result.stdout.strip().split("\n"): + if line.strip() and ":" in line: # Extract filename and error details if file_path.name in line: continue # Skip filename line msg = f"Yamllint error: {line.strip()}" logger.error(msg) # Note: NOT adding to self.errors to allow other validations to continue - + except FileNotFoundError: logger.warning("yamllint not found - skipping yamllint validation") except Exception as e: From 2755872b473c2a0762a368ab1ba8d5a9620dd00c Mon Sep 17 00:00:00 2001 From: Oskar Gorczowski Date: Mon, 8 Dec 2025 13:37:30 +0100 Subject: [PATCH 5/6] pre-commit bandit --- nac_validate/cli/main.py | 1 - nac_validate/validator.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/nac_validate/cli/main.py b/nac_validate/cli/main.py index cf3931b..a70fb2f 100644 --- a/nac_validate/cli/main.py +++ b/nac_validate/cli/main.py @@ -9,7 +9,6 @@ import typer -import nac_validate import nac_validate.validator from ..exceptions import ( diff --git a/nac_validate/validator.py b/nac_validate/validator.py index 0b3bede..7eec256 100644 --- a/nac_validate/validator.py +++ b/nac_validate/validator.py @@ -5,7 +5,7 @@ import importlib.util import logging import os -import subprocess +import subprocess # nosec B404 import sys import warnings from inspect import signature @@ -86,7 +86,7 @@ def _run_yamllint(self, file_path: Path) -> None: # NAC-specific yamllint configuration - minimal validation with only new-lines and anchors config = "{extends: relaxed, rules: {key-duplicates: disable, new-line-at-end-of-file: disable, hyphens: disable, indentation: disable, colons: disable, commas: disable, empty-lines: disable, line-length: disable, trailing-spaces: {level: warning}, new-lines: enable}}" - result = subprocess.run( + result = subprocess.run( # nosec B603 B607 ["yamllint", "-d", config, str(file_path)], capture_output=True, text=True, From 695f817725f0ea30585bfad1e8ef8436129ca2b9 Mon Sep 17 00:00:00 2001 From: Oskar Gorczowski Date: Wed, 10 Dec 2025 09:43:40 +0100 Subject: [PATCH 6/6] sync yamllint config with ansible --- nac_validate/validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nac_validate/validator.py b/nac_validate/validator.py index 7eec256..b8c8cc4 100644 --- a/nac_validate/validator.py +++ b/nac_validate/validator.py @@ -84,7 +84,7 @@ def _run_yamllint(self, file_path: Path) -> None: try: # NAC-specific yamllint configuration - minimal validation with only new-lines and anchors - config = "{extends: relaxed, rules: {key-duplicates: disable, new-line-at-end-of-file: disable, hyphens: disable, indentation: disable, colons: disable, commas: disable, empty-lines: disable, line-length: disable, trailing-spaces: {level: warning}, new-lines: enable}}" + config = "{extends: relaxed, rules: {key-duplicates: disable, new-line-at-end-of-file: disable, hyphens: disable, indentation: disable, colons: disable, commas: disable, empty-lines: disable, line-length: disable, trailing-spaces: disable, new-lines: enable}}" result = subprocess.run( # nosec B603 B607 ["yamllint", "-d", config, str(file_path)],