This commit is contained in:
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
for cmd in git sops openssl; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
echo "check-sops-sync: required command missing: ${cmd}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${SOPS_SYNC_HMAC_KEY:-}" ]; then
|
||||
echo "check-sops-sync: SOPS_SYNC_HMAC_KEY is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
regex_script="${repo_root}/scripts/lib/sops-path-regexes"
|
||||
lib_script="${repo_root}/scripts/lib/sops-sync-lib"
|
||||
|
||||
if [ ! -x "${regex_script}" ]; then
|
||||
echo "check-sops-sync: missing helper ${regex_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${lib_script}" ]; then
|
||||
echo "check-sops-sync: missing helper ${lib_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. "${lib_script}"
|
||||
|
||||
regexes=("${(@f)$("${regex_script}")}")
|
||||
fail=0
|
||||
|
||||
while IFS= read -r tracked_path; do
|
||||
[ -n "${tracked_path}" ] || continue
|
||||
[[ "${tracked_path}" == *.enc.* ]] || continue
|
||||
|
||||
if ! matches_any_rule "${tracked_path}" "${regexes[@]}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
sidecar_path=$(hmac_sidecar_for_enc "${tracked_path}")
|
||||
|
||||
if ! git ls-files --error-unmatch -- "${sidecar_path}" >/dev/null 2>&1; then
|
||||
echo "check-sops-sync: missing sync sidecar for ${tracked_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_sops_encrypted_file "${tracked_path}"; then
|
||||
echo "check-sops-sync: file is not valid SOPS-encrypted content: ${tracked_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
sidecar_value=$(read_sidecar "${sidecar_path}" || true)
|
||||
if [ -z "${sidecar_value}" ]; then
|
||||
echo "check-sops-sync: sidecar is empty: ${sidecar_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! computed_hmac=$(decrypt_enc_to_plain "${tracked_path}" | compute_hmac_for_stdin); then
|
||||
echo "check-sops-sync: failed to decrypt ${tracked_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${computed_hmac}" != "${sidecar_value}" ]; then
|
||||
echo "check-sops-sync: decrypted plaintext HMAC does not match sidecar for ${tracked_path}" >&2
|
||||
fail=1
|
||||
fi
|
||||
done < <(git ls-files)
|
||||
|
||||
exit "${fail}"
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
for cmd in git sops openssl; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
echo "pre-commit: required command missing: ${cmd}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${SOPS_SYNC_HMAC_KEY:-}" ]; then
|
||||
echo "pre-commit: SOPS_SYNC_HMAC_KEY is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
regex_script="${repo_root}/scripts/lib/sops-path-regexes"
|
||||
lib_script="${repo_root}/scripts/lib/sops-sync-lib"
|
||||
|
||||
if [ ! -x "${regex_script}" ]; then
|
||||
echo "pre-commit: missing helper ${regex_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${lib_script}" ]; then
|
||||
echo "pre-commit: missing helper ${lib_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. "${lib_script}"
|
||||
|
||||
regexes=("${(@f)$("${regex_script}")}")
|
||||
typeset -A candidates
|
||||
|
||||
while IFS= read -r staged_path; do
|
||||
[ -n "${staged_path}" ] || continue
|
||||
|
||||
candidate_enc=""
|
||||
if [[ "${staged_path}" == *.enc.yaml ]]; then
|
||||
candidate_enc="${staged_path}"
|
||||
elif [[ "${staged_path}" == *.yaml ]]; then
|
||||
if git ls-files --error-unmatch -- "${staged_path}" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
candidate_enc=$(plain_to_enc "${staged_path}") || continue
|
||||
fi
|
||||
|
||||
[ -n "${candidate_enc}" ] || continue
|
||||
if matches_any_rule "${candidate_enc}" "${regexes[@]}"; then
|
||||
candidates["${candidate_enc}"]=1
|
||||
fi
|
||||
done < <(git diff --cached --name-only --diff-filter=ACMR)
|
||||
|
||||
for enc_path in "${(@k)candidates}"; do
|
||||
plain_path=$(enc_to_plain "${enc_path}") || continue
|
||||
sidecar_path=$(hmac_sidecar_for_enc "${enc_path}")
|
||||
|
||||
if [ ! -f "${plain_path}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if has_unstaged_changes "${plain_path}"; then
|
||||
echo "pre-commit: plaintext file has unstaged changes: ${plain_path}" >&2
|
||||
echo "pre-commit: stage or revert the plaintext change before committing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_hmac=$(compute_hmac_for_file "${plain_path}")
|
||||
tracked_hmac=""
|
||||
if [ -f "${sidecar_path}" ]; then
|
||||
tracked_hmac=$(read_sidecar "${sidecar_path}" || true)
|
||||
fi
|
||||
|
||||
if [ "${current_hmac}" = "${tracked_hmac}" ] && [ -f "${enc_path}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
encrypt_plain_to_enc "${plain_path}" "${enc_path}"
|
||||
write_sidecar "${sidecar_path}" "${current_hmac}"
|
||||
git add "${enc_path}" "${sidecar_path}"
|
||||
done
|
||||
|
||||
for enc_path in "${(@k)candidates}"; do
|
||||
sidecar_path=$(hmac_sidecar_for_enc "${enc_path}")
|
||||
|
||||
if ! git cat-file -e ":${enc_path}" 2>/dev/null; then
|
||||
echo "pre-commit: staged encrypted file missing: ${enc_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git cat-file -e ":${sidecar_path}" 2>/dev/null; then
|
||||
echo "pre-commit: staged sync sidecar missing: ${sidecar_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp_enc=$(mktemp)
|
||||
trap 'rm -f "${tmp_enc}"' EXIT INT TERM
|
||||
git show ":${enc_path}" > "${tmp_enc}"
|
||||
|
||||
staged_hmac=$(git show ":${sidecar_path}" | tr -d '\r\n')
|
||||
computed_hmac=$(decrypt_enc_to_plain "${tmp_enc}" | compute_hmac_for_stdin)
|
||||
|
||||
rm -f "${tmp_enc}"
|
||||
trap - EXIT INT TERM
|
||||
|
||||
if [ "${computed_hmac}" != "${staged_hmac}" ]; then
|
||||
echo "pre-commit: staged encrypted file and sidecar are out of sync: ${enc_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
hook_path="${repo_root}/.git/hooks/pre-commit"
|
||||
target="${repo_root}/scripts/git-hooks/pre-commit"
|
||||
|
||||
mkdir -p "${repo_root}/.git/hooks"
|
||||
cat > "${hook_path}" <<EOF
|
||||
#!/usr/bin/env zsh
|
||||
exec "${target}" "\$@"
|
||||
EOF
|
||||
|
||||
chmod +x "${hook_path}"
|
||||
echo "Installed git hook: ${hook_path}"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
sops_file="${repo_root}/.sops.yaml"
|
||||
|
||||
if [ ! -f "${sops_file}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v yq >/dev/null 2>&1; then
|
||||
yq -r '.creation_rules[]?.path_regex | select(. != null)' "${sops_file}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "${sops_file}" <<'PY'
|
||||
import sys
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except Exception as exc:
|
||||
raise SystemExit(f"python3 fallback requires PyYAML: {exc}")
|
||||
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||||
data = yaml.safe_load(handle) or {}
|
||||
|
||||
for rule in data.get("creation_rules") or []:
|
||||
regex = rule.get("path_regex")
|
||||
if regex:
|
||||
print(regex)
|
||||
PY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Unable to read .sops.yaml path_regex values: install yq or python3 with PyYAML" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
matches_any_rule() {
|
||||
local path="$1"
|
||||
shift
|
||||
local regex
|
||||
for regex in "$@"; do
|
||||
[[ -n "${regex}" ]] || continue
|
||||
if [[ "${path}" =~ ${regex} ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
enc_to_plain() {
|
||||
local enc="$1"
|
||||
[[ "${enc}" == *.enc.yaml ]] || return 1
|
||||
print -r -- "${enc%.enc.yaml}.yaml"
|
||||
}
|
||||
|
||||
plain_to_enc() {
|
||||
local plain="$1"
|
||||
[[ "${plain}" == *.yaml ]] || return 1
|
||||
if [[ "${plain}" == *.enc.yaml ]]; then
|
||||
print -r -- "${plain}"
|
||||
else
|
||||
print -r -- "${plain%.yaml}.enc.yaml"
|
||||
fi
|
||||
}
|
||||
|
||||
hmac_sidecar_for_enc() {
|
||||
local enc="$1"
|
||||
print -r -- "${enc}.sync-hmac"
|
||||
}
|
||||
|
||||
encrypt_plain_to_enc() {
|
||||
local plain="$1"
|
||||
local enc="$2"
|
||||
sops --encrypt --input-type yaml --output-type yaml --output "${enc}" "${plain}"
|
||||
}
|
||||
|
||||
decrypt_enc_to_plain() {
|
||||
local enc="$1"
|
||||
sops --decrypt "${enc}"
|
||||
}
|
||||
|
||||
compute_hmac_for_file() {
|
||||
local path="$1"
|
||||
openssl dgst -sha256 -hmac "${SOPS_SYNC_HMAC_KEY}" "${path}" | awk '{print $NF}'
|
||||
}
|
||||
|
||||
compute_hmac_for_stdin() {
|
||||
openssl dgst -sha256 -hmac "${SOPS_SYNC_HMAC_KEY}" | awk '{print $NF}'
|
||||
}
|
||||
|
||||
read_sidecar() {
|
||||
local sidecar="$1"
|
||||
[ -f "${sidecar}" ] || return 1
|
||||
tr -d '\r\n' < "${sidecar}"
|
||||
}
|
||||
|
||||
write_sidecar() {
|
||||
local sidecar="$1"
|
||||
local value="$2"
|
||||
print -r -- "${value}" > "${sidecar}"
|
||||
}
|
||||
|
||||
is_sops_encrypted_file() {
|
||||
local path="$1"
|
||||
[ -f "${path}" ] || return 1
|
||||
grep -q 'sops:' "${path}" && grep -q 'ENC\[' "${path}"
|
||||
}
|
||||
|
||||
has_unstaged_changes() {
|
||||
local path="$1"
|
||||
if git ls-files --error-unmatch -- "${path}" >/dev/null 2>&1; then
|
||||
git diff --quiet -- "${path}" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if git diff --cached --name-only -- "${path}" | grep -Fxq "${path}"; then
|
||||
git diff --quiet -- "${path}" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
Reference in New Issue
Block a user