Plugin System¶
This document covers the compliance plugin architecture: the audit command that hosts plugins, the plugin registry and its trust model, the dynamic loader, and the panic-recovery contract that keeps one misbehaving plugin from corrupting an audit run. For high-level system context see overview.md; for how audit reports flow through the render pipeline see pipelines.md.
Authoritative references. The canonical documentation for plugin implementation and the hardest-won operational gotchas lives in two places and should be read alongside this page:
- Plugin Development Guide — compliance plugin and device parser development (APIs, lifecycle, examples).
- GOTCHAS.md §2 Plugin Architecture — registry independence, panic recovery,
SetPluginDirordering, info-severity semantics, dynamic-plugin trust model.
Audit Command Architecture¶
Overview¶
The opndossier audit command provides the dedicated, first-class entry point for security audit and compliance checks. It uses the underlying audit/compliance engine through a CLI surface optimized for audit-specific workflows.
Command Structure and Execution Flow¶
-
Command Definition (
cmd/audit.go): -
Declares audit-specific flags:
--mode(blue/red),--plugins(compliance checks),--plugin-dir(dynamic plugin loading) - Reuses shared output flags:
--format,--output,--wrap,--section,--comprehensive,--redact PreRunEvalidation enforces:- Valid audit mode (blue, red)
--pluginsflag only accepted with--mode blue(compliance checks only run in blue mode)--outputflag rejected when auditing multiple files (prevents output clobbering)
- Plugin name validation deferred to post-initialization (
ValidateModeConfigininternal/audit/mode_controller.go) to support dynamic plugins loaded from--plugin-dir -
Shell completions for
--pluginsflag use registry-backedregistryPluginNames()function, mirroring theValidDeviceTypespattern for dynamic discovery of available plugins -
Execution Flow (
runAudit): -
Validates device type flag before any file processing
- Processes multiple input files concurrently with configurable semaphore (defaults to
runtime.NumCPU()) - Buffers all results before emission to prevent interleaved stdout writes or file overwrites
- Each file processed via
generateAuditOutput(parsing + audit generation, no I/O) -
Results emitted serially via
emitAuditResultafter all processing completes -
Output Emission (
cmd/audit_output.go): -
emitAuditResulthandles file vs stdout emission with format-specific rendering - Markdown output to stdout uses glamour for styled terminal rendering
- Non-markdown formats (JSON, YAML, text, HTML) written raw
- File output uses standard file export without terminal styling
Related Documentation¶
For complete implementation details of the two-phase validation pattern (CLI parsing vs. post-initialization validation), see:
- docs/solutions/logic-errors/cli-prerun-validation-timing-dynamic-plugins.md — Deferred plugin validation pattern for dynamic plugin support
Architectural Patterns¶
Shared Validation Extraction¶
The validateOutputFlags() helper (in cmd/shared_flags.go) was extracted from validateConvertFlags() to share format, wrap, and section validation logic between audit and convert commands:
- Validates: Format against
converter.DefaultRegistry, wrap width range, mutual exclusivity of--wrapand--no-wrap - Warns: When section filtering used with JSON/YAML (sections ignored in structured formats)
- Reused by: Both
convertandauditcommands callvalidateOutputFlags()in theirPreRunEhooks - Command-specific validation: Each command performs its own audit-mode/plugin validation on command-specific flag variables
Multi-File Output Naming¶
When auditing multiple files, each report is auto-named to prevent filename collisions:
- Pattern:
<escaped-path>_<basename>-audit.<ext> - Escaping: Lossless tilde-based escaping via
escapePathSegment(): - Tildes become
~~(escape character doubling) - Underscores become
~u(freeing underscore as segment separator) - Prevents boundary ambiguity:
"a_/b"→"a~u_b","a/_b"→"a_~ub"(unambiguous) - Absolute paths: Marked with
~aprefix segment - Examples:
config.xml→config-audit.mdprod/site-a/config.xml→prod_site-a_config-audit.md~/configs/edge.xml→~a_home_user_configs_edge-audit.md
Plugin Mode Coupling¶
--pluginsflag only accepted with--mode blue(enforced inPreRunE)- Red mode does not execute compliance checks
- When no plugins specified in blue mode, all available plugins run (resolved in
internal/audit/mode_controller.go)
Plugin Registry¶
audit.PluginManager owns a single PluginRegistry supplied at construction time: NewPluginManager(logger, reg). Pass nil to allocate a fresh private registry (the common case for short-lived programmatic callers), or pass a shared *PluginRegistry when multiple managers or subsystems must observe the same plugin set (e.g., CLI helpers and the audit pipeline).
pm.InitializePlugins()populates the registry supplied toNewPluginManager.- Registry methods (
ListPlugins,GetPlugin) are protected bysync.RWMutexand are safe for concurrent access. AfterInitializePluginsreturns, the registry is effectively read-only. - The legacy package-level global registry (
GetGlobalRegistry,RegisterGlobalPlugin,GetGlobalPlugin,ListGlobalPlugins) is// Deprecated:and scheduled for removal in v2.0. New code must not depend on it.
See GOTCHAS.md §2.1 for the historical context on the two-registry bug this consolidation eliminated.
Plugin Selection and the --plugins Flag¶
- The
--pluginsCLI flag is only valid with--mode blue;PreRunErejects it otherwise. - Plugin-name validation is deferred to
ValidateModeConfig(post-init) so that dynamically loaded plugins from--plugin-dirare visible to the check. - When
--pluginsis omitted, all available plugins run (the "all available" default is resolved against the live registry after dynamic loading completes). - Shell completions for
--pluginsare backed byregistryPluginNames(), mirroring theValidDeviceTypespattern so new plugins become discoverable automatically.
Dynamic Plugin Loader¶
PluginRegistry.LoadDynamicPlugins uses Go's plugin.Open() to load .so files from a directory at runtime. Two ordering and trust invariants must be preserved:
SetPluginDirmust precedeInitializePlugins.PluginManager.SetPluginDir(dir, explicit)mutates a field thatInitializePluginsreads only during its execution. Setting the directory afterward has no observable effect. See GOTCHAS.md §2.3.- Loading is opt-in. Dynamic plugin loading requires an explicit
--plugin-dirflag (or the equivalent config key). There is no./pluginsauto-discovery —PluginManager.InitializePluginsonly callsLoadDynamicPluginswhen the configured directory is non-empty. Plugins are never fetched from the network. See GOTCHAS.md §2.5.
Trust Model¶
Dynamic plugins execute with the full privileges of the opnDossier process. There is no signature verification, no checksum validation, and no sandboxing.
- Any
.soin the plugin directory is loaded and executed. - A malicious or compromised plugin has the same filesystem, environment, and network access as opnDossier itself.
- Mitigations (opt-in, operator-owned):
- Restrict filesystem permissions on the plugin directory.
- Only load plugins built from reviewed source code.
- Avoid pointing
--plugin-dirat world-writable directories in shared or CI environments.
The trust model is intentionally minimal — opnDossier does not try to be a plugin sandbox. Operators who need stronger isolation should run opnDossier under OS-level sandboxing (e.g., seccomp, AppArmor, containers) rather than relying on the loader. See GOTCHAS.md §2.5 for the canonical trust statement.
Panic Recovery Contract¶
RunComplianceChecks wraps every plugin's RunChecks() call in defer recover(). The invariant is simple and must be preserved: every selected plugin appears in the result maps, even if it panicked.
When a plugin panics:
- The recovery path populates
PluginFindings,PluginInfo, andCompliancewith safe defaults using thepluginNamestring already in scope. - The recovery path does not call methods on the panicked plugin (
Name(),Version(),Description(),GetControls()) — post-panic internal state may be corrupt, so further method calls are unsafe. - The
Versionfield is set to"unknown (panicked)"and the compliance map is emitted empty. - Execution falls through a
continueto the next plugin; no other plugins are skipped as a side effect.
See GOTCHAS.md §2.2 for the full rationale and the tests that enforce this invariant.
Severity, Compliance, and Inventory Semantics¶
The audit engine draws a clean line between severity (triage priority) and compliance status (pass/fail). Several subtle rules follow from that separation:
- Info severity does not bypass compliance. A finding with
Severity == "info"that references a control still flips that control to non-compliant. Severity only affects presentation ordering and summary counts. - Inventory controls are excluded from the compliance map. Controls with
Type: "inventory"are intentionally omitted from theevaluatedslice returned byRunChecksand surface only in the "Configuration Notes" section of the report. - Unrecognized severity strings are counted in a private
unknownbucket bycountSeverities. Callers that have access to a logger should emit a warning whencounts.unknown > 0.
See GOTCHAS.md §2.4 for the canonical statement of these rules.
How Compliance Results Flow to Output¶
Audit compliance results flow from the plugin registry into the standard multi-format export pipeline:
cmd/audit_handler.gocallsmapAuditReportToComplianceResults()to convertaudit.Reportintocommon.ComplianceResults.handleAuditMode()creates a shallow copy ofCommonDeviceand sets itsComplianceResultsfield to the mapped results.- The enriched device is passed to
generateWithProgrammaticGenerator(), which dispatches to theFormatHandlerfromDefaultRegistry(markdown, JSON, YAML, text, or HTML). - For markdown,
BuildAuditSection()ininternal/converter/builder/renders per-plugin sections, findings tables, and summary. For structured formats,ComplianceResultsis serialized directly.
This is the same pipeline described in detail in pipelines.md — Audit-to-Export Mapping; the plugin system simply populates the ComplianceResults field before that pipeline runs.
Further Reading¶
- Plugin Development Guide — step-by-step authoring guide for new compliance plugins and device parsers.
- GOTCHAS.md §2 Plugin Architecture — authoritative list of plugin-system gotchas.
- GOTCHAS.md §8 Audit Command — mode/plugin coupling, concurrent generation, multi-file output.
- docs/solutions/logic-errors/cli-prerun-validation-timing-dynamic-plugins.md — deferred validation pattern used by
--plugins.