DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Prompt Injection Attacks and Hidden Security Risks in LLM Applications
  • Testing Strategies for Web Development Code Generated by LLMs
  • Conversational Risk Accumulation: Stateful Guardrails Beyond Single-Turn LLM Checks
  • Security in the Age of MCP: Preventing "Hallucinated Privilege"

Trending

  • Before the AI Coding Agent Writes Code: Structuring Scattered Requirements With PARA
  • Building Production-Grade Delta Lake Pipelines With Apache Spark on Databricks
  • RAG Done Right: When to Use SQL, Search, and Vector Retrieval and How To Combine Them
  • Everything About HTTPS and SSL in Java
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Every SOC Today Is Answering the Wrong Question

Every SOC Today Is Answering the Wrong Question

Rethink SOC architecture with Continuous Evidence Graphs that connect identities, sessions, and resources instead of relying on alert timelines.

By 
Igboanugo David Ugochukwu user avatar
Igboanugo David Ugochukwu
DZone Core CORE ·
Jul. 17, 26 · Opinion
Likes (0)
Comment
Save
Tweet
Share
296 Views

Join the DZone community and get the full member experience.

Join For Free

Ask most detection engineers what a SOC does, and they'll say: it finds compromised machines.

That's the wrong question. Attackers stopped compromising machines as the primary objective years ago — machines are just where identities and trust relationships happen to execute. A stolen session token, a federated role assumption, an over-scoped service account: none of those are "a machine got popped." They're a trust relationship quietly doing exactly what it was configured to do, on behalf of someone who shouldn't have it.

Security vendors still model attacks as timelines — a chronological alert feed you scroll through. Modern intrusions don't move on a timeline. They move on a graph: identity to session, session to role, role to resource, resource to the next identity down the chain. A timeline shows you that five things happened. A graph shows you how they're connected. Only one of those lets you answer the question that actually matters during an incident: what else can this attacker already reach?

That distinction — timeline versus graph — is the entire argument of this piece. I'm going to call the architecture that follows from it a Continuous Evidence Graph (CEG): a security data model where every event is a node, every relationship between identities, sessions, and resources is a persistent edge, and risk accumulates across that structure instead of resetting with every new alert.

I built a working, if early, implementation of this idea. It's called SentinelIQ; it's open source, and I'll be honest about exactly how much of the CEG model it currently implements versus how much is still on the roadmap — because the gap between the two is itself the most useful part of this article.

Repo: https://github.com/Drechi3/SentinelIQ

The Pitch Everyone Is Selling, and Why It Doesn't Hold Up

Walk any security conference floor in 2026, and you'll hear the same pitch, phrased six different ways: "Our AI triages alerts so your analysts don't have to." Vendors have poured large language models on top of legacy SIEM pipelines and called it autonomy.

It isn't autonomy. It's a chatbot bolted onto a firehose.

The reason isn't that LLMs are too weak for security work. It's that the architecture feeding them was designed for humans reading dashboards, not for a reasoning system that needs structured, connected, temporally-aware evidence. You cannot hand a language model a stream of disconnected alerts — high CPU, new admin login, outbound connection to unfamiliar IP — and expect it to reconstruct a coherent attack narrative. Humans do that reconstruction today, slowly, by holding context in their heads across multiple tools. Ask the model to do the same thing without giving it a way to hold context, and it will hallucinate a narrative that sounds plausible and is wrong.

The fix isn't a smarter model. It's a different substrate underneath the model — one built from evidence graphs, identity context, and risk propagation, with the LLM sitting at the explanation layer instead of the detection layer.

That's the architecture this article lays out.

Why "SIEM → Alert → Analyst" Breaks Down

The traditional pipeline looks like this:

Plain Text
 
Logs / Telemetry → Correlation Rules → Alert → Analyst Triage → Escalation


Three structural problems show up the moment you scale this past a few hundred assets:

  1. Alerts are stateless. A correlation rule fires on a pattern match at time T. It knows nothing about what happened at T-minus-one-hour on a different host, under a different account, in a different cloud region — even if that earlier event is the actual first stage of the same intrusion.
  2. Identity is bolted on, not native. Most SIEMs treat a username as a string field. They don't model the fact that a service account, a human account, and a workload identity federated through OIDC might all resolve to the same effective privilege boundary. Attackers pivot across exactly these boundaries because defenders don't model them as connected.
  3. Confidence is binary. An alert either fires or it doesn't. There's no notion of "this behavior is 30% more suspicious given what happened on the adjacent host two days ago." Real intrusions are built from a chain of individually low-confidence signals. Rule-based systems can't accumulate that kind of evidence; they only threshold it.

Layering an LLM on top of this pipeline just moves the same structural blindness into natural language. The model summarizes an alert queue fluently — and confidently misses a lateral movement chain that a graph would have made visually obvious in one query.

Where SentinelIQ Stands Today

Before I describe the full target architecture, here's the honest state of the reference implementation, because a manifesto with no working code behind it is just marketing.

SentinelIQ, as it runs today, already does the part most POCs skip entirely: it ingests security events through a FastAPI layer, scores them through a UEBA risk engine, and — this is the part I actually care about — builds a live, in-memory attack graph as events arrive, rather than treating each event as a standalone alert. Here's the actual graph model, unedited, from attack_graph.py:

Python
 
class Node:
    def __init__(self, node_id):
        self.id = node_id
        self.label = node_id
        self.first_seen = datetime.utcnow().isoformat()
        self.event_count = 0
        self.risk_accumulator = 0


class Edge:
    def __init__(self, s, t):
        self.id = f"{s}->{t}"
        self.source = s
        self.target = t
        self.weight = 0
        self.events = []


class AttackGraph:
    def add_node(self, node_id):
        if node_id not in self.nodes:
            self.nodes[node_id] = Node(node_id)
        self.nodes[node_id].event_count += 1

    def add_edge(self, s, t, risk, event):
        key = f"{s}->{t}"
        if key not in self.edges:
            self.edges[key] = Edge(s, t)
        e = self.edges[key]
        e.weight += risk
        e.events.append({"type": event, "risk": risk})


That's a real, running accumulator: every user-to-IP relationship becomes a weighted edge, and edge weight grows every time the same relationship reappears with risk attached. It's the seed of a Continuous Evidence Graph — nodes that persist, edges that accumulate weight over time instead of resetting per-alert.

What it isn't yet, and I want to be direct about this because the gap is the roadmap: the correlation logic is currently a single hardcoded mapping, not a general ATT&CK path-matcher —

Python
 
def correlate_event(event, ueba, intel):
    risk = ueba["risk_score"]
    malicious = intel["malicious"]

    if malicious and risk >= 60:
        return "CONFIRMED_ATTACK (T1110 Brute Force)"

    if malicious and risk >= 30:
        return "SUSPICIOUS_ACTIVITY (T1110 Brute Force)"

    return "NORMAL (T1110 Brute Force)"


— and the graph lives in process memory, not a graph database, so it doesn't survive a restart or scale past a single node. Both of those are exactly what the project's own roadmap already names: graph database integration, broader ATT&CK coverage, and an LLM-powered analyst layer.

That gap is the rest of this article. Below is the architecture SentinelIQ is evolving toward, and why each addition solves a specific limitation the current version has.

The Target Architecture: Evidence Graphs as the Core Data Model

Instead of a linear pipeline, the design below treats every event as a persistent node in a graph, connected by relationships that matter operationally: "authenticated as," "spawned by," "communicated with," "assumed role of," "resolved to."

Plain Text
 
Telemetry (logs, EDR, network, cloud audit, identity provider)
        │
        ▼
Event Sourcing Layer (immutable append-only log — Kafka)
        │
        ▼
Evidence Graph Construction (Neo4j / graph DB)
        │
        ▼
Identity Context Resolution (map accounts → real identities → privilege scope)
        │
        ▼
Attack Graph Generation (MITRE ATT&CK-mapped path finding)
        │
        ▼
Risk Propagation Engine (Bayesian confidence scoring across connected nodes)
        │
        ▼
LLM Explanation Layer (retrieval-augmented reasoning over the graph, not raw logs)
        │
        ▼
Human Decision (analyst reviews a ranked, explained hypothesis — not a raw alert)
        │
        ▼
Automated Containment (scoped, reversible actions gated by policy — OPA)


The key architectural decision: the LLM never sees raw telemetry. It sees a curated subgraph — the specific nodes and edges relevant to a hypothesis — retrieved on demand. This is the same principle behind retrieval-augmented generation in any other domain: give the model a small, relevant, structured context instead of an enormous, noisy one, and both accuracy and cost improve together.

Layer by Layer

1. Event Sourcing: Kafka as the System of Record

Every raw event — a Sysmon process-creation log, a CloudTrail API call, an Okta sign-in — is appended to an immutable log. Nothing is mutated in place. This matters for two reasons: it lets you replay history to rebuild a graph state as of any point in time (essential for incident response — "what did the environment look like six hours before detection?"), and it decouples ingestion rate from processing rate, since graph construction can run as a consumer that lags without losing data.

Python
 
# Simplified Kafka producer for identity events

from kafka import KafkaProducer
import json

producer = KafkaProducer(
    bootstrap_servers=['kafka-broker:9092'],
    value_serializer=lambda v: json.dumps(v).encode('utf-8')
)

def emit_identity_event(event: dict):
    producer.send(
        'identity-events',
        value={
            "event_id": event["id"],
            "principal": event["principal"],       # e.g. arn:aws:sts::...
            "action": event["action"],
            "resource": event["resource"],
            "source_ip": event["source_ip"],
            "timestamp": event["timestamp"],
            "session_context": event.get("mfa_verified", False),
        }
    )


2. Evidence Graph Construction

Each event becomes a node; relationships become edges. A process-creation event connects parent_process → child_process. An authentication event connects identity → session → resource_accessed. The graph is what lets a query like "show every resource this session ultimately touched" return a real answer instead of requiring an analyst to manually join five different log sources.

Cypher
 
// Neo4j: find all resources reachable from a suspicious session
// within 3 hops, weighted by recency

MATCH (s:Session {session_id: $sid})-[:ACCESSED|ASSUMED_ROLE|SPAWNED*1..3]->(r)
RETURN r.name, r.type, r.risk_score
ORDER BY r.risk_score DESC
LIMIT 25


This single query replaces what would otherwise be a manual, multi-tool pivot across a SIEM, a CSPM tool, and an identity provider's audit log — the exact workflow that eats hours during real incident response.

3. Identity Context Resolution

This is the layer most vendors skip, and it's the one that matters most in cloud environments. A single human identity might resolve to a local IdP account, a federated SAML session, an assumed IAM role, and a Kubernetes service account token — four different-looking principals in four different log sources, all representing one actual blast radius.

Python
 
def resolve_effective_identity(principal: str, graph_client) -> dict:
    """
    Walks federation/assumption chains to find the root identity
    and the full set of privileges reachable from it.
    """
    chain = graph_client.query("""
        MATCH path = (root:Identity)-[:FEDERATES_TO|ASSUMES_ROLE*0..5]->(p:Principal {id: $principal})
        RETURN root, [n IN nodes(path) | n.id] AS chain
    """, principal=principal)

    if not chain:
        return {
            "principal": principal,
            "root_identity": principal,
            "chain": []
        }

    return {
        "principal": principal,
        "root_identity": chain[0]["root"]["id"],
        "chain": chain[0]["chain"],
    }


Without this resolution step, an attack graph will show four disconnected low-severity anomalies instead of one connected, high-severity privilege chain.

4. Attack Graph Generation Against MITRE ATT&CK

Once identity is resolved, individual events get tagged against ATT&CK techniques, and the graph traversal engine looks for paths that match known tactic progressions — reconnaissance into initial access into privilege escalation — rather than isolated technique hits.

Python
 
ATTACK_STAGE_ORDER = [
    "reconnaissance", "initial_access", "execution",
    "persistence", "privilege_escalation", "defense_evasion",
    "credential_access", "lateral_movement", "exfiltration", "impact"
]

def score_path_progression(tagged_events: list[dict]) -> float:
    """
    Rewards event sequences that progress forward through the
    ATT&CK kill chain in time order; a single stage repeating
    scores lower than a chain that advances.
    """
    stages_seen = [
        ATTACK_STAGE_ORDER.index(e["stage"]) for e in tagged_events
        if e["stage"] in ATTACK_STAGE_ORDER
    ]

    if len(stages_seen) < 2:
        return 0.1

    forward_moves = sum(
        1 for a, b in zip(stages_seen, stages_seen[1:]) if b > a
    )

    return forward_moves / max(len(stages_seen) - 1, 1)


5. Risk Propagation With Bayesian Confidence

Instead of thresholding each event independently, confidence propagates through the graph. A moderately suspicious login becomes much more suspicious if it's one hop away from a node that already scored high. SentinelIQ's risk_accumulator field on every Node is the placeholder for exactly this — right now it only accumulates the node's own events; it doesn't yet pull risk from neighbors. Formalizing that pull is a one-equation problem:

For a node v with neighbors N(v), the propagated risk at iteration t+1 is:

Plain Text
 
R_(t+1)(v) = α · R_t(v) + β · Σ_{u ∈ N(v)} w(u,v) · R_t(u)


where α is how much a node trusts its own evidence, β is how much it trusts its neighbors, and w(u,v) is edge confidence (the same weight field already being accumulated in Edge). Run this for two or three iterations and a node with no direct evidence of compromise, but three high-risk neighbors, converges toward a high score — which is precisely the "quiet pivot host" pattern that stateless correlation rules miss every time.

Python
 
def propagate_risk(graph, decay=0.6, iterations=3):
    """
    Simple belief-propagation-style pass: a node's risk score
    is boosted by the risk of its neighbors, discounted by
    graph distance and edge confidence.
    """
    for _ in range(iterations):
        updates = {}

        for node in graph.nodes():
            neighbor_risk = sum(
                graph.nodes[n]["risk"] * graph.edges[node, n].get("confidence", 0.5)
                for n in graph.neighbors(node)
            )

            updates[node] = min(
                1.0,
                graph.nodes[node]["risk"] + decay * neighbor_risk /
                max(len(list(graph.neighbors(node))), 1)
            )

        for node, new_risk in updates.items():
            graph.nodes[node]["risk"] = new_risk

    return graph


This is the mechanism that lets low-confidence signals accumulate into a high-confidence finding — the thing rule-based SIEMs structurally cannot do.

6. The LLM Explanation Layer

The model's job here is narrow and disciplined: take a retrieved subgraph — already scored, already tagged against ATT&CK — and produce a human-readable hypothesis with explicit citations back to the underlying evidence nodes. It does not invent the graph. It explains the graph.

Python
 
def build_explanation_prompt(subgraph_summary: dict) -> str:
    return f"""You are producing an incident hypothesis for a human analyst.
Use ONLY the evidence provided below.
Do not infer facts not present.
Cite the node ID for every claim you make.

Evidence nodes:
{json.dumps(subgraph_summary['nodes'], indent=2)}

Risk-scored paths:
{json.dumps(subgraph_summary['paths'], indent=2)}

Produce:
1. A one-paragraph hypothesis of what is happening.
2. The three most important evidence nodes supporting it, cited by ID.
3. A confidence level (low/medium/high) with a one-sentence justification.
4. The single most useful next containment action, and its blast radius.
"""


Constraining the model to cite node IDs is what makes this auditable. An analyst — or a compliance reviewer six months later — can walk from the model's sentence straight back to the log line that produced it. That traceability is the difference between "AI-assisted" and "AI-generated fiction that happens to be well-formatted."

7. Human Decision and Scoped Automated Containment

The human stays in the loop for anything irreversible. What automation handles is scoped, reversible action — isolating a single host from the network, revoking a single session token — gated by policy written in Open Policy Agent so containment logic is testable and version-controlled, not buried in a vendor's black box.

Shell
 
package containment

default allow_isolate = false

allow_isolate {
    input.action == "isolate_host"
    input.risk_score > 0.85
    input.blast_radius_hosts <= 1
    input.requires_human_approval == false
}

allow_isolate {
    input.action == "isolate_host"
    input.risk_score > 0.6
    input.human_approved == true
}


Why the Gap Is the Point

Every generation of infrastructure eventually discovers that the abstractions it trusted stopped being sufficient. Firewalls gave way to Zero Trust. Static IAM gave way to continuous identity evaluation. Signature detection gave way to behavioral analytics. Alert-based SOCs are the next abstraction due for replacement — not because the analysts running them are doing anything wrong, but because the data model underneath them was never built to accumulate evidence across time and identity in the first place.

AI will not replace analysts. But a system that remembers, reasons over, and can explain evidence across a persistent graph can replace the architecture analysts are currently forced to work inside — one alert, one tool, one tab at a time.

SentinelIQ is my attempt at building toward that, in the open, with the current limitations left visible rather than hidden. The in-memory graph, the single hardcoded technique mapping, the lack of a real graph database — none of that is dressed up here as more than it is. What I'd ask a reader to take from this isn't "the system is finished." It's that the direction is right, the current code proves the core idea works end-to-end, and the roadmap from here — graph database backing, broadened ATT&CK coverage, an LLM explanation layer constrained to cite its evidence — is concrete enough to execute against, not just to gesture at.

That's a more useful thing to have built than a finished demo. Finished demos get forgotten. Correct architectural bets, executed visibly over time, are what get someone to open a repo and actually read the code.

Graph (Unix) large language model security

Opinions expressed by DZone contributors are their own.

Related

  • Prompt Injection Attacks and Hidden Security Risks in LLM Applications
  • Testing Strategies for Web Development Code Generated by LLMs
  • Conversational Risk Accumulation: Stateful Guardrails Beyond Single-Turn LLM Checks
  • Security in the Age of MCP: Preventing "Hallucinated Privilege"

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook