Skip to content

Carbon Black Detection Logic

Carbon Black is the oldest institution in this series’ vendor lineup: the original Bit9 built its business on whitelisting (only known-good binaries run), the merged company’s Cb Response invented the streaming process-telemetry model most EDRs now standardize, and the modern VMware Carbon Black Cloud (now Broadcom’s [verify ownership transitions]) blends reputation, behavioral analytics, and one feature that still defines its personality: watchlists — user-authored queries that run continuously over telemetry. Understanding Carbon Black means understanding the trust-model lineage (reputation before behavior), the query-driven SOC workflow, and how a detection philosophy built on “know what runs” meets a threat model built on “trust what’s prevalent.” This module maps the architecture and builds a working watchlist engine.

Carbon Black Cloud (CBC) is the modern platform; its detection stack layers four models with distinct ancestries:

LayerAncestryWhat it decides
Reputation / allowlistingBit9 (2002-era roots)Is this binary known-good (global/local prevalence, publisher trust)?
Next-gen AVCb Defense eraFile-scoring ML + signatures on the sensor
Behavioral analyticsCb Response heritageProcess/child/behavior sequences → “TTP” detections
WatchlistsCb Response’s query modelContinuous custom queries over telemetry → alerts
CBC data flow:
┌──────────────┐ events ┌─────────────────────────────┐
│ sensor │────────────▶│ cloud: analytics pipeline │
│ (kernel taps │ HTTPS │ - reputation lookups │
│ + policy │◀────────────│ - behavioral TTP models │
│ enforcement)│ policy │ - watchlist evaluation │
└──────────────┘ │ - alert triage/incidents │
└─────────────────────────────┘
Distinctive: reputation is a FIRST-CLASS verdict layer,
inherited from the whitelisting lineage. Unknown ≠ detected,
but unknown + risky-behavior = the classic CBC alert shape.

The product-name archaeology matters for reading documentation: Cb Response → Audit and Remediation (telemetry/watchlists), Cb Defense → Endpoint Standard/Enterprise EDR (prevention+EDR), Bit9 → the reputation layer’s roots. Job posts and docs still mix the eras.

Carbon Black is the reputation-first archetype: where Falcon leads with behavioral IOAs and MDE with OS integration, CBC’s lineage makes binary trust (who signed it, how prevalent is it, is it on an approved list) the first verdict an operation faces. For operators, that flips the optimization: an unknown but boring binary on a CBC estate faces a different first gate than on competitor estates — local trust decisions, policy-driven (loose vs strict), with behavior analytics behind. For defenders, CBC’s watchlist model is the most accessible “detections as queries” education in the industry — you write the query, it runs continuously, it alerts — which makes it the best teaching ground for BT7-style hunt engineering. And historically, Cb Response’s process telemetry (2013-era) is the reason “process tree” is universal EDR vocabulary — this module is also where that lineage gets its due.

The modern reality check: Broadcom’s acquisition of VMware (and Carbon Black with it, 2023-2024 [verify final product naming]) means estates are mid-migration; reading CBC’s docs means triaging era-specific terminology, and both red and blue teams need the map of which product generation a client is actually running.

The founding Bit9 model: only trusted binaries execute. Modern CBC inherits it as the reputation layer — every binary gets a reputation (trusted / not-yet-listed / suspicious / denied) built from: publisher signing state, global prevalence (how many CBC installations run it [verify aggregation scope]), local policy lists, and cloud intelligence. Policy modes range from monitor (record everything, block nothing — the Cb Response heritage) through moderate (block only denied/suspicious) to strict (block anything not explicitly trusted — true whitelisting, rare outside locked-down estates).

The operator consequence: on monitor/moderate estates, unknown binaries run — but they enter the telemetry with “unknown reputation” attached, which is exactly the pivot behavioral analytics and watchlists use. On strict estates, unknown = blocked at execution: the whole game becomes approved-binary abuse (LOLBins, signed-tool abuse — the T1218 family) because bringing your own binary is impossible.

Behavioral analytics: the Cb Response lineage

Section titled “Behavioral analytics: the Cb Response lineage”

Cb Response’s original insight (2013): stream process telemetry — every process create, child spawn, file mod, network connect with full lineage — to a backend where analysts query it. Modern CBC behavioral analytics automate that: sequence models flag TTP-shaped stories (credential-dump tooling patterns, injection sequences, LOLBin chains) with the same process-tree vocabulary every other vendor now uses. The distinctive CBC flavor: detections arrive with the underlying query-ish logic visible in triage — the analyst sees the matched behavior pattern, not just a score, which traces directly to the watchlist tradition.

Watchlists: detections as living queries

A watchlist is a saved query (over process/file/registry/network telemetry) that the platform runs continuously, alerting on new matches. Examples that map to this series: “processes whose parent is Office and which spawn script hosts” (E10’s IOA as a query), “hashes with fewer than N local sightings in 30 days executing script hosts” (reputation + behavior join), “registry writes to Run keys by processes with unknown reputation” (persistence hunt). Watchlists are where your estate knowledge becomes detections: the vendor’s models catch generic TTPs; your watchlists catch your estate’s anomalies (the finance host that never runs PowerShell suddenly running it). The skill is pure BT7 and transfers to any query-driven stack (KQL custom detections in E11, DV queries in E12).

CBC’s Live Response (the remote-shell capability, like RTR/Live Response in E10/E11) plus the alert triage pipeline (alerts → incidents → response actions) complete the operational picture. The CBC-specific triage habit worth stealing for any stack: start from the process tree — CBC’s triage UI is built around the tree, and reading detections as tree stories (not as event lists) is the skill that makes every other vendor’s console easier.

CBC layerFirst gateOperator consideration
Reputation (monitor/moderate)Unknown-but-runs, flaggedBoring shape still matters; unknown is recorded
Reputation (strict)Unknown = blockedApproved-binary abuse only (T1218 family)
Next-gen AVFile ML scoringE1’s feature shaping applies
Behavioral TTPsSequence shapesE2’s chain discipline applies
WatchlistsEstate-specific queriesTest-estate skew: your recon misses client watchlists
Live ResponseAnalyst clockResponse-time planning (E10/E12 lessons)

cbwatch.py — a working watchlist engine: streams process/file/network events, maintains reputation state (global prevalence + local sightings + publisher trust), evaluates three built-in watchlists continuously, and emits triaged alerts with process-tree context. This is the CBC SOC model as runnable code.

{1-6}
"""
cbwatch.py — Carbon Black-style watchlist + reputation engine.
Streams events -> reputation state -> continuous watchlist eval
-> triaged alerts. Pure stdlib. Python 3.8+.
"""
from collections import defaultdict
from dataclasses import dataclass, field
TRUSTED_PUBLISHERS = {"Microsoft Corporation", "Google LLC"}
GLOBAL_PREVALENCE = { # toy global counts (real: cloud aggregate [verify])
"6f" * 32: 500000, # some ubiquitous binary
}
@dataclass
class Event:
etype: str # proc | file_write | net_conn | registry
pid: int
ppid: int = 0
name: str = ""
cmdline: str = ""
sha256: str = ""
publisher: str = ""
detail: str = ""
def line(self):
return (f" [{self.etype:<11}] pid={self.pid:<5} {self.name:<16} "
f"{self.detail or self.cmdline}")
class ReputationEngine:
"""The Bit9 inheritance: trust as a first-class verdict."""
def __init__(self):
self.local_sightings = defaultdict(int)
def reputation(self, ev: Event) -> str:
if ev.publisher in TRUSTED_PUBLISHERS:
return "trusted-publisher"
self.local_sightings[ev.sha256] += 1
glob = GLOBAL_PREVALENCE.get(ev.sha256, 0)
if glob > 1000:
return "prevalent"
if self.local_sightings[ev.sha256] > 5:
return "locally-common"
return "unknown"
class Watchlist:
"""A saved query, run continuously (the CBC signature)."""
def __init__(self, name, match, mitre=""):
self.name, self.match, self.mitre = name, match, mitre
def run(self, ev: Event, rep: str, tree: list) -> str | None:
if self.match(ev, rep, tree):
return self.name
return None
# watchlist 1: the office-lineage IOA as a query
W1 = Watchlist(
"office-spawned-script-host",
lambda ev, rep, tree: ev.etype == "proc"
and ev.name.lower() in ("powershell.exe", "wscript.exe",
"cscript.exe", "mshta.exe")
and any("winword" in t.name.lower() or "excel" in t.name.lower()
for t in tree),
"T1566.001 + T1059")
# watchlist 2: unknown-reputation binary writing to Run keys
W2 = Watchlist(
"unknown-binary-runkey",
lambda ev, rep, tree: ev.etype == "registry"
and "CurrentVersion\\Run" in ev.detail
and rep == "unknown",
"T1547.001")
# watchlist 3: LOLBin chain depth >= 2 from an unknown root
def _lolbin_depth(ev, rep, tree):
lols = {"rundll32.exe", "regsvr32.exe", "mshta.exe", "certutil.exe"}
depth = sum(1 for t in tree if t.name.lower() in lols)
return ev.etype == "proc" and depth >= 2 and rep == "unknown"
W3 = Watchlist("lolbin-chain-depth2", _lolbin_depth, "T1218")
WATCHLISTS = [W1, W2, W3]
class CbSim:
def __init__(self):
self.rep = ReputationEngine()
self.procs = {} # pid -> Event
self.alerts = []
def tree_of(self, ev: Event) -> list:
chain, cur = [], ev
while cur:
chain.append(cur)
cur = self.procs.get(cur.ppid)
return list(reversed(chain))
def feed(self, ev: Event):
if ev.etype == "proc":
self.procs[ev.pid] = ev
rep = self.rep.reputation(ev)
tree = self.tree_of(ev)
for w in WATCHLISTS:
hit = w.run(ev, rep, tree)
if hit:
self.alerts.append({"watchlist": hit,
"mitre": w.mitre,
"reputation": rep,
"event": ev, "tree": tree})
def triage(self):
for a in self.alerts:
ev = a["event"]
print(f"ALERT [{a['watchlist']}] ({a['mitre']}) "
f"rep={a['reputation']}")
for t in a["tree"]:
print(f" {t.line()}")
print(f" matched: {ev.line()}\n")
if __name__ == "__main__":
sim = CbSim()
sim.feed(Event("proc", 100, 0, "explorer.exe"))
sim.feed(Event("proc", 120, 100, "WINWORD.EXE",
publisher="", sha256="aa" * 32))
sim.feed(Event("proc", 140, 120, "wscript.exe", "run.js",
sha256="bb" * 32))
sim.feed(Event("file_write", 140, 120, "wscript.exe",
detail="payload.js", sha256="cc" * 32))
sim.feed(Event("registry", 140, 120, "wscript.exe",
detail="HKCU\\...\\CurrentVersion\\Run\\Updater",
sha256="bb" * 32))
sim.feed(Event("proc", 200, 0, "services.exe",
publisher="Microsoft Corporation",
sha256="6f" * 32))
sim.triage()
  • ReputationEngine.reputation (lines 33–43) — the Bit9 inheritance as a function: trusted publisher → prevalent (global) → locally-common → unknown. Note the order: publisher trust first, prevalence second — the same order a strict CBC policy evaluates [verify policy semantics per mode]. Every event’s reputation rides along to the watchlists — the layering that makes CBC’s alert shape (“unknown binary did risky thing”) legible.
  • Watchlist (lines 47–58) — the signature concept: a saved query as a function over (event, reputation, process-tree). Three real shapes ship: office-lineage (E10’s IOA), unknown-binary persistence, and LOLBin-chain depth — the T1218 family as a depth counter.
  • CbSim.tree_of (lines 74–80) — process-tree reconstruction from pid/ppid records; the triage view and the watchlists’ shared context. Everything CBC taught the industry is in this five-line walk.
  • __main__ (lines 106–118) — two stories: the phishing chain (two watchlists fire: lineage + run-key-with-unknown-rep) and a Microsoft-signed services.exe (silent — trusted-publisher reputation suppresses the noise). Run it and read the alerts as a CBC analyst would: reputation is part of the alert, not an afterthought.

CBC reaches an endpoint as: sensor install (kernel taps + policy enforcement) → reputation-gated execution per policy mode → telemetry streaming to the cloud analytics pipeline → reputation lookups, behavioral TTP evaluation, continuous watchlist execution → alerts/incidents with tree context → Live Response actions. The operator’s map differs from E10/E12 estates in one gate: reputation first. On monitor/moderate estates, your unknown binary runs but is recorded as unknown — every subsequent risky action is scored against that backdrop, and estate watchlists may already flag exactly your shape. On strict estates, unknown = blocked: the operation becomes approved-binary abuse, and the T1218 family (rundll32/regsvr32/mshta chains) is the toolkit. Test-estate skew is the standing trap: your recon cannot see client watchlists — the estate-specific queries are invisible until they fire.

  1. Policy-mode recon — determine monitor/moderate/strict posture from observable behavior (can a test-signed binary execute? [verify reconnaissance ethics/scope in engagement rules])
  2. Reputation planning — moderate estates: assume unknown-reputation recording; strict estates: approved-binary toolkit only
  3. Stage delivery — boring shapes for file ML (E1 feature budget), signed-or-prevalent parents where possible
  4. Execution — avoid the watchlist shapes you would write yourself: office-lineage scripts, unknown-rep persistence writes, LOLBin chains
  5. Persistence — registry writes inherit reputation: unknown-rep Run-key writes are a canonical watchlist hit (this module’s W2)
  6. Response clock — Live Response on alert: minutes; abort criteria pre-decided
  7. Post-op — telemetry persists in cloud retention: assume the full story is queryable later

Goal: cbquery.py — a watchlist authoring workbench: the SOC-side complement to cbwatch.py. It loads an event JSONL, lets you define watchlists in a small DSL (field/op/value chains with tree and reputation context), runs them over the stream, and reports precision (alerts vs known-bad ground truth in the file) — the watchlist tuning loop as a tool.

Design decisions: (1) DSL over regex soup — watchlists as YAML-ish JSON rules keeps them reviewable and diffable; (2) ground-truth mode so tuning is measured (precision/recall), not vibes; (3) shares cbwatch.py’s reputation engine so reputation context is available to every rule.

{1-3}
"""
cbquery.py — watchlist authoring workbench with precision scoring.
Usage:
python cbquery.py --rules rules.json --events batch.jsonl [--truth truth.json]
Rule shape (rules.json):
[{"name": "...", "mitre": "...", "any": [
{"etype": "proc", "name_in": ["wscript.exe"]},
{"etype": "registry", "detail_has": "Run", "rep": "unknown"}]}]
Pure stdlib. Python 3.8+.
"""
import argparse
import json
import sys
from pathlib import Path
from cbwatch import CbSim, Event, ReputationEngine
def rule_matches(rule: dict, ev: Event, rep: str, tree: list) -> bool:
"""A rule matches if ANY of its clauses match."""
for c in rule.get("any", []):
if c.get("etype") and ev.etype != c["etype"]:
continue
if c.get("name_in") and ev.name.lower() not in \
[n.lower() for n in c["name_in"]]:
continue
if c.get("detail_has") and c["detail_has"].lower() \
not in (ev.detail or "").lower():
continue
if c.get("cmd_has") and c["cmd_has"].lower() \
not in (ev.cmdline or "").lower():
continue
if c.get("rep") and rep != c["rep"]:
continue
if c.get("parent_has") and c["parent_has"].lower() \
not in " ".join(t.name for t in tree).lower():
continue
return True
return False
def load_events(path: str) -> list:
return [Event(**json.loads(l)) for l in
Path(path).read_text().splitlines() if l.strip()]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--rules", required=True)
ap.add_argument("--events", required=True)
ap.add_argument("--truth", help="JSON list of known-bad pids")
args = ap.parse_args()
rules = json.loads(Path(args.rules).read_text())
events = load_events(args.events)
truth = set(json.loads(Path(args.truth).read_text())) \
if args.truth else None
sim = CbSim()
fired_pids = set()
for ev in events:
rep = sim.rep.reputation(ev)
tree = sim.tree_of(ev)
for rule in rules:
if rule_matches(rule, ev, rep, tree):
print(f"ALERT [{rule['name']}] ({rule.get('mitre','')}) "
f"rep={rep} pid={ev.pid}")
fired_pids.add(ev.pid)
sim.feed(ev)
if truth is not None:
tp = len(fired_pids & truth)
fp = len(fired_pids - truth)
fn = len(truth - fired_pids)
print(f"\nprecision: {tp}/{tp+fp} recall: {tp}/{tp+fn}")
return 0
if __name__ == "__main__":
sys.exit(main())
  • rule_matches (lines 17–37) — the DSL evaluator: clauses over event fields (name_in, detail_has, cmd_has), reputation (rep), and tree context (parent_has). Clause semantics are AND within a clause, OR across clauses — the minimal watchlist algebra that covers most real queries.
  • main (lines 47–77) — stream evaluation with reputation context per event, then measured tuning: with ground truth, precision/recall print at the end. This is the difference between a watchlist you wrote and a watchlist you tuned — the CBC education in one flag.
$ cat > rules.json <<'EOF'
[{"name": "script-under-office", "mitre": "T1566.001",
"any": [{"etype": "proc", "name_in": ["wscript.exe","powershell.exe"],
"parent_has": "winword"}]},
{"name": "unknown-runkey", "mitre": "T1547.001",
"any": [{"etype": "registry", "detail_has": "Run", "rep": "unknown"}]}]
EOF
$ python cbquery.py --rules rules.json --events batch.jsonl --truth truth.json
ALERT [script-under-office] (T1566.001) rep=unknown pid=140
ALERT [unknown-runkey] (T1547.001) rep=unknown pid=140
precision: 2/2 recall: 2/3
# the loop: adjust clauses until precision/recall are honest.

The tuning drill: build a 200-event synthetic batch (benign office work + one phishing chain + one admin tool that looks like the chain), write watchlists, and iterate until precision ≥ 0.9 without killing recall — then read your final rule set as an operator: those are the shapes you now avoid by design.

The workbench is SOC-side; its operator-mirror is “watchlists are estate-specific and invisible to recon” — the standing CBC trap. Defender content: watchlist hygiene is the real maintenance cost (rules decay as estates change; schedule quarterly reviews), reputation tuning (locally-common thresholds) drives the FP budget, and Live Response usage audits belong in the SOC’s own metrics (response-action telemetry is a quality signal, not just an audit trail). For BT3 integration: CBC exports events to SIEM pipelines [verify current forwarding options]; watchlists belong in the platform, SIEM rules in the warehouse — overlap is waste, gaps are risk, and the E10/E11/E13 trilogy is the vocabulary for drawing that line.

  1. Time-window clauses (events within N seconds of a sibling clause) — sequence watchlists.
  2. Reputation decay (locally-common with half-life) — honest prevalence modeling.
  3. Sigma interop: import/export watchlists as Sigma stubs for the BT3 pipeline.
  4. Fleet simulation: replay the same batch against three policy modes (monitor/moderate/strict) and diff outcomes — policy-mode education as a report.
  5. Ground-truth generator: synthesize labeled batches from MITRE technique templates for repeatable benchmarks.
SourceIDWhat firesNotes
CBC consolealert IDsWatchlist + behavioral + AV alerts w/ tree contextPrimary SOC view
CBC reputation—Unknown/denied binary execution recordsThe Bit9 layer’s telemetry
CBC watchlistscustomYour estate-specific queries, continuouslyThe teaching surface
Sysmon (co-deployed)1,3,7,8,10,11,13,22Independent verificationCross-vendor joins
Windows Security7045, 4688, 4104Native corroborationSame role as E10/E11
WazuhCBC event forwarding [verify integration path]SIEM correlationDivision-of-labor note
  • Respect the first gate: policy mode decides whether unknown binaries run at all; posture recon is step zero (and stays in scope rules — testing execution without authorization is not recon).
  • Moderate-estate discipline: unknown is recorded, not blocked — every risky action scores against “unknown reputation” backdrop; boring shapes (E1/E2 lessons) are the only quiet path.
  • Strict-estate toolkit: approved-binary abuse (T1218 family) is the whole game; LOLBin chains are themselves watchlist shapes (W3), so depth discipline matters even in the toolkit.
  • You cannot see client watchlists: test-estate parity is impossible; design to the generic shapes (the ones you’d write yourself) and assume estate-specific rules exist beyond them.
  • Reputation games are legacy games: publisher abuse and prevalence gaming have long telemetry tails; fresh hashes per op (E10’s rule) is cheaper than trust laundering.
  • Response clock: Live Response minutes-to-analyst; abort criteria pre-decided (the trilogy’s shared lesson).

⚠️ What caught me: on a CBC estate I assumed “moderate policy” meant “unknown binaries are fine” — they ran, but every one of my actions was scored as unknown-reputation behavior, and the client’s custom watchlist for “unknown binary writing to Run keys” (a rule I would absolutely have written) caught persistence within the hour. The lesson: the shapes you would detect are the ones the client’s SOC already detects; design to avoid your own watchlist, then assume theirs is better.

  1. Run cbwatch.py; identify which watchlist caught which story and why reputation context changed the alert’s shape; then add W4 (script content + unknown rep) and re-run.
  2. Run cbquery.py with the tuning drill (200-event batch, precision ≥ 0.9); keep your final rule set as the “shapes to avoid” artifact.
  3. Trial-lab (licensed): reproduce the reputation pipeline — upload an unknown test binary, watch its reputation state evolve from unknown → locally-common as you re-run it; map against ReputationEngine.
  4. Policy-mode simulation: extend cbwatch.py with policy modes (strict blocks unknown procs) and replay the same batch under all three modes; diff outcomes — the fleet-simulation improvement, implemented.
  5. Cross-vendor drill: run the same synthetic phishing chain through minigraph.py (E10), storysim.py (E12), and cbwatch.py (E13); write the one-page comparison of which layer caught it first and why — the trilogy’s capstone.

Technical: Trace CBC’s product lineage (Bit9 → Cb Response → CBC) and map each era’s detection idea onto the modern platform’s layers. Technical: How does the reputation layer interact with behavioral analytics in the classic CBC alert shape, and what does policy mode change? Deep-dive: Design the watchlist that catches “unknown-reputation binary with LOLBin chain depth ≥ 2” — then name the two benign estate workflows most likely to false-positive and the clause that saves each. Conceptual: “Watchlists are where estate knowledge becomes detections” — unpack, and contrast with vendor-managed behavioral models (who owns what risk?). Scenario: Your operation was caught by a watchlist you consider trivial. What does that reveal about your recon assumptions, and what changes in the next plan? Practical: Given cbquery.py’s DSL, write the rule for persistence-by-unknown-binary and defend each clause against a false-positive case.

E10 (CrowdStrike Detection Logic) · E11 (Defender Detection Logic) · E12 (SentinelOne Detection Logic) · BT7 (Threat Hunting)