From e5f376d796c27af284a71e5160e144871ea81efc Mon Sep 17 00:00:00 2001 From: "tmt-homelab-agent[bot]" Date: Fri, 23 Oct 2026 21:45:34 -0700 Subject: [PATCH] chore(harness): drop corvus-splunk from the dev-harness scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit corvus-splunk is a Splunk app (conf/XML/CSV), not a Python component. Its harness could only parse its own files and cross-reference names — it proved the app is self-consistent, not that Splunk accepts it, so the guard earned nothing and the umbrella carried a sixth component for it. - root Makefile: ALL loses corvus-splunk; the comment block now says it is out of harness scope instead of listing it among the legacy guards - ci-v2.yaml: the harness matrix is the five in-scope components - delete corvus-splunk/{Makefile,validate.py,.gitignore} — harness files only - docs/DEV-READINESS-REVIEW.md: the splunk rows and the umbrella paragraph say out of harness scope, not "validates config" The Splunk app itself (default/, lookups/, metadata/, bin/) is untouched. Verified from this worktree: make cycle runs five components (v2, sdk, cli, server, hooks) and exits 0; make lint/build/test/fix exit 0 and make fix is idempotent; ci-v2.yaml parses with a 5-entry matrix; git diff --stat HEAD -- corvus-splunk/ shows only the three harness-file deletions. --- .github/workflows/ci-v2.yaml | 5 +- Makefile | 21 ++-- corvus-splunk/.gitignore | 3 - corvus-splunk/Makefile | 28 ----- corvus-splunk/validate.py | 197 ----------------------------------- docs/DEV-READINESS-REVIEW.md | 31 +++--- 6 files changed, 33 insertions(+), 252 deletions(-) delete mode 100644 corvus-splunk/.gitignore delete mode 100644 corvus-splunk/Makefile delete mode 100644 corvus-splunk/validate.py diff --git a/.github/workflows/ci-v2.yaml b/.github/workflows/ci-v2.yaml index b17e125..5cc1861 100644 --- a/.github/workflows/ci-v2.yaml +++ b/.github/workflows/ci-v2.yaml @@ -5,8 +5,8 @@ name: CI v2 # every PR and main push runs everything. # workflow_dispatch stays as the manual re-run lever for any sha. # -# EVERY component runs its own harness. The live three (v2, sdk, cli) run a real -# build > lint > test cycle; the legacy/config three run their guards. The +# EVERY component in harness scope runs its own harness. The live three (v2, sdk, +# cli) run a real build > lint > test cycle; the legacy two run their guards. The # interpreter is pinned to 3.12 here and in the Makefiles — a mismatch changes the # coverage number and used to hide behind a 7-point tolerance. on: @@ -28,7 +28,6 @@ jobs: - corvus-cli - corvus-server - corvus-hooks - - corvus-splunk defaults: run: working-directory: ${{ matrix.component }} diff --git a/Makefile b/Makefile index f46ff52..279a759 100644 --- a/Makefile +++ b/Makefile @@ -3,15 +3,18 @@ # that runs them all, so "does the repo work?" has one answer. # # Live components (real harnesses): corvus-v2, corvus-sdk, corvus-cli -# Legacy/config guards: corvus-server, corvus-hooks, corvus-splunk +# Legacy guards: corvus-server, corvus-hooks # v1 is archived at branch v1-archive and is NOT deployed; its guards exist only # so archived code cannot silently rot. No tests are resurrected for it. # -# The component lists below are NOT cosmetic: the legacy three do not declare -# build/test (nothing to build or test), and corvus-splunk has no lint/fix (it is -# config, not Python). Looping a target over a component that lacks it aborts the -# whole umbrella with "No rule to make target", so each target runs over exactly -# the components that define it. +# corvus-splunk is OUT of harness scope: it is a Splunk app (conf/XML/CSV), not a +# Python component, so it declares no build/lint/test and this harness proves +# nothing about it. Its app files stay in the repo untouched. +# +# The component lists below are NOT cosmetic: the legacy two do not declare +# build/test (nothing to build or test). Looping a target over a component that +# lacks it aborts the whole umbrella with "No rule to make target", so each target +# runs over exactly the components that define it. # # The interpreter is PINNED to python3.12 (CI uses 3.12). Bare `python3` on this # host is 3.15, and running the suite on it changes the coverage number enough to @@ -21,9 +24,9 @@ PYTHON ?= python3.12 # Live three: the full harness. LIVE := corvus-v2 corvus-sdk corvus-cli -# Everything: setup, cycle and clean exist everywhere. -ALL := $(LIVE) corvus-server corvus-hooks corvus-splunk -# Python components: they lint and autofix. corvus-splunk is config (validate only). +# Everything in harness scope: setup, cycle and clean exist everywhere. +ALL := $(LIVE) corvus-server corvus-hooks +# Python components: they lint and autofix. PY := $(LIVE) corvus-server corvus-hooks V2 := corvus-v2 diff --git a/corvus-splunk/.gitignore b/corvus-splunk/.gitignore deleted file mode 100644 index d505c5a..0000000 --- a/corvus-splunk/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Component-local artifacts (the root .gitignore is owned elsewhere) -__pycache__/ -*.pyc diff --git a/corvus-splunk/Makefile b/corvus-splunk/Makefile deleted file mode 100644 index 1a14888..0000000 --- a/corvus-splunk/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -# The corvus-splunk guard — a config-only Splunk app, so `validate` IS the whole guard. -# -# There is deliberately NO venv: validate.py is stdlib-only (json, csv, xml.etree), so -# there is nothing to install and the VENV/PY/PIP/RUFF variables have no meaning here. -# It still runs on the pinned python3.12 for CI parity — bare `python3` on this host is -# 3.15, and a syntax check under the wrong interpreter proves the wrong thing. -# -# Proves: every default/*.conf parses, every dashboard XML is well-formed, every lookup -# CSV has a consistent column count, the transforms/props/nav cross-references resolve, -# and bin/ compiles. -# Cannot prove: that Splunk accepts the app, that the SPL is valid, or that the -# dashboards render — those need a live Splunk instance. - -PYTHON ?= python3.12 - -.PHONY: setup validate cycle clean - -setup: ## Nothing to install — validate.py is stdlib-only, so there is no venv - @echo "setup: ok — no venv by design: validate.py imports only json/csv/xml.etree" - -validate: ## Parse every conf/XML/CSV, resolve the cross-references, compile bin/ - $(PYTHON) validate.py - -cycle: validate ## The guard, one command - -clean: ## The byte-compile artifacts (validate.py itself writes nothing) - rm -rf __pycache__ bin/__pycache__ .pytest_cache - @echo "clean: ok — validate.py writes no artifacts of its own" diff --git a/corvus-splunk/validate.py b/corvus-splunk/validate.py deleted file mode 100644 index 900abc8..0000000 --- a/corvus-splunk/validate.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the corvus-splunk Splunk app. - -This app is config-only, so this script IS the whole guard. It proves: - * every default/*.conf parses as a [stanza] / key = value file - * every dashboard/nav XML is well-formed - * every lookup CSV has a header and a consistent column count - * the transforms.conf -> lookups/, props.conf -> transforms.conf and - nav/default.xml -> views/ cross-references all resolve - * every bin/*.py compiles - * any JSON/YAML that appears parses (there are none today) - -It CANNOT prove: that Splunk accepts the app, that the SPL in the saved searches is -valid, or that the dashboards render. Those all need a live Splunk instance. - -Stdlib only by design — no venv, no dependencies. -""" - -from __future__ import annotations - -import csv -import json -import sys -import xml.etree.ElementTree as ET -from pathlib import Path - -ROOT = Path(__file__).resolve().parent -DEFAULT = ROOT / "default" -LOOKUPS = ROOT / "lookups" -VIEWS = DEFAULT / "data" / "ui" / "views" - -# Views Splunk ships itself: a nav reference to one of these needs no file here. -BUILTIN_VIEWS = {"search", "dashboards", "reports", "alerts", "help", "launcher"} - -REQUIRED = [ - DEFAULT / "app.conf", - DEFAULT / "indexes.conf", - DEFAULT / "macros.conf", - DEFAULT / "props.conf", - DEFAULT / "savedsearches.conf", - DEFAULT / "transforms.conf", - ROOT / "metadata" / "default.meta", - LOOKUPS, -] - -errors: list[str] = [] - - -def check(ok: bool, message: str) -> bool: - if not ok: - errors.append(message) - return ok - - -def parse_conf(path: Path) -> dict[str, dict[str, str]]: - """Parse a Splunk .conf into {stanza: {key: value}}. - - Handles '#' comments, blank lines and backslash line continuations. The - continuations matter: savedsearches.conf splits long SPL across lines, and a - naive one-key-per-line parser false-fails on them. - """ - stanzas: dict[str, dict[str, str]] = {} - current: dict[str, str] | None = None - pending = "" - for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): - line = pending + raw - pending = "" - if line.rstrip().endswith("\\"): - pending = line.rstrip()[:-1] + " " - continue - stripped = line.strip() - if not stripped or stripped.startswith("#"): - continue - if stripped.startswith("[") and stripped.endswith("]"): - name = stripped[1:-1].strip() - if not name: - errors.append(f"{path.name}:{lineno}: empty stanza name") - elif name in stanzas: - errors.append(f"{path.name}:{lineno}: duplicate stanza [{name}]") - current = stanzas.setdefault(name, {}) - continue - if current is None: - errors.append(f"{path.name}:{lineno}: content before the first [stanza]") - continue - if "=" not in stripped: - errors.append(f"{path.name}:{lineno}: not a 'key = value' line: {stripped[:60]!r}") - continue - key, _, value = stripped.partition("=") - current[key.strip()] = value.strip() - if pending: - errors.append(f"{path.name}: file ends inside a line continuation") - return stanzas - - -def main() -> int: - counts = {"conf": 0, "xml": 0, "csv": 0, "py": 0} - - # 1. the metadata files a Splunk app cannot ship without - for path in REQUIRED: - check(path.exists(), f"missing required path: {path.relative_to(ROOT)}") - - # 2. every .conf parses - confs: dict[str, dict[str, dict[str, str]]] = {} - for path in sorted(DEFAULT.glob("*.conf")): - confs[path.name] = parse_conf(path) - counts["conf"] += 1 - print(f" conf default/{path.name}: {len(confs[path.name])} stanza(s)") - - # 3. every XML is well-formed - for path in sorted(DEFAULT.rglob("*.xml")): - counts["xml"] += 1 - try: - ET.parse(path) - print(f" xml {path.relative_to(ROOT)}: well-formed") - except ET.ParseError as exc: - errors.append(f"{path.relative_to(ROOT)}: malformed XML: {exc}") - - # 4. every lookup CSV has a header and a consistent column count - for path in sorted(LOOKUPS.glob("*.csv")): - counts["csv"] += 1 - rows = list(csv.reader(path.read_text(encoding="utf-8").splitlines())) - if not rows: - errors.append(f"lookups/{path.name}: empty CSV") - continue - width = len(rows[0]) - for lineno, row in enumerate(rows[1:], start=2): - if len(row) != width: - errors.append(f"lookups/{path.name}:{lineno}: {len(row)} columns, header has {width}") - print(f" csv lookups/{path.name}: {len(rows) - 1} row(s) x {width} col(s)") - - # 5. transforms.conf filename = X must exist under lookups/ - transforms = confs.get("transforms.conf", {}) - for stanza, keys in transforms.items(): - filename = keys.get("filename") - if filename: - check((LOOKUPS / filename).exists(), f"transforms.conf [{stanza}] points at missing lookup {filename!r}") - - # 6. props.conf LOOKUP- = ... must resolve - for stanza, keys in confs.get("props.conf", {}).items(): - for key, value in keys.items(): - if not key.upper().startswith("LOOKUP-"): - continue - target = value.split()[0] if value.split() else "" - check(target in transforms, f"props.conf [{stanza}] {key} references unknown transforms stanza {target!r}") - - # 7. nav/default.xml view references must resolve to views/.xml - nav = DEFAULT / "data" / "ui" / "nav" / "default.xml" - if nav.exists(): - for view in ET.parse(nav).getroot().iter("view"): - name = view.get("name") or "" - if name in BUILTIN_VIEWS: - continue - check((VIEWS / f"{name}.xml").exists(), f"nav/default.xml references view {name!r} with no views/{name}.xml") - - # 8. every bin/ script compiles (in memory — writes nothing) - for path in sorted((ROOT / "bin").glob("*.py")): - counts["py"] += 1 - try: - compile(path.read_text(encoding="utf-8"), str(path), "exec") - print(f" py bin/{path.name}: compiles") - except SyntaxError as exc: - errors.append(f"bin/{path.name}: syntax error: {exc}") - - # 9. JSON, and YAML if any appears (there are none today) - for path in sorted(ROOT.rglob("*.json")): - try: - json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - errors.append(f"{path.relative_to(ROOT)}: malformed JSON: {exc}") - yamls = sorted(ROOT.rglob("*.yaml")) + sorted(ROOT.rglob("*.yml")) - if yamls: - try: - import yaml - except ImportError: - errors.append("YAML files present but PyYAML is unavailable — refusing to skip them silently") - else: - for path in yamls: - try: - yaml.safe_load(path.read_text(encoding="utf-8")) - except yaml.YAMLError as exc: - errors.append(f"{path.relative_to(ROOT)}: malformed YAML: {exc}") - - if errors: - print(f"\nvalidate: FAILED — {len(errors)} problem(s)", file=sys.stderr) - for problem in errors: - print(f" - {problem}", file=sys.stderr) - return 1 - - print( - f"\nvalidate: ok — {counts['conf']} conf, {counts['xml']} xml, " - f"{counts['csv']} csv, {counts['py']} py, 0 json, {len(yamls)} yaml" - ) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/DEV-READINESS-REVIEW.md b/docs/DEV-READINESS-REVIEW.md index cf574cc..b0a3393 100644 --- a/docs/DEV-READINESS-REVIEW.md +++ b/docs/DEV-READINESS-REVIEW.md @@ -17,7 +17,7 @@ repo's README and CLAUDE.md are not instruments: they describe v1 and are stale | `corvus-cli` | typer CLI | **LIVE PACKAGE, v1 protocol** | 214 statements; 50 tests (added on this branch), 100% coverage, `MockTransport` (`make cycle`); 15 `/ops` paths, **3** served by v2; no evidence-export command. | | `corvus-server` | v1 platform (FastAPI, CMDB, runbooks, Neo4j) | **LEGACY** — archived at `v1-archive`, not deployed | `src/` absent on this branch (`ls corvus-server/src` → No such file); `Dockerfile:35` still `COPY src/ src/`; two modules remain; 15 runbooks; 0 tests. | | `corvus-hooks` | v1 governance hooks (CC PreToolUse/PostToolUse, 3 adapters, installer) | **LEGACY** — not deployed | stdlib scripts; default URL `http://localhost:9420` and v1 route `/ops/events/targets/{target}/status` (`corvus_core.py:28,80`); 0 tests; v1 copy archived at `~/.claude/hooks/_archive-corvus-decom-2026-05-28`. | -| `corvus-splunk` | Splunk app (4 dashboards, 11 searches, 3 lookups) | **LEGACY config** — not deployed | expects `index=corvus sourcetype="corvus:ocsf"` + `class_uid` (`default/macros.conf:4,13-29`); app version 0.1.0; 0 tests; no Splunk instance locally. | +| `corvus-splunk` | Splunk app (4 dashboards, 11 searches, 3 lookups) | **LEGACY config** — not deployed, **out of harness scope** | expects `index=corvus sourcetype="corvus:ocsf"` + `class_uid` (`default/macros.conf:4,13-29`); app version 0.1.0; 0 tests; no Splunk instance locally; no `Makefile` (`ls corvus-splunk/Makefile` → absent), so the harness neither builds nor validates it. | The `Status` column carries the distinction that matters: a component can be **live as a package** (builds, lints, tests, CI-gated) while speaking a protocol nothing serves. `corvus-cli` and @@ -109,7 +109,8 @@ NOT reachable from either entrypoint: __init__, adapter, attested, cc_adapter, c ## 4. What the dev harness proves today -Each component owns `/Makefile`; the root `Makefile` runs every component's `cycle`. +Each component in harness scope owns `/Makefile`; the root `Makefile` runs every +component's `cycle`. | Component | Targets | Evidence from my run | What it cannot prove | |---|---|---|---| @@ -118,20 +119,26 @@ Each component owns `/Makefile`; the root `Makefile` runs every compo | `corvus-cli` | setup build lint test fix cycle clean | `make cycle`: "import: ok"; "`corvus --help`" and "`corvus check-target --help`" exit 0; "All checks passed!"; "50 passed in 0.72s"; "TOTAL 214 0 100%" | parity with v2; any command that reads decisions/capture/audit evidence | | `corvus-server` | setup lint import fix cycle clean | `make cycle`: "All checks passed!"; "import: ok on 3.12.15 — soc2 APIRouter(3 routes), typer app 'corvus' (11 commands)" | anything behavioral: no tests, no runnable app | | `corvus-hooks` | setup lint import fix cycle clean | `make cycle`: "All checks passed!"; "import: byte-compiled 8 scripts"; "import: ok — corvus_core imported, public API present" | that the hooks work against any server (they target v1 routes) | -| `corvus-splunk` | setup validate cycle clean | `make cycle`: "validate: ok — 6 conf, 5 xml, 3 csv, 1 py" | that Splunk accepts the app, that the SPL is valid, that the dashboards render | +| `corvus-splunk` | — (out of harness scope) | none: no `Makefile` and no entry in the root lists, so `make cycle` never reaches it | anything about the app — acceptance, SPL validity, rendering (needs a live Splunk) | Two contract deviations, stated plainly: `corvus-v2` has no `pyproject.toml`, so its `build` is a -live-socket proof instead of `pip install -e .`; and the three legacy components replace -`build`/`test` with `import`/`validate` because there is no installable package and no suite. +live-socket proof instead of `pip install -e .`; and the two legacy components in harness scope +(`corvus-server`, `corvus-hooks`) replace `build`/`test` with `import` because there is no +installable package and no suite. Consequence for the umbrella, and its fix: the root's `build`/`lint`/`test`/`fix` first looped over all six components and aborted at the first one lacking the target -(`make -C corvus-hooks test` → "No rule to make target 'test'"; `make -C corvus-splunk build` → -same, and splunk has no `lint` or `fix` either). The root Makefile now declares per-target -component lists — `LIVE` (the three with build/test), `PY` (the five that lint and autofix), and -`ALL` (setup/cycle/clean) — so every root target resolves. Verified on this worktree: `make lint`, -`make build`, `make test` and `make fix` all exit 0, and `make fix` on the clean tree leaves -`git status` byte-identical (it is idempotent). `make check` is an alias for `make cycle`. +(`make -C corvus-hooks test` → "No rule to make target 'test'"). The root Makefile now declares +per-target component lists — `LIVE` (the three with build/test), `PY` (the five that lint and +autofix), and `ALL` (setup/cycle/clean, the same five) — so every root target resolves. + +`corvus-splunk` is out of harness scope, not a config guard: it is a Splunk app (conf/XML/CSV), +not a Python component, so it declares no build/lint/test and the harness proves nothing about it. +Its harness files (`Makefile`, `validate.py`, `.gitignore`) are gone; the app itself +(`default/`, `lookups/`, `metadata/`, `bin/`) is untouched. Verified on this worktree: +`make cycle` runs five components and exits 0; `make lint`, `make build`, `make test` and +`make fix` all exit 0, and `make fix` on the clean tree leaves `git status` byte-identical +(it is idempotent). `make check` is an alias for `make cycle`. ## 5. Plan mapping — the phase that needs each component next @@ -142,7 +149,7 @@ component lists — `LIVE` (the three with build/test), `PY` (the five that lint | `corvus-cli` | **P5** | "the audit CLI's evidence export" | 50 mocked tests at 100%, entrypoint dispatch | v1 protocol; no decision/audit/evidence commands exist | | `corvus-server` | **P2** (+P4) | the runbook schema + validation gate; the knowledge base; blind-spot detection "ported from the old implementation where the concept survives review" | lint + import of the two remaining modules | `src/` is in `v1-archive`; 15 runbooks with no schema gate; 78 archived test files / 746 test functions that cannot run on this branch | | `corvus-hooks` | **P1** | ADR-004's adapter contract, its conformance suite, and the first two real adapters | lint + byte-compile + `corvus_core` import | v1 routes; no adapter-conformance fixture; the one real v2 adapter is outside the repo (§7.4) | -| `corvus-splunk` | **P3** | "The Splunk app validated against the rebuilt OCSF events" | config/XML/CSV parse + cross-reference validation | v2 emits no OCSF and forwards nothing to a SIEM (§7.6) | +| `corvus-splunk` | **P3** | "The Splunk app validated against the rebuilt OCSF events" | nothing — out of harness scope (no `Makefile`) | v2 emits no OCSF and forwards nothing to a SIEM (§7.6); the app has no harness at all | **THE acceptance gate is untestable today.** "A new harness reaches full governance parity by shipping only an adapter" needs a fixture that runs an adapter against the engine and checks the