dissect.target 3.8.dev30__py3-none-any.whl → 3.8.dev32__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. dissect/target/plugins/browsers/browser.py +69 -14
  2. dissect/target/plugins/browsers/chrome.py +15 -4
  3. dissect/target/plugins/browsers/chromium.py +77 -7
  4. dissect/target/plugins/browsers/edge.py +15 -4
  5. dissect/target/plugins/browsers/firefox.py +113 -10
  6. dissect/target/plugins/browsers/iexplore.py +90 -15
  7. dissect/target/plugins/os/unix/bsd/_os.py +41 -0
  8. dissect/target/plugins/os/unix/bsd/freebsd/__init__.py +0 -0
  9. dissect/target/plugins/os/unix/bsd/freebsd/_os.py +26 -0
  10. dissect/target/plugins/os/unix/bsd/ios/__init__.py +0 -0
  11. dissect/target/plugins/os/unix/bsd/ios/_os.py +45 -0
  12. dissect/target/plugins/os/unix/bsd/openbsd/__init__.py +0 -0
  13. dissect/target/plugins/os/unix/bsd/openbsd/_os.py +30 -0
  14. dissect/target/plugins/os/unix/bsd/osx/__init__.py +0 -0
  15. dissect/target/plugins/os/unix/bsd/osx/_os.py +65 -0
  16. {dissect.target-3.8.dev30.dist-info → dissect.target-3.8.dev32.dist-info}/METADATA +5 -4
  17. {dissect.target-3.8.dev30.dist-info → dissect.target-3.8.dev32.dist-info}/RECORD +23 -14
  18. {dissect.target-3.8.dev30.dist-info → dissect.target-3.8.dev32.dist-info}/WHEEL +1 -1
  19. {dissect.target-3.8.dev30.data → dissect/target}/data/autocompletion/target_bash_completion.sh +0 -0
  20. {dissect.target-3.8.dev30.dist-info → dissect.target-3.8.dev32.dist-info}/COPYRIGHT +0 -0
  21. {dissect.target-3.8.dev30.dist-info → dissect.target-3.8.dev32.dist-info}/LICENSE +0 -0
  22. {dissect.target-3.8.dev30.dist-info → dissect.target-3.8.dev32.dist-info}/entry_points.txt +0 -0
  23. {dissect.target-3.8.dev30.dist-info → dissect.target-3.8.dev32.dist-info}/top_level.txt +0 -0
@@ -20,11 +20,26 @@ GENERIC_HISTORY_RECORD_FIELDS = [
20
20
  ("uri", "from_url"),
21
21
  ("path", "source"),
22
22
  ]
23
+ GENERIC_DOWNLOAD_RECORD_FIELDS = [
24
+ ("datetime", "ts_start"),
25
+ ("datetime", "ts_end"),
26
+ ("string", "browser"),
27
+ ("varint", "id"),
28
+ ("path", "path"),
29
+ ("uri", "url"),
30
+ ("filesize", "size"),
31
+ ("varint", "state"),
32
+ ("path", "source"),
33
+ ]
23
34
 
24
35
  BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
25
36
  "browser/history", GENERIC_HISTORY_RECORD_FIELDS
26
37
  )
27
38
 
39
+ BrowserDownloadRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
40
+ "browser/download", GENERIC_DOWNLOAD_RECORD_FIELDS
41
+ )
42
+
28
43
 
29
44
  class BrowserPlugin(Plugin):
30
45
  """General browser plugin.
@@ -51,28 +66,42 @@ class BrowserPlugin(Plugin):
51
66
  for entry in self.BROWSERS:
52
67
  try:
53
68
  self._plugins.append(getattr(self.target, entry))
54
- except Exception: # noqa
69
+ except Exception:
55
70
  target.log.exception("Failed to load browser plugin: %s", entry)
56
71
 
57
- def check_compatible(self):
72
+ def check_compatible(self) -> bool:
73
+ """Perform a compatibility check with the target.
74
+ This function checks if any of the supported browser plugins
75
+ can be used. Otherwise it should raise an ``UnsupportedPluginError``.
76
+ Raises:
77
+ UnsupportedPluginError: If the plugin could not be loaded.
78
+ """
58
79
  if not len(self._plugins):
59
80
  raise UnsupportedPluginError("No compatible browser plugins found")
60
81
 
61
- def _func(self, f):
62
- for p in self._plugins:
82
+ def _func(self, func_name: str):
83
+ """Return the supported browser plugin records.
84
+
85
+ Args:
86
+ func_name: Exported function of the browser plugin to find.
87
+
88
+ Yields:
89
+ Record from the browser function.
90
+ """
91
+ for plugin_name in self._plugins:
63
92
  try:
64
- for entry in getattr(p, f)():
93
+ for entry in getattr(plugin_name, func_name)():
65
94
  yield entry
66
95
  except Exception:
67
- self.target.log.exception("Failed to execute browser plugin: %s.%s", p._name, f)
96
+ self.target.log.exception("Failed to execute browser plugin: %s.%s", plugin_name._name, func_name)
68
97
 
69
98
  @export(record=BrowserHistoryRecord)
70
99
  def history(self):
71
- """Return browser history records from all browsers installed.
100
+ """Return browser history records from all browsers installed and supported.
72
101
 
73
102
  Historical browser records for Chrome, Chromium, Edge (Chromium), Firefox, and Internet Explorer are returned.
74
103
 
75
- Yields BrowserHistoryRecords with the following fields:
104
+ Yields BrowserHistoryRecord with the following fields:
76
105
  hostname (string): The target hostname.
77
106
  domain (string): The target domain.
78
107
  ts (datetime): Visit timestamp.
@@ -91,12 +120,38 @@ class BrowserPlugin(Plugin):
91
120
  from_url (uri): URL of the "from" visit.
92
121
  source: (path): The source file of the history record.
93
122
  """
94
- for e in self._func("history"):
95
- yield e
123
+ yield from self._func("history")
96
124
 
125
+ @export(record=BrowserDownloadRecord)
126
+ def downloads(self):
127
+ """Return browser download records from all browsers installed and supported.
97
128
 
98
- def try_idna(s):
129
+ Yields:
130
+ BrowserDownloadRecord with the following fieds:
131
+ hostname (string): The target hostname.
132
+ domain (string): The target domain.
133
+ ts_start (datetime): Download start timestamp.
134
+ ts_end (datetime): Download end timestamp.
135
+ browser (string): The browser from which the records are generated from.
136
+ id (string): Record ID.
137
+ path (string): Download path.
138
+ url (uri): Download URL.
139
+ size (varint): Download file size.
140
+ state (varint): Download state number.
141
+ source: (path): The source file of the download record.
142
+ """
143
+ yield from self._func("downloads")
144
+
145
+
146
+ def try_idna(url: str) -> bytes:
147
+ """Attempts to convert a possible Unicode url to ASCII using the IDNA standard.
148
+
149
+ Args:
150
+ url: A String containing the url to be converted.
151
+
152
+ Returns: Bytes object with the ASCII version of the url.
153
+ """
99
154
  try:
100
- return s.encode("idna")
101
- except Exception: # noqa
102
- return s
155
+ return url.encode("idna")
156
+ except Exception:
157
+ return url
@@ -1,7 +1,10 @@
1
1
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
2
2
  from dissect.target.helpers.record import create_extended_descriptor
3
3
  from dissect.target.plugin import Plugin, export
4
- from dissect.target.plugins.browsers.browser import GENERIC_HISTORY_RECORD_FIELDS
4
+ from dissect.target.plugins.browsers.browser import (
5
+ GENERIC_DOWNLOAD_RECORD_FIELDS,
6
+ GENERIC_HISTORY_RECORD_FIELDS,
7
+ )
5
8
  from dissect.target.plugins.browsers.chromium import ChromiumMixin
6
9
 
7
10
 
@@ -20,11 +23,19 @@ class ChromePlugin(ChromiumMixin, Plugin):
20
23
  # Macos
21
24
  "Library/Application Support/Google/Chrome/Default",
22
25
  ]
23
- HISTORY_RECORD = create_extended_descriptor([UserRecordDescriptorExtension])(
26
+ BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
24
27
  "browser/chrome/history", GENERIC_HISTORY_RECORD_FIELDS
25
28
  )
29
+ BrowserDownloadRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
30
+ "browser/chrome/download", GENERIC_DOWNLOAD_RECORD_FIELDS
31
+ )
26
32
 
27
- @export(record=HISTORY_RECORD)
33
+ @export(record=BrowserHistoryRecord)
28
34
  def history(self):
29
35
  """Return browser history records for Google Chrome."""
30
- yield from ChromiumMixin.history(self, "chrome")
36
+ yield from super().history("chrome")
37
+
38
+ @export(record=BrowserDownloadRecord)
39
+ def downloads(self):
40
+ """Return browser download records for Google Chrome."""
41
+ yield from super().downloads("chrome")
@@ -1,10 +1,11 @@
1
+ from collections import defaultdict
1
2
  from typing import Iterator
2
3
 
3
4
  from dissect.sql import sqlite3
4
5
  from dissect.sql.exceptions import Error as SQLError
5
6
  from dissect.sql.sqlite3 import SQLite3
6
7
  from dissect.util.ts import webkittimestamp
7
- from flow.record import Record
8
+ from flow.record.fieldtypes import path
8
9
 
9
10
  from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
10
11
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
@@ -12,6 +13,7 @@ from dissect.target.helpers.fsutil import TargetPath
12
13
  from dissect.target.helpers.record import create_extended_descriptor
13
14
  from dissect.target.plugin import Plugin, export
14
15
  from dissect.target.plugins.browsers.browser import (
16
+ GENERIC_DOWNLOAD_RECORD_FIELDS,
15
17
  GENERIC_HISTORY_RECORD_FIELDS,
16
18
  try_idna,
17
19
  )
@@ -21,11 +23,14 @@ class ChromiumMixin:
21
23
  """Mixin class with methods for Chromium-based browsers."""
22
24
 
23
25
  DIRS = []
24
- HISTORY_RECORD = create_extended_descriptor([UserRecordDescriptorExtension])(
26
+ BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
25
27
  "browser/chromium/history", GENERIC_HISTORY_RECORD_FIELDS
26
28
  )
29
+ BrowserDownloadRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
30
+ "browser/chromium/download", GENERIC_DOWNLOAD_RECORD_FIELDS
31
+ )
27
32
 
28
- def history(self, browser_name: str = None) -> Iterator[Record]:
33
+ def history(self, browser_name: str = None) -> Iterator[BrowserHistoryRecord]:
29
34
  """Return browser history records from supported Chromium-based browsers.
30
35
 
31
36
  Args:
@@ -55,7 +60,7 @@ class ChromiumMixin:
55
60
  for user, db_file, db in self._iter_db("History"):
56
61
  try:
57
62
  urls = {row.id: row for row in db.table("urls").rows()}
58
- visits: dict = {}
63
+ visits = {}
59
64
 
60
65
  for row in db.table("visits").rows():
61
66
  visits[row.id] = row
@@ -67,7 +72,7 @@ class ChromiumMixin:
67
72
  else:
68
73
  from_visit, from_url = None, None
69
74
 
70
- yield self.HISTORY_RECORD(
75
+ yield self.BrowserHistoryRecord(
71
76
  ts=webkittimestamp(row.visit_time),
72
77
  browser=browser_name,
73
78
  id=row.id,
@@ -89,6 +94,66 @@ class ChromiumMixin:
89
94
  except SQLError as e:
90
95
  self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
91
96
 
97
+ def downloads(self, browser_name: str = None) -> Iterator[BrowserDownloadRecord]:
98
+ """Return browser download records from supported Chromium-based browsers.
99
+
100
+ Args:
101
+ browser_name: The name of the browser as a string.
102
+ Yields:
103
+ Records with the following fields:
104
+ hostname (string): The target hostname.
105
+ domain (string): The target domain.
106
+ ts_start (datetime): Download start timestamp.
107
+ ts_ed (datetime): Download end timestamp.
108
+ browser (string): The browser from which the records are generated from.
109
+ id (string): Record ID.
110
+ path (string): Download path.
111
+ url (uri): Download URL.
112
+ size (varint): Download file size.
113
+ state (varint): Download state number.
114
+ source: (path): The source file of the download record.
115
+ Raises:
116
+ SQLError: If the history file could not be processed.
117
+ """
118
+ for user, db_file, db in self._iter_db("History"):
119
+ try:
120
+ download_chains = defaultdict(list)
121
+ for row in db.table("downloads_url_chains"):
122
+ download_chains[row.id].append(row)
123
+
124
+ for chain in download_chains.values():
125
+ chain.sort(key=lambda row: row.chain_index)
126
+
127
+ for row in db.table("downloads").rows():
128
+ download_path = row.target_path
129
+ if download_path and self.target.os == "windows":
130
+ download_path = path.from_windows(download_path)
131
+ elif download_path:
132
+ download_path = path.from_posix()(download_path)
133
+
134
+ url = None
135
+ download_chain = download_chains.get(row.id)
136
+
137
+ if download_chain:
138
+ url = download_chain[-1].url
139
+ url = try_idna(url)
140
+
141
+ yield self.BrowserDownloadRecord(
142
+ ts_start=webkittimestamp(row.start_time),
143
+ ts_end=webkittimestamp(row.end_time) if row.end_time else None,
144
+ browser=browser_name,
145
+ id=row.get("id"),
146
+ path=download_path,
147
+ url=url,
148
+ size=row.get("total_bytes"),
149
+ state=row.get("state"),
150
+ source=str(db_file),
151
+ _target=self.target,
152
+ _user=user,
153
+ )
154
+ except SQLError as e:
155
+ self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
156
+
92
157
  def _iter_db(self, filename: str) -> Iterator[SQLite3]:
93
158
  """Generate a connection to a sqlite history database files.
94
159
 
@@ -151,7 +216,12 @@ class ChromiumPlugin(ChromiumMixin, Plugin):
151
216
  "AppData/Local/Chromium/User Data/Default",
152
217
  ]
153
218
 
154
- @export(record=ChromiumMixin.HISTORY_RECORD)
219
+ @export(record=ChromiumMixin.BrowserHistoryRecord)
155
220
  def history(self):
156
221
  """Return browser history records for Chromium browser."""
157
- yield from ChromiumMixin.history(self, "chromium")
222
+ yield from super().history("chromium")
223
+
224
+ @export(record=ChromiumMixin.BrowserDownloadRecord)
225
+ def downloads(self):
226
+ """Return browser download records for Chromium browser."""
227
+ yield from super().downloads("chromium")
@@ -1,7 +1,10 @@
1
1
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
2
2
  from dissect.target.helpers.record import create_extended_descriptor
3
3
  from dissect.target.plugin import Plugin, export
4
- from dissect.target.plugins.browsers.browser import GENERIC_HISTORY_RECORD_FIELDS
4
+ from dissect.target.plugins.browsers.browser import (
5
+ GENERIC_DOWNLOAD_RECORD_FIELDS,
6
+ GENERIC_HISTORY_RECORD_FIELDS,
7
+ )
5
8
  from dissect.target.plugins.browsers.chromium import ChromiumMixin
6
9
 
7
10
 
@@ -16,11 +19,19 @@ class EdgePlugin(ChromiumMixin, Plugin):
16
19
  # Macos
17
20
  "Library/Application Support/Microsoft Edge/Default",
18
21
  ]
19
- HISTORY_RECORD = create_extended_descriptor([UserRecordDescriptorExtension])(
22
+ BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
20
23
  "browser/edge/history", GENERIC_HISTORY_RECORD_FIELDS
21
24
  )
25
+ BrowserDownloadRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
26
+ "browser/edge/download", GENERIC_DOWNLOAD_RECORD_FIELDS
27
+ )
22
28
 
23
- @export(record=HISTORY_RECORD)
29
+ @export(record=BrowserHistoryRecord)
24
30
  def history(self):
25
31
  """Return browser history records for Microsoft Edge."""
26
- yield from ChromiumMixin.history(self, "edge")
32
+ yield from super().history("edge")
33
+
34
+ @export(record=BrowserDownloadRecord)
35
+ def downloads(self):
36
+ """Return browser download records for Microsoft Edge."""
37
+ yield from super().downloads("edge")
@@ -1,20 +1,22 @@
1
+ import json
2
+ from typing import Iterator
3
+
1
4
  from dissect.sql import sqlite3
2
5
  from dissect.sql.exceptions import Error as SQLError
3
- from dissect.util.ts import from_unix_us
6
+ from dissect.sql.sqlite3 import SQLite3
7
+ from dissect.util.ts import from_unix_ms, from_unix_us
8
+ from flow.record.fieldtypes import path
4
9
 
5
10
  from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
6
11
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
7
12
  from dissect.target.helpers.record import create_extended_descriptor
8
13
  from dissect.target.plugin import Plugin, export
9
14
  from dissect.target.plugins.browsers.browser import (
15
+ GENERIC_DOWNLOAD_RECORD_FIELDS,
10
16
  GENERIC_HISTORY_RECORD_FIELDS,
11
17
  try_idna,
12
18
  )
13
19
 
14
- FirefoxBrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
15
- "browser/firefox/history", GENERIC_HISTORY_RECORD_FIELDS
16
- )
17
-
18
20
 
19
21
  class FirefoxPlugin(Plugin):
20
22
  """Firefox browser plugin."""
@@ -27,6 +29,12 @@ class FirefoxPlugin(Plugin):
27
29
  ".mozilla/firefox",
28
30
  "snap/firefox/common/.mozilla/firefox",
29
31
  ]
32
+ BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
33
+ "browser/firefox/history", GENERIC_HISTORY_RECORD_FIELDS
34
+ )
35
+ BrowserDownloadRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
36
+ "browser/firefox/download", GENERIC_DOWNLOAD_RECORD_FIELDS
37
+ )
30
38
 
31
39
  def __init__(self, target):
32
40
  super().__init__(target)
@@ -43,7 +51,14 @@ class FirefoxPlugin(Plugin):
43
51
  if not len(self.users_dirs):
44
52
  raise UnsupportedPluginError("No Firefox directories found")
45
53
 
46
- def _iter_db(self, filename):
54
+ def _iter_db(self, filename: str) -> Iterator[SQLite3]:
55
+ """Yield opened history database files of all users.
56
+
57
+ Args:
58
+ filename: The filename of the database.
59
+ Yields:
60
+ Opened SQLite3 databases.
61
+ """
47
62
  for user, cur_dir in self.users_dirs:
48
63
  for profile_dir in cur_dir.iterdir():
49
64
  if profile_dir.is_dir():
@@ -55,11 +70,11 @@ class FirefoxPlugin(Plugin):
55
70
  except SQLError as e:
56
71
  self.target.log.warning("Could not open %s file: %s", filename, db_file, exc_info=e)
57
72
 
58
- @export(record=FirefoxBrowserHistoryRecord)
59
- def history(self):
73
+ @export(record=BrowserHistoryRecord)
74
+ def history(self) -> Iterator[BrowserHistoryRecord]:
60
75
  """Return browser history records from Firefox.
61
76
 
62
- Yields FirefoxBrowserHistoryRecord with the following fields:
77
+ Yields BrowserHistoryRecord with the following fields:
63
78
  hostname (string): The target hostname.
64
79
  domain (string): The target domain.
65
80
  ts (datetime): Visit timestamp.
@@ -93,7 +108,7 @@ class FirefoxPlugin(Plugin):
93
108
  else:
94
109
  from_visit, from_place = None, None
95
110
 
96
- yield FirefoxBrowserHistoryRecord(
111
+ yield self.BrowserHistoryRecord(
97
112
  ts=from_unix_us(row.visit_date),
98
113
  browser="firefox",
99
114
  id=row.id,
@@ -114,3 +129,91 @@ class FirefoxPlugin(Plugin):
114
129
  )
115
130
  except SQLError as e:
116
131
  self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
132
+
133
+ @export(record=BrowserDownloadRecord)
134
+ def downloads(self) -> Iterator[BrowserDownloadRecord]:
135
+ """Return browser download records from Firefox.
136
+
137
+ Yields BrowserDownloadRecord with the following fields:
138
+ hostname (string): The target hostname.
139
+ domain (string): The target domain.
140
+ ts_start (datetime): Download start timestamp.
141
+ ts_end (datetime): Download end timestamp.
142
+ browser (string): The browser from which the records are generated from.
143
+ id (string): Record ID.
144
+ path (string): Download path.
145
+ url (uri): Download URL.
146
+ size (varint): Download file size.
147
+ state (varint): Download state number.
148
+ source: (path): The source file of the download record.
149
+ """
150
+ for user, db_file, db in self._iter_db("places.sqlite"):
151
+ try:
152
+ places = {row.id: row for row in db.table("moz_places").rows()}
153
+ attributes = {row.id: row.name for row in db.table("moz_anno_attributes").rows()}
154
+ annotations = {}
155
+
156
+ for row in db.table("moz_annos"):
157
+ attribute_name = attributes.get(row.anno_attribute_id, row.anno_attribute_id)
158
+
159
+ if attribute_name == "downloads/metaData":
160
+ content = json.loads(row.content)
161
+ else:
162
+ content = row.content
163
+
164
+ if row.place_id not in annotations:
165
+ annotations[row.place_id] = {"id": row.id}
166
+
167
+ annotations[row.place_id][attribute_name] = {
168
+ "content": content,
169
+ "flags": row.flags,
170
+ "expiration": row.expiration,
171
+ "type": row.type,
172
+ "date_added": from_unix_us(row.dateAdded),
173
+ "last_modified": from_unix_us(row.lastModified),
174
+ }
175
+
176
+ for place_id, annotation in annotations.items():
177
+ if "downloads/metaData" not in annotation:
178
+ continue
179
+
180
+ metadata = annotation.get("downloads/metaData", {})
181
+
182
+ ts_end = None
183
+ size = None
184
+ state = None
185
+
186
+ content = metadata.get("content")
187
+ if content:
188
+ ts_end = metadata.get("content").get("endTime")
189
+ ts_end = from_unix_ms(ts_end) if ts_end else None
190
+
191
+ size = content.get("fileSize")
192
+ state = content.get("state")
193
+
194
+ dest_file_info = annotation.get("downloads/destinationFileURI", {})
195
+ download_path = dest_file_info.get("content")
196
+
197
+ if download_path and self.target.os == "windows":
198
+ download_path = path.from_windows(download_path)
199
+ elif download_path:
200
+ download_path = path.from_posix(download_path)
201
+
202
+ place = places.get(place_id)
203
+ url = place.get("url")
204
+ url = try_idna(url) if url else None
205
+
206
+ yield self.BrowserDownloadRecord(
207
+ ts_start=dest_file_info.get("date_added"),
208
+ ts_end=ts_end,
209
+ browser="firefox",
210
+ id=annotation.get("id"),
211
+ path=download_path,
212
+ url=url,
213
+ size=size,
214
+ state=state,
215
+ source=str(db_file),
216
+ _target=self.target,
217
+ )
218
+ except SQLError as e:
219
+ self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
@@ -9,23 +9,30 @@ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExt
9
9
  from dissect.target.helpers.record import create_extended_descriptor
10
10
  from dissect.target.plugin import Plugin, export
11
11
  from dissect.target.plugins.browsers.browser import (
12
+ GENERIC_DOWNLOAD_RECORD_FIELDS,
12
13
  GENERIC_HISTORY_RECORD_FIELDS,
13
14
  try_idna,
14
15
  )
15
16
  from dissect.target.plugins.general.users import UserDetails
16
17
  from dissect.target.target import Target
17
18
 
18
- IEBrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
19
- "browser/ie/history", GENERIC_HISTORY_RECORD_FIELDS
20
- )
21
-
22
19
 
23
20
  class WebCache:
21
+ """Class for opening and pre-processing IE WebCache file."""
22
+
24
23
  def __init__(self, target: Target, fh: BinaryIO):
25
24
  self.target = target
26
25
  self.db = esedb.EseDB(fh)
27
26
 
28
27
  def find_containers(self, name: str) -> table.Table:
28
+ """Look up all ``ContainerId`` values for a given container name.
29
+
30
+ Args:
31
+ name: The container name to look up all container IDs of.
32
+
33
+ Yields:
34
+ All ``ContainerId`` values for the requested container name.
35
+ """
29
36
  try:
30
37
  for container_record in self.db.table("Containers").records():
31
38
  if record_name := container_record.get("Name"):
@@ -37,6 +44,14 @@ class WebCache:
37
44
  pass
38
45
 
39
46
  def _iter_records(self, name: str) -> Iterator[record.Record]:
47
+ """Yield records from a Webcache container.
48
+
49
+ Args:
50
+ name: The container name.
51
+
52
+ Yields:
53
+ Records from specified Webcache container.
54
+ """
40
55
  for container in self.find_containers(name):
41
56
  try:
42
57
  yield from container.records()
@@ -45,8 +60,13 @@ class WebCache:
45
60
  continue
46
61
 
47
62
  def history(self) -> Iterator[record.Record]:
63
+ """Yield records from the history webcache container."""
48
64
  yield from self._iter_records("history")
49
65
 
66
+ def downloads(self) -> Iterator[record.Record]:
67
+ """Yield records from the iedownload webcache container."""
68
+ yield from self._iter_records("iedownload")
69
+
50
70
 
51
71
  class InternetExplorerPlugin(Plugin):
52
72
  """Internet explorer browser plugin."""
@@ -56,8 +76,13 @@ class InternetExplorerPlugin(Plugin):
56
76
  DIRS = [
57
77
  "AppData/Local/Microsoft/Windows/WebCache",
58
78
  ]
59
-
60
79
  CACHE_FILENAME = "WebCacheV01.dat"
80
+ BrowserDownloadRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
81
+ "browser/ie/download", GENERIC_DOWNLOAD_RECORD_FIELDS
82
+ )
83
+ BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
84
+ "browser/ie/history", GENERIC_HISTORY_RECORD_FIELDS
85
+ )
61
86
 
62
87
  def __init__(self, target: Target):
63
88
  super().__init__(target)
@@ -74,11 +99,30 @@ class InternetExplorerPlugin(Plugin):
74
99
  if not len(self.users_dirs):
75
100
  raise UnsupportedPluginError("No Internet Explorer directories found")
76
101
 
77
- @export(record=IEBrowserHistoryRecord)
78
- def history(self) -> Iterator[IEBrowserHistoryRecord]:
102
+ def _iter_cache(self) -> Iterator[WebCache]:
103
+ """Yield open IE Webcache files.
104
+
105
+ Args:
106
+ filename: Name of the Webcache file.
107
+
108
+ Yields:
109
+ Open Webcache file.
110
+
111
+ Raises:
112
+ FileNoteFoundError: If the webcache file could not be found.
113
+ """
114
+ for user, cdir in self.users_dirs:
115
+ cache_file = cdir.joinpath(self.CACHE_FILENAME)
116
+ try:
117
+ yield user, cache_file, WebCache(self.target, cache_file.open())
118
+ except FileNotFoundError:
119
+ self.target.log.warning("Could not find %s file: %s", self.CACHE_FILENAME, cache_file)
120
+
121
+ @export(record=BrowserHistoryRecord)
122
+ def history(self) -> Iterator[BrowserHistoryRecord]:
79
123
  """Return browser history records from Internet Explorer.
80
124
 
81
- Yields IEBrowserHistoryRecord with the following fields:
125
+ Yields BrowserHistoryRecord with the following fields:
82
126
  hostname (string): The target hostname.
83
127
  domain (string): The target domain.
84
128
  ts (datetime): Visit timestamp.
@@ -97,12 +141,7 @@ class InternetExplorerPlugin(Plugin):
97
141
  from_url (uri): URL of the "from" visit.
98
142
  source: (path): The source file of the history record.
99
143
  """
100
- for user, cdir in self.users_dirs:
101
- cache_file = cdir.joinpath(self.CACHE_FILENAME)
102
- if not cache_file.exists():
103
- continue
104
-
105
- cache = WebCache(self.target, cache_file.open())
144
+ for user, cache_file, cache in self._iter_cache():
106
145
  for container_record in cache.history():
107
146
  if not container_record.get("Url"):
108
147
  continue
@@ -113,7 +152,7 @@ class InternetExplorerPlugin(Plugin):
113
152
  if accessed_time := container_record.get("AccessedTime"):
114
153
  ts = wintimestamp(accessed_time)
115
154
 
116
- yield IEBrowserHistoryRecord(
155
+ yield self.BrowserHistoryRecord(
117
156
  ts=ts,
118
157
  browser="iexplore",
119
158
  id=container_record.get("EntryId"),
@@ -132,3 +171,39 @@ class InternetExplorerPlugin(Plugin):
132
171
  _target=self.target,
133
172
  _user=user,
134
173
  )
174
+
175
+ @export(record=BrowserDownloadRecord)
176
+ def downloads(self) -> Iterator[BrowserDownloadRecord]:
177
+ """Return browser downloads records from Internet Explorer.
178
+
179
+ Yields BrowserDownloadRecord with the following fields:
180
+ hostname (string): The target hostname.
181
+ domain (string): The target domain.
182
+ ts_start (datetime): Download start timestamp.
183
+ ts_end (datetime): Download end timestamp.
184
+ browser (string): The browser from which the records are generated from.
185
+ id (string): Record ID.
186
+ path (string): Download path.
187
+ url (uri): Download URL.
188
+ size (varint): Download file size.
189
+ state (varint): Download state number.
190
+ source: (path): The source file of the download record.
191
+ """
192
+ for user, cache_file, cache in self._iter_cache():
193
+ for container_record in cache.downloads():
194
+ response_headers = container_record.ResponseHeaders.decode("utf-16-le", errors="ignore")
195
+ ref_url, mime_type, temp_download_path, down_url, down_path = response_headers.split("\x00")[-6:-1]
196
+
197
+ yield self.BrowserDownloadRecord(
198
+ ts_start=None,
199
+ ts_end=wintimestamp(container_record.AccessedTime),
200
+ browser="iexplore",
201
+ id=container_record.EntryId,
202
+ path=down_path,
203
+ url=down_url,
204
+ size=None,
205
+ state=None,
206
+ source=str(cache_file),
207
+ _target=self.target,
208
+ _user=user,
209
+ )
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional
4
+
5
+ from dissect.target.filesystem import Filesystem
6
+ from dissect.target.plugin import OperatingSystem, export
7
+ from dissect.target.plugins.os.unix._os import UnixPlugin
8
+ from dissect.target.target import Target
9
+
10
+
11
+ class BsdPlugin(UnixPlugin):
12
+ def __init__(self, target: Target):
13
+ super().__init__(target)
14
+
15
+ @classmethod
16
+ def detect(cls, target: Target) -> Optional[Filesystem]:
17
+ for fs in target.filesystems:
18
+ # checking the existence of /var/authpf for free- and openbsd
19
+ # checking the existence of /var/at for net- and freebsd
20
+ if fs.exists("/usr/obj") or fs.exists("/altroot") or fs.exists("/etc/pf.conf"):
21
+ return fs
22
+
23
+ return None
24
+
25
+ @export(property=True)
26
+ def os(self) -> str:
27
+ return OperatingSystem.BSD.value
28
+
29
+ @export(property=True)
30
+ def hostname(self) -> Optional[str]:
31
+ fh = self.target.fs.path("/etc/rc.conf")
32
+
33
+ for line in fh.open("rt").readlines():
34
+ if line.startswith("hostname"):
35
+ hostname = line.rstrip().split("=", maxsplit=1)[1].replace('"', "")
36
+ return hostname
37
+
38
+ @export(property=True)
39
+ def ips(self) -> Optional[List[str]]:
40
+ self.target.log.error(f"ips plugin not implemented for {self.__class__}")
41
+ return None
File without changes
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from dissect.target.filesystem import Filesystem
6
+ from dissect.target.plugin import export
7
+ from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
8
+ from dissect.target.target import Target
9
+
10
+
11
+ class FreeBsdPlugin(BsdPlugin):
12
+ def __init__(self, target: Target):
13
+ super().__init__(target)
14
+ self._os_release = self._parse_os_release("/bin/freebsd-version*")
15
+
16
+ @classmethod
17
+ def detect(cls, target: Target) -> Optional[Filesystem]:
18
+ for fs in target.filesystems:
19
+ if fs.exists("/net") or fs.exists("/.sujournal"):
20
+ return fs
21
+
22
+ return None
23
+
24
+ @export(property=True)
25
+ def version(self) -> Optional[str]:
26
+ return self._os_release.get("USERLAND_VERSION")
File without changes
@@ -0,0 +1,45 @@
1
+ import plistlib
2
+
3
+ from dissect.target.plugin import OperatingSystem, export
4
+ from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
5
+
6
+
7
+ class IOSPlugin(BsdPlugin):
8
+ @classmethod
9
+ def detect(cls, target):
10
+ for fs in target.filesystems:
11
+ if fs.exists("/private/var/preferences"):
12
+ return fs
13
+
14
+ return None
15
+
16
+ @classmethod
17
+ def create(cls, target, sysvol):
18
+ target.fs.mount("/", sysvol)
19
+ return cls(target)
20
+
21
+ @export(property=True)
22
+ def hostname(self):
23
+ path = self.target.fs.path("/private/var/preferences/SystemConfiguration/preferences.plist")
24
+
25
+ if not path.exists():
26
+ return None
27
+
28
+ preferences = plistlib.load(path.open())
29
+ return preferences["System"]["System"]["ComputerName"]
30
+
31
+ @export(property=True)
32
+ def ips(self):
33
+ raise NotImplementedError
34
+
35
+ @export(property=True)
36
+ def version(self):
37
+ raise NotImplementedError
38
+
39
+ @export(property=True)
40
+ def users(self):
41
+ raise NotImplementedError
42
+
43
+ @export(property=True)
44
+ def os(self) -> str:
45
+ return OperatingSystem.IOS.value
File without changes
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from dissect.target.filesystem import Filesystem
6
+ from dissect.target.plugin import export
7
+ from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
8
+ from dissect.target.target import Target
9
+
10
+
11
+ class OpenBsdPlugin(BsdPlugin):
12
+ def __init__(self, target: Target):
13
+ super().__init__(target)
14
+ self._hostname_dict = self._parse_hostname_string(["/etc/myname"])
15
+
16
+ @classmethod
17
+ def detect(cls, target: Target) -> Optional[Filesystem]:
18
+ for fs in target.filesystems:
19
+ if fs.exists("/bsd") or fs.exists("/bsd.rd") or fs.exists("/bsd.mp") or fs.exists("/bsd.mp"):
20
+ return fs
21
+
22
+ return None
23
+
24
+ @export(property=True)
25
+ def version(self) -> Optional[str]:
26
+ return None
27
+
28
+ @export(property=True)
29
+ def hostname(self) -> Optional[str]:
30
+ return self._hostname_dict.get("hostname", None)
File without changes
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import plistlib
4
+ from typing import Iterator, Optional
5
+
6
+ from dissect.target.filesystem import Filesystem
7
+ from dissect.target.helpers.record import UnixUserRecord
8
+ from dissect.target.plugin import OperatingSystem, export
9
+ from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
10
+ from dissect.target.target import Target
11
+
12
+
13
+ class MacPlugin(BsdPlugin):
14
+ VERSION = "/System/Library/CoreServices/SystemVersion.plist"
15
+ GLOBAL = "/Library/Preferences/.GlobalPreferences.plist"
16
+ SYSTEM = "/Library/Preferences/SystemConfiguration/preferences.plist"
17
+
18
+ @classmethod
19
+ def detect(cls, target: Target) -> Optional[Filesystem]:
20
+ for fs in target.filesystems:
21
+ if fs.exists("/Library") and fs.exists("/Applications"):
22
+ return fs
23
+
24
+ return None
25
+
26
+ @classmethod
27
+ def create(cls, target: Target, sysvol: Filesystem) -> MacPlugin:
28
+ target.fs.mount("/", sysvol)
29
+ return cls(target)
30
+
31
+ @export(property=True)
32
+ def hostname(self) -> Optional[str]:
33
+ for path in ["/Library/Preferences/SystemConfiguration/preferences.plist"]:
34
+ try:
35
+ preferencesPlist = self.target.fs.open(path).read().rstrip()
36
+ preferences = plistlib.loads(preferencesPlist)
37
+ return preferences["System"]["System"]["ComputerName"]
38
+
39
+ except FileNotFoundError:
40
+ pass
41
+
42
+ @export(property=True)
43
+ def ips(self) -> Optional[list[str]]:
44
+ raise NotImplementedError
45
+
46
+ @export(property=True)
47
+ def version(self) -> Optional[str]:
48
+ for path in ["/System/Library/CoreServices/SystemVersion.plist"]:
49
+ try:
50
+ systemVersionPlist = self.target.fs.open(path).read().rstrip()
51
+ systemVersion = plistlib.loads(systemVersionPlist)
52
+ productName = systemVersion["ProductName"]
53
+ productUserVisibleVersion = systemVersion["ProductUserVisibleVersion"]
54
+ productBuildVersion = systemVersion["ProductBuildVersion"]
55
+ return f"{productName} {productUserVisibleVersion} ({productBuildVersion})"
56
+ except FileNotFoundError:
57
+ pass
58
+
59
+ @export(record=UnixUserRecord)
60
+ def users(self) -> Iterator[UnixUserRecord]:
61
+ raise NotImplementedError
62
+
63
+ @export(property=True)
64
+ def os(self) -> str:
65
+ return OperatingSystem.OSX.value
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.8.dev30
3
+ Version: 3.8.dev32
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
- Home-page: https://github.com/fox-it/dissect.target
6
- Author: Dissect Team
7
- Author-email: dissect@fox-it.com
5
+ Author-email: Dissect Team <dissect@fox-it.com>
8
6
  License: Affero General Public License v3
7
+ Project-URL: homepage, https://dissect.tools
8
+ Project-URL: documentation, https://docs.dissect.tools/en/latest/projects/dissect.target
9
+ Project-URL: repository, https://github.com/fox-it/dissect.target
9
10
  Classifier: Programming Language :: Python :: 3
10
11
  Requires-Python: ~=3.9
11
12
  Description-Content-Type: text/markdown
@@ -17,6 +17,7 @@ dissect/target/containers/vdi.py,sha256=_kRUu8jlHSHVWUaE6xkwBR8UTtRKOcGMdfW3FP8O
17
17
  dissect/target/containers/vhd.py,sha256=sWhtwG6HRQgerP2PODRSxkJb14khamZj1UWWLa0KEHY,1104
18
18
  dissect/target/containers/vhdx.py,sha256=NYZpGPhEj_P2_yAZe233Uuml_0fqIlHrh-fs6idRPJY,992
19
19
  dissect/target/containers/vmdk.py,sha256=RSoS9jSazrKVCqjyG36ahpPjY7MgJAQ3cgE7lqDtOZs,1032
20
+ dissect/target/data/autocompletion/target_bash_completion.sh,sha256=7iHF2JMXtJJNCtbR2fnIP2BnRUSJxKeKW9zpyX-vJLo,3569
20
21
  dissect/target/filesystems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  dissect/target/filesystems/ad1.py,sha256=YlNWF1wvjrEe2Y4H45CtdgeLdJZ5tUeS_XE1rTWpq7A,2257
22
23
  dissect/target/filesystems/cb.py,sha256=FBjJhI9Q-l-0G8eVA4NhF4IyNkEhTP3wzsD7gLmqBvo,3594
@@ -92,12 +93,12 @@ dissect/target/plugins/apps/webservers/iis.py,sha256=RIS_1w9hcQSfF3-83MsRsT8EJeY
92
93
  dissect/target/plugins/apps/webservers/nginx.py,sha256=P6lkAJwf4U8pyaIHt37DKyGYhJ2a-Mb7I-msZNRuuG8,4006
93
94
  dissect/target/plugins/apps/webservers/webservers.py,sha256=00a0GWWK_9CLuZnXJEyHitHxHaGK-HUrmHOK6jpeiXg,2156
94
95
  dissect/target/plugins/browsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
- dissect/target/plugins/browsers/browser.py,sha256=hCDAjiezYaulNRE9Hmy1B8VgB0zvWAevLkUXdqzBt24,3411
96
- dissect/target/plugins/browsers/chrome.py,sha256=Yq2bV6RNFAp-xkq6ymwFSX5HMr8nyl51WSAa7nalQJQ,1173
97
- dissect/target/plugins/browsers/chromium.py,sha256=P3Z_pW-3sUe6FtRco-gPK9KnpNiuNKakt_X-WIIR3VY,6365
98
- dissect/target/plugins/browsers/edge.py,sha256=znep-McomUvtAFVn6un5RJm7D_iloixAbz0AMuxJksU,959
99
- dissect/target/plugins/browsers/firefox.py,sha256=Qm9LgYlQAIiFGk5c95S5abZG-JXr38NkDZRNGvjzVpQ,4899
100
- dissect/target/plugins/browsers/iexplore.py,sha256=Qj7sVz14hvodlmTxK7165WMhtsVps0xCWMbyZl8Jruk,5144
96
+ dissect/target/plugins/browsers/browser.py,sha256=Bm-MuRkEi-hyaQnB02t2bz3EghRoTPOtkW5jtrw6LWU,5491
97
+ dissect/target/plugins/browsers/chrome.py,sha256=0qy_IvQ6hFxslClpfmlyTH3VOokB6VP8fnukXrbNDU4,1559
98
+ dissect/target/plugins/browsers/chromium.py,sha256=3lVEbJl1jmd3kjioWzrYkfHUNzdpzX9KZ7nr1BNJLrg,9570
99
+ dissect/target/plugins/browsers/edge.py,sha256=uVJqHN5CD0oLuJ8D4PIfIao4iui4HEekbs5el8CDwx8,1342
100
+ dissect/target/plugins/browsers/firefox.py,sha256=BPC8M4OWkw9-bWz1LYEdVS78XfQQugi1OoA8WiL6r84,9347
101
+ dissect/target/plugins/browsers/iexplore.py,sha256=pzboThhidWubxiR5d82wheINT1bMfa-66jxEbDlw66g,8251
101
102
  dissect/target/plugins/child/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
103
  dissect/target/plugins/child/esxi.py,sha256=RzfrnFWPYL-y67zU-vRJzzTGYT1MBNWrFh4so0L7dR0,554
103
104
  dissect/target/plugins/child/hyperv.py,sha256=lmhLQD1JSn-EpGKc7RNUAzESmfosz7RwGCm2JHWK-pU,2185
@@ -138,6 +139,15 @@ dissect/target/plugins/os/unix/packagemanager.py,sha256=-mxNhDjvj497-wBvu3z22316
138
139
  dissect/target/plugins/os/unix/services.py,sha256=kL50akbOTb0sjmrTNZx3PmEhXe_jodrIgsQ39NhOalc,2887
139
140
  dissect/target/plugins/os/unix/shadow.py,sha256=7ztW_fYLihxNjS2opFToF-xKZngYDGcTEbZKnodRkc8,3409
140
141
  dissect/target/plugins/os/unix/ssh.py,sha256=HUcl73hF8d43s7hpyvnewErWdz4jeO8tmD13x7fgSvw,8207
142
+ dissect/target/plugins/os/unix/bsd/_os.py,sha256=e5rttTOFOmd7e2HqP9ZZFMEiPLBr-8rfH0XH1IIeroQ,1372
143
+ dissect/target/plugins/os/unix/bsd/freebsd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
144
+ dissect/target/plugins/os/unix/bsd/freebsd/_os.py,sha256=Vqiyn08kv1IioNUwpgtBJ9SToCFhLCsJdpVhl5E7COM,789
145
+ dissect/target/plugins/os/unix/bsd/ios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
+ dissect/target/plugins/os/unix/bsd/ios/_os.py,sha256=Sw34LFMtIRkA9YHc0CSgGmecIJTz34vyPoZD9kJDqqA,1134
147
+ dissect/target/plugins/os/unix/bsd/openbsd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
+ dissect/target/plugins/os/unix/bsd/openbsd/_os.py,sha256=9npz-osM-wHmjOACUqof5N5HJeps7J8KuyenUS5MZDs,923
149
+ dissect/target/plugins/os/unix/bsd/osx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
+ dissect/target/plugins/os/unix/bsd/osx/_os.py,sha256=W19r0vSb10jwQsiduibF_pJx6VjPw-OIm2Xy0ok31Lg,2407
141
151
  dissect/target/plugins/os/unix/linux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
152
  dissect/target/plugins/os/unix/linux/_os.py,sha256=BFq1PB1QA9YU8lMZiAv5tZJhmRqjzW0ibXGxl0llY5M,2467
143
153
  dissect/target/plugins/os/unix/linux/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -241,11 +251,10 @@ dissect/target/volumes/bde.py,sha256=gYGg5yF9MNARwNzEkrEfZmKkxyZW4rhLkpdnPJCbhGk
241
251
  dissect/target/volumes/disk.py,sha256=95grSsPt1BLVpKwTclwQYzPFGKTkFFqapIk0RoGWf38,968
242
252
  dissect/target/volumes/lvm.py,sha256=zXAfszxNR6tOGrKAtAa_E-JhjI-sXQyR4VYLXD-kqCw,1616
243
253
  dissect/target/volumes/vmfs.py,sha256=mlAJ8278tYaoRjk1u6tFFlCaDQUrVu5ZZE4ikiFvxi8,1707
244
- dissect.target-3.8.dev30.data/data/autocompletion/target_bash_completion.sh,sha256=7iHF2JMXtJJNCtbR2fnIP2BnRUSJxKeKW9zpyX-vJLo,3569
245
- dissect.target-3.8.dev30.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
246
- dissect.target-3.8.dev30.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
247
- dissect.target-3.8.dev30.dist-info/METADATA,sha256=1W8srhOjCCcGfuerHQxB9xm0l4FzD2Zls6tmB2IA1n0,9610
248
- dissect.target-3.8.dev30.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
249
- dissect.target-3.8.dev30.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
250
- dissect.target-3.8.dev30.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
251
- dissect.target-3.8.dev30.dist-info/RECORD,,
254
+ dissect.target-3.8.dev32.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
255
+ dissect.target-3.8.dev32.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
256
+ dissect.target-3.8.dev32.dist-info/METADATA,sha256=Xxk_a-9Tb-0_dvcelF-C5KTFviYGegIYrD76c9l0JVA,9752
257
+ dissect.target-3.8.dev32.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
258
+ dissect.target-3.8.dev32.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
259
+ dissect.target-3.8.dev32.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
260
+ dissect.target-3.8.dev32.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: bdist_wheel (0.40.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5