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
- Declares audit-specific flags:
-
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)
- Tildes become
- 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.