Project Goal

-LangGraph orchestration: agent decides which tools to run based on findings (not a fixed pipeline)

-Connects all prior tools: AutoTriage (identify suspicious evidence) → VolAI (finds suspicious processes or live network connections in files) → IAM Auditor (checks IAM privileges and related identity profiles) → AI PCAP Analyst (looks at any PCAP files and identifies suspicious traffic) → AI Detection Engineer (correlates findings and creates detection rules not identified)

-Generates three report formats: executive brief (CEO), technical findings (analyst), legal evidence (court)

-Generates ATT&CK heatmap: visualize all detected techniques across the incident

The benefit of using an agent is that this can be run on any memory dump in a continuous use of the tools. The decide_next() router looks at the evidence found after every tool and picks what runs next —once verified it takes different paths depending on which evidence is available.

LangGraph

LangGraph’s core revolves around a graph of nodes that all read and write to one shared state object, connected by edges with some of those edges being conditional: instead of “node A always goes to node B”, a function inspects the current state and returns which node should run next – function: add_conditional_edges.

Before LangGraph (and tools like it), people built this by hand with a while loop and a big if/elif chain — LangGraph just formalizes that pattern with a proper API and makes the state transitions inspectable.

Every node in ARIA reads the entire case state — every finding any other tool has produced so far — not just its own functions’ evidence. That’s what lets the Detection Engineer node correlation all the evidence at the end.

Agents vs pipeline

Automation pipeline – directed sequence: step 1, step 2, step 3 call each other like a script

The logic that decides “what happens next” is baked into the code structure itself — you’d need to edit the pipeline to change the order.

An agent makes decisions on what to do from what was found: system observes results and decides the next action based on that observation

ARIA’s agent works like this:

The routing decisions in decide_next() aren’t made by an LLM reasoning over the state in natural language — they’re made by rule-based logic signals (credential_theft, needs_pcap_analysis, etc.) that other tools wrote.

Why? Routing becomes deterministic, auditable, and testable (a defense attorney can ask “why did the system decide to run VolAI here” and you can point to an exact boolean condition).

LLM calls

Here me out…what if we swapped the decide_next router out for an LLM that can freely decide what to call next based on the state.

Right now, decide_next() is a deterministic function: it reads booleans state[“signals”] and returns a tool name. The graph structure doesn’t care how a routing decision gets made, only that something makes it and returns a valid node name.

So what would actually change if you swapped in an LLM?

Prompt would look something like: “here’s what’s run so far, what each tool found, here’s what’s still available — what should run next, or are we done?”

model reads state[“signals”] dict → reasons over the whole picture in natural language

Benefit: LLM can (1) weigh signals in combinations the original engineer never anticipated, (2) handle ambiguous or conflicting evidence the way a human analyst triages a messy case, (3) adjust its own prioritization without anyone needing to touch the code.

Downside: bye bye determinism. The same case can run twice and be routed differently. “Why did the system decide to check IAM before PCAP” now has to be answered by pointing at a model completion instead of a boolean expression. This makes it difficult to justify an investigative decision.

Attack surface increases…if any evidence content (a filename, a log line, a ransom note’s text) flows into the routing prompt unsanitized, there’s now a prompt-injection path where an attacker’s artifacts could manipulate the investigator’s control flow. And every routing decision now costs an API call, with the latency and failure modes that implies, versus a function call that returns in microseconds.

Two different purposes. For the evidence route, the define_next router shows definitive evidence that can map a story. For ambiguous evidence or more complex cases, the LLM route can handle the reasoning. For this project, I chose the deterministic route as it provides enough evidence to make a storyline of an attacker’s whereabouts. As LLMs and agents increase it is important to know when to use which one depending on the purpose and scale of your project.

LangGraph Orchestration

LangGraph agent deployed to ingest disk image + memory dump + logs + PCAP → complete IR report in one command

  1. Shared state (aria/state.py)

Every node reads from and writes to one Incident State object — a TypedDict with fields like:

findings: list[Finding]
signals: dict[str, Any]          #IMP 
completed_tools: set[str]
attack_techniques: dict[str, int]

signals is where each tool leaves breadcrumbs like needs_memory_analysis, credential_theft, needs_pcap_analysis. No tools are called directly— tools only write to signals, and the orchestrator reads signals to decide what happens next.

  1. Tool nodes

Each tool (aria/tools/autotriage.py, volai.py, etc.) has one function: run(state) -> list[Finding].

Ex. In autotriage.py, after scanning the disk manifest:

if signals.get("malware_dropped") or signals.get("ransomware_indicators"):
    signals["needs_memory_analysis"] = True

AutoTriage doesn’t call VolAII, just raises a flag. Passes it on to form the evidence for “what happened next.”

graph.py wraps each tool module in a small node function (_make_node) that calls module.run(state) → merges the findings back into state via record_findings()→ logs a tool_runs entry with a timestamp and a reason

  1. The router: decide_next(state)

After every evidence-gathering node runs, this function looks at three things:

and builds a priority list:

if NODE_VOLAI not in completed and _has_artifact(state, "memory"):
    priority = 3 if signals.get("needs_memory_analysis") else 1
    candidates.append((NODE_VOLAI, priority))

Same pattern for IAM Auditor and PCAP Analyst. It sorts candidates by priority and returns the winner. If nothing’s left to gather, it routes to the Detection Engineer. If that’s also done, it returns “END”.

  1. Wiring it into LangGraph (build_graph())
graph = StateGraph(IncidentState)
for name in _TOOL_MODULES:
    graph.add_node(name, _make_node(name))
graph.set_entry_point(NODE_AUTOTRIAGE)

for node in (NODE_AUTOTRIAGE, NODE_VOLAI, NODE_IAM, NODE_PCAP):
    graph.add_conditional_edges(node, decide_next, routing_map) #non-linear

graph.add_edge(NODE_DETECTION, END)

AutoTriage, VolAI, IAM Auditor, or PCAP Analyst runs → LangGraph calls decide_next(state) and routes to whatever node name it returns

Demo

ariatr

Running case against agent → reports written

ariat1

ATT&CK techniques found

ariafind

signals that produce true

Incidence Brief

Case ID: ARIA-20260810-C8FD17

Prepared for: Executive leadership / Board

Prepared by: ARIA (AI Response & Investigation Agent), reviewed by cybersecbella

Case opened: 2026–08–10T17:04:12.791246+00:00

What Happened, in Plain Terms

Key Findings

## Evidence

## Technical Findings + Timeline