first commit

This commit is contained in:
2026-07-11 23:54:36 +07:00
commit b07562921b
40 changed files with 3813 additions and 0 deletions

0
core/__init__.py Normal file
View File

76
core/base.py Normal file
View File

@@ -0,0 +1,76 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
from pydantic import BaseModel
from core.plugin_types import EmptyPluginConfig
from core.schemas.results import AuditResults
if TYPE_CHECKING:
from core.schemas.config import ResolvedPluginConfig
from reporting.models import AuditReport, RenderedReport
TReportTransport = TypeVar("TReportTransport", bound=BaseModel)
TWorkerConfig = TypeVar("TWorkerConfig", bound=BaseModel)
class BaseWorker(ABC):
config_model: type[BaseModel] = EmptyPluginConfig
def __init__(self, config: BaseModel | dict[str, Any] | None = None) -> None:
"""
Config dict will be provided on initialization, providing settings, parsed from config.yaml
"""
self.raw_config = config
self.config: BaseModel | dict[str, Any]
if isinstance(config, BaseModel):
self.config = config.model_dump(mode="python")
else:
self.config = config or {}
@abstractmethod
def run(self) -> AuditResults:
"""
Executes the scanning.
Method should return AuditResults object, otherwise the output won't be processed.
"""
...
class BaseRenderer(ABC):
content_type: str
media_type: str
file_extension: str
aliases: tuple[str, ...] = ()
config_model: type[BaseModel] = EmptyPluginConfig
@abstractmethod
def render(self, report: AuditReport, config: BaseModel) -> RenderedReport: ...
class BaseTransport(ABC):
transport_type: str
config_model: type[BaseModel] = EmptyPluginConfig
@abstractmethod
def publish(self, rendered_report: RenderedReport, transport: ResolvedPluginConfig) -> str: ...
class TypedTransport(BaseTransport, Generic[TReportTransport], ABC):
config_model: type[TReportTransport]
def publish(self, rendered_report: RenderedReport, transport: ResolvedPluginConfig) -> str:
if not isinstance(transport.config, self.config_model):
raise TypeError(
f"Transport '{self.transport_type}' expected {self.config_model.__name__}, "
f"got {type(transport.config).__name__}"
)
return self.publish_typed(rendered_report, cast(TReportTransport, transport.config))
@abstractmethod
def publish_typed(
self, rendered_report: RenderedReport, transport: TReportTransport
) -> str: ...

26
core/evaluation.py Normal file
View File

@@ -0,0 +1,26 @@
from core.schemas.config import Config, ResolvedConfig
from core.schemas.results import AuditResults
from core.schemas.status import AuditSeverity
SEVERITY_WEIGHTS = {
AuditSeverity.LOW: 1,
AuditSeverity.MEDIUM: 2,
AuditSeverity.HIGH: 3,
AuditSeverity.CRITICAL: 4,
}
def evaluate_exit_code(results: list[AuditResults], config: Config | ResolvedConfig) -> int:
severity_threshold = SEVERITY_WEIGHTS.get(
config.allow_failures_below, SEVERITY_WEIGHTS[AuditSeverity.HIGH]
)
threshold_crossed = any(
SEVERITY_WEIGHTS.get(finding.severity, 1) >= severity_threshold
for result in results
for finding in result.findings
)
if threshold_crossed and config.strict_mode:
return 1
return 0

199
core/module_loader.py Normal file
View File

@@ -0,0 +1,199 @@
from __future__ import annotations
import importlib
import logging
from importlib.metadata import entry_points
from pathlib import Path
from typing import Any
from pydantic import BaseModel
from core.base import BaseWorker
from core.plugin_types import HYDROGEN_API_VERSION, EmptyPluginConfig
from core.schemas import ModuleManifest
from core.schemas.config import Config, ResolvedConfig
from core.schemas.modules import LoadedModule, LoadedWorker, ModuleLoadResult
from reporting.models import PluginRuntimeError
logger = logging.getLogger(__name__)
def load_module(package_name: str) -> LoadedModule | None:
module = importlib.import_module(package_name)
raw_manifest = getattr(module, "MANIFEST", None)
if raw_manifest is None:
logger.warning("module package %s does not export MANIFEST", package_name)
return None
manifest = ModuleManifest.model_validate(raw_manifest)
if manifest.api_version != HYDROGEN_API_VERSION:
raise ValueError(
f"module package {package_name} targets api_version={manifest.api_version}, "
f"expected {HYDROGEN_API_VERSION}"
)
builder = getattr(module, "build_worker", None)
if builder is None:
logger.warning("module package %s does not export build_worker", package_name)
return None
if not callable(builder):
logger.warning("build_worker in package %s is not callable", package_name)
return None
config_model = getattr(module, "CONFIG_MODEL", None)
if config_model is None:
config_model = getattr(getattr(builder, "__self__", None), "config_model", None)
if config_model is None:
config_model = EmptyPluginConfig
if not isinstance(config_model, type) or not issubclass(config_model, BaseModel):
raise TypeError(f"module package {package_name} exports invalid CONFIG_MODEL")
return LoadedModule(
package_name=package_name,
manifest=manifest,
build_worker=builder,
config_model=config_model,
)
def build_worker(
loaded_module: LoadedModule, config: BaseModel | dict[str, Any]
) -> LoadedWorker | None:
worker = loaded_module.build_worker(config)
if not isinstance(worker, BaseWorker):
logger.warning(
"build_worker in package %s did not return BaseWorker", loaded_module.package_name
)
return None
return LoadedWorker(worker=worker, manifest=loaded_module.manifest, config=config)
def discover_modules(path: Path, config: Config, package_prefix: str = "modules") -> list[str]:
package_names: list[str] = []
if path.exists():
for entry in path.iterdir():
if not entry.is_dir() or entry.name.startswith("__"):
continue
if not (entry / "__init__.py").exists():
continue
package_names.append(f"{package_prefix}.{entry.name}")
package_names.extend(config.plugin_packages.modules)
try:
module_entry_points = entry_points(group="hydrogen.modules")
except TypeError:
module_entry_points = entry_points().select(group="hydrogen.modules")
for module_entry_point in module_entry_points:
package_names.append(module_entry_point.value.partition(":")[0])
return list(dict.fromkeys(package_names))
def load_modules(path: Path, config: Config, package_prefix: str = "modules") -> ModuleLoadResult:
workers: list[LoadedWorker] = []
errors: list[PluginRuntimeError] = []
for package_name in discover_modules(path, config, package_prefix):
try:
module = load_module(package_name)
if module is None:
continue
if module.manifest.category in config.exclude_categories:
logger.info(
"module %s was skipped due to config exclusion", module.manifest.identifier
)
continue
module_config = config.modules.get(module.manifest.identifier, {})
validated_config = module.config_model.model_validate(module_config)
worker = build_worker(module, validated_config)
if worker is not None:
workers.append(worker)
logger.info("module %s has been successfully loaded!", module.manifest.identifier)
except Exception as exc:
logger.exception("failed to load module package %s", package_name)
errors.append(
PluginRuntimeError(
plugin_kind="module",
plugin_name=package_name,
stage="load",
message=str(exc),
)
)
return ModuleLoadResult(loaded_workers=workers, errors=errors)
def load_prevalidated_workers(
loaded_modules: list[LoadedModule],
resolved_config: ResolvedConfig,
) -> ModuleLoadResult:
workers: list[LoadedWorker] = []
errors: list[PluginRuntimeError] = []
for loaded_module in loaded_modules:
if loaded_module.manifest.category in resolved_config.exclude_categories:
continue
module_config = resolved_config.modules.get(loaded_module.manifest.identifier)
if module_config is None:
continue
try:
worker = build_worker(loaded_module, module_config)
if worker is None:
errors.append(
PluginRuntimeError(
plugin_kind="module",
plugin_name=loaded_module.manifest.identifier,
stage="build_worker",
message="build_worker returned an invalid worker instance",
)
)
continue
workers.append(worker)
except Exception as exc:
logger.exception("failed to build worker for module %s", loaded_module.package_name)
errors.append(
PluginRuntimeError(
plugin_kind="module",
plugin_name=loaded_module.manifest.identifier,
stage="build_worker",
message=str(exc),
)
)
return ModuleLoadResult(loaded_workers=workers, errors=errors)
def load_module_descriptors(
path: Path, config: Config, package_prefix: str = "modules"
) -> tuple[list[LoadedModule], list[PluginRuntimeError]]:
modules: list[LoadedModule] = []
errors: list[PluginRuntimeError] = []
for package_name in discover_modules(path, config, package_prefix):
try:
module = load_module(package_name)
if module is not None:
modules.append(module)
except Exception as exc:
logger.exception("failed to discover module package %s", package_name)
errors.append(
PluginRuntimeError(
plugin_kind="module",
plugin_name=package_name,
stage="discovery",
message=str(exc),
)
)
return modules, errors

155
core/plugin_types.py Normal file
View File

@@ -0,0 +1,155 @@
from __future__ import annotations
import inspect
import logging
import pkgutil
from dataclasses import dataclass
from importlib import import_module
from importlib.metadata import entry_points
from types import ModuleType
from typing import Any, TypeVar
from pydantic import BaseModel
logger = logging.getLogger(__name__)
TPlugin = TypeVar("TPlugin", bound=object)
HYDROGEN_API_VERSION = "1"
class EmptyPluginConfig(BaseModel):
pass
@dataclass(frozen=True)
class PluginLoadError:
plugin_kind: str
source: str
message: str
@dataclass(frozen=True)
class PluginDescriptor:
plugin: object
source: str
@dataclass(frozen=True)
class PluginGroupConfig:
package_names: list[str]
entry_point_group: str
plugin_kind: str
def discover_plugins(
group_config: PluginGroupConfig,
base_class: type[TPlugin],
) -> tuple[list[PluginDescriptor], list[PluginLoadError]]:
descriptors: list[PluginDescriptor] = []
errors: list[PluginLoadError] = []
for package_name in group_config.package_names:
try:
for plugin_class in _discover_plugin_classes(package_name, base_class):
descriptors.append(
PluginDescriptor(plugin=plugin_class(), source=f"package:{package_name}")
)
except Exception as exc:
logger.exception(
"failed to discover %s package %s", group_config.plugin_kind, package_name
)
errors.append(
PluginLoadError(
plugin_kind=group_config.plugin_kind,
source=f"package:{package_name}",
message=str(exc),
)
)
try:
available_entry_points = entry_points(group=group_config.entry_point_group)
except TypeError:
available_entry_points = entry_points().select(group=group_config.entry_point_group)
for entry_point in available_entry_points:
try:
candidate = entry_point.load()
plugin = _instantiate_entry_point_plugin(candidate, base_class)
descriptors.append(
PluginDescriptor(
plugin=plugin,
source=f"entry-point:{group_config.entry_point_group}:{entry_point.name}",
)
)
except Exception as exc:
logger.exception(
"failed to load %s entry point %s",
group_config.plugin_kind,
entry_point.name,
)
errors.append(
PluginLoadError(
plugin_kind=group_config.plugin_kind,
source=f"entry-point:{group_config.entry_point_group}:{entry_point.name}",
message=str(exc),
)
)
return descriptors, errors
def ensure_plugin_config_model(plugin: object) -> type[BaseModel]:
config_model = getattr(plugin, "config_model", EmptyPluginConfig)
if not inspect.isclass(config_model) or not issubclass(config_model, BaseModel):
raise TypeError(f"plugin {type(plugin).__name__} has invalid config_model")
return config_model
def _instantiate_entry_point_plugin(candidate: Any, base_class: type[TPlugin]) -> TPlugin:
if inspect.isclass(candidate):
if not issubclass(candidate, base_class):
raise TypeError(f"{candidate.__name__} is not a {base_class.__name__}")
if inspect.isabstract(candidate):
raise TypeError(f"{candidate.__name__} is abstract")
return candidate()
if not isinstance(candidate, base_class):
raise TypeError(f"entry point did not return {base_class.__name__}")
return candidate
def _discover_plugin_classes(
package_name: str, base_class: type[TPlugin]
) -> tuple[type[TPlugin], ...]:
package = import_module(package_name)
modules = [package, *(_load_modules(package))]
discovered: list[type[TPlugin]] = []
seen: set[type[object]] = set()
for module in modules:
for _, candidate in inspect.getmembers(module, inspect.isclass):
if candidate in seen:
continue
if candidate is base_class or not issubclass(candidate, base_class):
continue
if inspect.isabstract(candidate):
continue
if candidate.__module__ != module.__name__:
continue
seen.add(candidate)
discovered.append(candidate)
return tuple(discovered)
def _load_modules(package: ModuleType) -> list[ModuleType]:
if not hasattr(package, "__path__"):
return []
modules: list[ModuleType] = []
for module_info in pkgutil.walk_packages(package.__path__, prefix=f"{package.__name__}."):
modules.append(import_module(module_info.name))
return modules

171
core/runner.py Normal file
View File

@@ -0,0 +1,171 @@
from __future__ import annotations
import logging
import sys
from collections import Counter
from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait
from pathlib import Path
from time import perf_counter
from core.evaluation import evaluate_exit_code
from core.module_loader import load_module_descriptors, load_prevalidated_workers
from core.runtime import resolve_config, validate_reporting_plugins
from core.schemas.config import Config
from core.schemas.results import AuditResults
from core.schemas.status import AuditStatus
from reporting.bootstrap import bootstrap_reporting
from reporting.models import ModuleAuditResult, ModuleExecutionStats, PluginRuntimeError
from reporting.service import ReportService
logger = logging.getLogger(__name__)
def run_audit(modules_path: Path, config: Config, package_prefix: str = "modules") -> int:
_configure_logging(config)
loaded_modules, module_discovery_errors = load_module_descriptors(
modules_path, config, package_prefix
)
reporting_bootstrap = bootstrap_reporting(config.plugin_packages)
reporting_validation_errors = validate_reporting_plugins(
reporting_bootstrap.renderer_registry,
reporting_bootstrap.transport_registry,
)
resolved_config, config_errors = resolve_config(
config,
loaded_modules,
reporting_bootstrap.renderer_registry,
reporting_bootstrap.transport_registry,
reporting_bootstrap.errors,
)
worker_load_result = load_prevalidated_workers(loaded_modules, resolved_config)
plugin_errors = [
*module_discovery_errors,
*reporting_validation_errors,
*config_errors,
*worker_load_result.errors,
]
results = _run_workers(worker_load_result.loaded_workers, resolved_config)
exit_code = evaluate_exit_code([r.result for r in results], resolved_config)
report_service = ReportService(
renderer_registry=reporting_bootstrap.renderer_registry,
transport_registry=reporting_bootstrap.transport_registry,
)
report = report_service.build_report(results, exit_code, plugin_errors)
if resolved_config.dry_run:
logger.info("dry_run enabled, skipping report publish")
else:
publish_errors = _publish_reports(report_service, report, resolved_config.reports)
if publish_errors:
report.plugin_errors.extend(publish_errors)
return report.exit_code
def _run_workers(workers: list, config) -> list[ModuleAuditResult]:
if not workers:
return []
indexed_workers = list(enumerate(workers))
results_by_index: dict[int, ModuleAuditResult] = {}
with ThreadPoolExecutor(max_workers=config.max_concurrency) as executor:
pending: dict[Future[ModuleAuditResult], int] = {}
next_index = 0
while next_index < len(indexed_workers) and len(pending) < config.max_concurrency:
index, worker = indexed_workers[next_index]
pending[executor.submit(_run_single_worker, worker)] = index
next_index += 1
stop_submitting = False
while pending:
done, _ = wait(tuple(pending), return_when=FIRST_COMPLETED)
for future in done:
index = pending.pop(future)
result = future.result()
results_by_index[index] = result
if config.fail_fast and result.result.status is AuditStatus.FAIL:
stop_submitting = True
while (
not stop_submitting
and next_index < len(indexed_workers)
and len(pending) < config.max_concurrency
):
index, worker = indexed_workers[next_index]
pending[executor.submit(_run_single_worker, worker)] = index
next_index += 1
return [results_by_index[index] for index in sorted(results_by_index)]
def _run_single_worker(worker) -> ModuleAuditResult:
started_at = perf_counter()
try:
result = worker.worker.run()
duration_seconds = perf_counter() - started_at
counts = Counter(finding.severity for finding in result.findings)
return ModuleAuditResult(
module=worker.manifest,
result=result,
stats=ModuleExecutionStats(
duration_seconds=duration_seconds, finding_counts=dict(counts)
),
)
except Exception as exc:
logger.exception("worker %s failed during execution", worker.manifest.identifier)
duration_seconds = perf_counter() - started_at
failed_result = AuditResults(status=AuditStatus.FAIL, findings=[], risk_level=1.0)
return ModuleAuditResult(
module=worker.manifest,
result=failed_result,
stats=ModuleExecutionStats(duration_seconds=duration_seconds, finding_counts={}),
error=PluginRuntimeError(
plugin_kind="module",
plugin_name=worker.manifest.identifier,
stage="run",
message=str(exc),
),
)
def _configure_logging(config: Config) -> None:
level_name = config.logging.level.upper()
level = getattr(logging, level_name, logging.INFO)
root_logger = logging.getLogger()
stream = sys.stdout if config.logging.output.value == "stdout" else sys.stderr
handler = logging.StreamHandler(stream)
handler.setLevel(level)
handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s"))
root_logger.handlers.clear()
root_logger.addHandler(handler)
root_logger.setLevel(level)
def _publish_reports(report_service, report, outputs) -> list[PluginRuntimeError]:
errors: list[PluginRuntimeError] = []
for output in outputs:
try:
report_service.publish(report, output)
except Exception as exc:
logger.exception(
"failed to publish output %s via %s",
output.renderer.type,
output.transport.type,
)
errors.append(
PluginRuntimeError(
plugin_kind="report_output",
plugin_name=f"{output.renderer.type}->{output.transport.type}",
stage="publish",
message=str(exc),
)
)
return errors

148
core/runtime.py Normal file
View File

@@ -0,0 +1,148 @@
from __future__ import annotations
import logging
from pydantic import BaseModel, ValidationError
from core.base import BaseRenderer, BaseTransport
from core.plugin_types import PluginLoadError, ensure_plugin_config_model
from core.schemas.config import Config, ResolvedConfig, ResolvedPluginConfig, ResolvedReportOutput
from core.schemas.modules import LoadedModule
from reporting.models import PluginRuntimeError
from reporting.registry import RendererRegistry
from reporting.transport_registry import TransportRegistry
logger = logging.getLogger(__name__)
def resolve_config(
config: Config,
loaded_modules: list[LoadedModule],
renderer_registry: RendererRegistry,
transport_registry: TransportRegistry,
plugin_errors: list[PluginLoadError] | None = None,
) -> tuple[ResolvedConfig, list[PluginRuntimeError]]:
runtime_errors = [_plugin_load_error_to_runtime_error(error) for error in plugin_errors or ()]
resolved_modules: dict[str, BaseModel] = {}
seen_module_ids: set[str] = set()
for module in loaded_modules:
if module.manifest.identifier in seen_module_ids:
runtime_errors.append(
PluginRuntimeError(
plugin_kind="module",
plugin_name=module.manifest.identifier,
stage="discovery",
message="duplicate module identifier",
)
)
continue
seen_module_ids.add(module.manifest.identifier)
raw_module_config = config.modules.get(module.manifest.identifier, {})
try:
resolved_modules[module.manifest.identifier] = module.config_model.model_validate(
raw_module_config
)
except ValidationError as exc:
runtime_errors.append(
PluginRuntimeError(
plugin_kind="module",
plugin_name=module.manifest.identifier,
stage="config_validation",
message=str(exc),
)
)
resolved_outputs: list[ResolvedReportOutput] = []
for output in config.reports.outputs:
try:
renderer = renderer_registry.get(output.renderer.type)
transport = transport_registry.get(output.transport.type)
resolved_outputs.append(
ResolvedReportOutput(
renderer=_resolve_plugin_config(
output.renderer.type, output.renderer.payload(), renderer
),
transport=_resolve_plugin_config(
output.transport.type, output.transport.payload(), transport
),
)
)
except (ValidationError, ValueError, TypeError) as exc:
runtime_errors.append(
PluginRuntimeError(
plugin_kind="report_output",
plugin_name=f"{output.renderer.type}->{output.transport.type}",
stage="config_validation",
message=str(exc),
)
)
resolved_config = ResolvedConfig(
exclude_categories=config.exclude_categories,
allow_failures_below=config.allow_failures_below,
strict_mode=config.strict_mode,
fail_fast=config.fail_fast,
dry_run=config.dry_run,
max_concurrency=config.max_concurrency,
logging=config.logging,
plugin_packages=config.plugin_packages,
reports=resolved_outputs,
modules=resolved_modules,
)
return resolved_config, runtime_errors
def validate_reporting_plugins(
renderer_registry: RendererRegistry,
transport_registry: TransportRegistry,
) -> list[PluginRuntimeError]:
errors: list[PluginRuntimeError] = []
errors.extend(_validate_registry_plugins("renderer", renderer_registry._renderers.values()))
errors.extend(_validate_registry_plugins("transport", transport_registry._transports.values()))
return errors
def _validate_registry_plugins(plugin_kind: str, plugins: object) -> list[PluginRuntimeError]:
seen: set[type[object]] = set()
errors: list[PluginRuntimeError] = []
for plugin in plugins:
plugin_type = type(plugin)
if plugin_type in seen:
continue
seen.add(plugin_type)
try:
ensure_plugin_config_model(plugin)
except TypeError as exc:
errors.append(
PluginRuntimeError(
plugin_kind=plugin_kind,
plugin_name=plugin_type.__name__,
stage="plugin_validation",
message=str(exc),
)
)
return errors
def _resolve_plugin_config(
plugin_type: str,
payload: dict[str, object],
plugin: BaseRenderer | BaseTransport,
) -> ResolvedPluginConfig:
config_model = ensure_plugin_config_model(plugin)
return ResolvedPluginConfig(type=plugin_type, config=config_model.model_validate(payload))
def _plugin_load_error_to_runtime_error(error: PluginLoadError) -> PluginRuntimeError:
return PluginRuntimeError(
plugin_kind=error.plugin_kind,
plugin_name=error.source,
stage="discovery",
message=error.message,
)

5
core/schemas/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .manifest import ModuleManifest
from .results import AuditFindings, AuditResults
from .status import AuditSeverity, AuditStatus
__all__ = ["AuditFindings", "AuditResults", "AuditSeverity", "AuditStatus", "ModuleManifest"]

83
core/schemas/config.py Normal file
View File

@@ -0,0 +1,83 @@
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from core.schemas.status import AuditSeverity
class LoggingOutput(StrEnum):
STDOUT = "stdout"
STDERR = "stderr"
class LoggingConfig(BaseModel):
level: str = Field("INFO", min_length=1)
output: LoggingOutput = Field(LoggingOutput.STDOUT)
class PluginPackagesConfig(BaseModel):
renderers: list[str] = Field(default_factory=lambda: ["reporting.exporters"])
transports: list[str] = Field(default_factory=lambda: ["reporting.transports"])
modules: list[str] = Field(default_factory=list)
class PluginConfigRef(BaseModel):
model_config = ConfigDict(extra="allow")
type: str = Field(min_length=1)
def payload(self) -> dict[str, Any]:
return dict(self.model_extra or {})
class ReportOutputConfig(BaseModel):
renderer: PluginConfigRef = Field()
transport: PluginConfigRef = Field()
class ReportsConfig(BaseModel):
outputs: list[ReportOutputConfig] = Field(min_length=1)
class Config(BaseModel):
exclude_categories: list[str] = Field(default_factory=list)
allow_failures_below: AuditSeverity = Field()
strict_mode: bool = Field()
fail_fast: bool = Field(False)
dry_run: bool = Field(False)
max_concurrency: int = Field(4, ge=1)
logging: LoggingConfig = Field(default_factory=LoggingConfig)
plugin_packages: PluginPackagesConfig = Field(default_factory=PluginPackagesConfig)
reports: ReportsConfig = Field()
modules: dict[str, dict[str, Any]] = Field(default_factory=dict)
@dataclass(frozen=True)
class ResolvedPluginConfig:
type: str
config: BaseModel
@dataclass(frozen=True)
class ResolvedReportOutput:
renderer: ResolvedPluginConfig
transport: ResolvedPluginConfig
@dataclass(frozen=True)
class ResolvedConfig:
exclude_categories: list[str]
allow_failures_below: AuditSeverity
strict_mode: bool
fail_fast: bool
dry_run: bool
max_concurrency: int
logging: LoggingConfig
plugin_packages: PluginPackagesConfig
reports: list[ResolvedReportOutput]
modules: dict[str, BaseModel]

15
core/schemas/manifest.py Normal file
View File

@@ -0,0 +1,15 @@
from pydantic import BaseModel, ConfigDict, Field
class ModuleManifest(BaseModel):
model_config = ConfigDict(
extra="forbid",
frozen=True,
)
identifier: str = Field(pattern=r"^[a-z][a-z0-9_-]*$")
name: str = Field(min_length=1)
category: str = Field(min_length=1)
version: str = Field(pattern=r"^\d+\.\d+\.\d+$")
api_version: str = Field("1", pattern=r"^\d+$")
description: str = ""

32
core/schemas/modules.py Normal file
View File

@@ -0,0 +1,32 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from pydantic import BaseModel
from core.base import BaseWorker
from core.schemas import ModuleManifest
from reporting.models import PluginRuntimeError
WorkerBuilder = Callable[[BaseModel | dict[str, Any]], BaseWorker]
@dataclass(frozen=True)
class LoadedModule:
package_name: str
manifest: ModuleManifest
build_worker: WorkerBuilder
config_model: type[BaseModel]
@dataclass(frozen=True)
class LoadedWorker:
worker: BaseWorker
manifest: ModuleManifest
config: BaseModel | dict[str, Any]
@dataclass(frozen=True)
class ModuleLoadResult:
loaded_workers: list[LoadedWorker]
errors: list[PluginRuntimeError]

17
core/schemas/results.py Normal file
View File

@@ -0,0 +1,17 @@
from pydantic import BaseModel, Field
from core.schemas.status import AuditSeverity, AuditStatus
class AuditFindings(BaseModel):
name: str = Field()
description: str = Field("")
severity: AuditSeverity = Field()
class AuditResults(BaseModel):
status: AuditStatus = Field()
findings: list[AuditFindings] = Field()
risk_level: float = Field(
description="value from 0.0 to 1.0, indicating how dangerous the auditresults are."
)

14
core/schemas/status.py Normal file
View File

@@ -0,0 +1,14 @@
from enum import StrEnum
class AuditStatus(StrEnum):
PASS = "pass"
SKIPPED = "skipped"
FAIL = "fail"
class AuditSeverity(StrEnum):
CRITICAL = "critical"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"