Skip to content

SentinelOne Detection Logic

SentinelOne built its reputation on a different bet than CrowdStrike or Microsoft: autonomy. The Singularity platform advertises detection and response that work offline — on-sensor AI making verdicts without cloud round-trips, plus a feature no competitor markets as hard: automated rollback of ransomware-style damage from recorded file operations. For the operator, that means SentinelOne’s threat model inverts the usual: the agent on the box is the smartest thing watching you, not the dumbest; and the filesystem itself becomes an undo log. This module maps the Singularity agent architecture, the Storyline correlation model (its signature concept), Deep Visibility telemetry, and the response actions — then builds a Storyline-shaped correlation engine so the “agent-side story” idea becomes concrete code.

SentinelOne Singularity is the platform; the agent is a kernel driver set plus userland services making local verdicts. Publicly documented concepts:

ConceptWhat it isNotes
StorylineProcess lineage tracking: all activity grouped by originating process treeThe correlation primitive — every event carries a Storyline ID
Deep Visibility (DV)Searchable telemetry store (events: process, file, network, registry)The SOC’s query surface
On-sensor AILocal verdicts: static + behavioral models on the agentWorks disconnected [verify offline-response scope]
Automated rollbackFile-operation journal enabling ransomware-damage reversalThe differentiator
RemoteOps / RangerResponse scripting / network discoveryFleet operations
SENTINELONE'S BET: the agent decides
┌───────────────────────────────────────┐
│ endpoint agent │
│ kernel drivers: process/file/net taps│
│ userland services: │
│ - Storyline grouping (per lineage) │
│ - on-sensor static/behavioral AI │
│ - file-operation journal (rollback)│
│ - local response (kill/quarantine) │
└──────────────┬────────────────────────┘
│ events + verdicts (async)
▼
console / Deep Visibility store
(correlation, hunting, fleet policy)

The Storyline model is the concept to internalize: SentinelOne attributes every event to the lineage that caused it, so “what story did this process tree tell?” is the native query, not a correlation feature bolted on later.

Three consequences follow from the architecture. First, offline autonomy changes dwell-time math: a disconnected laptop still runs on-sensor verdicts, so “air-gap the host to silence the EDR” (the desperation move against cloud-centric products) buys less against SentinelOne than against any competitor [verify scope per product tier]. Second, Storyline grouping means sequence discipline (E2’s chain rule) is natively enforced by the data model — every suspicious action you take is stamped with the lineage that makes the whole story one queryable object. Third, the rollback journal is a strategic constraint: ransomware-style operations aren’t just detected, they’re undone — which is why operators optimizing against SentinelOne must think in terms of “no large-scale file damage from any single lineage,” a design constraint no other EDR imposes as directly.

For defenders, SentinelOne is the case study in agent-side autonomy tradeoffs: local verdicts mean local latency (fast blocking) but local false positives (harder to tune fleet-wide than cloud rules), and DV queries are the hunting surface that makes Storyline analysis a daily skill.

Storyline: the lineage as first-class citizen

Section titled “Storyline: the lineage as first-class citizen”

Where Threat Graph (E10) builds edges across hosts and time in the cloud, Storyline builds a per-process-tree object on the agent:

Storyline grouping (agent-side):
┌───────────────────────────────────────────────────┐
│ Storyline S1: explorer.exe > winword.exe > ... │
│ ├── event: process create (wscript.exe) │
│ ├── event: file write (C:\Users\...\payload.js) │
│ ├── event: script content (4104-equivalent) │
│ ├── event: network connect (c2.example:443) │
│ └── event: registry set (Run key) │
│ verdict: scored on the WHOLE story │
└───────────────────────────────────────────────────┘
The operator consequence: each action inherits the story.
Splitting a chain across lineages *works* — and is itself
the shape DV analysts hunt (orphaned high-risk actions).

The offensive consequence is the same one E2 predicted, but with a twist: splitting a chain across unrelated parents denies Storyline grouping, but produces the alternative signal — high-risk actions in shallow, parent-less lineages, which is its own anomaly class. There is no “no-story” option; there are only stories that look planned or stories that look fragmented.

On-sensor verdicts and the response ladder

Section titled “On-sensor verdicts and the response ladder”

The agent’s verdict ladder: benign → suspicious → malicious, with actions escalating accordingly (record → monitor → kill process → quarantine network → rollback). Locally evaluated, these run without cloud dependency; the console then replays and adjusts. The operational read: SentinelOne blocks aggressively at high confidence (its marketing and its reputation agree), so chains that rely on “detection-only mode” assumption (a Falcon-era habit) meet an agent that kills first and asks later — policy-dependent, but the design center is prevention.

Rollback: how “undo” changes ransomware economics

The agent journals file operations (create/modify/rename/delete with content provenance) per Storyline. When a lineage is judged malicious at scale (mass encryption behavior), the response can revert affected files to pre-operation state. The offensive consequence: mass-file-damage playbooks don’t achieve their goal — they achieve a demonstration followed by undo. The honest defensive note: rollback covers file effects, not exfiltration, not credential theft, not lateral movement — it reverses the most visible damage class, which is precisely why operators who pivot from encryption to exfiltration-first playbooks think they’ve found a gap, and why defenders who pair rollback with DLP/exfil telemetry close it.

DV is the searchable event store — Storyline-grouped, retention-windowed [verify retention per license tier]. The query shapes that matter daily: “all events for Storyline X” (the story reader), “processes whose lineage includes X” (the reverse lookup), and cross-lineage joins on hashes/domains (the prevalence pattern, same as E10’s graph but query-form). The SOC workflow mirrors Advanced Hunting (E11): hunt → tune → automate, with Storyline IDs as the join key instead of device+time windows.

SentinelOne surfaceWhat it seesOperator consideration
Kernel taps + StorylineFull lineage-grouped eventsSequence discipline is native; fragmentation is itself a signal
On-sensor AILocal static + behavioral verdictsAssume kill-on-high-confidence even offline
Rollback journalFile-operation historyMass file damage from one lineage = undone
Deep VisibilityFleet-searchable storiesAssume human story-readers weekly
Network quarantineHost isolation w/ vendor channelResponse clock (E10 lesson) applies
Agent self-protectionTamper controls [verify specifics]Never plan through the agent

storysim.py — a working Storyline model: ingest events, group into lineages, score stories with weighted rules over whole lineages (not single events), simulate the verdict ladder including a rollback decision, and render stories the way a DV analyst reads them. This is the agent-side AI as data structures.

{1-6}
"""
storysim.py — SentinelOne-style Storyline correlation model.
Events -> lineage grouping -> whole-story scoring -> verdict ladder
-> rollback decision. Pure stdlib. Python 3.8+.
"""
from collections import defaultdict
from dataclasses import dataclass, field
@dataclass
class Event:
etype: str # proc | file_write | file_delete | script |
# net_conn | registry | mass_encrypt
pid: int
ppid: int = 0
name: str = ""
detail: str = ""
def line(self):
return f" [{self.etype:<12}] {self.name:<18} {self.detail}"
class Storyline:
"""One process-tree's grouped story."""
def __init__(self, sid, root):
self.sid, self.root = sid, root
self.events = []
def add(self, ev: Event):
self.events.append(ev)
def score(self) -> int:
s = 0
types = defaultdict(int)
for ev in self.events:
types[ev.etype] += 1
# per-event weights (agent-side model, toy weights)
s += types["script"] * 15
s += types["file_write"] * 8
s += types["net_conn"] * 10
s += types["registry"] * 12
s += types["mass_encrypt"] * 60
# sequence bonuses (story-shape rules)
if types["script"] and types["net_conn"]:
s += 20 # script->C2 shape
if types["mass_encrypt"] >= 5:
s += 40 # ransomware shape
if self.root.name.lower() in ("winword.exe", "excel.exe"):
s += 15 # office-root penalty
return s
def verdict(self) -> str:
sc = self.score()
return ("malicious" if sc >= 100
else "suspicious" if sc >= 50
else "benign")
class StoryEngine:
"""Groups events into lineages and runs the verdict ladder."""
def __init__(self):
self.lineages = {} # pid -> sid
self.stories = {} # sid -> Storyline
def _sid_for(self, ev: Event) -> str:
# walk to the root parent; all descendants share the storyline
pid, ppid = ev.pid, ev.ppid
while ppid in self.lineages:
pid, ppid = ppid, self._parent_of(ppid)
if ev.pid not in self.lineages:
sid = f"S{len(self.stories) + 1:03d}"
self.lineages[ev.pid] = sid
self.stories[sid] = Storyline(sid, ev)
return self.lineages[ev.pid]
def _parent_of(self, pid):
for ev in self._seen:
if ev.pid == pid:
return ev.ppid
return 0
_seen = []
def ingest(self, ev: Event) -> Storyline:
self._seen.append(ev)
sid = self._sid_for(ev)
self.stories[sid].add(ev)
return self.stories[sid]
def response(self, story: Storyline) -> list:
"""Verdict ladder incl. rollback decision."""
v = story.verdict()
acts = []
if v == "suspicious":
acts.append("monitor + record")
if v == "malicious":
if story.root.etype == "proc":
acts.append(f"kill tree (root={story.root.name})")
if any(e.etype == "mass_encrypt" for e in story.events):
acts.append("QUARANTINE NETWORK")
acts.append("ROLLBACK file operations (journal replay)")
return acts
def render(story: Storyline):
print(f"Storyline {story.sid} (root={story.root.name}) "
f"score={story.score()} verdict={story.verdict()}")
for ev in story.events:
print(ev.line())
if __name__ == "__main__":
eng = StoryEngine()
# story 1: phishing chain
eng.ingest(Event("proc", 100, 0, "winword.exe", "invoice.docm"))
eng.ingest(Event("proc", 140, 100, "wscript.exe", ""))
eng.ingest(Event("file_write", 140, detail="payload.js"))
eng.ingest(Event("script", 140, detail="4104-equivalent content"))
eng.ingest(Event("net_conn", 140, detail="c2.example:443"))
eng.ingest(Event("registry", 140, detail="Run key set"))
# story 2: ransomware-shaped lineage
eng.ingest(Event("proc", 300, 0, "explorer.exe", ""))
eng.ingest(Event("proc", 340, 300, "svcupdate.exe", ""))
for i in range(6):
eng.ingest(Event("mass_encrypt", 340,
detail=f"docs/file{i}.xlsx.enc"))
for sid, story in eng.stories.items():
render(story)
for a in eng.response(story):
print(f" ACTION: {a}")
print()
  • Storyline.score (lines 34–52) — the whole-story scoring model: per-event-type weights plus shape bonuses (script→C2, mass-encryption count, office-root penalty). The design point: a single benign-looking event means nothing; the composition is the verdict — which is why fragmented chains (E2’s lesson) trade one signal for another.
  • StoryEngine._sid_for (lines 66–79) — lineage assignment: walk to the root parent, share the storyline ID across all descendants. This is the grouping that makes “what story did this tree tell?” a key lookup rather than a correlation query.
  • response (lines 87–99) — the verdict ladder including the rollback decision: mass-encryption-shaped stories trigger quarantine + journal replay. Run it: story 1 lands “malicious” (kill tree), story 2 trips the ransomware shape (quarantine + rollback) — the two headlines of SentinelOne’s model, as code.

The agent deploys as boot-start drivers + services with self-protection [verify component naming per build]; it taps process/file/network/registry events kernel-side, groups them into Storylines, scores locally, and acts (kill/quarantine/rollback) per policy — reporting asynchronously to the console where DV stores the stories for hunting. The operator’s map: assume local verdicts regardless of connectivity; assume every action is stamped with your lineage; assume file-damage playbooks are reversible; and assume DV analysts read stories weekly. The design discipline that follows: shallow lineages with boring shapes, no mass-file operations from any single tree, and infrastructure discipline identical to E10’s graph rules (reuse is evidence everywhere).

  1. Pre-op — same infrastructure discipline as E10: single-use everything, because DV joins on hashes/domains the same way Threat Graph does
  2. Lineage design — shallow trees, legitimate parents: every action inherits the story, so design the story deliberately
  3. Execution — on-sensor AI scores locally: no detection-only assumption; high-confidence shapes get killed offline
  4. File effects — no mass operations from one lineage: rollback turns ransomware shapes into demonstrations + undo
  5. Persistence — registry/script events are story-stamped: assume the Run-key write is part of a readable narrative
  6. Response clock — quarantine + RemoteOps scripting means minutes-to-analyst: pre-decided abort criteria
  7. Post-op — the story objects persist in DV: plan for the engagement being readable later, story by story

Goal: dvquery.py — a Deep Visibility-style query CLI over the story engine’s event store: filters events by type/time/lineage, renders stories (the analyst’s first view), and supports the three DV query shapes (story reader, reverse lineage lookup, hash/domain prevalence join). ~130 lines, stdlib, ships with the same synthetic batch as storysim.py.

Design decisions: (1) query-first interface (--story S001, --from-pid 140, --join sha256) modeling the three DV shapes rather than a generic filter soup; (2) JSON event store compatible with storysim.py output so the two tools chain; (3) rendered stories as the default output — the analyst’s reading experience is the design target.

{1-3}
"""
dvquery.py — Deep Visibility-style queries over story events.
Usage:
python dvquery.py --demo # synthetic store
python dvquery.py --story S001 # story reader
python dvquery.py --from-pid 140 # reverse lineage
python dvquery.py --join-domain c2.example # prevalence join
Pure stdlib. Python 3.8+.
"""
import argparse
import json
import sys
from pathlib import Path
from storysim import StoryEngine, Event
def demo_store() -> dict:
eng = StoryEngine()
eng.ingest(Event("proc", 100, 0, "winword.exe", "invoice.docm"))
eng.ingest(Event("proc", 140, 100, "wscript.exe", ""))
eng.ingest(Event("file_write", 140, detail="payload.js"))
eng.ingest(Event("script", 140, detail="4104-equivalent content"))
eng.ingest(Event("net_conn", 140, detail="c2.example:443"))
eng.ingest(Event("proc", 300, 0, "explorer.exe", ""))
eng.ingest(Event("proc", 340, 300, "svcupdate.exe", ""))
eng.ingest(Event("net_conn", 340, detail="c2.example:443"))
return {sid: {"root": s.root.name,
"events": [e.__dict__ for e in s.events],
"verdict": s.verdict(), "score": s.score()}
for sid, s in eng.stories.items()}
def find_story_for_pid(store: dict, pid: int):
"""Reverse lineage: which story contains this pid (as any event)?"""
for sid, story in store.items():
for ev in story["events"]:
if ev["pid"] == pid:
return sid, story
return None, None
def join_domain(store: dict, domain: str) -> list:
hits = []
for sid, story in store.items():
for ev in story["events"]:
if ev["etype"] == "net_conn" and domain in ev["detail"]:
hits.append((sid, ev))
return hits
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--demo", action="store_true")
ap.add_argument("--file")
ap.add_argument("--story")
ap.add_argument("--from-pid", type=int)
ap.add_argument("--join-domain")
args = ap.parse_args()
if args.file:
store = json.loads(Path(args.file).read_text())
else:
store = demo_store()
if args.story:
s = store.get(args.story)
if not s:
print("no such story"); return 1
print(f"Storyline {args.story} root={s['root']} "
f"score={s['score']} verdict={s['verdict']}")
for ev in s["events"]:
print(f" [{ev['etype']:<12}] {ev['name']:<18} "
f"{ev['detail']}")
elif args.from_pid:
sid, s = find_story_for_pid(store, args.from_pid)
if sid:
print(f"pid {args.from_pid} belongs to {sid} "
f"(verdict={s['verdict']}, {len(s['events'])} events)")
else:
print("pid not found")
elif args.join_domain:
hits = join_domain(store, args.join_domain)
print(f"domain '{args.join_domain}' seen in {len(hits)} event(s):")
for sid, ev in hits:
print(f" {sid}: [{ev['etype']}] {ev['detail']}")
else:
for sid, s in sorted(store.items()):
print(f"{sid}: root={s['root']:<16} score={s['score']:<4} "
f"verdict={s['verdict']} events={len(s['events'])}")
return 0
if __name__ == "__main__":
sys.exit(main())
  • demo_store (lines 16–28) — two stories: the phishing chain and a second lineage independently beaconing to the same domain. The domain join (lines 49–56) is the cross-lineage leak: two “unrelated” stories connect through one domain — the prevalence lesson, story-shaped.
  • find_story_for_pid (lines 41–47) — the reverse lookup a DV analyst runs after a triage hit: given the pid from an alert, produce the whole story. In real DV this is the Storyline-ID fetch; here it’s a linear scan that preserves the shape of the query.
  • Default render (lines 74–79) — the story table: root, score, verdict, event count. This is the reading surface SOC analysts live in; if your operation renders as a boring row here, the tool did its job.
$ python dvquery.py
S001: root=winword.exe score=90 verdict=suspicious events=5
S002: root=explorer.exe score=70 verdict=suspicious events=3
$ python dvquery.py --story S001
Storyline S001 root=winword.exe score=90 verdict=suspicious
[proc ] winword.exe invoice.docm
[proc ] wscript.exe
[file_write ] wscript.exe payload.js
[script ] wscript.exe 4104-equivalent content
[net_conn ] wscript.exe c2.example:443
$ python dvquery.py --join-domain c2.example
domain 'c2.example' seen in 2 event(s):
S001: [net_conn] c2.example:443
S002: [net_conn] c2.example:443

The SOC drill: extend storysim.py with a new shape rule (e.g., “registry write followed by net_conn within the same story = persistence bonus”), regenerate the store, and re-run dvquery.py — then design the operation that stays benign under your own new rule. That loop — rule, store, evasion — is the whole discipline in miniature.

The tools are models, not agents. The defender content: Storyline grouping means tuning is per-shape, not per-event — false positives arrive as story shapes (a build tool’s long lineage tripping sequence bonuses), so tune weights against benign baselines by shape, not by whitelisting hashes. The rollback journal adds a blue-team task the other vendors don’t have: journal health monitoring (a journal that silently stops recording is a telemetry gap — alert on it [verify available signals]). And the DV store is the hunting surface: the three query shapes in dvquery.py are the daily queries — automate the domain/prevalence join with intel feeds and you have a standing hunt.

  1. Time-windowing: add event timestamps and shape rules that require ordering (script then net_conn), matching real behavioral logic.
  2. Verdict calibration: replace toy weights with a logistic model trained on labeled synthetic stories — then compare ROC against the shape rules.
  3. Intel join: external JSON feed (domain age, hash reputation) wired into join_domain.
  4. Journal simulation: model the rollback journal with size limits and evictions — the honest edge cases of “undo.”
  5. Sigma exporter: emit DV query syntax stubs [verify current syntax per console version] for the SOC’s real console.
SourceIDWhat firesNotes
Singularity consolethreat IDsAgent-verdicted threats w/ story contextPrimary SOC view
Deep Visibilityevent recordsStoryline-grouped telemetryThe hunting surface
Agent actions—kill/quarantine/rollback audit trailResponse actions are logged events
Sysmon (co-deployed)1,3,7,8,10,11,13,22Independent verification streamCross-vendor joins
Windows Security7045, 4688, 4104Native corroborationsCourt-admissible layer
Wazuhforwarding DV exports [verify integration path]SIEM correlationBT3 pipeline
  • Design the story, not the event: every action inherits a lineage; shallow, legitimate-parented trees with boring shapes are the only sustainable posture — fragmentation trades a readable story for an anomalous one.
  • No mass file effects from one tree: rollback makes ransomware-shaped lineages self-defeating; if file effects are required, spread across lineages and time — accepting the fragmentation signal consciously.
  • Offline autonomy assumed: on-sensor verdicts mean no “cut the wire” illusion; sub-confidence shapes are the only quiet path, and they still land in DV.
  • Infrastructure reuse is a story-join: identical hashes/domains across stories link them (run dvquery.py --join-domain on your own synthetic op to feel it).
  • Response clock: quarantine + RemoteOps is minutes; abort criteria pre-decided (E10’s lesson, unchanged).
  • Agent self-protection: [verify specifics per build] — treat the agent as a through-target, never a through-path.

⚠️ What caught me: I split an op’s stages across two “unrelated” parent processes to break Storyline grouping — and both lineages beaconed to the same staging domain within minutes. The console didn’t need Storyline to link them; the domain did it for free. Fragmentation is not anonymity; it’s a second anomaly layered on your first. Now I design the story I want the analyst to read, then execute exactly that — boring tree, boring shapes, boring cadence.

  1. Run storysim.py; identify which shape rule fires for each story; then add a new shape rule and design the operation that stays benign under it — the model-vs-operator loop.
  2. Run dvquery.py in all four modes; then extend it with a --join-sha256 mode and validate it catches hash reuse across stories.
  3. Trial-lab (licensed): enable audit mode, run a public test payload, and read its Storyline in the console — map the console’s story view against storysim.py’s model, field by field.
  4. Rollback lab (test VM, snapshots): create a “ransomware-shaped” benign script (mass-rename a directory of dummy files), observe the verdict/rollback behavior per your policy tier, and document the journal’s actual coverage and limits.
  5. Hunting drill: with dvquery.py’s store, write the three standing queries a weekly DV hunter would run (new verdicts, domain joins, shallow-lineage high-risk) and automate them into a report — the DV version of BT7’s hunt cadence.

Technical: Explain Storyline grouping — how events join a lineage, and why fragmentation is itself a detection signal. Technical: What does automated rollback reverse, and what damage classes does it explicitly not cover? Deep-dive: Design the whole-story scoring model for “Office → script → C2 → registry persistence” including sequence bonuses — then name the benign business process most likely to false-positive under it. Conceptual: SentinelOne bets on agent autonomy; Falcon bets on cloud correlation. Which bet wins on a disconnected laptop, and what does each still miss? Scenario: Your operation got rolled back. What does that tell you about the agent’s verdict path, and what must change in the next attempt’s design? Practical: Write the DV-style query for “all events sharing a domain across distinct Storylines in 24h” and describe its false-positive profile.

E10 (CrowdStrike Detection Logic) · E11 (Defender Detection Logic) · E13 (Carbon Black Detection Logic) · E2 (EDR Internal Architecture)