Repository navigation
feat(api): add approval, idempotency, audit and rescan gate for remediation (#266) - #364
Conversation
|
Thanks for picking up the approve-side race. I read through Two gaps are left in the same area. The first one I reproduced. 1. One extra statement in the migration closes it (after I added this, the same CREATE TRIGGER trg_remediation_audit_log_no_truncate
BEFORE TRUNCATE ON remediation_audit_log
FOR EACH STATEMENT EXECUTE FUNCTION remediation_audit_log_append_only();It would also help to 2. Approval binds to the playbook path, not to what will run. A small fix:
This is the same "approve X, execute exactly X" check we use for deploy approvals here: the approval is bound to the plan's sha256 and re-verified before dispatch. Glad to send (1) as a follow-up PR against this branch if that's useful. |
…proval to the script Two gaps found in review of OWASP#364: - Row-level triggers do not fire for TRUNCATE, so the whole audit trail could be emptied in one statement. Add a statement-level BEFORE TRUNCATE trigger using the same append-only function. Test teardown no longer relies on TRUNCATE and switches the triggers off explicitly instead. - Approval covered the playbook path, not its contents. propose now stores the script's SHA-256 and the target (resource_id, subscription_id); the approved audit row records them, and begin_execution recomputes the hash and refuses, with an audited playbook_changed refusal, if the script changed or vanished. The stored path is never trusted: it must resolve to a regular file under playbooks/. Reported by babyblueviper1 on OWASP#364. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
8280251 to
ac38744
Compare
…iation (OWASP#266) Agent-driven remediation can change customer infrastructure, so the gate that protects it has to exist before any execution path does. This adds it as proposal-only: nothing runs a playbook. - remediation_actions and an append-only remediation_audit_log, with a partial unique index allowing one live action per finding - RemediationGate: propose, approve, reject, begin_execution, complete_execution and verify, each a single conditional UPDATE plus an audit row in the same transaction - execution requires an approved action and an allowlisted playbook; the allowlist ships empty, and begin_execution grants at most once, so a retry or a concurrent caller never executes twice - a remediation is pending verification, not done, until a later completed rescan reports PASS for the same rule and resource; FAIL stays open and a missing, UNKNOWN or ERROR evaluation is inconclusive - PostgreSQL tests for concurrent proposal, approval, execution and verification, and for the audit trail - update the pinned Alembic head in the migration test Closes OWASP#266 Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
Build the partial unique index with op.execute instead of passing an f-string to sa.text, matching the other migrations, and annotate the single fixed-fragment UPDATE in RemediationGate._transition the same way api/routes/resources.py does. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…proval to the script Two gaps found in review of OWASP#364: - Row-level triggers do not fire for TRUNCATE, so the whole audit trail could be emptied in one statement. Add a statement-level BEFORE TRUNCATE trigger using the same append-only function. Test teardown no longer relies on TRUNCATE and switches the triggers off explicitly instead. - Approval covered the playbook path, not its contents. propose now stores the script's SHA-256 and the target (resource_id, subscription_id); the approved audit row records them, and begin_execution recomputes the hash and refuses, with an audited playbook_changed refusal, if the script changed or vanished. The stored path is never trusted: it must resolve to a regular file under playbooks/. Reported by babyblueviper1 on OWASP#364. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
ac38744 to
670a751
Compare
|
Checked
One note for when the execution side lands (nothing runs a playbook in this PR, so it's not a blocker here). The hash is checked when the grant is issued, but the executor will read the script from disk again when it runs. To make the binding hold end to end, the executor should either read the bytes once, check them against the grant's |
What does this PR do?
Adds the execution gate for any future agent-driven remediation: human approval, an empty allowlist, at-most-once execution, an append-only audit trail, and rescan-based verification. It is proposal-only, and nothing in the repo runs a playbook.
Type of change
api/modelsand Alembic)Design
Full write-up in
docs/remediation-gate.md. Lifecycle:with
REJECTED,EXECUTION_FAILEDandVERIFICATION_FAILEDbranches. Every transition is a single conditionalUPDATE ... WHERE status = ANY(...)plus an audit row in the same transaction, so concurrency is decided by the database rather than by check-then-write in Python.Mapping to the issue:
EXECUTION_ALLOWLIST = frozenset(). A refused attempt is audited.begin_executiononly matchesAPPROVEDrows.APPROVED -> EXECUTINGis won by exactly one caller. Everyone else getsInvalidTransition, never a grant. A crashed runner leaves the rowEXECUTINGand is not retried automatically.remediation_audit_log, append-only via trigger.VERIFIEDneeds a later completed rescan of the same subscription whose rule evaluation (#263) for that resource isPASS.FAILstays open asVERIFICATION_FAILED. Missing,UNKNOWNorERRORis inconclusive and never closes the action.It also covers the concern @babyblueviper1 raised on the issue: approval is a compare-and-swap, so two concurrent
approvecalls on onePROPOSEDrow cannot both win.Rule details (if applicable)
N/A, no scan rule is added or changed.
Testing
tests/test_remediation_gate_postgres.py: 29 tests against PostgreSQL covering every exit criterion, including 8-way concurrent proposal, approval, execution and verificationb6d2f8a4c1e7)pytest tests/passes locally (1509 passed, 3 skipped) against PostgreSQL 15;ruff checkandruff format --checkare cleanThe existing migration test pins the head revision, so
_HEADintests/test_scan_admission_migration_postgres.pyis bumped.Not in scope (follow-ups)
HTTP routes and UI for propose/approve, generating proposals from findings, separation of duties (approver must differ from proposer), and authenticating the
actoridentity. Listed in the doc.Related issue
Closes #266
Checklist
Signed-off-bytrailer (git commit -s; seedocs/dco.md)