feat(secrev): Plane-1 Phase 2 — coordinator + dependency-cve checker #16
93 changed files with 3882 additions and 2 deletions
|
|
@ -522,7 +522,7 @@ def _build_transport(args: argparse.Namespace) -> Any:
|
|||
``--help``, and ledger commands never need a token):
|
||||
|
||||
* ``slack`` -> :func:`build_live_slack_transport` over ``SLACK_BOT_TOKEN`` /
|
||||
``SLACK_CHANNEL``.
|
||||
``SLACK_CHANNEL_ID``.
|
||||
* ``github`` -> :func:`build_live_github_transport` over ``GITHUB_TOKEN`` and
|
||||
the issue thread ``GITHUB_OWNER`` / ``GITHUB_REPO`` /
|
||||
``GITHUB_ISSUE_NUMBER``. This is the §3.3.1 human-gate I/O only (post an
|
||||
|
|
@ -540,7 +540,7 @@ def _build_transport(args: argparse.Namespace) -> Any:
|
|||
if args.transport == "slack":
|
||||
from agent_team.transport.slack_live import build_live_slack_transport
|
||||
|
||||
channel = os.environ.get("SLACK_CHANNEL", "")
|
||||
channel = os.environ.get("SLACK_CHANNEL_ID", "")
|
||||
return build_live_slack_transport(channel)
|
||||
if args.transport == "github":
|
||||
from agent_team.transport.github_live import build_live_github_transport
|
||||
|
|
|
|||
504
security-review/checker_coordinator.sh
Executable file
504
security-review/checker_coordinator.sh
Executable file
|
|
@ -0,0 +1,504 @@
|
|||
#!/usr/bin/env bash
|
||||
# checker_coordinator.sh — Plane-1 coordinator for the R720 agent-team.
|
||||
#
|
||||
# Design refs: docs/r720-agent-team-design.md §5 (Coordination model), §6.1/§6.6 (ONE shared
|
||||
# cap across all roles — critical for the Claude subscription draw), §6.7 (state durability +
|
||||
# backup: atomic write-temp-then-rename, schema-version + content-hash + logical-consistency
|
||||
# integrity check, park-on-corrupt), §7 Phase 2 ("coordinator + second checker; run a forced
|
||||
# budget-squeeze dry-run to prove deferral-not-drop + COVERAGE ALARM").
|
||||
#
|
||||
# WHAT IT DOES:
|
||||
# Orchestrates the Plane-1 Tier-1 checkers (compliance-drift, dependency-cve) under ONE shared
|
||||
# budget + versioned rotation/coverage state. Nightly it (mirrors nightly_sweep + §5):
|
||||
# 1) loads the shared budget ledger + the versioned rotation/coverage state (integrity-checked)
|
||||
# 2) runs the CANARY SUITE FIRST — each role's checker with --canary; a miss is a COMPLACENCY
|
||||
# ALARM + that role is SKIPPED this run (never run a degraded role silently)
|
||||
# 3) fans out roles due to run (deferred-first, then rotation) under the SHARED cap; a role
|
||||
# whose estimated cost would exceed the ceiling is DEFERRED (recorded), never dropped
|
||||
# 4) raises a COVERAGE ALARM if any role's last_run slips past MAX_CYCLE_NIGHTS
|
||||
# 5) collects each run checker's report JSON, merges + DEDUPS across checkers, prioritizes
|
||||
# 6) routes ALARM-only (D3): confirmed critical/high -> Slack ALARM; everything else -> a
|
||||
# combined mode-600 coordinator report; a fully clean run posts NOTHING
|
||||
#
|
||||
# SUBSTRATE REUSE (lib/sweep_substrate.sh, sourced — bash dynamic scoping):
|
||||
# add_spend / over_budget -> shared budget ledger (read TOTAL_SPEND/TOTAL_BUDGET_USD)
|
||||
# redact / post_slack_alarm-> Slack delivery (read SLACK_WEBHOOK_URL, REPORT_DIR, SWEEP_LOG)
|
||||
# to_epoch -> cycle-age accounting for the COVERAGE alarm
|
||||
# The coordinator does NOT re-implement these; it provides the globals the contract names.
|
||||
#
|
||||
# STATE DURABILITY (design §6.7): both the budget ledger and the rotation/coverage state are
|
||||
# written ATOMICALLY (temp + rename) and integrity-checked on load = schema_version match +
|
||||
# stored content_hash + a logical-consistency check. On corruption the coordinator refuses to
|
||||
# proceed silently -> it PARKS that store + ALARMs; the budget ledger is rebuildable (a new UTC
|
||||
# day resets the day's spend), the rotation state is rebuildable from report history.
|
||||
#
|
||||
# SCOPE / SAFETY: read-only orchestration. Does NOT install systemd units, does NOT touch
|
||||
# agent_team/ or agent-team/, does NOT re-clone by default (checkers reuse $MIRROR_DIR; a
|
||||
# checker's own --refresh is the only network path and is not invoked here). See the
|
||||
# "PROVISIONING (NOT DONE HERE)" footer.
|
||||
#
|
||||
# Exit: 0 = ran (whether or not it alarmed); 2 = setup/usage error; 3 = a canary/assertion FAILED.
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
log() { echo "[coordinator] $*" >&2; }
|
||||
die() { echo "[coordinator] FATAL: $*" >&2; exit 2; }
|
||||
|
||||
# --- Shared substrate ---------------------------------------------------------
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SUBSTRATE="$HERE/lib/sweep_substrate.sh"
|
||||
[ -f "$SUBSTRATE" ] || die "shared substrate not found: $SUBSTRATE"
|
||||
# shellcheck source=lib/sweep_substrate.sh
|
||||
. "$SUBSTRATE"
|
||||
|
||||
CHECKERS_DIR="$HERE/checkers"
|
||||
|
||||
# --- Config + defaults (env, all optional) ------------------------------------
|
||||
GH_ORG="${GH_ORG:-Sea-Haven-Industries}"
|
||||
MIRROR_DIR="${MIRROR_DIR:-$HOME/repo-mirrors}"
|
||||
REPORT_ROOT="${REPORT_ROOT:-$HOME/sweep-reports}"
|
||||
TOTAL_BUDGET_USD="${TOTAL_BUDGET_USD:-120}" # ONE shared cap across ALL roles (design §6.1)
|
||||
MAX_CYCLE_NIGHTS="${MAX_CYCLE_NIGHTS:-6}" # COVERAGE alarm if a role slips past this many days
|
||||
SCHEMA_VERSION=1 # bump when a state-file shape changes
|
||||
|
||||
DRY_RUN=0 # --dry-run: compose alarms/reports but DO NOT post (routing dry-run)
|
||||
CANARY=0 # --canary: run every role's canary + assert all pass (offline)
|
||||
SQUEEZE=0 # --squeeze-dry-run: Phase-2 acceptance — force deferral + COVERAGE proof
|
||||
# --once is accepted for parity with the sweep (single pass; this script IS a single pass).
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
checker_coordinator.sh — Plane-1 coordinator (shared budget + versioned rotation, read-only)
|
||||
|
||||
--canary run EVERY role's canary and assert all pass (offline); post nothing
|
||||
--dry-run run roles but compose alarms/reports WITHOUT posting (routing dry-run)
|
||||
--squeeze-dry-run Phase-2 acceptance test: force a tiny TOTAL_BUDGET_USD so a role MUST be
|
||||
DEFERRED (not dropped) AND simulate enough elapsed cycles to trip the
|
||||
COVERAGE ALARM; prints the deferred list + COVERAGE alarm, posts nothing
|
||||
--once single coordination pass (this script is always a single pass)
|
||||
-h|--help this help
|
||||
|
||||
Env: TOTAL_BUDGET_USD MAX_CYCLE_NIGHTS REPORT_ROOT MIRROR_DIR GH_ORG GH_TOKEN SLACK_WEBHOOK_URL
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--canary) CANARY=1 ;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--squeeze-dry-run) SQUEEZE=1; DRY_RUN=1 ;;
|
||||
--once) : ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "unknown arg: $1 (see --help)" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
command -v jq >/dev/null || die "jq is required"
|
||||
command -v git >/dev/null || die "git is required"
|
||||
|
||||
# --- Report dir (mode 600 reports; matches sweep conventions) -----------------
|
||||
umask 077
|
||||
UTC_DATE="$(date -u +%Y-%m-%d)"
|
||||
UTC_STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
REPORT_DIR="$REPORT_ROOT/coordinator/$UTC_DATE"
|
||||
mkdir -p "$REPORT_DIR"; chmod 700 "$REPORT_ROOT" "$REPORT_DIR" 2>/dev/null || true
|
||||
# shellcheck disable=SC2034 # read by the sourced substrate (post_slack_alarm) via dynamic scope
|
||||
SWEEP_LOG="$REPORT_DIR/coordinator.log" # name the substrate's post_slack_alarm() references
|
||||
REPORT_JSON="$REPORT_DIR/coordinator.json"
|
||||
REPORT_TXT="$REPORT_DIR/coordinator.txt"
|
||||
|
||||
BUDGET_LEDGER="${BUDGET_LEDGER:-$REPORT_ROOT/.budget-ledger.json}"
|
||||
COORD_STATE="${COORD_STATE:-$REPORT_ROOT/.coordinator-state.json}"
|
||||
|
||||
log "=== checker_coordinator $UTC_STAMP (canary=$CANARY dry_run=$DRY_RUN squeeze=$SQUEEZE) ==="
|
||||
|
||||
# In the squeeze acceptance test, force a budget so small the SECOND role cannot fit.
|
||||
if [ "$SQUEEZE" -eq 1 ]; then
|
||||
TOTAL_BUDGET_USD="0.01"
|
||||
log "SQUEEZE: forcing TOTAL_BUDGET_USD=\$$TOTAL_BUDGET_USD so at least one role must DEFER"
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# REGISTRY — Tier-1 checker roles (name | script | est per-run cost USD | cadence-days).
|
||||
# A simple in-script table, easy to extend in later phases (add doc-drift, aws-posture...).
|
||||
# Cost is the shared-budget DRAW estimate (these checkers are deterministic/cheap; a future
|
||||
# agentic-judge role would carry a real Claude cost). Cadence is informational here.
|
||||
# ==============================================================================
|
||||
declare -a ROLES=(
|
||||
"compliance-drift|$CHECKERS_DIR/compliance-drift.sh|0.00|1"
|
||||
"dependency-cve|$CHECKERS_DIR/dependency-cve.sh|0.00|1"
|
||||
)
|
||||
role_field() { echo "$1" | cut -d'|' -f"$2"; }
|
||||
|
||||
# In SQUEEZE mode, assign non-zero costs so the shared cap is meaningful: the first role fits,
|
||||
# the second cannot — proving deferral-not-drop deterministically regardless of real cost.
|
||||
if [ "$SQUEEZE" -eq 1 ]; then
|
||||
ROLES=(
|
||||
"compliance-drift|$CHECKERS_DIR/compliance-drift.sh|0.008|1"
|
||||
"dependency-cve|$CHECKERS_DIR/dependency-cve.sh|0.008|1"
|
||||
)
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# DURABLE STATE (design §6.7): atomic write-temp-then-rename + integrity check.
|
||||
# Integrity = schema_version match + stored content_hash + logical-consistency.
|
||||
# content_hash is computed over the state WITHOUT its own hash field (canonical jq -S -c).
|
||||
# ==============================================================================
|
||||
state_hash() { # state_json_without_hash -> hex
|
||||
if command -v sha256sum >/dev/null 2>&1; then echo "$1" | jq -S -cj 'del(.content_hash)' | sha256sum | cut -d' ' -f1
|
||||
elif command -v shasum >/dev/null 2>&1; then echo "$1" | jq -S -cj 'del(.content_hash)' | shasum -a 256 | cut -d' ' -f1
|
||||
else echo "$1" | jq -S -cj 'del(.content_hash)' | cksum | cut -d' ' -f1; fi
|
||||
}
|
||||
atomic_write_state() { # path json
|
||||
local path="$1" json="$2" h tmp
|
||||
h="$(state_hash "$json")"
|
||||
json="$(echo "$json" | jq -c --arg h "$h" '.content_hash=$h')"
|
||||
tmp="$(mktemp "${path}.XXXXXX")"
|
||||
printf '%s\n' "$json" > "$tmp"
|
||||
chmod 600 "$tmp" 2>/dev/null || true
|
||||
mv -f "$tmp" "$path" # rename is atomic on the same filesystem
|
||||
}
|
||||
# Verify integrity; echo "ok" or a reason. schema + hash + logical-consistency.
|
||||
verify_state() { # path expected_schema -> "ok" | reason
|
||||
local path="$1" want="$2" json sv stored calc
|
||||
json="$(cat "$path" 2>/dev/null)" || { echo "unreadable"; return; }
|
||||
echo "$json" | jq -e 'type=="object"' >/dev/null 2>&1 || { echo "not-json-object"; return; }
|
||||
sv="$(echo "$json" | jq -r '.schema_version // empty')"
|
||||
[ "$sv" = "$want" ] || { echo "schema-mismatch(got=${sv:-none} want=$want)"; return; }
|
||||
stored="$(echo "$json" | jq -r '.content_hash // empty')"
|
||||
[ -n "$stored" ] || { echo "missing-content-hash"; return; }
|
||||
calc="$(state_hash "$json")"
|
||||
[ "$stored" = "$calc" ] || { echo "content-hash-mismatch"; return; }
|
||||
echo "ok"
|
||||
}
|
||||
|
||||
declare -a STATE_ALARMS=()
|
||||
|
||||
# --- Budget ledger: {schema_version, day, spend, content_hash}. New UTC day resets spend. ----
|
||||
TOTAL_SPEND="0"
|
||||
load_budget_ledger() {
|
||||
if [ -f "$BUDGET_LEDGER" ]; then
|
||||
local v; v="$(verify_state "$BUDGET_LEDGER" "$SCHEMA_VERSION")"
|
||||
if [ "$v" != "ok" ]; then
|
||||
STATE_ALARMS+=( "*STATE ALARM*: budget ledger corrupt ($v) — rebuilt for $UTC_DATE (rebuildable; a new UTC day resets spend)." )
|
||||
log "budget ledger integrity FAIL: $v — rebuilding (park-on-corrupt, design §6.7)"
|
||||
TOTAL_SPEND="0"
|
||||
else
|
||||
local day; day="$(jq -r '.day // empty' "$BUDGET_LEDGER")"
|
||||
if [ "$day" = "$UTC_DATE" ]; then TOTAL_SPEND="$(jq -r '.spend // 0' "$BUDGET_LEDGER")"
|
||||
else log "budget ledger from $day — new UTC day, resetting day spend"; TOTAL_SPEND="0"; fi
|
||||
fi
|
||||
fi
|
||||
log "budget: shared cap \$$TOTAL_BUDGET_USD, day spend so far \$$TOTAL_SPEND ($UTC_DATE)"
|
||||
}
|
||||
save_budget_ledger() {
|
||||
atomic_write_state "$BUDGET_LEDGER" \
|
||||
"$(jq -n --argjson sv "$SCHEMA_VERSION" --arg day "$UTC_DATE" --argjson sp "$TOTAL_SPEND" \
|
||||
'{schema_version:$sv, day:$day, spend:$sp}')"
|
||||
}
|
||||
|
||||
# --- Coordinator state: {schema_version, cycle_start, last_run:{role:date}, deferred:[], content_hash} ---
|
||||
declare -A LAST_RUN=(); declare -a DEFERRED=(); CYCLE_START="$UTC_DATE"
|
||||
load_coord_state() {
|
||||
if [ -f "$COORD_STATE" ]; then
|
||||
local v; v="$(verify_state "$COORD_STATE" "$SCHEMA_VERSION")"
|
||||
if [ "$v" != "ok" ]; then
|
||||
STATE_ALARMS+=( "*STATE ALARM*: coordinator state corrupt ($v) — rebuilt (rebuildable from report history; rotation restarts)." )
|
||||
log "coordinator state integrity FAIL: $v — rebuilding (park-on-corrupt, design §6.7)"
|
||||
return
|
||||
fi
|
||||
CYCLE_START="$(jq -r '.cycle_start // empty' "$COORD_STATE")"; [ -n "$CYCLE_START" ] || CYCLE_START="$UTC_DATE"
|
||||
while IFS=$'\t' read -r role date; do [ -n "$role" ] && LAST_RUN["$role"]="$date"; done \
|
||||
< <(jq -r '(.last_run // {}) | to_entries[] | "\(.key)\t\(.value)"' "$COORD_STATE")
|
||||
while IFS= read -r role; do [ -n "$role" ] && DEFERRED+=( "$role" ); done \
|
||||
< <(jq -r '(.deferred // [])[]' "$COORD_STATE")
|
||||
fi
|
||||
}
|
||||
save_coord_state() {
|
||||
local lr="{}"
|
||||
for role in "${!LAST_RUN[@]}"; do
|
||||
lr="$(echo "$lr" | jq -c --arg k "$role" --arg v "${LAST_RUN[$role]}" '.[$k]=$v')"
|
||||
done
|
||||
local df="[]"
|
||||
if [ "${#DEFERRED[@]}" -gt 0 ]; then df="$(printf '%s\n' "${DEFERRED[@]}" | jq -R . | jq -cs 'unique')"; fi
|
||||
atomic_write_state "$COORD_STATE" \
|
||||
"$(jq -n --argjson sv "$SCHEMA_VERSION" --arg cs "$CYCLE_START" --argjson lr "$lr" --argjson df "$df" \
|
||||
'{schema_version:$sv, cycle_start:$cs, last_run:$lr, deferred:$df}')"
|
||||
}
|
||||
|
||||
load_budget_ledger
|
||||
load_coord_state
|
||||
|
||||
# In the squeeze test, backdate cycle_start + a role's last_run so the COVERAGE ALARM trips
|
||||
# deterministically (simulate enough elapsed cycles). This proves the COVERAGE path without
|
||||
# waiting MAX_CYCLE_NIGHTS real days.
|
||||
if [ "$SQUEEZE" -eq 1 ]; then
|
||||
OLD_DATE="$(to_epoch "$UTC_DATE")"; OLD_DATE=$(( OLD_DATE - (MAX_CYCLE_NIGHTS + 2) * 86400 ))
|
||||
# portable epoch -> YYYY-MM-DD
|
||||
OLD_DATE_STR="$(date -u -d "@$OLD_DATE" +%Y-%m-%d 2>/dev/null || date -u -r "$OLD_DATE" +%Y-%m-%d 2>/dev/null || echo "$UTC_DATE")"
|
||||
CYCLE_START="$OLD_DATE_STR"
|
||||
LAST_RUN["dependency-cve"]="$OLD_DATE_STR" # this role has not run in > MAX_CYCLE_NIGHTS
|
||||
log "SQUEEZE: backdated cycle_start + dependency-cve last_run to $OLD_DATE_STR (> ${MAX_CYCLE_NIGHTS}d) to trip COVERAGE"
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# 1) CANARY SUITE FIRST — each role's checker --canary; a miss = COMPLACENCY ALARM + skip.
|
||||
# ==============================================================================
|
||||
declare -a ALARM_LINES=(); declare -A CANARY_OK=()
|
||||
for entry in "${ROLES[@]}"; do
|
||||
role="$(role_field "$entry" 1)"; script="$(role_field "$entry" 2)"
|
||||
if [ ! -x "$script" ] && [ ! -f "$script" ]; then
|
||||
CANARY_OK["$role"]=0
|
||||
ALARM_LINES+=( "*COMPLACENCY ALARM*: role '$role' checker missing ($script) — skipped." )
|
||||
continue
|
||||
fi
|
||||
set +e
|
||||
bash "$script" --canary >"$REPORT_DIR/$role.canary.log" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
CANARY_OK["$role"]=1; log "canary PASS: $role"
|
||||
else
|
||||
CANARY_OK["$role"]=0
|
||||
ALARM_LINES+=( "*COMPLACENCY ALARM*: role '$role' canary FAILED (rc=$rc) — skipped this run. See \`$REPORT_DIR/$role.canary.log\`." )
|
||||
log "canary FAIL: $role (rc=$rc) — will SKIP this role"
|
||||
fi
|
||||
done
|
||||
|
||||
# --canary mode: assert every role's canary passed, then stop (offline; post nothing).
|
||||
if [ "$CANARY" -eq 1 ]; then
|
||||
fail=0
|
||||
for entry in "${ROLES[@]}"; do
|
||||
role="$(role_field "$entry" 1)"
|
||||
[ "${CANARY_OK[$role]:-0}" -eq 1 ] || { echo "[coordinator] CANARY FAIL: role '$role' did not pass" >&2; fail=1; }
|
||||
done
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "[coordinator] CANARY SUITE FAILED — at least one role's canary did not pass." >&2
|
||||
exit 3
|
||||
fi
|
||||
log "canary suite PASS: all ${#ROLES[@]} role(s) green."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# 2) FAN-OUT under the SHARED cap. Order: DEFERRED roles first, then by rotation
|
||||
# (oldest last_run first). A role whose est cost would exceed the ceiling is DEFERRED
|
||||
# (recorded), never dropped. A degraded (canary-failed) role is skipped.
|
||||
# ==============================================================================
|
||||
# Build the run order: deferred-first, then never-run, then oldest-last_run.
|
||||
order_roles() {
|
||||
local entry role lr key
|
||||
for entry in "${ROLES[@]}"; do
|
||||
role="$(role_field "$entry" 1)"
|
||||
# is it currently deferred?
|
||||
if printf '%s\n' ${DEFERRED[@]+"${DEFERRED[@]}"} | grep -qxF "$role"; then
|
||||
echo "0000000000|$role"; continue
|
||||
fi
|
||||
lr="${LAST_RUN[$role]:-}"
|
||||
if [ -z "$lr" ]; then key="0000000001"; else key="$(to_epoch "$lr")"; fi
|
||||
echo "$key|$role"
|
||||
done | sort -n | cut -d'|' -f2
|
||||
}
|
||||
|
||||
declare -a NEW_DEFERRED=(); declare -a RAN_ROLES=()
|
||||
declare -a RUN_REPORT_JSONS=()
|
||||
while IFS= read -r role; do
|
||||
[ -n "$role" ] || continue
|
||||
# find the registry entry
|
||||
entry=""; for e in "${ROLES[@]}"; do [ "$(role_field "$e" 1)" = "$role" ] && entry="$e"; done
|
||||
[ -n "$entry" ] || continue
|
||||
script="$(role_field "$entry" 2)"; cost="$(role_field "$entry" 3)"
|
||||
|
||||
# Skip degraded roles (canary failed) — never run silently degraded.
|
||||
if [ "${CANARY_OK[$role]:-0}" -ne 1 ]; then
|
||||
log "skip $role: canary not green (already alarmed)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Budget headroom check: would this role's est cost push us over the SHARED ceiling?
|
||||
projected="$(jq -n --argjson s "$TOTAL_SPEND" --argjson c "$cost" '$s + $c')"
|
||||
if jq -n --argjson p "$projected" --argjson cap "$TOTAL_BUDGET_USD" -e '$cap > 0 and $p > $cap' >/dev/null 2>&1; then
|
||||
NEW_DEFERRED+=( "$role" )
|
||||
log "DEFER $role: est \$$cost would exceed shared cap \$$TOTAL_BUDGET_USD (spend \$$TOTAL_SPEND) — DEFERRED, not dropped"
|
||||
ALARM_LINES+=( "*$role* DEFERRED: est \$$cost over shared cap \$$TOTAL_BUDGET_USD (day spend \$$TOTAL_SPEND). Will run next eligible night." )
|
||||
continue
|
||||
fi
|
||||
|
||||
# Run the checker in --dry-run (the coordinator owns routing; checkers must not post).
|
||||
# In SQUEEZE mode (synthetic acceptance test, may run on a box without $MIRROR_DIR) point the
|
||||
# checker at its own fixture via --targets so the "ran" role succeeds deterministically; this
|
||||
# keeps the deferral/COVERAGE proof self-contained. Normal runs use the real mirror set.
|
||||
log "--- run role: $role (est \$$cost) ---"
|
||||
set +e
|
||||
if [ "$SQUEEZE" -eq 1 ]; then
|
||||
bash "$script" --dry-run --no-api --targets "$CHECKERS_DIR/fixtures/$role/clean-repo" \
|
||||
>"$REPORT_DIR/$role.run.log" 2>&1
|
||||
else
|
||||
bash "$script" --dry-run >"$REPORT_DIR/$role.run.log" 2>&1
|
||||
fi
|
||||
rc=$?
|
||||
set -e
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
ALARM_LINES+=( "*$role*: checker run error (rc=$rc). See \`$REPORT_DIR/$role.run.log\`." )
|
||||
log "$role run error rc=$rc (logged) — NOT collecting its report (avoid stale/partial findings)"
|
||||
else
|
||||
# Collect the checker's own report JSON (REPORT_ROOT/<role>/<date>/<role>.json) only on a
|
||||
# clean run — a failed run could leave a stale report from an earlier (e.g. canary) pass,
|
||||
# and folding that in would misattribute findings.
|
||||
src="$REPORT_ROOT/$role/$UTC_DATE/$role.json"
|
||||
if [ -f "$src" ]; then rj="$REPORT_DIR/$role.json"; cp -f "$src" "$rj"; RUN_REPORT_JSONS+=( "$rj" ); fi
|
||||
fi
|
||||
|
||||
# Account spend, record last_run, drop from deferred.
|
||||
add_spend "$cost"
|
||||
LAST_RUN["$role"]="$UTC_DATE"
|
||||
RAN_ROLES+=( "$role" )
|
||||
done < <(order_roles)
|
||||
|
||||
# New deferral set = roles deferred this run, plus any previously-deferred role we did NOT run.
|
||||
for role in ${DEFERRED[@]+"${DEFERRED[@]}"}; do
|
||||
printf '%s\n' ${RAN_ROLES[@]+"${RAN_ROLES[@]}"} | grep -qxF "$role" && continue
|
||||
printf '%s\n' ${NEW_DEFERRED[@]+"${NEW_DEFERRED[@]}"} | grep -qxF "$role" && continue
|
||||
NEW_DEFERRED+=( "$role" )
|
||||
done
|
||||
DEFERRED=( ${NEW_DEFERRED[@]+"${NEW_DEFERRED[@]}"} )
|
||||
|
||||
log "ran: ${RAN_ROLES[*]:-none} | deferred: ${DEFERRED[*]:-none} | day spend \$$TOTAL_SPEND/\$$TOTAL_BUDGET_USD"
|
||||
|
||||
# ==============================================================================
|
||||
# 3) COVERAGE ALARM — any role whose last_run is older than MAX_CYCLE_NIGHTS days
|
||||
# (or never run and deferred that long) is behind (design §5).
|
||||
# ==============================================================================
|
||||
NOW_EPOCH="$(to_epoch "$UTC_DATE")"
|
||||
for entry in "${ROLES[@]}"; do
|
||||
role="$(role_field "$entry" 1)"
|
||||
lr="${LAST_RUN[$role]:-}"
|
||||
if [ -z "$lr" ]; then ref="$CYCLE_START"; else ref="$lr"; fi
|
||||
age=$(( ( NOW_EPOCH - $(to_epoch "$ref") ) / 86400 ))
|
||||
if [ "$age" -ge "$MAX_CYCLE_NIGHTS" ]; then
|
||||
ALARM_LINES+=( "*COVERAGE ALARM*: role '$role' not run in ${age}d (last=${lr:-never, cycle since $CYCLE_START}, max $MAX_CYCLE_NIGHTS). Deferred=$(printf '%s\n' ${DEFERRED[@]+"${DEFERRED[@]}"} | grep -qxF "$role" && echo yes || echo no). Raise budget or check failures." )
|
||||
log "COVERAGE ALARM: $role age ${age}d >= $MAX_CYCLE_NIGHTS"
|
||||
fi
|
||||
done
|
||||
|
||||
# Persist state (atomic + hashed). Even in dry-run we persist so rotation advances; the
|
||||
# squeeze test runs dry, so guard: in SQUEEZE we do NOT persist (it is a synthetic scenario).
|
||||
if [ "$SQUEEZE" -eq 0 ]; then
|
||||
save_budget_ledger
|
||||
save_coord_state
|
||||
else
|
||||
log "SQUEEZE: synthetic scenario — NOT persisting state."
|
||||
fi
|
||||
|
||||
# Fold any state-integrity alarms in.
|
||||
for x in ${STATE_ALARMS[@]+"${STATE_ALARMS[@]}"}; do ALARM_LINES+=( "$x" ); done
|
||||
|
||||
# ==============================================================================
|
||||
# 4) COLLECT + DEDUP + PRIORITIZE across the run checkers' reports.
|
||||
# DEDUP rule: same (repo + check + title) OR identical finding id -> one. Sort by severity.
|
||||
# ==============================================================================
|
||||
ALL_FINDINGS="[]"
|
||||
if [ "${#RUN_REPORT_JSONS[@]}" -gt 0 ]; then
|
||||
ALL_FINDINGS="$(jq -s '
|
||||
[ .[].findings[]? ]
|
||||
| unique_by(.id) # identical id -> one
|
||||
| unique_by([.repo, .check, .title]) # same repo+check+title -> one
|
||||
| sort_by( {critical:0, high:1, medium:2, low:3, info:4, unverified:5}[.severity] // 6 )
|
||||
' "${RUN_REPORT_JSONS[@]}" 2>/dev/null || echo '[]')"
|
||||
fi
|
||||
N_FIND="$(echo "$ALL_FINDINGS" | jq 'length')"
|
||||
N_CRITHIGH="$(echo "$ALL_FINDINGS" | jq '[.[]|select(.severity=="critical" or .severity=="high")] | length')"
|
||||
declare -a CRITHIGH_LINES=()
|
||||
while IFS= read -r line; do [ -n "$line" ] && CRITHIGH_LINES+=( "$line" ); done < <(
|
||||
echo "$ALL_FINDINGS" | jq -r '.[] | select(.severity=="critical" or .severity=="high")
|
||||
| "*\(.repo)* [\(.severity)] \(.title)"')
|
||||
|
||||
# ==============================================================================
|
||||
# 5) ASSEMBLE the combined coordinator report (JSON + text), mode 600.
|
||||
# ==============================================================================
|
||||
DEFERRED_JSON="[]"; [ "${#DEFERRED[@]}" -gt 0 ] && DEFERRED_JSON="$(printf '%s\n' "${DEFERRED[@]}" | jq -R . | jq -cs .)"
|
||||
RAN_JSON="[]"; [ "${#RAN_ROLES[@]}" -gt 0 ] && RAN_JSON="$(printf '%s\n' "${RAN_ROLES[@]}" | jq -R . | jq -cs .)"
|
||||
ALARMS_JSON="[]"; [ "${#ALARM_LINES[@]}" -gt 0 ] && ALARMS_JSON="$(printf '%s\n' "${ALARM_LINES[@]}" | jq -R . | jq -cs .)"
|
||||
|
||||
jq -n \
|
||||
--arg ts "$UTC_STAMP" --arg org "$GH_ORG" \
|
||||
--argjson cap "$TOTAL_BUDGET_USD" --argjson spend "$TOTAL_SPEND" \
|
||||
--argjson ran "$RAN_JSON" --argjson deferred "$DEFERRED_JSON" \
|
||||
--argjson findings "$ALL_FINDINGS" --argjson alarms "$ALARMS_JSON" \
|
||||
'{coordinator:"plane1", generated:$ts, org:$org,
|
||||
shared_budget_usd:$cap, day_spend_usd:$spend,
|
||||
ran_roles:$ran, deferred_roles:$deferred,
|
||||
finding_count:($findings|length),
|
||||
crit_high:([$findings[]|select(.severity=="critical" or .severity=="high")]|length),
|
||||
findings:$findings, alarms:$alarms}' > "$REPORT_JSON"
|
||||
|
||||
{
|
||||
echo "plane-1 coordinator report — $UTC_STAMP"
|
||||
echo "org=$GH_ORG shared_cap=\$$TOTAL_BUDGET_USD day_spend=\$$TOTAL_SPEND"
|
||||
echo "ran: ${RAN_ROLES[*]:-none}"
|
||||
echo "deferred (NOT dropped): ${DEFERRED[*]:-none}"
|
||||
echo "findings: $N_FIND ($N_CRITHIGH crit/high)"
|
||||
echo
|
||||
echo "$ALL_FINDINGS" | jq -r '.[] | "• [\(.severity)] \(.repo): \(.title)"'
|
||||
if [ "${#ALARM_LINES[@]}" -gt 0 ]; then
|
||||
echo; echo "alarms:"; printf ' - %s\n' "${ALARM_LINES[@]}"
|
||||
fi
|
||||
} > "$REPORT_TXT"
|
||||
chmod 600 "$REPORT_JSON" "$REPORT_TXT" 2>/dev/null || true
|
||||
log "report: $REPORT_JSON ($N_FIND finding(s), ${#ALARM_LINES[@]} alarm line(s))"
|
||||
|
||||
# ==============================================================================
|
||||
# 6) ROUTE (ALARM-only, D3): confirmed crit/high OR any alarm line -> Slack ALARM;
|
||||
# everything else -> the mode-600 report only; a fully clean run posts NOTHING.
|
||||
# ==============================================================================
|
||||
ALARM=0
|
||||
[ "$N_CRITHIGH" -gt 0 ] && ALARM=1
|
||||
[ "${#ALARM_LINES[@]}" -gt 0 ] && ALARM=1
|
||||
|
||||
# Squeeze acceptance: print the proof lines explicitly to stdout.
|
||||
if [ "$SQUEEZE" -eq 1 ]; then
|
||||
echo "=== SQUEEZE ACCEPTANCE (Phase-2) ==="
|
||||
echo "DEFERRED (not dropped): ${DEFERRED[*]:-none}"
|
||||
printf '%s\n' ${ALARM_LINES[@]+"${ALARM_LINES[@]}"} | grep -E 'COVERAGE ALARM|DEFERRED' || true
|
||||
echo "===================================="
|
||||
fi
|
||||
|
||||
if [ "$ALARM" -ne 1 ]; then
|
||||
log "clean run — no crit/high findings, no alarm conditions. Posting NOTHING (ALARM-only policy)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ALARM_BODY=""
|
||||
[ "${#CRITHIGH_LINES[@]}" -gt 0 ] && ALARM_BODY="$(printf '%s\n' "${CRITHIGH_LINES[@]}" | sed 's/^/• /')"
|
||||
META_BODY="$(printf '%s\n' ${ALARM_LINES[@]+"${ALARM_LINES[@]}"} | sed 's/^/• /')"
|
||||
SLACK_TEXT=":satellite_antenna: *Sea Haven Plane-1 coordinator — ALARM* ($UTC_STAMP)
|
||||
ran: ${RAN_ROLES[*]:-none} · deferred: ${DEFERRED[*]:-none} · spend \$$TOTAL_SPEND/\$$TOTAL_BUDGET_USD
|
||||
$N_CRITHIGH confirmed crit/high finding(s):
|
||||
$ALARM_BODY
|
||||
|
||||
coordination alarms:
|
||||
$META_BODY
|
||||
Combined report (mode 600): \`$REPORT_JSON\` (on R720)"
|
||||
SLACK_TEXT="$(echo "$SLACK_TEXT" | redact)"
|
||||
|
||||
echo "$SLACK_TEXT" >&2
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log "DRY-RUN: alarm composed but NOT posted (routing dry-run, design §7 Phase 2)."
|
||||
exit 0
|
||||
fi
|
||||
post_slack_alarm "$SLACK_TEXT"
|
||||
exit 0
|
||||
|
||||
# ==============================================================================
|
||||
# PROVISIONING (NOT DONE HERE — gated, Phase 6):
|
||||
# - No systemd unit / timer is installed by this script. Wiring it into the live
|
||||
# sea-haven-secrev schedule (or a sibling timer) is provisioning and is gated.
|
||||
# - This coordinator runs ONLY the Plane-1 Tier-1 checkers (compliance-drift,
|
||||
# dependency-cve). doc-drift / aws-posture / planner / fixer are later phases.
|
||||
# - It does NOT re-clone (checkers reuse $MIRROR_DIR); a checker's own --refresh is the
|
||||
# only network path and is not invoked here.
|
||||
# - It does NOT touch agent_team/ or agent-team/, and installs no systemd units.
|
||||
# - Confluence + project_r720_agent_team memory updates are docs-as-you-go obligations.
|
||||
# ==============================================================================
|
||||
587
security-review/checkers/dependency-cve.sh
Executable file
587
security-review/checkers/dependency-cve.sh
Executable file
|
|
@ -0,0 +1,587 @@
|
|||
#!/usr/bin/env bash
|
||||
# dependency-cve.sh — Plane-1 / Tier-1 checker for the R720 agent-team.
|
||||
#
|
||||
# Design refs: docs/r720-agent-team-design.md §4 (Tier 1 roster: dependency-cve —
|
||||
# "Cross-ref lockfiles vs advisories org-wide; report + feed fixer. Complements Dependabot")
|
||||
# and §7 Phase 2 ("coordinator + second checker"). This is the SECOND Plane-1 checker built
|
||||
# on the Phase-0 shared substrate (lib/sweep_substrate.sh); it mirrors compliance-drift.sh's
|
||||
# conventions verbatim so the coordinator (§5) can drive both identically.
|
||||
#
|
||||
# WHAT IT DOES (read-only):
|
||||
# Scans the SAME shallow clean clones nightly_sweep.sh already produced in $MIRROR_DIR — it
|
||||
# does NOT re-clone (mirrors-first; an optional --refresh re-runs discovery+mirror via the
|
||||
# shared substrate). In each mirror it parses dependency lockfiles/manifests with PINNED,
|
||||
# exact versions, extracts (ecosystem, package, version) tuples, and cross-references them
|
||||
# against the OSV advisory database to flag known-vulnerable pinned deps. This complements
|
||||
# Dependabot (design §4): it is org-wide, runs on the server-side mirrors, and feeds the
|
||||
# fixer queue in a later phase.
|
||||
#
|
||||
# Manifests parsed (and the OSV ecosystem each maps to):
|
||||
# requirements.txt -> PyPI (only EXACT '==' pins; ranges/unpinned are skipped)
|
||||
# poetry.lock -> PyPI ([[package]] name/version blocks)
|
||||
# Pipfile.lock -> PyPI (default+develop, "==x.y.z" version strings)
|
||||
# package-lock.json -> npm (packages[].version / dependencies[].version)
|
||||
# yarn.lock -> npm ("pkg@range:\n version \"x\"" stanzas)
|
||||
# packages.lock.json -> NuGet (.dependencies[tfm][pkg].resolved)
|
||||
# *.csproj -> NuGet (<PackageReference Include=.. Version=..>)
|
||||
# Only EXACTLY-pinned versions are cross-referenced (an unpinned/range spec has no single
|
||||
# version to query and is not a confirmed vulnerable artifact — no false alarms on no-data,
|
||||
# memory feedback_cloudwatch_alarms).
|
||||
#
|
||||
# ADVISORY SOURCE (live): OSV batch API POST https://api.osv.dev/v1/querybatch (NO auth token).
|
||||
# Guarded behind a --no-api / offline check exactly like compliance-drift's GitHub-API checks:
|
||||
# on missing curl OR a failed/empty network response, the API lookup is SKIPPED and noted in
|
||||
# the report — a vuln is NEVER reported on missing advisory data. Network calls are minimal
|
||||
# (one batched POST) and fail-safe.
|
||||
#
|
||||
# AGENTIC TIEBREAK (design §4, "Claude + GPT tiebreak"): OPTIONAL and only relevant in LIVE mode
|
||||
# for ambiguous severity. For THIS phase the deterministic OSV core is the whole checker — NO
|
||||
# LLM is invoked in --canary/--dry-run. A clearly-marked inert stub hook (maybe_tiebreak) marks
|
||||
# the future seam; it does nothing offline and nothing in this phase.
|
||||
#
|
||||
# CANARY / DRY-RUN (offline, no network, no token):
|
||||
# --canary runs against a planted fixture (checkers/fixtures/dependency-cve/) and asserts the
|
||||
# known vuln count against EXPECTED_VULN_COUNT (exit 3 on mismatch). Because OSV needs network,
|
||||
# the canary consults a LOCAL offline advisory fixture (fixtures/dependency-cve/osv-advisories.json)
|
||||
# INSTEAD of the network — so it is fully offline + deterministic. --canary implies --dry-run +
|
||||
# --no-api. This is the anti-complacency floor (design §6.4) AND the routing dry-run (§7 Phase 2):
|
||||
# with --dry-run the Slack alarm is composed + printed but NOT POSTed.
|
||||
#
|
||||
# SCOPE / SAFETY:
|
||||
# Read-only. Fixtures ship git metadata as dotgit/ (renamed to .git/ at run time) so they
|
||||
# commit into THIS repo without becoming submodules — the SAME trick compliance-drift uses.
|
||||
# Does NOT touch agent_team/ or agent-team/, and is NOT wired into systemd — that is Phase-6
|
||||
# provisioning (gated). See the "PROVISIONING (NOT DONE HERE)" note at the bottom.
|
||||
#
|
||||
# Exit: 0 = ran (whether or not it alarmed); 2 = setup/usage error; 3 = canary assertion FAILED.
|
||||
set -euo pipefail
|
||||
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
log() { echo "[dependency-cve] $*" >&2; }
|
||||
die() { echo "[dependency-cve] FATAL: $*" >&2; exit 2; }
|
||||
|
||||
# --- Shared substrate ---------------------------------------------------------
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SUBSTRATE="$HERE/../lib/sweep_substrate.sh"
|
||||
[ -f "$SUBSTRATE" ] || die "shared substrate not found: $SUBSTRATE"
|
||||
# shellcheck source=../lib/sweep_substrate.sh
|
||||
. "$SUBSTRATE"
|
||||
|
||||
# --- Config + defaults (env, all optional) ------------------------------------
|
||||
GH_ORG="${GH_ORG:-Sea-Haven-Industries}"
|
||||
MIRROR_DIR="${MIRROR_DIR:-$HOME/repo-mirrors}"
|
||||
REPORT_ROOT="${REPORT_ROOT:-$HOME/sweep-reports/dependency-cve}"
|
||||
OSV_BATCH_URL="${OSV_BATCH_URL:-https://api.osv.dev/v1/querybatch}"
|
||||
|
||||
REFRESH=0 # --refresh: re-run discovery+mirror via substrate (network). Default: reuse mirrors.
|
||||
DO_API=1 # --no-api: skip the OSV advisory lookup (offline). Without it, nothing matches.
|
||||
DRY_RUN=0 # --dry-run: compose the Slack alarm but DO NOT post it (routing dry-run).
|
||||
CANARY=0 # --canary: run against the planted fixture + assert the known vuln count.
|
||||
TARGETS_OVERRIDE="" # --targets "p1 p2": scan explicit dirs instead of the mirror set.
|
||||
ADVISORIES_FILE="" # --advisories-file PATH: consult a local advisory JSON instead of the OSV API.
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
dependency-cve.sh — Plane-1 Tier-1 vulnerable-dependency checker (read-only)
|
||||
|
||||
--canary run against the planted fixture and assert the known vuln count
|
||||
(implies --dry-run + --no-api; uses the OFFLINE advisory fixture)
|
||||
--dry-run compose the Slack alarm but DO NOT post it (routing dry-run)
|
||||
--no-api skip the OSV advisory lookup (offline; nothing can match)
|
||||
--advisories-file P consult a LOCAL advisory JSON at P instead of the OSV network API
|
||||
(offline + deterministic; same file shape as the canary fixture)
|
||||
--refresh re-discover + re-mirror via the shared substrate before scanning (network)
|
||||
--targets "a b" scan these explicit repo dirs instead of \$MIRROR_DIR/* (no clone)
|
||||
-h|--help this help
|
||||
|
||||
Env: GH_ORG MIRROR_DIR REPORT_ROOT GH_TOKEN SLACK_WEBHOOK_URL OSV_BATCH_URL
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--canary) CANARY=1; DRY_RUN=1; DO_API=0 ;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--no-api) DO_API=0 ;;
|
||||
--advisories-file) shift; ADVISORIES_FILE="${1:-}" ;;
|
||||
--refresh) REFRESH=1 ;;
|
||||
--targets) shift; TARGETS_OVERRIDE="${1:-}" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "unknown arg: $1 (see --help)" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
command -v jq >/dev/null || die "jq is required"
|
||||
command -v git >/dev/null || die "git is required"
|
||||
|
||||
# --- Report dir (mode 600 reports; matches sweep conventions) -----------------
|
||||
umask 077
|
||||
UTC_DATE="$(date -u +%Y-%m-%d)"
|
||||
UTC_STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
REPORT_DIR="$REPORT_ROOT/$UTC_DATE"
|
||||
mkdir -p "$REPORT_DIR"; chmod 700 "$REPORT_ROOT" "$REPORT_DIR" 2>/dev/null || true
|
||||
# shellcheck disable=SC2034 # read by the sourced substrate (post_slack_alarm) via dynamic scope
|
||||
SWEEP_LOG="$REPORT_DIR/dependency-cve.log" # name the substrate's post_slack_alarm() references
|
||||
REPORT_JSON="$REPORT_DIR/dependency-cve.json"
|
||||
REPORT_TXT="$REPORT_DIR/dependency-cve.txt"
|
||||
|
||||
log "=== dependency-cve $UTC_STAMP (canary=$CANARY dry_run=$DRY_RUN api=$DO_API refresh=$REFRESH) ==="
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# FINDINGS (spirit of finding.schema.json so the coordinator can route like an agentic
|
||||
# finding). category="other" (a vulnerable-dependency is not one of the schema's security
|
||||
# categories); status="confirmed" only for an exact pinned version that MATCHES an advisory.
|
||||
# A pinned dep with NO advisory match is NOT a finding; an unqueryable/skipped advisory lookup
|
||||
# is NOT a finding (memory feedback_cloudwatch_alarms: no false alarms on missing data).
|
||||
# ------------------------------------------------------------------------------
|
||||
declare -a FINDINGS=()
|
||||
add_finding() { # repo id title severity pkg version advisory_id summary fixed_version
|
||||
local repo="$1" id="$2" title="$3" sev="$4" pkg="$5" ver="$6" adv="$7" summ="$8" fixed="$9"
|
||||
FINDINGS+=( "$(jq -n \
|
||||
--arg repo "$repo" --arg id "$id" --arg title "$title" --arg sev "$sev" \
|
||||
--arg pkg "$pkg" --arg ver "$ver" --arg adv "$adv" --arg summ "$summ" --arg fixed "$fixed" \
|
||||
'{repo:$repo, id:($repo+"-"+$id), title:$title, severity:$sev, category:"other",
|
||||
check:"vulnerable-dependency", status:"confirmed",
|
||||
proof:{package:$pkg, version:$ver, advisory_id:$adv, summary:$summ, fixed_version:$fixed}}')" )
|
||||
}
|
||||
declare -a SKIPPED_CHECKS=() # (repo:reason) lookups skipped on missing data — reported, never alarmed
|
||||
note_skip() { SKIPPED_CHECKS+=( "$1" ); }
|
||||
|
||||
# Severity normalizer: map OSV/GHSA strings + CVSS scores into the schema's enum.
|
||||
norm_sev() { # raw_severity cvss_score -> critical|high|medium|low
|
||||
local raw; raw="$(echo "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
||||
local cvss="${2:-}"
|
||||
case "$raw" in
|
||||
critical) echo critical; return ;;
|
||||
high) echo high; return ;;
|
||||
moderate|medium) echo medium; return ;;
|
||||
low) echo low; return ;;
|
||||
esac
|
||||
# Fall back to CVSS base score banding (NVD/CVSSv3 thresholds).
|
||||
if [ -n "$cvss" ] && [ "$cvss" != "null" ]; then
|
||||
awk -v c="$cvss" 'BEGIN{
|
||||
if (c+0>=9.0) print "critical";
|
||||
else if (c+0>=7.0) print "high";
|
||||
else if (c+0>=4.0) print "medium";
|
||||
else print "low"; }'
|
||||
return
|
||||
fi
|
||||
echo medium # unknown severity: medium (a real match we cannot rank), never dropped
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# MANIFEST PARSERS — each emits "ECOSYSTEM<TAB>package<TAB>version" lines (exact pins only).
|
||||
# Pure text/jq parsing; no project tooling invoked. Unknown/odd lines are skipped silently.
|
||||
# ==============================================================================
|
||||
|
||||
# requirements.txt: only EXACT '==' pins (skip ranges, markers, comments, -e/-r includes, extras).
|
||||
parse_requirements() { # file
|
||||
local f="$1"
|
||||
sed -E 's/[[:space:]]*#.*$//' "$f" 2>/dev/null \
|
||||
| grep -E '==' \
|
||||
| while IFS= read -r line; do
|
||||
line="$(echo "$line" | tr -d '[:space:]')"
|
||||
[ -n "$line" ] || continue
|
||||
case "$line" in -*|.*|git+*|http*) continue ;; esac
|
||||
# strip extras: pkg[extra]==1.2.3 -> pkg
|
||||
local name ver
|
||||
name="$(echo "$line" | sed -E 's/\[[^]]*\].*//; s/[<>=!~;].*$//')"
|
||||
ver="$(echo "$line" | sed -E 's/^[^=]*==//; s/[ ;].*$//')"
|
||||
# only a clean exact version (digits/dots/alnum), no range operators left
|
||||
case "$ver" in *','*|*'<'*|*'>'*|*'*'*|'') continue ;; esac
|
||||
[ -n "$name" ] && [ -n "$ver" ] && printf 'PyPI\t%s\t%s\n' "$name" "$ver"
|
||||
done
|
||||
}
|
||||
|
||||
# poetry.lock: [[package]] blocks with name = "x" / version = "y".
|
||||
parse_poetry_lock() { # file
|
||||
local f="$1"
|
||||
awk '
|
||||
/^\[\[package\]\]/ { name=""; ver=""; next }
|
||||
/^name = / { gsub(/^name = "|"$/,""); name=$0; next }
|
||||
/^version = / { gsub(/^version = "|"$/,""); ver=$0;
|
||||
if (name!="" && ver!="") printf "PyPI\t%s\t%s\n", name, ver; next }
|
||||
' "$f" 2>/dev/null
|
||||
}
|
||||
|
||||
# Pipfile.lock: JSON; default + develop maps; versions look like "==1.2.3".
|
||||
parse_pipfile_lock() { # file
|
||||
local f="$1"
|
||||
jq -r '
|
||||
(.default // {}) * (.develop // {}) | to_entries[]
|
||||
| select(.value.version != null)
|
||||
| .key as $n | (.value.version | sub("^=="; "")) as $v
|
||||
| select($v | test("^[0-9][0-9A-Za-z.+-]*$"))
|
||||
| "PyPI\t\($n)\t\($v)"
|
||||
' "$f" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# package-lock.json: prefer v2/v3 .packages (node_modules/<name> keys), else v1 .dependencies.
|
||||
parse_package_lock() { # file
|
||||
local f="$1"
|
||||
jq -r '
|
||||
if (.packages != null) then
|
||||
(.packages | to_entries[]
|
||||
| select(.key | startswith("node_modules/"))
|
||||
| select(.value.version != null)
|
||||
| (.key | sub("^.*node_modules/"; "")) as $n
|
||||
| "npm\t\($n)\t\(.value.version)")
|
||||
elif (.dependencies != null) then
|
||||
[paths(objects | has("version")) as $p | {n: $p[-1], v: (getpath($p).version)}]
|
||||
| .[] | select(.v != null) | "npm\t\(.n)\t\(.v)"
|
||||
else empty end
|
||||
' "$f" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# yarn.lock: stanzas "spec@range, spec@range:\n version \"x.y.z\"".
|
||||
parse_yarn_lock() { # file
|
||||
local f="$1"
|
||||
awk '
|
||||
/^[^[:space:]#].*:[[:space:]]*$/ {
|
||||
# header line: take first spec, strip trailing colon + quotes, derive package name
|
||||
hdr=$0; sub(/:[[:space:]]*$/,"",hdr);
|
||||
split(hdr, specs, ", "); first=specs[1]; gsub(/"/,"",first);
|
||||
# package name = everything before the LAST @ (handles @scope/pkg@range)
|
||||
at=0; for (i=2;i<=length(first);i++){ if (substr(first,i,1)=="@") at=i }
|
||||
pkg=(at>1)? substr(first,1,at-1) : first;
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+version / {
|
||||
v=$0; gsub(/^[[:space:]]+version[[:space:]]+"?|"?[[:space:]]*$/,"",v);
|
||||
if (pkg!="" && v!="") printf "npm\t%s\t%s\n", pkg, v;
|
||||
pkg=""; next
|
||||
}
|
||||
' "$f" 2>/dev/null
|
||||
}
|
||||
|
||||
# packages.lock.json (NuGet): .dependencies[tfm][pkg].resolved.
|
||||
parse_packages_lock() { # file
|
||||
local f="$1"
|
||||
jq -r '
|
||||
(.dependencies // {}) | to_entries[] | .value | to_entries[]
|
||||
| select(.value.resolved != null)
|
||||
| "NuGet\t\(.key)\t\(.value.resolved)"
|
||||
' "$f" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# *.csproj (NuGet): <PackageReference Include="X" Version="Y" />.
|
||||
parse_csproj() { # file
|
||||
local f="$1"
|
||||
grep -oE '<PackageReference[^>]*>' "$f" 2>/dev/null \
|
||||
| while IFS= read -r tag; do
|
||||
local inc ver
|
||||
inc="$(echo "$tag" | sed -nE 's/.*Include="([^"]+)".*/\1/p')"
|
||||
ver="$(echo "$tag" | sed -nE 's/.*Version="([^"]+)".*/\1/p')"
|
||||
# only exact versions (no range brackets/commas/wildcards)
|
||||
case "$ver" in ''|*'['*|*']'*|*'('*|*')'*|*','*|*'*'*) continue ;; esac
|
||||
[ -n "$inc" ] && [ -n "$ver" ] && printf 'NuGet\t%s\t%s\n' "$inc" "$ver"
|
||||
done
|
||||
}
|
||||
|
||||
# Extract ALL (ecosystem, package, version) tuples from one repo dir. Dedup at the end.
|
||||
extract_deps() { # repo_dir -> TSV "ECOSYSTEM\tpackage\tversion" on stdout
|
||||
local dir="$1" f
|
||||
# requirements.txt (any depth, excluding .git)
|
||||
while IFS= read -r f; do [ -n "$f" ] && parse_requirements "$f"; done \
|
||||
< <(find "$dir" -maxdepth 4 -name requirements.txt -not -path '*/.git/*' 2>/dev/null)
|
||||
while IFS= read -r f; do [ -n "$f" ] && parse_poetry_lock "$f"; done \
|
||||
< <(find "$dir" -maxdepth 4 -name poetry.lock -not -path '*/.git/*' 2>/dev/null)
|
||||
while IFS= read -r f; do [ -n "$f" ] && parse_pipfile_lock "$f"; done \
|
||||
< <(find "$dir" -maxdepth 4 -name Pipfile.lock -not -path '*/.git/*' 2>/dev/null)
|
||||
while IFS= read -r f; do [ -n "$f" ] && parse_package_lock "$f"; done \
|
||||
< <(find "$dir" -maxdepth 4 -name package-lock.json -not -path '*/.git/*' 2>/dev/null)
|
||||
while IFS= read -r f; do [ -n "$f" ] && parse_yarn_lock "$f"; done \
|
||||
< <(find "$dir" -maxdepth 4 -name yarn.lock -not -path '*/.git/*' 2>/dev/null)
|
||||
while IFS= read -r f; do [ -n "$f" ] && parse_packages_lock "$f"; done \
|
||||
< <(find "$dir" -maxdepth 4 -name packages.lock.json -not -path '*/.git/*' 2>/dev/null)
|
||||
while IFS= read -r f; do [ -n "$f" ] && parse_csproj "$f"; done \
|
||||
< <(find "$dir" -maxdepth 4 -name '*.csproj' -not -path '*/.git/*' 2>/dev/null)
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# ADVISORY LOOKUP
|
||||
# ==============================================================================
|
||||
# OFFLINE: consult a local advisory file (the canary fixture, or --advisories-file). Keyed by
|
||||
# "ECOSYSTEM|package|version" -> array of {id,summary,severity,cvss,fixed_version}. Deterministic.
|
||||
lookup_offline() { # advisories_file ecosystem package version -> advisory JSON array (or [])
|
||||
local af="$1" eco="$2" pkg="$3" ver="$4"
|
||||
jq -c --arg k "$eco|$pkg|$ver" '(.advisories[$k] // [])' "$af" 2>/dev/null || echo '[]'
|
||||
}
|
||||
|
||||
# LIVE: one batched POST to the OSV querybatch API (no token). Returns one results[] per query
|
||||
# in input order. Fail-safe: on missing curl, transport failure, or a non-array body, returns ""
|
||||
# (the caller then SKIPS — never alarms on missing advisory data).
|
||||
osv_querybatch() { # queries_json (array of {package:{ecosystem,name},version}) -> results JSON or ""
|
||||
local queries="$1"
|
||||
command -v curl >/dev/null || { return 1; }
|
||||
local body
|
||||
body="$(curl -fsS -X POST -H 'Content-Type: application/json' \
|
||||
--max-time 30 \
|
||||
--data "$(jq -n --argjson q "$queries" '{queries:$q}')" \
|
||||
"$OSV_BATCH_URL" 2>>"$REPORT_DIR/osv.log")" || return 1
|
||||
echo "$body" | jq -e '.results | type=="array"' >/dev/null 2>&1 || return 1
|
||||
echo "$body"
|
||||
}
|
||||
|
||||
# Inert future seam (design §4 "Claude + GPT tiebreak"): in LIVE mode, an ambiguous-severity
|
||||
# advisory could be escalated to a cross-family judge. This phase keeps the deterministic core
|
||||
# ONLY — the stub does nothing and is never reached offline / in canary / dry-run.
|
||||
maybe_tiebreak() { # advisory_json (no-op stub; phase-2 intentionally inert)
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# TARGET RESOLUTION
|
||||
# ==============================================================================
|
||||
declare -a REPO_NAMES=(); declare -A REPO_DIR=()
|
||||
|
||||
if [ "$CANARY" -eq 1 ]; then
|
||||
FIXTURE_ROOT="$HERE/fixtures/dependency-cve"
|
||||
[ -d "$FIXTURE_ROOT" ] || die "canary fixture missing: $FIXTURE_ROOT"
|
||||
# The canary is OFFLINE: it consults the planted advisory fixture instead of the OSV network,
|
||||
# unless an explicit --advisories-file override was given.
|
||||
[ -n "$ADVISORIES_FILE" ] || ADVISORIES_FILE="$FIXTURE_ROOT/osv-advisories.json"
|
||||
[ -f "$ADVISORIES_FILE" ] || die "canary advisory fixture missing: $ADVISORIES_FILE"
|
||||
# Fixtures ship git metadata as dotgit/ (not .git/) so they are committable into THIS repo
|
||||
# without becoming nested submodules. Materialize: copy + rename dotgit -> .git into a mode-700
|
||||
# temp area removed on exit (same trick as compliance-drift.sh).
|
||||
FIXTURE_WORK="$(mktemp -d "${TMPDIR:-/tmp}/dependency-cve-canary.XXXXXX")"
|
||||
trap 'rm -rf "$FIXTURE_WORK"' EXIT
|
||||
log "canary: materializing planted fixtures from $FIXTURE_ROOT into $FIXTURE_WORK"
|
||||
for d in "$FIXTURE_ROOT"/*/; do
|
||||
[ -d "$d/dotgit" ] || continue # only fixture repos (skip README.md, *.json etc.)
|
||||
nm="$(basename "$d")"
|
||||
cp -R "$d" "$FIXTURE_WORK/$nm"
|
||||
mv "$FIXTURE_WORK/$nm/dotgit" "$FIXTURE_WORK/$nm/.git"
|
||||
# Manifests are stored as <name>.fixture so GitHub's dependency graph / the
|
||||
# dependency-review CI action does NOT parse the deliberately-vulnerable canary
|
||||
# pins as real project dependencies. Restore their real names in the materialized
|
||||
# work area so the checker's per-ecosystem parsers dispatch correctly (same
|
||||
# committable-without-side-effects rationale as the dotgit/ rename above).
|
||||
while IFS= read -r ff; do
|
||||
[ -n "$ff" ] && mv "$ff" "${ff%.fixture}"
|
||||
done < <(find "$FIXTURE_WORK/$nm" -name '*.fixture' -not -path '*/.git/*' 2>/dev/null)
|
||||
REPO_NAMES+=( "$nm" ); REPO_DIR["$nm"]="$FIXTURE_WORK/$nm"
|
||||
done
|
||||
elif [ -n "$TARGETS_OVERRIDE" ]; then
|
||||
# shellcheck disable=SC2206 # intentional word-split of the space-separated --targets list
|
||||
arr=( $TARGETS_OVERRIDE )
|
||||
for p in "${arr[@]}"; do p="${p/#\~/$HOME}"; nm="$(basename "$p")"; REPO_NAMES+=( "$nm" ); REPO_DIR["$nm"]="$p"; done
|
||||
log "explicit targets: ${REPO_NAMES[*]}"
|
||||
else
|
||||
if [ "$REFRESH" -eq 1 ]; then
|
||||
[ -n "${GH_TOKEN:-}" ] || die "--refresh needs GH_TOKEN"
|
||||
command -v curl >/dev/null || die "--refresh needs curl"
|
||||
mkdir -p "$MIRROR_DIR"
|
||||
log "refresh: re-discovering + mirroring via shared substrate (no separate clone path)"
|
||||
DISCOVERED="$REPORT_DIR/discovered.tsv"
|
||||
if discover_repos > "$DISCOVERED" 2>>"$REPORT_DIR/discover.log" && [ -s "$DISCOVERED" ]; then
|
||||
while IFS=$'\t' read -r name url branch; do
|
||||
[ -n "$name" ] || continue
|
||||
mirror_repo "$name" "$url" "$branch" || log " mirror FAILED: $name (will use stale mirror if present)"
|
||||
done < "$DISCOVERED"
|
||||
else
|
||||
log "discovery failed — falling back to existing mirrors (coverage may be stale)"
|
||||
fi
|
||||
fi
|
||||
# Default + post-refresh: enumerate EXISTING mirrors. No clone here — reuse the sweep's clones.
|
||||
[ -d "$MIRROR_DIR" ] || die "mirror dir not found: $MIRROR_DIR (run nightly_sweep.sh first, or use --refresh/--targets)"
|
||||
for d in "$MIRROR_DIR"/*/; do
|
||||
[ -d "$d/.git" ] || continue
|
||||
nm="$(basename "$d")"; REPO_NAMES+=( "$nm" ); REPO_DIR["$nm"]="${d%/}"
|
||||
done
|
||||
log "reusing ${#REPO_NAMES[@]} existing mirror(s) in $MIRROR_DIR (no re-clone)"
|
||||
fi
|
||||
|
||||
[ "${#REPO_NAMES[@]}" -gt 0 ] || die "no repos to scan"
|
||||
|
||||
# Decide HOW advisories are looked up: offline file, or the live OSV API, or skip entirely.
|
||||
# An explicit --advisories-file always wins (offline + deterministic, even without --canary).
|
||||
ADV_MODE="none"
|
||||
if [ -n "$ADVISORIES_FILE" ]; then
|
||||
[ -f "$ADVISORIES_FILE" ] || die "advisories file not found: $ADVISORIES_FILE"
|
||||
ADV_MODE="offline"
|
||||
elif [ "$DO_API" -eq 1 ] && command -v curl >/dev/null; then
|
||||
ADV_MODE="api"
|
||||
elif [ "$DO_API" -eq 1 ]; then
|
||||
log "OSV lookup requested but curl unavailable — skipping advisory match (no false alarms on missing data)"
|
||||
fi
|
||||
log "advisory mode: $ADV_MODE"
|
||||
|
||||
# ==============================================================================
|
||||
# RUN: extract deps per repo, then cross-reference against advisories
|
||||
# ==============================================================================
|
||||
for nm in "${REPO_NAMES[@]}"; do
|
||||
dir="${REPO_DIR[$nm]}"
|
||||
# Unique (ecosystem, package, version) tuples for this repo.
|
||||
deps_tsv="$(extract_deps "$dir" | sort -u || true)"
|
||||
ndeps=0; [ -n "$deps_tsv" ] && ndeps="$(printf '%s\n' "$deps_tsv" | grep -c . || true)"
|
||||
log " [$nm] extracted $ndeps pinned dependency tuple(s)"
|
||||
[ "$ndeps" -gt 0 ] || { note_skip "$nm:no-pinned-deps"; continue; }
|
||||
|
||||
if [ "$ADV_MODE" = "none" ]; then
|
||||
note_skip "$nm:advisory-lookup-skipped(offline/no-curl)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$ADV_MODE" = "offline" ]; then
|
||||
# Deterministic local lookup, one tuple at a time.
|
||||
while IFS=$'\t' read -r eco pkg ver; do
|
||||
[ -n "$pkg" ] || continue
|
||||
advs="$(lookup_offline "$ADVISORIES_FILE" "$eco" "$pkg" "$ver")"
|
||||
cnt="$(echo "$advs" | jq 'length' 2>/dev/null || echo 0)"
|
||||
[ "${cnt:-0}" -gt 0 ] || continue
|
||||
i=0
|
||||
while [ "$i" -lt "$cnt" ]; do
|
||||
adv="$(echo "$advs" | jq -c --argjson i "$i" '.[$i]')"
|
||||
aid="$(echo "$adv" | jq -r '.id // "UNKNOWN"')"
|
||||
summ="$(echo "$adv" | jq -r '.summary // ""')"
|
||||
rawsev="$(echo "$adv"| jq -r '.severity // ""')"
|
||||
cvss="$(echo "$adv" | jq -r '.cvss // empty')"
|
||||
fixed="$(echo "$adv" | jq -r '.fixed_version // ""')"
|
||||
sev="$(norm_sev "$rawsev" "$cvss")"
|
||||
maybe_tiebreak "$adv" # inert in this phase
|
||||
add_finding "$nm" "vuln-$(echo "${pkg}-${ver}-${aid}" | tr -c 'A-Za-z0-9-' '-')" \
|
||||
"$pkg $ver is vulnerable ($aid)" "$sev" \
|
||||
"$pkg" "$ver" "$aid" "$summ" "$fixed"
|
||||
i=$((i+1))
|
||||
done
|
||||
done <<< "$deps_tsv"
|
||||
continue
|
||||
fi
|
||||
|
||||
# ADV_MODE = api: build ONE batched OSV query for all this repo's tuples (minimal network).
|
||||
queries="$(printf '%s\n' "$deps_tsv" | jq -R -s '
|
||||
[ split("\n")[] | select(length>0) | split("\t")
|
||||
| {package:{ecosystem:.[0], name:.[1]}, version:.[2]} ]')"
|
||||
# Keep a parallel TSV array so we can re-associate results[] (OSV preserves input order).
|
||||
if ! results="$(osv_querybatch "$queries")"; then
|
||||
note_skip "$nm:osv-querybatch-failed" # transport/HTTP failure -> skip, NEVER alarm
|
||||
continue
|
||||
fi
|
||||
# Walk each tuple alongside its result entry.
|
||||
idx=0
|
||||
while IFS=$'\t' read -r eco pkg ver; do
|
||||
[ -n "$pkg" ] || continue
|
||||
vulns="$(echo "$results" | jq -c --argjson i "$idx" '(.results[$i].vulns // [])')"
|
||||
idx=$((idx+1))
|
||||
vcnt="$(echo "$vulns" | jq 'length' 2>/dev/null || echo 0)"
|
||||
[ "${vcnt:-0}" -gt 0 ] || continue
|
||||
j=0
|
||||
while [ "$j" -lt "$vcnt" ]; do
|
||||
v="$(echo "$vulns" | jq -c --argjson j "$j" '.[$j]')"
|
||||
aid="$(echo "$v" | jq -r '.id // "UNKNOWN"')"
|
||||
summ="$(echo "$v" | jq -r '.summary // (.details // "" | .[0:160])')"
|
||||
# OSV severity: prefer database_specific.severity, else the CVSS vector score band.
|
||||
rawsev="$(echo "$v" | jq -r '.database_specific.severity // ""')"
|
||||
cvss="$(echo "$v" | jq -r '[.severity[]? | select(.type|test("CVSS")) | .score] | .[0] // empty' \
|
||||
| grep -oE '[0-9]+\.[0-9]+' | head -1 || true)"
|
||||
fixed="$(echo "$v" | jq -r '
|
||||
[.affected[]?.ranges[]?.events[]? | select(.fixed != null) | .fixed] | .[0] // ""')"
|
||||
sev="$(norm_sev "$rawsev" "$cvss")"
|
||||
maybe_tiebreak "$v" # inert in this phase
|
||||
add_finding "$nm" "vuln-$(echo "${pkg}-${ver}-${aid}" | tr -c 'A-Za-z0-9-' '-')" \
|
||||
"$pkg $ver is vulnerable ($aid)" "$sev" \
|
||||
"$pkg" "$ver" "$aid" "$summ" "$fixed"
|
||||
j=$((j+1))
|
||||
done
|
||||
done <<< "$deps_tsv"
|
||||
done
|
||||
|
||||
# ==============================================================================
|
||||
# ASSEMBLE REPORT (JSON + text), mode 600 (identical shape to compliance-drift)
|
||||
# ==============================================================================
|
||||
if [ "${#FINDINGS[@]}" -gt 0 ]; then
|
||||
FINDINGS_JSON="$(printf '%s\n' "${FINDINGS[@]}" | jq -cs .)"
|
||||
else
|
||||
FINDINGS_JSON="[]"
|
||||
fi
|
||||
if [ "${#SKIPPED_CHECKS[@]}" -gt 0 ]; then
|
||||
SKIPPED_JSON="$(printf '%s\n' "${SKIPPED_CHECKS[@]}" | jq -R . | jq -cs .)"
|
||||
else
|
||||
SKIPPED_JSON="[]"
|
||||
fi
|
||||
|
||||
N_VULN="$(echo "$FINDINGS_JSON" | jq 'length')"
|
||||
N_HIGH="$(echo "$FINDINGS_JSON" | jq '[.[]|select(.severity=="high" or .severity=="critical")] | length')"
|
||||
N_REPOS_VULN="$(echo "$FINDINGS_JSON" | jq '[.[].repo] | unique | length')"
|
||||
|
||||
jq -n \
|
||||
--arg checker "dependency-cve" --arg ts "$UTC_STAMP" --arg org "$GH_ORG" \
|
||||
--arg advmode "$ADV_MODE" --argjson scanned "${#REPO_NAMES[@]}" \
|
||||
--argjson findings "$FINDINGS_JSON" --argjson skipped "$SKIPPED_JSON" \
|
||||
'{checker:$checker, generated:$ts, org:$org, advisory_mode:$advmode,
|
||||
repos_scanned:$scanned, vuln_count:($findings|length),
|
||||
repos_with_vulns:([$findings[].repo]|unique|length),
|
||||
findings:$findings, skipped_checks:$skipped}' > "$REPORT_JSON"
|
||||
|
||||
{
|
||||
echo "dependency-cve report — $UTC_STAMP"
|
||||
echo "org=$GH_ORG repos_scanned=${#REPO_NAMES[@]} advisory_mode=$ADV_MODE"
|
||||
echo "vulnerable deps: $N_VULN ($N_HIGH high/critical) across $N_REPOS_VULN repo(s)"
|
||||
echo
|
||||
echo "$FINDINGS_JSON" | jq -r '.[] | "• [\(.severity)] \(.repo): \(.title)\n fix: upgrade \(.proof.package) -> \(.proof.fixed_version) (\(.proof.summary))"'
|
||||
if [ "$(echo "$SKIPPED_JSON" | jq 'length')" -gt 0 ]; then
|
||||
echo; echo "skipped (missing data — NOT counted as a vuln):"
|
||||
echo "$SKIPPED_JSON" | jq -r '.[] | " - \(.)"'
|
||||
fi
|
||||
} > "$REPORT_TXT"
|
||||
chmod 600 "$REPORT_JSON" "$REPORT_TXT" 2>/dev/null || true
|
||||
|
||||
log "report: $REPORT_JSON ($N_VULN vuln finding(s), $N_REPOS_VULN repo(s))"
|
||||
|
||||
# ==============================================================================
|
||||
# CANARY ASSERTION (anti-complacency floor, design §6.4)
|
||||
# ==============================================================================
|
||||
if [ "$CANARY" -eq 1 ]; then
|
||||
EXPECT_FILE="$HERE/fixtures/dependency-cve/EXPECTED_VULN_COUNT"
|
||||
[ -f "$EXPECT_FILE" ] || die "canary expected-count file missing: $EXPECT_FILE"
|
||||
EXPECTED="$(tr -dc '0-9' < "$EXPECT_FILE")"
|
||||
log "canary assertion: expected vuln=$EXPECTED, got=$N_VULN"
|
||||
if [ "$N_VULN" -ne "$EXPECTED" ]; then
|
||||
echo "[dependency-cve] CANARY FAIL: planted-vuln count mismatch (expected $EXPECTED, got $N_VULN)" >&2
|
||||
echo " -> a parser or the advisory match regressed, or the fixture changed. See $REPORT_TXT." >&2
|
||||
exit 3
|
||||
fi
|
||||
log "canary PASS: all $EXPECTED planted vulnerable deps detected."
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# ALARM-ONLY ROUTING (clean = silent; memory feedback_cloudwatch_alarms)
|
||||
# ==============================================================================
|
||||
if [ "$N_VULN" -eq 0 ]; then
|
||||
log "no vulnerable dependencies — posting NOTHING to Slack (ALARM-only policy)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ALARM_BODY="$(echo "$FINDINGS_JSON" | jq -r '
|
||||
group_by(.repo)[] | "*\(.[0].repo)*: " + ([.[] | "[\(.severity)] \(.title)"] | join("; "))' | sed 's/^/• /')"
|
||||
SLACK_TEXT=":lock: *Sea Haven dependency-cve — ALARM* ($UTC_STAMP)
|
||||
$N_VULN vulnerable pinned dependency(ies) across $N_REPOS_VULN repo(s) ($N_HIGH high/critical):
|
||||
$ALARM_BODY
|
||||
|
||||
Source: OSV advisory DB ($ADV_MODE) · complements Dependabot
|
||||
Report (mode 600): \`$REPORT_JSON\` (on R720)"
|
||||
SLACK_TEXT="$(echo "$SLACK_TEXT" | redact)"
|
||||
|
||||
echo "$SLACK_TEXT" >&2
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log "DRY-RUN: alarm composed but NOT posted (routing dry-run, design §7 Phase 2)."
|
||||
exit 0
|
||||
fi
|
||||
post_slack_alarm "$SLACK_TEXT"
|
||||
exit 0
|
||||
|
||||
# ==============================================================================
|
||||
# PROVISIONING (NOT DONE HERE — gated, Phase 6):
|
||||
# - No systemd unit / timer is installed by this script. Wiring it into the live
|
||||
# sea-haven-secrev schedule (or a sibling timer) is provisioning and is gated.
|
||||
# - The coordinator (design §5, checker_coordinator.sh) runs this alongside other
|
||||
# Tier-1 checkers under one shared budget + versioned rotation state.
|
||||
# - The LIVE "Claude + GPT tiebreak" severity-judge (design §4) is the only LLM seam;
|
||||
# it is an inert stub here (maybe_tiebreak) and stays off in canary/dry-run/offline.
|
||||
# - Confluence + project_r720_agent_team memory updates are docs-as-you-go obligations
|
||||
# for the build session, tracked outside this script.
|
||||
# ==============================================================================
|
||||
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
42
security-review/checkers/fixtures/dependency-cve/README.md
Normal file
42
security-review/checkers/fixtures/dependency-cve/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# dependency-cve canary fixtures
|
||||
|
||||
Planted-vulnerable-dependency corpus for `checkers/dependency-cve.sh --canary` (offline,
|
||||
no network/token). The checker asserts the total vulnerable-dependency count equals
|
||||
`EXPECTED_VULN_COUNT` (anti-complacency floor, design §6.4). If extraction or matching
|
||||
regresses (a parser stops firing, or the advisory match breaks), the count drops and the
|
||||
canary FAILS (exit 3).
|
||||
|
||||
## Offline advisory source
|
||||
|
||||
OSV needs the network, so the canary CANNOT call `api.osv.dev`. Instead, `--canary`
|
||||
(and the `--advisories-file PATH` override) makes the checker consult the local
|
||||
`osv-advisories.json` fixture INSTEAD of the network — keyed by `ECOSYSTEM|package|version`.
|
||||
This keeps the canary fully offline and deterministic. The fixture mirrors real advisory
|
||||
ids/summaries/fixed-versions so a finding looks like a live one, but nothing is fetched.
|
||||
|
||||
## Fixture repos (each a real git checkout; `dotgit/` is renamed to `.git/` at run time)
|
||||
|
||||
The git metadata is shipped as `dotgit/` (not `.git/`) so these commit into the orchestrator
|
||||
repo WITHOUT becoming nested submodules — the SAME trick `compliance-drift` fixtures use. The
|
||||
checker copies each fixture to a temp area and renames `dotgit` → `.git` before scanning.
|
||||
|
||||
| Fixture | Ecosystem | Pinned deps | Vulnerable match | Count |
|
||||
|---|---|---|---|---|
|
||||
| `vuln-py-repo` | PyPI (`requirements.txt`) | `flask==2.0.1`, `jinja2==2.11.2`, `requests==2.31.0` | `jinja2==2.11.2` → `GHSA-g3rq-g295-4j3m` | 1 |
|
||||
| `vuln-js-repo` | npm (`package-lock.json`) | `lodash 4.17.15`, `left-pad 1.3.0` | `lodash 4.17.15` → `GHSA-p6mc-m468-83gw` | 1 |
|
||||
| `clean-repo` | PyPI (`requirements.txt`) | `requests==2.31.0`, `urllib3==2.2.1` | none (no advisory entry) | 0 |
|
||||
|
||||
Total = **2** (`EXPECTED_VULN_COUNT`). Two ecosystems are exercised (PyPI + npm) so a
|
||||
regression in either parser is caught.
|
||||
|
||||
When you add/remove a parser, a fixture, or an advisory entry, update the fixture(s),
|
||||
`osv-advisories.json`, and `EXPECTED_VULN_COUNT` in the same commit (the canary edit is
|
||||
itself caught on the next run — design §6.4).
|
||||
|
||||
**Manifest naming:** the dependency manifests are stored with a `.fixture` suffix
|
||||
(`requirements.txt.fixture`, `package-lock.json.fixture`) so GitHub's dependency graph /
|
||||
the `dependency-review` CI action does NOT parse the deliberately-vulnerable canary pins as
|
||||
real project dependencies (which would fail the PR gate). The checker's `--canary`
|
||||
materialization strips the `.fixture` suffix in its temp work area before scanning, so the
|
||||
per-ecosystem parsers still dispatch on the real names. Keep this suffix on any new
|
||||
manifest fixture.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# clean-repo
|
||||
Fixture: only non-vulnerable pinned deps; must produce NO findings.
|
||||
|
|
@ -0,0 +1 @@
|
|||
fixture
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[user]
|
||||
email = t@t
|
||||
name = t
|
||||
[commit]
|
||||
gpgsign = false
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
|
|
@ -0,0 +1 @@
|
|||
0000000000000000000000000000000000000000 a480a93df80db47ae333cdbdeb6b7fa3338945fe t <t@t> 1781808419 -0400 commit (initial): fixture
|
||||
|
|
@ -0,0 +1 @@
|
|||
0000000000000000000000000000000000000000 a480a93df80db47ae333cdbdeb6b7fa3338945fe t <t@t> 1781808419 -0400 commit (initial): fixture
|
||||
|
|
@ -0,0 +1 @@
|
|||
x+)JMU°0d040031QrutñuÕËMa8v¿î‰ûþèU»=#—ýU(z!(UT”ZXšY”š›šWR¬WRQÂðŒ+íì#ÝLÏ>œý©7ñôÍ<C3B4>ûº›â$
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
xֱA@0Pk§ר‰5‰<35>°·ה×C<C397><43>?M«ֲם½חװ†¾j°«ll“D«חנ<D797>%ֹ£~ ±}<7D>RׂזT)^b<>p•|#&ףe,+<2B>@xז®d<>.
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
a480a93df80db47ae333cdbdeb6b7fa3338945fe
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# all current / non-vulnerable pins
|
||||
requests==2.31.0
|
||||
urllib3==2.2.1
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_comment": "Offline advisory fixture for dependency-cve.sh --canary (and --advisories-file). This stands in for the live OSV querybatch API so the canary is fully offline + deterministic. Each entry is keyed by 'ECOSYSTEM|package|version' (ECOSYSTEM matches OSV ecosystem names: PyPI, npm, NuGet) and carries the fields the checker emits in a finding's proof. These mirror REAL advisories (GHSA/CVE ids + summaries + fixed versions) so the fixture is realistic, but the checker NEVER reaches the network in canary mode — it reads only this file.",
|
||||
"advisories": {
|
||||
"PyPI|jinja2|2.11.2": [
|
||||
{
|
||||
"id": "GHSA-g3rq-g295-4j3m",
|
||||
"summary": "Jinja2 ReDoS in the urlize filter via the urlize regex",
|
||||
"severity": "high",
|
||||
"cvss": 7.5,
|
||||
"fixed_version": "2.11.3"
|
||||
}
|
||||
],
|
||||
"npm|lodash|4.17.15": [
|
||||
{
|
||||
"id": "GHSA-p6mc-m468-83gw",
|
||||
"summary": "Prototype pollution in lodash (zipObjectDeep / set / setWith)",
|
||||
"severity": "high",
|
||||
"cvss": 7.4,
|
||||
"fixed_version": "4.17.19"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# vuln-js-repo
|
||||
Fixture: pins lodash 4.17.15 (planted, known-vulnerable per the offline advisory fixture).
|
||||
|
|
@ -0,0 +1 @@
|
|||
fixture
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[user]
|
||||
email = t@t
|
||||
name = t
|
||||
[commit]
|
||||
gpgsign = false
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
|
|
@ -0,0 +1 @@
|
|||
0000000000000000000000000000000000000000 a3670ed0a5d8a573dd0a05aef0062738cfc99512 t <t@t> 1781808419 -0400 commit (initial): fixture
|
||||
|
|
@ -0,0 +1 @@
|
|||
0000000000000000000000000000000000000000 a3670ed0a5d8a573dd0a05aef0062738cfc99512 t <t@t> 1781808419 -0400 commit (initial): fixture
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
x%Ì[
|
||||
1P¿güQ°Å‚p)S
II;£î^Äœ“XÂñ²Ùb]XÜ£;£¦Ó½¾ÇbtC«ÒÁšcŸqòáêûÆQ垢/q?IÆLÐR¸
|
||||
!æµvµÊ?Üûé¢'T
|
||||
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
|||
x}лA
|
||||
┐@FА╝Г╧@!┴#⌠@)^еNЪ║.D"x|е■╥Щxu[в%HTяб╛pЧ╨╘qаUС│E}jnn╣@зNС©╜Sп+╕x⌠cкБТДл°Й=Э!╘-GЛИэt$╟
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
a3670ed0a5d8a573dd0a05aef0062738cfc99512
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "vuln-js-repo",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vuln-js-repo",
|
||||
"version": "1.0.0",
|
||||
"dependencies": { "lodash": "4.17.15", "left-pad": "1.3.0" }
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-fake"
|
||||
},
|
||||
"node_modules/left-pad": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
|
||||
"integrity": "sha512-fake"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# vuln-py-repo
|
||||
Fixture: pins jinja2==2.11.2 (planted, known-vulnerable per the offline advisory fixture).
|
||||
|
|
@ -0,0 +1 @@
|
|||
fixture
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[user]
|
||||
email = t@t
|
||||
name = t
|
||||
[commit]
|
||||
gpgsign = false
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
|
|
@ -0,0 +1 @@
|
|||
0000000000000000000000000000000000000000 a318bef74d77c826d0137c7db34aa5a539dbcee3 t <t@t> 1781808419 -0400 commit (initial): fixture
|
||||
|
|
@ -0,0 +1 @@
|
|||
0000000000000000000000000000000000000000 a318bef74d77c826d0137c7db34aa5a539dbcee3 t <t@t> 1781808419 -0400 commit (initial): fixture
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
x%ּA
|
||||
ֲ0P׳=ֵ7
|
||||
&˜,\<08>z<EFBFBD>„N0u˜׃´<D783><C2B4>‹x€ק2·<32>p½<1D>״V§»3ׂ6<ך§¯Fwh•s•9ֵq<D6B5>>qRNׂi÷א%ם-מ'ֹRf‚’¡? ®BH׃V—f;ֺ?<<3C>ב<0B>
|
||||
'A
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
a318bef74d77c826d0137c7db34aa5a539dbcee3
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# pinned deps for the python service
|
||||
flask==2.0.1
|
||||
jinja2==2.11.2
|
||||
requests==2.31.0
|
||||
Reference in a new issue