diff --git a/.claude/skills/running-tests/SKILL.md b/.claude/skills/running-tests/SKILL.md index 9e2ab3d..774e0ba 100644 --- a/.claude/skills/running-tests/SKILL.md +++ b/.claude/skills/running-tests/SKILL.md @@ -26,7 +26,8 @@ Default to targeted. Only run the full suite for broad changes or as a pre-PR ga | What changed | Run | | ------------------------------------------------------------- | ---------------------------------------- | | A specific functional area (e.g. format handling, log levels) | The matching suite(s) only | -| `logging.sh` core logic (init, output routing, sanitization) | Full suite | +| `logging.sh` core logic (init, output routing, sanitization) | Full suite, then the cross-shell run | +| Any change to `logging.sh` | Cross-shell run (see below) | | `install.sh` | `test_install` only | | Config file parsing | `test_config` and `test_config_security` | | Security-related code | Relevant security suites + full suite | @@ -45,6 +46,7 @@ After discovering available suites, match by name: * Journal integration → `test_journal_logging` * Sanitisation (ANSI, newlines, paths) → `test_ansi_injection`, `test_unsafe_newlines`, `test_path_traversal`, `test_script_name_sanitization` * Security hardening → `test_environment_security`, `test_sensitive_data`, `test_config_security`, `test_toctou_protection` +* Behaviour that must match in every shell → `test_shell_compat`, `test_posix_compat`, `test_validator_equivalence`, `test_sanitize_equivalence` * `install.sh` → `test_install` * JUnit/CI reporting → `test_junit_output` @@ -64,28 +66,45 @@ Or via Make (quieter output): make test ``` +### Cross-shell run (required after any change to `logging.sh`) + +`logging.sh` runs in bash 3.2+, bash `--posix`, zsh, dash and busybox ash. A plain `./tests/run_tests.sh` runs the four cross-shell suites under bash only, so a failure that +appears only in dash, busybox or zsh is missed. After any change to `logging.sh`, also run: + +```bash +TEST_SHELLS="bash,bash --posix,zsh,dash,busybox sh" ./tests/run_tests.sh test_shell_compat test_validator_equivalence test_sanitize_equivalence test_posix_compat +``` + +* Commas separate shell commands, so a command can carry flags (`bash --posix`, `busybox sh`). +* A shell that is not installed is skipped. Add `TEST_REQUIRE_SHELLS="zsh dash busybox"` to make a missing shell fail the run, as CI does. +* A skipped shell is not a pass. Report which shells actually ran. +* See [docs/testing.md](../../../docs/testing.md#shell-matrix-suites) for the details of each suite. + ### Single suite ```bash ./tests/run_tests.sh ``` -`` is the filename without the `test_` prefix **and** without `.sh`: +`` is the filename without `.sh`. It **keeps** the `test_` prefix: ```bash # Correct -./tests/run_tests.sh log_levels -./tests/run_tests.sh config -./tests/run_tests.sh initialization +./tests/run_tests.sh test_log_levels +./tests/run_tests.sh test_config +./tests/run_tests.sh test_initialization # Also accepted — runner strips the extension ./tests/run_tests.sh test_log_levels.sh + +# Wrong — "Test file not found" +./tests/run_tests.sh log_levels ``` ### Multiple suites in one invocation ```bash -./tests/run_tests.sh config config_security runtime_config +./tests/run_tests.sh test_config test_config_security test_runtime_config ``` ### JUnit XML output (for CI artefacts) @@ -145,6 +164,7 @@ Exit code `0` = all passed. Exit code `1` = at least one failure. Fix all failur ## Mistakes to avoid * Do **not** run from inside the `tests/` directory — always run from repo root. -* Do **not** pass the full path (`tests/test_config.sh`) — pass the suite name only. +* Do **not** pass the full path (`tests/test_config.sh`) — pass the suite name only, with its `test_` prefix. * Do **not** skip the discovery step and guess suite names — the list changes as the project grows. +* Do **not** treat a bash-only pass as enough after changing `logging.sh` — run the cross-shell command above. * `test_helpers.sh` and `test_example.sh` are never valid suite arguments. diff --git a/.claude/skills/security-review/SKILL.md b/.claude/skills/security-review/SKILL.md index 5dbc2ce..9d10fee 100644 --- a/.claude/skills/security-review/SKILL.md +++ b/.claude/skills/security-review/SKILL.md @@ -14,7 +14,7 @@ description: > Apply to any change that touches: -* Message processing or formatting (`_sanitize_log_message`, `_strip_ansi_codes`, format strings) +* Message processing or formatting (`_sanitize_log_message_into`, `_strip_ansi_codes_into`, format strings) * File path handling (`init_logger`, `_parse_config_file`, `_validate_config_file_path`) * Configuration parsing (new config keys, value parsing, `_parse_config_file`) * Journal/`logger` integration (`_find_and_validate_logger`, `_write_to_journal`) @@ -25,15 +25,26 @@ Apply to any change that touches: ## Domain 1: Input sanitization (log injection) -Every message written to file or journal passes through `_sanitize_log_message`, which calls -both `_strip_ansi_codes` (strips CSI, OSC, DCS, PM, APC sequences) and newline replacement. +Every message written to file or journal passes through `_sanitize_log_message_into`, which +replaces newline, carriage return and tab and calls `_strip_ansi_codes_into`. That removes every CSI +sequence, OSC/DCS/SOS/PM/APC strings, and other control characters (C0 other than newline, +carriage return and tab; C1; U+2028 and U+2029; bidirectional formatting characters U+202A to +U+202E and U+2066 to U+2069). The `_into` functions set `_LOGGER_TEXT` and avoid a subshell. The +older `_sanitize_log_message` and `_strip_ansi_codes` only print that result, and the message +path does not use them. + +These characters are kept on purpose (audit finding INF-01), so do not report them as a gap: +U+200E and U+200F (left-to-right and right-to-left marks), U+061C (Arabic letter mark), U+200B +(zero width space) and U+FEFF (byte order mark). They cannot reverse a line the way the removed +overrides and isolates can. **What to check:** * New `_log_message` callers pass user-supplied content as the `$message` argument — not as part of `$level_name`, which is never sanitized (it comes from internal constants only). -* No new code path writes directly to `$LOG_FILE` without first calling `_sanitize_log_message`. -* If you add a new ANSI sequence type, update `_strip_ansi_codes` **and** add a test in +* No new code path writes directly to `$LOG_FILE` without first calling + `_sanitize_log_message_into`. +* If you add a new ANSI sequence type, update `_strip_ansi_codes_into` **and** add a test in `test_ansi_injection.sh`. * Format string changes: `LOG_FORMAT` uses `%d`, `%l`, `%s`, `%m`, `%z` only. Never interpolate raw user data into the format string itself. @@ -73,8 +84,8 @@ immediate validation to minimize the TOCTOU window. **Test suites for this domain:** ```bash -./tests/run_tests.sh toctou_protection -./tests/run_tests.sh path_traversal +./tests/run_tests.sh test_toctou_protection +./tests/run_tests.sh test_path_traversal ``` --- @@ -102,8 +113,8 @@ type-specific validation. **Test suites for this domain:** ```bash -./tests/run_tests.sh config_security -./tests/run_tests.sh config +./tests/run_tests.sh test_config_security +./tests/run_tests.sh test_config ``` --- @@ -119,13 +130,19 @@ poisoning constants. * New global constants that should not be user-overridable: unset before assignment, then declare `readonly`. Follow the existing guard pattern: - ```bash - if ! readonly -p 2>/dev/null | grep -q "declare -[^ ]*r[^ ]* MY_CONST="; then + ```sh + if ! _is_readonly MY_CONST; then unset MY_CONST 2>/dev/null || true - readonly MY_CONST="value" + _logger_readonly MY_CONST="value" fi ``` + `_is_readonly` tests a variable by trying to unset it in a subshell, which fails only for a + readonly variable. Do not test with `readonly -p | grep ...`: the output differs between shells, + and a value that contains a newline and text such as `readonly NAME=` can make a variable look + readonly (the PR #147 review showed this). `_logger_readonly` keeps the constant global in zsh + when the file is sourced from inside a function. Do not call `readonly` directly. + * New mutable state variables (like `LOG_FILE`): these must not be `readonly`. Ensure they are reset to a safe default on each `source` so pre-set environment values cannot persist. * `LOGGER_FILE_ERROR_REPORTED` and `LOGGER_JOURNAL_ERROR_REPORTED`: explicitly unset on @@ -137,7 +154,7 @@ poisoning constants. **Test suites for this domain:** ```bash -./tests/run_tests.sh environment_security +./tests/run_tests.sh test_environment_security ``` --- @@ -183,7 +200,7 @@ during interactive sessions regardless of level configuration. **Test suites for this domain:** ```bash -./tests/run_tests.sh sensitive_data +./tests/run_tests.sh test_sensitive_data ``` --- @@ -218,10 +235,14 @@ value, and `SCRIPT_NAME` at the end of `init_logger` regardless of source. Run all security suites together: ```bash -./tests/run_tests.sh ansi_injection unsafe_newlines path_traversal toctou_protection \ - config_security environment_security sensitive_data script_name_sanitization +./tests/run_tests.sh test_ansi_injection test_unsafe_newlines test_path_traversal \ + test_toctou_protection test_config_security test_environment_security \ + test_sensitive_data test_script_name_sanitization ``` +The suite arguments need the `test_` prefix. `./tests/run_tests.sh ansi_injection` fails with +"Test file not found". + After any security-related change, always follow with the full suite: ```bash @@ -233,8 +254,8 @@ After any security-related change, always follow with the full suite: ## Security checklist before committing * [ ] New external input is sanitized before being written to file, journal, or console -* [ ] No new code path bypasses `_sanitize_log_message` -* [ ] No new constant is set without unset + readonly guard against env override +* [ ] No new code path bypasses `_sanitize_log_message_into` +* [ ] No new constant is set without an `_is_readonly` guard, unset and `_logger_readonly` * [ ] New mutable state is reset to safe default on source * [ ] New file path handling uses noclobber → symlink check → existence check → regular-file check → writable check * [ ] New config key uses an appropriate typed validator; metacharacter injection is not possible diff --git a/.claude/skills/writing-tests/SKILL.md b/.claude/skills/writing-tests/SKILL.md index 113c117..4793cf6 100644 --- a/.claude/skills/writing-tests/SKILL.md +++ b/.claude/skills/writing-tests/SKILL.md @@ -116,16 +116,36 @@ if [[ -z "${ZSH_VERSION:-}" ]]; then fi ``` -## Step 6 — Run your suite, then the full suite +## Step 6 — Cover other shells when behaviour must match + +`logging.sh` is POSIX sh plus `local` and also runs in bash `--posix`, zsh, dash and busybox ash. The suites run under bash. If the new behaviour must be the same in every shell, add a +scenario instead of (or as well as) a bash test: + +* Create `tests/compat/scenarios/.sh`: a short POSIX sh script starting with `# shellcheck shell=sh`, which sources `"$LOGGER_ROOT/logging.sh"` and writes any log file to + `"$SCENARIO_DIR/test.log"`. An optional `# compat-locale: C.UTF-8` header line sets the locale (default `C`). +* No registration is needed. `test_shell_compat` runs every scenario under each shell in `TEST_SHELLS` and compares the results with bash. +* List a shell and scenario that is expected to differ in `tests/compat/known_failures.txt` (`|`). Never add an entry to hide a new regression. +* Do **not** regenerate the golden files in `tests/compat/golden/` from the current `logging.sh`. They record behaviour before the POSIX conversion. +* Helpers for loops over shells are in `tests/test_helpers.sh`: `test_shell_list`, `require_shell`, `run_with_logger_in_shell`, `shell_counts_characters`, `run_with_timeout`. + +See [Tests that run under more than one shell](../../../docs/writing-tests.md#tests-that-run-under-more-than-one-shell). + +## Step 7 — Run your suite, then the full suite ```bash # New suite only first -./tests/run_tests.sh +./tests/run_tests.sh test_ # Then confirm no regressions ./tests/run_tests.sh ``` +After any change to `logging.sh`, also run the cross-shell suites (see the `running-tests` skill): + +```bash +TEST_SHELLS="bash,bash --posix,zsh,dash,busybox sh" ./tests/run_tests.sh test_shell_compat test_validator_equivalence test_sanitize_equivalence test_posix_compat +``` + ## Checklist before committing * [ ] Function called at the bottom of the suite file @@ -134,5 +154,7 @@ fi * [ ] Every assertion uses `|| return` * [ ] All file I/O uses `$TEST_DIR` * [ ] `init_logger` called after `start_test`, not at suite top-level -* [ ] Suite passes: `./tests/run_tests.sh ` +* [ ] Suite passes: `./tests/run_tests.sh test_` * [ ] Full suite still passes: `./tests/run_tests.sh` +* [ ] If `logging.sh` changed: cross-shell run passes with `TEST_SHELLS` (or each skipped shell is reported) +* [ ] If the behaviour must match across shells: scenario added in `tests/compat/scenarios/` diff --git a/README.md b/README.md index 7fef1c4..1136537 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,28 @@ ![Logo](assets/images/logo-640x320.png) -A flexible, reusable logging module for Bash scripts that provides standardized logging functionality with various configuration options. +A flexible, reusable logging module for shell scripts that provides standardized logging functionality with various configuration options. +It runs in bash 3.2+, zsh, dash and busybox ash from one file. > [!NOTE] -> This is a expansion of the originally published [bash_logging GitHub gist](https://gist.github.com/GingerGraham/99af97eed2cd89cd047a2088947a5405) published by [@GingerGraham](https://github.com/GingerGraham). +> **Upgrading from 2.x?** 3.0 is a major release with some behaviour changes. See [Upgrading to 3.0](docs/upgrading-to-3.0.md) before you update. ## Primary artifact The core deliverable of this repository is **[logging.sh](logging.sh)**. It is intentionally kept -as a single, self-contained (and yes, long) Bash file so you can drop it next to your -scripts and `source` it without any packaging steps. Everything else in the repo - docs, +as a single, self-contained (and yes, long) shell file, written in POSIX sh plus `local`, so you can drop it next to your +scripts and `source` it (or `.` it in a POSIX sh script) without any packaging steps. The file keeps a `#!/usr/bin/env bash` line only so that +running it directly prints its usage message. Everything else in the repo - docs, demos, and pipeline scripts - exists to support using that file. +> [!NOTE] +> This is an expansion of the originally published +> [bash_logging GitHub gist](https://gist.github.com/GingerGraham/99af97eed2cd89cd047a2088947a5405) +> published by [@GingerGraham](https://github.com/GingerGraham). + ## Features +* Runs in bash 3.2+, bash `--posix`, zsh, dash and busybox ash from one file * Standard syslog log levels (DEBUG, INFO, WARN, ERROR, CRITICAL, etc.) * Console output with color-coding by severity * Configurable stdout/stderr output stream split @@ -36,7 +44,8 @@ demos, and pipeline scripts - exists to support using that file. * Runtime configuration changes * Special handling for sensitive data * Secure-by-default newline sanitization to prevent log injection -* Secure-by-default ANSI code stripping to prevent terminal manipulation attacks +* Secure-by-default removal of terminal control sequences and other control characters to prevent terminal manipulation attacks + (see [Upgrading to 3.0](docs/upgrading-to-3.0.md#messages-and-control-characters) for the full list) * Configurable log line length limits for DoS resistance * TOCTOU race condition protection during log file creation @@ -53,12 +62,15 @@ demos, and pipeline scripts - exists to support using that file. `scripts/check-bash32-compat.sh` rejects bash 4+ syntax and `scripts/check-posix-compat.sh` rejects syntax that dash does not support. See [Shell Compatibility](docs/shell-compatibility.md) for the few documented differences between shells (script name detection and how truncation -counts multi-byte characters). Upgrading from 2.x? See [Upgrading to 3.0](docs/upgrading-to-3.0.md). +counts multi-byte characters). ## Quick Start +In a bash or zsh script: + ```bash -# Source the logging module (use ". /path/to/logging.sh" in a POSIX sh script) +#!/usr/bin/env bash +# Source the logging module source /path/to/logging.sh # Initialize the logger @@ -69,6 +81,18 @@ log_info "Application started" log_error "Something went wrong" ``` +In a POSIX sh script (dash, busybox ash, or any `/bin/sh` on Debian, Ubuntu and Alpine), use `.` instead of `source`: + +```sh +#!/bin/sh +# Source the logging module +. /path/to/logging.sh + +init_logger +log_info "Application started" +log_error "Something went wrong" +``` + See [Getting Started](docs/getting-started.md) for detailed installation and basic usage instructions. ## Documentation diff --git a/SECURITY.md b/SECURITY.md index 07e3fe7..0320156 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -242,7 +242,7 @@ This minimal dependency surface reduces supply chain risk. When using bash-logger in your applications: 1. **Never log sensitive data** with standard functions -2. **Set restrictive permissions** on log files (600 or 640) +2. **Set restrictive permissions** on log files. A file that `init_logger` creates is already mode 600. Use 640 and `chgrp` only to share it with a group 3. **Validate user input** before logging 4. **Review journal access** if using systemd integration 5. **Rotate logs** to prevent information accumulation diff --git a/docs/README.md b/docs/README.md index c798420..aaaa2b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Bash Logger Documentation -Welcome to the bash-logger documentation! This directory contains comprehensive guides for using the bash logging module. +Welcome to the bash-logger documentation! This directory contains comprehensive guides for using bash-logger. ## Getting Started diff --git a/docs/api-reference.md b/docs/api-reference.md index 0849fd4..797b7c9 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -64,25 +64,25 @@ init_logger [options] **Options:** -| Option | Short | Description | Default | -| ----------------------------- | ----- | ------------------------------------------------ | ----------- | -| `--level LEVEL` | `-d` | Set log level (DEBUG, INFO, WARN, ERROR, etc.) | INFO | -| `--log FILE` | `-l` | Write logs to file | (none) | -| `--quiet` | `-q` | Disable console output | false | -| `--verbose` | `-v` | Enable DEBUG level logging | false | -| `--journal` | `-j` | Enable system journal logging | false | -| `--tag TAG` | `-t` | Set journal tag | (script) | -| `--facility FACILITY` | `-F` | Set syslog facility for journal logging | daemon | -| `--utc` | `-u` | Use UTC timestamps instead of local time | false | -| `--format FORMAT` | `-f` | Set log message format | (see below) | -| `--color` | | Force color output | auto | -| `--no-color` | | Disable color output | auto | -| `--config FILE` | `-c` | Load configuration from INI file | (none) | -| `--stderr-level LEVEL` | `-e` | Messages at/above this level go to stderr | ERROR | -| `--unsafe-allow-newlines` | `-U` | Allow newlines in log messages (unsafe) | false | -| `--unsafe-allow-ansi-codes` | `-A` | Allow ANSI escape codes in log messages (unsafe) | false | -| `--max-line-length LENGTH` | | Max message length before formatting | 4096 | -| `--max-journal-length LENGTH` | | Max message length for journal | 4096 | +| Option | Short | Description | Default | +| ----------------------------- | ----- | ----------------------------------------------------------- | ----------- | +| `--level LEVEL` | `-d` | Set log level (DEBUG, INFO, WARN, ERROR, etc.) | INFO | +| `--log FILE` | `-l` | Write logs to file | (none) | +| `--quiet` | `-q` | Disable console output | false | +| `--verbose` | `-v` | Enable DEBUG level logging | false | +| `--journal` | `-j` | Enable system journal logging | false | +| `--tag TAG` | `-t` | Set journal tag (validated, see below) | (script) | +| `--facility FACILITY` | `-F` | Set syslog facility for journal logging | daemon | +| `--utc` | `-u` | Use UTC timestamps instead of local time | false | +| `--format FORMAT` | `-f` | Set log message format | (see below) | +| `--color` | | Force color output | auto | +| `--no-color` | | Disable color output | auto | +| `--config FILE` | `-c` | Load configuration from INI file | (none) | +| `--stderr-level LEVEL` | `-e` | Messages at/above this level go to stderr | ERROR | +| `--unsafe-allow-newlines` | `-U` | Allow newlines in log messages (unsafe) | false | +| `--unsafe-allow-ansi-codes` | `-A` | Keep escape sequences and other control characters (unsafe) | false | +| `--max-line-length LENGTH` | | Max message length before formatting | 4096 | +| `--max-journal-length LENGTH` | | Max message length for journal | 4096 | **Default Format:** @@ -607,8 +607,9 @@ log_to_journal LEVEL MESSAGE the message is silently suppressed with return code `0`, matching all other log functions. This suppression happens before any logger availability check, so no warning is emitted even when `logger` is not available. -* Applies the same sanitisation (newline and ANSI stripping) and truncation rules as all - other log functions. +* Applies the same sanitisation (newline replacement, and removal of terminal control sequences + and other control characters) and truncation rules as all other log functions. See + [Sensitive Data](sensitive-data.md#terminal-control-sequence-injection-prevention) for what is removed. * Console and file output follow the normal `CONSOLE_LOG` and `LOG_FILE` settings. * When `logger` is not available or fails validation (for example when the binary is missing, is outside the trusted system directories, or no longer matches the path validated earlier) @@ -840,7 +841,13 @@ set_journal_tag "tag" **Parameters:** -* `tag` - String identifier for journal entries +* `tag` - String identifier for journal entries. It must not be empty or longer than 64 characters, and must not contain control characters or shell metacharacters. + See [Tag Rules](journal-logging.md#tag-rules) + +**Returns:** + +* `0` - The tag was changed +* `1` - The tag is invalid. A message explains why, and the previous tag is kept **Examples:** @@ -1042,7 +1049,7 @@ set_unsafe_allow_ansi_codes false * Logs a CONFIG message documenting the change (in red when enabling unsafe mode as a warning) * Takes effect immediately for all subsequent log messages -* Affects `_strip_ansi_codes()` behavior in `_sanitize_log_message()` +* Keeps escape sequences and the other control characters in messages while enabled **Security Considerations:** diff --git a/docs/configuration.md b/docs/configuration.md index 33a98eb..bfb0af7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration -The Bash Logging Module supports loading configuration from INI-style files. This is useful for centralizing logging +bash-logger supports loading configuration from INI-style files. This is useful for centralizing logging configuration across multiple scripts, allowing users to customize logging without modifying scripts, and separating configuration from code. @@ -89,8 +89,8 @@ verbose = false # Warning: true disables sanitization and can allow log injection unsafe_allow_newlines = false -# Allow ANSI color codes in log messages: true/false -# Warning: true disables ANSI sanitization and can allow escape-sequence injection +# Keep escape sequences and other control characters in log messages: true/false +# Warning: true disables this sanitization and can allow escape-sequence injection unsafe_allow_ansi_codes = false # Maximum message length before formatting for console/file output (0 = unlimited) @@ -104,25 +104,25 @@ max_journal_length = 4096 ## Configuration Keys -| Key | Aliases | Values | Default | Description | -| ------------------------- | ------------------------------------------- | ----------------------------------------------------------------- | ----------------- | ----------------------------------------- | -| `level` | `log_level` | DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY, 0-7 | `INFO` | Minimum log level | -| `format` | `log_format` | Format string | `%d [%l] [%s] %m` | Log message format | -| `log_file` | `logfile`, `file` | Path | (none) | Log file path | -| `script_name` | `scriptname`, `name` | String | auto-detected | Script name in log messages | -| `journal` | `use_journal` | true/false, yes/no, on/off, 1/0 | `true` | Enable journal logging | -| `tag` | `journal_tag` | String | script name | Journal/syslog tag | -| `utc` | `use_utc` | true/false, yes/no, on/off, 1/0 | `false` | Use UTC timestamps | -| `color` | `colour`, `colors`, `colours`, `use_colors` | auto, always, never | `auto` | Color mode | -| `stderr_level` | `stderr-level` | Log level | `ERROR` | Minimum level for stderr | -| `quiet` | - | true/false, yes/no, on/off, 1/0 | `false` | Disable console output | -| `console_log` | - | true/false, yes/no, on/off, 1/0 | `true` | Enable console output (inverse of quiet) | -| `verbose` | - | true/false, yes/no, on/off, 1/0 | `false` | Enable DEBUG level | -| `unsafe_allow_newlines` | `unsafe-allow-newlines` | true/false, yes/no, on/off, 1/0 | `false` | Allow newlines in log messages (unsafe) | -| `unsafe_allow_ansi_codes` | `unsafe-allow-ansi-codes` | true/false, yes/no, on/off, 1/0 | `false` | Allow ANSI codes in log messages (unsafe) | -| `max_line_length` | `max-line-length`, `log_max_line_length` | Non-negative integer (0 = unlimited) | `4096` | Max message length before formatting | -| `max_journal_length` | `max-journal-length`, `journal_max_length` | Non-negative integer (0 = unlimited) | `4096` | Max message length for journal | -| `init_message` | `log_init_message` | true/false, yes/no, on/off, 1/0 | `true` | Write INIT entry to log file on startup | +| Key | Aliases | Values | Default | Description | +| ------------------------- | ------------------------------------------- | ----------------------------------------------------------------- | ----------------- | ----------------------------------------------------------- | +| `level` | `log_level` | DEBUG, INFO, NOTICE, WARN, ERROR, CRITICAL, ALERT, EMERGENCY, 0-7 | `INFO` | Minimum log level | +| `format` | `log_format` | Format string | `%d [%l] [%s] %m` | Log message format | +| `log_file` | `logfile`, `file` | Path | (none) | Log file path | +| `script_name` | `scriptname`, `name` | String | auto-detected | Script name in log messages | +| `journal` | `use_journal` | true/false, yes/no, on/off, 1/0 | `true` | Enable journal logging | +| `tag` | `journal_tag` | String | script name | Journal/syslog tag | +| `utc` | `use_utc` | true/false, yes/no, on/off, 1/0 | `false` | Use UTC timestamps | +| `color` | `colour`, `colors`, `colours`, `use_colors` | auto, always, never | `auto` | Color mode | +| `stderr_level` | `stderr-level` | Log level | `ERROR` | Minimum level for stderr | +| `quiet` | - | true/false, yes/no, on/off, 1/0 | `false` | Disable console output | +| `console_log` | - | true/false, yes/no, on/off, 1/0 | `true` | Enable console output (inverse of quiet) | +| `verbose` | - | true/false, yes/no, on/off, 1/0 | `false` | Enable DEBUG level | +| `unsafe_allow_newlines` | `unsafe-allow-newlines` | true/false, yes/no, on/off, 1/0 | `false` | Allow newlines in log messages (unsafe) | +| `unsafe_allow_ansi_codes` | `unsafe-allow-ansi-codes` | true/false, yes/no, on/off, 1/0 | `false` | Keep escape sequences and other control characters (unsafe) | +| `max_line_length` | `max-line-length`, `log_max_line_length` | Non-negative integer (0 = unlimited) | `4096` | Max message length before formatting | +| `max_journal_length` | `max-journal-length`, `journal_max_length` | Non-negative integer (0 = unlimited) | `4096` | Max message length for journal | +| `init_message` | `log_init_message` | true/false, yes/no, on/off, 1/0 | `true` | Write INIT entry to log file on startup | ### Syslog Facility Key @@ -531,7 +531,7 @@ Track configuration changes: ```ini # logging.conf -# Version: 2.1.0 +# Config revision: 3 # Last updated: 2026-01-15 [logging] diff --git a/docs/examples.md b/docs/examples.md index 0e98133..fc480ca 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,6 +1,9 @@ # Examples -This document provides comprehensive examples demonstrating various use cases for the Bash Logging Module. +This document provides comprehensive examples demonstrating various use cases for bash-logger. + +The examples are bash scripts: they start with `#!/bin/bash` and use `source` and `[[ ]]`. The library itself is POSIX sh plus `local`, and a POSIX sh script calls it the +same way, with `.` in place of `source` (see [POSIX sh Script](#posix-sh-script)). See [Shell Compatibility](shell-compatibility.md) for the supported shells. ## Table of Contents @@ -8,6 +11,7 @@ This document provides comprehensive examples demonstrating various use cases fo * [Simple Script Logging](#simple-script-logging) * [Logging to File with Verbose Output](#logging-to-file-with-verbose-output) * [Comprehensive Logging Configuration](#comprehensive-logging-configuration) + * [POSIX sh Script](#posix-sh-script) * [Configuration File Examples](#configuration-file-examples) * [Using a Configuration File](#using-a-configuration-file) * [Environment-Specific Configuration](#environment-specific-configuration) @@ -96,6 +100,26 @@ init_logger \ log_info "Application initialized with comprehensive logging" ``` +### POSIX sh Script + +The same library calls work in a script that runs under dash or busybox ash. Source the file with `.`, and use `[ ]` in place of `[[ ]]`: + +```sh +#!/bin/sh + +# Source the logging module (use "." because "source" is not available in every sh) +. /path/to/logging.sh + +init_logger --log "/var/log/myapp.log" --level INFO + +log_info "Starting backup" + +if [ ! -d "/srv/data" ]; then + log_error "Source directory is missing" + exit 1 +fi +``` + ## Configuration File Examples ### Using a Configuration File diff --git a/docs/formatting.md b/docs/formatting.md index 25e34f0..85c06f9 100644 --- a/docs/formatting.md +++ b/docs/formatting.md @@ -1,6 +1,6 @@ # Formatting -The Bash Logging Module allows you to customize the format of log messages using special placeholders. +bash-logger allows you to customize the format of log messages using special placeholders. ## Table of Contents diff --git a/docs/getting-started.md b/docs/getting-started.md index e04daa7..f5ed96b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,6 @@ # Getting Started -This guide will help you get started with the Bash Logging Module quickly. +This guide will help you get started with bash-logger quickly. ## Table of Contents @@ -112,14 +112,15 @@ curl -fsSL https://raw.githubusercontent.com/GingerGraham/bash-logger/main/insta **Installing a Specific Version:** To install a specific version instead of the latest release, set the `INSTALL_VERSION` environment variable to a valid -release tag: +release tag. Replace `` with a tag from the [releases page](https://github.com/GingerGraham/bash-logger/releases), such as `3.0.0`. +Only 3.x receives fixes (see [SECURITY.md](../SECURITY.md#supported-versions)): ```bash # Install a specific version (user installation) -INSTALL_VERSION=2.7.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/GingerGraham/bash-logger/main/install.sh)" +INSTALL_VERSION= bash -c "$(curl -fsSL https://raw.githubusercontent.com/GingerGraham/bash-logger/main/install.sh)" # Install a specific version (system-wide) -curl -fsSL https://raw.githubusercontent.com/GingerGraham/bash-logger/main/install.sh | sudo INSTALL_VERSION=2.7.0 bash -s -- --system +curl -fsSL https://raw.githubusercontent.com/GingerGraham/bash-logger/main/install.sh | sudo INSTALL_VERSION= bash -s -- --system ``` For more details on available options, run the install script with the `--help` flag. @@ -360,6 +361,25 @@ log_debug "Now you can see debug messages!" log_info "Script completed successfully" ``` +To use the same script in a POSIX sh script (dash, busybox ash, or `/bin/sh` on Debian, Ubuntu and Alpine), start it with `#!/bin/sh` and source the library with `.`, +because `source` is not available in every `sh`. The library calls are the same. See [Shell Compatibility](shell-compatibility.md) for the supported shells. + +```sh +#!/bin/sh + +# Source the logging module +. ./logging.sh + +# Initialize with default settings +init_logger + +log_info "Hello, logging!" +log_debug "This debug message won't appear (log level is INFO by default)" + +set_log_level DEBUG +log_debug "Now you can see debug messages!" +``` + Run it: ```bash @@ -383,6 +403,9 @@ init_logger --verbose init_logger --log "/tmp/myapp.log" ``` +A log file that `init_logger` creates is readable and writable only by its owner (mode 600), and so is any directory it creates for it. To let a group read it, create the file +first with the mode you want, or change the mode afterwards. See [Log File Security](sensitive-data.md#log-file-security-toctou-protection). + ### Quiet Mode ```bash diff --git a/docs/initialization.md b/docs/initialization.md index 503ab98..f34b48f 100644 --- a/docs/initialization.md +++ b/docs/initialization.md @@ -1,6 +1,6 @@ # Initialization -The `init_logger` function is the entry point for configuring the Bash Logging Module. It must be called before using +The `init_logger` function is the entry point for configuring bash-logger. It must be called before using any logging functions. ## Table of Contents @@ -58,7 +58,7 @@ The `init_logger` function accepts the following options: | Option | Description | | ----------------------------------------------- | ----------------------------------------------------------------------------------- | | `-c, --config FILE` | Load configuration from an INI file (CLI args override config values) | -| `-l, --log, --logfile, --log-file, --file FILE` | Specify a log file to write logs to | +| `-l, --log, --logfile, --log-file, --file FILE` | Specify a log file to write logs to (created private; see below) | | `-n, --name, --script-name NAME` | Set custom script name for log messages (overrides auto-detection) | | `-q, --quiet` | Disable console output | | `-v, --verbose, --debug` | Set log level to DEBUG (most verbose) | @@ -67,16 +67,23 @@ The `init_logger` function accepts the following options: | `-f, --format FORMAT` | Set custom log format | | `-u, --utc` | Use UTC time instead of local time | | `-j, --journal` | Enable logging to systemd journal | -| `-t, --tag TAG` | Set custom tag for journal logs (default: script name) | +| `-t, --tag TAG` | Set custom tag for journal logs (default: script name; see below) | | `-F, --facility FACILITY` | Set syslog facility for journal logs (default: daemon) | | `--color, --colour` | Explicitly enable color output (default: auto-detect) | | `--no-color, --no-colour` | Disable color output | | `--no-init-message` | Suppress the INIT entry written to the log file on initialization | | `-U, --unsafe-allow-newlines` | Allow newlines in log messages (not recommended; disables sanitization) | -| `-A, --unsafe-allow-ansi-codes` | Allow ANSI escape codes in log messages (not recommended; disables sanitization) | +| `-A, --unsafe-allow-ansi-codes` | Keep escape sequences and other control characters in messages (not recommended) | | `--max-line-length LENGTH` | Max message length before formatting for console/file output (0 = unlimited) | | `--max-journal-length LENGTH` | Max message length before formatting for journal output (0 = unlimited) | +**Journal tags:** `-t` and `--tag` are validated in the same way as the `tag` configuration key and `set_journal_tag`: a tag must not be empty, longer than 64 characters, +or contain control characters or shell metacharacters. An invalid tag prints an error, and the previous tag is kept. See [Tag Rules](journal-logging.md#tag-rules). + +**Log file access:** A log file that `init_logger` creates, and any directory it creates for it, is readable only by its owner (mode 600 and 700). Existing files and directories keep +their permissions. To let a group read the log, create the file first with the mode you want, or change the mode afterwards. See +[Log File Security](sensitive-data.md#log-file-security-toctou-protection) for the symbolic link checks. + ## Common Initialization Patterns ### Default Configuration @@ -398,31 +405,39 @@ esac ### 6. Be Aware of Environment Variable Overrides -bash-logger uses global variables that are initialized with default values when the module is sourced. -These variables can be modified after sourcing but before calling `init_logger()`. -Understanding this behavior is important to avoid unexpected configuration: +Sourcing `logging.sh` sets every setting to its default. A value that the parent shell or environment exports is discarded, with one exception: `SCRIPT_NAME`. +Understanding this is important to avoid unexpected configuration: **How it works:** ```bash -# Variables are reset to defaults when sourcing -source /path/to/logging.sh # LOG_FILE="", USE_COLORS="auto", etc. +# Sourcing resets these to their defaults, whatever the environment held: +# LOG_FILE, LOG_FORMAT, USE_COLORS, CURRENT_LOG_LEVEL, LOG_STDERR_LEVEL, USE_JOURNAL +# (and the other settings: JOURNAL_TAG, USE_UTC, CONSOLE_LOG, the unsafe flags and the line limits) +export LOG_FILE="/tmp/inherited.log" +source /path/to/logging.sh # LOG_FILE="" -# You can override defaults BEFORE calling init_logger +# Assignments made AFTER sourcing and BEFORE init_logger are used LOG_FILE="/tmp/my.log" USE_COLORS="never" init_logger # Uses the modified values # Or pass them as arguments (cleaner approach) source /path/to/logging.sh -init_logger --log "/tmp/my.log" --colors never +init_logger --log "/tmp/my.log" --no-color ``` +**SCRIPT_NAME is inherited:** + +`SCRIPT_NAME` is the only setting that survives sourcing. If the environment sets it and no `--name` option or `script_name` config key is given, `init_logger` uses it as the script name. +`init_logger` sanitizes the value like any other script name, so characters outside `A-Z`, `a-z`, `0-9`, `.`, `_` and `-` become `_`. +Pass `--name`, or run `unset SCRIPT_NAME` before sourcing, if the name must not depend on the caller's environment. + **Potential Issues:** -* Unintended configuration if parent shell/environment sets these variables -* In multi-user systems, one user could affect another's logging by exporting variables -* Automation/CI systems might unexpectedly override logging configuration +* An inherited `SCRIPT_NAME` appears in every log line. A parent process, a CI system or a wrapper script can change it by exporting `SCRIPT_NAME` +* Code that runs between `source` and `init_logger` can assign a setting directly, and `init_logger` then starts from that value. + Options and configuration file values given to `init_logger` still override it **Best Practices:** @@ -436,24 +451,16 @@ init_logger --log "/var/log/myapp.log" --level INFO # GOOD: Use configuration files for complex setups init_logger --config /etc/myapp/logging.conf - -# CAUTION: This can be affected by parent environment -source /path/to/logging.sh -init_logger # May use LOG_FILE from environment ``` -For **secure or critical applications**, avoid relying on inherited environment variables: +For **secure or critical applications**, set the script name explicitly, and leave the other settings to `init_logger` arguments or a configuration file: ```bash #!/bin/bash - -# Explicitly unset any pre-existing bash-logger variables to ensure clean state -unset LOG_FILE USE_COLORS LOG_FORMAT CURRENT_LOG_LEVEL LOG_STDERR_LEVEL SCRIPT_NAME USE_JOURNAL - source /path/to/logging.sh -# Now initialize with only explicit configuration -init_logger --log "/secure/location/app.log" --level WARN +# --name replaces any SCRIPT_NAME inherited from the environment +init_logger --name "myapp" --log "/secure/location/app.log" --level WARN ``` ## Examples by Use Case diff --git a/docs/journal-logging.md b/docs/journal-logging.md index 32027ff..d0a2c85 100644 --- a/docs/journal-logging.md +++ b/docs/journal-logging.md @@ -1,6 +1,6 @@ # Journal Logging -The Bash Logging Module can integrate with systemd's journal logging system, making it ideal for system services and +bash-logger can integrate with systemd's journal logging system, making it ideal for system services and applications running on modern Linux distributions. ## Table of Contents @@ -17,6 +17,7 @@ applications running on modern Linux distributions. * [Journal Tags](#journal-tags) * [Default Tag](#default-tag) * [Custom Tag](#custom-tag) + * [Tag Rules](#tag-rules) * [Tag Best Practices](#tag-best-practices) * [Journal Facilities](#journal-facilities) * [Default Facility](#default-facility) @@ -47,6 +48,7 @@ applications running on modern Linux distributions. * [Journal Not Receiving Logs](#journal-not-receiving-logs) * [Permission Issues](#permission-issues) * [Verifying Logs Are Received](#verifying-logs-are-received) +* [Using a Stub logger in Tests](#using-a-stub-logger-in-tests) * [Performance Considerations](#performance-considerations) * [Related Documentation](#related-documentation) @@ -170,6 +172,40 @@ init_logger --journal --tag "backup-service" init_logger --journal --tag "database-monitor" ``` +### Tag Rules + +A tag given with `--tag` (`-t`), with `set_journal_tag` or as the `tag` configuration key must: + +* not be empty +* be at most 64 characters long +* not contain control characters (ASCII control characters, and in a UTF-8 locale the C1 controls and U+2028 and U+2029) +* not contain shell metacharacters: `$` `` ` `` `;` `|` `&` `<` `>` `(` `)` `{` `}` `[` `]` `\` + +Spaces and other printable characters are allowed. + +**`--tag` and `set_journal_tag` reject an invalid tag.** They print a message that says why, and the previous tag is kept. If there was no previous tag, the default (the script name) is used. +`init_logger` carries on, and `set_journal_tag` returns 1: + +```bash +$ init_logger --journal --tag "my;app" +Warning: Journal tag contains shell metacharacters + Hint: Keeping existing journal tag '' +$ set_journal_tag "bad|tag" +Warning: Journal tag contains shell metacharacters +$ echo $? +1 +``` + +| Problem | Message | +| ------------------- | ------------------------------------------------------------- | +| Empty | `Warning: Empty journal tag` | +| Longer than 64 | `Error: journal tag exceeds maximum length of 64 (actual: N)` | +| Control characters | `Error: journal tag contains control characters` | +| Shell metacharacter | `Warning: Journal tag contains shell metacharacters` | + +**The `tag` configuration key corrects the tag instead.** It prints a warning with a line number, and a tag that is longer than 64 characters is cut to 64 characters. +For any other invalid tag, every character other than letters, digits, `.`, `_` and `-` is replaced by `_`. See [Configuration](configuration.md). + ### Tag Best Practices 1. **Use application names** - Consistent naming across deployments @@ -582,6 +618,39 @@ log_error "Test error 1" journalctl -t "test-*" -n 5 ``` +## Using a Stub logger in Tests + +The library runs only a `logger` that it has validated. `LOGGER_PATH` is set when validation succeeds, and it is then made readonly. The journal writer refuses any `LOGGER_PATH` that is not readonly. +A test that assigns `LOGGER_PATH` to a stub script, as 2.x allowed, finds that the stub is never run. + +Making the value readonly is necessary but not enough when a real `logger` is installed. Journal logging, `check_logger_available` and `log_to_journal` also check the `logger` found on `PATH`. +They disable journal logging if it differs from a locked `LOGGER_PATH`. The library's own tests therefore lock the stub and replace the two internal validation functions: + +```bash +stub_dir=$(mktemp -d) +cat > "$stub_dir/stub_logger" <<'EOF' +#!/bin/sh +echo "$*" >> "$(dirname "$0")/journal_capture.log" +EOF +chmod +x "$stub_dir/stub_logger" + +source /path/to/logging.sh + +# Lock the stub, as validation does for the real logger +LOGGER_PATH="$stub_dir/stub_logger" +readonly LOGGER_PATH + +# Replace the checks that look for the real logger (internal functions, as in tests/test_journal_logging.sh) +_find_and_validate_logger() { return 0; } +check_logger_available() { return 0; } + +init_logger --journal --tag "test" --quiet +log_info "captured by the stub" +``` + +The stub receives the arguments that `logger` would get, for example `-p daemon.info -t test -- captured by the stub`. These two functions are internal and can change between releases. +See [Using a Stub logger in Tests](testing.md#using-a-stub-logger-in-tests) for the same pattern in the test suite. + ## Performance Considerations Journal logging is generally fast, but consider: diff --git a/docs/log-levels.md b/docs/log-levels.md index 91cb6a6..831f22d 100644 --- a/docs/log-levels.md +++ b/docs/log-levels.md @@ -1,6 +1,6 @@ # Log Levels -The Bash Logging Module supports standard syslog log levels, providing a hierarchy of severity from most critical to +bash-logger supports standard syslog log levels, providing a hierarchy of severity from most critical to least critical. ## Table of Contents diff --git a/docs/output-streams.md b/docs/output-streams.md index 42aa9ea..cf004ac 100644 --- a/docs/output-streams.md +++ b/docs/output-streams.md @@ -1,6 +1,6 @@ # Output Streams -The Bash Logging Module intelligently splits console output between stdout and stderr based on message severity, +bash-logger intelligently splits console output between stdout and stderr based on message severity, following Unix conventions. ## Table of Contents @@ -280,7 +280,7 @@ See [Configuration](configuration.md) for more details. # Capture both stdout and stderr to same file ./script.sh > output.log 2>&1 -# Modern syntax (Bash 4+) +# Shorthand for the same redirection (bash and zsh only; use the form above in sh scripts) ./script.sh &> output.log ``` diff --git a/docs/runtime-configuration.md b/docs/runtime-configuration.md index 1d595e5..3840290 100644 --- a/docs/runtime-configuration.md +++ b/docs/runtime-configuration.md @@ -1,6 +1,6 @@ # Runtime Configuration -The Bash Logging Module allows you to change configuration settings during script execution, providing flexibility for +bash-logger allows you to change configuration settings during script execution, providing flexibility for different phases of operation. ## Table of Contents diff --git a/docs/sensitive-data.md b/docs/sensitive-data.md index d870bbf..1d9a55d 100644 --- a/docs/sensitive-data.md +++ b/docs/sensitive-data.md @@ -1,6 +1,6 @@ # Sensitive Data -The Bash Logging Module provides a special function for handling sensitive information that should not be written to +bash-logger provides a special function for handling sensitive information that should not be written to persistent storage. ## Table of Contents @@ -16,7 +16,7 @@ persistent storage. * [Security Considerations](#security-considerations) * [Console Output Security](#console-output-security) * [Production Environments](#production-environments) - * [ANSI Code Injection Prevention](#ansi-code-injection-prevention) + * [Terminal Control Sequence Injection Prevention](#terminal-control-sequence-injection-prevention) * [Log File Security (TOCTOU Protection)](#log-file-security-toctou-protection) * [Alternative Approaches](#alternative-approaches) * [1. Redact Sensitive Values](#1-redact-sensitive-values) @@ -166,35 +166,68 @@ else fi ``` -### ANSI Code Injection Prevention +### Terminal Control Sequence Injection Prevention -bash-logger provides secure-by-default protection against ANSI escape sequence injection attacks. This is important -because malicious ANSI codes can: +bash-logger provides secure-by-default protection against terminal control sequences and control characters in messages. This is important +because malicious sequences can: * **Manipulate terminal display** - Clear screen, reposition cursor -* **Hide information** - Make previous output invisible +* **Hide information** - Make previous output invisible, for example with a color that matches the background * **Spoof messages** - Create fake error or success messages * **Enable social engineering** - Change window titles, fake prompts * **Exploit terminal bugs** - Some terminal emulators have CVEs triggered by specific sequences +* **Reorder text** - Bidirectional formatting characters can make a log line read in a different order than it is stored **Default Behavior:** ```bash -# By default, ANSI codes in user input are stripped +# By default, escape sequences in user input are removed malicious_input=$'\e[2J\e[HFAKE ERROR\e[0m' log_error "Processing: $malicious_input" # Output: "Processing: FAKE ERROR" (without escape sequences) ``` -**ANSI Code Protection:** +**What Is Removed:** -* All user input is automatically scrubbed of ANSI escape sequences +* **Every CSI sequence** as ECMA-48 defines it (`ESC [` followed by parameter bytes, intermediate bytes and a final byte), such as colors, cursor movement and `ESC[2J` +* **OSC, DCS, SOS, PM and APC strings**, up to their terminator or the next escape character +* **Other control characters:** the C0 controls other than newline, carriage return and tab (such as backspace, BEL, vertical tab, form feed and DEL), the C1 controls, + the line and paragraph separators U+2028 and U+2029, and the bidirectional formatting characters U+202A to U+202E and U+2066 to U+2069 +* **No escape character is left behind.** A lone escape character or an incomplete sequence is removed as well + +Newline, carriage return and tab are replaced by spaces. That is controlled separately by `--unsafe-allow-newlines`. + +**Very Long Messages:** + +A message much longer than the line limits is cut before it is sanitized, because sanitizing takes longer the longer the message is. +The cut is at four times the larger of the console and journal line limits, and never below 8,192 characters. +The logged text is the same, except that a message made mostly of escape sequences now ends with `...[truncated]`. With a line limit of 0, nothing is cut. + +For the full list and the changes from 2.x, see [Messages and control characters](upgrading-to-3.0.md#messages-and-control-characters). + +**Protection:** + +* All user input is automatically scrubbed of terminal control sequences and control characters * Library-generated colors (for log levels) are preserved * This protection is transparent - no code changes needed -**If You Need ANSI Codes in Log Messages:** +**Characters That Are Kept:** + +Some invisible characters are not removed, because they cannot reverse or hide a log line the way the removed bidirectional overrides and isolates can: + +| Character | Name | +| --------- | --------------------------------------------------------- | +| U+200E | LEFT-TO-RIGHT MARK (bidirectional mark) | +| U+200F | RIGHT-TO-LEFT MARK (bidirectional mark) | +| U+061C | ARABIC LETTER MARK (bidirectional mark) | +| U+200B | ZERO WIDTH SPACE | +| U+FEFF | ZERO WIDTH NO-BREAK SPACE, also used as a byte order mark | -Only enable unsafe mode if you have complete control over all logged content: +The bidirectional marks can only move neutral characters such as punctuation. The zero-width characters can only hide differences between similar text. + +**If You Need Escape Sequences in Log Messages:** + +Unsafe mode keeps escape sequences, and keeps the other control characters listed above as well. Only enable it if you have complete control over all logged content: ```bash init_logger --unsafe-allow-ansi-codes # Not recommended @@ -205,13 +238,14 @@ set_unsafe_allow_ansi_codes true **See Also:** -* [ANSI Code Injection Protection](../examples.md#ansi-code-injection-protection) in Examples +* [Upgrading to 3.0](upgrading-to-3.0.md#messages-and-control-characters) - Full list of removed characters * [api-reference.md](api-reference.md#set_unsafe_allow_ansi_codes) - set_unsafe_allow_ansi_codes function * [runtime-configuration.md](runtime-configuration.md#set_unsafe_allow_ansi_codes) - Runtime control ### Log File Security (TOCTOU Protection) -bash-logger includes protection against Time-of-Check Time-of-Use (TOCTOU) race condition attacks during log file creation. This prevents attackers from redirecting log output to unintended locations. +bash-logger includes protection against Time-of-Check Time-of-Use (TOCTOU) race condition attacks during log file creation, and it checks the path again on every write. +This prevents attackers from redirecting log output to unintended locations. **Attack Scenario Prevented:** @@ -234,6 +268,10 @@ ln -s /etc/passwd /tmp/app.log 2. **Symlink Detection** - Immediately rejects symbolic links 3. **File Type Validation** - Ensures only regular files are used for logging 4. **Write Permission Check** - Validates write access before use +5. **Re-checked on every write** - Each entry checks the path for a symbolic link again. If something replaced the log file with a link after `init_logger` ran, nothing is + written through the link. One error is printed, and the entry goes to stderr instead +6. **Private by default** - A log file that `init_logger` creates is mode 600, and any directory it creates for the file is mode 700, whatever the caller's umask. + Existing files and directories keep their permissions. To let a group read the log, create the file first with the mode you want, or change the mode afterwards **What This Protects Against:** @@ -249,9 +287,17 @@ If a security issue is detected during initialization, you'll see: ```bash Error: Log file path is a symbolic link Error: Log file exists but is not a regular file (may be a directory or device) -Error: Log file '/path/to/log' is not writable +Error: Log file is not writable ``` +If the log file is replaced by a symbolic link after initialization, the first write that finds it prints: + +```bash +ERROR: Log file path is now a symbolic link; not writing to it +``` + +The message is printed once. Later entries go to stderr until the link is removed. See [Troubleshooting](troubleshooting.md#log-file-path-is-now-a-symbolic-link). + **Best Practices:** ```bash diff --git a/docs/testing.md b/docs/testing.md index 9fa18dc..e52755d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -683,6 +683,35 @@ if ! check_logger_available; then fi ``` +#### Using a Stub logger in Tests + +Journal tests use a stub script in place of the system `logger`. The library runs a `logger` only when `LOGGER_PATH` has been validated and made readonly, so a stub needs the following. +`tests/test_journal_logging.sh` has the pattern (`_create_stub_logger` writes the stub and a capture file in `$TEST_DIR`): + +```bash +result=$(bash -c " + source '$PROJECT_ROOT/logging.sh' + + # Assign after sourcing, because sourcing sets LOGGER_PATH to '' unless it is already readonly. + # Make the value readonly: the library refuses a LOGGER_PATH that is not. + LOGGER_PATH='$STUB_LOGGER'; readonly LOGGER_PATH + + # Stop the library from looking for the real logger. Without this, journal logging is disabled + # when a real logger is installed, because its path differs from the locked one. + _find_and_validate_logger() { return 0; } + check_logger_available() { return 0; } + + init_logger --no-color --quiet + log_to_journal INFO 'message for the stub' +" 2>&1) + +assert_file_contains "$STUB_CAPTURE" "message for the stub" || return +``` + +* A stub that is assigned to `LOGGER_PATH` but not made readonly is never run. This is the usual reason a test that worked on 2.x stops capturing journal calls. +* Do the assignment in the same shell that runs the library, inside the `bash -c` script. A test function that sets the variable does not affect the child shell. +* The two function overrides are internal. If they change, update every suite that uses them. See [Journal Logging](journal-logging.md#using-a-stub-logger-in-tests). + ### Best Practices 1. **Descriptive Names**: Use clear, specific test names diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2769202..b226172 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting -This guide helps you diagnose and resolve common issues with the Bash Logging Module. +This guide helps you diagnose and resolve common issues with bash-logger. ## Table of Contents @@ -10,6 +10,7 @@ This guide helps you diagnose and resolve common issues with the Bash Logging Mo * [Initialization Issues](#initialization-issues) * [Logger Not Initialized](#logger-not-initialized) * [Initialization Fails](#initialization-fails) + * [Script Name Shows as unknown](#script-name-shows-as-unknown) * [Configuration File Issues](#configuration-file-issues) * [Configuration File Not Loaded](#configuration-file-not-loaded) * [Invalid Configuration Values](#invalid-configuration-values) @@ -17,6 +18,7 @@ This guide helps you diagnose and resolve common issues with the Bash Logging Mo * [File Logging Issues](#file-logging-issues) * [No Output to Log File](#no-output-to-log-file) * [Log File Permission Denied](#log-file-permission-denied) + * [Other Users Cannot Read the Log File](#other-users-cannot-read-the-log-file) * [Understanding Error Messages Without Path Disclosure](#understanding-error-messages-without-path-disclosure) * [Journal Logging Issues](#journal-logging-issues) * [Journal Logs Not Appearing](#journal-logs-not-appearing) @@ -28,6 +30,7 @@ This guide helps you diagnose and resolve common issues with the Bash Logging Mo * [Output Stream Issues](#output-stream-issues) * [Logs Going to Wrong Stream](#logs-going-to-wrong-stream) * [Cannot Separate Stdout and Stderr](#cannot-separate-stdout-and-stderr) + * [CONFIG Notices Appear on Stderr](#config-notices-appear-on-stderr) * [Runtime Configuration Issues](#runtime-configuration-issues) * [set_log_level Not Working](#set_log_level-not-working) * [Runtime Changes Not Persisting](#runtime-changes-not-persisting) @@ -48,9 +51,11 @@ This guide helps you diagnose and resolve common issues with the Bash Logging Mo * ["command not found: init_logger"](#command-not-found-init_logger) * ["permission denied" when writing log file](#permission-denied-when-writing-log-file) * ["invalid log level"](#invalid-log-level) + * ["requires a value"](#requires-a-value) * ["Configuration file not found" / "Configuration file not readable"](#configuration-file-not-found--configuration-file-not-readable) * ["Cannot create log directory" / "Cannot create log file"](#cannot-create-log-directory--cannot-create-log-file) * ["Log file path is a symbolic link"](#log-file-path-is-a-symbolic-link) + * ["Log file path is now a symbolic link"](#log-file-path-is-now-a-symbolic-link) * ["Log file is not writable" / "Failed to write to log file"](#log-file-is-not-writable--failed-to-write-to-log-file) * [Getting Help](#getting-help) * [Related Documentation](#related-documentation) @@ -185,6 +190,25 @@ init_logger --log "$HOME/logs/app.log" **Note:** bash-logger rejects symbolic links for security reasons. This prevents attackers from redirecting your logs to sensitive system files. Always use direct file paths for log files. +### Script Name Shows as unknown + +**Problem:** Log lines show `[unknown]` where the script name should be + +**Cause:** + +* In zsh before 3.0, every log line showed `unknown`. 3.0 detects the name of the script that called `init_logger`. If a log parser or alert matches `[unknown]` for zsh scripts, update the pattern. +* In any shell, commands that come from `sh -c '...'` or standard input have no script file. bash reports `unknown`. dash, busybox ash and zsh report the shell's name. +* When `init_logger` is called from a file that the main script sources, bash and zsh report that file, and dash and busybox ash report the main script. + +**Solution:** + +```bash +# Set the name explicitly; it is the same in every shell +init_logger --name "myapp" +``` + +See [Script name detection](shell-compatibility.md#script-name-detection) for the full rules. + ## Configuration File Issues ### Configuration File Not Loaded @@ -354,16 +378,17 @@ The logger will: **Validation Summary:** -| Configuration Item | Validation | On Error | -| -------------------- | -------------------------------------------------- | ------------------------------------------- | -| `level` | Valid level name or 0-7 | Use INFO, display warning with valid values | -| `log_file` | Absolute path, no control chars, no shell patterns | Skip file logging, display error | -| `tag` | Max 64 chars, no control chars | Truncate or sanitize, display warning | -| `max_line_length` | Integer 0-1,048,576 | Use default (4096), display warning | -| `max_journal_length` | Integer 0-1,048,576 | Use default (4096), display warning | -| Boolean values | true/false, yes/no, on/off, 1/0 (case-insensitive) | Display warning, keep previous value | -| All values | Max 4,096 characters | Truncate, display warning | -| Unknown keys | N/A | Display warning with list of valid keys | +| Configuration Item | Validation | On Error | +| -------------------------------- | -------------------------------------------------- | ------------------------------------------- | +| `level` | Valid level name or 0-7 | Use INFO, display warning with valid values | +| `log_file` | Absolute path, no control chars, no shell patterns | Skip file logging, display error | +| `tag` | Max 64 chars, no control chars, no shell metachars | Truncate to 64 or sanitize, display warning | +| `-t`, `--tag`, `set_journal_tag` | Same rules as `tag` | Keep previous tag, display error or warning | +| `max_line_length` | Integer 0-1,048,576 | Use default (4096), display warning | +| `max_journal_length` | Integer 0-1,048,576 | Use default (4096), display warning | +| Boolean values | true/false, yes/no, on/off, 1/0 (case-insensitive) | Display warning, keep previous value | +| All values | Max 4,096 characters | Truncate, display warning | +| Unknown keys | N/A | Display warning with list of valid keys | **Solution:** @@ -432,14 +457,39 @@ ls -l /var/log/myapp.log # Fix ownership sudo chown $USER:$USER /var/log/myapp.log -# Fix permissions -chmod 644 /var/log/myapp.log +# Fix permissions: the owner needs read and write access +# (600 is the mode init_logger gives a file it creates) +chmod 600 /var/log/myapp.log # Or use a user-writable location init_logger --log "$HOME/logs/app.log" mkdir -p "$HOME/logs" ``` +### Other Users Cannot Read the Log File + +**Problem:** The log file exists, but another user or group, such as a log shipper or a monitoring account, gets "Permission denied" when reading it + +**Cause:** Since 3.0, a log file that `init_logger` creates is mode 600, and any directory it creates for it is mode 700. In 2.x the caller's umask decided, usually 644 and 755. +Files and directories that already exist keep their permissions. + +**Solution:** + +```bash +# Option 1: create the file with the mode you want before init_logger +# (an existing file keeps its mode, so init_logger does not change it) +sudo install -m 640 -o myuser -g myapp /dev/null /var/log/myapp/app.log +init_logger --log "/var/log/myapp/app.log" + +# Option 2: change the mode after init_logger created the file +init_logger --log "/var/log/myapp/app.log" +chgrp myapp /var/log/myapp/app.log +chmod 640 /var/log/myapp/app.log +``` + +If `init_logger` created the directory, it is mode 700 and the group cannot enter it either. Run `chmod 750 /var/log/myapp` as well, or create the directory first. +See [Log File Security](sensitive-data.md#log-file-security-toctou-protection). + ### Understanding Error Messages Without Path Disclosure **Background:** For security reasons (defense-in-depth against information disclosure, CWE-209), error messages @@ -520,16 +570,17 @@ grep -E 'log_file|logfile|file' /path/to/config.conf Error messages include actionable hints: -| Error Message | Hint Provided | What to Check | -| -------------------------------- | -------------------------------------------- | ---------------------------------------- | -| Configuration file not found | Check the --config argument | Verify path passed to init_logger | -| Configuration file not readable | Check file permissions | Run `ls -l` on config file | -| Cannot create log directory | Check --log argument and parent permissions | Verify parent directory is writable | -| Log file path is a symbolic link | Verify --log doesn't point to symlink | Security check, use regular file | -| Cannot create log file | Check directory permissions | Verify directory exists and is writable | -| Log file is not a regular file | Check --log argument | Might be pointing to directory or device | -| Log file is not writable | Check file permissions | Run `chmod` or use different location | -| Failed to write to log file | Check permissions, disk space, or if deleted | Check `df -h` for disk space | +| Error Message | Hint Provided | What to Check | +| ------------------------------------ | ------------------------------------------------------------ | -------------------------------------------------------------------- | +| Configuration file not found | Check the --config argument | Verify path passed to init_logger | +| Configuration file not readable | Check file permissions | Run `ls -l` on config file | +| Cannot create log directory | Check --log argument and parent permissions | Verify parent directory is writable | +| Log file path is a symbolic link | Verify --log doesn't point to symlink | Security check, use regular file | +| Log file path is now a symbolic link | Something replaced the log file after init_logger checked it | Find what created the link; entries go to stderr until it is removed | +| Cannot create log file | Check directory permissions | Verify directory exists and is writable | +| Log file is not a regular file | Check --log argument | Might be pointing to directory or device | +| Log file is not writable | Check file permissions | Run `chmod` or use different location | +| Failed to write to log file | Check permissions, disk space, or if deleted | Check `df -h` for disk space | **6. Common Path Issues** @@ -726,6 +777,28 @@ init_logger --stderr-level ERROR ./script.sh > stdout.log 2> stderr.log ``` +### CONFIG Notices Appear on Stderr + +**Problem:** `[CONFIG]` lines from `set_*` functions appear on stderr and not on stdout + +**Cause:** In 3.0 a `CONFIG` notice goes to the same stream as a NOTICE message, so it follows the stderr level. In 2.x it always went to stdout. +With `--stderr-level` set to `NOTICE`, `INFO` or `DEBUG`, `CONFIG` notices go to stderr. With the default level (`ERROR`) they stay on stdout. + +**Solution:** + +```bash +# Keep CONFIG notices on stdout: leave the stderr level at WARN or less verbose +init_logger --stderr-level WARN + +# Or read them from stderr +./script.sh 2> stderr.log + +# Or do not print them +init_logger --quiet --log "/var/log/app.log" +``` + +See [Runtime Configuration Notices](output-streams.md#runtime-configuration-notices). + ## Runtime Configuration Issues ### set_log_level Not Working @@ -993,6 +1066,29 @@ init_logger --level ERROR # Correct # Not: init_logger --level TRACE ``` +### "requires a value" + +```text +Error: --format requires a value +``` + +**Cause:** An option that takes a value was the last argument, or its value was missing. This applies to `--config`, `--level`, `--format`, `--log`, `--name`, `--tag` and `--stderr-level`. +`init_logger` returns 1. In 2.x under bash, this looped forever. + +The usual cause is an unquoted variable that is empty, because the shell then drops it from the argument list. + +**Solution:** + +```bash +# Wrong when FORMAT is empty: the argument disappears +init_logger --format $FORMAT + +# Check the value first, and quote it +if [ -n "$FORMAT" ]; then + init_logger --format "$FORMAT" +fi +``` + ### "Configuration file not found" / "Configuration file not readable" **Cause:** Config file path is incorrect or file has wrong permissions @@ -1049,9 +1145,39 @@ init_logger --log /actual/path.log **Note:** bash-logger rejects symbolic links for security reasons to prevent log redirection attacks. +### "Log file path is now a symbolic link" + +```text +ERROR: Log file path is now a symbolic link; not writing to it + Hint: Something replaced the log file after init_logger checked it +``` + +**Cause:** `init_logger` accepted the log file, but a symbolic link was put in its place while the script was running. +bash-logger checks the path again before every entry and does not write through a link. +The error is printed once. Until the link is removed, each entry is printed to stderr and not written to the file. The script keeps running. + +**Solution:** + +```bash +# Check what is at the path now, and where the link points +ls -l /path/to/app.log + +# Find out what created it (a deployment step, another user, or an attack), +# then remove the link and restore the file +rm /path/to/app.log +touch /path/to/app.log +``` + +Later entries are written to the file again once the link is gone. Keep log files in a directory that other users cannot write to, so that they cannot create the link. + ### "Log file is not writable" / "Failed to write to log file" -**Cause:** File exists but lacks write permissions, or disk is full +**Cause:** + +* `Log file is not writable` comes from `init_logger`: the file exists but you cannot write to it +* `Failed to write to log file` comes later, while the script runs: the file became unwritable or was deleted, or the disk is full + +A failed write prints one error, and the entry goes to stderr so it is not lost. The shell's own message, such as `Is a directory`, no longer appears next to it. **Solution:** @@ -1061,8 +1187,9 @@ See [Understanding Error Messages Without Path Disclosure](#understanding-error- # Check file permissions ls -l "$LOG_FILE_PATH" -# Fix permissions -chmod 644 "$LOG_FILE_PATH" +# Fix permissions: the owner needs read and write access +# (600 is the mode init_logger gives a file it creates) +chmod 600 "$LOG_FILE_PATH" # Check disk space df -h /path/to/log/directory/ @@ -1089,13 +1216,28 @@ init_logger log_info "Test message" ``` -1. **Check your Bash version:** +1. **Check that your shell is supported, and its version:** + + `logging.sh` runs in bash 3.2 or later, bash `--posix`, zsh, dash and busybox ash. See [Shell Compatibility](shell-compatibility.md#supported-shells) for the list. + Find the version of the shell that runs your script: ```bash -bash --version -# Module requires Bash 3.2 or later (the /bin/bash that macOS ships is 3.2) +# bash (3.2 or later; the /bin/bash that macOS ships is 3.2) +echo "$BASH_VERSION" + +# zsh +zsh --version + +# busybox ash (/bin/sh on Alpine) +busybox | head -1 + +# dash has no version option. Ask the package manager: +dpkg -s dash | grep '^Version' # Debian, Ubuntu +rpm -q dash # Fedora, RHEL ``` + To see which shell runs a script, add `ps -p $$ -o comm=` to it. + 1. **Verify file permissions:** ```bash diff --git a/docs/upgrading-to-3.0.md b/docs/upgrading-to-3.0.md index 5cbb39b..567ab84 100644 --- a/docs/upgrading-to-3.0.md +++ b/docs/upgrading-to-3.0.md @@ -36,6 +36,9 @@ what a log line shows. and paragraph separators U+2028 and U+2029, and the bidirectional formatting characters U+202A to U+202E and U+2066 to U+2069. 2.x logged them unchanged. Newline, carriage return and tab are still replaced by spaces, as before. +* **Some invisible characters are still kept:** the bidirectional marks U+200E, U+200F and U+061C, + and the zero-width characters U+200B and U+FEFF. They cannot reverse a line the way the removed + overrides and isolates can. * **No escape character is left behind.** 2.x left a lone escape character, an incomplete sequence, and some nested sequences in the log. It also removed an escape character together with the first byte of a UTF-8 character after it, which left invalid UTF-8; 3.0 diff --git a/docs/writing-tests.md b/docs/writing-tests.md index 12769e9..9c381ce 100644 --- a/docs/writing-tests.md +++ b/docs/writing-tests.md @@ -21,6 +21,11 @@ For the full assertion and helper API reference, see [Testing](testing.md). * [A flag or config option is respected](#a-flag-or-config-option-is-respected) * [Invalid input is handled gracefully](#invalid-input-is-handled-gracefully) * [A test is skipped when a dependency is unavailable](#a-test-is-skipped-when-a-dependency-is-unavailable) +* [Tests that run under more than one shell](#tests-that-run-under-more-than-one-shell) + * [Adding a scenario](#adding-a-scenario) + * [Known failures](#known-failures) + * [Golden files](#golden-files) + * [Shell helpers](#shell-helpers) * [What not to do](#what-not-to-do) * [Getting started quickly](#getting-started-quickly) @@ -356,6 +361,64 @@ test_journal_logging_writes_entry `skip_test` records the test as skipped in the summary and JUnit output. Always `return` immediately after calling it — execution must not continue into the test body. +## Tests that run under more than one shell + +`logging.sh` is POSIX sh plus `local`, and it runs in bash 3.2+, bash `--posix`, zsh, dash and busybox ash. The test suites themselves run under bash. A change to `logging.sh` needs a +check under the other shells as well. See [Shell Matrix Suites](testing.md#shell-matrix-suites) for the commands, `TEST_SHELLS` and `TEST_REQUIRE_SHELLS`. + +Use a normal test (everything above) when behaviour does not depend on the shell. +Use the patterns below when a new behaviour must give the same result in every shell, or when a shell is known to differ. + +### Adding a scenario + +`test_shell_compat.sh` runs every script in `tests/compat/scenarios/` under bash to get a reference result, then under each other shell in `TEST_SHELLS`. It compares stdout, stderr, the log file +and the exit status. To cover a new behaviour, add a scenario file: + +```sh +# shellcheck shell=sh +# Short description of the behaviour +. "$LOGGER_ROOT/logging.sh" +init_logger --no-color --log "$SCENARIO_DIR/test.log" --format "%l|%m" +log_info "the behaviour under test" +``` + +* Write it in POSIX sh. It starts with `# shellcheck shell=sh`, sources `"$LOGGER_ROOT/logging.sh"` with `.`, and writes any log file to `"$SCENARIO_DIR/test.log"`. +* Scenarios run with `LC_ALL=C`. To use another locale, add a header line such as `# compat-locale: C.UTF-8`. The scenario is skipped if the locale is not installed. +* There is no registration step and no assertion. The suite discovers the file, and the test passes when every shell matches bash. + +### Known failures + +If a shell is expected to differ from bash for now, add a line to `tests/compat/known_failures.txt` in the form `|`. +The scenario must exist. An entry for dash and `colors.sh` is written `dash|colors.sh`. + +The suite reports a listed pair as skipped while it differs. It fails once the pair matches bash, so remove the line when the difference is fixed. +Do not add an entry to hide a new regression. + +### Golden files + +`test_validator_equivalence.sh` and `test_sanitize_equivalence.sh` compare the current code with results that were recorded from bash before the POSIX conversion (`validators.tsv`, +`validators_utf8.tsv` and `sanitize.tsv` in `tests/compat/golden/`). Their purpose is to show that the conversion changed nothing, so: + +* Do not regenerate these files from the current `logging.sh`. A regenerated file makes the suite pass whatever the code does. +* A mismatch means behaviour changed. Fix the code, or decide that the change is intended. +* An intended change in how messages are sanitized is recorded in `sanitize_3.0.tsv`, with a reason for each row, by `tests/compat/generate_sanitize_changes.sh`. That script takes both the 2.7.0 + `logging.sh` and the current one. `sanitize.tsv` stays the record of 2.7.0. + +### Shell helpers + +`tests/test_helpers.sh` provides these helpers for tests that use more than one shell: + +| Helper | Use | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `test_shell_list` | Prints the shells in `TEST_SHELLS`, one per line (default `bash`). Loop over it to run a test in each shell. | +| `require_shell NAME` | Skips the test when a shell is not installed, or fails it when the shell is in `TEST_REQUIRE_SHELLS`. | +| `run_with_logger_in_shell SHELL` | Runs a snippet in the named shell after sourcing `logging.sh`. | +| `shell_counts_characters COMMAND` | Succeeds for bash and zsh, which count characters. dash and busybox ash count bytes, so length checks differ. | +| `run_with_timeout SECONDS CMD...` | Stops a command after a number of seconds, including its child processes, without needing `timeout`. | +| `list_descendant_pids PID` | Prints the PIDs of a process's descendants. `run_with_timeout` uses it; call it to clean up helper processes. | + +Write the call as `require_shell zsh || return 0` so that a skipped test does not end the suite with a non-zero status. + ## What not to do **Forgetting `|| return` after an assertion** diff --git a/tests/README.md b/tests/README.md index 8de444e..5ce2cc5 100755 --- a/tests/README.md +++ b/tests/README.md @@ -42,15 +42,25 @@ TEST_PARALLEL_JOBS=4 ./run_tests.sh ## Test Suites -The test suite includes 201 tests across 7 test suites: +Each `tests/test_*.sh` file is one suite. List the current suites and run them to get the current number of tests: -* **test_log_levels.sh** (12 tests) - Log level functionality -* **test_initialization.sh** (21 tests) - Logger initialization -* **test_output.sh** (17 tests) - Output routing and formatting -* **test_format.sh** (16 tests) - Message format templates -* **test_config.sh** (21 tests) - Configuration file parsing -* **test_runtime_config.sh** (16 tests) - Runtime configuration changes -* **test_unsafe_newlines.sh** (17 tests) - Log injection prevention and unsafe mode +```bash +ls test_*.sh +./run_tests.sh +``` + +The suites fall into these areas: + +* **Levels, initialization and output** - `test_log_levels`, `test_initialization`, `test_output`, `test_source_guard`, `test_journal_logging` +* **Formatting** - `test_format` +* **Configuration** - `test_config`, `test_runtime_config` +* **Security: message content** - `test_ansi_injection`, `test_unsafe_newlines`, `test_mixed_sanitization_modes`, `test_script_name_sanitization`, `test_sensitive_data` +* **Security: files, environment and input** - `test_path_traversal`, `test_toctou_protection`, `test_environment_security`, `test_config_security`, `test_fuzzing` +* **Robustness** - `test_edge_cases`, `test_error_conditions`, `test_concurrent_access`, `test_resource_limits` +* **Cross-shell compatibility** - `test_shell_compat`, `test_posix_compat`, `test_zsh_compat`, `test_validator_equivalence`, `test_sanitize_equivalence`, `test_bash32_compat_check` +* **Repository tooling and test infrastructure** - `test_install`, `test_license_year`, `test_junit_output`, `test_runner` + +`test_example.sh` is a template for new suites and `test_helpers.sh` is the shared library. The runner does not run either of them. ## Test Output @@ -63,19 +73,33 @@ Running test_log_levels... ✓ Log level constants are defined ✓ FATAL is alias for EMERGENCY ... -✓ test_log_levels: 12 passed +✓ test_log_levels: N passed ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Test Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Total Tests: 201 -Passed: 201 +Total Tests: N +Passed: N Failed: 0 +Skipped: 0 All tests passed! ``` +## Cross-Shell Runs + +The library runs in bash 3.2+, bash `--posix`, zsh, dash and busybox ash. The test runner itself runs under bash. Four suites run `logging.sh` under every shell listed in `TEST_SHELLS`, +which defaults to `bash`: `test_shell_compat`, `test_validator_equivalence`, `test_sanitize_equivalence` and `test_posix_compat`. Run them after any change to `logging.sh`: + +```bash +TEST_SHELLS="bash,bash --posix,zsh,dash,busybox sh" \ + ./run_tests.sh test_shell_compat test_validator_equivalence test_sanitize_equivalence test_posix_compat +``` + +A shell that is not installed is skipped, unless it is named in `TEST_REQUIRE_SHELLS`. For `TEST_REQUIRE_SHELLS`, the scenario format and the golden files, see +[Shell Matrix Suites](../docs/testing.md#shell-matrix-suites). + ## Documentation For comprehensive information about the test suite, see: @@ -94,9 +118,11 @@ For comprehensive information about the test suite, see: * **run_tests.sh** - Main test runner * **test_helpers.sh** - Assertion functions and test utilities * **test\_\*.sh** - Individual test suites +* **compat/** - Scenarios, golden files and known failures for the cross-shell suites ## Requirements -* Bash 4.0 or later +* Bash 3.2 or later (the Bash 3.2 that macOS ships as `/bin/bash` is supported) * Standard Unix utilities (cat, grep, wc, date, mkdir, touch) * Optional: `logger` command (for journal logging tests) +* Optional: `zsh`, `dash` and `busybox` (for cross-shell runs)