diff --git a/README.md b/README.md index 9d3000a..2ef0eae 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,18 @@ is stale: This keeps local AI answers grounded in repo files and runbooks. +## Red/Blue Loop + +Safe red-team/blue-team checks are grouped under one command: + +```bash +./jeannie red-blue plan +./jeannie red-blue run --local +``` + +Local mode runs deterministic prompt-injection and eval checks. Live scans are +listed but gated so they can run from the Debian host when intended. + ## Deploying From the Debian server: diff --git a/README.md.tmpl b/README.md.tmpl index 6317e6d..6533208 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -244,6 +244,18 @@ is stale: This keeps local AI answers grounded in repo files and runbooks. +## Red/Blue Loop + +Safe red-team/blue-team checks are grouped under one command: + +```bash +./{{ main_script }} red-blue plan +./{{ main_script }} red-blue run --local +``` + +Local mode runs deterministic prompt-injection and eval checks. Live scans are +listed but gated so they can run from the Debian host when intended. + ## Deploying From the Debian server: diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index 22f1a04..49143b0 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -387,6 +387,10 @@ the public website deployment. `security/prompt-injection-lab`. Cases cover hostile runbooks, tool output, fake dashboard mitigations, and unsafe command arguments. +`red-blue {plan|run|ledger}` +: Plan or run the homelab red-team/blue-team loop. `run --local` executes only +deterministic local checks; live security scans remain explicit. + Kyverno hardening policies : `apps/supply-chain-policy` includes audit-mode checks for privileged pods, privilege escalation, hostPath use, resource requests/limits, and mutable image diff --git a/infra/red-blue-loop/README.md b/infra/red-blue-loop/README.md new file mode 100644 index 0000000..86296ca --- /dev/null +++ b/infra/red-blue-loop/README.md @@ -0,0 +1,16 @@ +# Red-Team / Blue-Team Loop + +This directory defines the safe recurring security loop for the homelab. The +first implementation runs local deterministic checks and prints the gated live +checks that should run from the Debian host. + +Run: + +```bash +./jeannie red-blue plan +./jeannie red-blue run --local +``` + +The loop is intentionally top-risk oriented: fix, accept, or defer findings in +`infra/red-blue-loop/ledger.tsv`. + diff --git a/infra/red-blue-loop/checks.tsv b/infra/red-blue-loop/checks.tsv new file mode 100644 index 0000000..a36e6f1 --- /dev/null +++ b/infra/red-blue-loop/checks.tsv @@ -0,0 +1,8 @@ +id mode command risk +prompt_injection local ./jeannie prompt-injection-lab run prompt injection regressions +ai_evals local ./jeannie ai-evals run Jeannie/RAG behavior regressions +secret_scan live ./jeannie security-secrets secret leakage +web_scan live ./jeannie security-web public web exposure +k8s_policy live ./jeannie security-k8s Kubernetes hardening drift +nuclei live ./jeannie security-nuclei public known-CVE exposure + diff --git a/infra/red-blue-loop/ledger.tsv b/infra/red-blue-loop/ledger.tsv new file mode 100644 index 0000000..9974e87 --- /dev/null +++ b/infra/red-blue-loop/ledger.tsv @@ -0,0 +1,3 @@ +date finding status owner notes +2026-06-30 initial-loop accepted jv Loop created; add real findings as fixed, accepted, or deferred. + diff --git a/jeannie b/jeannie index a64431e..2f7342e 100755 --- a/jeannie +++ b/jeannie @@ -6346,6 +6346,10 @@ ai_memory() { "${REPO_ROOT}/scripts/ai-memory" "${@:2}" } +red_blue_loop() { + "${REPO_ROOT}/scripts/red-blue-loop" "${@:2}" +} + validate_homelab() { "${REPO_ROOT}/scripts/validate-homelab" } @@ -6606,6 +6610,7 @@ Security Learning security-runtime Check runtime security sensors. security-attack-path Print prioritized attack-path findings. prompt-injection-lab {list|run|show} Run local prompt-injection defense drills. + red-blue {plan|run|ledger} Run or plan safe red-team/blue-team loop. AI And Indexing ai-index Build the local homelab RAG index. @@ -6890,6 +6895,9 @@ case "${1:-}" in prompt-injection-lab) prompt_injection_lab "$@" ;; + red-blue) + red_blue_loop "$@" + ;; openwrt) openwrt ;; diff --git a/scripts/explain b/scripts/explain index 58716c9..01d8ba4 100755 --- a/scripts/explain +++ b/scripts/explain @@ -34,7 +34,7 @@ EOF is_read_only_command() { case "$1" in - status|capacity|capacity-advisor|capacity-limits|recover-plan|incident|safety-case|agent-sandbox|scorecard|gitops-status|cert-check|backup-status|synthetic-checks|resource-budget|route-inventory|golden-ledger|workers|change-journal|map|validate|access-audit|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|secrets-check|tailnet-policy-check|ai-check|ask|ai-evals|ai-scheduler|ai-memory|impact|review-last-change|security-scan|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-attack-path|prompt-injection-lab|control-plane) + status|capacity|capacity-advisor|capacity-limits|recover-plan|incident|safety-case|agent-sandbox|scorecard|gitops-status|cert-check|backup-status|synthetic-checks|resource-budget|route-inventory|golden-ledger|workers|change-journal|map|validate|access-audit|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|secrets-check|tailnet-policy-check|ai-check|ask|ai-evals|ai-scheduler|ai-memory|impact|review-last-change|security-scan|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-attack-path|prompt-injection-lab|red-blue|control-plane) return 0 ;; *) diff --git a/scripts/red-blue-loop b/scripts/red-blue-loop new file mode 100755 index 0000000..0922089 --- /dev/null +++ b/scripts/red-blue-loop @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Run or plan the homelab red-team/blue-team loop.""" + +from __future__ import annotations + +import argparse +import csv +import subprocess +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +CHECKS_FILE = REPO_ROOT / "infra" / "red-blue-loop" / "checks.tsv" +LEDGER_FILE = REPO_ROOT / "infra" / "red-blue-loop" / "ledger.tsv" + + +def checks() -> list[dict[str, str]]: + with CHECKS_FILE.open(encoding="utf-8", newline="") as handle: + return list(csv.DictReader(handle, delimiter="\t")) + + +def plan() -> int: + print("Red/Blue Loop") + print("=============") + for row in checks(): + print(f"{row['mode']:5} {row['id']:18} {row['command']}") + print(f" risk: {row['risk']}") + print() + print(f"Ledger: {LEDGER_FILE.relative_to(REPO_ROOT)}") + return 0 + + +def run(local_only: bool) -> int: + failures = 0 + selected = [row for row in checks() if row["mode"] == "local" or not local_only] + print("Red/Blue Loop Run") + print("=================") + for row in selected: + print(f"\n== {row['id']} ==") + if row["mode"] != "local" and local_only: + print("skip: live check gated") + continue + process = subprocess.run( + row["command"].split(), + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + ) + print(process.stdout.rstrip()) + if process.returncode != 0: + failures += 1 + print() + print(f"failures={failures}") + print(f"ledger={LEDGER_FILE.relative_to(REPO_ROOT)}") + return 1 if failures else 0 + + +def ledger() -> int: + print(LEDGER_FILE.read_text(encoding="utf-8").rstrip()) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser("plan") + run_parser = subparsers.add_parser("run") + run_parser.add_argument("--local", action="store_true", help="Run only deterministic local checks.") + subparsers.add_parser("ledger") + args = parser.parse_args() + if args.command == "plan": + return plan() + if args.command == "run": + return run(local_only=args.local) + if args.command == "ledger": + return ledger() + return 2 + + +if __name__ == "__main__": + raise SystemExit(main())