@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.
- package/CHANGELOG.md +182 -0
- package/README.md +1163 -163
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/COPYRIGHT +1 -1
- package/docs/ECOSYSTEM.md +220 -0
- package/docs/GOLD-STANDARD-AUDIT.md +352 -0
- package/docs/GOVERNANCE.md +1 -1
- package/docs/MAINTAINERS.md +1 -1
- package/docs/MINIMUM-TOOLCHAIN.md +100 -0
- package/docs/README.md +1 -1
- package/docs/STRUCTURE.md +1 -1
- package/docs/architecture/ARCHITECTURE.md +11 -108
- package/docs/architecture/REPO_LAYOUT.md +3 -3
- package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +121 -0
- package/docs/index.md +3 -3
- package/docs/manual/00-introduction.md +1 -1
- package/docs/manual/03-reference/01-dot-cli.md +28 -3
- package/docs/manual/command-index.md +24 -4
- package/docs/manual/concept-index.md +2 -0
- package/docs/migration/README.md +81 -0
- package/docs/migration/from-bare-git-repo.md +156 -0
- package/docs/migration/from-gnu-stow.md +165 -0
- package/docs/migration/from-plain-chezmoi.md +148 -0
- package/docs/migration/from-yadm.md +187 -0
- package/docs/operations/PERFORMANCE_BUDGETS.md +196 -0
- package/docs/operations/REGISTRY.md +1 -1
- package/docs/operations/RELEASE_PIPELINE.md +4 -4
- package/docs/operations/TESTING.md +3 -3
- package/docs/operations/TRACEABILITY.md +1 -0
- package/docs/packaging.md +222 -0
- package/docs/reference/FEATURE-MATRIX.md +646 -0
- package/docs/reference/TOOLS.md +1 -1
- package/docs/reference/UTILS.md +1 -0
- package/docs/security/COMPLIANCE.md +1 -1
- package/docs/security/DISCLOSURE.md +4 -4
- package/docs/security/FUZZING.md +113 -18
- package/docs/security/KEY_ROTATION.md +1 -1
- package/docs/security/SCORECARD.md +3 -3
- package/docs/security/VERIFY_RELEASE.md +2 -2
- package/install.sh +6 -6
- package/package.json +2 -2
- package/scripts/ci/check-copyright-headers.sh +1 -1
- package/scripts/ci/check-shell-preamble.sh +1 -1
- package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
- package/scripts/demo/record.sh +1 -1
- package/scripts/diagnostics/a2a-conformance.sh +1 -1
- package/scripts/diagnostics/alias-governance.sh +30 -3
- package/scripts/diagnostics/aliases-cheatsheet.sh +1 -1
- package/scripts/diagnostics/aliases-manifest.sh +1 -1
- package/scripts/diagnostics/attest-verify.sh +147 -0
- package/scripts/diagnostics/benchmark.sh +1 -1
- package/scripts/diagnostics/conflicts.sh +1 -1
- package/scripts/diagnostics/doctor-unified.sh +6 -2
- package/scripts/diagnostics/doctor.sh +56 -10
- package/scripts/diagnostics/drift-dashboard.sh +3 -2
- package/scripts/diagnostics/health.sh +43 -10
- package/scripts/diagnostics/history-analysis.sh +1 -1
- package/scripts/diagnostics/mcp-doctor.sh +2 -2
- package/scripts/diagnostics/perf.sh +1 -1
- package/scripts/diagnostics/scorecard.sh +3 -2
- package/scripts/diagnostics/secret-governance.sh +1 -1
- package/scripts/diagnostics/security-score.sh +1 -1
- package/scripts/diagnostics/smoke-test.sh +1 -1
- package/scripts/diagnostics/snapshot.sh +1 -1
- package/scripts/diagnostics/verify.sh +1 -1
- package/scripts/diagnostics/verify_state.sh +1 -1
- package/scripts/diagnostics/version-locks.sh +1 -1
- package/scripts/diagnostics/workstation-attestation.sh +26 -1
- package/scripts/dot/commands/agent.sh +64 -14
- package/scripts/dot/commands/agents.sh +24 -8
- package/scripts/dot/commands/ai.sh +20 -7
- package/scripts/dot/commands/aliases.sh +1 -1
- package/scripts/dot/commands/appearance.sh +1 -1
- package/scripts/dot/commands/completion.sh +42 -5
- package/scripts/dot/commands/core.sh +1 -1
- package/scripts/dot/commands/diagnostics.sh +1 -1
- package/scripts/dot/commands/env-emit.sh +1 -1
- package/scripts/dot/commands/fleet.sh +41 -18
- package/scripts/dot/commands/init.sh +1 -1
- package/scripts/dot/commands/lint.sh +1 -1
- package/scripts/dot/commands/manual.sh +1 -1
- package/scripts/dot/commands/meta.sh +115 -10
- package/scripts/dot/commands/patterns.sh +1 -1
- package/scripts/dot/commands/registry.sh +43 -7
- package/scripts/dot/commands/restore.sh +1 -1
- package/scripts/dot/commands/secrets.sh +1 -1
- package/scripts/dot/commands/security.sh +1 -1
- package/scripts/dot/commands/tools.sh +21 -7
- package/scripts/fonts/install-nerd-fonts.sh +1 -1
- package/scripts/fonts/patch-fonts.sh +1 -1
- package/scripts/git-hooks/install.sh +1 -1
- package/scripts/git-hooks/pre-commit-audit.sh +2 -2
- package/scripts/lib/secrets_provider.sh +19 -4
- package/scripts/nvim/headless-upgrade.lua +81 -0
- package/scripts/ops/ai-setup.sh +1 -1
- package/scripts/ops/bundle.sh +1 -1
- package/scripts/ops/chaos.sh +1 -1
- package/scripts/ops/chezmoi-apply.sh +1 -1
- package/scripts/ops/chezmoi-diff.sh +1 -1
- package/scripts/ops/chezmoi-remove.sh +3 -3
- package/scripts/ops/chezmoi-update.sh +6 -2
- package/scripts/ops/heal-chezmoi.sh +1 -1
- package/scripts/ops/heal-system.sh +1 -1
- package/scripts/ops/heal-tools.sh +1 -1
- package/scripts/ops/heal.sh +1 -1
- package/scripts/ops/post-apply-repair.sh +1 -1
- package/scripts/ops/prewarm.sh +1 -1
- package/scripts/ops/release.sh +1 -1
- package/scripts/ops/rollback.sh +11 -2
- package/scripts/ops/setup.sh +1 -1
- package/scripts/ops/teleport.sh +1 -1
- package/scripts/qa/check-feature-matrix.sh +296 -0
- package/scripts/qa/check-version-consistency.sh +7 -119
- package/scripts/qa/coverage-baseline.sh +1 -1
- package/scripts/qa/docs-coverage.sh +9 -3
- package/scripts/qa/examples-coverage.sh +1 -1
- package/scripts/qa/reliability-audit.sh +1 -1
- package/scripts/qa/scorecard-snapshot.sh +1 -1
- package/scripts/qa/traceability-coverage.sh +9 -2
- package/scripts/qa/validate-examples.sh +66 -3
- package/scripts/qa/wsl-contract.sh +1 -1
- package/scripts/secrets/age-init.sh +1 -1
- package/scripts/secrets/create-secrets-file.sh +1 -1
- package/scripts/secrets/encrypt-ssh-key.sh +1 -1
- package/scripts/security/backup.sh +1 -1
- package/scripts/security/check-disclosure-key-expiry.sh +1 -1
- package/scripts/security/dns-doh.sh +1 -1
- package/scripts/security/encryption-check.sh +1 -1
- package/scripts/security/enforce-policies.sh +315 -98
- package/scripts/security/firewall.sh +1 -1
- package/scripts/security/lock-configs.sh +1 -1
- package/scripts/security/lock-screen.sh +1 -1
- package/scripts/security/manage-secrets.sh +1 -1
- package/scripts/security/ssh-cert.sh +1 -1
- package/scripts/security/telemetry-kill.sh +1 -1
- package/scripts/security/usb-safety.sh +1 -1
- package/scripts/theme/apply-gnome-theme.sh +1 -1
- package/scripts/theme/extract-heic-frames.sh +1 -1
- package/scripts/theme/extract-theme.py +284 -6
- package/scripts/theme/install-boot-logo.sh +1 -1
- package/scripts/theme/install-catppuccin-themes.sh +1 -1
- package/scripts/theme/install-cursors.sh +1 -1
- package/scripts/theme/install-file-icons.sh +1 -1
- package/scripts/theme/install-grub-theme.sh +1 -1
- package/scripts/theme/install-lock-icon.sh +1 -1
- package/scripts/theme/merge-wallpaper.sh +1 -1
- package/scripts/theme/rebuild-themes.sh +64 -5
- package/scripts/theme/switch.sh +46 -19
- package/scripts/theme/wallpaper-rotate.sh +1 -1
- package/scripts/theme/wallpaper-sync.sh +1 -1
- package/scripts/tools/cmatrix.sh +1 -1
- package/scripts/tools/detect-collisions.py +1 -1
- package/scripts/tools/emoji-picker.sh +1 -1
- package/scripts/tools/figlet-banner.sh +1 -1
- package/scripts/tools/log-rotate.sh +1 -1
- package/scripts/tools/lolcat-wrap.sh +1 -1
- package/scripts/tools/pipes.sh +1 -1
- package/scripts/tuning/linux.sh +1 -1
- package/scripts/tuning/macos.sh +1 -1
- package/scripts/uninstall.sh +1 -1
- package/scripts/verify-release-versions +156 -0
- package/scripts/version-sync.sh +63 -3
- 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
|
|
69
|
+
local missing_required=()
|
|
70
|
+
local tool
|
|
43
71
|
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
for tool in grep find git; do
|
|
73
|
+
have_tool "$tool" || missing_required+=("$tool")
|
|
74
|
+
done
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 [[ ${#
|
|
54
|
-
|
|
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" "
|
|
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
|
-
#
|
|
67
|
-
|
|
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
|
-
#
|
|
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
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'[0-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 < <(
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
for
|
|
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
|
-
|
|
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 < <(
|
|
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
|
-
|
|
147
|
-
|
|
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 < <(
|
|
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
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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 < <(
|
|
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
|
-
|
|
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.
|
|
279
|
-
3.
|
|
280
|
-
4.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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"
|