CLI Command Responsibilities#
The opnDossier CLI architecture defines six distinct top-level commands for processing firewall configuration files (OPNsense and pfSense config.xml), each with a single, well-defined responsibility. The commands follow a consistent architectural pattern built on the Cobra framework, sharing common parsing infrastructure while delivering specialized output formats and functionality. This separation of concerns ensures stable API surfaces and prevents feature drift across multiple entry points.
The six commands are: display (styled terminal view), convert (multi-format export), audit (security/compliance reports), sanitize (sensitive data redaction), validate (structural validation), and list (capability discovery). Each command processes firewall configuration files with a distinct input/output contract, from terminal visualization to format conversion, security auditing, data sanitization, validation, and capability enumeration.
The audit command was introduced in PR #454 (March 2026) as a dedicated first-class entry point, replacing the previous convert --audit-mode workflow. This architectural decision established clearer command boundaries, enabled specialized output handling including glamour-rendered terminal output, and provided concurrent multi-file processing capabilities. The evolution reflects a broader design philosophy prioritizing single-responsibility commands with stable, predictable interfaces. The command originally supported three modes (standard/blue/red), but standard mode was removed in March 2026 as it duplicated the neutral documentation functionality already provided by the convert command without adding audit-specific value.
Command Overview and Input/Output Contracts#
Each of the six commands accepts firewall configuration files (OPNsense and pfSense config.xml) as input but serves a distinct purpose in the configuration processing workflow. The commands automatically detect device type by inspecting the XML root element (<opnsense> vs <pfsense>), though users can override detection via the global --device-type flag (values: opnsense, pfsense). The following sections detail their individual responsibilities, input/output contracts, and implementation characteristics.
display: Styled Terminal View#
The display command renders firewall configurations as styled markdown in the terminal using the Glamour library. Unlike other commands, display is exclusively for terminal visualization and does not support file output.
Input/Output Contract:
- Input: Single
config.xmlfile - Output: Glamour-rendered markdown to stdout only (no file export)
Key Flags:
--theme(auto/dark/light/none) — controls Glamour rendering style and color scheme- Shared flags:
--section,--wrap,--comprehensive,--include-tunables,--redact
Implementation Details: The command parses config.xml via parser.NewFactory(), which automatically detects device type by inspecting the root element (<opnsense> vs <pfsense>) and dispatches to the appropriate parser registered via init() functions in pkg/parser/opnsense and pkg/parser/pfsense. The command generates markdown through the builder and renders via Glamour to stdout. Options are wired through buildDisplayOptions() following the standard precedence chain: CLI flags > config file > defaults.
convert: Multi-Format Configuration Export#
The convert command is the primary workhorse for transforming config.xml into structured documentation formats. As the largest CLI file at 803 lines, it supports five output formats with extensive configuration options.
Input/Output Contract:
- Input: One or more
config.xmlfiles - Output: stdout or file in markdown/JSON/YAML/text/HTML format (default: markdown)
Key Flags:
--format, -f— output format selection (markdown, json, yaml, text, html)--output, -o— output file path--force— overwrite without prompt- Shared flags:
--section,--wrap,--comprehensive,--include-tunables,--redact
Implementation Details: The command processes files concurrently using a semaphore to parallelize multi-file operations. Each file is parsed via the shared factory pattern, then builds options with buildConversionOptions(), and dispatches to format handlers via converter.DefaultRegistry. The command includes overwrite protection with user prompts unless --force is specified, preventing accidental data loss.
audit: Security and Compliance Reports#
The audit command provides dedicated security audit and compliance checking as a first-class CLI entry point. Introduced in PR #454, it separates security concerns from general format conversion and supports concurrent multi-file processing with specialized output handling.
Input/Output Contract:
- Input: One or more
config.xmlfiles (concurrent multi-file support) - Output: stdout (glamour-styled markdown) or auto-named files
Key Flags:
--mode(blue/red, default: blue) — audit mode selection for different security perspectives--plugins(stig,sans,firewall; blue mode only) — compliance framework plugins--plugin-dir— directory for dynamic.soplugin loading- Shared output flags:
--format,--output,--wrap,--section,--comprehensive, etc.
Implementation Details: The command processes multiple files concurrently with semaphore limiting (runtime.NumCPU()) to maximize throughput on multi-core systems. The implementation is split across two files: audit.go handles command definition, flags, validation, and core execution logic, while audit_output.go manages output emission and path derivation.
Multi-file naming uses tilde-based escaping to create unique output filenames: prod/site-a/config.xml → prod_site-a_config-audit.md. Markdown output to stdout is glamour-rendered for styled terminal display, while file output and non-markdown formats are written raw. PreRunE validation enforces strict flag constraints: valid audit modes, valid plugin names, --plugins only with --mode blue, and rejects --output when processing multiple files to prevent output path collisions.
sanitize: Sensitive Data Redaction#
The sanitize command redacts sensitive data from config.xml files with referential integrity, ensuring that identical original values are consistently mapped to the same redacted values throughout the document. Unlike other commands, sanitize operates directly on XML without parsing to the internal CommonDevice model.
Input/Output Contract:
- Input: Single
config.xmlfile - Output: Sanitized XML to stdout or file
Key Flags:
--mode, -m(aggressive/moderate/minimal, default: moderate) — controls redaction level--output, -o— output file path--mapping— JSON mapping file path for documenting original→redacted value mappings--force— overwrite without prompt
Sanitization Modes:
- Aggressive: passwords, keys, certificates, all IP addresses, MAC addresses, emails, hostnames, usernames, domains
- Moderate (default): passwords, keys, authserver LDAP values, public IP addresses, MAC addresses, emails (preserves private IPs and hostnames outside
system/authserverfor network topology analysis) - Minimal: passwords, secrets, API keys, PSKs, private keys, SSH keys, and sensitive system/authserver LDAP values (preserves most network information for troubleshooting, excluding authserver hosts)
Implementation Details: The command does NOT parse to CommonDevice; instead, it operates on raw XML via internal/sanitizer/ for direct manipulation. Referential integrity is maintained through consistent value mapping: if 10.0.1.5 is redacted to 192.0.2.1, all occurrences of 10.0.1.5 throughout the document receive the same redacted value. The command can optionally write a mapping file documenting all original→redacted transformations for reverse lookup during analysis.
validate: Structural and Semantic Validation#
The validate command validates firewall configuration structure and content, performing both structural XML validation and semantic field-level checks. Unlike other commands with extensive flag sets, validate has minimal command-specific flags, primarily relying on global configuration.
Input/Output Contract:
- Input: One or more
config.xmlfiles - Output: Validation pass/fail status with structured error messages (human-readable or JSON)
Key Flags:
--json-output— outputs validation errors in JSON format for machine consumption (validate command only)- Global flags:
--verbose,--quiet,--device-type
Implementation Details: The command processes multiple files concurrently and calls the factory with validateMode=true as the fourth parameter to trigger strict ParseAndValidate instead of the standard Parse method. This enables both structural and semantic validation checks.
On validation failure, the command outputs structured error messages in the format ❌ config.xml: validation failed with 3 errors: hostname is required... followed by detailed field-level error descriptions. With --json-output, errors are emitted in JSON format for machine consumption. Success produces a simple ✅ config.xml: Valid message. Exit codes are atomically tracked across goroutines via updateMaxExitCode to ensure proper CLI behavior in CI/CD pipelines.
The --json-output flag is scoped exclusively to the validate command because no other commands need JSON-formatted validation output. This scoping decision prevents flag pollution on other commands where the flag would have no effect.
list: Capability Discovery#
The list command provides machine-readable enumeration of the binary's supported capabilities (plugins, devices, formats) for AI agents and automation without requiring --help text parsing. Unlike the other commands, list does not process configuration files; it enumerates internal registries.
Input/Output Contract:
- Input: None (reads internal registries)
- Output: One name per line (default) or JSON array with
--json
Subcommands:
list plugins— List available audit/compliance pluginslist devices— List supported device parserslist formats— List available output formats
Key Flags:
--json(all subcommands) — emit structured JSON array with name/description/version fields--plugin-dir(plugins only) — directory for dynamic.soplugin loading
Implementation Details: The command group is defined in cmd/list.go as a parent with three child subcommands: list_plugins.go, list_devices.go, and list_formats.go. Each subcommand wraps an internal registry (audit.PluginRegistry, parser.DefaultRegistry(), converter.DefaultRegistry) with stable CLI interfaces. The shared emitList() helper in cmd/list.go provides consistent output formatting across all three subcommands: one name per line by default for easy piping, or structured JSON with --json.
Empty registries return [] (never null) and exit 0. The list devices and list formats subcommands are marked lightweight to skip full config initialization for faster startup. The list plugins subcommand runs the same PluginManager.InitializePlugins path as the audit command, including the dynamic-plugin trust-model warning when --plugin-dir is supplied.
Evolution from convert --audit-mode to Dedicated audit Command#
The audit functionality in opnDossier underwent significant architectural evolution, moving from an integrated feature within the convert command to a standalone, purpose-built command. This transition exemplifies the project's commitment to single-responsibility design and stable command interfaces.
Historical Context#
The audit feature underwent three major iterations:
-
PR #175 (January 2026): Stubbed audit mode code was removed from
cmd/convert.goentirely and deferred to v2.1. -
PR #224 (February 2026): Audit mode was re-integrated into the CLI, wired into the
convertcommand with flags:--audit-mode,--audit-blackhat,--audit-plugins. Addedcmd/audit_handler.gowithhandleAuditMode(). -
PR #454 (March 2026): Audit flags removed from
convert; dedicatedopndossier auditcommand introduced as the canonical entry point with three modes: standard (neutral documentation), blue (defensive audit), and red (attack surface analysis). -
PR #465 (March 2026): Standard mode removed from the
auditcommand as it duplicatedconvertfunctionality without audit-specific value. Neutral, comprehensive documentation is now exclusively handled by theconvertcommand, leavingauditfocused on security-specific perspectives (blue and red modes).
PR #454: Key Changes and Rationale#
PR #454 introduced the opndossier audit command to cleanly separate command responsibilities. The architectural rationale centered on four core principles:
- Command responsibility clarity: Each command should have a single, well-defined purpose. Security auditing is functionally distinct from format conversion.
- Stable API surface: Audit-specific flags (
--mode,--plugins) don't belong on a general-purpose conversion command where they create conceptual confusion. - Consistent output handling: A dedicated command enables specialized output behaviors like glamour-rendered terminal output and intelligent multi-file naming.
- Prevention of drift: Centralizing audit concerns in a single entry point prevents the dual-maintenance burden of keeping two command paths synchronized.
Flags Removed from convert:
--audit-mode→ migrated as--modeto theauditcommand--audit-plugins→ migrated as--pluginsto theauditcommand--audit-blackhat→ removed entirely as non-functional placeholder--plugin-dir→ migrated to theauditcommand
Importantly, these flags were never released on the convert command's public surface, making the transition seamless without breaking changes to existing users. The addSharedAuditFlags() function was removed from cmd/shared_flags.go as part of this cleanup.
--blackhat Flag Removal#
The --audit-blackhat flag and associated addSnarkyCommentary() function were removed in PR #454 because they constituted "a placeholder that set two metadata keys with no real functionality." The flag had been added in PR #224 with the intention of supporting "red team attack surface analysis with blackhat commentary," but was never meaningfully implemented beyond setting metadata. Rather than carrying forward non-functional code, the project opted for clean removal.
Comparison: audit vs convert --audit-mode#
The following table highlights the key differences between the dedicated audit command and the legacy convert --audit-mode approach:
| Aspect | opndossier audit | convert --audit-mode |
|---|---|---|
| Flag names | --mode, --plugins (unprefixed) | --audit-mode, --audit-plugins (prefixed) |
| Multi-file processing | Concurrent with auto-named outputs | Sequential with explicit output paths |
| Terminal output | Glamour-styled markdown for enhanced readability | Raw markdown output |
| Introduced | PR #454 (March 2026) | PR #224 (February 2026) |
| Status | Canonical entry point for audit workflows | Legacy approach, superseded but retained for compatibility |
Outstanding Work: TODO #457#
Internal audit plumbing was intentionally retained in PR #454 for follow-up cleanup. Issue #457 tracks the removal of dead audit code from the convert command implementation, including sharedAuditMode package-level variables and audit-related parameters in generateOutputByFormat that remain in the codebase even though the public CLI surface no longer exposes those flags. This technical debt represents internal implementation details that no longer serve a purpose after the command separation.
Shared Architectural Patterns#
Despite their distinct responsibilities, all six commands share common architectural patterns that promote code reuse, consistency, and maintainability. These shared patterns span configuration parsing, flag management, options construction, and error handling.
Config.xml Processing Pipeline#
All commands except sanitize and list use the same parsing approach through the factory pattern:
parser.NewFactory(cfgparser.NewXMLParser()).CreateDevice(ctx, file, resolveDeviceType(), validateMode)
The factory automatically detects device type by inspecting the XML root element (<opnsense> vs <pfsense>) and dispatches to the appropriate parser registered via init() functions. cmd/root.go imports both parsers via blank imports for automatic registration:
_ "github.com/EvilBit-Labs/opnDossier/pkg/parser/opnsense" // self-registers OPNsense parser
_ "github.com/EvilBit-Labs/opnDossier/pkg/parser/pfsense" // self-registers pfSense parser
The fourth parameter (validateMode) is true only for the validate command, which triggers strict ParseAndValidate instead of the standard Parse method. This unified parsing approach ensures consistent behavior across commands while allowing validation-specific logic where needed. The sanitize command diverges from this pattern by operating directly on raw XML for performance and referential integrity requirements. The list command does not process configuration files at all; it enumerates internal registries.
Shared Flag Infrastructure#
Shared flags are defined in cmd/shared_flags.go as package-level variables that multiple commands can reference. These include sharedIncludeTunables, sharedComprehensive, sharedRedact, sharedSections, sharedWrapWidth, and other configuration options that apply across different output modes.
The deviceTypeDescriptions map in cmd/shared_flags.go includes entries for both "opnsense" and "pfsense" to provide shell completion suggestions for the global --device-type flag.
validateOutputFlags() in cmd/shared_flags.go centralizes format, wrap, and section validation logic for the convert and audit commands. This function:
- Validates requested format against
converter.DefaultRegistryto ensure the format handler exists - Enforces
--wrap/--no-wrapmutual exclusivity to prevent conflicting configuration - Warns when
--sectionfiltering is used with JSON/YAML formats where section filtering may not apply semantically
This centralized validation prevents duplicate logic and ensures consistent flag behavior across commands.
Options Builder Pattern#
Command handlers transform flags into typed converter.Options structures via builder functions that encapsulate all configuration state. These builders follow a consistent precedence chain: CLI flags > config file values > defaults. This pattern ensures that user intentions expressed via CLI flags always take precedence, while configuration files provide convenient defaults.
Known Issue: Issue #412 documented a critical bug where buildDisplayOptions() failed to wire sharedIncludeTunables, causing the --include-tunables flag to be silently ignored. This issue exemplifies the importance of comprehensive flag wiring. The fix added opt.IncludeTunables = sharedIncludeTunables to complete the wiring, and established a testing pattern to prevent similar regressions.
Audit Handler Flow#
handleAuditMode() in cmd/audit_handler.go orchestrates audit execution for both the dedicated audit command and the legacy convert --audit-mode workflow. This shared handler ensures consistent audit behavior regardless of entry point:
- Creates
PluginManagerinstance, callsSetPluginDir()if custom plugin directory specified - Calls
InitializePlugins()to register built-in plugins (STIG, SANS, Firewall) and load dynamic.soplugins - Checks
GetLoadResult()and warns on failed dynamic plugin loads (non-fatal) - Runs
PluginManager.RunComplianceAudit()to execute compliance checks, producing anaudit.Report - Maps audit report to
common.ComplianceResultsviamapAuditReportToComplianceResults() - Assigns compliance results to
device.ComplianceCheckson a shallow device copy - Generates output through format handlers via the standard generator pipeline
This centralized handler prevents code duplication and ensures that audit behavior remains consistent across both entry points.
Error Handling Patterns#
All commands follow consistent error handling patterns that distinguish between fatal errors and recoverable warnings. Commands use fmt.Errorf("context: %w", err) wrapping throughout to provide error context while preserving the underlying error for inspection. PreRunE hooks catch invalid flag combinations before execution begins, providing fast feedback to users.
Fatal errors (e.g., file not found, parse failures) halt processing immediately. Conversion warnings (non-fatal issues like empty firewall rule fields, missing NAT data, gateway issues) are logged via structured logging and processing continues. These warnings can be suppressed with the --quiet flag when running in automated environments.
Rendering Behavior Differences#
The six commands exhibit different rendering behaviors based on their intended use cases. Commands optimized for terminal display use Glamour for styled markdown, while file-oriented commands write raw content:
| Context | Rendering |
|---|---|
display command → stdout | Always glamour-rendered, themed markdown (user-configurable theme) |
audit command → stdout, markdown format | Glamour-rendered styled markdown for enhanced terminal readability |
audit command → stdout, non-markdown format | Raw output (JSON/YAML/text/HTML written directly without styling) |
audit command → file output | Raw content (no glamour styling, optimized for file storage) |
convert command → stdout | Raw markdown output (no glamour styling) |
convert command → file | Raw output via format handler (all formats) |
list command → stdout | Raw text or JSON (no glamour styling, optimized for shell pipelines and automation) |
This distinction ensures that terminal-focused workflows benefit from enhanced readability, while file-based workflows produce clean, parseable output suitable for automation and version control.
Relevant Code Files#
The following table catalogs the primary implementation files for the CLI command architecture, including line counts and core responsibilities:
| File | Role | Lines |
|---|---|---|
cmd/audit.go | Audit command implementation: flags, PreRunE validation, runAudit, generateAuditOutput | ~400 |
cmd/audit_output.go | Audit output handling: emitAuditResult, deriveAuditOutputPath, path escaping | ~200 |
cmd/audit_handler.go | Shared audit orchestration: handleAuditMode(), mapAuditReportToComplianceResults() | ~150 |
cmd/convert.go | Convert command: buildConversionOptions(), format dispatch, overwrite protection | 803 |
cmd/display.go | Display command: buildDisplayOptions(), glamour rendering, theme selection | ~330 |
cmd/validate.go | Validate command: ParseAndValidate invocation, structured error formatting | ~220 |
cmd/sanitize.go | Sanitize command: sanitization mode selection, mapping file generation | ~320 |
cmd/list.go | List command group parent: emitList() helper, listEntry interface | ~93 |
cmd/list_plugins.go | Plugin enumeration: wraps audit.PluginRegistry with --plugin-dir support | ~122 |
cmd/list_devices.go | Device parser enumeration: wraps parser.DefaultRegistry() | ~72 |
cmd/list_formats.go | Format enumeration: wraps converter.DefaultRegistry | ~76 |
cmd/shared_flags.go | Shared infrastructure: flag variables, validateOutputFlags(), helper functions | ~400 |
cmd/root.go | Root command: global flags, parser registration via blank import | 376 |
Related Topics#
- Parser Factory Pattern: Registration and dispatch system for automatic device type detection via XML root element inspection (
<opnsense>vs<pfsense>) and multi-device parser registration - Converter Registry: Format handler registration and dispatch mechanism
- Compliance Plugin System: Pluggable security and compliance checks with STIG, SANS, and firewall frameworks
- CommonDevice Model: Shared internal representation of parsed firewall configurations (OPNsense and pfSense)
- Glamour Rendering: Terminal styling for markdown output in
displayandauditcommands