dfindexeddb 20240501__tar.gz → 20240519__tar.gz

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.
Files changed (43) hide show
  1. {dfindexeddb-20240501/dfindexeddb.egg-info → dfindexeddb-20240519}/PKG-INFO +34 -13
  2. {dfindexeddb-20240501 → dfindexeddb-20240519}/README.md +30 -12
  3. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/blink.py +5 -0
  4. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/record.py +11 -6
  5. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/cli.py +16 -6
  6. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/webkit.py +15 -7
  7. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/cli.py +69 -6
  8. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/log.py +9 -3
  9. dfindexeddb-20240519/dfindexeddb/leveldb/plugins/__init__.py +17 -0
  10. dfindexeddb-20240519/dfindexeddb/leveldb/plugins/chrome_notifications.py +135 -0
  11. dfindexeddb-20240519/dfindexeddb/leveldb/plugins/interface.py +36 -0
  12. dfindexeddb-20240519/dfindexeddb/leveldb/plugins/manager.py +75 -0
  13. dfindexeddb-20240519/dfindexeddb/leveldb/plugins/notification_database_data_pb2.py +38 -0
  14. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/record.py +33 -1
  15. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/utils.py +34 -0
  16. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/version.py +1 -1
  17. {dfindexeddb-20240501 → dfindexeddb-20240519/dfindexeddb.egg-info}/PKG-INFO +34 -13
  18. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/SOURCES.txt +6 -1
  19. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/requires.txt +4 -0
  20. {dfindexeddb-20240501 → dfindexeddb-20240519}/pyproject.toml +8 -4
  21. {dfindexeddb-20240501 → dfindexeddb-20240519}/AUTHORS +0 -0
  22. {dfindexeddb-20240501 → dfindexeddb-20240519}/LICENSE +0 -0
  23. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/__init__.py +0 -0
  24. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/errors.py +0 -0
  25. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/__init__.py +0 -0
  26. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/__init__.py +0 -0
  27. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/definitions.py +0 -0
  28. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/chromium/v8.py +0 -0
  29. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/firefox/__init__.py +0 -0
  30. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/__init__.py +0 -0
  31. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/definitions.py +0 -0
  32. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/safari/record.py +0 -0
  33. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/indexeddb/utils.py +0 -0
  34. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/__init__.py +0 -0
  35. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/definitions.py +0 -0
  36. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/descriptor.py +0 -0
  37. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/ldb.py +0 -0
  38. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb/leveldb/utils.py +0 -0
  39. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/dependency_links.txt +0 -0
  40. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/entry_points.txt +0 -0
  41. {dfindexeddb-20240501 → dfindexeddb-20240519}/dfindexeddb.egg-info/top_level.txt +0 -0
  42. {dfindexeddb-20240501 → dfindexeddb-20240519}/setup.cfg +0 -0
  43. {dfindexeddb-20240501 → dfindexeddb-20240519}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dfindexeddb
3
- Version: 20240501
3
+ Version: 20240519
4
4
  Summary: dfindexeddb is an experimental Python tool for performing digital forensic analysis of IndexedDB and leveldb files.
5
5
  Author-email: Syd Pleno <sydp@google.com>
6
6
  Maintainer-email: dfIndexeddb Developers <dfindexeddb-dev@googlegroups.com>
@@ -219,6 +219,9 @@ License-File: LICENSE
219
219
  License-File: AUTHORS
220
220
  Requires-Dist: python-snappy==0.6.1
221
221
  Requires-Dist: zstd==1.5.5.1
222
+ Provides-Extra: plugins
223
+ Requires-Dist: protobuf; extra == "plugins"
224
+ Requires-Dist: dfdatetime; extra == "plugins"
222
225
 
223
226
  # dfIndexeddb
224
227
 
@@ -255,6 +258,12 @@ include:
255
258
  $ pip install dfindexeddb
256
259
  ```
257
260
 
261
+ To also install the dependencies for leveldb/indexeddb plugins, run
262
+ ```
263
+ $ pip install 'dfindexeddb[plugins]'
264
+ ```
265
+
266
+
258
267
  ## Installation from source
259
268
 
260
269
  1. [Linux] Install the snappy compression development package
@@ -273,6 +282,11 @@ include:
273
282
  $ pip install .
274
283
  ```
275
284
 
285
+ To also install the dependencies for leveldb/indexeddb plugins, run
286
+ ```
287
+ $ pip install '.[plugins]'
288
+ ```
289
+
276
290
  ## Usage
277
291
 
278
292
  Two CLI tools for parsing IndexedDB/LevelDB files are available after
@@ -359,7 +373,15 @@ options:
359
373
  To parse records from a LevelDB folder, use the following command:
360
374
 
361
375
  ```
362
- dfindexeddb db -s SOURCE
376
+ dfleveldb db -s SOURCE
377
+ ```
378
+
379
+ To parse records from a LevelDB folder, and use the sequence number to
380
+ determine recovered records and output as JSON, use the
381
+ following command:
382
+
383
+ ```
384
+ dfleveldb db -s SOURCE --use_sequence_number
363
385
  ```
364
386
 
365
387
  To parse blocks / physical records/ write batches / internal key records from a
@@ -383,15 +405,14 @@ following command:
383
405
 
384
406
  ```
385
407
  $ dfleveldb descriptor -s SOURCE [-o {json,jsonl,repr}] [-t {blocks,physical_records,versionedit} | -v]
386
-
387
- options:
388
- -h, --help show this help message and exit
389
- -s SOURCE, --source SOURCE
390
- The source leveldb file
391
- -o {json,jsonl,repr}, --output {json,jsonl,repr}
392
- Output format. Default is json
393
- -t {blocks,physical_records,versionedit}, --structure_type {blocks,physical_records,versionedit}
394
- Parses the specified structure. Default is versionedit.
395
- -v, --version_history
396
- Parses the leveldb version history.
397
408
  ```
409
+
410
+ #### Plugins
411
+
412
+ To apply a plugin parser for a leveldb file/folder, add the
413
+ `--plugin [Plugin Name]` argument. Currently, there is support for the
414
+ following artifacts:
415
+
416
+ | Plugin Name | Artifact Name |
417
+ | -------- | ------- |
418
+ | `ChromeNotificationRecord` | Chrome/Chromium Notifications |
@@ -33,6 +33,12 @@ include:
33
33
  $ pip install dfindexeddb
34
34
  ```
35
35
 
36
+ To also install the dependencies for leveldb/indexeddb plugins, run
37
+ ```
38
+ $ pip install 'dfindexeddb[plugins]'
39
+ ```
40
+
41
+
36
42
  ## Installation from source
37
43
 
38
44
  1. [Linux] Install the snappy compression development package
@@ -51,6 +57,11 @@ include:
51
57
  $ pip install .
52
58
  ```
53
59
 
60
+ To also install the dependencies for leveldb/indexeddb plugins, run
61
+ ```
62
+ $ pip install '.[plugins]'
63
+ ```
64
+
54
65
  ## Usage
55
66
 
56
67
  Two CLI tools for parsing IndexedDB/LevelDB files are available after
@@ -137,7 +148,15 @@ options:
137
148
  To parse records from a LevelDB folder, use the following command:
138
149
 
139
150
  ```
140
- dfindexeddb db -s SOURCE
151
+ dfleveldb db -s SOURCE
152
+ ```
153
+
154
+ To parse records from a LevelDB folder, and use the sequence number to
155
+ determine recovered records and output as JSON, use the
156
+ following command:
157
+
158
+ ```
159
+ dfleveldb db -s SOURCE --use_sequence_number
141
160
  ```
142
161
 
143
162
  To parse blocks / physical records/ write batches / internal key records from a
@@ -161,15 +180,14 @@ following command:
161
180
 
162
181
  ```
163
182
  $ dfleveldb descriptor -s SOURCE [-o {json,jsonl,repr}] [-t {blocks,physical_records,versionedit} | -v]
164
-
165
- options:
166
- -h, --help show this help message and exit
167
- -s SOURCE, --source SOURCE
168
- The source leveldb file
169
- -o {json,jsonl,repr}, --output {json,jsonl,repr}
170
- Output format. Default is json
171
- -t {blocks,physical_records,versionedit}, --structure_type {blocks,physical_records,versionedit}
172
- Parses the specified structure. Default is versionedit.
173
- -v, --version_history
174
- Parses the leveldb version history.
175
183
  ```
184
+
185
+ #### Plugins
186
+
187
+ To apply a plugin parser for a leveldb file/folder, add the
188
+ `--plugin [Plugin Name]` argument. Currently, there is support for the
189
+ following artifacts:
190
+
191
+ | Plugin Name | Artifact Name |
192
+ | -------- | ------- |
193
+ | `ChromeNotificationRecord` | Chrome/Chromium Notifications |
@@ -780,6 +780,9 @@ class V8ScriptValueDecoder:
780
780
 
781
781
  Returns:
782
782
  A parsed CryptoKey.
783
+
784
+ Raises:
785
+ ParserError: if there is an unexpected CryptoKeySubTag.
783
786
  """
784
787
  _, raw_key_byte = self.deserializer.decoder.DecodeUint8()
785
788
  key_byte = definitions.CryptoKeySubTag(raw_key_byte)
@@ -795,6 +798,8 @@ class V8ScriptValueDecoder:
795
798
  key_type, algorithm_parameters = self._ReadED25519Key()
796
799
  elif key_byte == definitions.CryptoKeySubTag.NO_PARAMS_KEY:
797
800
  key_type, algorithm_parameters = self.ReadNoParamsKey()
801
+ else:
802
+ raise errors.ParserError('Unexpected CryptoKeySubTag')
798
803
 
799
804
  _, raw_usages = self.deserializer.decoder.DecodeUint32Varint()
800
805
  usages = definitions.CryptoKeyUsage(raw_usages)
@@ -1275,14 +1275,17 @@ class ExternalObjectEntry(utils.FromDecoderMixin):
1275
1275
  filename = None
1276
1276
  last_modified = None
1277
1277
  token = None
1278
- elif (object_type ==
1279
- definitions.ExternalObjectType.FILE_SYSTEM_ACCESS_HANDLE):
1278
+ else:
1279
+ if (object_type ==
1280
+ definitions.ExternalObjectType.FILE_SYSTEM_ACCESS_HANDLE):
1281
+ _, token = decoder.DecodeBlobWithLength()
1282
+ else:
1283
+ token = None
1280
1284
  blob_number = None
1281
1285
  mime_type = None
1282
1286
  size = None
1283
1287
  filename = None
1284
1288
  last_modified = None
1285
- _, token = decoder.DecodeBlobWithLength()
1286
1289
 
1287
1290
  return cls(offset=base_offset + offset, object_type=object_type,
1288
1291
  blob_number=blob_number, mime_type=mime_type, size=size,
@@ -1417,20 +1420,22 @@ class FolderReader:
1417
1420
 
1418
1421
  def GetRecords(
1419
1422
  self,
1420
- use_manifest: bool = False
1423
+ use_manifest: bool = False,
1424
+ use_sequence_number: bool = False
1421
1425
  ) -> Generator[IndexedDBRecord, None, None]:
1422
1426
  """Yield LevelDBRecords.
1423
1427
 
1424
1428
  Args:
1425
1429
  use_manifest: True to use the current manifest in the folder as a means to
1426
1430
  find the active file set.
1427
-
1431
+ use_sequence_number: True to use the sequence number to determine the
1428
1432
  Yields:
1429
1433
  IndexedDBRecord.
1430
1434
  """
1431
1435
  leveldb_folder_reader = record.FolderReader(self.foldername)
1432
1436
  for leveldb_record in leveldb_folder_reader.GetRecords(
1433
- use_manifest=use_manifest):
1437
+ use_manifest=use_manifest,
1438
+ use_sequence_number=use_sequence_number):
1434
1439
  try:
1435
1440
  yield IndexedDBRecord.FromLevelDBRecord(
1436
1441
  leveldb_record)
@@ -20,6 +20,7 @@ from datetime import datetime
20
20
  import json
21
21
  import pathlib
22
22
 
23
+ from dfindexeddb import utils
23
24
  from dfindexeddb import version
24
25
  from dfindexeddb.indexeddb.chromium import blink
25
26
  from dfindexeddb.indexeddb.chromium import record as chromium_record
@@ -36,7 +37,7 @@ class Encoder(json.JSONEncoder):
36
37
  """A JSON encoder class for dfindexeddb fields."""
37
38
  def default(self, o):
38
39
  if dataclasses.is_dataclass(o):
39
- o_dict = dataclasses.asdict(o)
40
+ o_dict = utils.asdict(o)
40
41
  return o_dict
41
42
  if isinstance(o, bytes):
42
43
  out = []
@@ -83,7 +84,9 @@ def DbCommand(args):
83
84
  """The CLI for processing a directory as IndexedDB."""
84
85
  if args.format in ('chrome', 'chromium'):
85
86
  for db_record in chromium_record.FolderReader(
86
- args.source).GetRecords(use_manifest=args.use_manifest):
87
+ args.source).GetRecords(
88
+ use_manifest=args.use_manifest,
89
+ use_sequence_number=args.use_sequence_number):
87
90
  _Output(db_record, output=args.output)
88
91
  elif args.format == 'safari':
89
92
  for db_record in safari_record.FileReader(args.source).Records():
@@ -139,15 +142,22 @@ def App():
139
142
  help=(
140
143
  'The source IndexedDB folder (for chrome/chromium) '
141
144
  'or file (for safari).'))
145
+ recover_group = parser_db.add_mutually_exclusive_group()
146
+ recover_group.add_argument(
147
+ '--use_manifest',
148
+ action='store_true',
149
+ help='Use manifest file to determine active/deleted records.')
150
+ recover_group.add_argument(
151
+ '--use_sequence_number',
152
+ action='store_true',
153
+ help=(
154
+ 'Use sequence number and file offset to determine active/deleted '
155
+ 'records.'))
142
156
  parser_db.add_argument(
143
157
  '--format',
144
158
  required=True,
145
159
  choices=['chromium', 'chrome', 'safari'],
146
160
  help='The type of IndexedDB to parse.')
147
- parser_db.add_argument(
148
- '--use_manifest',
149
- action='store_true',
150
- help='Use manifest file to determine active/deleted records.')
151
161
  parser_db.add_argument(
152
162
  '-o',
153
163
  '--output',
@@ -290,6 +290,7 @@ class SerializedScriptValueDecoder():
290
290
  """
291
291
  _, length = self.decoder.DecodeUint32()
292
292
  array = JSArray()
293
+ self.object_pool.append(array)
293
294
  for _ in range(length):
294
295
  _, _ = self.decoder.DecodeUint32()
295
296
  _, value = self.DecodeValue()
@@ -314,13 +315,13 @@ class SerializedScriptValueDecoder():
314
315
  """Decodes an Object value."""
315
316
  tag = self.PeekTag()
316
317
  js_object = {}
318
+ self.object_pool.append(js_object)
317
319
  while tag != definitions.TerminatorTag:
318
320
  name = self.DecodeStringData()
319
321
  _, value = self.DecodeValue()
320
322
  js_object[name] = value
321
323
  tag = self.PeekTag()
322
324
  _ = self.decoder.DecodeUint32()
323
- self.object_pool.append(js_object)
324
325
  return js_object
325
326
 
326
327
  def DecodeStringData(self) -> str:
@@ -342,11 +343,11 @@ class SerializedScriptValueDecoder():
342
343
 
343
344
  if peeked_tag == definitions.StringPoolTag:
344
345
  _ = self.decoder.DecodeUint32()
345
- if len(self.constant_pool) < 0xff:
346
+ if len(self.constant_pool) <= 0xff:
346
347
  _, cp_index = self.decoder.DecodeUint8()
347
- elif len(self.constant_pool) < 0xffff:
348
+ elif len(self.constant_pool) <= 0xffff:
348
349
  _, cp_index = self.decoder.DecodeUint16()
349
- elif len(self.constant_pool) < 0xffffffff:
350
+ elif len(self.constant_pool) <= 0xffffffff:
350
351
  _, cp_index = self.decoder.DecodeUint32()
351
352
  else:
352
353
  raise errors.ParserError('Unexpected constant pool size value.')
@@ -450,6 +451,7 @@ class SerializedScriptValueDecoder():
450
451
  """Decodes a Map value."""
451
452
  tag = self.PeekSerializationTag()
452
453
  js_map = {} # TODO: make this into a JSMap (like JSArray/JSSet)
454
+ self.object_pool.append(js_map)
453
455
 
454
456
  while tag != definitions.SerializationTag.NON_MAP_PROPERTIES:
455
457
  _, key = self.DecodeValue()
@@ -468,13 +470,13 @@ class SerializedScriptValueDecoder():
468
470
  pool_tag = self.PeekTag()
469
471
 
470
472
  _, tag = self.decoder.DecodeUint32()
471
-
472
473
  return js_map
473
474
 
474
475
  def DecodeSetData(self) -> JSSet:
475
476
  """Decodes a SetData value."""
476
477
  tag = self.PeekSerializationTag()
477
478
  js_set = JSSet()
479
+ self.object_pool.append(js_set)
478
480
 
479
481
  while tag != definitions.SerializationTag.NON_SET_PROPERTIES:
480
482
  _, key = self.DecodeValue()
@@ -540,8 +542,13 @@ class SerializedScriptValueDecoder():
540
542
 
541
543
  def DecodeObjectReference(self) -> Any:
542
544
  """Decodes an ObjectReference value."""
543
- _, object_ref = self.decoder.DecodeUint8()
544
- return self.object_pool[object_ref - 1]
545
+ if len(self.object_pool) < 0xFF:
546
+ _, object_ref = self.decoder.DecodeUint8()
547
+ elif len(self.object_pool) < 0xFFFF:
548
+ _, object_ref = self.decoder.DecodeUint16()
549
+ else: # if len(self.object_pool) < 0xFFFFFFFF:
550
+ _, object_ref = self.decoder.DecodeUint32()
551
+ return self.object_pool[object_ref]
545
552
 
546
553
  def DecodeArrayBufferView(self) -> ArrayBufferView:
547
554
  """Decodes an ArrayBufferView value.
@@ -641,6 +648,7 @@ class SerializedScriptValueDecoder():
641
648
  value = self.DecodeArrayBuffer()
642
649
  elif tag == definitions.SerializationTag.ARRAY_BUFFER_VIEW:
643
650
  value = self.DecodeArrayBufferView()
651
+ self.object_pool.append(value)
644
652
  elif tag == definitions.SerializationTag.ARRAY_BUFFER_TRANSFER:
645
653
  value = self.DecodeArrayBufferTransfer()
646
654
  elif tag == definitions.SerializationTag.TRUE_OBJECT:
@@ -19,11 +19,13 @@ from datetime import datetime
19
19
  import json
20
20
  import pathlib
21
21
 
22
+ from dfindexeddb import utils
22
23
  from dfindexeddb import version
23
24
  from dfindexeddb.leveldb import descriptor
24
25
  from dfindexeddb.leveldb import ldb
25
26
  from dfindexeddb.leveldb import log
26
27
  from dfindexeddb.leveldb import record
28
+ from dfindexeddb.leveldb.plugins import manager
27
29
 
28
30
 
29
31
  _VALID_PRINTABLE_CHARACTERS = (
@@ -37,7 +39,7 @@ class Encoder(json.JSONEncoder):
37
39
  def default(self, o):
38
40
  """Returns a serializable object for o."""
39
41
  if dataclasses.is_dataclass(o):
40
- o_dict = dataclasses.asdict(o)
42
+ o_dict = utils.asdict(o)
41
43
  return o_dict
42
44
  if isinstance(o, bytes):
43
45
  out = []
@@ -66,13 +68,39 @@ def _Output(structure, output):
66
68
 
67
69
  def DbCommand(args):
68
70
  """The CLI for processing leveldb folders."""
71
+ if args.plugin and args.plugin == 'list':
72
+ for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
73
+ print(plugin)
74
+ return
75
+
76
+ if args.plugin:
77
+ plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
78
+ else:
79
+ plugin_class = None
80
+
69
81
  for leveldb_record in record.FolderReader(
70
- args.source).GetRecords(use_manifest=args.use_manifest):
71
- _Output(leveldb_record, output=args.output)
82
+ args.source).GetRecords(
83
+ use_manifest=args.use_manifest,
84
+ use_sequence_number=args.use_sequence_number):
85
+ if plugin_class:
86
+ plugin_record = plugin_class.FromLevelDBRecord(leveldb_record)
87
+ _Output(plugin_record, output=args.output)
88
+ else:
89
+ _Output(leveldb_record, output=args.output)
72
90
 
73
91
 
74
92
  def LdbCommand(args):
75
93
  """The CLI for processing ldb files."""
94
+ if args.plugin and args.plugin == 'list':
95
+ for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
96
+ print(plugin)
97
+ return
98
+
99
+ if args.plugin:
100
+ plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
101
+ else:
102
+ plugin_class = None
103
+
76
104
  ldb_file = ldb.FileReader(args.source)
77
105
 
78
106
  if args.structure_type == 'blocks':
@@ -83,7 +111,11 @@ def LdbCommand(args):
83
111
  elif args.structure_type == 'records' or not args.structure_type:
84
112
  # Prints key value record information.
85
113
  for key_value_record in ldb_file.GetKeyValueRecords():
86
- _Output(key_value_record, output=args.output)
114
+ if plugin_class:
115
+ plugin_record = plugin_class.FromKeyValueRecord(key_value_record)
116
+ _Output(plugin_record, output=args.output)
117
+ else:
118
+ _Output(key_value_record, output=args.output)
87
119
 
88
120
  else:
89
121
  print(f'{args.structure_type} is not supported for ldb files.')
@@ -91,6 +123,16 @@ def LdbCommand(args):
91
123
 
92
124
  def LogCommand(args):
93
125
  """The CLI for processing log files."""
126
+ if args.plugin and args.plugin == 'list':
127
+ for plugin, _ in manager.LeveldbPluginManager.GetPlugins():
128
+ print(plugin)
129
+ return
130
+
131
+ if args.plugin:
132
+ plugin_class = manager.LeveldbPluginManager.GetPlugin(args.plugin)
133
+ else:
134
+ plugin_class = None
135
+
94
136
  log_file = log.FileReader(args.source)
95
137
 
96
138
  if args.structure_type == 'blocks':
@@ -112,7 +154,11 @@ def LogCommand(args):
112
154
  or not args.structure_type):
113
155
  # Prints key value record information.
114
156
  for internal_key_record in log_file.GetParsedInternalKeys():
115
- _Output(internal_key_record, output=args.output)
157
+ if plugin_class:
158
+ plugin_record = plugin_class.FromKeyValueRecord(internal_key_record)
159
+ _Output(plugin_record, output=args.output)
160
+ else:
161
+ _Output(internal_key_record, output=args.output)
116
162
 
117
163
  else:
118
164
  print(f'{args.structure_type} is not supported for log files.')
@@ -144,6 +190,7 @@ def DescriptorCommand(args):
144
190
  else:
145
191
  print(f'{args.structure_type} is not supported for descriptor files.')
146
192
 
193
+
147
194
  def App():
148
195
  """The CLI app entrypoint for parsing leveldb files."""
149
196
  parser = argparse.ArgumentParser(
@@ -160,10 +207,17 @@ def App():
160
207
  required=True,
161
208
  type=pathlib.Path,
162
209
  help='The source leveldb directory')
163
- parser_db.add_argument(
210
+ recover_group = parser_db.add_mutually_exclusive_group()
211
+ recover_group.add_argument(
164
212
  '--use_manifest',
165
213
  action='store_true',
166
214
  help='Use manifest file to determine active/deleted records.')
215
+ recover_group.add_argument(
216
+ '--use_sequence_number',
217
+ action='store_true',
218
+ help=(
219
+ 'Use sequence number and file offset to determine active/deleted '
220
+ 'records.'))
167
221
  parser_db.add_argument(
168
222
  '-o',
169
223
  '--output',
@@ -173,6 +227,9 @@ def App():
173
227
  'repr'],
174
228
  default='json',
175
229
  help='Output format. Default is json')
230
+ parser_db.add_argument(
231
+ '--plugin',
232
+ help='Use plugin to parse records.')
176
233
  parser_db.set_defaults(func=DbCommand)
177
234
 
178
235
  parser_log = subparsers.add_parser(
@@ -191,6 +248,9 @@ def App():
191
248
  'repr'],
192
249
  default='json',
193
250
  help='Output format. Default is json')
251
+ parser_log.add_argument(
252
+ '--plugin',
253
+ help='Use plugin to parse records.')
194
254
  parser_log.add_argument(
195
255
  '-t',
196
256
  '--structure_type',
@@ -218,6 +278,9 @@ def App():
218
278
  'repr'],
219
279
  default='json',
220
280
  help='Output format. Default is json')
281
+ parser_ldb.add_argument(
282
+ '--plugin',
283
+ help='Use plugin to parse records.')
221
284
  parser_ldb.add_argument(
222
285
  '-t',
223
286
  '--structure_type',
@@ -152,7 +152,7 @@ class PhysicalRecord(utils.FromDecoderMixin):
152
152
  @classmethod
153
153
  def FromDecoder(
154
154
  cls, decoder: utils.LevelDBDecoder, base_offset: int = 0
155
- ) -> PhysicalRecord:
155
+ ) -> Optional[PhysicalRecord]:
156
156
  """Decodes a PhysicalRecord from the current position of a LevelDBDecoder.
157
157
 
158
158
  Args:
@@ -161,11 +161,13 @@ class PhysicalRecord(utils.FromDecoderMixin):
161
161
  read from.
162
162
 
163
163
  Returns:
164
- A PhysicalRecord.
164
+ A PhysicalRecord or None if the parsed header is 0.
165
165
  """
166
166
  offset, checksum = decoder.DecodeUint32()
167
167
  _, length = decoder.DecodeUint16()
168
168
  _, record_type_byte = decoder.DecodeUint8()
169
+ if checksum == 0 or length == 0 or record_type_byte == 0:
170
+ return None
169
171
  try:
170
172
  record_type = definitions.LogFilePhysicalRecordType(record_type_byte)
171
173
  except ValueError as error:
@@ -206,7 +208,11 @@ class Block:
206
208
  buffer_length = len(self.data)
207
209
 
208
210
  while buffer.tell() + PhysicalRecord.PHYSICAL_HEADER_LENGTH < buffer_length:
209
- yield PhysicalRecord.FromStream(buffer, base_offset=self.offset)
211
+ record = PhysicalRecord.FromStream(buffer, base_offset=self.offset)
212
+ if record:
213
+ yield record
214
+ else:
215
+ return
210
216
 
211
217
  @classmethod
212
218
  def FromStream(cls, stream: BinaryIO) -> Optional[Block]:
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2024 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Leveldb Plugin module."""
16
+
17
+ from dfindexeddb.leveldb.plugins import chrome_notifications
@@ -0,0 +1,135 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2024 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Parser plugin for Chrome Notifications."""
16
+ from __future__ import annotations
17
+
18
+ import dataclasses
19
+ import logging
20
+
21
+ from typing import Optional
22
+
23
+ try:
24
+ # pytype: disable=import-error
25
+ from dfdatetime import webkit_time
26
+ from dfindexeddb.leveldb.plugins import notification_database_data_pb2 as \
27
+ notification_pb2
28
+ # pytype: enable=import-error
29
+ _has_import_dependencies = True
30
+ except ImportError as err:
31
+ _has_import_dependencies = False
32
+ logging.warning((
33
+ 'Could not import dependencies for '
34
+ 'leveldb.plugins.chrome_notifications: %s'), err)
35
+
36
+ from dfindexeddb.indexeddb.chromium import blink
37
+ from dfindexeddb.leveldb.plugins import interface
38
+ from dfindexeddb.leveldb.plugins import manager
39
+
40
+
41
+ @dataclasses.dataclass
42
+ class ChromeNotificationRecord(interface.LeveldbPlugin):
43
+ """Chrome notification record."""
44
+ src_file: Optional[str] = None
45
+ offset: Optional[int] = None
46
+ key: Optional[str] = None
47
+ sequence_number: Optional[int] = None
48
+ type: Optional[int] = None
49
+ origin: Optional[str] = None
50
+ service_worker_registration_id: Optional[int] = None
51
+ notification_title: Optional[str] = None
52
+ notification_direction: Optional[str] = None
53
+ notification_lang: Optional[str] = None
54
+ notification_body: Optional[str] = None
55
+ notification_tag: Optional[str] = None
56
+ notification_icon: Optional[str] = None
57
+ notification_silent: Optional[bool] = None
58
+ notification_data: Optional[str] = None
59
+ notification_require_interaction: Optional[bool] = None
60
+ notification_time: Optional[str] = None
61
+ notification_renotify: Optional[bool] = None
62
+ notification_badge: Optional[str] = None
63
+ notification_image: Optional[str] = None
64
+ notification_id: Optional[str] = None
65
+ replaced_existing_notification: Optional[bool] = None
66
+ num_clicks: Optional[int] = None
67
+ num_action_button_clicks: Optional[int] = None
68
+ creation_time: Optional[str] = None
69
+ closed_reason: Optional[str] = None
70
+ has_triggered: Optional[bool] = None
71
+
72
+ @classmethod
73
+ def FromKeyValueRecord(
74
+ cls,
75
+ ldb_record
76
+ ) -> ChromeNotificationRecord:
77
+ record = cls()
78
+ record.offset = ldb_record.offset
79
+ record.key = ldb_record.key.decode()
80
+ record.sequence_number = ldb_record.sequence_number
81
+ record.type = ldb_record.record_type
82
+
83
+ if not ldb_record.value:
84
+ return record
85
+
86
+ # pylint: disable-next=no-member,line-too-long
87
+ notification_proto = notification_pb2.NotificationDatabaseDataProto() # pytype: disable=module-attr
88
+ notification_proto.ParseFromString(ldb_record.value)
89
+
90
+ record.origin = notification_proto.origin
91
+ record.service_worker_registration_id = (
92
+ notification_proto.service_worker_registration_id)
93
+ record.notification_title = notification_proto.notification_data.title
94
+ record.notification_direction = (
95
+ notification_proto.notification_data.direction)
96
+ record.notification_lang = notification_proto.notification_data.lang
97
+ record.notification_body = notification_proto.notification_data.body
98
+ record.notification_tag = notification_proto.notification_data.tag
99
+ record.notification_icon = notification_proto.notification_data.icon
100
+ record.notification_silent = notification_proto.notification_data.silent
101
+ record.notification_data = notification_proto.notification_data.data
102
+ record.notification_require_interaction = (
103
+ notification_proto.notification_data.require_interaction)
104
+ record.notification_time = webkit_time.WebKitTime(
105
+ timestamp=notification_proto.notification_data.timestamp
106
+ ).CopyToDateTimeString()
107
+ record.notification_renotify = notification_proto.notification_data.renotify
108
+ record.notification_badge = notification_proto.notification_data.badge
109
+ record.notification_image = notification_proto.notification_data.image
110
+ record.notification_id = notification_proto.notification_id
111
+ record.replaced_existing_notification = (
112
+ notification_proto.replaced_existing_notification)
113
+ record.num_clicks = notification_proto.num_clicks
114
+ record.num_action_button_clicks = (
115
+ notification_proto.num_action_button_clicks)
116
+ record.creation_time = webkit_time.WebKitTime(
117
+ timestamp=notification_proto.creation_time_millis
118
+ ).CopyToDateTimeString()
119
+ record.closed_reason = notification_proto.closed_reason
120
+ record.has_triggered = notification_proto.has_triggered
121
+
122
+ if not notification_proto.notification_data.data:
123
+ return record
124
+
125
+ notification_data = blink.V8ScriptValueDecoder(
126
+ raw_data=notification_proto.notification_data.data).Deserialize()
127
+ record.notification_data = notification_data
128
+
129
+ return record
130
+
131
+
132
+ # check if dependencies are in existence..
133
+
134
+ if _has_import_dependencies:
135
+ manager.PluginManager.RegisterPlugin(ChromeNotificationRecord)
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2024 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Interface for leveldb plugins."""
16
+ from typing import Any, Union
17
+
18
+ from dfindexeddb.leveldb import record
19
+ from dfindexeddb.leveldb import ldb
20
+ from dfindexeddb.leveldb import log
21
+
22
+ class LeveldbPlugin:
23
+ """The base leveldb plugin class."""
24
+
25
+ @classmethod
26
+ def FromLevelDBRecord(cls,
27
+ ldb_record: record.LevelDBRecord) -> Any:
28
+ """Parses a leveldb record."""
29
+ parsed_record = cls.FromKeyValueRecord(ldb_record.record)
30
+ ldb_record.record = parsed_record
31
+ return ldb_record
32
+
33
+ @classmethod
34
+ def FromKeyValueRecord(
35
+ cls, ldb_record: Union[ldb.KeyValueRecord, log.ParsedInternalKey]) -> Any:
36
+ """Parses a leveldb key value record."""
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2024 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Leveldb plugin manager."""
16
+ from typing import Type
17
+
18
+ from dfindexeddb.leveldb.plugins import interface
19
+
20
+
21
+ class LeveldbPluginManager:
22
+ """The leveldb plugin manager."""
23
+
24
+ _class_registry = {}
25
+
26
+ @classmethod
27
+ def GetPlugins(cls):
28
+ """Retrieves the registered leveldb plugins.
29
+
30
+ Yields:
31
+ tuple: containing:
32
+ str: the name of the leveldb plugin.
33
+ class: the plugin class.
34
+ """
35
+ yield from cls._class_registry.items()
36
+
37
+ @classmethod
38
+ def GetPlugin(cls, plugin_name: str) -> interface.LeveldbPlugin:
39
+ """Retrieves a class object of a specific plugin.
40
+
41
+ Args:
42
+ plugin_name: name of the plugin.
43
+
44
+ Returns:
45
+ the LeveldbPlugin class.
46
+
47
+ Raises:
48
+ KeyError: if the plugin is not found/registered in the manager.
49
+ """
50
+ try:
51
+ return cls._class_registry[plugin_name]
52
+ except KeyError:
53
+ raise KeyError(f'Plugin not found: {plugin_name}')
54
+
55
+ @classmethod
56
+ def RegisterPlugin(cls, plugin_class: Type[interface.LeveldbPlugin]):
57
+ """Registers a leveldb plugin.
58
+
59
+ Args:
60
+ plugin_class (class): the plugin class to register.
61
+
62
+ Raises:
63
+ KeyError: if class is already set for the corresponding name.
64
+ """
65
+ plugin_name = plugin_class.__name__
66
+ if plugin_name in cls._class_registry:
67
+ raise KeyError(f'Plugin already registered {plugin_name}')
68
+ cls._class_registry[plugin_name] = plugin_class
69
+
70
+ @classmethod
71
+ def ClearPlugins(cls):
72
+ """Clears all plugin registrations."""
73
+ cls._class_registry = {}
74
+
75
+ PluginManager = LeveldbPluginManager()
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: notification_database_data.proto
4
+ # Protobuf Python Version: 4.25.1
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+
16
+ # pylint: skip-file
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n notification_database_data.proto\"\xff\t\n\x1dNotificationDatabaseDataProto\x12\"\n\x1apersistent_notification_id\x18\x01 \x01(\x03\x12\x17\n\x0fnotification_id\x18\x05 \x01(\t\x12\x0e\n\x06origin\x18\x02 \x01(\t\x12&\n\x1eservice_worker_registration_id\x18\x03 \x01(\x03\x12&\n\x1ereplaced_existing_notification\x18\x06 \x01(\x08\x12\x12\n\nnum_clicks\x18\x07 \x01(\x05\x12 \n\x18num_action_button_clicks\x18\x08 \x01(\x05\x12\x1c\n\x14\x63reation_time_millis\x18\t \x01(\x03\x12%\n\x1dtime_until_first_click_millis\x18\n \x01(\x03\x12$\n\x1ctime_until_last_click_millis\x18\x0b \x01(\x03\x12\x1f\n\x17time_until_close_millis\x18\x0c \x01(\x03\x12\x42\n\rclosed_reason\x18\r \x01(\x0e\x32+.NotificationDatabaseDataProto.ClosedReason\x12J\n\x11notification_data\x18\x04 \x01(\x0b\x32/.NotificationDatabaseDataProto.NotificationData\x12\x15\n\rhas_triggered\x18\x0e \x01(\x08\x1a\xba\x01\n\x12NotificationAction\x12\x0e\n\x06\x61\x63tion\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0c\n\x04icon\x18\x03 \x01(\t\x12\x44\n\x04type\x18\x04 \x01(\x0e\x32\x36.NotificationDatabaseDataProto.NotificationAction.Type\x12\x13\n\x0bplaceholder\x18\x05 \x01(\t\"\x1c\n\x04Type\x12\n\n\x06\x42UTTON\x10\x00\x12\x08\n\x04TEXT\x10\x01\x1a\xe4\x03\n\x10NotificationData\x12\r\n\x05title\x18\x01 \x01(\t\x12L\n\tdirection\x18\x02 \x01(\x0e\x32\x39.NotificationDatabaseDataProto.NotificationData.Direction\x12\x0c\n\x04lang\x18\x03 \x01(\t\x12\x0c\n\x04\x62ody\x18\x04 \x01(\t\x12\x0b\n\x03tag\x18\x05 \x01(\t\x12\r\n\x05image\x18\x0f \x01(\t\x12\x0c\n\x04icon\x18\x06 \x01(\t\x12\r\n\x05\x62\x61\x64ge\x18\x0e \x01(\t\x12\x1d\n\x11vibration_pattern\x18\t \x03(\x05\x42\x02\x10\x01\x12\x11\n\ttimestamp\x18\x0c \x01(\x03\x12\x10\n\x08renotify\x18\r \x01(\x08\x12\x0e\n\x06silent\x18\x07 \x01(\x08\x12\x1b\n\x13require_interaction\x18\x0b \x01(\x08\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\x12\x42\n\x07\x61\x63tions\x18\n \x03(\x0b\x32\x31.NotificationDatabaseDataProto.NotificationAction\x12\x1e\n\x16show_trigger_timestamp\x18\x10 \x01(\x03\";\n\tDirection\x12\x11\n\rLEFT_TO_RIGHT\x10\x00\x12\x11\n\rRIGHT_TO_LEFT\x10\x01\x12\x08\n\x04\x41UTO\x10\x02\"4\n\x0c\x43losedReason\x12\x08\n\x04USER\x10\x00\x12\r\n\tDEVELOPER\x10\x01\x12\x0b\n\x07UNKNOWN\x10\x02')
18
+
19
+ _globals = globals()
20
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'notification_database_data_pb2', _globals)
22
+ if _descriptor._USE_C_DESCRIPTORS == False:
23
+ DESCRIPTOR._options = None
24
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA'].fields_by_name['vibration_pattern']._options = None
25
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA'].fields_by_name['vibration_pattern']._serialized_options = b'\020\001'
26
+ _globals['_NOTIFICATIONDATABASEDATAPROTO']._serialized_start=37
27
+ _globals['_NOTIFICATIONDATABASEDATAPROTO']._serialized_end=1316
28
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION']._serialized_start=589
29
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION']._serialized_end=775
30
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION_TYPE']._serialized_start=747
31
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONACTION_TYPE']._serialized_end=775
32
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA']._serialized_start=778
33
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA']._serialized_end=1262
34
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA_DIRECTION']._serialized_start=1203
35
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_NOTIFICATIONDATA_DIRECTION']._serialized_end=1262
36
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_CLOSEDREASON']._serialized_start=1264
37
+ _globals['_NOTIFICATIONDATABASEDATAPROTO_CLOSEDREASON']._serialized_end=1316
38
+ # @@protoc_insertion_point(module_scope)
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  """A module for records from LevelDB files."""
16
16
  from __future__ import annotations
17
+ from collections import defaultdict
17
18
  import dataclasses
18
19
  import pathlib
19
20
  import re
@@ -297,9 +298,38 @@ class FolderReader:
297
298
  record.recovered = True
298
299
  yield record
299
300
 
301
+ def _RecordsBySequenceNumber(self) -> Generator[LevelDBRecord, None, None]:
302
+ """Yields LevelDBRecords using the sequence number and file offset.
303
+
304
+ Yields:
305
+ LevelDBRecords.
306
+ """
307
+ unsorted_records = defaultdict(list)
308
+
309
+ for filename in self.foldername.iterdir():
310
+ for leveldb_record in LevelDBRecord.FromFile(filename):
311
+ if leveldb_record:
312
+ unsorted_records[leveldb_record.record.key].append(leveldb_record)
313
+ for _, unsorted_records in unsorted_records.items():
314
+ num_unsorted_records = len(unsorted_records)
315
+ if num_unsorted_records == 1:
316
+ unsorted_records[0].recovered = False
317
+ yield unsorted_records[0]
318
+ else:
319
+ for i, record in enumerate(sorted(
320
+ unsorted_records, key=lambda x: (
321
+ x.record.sequence_number, x.record.offset)),
322
+ start=1):
323
+ if i == num_unsorted_records:
324
+ record.recovered = False
325
+ else:
326
+ record.recovered = True
327
+ yield record
328
+
300
329
  def GetRecords(
301
330
  self,
302
- use_manifest: bool = False
331
+ use_manifest: bool = False,
332
+ use_sequence_number: bool = False
303
333
  ) -> Generator[LevelDBRecord, None, None]:
304
334
  """Yield LevelDBRecords.
305
335
 
@@ -312,6 +342,8 @@ class FolderReader:
312
342
  """
313
343
  if use_manifest:
314
344
  yield from self._RecordsByManifest()
345
+ elif use_sequence_number:
346
+ yield from self._RecordsBySequenceNumber()
315
347
  else:
316
348
  for filename in self.foldername.iterdir():
317
349
  yield from LevelDBRecord.FromFile(filename)
@@ -14,6 +14,8 @@
14
14
  # limitations under the License.
15
15
  """Utilities for dfindexeddb."""
16
16
  from __future__ import annotations
17
+ import copy
18
+ import dataclasses
17
19
  import io
18
20
  import os
19
21
  import struct
@@ -259,3 +261,35 @@ class FromDecoderMixin:
259
261
  """
260
262
  stream = io.BytesIO(raw_data)
261
263
  return cls.FromStream(stream=stream, base_offset=base_offset)
264
+
265
+
266
+ def asdict(obj, *, dict_factory=dict): # pylint: disable=invalid-name
267
+ """Custom implementation of the asdict dataclasses method to include the
268
+ class name under the __type__ attribute name.
269
+ """
270
+ if not dataclasses.is_dataclass(obj):
271
+ raise TypeError("asdict() should be called on dataclass instances")
272
+ return _asdict_inner(obj, dict_factory)
273
+
274
+
275
+ def _asdict_inner(obj, dict_factory):
276
+ """Custom implementation of the _asdict_inner dataclasses method."""
277
+ if dataclasses.is_dataclass(obj):
278
+ result = [('__type__', obj.__class__.__name__)]
279
+ for f in dataclasses.fields(obj):
280
+ value = _asdict_inner(getattr(obj, f.name), dict_factory)
281
+ result.append((f.name, value))
282
+ return dict_factory(result)
283
+
284
+ if isinstance(obj, tuple) and hasattr(obj, '_fields'):
285
+ return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
286
+
287
+ if isinstance(obj, (list, tuple)):
288
+ return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
289
+
290
+ if isinstance(obj, dict):
291
+ return type(obj)((_asdict_inner(k, dict_factory),
292
+ _asdict_inner(v, dict_factory))
293
+ for k, v in obj.items())
294
+
295
+ return copy.deepcopy(obj)
@@ -15,7 +15,7 @@
15
15
  """Version information for dfIndexeddb."""
16
16
 
17
17
 
18
- __version__ = "20240501"
18
+ __version__ = "20240519"
19
19
 
20
20
 
21
21
  def GetVersion():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dfindexeddb
3
- Version: 20240501
3
+ Version: 20240519
4
4
  Summary: dfindexeddb is an experimental Python tool for performing digital forensic analysis of IndexedDB and leveldb files.
5
5
  Author-email: Syd Pleno <sydp@google.com>
6
6
  Maintainer-email: dfIndexeddb Developers <dfindexeddb-dev@googlegroups.com>
@@ -219,6 +219,9 @@ License-File: LICENSE
219
219
  License-File: AUTHORS
220
220
  Requires-Dist: python-snappy==0.6.1
221
221
  Requires-Dist: zstd==1.5.5.1
222
+ Provides-Extra: plugins
223
+ Requires-Dist: protobuf; extra == "plugins"
224
+ Requires-Dist: dfdatetime; extra == "plugins"
222
225
 
223
226
  # dfIndexeddb
224
227
 
@@ -255,6 +258,12 @@ include:
255
258
  $ pip install dfindexeddb
256
259
  ```
257
260
 
261
+ To also install the dependencies for leveldb/indexeddb plugins, run
262
+ ```
263
+ $ pip install 'dfindexeddb[plugins]'
264
+ ```
265
+
266
+
258
267
  ## Installation from source
259
268
 
260
269
  1. [Linux] Install the snappy compression development package
@@ -273,6 +282,11 @@ include:
273
282
  $ pip install .
274
283
  ```
275
284
 
285
+ To also install the dependencies for leveldb/indexeddb plugins, run
286
+ ```
287
+ $ pip install '.[plugins]'
288
+ ```
289
+
276
290
  ## Usage
277
291
 
278
292
  Two CLI tools for parsing IndexedDB/LevelDB files are available after
@@ -359,7 +373,15 @@ options:
359
373
  To parse records from a LevelDB folder, use the following command:
360
374
 
361
375
  ```
362
- dfindexeddb db -s SOURCE
376
+ dfleveldb db -s SOURCE
377
+ ```
378
+
379
+ To parse records from a LevelDB folder, and use the sequence number to
380
+ determine recovered records and output as JSON, use the
381
+ following command:
382
+
383
+ ```
384
+ dfleveldb db -s SOURCE --use_sequence_number
363
385
  ```
364
386
 
365
387
  To parse blocks / physical records/ write batches / internal key records from a
@@ -383,15 +405,14 @@ following command:
383
405
 
384
406
  ```
385
407
  $ dfleveldb descriptor -s SOURCE [-o {json,jsonl,repr}] [-t {blocks,physical_records,versionedit} | -v]
386
-
387
- options:
388
- -h, --help show this help message and exit
389
- -s SOURCE, --source SOURCE
390
- The source leveldb file
391
- -o {json,jsonl,repr}, --output {json,jsonl,repr}
392
- Output format. Default is json
393
- -t {blocks,physical_records,versionedit}, --structure_type {blocks,physical_records,versionedit}
394
- Parses the specified structure. Default is versionedit.
395
- -v, --version_history
396
- Parses the leveldb version history.
397
408
  ```
409
+
410
+ #### Plugins
411
+
412
+ To apply a plugin parser for a leveldb file/folder, add the
413
+ `--plugin [Plugin Name]` argument. Currently, there is support for the
414
+ following artifacts:
415
+
416
+ | Plugin Name | Artifact Name |
417
+ | -------- | ------- |
418
+ | `ChromeNotificationRecord` | Chrome/Chromium Notifications |
@@ -33,4 +33,9 @@ dfindexeddb/leveldb/descriptor.py
33
33
  dfindexeddb/leveldb/ldb.py
34
34
  dfindexeddb/leveldb/log.py
35
35
  dfindexeddb/leveldb/record.py
36
- dfindexeddb/leveldb/utils.py
36
+ dfindexeddb/leveldb/utils.py
37
+ dfindexeddb/leveldb/plugins/__init__.py
38
+ dfindexeddb/leveldb/plugins/chrome_notifications.py
39
+ dfindexeddb/leveldb/plugins/interface.py
40
+ dfindexeddb/leveldb/plugins/manager.py
41
+ dfindexeddb/leveldb/plugins/notification_database_data_pb2.py
@@ -1,2 +1,6 @@
1
1
  python-snappy==0.6.1
2
2
  zstd==1.5.5.1
3
+
4
+ [plugins]
5
+ protobuf
6
+ dfdatetime
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dfindexeddb"
7
- version = "20240501"
7
+ version = "20240519"
8
8
  requires-python = ">=3.8"
9
9
  description = "dfindexeddb is an experimental Python tool for performing digital forensic analysis of IndexedDB and leveldb files."
10
10
  license = {file = "LICENSE"}
@@ -22,18 +22,22 @@ classifiers = [
22
22
  'Programming Language :: Python',
23
23
  ]
24
24
 
25
+ [project.optional-dependencies]
26
+ plugins = ["protobuf", "dfdatetime"]
27
+
25
28
  [project.scripts]
26
29
  dfindexeddb = "dfindexeddb.indexeddb.cli:App"
27
30
  dfleveldb = "dfindexeddb.leveldb.cli:App"
28
31
 
29
32
  [tool.setuptools]
30
33
  packages = [
31
- "dfindexeddb",
32
- "dfindexeddb.indexeddb",
34
+ "dfindexeddb",
35
+ "dfindexeddb.indexeddb",
33
36
  "dfindexeddb.indexeddb.chromium",
34
37
  "dfindexeddb.indexeddb.firefox",
35
- "dfindexeddb.indexeddb.safari",
38
+ "dfindexeddb.indexeddb.safari",
36
39
  "dfindexeddb.leveldb",
40
+ "dfindexeddb.leveldb.plugins",
37
41
  ]
38
42
 
39
43
  [project.urls]
File without changes
File without changes
File without changes
File without changes