@aldegad/safedeps 2.1.1 → 2.4.0

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.
@@ -0,0 +1,212 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # safedeps doctor — repo security posture check.
5
+ #
6
+ # Diagnoses whether the per-repo secret-leak lane is set up (.gitleaks policy +
7
+ # .githooks/pre-commit + active core.hooksPath + an available scanner) and
8
+ # reports the global dependency-install gate too. Read-only by default; `--fix`
9
+ # scaffolds the missing pieces (hooks init) and activates them (hooks install).
10
+ #
11
+ # Exit codes: 0 = no gaps in the secret-leak lane, 1 = gaps remain.
12
+
13
+ GATES_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ # shellcheck source=./repo-profile.sh
15
+ source "$GATES_LIB_DIR/repo-profile.sh"
16
+
17
+ REPO_ROOT=""
18
+ FIX=0
19
+ JSON_MODE="${SAFEDEPS_JSON_MODE:-0}"
20
+
21
+ usage() {
22
+ printf 'Usage: safedeps doctor [--root <repo>] [--fix] [--json]\n' >&2
23
+ }
24
+
25
+ while [ $# -gt 0 ]; do
26
+ case "$1" in
27
+ --root) REPO_ROOT="${2:?--root needs a path}"; shift 2 ;;
28
+ --fix) FIX=1; shift ;;
29
+ --json) JSON_MODE=1; shift ;;
30
+ -h|--help) usage; exit 0 ;;
31
+ *) usage; exit 64 ;;
32
+ esac
33
+ done
34
+
35
+ if [ -z "$REPO_ROOT" ]; then REPO_ROOT="$(pwd)"; fi
36
+ REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
37
+
38
+ # --- check collection ---------------------------------------------------------
39
+ # Each check appends a row: <lane>\t<status>\t<label>\t<remedy>
40
+ # status ∈ ok | gap | na
41
+ CHECK_ROWS=()
42
+ GAPS=0
43
+
44
+ add_check() {
45
+ local lane="$1" status="$2" label="$3" remedy="${4:-}"
46
+ CHECK_ROWS+=("${lane}"$'\t'"${status}"$'\t'"${label}"$'\t'"${remedy}")
47
+ # The exit code reflects the per-repo secret-leak lane only. The global
48
+ # dependency-install gate is reported (✗) but is a per-machine concern, so it
49
+ # does not gate this repo's posture result.
50
+ if [ "$status" = "gap" ] && [ "$lane" = "secret" ]; then GAPS=$((GAPS + 1)); fi
51
+ }
52
+
53
+ scanner_available() {
54
+ if command -v gitleaks >/dev/null 2>&1; then printf 'gitleaks'; return 0; fi
55
+ if command -v docker >/dev/null 2>&1; then printf 'docker'; return 0; fi
56
+ return 1
57
+ }
58
+
59
+ dependency_gate_root() {
60
+ # The dependency-install gate is installed globally as a skill symlink for
61
+ # whichever engine(s) are present.
62
+ local found=""
63
+ [ -e "$HOME/.claude/skills/safedeps" ] && found="${found:+${found}, }~/.claude/skills/safedeps"
64
+ [ -e "$HOME/.codex/skills/safedeps" ] && found="${found:+${found}, }~/.codex/skills/safedeps"
65
+ printf '%s' "$found"
66
+ }
67
+
68
+ run_checks() {
69
+ CHECK_ROWS=()
70
+ GAPS=0
71
+
72
+ local is_git=0
73
+ if git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then is_git=1; fi
74
+
75
+ local profile="(n/a)"
76
+ if [ "$is_git" = 1 ]; then
77
+ add_check secret ok "git worktree"
78
+ profile="$(safedeps_repo_profile "$REPO_ROOT")"
79
+
80
+ local config_path config_label
81
+ config_path="$(safedeps_gitleaks_config "$REPO_ROOT" "$profile")"
82
+ config_label="gitleaks config ($(basename "$config_path"))"
83
+ if [ -f "$config_path" ]; then
84
+ add_check secret ok "$config_label"
85
+ else
86
+ add_check secret gap "$config_label" "safedeps hooks init --root \"$REPO_ROOT\""
87
+ fi
88
+
89
+ local hook_file="$REPO_ROOT/.githooks/pre-commit"
90
+ if [ -x "$hook_file" ]; then
91
+ add_check secret ok ".githooks/pre-commit (executable)"
92
+ elif [ -f "$hook_file" ]; then
93
+ add_check secret gap ".githooks/pre-commit (not executable)" "chmod +x \"$hook_file\""
94
+ else
95
+ add_check secret gap ".githooks/pre-commit (present)" "safedeps hooks init --root \"$REPO_ROOT\""
96
+ fi
97
+
98
+ local hooks_path
99
+ hooks_path="$(git -C "$REPO_ROOT" config --get core.hooksPath || true)"
100
+ if [ "$hooks_path" = ".githooks" ]; then
101
+ add_check secret ok "git hooks active (core.hooksPath=.githooks)"
102
+ else
103
+ add_check secret gap "git hooks active (core.hooksPath=${hooks_path:-<unset>})" "safedeps hooks install --root \"$REPO_ROOT\""
104
+ fi
105
+ else
106
+ add_check secret na "git worktree (secret lane needs git)"
107
+ fi
108
+
109
+ local scanner
110
+ if scanner="$(scanner_available)"; then
111
+ add_check secret ok "secret scanner available (${scanner})"
112
+ else
113
+ add_check secret gap "secret scanner available (gitleaks or docker)" "brew install gitleaks"
114
+ fi
115
+
116
+ local gate_root
117
+ gate_root="$(dependency_gate_root)"
118
+ if [ -n "$gate_root" ]; then
119
+ add_check deps ok "dependency-install gate installed (${gate_root})"
120
+ else
121
+ add_check deps gap "dependency-install gate installed" "node scripts/install/install-safedeps-hooks.mjs"
122
+ fi
123
+
124
+ DOCTOR_PROFILE="$profile"
125
+ }
126
+
127
+ emit_human() {
128
+ local sym
129
+ printf 'safedeps doctor — repo security posture\n'
130
+ printf 'repo: %s\n' "$REPO_ROOT"
131
+ printf 'profile: %s\n\n' "$DOCTOR_PROFILE"
132
+
133
+ printf 'Secret-leak lane (per-repo)\n'
134
+ local row lane status label remedy
135
+ for row in "${CHECK_ROWS[@]}"; do
136
+ IFS=$'\t' read -r lane status label remedy <<< "$row"
137
+ [ "$lane" = "secret" ] || continue
138
+ case "$status" in
139
+ ok) sym='✓' ;;
140
+ gap) sym='✗' ;;
141
+ *) sym='–' ;;
142
+ esac
143
+ if [ "$status" = "gap" ] && [ -n "$remedy" ]; then
144
+ printf ' %s %-44s → %s\n' "$sym" "$label" "$remedy"
145
+ else
146
+ printf ' %s %s\n' "$sym" "$label"
147
+ fi
148
+ done
149
+
150
+ printf '\nDependency-install gate (global, all repos)\n'
151
+ for row in "${CHECK_ROWS[@]}"; do
152
+ IFS=$'\t' read -r lane status label remedy <<< "$row"
153
+ [ "$lane" = "deps" ] || continue
154
+ case "$status" in
155
+ ok) sym='✓' ;;
156
+ gap) sym='✗' ;;
157
+ *) sym='–' ;;
158
+ esac
159
+ if [ "$status" = "gap" ] && [ -n "$remedy" ]; then
160
+ printf ' %s %-44s → %s\n' "$sym" "$label" "$remedy"
161
+ else
162
+ printf ' %s %s\n' "$sym" "$label"
163
+ fi
164
+ done
165
+
166
+ printf '\n'
167
+ if [ "$GAPS" -eq 0 ]; then
168
+ printf 'No gaps in the secret-leak lane. The agent is on guard.\n'
169
+ else
170
+ printf '%d gap(s) in the secret-leak lane.\n' "$GAPS"
171
+ printf 'Fix all at once: safedeps doctor --fix --root "%s"\n' "$REPO_ROOT"
172
+ fi
173
+ }
174
+
175
+ emit_json() {
176
+ local rows_json="[]" row lane status label remedy
177
+ for row in "${CHECK_ROWS[@]}"; do
178
+ IFS=$'\t' read -r lane status label remedy <<< "$row"
179
+ rows_json="$(jq -c \
180
+ --arg lane "$lane" --arg status "$status" --arg label "$label" --arg remedy "$remedy" \
181
+ '. + [{lane:$lane, status:$status, label:$label, remedy:($remedy|select(length>0))}]' \
182
+ <<< "$rows_json")"
183
+ done
184
+ jq -nc \
185
+ --arg repo "$REPO_ROOT" \
186
+ --arg profile "$DOCTOR_PROFILE" \
187
+ --argjson gaps "$GAPS" \
188
+ --argjson checks "$rows_json" \
189
+ '{command:"doctor", repo:$repo, profile:$profile, gaps:$gaps, ok:($gaps==0), checks:$checks}'
190
+ }
191
+
192
+ # --- fix pass -----------------------------------------------------------------
193
+ if [ "$FIX" -eq 1 ]; then
194
+ if ! git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
195
+ printf 'ERROR: --fix needs a git worktree (the secret lane is git-scoped): %s\n' "$REPO_ROOT" >&2
196
+ exit 1
197
+ fi
198
+ [ "$JSON_MODE" -eq 1 ] || printf 'safedeps doctor --fix: scaffolding + activating the secret-leak lane...\n\n'
199
+ bash "$GATES_LIB_DIR/hooks.sh" init --root "$REPO_ROOT"
200
+ bash "$GATES_LIB_DIR/hooks.sh" install --root "$REPO_ROOT"
201
+ [ "$JSON_MODE" -eq 1 ] || printf '\n'
202
+ fi
203
+
204
+ run_checks
205
+
206
+ if [ "$JSON_MODE" -eq 1 ]; then
207
+ emit_json
208
+ else
209
+ emit_human
210
+ fi
211
+
212
+ [ "$GAPS" -eq 0 ]
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # safedeps hooks install|check — generic repo-local git hook activation.
5
+ # Absorbed from kuma-studio scripts/security/{install,check}-hooks.sh.
6
+ # The repo's privacy/secret policy lives in its own .githooks/pre-commit;
7
+ # this command only manages hook activation, not the policy content.
8
+
9
+ GATES_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ # shellcheck source=./repo-profile.sh
11
+ source "$GATES_LIB_DIR/repo-profile.sh"
12
+
13
+ SUB=""
14
+ REPO_ROOT=""
15
+ HOOKS_PATH=".githooks"
16
+ AUTO=0
17
+
18
+ usage() {
19
+ printf 'Usage: safedeps hooks <install|check|init> [--root <repo>] [--hooks-path <dir>] [--auto]\n' >&2
20
+ }
21
+
22
+ while [ $# -gt 0 ]; do
23
+ case "$1" in
24
+ install|check|init) SUB="$1"; shift ;;
25
+ --root) REPO_ROOT="${2:?--root needs a path}"; shift 2 ;;
26
+ --hooks-path) HOOKS_PATH="${2:?--hooks-path needs a dir}"; shift 2 ;;
27
+ --auto) AUTO=1; shift ;;
28
+ -h|--help) usage; exit 0 ;;
29
+ *) usage; exit 64 ;;
30
+ esac
31
+ done
32
+
33
+ if [ -z "$SUB" ]; then usage; exit 64; fi
34
+ if [ -z "$REPO_ROOT" ]; then REPO_ROOT="$(pwd)"; fi
35
+ REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
36
+
37
+ if ! git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
38
+ if [ "$SUB" = "install" ] && [ "$AUTO" -eq 1 ]; then
39
+ printf 'safedeps hooks: skipped install (not a git worktree)\n'
40
+ exit 0
41
+ fi
42
+ printf 'ERROR: not inside a git worktree: %s\n' "$REPO_ROOT" >&2
43
+ exit 1
44
+ fi
45
+
46
+ HOOK_FILE="$REPO_ROOT/$HOOKS_PATH/pre-commit"
47
+
48
+ case "$SUB" in
49
+ init)
50
+ # Scaffold the repo's secret-leak policy from starter templates. Non-destructive:
51
+ # an existing file is reported and skipped, so repo-owned edits survive a re-run.
52
+ # `init` only drops files; `install` activates them (core.hooksPath).
53
+ TEMPLATES_DIR="$GATES_LIB_DIR/templates"
54
+ PROFILE="$(safedeps_repo_profile "$REPO_ROOT")"
55
+ created=0
56
+
57
+ scaffold() {
58
+ local dest="$1" tmpl="$2" mode="$3"
59
+ if [ -e "$dest" ]; then
60
+ printf 'safedeps hooks init: exists, skipped: %s\n' "$dest"
61
+ return 0
62
+ fi
63
+ if [ ! -f "$tmpl" ]; then
64
+ printf 'ERROR: template missing: %s\n' "$tmpl" >&2
65
+ exit 1
66
+ fi
67
+ mkdir -p "$(dirname "$dest")"
68
+ cp "$tmpl" "$dest"
69
+ chmod "$mode" "$dest"
70
+ printf 'safedeps hooks init: created %s\n' "$dest"
71
+ created=$((created + 1))
72
+ }
73
+
74
+ if [ "$PROFILE" = "private" ]; then
75
+ scaffold "$REPO_ROOT/.gitleaks.private.toml" "$TEMPLATES_DIR/gitleaks.private.toml.tmpl" 0644
76
+ else
77
+ scaffold "$REPO_ROOT/.gitleaks.toml" "$TEMPLATES_DIR/gitleaks.toml.tmpl" 0644
78
+ fi
79
+ scaffold "$REPO_ROOT/$HOOKS_PATH/pre-commit" "$TEMPLATES_DIR/pre-commit.tmpl" 0755
80
+
81
+ printf 'safedeps hooks init: profile=%s, %d file(s) created.\n' "$PROFILE" "$created"
82
+ if [ "$created" -gt 0 ]; then
83
+ printf ' Review the scaffolded .gitleaks policy, then activate:\n'
84
+ printf ' safedeps hooks install --root "%s"\n' "$REPO_ROOT"
85
+ fi
86
+ ;;
87
+ install)
88
+ if [ ! -f "$HOOK_FILE" ]; then
89
+ printf 'ERROR: hook file not found: %s\n' "$HOOK_FILE" >&2
90
+ printf ' the repo must provide its own %s/pre-commit policy.\n' "$HOOKS_PATH" >&2
91
+ exit 1
92
+ fi
93
+ chmod +x "$HOOK_FILE"
94
+ git -C "$REPO_ROOT" config core.hooksPath "$HOOKS_PATH"
95
+ printf 'safedeps hooks: installed repo-local git hooks at %s/%s\n' "$REPO_ROOT" "$HOOKS_PATH"
96
+ printf 'safedeps hooks: core.hooksPath = %s\n' "$(git -C "$REPO_ROOT" config --get core.hooksPath)"
97
+ ;;
98
+ check)
99
+ local_expected="$HOOKS_PATH"
100
+ actual="$(git -C "$REPO_ROOT" config --get core.hooksPath || true)"
101
+ if [ "$actual" != "$local_expected" ]; then
102
+ cat >&2 <<EOF
103
+ ERROR: repo-local git hooks are not active.
104
+
105
+ Expected core.hooksPath: $local_expected
106
+ Actual core.hooksPath: ${actual:-<unset>}
107
+
108
+ Run:
109
+ safedeps hooks install --root "$REPO_ROOT"
110
+ EOF
111
+ exit 1
112
+ fi
113
+ if [ ! -x "$HOOK_FILE" ]; then
114
+ printf 'ERROR: %s is not executable.\n' "$HOOK_FILE" >&2
115
+ exit 1
116
+ fi
117
+ if ! command -v gitleaks >/dev/null 2>&1; then
118
+ if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then
119
+ cat >&2 <<'EOF'
120
+ ERROR: neither local gitleaks nor a running Docker daemon is available.
121
+
122
+ Choose one:
123
+ brew install gitleaks
124
+ open -a Docker
125
+ EOF
126
+ exit 1
127
+ fi
128
+ fi
129
+ printf 'safedeps hooks: active (core.hooksPath = %s)\n' "$local_expected"
130
+ ;;
131
+ esac
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # Generic repo security profile + gitleaks config resolution.
3
+ # Absorbed from kuma-studio scripts/security/repo-profile.sh and made generic:
4
+ # the private profile is detected by a "-private" suffix convention instead of a
5
+ # hard-coded repo name, and overrides accept both SAFEDEPS_* and legacy KUMA_* env.
6
+
7
+ safedeps_repo_profile() {
8
+ local repo_root="${1:?repo root required}"
9
+ local override="${SAFEDEPS_REPO_PROFILE:-${KUMA_SECURITY_REPO_PROFILE:-}}"
10
+
11
+ case "$override" in
12
+ public|private)
13
+ printf '%s\n' "$override"
14
+ return 0
15
+ ;;
16
+ "")
17
+ ;;
18
+ *)
19
+ printf 'ERROR: repo profile override must be "public" or "private", got: %s\n' "$override" >&2
20
+ return 64
21
+ ;;
22
+ esac
23
+
24
+ local origin_url repo_leaf
25
+ origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
26
+ repo_leaf="$(basename "$repo_root")"
27
+
28
+ # Convention: a repo whose origin slug or directory leaf ends in "-private" is
29
+ # the private profile (e.g. kuma-studio-private). Everything else is public.
30
+ if [[ "$origin_url" =~ (^|[/:-])[A-Za-z0-9._-]*-private(\.git)?$ ]] || [[ "$repo_leaf" == *-private ]]; then
31
+ printf 'private\n'
32
+ return 0
33
+ fi
34
+
35
+ printf 'public\n'
36
+ }
37
+
38
+ safedeps_gitleaks_config() {
39
+ local repo_root="${1:?repo root required}"
40
+ local profile="${2:?profile required}"
41
+ local override="${SAFEDEPS_GITLEAKS_CONFIG:-${KUMA_GITLEAKS_CONFIG:-}}"
42
+
43
+ if [ -n "$override" ]; then
44
+ printf '%s\n' "$override"
45
+ return 0
46
+ fi
47
+
48
+ case "$profile" in
49
+ private)
50
+ printf '%s/.gitleaks.private.toml\n' "$repo_root"
51
+ ;;
52
+ public)
53
+ printf '%s/.gitleaks.toml\n' "$repo_root"
54
+ ;;
55
+ *)
56
+ printf 'ERROR: unknown security profile: %s\n' "$profile" >&2
57
+ return 64
58
+ ;;
59
+ esac
60
+ }
@@ -0,0 +1,94 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # safedeps scan secrets — generic gitleaks runner.
5
+ # Absorbed from kuma-studio scripts/security/run-gitleaks.sh and made generic:
6
+ # repo root comes from --root (default: cwd), config from repo profile or override.
7
+ # Preference order: local gitleaks binary -> Docker image (explicit, printed).
8
+
9
+ GATES_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ # shellcheck source=./repo-profile.sh
11
+ source "$GATES_LIB_DIR/repo-profile.sh"
12
+
13
+ REPO_ROOT=""
14
+ MODE="repo"
15
+ CONFIG_OVERRIDE=""
16
+ IMAGE="${SAFEDEPS_GITLEAKS_IMAGE:-${KUMA_GITLEAKS_IMAGE:-ghcr.io/gitleaks/gitleaks:latest}}"
17
+
18
+ usage() {
19
+ printf 'Usage: safedeps scan secrets [--repo|--worktree|--staged] [--root <repo>] [--config <path>]\n' >&2
20
+ }
21
+
22
+ while [ $# -gt 0 ]; do
23
+ case "$1" in
24
+ --repo) MODE="repo"; shift ;;
25
+ --worktree) MODE="worktree"; shift ;;
26
+ --staged) MODE="staged"; shift ;;
27
+ --root) REPO_ROOT="${2:?--root needs a path}"; shift 2 ;;
28
+ --config) CONFIG_OVERRIDE="${2:?--config needs a path}"; shift 2 ;;
29
+ secrets) shift ;; # allow `scan secrets ...`
30
+ -h|--help) usage; exit 0 ;;
31
+ *) usage; exit 64 ;;
32
+ esac
33
+ done
34
+
35
+ if [ -z "$REPO_ROOT" ]; then REPO_ROOT="$(pwd)"; fi
36
+ REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
37
+
38
+ REPO_PROFILE="$(safedeps_repo_profile "$REPO_ROOT")"
39
+ if [ -n "$CONFIG_OVERRIDE" ]; then
40
+ CONFIG_PATH="$CONFIG_OVERRIDE"
41
+ else
42
+ CONFIG_PATH="$(safedeps_gitleaks_config "$REPO_ROOT" "$REPO_PROFILE")"
43
+ fi
44
+ CONFIG_BASENAME="$(basename "$CONFIG_PATH")"
45
+
46
+ cd "$REPO_ROOT"
47
+
48
+ if [ ! -f "$CONFIG_PATH" ]; then
49
+ printf 'ERROR: gitleaks config does not exist: %s\n' "$CONFIG_PATH" >&2
50
+ exit 1
51
+ fi
52
+
53
+ printf 'safedeps secret scan: profile=%s config=%s mode=%s\n' "$REPO_PROFILE" "$CONFIG_BASENAME" "$MODE" >&2
54
+
55
+ SCAN_ROOT="$REPO_ROOT"
56
+
57
+ LOCAL_ARGS=(git --no-banner --redact --verbose --config "$CONFIG_PATH")
58
+ DOCKER_ARGS=(git --no-banner --redact --verbose --config "/repo/$CONFIG_BASENAME")
59
+
60
+ if [ "$MODE" = "staged" ]; then
61
+ LOCAL_ARGS+=(--pre-commit --staged)
62
+ DOCKER_ARGS+=(--pre-commit --staged)
63
+ elif [ "$MODE" = "worktree" ]; then
64
+ LOCAL_ARGS=(dir --no-banner --redact --verbose --config "$CONFIG_PATH")
65
+ DOCKER_ARGS=(dir --no-banner --redact --verbose --config "/repo/$CONFIG_BASENAME")
66
+ fi
67
+
68
+ LOCAL_ARGS+=("$SCAN_ROOT")
69
+ if [ "$MODE" = "worktree" ]; then
70
+ DOCKER_ARGS+=("/repo")
71
+ else
72
+ DOCKER_ARGS+=(/repo)
73
+ fi
74
+
75
+ if command -v gitleaks >/dev/null 2>&1; then
76
+ gitleaks "${LOCAL_ARGS[@]}"
77
+ exit $?
78
+ fi
79
+
80
+ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
81
+ docker run --rm -v "$REPO_ROOT:/repo" -w /repo "$IMAGE" "${DOCKER_ARGS[@]}"
82
+ exit $?
83
+ fi
84
+
85
+ cat >&2 <<EOF
86
+ ERROR: gitleaks is not available.
87
+
88
+ Choose one:
89
+ 1. Install locally: brew install gitleaks
90
+ 2. Or start Docker so the scan can use: $IMAGE
91
+
92
+ The scan is blocked (fail-closed) until a scanner is available.
93
+ EOF
94
+ exit 1
@@ -0,0 +1,45 @@
1
+ # .gitleaks.private.toml — safedeps starter secret-scan policy (PRIVATE profile).
2
+ #
3
+ # Resolved when the repo's origin slug or directory leaf ends in "-private",
4
+ # or when SAFEDEPS_REPO_PROFILE=private. Use this for the stricter policy a
5
+ # private repo wants on top of the public defaults.
6
+ #
7
+ # safedeps OWNS execution; THIS FILE is the repo's POLICY — you own it.
8
+ # `safedeps hooks init` scaffolds this once and will NOT overwrite your edits.
9
+
10
+ title = "safedeps secret-scan policy (private)"
11
+
12
+ # Inherit gitleaks' built-in ruleset. To also layer the repo's public policy,
13
+ # point `path` at it instead of (or in addition to) useDefault.
14
+ [extend]
15
+ useDefault = true
16
+ # path = ".gitleaks.toml"
17
+
18
+ # --- Extra rules layered on top of the defaults -------------------------------
19
+
20
+ [[rules]]
21
+ id = "safedeps-dotenv-file"
22
+ description = "Committed .env file with an assigned secret value"
23
+ path = '''(^|/)\.env(\.[\w.-]+)?$'''
24
+ regex = '''(?i)(secret|token|password|passwd|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret)\s*[:=]\s*\S{6,}'''
25
+ [rules.allowlist]
26
+ paths = [
27
+ '''(^|/)\.env\.example$''',
28
+ '''(^|/)\.env\.sample$''',
29
+ '''(^|/)\.env\.template$''',
30
+ ]
31
+
32
+ # Private repos often also flag internal identifiers (infra hosts, account ids).
33
+ # Add your own, e.g.:
34
+ # [[rules]]
35
+ # id = "safedeps-internal-host"
36
+ # description = "Internal hostname / infra endpoint"
37
+ # regex = '''(?i)\b[\w.-]+\.internal\.example\.com\b'''
38
+
39
+ # --- Repo-owned allowlist — CUSTOMIZE FOR THIS REPO ---------------------------
40
+ [allowlist]
41
+ description = "Repo-specific allowlist (tune me)"
42
+ paths = [
43
+ '''(^|/)\.gitleaks\.toml$''',
44
+ '''(^|/)\.gitleaks\.private\.toml$''',
45
+ ]
@@ -0,0 +1,43 @@
1
+ # .gitleaks.toml — safedeps starter secret-scan policy (public repo profile).
2
+ #
3
+ # safedeps OWNS execution (it runs gitleaks via `safedeps scan secrets`).
4
+ # THIS FILE is the repo's POLICY — you own it. Tune the [allowlist] below for
5
+ # this repo. `safedeps hooks init` scaffolds this file once and will NOT
6
+ # overwrite it on a re-run, so your edits are safe.
7
+ #
8
+ # gitleaks config reference: https://github.com/gitleaks/gitleaks#configuration
9
+
10
+ title = "safedeps secret-scan policy"
11
+
12
+ # Inherit gitleaks' built-in ruleset (AWS / GCP / private keys / OAuth tokens /
13
+ # generic high-entropy secrets, etc.). This is the bulk of the coverage.
14
+ [extend]
15
+ useDefault = true
16
+
17
+ # --- Extra rules layered on top of the defaults -------------------------------
18
+
19
+ # Flag a committed real dotenv file with an assigned secret. The .example /
20
+ # .sample / .template variants are allowlisted below so docs stay committable.
21
+ [[rules]]
22
+ id = "safedeps-dotenv-file"
23
+ description = "Committed .env file with an assigned secret value"
24
+ path = '''(^|/)\.env(\.[\w.-]+)?$'''
25
+ regex = '''(?i)(secret|token|password|passwd|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret)\s*[:=]\s*\S{6,}'''
26
+ [rules.allowlist]
27
+ paths = [
28
+ '''(^|/)\.env\.example$''',
29
+ '''(^|/)\.env\.sample$''',
30
+ '''(^|/)\.env\.template$''',
31
+ ]
32
+
33
+ # --- Repo-owned allowlist — CUSTOMIZE FOR THIS REPO ---------------------------
34
+ [allowlist]
35
+ description = "Repo-specific allowlist (tune me)"
36
+ paths = [
37
+ '''(^|/)\.gitleaks\.toml$''',
38
+ '''(^|/)\.gitleaks\.private\.toml$''',
39
+ # Add test fixtures / docs that legitimately contain secret-shaped strings:
40
+ # '''(^|/)test/fixtures/''',
41
+ ]
42
+ # regexes = ['''EXAMPLE_PLACEHOLDER_[A-Z0-9]+''']
43
+ # stopwords = ["example", "dummy", "placeholder"]
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # .githooks/pre-commit — safedeps secret-leak gate (scaffolded by `safedeps hooks init`).
3
+ #
4
+ # Blocks a commit when gitleaks finds a secret in the STAGED changes, so a key
5
+ # or a real .env never enters the repo's history. The detection POLICY lives in
6
+ # this repo's .gitleaks.toml (public) / .gitleaks.private.toml (private);
7
+ # safedeps owns execution. This hook delegates to one canonical scanner path:
8
+ # `safedeps scan secrets --staged`.
9
+ #
10
+ # Intentional bypass (use sparingly — you own the risk): git commit --no-verify
11
+ set -euo pipefail
12
+
13
+ repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
14
+
15
+ # Resolve the safedeps CLI: PATH first, then the known skill install locations.
16
+ resolve_safedeps() {
17
+ if command -v safedeps >/dev/null 2>&1; then printf 'safedeps'; return 0; fi
18
+ local candidate
19
+ for candidate in \
20
+ "${SAFEDEPS_BIN:-}" \
21
+ "${HOME}/.claude/skills/safedeps/bin/safedeps" \
22
+ "${HOME}/.codex/skills/safedeps/bin/safedeps"; do
23
+ if [ -n "${candidate}" ] && [ -x "${candidate}" ]; then
24
+ printf '%s' "${candidate}"
25
+ return 0
26
+ fi
27
+ done
28
+ return 1
29
+ }
30
+
31
+ if ! sd=$(resolve_safedeps); then
32
+ cat >&2 <<'EOF'
33
+ safedeps pre-commit: cannot locate the `safedeps` CLI.
34
+
35
+ The secret-scan gate is FAIL-CLOSED — the commit is blocked because the scanner
36
+ could not run (no silent skip). Fix one of:
37
+ - put `safedeps` on PATH, or
38
+ - export SAFEDEPS_BIN=/path/to/bin/safedeps, or
39
+ - install the skill: node scripts/install/install-safedeps-hooks.mjs
40
+
41
+ To bypass intentionally (you own the risk): git commit --no-verify
42
+ EOF
43
+ exit 1
44
+ fi
45
+
46
+ # Delegate to the single canonical scanner. A non-zero exit means either a
47
+ # secret was found OR no scanner (gitleaks/docker) is available — both
48
+ # fail-closed and block the commit.
49
+ exec "${sd}" scan secrets --staged --root "${repo_root}"