Documents
Commit And Security Standards
Commit And Security Standards
Type
Topic
Status
Published
Created
Feb 27, 2026
Updated
Apr 19, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Commit and Security Standards#

The opnDossier project enforces comprehensive commit message standards and security-first design principles to maintain code quality, traceability, and operational safety. The project uses Conventional Commits format with mandatory scope requirements and DCO sign-off for all contributions. Security practices include multi-layered XML input validation, environment variable-based secrets management, and secure build configurations.

These standards are enforced through pre-commit hooks, CI/CD pipeline automation, and dedicated security testing. The project prioritizes offline-first operation, zero telemetry, and airgap compatibility while maintaining comprehensive supply chain security through SLSA provenance and Cosign signing.

Commit Standards#

Conventional Commits Framework#

The project enforces Conventional Commits specification with the format: type(scope): description

Required format elements:

  • Type - Required prefix indicating change category
  • Scope - REQUIRED - indicates affected component
  • Description - Brief summary in imperative mood

Example: feat(parser): add support for new XML schema

Commit Types#

Standard commit types:

  • feat - New features and functionality
  • fix - Bug fixes and defect resolution
  • docs - Documentation changes
  • style - Code formatting (no logic changes)
  • refactor - Code restructuring without behavior changes
  • perf - Performance improvements
  • test - Test additions/modifications
  • build - Build system changes
  • ci - CI/CD pipeline changes
  • chore - Maintenance tasks

Commit Scopes#

Scopes are REQUIRED and identify the affected codebase component:

  • (cli) - Command-line interface and Cobra commands
  • (parser) - XML parsing and cfgparser module
  • (converter) - Data conversion and markdown generation
  • (audit) - Audit engine and compliance features
  • (config) - Configuration management with Viper
  • (display) - Terminal display and formatting
  • (docs) - Documentation and user guides
  • (model) - Data models and schema
  • (plugin) - Compliance plugin system
  • (builder) - Programmatic markdown builder

Commit Message Rules#

Message formatting requirements:

Breaking change examples:

feat(api)!: change configuration file format

fix(parser)!: remove deprecated XML attribute support

BREAKING CHANGE: The legacy XML format is no longer supported

DCO Sign-off Requirement#

All commits require DCO (Developer Certificate of Origin) sign-off:

git commit -s -m "feat(parser): add support for new XML schema"

The -s flag adds a Signed-off-by line certifying agreement with the Developer Certificate of Origin.

Commit Validation Enforcement#

Commit message validation is enforced through pre-commit hooks:

# Install pre-commit hooks with commit-msg validation
just install
# Runs: mise install && pre-commit install --hook-type pre-commit --hook-type commit-msg --hook-type pre-push

Manual installation (if needed):

pre-commit install --hook-type pre-commit --hook-type commit-msg --hook-type pre-push

Two-tier hook system:

  • pre-commit hooks (fast, every commit) - formatters, lint config verification, and golangci-lint with auto-fix
  • pre-push hook (slow, once per push) - runs full just ci-check suite including test-race (the Go race detector cannot be hosted reliably on GitHub Actions runners, so the pre-push hook is the enforcement point)

git-cliff parses commits using Conventional Commits for changelog generation:

[git]
conventional_commits = true
filter_unconventional = true # Filters non-conventional commits

Security Standards#

Core Security Principles#

The project enforces security-first design principles:

  1. No hardcoded secrets - Never include API keys, passwords, or sensitive data in code
  2. Environment variables - Use OPNDOSSIER_* prefixed environment variables for sensitive configuration
  3. Input validation - Always validate and sanitize XML input files before processing
  4. Secure defaults - Apply secure configuration defaults by design
  5. Error message safety - Avoid exposing sensitive information in error messages
  6. Airgap compatibility - Full functionality in isolated/air-gapped environments
  7. No telemetry - Zero external data transmission

Input Validation#

Multi-layered XML validation in internal/cfgparser/xml.go:

Size Limiting (prevents XML bombs):

const DefaultMaxInputSize = 10 * 1024 * 1024 // 10MB

type XMLParser struct {
    MaxInputSize int64
}

func (p *XMLParser) Parse(ctx context.Context, r io.Reader) (*schema.OpnSenseDocument, error) {
    limitedReader := io.LimitReader(r, p.MaxInputSize)
    // Process with size limit to prevent memory exhaustion
}

XXE Attack Prevention:

dec := xml.NewDecoder(limitedReader)
dec.Entity = map[string]string{} // Empty entity map prevents XXE attacks

Charset Validation:

func charsetReader(charset string, input io.Reader) (io.Reader, error) {
    normalizedCharset := strings.ToLower(strings.TrimSpace(charset))

    switch normalizedCharset {
    case "us-ascii", "ascii", "utf-8", "utf8", "iso-8859-1":
        return input, nil
    default:
        return nil, fmt.Errorf("unsupported charset: %s", charset)
    }
}

Additional validation in internal/validator/opnsense.go validates IP addresses, port ranges, and configuration semantics.

GID/UID Validation Standard#

Unix GID 0 (wheel/root group) and UID 0 (root user) are valid system identifiers. Validator checks must use gid < 0 / uid < 0 to reject only negative values, correctly allowing zero.

Error message requirement: Messages must say "non-negative integer", not "positive integer".

Reference: GOTCHAS.md section 6.1

This standard ensures correct validation of Unix system identifiers throughout the codebase.

Error Handling Without Sensitive Exposure#

Structured error handling in internal/cfgparser/errors.go:

type ParseError struct {
    Line int // Line number where error occurred
    Column int // Column number where error occurred 
    Message string // Human-readable message (no sensitive data)
}

func (e *ParseError) Error() string {
    return fmt.Sprintf("parse error at line %d, column %d: %s", 
        e.Line, e.Column, e.Message)
}

// Wraps XML errors without exposing file content
func WrapXMLSyntaxError(err error, elementPath string) error {
    var syntaxErr *xml.SyntaxError
    if errors.As(err, &syntaxErr) {
        message := syntaxErr.Msg
        if elementPath != "" {
            message = fmt.Sprintf("%s (in element path: %s)", message, elementPath)
        }
        return &ParseError{Line: syntaxErr.Line, Message: message}
    }
    return &ParseError{Message: "XML error: " + err.Error()}
}

Data sanitization in internal/sanitizer/sanitizer.go redacts sensitive data:

func (s *Sanitizer) sanitizeCommentContent(content string) string {
    words := strings.Fields(content)
    for i, word := range words {
        should, _ := s.engine.ShouldRedactValue("comment", word)
        if should {
            words[i] = s.engine.Redact("comment", word)
        }
    }
    return strings.Join(words, " ")
}

Secrets Management#

Environment variable configuration in internal/config/config.go:

// Configuration precedence: CLI flags > env vars > config file > defaults
v.SetEnvPrefix("OPNDOSSIER")
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
v.AutomaticEnv()

// Explicit nested key binding for complex configs
nestedEnvBindings := map[string]string{
    "display.width": "DISPLAY_WIDTH",
    "logging.level": "LOGGING_LEVEL",
    "validation.strict": "VALIDATION_STRICT",
}
for key, envSuffix := range nestedEnvBindings {
    v.BindEnv(key, "OPNDOSSIER_"+envSuffix)
}

Configuration precedence order:

  1. CLI flags (highest priority)
  2. Environment variables (OPNDOSSIER_*)
  3. Configuration file (~/.opnDossier.yaml)
  4. Default values (lowest priority)

Sensitive Data Detection#

Pattern detection utilities in internal/sanitizer/patterns.go:

func IsPrivateIP(s string) bool {
    ip := net.ParseIP(s)
    if ip == nil {
        return false
    }
    // Check IPv4 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
    for _, r := range privateIPRanges {
        if r.Contains(ip) {
            return true
        }
    }
    return false
}

func LooksLikePassword(fieldName string) bool {
    lower := strings.ToLower(fieldName)
    passwordKeywords := []string{
        "password", "passwd", "pass", "secret", "key", "token",
        "credential", "auth", "prv", "private",
    }
    for _, kw := range passwordKeywords {
        if strings.Contains(lower, kw) {
            return true
        }
    }
    return false
}

func IsCertificate(s string) bool {
    if IsPEM(s) {
        return strings.Contains(s, "CERTIFICATE")
    }
    return IsBase64(s)
}

Dependency Security#

The project implements comprehensive dependency security:

  • Minimal dependencies - Reduced attack surface (except for cryptography—never write custom crypto)
  • gosec integration - Local security scanning via just scan
  • Supply chain security - Go module checksums verify integrity
  • SBOM generation - CycloneDX format for transparency
  • Continuous vulnerability scanning - .github/workflows/security.yml runs govulncheck (Go vulnerability database), CodeQL (semantic analysis), and Trivy (filesystem scan + misconfiguration detection) on push/PR and weekly

Operational Security Testing#

Automated security guarantees in internal/security_test.go:

func TestNoNetworkDependencies(t *testing.T) {
    forbiddenPackages := []string{
        "net/http",
        "net/rpc",
        "golang.org/x/net",
        // Analytics/telemetry packages
        "github.com/getsentry",
        "github.com/DataDog",
    }
    // Verifies no network-dependent imports exist
}

func TestNoTelemetry(t *testing.T) {
    telemetryPackages := []string{
        "sentry", "datadog", "newrelic", "bugsnag",
        "segment", "mixpanel", "amplitude",
    }
    // Verifies no telemetry package imports exist
}

Secure Build Practices#

Build Configuration#

The project uses secure build flags in justfile for hardened binaries:

# Secure release build
build-release:
    @CGO_ENABLED=0 {{ mise_exec }} go build -trimpath -ldflags="-s -w" -o {{ binary_name }} main.go

Security flags explained:

  • CGO_ENABLED=0 - Static compilation without C dependencies (prevents dynamic library attacks)
  • -trimpath - Removes local file system paths for reproducible builds
  • -ldflags="-s -w" - Strips debug information (-s) and symbol tables (-w)

GoReleaser configuration in .goreleaser.yaml applies these flags to all release builds:

builds:
  - id: opndossier
    binary: opndossier
    main: ./main.go
    env:
      - CGO_ENABLED=0
    goos: [freebsd, linux, darwin, windows]
    goarch: [amd64, arm64]
    flags:
      - -trimpath
    ldflags:
      - >-
        -s -w
        -X main.version={{.Version}}
        -X main.commit={{.Commit}}
        -X main.date={{ .CommitDate }}
    mod_timestamp: "{{ .CommitTimestamp }}"

Security Scanning#

Local scanning commands:

# Run gosec security scanner
just scan

# Generate SBOM (Software Bill of Materials)
just sbom

# Run all security checks
just security-all

gosec scanning in justfile:

scan:
    @echo "Running security scan..."
    @{{ mise_exec }} gosec ./...

Note: gosec runs locally but is not integrated into CI/CD pipeline. Continuous vulnerability scanning is handled by .github/workflows/security.yml which runs govulncheck, CodeQL, and Trivy on push/PR and weekly.

SBOM Generation#

Multiple SBOM generation approaches:

sboms:
  - id: binary-sbom
    documents:
      - "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.bom.json"
    artifacts: binary
    cmd: cyclonedx-gomod
    args: ["bin", "-json", "-output", "$document", "$artifact"]

  - id: module-sbom
    documents:
      - "{{ .ProjectName }}_{{ .Version }}_module.bom.json"
    artifacts: any
    cmd: cyclonedx-gomod
    args: ["mod", "-licenses", "-std", "-json", "-output", "$document", "../"]

SBOM generation in CI is gated to push-to-main only:

sbom:
  name: Generate SBOM
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  steps:
    - name: Build binary
      run: just build-release
    - name: Generate SBOM from binary (CycloneDX)
      uses: CycloneDX/gh-gomod-generate-sbom@efc74245d6802c8cefd925620515442756c70d8f
      with:
        args: bin -output sbom-binary.cyclonedx.json ./opndossier

The authoritative SBOMs are generated by GoReleaser at release time (see .goreleaser.yaml sboms: section). .github/workflows/sbom.yml runs on a weekly schedule to catch dependency drift between releases.

Supply Chain Security#

SLSA Provenance#

SLSA Level 3 provenance generation in release workflow:

provenance:
  needs: goreleaser
  runs-on: ubuntu-latest
  permissions:
    contents: write
    id-token: write
    attestations: write
  steps:
    - name: Generate attestation for checksums
      uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
      with:
        subject-path: dist/*checksums*.txt

    - name: Generate attestation for archives
      uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
      with:
        subject-path: |
          dist/*.tar.gz
          dist/*.zip

Cosign Keyless Signing#

Artifact signing with Cosign and Sigstore:

signs:
  - id: checksum-cosign
    cmd: cosign
    signature: "${artifact}.sigstore.json"
    args:
      - "sign-blob"
      - "--bundle=${signature}"
      - "${artifact}"
      - "--yes"
    artifacts: checksum
    output: true

Signature verification in release workflow:

- name: Verify checksum signature
  run: |
    cosign verify-blob \
      --certificate-identity "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@${GITHUB_REF}" \
      --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
      --bundle "$SIGNATURE" \
      "$CHECKSUMS"

Additional Security Monitoring#

  • OSSF Scorecard - Security health metrics
  • Dependabot - Automated dependency updates
  • govulncheck - Go vulnerability database scanning (continuous via security.yml)
  • CodeQL - Semantic static application security testing (continuous via security.yml)
  • Trivy - Filesystem scan + misconfiguration detection (continuous via security.yml)

Implementation and Enforcement#

Code Quality Standards#

The project enforces comprehensive quality requirements:

  • Linting - All code must pass golangci-lint
  • Test Coverage - >80% coverage required for new functionality
  • Documentation - User-facing changes require documentation updates
  • Go Best Practices - Follow Google Go Style Guide
  • Pre-commit Checks - All checks must pass before PR submission

Error Handling Pattern#

Consistent error wrapping throughout the codebase:

// Validation errors with wrapped context
func validateLevel(level string) error {
    switch strings.ToLower(level) {
    case "debug", "info", "warn", "warning", "error", "":
        return nil
    default:
        return fmt.Errorf("%w: %s", ErrInvalidLogLevel, level)
    }
}

// Parser errors with location context
func handleXMLError(err error, dec *xml.Decoder) error {
    if wrappedErr := WrapXMLSyntaxErrorWithOffset(err, "opnsense", dec); wrappedErr != nil {
        return fmt.Errorf("failed to decode XML: %w", wrappedErr)
    }
    return fmt.Errorf("failed to read token: %w", err)
}

// Plugin manager errors with operation context
result, err := pm.registry.RunComplianceChecks(device, pluginNames)
if err != nil {
    return nil, fmt.Errorf("compliance audit failed: %w", err)
}

Custom error types with Unwrap support in internal/export/file.go:

type Error struct {
    Operation string
    Path string
    Message string
    Cause error // Wrapped error
}

func (e *Error) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("export %s failed for %s: %s (caused by: %v)", 
            e.Operation, e.Path, e.Message, e.Cause)
    }
    return fmt.Sprintf("export %s failed for %s: %s", 
        e.Operation, e.Path, e.Message)
}

func (e *Error) Unwrap() error {
    return e.Cause
}

Structured Logging#

charmbracelet/log wrapper in internal/logging/logger.go:

type Logger struct {
    *log.Logger
}

type Config struct {
    Level string // "debug", "info", "warn", "error"
    Format string // "text" or "json"
    Output io.Writer
    ReportCaller bool
    ReportTimestamp bool
}

// Usage examples
logger.Info("Starting conversion", "input_file", inputPath)
logger.Debug("Processing section", "section", sectionName, "count", itemCount)

// Field-based logging
fieldLogger := logger.WithFields("key1", "value1", "key2", "value2")
fieldLogger.Info("test message")

// Sub-logger with subsystem
subLogger := logger.Sub("parser")
subLogger.Info("test message") // Adds "subsystem": "parser"

Logger configuration from cmd/root.go:

defaultLoggerConfig = logging.Config{
    Level: "info",
    Format: "text",
    Output: os.Stderr,
    ReportCaller: true,
    ReportTimestamp: true,
}

Testing Standards#

Table-driven tests in internal/cfgparser/xml_test.go:

func TestXMLParser_Parse(t *testing.T) {
    tests := []struct {
        name string
        input string
        expected *schema.OpnSenseDocument
        wantErr bool
    }{
        {
            name: "valid config",
            input: `<opnsense><version>1.2.3</version></opnsense>`,
            expected: &schema.OpnSenseDocument{Version: "1.2.3"},
            wantErr: false,
        },
        {
            name: "invalid xml",
            input: `<opnsense><version>1.2.3</version>`,
            expected: nil,
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            p := NewXMLParser()
            result, err := p.Parse(context.Background(), strings.NewReader(tt.input))

            if tt.wantErr {
                require.Error(t, err)
                assert.Nil(t, result)
            } else {
                require.NoError(t, err)
                assert.Equal(t, tt.expected.Version, result.Version)
            }
        })
    }
}

Benchmark tests in internal/cfgparser/xml_test.go:

func BenchmarkXMLParser_Parse(b *testing.B) {
    sampleFile := filepath.Join("testdata", "config.xml.sample")
    parser := NewXMLParser()

    for b.Loop() {
        file, err := os.Open(sampleFile)
        if err != nil {
            b.Fatal(err)
        }

        _, err = parser.Parse(context.Background(), file)
        if err != nil {
            _ = file.Close()
            b.Fatal(err)
        }

        _ = file.Close()
    }
}

Test commands from justfile:

just test # Run all tests
just test-coverage # Generate coverage report
just test-race # Run with race detector
just coverage # Open coverage in browser
just bench # Run benchmarks

Pre-commit Hooks#

Pre-commit configuration in .pre-commit-config.yaml:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-added-large-files
        args: ["--maxkb=1024"]
      - id: check-case-conflict
      - id: check-merge-conflict
      - id: check-yaml
      - id: check-json
      - id: end-of-file-fixer
      - id: trailing-whitespace

Installation via justfile:

install:
    @mise install
    @{{ mise_exec }} pre-commit install --hook-type pre-commit --hook-type commit-msg --hook-type pre-push
    @{{ mise_exec }} go mod tidy

CI/CD Pipeline#

Comprehensive CI checks in .github/workflows/ci.yml:

  • Lint job - golangci-lint with strict configuration
  • Test jobs - Multi-platform (Ubuntu, macOS, Windows) and Go version matrix (stable, oldstable) with >80% coverage
  • Integration tests - Real OPNsense configuration parsing
  • Coverage reporting - Codecov integration
  • Build verification - Ensure clean builds on all platforms
  • SBOM generation - CycloneDX (push-to-main only)

Local CI equivalent:

just ci-check # Runs all CI checks locally

Pull Request Requirements#

PR process from CONTRIBUTING.md:

  1. All automated CI checks must pass
  2. Commit messages must follow Conventional Commits
  3. All commits require DCO sign-off (git commit -s)
  4. Minimum 80% test coverage for new code
  5. At least one maintainer review required
  6. Breaking changes require community discussion

Relevant Code Files#

File PathPurposeKey Security/Standards Features
justfileBuild automationSecure build flags, gosec integration, SBOM generation
.goreleaser.yamlRelease configurationSecure builds, SBOM, Cosign signing
cliff.tomlChangelog generationConventional Commits parsing, filtering
.pre-commit-config.yamlPre-commit hooksFile validation, format checking
internal/cfgparser/xml.goXML parsingSize limits, XXE prevention, charset validation
internal/cfgparser/errors.goError handlingStructured errors without sensitive data exposure
internal/config/config.goConfigurationOPNDOSSIER_* env var handling, secure defaults
internal/sanitizer/patterns.goPattern detectionIP, password, certificate detection
internal/sanitizer/sanitizer.goData sanitizationSensitive data redaction
internal/validator/opnsense.goConfiguration validationIP, port, semantic validation
internal/security_test.goSecurity testingNo network, no telemetry verification
internal/logging/logger.goStructured loggingcharmbracelet/log wrapper
.github/workflows/ci.ymlCI pipelineMulti-platform testing, Go version matrix, SBOM generation
.github/workflows/security.ymlSecurity scanninggovulncheck, CodeQL, Trivy (continuous + weekly)
.github/workflows/release.ymlRelease workflowSLSA provenance, Cosign verification
.github/workflows/scorecard.ymlSecurity scorecardOSSF security metrics