Polly C. DeNile — Find Evil! Hackathon Submission

Inspiration

The cloud folks have this thing called "admission controllers." A container spec hits the Kubernetes API server, the admission controller says "nah, this doesn't have resource limits" or "nah, you can't run as root," and the deploy never happens. The bad thing never touches the cluster.

We kept coming back to a question: why don't we have this for AI agents?

An agent running rm -rf /cases/evidence/ is the same class of problem as a pod deploying without resource limits. Both are tool calls that should have been evaluated against policy before they executed. The difference is Kubernetes solved this problem a decade ago, and the agent ecosystem is still relying on prompt-based guardrails — instructions the model can ignore whenever the context window gets crowded.

We at Polly C. DeNile work in the cloud world. We use agents to orchestrate infrastructure, and we struggle with the same things listed in the hackathon brief: agents that won't stop screaming about forks found in the kitchen, and agents that ignore read-only instructions and agents that run destructive commands because nothing structurally prevented them from doing so. The DFIR domain makes these problems existential. So we asked: can the same policy engine that powers Kubernetes admission controllers work in front of an agent's bash tool?

Turns out, yes. And once we proved it worked at the application level, we proved it works at the container runtime level too.

What it does

Two projects, one thesis: enforce policy on agent tool execution the same way Kubernetes enforces policy on deployments — structurally, not with prompts.

Making the Agent Smarter (sift-mcp fork)

We forked AppliedIR/sift-mcp, the most mature AI-assisted DFIR platform targeting SIFT Workstation, and added three capabilities that improve how the agent understands its tools and verifies its own work. Our fork has branch upstream/standalone-features for evaluation and potential upstream contribution in the future.

Full SIFT Tool Surface Enumeration. The upstream platform catalogs 65 forensic tools with FK (forensic knowledge) enrichment. The SIFT Workstation actually installs 366 distinct forensic commands — the remaining 301 are runnable via run_command but invisible to the agent because they have no catalog entry. We built an offline manifest generator that parses the authoritative teamdfir/sift-saltstack salt states (the only reliable enumeration method — dotnet binaries like Zimmerman's EZ Tools aren't visible via PATH scan) and resolves package names to command-line binaries via dpkg -L ground truth. The result: a manifest with 366 commands bucketed by enrichment status, merged with the catalog at runtime so the agent can see and reason about the full tool surface.

Evidence Knowledge Graph (KAG-lite). The upstream platform stores investigation state across JSON files — findings, timeline, evidence, IOCs — plus JSONL audit logs. Provenance relationships exist implicitly, but no tool makes them traversable. We added a NetworkX-based in-memory directed graph with 7 node types and 8 edge types, exposed as 6 MCP query tools: evidence_chain (full provenance path from finding to raw evidence, with gap detection), cross_reference (entity search across all node types), temporal_neighbors (events within a time window for sequence analysis), corroboration_map (source depth and weak-support flags per finding), host_summary (per-host aggregation), and rebuild_evidence_graph. The graph builds deterministically from existing case JSON files — zero LLM tokens consumed, zero infrastructure required. Each node type and edge type was chosen because it answers a question an analyst actually asks. We started with everything connected to everything and quickly learned that graph was useless — too many edges, no signal. The final schema is tight because forensic reasoning demands it.

Forensic Critic Subagent. A Claude Code subagent that auto-spawns after record_finding() to adversarially verify every staged finding against raw evidence. The critic traverses the evidence graph for provenance completeness, reads the referenced audit entries to get raw tool output, checks every factual claim — filenames, timestamps, hashes, Event IDs, process relationships — against the actual data, and assigns per-claim verdicts: CONFIRMED, UNCONFIRMED, CONTRADICTED, or OVERSTATED. It runs a hallucination check for any artifact referenced in the finding that doesn't appear in tool output or evidence files. Findings flagged as inaccurate get corrected before the human examiner ever sees them. The critic is a separate agent instance with its own model invocation — not the investigating agent re-evaluating its own work — which is what the self-correction literature calls the "external feedback strategy," applied here with deterministic tool verification instead of another LLM opinion.

Making the Agent Trustworthy (agentcontainers fork)

We forked agentcontainers, a container runtime that provides immutable, least-privilege environments for AI agents with a Rust/eBPF kernel enforcer. What it didn't do was understand MCP. The upstream treats agent containers as opaque processes — it can approve or deny a binary execution, but it has no visibility into MCP tool calls, no way to evaluate policy at the tool-argument level, and no audit trail that correlates a forensic finding back to the specific tools/call that produced it.

We added MCP-awareness. The central addition is a Go reverse proxy that speaks MCP Streamable HTTP on the client side and manages backend MCP servers via three transport modes: stdio (proxy starts a container, attaches to stdin/stdout, speaks MCP JSON-RPC over the Docker attach stream), container+http (proxy starts a container exposing an internal HTTP endpoint, resolves the bridge IP via Docker inspect, probes for TCP readiness), and remote (connects to an external MCP server by URL, e.g. wintools-mcp on a Windows host). Backend lifecycle follows a freeze→enforce→resume pattern: the container starts paused, the proxy registers it with the eBPF enforcer and applies kernel policy, then unpauses before the MCP handshake.

Every tools/call request is intercepted before forwarding. For shell commands, the proxy decomposes the command using a full shell parser (mvdan.cc/sh) — extracting the binary, flags, input paths, output paths, and program text from compound commands, handling pipes, subshells, and substitutions. Then it evaluates OPA policy compiled from the same security.yaml practitioners already edit. On violation, the agent gets structured denial feedback — the full reason list, not a generic error — so it can self-correct in one shot. Policy compiles once at startup and evaluates in-process: single-digit millisecond overhead per tool call.

The proxy bridges two enforcement layers that the upstream provides separately. Host-side (proxy): OPA evaluates tool arguments — flag allowlists, path restrictions, output directory enforcement. This is where forensic-specific policy lives. Kernel-side (eBPF enforcer): per-cgroup BPF hooks enforce network egress and filesystem access. The fork extends this with PrepareToolCall / CompleteToolCall RPCs that correlate kernel events to specific MCP tool calls. A policy-allowed fls command still cannot write to the evidence directory or phone home — the kernel blocks it regardless of what OPA said.

Evidence immutability is kernel-enforced, not prompt-based. Evidence directories are bind-mounted :ro into backend containers — the kernel returns EROFS on any write attempt. OPA policy prevents output outside the case directory. rm protection blocks deletion of evidence and case data. The combination means there is no prompt the agent can follow or ignore that changes the enforcement outcome.

Three independent hash-chained JSONL audit trails — proxy (every tools/call with OPA decision), enforcer (kernel events correlated to tool calls), and approval (HITL decisions with examiner identity) — make every finding traceable. A judge can walk the chain: finding → record_finding tool call → the run_command that produced the evidence → kernel events confirming what the process actually accessed → registered evidence files.

The MCP proxy covers tool calls that flow through the MCP server, but agentic harnesses also execute tools natively — Claude Code's Bash and Edit tools, OpenCode's tool.execute.before plugin, Pi's tool_call extension. These bypass the MCP server entirely. Harness hooks can intercept these native tool calls at the PreToolUse / PostToolUse boundary, communicate with the approval broker via Unix socket keyed by tool_use_id, and route them through the same OPA policy evaluation and audit chain as proxy-path calls. One decision engine, multiple adapters — no unprotected execution path regardless of how the agent invokes a tool.

How we built it

The architectural decisions mattered more than the implementation details.

Why an MCP reverse proxy instead of typed tool wrappers. We looked hard at codegen to build 200+ specific typed MCP functions (get_amcache(), extract_mft_timeline(), etc.) but the overhead was high and as tools update and syntax changes, the system was too fragile to thrive long term. The proxy intercepts the generic run_command path and evaluates every command against policy. The tradeoff is that you don't get type safety on individual tool calls, but you get a guarantee that nothing executes unevaluated.

Why NetworkX instead of a full graph database. The case files the platform produces — findings, timeline, IOCs, audit entries — are a knowledge graph waiting to happen. We wanted to prove the value of structured graph reasoning for forensic evidence before committing to infrastructure. NetworkX in-memory, lazy rebuild on source file changes, zero deployment complexity. It proved the point: agents used the graph tools to self-correct across multiple investigation runs.

Why OPA. The structured decision format is what makes it work for agents. A denied command comes back with every policy violation, not just the first. The agent doesn't guess what went wrong or retry blindly — it gets a list of every rule it tripped and can reformulate in one shot. This is the same engine, the same decision format, the same policy-as-code pattern that already runs in production Kubernetes clusters. We compile practitioner-friendly YAML to Rego behind the scenes, so IR professionals keep editing the same YAML they already know.

Challenges we ran into

Docker Engine vs. Docker Desktop on the SIFT VM. The agentcontainers backend assumes Docker Desktop's sandboxd VM isolation layer, which isn't present on the SIFT Workstation running Docker Engine directly. This isn't a minor configuration difference: it changes the containment model. On Docker Desktop, the VM is the outer security boundary and eBPF is defense-in-depth. On Docker Engine, eBPF on the host kernel IS the security boundary. The solution was extending DockerRuntime with a Docker Engine code path using --cgroupns=host with an enforcer sidecar, treating eBPF as the primary containment boundary rather than relying on a VM layer that doesn't exist. sandboxd becomes optional defense-in-depth when available, not a hard requirement.

The container+http transport didn't exist. The upstream assumed all container backends speak MCP over stdio. The SIFT gateway exposes an HTTP endpoint. We implemented dialHTTPContainer following the freeze→enforce→resume lifecycle: start container paused, register with enforcer, apply kernel policy, resolve bridge IP via container inspect, probe for TCP readiness, then connect via HTTP transport. The unenforced window is the start→pause interval only.

Getting the forensic critic to fire reliably. Claude Code's subagent delegation is description-driven — the LLM decides when to delegate based on the subagent's description field. We had to tune the record_finding() response envelope to include an explicit verification prompt that triggers delegation, and update the investigation methodology to make verification non-optional. The tension is real: every critic invocation burns context window and adds latency, so it has to be thorough enough to catch real issues but fast enough to not bottleneck the investigation.

The evidence graph schema. We started with everything connected to everything and quickly realized the graph was useless. Too many edges, no signal. The final schema — 7 node types, 8 edge types — was chosen because each relationship answers a question an analyst actually asks: "what evidence supports this finding?" (evidence_chain), "what else happened around this time?" (temporal_neighbors), "is this finding corroborated by multiple sources?" (corroboration_map). Fewer edges, more signal.

Accomplishments that we're proud of

Four enforcement layers, none prompt-based. OPA policy evaluation at the proxy level, eBPF kernel hooks at the cgroup level, read-only Docker bind mounts for evidence immutability, and a tamper-evident triple audit chain for traceability. This is architectural enforcement — the kind a practitioner can stand behind because it doesn't depend on the model following instructions.

Full SIFT tool surface visibility. 366 commands enumerated and bucketed, up from 65 in the upstream catalog. Salt-state parsing as the authoritative source — the only method that catches dotnet binaries and manual installs that PATH scans miss.

Evidence graph with zero LLM token cost. The graph builds deterministically from existing case JSON files. No entity extraction, no embedding, no API calls. Structured graph reasoning — evidence chains, temporal correlation, corroboration scoring — from data the platform already produces.

Automated finding verification that catches real issues. The forensic critic has surfaced overstated temporal claims, incomplete provenance chains, and findings with single-source support across multiple investigation runs. The external feedback pattern with deterministic tool verification, not another LLM opinion.

A reproducible judge experience. bootstrap.sh sets up a fresh Ubuntu host — installs Docker, pulls the CLI binary and enforcer image, configures BPF LSM. up.sh brings up the full enforced stack. No compiler, no build step. Clone → bootstrap → running.

What we learned

The admission controller pattern translates almost 1:1 from Kubernetes to agent tool execution. In k8s, a pod spec hits the API server, the admission controller evaluates policy, and the pod either gets created or rejected with reasons. Here, a bash command hits the proxy, OPA evaluates policy, and the command either runs (under kernel enforcement) or gets rejected with reasons. Same flow, same engine, different domain. The pattern is general.

OPA's structured denial format is genuinely perfect for agent self-correction. Most enforcement systems return "denied" and leave the caller guessing. OPA returns every policy that fired with its specific reason. For an agent that needs to reformulate a command, that's the difference between blind retry loops and single-shot correction.

The evidence knowledge graph taught us that KAG (Knowledge-Augmented Generation) doesn't require a graph database or LLM-based entity extraction when your data is already structured. The case files are a knowledge graph waiting to happen — you just have to make the implicit relationships explicit and queryable.

What's next for Polly C. DeNile

I think the biggest thing I want to address in this project is this: we proved the concept but each analyst use case will be different and finding the right mix of linux capabilities, mounts, etc for your needs. Lord knows I about threw my laptop out the window a few times. We built skills for this in our repo, but I plan to address this even further with a few ideas. It needs to be democratized: First a agentcontainers diagnose | agentcontainers debug command to help analysts tune their agentcontainer.json to suit their cases. Use LLM api keys to have Claude or OpenAI help explain what's going wrong. Second I want the shells scripts that drive the demo to be platformed into the agentcontainer code itself so that its all self contained. Last I want to explore other sandbox engines (like MacOS' newly released container project and more).

From KAG-lite to full KAG. Our NetworkX graph proved that structured knowledge graph reasoning adds real value to forensic investigations. Research supports scaling this up: Liang et al.'s KAG framework (arXiv:2409.13731, the engine behind OpenSPG) demonstrated 19.6% F1 improvement on HotpotQA and 33.5% on 2WikiMultiHopQA over state-of-the-art RAG methods, specifically because knowledge graph reasoning handles multi-hop provenance chains, temporal relations, and logical dependencies that vector similarity search cannot. Forensic evidence analysis is exactly this class of problem — "this Amcache entry was produced by this tool execution, which references this evidence file, which was mounted at this timestamp" is a multi-hop reasoning chain, not a similarity lookup. As OpenSPG's KAG framework matures, replacing our NetworkX implementation with full KAG would bring the logical form solver, knowledge alignment, and hybrid reasoning engine to forensic investigations. More details can be found in the arXiv paper here.

Expanding the tool surface. 331 of the 366 enumerated SIFT commands are in bucket C — runnable but unenriched. Each one that gets FK enrichment is another tool the agent can reason about instead of discovering by accident. Four bucket-B targets (WxTCmd, densityscout, hindsight.py, photorec) are immediate wins — the FK knowledge already exists, just needs catalog wiring.

Beyond DFIR. The admission controller pattern for agent tool execution isn't domain-specific. The same architecture — YAML-defined policy compiled to Rego, evaluated by OPA, enforced by kernel sandboxing — applies anywhere agents execute system commands. Cloud CLI gating, database operation controls, CI/CD pipeline enforcement. But that's the post-hackathon roadmap. Right now, the DFIR community needs this, and that's where the work is.

Built With

  • agentcontainers
  • aya
  • bash
  • claude
  • claude-code
  • devcontainers
  • ebpf
  • fastapi
  • golang
  • mcp
  • opa
  • python
  • rego
  • rust
  • sift-mcp
  • sift-workstation
  • valhuntir
Share this project:

Updates