Grapl: A Graph Platform for Detection and Response
Edge
Node
Node
Node
Edge
Edge
Manage from reality because that's the prepared Defenders Mindset
https://github.com/BloodHoundAD/BloodHound
[
{
'ppid': 100,
'pid': 250,
'action': 'started',
'time': '2019-02-24 18:21:25'
},
{
'ppid': 250,
'pid': 350,
'action': 'started',
'time': '2019-02-24 18:22:31'
}
]
[
{
'pid': 350,
'action': 'connected_to',
'domain': 'evil.com',
'time': '2019-02-24 18:24:27'
},
{
'pid': 350,
'action': 'created_file',
'path': 'downloads/evil.exe',
'time': '2019-02-24 18:28:31'
}
]
seen_at: '2019-02-24 18:21:25'
created_at: '2019-02-24 18:21:25'
created_at: '2019-02-24 18:22:31'
seen_at: '2019-02-24 18:24:27'
seen_at: '2019-02-24 18:28:31'
seen_at: '2019-02-24 18:22:31'
seen_at: '2019-02-24 18:24:27'
seen_at: '2019-02-24 18:28:31'
Grapl
Graph Analytics Platform
for Detection, Forensics, and Incident Response
- Consumes logs
- Turn logs into subgraphs
- Merge subgraphs into master graph
- Executes code against master graph to find scary patterns
- Scopes engagements through graph expansion
{
"host_id": "cobrien-mac",
"parent_pid": 3,
"pid": 4,
"image_name": "word.exe",
"create_time": 600,
}
{
"host_id": "cobrien-mac",
"parent_pid": 4,
"pid": 5,
"image_name": "payload.exe",
"create_time": 650,
}
word.exe
payload.exe
ssh.exe
/secret/file
evil.com
explorer.exe
payload.exe
word.exe
Process {
node_key: string ,
asset_id: string ,
process_id: int ,
process_guid: string ,
created_timestamp: int ,
terminated_timestamp: int ,
last_seen_timestamp: int ,
process_name: string ,
process_command_line: string ,
process_integrity_level: string ,
operating_system: string ,
process_path: File ,
children: [Process] ,
created_files: [File] ,
deleted_files: [File] ,
read_files: [File] ,
wrote_files: [File] ,
}
File {
node_key: string ,
asset_id: string ,
created_timestamp: int ,
deleted_timestamp: int ,
last_seen_timestamp: int ,
file_name: string ,
file_path: string ,
file_extension: string ,
file_mime_type: string ,
file_size: int ,
file_version: string ,
file_description: string ,
file_product: string ,
file_company: string ,
file_directory: string ,
file_inode: int ,
file_hard_links: int ,
md5_hash: string ,
sha1_hash: string ,
sha256_hash: string ,
}
OutboundConnection {
port: int,
created: int,
ended: int,
bound_connection: InboundConnection
}
InboundConnection {
port: int,
created: int,
ended: int,
}
Asset {
operating_system: string,
processes: [Process],
}
ExternalIp {
port: int,
address: str,
}
{
'pid': 100,
'type': 'process_start',
'timestamp': 1551753726,
'image': '/usr/bin/program',
}
{
'pid': 100,
'type': 'file_read',
'path': '/home/.cache/file',
'timestamp': 1551754726
}
{
'pid': 100,
'type': 'process_terminate',
'timestamp': 1551755726,
'image': '/usr/bin/program',
}
pid: 100
created: 1551753726
terminated: 1551755726
image: '/usr/bin/program'
node_key: <uuid>
pid 250
C - Process Create Event
C - 0
0 10 20 30 40 50 60 70 80
{
'pid': 250,
'created_at': 30
}
C - 1
ID - 0
{
'pid': 250,
'created_at': 50
}
ID - 1
pid 250
C - Process Create Event
C - 0
0 10 20 30 40 50 60 70 80
{
'pid': 250,
'seen_at': 30
}
C - 1
ID - 0
{
'type': 'Process Create',
'image': 'C:\Program Files\Microsoft Office\winword.exe',
'pid': 1200,
'ppid': 1080
}
{
'type': 'Process Create',
'image': 'C:\Windows\WindowsPowershell\v1.0\powershell.exe',
'pid': 2590,
'ppid': 1200,
}
word.exe
powershell.exe
word.exe
payload.exe
wmiexec.exe
/secret/file
evil.com
explorer.exe
word.exe
payload.exe
word.exe
evil.com
word.exe talking to non-whitelisted domain
word.exe spawning child process
cmd.exe
payload.exe
wmiexec.exe
cmd.exe
wmiexec from non standard grandparent process
word.exe
payload.exe
evil.com
Process with network access creates file, executes child from it
from pydgraph import DgraphClient, DgraphClientStub
from grapl_analyzer.entity_views import SubgraphView
def _analyzer(client: DgraphClient, graph: SubgraphView, sender: Any):
# Detect processes that delete their parent processes' binary file
for process in graph.process_iter():
parent = process.get_parent()
if not parent: continue
deleted_files = process.get_deleted_files()
if not deleted_files: continue
bin_file = parent.get_bin_file()
for deleted_file in deleted_files:
if deleted_file.path == bin_file.path:
sender.send(process)
from pydgraph import DgraphClient, DgraphClientStub
from grapl_analyzer.counters import ParentChildCounter
from grapl_analyzer.entity_views import SubgraphView
def _analyzer(client: DgraphClient, graph: SubgraphView, sender: Any):
counter = ParentChildCounter(client)
# Detect unique parent-child process combinations, where we have seen
# the child process at least once before
for process in graph.process_iter():
children = process.get_children() or []
for child in children:
combo_count = counter.get_count_for(
process.image_name,
child.image_name,
excluding=process.node_key
)
child_image_count = counter.get_count_for(child.image_name,
excluding=process.node_key)
if combo_count >= Seen.Once and child_image_count == Seen.Many:
sender.send(process)
{
'pid': 250,
'ppid': 150,
'created_at': 1551565127,
'hash': 'acbd18db4cc2f85cedef654fccc4a4d8',
'image_name': '/home/user/downloads/evil.sh'
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
{
~~: ~~
~~: ~~
}
Search Window
pid collision
{
~~: ~~
~~: ~~
}
Alert
engagement = Engagement(engagement_id)
suspect_file = FileView(node_key="node key from engagement")
creator = suspect_file.get_creator()
spawned_procs = suspect_file.get_spawned_from()
engagement.add_processes(spawned_procs)
raw-logs
sysmon-log-parser
generic-log-parser
unidentified-subgraphs
node-identifier
graph-merger
raw-logs
sysmon-log-parser
generic-log-parser
custom-parser
let parent = ProcessDescriptionBuilder::default()
.host_id(process_start.computer)
.state(ProcessState::Existing)
.process_id(process_start.ppid)
.last_seen_timestamp(timestamp)
.build()?;
let child = ProcessDescriptionBuilder::default()
.host_id(process_start.computer)
.process_name(get_image_name(&process_start.image)?)
.state(ProcessState::Created)
.process_id(process_start.pid)
.created_timestamp(timestamp)
.build()?;
{
'action': 'Process Create',
'computer': 'winuser-laptop',
'ppid': 100,
'pid: 150,
'image': 'C:\\Users\\Demo\\Downloads\\foo.exe',
'timestamp': 1551578297
}
let child_exe = FileDescriptionBuilder::default()
.hostt_id(process_start.computer)
.state(FileState::Existing)
.last_seen_timestamp(timestamp)
.file_path(process_start.image)
.build()?;
{
'action': 'Process Create',
'computer': 'winuser-laptop',
'ppid': 100,
'pid: 150,
'image': 'C:\\Users\\Demo\\Downloads\\foo.exe',
'timestamp': 1551578297
}
let mut graph = GraphDescription::new(
timestamp
);
graph.add_edge("bin_file",
child.clone_key(),
child_exe.clone_key()
);
graph.add_node(child_exe);
graph.add_edge("children",
parent.clone_key(),
child.clone_key());
graph.add_node(parent);
graph.add_node(child);
$ git clone git@github.com:insanitybit/grapl.git
$ cd ./grapl/grapl-cdk/
$ npm install -g
$ <your editor> ./.env
BUCKET_PREFIX="<unique identifier>"
$ ./deploy_all.sh
cd /path/to/grapl/
python ./gen-raw-logs.py <bucket prefix>
Questions
[Updated] Grapl: A Graph Platform for Detection and Response
By Colin
[Updated] Grapl: A Graph Platform for Detection and Response
- 976