@sebastienrousseau/dotfiles 0.2.519 → 0.2.520

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +1163 -163
  3. package/docs/ARCHITECTURE.md +117 -0
  4. package/docs/COPYRIGHT +1 -1
  5. package/docs/ECOSYSTEM.md +220 -0
  6. package/docs/GOLD-STANDARD-AUDIT.md +352 -0
  7. package/docs/GOVERNANCE.md +1 -1
  8. package/docs/MAINTAINERS.md +1 -1
  9. package/docs/MINIMUM-TOOLCHAIN.md +100 -0
  10. package/docs/README.md +1 -1
  11. package/docs/STRUCTURE.md +1 -1
  12. package/docs/architecture/ARCHITECTURE.md +11 -108
  13. package/docs/architecture/REPO_LAYOUT.md +3 -3
  14. package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +121 -0
  15. package/docs/index.md +3 -3
  16. package/docs/manual/00-introduction.md +1 -1
  17. package/docs/manual/03-reference/01-dot-cli.md +28 -3
  18. package/docs/manual/command-index.md +24 -4
  19. package/docs/manual/concept-index.md +2 -0
  20. package/docs/migration/README.md +81 -0
  21. package/docs/migration/from-bare-git-repo.md +156 -0
  22. package/docs/migration/from-gnu-stow.md +165 -0
  23. package/docs/migration/from-plain-chezmoi.md +148 -0
  24. package/docs/migration/from-yadm.md +187 -0
  25. package/docs/operations/PERFORMANCE_BUDGETS.md +196 -0
  26. package/docs/operations/REGISTRY.md +1 -1
  27. package/docs/operations/RELEASE_PIPELINE.md +4 -4
  28. package/docs/operations/TESTING.md +3 -3
  29. package/docs/operations/TRACEABILITY.md +1 -0
  30. package/docs/packaging.md +222 -0
  31. package/docs/reference/FEATURE-MATRIX.md +646 -0
  32. package/docs/reference/TOOLS.md +1 -1
  33. package/docs/reference/UTILS.md +1 -0
  34. package/docs/security/COMPLIANCE.md +1 -1
  35. package/docs/security/DISCLOSURE.md +4 -4
  36. package/docs/security/FUZZING.md +113 -18
  37. package/docs/security/KEY_ROTATION.md +1 -1
  38. package/docs/security/SCORECARD.md +3 -3
  39. package/docs/security/VERIFY_RELEASE.md +2 -2
  40. package/install.sh +6 -6
  41. package/package.json +2 -2
  42. package/scripts/ci/check-copyright-headers.sh +1 -1
  43. package/scripts/ci/check-shell-preamble.sh +1 -1
  44. package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
  45. package/scripts/demo/record.sh +1 -1
  46. package/scripts/diagnostics/a2a-conformance.sh +1 -1
  47. package/scripts/diagnostics/alias-governance.sh +30 -3
  48. package/scripts/diagnostics/aliases-cheatsheet.sh +1 -1
  49. package/scripts/diagnostics/aliases-manifest.sh +1 -1
  50. package/scripts/diagnostics/attest-verify.sh +147 -0
  51. package/scripts/diagnostics/benchmark.sh +1 -1
  52. package/scripts/diagnostics/conflicts.sh +1 -1
  53. package/scripts/diagnostics/doctor-unified.sh +6 -2
  54. package/scripts/diagnostics/doctor.sh +56 -10
  55. package/scripts/diagnostics/drift-dashboard.sh +3 -2
  56. package/scripts/diagnostics/health.sh +43 -10
  57. package/scripts/diagnostics/history-analysis.sh +1 -1
  58. package/scripts/diagnostics/mcp-doctor.sh +2 -2
  59. package/scripts/diagnostics/perf.sh +1 -1
  60. package/scripts/diagnostics/scorecard.sh +3 -2
  61. package/scripts/diagnostics/secret-governance.sh +1 -1
  62. package/scripts/diagnostics/security-score.sh +1 -1
  63. package/scripts/diagnostics/smoke-test.sh +1 -1
  64. package/scripts/diagnostics/snapshot.sh +1 -1
  65. package/scripts/diagnostics/verify.sh +1 -1
  66. package/scripts/diagnostics/verify_state.sh +1 -1
  67. package/scripts/diagnostics/version-locks.sh +1 -1
  68. package/scripts/diagnostics/workstation-attestation.sh +26 -1
  69. package/scripts/dot/commands/agent.sh +64 -14
  70. package/scripts/dot/commands/agents.sh +24 -8
  71. package/scripts/dot/commands/ai.sh +20 -7
  72. package/scripts/dot/commands/aliases.sh +1 -1
  73. package/scripts/dot/commands/appearance.sh +1 -1
  74. package/scripts/dot/commands/completion.sh +42 -5
  75. package/scripts/dot/commands/core.sh +1 -1
  76. package/scripts/dot/commands/diagnostics.sh +1 -1
  77. package/scripts/dot/commands/env-emit.sh +1 -1
  78. package/scripts/dot/commands/fleet.sh +41 -18
  79. package/scripts/dot/commands/init.sh +1 -1
  80. package/scripts/dot/commands/lint.sh +1 -1
  81. package/scripts/dot/commands/manual.sh +1 -1
  82. package/scripts/dot/commands/meta.sh +115 -10
  83. package/scripts/dot/commands/patterns.sh +1 -1
  84. package/scripts/dot/commands/registry.sh +43 -7
  85. package/scripts/dot/commands/restore.sh +1 -1
  86. package/scripts/dot/commands/secrets.sh +1 -1
  87. package/scripts/dot/commands/security.sh +1 -1
  88. package/scripts/dot/commands/tools.sh +21 -7
  89. package/scripts/fonts/install-nerd-fonts.sh +1 -1
  90. package/scripts/fonts/patch-fonts.sh +1 -1
  91. package/scripts/git-hooks/install.sh +1 -1
  92. package/scripts/git-hooks/pre-commit-audit.sh +2 -2
  93. package/scripts/lib/secrets_provider.sh +19 -4
  94. package/scripts/nvim/headless-upgrade.lua +81 -0
  95. package/scripts/ops/ai-setup.sh +1 -1
  96. package/scripts/ops/bundle.sh +1 -1
  97. package/scripts/ops/chaos.sh +1 -1
  98. package/scripts/ops/chezmoi-apply.sh +1 -1
  99. package/scripts/ops/chezmoi-diff.sh +1 -1
  100. package/scripts/ops/chezmoi-remove.sh +3 -3
  101. package/scripts/ops/chezmoi-update.sh +6 -2
  102. package/scripts/ops/heal-chezmoi.sh +1 -1
  103. package/scripts/ops/heal-system.sh +1 -1
  104. package/scripts/ops/heal-tools.sh +1 -1
  105. package/scripts/ops/heal.sh +1 -1
  106. package/scripts/ops/post-apply-repair.sh +1 -1
  107. package/scripts/ops/prewarm.sh +1 -1
  108. package/scripts/ops/release.sh +1 -1
  109. package/scripts/ops/rollback.sh +11 -2
  110. package/scripts/ops/setup.sh +1 -1
  111. package/scripts/ops/teleport.sh +1 -1
  112. package/scripts/qa/check-feature-matrix.sh +296 -0
  113. package/scripts/qa/check-version-consistency.sh +7 -119
  114. package/scripts/qa/coverage-baseline.sh +1 -1
  115. package/scripts/qa/docs-coverage.sh +9 -3
  116. package/scripts/qa/examples-coverage.sh +1 -1
  117. package/scripts/qa/reliability-audit.sh +1 -1
  118. package/scripts/qa/scorecard-snapshot.sh +1 -1
  119. package/scripts/qa/traceability-coverage.sh +9 -2
  120. package/scripts/qa/validate-examples.sh +66 -3
  121. package/scripts/qa/wsl-contract.sh +1 -1
  122. package/scripts/secrets/age-init.sh +1 -1
  123. package/scripts/secrets/create-secrets-file.sh +1 -1
  124. package/scripts/secrets/encrypt-ssh-key.sh +1 -1
  125. package/scripts/security/backup.sh +1 -1
  126. package/scripts/security/check-disclosure-key-expiry.sh +1 -1
  127. package/scripts/security/dns-doh.sh +1 -1
  128. package/scripts/security/encryption-check.sh +1 -1
  129. package/scripts/security/enforce-policies.sh +315 -98
  130. package/scripts/security/firewall.sh +1 -1
  131. package/scripts/security/lock-configs.sh +1 -1
  132. package/scripts/security/lock-screen.sh +1 -1
  133. package/scripts/security/manage-secrets.sh +1 -1
  134. package/scripts/security/ssh-cert.sh +1 -1
  135. package/scripts/security/telemetry-kill.sh +1 -1
  136. package/scripts/security/usb-safety.sh +1 -1
  137. package/scripts/theme/apply-gnome-theme.sh +1 -1
  138. package/scripts/theme/extract-heic-frames.sh +1 -1
  139. package/scripts/theme/extract-theme.py +284 -6
  140. package/scripts/theme/install-boot-logo.sh +1 -1
  141. package/scripts/theme/install-catppuccin-themes.sh +1 -1
  142. package/scripts/theme/install-cursors.sh +1 -1
  143. package/scripts/theme/install-file-icons.sh +1 -1
  144. package/scripts/theme/install-grub-theme.sh +1 -1
  145. package/scripts/theme/install-lock-icon.sh +1 -1
  146. package/scripts/theme/merge-wallpaper.sh +1 -1
  147. package/scripts/theme/rebuild-themes.sh +64 -5
  148. package/scripts/theme/switch.sh +46 -19
  149. package/scripts/theme/wallpaper-rotate.sh +1 -1
  150. package/scripts/theme/wallpaper-sync.sh +1 -1
  151. package/scripts/tools/cmatrix.sh +1 -1
  152. package/scripts/tools/detect-collisions.py +1 -1
  153. package/scripts/tools/emoji-picker.sh +1 -1
  154. package/scripts/tools/figlet-banner.sh +1 -1
  155. package/scripts/tools/log-rotate.sh +1 -1
  156. package/scripts/tools/lolcat-wrap.sh +1 -1
  157. package/scripts/tools/pipes.sh +1 -1
  158. package/scripts/tuning/linux.sh +1 -1
  159. package/scripts/tuning/macos.sh +1 -1
  160. package/scripts/uninstall.sh +1 -1
  161. package/scripts/verify-release-versions +156 -0
  162. package/scripts/version-sync.sh +63 -3
  163. package/LICENSE +0 -21
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Security Policy Enforcement Script
5
5
  # This script enforces security policies across the dotfiles repository
@@ -29,65 +29,182 @@ log() {
29
29
  echo "[${timestamp}] [${level}] ${message}" | tee -a "${LOG_FILE}"
30
30
  }
31
31
 
32
+ # Per-check outcomes, recorded by main() and rendered by generate_report().
33
+ # Parallel arrays rather than an associative array: this runs under bash 3.2 on
34
+ # stock macOS, where `declare -A` does not exist.
35
+ CHECK_NAMES=()
36
+ CHECK_RESULTS=()
37
+
38
+ record_check() {
39
+ CHECK_NAMES+=("$1")
40
+ CHECK_RESULTS+=("$2")
41
+ }
42
+
32
43
  # Error handling
33
44
  error_exit() {
34
45
  log "ERROR" "$1"
35
46
  exit 1
36
47
  }
37
48
 
38
- # Check if required tools are installed
49
+ # Check if required tools are installed.
50
+ #
51
+ # Only the tools every check needs are fatal. gitleaks, shellcheck and opa each
52
+ # back exactly one check, and treating them as fatal meant a machine without
53
+ # opa ran NO checks at all — as a pre-commit hook, that is a gate that silently
54
+ # does nothing on most contributors' machines. A missing optional tool now skips
55
+ # its own check and nothing else.
56
+ #
57
+ # A skip is never counted as a pass: it is reported as SKIPPED in the output and
58
+ # in the report, and DOTFILES_POLICY_STRICT=1 turns any skip into a hard error
59
+ # so CI can demand the full set.
60
+ MISSING_OPTIONAL=()
61
+
62
+ have_tool() {
63
+ command -v "$1" >/dev/null 2>&1
64
+ }
65
+
39
66
  check_dependencies() {
40
67
  log "INFO" "Checking dependencies..."
41
68
 
42
- local missing_tools=()
69
+ local missing_required=()
70
+ local tool
43
71
 
44
- # Required tools
45
- local tools=("opa" "gitleaks" "shellcheck" "grep" "find")
72
+ for tool in grep find git; do
73
+ have_tool "$tool" || missing_required+=("$tool")
74
+ done
46
75
 
47
- for tool in "${tools[@]}"; do
48
- if ! command -v "$tool" &>/dev/null; then
49
- missing_tools+=("$tool")
50
- fi
76
+ if [[ ${#missing_required[@]} -gt 0 ]]; then
77
+ error_exit "Missing required tools: ${missing_required[*]}"
78
+ fi
79
+
80
+ for tool in gitleaks shellcheck opa; do
81
+ have_tool "$tool" || MISSING_OPTIONAL+=("$tool")
51
82
  done
52
83
 
53
- if [[ ${#missing_tools[@]} -gt 0 ]]; then
54
- error_exit "Missing required tools: ${missing_tools[*]}"
84
+ if [[ ${#MISSING_OPTIONAL[@]} -gt 0 ]]; then
85
+ if [[ "${DOTFILES_POLICY_STRICT:-0}" == "1" ]]; then
86
+ error_exit "Missing tools (strict mode): ${MISSING_OPTIONAL[*]}"
87
+ fi
88
+ log "WARN" "Not installed, related checks will be SKIPPED: ${MISSING_OPTIONAL[*]}"
55
89
  fi
56
90
 
57
- log "INFO" "All dependencies satisfied"
91
+ log "INFO" "Dependency check complete"
58
92
  }
59
93
 
60
94
  # Scan for hardcoded secrets
95
+ # repo_files [pattern] — NUL-separated list of TRACKED files.
96
+ #
97
+ # Every check below used to walk the working tree with `find`, which pulls in
98
+ # whatever happens to be sitting there: coverage/ alone is 165 MB of xtrace
99
+ # output and produced 50 of the 52 gitleaks findings on a clean checkout, and
100
+ # it is gitignored at .gitignore:80 with zero tracked files. A policy gate
101
+ # about the repository should examine the repository, so this asks git.
102
+ #
103
+ # Falls back to find when run outside a work tree (a tarball, say), with the
104
+ # same exclusions spelled out rather than assumed.
105
+ repo_files() {
106
+ local pattern="${1:-}"
107
+ if git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
108
+ if [[ -n "$pattern" ]]; then
109
+ git -C "${REPO_ROOT}" ls-files -z -- "$pattern"
110
+ else
111
+ git -C "${REPO_ROOT}" ls-files -z
112
+ fi
113
+ else
114
+ if [[ -n "$pattern" ]]; then
115
+ find "${REPO_ROOT}" -name "$pattern" -type f \
116
+ -not -path "*/.git/*" -not -path "*/coverage/*" -not -path "*/node_modules/*" -print0
117
+ else
118
+ find "${REPO_ROOT}" -type f \
119
+ -not -path "*/.git/*" -not -path "*/coverage/*" -not -path "*/node_modules/*" -print0
120
+ fi
121
+ fi
122
+ }
123
+
61
124
  scan_secrets() {
62
125
  log "INFO" "Scanning for hardcoded secrets..."
63
126
 
64
127
  local violations=0
65
128
 
66
- # Run gitleaks
67
- if gitleaks detect --source="${REPO_ROOT}" --config="${REPO_ROOT}/config/gitleaks.toml" --no-git >/dev/null 2>&1; then
129
+ # gitleaks in GIT mode, not --no-git.
130
+ #
131
+ # --no-git scanned the working tree, which meant gitignored build output
132
+ # (coverage/, 165 MB) and, worse, it cannot honour .gitleaksignore: those
133
+ # entries are fingerprinted `<commit>:<file>:<rule>:<line>`, and with no
134
+ # commit to key on, every allowlisted false positive came back. That is how
135
+ # this reported "potential secrets" while `gitleaks detect` over all 2278
136
+ # commits reported none.
137
+ #
138
+ # Git mode is also ~18x faster here: 10s against 3m0s.
139
+ local gl_out
140
+ if ! have_tool gitleaks; then
141
+ log "WARN" "⏭ gitleaks SKIPPED (not installed) — pattern scan still runs"
142
+ elif gl_out="$(gitleaks detect --source="${REPO_ROOT}" \
143
+ --config="${REPO_ROOT}/config/gitleaks.toml" \
144
+ --redact --no-banner 2>&1)"; then
68
145
  log "INFO" "✅ No secrets detected by gitleaks"
69
146
  else
70
147
  log "WARN" "❌ Potential secrets detected by gitleaks"
148
+ printf '%s\n' "$gl_out" | grep -E '^(Finding|File|RuleID|Commit):' | head -40 >>"${LOG_FILE}" 2>/dev/null || true
71
149
  violations=$((violations + 1))
72
150
  fi
73
151
 
74
- # Manual pattern checks
152
+ # A short list of patterns that are unambiguous on sight, scanned over
153
+ # TRACKED files only.
154
+ #
155
+ # The previous list could not pass on any real repository. `[0-9a-f]{32,}`
156
+ # matches every git SHA, checksum and fingerprint in the tree, so it fired
157
+ # unconditionally; and `key\s*=\s*"..."` matches documentation, test
158
+ # fixtures and any config key whose name ends in "key". Four of the six
159
+ # patterns reported a violation on a clean checkout, which trains a reader
160
+ # to ignore the output — the opposite of what a gate is for.
161
+ #
162
+ # What survives is the shapes that do not occur by accident. Everything
163
+ # else is gitleaks' job, and it does it with entropy analysis and an
164
+ # allowlist rather than a bare grep.
75
165
  local secret_patterns=(
76
- 'password\s*=\s*["\047][^"\047]*["\047]'
77
- 'token\s*=\s*["\047][^"\047]*["\047]'
78
- 'secret\s*=\s*["\047][^"\047]*["\047]'
79
- 'key\s*=\s*["\047][^"\047]*["\047]'
80
- 'AKIA[0-9A-Z]{16}' # AWS Access Key
81
- '[0-9a-f]{32,}' # Potential hash/token
166
+ 'AKIA[0-9A-Z]{16}' # AWS access key id
167
+ 'ASIA[0-9A-Z]{16}' # AWS temporary access key id
168
+ '-----BEGIN [A-Z ]*PRIVATE KEY-----' # any PEM private key block
169
+ 'xox[baprs]-[0-9A-Za-z-]{10,}' # Slack token
170
+ 'ghp_[0-9A-Za-z]{36}' # GitHub personal access token
171
+ 'AIza[0-9A-Za-z_-]{35}' # Google API key
82
172
  )
83
173
 
174
+ # Two kinds of file contain these shapes on purpose, and both already
175
+ # declare it using gitleaks' own conventions rather than a list kept here:
176
+ #
177
+ # * the gitleaks config itself — it allowlists a fake AWS key by value,
178
+ # so a rule-definition file necessarily holds the shape its rules
179
+ # match. (Deliberately not quoted here: writing the literal out made
180
+ # THIS file match its own pattern, which is the same self-match the
181
+ # original guarded against with --exclude=enforce-policies.sh);
182
+ # * fixtures that test secret detection — the atuin history filter's test
183
+ # carries `export AWS_SECRET_ACCESS_KEY=ASIA... # gitleaks:allow`,
184
+ # because the point of the test is that such a line gets filtered.
185
+ #
186
+ # Honouring `gitleaks:allow` means one convention, already understood by
187
+ # contributors and by gitleaks, instead of a second exclusion list here
188
+ # that would drift out of step with the first.
189
+ local pattern hits f line
84
190
  for pattern in "${secret_patterns[@]}"; do
85
- if grep -r -E -i "${pattern}" "${REPO_ROOT}" \
86
- --exclude-dir=".git" \
87
- --exclude="*.log" \
88
- --exclude="enforce-policies.sh" \
89
- --exclude="environment-template.env" >/dev/null 2>&1; then
90
- log "WARN" "❌ Potential hardcoded credential pattern found: ${pattern}"
191
+ hits=""
192
+ while IFS= read -r -d '' f; do
193
+ case "$f" in
194
+ config/gitleaks.toml | .gitleaks.toml) continue ;;
195
+ esac
196
+ [[ -f "${REPO_ROOT}/$f" ]] || continue
197
+ while IFS= read -r line; do
198
+ [[ "$line" == *"gitleaks:allow"* ]] && continue
199
+ hits="${hits}${f}\n"
200
+ break
201
+ done < <(LC_ALL=C grep -E "${pattern}" "${REPO_ROOT}/$f" 2>/dev/null || true)
202
+ done < <(repo_files)
203
+ if [[ -n "$hits" ]]; then
204
+ log "WARN" "❌ Credential pattern ${pattern} found in:"
205
+ printf '%b' "$hits" | head -10 | while IFS= read -r h; do
206
+ [[ -n "$h" ]] && log "WARN" " ${h}"
207
+ done
91
208
  violations=$((violations + 1))
92
209
  fi
93
210
  done
@@ -105,28 +222,49 @@ check_file_permissions() {
105
222
 
106
223
  local violations=0
107
224
 
108
- # Check for overly permissive files
225
+ # World-writable is the property worth catching. The old test was
226
+ # `[[ $perms -gt 755 ]]`, comparing an octal mode as a decimal number,
227
+ # which lets 764 (group-writable) through while flagging 775.
228
+ # Paths are reported repository-relative, the way every other check reports
229
+ # them, so the output is diffable between machines.
230
+ #
231
+ # Symlinks are skipped. A symlink carries its own mode bits, but the kernel
232
+ # ignores them: access is governed entirely by the target, which is checked
233
+ # on its own if it is tracked. Those bits are also not portable — Linux
234
+ # creates symlinks 777 and macOS 755 — so including them made this pass on a
235
+ # Mac and fail on a runner for the same eight tracked symlinks
236
+ # (.gitleaks.toml, .pre-commit-config.yaml and friends, all pointing into
237
+ # config/). `[[ -f ]]` follows the link, so it cannot filter them; -L must be
238
+ # tested first.
239
+ local file perms abs
109
240
  while IFS= read -r -d '' file; do
110
- if [[ -f "$file" ]]; then
111
- local perms
112
- perms=$(stat -c %a "$file" 2>/dev/null || stat -f %OLp "$file")
113
- if [[ $perms -gt 755 ]]; then
114
- log "WARN" "❌ Overly permissive file: ${file} (${perms})"
115
- violations=$((violations + 1))
116
- fi
241
+ abs="${REPO_ROOT}/${file}"
242
+ [[ -L "$abs" ]] && continue
243
+ [[ -f "$abs" ]] || continue
244
+ perms=$(stat -c %a "$abs" 2>/dev/null || stat -f %OLp "$abs" 2>/dev/null) || continue
245
+ # Other-writable is the bit that matters; group-writable in a repo is
246
+ # normal on shared checkouts.
247
+ if ((8#${perms} & 8#0002)); then
248
+ log "WARN" "❌ World-writable file: ${file} (${perms})"
249
+ violations=$((violations + 1))
117
250
  fi
118
- done < <(find "${REPO_ROOT}" -type f -not -path "*/.git/*" -print0)
119
-
120
- # Check for executable text files
121
- local text_extensions=("md" "txt" "json" "yaml" "yml" "toml")
122
-
123
- for ext in "${text_extensions[@]}"; do
251
+ done < <(repo_files)
252
+
253
+ # Executable text files. `find -executable` is GNU-only: on macOS, where
254
+ # this hook actually runs for most contributors, that predicate is an
255
+ # error and the whole loop found nothing. Test the file instead.
256
+ # Symlinks skipped here for the same reason: `-x` follows the link, so a
257
+ # tracked symlink pointing at any executable would be reported as an
258
+ # executable text file.
259
+ local ext
260
+ for ext in md txt json yaml yml toml; do
124
261
  while IFS= read -r -d '' file; do
125
- if [[ -x "$file" ]]; then
262
+ [[ -L "${REPO_ROOT}/${file}" ]] && continue
263
+ if [[ -x "${REPO_ROOT}/${file}" ]]; then
126
264
  log "WARN" "❌ Executable text file: ${file}"
127
265
  violations=$((violations + 1))
128
266
  fi
129
- done < <(find "${REPO_ROOT}" -name "*.${ext}" -executable -not -path "*/.git/*" -print0)
267
+ done < <(repo_files "*.${ext}")
130
268
  done
131
269
 
132
270
  if [[ $violations -eq 0 ]]; then
@@ -140,14 +278,25 @@ check_file_permissions() {
140
278
  validate_shell_scripts() {
141
279
  log "INFO" "Validating shell scripts..."
142
280
 
281
+ if ! have_tool shellcheck; then
282
+ log "WARN" "⏭ Shell script validation SKIPPED (shellcheck not installed)"
283
+ return 2
284
+ fi
285
+
143
286
  local violations=0
144
287
 
288
+ # Tracked scripts only, at the severity CI gates on. Running plain
289
+ # `shellcheck` here contradicted tools/ci and the Shell Lint workflow,
290
+ # which use `-S error` / `-S warning`, so this reported "violations" for
291
+ # style notes that the repository has deliberately decided not to gate.
292
+ local script
145
293
  while IFS= read -r -d '' script; do
146
- if ! shellcheck "$script" >/dev/null 2>&1; then
147
- log "WARN" "❌ ShellCheck violations in: ${script}"
294
+ [[ -f "${REPO_ROOT}/${script}" ]] || continue
295
+ if ! shellcheck -S error -x "${REPO_ROOT}/${script}" >/dev/null 2>&1; then
296
+ log "WARN" "❌ ShellCheck errors in: ${script}"
148
297
  violations=$((violations + 1))
149
298
  fi
150
- done < <(find "${REPO_ROOT}" -name "*.sh" -not -path "*/.git/*" -print0)
299
+ done < <(repo_files "*.sh")
151
300
 
152
301
  if [[ $violations -eq 0 ]]; then
153
302
  log "INFO" "✅ All shell scripts pass validation"
@@ -156,37 +305,20 @@ validate_shell_scripts() {
156
305
  return $violations
157
306
  }
158
307
 
159
- # Check environment variable usage
160
- check_environment_variables() {
161
- log "INFO" "Checking environment variable usage..."
162
-
163
- local violations=0
164
-
165
- # Look for unprotected variable assignments
166
- while IFS= read -r -d '' file; do
167
- # Skip binary files and specific files
168
- if file "$file" | grep -q "text" && [[ ! "$file" =~ \.(log|env)$ ]]; then
169
- # Check for variables that should use default patterns
170
- if grep -E '^[A-Z_]+=.+$' "$file" >/dev/null 2>&1; then
171
- # Check if they use proper defaulting
172
- if ! grep -E '\$\{[A-Z_]+:-[^}]*\}' "$file" >/dev/null 2>&1; then
173
- local suspicious_vars
174
- suspicious_vars=$(grep -E '^[A-Z_]+=.+$' "$file" | head -3)
175
- if [[ -n "$suspicious_vars" ]]; then
176
- log "WARN" "❌ Potential hardcoded variables in ${file}: ${suspicious_vars}"
177
- violations=$((violations + 1))
178
- fi
179
- fi
180
- fi
181
- fi
182
- done < <(find "${REPO_ROOT}" -type f -not -path "*/.git/*" -not -path "*/.github/security-policies/*" -print0)
183
-
184
- if [[ $violations -eq 0 ]]; then
185
- log "INFO" "✅ Environment variable usage is appropriate"
186
- fi
187
-
188
- return $violations
189
- }
308
+ # check_environment_variables() was removed here.
309
+ #
310
+ # It warned when a file contained `^[A-Z_]+=...` but did not also contain
311
+ # `${VAR:-default}` ANYWHERE in the same file. That is not a security
312
+ # property: one defaulted variable anywhere exempted every assignment in the
313
+ # file, and a file with no defaults at all — a constants file, a .env
314
+ # template, a generated manifest — was flagged regardless of whether any
315
+ # value was sensitive. It ran `file` on every path in the working tree to
316
+ # decide what to read.
317
+ #
318
+ # There is no threshold that makes the condition meaningful, so it is gone
319
+ # rather than tuned. Hardcoded credentials are what gitleaks and the pattern
320
+ # list in scan_secrets() are for, and they judge the value rather than
321
+ # whether a neighbouring line happens to use parameter expansion.
190
322
 
191
323
  # Validate OPA policies
192
324
  validate_policies() {
@@ -197,6 +329,11 @@ validate_policies() {
197
329
  return 1
198
330
  fi
199
331
 
332
+ if ! have_tool opa; then
333
+ log "WARN" "⏭ OPA policy validation SKIPPED (opa not installed)"
334
+ return 2
335
+ fi
336
+
200
337
  if opa test "${POLICIES_DIR}" >/dev/null 2>&1; then
201
338
  log "INFO" "✅ OPA policies are valid"
202
339
  return 0
@@ -228,14 +365,27 @@ check_sensitive_files() {
228
365
  "id_ed25519"
229
366
  )
230
367
 
368
+ # Tracked files only. A `.env` a contributor keeps locally, or a key in an
369
+ # ignored scratch directory, is not something this repository ships — and
370
+ # flagging it trains people to ignore the check. What matters is whether
371
+ # such a file is committed.
372
+ #
373
+ # Each name is queried at the root AND at any depth. A git pathspec with a
374
+ # wildcard (`*.key`) already matches at any depth, because `*` spans `/` in
375
+ # pathspecs; a bare name (`id_rsa`, `.env`) matches ONLY at the repository
376
+ # root. Without the `*/` form, a committed `.ssh/id_rsa` — the likeliest
377
+ # place for a private key to appear — would not have been reported at all.
378
+ local pattern file
231
379
  for pattern in "${sensitive_patterns[@]}"; do
232
380
  while IFS= read -r -d '' file; do
233
- # Skip allowed files
234
- if [[ ! "$file" =~ (environment-template\.env|\.pub$) ]]; then
235
- log "WARN" "❌ Sensitive file detected: ${file}"
381
+ if [[ ! "$file" =~ (environment-template\.env|\.pub$|KEYS\.asc$) ]]; then
382
+ log "WARN" "❌ Sensitive file committed: ${file}"
236
383
  violations=$((violations + 1))
237
384
  fi
238
- done < <(find "${REPO_ROOT}" -name "${pattern}" -not -path "*/.git/*" -print0)
385
+ done < <(
386
+ repo_files "${pattern}"
387
+ repo_files "*/${pattern}"
388
+ )
239
389
  done
240
390
 
241
391
  if [[ $violations -eq 0 ]]; then
@@ -245,6 +395,25 @@ check_sensitive_files() {
245
395
  return $violations
246
396
  }
247
397
 
398
+ # Render the checklist from what actually ran, so a failing check cannot show
399
+ # a tick. The previous version printed six fixed ✅ lines regardless of outcome,
400
+ # including one for a check that no longer exists.
401
+ _report_checklist() {
402
+ local i
403
+ # An empty array expansion is an unbound-variable error under `set -u` on
404
+ # bash 3.2, so the guard is load-bearing, not defensive noise.
405
+ [[ ${#CHECK_NAMES[@]} -eq 0 ]] && return 0
406
+ for i in "${!CHECK_NAMES[@]}"; do
407
+ if [[ "${CHECK_RESULTS[$i]}" == "SKIPPED" ]]; then
408
+ echo "- ⏭ ${CHECK_NAMES[$i]} — SKIPPED (required tool not installed)"
409
+ elif [[ "${CHECK_RESULTS[$i]}" == "0" ]]; then
410
+ echo "- ✅ ${CHECK_NAMES[$i]}"
411
+ else
412
+ echo "- ❌ ${CHECK_NAMES[$i]} (${CHECK_RESULTS[$i]} violation(s))"
413
+ fi
414
+ done
415
+ }
416
+
248
417
  # Generate security report
249
418
  generate_report() {
250
419
  local total_violations="$1"
@@ -261,12 +430,7 @@ generate_report() {
261
430
 
262
431
  ## Security Checks Performed
263
432
 
264
- - ✅ Secrets scanning (gitleaks + manual patterns)
265
- - ✅ File permissions validation
266
- - ✅ Shell script analysis (shellcheck)
267
- - ✅ Environment variable usage check
268
- - ✅ OPA policy validation
269
- - ✅ Sensitive files detection
433
+ $(_report_checklist)
270
434
 
271
435
  ## Security Score
272
436
 
@@ -275,10 +439,9 @@ $(if [[ $total_violations -eq 0 ]]; then echo "🟢 **PASSED** - No security vio
275
439
  ## Recommendations
276
440
 
277
441
  1. Review any flagged violations in the audit log
278
- 2. Update hardcoded values to use environment variables
279
- 3. Ensure proper file permissions (644 for files, 755 for executables)
280
- 4. Use the provided environment template for configuration
281
- 5. Run this script regularly as part of your development workflow
442
+ 2. Ensure proper file permissions (644 for files, 755 for executables)
443
+ 3. Keep secrets out of tracked files; use the environment template for configuration
444
+ 4. Run this script regularly as part of your development workflow
282
445
 
283
446
  ## Next Steps
284
447
 
@@ -302,15 +465,69 @@ main() {
302
465
 
303
466
  check_dependencies
304
467
 
468
+ # Report what is installed and stop, without scanning.
469
+ #
470
+ # A full pass walks every tracked file and runs shellcheck over each tracked
471
+ # script — ~85s here. That is the right cost for a gate and the wrong cost
472
+ # for a caller that only wants to know whether the tooling is present, or a
473
+ # smoke test checking the command is wired up.
474
+ #
475
+ # The feature-matrix test got this for free from a bug: gitleaks and opa were
476
+ # fatal dependencies, so on a machine without them the script exited before
477
+ # doing any work. Fixing that turned a sub-second test into a full scan
478
+ # inside a 120s budget, and it began failing on the macOS runners. This is
479
+ # the same fast path, made deliberate and documented rather than accidental.
480
+ if [[ "${DOTFILES_POLICY_DEPS_ONLY:-0}" == "1" ]]; then
481
+ log "INFO" "Dependency check only (DOTFILES_POLICY_DEPS_ONLY=1); no scan performed"
482
+ ui_ok "Dependencies checked"
483
+ exit 0
484
+ fi
485
+
305
486
  local total_violations=0
306
487
 
307
- # Run all checks
308
- scan_secrets || total_violations=$((total_violations + $?))
309
- check_file_permissions || total_violations=$((total_violations + $?))
310
- validate_shell_scripts || total_violations=$((total_violations + $?))
311
- check_environment_variables || total_violations=$((total_violations + $?))
312
- validate_policies || total_violations=$((total_violations + 1))
313
- check_sensitive_files || total_violations=$((total_violations + $?))
488
+ # Run all checks, recording each outcome for the report.
489
+ local rc
490
+
491
+ local label
492
+
493
+ rc=0
494
+ scan_secrets || rc=$?
495
+ label="Secrets scanning (gitleaks + high-signal patterns)"
496
+ have_tool gitleaks || label="Secrets scanning (pattern scan only, gitleaks absent)"
497
+ record_check "$label" "$rc"
498
+ total_violations=$((total_violations + rc))
499
+
500
+ rc=0
501
+ check_file_permissions || rc=$?
502
+ record_check "File permissions validation" "$rc"
503
+ total_violations=$((total_violations + rc))
504
+
505
+ # rc 2 is the skip sentinel: the tool that backs this check is absent. A skip
506
+ # adds nothing to the violation count and is rendered as SKIPPED, never as a
507
+ # tick — use DOTFILES_POLICY_STRICT=1 to make it an error instead.
508
+ rc=0
509
+ validate_shell_scripts || rc=$?
510
+ if have_tool shellcheck; then
511
+ record_check "Shell script analysis (shellcheck)" "$rc"
512
+ total_violations=$((total_violations + rc))
513
+ else
514
+ record_check "Shell script analysis (shellcheck)" "SKIPPED"
515
+ fi
516
+
517
+ rc=0
518
+ validate_policies || rc=$?
519
+ if have_tool opa; then
520
+ [[ $rc -ne 0 ]] && rc=1
521
+ record_check "OPA policy validation" "$rc"
522
+ total_violations=$((total_violations + rc))
523
+ else
524
+ record_check "OPA policy validation" "SKIPPED"
525
+ fi
526
+
527
+ rc=0
528
+ check_sensitive_files || rc=$?
529
+ record_check "Sensitive files detection" "$rc"
530
+ total_violations=$((total_violations + rc))
314
531
 
315
532
  # Generate report
316
533
  generate_report "$total_violations"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  ## Configure system firewall with secure defaults.
5
5
  ##
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # MIT License
3
- # SPDX-License-Identifier: MIT
3
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
4
4
  # Copyright (c) 2015-2026 Sebastien Rousseau
5
5
  # See LICENSE file for details.
6
6
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  ## Secure Configuration Management.
5
5
  ##
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # shellcheck shell=bash
5
5
  set -euo pipefail
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  ## Apply GNOME Theme — Apply Catppuccin themes to GNOME desktop.
5
5
  ##
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # extract-heic-frames.sh — extract per-frame PNGs from dynamic HEIC wallpapers.
5
5
  #