mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-03 01:46:55 +02:00
The skill made the model retype a long safe-Git prefix on each call and a
dropped flag failed shared-libs-read-only. bin/gstack-safe-git applies the
fixed env + flag prefix, adds --no-ext-diff --no-textconv to log/show/diff,
allows diff only between two explicit object IDs and ls-files only in the
NUL-delimited overlay form, and refuses every other shape with one line
naming the allowed forms. The template now points at the installed helper
(host global runtime via {{SAFE_GIT}}) and drops the prose it enforces.
Fixtures resolve the helper to this checkout, the git shim records the safety
environment, and isGuardedGitRequest requires the complete prefix (env
included) for every repository read.
150 lines
5.4 KiB
Bash
Executable File
150 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-safe-git — run one allowlisted, read-only Git query for audits that
|
|
# must not execute project-controlled code (/deslop-shared-libs).
|
|
#
|
|
# Usage: gstack-safe-git [-C <dir>] <subcommand> [args...]
|
|
#
|
|
# Every invocation runs `git` with this fixed prefix; callers cannot add or
|
|
# override it:
|
|
# GIT_OPTIONAL_LOCKS=0 GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0
|
|
# git --no-pager --no-lazy-fetch --no-replace-objects
|
|
# -c core.fsmonitor=false -c log.showSignature=false -c diff.submodule=short
|
|
#
|
|
# Only query shapes that cannot run clean/process filters, textconv or external
|
|
# diff drivers, signature verifiers, pagers, transports, or index/ref writes are
|
|
# forwarded. log/show/diff always get --no-ext-diff --no-textconv; diff is only
|
|
# between two explicit object IDs. Everything else is refused with exit 2 and
|
|
# a one-line message naming the allowed forms. Git's own exit status passes
|
|
# through unchanged, including 129 when this Git lacks --no-lazy-fetch.
|
|
#
|
|
# The script sources nothing and executes only `git` from PATH.
|
|
set -euo pipefail
|
|
|
|
ALLOWED='allowed: rev-parse, symbolic-ref [--short] <ref>, branch --show-current, remote [-v | get-url <name>], config --get|--get-all|--get-regexp <key>, log, show, ls-tree, cat-file, rev-list, merge-base, for-each-ref, show-ref, grep, diff <object-id> <object-id> [-- <path>...], ls-files --cached --others --exclude-standard -z [-- <path>...]'
|
|
|
|
refuse() {
|
|
echo "gstack-safe-git: refused: $1; $ALLOWED" >&2
|
|
exit 2
|
|
}
|
|
|
|
dir_args=()
|
|
if [ "${1:-}" = "-C" ]; then
|
|
[ $# -ge 2 ] || refuse "-C needs a directory"
|
|
dir_args=(-C "$2")
|
|
shift 2
|
|
fi
|
|
[ $# -ge 1 ] || refuse "no subcommand"
|
|
sub=$1
|
|
shift
|
|
case "$sub" in
|
|
-*) refuse "global option '$sub' (only a leading -C <dir> is accepted; the safety -c settings are fixed)" ;;
|
|
rev-parse|symbolic-ref|branch|remote|config|log|show|ls-tree|cat-file|rev-list|merge-base|for-each-ref|show-ref|grep|diff|ls-files) ;;
|
|
*) refuse "'$sub' is not an allowlisted read" ;;
|
|
esac
|
|
|
|
for arg in "$@"; do
|
|
[ "$arg" = "--" ] && break
|
|
case "$arg" in
|
|
--output|--output=*) refuse "'$arg' writes files" ;;
|
|
--ext-diff|--textconv|--filters|--path|--path=*) refuse "'$arg' can run configured diff drivers or filters" ;;
|
|
--show-signature|*%G*|*'%(signature'*) refuse "'$arg' runs a signature verifier" ;;
|
|
--no-index|--recurse-submodules) refuse "'$arg' reads outside the repository's committed objects" ;;
|
|
esac
|
|
done
|
|
|
|
positional_before_dashdash() {
|
|
local count=0 arg
|
|
for arg in "$@"; do
|
|
[ "$arg" = "--" ] && break
|
|
case "$arg" in -*) ;; *) count=$((count + 1)) ;; esac
|
|
done
|
|
echo "$count"
|
|
}
|
|
|
|
extra=()
|
|
case "$sub" in
|
|
rev-parse|ls-tree|cat-file|rev-list|merge-base|for-each-ref|show-ref) ;;
|
|
log|show) extra=(--no-ext-diff --no-textconv) ;;
|
|
grep)
|
|
for arg in "$@"; do
|
|
[ "$arg" = "--" ] && break
|
|
case "$arg" in
|
|
-O*|--open-files-in-pager*) refuse "'$arg' launches a pager program" ;;
|
|
esac
|
|
done
|
|
;;
|
|
symbolic-ref)
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-q|--quiet|--short|--no-recurse) ;;
|
|
-*) refuse "symbolic-ref '$arg' is not a read" ;;
|
|
esac
|
|
done
|
|
[ "$(positional_before_dashdash "$@")" = 1 ] || refuse "symbolic-ref reads exactly one ref"
|
|
;;
|
|
branch)
|
|
[ "$*" = "--show-current" ] || refuse "branch is limited to 'branch --show-current'"
|
|
;;
|
|
remote)
|
|
case "$*" in
|
|
''|-v|--verbose) ;;
|
|
*)
|
|
[ "${1:-}" = "get-url" ] || refuse "remote is limited to listing and get-url"
|
|
shift_count=0
|
|
for arg in "${@:2}"; do
|
|
case "$arg" in
|
|
--push|--all) ;;
|
|
-*) refuse "remote get-url '$arg'" ;;
|
|
*) shift_count=$((shift_count + 1)) ;;
|
|
esac
|
|
done
|
|
[ "$shift_count" = 1 ] || refuse "remote get-url takes one remote name"
|
|
;;
|
|
esac
|
|
;;
|
|
config)
|
|
case "${1:-}" in
|
|
--get|--get-all|--get-regexp) ;;
|
|
*) refuse "config is limited to --get, --get-all and --get-regexp" ;;
|
|
esac
|
|
[ $# -ge 2 ] && [ $# -le 3 ] || refuse "config reads take a key and an optional value pattern"
|
|
for arg in "${@:2}"; do
|
|
case "$arg" in -*) refuse "config '$arg'" ;; esac
|
|
done
|
|
;;
|
|
diff)
|
|
ids=0
|
|
for arg in "$@"; do
|
|
[ "$arg" = "--" ] && break
|
|
case "$arg" in
|
|
--cached|--staged|--merge-base|--merge-base=*) refuse "diff '$arg' compares the index or derived revisions" ;;
|
|
-*) ;;
|
|
*)
|
|
[[ "$arg" =~ ^[0-9a-fA-F]{7,64}$ ]] || refuse "diff operand '$arg' is not an explicit object ID (put paths after --)"
|
|
ids=$((ids + 1))
|
|
;;
|
|
esac
|
|
done
|
|
[ "$ids" = 2 ] || refuse "diff needs exactly two explicit committed object IDs, never the worktree or index"
|
|
extra=(--no-ext-diff --no-textconv)
|
|
;;
|
|
ls-files)
|
|
nul=0
|
|
for arg in "$@"; do
|
|
[ "$arg" = "--" ] && break
|
|
case "$arg" in
|
|
-z) nul=1 ;;
|
|
--cached|--others|--exclude-standard|--stage) ;;
|
|
*) refuse "ls-files '$arg' (the overlay form is 'ls-files --cached --others --exclude-standard -z [-- <path>...]')" ;;
|
|
esac
|
|
done
|
|
[ "$nul" = 1 ] || refuse "ls-files output must be NUL-delimited with -z"
|
|
;;
|
|
esac
|
|
|
|
unset GIT_EXTERNAL_DIFF GIT_CONFIG_PARAMETERS GIT_CONFIG_COUNT
|
|
export GIT_OPTIONAL_LOCKS=0 GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0
|
|
exec git --no-pager --no-lazy-fetch --no-replace-objects \
|
|
-c core.fsmonitor=false -c log.showSignature=false -c diff.submodule=short \
|
|
${dir_args[@]+"${dir_args[@]}"} "$sub" ${extra[@]+"${extra[@]}"} "$@"
|