A small, runnable reference implementation of governed security automation: AI agents may investigate freely and propose response actions, and execution stays constrained by Policy as Code, scoped to one authorized call, and confirmed against authoritative state afterwards.
It is built on REMORA, an assurance engine for tool-using agents. The engine is a dependency, not a copy: the decisions, the execution authority and the argument binding in this repository are the real implementation running in-process. Remove a control and the tests here fail.
User intent
-> Agent proposes an action
-> The action is authorized
-> time passes, state changes, arguments move
-> something executes
-> Was it still exactly the authorized action?
-> Did it actually have the intended effect?
Most agent tooling answers the first question and stops. In security response the last two are where the cost is: an approval redeemed for a different target, or an incident record that says a host was contained when it was not.
python -m pip install git+https://github.com/darklordVirtual/REMORA-research
python -m pip install -e .
python -m scenarios.suspicious_login
python -m scenarios.endpoint_isolation
python -m scenarios.threat_hunt
python -m scenarios.purple_team_validation
python -m scenarios.ot_engineering_workstation
python -m pytest tests/ -qNo API keys, no network, no services. The scenarios print incident-shaped records.
Two excerpts from an actual run, in the form an incident record would keep them.
Confidence is not authority. The agent has correlated two independent signals and is at 0.97, and the engine still declines to act alone:
-- The agent proposes disabling the account.
action : disable_account(user_principal_name='erik.hansen@example.no')
engine verdict: ESCALATE
schema_unverified_verify
evidence_insufficient
policy : within_scope
All containment preconditions satisfied.
OUTCOME : HELD_FOR_APPROVAL
-- The incident lead approves that exact call.
action : disable_account(user_principal_name='erik.hansen@example.no')
engine verdict: HUMAN_APPROVED
approved by : ir.lead
authority : one-shot lease 59095483
dispatch : executed
effect : VERIFIED
erik.hansen@example.no reads back as disabled.
OUTCOME : EXECUTED
Accepted is not done. The EDR takes the isolation request, returns a ticket, and the host stays on the network:
-- WS-9002: the EDR accepts the request and nothing is contained.
action : isolate_endpoint(endpoint_id='WS-9002')
approved by : ir.lead
authority : one-shot lease c1da8209
dispatch : executed
effect : MISMATCH
WS-9002 accepted isolation ticket ISO-WS-9002-162702 and
still reads back as connected. The request succeeded; the
containment did not.
OUTCOME : EXECUTED_EFFECT_UNCONFIRMED
A platform that reads an accepted request as a completed containment writes "endpoint isolated" into the incident record and closes the task. The host keeps its network path, and nobody looks again, because the record says it is handled.
suspicious_login works an impossible-travel detection. The agent reads the
identity log and endpoint telemetry autonomously, correlates two independent
signals, and proposes disabling the account. With a trust score of 0.97 the
engine returns ESCALATE. High model confidence does not become authority: a
critical identity action is held for a person because of what it is, not
because of how sure the agent was. A named approver then authorizes one exact
call, and the account is confirmed disabled by reading it back.
endpoint_isolation runs three isolations that end three ways. One lands and is
confirmed. One is accepted by the EDR, returns a ticket, and leaves the host on
the network. One is a domain controller outside the responder's scope and never
reaches the EDR. The middle case is the interesting one, and it is the reason
EXECUTED and EXECUTED_EFFECT_UNCONFIRMED are different words.
threat_hunt is hypothesis generation with no hands. The hunting agent
holds telemetry_read and nothing else, so a containment proposal from inside
a hunt is refused on the scope it does not hold, before the strength of the
finding is considered. There is no prompt asking the agent not to contain
things, because a prompt is a request and this is a boundary.
purple_team_validation emulates MITRE ATT&CK techniques and verifies
detection from telemetry rather than assuming it from a completed emulation.
Two of three are detected; the third is recorded as a coverage gap. An
exercise that asked whether the emulation finished would have reported three
of three.
ot_engineering_workstation is the case where the IT reflex is wrong. An
engineering workstation in the level-3 DMZ shows remote execution followed by
PLC programming software launched outside a change window. Isolating the host
outright would sever the process-control path and turn an intrusion response
into a process trip, so the permitted containment cuts the IT-facing path
only, and the read-back confirms both halves: IT path down, control path up.
The agent then proposes correcting the PLC logic, and is refused for a reason
no amount of evidence would move. This broker holds no process-control
engineering scope, and no executor is registered for that tool at all. A
safety-instrumented device is refused even for an identity that does hold the
scope, because an SIS change runs through management of change rather than
through incident response.
| Control | Attack it survives | Test |
|---|---|---|
| Registry-resolved risk metadata | An agent describing its own action as low risk | test_an_unregistered_tool_is_refused_rather_than_defaulted |
| Risk-tier routing | High model confidence used as authority | test_high_model_confidence_does_not_authorize_a_critical_action |
| Authority scopes, reads included | A caller with no scopes querying the SIEM | test_no_scope_cannot_read_telemetry |
| Authority scopes | A read-only hunting agent reaching a containment tool | test_a_read_scope_cannot_reach_a_containment_action |
| An approval answers an assessment | A signed approval for an action nobody assessed | test_an_approval_cannot_exist_without_a_proposal |
| Approval-proposal correspondence | An approval for proposal A authorizing proposal B | test_an_approval_for_one_proposal_cannot_authorize_another |
| Single-use approval | The same signature presented twice | test_an_approval_is_single_use |
| Exact-call binding | The target changing between approval and execution | test_the_target_cannot_change_between_approval_and_execution |
| Single-use authority | An approval replayed at the dispatcher | test_a_spent_authorization_cannot_be_redeemed_twice |
| Time-bounded authority | An old approval used as a standing one | test_an_expired_authorization_is_refused |
| Scope containment | A /8 block approved under a /24 authorization |
test_a_widened_firewall_block_is_refused_by_scope |
| Effect verification | An accepted request that never applied | test_an_accepted_request_that_did_not_apply_is_not_reported_as_success |
| No process-control capability | A security agent proposing a PLC write | test_a_security_responder_cannot_write_to_a_plc |
| Safety-instrumented protection | An SIS change proposed by an engineer who holds the scope | test_a_safety_instrumented_device_is_refused_even_with_the_scope |
| OT containment stays off the process | A containment that also severed the control path | test_a_severed_control_path_is_reported_as_a_mismatch |
The tampering tests substitute targets that are inside the responder's authorized set on purpose. A refusal that only worked because the second target was forbidden would prove the scope check, not the binding.
Telemetry
|
Detection
|
Agent investigation <- read scope only, runs autonomously
|
REMORA assessment <- ACCEPT / VERIFY / ABSTAIN / ESCALATE
|
Containment policy <- organisation scope; can only narrow
|
Human approval <- answers one open, assessed proposal
|
One-shot execution authority <- bound to this exact call, single use
|
Policy enforcement point <- re-verifies the binding before running
|
Security action executor <- holds the credential; the agent never does
|
Effect verification <- authoritative read-back
|
Evidence
architecture/threat-response-flow.md walks each stage and says what it does
not cover.
secops/registry.py governance metadata per security action
secops/policy.py Policy as Code: scopes, severity, corroboration, blast radius
secops/authority.py assessment -> policy -> approval -> authority -> dispatch -> verification
secops/wiring.py executors paired with the readers that confirm them
tools/estate.py a small mutable IT and OT estate, so read-back has something to read
scenarios/ five runnable workflows
tests/ thirty-six tests, most of them written as attacks
CodeQL runs on every push and pull request with the security-and-quality
pack, and weekly so a new query reaches this code without waiting for a
commit. GitHub Actions are pinned to commit SHAs rather than tags.
The first run raised two findings and both were acted on. An unused import was removed. Clear-text logging of identity telemetry in the login scenario was mitigated: the data there is fabricated so nothing leaked, but the finding was right about the shape, since that output is written to be pasted into a ticket. Source addresses are now masked to the network, which the impossible-travel narrative did not need anyway.
The query still flags the line, because the timestamp, country and MFA state that remain come from the same record. That residue is dismissed in the repository's code-scanning view with the reason written out rather than left open, since those three fields are precisely what the narrative exists to show. Open alerts: none.
This is a reference profile, not a product, and not a security control anyone should deploy as-is.
The estate in tools/estate.py is simulated. Nothing here talks to a real EDR,
SIEM or identity provider, and the integration surface is the part a real
deployment has to build.
Responder authority is a dataclass. In a deployment it comes from the authenticated identity at the transport boundary, never from the request body.
Credential custody separation is modelled in-process. The agent's code path
has no route to the tool callables, so calling one directly is a deviation
someone has to write rather than a default. A Python underscore prefix is a
convention and not a security boundary: anything in this interpreter can reach
broker._dispatcher, and one test deliberately does. A production deployment
requires the executor and the credential path to sit behind an independently
enforced process, service or network boundary. The structure here is the
design; that boundary is what makes it a control.
The properties demonstrated are the ones the tests assert and no more. In particular this repository shows that an authorization cannot be transferred to a different call, replayed, or claimed as an effect without a read-back. It does not show that the surrounding system cannot be bypassed by other means: the executor holds real credentials by design and can reach the downstream system directly, which is a deployment boundary rather than a code one.
Apache-2.0. The REMORA engine it depends on is source-available under BUSL-1.1; this profile is separately licensed so the patterns, the ToolSpec shape and the adversarial tests can be read and reused freely.