opnDossier System Architecture¶
Overview¶
opnDossier is a CLI-based multi-device firewall configuration processor designed with an offline-first, operator-focused architecture. Currently supports OPNsense and pfSense with an extensible architecture for additional device types. The system transforms complex XML configuration files into human-readable markdown documentation, following security-first principles and air-gap compatibility.

High-Level Architecture¶
Core Design Principles¶
- Offline-First: Zero external dependencies, complete air-gap compatibility, no runtime network calls
- Operator-Focused: Built for network administrators and operators, preserves operator control and visibility
- Framework-First: Leverages established Go libraries (Cobra, Charm ecosystem) before custom plumbing
- Structured Data: Maintains configuration hierarchy and relationships, prefers typed models over ad-hoc strings
- Security-First: No telemetry, input validation, secure processing, restrictive file permissions
- Polish Over Scale: Smaller, well-documented feature set with sane defaults over large inconsistent surface area
For the complete philosophical foundation and ethical constraints, see CONTRIBUTING.md Core Philosophy section.
Architecture Pattern¶
- Monolithic CLI Application with clear separation of concerns
- Single Binary Distribution for easy deployment
- Local Processing Only - no external network calls
- Streaming Data Pipeline from XML input to various output formats
Technology Stack¶
Built with modern Go practices and established libraries:
| Component | Technology |
|---|---|
| CLI Framework | Cobra |
| Configuration | Viper |
| CLI Enhancement | Charm Fang |
| Terminal Styling | Charm Lipgloss |
| Markdown Rendering | Charm Glamour |
| Markdown Generation | nao1215/markdown |
| XML Processing | Go's built-in encoding/xml |
| Structured Logging | Charm Log |
| Minimum Go Version | Go 1.26+ |
The CLI uses a layered architecture: Cobra provides command structure and argument parsing, Viper handles layered configuration management (files, env, flags) for opnDossier's own settings (CLI preferences, display options), and Fang adds enhanced UX features like styled help, automatic version flags, and shell completion. Note that Viper manages opnDossier configuration, while OPNsense config.xml parsing is handled separately by internal/cfgparser/.
Public Package Boundaries and Interface Injection¶
The pkg/internal/ Import Boundary¶
pkg/ packages must NEVER import internal/ packages. Any type exposed through a pkg/ struct field must itself live in pkg/ or stdlib. This enforces a strict architectural boundary that ensures external consumers can use the public API without encountering Go's internal/ access restrictions.
Key Principle: When moving types from internal/ to pkg/, audit all struct fields for leaked internal types and define public equivalents in pkg/ (e.g., pkg/model.Severity replaces internal/analysis.Severity in ConversionWarning).
Boundary Verification¶
Before committing changes to pkg/ packages, run this command to catch boundary violations:
This checks for any production code in pkg/ that imports internal/ packages. Test files (*_test.go) are allowed to import internal/ packages since Go's access restrictions only apply to external consumers.
Interface Injection Pattern¶
When pkg/ packages need functionality from internal/ packages, use interface injection instead of moving entire dependency chains:
- Define an interface in
pkg/with the required methods - Inject the concrete implementation at the
cmd/layer where bothpkg/andinternal/packages are accessible - Use the interface type in
pkg/package constructors and fields
Canonical Example: XMLDecoder¶
The pkg/parser.XMLDecoder interface demonstrates this pattern:
// pkg/parser/factory.go
type XMLDecoder interface {
Parse(ctx context.Context, r io.Reader) (*schema.OpnSenseDocument, error)
ParseAndValidate(ctx context.Context, r io.Reader) (*schema.OpnSenseDocument, error)
}
func NewFactory(decoder XMLDecoder) *Factory {
return &Factory{xmlDecoder: decoder, registry: DefaultRegistry()}
}
// NewFactoryWithRegistry allows test isolation with a custom registry.
func NewFactoryWithRegistry(decoder XMLDecoder, reg *DeviceParserRegistry) *Factory {
return &Factory{xmlDecoder: decoder, registry: reg}
}
Application code in cmd/ wires the concrete implementation:
This allows pkg/parser to use XML parsing functionality from internal/cfgparser without importing it directly.
Structural Typing for Sub-Packages¶
Go's structural typing allows pkg/ sub-packages to define their own interface that internal/ types satisfy without importing them. In PR #437, the OPNsense parser was refactored to use the exported parser.XMLDecoder interface directly instead of a local xmlDecoder interface. This change was made because:
- The
parser.XMLDecoderinterface is already exported in the public API - The local interface was redundant and added unnecessary indirection
- Using the exported interface enables better type safety and documentation
- It clarifies the dependency contract for external consumers
// pkg/parser/opnsense/parser.go
func NewParser(decoder parser.XMLDecoder) *Parser {
return &Parser{decoder: decoder}
}
The internal/cfgparser.XMLParser type satisfies the parser.XMLDecoder interface through structural compatibility, without requiring an explicit import of internal/cfgparser.
Unexporting Types Pattern¶
When making a type unexported (e.g., Converter → converter) to reduce API surface area, provide a convenience function for external test packages that cannot access unexported constructors:
// pkg/parser/opnsense/converter.go
type converter struct {
// unexported fields
}
// ConvertDocument provides public access for testing and external consumers
func ConvertDocument(doc *schema.OpnSenseDocument) (*common.CommonDevice, []common.ConversionWarning, error) {
c := &converter{}
return c.ToCommonDevice(doc)
}
This allows external test packages to use the conversion functionality without accessing the unexported converter type directly.
Related Documentation¶
For detailed examples and the historical context of fixing pkg/internal/ boundary violations, see:
For practical developer guidance on public package purity and the boundary verification command, see CONTRIBUTING.md Go Development Standards section.
Services and Components¶
1. CLI Interface Layer¶
- Framework: Cobra CLI
- Responsibility: Command parsing, user interaction, error handling, warning propagation
- Key Files:
cmd/root.go,cmd/convert.go,cmd/display.go,cmd/validate.go,cmd/audit.go,cmd/audit_output.go - Warning Handling: All commands log conversion warnings via structured logging; warnings suppressed when
--quietflag is used - File Organization: Audit command split into two files following file-size guidelines:
audit.go— Command definition, flags,PreRunEvalidation,runAudit, andgenerateAuditOutputaudit_output.go— Output emission logic (emitAuditResult), path derivation (deriveAuditOutputPath), and segment escaping (escapePathSegment)
2. Configuration Management¶
- Framework: spf13/viper
- Sources: CLI flags > Environment variables > Config file > Defaults
- Format: YAML configuration files
- Precedence: Standard order where environment variables override config files for deployment flexibility
3. Analysis Infrastructure¶
- Package:
internal/analysis/ - Responsibility: Shared analysis logic and canonical finding types for converter, processor, audit, and compliance packages
- Key Types:
Findingstruct,Severitytype with validation helpers - Shared Functions:
ComputeStatistics()- Statistics computation for configuration items, services, and security featuresComputeAnalysis()- Detection logic for dead rules, unused interfaces, security, performance, and consistency issuesDetectDeadRules()- Dead rule detection with structuredKindfield ("unreachable"or"duplicate"). Uses typed constants for rule type comparisons (e.g.,rule.Type == common.RuleTypeBlock)DetectUnusedInterfaces()- Unused interface detection across rules, DHCP, DNS, VPN, and load balancerRulesEquivalent()- Rule comparison includingDisabledfield and normalized interface order- Defensive API: All exported
Compute*functions include nil guards for safe use with nil arguments - Export Model:
ComplianceResults,ComplianceFinding,PluginComplianceResult,ComplianceControl,ComplianceResultSummary,CompliancePluginInfo,ComplianceAttackSurfaceinpkg/model/enrichment.go - Purpose: Eliminates duplicated detection and statistics logic, ensures consistency across all analysis-related packages. Analysis code uses typed enum constants instead of string literals, providing compile-time safety for rule type checks and security severity levels
- Usage: Also used in
ConversionWarningtype for severity classification of non-fatal conversion issues
4. Data Processing Engine¶
Device Parser Registry¶
- Package:
pkg/parser/ - Pattern: Self-registration via
init()+ blank imports (mirrorsdatabase/sqldriver pattern) - Key Types:
DeviceParserRegistry,ConstructorFunc,DeviceParserinterface - Singleton:
parser.DefaultRegistry()returns the global registry;parser.NewDeviceParserRegistry()for test isolation - Registration: Each parser package calls
parser.Register("rootElement", factory)frominit() - Dispatch:
Factory.CreateDevice()auto-detects device type from the XML root element via registry lookup, or accepts an explicit--device-typeoverride - Built-in: OPNsense parser self-registers in
pkg/parser/opnsense/parser.go - Extensibility: External parsers register via blank import in the consumer binary (see Plugin Development Guide)
- Blank Import Requirement:
cmd/root.go(and test files usingparser.NewFactory()) must import both device parsers to trigger registration:
XML Parser Component¶
- Technology: Go's built-in
encoding/xml - Input: OPNsense and pfSense config.xml files
- Output: Structured Go data types
- Features: Schema validation, error reporting, automatic charset conversion (UTF-8, US-ASCII, ISO-8859-1, Windows-1252)
- Shared Security Hardening:
pkg/parser/xmlutil.goprovidesNewSecureXMLDecoder()andCharsetReader()for XXE protection, input size limits, and charset handling used by both OPNsense and pfSense parsers
Data Converter Component¶
- Input: Parsed XML structures
- Output: Markdown content, conversion warnings
- Features: Hierarchy preservation, metadata injection, non-fatal issue tracking
- Warning Generation: Accumulates conversion warnings for incomplete or problematic configuration elements (empty firewall rule fields, missing NAT rule data, gateway issues, user/certificate problems, HA configuration warnings)
- Analysis Integration: Delegates to
internal/analysis/forComputeStatistics()andComputeAnalysis()(shared, not mirrored) - Audit Report Rendering: Delegates compliance audit report rendering to
internal/converter/builder/viaBuildAuditSection()andWriteAuditSection()methods - Audit Mode Integration: In audit mode,
cmd/audit_handler.gomapsaudit.Reporttocommon.ComplianceResultsand populates theComplianceChecksfield on a shallow copy ofCommonDevice, enabling multi-format output (markdown, JSON, YAML, text, HTML) through the standard generation pipeline
Output Renderer Component¶
- Formats: Markdown, JSON, YAML, plain text, HTML (registered as handlers in
DefaultRegistry) - Format Dispatch:
FormatRegistrypattern provides centralized format metadata and handler dispatch - Technologies: Charm Lipgloss (styling) + Charm Glamour (rendering)
- Format Registration:
DefaultRegistrymanages format names, aliases (txt, htm, md, yml), file extensions, and validation
5. Output Systems¶
- Terminal Display: Syntax-highlighted, styled terminal output via
displaycommand andauditcommand (glamour rendering for markdown to stdout) - File Export: Multi-format file generation (markdown, JSON, YAML, text, HTML)
- Multi-File Audit Output: Auto-naming with lossless tilde-based path escaping prevents filename collisions (e.g.,
prod/site-a/config.xml→prod_site-a_config-audit.md)
Data Model Architecture¶
opnDossier uses a hierarchical model structure that mirrors the OPNsense XML configuration while organizing functionality into logical domains:
graph TB
subgraph "Root Configuration"
ROOT[Opnsense Root]
META[Metadata & Global Settings]
end
subgraph "System Domain"
SYS[System Configuration]
USERS[User Management]
GROUPS[Group Management]
SYSCFG[System Services Config]
end
subgraph "Network Domain"
NET[Network Configuration]
IFACES[Interface Management]
ROUTING[Routing & Gateways]
VLAN[VLAN Configuration]
end
subgraph "Security Domain"
SEC[Security Configuration]
FIREWALL[Firewall Rules]
NAT[NAT Configuration]
VPN[VPN Services]
CERTS[Certificate Management]
end
subgraph "Services Domain"
SVC[Services Configuration]
DNS[DNS Services]
DHCP[DHCP Services]
MONITOR[Monitoring Services]
WEB[Web Services]
end
ROOT --> META
ROOT --> SYS
ROOT --> NET
ROOT --> SEC
ROOT --> SVC
SYS --> USERS
SYS --> GROUPS
SYS --> SYSCFG
NET --> IFACES
NET --> ROUTING
NET --> VLAN
SEC --> FIREWALL
SEC --> NAT
SEC --> VPN
SEC --> CERTS
SVC --> DNS
SVC --> DHCP
SVC --> MONITOR
SVC --> WEB
This hierarchical structure provides:
- Logical Organization: Related configuration grouped by functional domain
- Maintainability: Easier to locate and modify specific configuration types
- Extensibility: New features can be added to appropriate domains
- Validation: Domain-specific validation rules improve data integrity
- API Evolution: JSON tags enable better REST API integration
- Compliance Data:
ComplianceResultsfield (formerlyComplianceChecks) is a rich nested structure containingMode,Findings,PluginResultsmap with per-pluginPluginComplianceResultinstances,Summary, andMetadata
Type Safety with Enums¶
The model package enforces type safety through typed string enums for configuration domains where arbitrary string values historically led to validation and refactoring challenges:
Firewall Rule Types¶
type FirewallRuleType string
const (
RuleTypePass FirewallRuleType = "pass" // Allow traffic
RuleTypeBlock FirewallRuleType = "block" // Silently drop traffic
RuleTypeReject FirewallRuleType = "reject" // Drop and send rejection
)
NAT Configuration¶
type NATOutboundMode string
const (
OutboundAutomatic NATOutboundMode = "automatic" // Automatic rules
OutboundHybrid NATOutboundMode = "hybrid" // Mixed auto/manual
OutboundAdvanced NATOutboundMode = "advanced" // Manual only
OutboundDisabled NATOutboundMode = "disabled" // NAT disabled
)
Network Configuration¶
type IPProtocol string
const (
IPProtocolInet IPProtocol = "inet" // IPv4
IPProtocolInet6 IPProtocol = "inet6" // IPv6
)
type FirewallDirection string
const (
DirectionIn FirewallDirection = "in" // Inbound traffic
DirectionOut FirewallDirection = "out" // Outbound traffic
DirectionAny FirewallDirection = "any" // Bidirectional
)
type LAGGProtocol string
const (
LAGGProtocolLACP LAGGProtocol = "lacp" // IEEE 802.3ad
LAGGProtocolFailover LAGGProtocol = "failover" // Active/standby
LAGGProtocolLoadBalance LAGGProtocol = "loadbalance" // Hash-based
LAGGProtocolRoundRobin LAGGProtocol = "roundrobin" // Round-robin
)
type VIPMode string
const (
VIPModeCarp VIPMode = "carp" // CARP failover
VIPModeIPAlias VIPMode = "ipalias" // IP alias
VIPModeProxyARP VIPMode = "proxyarp" // ARP proxy
)
Benefits of Typed Enums¶
- Compile-Time Safety: Type system prevents invalid assignments like
rule.Type = "invalid"— compiler enforces valid constants - Refactoring Support: IDE rename operations update all references across 70 files without grep-based search/replace
- Documentation: Enum constants provide inline documentation at usage sites (
RuleTypePassis self-documenting vs"pass") - Autocomplete: IDEs offer completion suggestions for valid enum values
- Magic String Elimination: No bare string literals like
"pass","block","reject"scattered across analysis, diff, converter, and plugin packages
Multi-Device Model Layer Architecture¶
opnDossier separates XML-specific DTOs from the domain model consumed by all downstream components. This enables support for multiple device types (OPNsense and pfSense today, Cisco ASA in the future) behind a single CommonDevice abstraction.
graph TD
A["pkg/schema/opnsense/ — XML DTOs (OPNsense-shaped structs)"]
B["pkg/parser/opnsense/ — OPNsense parser + converter"]
C["pkg/schema/pfsense/ — XML DTOs (pfSense-shaped structs)"]
D["pkg/parser/pfsense/ — pfSense parser + converter"]
E["pkg/model/ — CommonDevice domain model"]
F["internal/analysis/ — Canonical Finding + Severity types"]
G["Consumers: processor / converter / markdown / audit / diff / plugins"]
A --> B
C --> D
B --> E
D --> E
E --> G
F --> G
Layer Responsibilities¶
pkg/schema/opnsense/— XML DTO layer. Carriesxml:""tags and mirrors the OPNsense config.xml structure. This layer is untouched by downstream consumers.pkg/parser/opnsense/— Containsparser.goandconverter.go. Reads schema DTOs and emits*common.CommonDevicewith conversion warnings. Converts OPNsense XML string values to typed enum constants (e.g.,"pass"→common.RuleTypePass,"automatic"→common.OutboundAutomatic). This is the only package that importspkg/schema/opnsense/.pkg/schema/pfsense/— XML DTO layer for pfSense. Follows copy-on-write pattern: reuses OPNsense types where XML structures are identical (e.g.,Interface,Destination,Source), forks locally at divergence points (e.g.,InboundRuleuses<target>instead of<internalip>,FilterRuleadds pfSense-specific fields likeID,Tag,OS,AssociatedRuleID). Documented inpkg/schema/pfsense/README.md.pkg/parser/pfsense/— Containsparser.go,converter.go, and subsystem converters. Manages its own XML decoding viaparser.NewSecureXMLDecoder()(pfSense parser doesn't useinternal/cfgparser.NewXMLParser()because the sharedXMLDecoderinterface returns*schema.OpnSenseDocument). Emits*common.CommonDevicewith conversion warnings.pkg/model/— Device-agnostic domain model. No XML tags. Defines typed string enums for firewall rules (RuleType,Direction,IPProtocol), NAT configurations (OutboundMode), and network elements (LAGGProtocol,VIPMode). All consumer code (processor, converter, markdown, audit, diff, compliance plugins) operates onCommonDevice. IncludesConversionWarningtype for non-fatal issues andComplianceResultstype (with nestedComplianceFinding,PluginComplianceResult,ComplianceControl,ComplianceResultSummary,CompliancePluginInfo,ComplianceAttackSurface) for compliance audit data representation. AddsDeviceType.DisplayName()method for dynamic report headers (e.g., "OPNsense" vs "pfSense").internal/analysis/— Shared analysis logic and canonical finding types. Provides detection functions (DetectDeadRules,DetectUnusedInterfaces,DetectSecurityIssues,DetectPerformanceIssues,DetectConsistency), statistics computation (ComputeStatistics), analysis aggregation (ComputeAnalysis), and rule comparison (RulesEquivalent). Uses typed constants for rule type comparisons (e.g.,rule.Type == common.RuleTypeBlock) instead of string literals. Used by bothinternal/converterandinternal/processorto eliminate duplicated logic.pkg/parser/factory.go—FactoryandDeviceParserinterface. Uses theDeviceParserRegistryfor device type dispatch. Auto-detects the device type from the XML root element or uses the--device-typeflag to bypass auto-detection. Returns 3 values: device model, warnings slice, and error.
Schema Reuse Pattern¶
pfSense schema follows a copy-on-write approach to minimize duplication:
- Reuse OPNsense types when XML structure is identical (e.g.,
opnsense.Interface,opnsense.Source,opnsense.Destination,opnsense.Outbound,opnsense.SSHConfig) - Fork locally when pfSense diverges (e.g.,
InboundRulefor<target>vs<internalip>,Groupfor[]string Privvs single privilege,Systemfor[]string DNSServersvs single server,FilterRulefor pfSense-specific fields) - Document differences in
pkg/schema/pfsense/README.mdwith complete structural reference covering 50+ top-level sections
pfSense-Specific Types¶
Key pfSense types that differ from OPNsense:
InboundRule— NAT port forward rule using<target>field instead of OPNsense's<internalip>FilterRule— Firewall rule with pfSense-specific fields:ID,Tag,Tagged,OS,AssociatedRuleID,MaxSrcStates, plus additional rate-limiting and state fieldsGroup— Group with[]string Privarray (per-group privileges) instead of OPNsense's single privilege modelSystem— System config with[]string DNSServers(repeating<dnsserver>elements) instead of single DNS server stringUser— User account withBcryptHashfield instead of OPNsense'sPasswordfield (SHA-based)
Parser Independence¶
The pfSense parser operates independently from the OPNsense parser:
- Self-contained XML decoding: Uses
parser.NewSecureXMLDecoder()directly instead ofinternal/cfgparser.NewXMLParser()because the sharedXMLDecoderinterface is typed to return*schema.OpnSenseDocument - Shared security hardening: Both parsers use the same
NewSecureXMLDecoder()andCharsetReader()frompkg/parser/xmlutil.gofor XXE protection, input size limits, and charset handling (UTF-8, US-ASCII, ISO-8859-1, Windows-1252) - Registry-based registration: Self-registers via
init()inpkg/parser/pfsense/parser.goto handle<pfsense>root elements
Device Type Detection¶
The --device-type flag is exposed on all config-reading commands (convert, display, audit, diff, validate). When specified, it bypasses auto-detection and validates against the parser registry; error messages dynamically list supported devices from registry.List(). When omitted, parser.Factory inspects the root XML element to select the correct parser from the registry.
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)
- Valid plugin names (stig, sans, firewall)
--pluginsflag only accepted with--mode blue(compliance checks only run in blue mode)--outputflag rejected when auditing multiple files (prevents output clobbering)
-
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
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)
DeviceParser Registry Pattern¶
opnDossier uses a pluggable DeviceParser registry that enables external Go projects to register custom device parsers at compile time. This pattern follows the database/sql driver registration model, replacing hardcoded switch statements with a thread-safe registry.
Registry Architecture¶
// ConstructorFunc is the factory function signature for creating DeviceParser instances
type ConstructorFunc = func(XMLDecoder) DeviceParser
// DeviceParserRegistry manages registered DeviceParser constructors
type DeviceParserRegistry struct {
mu sync.RWMutex
parsers map[string]ConstructorFunc
}
Key Components¶
1. Thread-Safe Operations¶
The registry uses sync.RWMutex for concurrent access:
Register(deviceType, fn)— Registers a parser constructor (panics on duplicates, nil functions, or empty device types)Get(deviceType)— Returns(ConstructorFunc, bool)for thread-safe lookups with nil guardsList()— Returns sorted slice of registered device type names
2. Self-Registration via init()¶
Parser packages register themselves using init() functions:
// pkg/parser/opnsense/parser.go
func NewParserFactory(decoder parser.XMLDecoder) parser.DeviceParser {
return NewParser(decoder)
}
func init() {
parser.Register("opnsense", NewParserFactory)
}
3. CRITICAL: Blank Import Requirement¶
All code using parser.NewFactory() MUST include blank imports for parser packages to ensure init() functions execute:
import (
_ "github.com/EvilBit-Labs/opnDossier/pkg/parser/opnsense" // Register OPNsense parser
_ "github.com/EvilBit-Labs/opnDossier/pkg/parser/pfsense" // Register pfSense parser
)
Without these blank imports, the parsers never register and the factory has no parsers available. This gotcha is documented in GOTCHAS.md §7.1 and affects:
cmd/root.go— CLI entry point- All test files using
parser.NewFactory()orparser.DefaultRegistry()
4. Factory Integration¶
factory.go uses registry-based dispatch instead of hardcoded switch statements:
func (f *Factory) createWithOverride(ctx context.Context, r io.Reader, override string, validateMode bool) (*common.CommonDevice, []common.ConversionWarning, error) {
fn, ok := f.registry.Get(override)
if !ok {
return nil, nil, fmt.Errorf(
"unsupported device type override: %s; supported: %s",
override, strings.Join(f.registry.List(), ", "),
)
}
return parseDevice(ctx, fn(f.xmlDecoder), r, validateMode)
}
Error messages dynamically list supported devices from registry.List(), eliminating hardcoded device type strings.
5. Test Isolation with NewFactoryWithRegistry()¶
Tests requiring isolated registry state use NewFactoryWithRegistry():
func TestCustomParser(t *testing.T) {
reg := parser.NewDeviceParserRegistry()
reg.Register("testdevice", testParserFactory)
factory := parser.NewFactoryWithRegistry(mockDecoder, reg)
// Test without polluting global registry
}
CLI Integration¶
cmd/shared_flags.go functions derive device type lists dynamically from parser.DefaultRegistry():
ValidDeviceTypes()— Shell completion usingregistry.List()validateDeviceType()— Validation usingregistry.Get()with dynamic error messagesresolveDeviceType()— Type-safe device type resolution that converts the raw--device-typeflag value into acommon.DeviceTypeenum constant for built-in types (opnsense, pfsense) or falls back to casting the normalized registry key for third-party parsers
The resolveDeviceType() function replaces the previous sharedDeviceType string pattern, providing compile-time safety for built-in device types while maintaining extensibility for externally registered parsers. This approach eliminates hardcoded "opnsense" strings with registry queries, enabling automatic CLI support for new parsers via self-registration.
Benefits¶
- Compile-Time Extensibility: External projects register parsers via blank imports
- Zero Hardcoded Strings: Device types discovered from registry at runtime
- Thread-Safe: Concurrent access protected by RWMutex
- Test Isolation: Custom registries prevent global state pollution
- Dynamic Error Messages: Supported device lists always accurate
Related Documentation¶
For complete implementation details, error-handling patterns, and gotchas, see:
- docs/solutions/architecture-issues/pluggable-deviceparser-registry-pattern.md
- GOTCHAS.md §7.1 — Blank import requirement
For practical developer guidance on the DeviceParser registry pattern and blank import footgun, see CONTRIBUTING.md Go Development Standards section.
Data Flow Architecture¶
The data processing pipeline follows a clear multi-stage architecture documented in CONTRIBUTING.md Data Processing Pipeline section:
- Ingestion: Device-specific parsers parse configuration files → schema documents
- OPNsense:
internal/cfgparser/parsesconfig.xml→pkg/schema/opnsense.OpnSenseDocument - pfSense:
pkg/parser/pfsense/parser.goparsesconfig.xml→pkg/schema/pfsense.Document - Conversion: Device-specific converters transform schema documents →
pkg/model.CommonDevicewith conversion warnings - OPNsense:
pkg/parser/opnsense/transformsOpnSenseDocument→CommonDevice - pfSense:
pkg/parser/pfsense/transformsDocument→CommonDevice - XML string values are converted to typed enum constants (e.g.,
rule.TypeXML string"pass"becomescommon.RuleTypePass) - Export Enrichment:
internal/converter/enrichment.gopopulates statistics, analysis, security assessment viaprepareForExport() - Export: Registry-driven multi-format output (markdown, json, yaml, text, html) via
FormatRegistry. Typed enums serialize back to string values during JSON/YAML marshaling (e.g.,common.RuleTypePass→"pass") - Report Generation: Audience-aware reports built through
builder.MarkdownBuilderwith dynamic headers usingDeviceType.DisplayName()(e.g., "OPNsense Configuration Summary" vs "pfSense Configuration Summary")
sequenceDiagram
participant User
participant CLI
participant ConfigMgr as Config Manager
participant Parser as XML Parser
participant Converter
participant Registry as FormatRegistry
participant Renderer
participant Output
User->>CLI: opndossier convert config.xml
CLI->>ConfigMgr: Load configuration
ConfigMgr-->>CLI: Configuration object
CLI->>Parser: Parse XML file
alt Valid XML
Parser->>Parser: Validate structure
Parser->>Converter: Transform data
Note over Converter: All findings use<br/>canonical analysis.Finding<br/>Warnings collected via addWarning()
Converter-->>Parser: Structured data + warnings
Parser-->>CLI: Device model + warnings + nil error
CLI->>CLI: Log warnings (respects --quiet flag)
CLI->>Registry: Get handler for format
Registry-->>CLI: FormatHandler
CLI->>Renderer: Generate via handler
alt Terminal display
Renderer->>Output: Styled terminal
Output-->>User: Visual output
else File export
Renderer->>Output: Write file
Output-->>User: Confirmation
end
else Invalid XML
Parser-->>CLI: nil + nil + error details
CLI-->>User: Error message
end
Note on Format Dispatch: The Renderer component uses the FormatRegistry for format dispatch rather than switch statements. DefaultRegistry manages all format metadata (names, aliases, extensions) and provides FormatHandler implementations for centralized format handling.
Programmatic Generation Architecture¶
Core Architecture¶
opnDossier uses programmatic markdown generation via the MarkdownBuilder component, delivering high performance, type safety, and enhanced developer experience.
sequenceDiagram
participant User
participant CLI
participant Parser as XML Parser
participant Builder as MarkdownBuilder
participant Methods as Go Methods
participant Renderer
participant Output
User->>CLI: opndossier convert config.xml
CLI->>Parser: Parse XML file
Parser-->>CLI: Structured data
CLI->>Builder: Create builder instance
Builder->>Methods: Direct method calls
Methods->>Methods: Type-safe operations
Methods-->>Builder: Structured content
Builder->>Renderer: Optimized string building
Renderer->>Output: Final markdown
Output-->>User: Generated report
Key Architectural Features¶
1. Performance Optimizations¶
The programmatic approach delivers significant performance improvements:
- Memory Usage: Reduced allocations through direct string building
- Generation Speed: Fast generation via method-based approach
- Throughput: High reports per second
- Scalability: Consistent performance across all dataset sizes
Performance can be measured using benchmarks in internal/converter/markdown_bench_test.go.
2. Type Safety¶
graph TB
subgraph "Programmatic Generation"
P1[Go Methods] --> P2[Compile-time Validation]
P2 --> P3[Type-safe Operations]
P3 --> P4[Explicit Error Handling]
P4 --> P5[Structured Results]
end
style P2 fill:#99ff99
style P3 fill:#99ff99
style P4 fill:#99ff99
3. Security Enhancements (Red Team Focus)¶
- Output Obfuscation: Built-in capabilities for sensitive data handling
- Complete Offline Support: No external dependencies
- Memory Safety: Improved handling of large configurations
- Error Isolation: Structured error handling prevents information leakage
MarkdownBuilder Component Architecture¶
The ReportBuilder interface follows the Interface Segregation Principle (SOLID), composing three focused sub-interfaces that were split from the original monolithic interface in PR #431 (issue #323):
SectionBuilder(9 methods): Build*Section methods for rendering individual configuration domainsTableWriter(11 methods): Write*Table methods for formatting data tablesReportComposer(3 methods): SetIncludeTunables, BuildStandardReport, and BuildComprehensiveReport
This composition provides full backward compatibility—existing code using ReportBuilder continues to work unchanged—while enabling consumers to depend only on the methods they actually use.
classDiagram
class SectionBuilder {
<<interface>>
+BuildSystemSection(data) string
+BuildNetworkSection(data) string
+BuildSecuritySection(data) string
+BuildServicesSection(data) string
+BuildIPsecSection(data) string
+BuildOpenVPNSection(data) string
+BuildHASection(data) string
+BuildIDSSection(data) string
+BuildAuditSection(data) string
}
class TableWriter {
<<interface>>
+WriteFirewallRulesTable(md, rules) *Markdown
+WriteInterfaceTable(md, interfaces) *Markdown
+WriteUserTable(md, users) *Markdown
+WriteGroupTable(md, groups) *Markdown
+WriteSysctlTable(md, sysctl) *Markdown
+WriteOutboundNATTable(md, rules) *Markdown
+WriteInboundNATTable(md, rules) *Markdown
+WriteVLANTable(md, vlans) *Markdown
+WriteStaticRoutesTable(md, routes) *Markdown
+WriteDHCPSummaryTable(md, scopes) *Markdown
+WriteDHCPStaticLeasesTable(md, leases) *Markdown
}
class ReportComposer {
<<interface>>
+SetIncludeTunables(v bool)
+BuildStandardReport(data) (string, error)
+BuildComprehensiveReport(data) (string, error)
}
class ReportBuilder {
<<interface>>
}
class MarkdownBuilder {
-device *common.CommonDevice
-options BuildOptions
-logger *Logger
+CalculateSecurityScore(data) int
+AssessRiskLevel(severity) string
+FilterSystemTunables(tunables, filter) []SysctlItem
+GroupServicesByStatus(services) map[string][]Service
+FormatInterfaceLinks(interfaces) string
+EscapeMarkdownSpecialChars(input) string
}
class SecurityAssessor {
+CalculateSecurityScore(data) int
+AssessRiskLevel(severity) string
+AssessServiceRisk(service) string
+DetermineSecurityZone(interface) string
}
class DataTransformer {
+FilterSystemTunables(tunables, filter) []SysctlItem
+GroupServicesByStatus(services) map[string][]Service
+FormatSystemStats(data) map[string]interface{}
}
class StringFormatter {
+EscapeMarkdownSpecialChars(input) string
+FormatTimestamp(timestamp) string
+TruncateDescription(text, length) string
+FormatBoolean(value) string
}
ReportBuilder *-- SectionBuilder : composes
ReportBuilder *-- TableWriter : composes
ReportBuilder *-- ReportComposer : composes
ReportBuilder <|.. MarkdownBuilder : implements
MarkdownBuilder o-- SecurityAssessor
MarkdownBuilder o-- DataTransformer
MarkdownBuilder o-- StringFormatter
Consumer-Local Interface Narrowing¶
HybridGenerator demonstrates the consumer-local interface narrowing pattern (documented in AGENTS.md §5.9a). It defines a private reportGenerator interface that exposes only the four methods it directly calls:
SetIncludeTunables,BuildAuditSection,BuildStandardReport, andBuildComprehensiveReport-- all listed directly, not via embedded sub-interfaces
The HybridGenerator.builder field is typed as this narrower reportGenerator interface internally. Public methods (SetBuilder, GetBuilder) continue to accept and return the full ReportBuilder interface, maintaining backward compatibility. The GetBuilder method uses a two-value type assertion to recover the full interface when needed.
FormatRegistry Integration¶
HybridGenerator delegates format-specific generation to FormatHandler implementations retrieved from DefaultRegistry (documented in AGENTS.md §5.9b). The handlerForFormat() helper function resolves the format string to a handler via the registry; format defaulting (to markdown) is handled earlier via DefaultOptions / CLI configuration, so handlerForFormat() expects a non-empty, registered format string. Each handler implements:
FileExtension()- Returns the file extension for the format (e.g., ".md", ".json")Aliases()- Returns alternative format names (e.g., "md" for markdown, "yml" for yaml)Generate()- Creates documentation as a string via the generatorGenerateToWriter()- Streams documentation directly to an io.Writer
Handler dispatch replaces the previous switch statement approach, enabling centralized format metadata management and simplified addition of new formats through DefaultRegistry registration.
Data Flow Pipeline (Programmatic Mode)¶
graph TD
subgraph "Input Processing"
XML[OPNsense XML] --> Parser[Enhanced Parser]
Parser --> Model[Structured Model]
end
subgraph "Programmatic Generation Engine"
Model --> Builder[MarkdownBuilder]
Builder --> Security[SecurityAssessor]
Builder --> Transform[DataTransformer]
Builder --> Format[StringFormatter]
Security --> Methods[Method-Based Generation]
Transform --> Methods
Format --> Methods
end
subgraph "Output Optimization"
Methods --> StringBuild[Optimized String Building]
StringBuild --> Render[Direct Rendering]
Render --> Output[Markdown Output]
end
subgraph "Performance Characteristics"
Metrics[Performance Metrics<br/>• Faster generation<br/>• Reduced memory<br/>• Increased throughput<br/>• Type-safe operations]
end
Output -.-> Metrics
style Builder fill:#99ff99,stroke:#333,stroke-width:4px
style Methods fill:#99ff99,stroke:#333,stroke-width:2px
style StringBuild fill:#99ff99,stroke:#333,stroke-width:2px
Method Categories and Performance¶
Security Assessment Methods¶
- CalculateSecurityScore: 1.59M operations/sec
- AssessRiskLevel: 92M operations/sec
- AssessServiceRisk: High-frequency assessment capability
Data Transformation Methods¶
- FilterSystemTunables: 797K operations/sec
- GroupServicesByStatus: 1.01M operations/sec
- FormatSystemStats: Optimized for large datasets
String Utility Methods¶
- EscapeMarkdownSpecialChars: Ultra-fast character processing
- FormatTimestamp: Efficient time formatting
- TruncateDescription: Word-boundary aware truncation
Section Builders¶
- BuildSystemSection: 1.7K operations/sec (comprehensive sections)
- BuildNetworkSection: 6.7K operations/sec
- BuildSecuritySection: 5.1K operations/sec
- BuildServicesSection: 13K operations/sec
- BuildAuditSection: Renders compliance audit sections including summary, plugin results, findings tables, and metadata
Memory Management Architecture¶
graph LR
subgraph "Programmatic Generation"
P1[Direct Methods] --> P2[Structured Building]
P2 --> P3[Pre-allocated Buffers]
P3 --> P4[Optimized Strings]
P4 --> P5[Efficient Memory]
P5 --> P6[Minimal Allocations]
end
style P5 fill:#99ff99
style P6 fill:#99ff99
Error Handling Architecture¶
// Structured error types
type ValidationError struct {
Field string
Value any
Message string
}
type GenerationError struct {
Component string
Operation string
Cause error
}
// Context-aware error handling
func (b *MarkdownBuilder) BuildSection(device *common.CommonDevice) (string, error) {
if err := b.validateInput(data); err != nil {
return "", &ValidationError{
Field: "input_data",
Value: data,
Message: fmt.Sprintf("invalid input: %v", err),
}
}
result, err := b.generateContent(data)
if err != nil {
return "", &GenerationError{
Component: "section_builder",
Operation: "content_generation",
Cause: err,
}
}
return result, nil
}
Modular Report Generator Architecture¶
Design Principles¶
Report generators in opnDossier follow a modular, self-contained architecture designed to support:
- Build-time feature selection via Go build flags
- Pro-level features through optional modules
- Independent development of report types
- Clean separation between shared infrastructure and report-specific logic
Module Structure¶
Each report generator should be a self-contained module with its own:
- Generation logic - All markdown/output construction
- Calculation logic - Security scoring, risk assessment, statistics
- Data transformations - Report-specific data processing
- Constants and mappings - Report-specific configuration
graph TB
subgraph "Shared Infrastructure"
Model[common.CommonDevice]
Helpers[Shared Helpers<br/>• String formatting<br/>• Markdown escaping<br/>• Table building]
end
subgraph "Report Generator Modules"
Conversion[Conversion Report<br/>• Generation logic<br/>• Calculations<br/>• Transformations]
Blue[Blue Team Report<br/>• Compliance checks<br/>• Security findings<br/>• Risk assessment]
Red[Red Team Report<br/>• Attack surface<br/>• Enumeration data<br/>• Pivot analysis]
Pro[Pro Reports<br/>• Advanced analytics<br/>• Custom formats<br/>• Enterprise features]
end
Model --> Conversion
Model --> Blue
Model --> Red
Model --> Pro
Helpers --> Conversion
Helpers --> Blue
Helpers --> Red
Helpers --> Pro
style Pro fill:#ffd700,stroke:#333,stroke-width:2px
Build Flag Integration¶
Report generators can be conditionally included using Go build tags:
This enables:
- Core builds with conversion and audit report types
- Pro builds with additional enterprise features
- Custom builds with specific report combinations
Implementation Guidelines¶
What Each Report Module Should Contain¶
Report modules are self-contained packages. Currently, report generation lives in internal/converter/builder/ and internal/converter/formatters/. As the system evolves to support Pro-level features, each report type may be extracted to its own package following this structure:
internal/converter/<report-type>/
├── generator.go # Main generation logic
├── calculations.go # Report-specific calculations
├── transformers.go # Data transformation functions
├── constants.go # Report-specific constants
└── <report-type>_test.go
What Should Remain Shared¶
common.CommonDevice- The parsed device-agnostic configuration modelanalysis.Finding- Canonical finding type for all analysis results- String helpers - Markdown escaping, formatting utilities
- Table builders - Generic markdown table construction
- Common interfaces -
ReportBuilder,Generatorinterfaces
Example Module Structure¶
// internal/reports/blueteam/generator.go
package blueteam
import (
"github.com/EvilBit-Labs/opnDossier/internal/analysis"
common "github.com/EvilBit-Labs/opnDossier/pkg/model"
"github.com/EvilBit-Labs/opnDossier/internal/converter/formatters"
)
type BlueTeamGenerator struct {
// All state and configuration for blue team reports
}
func (g *BlueTeamGenerator) Generate(device *common.CommonDevice) (string, error) {
// Self-contained generation using only model and helpers
score := g.calculateSecurityScore(device)
findings := g.analyzeCompliance(device)
return g.buildReport(device, score, findings)
}
// All calculation logic is internal to this module
func (g *BlueTeamGenerator) calculateSecurityScore(device *common.CommonDevice) int {
// Blue team specific scoring algorithm
}
// All findings returned use the canonical analysis.Finding type
func (g *BlueTeamGenerator) analyzeCompliance(device *common.CommonDevice) []analysis.Finding {
// Compliance analysis returning standardized findings
}
Benefits¶
- Independent Testing - Each report module can be tested in isolation
- Feature Gating - Pro features excluded from core builds
- Reduced Coupling - Changes to one report type don't affect others
- Clear Ownership - Each module has defined boundaries
- Extensibility - New report types added without modifying core
Audit-to-Export Mapping¶
The cmd/audit_handler.go module contains mapAuditReportToComplianceResults(), which converts the internal audit.Report structure into the export model common.ComplianceResults. This mapping enables multi-format output (markdown, JSON, YAML, text, HTML) for compliance audit data through the standard generation pipeline.
Mapping Process¶
- Top-level findings: Converts
audit.Findinginstances (which embedanalysis.Finding) tocommon.ComplianceFindinginstances, preservingAttackSurface,ExploitNotes, andControlfields - Per-plugin results: Maps each
audit.ComplianceResultin thereport.Compliancemap tocommon.PluginComplianceResult, including: - Plugin metadata (
PluginInfo) - Plugin-specific findings
- Summary statistics (
ComplianceResultSummary) - Control definitions (
ComplianceControl) - Per-control compliance status (boolean map)
- Aggregate summary: Computes summary statistics across all plugins and direct findings, including total/critical/high/medium/low counts and compliant/non-compliant control counts
- Metadata preservation: Clones the audit metadata map
Integration with Builder Layer¶
Once the mapping is complete, handleAuditMode() creates a shallow copy of the CommonDevice and populates its ComplianceChecks field with the mapped ComplianceResults. This enriched device is then passed to generateWithProgrammaticGenerator(), which delegates to the appropriate format handler via FormatRegistry. For markdown, BuildAuditSection() renders compliance sections; for JSON/YAML/text/HTML, the ComplianceChecks field is serialized directly or formatted according to the target format.
Data Storage Strategy¶
Local File System¶
- Configuration:
~/.opnDossier.yaml(user preferences) - Input: OPNsense XML files (any location)
- Output: Markdown files (user-specified or current directory)
Memory Management¶
- Structured Data: Go structs with XML/JSON tags
- Large Files: Streaming processing for memory efficiency
- Type Safety: Strong typing throughout the pipeline
No Persistent Storage¶
- Stateless Operation: Each run is independent
- No Database: All data flows through memory
- Temporary Files: Cleaned up automatically
External Integrations¶
Documentation System¶
- Technology: MkDocs with Material theme
- Purpose: Static documentation generation
- Deployment: Local development server, no runtime dependencies
Package Distribution¶
- Build System: GoReleaser for multi-platform builds
- Platforms: Linux, macOS, Windows (amd64, arm64)
- Distribution: GitHub Releases, package managers, direct download
- Formats: Binary archives, system packages (deb, rpm, apk)
Development Integration¶
- CI/CD: GitHub Actions
- Quality: golangci-lint, pre-commit hooks
- Testing: Go's built-in testing framework
- Task Runner: Just for development workflows
Air-Gap/Offline Considerations¶
Design for Isolation¶
graph LR
subgraph "Air-Gapped Environment"
subgraph "Secure Network"
FW[OPNsense Firewall]
OPS[Operator Workstation]
DOCS[Documentation Server]
end
subgraph "opnDossier Application"
BIN[Single Binary]
CFG[Local Config]
end
end
FW -->|config.xml| OPS
OPS -->|Executes| BIN
BIN -->|Uses| CFG
BIN -->|Generates| DOCS
Offline Capabilities¶
- Zero External Dependencies: All libraries embedded in binary
- No Network Calls: Completely self-contained operation
- Portable Deployment: Single binary, no installation required
- Data Exchange: File-based import/export only
Data Exchange Patterns¶
- Import: Local files, USB drives, network shares
- Export: Markdown, JSON, YAML, plain text, HTML
- Transfer: Standard file transfer protocols (SCP, SFTP, etc.)
FormatRegistry Pattern¶
Overview¶
The FormatRegistry pattern provides a centralized format dispatch mechanism that replaced scattered switch statements across 8+ locations. DefaultRegistry is the single source of truth for supported output formats, managing format names, aliases, file extensions, validation, and generation dispatch.
Key Components¶
FormatHandler Interface¶
Each format implements the FormatHandler interface:
type FormatHandler interface {
FileExtension() string
Aliases() []string
Generate(g *HybridGenerator, data *common.CommonDevice, opts Options) (string, error)
GenerateToWriter(g *HybridGenerator, w io.Writer, data *common.CommonDevice, opts Options) error
}
Registered Formats¶
DefaultRegistry manages five built-in format handlers:
| Format | Extension | Aliases | Handler Implementation |
|---|---|---|---|
| markdown | .md |
md | markdownHandler |
| json | .json |
- | jsonHandler |
| yaml | .yaml |
yml | yamlHandler |
| text | .txt |
txt | textHandler |
| html | .html |
htm | htmlHandler |
Adding a New Format¶
Adding a new format requires only registering a FormatHandler in newDefaultRegistry():
func newDefaultRegistry() *FormatRegistry {
r := NewFormatRegistry()
r.Register("markdown", &markdownHandler{})
r.Register("json", &jsonHandler{})
r.Register("yaml", &yamlHandler{})
r.Register("text", &textHandler{})
r.Register("html", &htmlHandler{})
// Add new formats here
return r
}
All validation, shell completion, and dispatch logic automatically picks up the new format.
Format Resolution and Validation¶
DefaultRegistry.Canonical(format)- Resolves aliases to canonical names (e.g., "md" → "markdown", "yml" → "yaml")DefaultRegistry.Get(format)- Returns theFormatHandlerfor a format or alias, returningErrUnsupportedFormatfor unknown formatsDefaultRegistry.ValidFormats()- Returns sorted slice of canonical format names for validationDefaultRegistry.Extensions()- Returns map of format name to file extension for file output
Integration Points¶
CLI Layer (cmd/)¶
- Format validation and shell completions use
DefaultRegistry.ValidFormats() - File extension lookup replaced switch statements with
handler.FileExtension() - Format descriptions maintained separately in
formatDescriptionsmap incmd/shared_flags.go
Config Layer (internal/config/)¶
ValidFormatsderived from registry withslices.Clone()for immutability
Generator Layer (internal/converter/)¶
HybridGenerator.Generate()useshandlerForFormat()to retrieve handlers- Handler dispatch via
handler.Generate()andhandler.GenerateToWriter() - Each handler delegates to generator's private format-specific methods
Processor Layer (internal/processor/)¶
processor.Transform()resolves aliases viaDefaultRegistry.Canonical()- Supports all five formats (markdown, json, yaml, text, html)
- Text and HTML formats delegate to exported
converter.StripMarkdownFormatting()andconverter.RenderMarkdownToHTML()
Design Rationale¶
- Single Source of Truth: Eliminates duplicated format lists across CLI, config, and generator layers
- Centralized Validation: Format validation occurs in one place via the registry
- Extensibility: New formats require only handler registration, no changes to dispatch logic
- Alias Support: Consistent alias resolution (txt, htm, md, yml) across all code paths
- Type Safety: Handler interface ensures consistent format implementation
Related Documentation¶
For detailed guidance on the FormatRegistry pattern and consumer-local interface narrowing, see AGENTS.md §5.9b.
For practical developer guidance on the FormatRegistry pattern, format addition workflow, and avoiding hardcoded switch statements, see CONTRIBUTING.md Go Development Standards section.
Versioned Data Strategy¶
Configuration Versioning¶
- Backward Compatibility: Support for older OPNsense versions
- Forward Compatibility: Graceful handling of newer configurations
- Version Detection: Automatic OPNsense version identification
- Migration Support: Utilities for format changes
Non-Destructive Processing¶
- Original Preservation: Input files never modified
- Timestamped Outputs: Version metadata in all outputs
- Audit Trail: Change tracking and diff generation
- Rollback Support: Easy reversion to previous states
Schema Evolution¶
graph TB
subgraph "Version Management"
V1[OPNsense v1.x<br/>Basic features]
V2[OPNsense v2.x<br/>Enhanced features]
V3[OPNsense v3.x<br/>Latest features]
end
subgraph "Compatibility Layer"
COMPAT[Version Handler]
MIGRATE[Migration Engine]
VALIDATE[Schema Validator]
end
subgraph "Processing Pipeline"
PARSER[XML Parser]
CONVERTER[Data Converter]
RENDERER[Output Renderer]
end
V1 --> COMPAT
V2 --> COMPAT
V3 --> COMPAT
COMPAT --> VALIDATE
COMPAT --> MIGRATE
MIGRATE --> PARSER
VALIDATE --> PARSER
PARSER --> CONVERTER
Note over CONVERTER: Accumulates warnings<br/>for incomplete data
CONVERTER --> RENDERER
Warning System¶
ConversionWarning Type¶
The ConversionWarning type captures non-fatal issues encountered during schema-to-CommonDevice conversion:
// ConversionWarning represents a non-fatal issue encountered during conversion
type ConversionWarning struct {
Field string // Dot-path of problematic field (e.g., "FirewallRules[0].Type")
Value string // Problematic value encountered
Message string // Human-readable description
Severity analysis.Severity // Importance of the warning
}
Warning Generation¶
The OPNsense converter (pkg/parser/opnsense/converter.go) accumulates warnings during conversion via the addWarning() method:
func (c *Converter) addWarning(field, value, message string, severity analysis.Severity) {
c.warnings = append(c.warnings, common.ConversionWarning{
Field: field,
Value: value,
Message: message,
Severity: severity,
})
}
Common Warning Scenarios¶
Warnings are generated for configuration elements with missing or incomplete data:
Firewall Rules¶
- Empty rule type: High severity warning when firewall rule has no type specified
- Missing source address: Medium severity warning for rules without source address
- Missing destination address: Medium severity warning for rules without destination address
- No interface assigned: Medium severity warning when interface field is empty
NAT Rules¶
- Outbound NAT without interface: Medium severity warning for unassigned outbound rules
- Inbound NAT missing internal IP: High severity warning for port forwards without target IP
- Inbound NAT without interface: Medium severity warning for unassigned inbound rules
Network Configuration¶
- Gateway missing address: Warnings for incomplete gateway definitions
- Gateway missing name: Warnings for unnamed gateways
System Configuration¶
- User missing name: Warnings for incomplete user accounts
- User missing UID: Warnings for users without unique identifiers
- Certificate problems: Warnings for invalid or incomplete certificates
- HA configuration issues: Warnings for high-availability misconfigurations
Warning Propagation¶
Warnings flow through the system alongside the device model:
- Converter generates warnings during
ToCommonDevice()conversion - DeviceParser returns warnings from
Parse()andParseAndValidate()methods - The Factory propagates warnings through
CreateDevice() - CLI commands log warnings via structured logging using
ctxLogger.Warn()
DeviceParser Interface¶
The DeviceParser interface signature returns 3 values to support warnings:
type DeviceParser interface {
// Parse reads and converts the configuration, returning non-fatal conversion warnings.
Parse(ctx context.Context, r io.Reader) (*common.CommonDevice, []common.ConversionWarning, error)
// ParseAndValidate reads, converts, and validates the configuration, returning non-fatal conversion warnings.
ParseAndValidate(ctx context.Context, r io.Reader) (*common.CommonDevice, []common.ConversionWarning, error)
}
Factory¶
The Factory.CreateDevice() method returns 3 values:
func (f *Factory) CreateDevice(
ctx context.Context,
r io.Reader,
deviceTypeOverride string,
validateMode bool,
) (*common.CommonDevice, []common.ConversionWarning, error)
CLI Integration¶
All configuration-reading commands (convert, display, validate, diff) handle warnings consistently:
device, warnings, err := parser.NewFactory(cfgparser.NewXMLParser()).CreateDevice(ctx, file, deviceType, validateMode)
if err != nil {
// Handle fatal error
}
// Log warnings unless --quiet flag is set
if cmdConfig == nil || !cmdConfig.IsQuiet() {
for _, w := range warnings {
ctxLogger.Warn("conversion warning",
"field", w.Field,
"message", w.Message,
"severity", w.Severity,
)
}
}
Quiet Mode Behavior¶
When the --quiet flag is used:
- Warnings are collected but not logged
- Only errors are reported
- Processing continues normally with warning suppression
- Useful for automated processing pipelines
Security Architecture¶
Threat Model¶
- Primary Threats: Malicious XML files, path traversal, resource exhaustion
- Not Addressed: Network attacks (offline operation), privilege escalation (user-level tool)
Security Controls¶
- Input Validation: XML schema validation, path sanitization, size limits at system boundaries
- Processing Security: Memory safety (Go runtime), type safety, error handling that prevents credential leakage
- Output Security: Path validation, restrictive file permissions (0600 for sensitive data), content sanitization
For secure coding principles, SNMP redaction patterns, and the canonical approach to safe error messages, see CONTRIBUTING.md Secure Coding Principles section and internal/processor/report.go.
Air-Gap Security Benefits¶
- No Network Attack Surface: Offline operation eliminates network-based threats
- No Data Exfiltration: Local processing only
- No Unauthorized Updates: Manual deployment only
- Audit-Friendly: All operations are local and traceable
Deployment Patterns¶
Single Binary Distribution¶
- Build: Cross-compiled Go binary
- Size: Minimal footprint (~10-20MB)
- Dependencies: None (all embedded)
- Installation: Drop-in replacement, no setup required
Multi-Platform Support¶
- Operating Systems: Linux, macOS, Windows
- Architectures: amd64, arm64
- Special: macOS universal binaries
- Packages: Native package formats for each platform
Enterprise Deployment¶
- Package Management: APT, RPM, Homebrew integration
- Code Signing: Verified binaries for security
- Bulk Deployment: Network share or USB distribution
- Configuration Management: YAML-based configuration
Quick Start Architecture Summary¶
- User provides OPNsense or pfSense config.xml file
- CLI parses command-line arguments and loads configuration (via
convert,display,audit,validate, ordiffcommands) - Factory auto-detects device type from XML root element (
<opnsense>or<pfsense>) and dispatches to appropriate parser - Converter transforms XML to
CommonDevice, accumulating conversion warnings for non-fatal issues - Parser returns 3 values: device model, warnings slice, error
- CLI logs warnings via structured logging (suppressed with
--quietflag) - FormatRegistry provides handler for requested format (markdown, JSON, YAML, text, HTML)
- Output Renderer generates documentation via format-specific handler with dynamic headers using
DeviceType.DisplayName() - User receives human-readable documentation in the requested format
Key Benefits: Offline operation, security-first design, operator-focused workflows, cross-platform compatibility, and comprehensive documentation generation from complex network configurations.
Audit Command: The opndossier audit command provides the supported entry point for security audit and compliance checks, with concurrent multi-file processing, glamour-styled terminal output, and auto-named report files to prevent collisions.