multivol 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
multivol/multivol.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # multivol.py
2
2
  # Entry point for MultiVolatility: orchestrates running Volatility2 and Volatility3 memory analysis in parallel using multiprocessing.
3
3
  import multiprocessing, time, os, argparse, sys
4
+ import docker
5
+ from datetime import datetime
4
6
  from rich.console import Console
5
7
  from rich.theme import Theme
6
8
  from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
@@ -9,9 +11,16 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskPr
9
11
  try:
10
12
  from .multi_volatility2 import multi_volatility2
11
13
  from .multi_volatility3 import multi_volatility3
14
+ from .strings import get_strings
12
15
  except ImportError:
13
16
  from multi_volatility2 import multi_volatility2
14
17
  from multi_volatility3 import multi_volatility3
18
+ from strings import get_strings
19
+
20
+
21
+
22
+ # Wrapper for Volatility 3 to use with imap
23
+
15
24
 
16
25
  # Wrapper for Volatility 3 to use with imap
17
26
  def vol3_wrapper(packed_args):
@@ -29,14 +38,17 @@ def runner(arguments):
29
38
  if not arguments.light and not arguments.full:
30
39
  arguments.light = True
31
40
 
41
+ if hasattr(arguments, "output_dir") and arguments.output_dir:
42
+ output_dir = arguments.output_dir
43
+ elif hasattr(arguments, "output") and arguments.output:
44
+ output_dir = arguments.output
45
+ else:
46
+ output_dir = f"output_{datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
47
+ os.makedirs(output_dir, exist_ok=True)
48
+
32
49
  # Handle Volatility2 mode
33
50
  if arguments.mode == "vol2":
34
51
  volatility2_instance = multi_volatility2()
35
- if hasattr(arguments, "output_dir") and arguments.output_dir:
36
- output_dir = arguments.output_dir
37
- else:
38
- output_dir = f"volatility2_{os.path.basename(arguments.dump)}__output"
39
- os.makedirs(output_dir, exist_ok=True)
40
52
  # Determine commands to run based on arguments
41
53
  if arguments.commands:
42
54
  commands = arguments.commands.split(",")
@@ -54,11 +66,6 @@ def runner(arguments):
54
66
  # Handle Volatility3 mode
55
67
  elif arguments.mode == "vol3":
56
68
  volatility3_instance = multi_volatility3()
57
- if hasattr(arguments, "output_dir") and arguments.output_dir:
58
- output_dir = arguments.output_dir
59
- else:
60
- output_dir = f"volatility3_{os.path.basename(arguments.dump)}__output"
61
- os.makedirs(output_dir, exist_ok=True)
62
69
  # Determine commands to run based on arguments
63
70
  if arguments.commands:
64
71
  commands = arguments.commands.split(",")
@@ -68,7 +75,10 @@ def runner(arguments):
68
75
  else:
69
76
  commands = volatility3_instance.getCommands("windows.full")
70
77
  elif arguments.linux:
71
- commands = volatility3_instance.getCommands("linux")
78
+ if arguments.light:
79
+ commands = volatility3_instance.getCommands("linux.light")
80
+ else:
81
+ commands = volatility3_instance.getCommands("linux.full")
72
82
 
73
83
  # Limit the number of parallel processes
74
84
  # Default to len(commands) (unlimited) if processes arg is not set or None
@@ -85,6 +95,38 @@ def runner(arguments):
85
95
 
86
96
  custom_theme = Theme({"info": "dim cyan", "warning": "magenta", "danger": "bold red"})
87
97
  console = Console(theme=custom_theme)
98
+
99
+ # Docker Image Check & Pull
100
+ try:
101
+ client = docker.from_env()
102
+ image_name = arguments.image
103
+ # Check if --image was passed in args. rudimentary check.
104
+ user_provided_image = "--image" in sys.argv
105
+
106
+ try:
107
+ client.images.get(image_name)
108
+ if not user_provided_image:
109
+ console.print(f"[dim cyan][*] No --image provided, using default image: {image_name}[/dim cyan]")
110
+ except docker.errors.ImageNotFound:
111
+ msg = f"[*] No --image provided, pulling default image: {image_name}" if not user_provided_image else f"[*] Pulling image: {image_name}"
112
+
113
+ # Use Progress with custom columns to put spinner at the end
114
+ with Progress(
115
+ TextColumn("{task.description}"),
116
+ SpinnerColumn("dots"),
117
+ transient=True,
118
+ console=console
119
+ ) as progress:
120
+ progress.add_task(f"[bold green]{msg}[/bold green]", total=None)
121
+ client.images.pull(image_name)
122
+
123
+ # Re-print the message so it persists in the log
124
+ console.print(f"[bold green]{msg}[/bold green]")
125
+ console.print(f"[bold green][*] Image {image_name} ready.[/bold green]")
126
+ except Exception as e:
127
+ console.print(f"[bold red]Warning: Docker check failed: {e}[/bold red]")
128
+ # We don't exit here, we let the individual/pool commands fail if they must, or maybe user has local setup issues.
129
+
88
130
  console.print("\n[bold green][+] Launching all commands...[/bold green]\n")
89
131
 
90
132
  # Use multiprocessing Manager for Lock
@@ -106,35 +148,47 @@ def runner(arguments):
106
148
  arguments.format,
107
149
  False, # quiet
108
150
  lock, # lock
109
- arguments.host_path
151
+ arguments.host_path,
152
+ getattr(arguments, "debug", False)
110
153
  ) for cmd in commands]
111
154
  )
155
+ # Vol2 Starmap doesn't use wrapper, so we can't easily report running/completed per module unless we wrap it too.
156
+ # For now we focus on Vol3
157
+ # But we should probably fix Vol2 too.
158
+ # TODO: Add wrapper for Vol2 or update vol2 logic.
112
159
  else:
160
+
113
161
  # Enforce priority execution for Info module to ensure symbols are downloaded/cached
114
162
  if arguments.windows:
115
163
  info_module = "windows.info.Info"
116
164
  else:
117
165
  info_module = "linux.bash.Bash"
118
- if arguments.windows or (arguments.linux and arguments.fetch_symbol):
119
- commands.remove(info_module)
120
- volatility3_instance.execute_command_volatility3(info_module,
121
- os.path.basename(arguments.dump),
122
- os.path.abspath(arguments.dump),
123
- arguments.symbols_path,
124
- arguments.image,
125
- os.path.abspath(arguments.cache_path),
126
- os.path.abspath(arguments.plugins_dir),
127
- output_dir,
128
- arguments.format,
129
- False, # quiet
130
- lock, # lock
131
- arguments.host_path,
132
- True if arguments.fetch_symbol else False
133
- )
166
+ if arguments.windows or arguments.linux:
167
+ if info_module in commands:
168
+ commands.remove(info_module)
169
+ volatility3_instance.execute_command_volatility3(info_module,
170
+ os.path.basename(arguments.dump),
171
+ os.path.abspath(arguments.dump),
172
+ arguments.symbols_path,
173
+ arguments.image,
174
+ os.path.abspath(arguments.cache_path),
175
+ os.path.abspath(arguments.plugins_dir),
176
+ output_dir,
177
+ arguments.format,
178
+ False, # quiet
179
+ lock, # lock
180
+ arguments.host_path,
181
+ True if getattr(arguments, "fetch_symbol", False) else False,
182
+ getattr(arguments, "debug", False),
183
+ getattr(arguments, "custom_symbol", None),
184
+ getattr(arguments, "scan_id", None)
185
+ )
134
186
 
135
187
 
136
188
  # Prepare arguments for imap
137
189
  # We must pass the instance because wrapper is global and doesn't see local variable
190
+ # Signature: command, dump, dump_dir, symbols_path, docker_image, cache_dir, plugin_dir, output_dir, format,
191
+ # quiet=False, lock=None, host_path=None, fetch_symbols=False, show_commands=False, custom_symbol=None, scan_id=None
138
192
  tasks_args = [(volatility3_instance, (cmd,
139
193
  os.path.basename(arguments.dump),
140
194
  os.path.abspath(arguments.dump),
@@ -144,10 +198,13 @@ def runner(arguments):
144
198
  os.path.abspath(arguments.plugins_dir),
145
199
  output_dir,
146
200
  arguments.format,
147
- False, # quiet=False so we see the output as it happens
148
- lock, # lock
201
+ False, # quiet=False so we see the output as it happens
202
+ lock, # lock
149
203
  arguments.host_path,
150
- True if arguments.fetch_symbol else False
204
+ getattr(arguments, "fetch_symbol", False), # fetch_symbols
205
+ getattr(arguments, "debug", False), # show_commands
206
+ getattr(arguments, "custom_symbol", None), # custom_symbol
207
+ getattr(arguments, "scan_id", None) # scan_id
151
208
  )) for cmd in commands]
152
209
 
153
210
  # Progress counters
@@ -168,6 +225,19 @@ def runner(arguments):
168
225
  failed_modules.append(command_name)
169
226
  if arguments.format == "json":
170
227
  console.print(f"[red][!] Failed to validate JSON for {command_name}[/red]")
228
+
229
+ console.print("\n[+] Starting strings...")
230
+
231
+ get_strings(
232
+ os.path.basename(arguments.dump),
233
+ os.path.abspath(arguments.dump),
234
+ output_dir,
235
+ arguments.image,
236
+ lock,
237
+ arguments.host_path
238
+ )
239
+
240
+ console.print("\n[+] Strings complete !")
171
241
 
172
242
  console.print(f"\n[bold green]Scan Complete![/bold green] Success: {success_count}, Failed: {failed_count}")
173
243
 
@@ -187,7 +257,7 @@ def runner(arguments):
187
257
 
188
258
  def main():
189
259
  # Argument parsing for CLI usage
190
- parser = argparse.ArgumentParser("MultiVolatility")
260
+ parser = argparse.ArgumentParser("multivol")
191
261
  parser.add_argument("--api", action="store_true", help="Start API server")
192
262
  parser.add_argument("--dev", action="store_true", help="Enable developer mode (hot reload)")
193
263
  parser.add_argument("--host-path", type=str, required=False, default=None, help="Root path of the project on the Host machine (required for Docker-in-Docker)")
@@ -198,7 +268,7 @@ def main():
198
268
  vol2_parser.add_argument("--profiles-path", help="Path to the directory with the profiles.", default=os.path.join(os.getcwd(), "volatility2_profiles"))
199
269
  vol2_parser.add_argument("--profile", help="Profile to use.", required=True)
200
270
  vol2_parser.add_argument("--dump", help="Dump to parse.", required=True)
201
- vol2_parser.add_argument("--image", help="Docker image to use.", required=True)
271
+ vol2_parser.add_argument("--image", help="Docker image to use.", required=False, default="sp00kyskelet0n/volatility2")
202
272
  vol2_parser.add_argument("--commands", help="Commands to run : command1,command2,command3", required=False)
203
273
  vol2_os_group = vol2_parser.add_mutually_exclusive_group(required=True)
204
274
  vol2_os_group.add_argument("--linux", action="store_true", help="For a Linux memory dump")
@@ -206,24 +276,31 @@ def main():
206
276
  vol2_parser.add_argument("--light", action="store_true", help="Use the main modules.")
207
277
  vol2_parser.add_argument("--full", action="store_true", help="Use all modules.")
208
278
  vol2_parser.add_argument("--format", help="Format of the outputs: json, text", required=False, default="text")
209
- vol2_parser.add_argument("--processes", type=int, required=False, default=None, help="Max number of concurrent processes")
279
+ vol2_parser.add_argument("--processes", type=int, required=False, default=None, help="Max number of concurrent processes.")
280
+ vol2_parser.add_argument("--output", required=False, help="Directory where outputs will be written (Default: output_YYYY_MM_DD_HH_MM_SS).")
210
281
 
211
282
  # Volatility3 argument group
212
283
  vol3_parser = subparser.add_parser("vol3", help="Use volatility3.")
213
284
  vol3_parser.add_argument("--dump", help="Dump to parse.", required=True)
214
- vol3_parser.add_argument("--image", help="Docker image to use.", required=True)
285
+ vol3_parser.add_argument("--image", help="Docker image to use.", required=False, default="sp00kyskelet0n/volatility3")
215
286
  vol3_parser.add_argument("--symbols-path", help="Path to the directory with the symbols.", required=False, default=os.path.join(os.getcwd(), "volatility3_symbols"))
216
287
  vol3_parser.add_argument("--cache-path", help="Path to directory with the cache for volatility3.", required=False, default=os.path.join(os.getcwd(), "volatility3_cache"))
217
288
  vol3_parser.add_argument("--plugins-dir", help="Path to directory with the plugins", required=False, default=os.path.join(os.getcwd(), "volatility3_plugins"))
218
289
  vol3_parser.add_argument("--commands", help="Commands to run : command1,command2,command3", required=False)
219
290
  vol3_os_group = vol3_parser.add_mutually_exclusive_group(required=True)
220
- vol3_os_group.add_argument("--linux", action="store_true", help="It's a Linux memory dump")
221
- vol3_os_group.add_argument("--windows", action="store_true", help="It's a Windows memory dump")
291
+ vol3_os_group.add_argument("--linux", action="store_true", help="It's a Linux memory dump.")
292
+ vol3_os_group.add_argument("--windows", action="store_true", help="It's a Windows memory dump.")
222
293
  vol3_parser.add_argument("--light", action="store_true", help="Use the principal modules.")
223
294
  vol3_parser.add_argument("--fetch-symbol", action="store_true", help="Fetch automatically symbol from github.com/Abyss-W4tcher/volatility3-symbols", required=False)
224
295
  vol3_parser.add_argument("--full", action="store_true", help="Use all modules.")
225
296
  vol3_parser.add_argument("--format", help="Format of the outputs: json, text", required=False, default="text")
226
297
  vol3_parser.add_argument("--processes", type=int, required=False, default=None, help="Max number of concurrent processes")
298
+ vol3_parser.add_argument("--output", required=False, help="Directory where outputs will be written (Default: output_YYYY_MM_DD_HH_MM_SS).")
299
+
300
+ # Global arguments
301
+ parser.add_argument("--debug", action="store_true", help="Show executed Docker commands")
302
+ parser.add_argument("--scan-id", help="Scan UUID for API status updates", required=False)
303
+
227
304
  args = parser.parse_args()
228
305
 
229
306
  if args.api:
@@ -243,13 +320,12 @@ def main():
243
320
  print("[-] --linux or --windows required.")
244
321
  sys.exit(1)
245
322
 
246
- # Prevent unsupported combinations for Volatility3 Linux
247
- if (args.mode == "vol3" and args.linux and args.light) or (args.mode == "vol3" and args.linux and args.full):
248
- print("[-] --linux not available with --full or --light")
323
+ if getattr(args, "fetch_symbol", False) and not args.linux:
324
+ print("[-] --fetch-symbol only available with --linux")
249
325
  sys.exit(1)
250
326
 
251
- if args.fetch_symbol and not args.linux:
252
- print("[-] --fetch-symbol only available with --linux")
327
+ if args.mode == "vol2" and getattr(args, "fetch_symbol", False):
328
+ print("[-] --fetch-symbol only available with vol3")
253
329
  sys.exit(1)
254
330
 
255
331
  # Validate output format
multivol/strings.py ADDED
@@ -0,0 +1,71 @@
1
+ import docker
2
+ import time
3
+ import os
4
+ from rich import print as rprint
5
+
6
+ def resolve_path(path, host_path):
7
+ # If host_path is set, replacing the current working directory prefix with host_path
8
+ if host_path:
9
+ if path.startswith("/storage"):
10
+ # Handle special storage mapping for Docker
11
+ # Map /storage -> {host_path}/storage/data
12
+ rel_path = os.path.relpath(path, "/storage")
13
+ return os.path.join(host_path, "storage", "data", rel_path)
14
+
15
+ if path.startswith(os.getcwd()):
16
+ rel_path = os.path.relpath(path, os.getcwd())
17
+ return os.path.join(host_path, rel_path)
18
+ return path
19
+
20
+ def safe_print(message, lock):
21
+ if lock:
22
+ with lock:
23
+ rprint(message)
24
+ else:
25
+ rprint(message)
26
+
27
+ def get_strings(dump, dump_dir, output_dir, docker_image, lock=False, host_path=None):
28
+ output_file = os.path.join(output_dir, f"strings_output.txt")
29
+ host_output_dir = resolve_path(os.path.abspath(output_dir), host_path)
30
+
31
+ host_dump_path = resolve_path(os.path.abspath(dump_dir), host_path)
32
+ host_dump_dir = os.path.dirname(host_dump_path)
33
+
34
+ volumes = {
35
+ host_dump_dir: {'bind': '/dump_dir', 'mode': 'ro'},
36
+ host_output_dir: {'bind': '/output', 'mode': 'rw'}
37
+ }
38
+
39
+ dump_filename = os.path.basename(dump)
40
+ cmd_args = f"strings /dump_dir/{dump_filename}"
41
+
42
+ client = docker.from_env()
43
+
44
+ try:
45
+ container = client.containers.run(
46
+ image=docker_image,
47
+ command=cmd_args,
48
+ volumes=volumes,
49
+ tty=True,
50
+ detach=True,
51
+ remove=False
52
+ )
53
+
54
+ with open(output_file, "wb") as file:
55
+ try:
56
+ for chunk in container.logs(stream=True):
57
+ file.write(chunk)
58
+ except Exception as log_err:
59
+ # Handle Docker log rotation errors
60
+ safe_print(f"[!] Log streaming interrupted: {log_err}, fetching remaining logs...", lock)
61
+ try:
62
+ remaining_logs = container.logs(stream=False)
63
+ file.write(remaining_logs)
64
+ except:
65
+ pass
66
+
67
+ container.wait()
68
+ container.remove()
69
+
70
+ except Exception as e:
71
+ safe_print(f"[!] Error running strings: {e}", lock)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multivol
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: MultiVolatility: Analyze memory dumps faster than ever with Volatility2 and Volatility3 in parallel using Docker
5
5
  Home-page: https://github.com/BoBNewz/MultiVolatility
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,12 @@
1
+ multivol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ multivol/api.py,sha256=vk-v4yNxS0KF0FEegGixZxExtXXgouZNn_l_zxxyGB8,58903
3
+ multivol/multi_volatility2.py,sha256=rLM3RLzIf_b7EZiMKd1_8v2Q5K5WEJhmxwZVS74FMsE,4668
4
+ multivol/multi_volatility3.py,sha256=Pd20_d_9USVpT7pnu0cimGUOd1vx8A4PH9EiYceiR4I,9187
5
+ multivol/multivol.py,sha256=iIqklfEtPQbVMo21dS5PwUdT83Qoj8tSRYuH__Py02A,17176
6
+ multivol/strings.py,sha256=zh-oxtuhWkq_Qz-dh2Cp8nB1wCUWDv-LNaqKkftYFgo,2409
7
+ multivol-0.1.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
8
+ multivol-0.1.4.dist-info/METADATA,sha256=Qx4_etq0dM737SlZI64C9adunOvn1JwqbKttSrEL51I,4292
9
+ multivol-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ multivol-0.1.4.dist-info/entry_points.txt,sha256=FM4lUHzrKUmV37U6IemQkRGXEJdgyB7-dwtw1jgwTQc,52
11
+ multivol-0.1.4.dist-info/top_level.txt,sha256=DcxSP883XnM_ad5TXyCIZkzDcYSoI1bPTie_AzHlN0A,9
12
+ multivol-0.1.4.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- multivol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- multivol/api.py,sha256=N_7Sm8Ys_rjRI9MLm-ThTMcDXe6JJU1G11p6zljvQUs,37503
3
- multivol/multi_volatility2.py,sha256=_Z2yxF05xLjzJSZBBisUEMT6WKy1YahbH4E-l41XvnI,9789
4
- multivol/multi_volatility3.py,sha256=9jSfw5ti9TKTGO_tbeM4IaCJfufpNQoR_wILHF4Rmbs,9608
5
- multivol/multivol.py,sha256=TI3y7jcC39XK3zxgwlKiOmTfOT69jzNDgbqf44p11Qc,13435
6
- multivol-0.1.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
- multivol-0.1.3.dist-info/METADATA,sha256=P_825nIZyAAIZlo-gXzPdjNpwdV-vK1UOXbybwQq_IY,4292
8
- multivol-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
- multivol-0.1.3.dist-info/entry_points.txt,sha256=FM4lUHzrKUmV37U6IemQkRGXEJdgyB7-dwtw1jgwTQc,52
10
- multivol-0.1.3.dist-info/top_level.txt,sha256=DcxSP883XnM_ad5TXyCIZkzDcYSoI1bPTie_AzHlN0A,9
11
- multivol-0.1.3.dist-info/RECORD,,