Skip to content

Commit 645a3cc

Browse files
committed
Add supported targets page with an extension
The contents get generated from the files inside of defaults_/*.csv
1 parent dbb2cab commit 645a3cc

File tree

5 files changed

+366
-0
lines changed

5 files changed

+366
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ _build/
33
__pycache__
44

55
docs/source/api/*/
6+
# The csv files inside this folder get automatically generated
7+
docs/source/supported_targets/
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
"""
2+
This extension generates csv files for our supported targets.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import csv
8+
import importlib
9+
import inspect
10+
from dataclasses import dataclass, field
11+
from functools import partial
12+
from pathlib import Path
13+
from typing import TYPE_CHECKING, Any, Callable, TextIO
14+
from unittest.mock import Mock, patch
15+
16+
from dissect.target.container import Container
17+
from dissect.target.filesystem import Filesystem
18+
from dissect.target.loader import LOADERS_BY_SCHEME, Loader
19+
from dissect.target.plugin import PLUGINS, ChildTargetPlugin, OSPlugin
20+
from dissect.target.volume import EncryptedVolumeSystem, LogicalVolumeSystem, VolumeSystem
21+
from sphinx.util.logging import getLogger
22+
from typing_extensions import Self
23+
24+
if TYPE_CHECKING:
25+
from collections.abc import Iterator
26+
from types import ModuleType
27+
28+
from sphinx.application import Sphinx
29+
30+
LOGGER = getLogger(__name__)
31+
ORIG_IMPORT = __import__
32+
33+
34+
@dataclass(eq=True, frozen=True)
35+
class CSVItemBase:
36+
module: str
37+
type_: str
38+
name: str
39+
description: str
40+
41+
@classmethod
42+
def from_class(cls, klass: type, spec: LookupSpec, info: dict[str, str], **kwargs) -> Self:
43+
_type = getattr(klass, "__type__", klass.__name__.removesuffix(spec.remove_suffix).lower())
44+
description = info.get(_type, "")
45+
46+
if not description:
47+
LOGGER.warning(
48+
("There was no description defined for %s. Please add a description for %r inside '_defaults/%s.csv'"),
49+
klass.__name__,
50+
_type,
51+
spec.name,
52+
)
53+
return cls(
54+
module=klass.__module__,
55+
type_=_type,
56+
name=klass.__name__,
57+
description=description,
58+
**kwargs,
59+
)
60+
61+
@property
62+
def sphinx_class_string(self) -> str:
63+
return f":class:`~{self.module}.{self.name}`"
64+
65+
def as_dict(self) -> dict[str, str]:
66+
return {
67+
"Class": self.sphinx_class_string,
68+
"Description": self.description,
69+
}
70+
71+
72+
class CSVItem(CSVItemBase):
73+
def as_dict(self) -> dict[str, str]:
74+
return {
75+
"Class": self.sphinx_class_string,
76+
"Type": f"``{self.type_}``",
77+
"Description": self.description,
78+
}
79+
80+
81+
@dataclass(eq=True, frozen=True)
82+
class LoaderCSVItem(CSVItemBase):
83+
shorthand: str = ""
84+
85+
def as_dict(self) -> dict[str, str]:
86+
shorthand = self.shorthand
87+
if self.type_ == "direct":
88+
shorthand = "--direct"
89+
90+
return {
91+
"Class": self.sphinx_class_string,
92+
"CMD Option": f"``{shorthand}``" if shorthand else "",
93+
"Description": self.description,
94+
}
95+
96+
97+
def parse_descriptions(csv_file: Path) -> dict[str, str]:
98+
target_dict: dict[str, str] = {}
99+
try:
100+
with csv_file.open("rt") as defaults:
101+
file = csv.DictReader(defaults)
102+
for line in file:
103+
target_dict.update({line["name"]: line["description"]})
104+
except FileNotFoundError:
105+
LOGGER.warning("missing defaults file at '_defaults/%s'", csv_file.name)
106+
return {}
107+
108+
return target_dict
109+
110+
111+
def _create_loader_items(spec: LookupSpec, info: dict[str, str]) -> set[CSVItemBase]:
112+
loader_items: set[LoaderCSVItem] = set()
113+
114+
with patch("builtins.__import__", side_effect=mocked_import):
115+
loader_items.update(
116+
LoaderCSVItem.from_class(klass.realattr, spec=spec, info=info, shorthand=f"-L {shorthand}")
117+
for (shorthand, klass) in LOADERS_BY_SCHEME.items()
118+
)
119+
loaders = importlib.import_module(spec.subclass_location)
120+
121+
loader_items.update(
122+
LoaderCSVItem.from_class(klass, spec=spec, info=info) for klass in _find_subclasses(loaders, spec)
123+
)
124+
125+
return loader_items
126+
127+
128+
def _create_os_items(spec: LookupSpec, info: dict[str, str]) -> set[CSVItemBase]:
129+
operating_systems: set[CSVItemBase] = set()
130+
131+
for plugin_desc in PLUGINS.__plugins__.__os__.values():
132+
module = importlib.import_module(plugin_desc.module)
133+
klass: type = getattr(module, plugin_desc.qualname)
134+
operating_systems.add(CSVItemBase.from_class(klass, spec=spec, info=info))
135+
136+
return operating_systems
137+
138+
139+
def _create_items(spec: LookupSpec, info: dict[str, str], item_class: type[CSVItemBase] = CSVItem) -> set[CSVItemBase]:
140+
base_module = importlib.import_module(spec.subclass_location)
141+
csv_items: set[CSVItemBase] = set()
142+
csv_items.update(
143+
item_class.from_class(class_, spec=spec, info=info) for class_ in _find_subclasses(base_module, spec)
144+
)
145+
146+
return csv_items
147+
148+
149+
def _create_partition_items(spec: LookupSpec, info: dict[str, str]) -> set[CSVItemBase]:
150+
partition_schemes: set[CSVItemBase] = set()
151+
152+
module = importlib.import_module(spec.subclass_location)
153+
partition_schemes.update(
154+
CSVItemBase.from_class(getattr(module, name), spec=spec, info=info) for name in module.__all__
155+
)
156+
157+
return partition_schemes
158+
159+
160+
def mocked_import(name: str, *args) -> ModuleType:
161+
"""Mock all the unknown imports"""
162+
try:
163+
return ORIG_IMPORT(name, *args)
164+
except ImportError:
165+
return Mock()
166+
167+
168+
def _find_subclasses(module: ModuleType, spec: LookupSpec) -> Iterator[type]:
169+
for path in Path(module.__spec__.origin).parent.iterdir():
170+
if not path.is_file():
171+
continue
172+
if path.stem == "__init__":
173+
continue
174+
175+
component = importlib.import_module(".".join([module.__name__, path.stem]))
176+
yield from _filter_subclasses(spec, component)
177+
178+
179+
def _filter_subclasses(spec: LookupSpec, module: ModuleType) -> Iterator[type]:
180+
exclusions: list[type] = spec.exclusions
181+
182+
if callable(exclusions):
183+
exclusions = exclusions()
184+
185+
for _, _class in inspect.getmembers(module):
186+
if not inspect.isclass(_class):
187+
continue
188+
189+
if _class is spec.base_class:
190+
continue
191+
192+
if _class in exclusions:
193+
continue
194+
195+
if issubclass(_class, spec.base_class):
196+
yield _class
197+
198+
199+
def write_to_csv(output_file: TextIO, items: list[CSVItemBase]) -> None:
200+
first_item = items[0].as_dict()
201+
202+
writer = csv.DictWriter(output_file, fieldnames=first_item.keys())
203+
writer.writeheader()
204+
writer.writerow(first_item)
205+
writer.writerows(item.as_dict() for item in items[1:])
206+
207+
208+
@dataclass
209+
class LookupSpec:
210+
name: str
211+
base_class: type | None
212+
remove_suffix: str = ""
213+
subclass_location: str = ""
214+
exclusions: list[type] | Callable[[], list[type]] = field(default_factory=list)
215+
item_function: Callable[[LookupSpec, dict[str, str]], set[CSVItemBase]] = _create_items
216+
217+
218+
def _loader_exclusions() -> list[type[Loader]]:
219+
return [loader.realattr for loader in LOADERS_BY_SCHEME.values()]
220+
221+
222+
SUPPORTED_SYSTEMS = [
223+
LookupSpec(
224+
name="loaders",
225+
base_class=Loader,
226+
remove_suffix="Loader",
227+
subclass_location="dissect.target.loaders",
228+
exclusions=_loader_exclusions,
229+
item_function=_create_loader_items,
230+
),
231+
LookupSpec(
232+
name="volumes",
233+
base_class=VolumeSystem,
234+
remove_suffix="VolumeSystem",
235+
subclass_location="dissect.target.volumes",
236+
exclusions=[EncryptedVolumeSystem, LogicalVolumeSystem],
237+
),
238+
LookupSpec(
239+
name="containers",
240+
base_class=Container,
241+
remove_suffix="Container",
242+
subclass_location="dissect.target.containers",
243+
),
244+
LookupSpec(
245+
name="filesystems",
246+
base_class=Filesystem,
247+
remove_suffix="Filesystem",
248+
subclass_location="dissect.target.filesystems",
249+
),
250+
LookupSpec(
251+
name="child_targets",
252+
base_class=ChildTargetPlugin,
253+
remove_suffix="ChildTargetPlugin",
254+
subclass_location="dissect.target.plugins.child",
255+
item_function=partial(_create_items, item_class=CSVItemBase),
256+
),
257+
LookupSpec(name="operating_systems", base_class=OSPlugin, remove_suffix="Plugin", item_function=_create_os_items),
258+
LookupSpec(
259+
name="partition_schemes",
260+
base_class=None,
261+
subclass_location="dissect.volume.disk.schemes",
262+
item_function=_create_partition_items,
263+
),
264+
]
265+
266+
267+
def builder_inited(app: Sphinx) -> None:
268+
dst = Path(app.srcdir).joinpath("supported_targets")
269+
dst.mkdir(exist_ok=True)
270+
271+
csv_default_dir = Path(app.srcdir).joinpath("_defaults")
272+
273+
for spec in SUPPORTED_SYSTEMS:
274+
info_dict = parse_descriptions(csv_default_dir.joinpath(f"{spec.name}.csv"))
275+
csv_items = list(spec.item_function(spec, info_dict))
276+
csv_items.sort(key=lambda x: x.name)
277+
with dst.joinpath(f"{spec.name}.csv").open("wt") as fh:
278+
write_to_csv(fh, csv_items)
279+
280+
281+
def setup(app: Sphinx) -> dict[str, Any]:
282+
app.connect("builder-inited", builder_inited)
283+
app.add_config_value("dissect_table_keep_files", False, "html")
284+
285+
return {
286+
"version": "0.1",
287+
"parallel_read_safe": True,
288+
"parallel_write_safe": True,
289+
}

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"sphinx_copybutton",
5050
"sphinx_design",
5151
"dissect_plugins",
52+
"supported_targets",
5253
]
5354

5455
# Define the canonical URL if you are using a custom domain on Read the Docs

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Get in touch, join us on `github <https://github.com/fox-it/dissect.target>`_!
9898
Shell </target-shell>
9999
Mount </target-mount>
100100
Acquire </acquire>
101+
Supported Targets </supported-targets>
101102
RDump </rdump>
102103

103104

docs/source/supported-targets.rst

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
Supported targets
2+
-----------------
3+
4+
This page contains a list of the systems that ``dissect`` supports.
5+
6+
Loaders
7+
~~~~~~~
8+
9+
.. Descriptions can be reworded by changing _defaults/loaders.csv
10+
.. csv-table:: Supported Loaders
11+
:header-rows: 1
12+
:file: /supported_targets/loaders.csv
13+
:widths: 15 10 25
14+
15+
16+
Containers
17+
~~~~~~~~~~
18+
19+
.. Descriptions can be reworded by changing _defaults/containers.csv
20+
.. csv-table:: Supported Containers
21+
:header-rows: 1
22+
:file: /supported_targets/containers.csv
23+
:widths: 15 10 25
24+
25+
26+
Partition Schemes
27+
~~~~~~~~~~~~~~~~~
28+
29+
.. Descriptions can be reworded by changing _defaults/containers.csv
30+
.. csv-table:: Supported Partition Schemes
31+
:header-rows: 1
32+
:file: /supported_targets/partition_schemes.csv
33+
:widths: 15 35
34+
35+
36+
Volume Systems
37+
~~~~~~~~~~~~~~
38+
39+
.. Descriptions can be reworded by changing _defaults/volumes.csv
40+
.. csv-table:: Supported Volume Systems
41+
:header-rows: 1
42+
:file: /supported_targets/volumes.csv
43+
:widths: 15 10 25
44+
45+
46+
Filesystems
47+
~~~~~~~~~~~
48+
49+
.. Descriptions can be reworded by changing _defaults/filesystems.csv
50+
.. csv-table:: Supported Filesystems
51+
:header-rows: 1
52+
:file: /supported_targets/filesystems.csv
53+
:widths: 15 10 25
54+
55+
56+
Operating Systems
57+
~~~~~~~~~~~~~~~~~
58+
59+
.. Descriptions can be reworded by changing _defaults/operating_systems.csv
60+
.. csv-table:: Supported Operating Systems
61+
:header-rows: 1
62+
:file: /supported_targets/operating_systems.csv
63+
:widths: 15 35
64+
65+
Child Targets
66+
~~~~~~~~~~~~~
67+
68+
.. Descriptions can be reworded by changing _defaults/operating_systems.csv
69+
.. csv-table:: Supported Child Targets
70+
:header-rows: 1
71+
:file: /supported_targets/child_targets.csv
72+
:widths: 15 35
73+

0 commit comments

Comments
 (0)