CommonDevice Export Model vs XML Schema DTOs#
Lead Section#
The opnDossier project implements a two-stage parsing architecture that separates XML-specific concerns from platform-agnostic domain logic through distinct data models: device-specific XML schema DTOs (OpnSenseDocument, pfsense.Document) and the unified CommonDevice export model.
Device-specific XML schemas serve as Data Transfer Objects (DTOs) that mirror each platform's config.xml structure exactly, using Go's encoding/xml package for deserialization. The OpnSenseDocument handles OPNsense configurations, while pfsense.Document handles pfSense configurations. These layers are consumed only by their respective XML parsers and converters, maintaining tight coupling to XML formats for correct marshaling/unmarshaling.
The CommonDevice model represents the normalized, platform-agnostic domain model used by all downstream components including CLI commands, processors, audit plugins, and format generators. It organizes configuration data into flattened structures with native Go types, eliminates XML-specific container patterns, and serves as the foundation for multi-device support across OPNsense, pfSense, and future platforms like FortiGate.
Purpose and Architecture#
Shared Schema Utilities: pkg/schema/shared/#
The pkg/schema/shared/ package provides reusable schema-layer primitives for handling liberal boolean and integer parsing across device-specific schemas (OPNsense, pfSense). These types solve the problem of XML-encoded configuration data whose textual encoding varies between sources even though the underlying semantics are identical.
Key Types:
FlexBool — Value-level liberal boolean for XML fields where the element is always emitted and its content carries the boolean signal. Recognizes the full truthy/falsy vocabulary via IsValueTrue() delegation. Marshals to XML as "1"/"0" for determinism, round-trips as native booleans in JSON/YAML. Use when element presence is not the signal — for presence-based booleans, use BoolFlag instead.
FlexInt — Liberal integer for XML fields that are semantically integers but may receive truthy/falsy strings ("on", "off", "yes", "no") in addition to numeric values. Truthy strings coerce to 1, falsy strings to 0, clean numerics pass through unchanged. Unknown strings return a wrapped error. Use on fields that must retain int semantics (e.g., a field that sometimes carries a count and sometimes a boolean toggle).
IsValueTrue() / IsValueFalse() — Canonical truthy/falsy parser functions shared by both OPNsense and pfSense. Recognized truthy values: "1", "on", "yes", "true", "enable", "enabled" (case-insensitive, whitespace-trimmed). Recognized falsy values: "0", "off", "no", "false", "disable", "disabled", "" (empty string). Unknown values return false from both functions, allowing callers to distinguish unknown from explicitly falsy if needed.
Vocabulary Note: Both OPNsense and pfSense emit the same liberal truthy vocabulary in practice, so these shared helpers unify parsing across both platforms.
XML Schema DTOs: Platform-Specific Data Transfer Objects#
The project defines two device-specific XML schema DTOs that handle platform-specific XML structures before normalization to CommonDevice:
OpnSenseDocument#
The OpnSenseDocument struct defined in pkg/schema/opnsense/opnsense.go functions as the OPNsense-specific DTO:
XML Coupling: Struct fields carry xml:"" tags that directly mirror OPNsense's XML element names and structure, enabling automatic marshaling/unmarshaling via Go's encoding/xml package.
Container Pattern: Uses explicit container structs for nested collections, such as VLANs (container) → VLAN[] (slice), with each container having XMLName as the first field for proper XML serialization.
Limited Scope: Only three components import this schema: the XML parser layer (internal/cfgparser/xml.go), the OPNsense converter (pkg/parser/opnsense/converter.go), and the validator (internal/validator/opnsense.go).
Type System: Uses custom types like BoolFlag for XML marshaling. BoolFlag implements presence+value semantics: absent element → false, <tag/> → true, <tag>body</tag> → delegates to shared.IsValueTrue() which recognizes a liberal vocabulary ("on", "yes", "true", "enable", "enabled", and their case variants, in addition to "1" and "0").
Kea DHCP4 Schema (pkg/schema/opnsense/kea.go): The Kea.Dhcp4 field in OPNsense is typed as the named KeaDhcp4 struct (previously an anonymous inline struct). KeaDhcp4 contains:
General— Enabled, Interfaces, FirewallRules, ValidLifetimeHighAvailability— Enabled, ThisServerName, MaxUnackedClientsSubnets []KeaSubnet— parsed from<subnets>/<subnet4>MVC ArrayField elementsReservations []KeaReservation— parsed from<reservations>/<reservation>elements; each reservation references its parent subnet by UUID
Three new types accompany KeaDhcp4:
KeaSubnet— UUID (attr), Subnet (CIDR), OptionData (KeaOptionData), Pools (newline-separated range strings or CIDR), NextServer, DescriptionKeaOptionData— DomainNameServers, DomainSearch, Routers (gateway), DomainName, NTPServers, TFTPServerName, BootFileName (all comma-separated strings)KeaReservation— UUID (attr), Subnet (parent subnet UUID), IPAddress, HWAddress, Hostname, Description, OptionData (KeaOptionData)
pfsense.Document#
The pfsense.Document struct defined in pkg/schema/pfsense/document.go handles pfSense configurations with structural differences from OPNsense:
Root Element: Uses <pfsense> XML root element (vs OPNsense's <opnsense>).
Type Reuse Strategy: Follows a copy-on-write pattern that reuses 25+ OPNsense types where XML structures are identical (interfaces, VLANs, certificates, gateways, DHCP), forking locally only at divergence points (NAT inbound rules, filter rules, users/groups, DNS servers). Reuses opnsense.BoolFlag for presence-based booleans.
Key Structural Differences:
- NAT Port Forwards:
<nat><rule>as direct children (vs OPNsense's<nat><inbound><rule>nesting) - NAT Redirect IP: Uses
<target>field (vs OPNsense's<internalip>) - User Passwords:
<bcrypt-hash>field (vs OPNsense's<password>with SHA-based hashing) - Group Privileges:
Priv []stringsupporting multiple<priv>elements per group - DNS Servers:
DNSServers []stringas repeating elements (vs OPNsense's single comma-separated string) - Firewall Rules: Adds pfSense-specific fields:
ID,Tag,Tagged,OS,AssociatedRuleID - Syslog Config: pfSense-specific structure with different log facility fields
Limited Scope: Imported only by the pfSense parser (pkg/parser/pfsense/parser.go), pfSense converter (pkg/parser/pfsense/converter.go), and pfSense validator (internal/validator/pfsense.go).
Shared Security: Both OPNsense and pfSense parsers delegate XML security hardening to pkg/parser/xmlutil.go (NewSecureXMLDecoder, CharsetReader), eliminating duplication while providing XXE protection, input size limits, and charset support (UTF-8, US-ASCII, ISO-8859-1, Windows-1252).
ConversionWarning: Non-Fatal Issue Reporting#
The ConversionWarning type defined in pkg/model/warning.go captures non-fatal issues encountered during schema-to-CommonDevice conversion:
type ConversionWarning struct {
Field string // Dot-path to problematic field (e.g., "FirewallRules[0].Type")
Value string // Problematic value encountered
Message string // Human-readable description
Severity analysis.Severity // Importance level (High/Medium/Low)
}
Warnings are generated when configuration data is incomplete or potentially misconfigured but parseable. Callers receive warnings alongside the parsed device model and should log or surface them without treating them as fatal errors.
CommonDevice: Platform-Agnostic Domain Model#
The CommonDevice struct serves as the central domain model with three categories of fields:
Core Configuration Fields (concrete types, populated during parsing):
- System configuration (hostname, DNS, web GUI settings)
- Network infrastructure (Interfaces, VLANs, Bridges, LAGGs, VirtualIPs)
- Firewall rules and NAT configuration
- VPN subsystems (OpenVPN, WireGuard, IPsec)
- Routing (gateways, gateway groups, static routes)
- Services (DHCP, DNS, NTP, SNMP, SSH)
- Security (certificates, CAs, IDS/IPS)
- User management (users, groups)
Optional Features (pointer types for features not universally configured):
- IDS/IPS (Suricata), Monit, Netflow/IPFIX, Traffic shaper, Captive portal, Cron jobs, Kea DHCP (general settings only; subnet and reservation data is in
DHCP []DHCPScope)
Enrichment Fields (pointer types):
- Statistics, Analysis, SecurityAssessment, PerformanceMetrics — populated by
prepareForExport()when nil, or pre-populated viaEnrichForExport()for multi-format scenarios - ComplianceResults (
*ComplianceResults) — populated by audit handler in audit mode, passed through byprepareForExport()
Consumer Reach: All consumer files across CLI, converters, processors, and audit plugins import from pkg/model/ to access the CommonDevice model as part of the public API.
Configuration Presence Detection Methods#
The CommonDevice model provides dedicated methods for detecting whether specific configuration sections exist. These methods were added to support value-type presence detection patterns after the model migrated from pointer types to value types:
HasDHCP(): Reports whether the device has any DHCP configuration. Both ISC and Kea DHCP scopes are normalized into the unified DHCP slice, so this method returns len(d.DHCP) > 0.
HasInterfaces(): Reports whether the device has any interface configuration.
HasNATConfig(): Reports whether the device has meaningful NAT configuration (delegates to NAT.HasData()).
HasRoutes(): Reports whether the device has any routing configuration (static routes, gateways, or gateway groups).
HasVLANs(): Reports whether the device has any VLAN configuration.
All methods return false if the receiver is nil, making them safe for chained operations. These methods are used by the diff engine for section-level added/removed detection.
Critical Structural Differences#
Interface Representations#
OpnSenseDocument: Uses the Bridge/VLAN container pattern with explicit container structs wrapping slice elements:
type VLANs struct {
XMLName xml.Name `xml:"vlans"`
VLAN []VLAN `xml:"vlan,omitempty"`
}
CommonDevice: Exposes slices directly without containers:
Interfaces []Interface `json:"interfaces"`
VLANs []VLAN `json:"vlans,omitempty"`
Bridges []Bridge `json:"bridges,omitempty"`
Each Interface has a .Name field rather than being keyed in a map.
Users and Groups Location#
OpnSenseDocument: Nested within the System struct as System.User[] and System.Group[], with users including nested APIKeys[] for API authentication secrets.
CommonDevice: Exposed at top level as Users []User and Groups []Group, allowing direct access without traversing the System object while maintaining the nested APIKeys []APIKey structure within each user.
Data Types: Bool vs String and Typed Enums#
OpnSenseDocument: Uses the custom BoolFlag type for fields that OPNsense stores with presence+value semantics. BoolFlag layers presence semantics over liberal value parsing: absent element → false, <tag/> → true (historical OPNsense convention), <tag>body</tag> → delegates to shared.IsValueTrue(body). This upgrade fixes a latent bug where <tag>0</tag> previously parsed as true. String fields store raw XML values like "pass", "block", "automatic", "lacp".
CommonDevice: Applies two categories of type transformations:
Boolean Conversion:
Disabled bool `json:"disabled"`
BlockPrivate bool `json:"block_private"`
EnableDHCP bool `json:"enable_dhcp"`
Typed Enum Constants: Certain string fields in OPNsense XML are mapped to typed enum constants in CommonDevice for compile-time type safety:
Type FirewallRuleType `json:"type"` // "pass" → common.RuleTypePass
Direction FirewallDirection `json:"direction"` // "in" → common.DirectionIn
IPProtocol IPProtocol `json:"ipProtocol"` // "inet" → common.IPProtocolInet
OutboundMode NATOutboundMode `json:"outboundMode"` // "automatic" → common.OutboundAutomatic
Protocol LAGGProtocol `json:"protocol"` // "lacp" → common.LAGGProtocolLACP
Mode VIPMode `json:"mode"` // "carp" → common.VIPModeCarp
The converter applies field-level type transformations during schema→CommonDevice conversion, casting XML string values to their corresponding typed constants. During JSON/YAML serialization, these typed enums serialize back to their original string values for compatibility.
Addressing Patterns#
OpnSenseDocument: Source and destination blocks implement hierarchical endpoint specification with mutually exclusive fields (Network, Address, Any) and priority logic:
func (s Source) EffectiveAddress() string {
if s.Network != "" {
return s.Network // Highest priority
}
if s.Address != "" {
return s.Address // Second priority
}
if s.IsAny() {
return "any" // Third priority
}
return ""
}
CommonDevice: Normalized RuleEndpoint structure with single address field:
type RuleEndpoint struct {
Address string `json:"address"`
Port string `json:"port,omitempty"`
Negated bool `json:"negated"`
}
The converter calls EffectiveAddress() to resolve priority and populate the single authoritative address field.
Firewall Rules#
OpnSenseDocument: Filter.Rule[] with complex nested source/destination structures and InterfaceList type for automatic comma-separated interface string conversion. Rule types, directions, and IP protocols are stored as plain XML strings.
CommonDevice: FirewallRules []FirewallRule with:
- Flattened
Source,DestinationasRuleEndpointstructs - Separate
SourcePort,DestinationPortfields at rule level Interfaces []string(converted from comma-separated viaInterfaceList.UnmarshalXML)- Native
booltypes forDisabled,Log,Floating,Quickflags - Typed enum constants for rule semantics:
Type common.FirewallRuleType— RuleTypePass, RuleTypeBlock, RuleTypeRejectDirection common.FirewallDirection— DirectionIn, DirectionOut, DirectionAnyIPProtocol common.IPProtocol— IPProtocolInet, IPProtocolInet6
The converter casts XML string values to typed constants:
Type: common.FirewallRuleType(rule.Type), // "pass" → RuleTypePass
Direction: common.FirewallDirection(rule.Direction), // "in" → DirectionIn
IPProtocol: common.IPProtocol(rule.IPProtocol), // "inet" → IPProtocolInet
Consumer code uses type-safe comparisons:
if rule.Type == common.RuleTypePass { ... }
NAT Configuration#
OpnSenseDocument: Separate NAT.Inbound[] and NAT.Outbound[] structures under Nat container, with manual append pattern in parser to build slices. Outbound NAT mode is stored as a plain XML string.
CommonDevice: Unified NATRules []NATRule at top level, with NAT direction conveyed via rule type field rather than container separation, simplifying queries for all NAT rules. Outbound NAT mode uses a typed enum:
type NATOutboundMode string
const (
OutboundAutomatic NATOutboundMode = "automatic" // Automatic outbound NAT rules
OutboundHybrid NATOutboundMode = "hybrid" // Automatic + manual rules
OutboundAdvanced NATOutboundMode = "advanced" // Manual rules only
OutboundDisabled NATOutboundMode = "disabled" // NAT disabled
)
The converter casts the XML mode string to the typed constant:
OutboundMode: common.NATOutboundMode(doc.Nat.Outbound.Mode)
The NATConfig struct includes a HasData() method that reports whether the configuration contains any meaningful data (any non-zero fields). This method serves as the single source of truth for NAT presence detection, used by both CommonDevice.HasNATConfig() and the diff engine to determine if a NAT section exists.
VPN Subsystems#
OpnSenseDocument: OpenVPN servers/clients under OpenVPN container, WireGuard instances under separate structure, IPsec under IPsec container with pre-shared keys.
CommonDevice: Unified VPN struct containing:
type VPN struct {
OpenVPN *OpenVPNConfig `json:"openVpn,omitempty"`
WireGuard *WireGuardConfig `json:"wireGuard,omitempty"`
IPsec *IPsecConfig `json:"ipsec,omitempty"`
}
Security Note: OpenVPN TLS keys, IPsec pre-shared keys, and WireGuard private keys are never mapped to CommonDevice as a defense-in-depth measure to prevent exposure.
DNS and DHCP#
OpnSenseDocument:
Unboundstruct for DNS resolver configurationDHCPdmap keyed by interface:DHCPd map[string]DHCPScope
CommonDevice:
DNS *DNSConfigat top level with nestedUnboundconfigurationDHCP []DHCPScopeas slice (not map) for consistent handling across ISC and Kea scopes- Converter transforms map to slice with interface association preserved in each scope's
.Interfacefield - Each
DHCPScopecarries aSourcefield ("isc"or"kea") identifying which DHCP server produced it; an emptySourceis treated as"isc"for backward compatibility - Kea subnets are converted by
convertKeaDHCPScopes()in the OPNsense converter and appended toDHCP []DHCPScopewithSource: "kea"; they are not stored onKeaDHCP *KeaDHCPConfig
SSH Configuration#
OpnSenseDocument: SSH settings nested in System.SSH struct.
CommonDevice: SSH settings exposed at top level as SSH *SSHConfig with fields:
type SSHConfig struct {
Enabled bool `json:"enabled"`
Port string `json:"port,omitempty"`
Group string `json:"group,omitempty"`
}
Note: No passwordauth field exists in the model. SSH password and HA password are both redacted when --redact flag is used.
Gateway and Routing#
OpnSenseDocument: Gateways in nested container structure at Gateways.GatewayItem[].
CommonDevice: Gateways at Routing.Gateways[] as flat array, with routing configuration grouped under a unified Routing struct:
type Routing struct {
Gateways []Gateway `json:"gateways,omitempty"`
GatewayGroups []GatewayGroup `json:"gatewayGroups,omitempty"`
StaticRoutes []StaticRoute `json:"staticRoutes,omitempty"`
}
Transformation Pipeline#
Three-Layer Architecture#
The enrichment pipeline follows this data flow:
- XML Parsing: Raw firewall XML → device-specific schema DTO (via platform-specific parsers using Go's
encoding/xml)- OPNsense: Raw XML →
schema.OpnSenseDocument(viainternal/cfgparser/xml.go) - pfSense: Raw XML →
pfsense.Document(viapkg/parser/pfsense/parser.go)
- OPNsense: Raw XML →
- Device Conversion: Device-specific schema →
(*common.CommonDevice, []common.ConversionWarning, error)- OPNsense:
schema.OpnSenseDocument→CommonDevice(viapkg/parser/opnsense/converter.go) - pfSense:
pfsense.Document→CommonDevice(viapkg/parser/pfsense/converter.go)
- OPNsense:
- Export Preparation:
prepareForExport()enriches with computed statistics, optional redaction, and consistency checking
The conversion layer returns three values: the converted device model, a slice of non-fatal conversion warnings, and any fatal error. Warnings indicate incomplete or potentially misconfigured data that parsers can handle gracefully.
Both parsers use the shared pkg/parser/xmlutil.go security layer:
NewSecureXMLDecoder()provides XXE protection and input size limitsCharsetReader()handles UTF-8, US-ASCII, ISO-8859-1, and Windows-1252 encodings
Converter Patterns#
Both OPNsense and pfSense converters follow consistent transformation patterns:
Warning Accumulation: Converters include an addWarning() accumulator method that records non-fatal issues during field-by-field transformation. Warnings are collected in a []common.ConversionWarning slice and returned alongside the converted device:
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,
})
}
Warning Scenarios: The converter generates warnings for several configuration issues:
- Firewall rules with empty type, source, destination, or interface fields
- NAT rules missing required fields (internal IP for inbound, interface for all rules)
- Gateways with empty address or name
- Users missing name or UID
- Certificates with empty PEM data
- HA sync targets configured without credentials
- Kea subnets with multiple pools (only the first pool is represented in the unified
DHCPScope)
Temp-Variable-Append Pattern: The converter uses a consistent pattern for transforming XML repeating elements into Go slices:
func (c *Converter) convertBridges(doc *schema.OpnSenseDocument) []common.Bridge {
if len(doc.Bridges.Bridge) == 0 {
return nil // Step 1: Return nil for empty
}
result := make([]common.Bridge, 0, len(doc.Bridges.Bridge)) // Step 2: Pre-allocate
for _, b := range doc.Bridges.Bridge { // Step 3: Loop
result = append(result, common.Bridge{ // Step 4: Append
BridgeIf: b.Bridgeif,
Members: splitNonEmpty(b.Members, ","),
Description: b.Descr,
})
}
return result // Step 5: Return
}
This pattern provides performance optimization via pre-allocated slices with capacity hints.
Field Priority Resolution: Sources and destinations implement strict priority logic where only one of Network/Address/Any is meaningfully populated. The converter calls EffectiveAddress() to resolve priority and populate the single address in CommonDevice.
Type Casting: The converter casts XML string values to typed enum constants for type safety:
Type: common.FirewallRuleType(rule.Type),
Direction: common.FirewallDirection(rule.Direction),
IPProtocol: common.IPProtocol(rule.IPProtocol), // Handles "inet", "inet6", "inet46" (pfSense)
This transformation preserves the original string values during JSON/YAML serialization while providing compile-time validation within Go code. The IPProtocol type includes IPProtocolInet46 constant to handle pfSense's dual-stack protocol specification.
Nested Transformations: Complex nested transformations handle Users with API Keys:
user := common.User{ Name: u.Name }
if len(u.APIKeys) > 0 {
user.APIKeys = make([]common.APIKey, 0, len(u.APIKeys))
for _, k := range u.APIKeys {
user.APIKeys = append(user.APIKeys, common.APIKey{
Key: k.Key, Secret: k.Secret, Privileges: k.Privileges,
})
}
}
Export Preparation and Enrichment#
The enrichment pipeline provides two entry points for populating computed fields:
EnrichForExport(*CommonDevice) — Public API memoization entry point for callers exporting the same device to multiple formats (JSON + YAML + Markdown, etc.). Populates DeviceType, Statistics, Analysis, SecurityAssessment, and PerformanceMetrics when nil. Designed to be called once before a format loop; subsequent prepareForExport() calls reuse the populated fields. analysis.ComputeStatistics and analysis.ComputeAnalysis are expensive (linear-or-worse in interfaces/rules/services) and dominate export time.
SECURITY WARNING: EnrichForExport does NOT redact sensitive fields. The resulting device carries plaintext secrets (e.g., SNMP community string in Statistics.ServiceDetails). Callers MUST NOT marshal or log the device directly. Always pass through prepareForExport or a downstream Generator that calls prepareForExport for redaction.
CACHE INVALIDATION: EnrichForExport memoizes a snapshot at call time. If the caller mutates fields that feed Statistics/Analysis (SNMP.ROCommunity, FirewallRules, Interfaces) after calling EnrichForExport, the cached values go stale. To refresh, clear the affected enrichment field and re-call EnrichForExport.
CONCURRENCY: EnrichForExport is NOT safe for concurrent use on the same device. Call it once before fanning out to parallel format exports. EnrichForExport on nil is a no-op (but prepareForExport still panics on nil).
prepareForExport(*CommonDevice, bool) — Per-format enrichment and redaction step. Creates a shallow copy, delegates enrichment to an internal enrich() helper when fields are nil, and applies redaction when requested. When enrichment fields are already populated (via EnrichForExport or a prior call), reuses those values. Statistics are computed from original unredacted data for accurate presence detection; redaction is applied afterward to the shallow copy.
Key Phases:
- Shallow copy creation — Protects original from mutation
- Enrichment — Delegates to
enrich()helper to populate nil DeviceType/Statistics/Analysis/SecurityAssessment/PerformanceMetrics; reuses existing values when present - Redaction application — Replaces 7 categories of sensitive fields with
[REDACTED] - Statistics post-redaction — SNMP community strings in computed ServiceDetails also redacted via clone-on-write
- ComplianceResults pass-through — Audit data populated externally by audit handler, passed through unchanged
Critical Design: Receives unredacted data for statistics computation, then redacts the copy, ensuring statistics accurately reflect configured services while protecting sensitive details. ComplianceResults is populated by mapAuditReportToComplianceResults() in cmd/audit_handler.go when running in audit mode and is not modified by prepareForExport().
Computed Fields#
The enrichment pipeline populates five types of computed fields:
Statistics — Aggregated counts and service detection:
- Interface counts by type (physical, VLAN, bridge, PPP, GIF, GRE, LAGG)
- Firewall rule counts by interface and type
- NAT entry counts (inbound/outbound)
- Service status (DHCP, DNS, SNMP, SSH, NTP enabled detection)
- Security features (block private/bogon, HTTPS WebGUI)
- Security score (0-100)
- Configuration complexity (0-100)
Analysis — Configuration issues and recommendations:
- Dead rule detection (shadowed rules, disabled rules)
- Unused interface detection
- Security issues (HTTP-only WebGUI, default SNMP community)
- Performance issues (excessive rules, disabled offloading)
- Consistency issues (invalid gateway format, DHCP without interface IP)
SecurityAssessment — Security posture evaluation:
- Overall security score
- Enabled security features inventory
- Vulnerabilities and recommendations
PerformanceMetrics — Complexity scoring
ComplianceResults (*ComplianceResults) — Comprehensive compliance audit results with nested structure:
Summary(*ComplianceResultSummary) — Aggregated pass/fail/skip counts across all pluginsPluginResults(map[string]PluginComplianceResult) — Per-plugin compliance results keyed by plugin nameFindings([]ComplianceFinding) — All findings across pluginsMode— Audit report mode (standard/blue/red)Metadata— Arbitrary audit metadata
PluginComplianceResult — Results from a single audit plugin:
PluginInfo(CompliancePluginInfo) — Plugin metadata (name, version, description)Findings([]ComplianceFinding) — Plugin-specific findingsSummary(*ComplianceResultSummary) — Plugin-specific countsControls([]ComplianceControl) — Control definitions evaluated by this pluginCompliance(map[string]bool) — Control ID → compliant/non-compliant status
ComplianceFinding — Individual compliance check result:
Type— Finding category (e.g., "compliance")Severity— Severity level (critical/high/medium/low/info)Title— Brief finding descriptionDescription— Detailed explanationRecommendation— Suggested corrective actionComponent— Affected configuration componentReferences— Related control IDs (e.g., "STIG-V-123456")
ComplianceControl — Compliance control definition from a plugin:
ID,Title,Description— Control metadataCategory,Severity— Classification fieldsRationale— Explanation of control importanceRemediation— How to achieve complianceReferences— External documentation links (NIST, CIS URLs)Tags— Categorization tagsMetadata— Additional structured data
ComplianceResultSummary — Aggregated statistics:
TotalFindings,CriticalFindings,HighFindings,MediumFindings,LowFindings— Finding counts by severityPluginCount— Number of plugins that contributed resultsCompliant,NonCompliant— Control pass/fail counts
Redaction System#
Seven categories of sensitive fields are redacted when the --redact flag is used:
- High Availability Password — CARP/pfsync credentials
- Certificate Private Keys — TLS/SSL private keys
- CA Private Keys — Locally-created Certificate Authority keys
- API Key Secrets — User authentication secrets
- SNMP Community String — Read-only authentication
- WireGuard Pre-Shared Keys — VPN tunnel PSKs
- DHCPv6 Authentication Secrets — DHCP authentication credentials
Never-Mapped Secrets (excluded at conversion layer to prevent exposure):
- OpenVPN TLS authentication keys
- IPsec pre-shared keys
- WireGuard interface private keys
SNMP Redaction Behavior: redactStatisticsServiceDetails is a non-mutating function that returns *common.Statistics. It clones the Statistics struct, ServiceDetails slice, and affected Details maps only when redaction is required. When no SNMP entry has sensitive keys, it returns the input pointer unchanged. This clone-on-write contract allows EnrichForExport to memoize a single Statistics shared across mixed redact=true and redact=false callers without leaking redacted values.
Usage and Export Integration#
JSON and YAML Export#
Both JSON and YAML exporters call prepareForExport(data, redact) before serialization, ensuring:
- Enrichment fields are populated identically across formats
- Redaction is consistently applied
- Statistics use original unredacted data for accuracy
- Original device remains unmodified (immutability guarantee)
The exported JSON/YAML includes all three field categories (core configuration, optional features, enrichment fields), providing API consumers with both raw configuration data and computed intelligence.
Best Practice: Callers exporting to multiple formats should call EnrichForExport once before the format loop, then call prepareForExport (or use a Generator) for each format. Single-format exports can continue using prepareForExport alone without calling EnrichForExport.
ComplianceResults Relationship: The common.ComplianceResults structure mirrors audit.Report but lives in the common package to avoid circular dependencies between the converter and audit subsystems. The compliance.Finding type is a type alias for analysis.Finding, allowing findings from both security analysis and compliance plugins to share the same underlying structure.
Markdown Export: DOES call prepareForExport, applying enrichment and optional redaction. When ComplianceResults is populated (audit mode), markdown reports append audit sections via BuildAuditSection() and WriteAuditSection() in internal/converter/builder/, rendering compliance findings, plugin summaries, and control details in structured markdown format.
Immutability Guarantee#
The prepareForExport function returns a modified copy rather than mutating the original, enabling:
- Multiple exports with different redaction settings from the same parsed configuration
- Safe concurrent access to the original CommonDevice
- Separation between parsing/conversion and export concerns
EnrichForExport populates enrichment fields in place for memoization. The device produced by EnrichForExport carries unredacted secrets and must not be marshaled directly — always pass through prepareForExport or a downstream Generator for redaction. The immutability guarantee applies to prepareForExport, not EnrichForExport.
API Consumer Perspective#
Public API Access: The CommonDevice model is now part of the public API in pkg/model/ and can be imported by external Go projects. All consumer code imports directly from pkg/model/ and pkg/parser/ without any re-export layer.
Conversion Warning Handling: CLI commands that invoke the parser factory receive warnings alongside the device model and log them via structured ctxLogger.Warn calls, respecting quiet mode suppression. Warnings are surfaced to users without treating them as fatal errors.
Import Patterns: External projects and internal consumers use the following imports:
import (
common "github.com/EvilBit-Labs/opnDossier/pkg/model"
"github.com/EvilBit-Labs/opnDossier/pkg/parser"
schema "github.com/EvilBit-Labs/opnDossier/pkg/schema/opnsense"
)
Factory Usage: The parser factory is created via parser.NewFactory(cfgparser.NewXMLParser()) and returns (*common.CommonDevice, []common.ConversionWarning, error) from its CreateDevice() method.
Architectural Constraints#
Type Safety and Enum Benefits#
The CommonDevice model uses typed enum constants across 70 files to provide compile-time validation while maintaining compatibility with XML input and JSON/YAML output:
Type Safety: Enum constants prevent invalid values at compile boundaries. Consumer code cannot accidentally assign "passthrough" to a FirewallRuleType field — only RuleTypePass, RuleTypeBlock, or RuleTypeReject are valid.
Serialization Compatibility: Typed enums serialize to their underlying string values during JSON/YAML export, preserving backward compatibility with existing API consumers:
Type: common.RuleTypePass // In-memory: typed constant
// JSON output: "type": "pass"
Code Clarity: Enum constants replace magic strings throughout the codebase:
// Before: if rule.Type == "pass" { ... }
// After: if rule.Type == common.RuleTypePass { ... }
Key Enum Types: FirewallRuleType, FirewallDirection, IPProtocol, NATOutboundMode, LAGGProtocol, VIPMode, FindingSeverity — used across firewall rules, NAT configuration, network infrastructure, and analysis findings.
Circular Dependency Prevention#
The enrichment pipeline cannot import internal/processor due to circular dependency constraints:
- Both packages import
pkg/model/as common ground - Analysis logic is mirrored, not shared, between converter and processor packages
- This is a deliberate trade-off: package independence valued over code deduplication
Multi-Device Support Design#
CommonDevice serves as the foundation for multi-device support, enabling integration of OPNsense, pfSense, and future firewall platforms through:
- Platform-agnostic field naming and structure
- Separation of XML-specific concerns into device-specific parsers
- Unified processor and audit plugin interface
- Parser registration via
init()functions: OPNsense parser registers for<opnsense>root elements, pfSense parser registers for<pfsense>root elements - Parser registry pattern planned for v2.0.0 (Issue #302) for extensible parser registration, enabling external projects to register custom DeviceParser implementations via
init()functions without modifying core opnDossier code
DeviceType.DisplayName(): The CommonDevice.DeviceType field uses DisplayName() method to render properly-cased platform names in markdown report titles ("OPNsense Configuration Summary", "pfSense Configuration Summary"), replacing hardcoded "OPNsense" strings.
Relevant Code Files#
| File Path | Purpose | Key Contents |
|---|---|---|
pkg/schema/shared/bool.go | Shared Boolean Parser | IsValueTrue, IsValueFalse — canonical truthy/falsy parser functions with liberal vocabulary |
pkg/schema/shared/flex_bool.go | FlexBool Value Type | Value-level liberal boolean with no presence semantics, delegates to IsValueTrue |
pkg/schema/shared/flex_int.go | FlexInt Value Type | Liberal integer parser handling boolean-like values (on/yes → 1, off/no → 0) |
pkg/schema/opnsense/opnsense.go | OpnSenseDocument XML Schema | Root XML DTO with xml:"" tags, container patterns, BoolFlag types |
pkg/schema/opnsense/kea.go | Kea DHCP4 Schema Types | KeaDhcp4, KeaSubnet, KeaOptionData, KeaReservation — full subnet and reservation parsing |
pkg/schema/pfsense/document.go | pfsense.Document XML Schema | Root XML DTO for pfSense with <pfsense> root element |
pkg/schema/pfsense/system.go | pfSense System Types | System, User (bcrypt-hash), Group (Priv []string), WebGUI |
pkg/schema/pfsense/security.go | pfSense Firewall/NAT Types | FilterRule, InboundRule (Target field), Nat structure |
pkg/schema/pfsense/services.go | pfSense Service Types | SyslogConfig, UnboundConfig, Cron, Widgets |
pkg/schema/pfsense/network.go | pfSense Network Types | DHCPv6 configuration structures |
pkg/schema/pfsense/README.md | pfSense Schema Reference | Complete structural documentation, listtags, version mapping, OPNsense differences |
pkg/model/device.go | CommonDevice Platform-Agnostic Model | Core configuration, optional features, enrichment fields, DeviceType with DisplayName() |
pkg/model/warning.go | Conversion Warning Type | Non-fatal issue reporting for schema-to-CommonDevice conversion |
pkg/model/firewall.go | Firewall Rules and NAT | FirewallRule, NATRule, RuleEndpoint structures; typed enums for FirewallRuleType, FirewallDirection, IPProtocol (includes IPProtocolInet46), NATOutboundMode |
pkg/model/network.go | Network Infrastructure | Interface, VLAN, Bridge, LAGG types; typed enums for LAGGProtocol, VIPMode |
pkg/model/system.go | System Configuration | System, SSHConfig, HAConfig structures |
pkg/model/services.go | Service Definitions | DHCPScope (Source, Description fields), DNSConfig, SNMP, NTP types; KeaDHCPConfig (general settings only) |
pkg/model/vpn.go | VPN Subsystems | VPN, OpenVPNConfig, WireGuardConfig, IPsecConfig |
pkg/model/routing.go | Routing Configuration | Routing, Gateway, GatewayGroup, StaticRoute |
pkg/model/users.go | User Management | User, Group, APIKey structures |
pkg/model/certificates.go | Certificate Management | Certificate, CA types |
pkg/parser/opnsense/converter.go | OPNsense to CommonDevice Transformer | Warning accumulation, temp-variable-append pattern, field priority resolution, nested transformations; appends Kea scopes to DHCP slice |
pkg/parser/pfsense/converter.go | pfSense to CommonDevice Transformer | Parallel converter to OPNsense with pfSense-specific type handling |
pkg/parser/pfsense/parser.go | pfSense Parser | Raw XML → pfsense.Document deserialization, auto-registration via init() |
pkg/parser/xmlutil.go | Shared XML Security | NewSecureXMLDecoder, CharsetReader for XXE protection, size limits, charset handling (UTF-8, ISO-8859-1, Windows-1252) |
internal/converter/enrichment.go | Export Preparation Pipeline | EnrichForExport (public API memoization entry point), prepareForExport, enrich (internal helper), redactStatisticsServiceDetails (clone-on-write), statistics, analysis, redaction system |
internal/converter/hybrid_generator.go | JSON/YAML Export Dispatcher | Format-specific serialization, enrichment integration |
internal/converter/builder/builder.go | Markdown Report Builder | Uses DeviceType.DisplayName() for dynamic platform headers |
internal/cfgparser/xml.go | OPNsense XML Parser | Raw XML → OpnSenseDocument deserialization, delegates to xmlutil for security |
internal/validator/opnsense.go | OPNsense Validator | Semantic validation for OPNsense schema |
internal/validator/pfsense.go | pfSense Validator | Semantic validation for pfSense schema, reuses IP/CIDR/MTU helpers |
pkg/parser/factory.go | Parser Factory | Device type detection, parser selection, warning propagation |
Related Topics#
- Multi-Device Architecture — Support for OPNsense, pfSense, and future platforms via CommonDevice abstraction
- Enrichment Pipeline — Statistics computation, analysis, security assessment, and redaction system
- Parser Registry Pattern — v2.0.0 extensibility mechanism for external parser registration
- Public API —
pkg/model/andpkg/parser/packages are public APIs importable by external Go projects