Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/zenzic/cli/_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ def count_per_file_ignores(config: ZenzicConfig) -> int:
if not isinstance(codes, list):
continue
normalized_codes = {
str(code).upper().strip()
str(code).strip().upper()
for code in codes
if isinstance(code, str) and str(code).upper().startswith("Z")
if isinstance(code, str) and str(code).strip().upper().startswith("Z")
}
total += len(normalized_codes)
return total
Expand Down
9 changes: 4 additions & 5 deletions src/zenzic/core/suppressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,11 @@ def is_suppressed(self, line_no: int, code: str) -> bool:
self.global_tracker.mark_directory_policy_used(pattern, code)
return True

suppressed = False
for d in self.directives:
if d.line_no == line_no and d.code == code:
if d.line_no == line_no and d.code == code and not d.consumed:
d.consumed = True
suppressed = True
return suppressed
return True
return False

def get_dead_suppressions(self) -> list["RuleFinding"]:
"""Yield Z603 findings for all directives that were never consumed."""
Expand Down Expand Up @@ -146,7 +145,7 @@ def __init__(self, config: "ZenzicConfig"):
if getattr(config, "governance", None) and config.governance.directory_policies:
for pattern, codes in config.governance.directory_policies.items():
for code in codes:
self.unused_dir_policies.add((pattern, str(code).upper()))
self.unused_dir_policies.add((pattern, str(code).strip().upper()))

if getattr(config, "excluded_file_patterns", None):
for pattern in config.excluded_file_patterns:
Expand Down
7 changes: 4 additions & 3 deletions src/zenzic/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,11 +704,12 @@ def _validate_no_swallowed_root_keys(data: dict[str, Any]) -> None:
tables_to_check = []
if isinstance(value, dict):
tables_to_check.append(value)
elif isinstance(value, list) and value and isinstance(value[0], dict):
tables_to_check.extend(value)
elif isinstance(value, list) and value:
tables_to_check.extend(item for item in value if isinstance(item, dict))

for table in tables_to_check:
swallowed = set(table.keys()) & root_keys
if isinstance(table, dict):
swallowed = set(table.keys()) & root_keys
if swallowed:
from rich.markup import escape

Expand Down
61 changes: 61 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SPDX-FileCopyrightText: 2026 PythonWoods <dev@pythonwoods.dev>
# SPDX-License-Identifier: Apache-2.0
"""Security and robustness tests for Zenzic core."""

from __future__ import annotations

from pathlib import Path
import pytest

from zenzic.models.config import ZenzicConfig
from zenzic.cli._governance import count_per_file_ignores
from zenzic.core.suppressions import SuppressionTracker


def test_ghost_policy_leading_space_remediation() -> None:
"""Vulnerability 1: Ensure leading/trailing spaces in per_file_ignores are counted in DQS."""
config_data = {
"governance": {
"per_file_ignores": {
"docs/index.md": [" Z101"]
}
}
}
config = ZenzicConfig(**config_data)

# Verify the space-prefixed code is properly counted as a suppression
per_file_count = count_per_file_ignores(config)
assert per_file_count == 1, "The leading-space ignore must be counted as a suppression to avoid DQS bypass"


def test_toml_bomb_mixed_type_array_remediation() -> None:
"""Vulnerability 2: Ensure mixed-type arrays in configuration tables do not crash the swallowed root validator."""
data = {
"custom_rules": [
{"id": "ZZ-TEST", "pattern": "test", "message": "test"},
42
]
}

# This should not raise an AttributeError crash
try:
ZenzicConfig._validate_no_swallowed_root_keys(data)
except AttributeError:
pytest.fail("Swallowed root validator crashed on mixed-type array item.")


def test_duplicate_inline_suppressions_remediation() -> None:
"""Vulnerability 3: Ensure duplicate inline suppressions are not fully consumed by a single finding."""
# We have two ignore comments for Z101 on line 1, but only one Z101 finding
text = "<!-- zenzic:ignore: Z101 --> <!-- zenzic:ignore: Z101 -->\n"
tracker = SuppressionTracker(Path("dummy.md"), text)

assert len(tracker.directives) == 2

# First Z101 finding is suppressed and consumes one directive
assert tracker.is_suppressed(1, "Z101")

# One of the two Z101 directives should be dead (unconsumed)
dead = tracker.get_dead_suppressions()
assert len(dead) == 1, "The duplicate inline ignore must remain unconsumed and trigger Z603"
assert dead[0].rule_id == "Z603"
Loading