mirror of
https://github.com/crypt0p3g/dpapi-toolkit
synced 2026-09-24 08:14:49 +00:00
184 lines
7.1 KiB
Python
184 lines
7.1 KiB
Python
"""Small, local-only plugin loader for application-specific DPAPI layers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from fnmatch import fnmatch
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
from types import ModuleType
|
|
|
|
|
|
PLUGIN_ROOT = Path(__file__).with_name("plugins")
|
|
PLUGIN_ID = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
|
|
MAX_MANIFEST_BYTES = 64 * 1024
|
|
WEB_FIELD = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
|
|
WEB_FIELD_KINDS = frozenset(("text", "password", "file", "checkbox"))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PluginWebField:
|
|
name: str
|
|
kind: str
|
|
label: str
|
|
placeholder: str
|
|
help: str
|
|
optional: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PluginManifest:
|
|
plugin_id: str
|
|
name: str
|
|
description: str
|
|
directory: Path
|
|
file_patterns: tuple[str, ...]
|
|
web_help: str
|
|
web_input_label: str
|
|
web_workflow_help: str
|
|
web_fields: tuple[PluginWebField, ...]
|
|
|
|
|
|
def discover_plugins() -> dict[str, PluginManifest]:
|
|
root = PLUGIN_ROOT.resolve()
|
|
manifests: dict[str, PluginManifest] = {}
|
|
if not root.is_dir():
|
|
return manifests
|
|
for directory in sorted(path for path in root.iterdir() if path.is_dir()):
|
|
manifest_path = directory / "plugin.json"
|
|
module_path = directory / "plugin.py"
|
|
if not manifest_path.is_file() or not module_path.is_file():
|
|
continue
|
|
if manifest_path.stat().st_size > MAX_MANIFEST_BYTES:
|
|
raise ValueError(f"plugin manifest is too large: {manifest_path}")
|
|
try:
|
|
raw = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise ValueError(f"invalid plugin manifest {manifest_path}: {exc}") from None
|
|
if not isinstance(raw, dict):
|
|
raise ValueError(f"plugin manifest must be an object: {manifest_path}")
|
|
plugin_id = raw.get("id")
|
|
name = raw.get("name")
|
|
description = raw.get("description", "")
|
|
patterns = raw.get("file_patterns", [])
|
|
web = raw.get("web", {})
|
|
if (
|
|
not isinstance(plugin_id, str)
|
|
or not PLUGIN_ID.fullmatch(plugin_id)
|
|
or plugin_id != directory.name
|
|
):
|
|
raise ValueError(f"plugin id must match its safe directory name: {directory}")
|
|
if not isinstance(name, str) or not name.strip():
|
|
raise ValueError(f"plugin {plugin_id} has no display name")
|
|
if not isinstance(description, str):
|
|
raise ValueError(f"plugin {plugin_id} description must be text")
|
|
if not isinstance(patterns, list) or not all(
|
|
isinstance(pattern, str) and pattern and "/" not in pattern and "\\" not in pattern
|
|
for pattern in patterns
|
|
):
|
|
raise ValueError(f"plugin {plugin_id} has invalid file patterns")
|
|
if not isinstance(web, dict):
|
|
raise ValueError(f"plugin {plugin_id} web configuration must be an object")
|
|
web_help = web.get("help", description)
|
|
web_input_label = web.get("input_label", "plugin artifact")
|
|
web_workflow_help = web.get(
|
|
"workflow_help",
|
|
"Tab 1 is not used for plugins. Add the main input here.",
|
|
)
|
|
raw_web_fields = web.get("fields", [])
|
|
if not isinstance(web_help, str):
|
|
raise ValueError(f"plugin {plugin_id} web help must be text")
|
|
if not isinstance(web_input_label, str) or not web_input_label.strip():
|
|
raise ValueError(f"plugin {plugin_id} web input label must be text")
|
|
if not isinstance(web_workflow_help, str) or not web_workflow_help.strip():
|
|
raise ValueError(f"plugin {plugin_id} web workflow help must be text")
|
|
if not isinstance(raw_web_fields, list) or len(raw_web_fields) > 16:
|
|
raise ValueError(f"plugin {plugin_id} has invalid web fields")
|
|
web_fields = []
|
|
seen_web_fields = set()
|
|
for field in raw_web_fields:
|
|
if not isinstance(field, dict):
|
|
raise ValueError(f"plugin {plugin_id} web field must be an object")
|
|
field_name = field.get("name")
|
|
kind = field.get("kind")
|
|
label = field.get("label")
|
|
placeholder = field.get("placeholder", "")
|
|
field_help = field.get("help", "")
|
|
optional = field.get("optional", False)
|
|
if (
|
|
not isinstance(field_name, str)
|
|
or not WEB_FIELD.fullmatch(field_name)
|
|
or field_name in seen_web_fields
|
|
or kind not in WEB_FIELD_KINDS
|
|
or not isinstance(label, str)
|
|
or not label.strip()
|
|
or not isinstance(placeholder, str)
|
|
or not isinstance(field_help, str)
|
|
or not isinstance(optional, bool)
|
|
):
|
|
raise ValueError(f"plugin {plugin_id} has an invalid web field")
|
|
seen_web_fields.add(field_name)
|
|
web_fields.append(PluginWebField(
|
|
field_name,
|
|
kind,
|
|
label.strip(),
|
|
placeholder,
|
|
field_help,
|
|
optional,
|
|
))
|
|
if plugin_id in manifests:
|
|
raise ValueError(f"duplicate plugin id: {plugin_id}")
|
|
manifests[plugin_id] = PluginManifest(
|
|
plugin_id,
|
|
name.strip(),
|
|
description.strip(),
|
|
directory.resolve(),
|
|
tuple(patterns),
|
|
web_help.strip(),
|
|
web_input_label.strip(),
|
|
web_workflow_help.strip(),
|
|
tuple(web_fields),
|
|
)
|
|
return manifests
|
|
|
|
|
|
def detect_plugin(filename: str) -> str | None:
|
|
matches = [
|
|
manifest.plugin_id
|
|
for manifest in discover_plugins().values()
|
|
if any(fnmatch(filename.casefold(), pattern.casefold()) for pattern in manifest.file_patterns)
|
|
]
|
|
return matches[0] if len(matches) == 1 else None
|
|
|
|
|
|
def load_plugin(plugin_id: str) -> tuple[PluginManifest, ModuleType]:
|
|
manifest = discover_plugins().get(plugin_id)
|
|
if manifest is None:
|
|
raise ValueError(f"unknown or unavailable plugin {plugin_id!r}")
|
|
root = PLUGIN_ROOT.resolve()
|
|
module_path = (manifest.directory / "plugin.py").resolve()
|
|
try:
|
|
module_path.relative_to(root)
|
|
except ValueError:
|
|
raise ValueError(f"plugin {plugin_id} escapes the plugin directory") from None
|
|
spec = importlib.util.spec_from_file_location(
|
|
f"dpapi_toolkit_plugin_{plugin_id}", module_path
|
|
)
|
|
if spec is None or spec.loader is None:
|
|
raise ValueError(f"cannot load plugin {plugin_id}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
if getattr(module, "PLUGIN_ID", None) != plugin_id or not callable(
|
|
getattr(module, "run", None)
|
|
):
|
|
raise ValueError(f"plugin {plugin_id} does not implement the required contract")
|
|
return manifest, module
|
|
|
|
|
|
def run_plugin(plugin_id: str, data, source_path, was_hex, args, core, emit):
|
|
manifest, module = load_plugin(plugin_id)
|
|
emit(f"[+] plugin: {manifest.name} ({manifest.plugin_id})")
|
|
return module.run(data, source_path, was_hex, args, core, emit)
|