1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141 | #!/usr/bin/env bash
# check-anonymity.sh — pre-publish audit for a repo about to go public.
#
# ./check-anonymity.sh <repo-dir> [ident]
#
# ident defaults to the repo's `git config user.name`. Exits non-zero on any
# hard failure; heuristic findings are warnings unless STRICT=1.
#
# Identity patterns (your real names/emails, one extended-regex per line,
# '#' comments) are read from an UNTRACKED local file so that no published
# byte ever contains them:
# $IDPAT_FILE, default ~/.config/cod-sursa/idpat
#
# Checks:
# 1 local git identity is <ident> / <ident>@noreply.local
# 2 every author+committer across all refs is exactly that identity
# 3 no identity-pattern bytes in ANY git object (all history, all refs)
# 4 no identity-pattern bytes in the working tree (incl. untracked-but-
# committable files is your job; this scans tracked files)
# 5 no build output (deploy/.site) committed
# 6 heuristics: secret-shaped tracked files/content, absolute home paths
set -euo pipefail
REPO=${1:?usage: check-anonymity.sh <repo-dir> [ident]}
REPO=$(cd "$REPO" && pwd)
IDENT=${2:-$(git -C "$REPO" config user.name || true)}
IDPAT_FILE=${IDPAT_FILE:-"$HOME/.config/cod-sursa/idpat"}
fail=0
warn=0
say() { printf '%s\n' "$*"; }
ok() { say " ok $*"; }
bad() { say " FAIL $*"; fail=1; }
note() { say " warn $*"; warn=1; }
say "== anonymity audit: $REPO (ident: ${IDENT:-<unset>})"
# ---- 0. patterns -----------------------------------------------------------
if [ ! -s "$IDPAT_FILE" ]; then
bad "identity patterns file missing/empty: $IDPAT_FILE"
say " create it (one regex per line) — the leak scan cannot run without it"
exit 1
fi
IDPAT=$(grep -v '^[[:space:]]*#' "$IDPAT_FILE" | grep -v '^[[:space:]]*$' | paste -sd'|' -)
[ -n "$IDPAT" ] || { bad "no usable patterns in $IDPAT_FILE"; exit 1; }
ok "patterns loaded from $IDPAT_FILE ($(printf '%s' "$IDPAT" | awk -F'|' '{print NF}') pattern(s))"
# ---- 1. local identity -----------------------------------------------------
NAME=$(git -C "$REPO" config user.name || true)
MAIL=$(git -C "$REPO" config user.email || true)
if [ -z "$IDENT" ]; then
bad "no ident given and repo has no local user.name"
elif [ "$NAME" = "$IDENT" ] && [ "$MAIL" = "$IDENT@noreply.local" ]; then
ok "local identity: $NAME <$MAIL>"
else
bad "local identity is '$NAME <$MAIL>', expected '$IDENT <$IDENT@noreply.local>'"
fi
# ---- 2. history identities -------------------------------------------------
if git -C "$REPO" rev-parse -q --verify HEAD >/dev/null 2>&1; then
AUTHORS=$(git -C "$REPO" log --all --format='%an <%ae>%n%cn <%ce>' | sort -u)
if [ "$AUTHORS" = "$IDENT <$IDENT@noreply.local>" ]; then
ok "history: single identity across all refs"
else
bad "history has non-anonymous identities:"
printf '%s\n' "$AUTHORS" | sed 's/^/ /'
fi
else
note "no commits yet — history checks skipped (set identity before committing!)"
fi
# ---- 3. identity bytes in any git object -----------------------------------
# --batch-all-objects covers every object in the db, packed or loose,
# including unreachable ones left behind by rewrites.
if git -C "$REPO" rev-parse -q --verify HEAD >/dev/null 2>&1; then
HITS=$(git -C "$REPO" cat-file --batch-all-objects --batch 2>/dev/null \
| grep -aicE "$IDPAT" || true)
if [ "${HITS:-0}" = "0" ]; then
ok "object scan: no identity bytes in any git object"
else
bad "identity patterns matched in $HITS line(s) of git object data"
say " locate with: git -C '$REPO' grep -iE '<pattern>' \$(git -C '$REPO' rev-list --all)"
say " (unreachable objects: rewrite happened? re-clone --no-local drops them,"
say " but the source of the match must still be found and removed)"
fi
fi
# ---- 4. identity bytes in tracked working tree -----------------------------
TREE_HITS=$(git -C "$REPO" ls-files -z 2>/dev/null \
| xargs -0 grep -ailE "$IDPAT" -- 2>/dev/null || true)
if [ -z "$TREE_HITS" ]; then
ok "working tree: no identity bytes in tracked files"
else
bad "identity patterns in tracked files:"
printf '%s\n' "$TREE_HITS" | sed 's/^/ /'
fi
# ---- 5. build output committed ---------------------------------------------
if git -C "$REPO" rev-parse -q --verify HEAD >/dev/null 2>&1 \
&& git -C "$REPO" ls-tree -r --name-only HEAD | grep -q '\.site'; then
bad "deploy/.site committed into the repo"
else
ok "no nested build output committed"
fi
# ---- 6. heuristics ---------------------------------------------------------
SECRET_PAT='BEGIN[[:print:]]*PRIVATE KEY|api[_-]?key[[:space:]"'\'':=]+[A-Za-z0-9_\-]{16,}|secret[[:space:]"'\'':=]+[A-Za-z0-9_\-]{16,}|Bearer [A-Za-z0-9_\-\.]{20,}|eyJ[A-Za-z0-9_\-]{20,}'
S_HITS=$(git -C "$REPO" ls-files -z 2>/dev/null \
| xargs -0 grep -alE "$SECRET_PAT" -- 2>/dev/null || true)
if [ -n "$S_HITS" ]; then
note "secret-shaped content in tracked files (verify these are placeholders):"
printf '%s\n' "$S_HITS" | sed 's/^/ /'
fi
if git -C "$REPO" ls-files | grep -qE '(^|/)\.env(\.|$)|id_rsa|id_ed25519|\.pem$|\.p12$'; then
note "credential-looking filenames are tracked:"
git -C "$REPO" ls-files | grep -E '(^|/)\.env(\.|$)|id_rsa|id_ed25519|\.pem$|\.p12$' | sed 's/^/ /'
fi
# bracketed first letters so this script's own bytes never spell a home-dir
# prefix — an identity pattern like the literal macOS home prefix would
# otherwise match the auditor itself
P_HITS=$(git -C "$REPO" ls-files -z 2>/dev/null \
| xargs -0 grep -alE '/[U]sers/[a-z]|/[h]ome/[a-z]' -- 2>/dev/null || true)
if [ -n "$P_HITS" ]; then
note "absolute home paths in tracked files (may reveal a username):"
printf '%s\n' "$P_HITS" | sed 's/^/ /'
fi
# ---- verdict ---------------------------------------------------------------
if [ "$fail" != 0 ]; then
say "== FAILED — do not publish"
exit 1
fi
if [ "$warn" != 0 ] && [ "${STRICT:-0}" = "1" ]; then
say "== warnings present and STRICT=1 — do not publish"
exit 1
fi
if [ "$warn" != 0 ]; then
say "== OK — safe to publish (with warnings above)"
else
say "== OK — safe to publish"
fi
|