Created
February 23, 2026 18:09
-
-
Save jmvrbanac/e165741f674db56ca1a0a6389f88b21a to your computer and use it in GitHub Desktop.
Application Change Report Generator for Compliance
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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