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.
Definition
Section titled “Definition”SentinelOne Singularity is the platform; the agent is a kernel driver set plus userland services making local verdicts. Publicly documented concepts:
| Concept | What it is | Notes |
|---|---|---|
| Storyline | Process lineage tracking: all activity grouped by originating process tree | The 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 AI | Local verdicts: static + behavioral models on the agent | Works disconnected [verify offline-response scope] |
| Automated rollback | File-operation journal enabling ransomware-damage reversal | The differentiator |
| RemoteOps / Ranger | Response scripting / network discovery | Fleet 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.
Why It Matters
Section titled “Why It Matters”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.
Detailed Explanation
Section titled “Detailed Explanation”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.
Deep Visibility: the hunting surface
Section titled “Deep Visibility: the hunting surface”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.
Common Techniques
Section titled “Common Techniques”| SentinelOne surface | What it sees | Operator consideration |
|---|---|---|
| Kernel taps + Storyline | Full lineage-grouped events | Sequence discipline is native; fragmentation is itself a signal |
| On-sensor AI | Local static + behavioral verdicts | Assume kill-on-high-confidence even offline |
| Rollback journal | File-operation history | Mass file damage from one lineage = undone |
| Deep Visibility | Fleet-searchable stories | Assume human story-readers weekly |
| Network quarantine | Host isolation w/ vendor channel | Response clock (E10 lesson) applies |
| Agent self-protection | Tamper controls [verify specifics] | Never plan through the agent |
Full Working Code
Section titled “Full Working Code”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 defaultdictfrom dataclasses import dataclass, field
@dataclassclass 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()Code Walkthrough
Section titled “Code Walkthrough”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.
How It Reaches The Target
Section titled “How It Reaches The Target”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).
Exploitation Chain
Section titled “Exploitation Chain”- Pre-op — same infrastructure discipline as E10: single-use everything, because DV joins on hashes/domains the same way Threat Graph does
- Lineage design — shallow trees, legitimate parents: every action inherits the story, so design the story deliberately
- Execution — on-sensor AI scores locally: no detection-only assumption; high-confidence shapes get killed offline
- File effects — no mass operations from one lineage: rollback turns ransomware shapes into demonstrations + undo
- Persistence — registry/script events are story-stamped: assume the Run-key write is part of a readable narrative
- Response clock — quarantine + RemoteOps scripting means minutes-to-analyst: pre-decided abort criteria
- Post-op — the story objects persist in DV: plan for the engagement being readable later, story by story
Building Your Own Tool
Section titled “Building Your Own Tool”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 joinPure stdlib. Python 3.8+."""import argparseimport jsonimport sysfrom 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())Walkthrough
Section titled “Walkthrough”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.
Compilation and test procedure
Section titled “Compilation and test procedure”$ python dvquery.pyS001: root=winword.exe score=90 verdict=suspicious events=5S002: root=explorer.exe score=70 verdict=suspicious events=3$ python dvquery.py --story S001Storyline 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.exampledomain 'c2.example' seen in 2 event(s): S001: [net_conn] c2.example:443 S002: [net_conn] c2.example:443The 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.
Detection considerations
Section titled “Detection considerations”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.
Improvements to add
Section titled “Improvements to add”- Time-windowing: add event timestamps and shape rules that require ordering (script then net_conn), matching real behavioral logic.
- Verdict calibration: replace toy weights with a logistic model trained on labeled synthetic stories — then compare ROC against the shape rules.
- Intel join: external JSON feed (domain age, hash reputation) wired into
join_domain. - Journal simulation: model the rollback journal with size limits and evictions — the honest edge cases of “undo.”
- Sigma exporter: emit DV query syntax stubs [verify current syntax per console version] for the SOC’s real console.
Detection — Defender View
Section titled “Detection — Defender View”| Source | ID | What fires | Notes |
|---|---|---|---|
| Singularity console | threat IDs | Agent-verdicted threats w/ story context | Primary SOC view |
| Deep Visibility | event records | Storyline-grouped telemetry | The hunting surface |
| Agent actions | — | kill/quarantine/rollback audit trail | Response actions are logged events |
| Sysmon (co-deployed) | 1,3,7,8,10,11,13,22 | Independent verification stream | Cross-vendor joins |
| Windows Security | 7045, 4688, 4104 | Native corroborations | Court-admissible layer |
| Wazuh | forwarding DV exports [verify integration path] | SIEM correlation | BT3 pipeline |
Evasion Techniques
Section titled “Evasion Techniques”- 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-domainon 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.
Lab Practice
Section titled “Lab Practice”- 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. - Run
dvquery.pyin all four modes; then extend it with a--join-sha256mode and validate it catches hash reuse across stories. - 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. - 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.
- 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.
Interview Questions
Section titled “Interview Questions”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.
Related Modules
Section titled “Related Modules”E10 (CrowdStrike Detection Logic) · E11 (Defender Detection Logic) · E13 (Carbon Black Detection Logic) · E2 (EDR Internal Architecture)