2ed2b3ae0f
One nova/<name>.py per user-facing core/ module. Each ≤50 lines, ≤3 FunctionDef (add_parser + run [+1 helper]), every user-function call resolves to a core.* import, no `if` statements except `if __name__`. Subcommands: - nova resolve → core.contract_resolver.resolve - nova decommission → core.decommission_transform.decommission_transform - nova env-transition detect|record → core.env_transition - nova env-check → core.environment_check.check - nova hitl → core.hitl_gates.attest (+ approver_from_env) - nova onboard → core.onboarding.generate_env_file - nova outbox → core.outbox_writer.write_event - nova publish-outputs → core.output_publisher.publish_to_ssm + format_comment - nova policy → core.policy_engine.get_engine + get_policy_root (status) - nova regression → core.regression_verify.run_regression + write_report - nova sod → core.separation_of_duties.check - nova readiness → core.submission_readiness.cli_main - nova attestation-matrix → core.attestation_matrix.cli_main (new thin wrapper) - nova confidence → core.confidence_signal.cli_main (new thin wrapper) core wrappers added (minimal): attestation_matrix.cli_main, confidence_signal.cli_main — extracted from their __main__ blocks so the nova subcommands stay thin. ---ci--- project: acdl phase: 1 milestone: v1.28 status: execute persona: cli-engineer ---/ci---
189 lines
7.4 KiB
Python
189 lines
7.4 KiB
Python
"""8-concern attestation matrix (REQ-109, D-084).
|
|
|
|
Implements the 8 concerns from `core/hitl_matrix_design.md` §10.4. The
|
|
concerns split into two tiers:
|
|
|
|
- **Offline-testable concerns** (run for real, no operator input):
|
|
contract NFRs, schema validity, policy pass.
|
|
- **Operator-supplied concerns** (require an uploaded signed evidence
|
|
artifact, validated for freshness + schema per D-084):
|
|
functional correctness, performance baseline, security posture,
|
|
operational readiness, incident response, capacity/cost, resilience,
|
|
dr-region deploy.
|
|
|
|
The operator-supplied evidence artifact is a JSON blob with `timestamp`,
|
|
`type`, `payload`, and an optional `signature` (JWS detached). Freshness
|
|
is validated against the window from §10.4. Signature verification runs
|
|
when `NOVA_ATTESTATION_SIGNING_KEY_ID` is set (dual-read via core/env.py:
|
|
NOVA_* preferred, ACDL_* fallback until P5); it is skipped + logged
|
|
when unset (dev/CI — D-089). The matrix fails loud if an operator-supplied
|
|
concern is missing or expired for prod/dr.
|
|
"""
|
|
|
|
import datetime
|
|
import os
|
|
import sys
|
|
from typing import Optional, Tuple
|
|
|
|
# Repo root on sys.path so `from core import env` resolves to THIS package
|
|
# when run as a script (avoids editable-installed third-party `core` shadow).
|
|
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if _REPO_ROOT not in sys.path:
|
|
sys.path.insert(0, _REPO_ROOT)
|
|
|
|
from core import env
|
|
|
|
|
|
# Freshness windows (days) from hitl_matrix_design.md §10.4.
|
|
FRESHNESS_DAYS = {
|
|
"functional_correctness": 1, # last 24h
|
|
"performance_baseline": 7, # last 7d
|
|
"security_posture": 1, # last 24h
|
|
"operational_readiness": 30, # last 30d history
|
|
"incident_response": 90, # last 90d
|
|
"capacity_cost": 30, # forecast valid next 30d
|
|
"resilience_dr_drill": 180, # last 180d
|
|
"resilience_chaos": 90, # last 90d
|
|
"resilience_backup": 30, # last 30d
|
|
"dr_region_deploy": 180, # last 180d
|
|
}
|
|
|
|
# Which concerns apply to which environment.
|
|
ENV_CONCERNS = {
|
|
"dev": [], # autonomous — no concerns
|
|
"qa": ["functional_correctness", "performance_baseline", "security_posture", "contract_nfrs"],
|
|
"prod": ["operational_readiness", "incident_response", "capacity_cost",
|
|
"resilience_dr_drill", "resilience_chaos", "resilience_backup", "contract_nfrs"],
|
|
"dr": ["dr_region_deploy", "contract_nfrs"],
|
|
}
|
|
|
|
# Offline-testable concerns (run for real).
|
|
OFFLINE_CONCERNS = {"contract_nfrs", "schema_validity", "policy_pass"}
|
|
|
|
# Operator-supplied concerns (require an uploaded artifact).
|
|
OPERATOR_CONCERNS = {
|
|
"functional_correctness", "performance_baseline", "security_posture",
|
|
"operational_readiness", "incident_response", "capacity_cost",
|
|
"resilience_dr_drill", "resilience_chaos", "resilience_backup",
|
|
"dr_region_deploy",
|
|
}
|
|
|
|
|
|
def _parse_ts(ts: str) -> Optional[datetime.datetime]:
|
|
try:
|
|
return datetime.datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
except (ValueError, AttributeError):
|
|
return None
|
|
|
|
|
|
def _is_fresh(artifact: dict, concern: str) -> bool:
|
|
ts = _parse_ts(artifact.get("timestamp", ""))
|
|
if ts is None:
|
|
return False
|
|
window_days = FRESHNESS_DAYS.get(concern, 30)
|
|
age = datetime.datetime.now(datetime.timezone.utc) - ts
|
|
# Reject future-dated artifacts (negative age) — a backdated/future
|
|
# timestamp must not bypass freshness validation.
|
|
if age.total_seconds() < 0:
|
|
return False
|
|
return age.days <= window_days
|
|
|
|
|
|
def _verify_signature(artifact: dict) -> bool:
|
|
"""Verify the JWS detached signature when NOVA_ATTESTATION_SIGNING_KEY_ID is set.
|
|
|
|
When unset (dev/CI — D-089), signature verification is skipped + logged.
|
|
Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.
|
|
"""
|
|
key_id = env.get_env("ATTESTATION_SIGNING_KEY_ID", "") or ""
|
|
if not key_id:
|
|
sys.stderr.write(
|
|
"[attestation] NOVA_ATTESTATION_SIGNING_KEY_ID unset — "
|
|
"signature verification skipped (dev/CI, D-089)\n"
|
|
)
|
|
return True
|
|
if "signature" not in artifact:
|
|
return False
|
|
# Real KMS verification would happen here (kms:Verify).
|
|
# For v1.9 the presence of a signature + a set key id is the check;
|
|
# full KMS Verify is a production-deployment step.
|
|
return bool(artifact.get("signature"))
|
|
|
|
|
|
def _check_offline(concern: str, evidence: dict) -> Tuple[bool, str]:
|
|
"""Run an offline-testable concern for real."""
|
|
if concern == "contract_nfrs":
|
|
# The contract NFR check is satisfied when the evidence bundle
|
|
# includes a valid contract validation result (offline-testable).
|
|
nfrs = evidence.get("contract_nfrs", {})
|
|
if nfrs.get("valid", True):
|
|
return (True, "contract NFRs valid")
|
|
return (False, f"contract NFR check failed: {nfrs.get('reason', 'invalid')}")
|
|
if concern == "schema_validity":
|
|
if evidence.get("schema_validity", {}).get("valid", True):
|
|
return (True, "schema valid")
|
|
return (False, "schema invalid")
|
|
if concern == "policy_pass":
|
|
policy = evidence.get("policy_pass", {})
|
|
if policy.get("passed", True):
|
|
return (True, "policy pass")
|
|
return (False, f"policy check failed: {policy.get('reason', 'fail')}")
|
|
return (True, f"{concern}: no offline check defined")
|
|
|
|
|
|
def _check_operator(concern: str, evidence: dict) -> Tuple[bool, str]:
|
|
"""Validate an operator-supplied evidence artifact for freshness + schema."""
|
|
artifact = evidence.get(concern)
|
|
if artifact is None:
|
|
return (False, f"{concern}: missing operator-supplied evidence artifact")
|
|
if not _is_fresh(artifact, concern):
|
|
return (False, f"{concern}: evidence artifact expired or missing timestamp")
|
|
if not _verify_signature(artifact):
|
|
return (False, f"{concern}: signature verification failed")
|
|
return (True, f"{concern}: evidence artifact valid + fresh")
|
|
|
|
|
|
def check(env: str, evidence: dict) -> Tuple[bool, str]:
|
|
"""Run the 8-concern attestation matrix for the target env.
|
|
|
|
Returns (ok, reason). ok=False means block the promotion.
|
|
Dev always passes (autonomous).
|
|
"""
|
|
concerns = ENV_CONCERNS.get(env, [])
|
|
if not concerns:
|
|
return (True, f"{env}: no concerns (autonomous)")
|
|
|
|
failures = []
|
|
for concern in concerns:
|
|
if concern in OFFLINE_CONCERNS:
|
|
ok, reason = _check_offline(concern, evidence)
|
|
elif concern in OPERATOR_CONCERNS:
|
|
ok, reason = _check_operator(concern, evidence)
|
|
else:
|
|
ok, reason = (True, f"{concern}: no check defined")
|
|
if not ok:
|
|
failures.append(reason)
|
|
|
|
if failures:
|
|
return (False, "; ".join(failures))
|
|
return (True, f"{env}: all {len(concerns)} concern(s) pass")
|
|
|
|
|
|
def cli_main(argv) -> int:
|
|
"""Thin CLI entry (P1): nova attestation-matrix <env> [evidence.json]."""
|
|
import json
|
|
if len(argv) < 2:
|
|
print("usage: attestation_matrix <env> [evidence.json]", file=sys.stderr)
|
|
return 2
|
|
_env = argv[1]
|
|
_evidence = {}
|
|
if len(argv) >= 3 and os.path.isfile(argv[2]):
|
|
with open(argv[2]) as f:
|
|
_evidence = json.load(f)
|
|
ok, reason = check(_env, _evidence)
|
|
print(f"ATTESTATION PASS: {reason}") if ok else print(f"ATTESTATION BLOCK: {reason}", file=sys.stderr)
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(cli_main(sys.argv)) |