whitehat-security/src/whitehat_security/cli.py

127 lines
4.8 KiB
Python
Raw Normal View History

from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
from .capacity import CapacitySample, characterize
from .differential import execute
from .e3 import CADENCE, PROBES
from .engagement import AuthorizationError, Engagement
from .fixtures import FixtureService, probe_suite
from .model import RunReport, utc_now
from .reporting import risk_nexus_message
def fixture_calibration() -> dict:
started = utc_now()
good = [execute(probe) for probe in probe_suite(FixtureService(enforce_tenant=True))]
bad = [execute(probe) for probe in probe_suite(FixtureService(enforce_tenant=False))]
detected = all(result.outcome == "finding" for result in bad)
rejected = all(result.outcome == "pass" for result in good)
return {
"schema_version": "whitehat-calibration/v1",
"evidence_class": "fixture",
"run_id": f"fixture-calibration-{started}",
"started_at": started,
"ended_at": utc_now(),
"outcome": "pass" if detected and rejected else "finding",
"expected": {"known_good": "pass", "known_bad": "finding"},
"known_good": [asdict(result) for result in good],
"known_bad": [asdict(result) for result in bad],
"limitations": [
"Offline fixture evidence calibrates the harness; it is not target assurance.",
"No network request, database connection, or live credential was used.",
],
}
def capacity_fixture() -> dict:
baseline = [
CapacitySample("aggressor", 10, 0, 100),
CapacitySample("neighbour", 12, 0, 80),
]
loaded = [
CapacitySample("aggressor", 25, 0.01, 120),
CapacitySample("neighbour", 18, 0.02, 60),
]
return asdict(characterize(
baseline=baseline, loaded=loaded, governor_bound=True,
aggressor_peak=10, aggressor_ceiling=10,
))
def validate_pack(path: Path) -> None:
data = json.loads(path.read_text(encoding="utf-8"))
required = {"schema_version", "target", "posture_claim", "attacker_model", "probes"}
missing = sorted(required - data.keys())
if missing:
raise ValueError(f"{path}: missing {', '.join(missing)}")
ids: set[str] = set()
for probe in data["probes"]:
for key in ("id", "operation", "route", "owner", "attacker", "absent"):
if key not in probe:
raise ValueError(f"{path}: probe missing {key}")
if probe["id"] in ids:
raise ValueError(f"{path}: duplicate probe id {probe['id']}")
ids.add(probe["id"])
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(prog="whitehat")
commands = parser.add_subparsers(dest="command", required=True)
fixtures = commands.add_parser("fixtures", help="calibrate E2 probes offline")
fixtures.add_argument("--output")
engagement = commands.add_parser("validate-engagement")
engagement.add_argument("path")
packs = commands.add_parser("validate-packs")
packs.add_argument("path")
commands.add_parser("e3-plan")
commands.add_parser("capacity-fixture")
message = commands.add_parser("risk-message")
message.add_argument("report")
args = parser.parse_args(argv)
if args.command == "fixtures":
result = fixture_calibration()
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
if args.output:
Path(args.output).write_text(rendered, encoding="utf-8")
else:
print(rendered, end="")
raise SystemExit(0 if result["outcome"] == "pass" else 1)
if args.command == "validate-engagement":
try:
record = Engagement.load(args.path)
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError) as error:
print(f"not authorized: {error}", file=sys.stderr)
raise SystemExit(2) from None
print(f"authorized: {record.raw['engagement_id']}")
return
if args.command == "validate-packs":
paths = sorted(Path(args.path).glob("*.json"))
if not paths:
raise SystemExit("no probe packs found")
for path in paths:
validate_pack(path)
print(f"validated {len(paths)} probe packs")
return
if args.command == "e3-plan":
print(json.dumps({"cadence": CADENCE, "probes": [asdict(probe) for probe in PROBES]},
indent=2, sort_keys=True))
return
if args.command == "capacity-fixture":
print(json.dumps(capacity_fixture(), indent=2, sort_keys=True))
return
if args.command == "risk-message":
report = RunReport(**json.loads(Path(args.report).read_text(encoding="utf-8")))
print(risk_nexus_message(report), end="")
return
raise SystemExit(2)
if __name__ == "__main__":
main(sys.argv[1:])