From 4bd41ba26541728c1076a380244867ed320fcc9e Mon Sep 17 00:00:00 2001 From: Martin Zeman Date: Sun, 26 Apr 2026 14:51:13 +0200 Subject: [PATCH] feat: add BashDevelopment and AuditScripts skills - BashDevelopment captures generic bash and POSIX hygiene: built-ins over subprocesses, fail-safe defaults, probe guarding, multi-line command substitution, quoting, exit-code semantics, function layout, lint expectations. - AuditScripts captures the language-agnostic audit-script architecture: severity ladder (OK/WARN/CRIT/INFO/UNKNOWN), key:value output contract, standalone-runnable checks, orchestrator dispatch, --strict exit-code mode, anti-patterns. Bash and Python reference snippets. - Both reference check-mac as the canonical implementation; cross-reference each other. Roster wiring (defaults.yaml) and README.md skills-table entries left for a follow-up. --- skills/AuditScripts/SKILL.md | 197 +++++++++++++++++++++++++++++ skills/AuditScripts/SKILL.yaml | 21 ++++ skills/BashDevelopment/SKILL.md | 200 ++++++++++++++++++++++++++++++ skills/BashDevelopment/SKILL.yaml | 19 +++ 4 files changed, 437 insertions(+) create mode 100644 skills/AuditScripts/SKILL.md create mode 100644 skills/AuditScripts/SKILL.yaml create mode 100644 skills/BashDevelopment/SKILL.md create mode 100644 skills/BashDevelopment/SKILL.yaml diff --git a/skills/AuditScripts/SKILL.md b/skills/AuditScripts/SKILL.md new file mode 100644 index 0000000..0cb6001 --- /dev/null +++ b/skills/AuditScripts/SKILL.md @@ -0,0 +1,197 @@ +--- +name: AuditScripts +version: 0.1.0 +description: "Architecture for security and health audit scripts: standalone-runnable checks, severity ladder with UNKNOWN, key:value output contract, orchestrator dispatch, exit-code semantics. Language-agnostic; reference implementations in Bash, applies to Python and other languages. USE WHEN writing audit tools, designing check-script contracts, adding checks to tools like check-mac, reviewing audit-script architecture, or porting an audit tool between languages." +--- + +# AuditScripts + +A contract for writing security and health audit scripts. The same contract works in Bash, Python, or any other language that can write structured output to stdout. Reference implementation: [check-mac][CHECKMAC] (Bash). For shell-specific idioms, see the companion skill [BashDevelopment](../BashDevelopment/SKILL.md). + +The architecture has three load-bearing pieces: + +1. **Check scripts**, each independently runnable, each emitting `key:value` lines. +2. **A severity ladder** with five steps including UNKNOWN. +3. **An orchestrator** that runs checks in series, dispatches severity codes to display, and decides exit-code semantics. + +## Severity Ladder + +Five severities, [Nagios plugin convention][NAGIOS] extended with UNKNOWN: + +| Code | Name | Meaning | +| ---- | ------- | ----------------------------------------------------------- | +| 0 | OK | Observed and matches expected | +| 1 | WARN | Observed misconfiguration, recommended fix | +| 2 | CRIT | Observed misconfiguration, critical | +| 3 | INFO | Informational, no action needed (hardware, hostname, etc.) | +| 4 | UNKNOWN | Could not determine, manual verification recommended | + +Two invariants: + +- **OK requires positive evidence.** A check passes only when it has read a value and confirmed it matches the expected setting. Empty source never silently passes. +- **UNKNOWN is the default for empty source.** When `defaults read` returns nothing, when a CLI is missing, when a configuration profile may have overridden the legacy plist domain, the result is UNKNOWN. Not OK, not CRIT. + +This makes a green run meaningful: every OK is backed by observation. UNKNOWN tells the user where to look manually. + +## Output Contract + +Each check writes `key:value` lines to stdout. Each `pass_:` carries a severity. Display values (versions, lists, counts) appear as additional lines for the orchestrator to render alongside the result. + +```text +version:14.5.1 +days_old:32 +pass_xprotect:0 +``` + +Lines are independent; order does not matter. Values may contain colons (IPv6 addresses, status strings, hostnames with ports), so parsers must split on the *first* colon only and keep the rest as-is. + +## Standard Pattern + +Default `$UNKNOWN`, flip on observed evidence (good or bad). Two flips per setting in the simplest case. + +### Bash + +```sh +#!/bin/bash +# Source: URL + +# Retrieve value +setting=$( + defaults read com.example.plist Key 2>/dev/null +) + +# Severity codes +OK=0; WARN=1; CRIT=2; INFO=3; UNKNOWN=4 + +pass_check=$UNKNOWN +[[ "$setting" == "expected" ]] && pass_check=$OK +[[ "$setting" == "bad-value" ]] && pass_check=$WARN + +echo "pass_check:$pass_check" +``` + +### Python + +```python +#!/usr/bin/env python3 +"""Check whether a managed setting matches the expected value.""" +import subprocess + +OK, WARN, CRIT, INFO, UNKNOWN = 0, 1, 2, 3, 4 + +result = subprocess.run( + ["defaults", "read", "com.example.plist", "Key"], + capture_output=True, text=True, +).stdout.strip() + +pass_check = UNKNOWN +if result == "expected": + pass_check = OK +elif result == "bad-value": + pass_check = WARN + +print(f"pass_check:{pass_check}") +``` + +In both languages, the empty-source case is preserved (no `result or "0"` defaulting) so UNKNOWN remains distinguishable from a positively-observed value. + +## Standalone Runnability + +Each check script must be runnable on its own without sourcing a lib or being driven by the orchestrator: + +```sh +./checks/filevault.sh +# → version:14.5 +# pass_filevault:0 +``` + +This means severity constants are defined inside each check (or imported from a tiny single-purpose module), not from a shared style lib that may not be reachable. The duplication cost (a few lines per file) is the price for the property: any check is its own test harness. + +The orchestrator-side helpers (color rendering, severity dispatch, summary counters) live in a separate lib that only the orchestrator sources. Checks know nothing about display. + +## Probe Guarding + +A check must not provoke an interactive installer or prompt. On macOS, `git --version` triggers the Xcode CLT GUI installer when CLT is absent. Guard external CLI invocations: + +```sh +# Bash +git_version="" +if command -v git >/dev/null 2>&1 && xcode-select -p >/dev/null 2>&1; then + git_version=$(git --version 2>/dev/null | awk '{print $3}') +fi +``` + +```python +# Python +import shutil + +git_version = "" +if shutil.which("git") and subprocess.run(["xcode-select", "-p"], capture_output=True).returncode == 0: + git_version = subprocess.run(["git", "--version"], capture_output=True, text=True).stdout.split()[-1] +``` + +The principle is language-agnostic: probe presence before invocation, treat absence as `pass_=$INFO` ("not installed, no action needed") rather than as a failure unless the check is specifically about the tool's installation. + +## Orchestrator + +The orchestrator runs each check, parses its output, dispatches severity codes to display, and accumulates counters. A minimal Bash sketch: + +```sh +issues=0 +unknowns=0 + +run() { "$DIR/$1.sh" 2>/dev/null; } +key() { echo "$data" | grep "^$1:" | cut -d: -f2-; } # f2-, never f2 + +pass() { printf " ✓ %s (%s)\n" "$1" "$2"; } +warn() { printf " ! %s (%s)\n" "$1" "$2"; ((issues++)); } +fail() { printf " ✗ %s (%s)\n" "$1" "$2"; ((issues++)); } +info() { printf " ℹ %s (%s)\n" "$1" "$2"; } +unknown() { printf " ? %s (%s)\n" "$1" "$2"; ((unknowns++)); } + +check() { + case "$1" in + 0) pass "$2" "$3" ;; + 1) warn "$2" "$4" ;; + 2) fail "$2" "$4" ;; + 3) info "$2" "$4" ;; + 4) unknown "$2" "Indeterminate" ;; + *) unknown "$2" "Invalid code" ;; + esac +} + +data=$(run filevault) +check "$(key pass_filevault)" "FileVault" "Enabled" "Disabled" +``` + +Track `issues` and `unknowns` separately. Display them separately in the summary. UNKNOWN is not an issue (the user has not observed a misconfiguration), but it is not a clean pass either. + +## Exit-Code Semantics + +Two modes, an explicit flag for the second: + +| Mode | Behavior | +| ---------- | ------------------------------------------------------------------------------------- | +| Default | Always exit `0`. The issues counter is a visual cue for humans. | +| `--strict` | Exit `1` when `issues + unknowns > 0`. For CI gates and bootstrap scripts. | + +Preserve the `0` default. Many users invoke the orchestrator interactively and pipe through `tee` or `less`; flipping the default to non-zero would surprise existing users and break those flows. CI users opt in via `--strict`, which counts UNKNOWN as a failure because a CI gate cannot defer to a human for manual verification. + +## Anti-Patterns + +- **`${var:-0}` defaulting on a managed key.** Collapses the empty case (UNKNOWN) into a known value (CRIT). Never apply this pattern to keys that may live in a configuration profile. +- **Default severity = OK.** Inverts the safety polarity. Always default to UNKNOWN or the relevant failure severity, then flip to OK on positive match. +- **Sourcing the orchestrator's style lib from a check.** Breaks standalone runnability when the check is run from a different working directory. +- **Probing tools without `command -v`.** On a fresh machine the probe itself can trigger an installer prompt mid-audit. +- **Spawning a subprocess for pattern matching the language can do natively.** `grep` for substring detection in Bash; `subprocess.run` with `grep` in Python; both are the wrong layer. + +## Why This Architecture + +- **Standalone runnability**, every check is its own test harness; run it, read the `pass_*` lines. +- **Separation of concerns**, data and testing in the check, display in the orchestrator. +- **Honesty**, UNKNOWN means "we did not observe", OK means "we observed and it is good." Empty source must never silently pass. +- **Language portability**, the contract is text-based; any language that can print `key:value` to stdout participates. +- **Replaceability**, the orchestrator can be rewritten without touching any check. + +[NAGIOS]: https://nagios-plugins.org/doc/guidelines.html#AEN78 +[CHECKMAC]: https://github.com/N4M3Z/check-mac diff --git a/skills/AuditScripts/SKILL.yaml b/skills/AuditScripts/SKILL.yaml new file mode 100644 index 0000000..1ef9f1e --- /dev/null +++ b/skills/AuditScripts/SKILL.yaml @@ -0,0 +1,21 @@ +sources: + - https://github.com/N4M3Z/check-mac + - https://github.com/N4M3Z/check-mac/blob/main/docs/decisions/ARCH-0002%20Nagios%20Return%20Codes.md + - https://github.com/N4M3Z/check-mac/blob/main/docs/decisions/ARCH-0003%20Handle%20Managed%20Devices.md + - https://nagios-plugins.org/doc/guidelines.html + +title: Audit Scripts +aliases: [HealthChecks, NagiosChecks, CheckScripts] +tags: [audit, security, health-check, nagios, language-agnostic, forge-dev] +keywords: + - "[[Health Check]]" + - "[[Audit]]" + - "[[Nagios]]" + - "[[macOS Security]]" +collection: "[[Skills]]" +created: 2026-04-26 +updated: 2026-04-26 +related: + - "[[BashDevelopment]]" + - "[[DefensiveProgramming]]" + - "[[CodeCleanup]]" diff --git a/skills/BashDevelopment/SKILL.md b/skills/BashDevelopment/SKILL.md new file mode 100644 index 0000000..0f144d2 --- /dev/null +++ b/skills/BashDevelopment/SKILL.md @@ -0,0 +1,200 @@ +--- +name: BashDevelopment +version: 0.1.0 +description: "Bash and POSIX shell conventions for the forge ecosystem — idioms over subprocesses, fail-safe defaults, probe guarding, multi-line command substitution, exit-code semantics. USE WHEN writing or reviewing shell scripts (audit tools, hooks, build scripts), designing CLI flag handling, or wiring shell tools into CI." +--- + +# BashDevelopment + +Conventions for writing bash and POSIX shell scripts in the forge ecosystem and adjacent personal projects. Reference implementation lives in [check-mac][CHECKMAC] and the build hooks under forge-core. + +For audit-script architecture (severity ladders, key:value output, orchestrator dispatch), see the companion skill [AuditScripts](../AuditScripts/SKILL.md). + +## Idioms + +Use bash built-ins instead of spawning subprocesses for things bash can do natively: + +```sh +# Substring (case-sensitive) +[[ "$status" == *"enabled"* ]] + +# Regex +[[ "$ports" =~ \.22[[:space:]] ]] +[[ "$dns" =~ ^(1\.1\.1\.1|8\.8\.8\.8)$ ]] + +# Wildcard +[[ "$groups" == *admin* ]] + +# Prefix / suffix removal +filename="${path##*/}" # basename +extension="${filename##*.}" # extension +trimmed="${var%, }" # strip trailing ", " +``` + +Avoid: + +```sh +echo "$var" | grep -q "pattern" # spawns a subprocess for what bash can do natively +basename="$(basename "$path")" # use ${path##*/} instead +``` + +Use `[[ ]]` for tests, never `[ ]`. `[[ ]]` is bash-aware (no word splitting, supports `=~`, supports `&&`/`||` inside). + +## Fail-Safe Defaults + +Default to the safe value, then flip on observed evidence. Never default to OK: + +```sh +pass_check=$CRIT +[[ "$setting" == "expected" ]] && pass_check=$OK +``` + +For values that may legitimately be empty (managed-config keys, profile-overridden plist domains), do not collapse empty into a known value: + +```sh +# WRONG — masks the empty case +setting=${setting:-0} + +# RIGHT — preserve the empty distinction +[[ -z "$setting" ]] && pass_check=$UNKNOWN +[[ "$setting" == "1" ]] && pass_check=$OK +[[ "$setting" == "0" ]] && pass_check=$CRIT +``` + +## Probe Guarding + +A probe must not provoke an interactive installer or prompt. On macOS, `git --version` triggers the Xcode CLT GUI installer when CLT is absent. Guard with `command -v`, plus `xcode-select -p` for Xcode-tooling probes: + +```sh +git_version="" +if command -v git >/dev/null 2>&1 && xcode-select -p >/dev/null 2>&1; then + git_version=$(git --version 2>/dev/null | awk '{print $3}') +fi +``` + +Apply the `command -v` guard before any third-party CLI (`brew`, `gpg`, `openssl`). An absent tool is not a misconfiguration unless the script is specifically *about* the tool's installation. + +## Multi-Line Command Substitution + +For readability, break long pipelines across lines: + +```sh +firewall_enabled=$( + defaults read /Library/Preferences/com.apple.alf globalstate 2>/dev/null +) +``` + +```sh +non_apple_kexts=$( + kmutil showloaded 2>/dev/null \ + | awk 'NR>1 {print $NF}' \ + | grep -v '^com\.apple\.' +) +``` + +Indent the body four spaces. Closing paren on its own line. Same shape works for `local val=$(...)`: + +```sh +local val +val=$( + defaults read "$DOMAIN" "$KEY" 2>/dev/null +) +``` + +## Variable Quoting + +Always quote variables in tests and command substitutions, except inside `[[ ]]` where bash does the right thing: + +```sh +# Quote in shell command lines +mv "$src" "$dst" +echo "$result" + +# [[ ]] does not require quoting for word-splitting, but quote for clarity +[[ "$status" == *enabled* ]] + +# Always quote command substitutions in calling sites +check "$(key pass_x)" "Label" "$ENABLED" "$DISABLED" +``` + +## Suppressing Expected Errors + +`2>/dev/null` to silence stderr from probes that may legitimately fail (absent plist key, missing CLI, no matching process): + +```sh +setting=$(defaults read com.apple.foo Bar 2>/dev/null) +listening=$(lsof -i :22 2>/dev/null) +``` + +Do not suppress stdout. Do not redirect to `/dev/null` everything wholesale — that hides bugs. Only silence stderr where empty output is the legitimate signal. + +## Exit-Code Semantics + +For tools that humans run interactively, exit `0` always and report status visually. For the same tool wired into CI, expose an explicit `--strict` flag that propagates non-zero on failure. Never make non-zero exit the default; that surprises every existing user and breaks pipes through `tee` or `less`. + +```sh +strict=0 +for arg in "$@"; do + case "$arg" in + --strict) strict=1 ;; + -h|--help) + echo "Usage: $0 [--strict]" + exit 0 + ;; + esac +done + +# ... do work, accumulate $issues ... + +if (( strict && issues > 0 )); then + exit 1 +fi +``` + +## Function Layout + +Helpers go in a sourced lib. Standalone scripts inline what they need. Use `local` for function-scope variables: + +```sh +key() { + local field="$1" + echo "$data" | grep "^${field}:" | cut -d: -f2- +} +``` + +`cut -d: -f2-` (with the trailing dash) preserves trailing colons in values; `-f2` truncates at the first colon and silently corrupts IPv6 addresses, hostnames with ports, and structured status strings. + +Avoid `local val=$(cmd)` because the assignment masks the return code of `cmd`. Split into two lines if the return code matters: + +```sh +local val +val=$(cmd) +``` + +## Linting + +Run `shellcheck` before committing. Treat warnings as bugs unless they flag a deliberate pre-existing pattern; in that case, add a `# shellcheck disable=SCxxxx` comment with a one-line reason. + +Common pre-existing patterns the lint may flag: + +- `SC2155` (mask return value with `local val=$(...)`): split into two lines, see above. +- `SC2154` (variable referenced but not assigned): expected for sourced libs that read variables set by the caller. +- `SC2034` (variable appears unused): expected for status-string constants in a sourced style lib. + +## Cross-Platform Considerations + +Bash version varies. macOS ships an old bash 3.2 at `/bin/bash`; modern bash (4+, 5+) is at `/opt/homebrew/bin/bash` or `/usr/local/bin/bash`. Scripts using `[[ ]]`, `${var//}`, and `=~` work in 3.2; associative arrays (`declare -A`) require 4+. + +If a script needs bash 4+, use `#!/usr/bin/env bash` and document the dependency. If portability to old macOS bash matters, use `#!/bin/bash` and stay in 3.2-compatible territory. + +For coreutils differences (`sed`, `date`, `readlink`), prefer POSIX flags or guard with `command -v gsed` / `command -v gdate`. + +## Code Style + +- Self-documenting names. A comment is a smell unless it explains *why*. +- 4-space indentation, never tabs. +- Liberal blank lines between logical sections. +- One `# Source:` comment at the top of any check or audit script pointing to the upstream reference. +- Lowercase function names with snake_case multi-word (`read_hook_input`). + +[CHECKMAC]: https://github.com/N4M3Z/check-mac diff --git a/skills/BashDevelopment/SKILL.yaml b/skills/BashDevelopment/SKILL.yaml new file mode 100644 index 0000000..383a0b7 --- /dev/null +++ b/skills/BashDevelopment/SKILL.yaml @@ -0,0 +1,19 @@ +sources: + - https://github.com/N4M3Z/check-mac + - https://www.shellcheck.net/ + - https://mywiki.wooledge.org/BashGuide + +title: Bash Development +aliases: [BashConventions, ShellPatterns, ForgeShell] +tags: [bash, shell, posix, scripting, forge-dev] +keywords: + - "[[Bash]]" + - "[[Shell Scripting]]" + - "[[POSIX]]" +collection: "[[Skills]]" +created: 2026-04-26 +updated: 2026-04-26 +related: + - "[[AuditScripts]]" + - "[[DefensiveProgramming]]" + - "[[CodeCleanup]]"