diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c7bc3..59aac20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.22.0] + +### Added +- **Assessment-lens grouping.** Every rule and finding is now tagged with one of three + security-assessment lenses — **Harness gap**, **Guardrail gap**, or **Attack vector** — + so AutonomyProof reads as a structured assessment, not a flat rule list. `rules list` groups + by lens; `rules explain` shows the lens; JSON reports add `assessmentCounts` and a + `category` on each finding; the HTML report shows a per-lens breakdown. Central mapping in + `rules/categories.py`, enforced complete by tests. + ## [0.21.0] ### Added diff --git a/README.md b/README.md index a7bfbb2..c5a57da 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,11 @@ known-vulnerable framework dependencies (version-validated CVEs), and more. Run for details. Every finding carries **OWASP Agentic, NIST AI RMF, ISO 42001, MITRE ATLAS/ATT&CK, and CVE** mappings where a genuine one exists. +Rules and findings are grouped into three **assessment lenses** — **Harness gaps** (missing +runtime controls), **Guardrail gaps** (absent/disabled safety controls), and **Attack vectors** +(exploitable capability paths) — so a scan reads as a structured security assessment. +`autonomyproof rules list` shows the catalogue grouped this way. + ### How the analysis works (and its limits) AutonomyProof is AST-based static analysis. It resolves imports and follows source tracking — @@ -146,7 +151,7 @@ re-run `autonomyproof baseline .` and commit the updated file in the same PR. Use the action directly: ```yaml -- uses: autonomyproof/autonomyproof-cli@v0.21.0 +- uses: autonomyproof/autonomyproof-cli@v0.22.0 with: target: . fail-on: high @@ -173,7 +178,7 @@ Gate locally before a commit ever leaves your machine: # .pre-commit-config.yaml repos: - repo: https://github.com/autonomyproof/autonomyproof-cli - rev: v0.21.0 + rev: v0.22.0 hooks: - id: autonomyproof ``` diff --git a/pyproject.toml b/pyproject.toml index f9df065..37769b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "autonomyproof" -version = "0.21.0" +version = "0.22.0" description = "Open-source local scanner that finds unsafe capabilities and missing guardrails in Python AI-agent code." readme = "README.md" requires-python = ">=3.11" diff --git a/src/autonomyproof/__init__.py b/src/autonomyproof/__init__.py index cd2e516..d905d23 100644 --- a/src/autonomyproof/__init__.py +++ b/src/autonomyproof/__init__.py @@ -4,4 +4,4 @@ __all__ = ["__version__"] -__version__ = "0.21.0" +__version__ = "0.22.0" diff --git a/src/autonomyproof/cli.py b/src/autonomyproof/cli.py index 79ffb74..7f8e56f 100644 --- a/src/autonomyproof/cli.py +++ b/src/autonomyproof/cli.py @@ -32,6 +32,8 @@ ) from autonomyproof.models import Finding, ScanResult, Severity from autonomyproof.reporters import write_html, write_json, write_sarif +from autonomyproof.rules.base import Rule +from autonomyproof.rules.categories import CATEGORY_ORDER from autonomyproof.rules.registry import all_rules, get_rule from autonomyproof.scanner import Scanner from autonomyproof.scoring import SCORE_DISCLAIMER @@ -306,9 +308,15 @@ def rules() -> None: @rules.command("list") def rules_list() -> None: - """List every rule.""" + """List every rule, grouped by assessment lens.""" + rules_by_category: dict[str, list[Rule]] = {} for rule in all_rules(): - click.echo(f"{rule.id} {rule.default_severity.value:8} {rule.name}") + rules_by_category.setdefault(rule.category, []).append(rule) + for category in CATEGORY_ORDER: + group = rules_by_category.get(category, []) + click.echo(f"\n{category} ({len(group)})") + for rule in group: + click.echo(f" {rule.id} {rule.default_severity.value:8} {rule.name}") @rules.command("explain") @@ -320,6 +328,7 @@ def rules_explain(rule_id: str) -> None: except KeyError as exc: raise click.ClickException(f"Unknown rule: {rule_id}") from exc click.echo(f"{rule.id} — {rule.name}") + click.echo(f"Category: {rule.category}") click.echo(f"Severity: {rule.default_severity.value}") click.echo(f"Description: {rule.description}") click.echo(f"Risk: {rule.risk}") diff --git a/src/autonomyproof/models.py b/src/autonomyproof/models.py index fdd97ce..77597dc 100644 --- a/src/autonomyproof/models.py +++ b/src/autonomyproof/models.py @@ -64,6 +64,7 @@ class Finding: fingerprint: str framework: str | None = None toolName: str | None = None + category: str = "Attack vector" def to_dict(self) -> dict[str, object]: """Serialize to a JSON-ready dict with the severity as a string.""" @@ -127,3 +128,10 @@ def severity_counts(self) -> dict[str, int]: for finding in self.findings: counts[finding.severity.value] += 1 return counts + + def category_counts(self) -> dict[str, int]: + """Return a count of findings per assessment lens, in display order.""" + counts: dict[str, int] = {} + for finding in self.findings: + counts[finding.category] = counts.get(finding.category, 0) + 1 + return counts diff --git a/src/autonomyproof/reporters/html_reporter.py b/src/autonomyproof/reporters/html_reporter.py index a68d1c8..5391eab 100644 --- a/src/autonomyproof/reporters/html_reporter.py +++ b/src/autonomyproof/reporters/html_reporter.py @@ -7,6 +7,7 @@ from jinja2 import Environment, select_autoescape from autonomyproof.models import ScanResult +from autonomyproof.rules.categories import CATEGORY_ORDER from autonomyproof.scoring import SCORE_DISCLAIMER _TEMPLATE = """ @@ -55,6 +56,8 @@ {{ tools|length }} tool(s){% if project.branch %} · branch {{ project.branch }}{% endif %}
Critical: {{ counts.critical }} · High: {{ counts.high }} · Medium: {{ counts.medium }} · Low: {{ counts.low }}
+ {% if assessment %}By assessment lens — + {% for category, count in assessment %}{{ category }}: {{ count }}{% if not loop.last %} · {% endif %}{% endfor %}
{% endif %} @@ -156,6 +159,11 @@ def render_html(result: ScanResult) -> str: capabilities=result.capabilities, findings=[f.to_dict() for f in result.findings], counts=result.severity_counts(), + assessment=[ + (category, count) + for category in CATEGORY_ORDER + if (count := result.category_counts().get(category, 0)) + ], errors=result.errors, disclaimer=SCORE_DISCLAIMER, ) diff --git a/src/autonomyproof/reporters/json_reporter.py b/src/autonomyproof/reporters/json_reporter.py index b96270f..68225ad 100644 --- a/src/autonomyproof/reporters/json_reporter.py +++ b/src/autonomyproof/reporters/json_reporter.py @@ -27,6 +27,7 @@ def build_report_dict(result: ScanResult) -> dict[str, object]: "score": result.score, "riskLevel": result.risk_level, "severityCounts": result.severity_counts(), + "assessmentCounts": result.category_counts(), "filesScanned": result.files_scanned, "rulesExecuted": result.rules_executed, "durationMs": result.duration_ms, diff --git a/src/autonomyproof/rules/base.py b/src/autonomyproof/rules/base.py index 6fe9b61..a975afa 100644 --- a/src/autonomyproof/rules/base.py +++ b/src/autonomyproof/rules/base.py @@ -12,6 +12,7 @@ from autonomyproof.frameworks import primary_framework from autonomyproof.models import Finding, Mappings, Severity from autonomyproof.redaction import redact +from autonomyproof.rules.categories import category_for def _code_tokens(analysis: FileAnalysis) -> str: @@ -108,6 +109,11 @@ class Rule: mappings: Mappings = Mappings() project_level: bool = False + @property + def category(self) -> str: + """The assessment lens (Harness gap / Guardrail gap / Attack vector) for this rule.""" + return category_for(self.id) + def check(self, ctx: RuleContext) -> Iterable[Finding]: """Yield findings for one file. Overridden by per-file rules.""" return [] @@ -144,6 +150,7 @@ def make_project_finding( fingerprint=compute_fingerprint(self.id, pctx.anchor_file, "