Skip to content

Commit 3e70f33

Browse files
authored
Merge branch 'fox-it:main' into feature/proxmox-implementation
2 parents 33b8161 + 8741da3 commit 3e70f33

File tree

5 files changed

+196
-96
lines changed

5 files changed

+196
-96
lines changed

acquire/acquire.py

Lines changed: 139 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,15 @@ class QuarantinedFiles(Module):
10641064
]
10651065

10661066

1067+
@register_module("--edr")
1068+
class EDR(Module):
1069+
DESC = "various Endpoint Detection and Response (EDR) logs"
1070+
SPEC = [
1071+
# Carbon Black
1072+
("dir", "sysvol/ProgramData/CarbonBlack/Logs"),
1073+
]
1074+
1075+
10671076
@register_module("--history")
10681077
class History(Module):
10691078
DESC = "browser history from IE, Edge, Firefox, and Chrome"
@@ -1655,7 +1664,7 @@ def _add_modules_for_profile(choice: str, operating_system: str, profile: dict,
16551664
return modules_selected
16561665

16571666

1658-
def acquire_target(target: Target, args: argparse.Namespace, output_ts: Optional[str] = None) -> list[str]:
1667+
def acquire_target(target: Target, args: argparse.Namespace, output_ts: Optional[str] = None) -> list[str | Path]:
16591668
acquire_gui = GUI()
16601669
files = []
16611670
output_ts = output_ts or get_utc_now_str()
@@ -1863,16 +1872,18 @@ def acquire_target(target: Target, args: argparse.Namespace, output_ts: Optional
18631872
return files
18641873

18651874

1866-
def upload_files(paths: list[Path], upload_plugin: UploaderPlugin, no_proxy: bool = False) -> None:
1875+
def upload_files(paths: list[str | Path], upload_plugin: UploaderPlugin, no_proxy: bool = False) -> None:
18671876
proxies = None if no_proxy else urllib.request.getproxies()
18681877
log.debug("Proxies: %s (no_proxy = %s)", proxies, no_proxy)
18691878

1879+
log.info('Uploading files: "%s"', " ".join(map(str, paths)))
18701880
try:
18711881
upload_files_using_uploader(upload_plugin, paths, proxies)
18721882
except Exception:
1873-
log.error("Upload %s FAILED. See log file for details.", paths)
1874-
GUI().message("Upload failed.")
1875-
log.exception("")
1883+
log.error('Upload FAILED for files: "%s". See log file for details.', " ".join(map(str, paths)))
1884+
raise
1885+
else:
1886+
log.info("Upload succeeded.")
18761887

18771888

18781889
class WindowsProfile:
@@ -2054,29 +2065,30 @@ class VolatileProfile:
20542065
}
20552066

20562067

2068+
def exit_success(default_args: list[str]):
2069+
log.info("Acquire finished successful")
2070+
log.info("Arguments: %s", " ".join(sys.argv[1:]))
2071+
log.info("Default Arguments: %s", " ".join(default_args))
2072+
log.info("Exiting with status code 0 (SUCCESS)")
2073+
sys.exit(0)
2074+
2075+
2076+
def exit_failure(default_args: list[str]):
2077+
log.error("Acquire FAILED")
2078+
log.error("Arguments: %s", " ".join(sys.argv[1:]))
2079+
log.error("Default Arguments: %s", " ".join(default_args))
2080+
log.error("Exiting with status code 1 (FAILURE)")
2081+
sys.exit(1)
2082+
2083+
20572084
def main() -> None:
20582085
parser = create_argument_parser(PROFILES, VOLATILE, MODULES)
20592086
args, rest = parse_acquire_args(parser, config=CONFIG)
20602087

2061-
# start GUI if requested through CLI / config
2062-
flavour = None
2063-
if args.gui == "always" or (
2064-
args.gui == "depends" and os.environ.get("PYS_KEYSOURCE") == "prompt" and len(sys.argv) == 1
2065-
):
2066-
flavour = platform.system()
2067-
2068-
acquire_gui = GUI(flavour=flavour, upload_available=args.auto_upload)
2069-
args.output, args.auto_upload, cancel = acquire_gui.wait_for_start(args)
2070-
20712088
# Since output has a default value, set it to None when output_file is defined
20722089
if args.output_file:
20732090
args.output = None
20742091

2075-
if cancel:
2076-
parser.exit(0)
2077-
2078-
# From here onwards, the GUI will be locked and cannot be closed because we're acquiring
2079-
20802092
try:
20812093
check_and_set_log_args(args)
20822094
except ValueError as err:
@@ -2093,65 +2105,118 @@ def main() -> None:
20932105

20942106
setup_logging(log, log_file, args.verbose, delay=args.log_delay)
20952107

2096-
log.info(ACQUIRE_BANNER)
2097-
log.info("User: %s | Admin: %s", get_user_name(), is_user_admin())
2098-
log.info("Arguments: %s", " ".join(sys.argv[1:]))
2099-
log.info("Default Arguments: %s", " ".join(args.config.get("arguments")))
2100-
log.info("")
2108+
acquire_successful = True
2109+
files_to_upload = [log_file]
2110+
acquire_gui = None
2111+
try:
2112+
log.info(ACQUIRE_BANNER)
2113+
log.info("User: %s | Admin: %s", get_user_name(), is_user_admin())
2114+
log.info("Arguments: %s", " ".join(sys.argv[1:]))
2115+
log.info("Default Arguments: %s", " ".join(args.config.get("arguments")))
2116+
log.info("")
21012117

2102-
plugins_to_load = [("cloud", MinIO)]
2103-
upload_plugins = UploaderRegistry("acquire.plugins", plugins_to_load)
2118+
# start GUI if requested through CLI / config
2119+
flavour = None
2120+
if args.gui == "always" or (
2121+
args.gui == "depends" and os.environ.get("PYS_KEYSOURCE") == "prompt" and len(sys.argv) == 1
2122+
):
2123+
flavour = platform.system()
2124+
acquire_gui = GUI(flavour=flavour, upload_available=args.auto_upload)
2125+
2126+
args.output, args.auto_upload, cancel = acquire_gui.wait_for_start(args)
2127+
if cancel:
2128+
log.info("Acquire cancelled")
2129+
exit_success(args.config.get("arguments"))
2130+
# From here onwards, the GUI will be locked and cannot be closed because we're acquiring
2131+
2132+
plugins_to_load = [("cloud", MinIO)]
2133+
upload_plugins = UploaderRegistry("acquire.plugins", plugins_to_load)
21042134

2105-
try:
21062135
check_and_set_acquire_args(args, upload_plugins)
2107-
except ValueError as err:
2108-
log.exception(err)
2109-
parser.exit(1)
21102136

2111-
if args.upload:
2137+
if args.upload:
2138+
try:
2139+
upload_files(args.upload, args.upload_plugin, args.no_proxy)
2140+
except Exception as err:
2141+
acquire_gui.message("Failed to upload files")
2142+
log.exception(err)
2143+
exit_failure(args.config.get("arguments"))
2144+
exit_success(args.config.get("arguments"))
2145+
2146+
target_paths = []
2147+
for target_path in args.targets:
2148+
target_path = args_to_uri([target_path], args.loader, rest)[0] if args.loader else target_path
2149+
if target_path == "local":
2150+
target_query = {}
2151+
if args.force_fallback:
2152+
target_query.update({"force-directory-fs": 1})
2153+
2154+
if args.fallback:
2155+
target_query.update({"fallback-to-directory-fs": 1})
2156+
2157+
target_query = urllib.parse.urlencode(target_query)
2158+
target_path = f"{target_path}?{target_query}"
2159+
target_paths.append(target_path)
2160+
21122161
try:
2113-
upload_files(args.upload, args.upload_plugin, args.no_proxy)
2162+
target_name = "Unknown" # just in case open_all already fails
2163+
for target in Target.open_all(target_paths):
2164+
target_name = "Unknown" # overwrite previous target name
2165+
target_name = target.name
2166+
log.info("Loading target %s", target_name)
2167+
log.info(target)
2168+
if target.os == "esxi" and target.name == "local":
2169+
# Loader found that we are running on an esxi host
2170+
# Perform operations to "enhance" memory
2171+
with esxi_memory_context_manager():
2172+
files_to_upload = acquire_children_and_targets(target, args)
2173+
else:
2174+
files_to_upload = acquire_children_and_targets(target, args)
21142175
except Exception:
2115-
log.exception("Failed to upload files")
2116-
return
2117-
2118-
target_paths = []
2119-
for target_path in args.targets:
2120-
target_path = args_to_uri([target_path], args.loader, rest)[0] if args.loader else target_path
2121-
if target_path == "local":
2122-
target_query = {}
2123-
if args.force_fallback:
2124-
target_query.update({"force-directory-fs": 1})
2176+
log.error("Failed to acquire target: %s", target_name)
2177+
if not is_user_admin():
2178+
log.error("Try re-running as administrator/root")
2179+
acquire_gui.message("This application must be run as administrator.")
2180+
raise
21252181

2126-
if args.fallback:
2127-
target_query.update({"fallback-to-directory-fs": 1})
2182+
files_to_upload = sort_files(files_to_upload)
21282183

2129-
target_query = urllib.parse.urlencode(target_query)
2130-
target_path = f"{target_path}?{target_query}"
2131-
target_paths.append(target_path)
2184+
except Exception as err:
2185+
log.error("Acquiring artifacts FAILED")
2186+
log.exception(err)
2187+
acquire_successful = False
2188+
else:
2189+
log.info("Acquiring artifacts succeeded")
21322190

21332191
try:
2134-
target_name = "Unknown" # just in case open_all already fails
2135-
for target in Target.open_all(target_paths):
2136-
target_name = "Unknown" # overwrite previous target name
2137-
target_name = target.name
2138-
log.info("Loading target %s", target_name)
2139-
log.info(target)
2140-
if target.os == "esxi" and target.name == "local":
2141-
# Loader found that we are running on an esxi host
2142-
# Perform operations to "enhance" memory
2143-
with esxi_memory_context_manager():
2144-
acquire_children_and_targets(target, args)
2145-
else:
2146-
acquire_children_and_targets(target, args)
2147-
except Exception:
2148-
if not is_user_admin():
2149-
log.error("Failed to load target: %s, try re-running as administrator/root", target_name)
2150-
acquire_gui.message("This application must be run as administrator.")
2192+
# The auto-upload of files is done at the very very end to make sure any
2193+
# logged exceptions are written to the log file before uploading.
2194+
# This means that any failures from this point on will not be part of the
2195+
# uploaded log files, they will be written to the logfile on disk though.
2196+
if args.auto_upload and args.upload_plugin and files_to_upload:
2197+
try:
2198+
log_file_handler = get_file_handler(log)
2199+
if log_file_handler:
2200+
log_file_handler.close()
2201+
2202+
upload_files(files_to_upload, args.upload_plugin)
2203+
except Exception:
2204+
if acquire_gui:
2205+
acquire_gui.message("Failed to upload files")
2206+
raise
2207+
2208+
if acquire_gui:
2209+
acquire_gui.finish()
21512210
acquire_gui.wait_for_quit()
2152-
parser.exit(1)
2153-
log.exception("Failed to load target: %s", target_name)
2154-
raise
2211+
2212+
except Exception as err:
2213+
acquire_successful = False
2214+
log.exception(err)
2215+
2216+
if acquire_successful:
2217+
exit_success(args.config.get("arguments"))
2218+
else:
2219+
exit_failure(args.config.get("arguments"))
21552220

21562221

21572222
def load_child(target: Target, child_path: Path) -> None:
@@ -2167,7 +2232,7 @@ def load_child(target: Target, child_path: Path) -> None:
21672232
return child
21682233

21692234

2170-
def acquire_children_and_targets(target: Target, args: argparse.Namespace) -> None:
2235+
def acquire_children_and_targets(target: Target, args: argparse.Namespace) -> list[str | Path]:
21712236
if args.child:
21722237
target = load_child(target, args.child)
21732238

@@ -2185,20 +2250,20 @@ def acquire_children_and_targets(target: Target, args: argparse.Namespace) -> No
21852250
if (args.children and not args.skip_parent) or not args.children:
21862251
total_targets += 1
21872252
counter += 1
2188-
acquire_gui.shard = (progress_limit // total_targets) * counter
2253+
acquire_gui.shard = int((progress_limit / total_targets) * counter)
21892254
try:
21902255
files.extend(acquire_target(target, args, args.start_time))
21912256

21922257
except Exception:
2193-
log.exception("Failed to acquire target")
2258+
log.error("Failed to acquire main target")
21942259
acquire_gui.message("Failed to acquire target")
21952260
acquire_gui.wait_for_quit()
21962261
raise
21972262

21982263
if args.children:
21992264
for child in target.list_children():
22002265
counter += 1
2201-
acquire_gui.shard = (100 // total_targets) * counter
2266+
acquire_gui.shard = int((progress_limit / total_targets) * counter)
22022267
try:
22032268
child_target = load_child(target, child.path)
22042269
except Exception:
@@ -2210,29 +2275,11 @@ def acquire_children_and_targets(target: Target, args: argparse.Namespace) -> No
22102275
child_files = acquire_target(child_target, args)
22112276
files.extend(child_files)
22122277
except Exception:
2213-
log.exception("Failed to acquire child target")
2278+
log.exception("Failed to acquire child target %s", child_target.name)
22142279
acquire_gui.message("Failed to acquire child target")
22152280
continue
22162281

2217-
files = sort_files(files)
2218-
2219-
if args.auto_upload:
2220-
log_file_handler = get_file_handler(log)
2221-
if log_file_handler:
2222-
log_file_handler.close()
2223-
2224-
log.info("")
2225-
try:
2226-
upload_files(files, args.upload_plugin)
2227-
acquire_gui.finish()
2228-
acquire_gui.wait_for_quit()
2229-
except Exception:
2230-
log.exception("Failed to upload files")
2231-
acquire_gui.message("Failed to upload files")
2232-
acquire_gui.wait_for_quit()
2233-
else:
2234-
acquire_gui.finish()
2235-
acquire_gui.wait_for_quit()
2282+
return files
22362283

22372284

22382285
def sort_files(files: list[Union[str, Path]]) -> list[Path]:

acquire/uploaders/plugin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import logging
24
from pathlib import Path
35
from typing import Any, Optional
@@ -31,7 +33,7 @@ def finish(self, client: Any) -> None:
3133

3234

3335
def upload_files_using_uploader(
34-
uploader: UploaderPlugin, paths: list[Path], proxies: Optional[dict[str, str]] = None
36+
uploader: UploaderPlugin, paths: list[str | Path], proxies: Optional[dict[str, str]] = None
3537
) -> None:
3638
"""Uploads the files in ``paths`` to a destination.
3739

acquire/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,12 @@ def check_and_set_acquire_args(
278278
ValueError: When an invalid combination of arguments is found.
279279
"""
280280
upload_plugin = None
281+
setattr(args, "upload_plugin", upload_plugin)
281282

282283
# check & set upload related configuration
284+
if args.upload and args.auto_upload:
285+
raise ValueError("only one of --upload or --auto-upload can be specified")
286+
283287
if args.upload or args.auto_upload:
284288
upload_mode = args.config.get("upload", {}).get("mode")
285289
if not upload_mode:
@@ -291,8 +295,7 @@ def check_and_set_acquire_args(
291295

292296
# If initialization of the plugin fails, a ValueError is raised
293297
upload_plugin = upload_plugin_cls(**args.config)
294-
295-
setattr(args, "upload_plugin", upload_plugin)
298+
setattr(args, "upload_plugin", upload_plugin)
296299

297300
if not args.upload:
298301
# check output related configuration

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525
"Topic :: Utilities",
2626
]
2727
dependencies = [
28-
"dissect.cstruct>=4.dev,<5",
28+
"dissect.cstruct>=4,<5",
2929
"dissect.target>=3.7,<4",
3030
]
3131
dynamic = ["version"]

0 commit comments

Comments
 (0)