blumeops/mise-tasks/review-compensating-controls
Erich Blume f6e392b80c
All checks were successful
Deploy Fly.io Proxy / deploy (push) Successful in 1m45s
C1: SHA-pin tooling dependencies (2026-04 cycle) (#344)
## Summary

Monthly tooling dependency refresh, with a one-time conversion from version-tag pins (`rev = "vX.Y.Z"`, `image:tag`, `>=`) to SHA / digest pins everywhere.

## Changes

- **prek hooks**: all `rev = "vX.Y.Z"` → commit SHA + `# vX.Y.Z` comment. Bumped trufflehog (3.94.0→3.95.2), kingfisher (1.91.0→1.97.0), ruff (0.15.7→0.15.12), shfmt (3.13.0→3.13.1), prettier (3.8.1→3.8.3), actionlint (1.7.11→1.7.12).
- **fly/Dockerfile**: tag pins → `image@sha256:...` digest pins. Bumped nginx (1.29.6→1.30.0-alpine), tailscale (v1.94.1→v1.94.2 — still inside the safe pre-1.96.5 range), alloy (v1.14.1→v1.16.0).
- **mise-tasks**: PEP 723 inline deps converted from `>=` to `==` (PEP 508 doesn't support hashes inline). All scripts pinned to current latest: rich 15.0.0, typer 0.25.0, pyyaml 6.0.3, httpx 0.28.1.
- **prek `additional_dependencies`**: ansible-lint==26.4.0, ansible-core==2.20.5.
- **taplo-lint**: pass `--no-schema`. Upstream's `--default-schema-catalogs` returns a format taplo v0.9.3 can't parse — we don't validate against TOML schemas anyway, so this turns off the broken catalog fetch.
- **docs/update-tooling-dependencies**: documents the SHA-pin convention, `docker buildx imagetools inspect` for digest lookup, and `prek clean` before re-verifying (cache grows to several GiB).

Forgejo workflow `actions/checkout@v6.0.2` was already at the latest SHA — no change.

## Test plan

- [x] `prek run --all-files` passes after `prek clean`
- [x] `deploy-fly` workflow builds and deploys the new fly image on merge
- [x] `fly status -a blumeops-proxy` healthy after deploy
- [x] Spot-check a few mise tasks (`mise run blumeops-tasks`, `mise run docs-check-links`) to confirm pinned deps resolve cleanly

Reviewed-on: #344
2026-04-30 16:51:43 -07:00

229 lines
7 KiB
Text
Executable file

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["pyyaml==6.0.3", "rich==15.0.0", "typer==0.25.0"]
# ///
#MISE description="Review the most stale compensating control"
#USAGE flag "--limit <limit>" default="10" help="Number of controls to show in the table"
"""Review compensating controls by staleness.
Reads ``compensating-controls.yaml`` and sorts by ``last-reviewed``.
Shows a staleness table, then displays the most stale control with all
references found in the codebase.
After reviewing, update the control entry:
last-reviewed: YYYY-MM-DD
Usage: mise run review-compensating-controls [--limit 10]
"""
import subprocess
import sys
from datetime import date
from pathlib import Path
from typing import Annotated
import typer
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
CONTROLS_FILE = Path(__file__).parent.parent / "compensating-controls.yaml"
REPO_ROOT = Path(__file__).parent.parent
def load_controls(path: Path) -> list[dict]:
data = yaml.safe_load(path.read_text())
return data.get("controls", [])
def parse_date(raw) -> date | None:
if raw is None:
return None
if isinstance(raw, date):
return raw
try:
return date.fromisoformat(str(raw))
except ValueError:
return None
def find_references(control_id: str) -> list[str]:
"""Find all files referencing a control ID using ripgrep."""
try:
result = subprocess.run(
["rg", "--no-heading", "-n", control_id, str(REPO_ROOT)],
capture_output=True,
text=True,
timeout=10,
)
lines = result.stdout.strip().splitlines()
# Exclude the controls file itself and this script
return [
ln
for ln in lines
if "compensating-controls.yaml" not in ln
and "review-compensating-controls" not in ln
]
except (FileNotFoundError, subprocess.TimeoutExpired):
return []
def main(
limit: Annotated[
int, typer.Option(help="Number of controls to show in the table")
] = 10,
) -> None:
console = Console()
today = date.today()
if not CONTROLS_FILE.exists():
console.print(
f"[bold red]Controls file not found:[/bold red] {CONTROLS_FILE}"
)
raise typer.Exit(code=1)
controls = load_controls(CONTROLS_FILE)
# Parse dates and build sortable entries
entries: list[tuple[dict, date | None]] = []
for ctrl in controls:
reviewed = parse_date(ctrl.get("last-reviewed"))
entries.append((ctrl, reviewed))
# Sort: never-reviewed first, then oldest
entries.sort(key=lambda e: (e[1] is not None, e[1] or date.min))
never_reviewed = sum(1 for _, r in entries if r is None)
# --- Summary panel ---
console.print()
console.print(
Panel(
f"[bold]{len(entries)}[/bold] compensating controls, "
f"[bold red]{never_reviewed}[/bold red] never reviewed",
title="[bold]Compensating Control Review Queue[/bold]",
border_style="cyan",
)
)
console.print()
# --- Staleness table ---
table = Table(show_header=True, header_style="bold")
table.add_column("#", justify="right")
table.add_column("Control ID")
table.add_column("Last Reviewed", justify="right")
table.add_column("Age (days)", justify="right")
table.add_column("Refs", justify="right")
for i, (ctrl, reviewed) in enumerate(entries[:limit], 1):
control_id = ctrl["id"]
refs = len(find_references(control_id))
if reviewed is None:
table.add_row(
str(i),
f"[red]{control_id}[/red]",
"[red]never[/red]",
"[red]—[/red]",
str(refs),
)
else:
age = (today - reviewed).days
style = "yellow" if age > 90 else ""
id_str = f"[{style}]{control_id}[/{style}]" if style else control_id
date_str = f"[{style}]{reviewed}[/{style}]" if style else str(reviewed)
age_str = f"[{style}]{age}[/{style}]" if style else str(age)
table.add_row(str(i), id_str, date_str, age_str, str(refs))
remaining = len(entries) - limit
if remaining > 0:
table.add_row("", f"[dim]… {remaining} more[/dim]", "", "", "")
console.print(table)
console.print()
# --- Most stale control detail ---
if not entries:
console.print("[bold red]No controls found![/bold red]")
raise typer.Exit(code=1)
top_ctrl, top_reviewed = entries[0]
control_id = top_ctrl["id"]
refs = find_references(control_id)
detail_lines = [
f"[bold cyan]{control_id}[/bold cyan]",
f"[dim]Last reviewed: {top_reviewed or 'never'}[/dim]",
"",
f"[bold]Description:[/bold] {top_ctrl.get('description', '').strip()}",
]
notes = top_ctrl.get("notes", "").strip()
if notes:
detail_lines.append(f"[bold]Notes:[/bold] {notes}")
console.print(
Panel(
"\n".join(detail_lines),
title="[bold]Up For Review[/bold]",
border_style="green",
)
)
console.print()
# --- References ---
if refs:
ref_table = Table(
show_header=True, header_style="bold", title="References in codebase"
)
ref_table.add_column("File", style="cyan")
ref_table.add_column("Line")
for ref in refs:
# rg output: file:line:content
parts = ref.split(":", 2)
if len(parts) >= 3:
filepath = parts[0].replace(str(REPO_ROOT) + "/", "")
line_no = parts[1]
content = parts[2].strip()
ref_table.add_row(f"{filepath}:{line_no}", content)
else:
ref_table.add_row(ref, "")
console.print(ref_table)
else:
console.print(
f"[yellow]No references to '{control_id}' found in the codebase.[/yellow]"
)
console.print()
# --- Review checklist ---
checklist = [
"[bold]Verification:[/bold]\n",
f"• {notes}\n" if notes else "",
"\n[bold]Review each reference:[/bold]\n",
"• For each muted finding referencing this control, confirm:\n",
" 1. The risk the original check guards against\n",
" 2. That this control actually mitigates that risk\n",
" 3. That the control is still in effect (not degraded or bypassed)\n",
"\n[bold]After review:[/bold]\n",
f"• Update compensating-controls.yaml: [cyan]last-reviewed: {today}[/cyan]\n",
"• If the control is no longer valid, either:\n",
" - Fix the underlying finding and remove the mute, or\n",
" - Document a new/updated compensating control\n",
"• Commit the change",
]
console.print(
Panel(
"".join(checklist),
title="[bold yellow]Review Guidance[/bold yellow]",
border_style="yellow",
)
)
if __name__ == "__main__":
typer.run(main)