Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6f3dbaf
Add initial Rapid7 Velociraptor artifacts plugin
May 1, 2024
bc5f1e4
Add comment
May 1, 2024
828705c
Remove comment
May 1, 2024
eb618f2
Open file-object without `with`
May 2, 2024
3c6f4b9
Map results
May 3, 2024
685850e
Add test
May 3, 2024
a030e0b
Merge branch 'main' into feature/velociraptor_plugin
May 6, 2024
68ffa6e
Add test
May 7, 2024
21d4fd2
Merge branch 'main' into feature/velociraptor_plugin
Aug 5, 2024
1a3f43d
Merge branch 'main' into feature/velociraptor_plugin
Dec 5, 2024
c0705d6
Merge branch 'feature/velociraptor_plugin' of github.com:Zawadidone/d…
Apr 10, 2025
0efa557
Merge remote-tracking branch 'upstream/main' into feature/velocirapto…
Apr 24, 2025
90e9590
Fix loader and implement suggestions coder review
Apr 25, 2025
ae769a0
Fix test
Apr 25, 2025
c1d63db
Merge remote-tracking branch 'upstream/main' into feature/velocirapto…
Apr 25, 2025
84aada4
Fix docstring
Apr 25, 2025
f0e6f11
Linting
Apr 25, 2025
0553e0b
Fix loader and tests
Apr 25, 2025
908291a
Linting
Apr 25, 2025
67cf363
Move Acquire to `apps.edr.acquire`
Apr 25, 2025
da87727
Fix nested records
Apr 25, 2025
96c8485
Fix typo
Apr 25, 2025
e5c03a9
Remove comment
Apr 25, 2025
984d598
Remove OS name from record name
Apr 25, 2025
e83fc3a
Fix record name
Apr 25, 2025
118cf13
Improve log message
Apr 25, 2025
41747fa
Apply suggestions code review
Apr 28, 2025
a80999a
Merge branch 'main' into feature/velociraptor_plugin
Apr 28, 2025
9ec0fd1
Small changes
Schamper Apr 29, 2025
88e7e70
Linting changes
Schamper Apr 29, 2025
588934c
Apply suggestions code review
Apr 29, 2025
ca3cb2b
Merge branch 'main' into feature/velociraptor_plugin
Apr 29, 2025
f83233f
Merge branch 'feature/velociraptor_plugin' of github.com:Zawadidone/d…
Apr 29, 2025
c74c753
Update tests/plugins/apps/edr/test_velociraptor.py
Schamper Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions dissect/target/loaders/velociraptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
from typing import TYPE_CHECKING
from urllib.parse import quote, unquote

from dissect.target.filesystem import VirtualFilesystem
from dissect.target.filesystems.dir import DirectoryFilesystem
from dissect.target.filesystems.zip import ZipFilesystem
from dissect.target.helpers.fsutil import basename, dirname, join
from dissect.target.loaders.dir import DirLoader, find_dirs, map_dirs
from dissect.target.plugin import OperatingSystem
from dissect.target.plugins.apps.edr.velociraptor import VELOCIRAPTOR_RESULTS

if TYPE_CHECKING:
from pathlib import Path
Expand All @@ -31,7 +33,11 @@
accessor_root = fs_root.joinpath(accessor)
if accessor_root.exists():
os_type, dirs = find_dirs(accessor_root)
if os_type in [OperatingSystem.UNIX, OperatingSystem.LINUX, OperatingSystem.OSX]:
if os_type in [
OperatingSystem.UNIX,
OperatingSystem.LINUX,
OperatingSystem.OSX,
]:
return os_type, [dirs[0]]

# Windows
Expand Down Expand Up @@ -69,7 +75,6 @@
# X: in URL encoding
if len(name) == 4 and name.endswith("%3A"):
return name[0].lower()

return None


Expand Down Expand Up @@ -137,6 +142,21 @@
zipfs=VelociraptorZipFilesystem,
)

if (results := self.root.joinpath("results")).is_dir():
# Map artifact results collected by Velociraptor
vfs = VirtualFilesystem()

for artifact in results.iterdir():
if not artifact.name.endswith(".json"):
continue

Check warning on line 151 in dissect/target/loaders/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/loaders/velociraptor.py#L151

Added line #L151 was not covered by tests

vfs.map_file_fh(artifact.name, artifact.open("rb"))

if (uploads := self.root.joinpath("uploads.json")).exists():
vfs.map_file_fh(uploads.name, uploads.open("rb"))

target.fs.mount(VELOCIRAPTOR_RESULTS, vfs)


class VelociraptorDirectoryFilesystem(DirectoryFilesystem):
def _resolve_path(self, path: str) -> Path:
Expand Down
Empty file.
85 changes: 85 additions & 0 deletions dissect/target/plugins/apps/edr/acquire.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from __future__ import annotations

import csv
import gzip
from typing import TYPE_CHECKING

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import TargetRecordDescriptor
from dissect.target.plugin import Plugin, export

if TYPE_CHECKING:
from collections.abc import Iterator

Check warning on line 12 in dissect/target/plugins/apps/edr/acquire.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/acquire.py#L12

Added line #L12 was not covered by tests

from dissect.target.target import Target

Check warning on line 14 in dissect/target/plugins/apps/edr/acquire.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/acquire.py#L14

Added line #L14 was not covered by tests

AcquireOpenHandlesRecord = TargetRecordDescriptor(
"filesystem/acquire_open_handles",
[
("path", "name"),
("string", "handle_type"),
("string", "object"),
("varint", "unique_process_id"),
("varint", "handle_value"),
("varint", "granted_access"),
("varint", "creator_back_trace_index"),
("varint", "object_type_index"),
("varint", "handle_attributes"),
("varint", "reserved"),
],
)

AcquireHashRecord = TargetRecordDescriptor(
"filesystem/acquire_hash",
[
("path", "path"),
("filesize", "filesize"),
("digest", "digest"),
],
)


class AcquirePlugin(Plugin):
"""Returns records from data collected by Acquire."""

__namespace__ = "acquire"

def __init__(self, target: Target):
super().__init__(target)
self.hash_file = target.fs.path("$metadata$/file-hashes.csv.gz")
self.open_handles_file = target.fs.path("$metadata$/open_handles.csv.gz")

def check_compatible(self) -> None:
if not self.hash_file.exists() and not self.open_handles_file.exists():
raise UnsupportedPluginError("No hash file or open handles found")

@export(record=AcquireHashRecord)
def hashes(self) -> Iterator[AcquireHashRecord]:
"""Return file hashes collected by Acquire.

An Acquire file container contains a file hashes csv when the hashes module was used. The content of this csv
file is returned.
"""
if self.hash_file.exists():
with self.hash_file.open() as fh, gzip.open(fh, "rt") as gz_fh:
for row in csv.DictReader(gz_fh):
yield AcquireHashRecord(
path=self.target.fs.path(row["path"]),
filesize=row["file-size"],
digest=(row["md5"] or None, row["sha1"] or None, row["sha256"] or None),
_target=self.target,
)

@export(record=AcquireOpenHandlesRecord)
def handles(self) -> Iterator[AcquireOpenHandlesRecord]:
"""Return open handles collected by Acquire.

An Acquire file container contains an open handles csv when the handles module was used. The content of this csv
file is returned.
"""
if self.open_handles_file.exists():
with self.open_handles_file.open() as fh, gzip.open(fh, "rt") as gz_fh:
for row in csv.DictReader(gz_fh):
if name := row.get("name"):
row.update({"name": self.target.fs.path(name)})
yield AcquireOpenHandlesRecord(**row, _target=self.target)
111 changes: 111 additions & 0 deletions dissect/target/plugins/apps/edr/velociraptor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations

import json
import re
import urllib.parse
from functools import lru_cache
from typing import TYPE_CHECKING

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor
from dissect.target.plugin import Plugin, export

if TYPE_CHECKING:
from collections.abc import Iterator

Check warning on line 14 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L14

Added line #L14 was not covered by tests

from flow.record import Record

Check warning on line 16 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L16

Added line #L16 was not covered by tests

from dissect.target.target import Target

Check warning on line 18 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L18

Added line #L18 was not covered by tests

VELOCIRAPTOR_RESULTS = "/$velociraptor_results$"
ISO_8601_PATTERN = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?"


class VelociraptorRecordBuilder:
def __init__(self, artifact_name: str):
self._create_event_descriptor = lru_cache(4096)(self._create_event_descriptor)
self.record_name = f"velociraptor/{artifact_name}"

def build(self, object: dict, target: Target) -> TargetRecordDescriptor:
"""Builds a Velociraptor record."""
record_values = {}
record_fields = []

record_values["_target"] = target

for key, value in object.items():
# Reserved by flow.record
if key.startswith("_"):
continue

key = key.lower().replace("(", "_").replace(")", "_")

if re.match(ISO_8601_PATTERN, str(value)):
record_type = "datetime"
elif isinstance(value, list):
record_type = "string[]"

Check warning on line 46 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L46

Added line #L46 was not covered by tests
elif isinstance(value, int):
record_type = "varint"
elif key == "hash":
record_type = "digest"
value = (value.get("MD5"), value.get("SHA1"), value.get("SHA256"))

Check warning on line 51 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L50-L51

Added lines #L50 - L51 were not covered by tests
elif isinstance(value, str):
record_type = "string"
elif isinstance(value, dict):
record_type = "record"
value = self.build(value, target)
else:
record_type = "dynamic"

Check warning on line 58 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L58

Added line #L58 was not covered by tests

record_fields.append((record_type, key))
record_values[key] = value

# tuple conversion here is needed for lru_cache
desc = self._create_event_descriptor(tuple(record_fields))
return desc(**record_values)

def _create_event_descriptor(self, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor:
return TargetRecordDescriptor(self.record_name, record_fields)


class VelociraptorPlugin(Plugin):
"""Returns records from Velociraptor artifacts."""

__namespace__ = "velociraptor"

def __init__(self, target: Target):
super().__init__(target)
self.results_dir = target.fs.path(VELOCIRAPTOR_RESULTS)

def check_compatible(self) -> None:
if not self.results_dir.exists():
raise UnsupportedPluginError("No Velociraptor artifacts found")

@export(record=DynamicDescriptor(["datetime"]))
def results(self) -> Iterator[Record]:
"""Return Rapid7 Velociraptor artifacts.

References:
- https://docs.velociraptor.app/docs/vql/artifacts/
"""
for artifact in self.results_dir.glob("*.json"):
# "Windows.KapeFiles.Targets%2FAll\ File\ Metadata.json" becomes "windows_kapefiles_targets"
artifact_name = (
urllib.parse.unquote(artifact.name.removesuffix(".json")).split("/")[0].lower().replace(".", "_")
)
record_builder = VelociraptorRecordBuilder(artifact_name)

for line in artifact.open("rt"):
if not (line := line.strip()):
continue

Check warning on line 100 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L100

Added line #L100 was not covered by tests

try:
object = json.loads(line)
yield record_builder.build(object, self.target)
except json.decoder.JSONDecodeError:
self.target.log.warning(

Check warning on line 106 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L105-L106

Added lines #L105 - L106 were not covered by tests
"Could not decode Velociraptor JSON log line in file %s: %s",
artifact,
line,
)
continue

Check warning on line 111 in dissect/target/plugins/apps/edr/velociraptor.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/edr/velociraptor.py#L111

Added line #L111 was not covered by tests
55 changes: 0 additions & 55 deletions dissect/target/plugins/filesystem/acquire_handles.py

This file was deleted.

52 changes: 0 additions & 52 deletions dissect/target/plugins/filesystem/acquire_hash.py

This file was deleted.

Git LFS file not shown
Git LFS file not shown
13 changes: 4 additions & 9 deletions tests/loaders/test_velociraptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def create_root(sub_dir: str, tmp_path: Path) -> Path:
f"uploads/{sub_dir}/%5C%5C%3F%5CGLOBALROOT%5CDevice%5CHarddiskVolumeShadowCopy1/$Extend",
f"uploads/{sub_dir}/%5C%5C%3F%5CGLOBALROOT%5CDevice%5CHarddiskVolumeShadowCopy1/windows/system32",
f"uploads/{sub_dir}/%5C%5C.%5CC%3A/%2ETEST",
"results",
]
root = tmp_path
mkdirs(root, paths)
Expand Down Expand Up @@ -52,16 +53,10 @@ def create_root(sub_dir: str, tmp_path: Path) -> Path:


@pytest.mark.parametrize(
("sub_dir", "other_dir"),
[
("mft", "auto"),
("ntfs", "auto"),
("ntfs_vss", "auto"),
("lazy_ntfs", "auto"),
("auto", "ntfs"),
],
"sub_dir",
["mft", "ntfs", "ntfs_vss", "lazy_ntfs", "auto"],
)
def test_windows_ntfs(sub_dir: str, other_dir: str, target_bare: Target, tmp_path: Path) -> None:
def test_windows_ntfs(sub_dir: str, target_bare: Target, tmp_path: Path) -> None:
root = create_root(sub_dir, tmp_path)

assert VelociraptorLoader.detect(root) is True
Expand Down
Empty file.
Loading
Loading