linear-mcp-fast 0.1.0__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.
Files changed (39) hide show
  1. ccl_chromium_reader/__init__.py +2 -0
  2. ccl_chromium_reader/ccl_chromium_cache.py +1335 -0
  3. ccl_chromium_reader/ccl_chromium_filesystem.py +302 -0
  4. ccl_chromium_reader/ccl_chromium_history.py +357 -0
  5. ccl_chromium_reader/ccl_chromium_indexeddb.py +1060 -0
  6. ccl_chromium_reader/ccl_chromium_localstorage.py +454 -0
  7. ccl_chromium_reader/ccl_chromium_notifications.py +268 -0
  8. ccl_chromium_reader/ccl_chromium_profile_folder.py +568 -0
  9. ccl_chromium_reader/ccl_chromium_sessionstorage.py +368 -0
  10. ccl_chromium_reader/ccl_chromium_snss2.py +332 -0
  11. ccl_chromium_reader/ccl_shared_proto_db_downloads.py +189 -0
  12. ccl_chromium_reader/common.py +19 -0
  13. ccl_chromium_reader/download_common.py +78 -0
  14. ccl_chromium_reader/profile_folder_protocols.py +276 -0
  15. ccl_chromium_reader/serialization_formats/__init__.py +0 -0
  16. ccl_chromium_reader/serialization_formats/ccl_blink_value_deserializer.py +401 -0
  17. ccl_chromium_reader/serialization_formats/ccl_easy_chromium_pickle.py +133 -0
  18. ccl_chromium_reader/serialization_formats/ccl_protobuff.py +276 -0
  19. ccl_chromium_reader/serialization_formats/ccl_v8_value_deserializer.py +627 -0
  20. ccl_chromium_reader/storage_formats/__init__.py +0 -0
  21. ccl_chromium_reader/storage_formats/ccl_leveldb.py +582 -0
  22. ccl_simplesnappy/__init__.py +1 -0
  23. ccl_simplesnappy/ccl_simplesnappy.py +306 -0
  24. linear_mcp_fast/__init__.py +8 -0
  25. linear_mcp_fast/__main__.py +6 -0
  26. linear_mcp_fast/reader.py +433 -0
  27. linear_mcp_fast/server.py +367 -0
  28. linear_mcp_fast/store_detector.py +117 -0
  29. linear_mcp_fast-0.1.0.dist-info/METADATA +160 -0
  30. linear_mcp_fast-0.1.0.dist-info/RECORD +39 -0
  31. linear_mcp_fast-0.1.0.dist-info/WHEEL +5 -0
  32. linear_mcp_fast-0.1.0.dist-info/entry_points.txt +2 -0
  33. linear_mcp_fast-0.1.0.dist-info/top_level.txt +4 -0
  34. tools_and_utilities/Chromium_dump_local_storage.py +111 -0
  35. tools_and_utilities/Chromium_dump_session_storage.py +92 -0
  36. tools_and_utilities/benchmark.py +35 -0
  37. tools_and_utilities/ccl_chrome_audit.py +651 -0
  38. tools_and_utilities/dump_indexeddb_details.py +59 -0
  39. tools_and_utilities/dump_leveldb.py +53 -0
@@ -0,0 +1,454 @@
1
+ """
2
+ Copyright 2021-2024, CCL Forensics
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+ """
21
+
22
+ import io
23
+ import bisect
24
+ import re
25
+ import sys
26
+ import pathlib
27
+ import types
28
+ import typing
29
+ import collections.abc as col_abc
30
+ import dataclasses
31
+ import datetime
32
+
33
+ from .storage_formats import ccl_leveldb
34
+ from .common import KeySearch
35
+
36
+ __version__ = "0.5"
37
+ __description__ = "Module for reading the Chromium leveldb localstorage format"
38
+ __contact__ = "Alex Caithness"
39
+
40
+ """
41
+ See: https://source.chromium.org/chromium/chromium/src/+/main:components/services/storage/dom_storage/local_storage_impl.cc
42
+ Meta keys:
43
+ Key = "META:" + storage_key (the host)
44
+ Value = protobuff: 1=timestamp (varint); 2=size in bytes (varint)
45
+
46
+ Record keys:
47
+ Key = "_" + storage_key + "\\x0" + script_key
48
+ Value = record_value
49
+
50
+ """
51
+
52
+ _META_PREFIX = b"META:"
53
+ _RECORD_KEY_PREFIX = b"_"
54
+ _CHROME_EPOCH = datetime.datetime(1601, 1, 1, 0, 0, 0)
55
+
56
+ EIGHT_BIT_ENCODING = "iso-8859-1"
57
+
58
+
59
+ def from_chrome_timestamp(microseconds: int) -> datetime.datetime:
60
+ return _CHROME_EPOCH + datetime.timedelta(microseconds=microseconds)
61
+
62
+
63
+ def decode_string(raw: bytes) -> str:
64
+ """
65
+ decodes a type-prefixed string - prefix of: 0=utf-16-le; 1=an extended ascii codepage (likely dependant on locale)
66
+ :param raw: raw prefixed-string data
67
+ :return: decoded string
68
+ """
69
+ prefix = raw[0]
70
+ if prefix == 0:
71
+ return raw[1:].decode("utf-16-le")
72
+ elif prefix == 1:
73
+ return raw[1:].decode(EIGHT_BIT_ENCODING)
74
+ else:
75
+ raise ValueError("Unexpected prefix, please contact developer")
76
+
77
+
78
+ @dataclasses.dataclass(frozen=True)
79
+ class StorageMetadata:
80
+ storage_key: str
81
+ timestamp: datetime.datetime
82
+ size_in_bytes: int
83
+ leveldb_seq_number: int
84
+
85
+ @classmethod
86
+ def from_protobuff(cls, storage_key: str, data: bytes, seq: int):
87
+ with io.BytesIO(data) as stream:
88
+ # This is a simple protobuff, so we'll read it directly, but with checks, rather than add a dependency
89
+ ts_tag = ccl_leveldb.read_le_varint(stream)
90
+ if (ts_tag & 0x07) != 0 or (ts_tag >> 3) != 1:
91
+ raise ValueError("Unexpected tag when reading StorageMetadata from protobuff")
92
+ timestamp = from_chrome_timestamp(ccl_leveldb.read_le_varint(stream))
93
+
94
+ size_tag = ccl_leveldb.read_le_varint(stream)
95
+ if (size_tag & 0x07) != 0 or (size_tag >> 3) != 2:
96
+ raise ValueError("Unexpected tag when reading StorageMetadata from protobuff")
97
+ size = ccl_leveldb.read_le_varint(stream)
98
+
99
+ return cls(storage_key, timestamp, size, seq)
100
+
101
+
102
+ @dataclasses.dataclass(frozen=True)
103
+ class LocalStorageRecord:
104
+ storage_key: str
105
+ script_key: str
106
+ value: str
107
+ leveldb_seq_number: int
108
+ is_live: bool
109
+
110
+ @property
111
+ def record_location(self) -> str:
112
+ return f"Leveldb Seq: {self.leveldb_seq_number}"
113
+
114
+
115
+ class LocalStorageBatch:
116
+ def __init__(self, meta: StorageMetadata, end_seq: int):
117
+ self._meta = meta
118
+ self._end = end_seq
119
+
120
+ @property
121
+ def storage_key(self) -> str:
122
+ return self._meta.storage_key
123
+
124
+ @property
125
+ def timestamp(self) -> datetime.datetime:
126
+ return self._meta.timestamp
127
+
128
+ @property
129
+ def start(self):
130
+ return self._meta.leveldb_seq_number
131
+
132
+ @property
133
+ def end(self):
134
+ return self._end
135
+
136
+ def __repr__(self):
137
+ return f"(storage_key={self.storage_key}, timestamp={self.timestamp}, start={self.start}, end={self.end})"
138
+
139
+
140
+ class LocalStoreDb:
141
+ def __init__(self, in_dir: pathlib.Path):
142
+ if not in_dir.is_dir():
143
+ raise IOError("Input directory is not a directory")
144
+
145
+ self._ldb = ccl_leveldb.RawLevelDb(in_dir)
146
+
147
+ self._storage_details = {} # storage_key: {seq_number: StorageMetadata}
148
+ self._flat_items = [] # [StorageMetadata|LocalStorageRecord] - used to batch items up
149
+ self._records = {} # storage_key: {script_key: {seq_number: LocalStorageRecord}}
150
+
151
+ for record in self._ldb.iterate_records_raw():
152
+ if record.user_key.startswith(_META_PREFIX) and record.state == ccl_leveldb.KeyState.Live:
153
+ # Only live records for metadata - not sure what we can reliably infer from deleted keys
154
+ storage_key = record.user_key.removeprefix(_META_PREFIX).decode(EIGHT_BIT_ENCODING)
155
+ self._storage_details.setdefault(storage_key, {})
156
+ metadata = StorageMetadata.from_protobuff(storage_key, record.value, record.seq)
157
+ self._storage_details[storage_key][record.seq] = metadata
158
+ self._flat_items.append(metadata)
159
+ elif record.user_key.startswith(_RECORD_KEY_PREFIX):
160
+ # We include deleted records here because we need them to build batches
161
+ storage_key_raw, script_key_raw = record.user_key.removeprefix(_RECORD_KEY_PREFIX).split(b"\x00", 1)
162
+ storage_key = storage_key_raw.decode(EIGHT_BIT_ENCODING)
163
+ script_key = decode_string(script_key_raw)
164
+
165
+ try:
166
+ value = decode_string(record.value) if record.state == ccl_leveldb.KeyState.Live else None
167
+ except UnicodeDecodeError as e:
168
+ # Some sites play games to test the browser's capabilities like encoding half of a surrogate pair
169
+ print(f"Error decoding record value at seq no {record.seq}; "
170
+ f"{storage_key} {script_key}: {record.value}")
171
+ continue
172
+
173
+ self._records.setdefault(storage_key, {})
174
+ self._records[storage_key].setdefault(script_key, {})
175
+
176
+ ls_record = LocalStorageRecord(
177
+ storage_key, script_key, value, record.seq, record.state == ccl_leveldb.KeyState.Live)
178
+ self._records[storage_key][script_key][record.seq] = ls_record
179
+ self._flat_items.append(ls_record)
180
+
181
+ self._storage_details = types.MappingProxyType(self._storage_details)
182
+ self._records = types.MappingProxyType(self._records)
183
+
184
+ self._all_storage_keys = frozenset(self._storage_details.keys() | self._records.keys()) # because deleted data.
185
+ self._flat_items.sort(key=lambda x: x.leveldb_seq_number)
186
+
187
+ # organise batches - this is made complex and slow by having to account for missing/deleted data
188
+ # we're looking for a StorageMetadata followed by sequential (in terms of seq number) LocalStorageRecords
189
+ # with the same storage key. Everything that falls within that chain can safely be considered a batch.
190
+ # Any break in sequence numbers or storage key is a fail and can't be considered part of a batch.
191
+ self._batches = {}
192
+ current_meta: typing.Optional[StorageMetadata] = None
193
+ current_end = 0
194
+ for item in self._flat_items: # pre-sorted
195
+ if isinstance(item, LocalStorageRecord):
196
+ if current_meta is None:
197
+ # no currently valid metadata so we can't attribute this record to anything
198
+ continue
199
+ elif item.leveldb_seq_number - current_end != 1 or item.storage_key != current_meta.storage_key:
200
+ # this record breaks a chain, so bundle up what we have and clear everything out
201
+ self._batches[current_meta.leveldb_seq_number] = LocalStorageBatch(current_meta, current_end)
202
+ current_meta = None
203
+ current_end = 0
204
+ else:
205
+ # contiguous and right storage key, include in the current chain
206
+ current_end = item.leveldb_seq_number
207
+ elif isinstance(item, StorageMetadata):
208
+ if current_meta is not None:
209
+ # this record breaks a chain, so bundle up what we have, set new start
210
+ self._batches[current_meta.leveldb_seq_number] = LocalStorageBatch(current_meta, current_end)
211
+ current_meta = item
212
+ current_end = item.leveldb_seq_number
213
+ else:
214
+ raise ValueError
215
+
216
+ if current_meta is not None:
217
+ self._batches[current_meta.leveldb_seq_number] = LocalStorageBatch(current_meta, current_end)
218
+
219
+ self._batch_starts = tuple(sorted(self._batches.keys()))
220
+
221
+ def iter_storage_keys(self) -> col_abc.Iterable[str]:
222
+ yield from self._storage_details.keys()
223
+
224
+ def contains_storage_key(self, storage_key: str) -> bool:
225
+ return storage_key in self._all_storage_keys
226
+
227
+ def iter_script_keys(self, storage_key: str) -> col_abc.Iterable[str]:
228
+ if storage_key not in self._all_storage_keys:
229
+ raise KeyError(storage_key)
230
+ if storage_key not in self._records:
231
+ raise StopIteration
232
+ yield from self._records[storage_key].keys()
233
+
234
+ def contains_script_key(self, storage_key: str, script_key: str) -> bool:
235
+ return script_key in self._records.get(storage_key, {})
236
+
237
+ def find_batch(self, seq: int) -> typing.Optional[LocalStorageBatch]:
238
+ """
239
+ Finds the batch that a record with the given sequence number belongs to
240
+ :param seq: leveldb sequence id
241
+ :return: the batch containing the given sequence number or None if no batch contains it
242
+ """
243
+
244
+ i = bisect.bisect_left(self._batch_starts, seq) - 1
245
+ if i < 0:
246
+ return None
247
+ start = self._batch_starts[i]
248
+ batch = self._batches[start]
249
+ if batch.start <= seq <= batch.end:
250
+ return batch
251
+ else:
252
+ return None
253
+
254
+ def iter_all_records(self, include_deletions=False) -> col_abc.Iterable[LocalStorageRecord]:
255
+ """
256
+ :param include_deletions: if True, records related to deletions will be included
257
+ (these will have None as values).
258
+ :return: iterable of LocalStorageRecords
259
+ """
260
+ for storage_key, script_dict in self._records.items():
261
+ for script_key, values in script_dict.items():
262
+ for seq, value in values.items():
263
+ if value.is_live or include_deletions:
264
+ yield value
265
+
266
+ def _iter_records_for_storage_key(
267
+ self, storage_key: str, include_deletions=False) -> col_abc.Iterable[LocalStorageRecord]:
268
+ """
269
+ :param storage_key: storage key (host) for the records
270
+ :param include_deletions: if True, records related to deletions will be included
271
+ (these will have None as values).
272
+ :return: iterable of LocalStorageRecords
273
+ """
274
+ if not self.contains_storage_key(storage_key):
275
+ raise KeyError(storage_key)
276
+ for script_key, values in self._records[storage_key].items():
277
+ for seq, value in values.items():
278
+ if value.is_live or include_deletions:
279
+ yield value
280
+
281
+ def _search_storage_keys(self, storage_key: KeySearch) -> list[str]:
282
+ if isinstance(storage_key, str):
283
+ return [storage_key]
284
+ elif isinstance(storage_key, re.Pattern):
285
+ return [x for x in self._all_storage_keys if storage_key.search(x)]
286
+ elif isinstance(storage_key, col_abc.Collection):
287
+ return list(set(storage_key) & self._all_storage_keys)
288
+ elif isinstance(storage_key, col_abc.Callable):
289
+ return [x for x in self._all_storage_keys if storage_key(x)]
290
+ else:
291
+ raise TypeError(f"Unexpected type: {type(storage_key)} (expects: {KeySearch})")
292
+
293
+ def iter_records_for_storage_key(
294
+ self, storage_key: KeySearch, *,
295
+ include_deletions=False, raise_on_no_result=True) -> col_abc.Iterable[LocalStorageRecord]:
296
+ """
297
+ :param storage_key: storage key (host) for the records. This can be one of: a single string;
298
+ a collection of strings; a regex pattern; a function that takes a string (the host) and returns a bool.
299
+ :param include_deletions: if True, records related to deletions will be included
300
+ :param raise_on_no_result: if True (the default) if no matching storage keys are found, raise a KeyError
301
+ (these will have None as values).
302
+ :return: iterable of LocalStorageRecords
303
+ """
304
+ if isinstance(storage_key, str):
305
+ if raise_on_no_result and not self.contains_storage_key(storage_key):
306
+ raise KeyError(storage_key)
307
+ yield from self._iter_records_for_storage_key(storage_key, include_deletions)
308
+ elif isinstance(storage_key, re.Pattern):
309
+ matched_keys = self._search_storage_keys(storage_key)
310
+ if raise_on_no_result and not matched_keys:
311
+ raise KeyError(f"Pattern: {storage_key.pattern}")
312
+ for key in matched_keys:
313
+ yield from self._iter_records_for_storage_key(key, include_deletions)
314
+ elif isinstance(storage_key, col_abc.Collection):
315
+ matched_keys = self._search_storage_keys(storage_key)
316
+ if raise_on_no_result and not matched_keys:
317
+ raise KeyError(storage_key)
318
+ for key in matched_keys:
319
+ yield from self._iter_records_for_storage_key(key, include_deletions)
320
+ elif isinstance(storage_key, col_abc.Callable):
321
+ matched_keys = self._search_storage_keys(storage_key)
322
+ if raise_on_no_result and not matched_keys:
323
+ raise KeyError(storage_key)
324
+ for key in matched_keys:
325
+ yield from self._iter_records_for_storage_key(key, include_deletions)
326
+ else:
327
+ raise TypeError(f"Unexpected type for storage key: {type(storage_key)} (expects: {KeySearch})")
328
+
329
+ def _iter_records_for_script_key(
330
+ self, storage_key: str, script_key: str, include_deletions=False) -> col_abc.Iterable[LocalStorageRecord]:
331
+ """
332
+ :param storage_key: storage key (host) for the records
333
+ :param script_key: script defined key for the records
334
+ :param include_deletions: if True, records related to deletions will be included
335
+ :return: iterable of LocalStorageRecords
336
+ """
337
+ if not self.contains_script_key(storage_key, script_key):
338
+ raise KeyError((storage_key, script_key))
339
+ for seq, value in self._records[storage_key][script_key].items():
340
+ if value.is_live or include_deletions:
341
+ yield value
342
+
343
+ def iter_records_for_script_key(
344
+ self, storage_key: KeySearch, script_key: KeySearch, *,
345
+ include_deletions=False, raise_on_no_result=True) -> col_abc.Iterable[LocalStorageRecord]:
346
+ """
347
+ :param storage_key: storage key (host) for the records. This can be one of: a single string;
348
+ a collection of strings; a regex pattern; a function that takes a string and returns a bool.
349
+ :param script_key: script defined key for the records. This can be one of: a single string;
350
+ a collection of strings; a regex pattern; a function that takes a string and returns a bool.
351
+ :param include_deletions: if True, records related to deletions will be included
352
+ :param raise_on_no_result: if True (the default) if no matching storage keys are found, raise a KeyError
353
+ (these will have None as values).
354
+ :return: iterable of LocalStorageRecords
355
+ """
356
+
357
+ if isinstance(storage_key, str) and isinstance(script_key, str):
358
+ if raise_on_no_result and not self.contains_script_key(storage_key, script_key):
359
+ raise KeyError((storage_key, script_key))
360
+ yield from self._iter_records_for_script_key(storage_key, script_key, include_deletions=include_deletions)
361
+ else:
362
+ matched_storage_keys = self._search_storage_keys(storage_key)
363
+ if raise_on_no_result and not matched_storage_keys:
364
+ raise KeyError((storage_key, script_key))
365
+
366
+ yielded = False
367
+ for matched_storage_key in matched_storage_keys:
368
+ if isinstance(script_key, str):
369
+ matched_script_keys = [script_key]
370
+ elif isinstance(script_key, re.Pattern):
371
+ matched_script_keys = [x for x in self._records[matched_storage_key].keys() if script_key.search(x)]
372
+ elif isinstance(script_key, col_abc.Collection):
373
+ script_key_set = set(script_key)
374
+ matched_script_keys = list(self._records[matched_storage_key].keys() & script_key_set)
375
+ elif isinstance(script_key, col_abc.Callable):
376
+ matched_script_keys = [x for x in self._records[matched_storage_key].keys() if script_key(x)]
377
+ else:
378
+ raise TypeError(f"Unexpected type for script key: {type(script_key)} (expects: {KeySearch})")
379
+
380
+ for key in matched_script_keys:
381
+ for seq, value in self._records[matched_storage_key][key].items():
382
+ if value.is_live or include_deletions:
383
+ yielded = True
384
+ yield value
385
+
386
+ if not yielded:
387
+ raise KeyError((storage_key, script_key))
388
+
389
+ def iter_metadata(self) -> col_abc.Iterable[StorageMetadata]:
390
+ """
391
+ :return: iterable of StorageMetaData
392
+ """
393
+ for meta in self._flat_items:
394
+ if isinstance(meta, StorageMetadata):
395
+ yield meta
396
+
397
+ def iter_metadata_for_storage_key(self, storage_key: str) -> col_abc.Iterable[StorageMetadata]:
398
+ """
399
+ :param storage_key: storage key (host) for the metadata
400
+ :return: iterable of StorageMetadata
401
+ """
402
+ if storage_key not in self._all_storage_keys:
403
+ raise KeyError(storage_key)
404
+ if storage_key not in self._storage_details:
405
+ return None
406
+ for seq, meta in self._storage_details[storage_key].items():
407
+ yield meta
408
+
409
+ def iter_batches(self) -> col_abc.Iterable[LocalStorageBatch]:
410
+ yield from self._batches.values()
411
+
412
+ def close(self):
413
+ self._ldb.close()
414
+
415
+ def __contains__(self, item: typing.Union[str, tuple[str, str]]) -> bool:
416
+ """
417
+ :param item: either the host as a str or a tuple of the host and a key (both str)
418
+ :return: if item is a str, returns true if that host is present, if item is a tuple of (str, str), returns True
419
+ if that host and key pair are present
420
+ """
421
+
422
+ if isinstance(item, str):
423
+ return item in self._all_storage_keys
424
+ elif isinstance(item, tuple) and len(item) == 2:
425
+ host, key = item
426
+ return host in self._all_storage_keys and key in self._records[host]
427
+ else:
428
+ raise TypeError("item must be a string or a tuple of (str, str)")
429
+
430
+ def __iter__(self):
431
+ """
432
+ iterates the hosts (storage keys) present
433
+ """
434
+ yield from self._all_storage_keys
435
+
436
+ def __enter__(self) -> "LocalStoreDb":
437
+ return self
438
+
439
+ def __exit__(self, exc_type, exc_val, exc_tb):
440
+ self.close()
441
+
442
+
443
+ def main(args):
444
+ in_ldb_path = pathlib.Path(args[0])
445
+ local_store = LocalStoreDb(in_ldb_path)
446
+
447
+ for rec in local_store.iter_all_records():
448
+ batch = local_store.find_batch(rec.leveldb_seq_number)
449
+ print(rec, batch)
450
+
451
+
452
+ if __name__ == '__main__':
453
+ main(sys.argv[1:])
454
+