From a84941d68168a1ed9ea7423b6477ae8218be73f9 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 14 May 2026 20:14:11 -0500 Subject: [PATCH] Rationalize default configuration file paths and update documentation --- README.md | 2 +- docs/architecture/architecture.md | 8 ++--- docs/architecture/public-contract.md | 2 +- docs/configuration.md | 5 +-- docs/release-checklist.md | 4 ++- internal/cli/run.go | 17 ++++++---- internal/cli/run_test.go | 49 ++++++++++++++++++++++++++++ internal/core/config/env.go | 10 +++++- 8 files changed, 80 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 560f5cd..912f5d7 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ For subprocess orchestration guidance, see [`docs/subprocess-operations.md`](doc Precedence: 1. defaults -2. config file (`--config`, `AUDITA_CONFIG`, or `/etc/audita/config.yml` when present) +2. config file (`--config`, `AUDITA_CONFIG`, or default search paths when present: `/usr/local/etc/audita/config.yml`, then `/etc/audita/config.yml`) 3. environment (`AUDITA_*`) 4. CLI flags diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md index 6e53bda..3e2797e 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/architecture.md @@ -198,7 +198,7 @@ audita config print-effective [--config ] Current runtime flow (`internal/cli/run.go`): 1. Build runtime config from: - defaults; - - file config source (`--config`, `AUDITA_CONFIG`, or `/etc/audita/config.yml` when present); + - file config source (`--config`, `AUDITA_CONFIG`, or default search paths when present: `/usr/local/etc/audita/config.yml`, then `/etc/audita/config.yml`); - environment overrides; - CLI overrides. 2. Parse flags and apply CLI overrides. @@ -304,9 +304,9 @@ File-config source behavior: - required to exist, otherwise process fails clearly. - `AUDITA_CONFIG` (when `--config` is not provided): - required to exist, otherwise process fails clearly. -- default path `/etc/audita/config.yml` (when neither explicit source is provided): - - used only when present; - - silently ignored when missing. +- default paths `/usr/local/etc/audita/config.yml`, then `/etc/audita/config.yml` (when neither explicit source is provided): + - first existing path in that order is used; + - both missing is silently ignored. Versioned file-config behavior (`internal/core/config/file_config.go`): - supported version: `version: 1`; diff --git a/docs/architecture/public-contract.md b/docs/architecture/public-contract.md index ffcc548..2d65f48 100644 --- a/docs/architecture/public-contract.md +++ b/docs/architecture/public-contract.md @@ -54,7 +54,7 @@ Precedence for `audita process`: Config source behavior: - `--config `: missing path is a clear failure - `AUDITA_CONFIG`: missing path is a clear failure -- default `/etc/audita/config.yml`: missing file is non-fatal +- defaults `/usr/local/etc/audita/config.yml`, then `/etc/audita/config.yml`: both missing is non-fatal ## Supported transcript input forms diff --git a/docs/configuration.md b/docs/configuration.md index d2f88fc..416910d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -26,13 +26,14 @@ For `audita process`, config path resolution is: 1. `--config ` if provided 2. `AUDITA_CONFIG` if set and `--config` is not provided -3. default `/etc/audita/config.yml` if present +3. default `/usr/local/etc/audita/config.yml` if present +4. fallback default `/etc/audita/config.yml` if present Missing-file behavior: - missing `--config` path: hard failure; - missing `AUDITA_CONFIG` path: hard failure; -- missing `/etc/audita/config.yml`: non-fatal, run continues. +- missing both default-path files: non-fatal, run continues. ## Precedence model diff --git a/docs/release-checklist.md b/docs/release-checklist.md index dd4e4af..e35c216 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -16,7 +16,9 @@ Use this checklist before cutting a pre-1.0 or 1.0 release candidate. - `audita config print-effective --config ` - Confirm precedence behavior: - defaults -> file config -> environment -> CLI. -- Confirm missing `/etc/audita/config.yml` is non-fatal when `--config`/`AUDITA_CONFIG` are unset. +- Confirm default config search order: + - `/usr/local/etc/audita/config.yml` first, then `/etc/audita/config.yml`. +- Confirm missing both default-path config files is non-fatal when `--config`/`AUDITA_CONFIG` are unset. ## Output schema checks diff --git a/internal/cli/run.go b/internal/cli/run.go index 87f3961..504b6e4 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -982,13 +982,15 @@ func findConfigPathOverride(args []string) (path string, set bool, err error) { return "", false, nil } +var statConfigPath = os.Stat + func resolveConfigPath(cliConfigPath string, cliConfigPathSet bool, lookup func(string) (string, bool)) (path string, source string, err error) { if cliConfigPathSet { path = strings.TrimSpace(cliConfigPath) if path == "" { return "", "", fmt.Errorf("--config requires a non-empty path") } - if _, statErr := os.Stat(path); statErr != nil { + if _, statErr := statConfigPath(path); statErr != nil { if os.IsNotExist(statErr) { return "", "", fmt.Errorf("config file not found: %s", path) } @@ -1002,7 +1004,7 @@ func resolveConfigPath(cliConfigPath string, cliConfigPathSet bool, lookup func( if path == "" { return "", "", fmt.Errorf("AUDITA_CONFIG must not be empty") } - if _, statErr := os.Stat(path); statErr != nil { + if _, statErr := statConfigPath(path); statErr != nil { if os.IsNotExist(statErr) { return "", "", fmt.Errorf("config file not found: %s", path) } @@ -1011,11 +1013,12 @@ func resolveConfigPath(cliConfigPath string, cliConfigPathSet bool, lookup func( return path, "env", nil } - defaultPath := config.DefaultConfigPath - if _, statErr := os.Stat(defaultPath); statErr == nil { - return defaultPath, "default", nil - } else if !os.IsNotExist(statErr) { - return "", "", fmt.Errorf("cannot access config file %s: %w", defaultPath, statErr) + for _, defaultPath := range config.DefaultConfigSearchPaths { + if _, statErr := statConfigPath(defaultPath); statErr == nil { + return defaultPath, "default", nil + } else if !os.IsNotExist(statErr) { + return "", "", fmt.Errorf("cannot access config file %s: %w", defaultPath, statErr) + } } return "", "", nil } diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index f251a97..d21303c 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -110,6 +110,55 @@ func TestResolveConfigPathDefaultIgnoredWhenMissing(t *testing.T) { } } +func TestResolveConfigPathDefaultPrefersUsrLocalOverEtc(t *testing.T) { + oldStat := statConfigPath + statConfigPath = func(path string) (os.FileInfo, error) { + if path == config.DefaultConfigPathUsrLocal || path == config.DefaultConfigPath { + return nil, nil + } + return nil, os.ErrNotExist + } + t.Cleanup(func() { statConfigPath = oldStat }) + + lookup := func(string) (string, bool) { return "", false } + path, source, err := resolveConfigPath("", false, lookup) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if source != "default" { + t.Fatalf("expected default source, got %q", source) + } + if path != config.DefaultConfigPathUsrLocal { + t.Fatalf("expected %q, got %q", config.DefaultConfigPathUsrLocal, path) + } +} + +func TestResolveConfigPathDefaultFallsBackToEtc(t *testing.T) { + oldStat := statConfigPath + statConfigPath = func(path string) (os.FileInfo, error) { + if path == config.DefaultConfigPathUsrLocal { + return nil, os.ErrNotExist + } + if path == config.DefaultConfigPath { + return nil, nil + } + return nil, os.ErrNotExist + } + t.Cleanup(func() { statConfigPath = oldStat }) + + lookup := func(string) (string, bool) { return "", false } + path, source, err := resolveConfigPath("", false, lookup) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if source != "default" { + t.Fatalf("expected default source, got %q", source) + } + if path != config.DefaultConfigPath { + t.Fatalf("expected %q, got %q", config.DefaultConfigPath, path) + } +} + func TestRunConfigValidateSuccess(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer diff --git a/internal/core/config/env.go b/internal/core/config/env.go index f9ff257..d924a01 100644 --- a/internal/core/config/env.go +++ b/internal/core/config/env.go @@ -6,7 +6,15 @@ import ( "strconv" ) -const DefaultConfigPath = "/etc/audita/config.yml" +const ( + DefaultConfigPath = "/etc/audita/config.yml" + DefaultConfigPathUsrLocal = "/usr/local/etc/audita/config.yml" +) + +var DefaultConfigSearchPaths = []string{ + DefaultConfigPathUsrLocal, + DefaultConfigPath, +} func LoadFromEnv() (Config, error) { cfg := Default()