Skip to content

Instantly share code, notes, and snippets.

@jmvrbanac
Created February 23, 2026 18:09
Show Gist options
  • Select an option

  • Save jmvrbanac/e165741f674db56ca1a0a6389f88b21a to your computer and use it in GitHub Desktop.

Select an option

Save jmvrbanac/e165741f674db56ca1a0a6389f88b21a to your computer and use it in GitHub Desktop.
Application Change Report Generator for Compliance
#!/usr/bin/env bash
#
# Compliance Git Change List Generator
#
# Generates an auditable git change log over a specified date range,
# with a timestamp fetched live from the US government time source (time.gov).
#
# Output file: {control_number}_{project_name}_YYYY-MM-DD-YYYY-MM-DD.txt
#
# Author : John Vrbanac <john.vrbanac@linux.com>
# License: MIT
#
set -euo pipefail
# ── Configuration ─────────────────────────────────────────────────────────────
GOV_TIME_URL="https://time.gov"
SEP="================================================================"
DIV="----------------------------------------------------------------"
# ── Helpers ───────────────────────────────────────────────────────────────────
die() {
printf '\nError: %s\n\n' "$*" >&2
exit 1
}
prompt_date() {
local label="$1" value
while true; do
read -rp " $label (YYYY-MM-DD): " value
if [[ "$value" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "$value"
return
fi
echo " Invalid format — please use YYYY-MM-DD." >&2
done
}
# ── Pre-flight checks ─────────────────────────────────────────────────────────
command -v git &>/dev/null || die "'git' is not installed or not in PATH."
command -v curl &>/dev/null || die "'curl' is not installed or not in PATH."
command -v sha256sum &>/dev/null || die "'sha256sum' is not installed or not in PATH."
REPO_DIR=$(git rev-parse --show-toplevel 2>/dev/null) \
|| die "This script must be run from inside a git repository."
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# ── Banner ────────────────────────────────────────────────────────────────────
echo ""
echo "$SEP"
echo " Compliance Git Change List Generator"
echo "$SEP"
printf " Repository : %s\n" "$REPO_DIR"
printf " Branch : %s\n" "$BRANCH"
echo "$SEP"
echo ""
# ── Control number + project name input ───────────────────────────────────────
read -rp " Control number: " CONTROL_NUM
[[ -n "$CONTROL_NUM" ]] || die "Control number cannot be empty."
# Sanitize for filename: keep digits and dots only, strip everything else
CONTROL_SLUG=$(echo "$CONTROL_NUM" | tr -cd '0-9.')
[[ -n "$CONTROL_SLUG" ]] || die "Control number produced an empty filename slug. Use digits and dots (e.g. 204.01)."
read -rp " Project name : " PROJECT_NAME_RAW
[[ -n "$PROJECT_NAME_RAW" ]] || die "Project name cannot be empty."
# Sanitize for filename: lowercase, spaces/hyphens to underscores, strip non-alphanumeric
PROJECT_SLUG=$(echo "$PROJECT_NAME_RAW" | tr '[:upper:]' '[:lower:]' | tr ' -' '_' | tr -cd 'a-z0-9_')
[[ -n "$PROJECT_SLUG" ]] || die "Project name produced an empty filename slug. Use letters, numbers, spaces, or hyphens."
# ── Date range input ──────────────────────────────────────────────────────────
START_DATE=$(prompt_date "Start date")
END_DATE=$(prompt_date "End date ")
[[ "$START_DATE" > "$END_DATE" ]] \
&& die "Start date ($START_DATE) must be on or before end date ($END_DATE)."
echo ""
# ── Fetch verified government time ────────────────────────────────────────────
# Both variables must be set in the current shell (not a subshell) so that
# GOV_RAW_HEADERS is still accessible when writing the report.
printf "Fetching verified time from %s ...\n" "$GOV_TIME_URL"
GOV_RAW_HEADERS=$(curl -sI --max-time 15 "$GOV_TIME_URL" 2>/dev/null) || true
[[ -n "$GOV_RAW_HEADERS" ]] \
|| die "Could not retrieve time from ${GOV_TIME_URL}. Check your network connection and try again."
GOV_TIME=$(printf '%s\n' "$GOV_RAW_HEADERS" \
| grep -i '^date:' | head -1 \
| sed 's/^[Dd]ate:[[:space:]]*//' | tr -d '\r\n') || true
[[ -n "$GOV_TIME" ]] \
|| die "Could not parse Date header from ${GOV_TIME_URL} response."
printf " Government-verified time : %s\n\n" "$GOV_TIME"
# ── Resolve boundary commits ──────────────────────────────────────────────────
echo "Resolving boundary commits ..."
# First commit ON the start date; if none, take the first commit after it
FIRST_COMMIT=$(
git -C "$REPO_DIR" log \
--since="${START_DATE} 00:00:00" \
--until="${START_DATE} 23:59:59" \
--format="%H" --reverse | head -1
) || true
if [[ -z "$FIRST_COMMIT" ]]; then
printf " No commits on %s — searching forward from that date ...\n" "$START_DATE"
FIRST_COMMIT=$(
git -C "$REPO_DIR" log \
--since="${START_DATE} 00:00:00" \
--format="%H" --reverse | head -1
) || true
[[ -n "$FIRST_COMMIT" ]] \
|| die "No commits found on or after '${START_DATE}' on branch '${BRANCH}'."
fi
# Last commit on the end date (end-of-day inclusive)
LAST_COMMIT=$(
git -C "$REPO_DIR" log \
--until="${END_DATE} 23:59:59" \
--format="%H" | head -1
) || true
[[ -n "$LAST_COMMIT" ]] \
|| die "No commits found on or before '${END_DATE}' on branch '${BRANCH}'."
# Display boundary info
FIRST_DISPLAY=$(git -C "$REPO_DIR" log -1 \
--format="%h %ad %s" --date=format:'%Y-%m-%d %H:%M:%S' "$FIRST_COMMIT")
LAST_DISPLAY=$(git -C "$REPO_DIR" log -1 \
--format="%h %ad %s" --date=format:'%Y-%m-%d %H:%M:%S' "$LAST_COMMIT")
printf " First commit : %s\n" "$FIRST_DISPLAY"
printf " Last commit : %s\n" "$LAST_DISPLAY"
COMMIT_COUNT=$(git -C "$REPO_DIR" rev-list --count \
--since="${START_DATE} 00:00:00" \
--until="${END_DATE} 23:59:59" \
HEAD)
printf " Commits in range : %s\n\n" "$COMMIT_COUNT"
# ── Check for existing output file ────────────────────────────────────────────
OUTPUT_FILE="${REPO_DIR}/${CONTROL_SLUG}_${PROJECT_SLUG}_${START_DATE}-${END_DATE}.txt"
if [[ -f "$OUTPUT_FILE" ]]; then
printf "Warning: Output file already exists:\n %s\n" "$OUTPUT_FILE"
read -rp " Overwrite? [y/N]: " confirm
echo ""
[[ "${confirm,,}" == "y" ]] || { echo "Aborted."; exit 0; }
fi
# ── Write report ──────────────────────────────────────────────────────────────
printf "Writing report ...\n"
{
printf '%s\n' "$SEP"
printf ' %s %s — Git Change List\n' "$CONTROL_NUM" "$PROJECT_NAME_RAW"
printf '%s\n' "$SEP"
printf ' Report generated : %s (system clock)\n' \
"$(date -u +'%Y-%m-%d %H:%M:%S UTC')"
printf ' Government-verified time: %s\n' "$GOV_TIME"
printf ' Time source : %s\n' "$GOV_TIME_URL"
printf ' Control number : %s\n' "$CONTROL_NUM"
printf ' Project : %s\n' "$PROJECT_NAME_RAW"
printf ' Repository : %s\n' "$REPO_DIR"
printf ' Branch : %s\n' "$BRANCH"
printf ' Date range : %s to %s (inclusive)\n' \
"$START_DATE" "$END_DATE"
printf ' First commit (SHA-1) : %s\n' "$FIRST_COMMIT"
printf ' Last commit (SHA-1) : %s\n' "$LAST_COMMIT"
printf ' Total commits : %s\n' "$COMMIT_COUNT"
printf '%s\n' "$SEP"
printf '\n'
printf '%s\n' '--- COMMIT LOG (chronological order, oldest first) ---'
printf '\n'
git -C "$REPO_DIR" log \
--since="${START_DATE} 00:00:00" \
--until="${END_DATE} 23:59:59" \
--reverse \
--date=format:'%Y-%m-%d %H:%M:%S %z' \
--format="Commit : %H%nAuthor : %an <%ae>%nDate : %ad%nSubject : %s%+b%n${DIV}"
printf '\n'
printf '%s\n' "$SEP"
printf '%s\n' '--- GOVERNMENT TIME SOURCE EVIDENCE ---'
printf '%s\n' "$SEP"
printf ' Method : HEAD %s\n' "$GOV_TIME_URL"
printf ' Purpose : Provides NIST-authoritative UTC time independent of local system clock\n'
printf '\n'
printf '%s\n' ' Raw HTTP response headers (unmodified):'
printf '\n'
printf '%s\n' "$GOV_RAW_HEADERS" | tr -d '\r' | sed 's/^/ /'
printf '\n'
printf '%s\n' "$SEP"
} > "$OUTPUT_FILE"
# ── Generate SHA-256 sidecar ──────────────────────────────────────────────────
CHECKSUM_FILE="${OUTPUT_FILE}.sha256"
(cd "$(dirname "$OUTPUT_FILE")" && sha256sum "$(basename "$OUTPUT_FILE")") > "$CHECKSUM_FILE"
CHECKSUM=$(cut -d' ' -f1 "$CHECKSUM_FILE")
printf '\nDone.\n'
printf ' Output file : %s\n' "$OUTPUT_FILE"
printf ' Checksum file : %s\n' "$CHECKSUM_FILE"
printf ' SHA-256 : %s\n' "$CHECKSUM"
printf ' Commits included : %s\n\n' "$COMMIT_COUNT"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment