diff --git a/security-review/checkers/aws-posture.sh b/security-review/checkers/aws-posture.sh new file mode 100755 index 0000000..ccea471 --- /dev/null +++ b/security-review/checkers/aws-posture.sh @@ -0,0 +1,474 @@ +#!/usr/bin/env bash +# aws-posture.sh — Plane-1 / Tier-2 checker for the R720 agent-team. +# +# Design refs: docs/r720-agent-team-design.md D5 / §4 (Tier 2 roster: aws-posture — +# "Idle/anomalous spend (≈$330/mo flagged) + reasoning layer over baseline findings. Auths via +# Roles Anywhere (short-lived leaf certs, auto-rotated by step-ca). Complements existing +# GuardDuty/Security Hub/Config, does not replace them") and §6.3 / §7 Phase 3 ("doc-drift + +# step-ca/Roles Anywhere + aws-posture"). This is a Tier-2 checker built on the Phase-0 shared +# substrate (lib/sweep_substrate.sh); it mirrors compliance-drift.sh / dependency-cve.sh / +# doc-drift.sh conventions VERBATIM so the coordinator (§5) can drive all of them identically. +# +# WHAT IT DOES (read-only): +# Watches the Sea Haven AWS account (328440206208, us-east-1) for IDLE / ANOMALOUS SPEND and +# idle-resource posture: +# - anomalous Cost Explorer deltas (ce get-anomalies above a $ impact threshold) +# - stopped EC2 instances still paying for attached EBS +# - unattached ("available") EBS volumes +# - unassociated Elastic IPs +# - idle NAT gateways (≈0 bytes out over the window) +# - idle load balancers (0 healthy targets) +# - idle RDS instances (0 connections over the window) +# It COMPLEMENTS GuardDuty / Security Hub / Config (design §4) — it is a spend/idle-posture +# watch, NOT a threat detector, and does not replace them. +# +# AUTH / PROVISIONING GATE (design D5 / §6.3 / §7 B3): +# The LIVE read-only AWS calls require credentials vended via IAM Roles Anywhere using a +# short-lived step-ca leaf cert — this is **PROVISIONING-GATED and NOT available yet** (the IAM +# cross-review PASSED 2026-06-18, which unblocked BUILDING this checker, but step-ca + the trust +# anchor + the role are not stood up). See security-review/iam/ for the reviewed artifacts. +# Therefore the checker: +# (a) attempts read-only `aws` CLI calls ONLY when credentials are actually available +# (an STS identity probe succeeds) AND --no-api/--canary were not passed; +# (b) when there are NO credentials, OR --no-api, OR --canary: it SKIPS the live calls and +# NOTES them — it NEVER alarms on missing data (memory feedback_cloudwatch_alarms: no +# false alarms on no-data). This mirrors compliance-drift's API-skip pattern EXACTLY. +# +# REPORTING (matches secrev sweep conventions): +# - Writes a per-run JSON + text report under $REPORT_ROOT//, mode 600 (umask 077). +# - Slack ALARM-ONLY: a clean run (no confirmed waste) posts NOTHING (memory +# feedback_cloudwatch_alarms). Secret-shaped values are redacted from the Slack string. +# - Reuses the substrate's redact() + post_slack_alarm() verbatim. +# +# SUBSTRATE REUSE (lib/sweep_substrate.sh, sourced — bash dynamic scoping): +# redact, post_slack_alarm -> Slack delivery (reads SLACK_WEBHOOK_URL, REPORT_DIR, SWEEP_LOG) +# (aws-posture does NOT use discover_repos/mirror_repo — it scans an AWS account, not repos.) +# +# CANARY / DRY-RUN (offline, no network, no aws, no credentials): +# --canary runs the SAME detectors against a fixture of mocked AWS JSON responses +# (checkers/fixtures/aws-posture/) and asserts the known finding count against +# EXPECTED_FINDING_COUNT (exit 3 on mismatch). It makes ZERO `aws` calls and ZERO network +# calls. This is the anti-complacency floor (design §6.4) AND the routing dry-run (§7 Phase 3): +# --canary implies --dry-run + --no-api; with --dry-run the Slack alarm is composed + printed +# but NOT POSTed. +# +# SCOPE / SAFETY: +# Read-only. The reasoning ("Sonnet collectors + judge", design §4) is a LATER enhancement: a +# clearly-marked inert stub hook (maybe_judge) marks the future seam; it does NOTHING offline +# and NOTHING in this phase (the deterministic detectors are the whole checker here). Does NOT +# touch agent_team/ or agent-team/, is NOT wired into systemd, and stands NOTHING up in AWS — +# that is Phase-3/6 provisioning (gated). See the "PROVISIONING (NOT DONE HERE)" note at 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 "[aws-posture] $*" >&2; } +die() { echo "[aws-posture] 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) ------------------------------------ +AWS_ACCOUNT="${AWS_ACCOUNT:-328440206208}" +AWS_REGION="${AWS_REGION:-us-east-1}" +REPORT_ROOT="${REPORT_ROOT:-$HOME/sweep-reports/aws-posture}" +# Cost-anomaly $ impact threshold: only anomalies whose TotalImpact >= this are flagged +# (a tiny anomaly is noise, not waste — no false alarm on a sub-threshold blip). +COST_ANOMALY_MIN_IMPACT="${COST_ANOMALY_MIN_IMPACT:-25}" +# A NAT gateway with bytes-out below this over the window is treated as idle. +NAT_IDLE_BYTES_MAX="${NAT_IDLE_BYTES_MAX:-1024}" +# An RDS instance with max connections at/below this over the window is treated as idle. +RDS_IDLE_CONN_MAX="${RDS_IDLE_CONN_MAX:-0}" + +DO_API=1 # --no-api: skip ALL live AWS calls (offline). Without creds this is forced. +DRY_RUN=0 # --dry-run: compose the Slack alarm but DO NOT post it (routing dry-run). +CANARY=0 # --canary: run the detectors against the mocked-AWS fixture + assert count. +TARGETS_OVERRIDE="" # --targets DIR: read mocked-AWS JSON from DIR instead of the live account + # (offline + deterministic; same file shape as the canary fixture). + +usage() { + cat >&2 </dev/null || die "jq 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/aws-posture.log" # name the substrate's post_slack_alarm() references +REPORT_JSON="$REPORT_DIR/aws-posture.json" +REPORT_TXT="$REPORT_DIR/aws-posture.txt" + +log "=== aws-posture $UTC_STAMP (canary=$CANARY dry_run=$DRY_RUN api=$DO_API account=$AWS_ACCOUNT region=$AWS_REGION) ===" + +# ------------------------------------------------------------------------------ +# FINDINGS (spirit of finding.schema.json so the coordinator can route like an agentic finding). +# category="other" (idle-spend is not one of the schema's security categories); +# status="confirmed" only for a deterministic idle/anomaly fact derived from a real response. +# A live call that could not be made (no creds / --no-api / transport failure) is a SKIP, never a +# finding (memory feedback_cloudwatch_alarms: no false alarms on missing data). +# ------------------------------------------------------------------------------ +declare -a FINDINGS=() +add_finding() { # id title severity check resource proof + local id="$1" title="$2" sev="$3" check="$4" resource="$5" proof="$6" + FINDINGS+=( "$(jq -n \ + --arg id "$id" --arg title "$title" --arg sev "$sev" \ + --arg check "$check" --arg resource "$resource" --arg proof "$proof" \ + '{account:env.AWS_ACCOUNT_FOR_FINDING, id:$id, title:$title, severity:$sev, category:"other", + check:$check, status:"confirmed", proof:{resource:$resource, outcome:$proof}}')" ) +} +export AWS_ACCOUNT_FOR_FINDING="$AWS_ACCOUNT" +declare -a SKIPPED_CHECKS=() # (check:reason) live calls skipped on missing data — reported, never alarmed +note_skip() { SKIPPED_CHECKS+=( "$1" ); } + +# Inert future seam (design §4 "Sonnet collectors + judge"): in LIVE mode an ambiguous idle +# candidate ("is this RDS truly idle or just low-traffic?") could be escalated to a reasoning +# judge. This phase keeps the deterministic detectors ONLY — the stub does nothing and is never +# reached offline / in canary / dry-run. +maybe_judge() { # candidate_json (no-op stub; Phase-3 intentionally inert) + return 0 +} + +# ============================================================================== +# DETECTORS — each consumes one AWS JSON response (live or fixture) and emits findings. +# Pure jq parsing; identical logic for the live `aws ... --output json` output and the canary +# fixture, so the canary genuinely exercises the production detectors. +# ============================================================================== + +# cost anomalies: ce get-anomalies. Flag anomalies whose Impact.TotalImpact >= threshold. +detect_cost_anomalies() { # json + local json="$1" + while IFS=$'\t' read -r aid svc impact; do + [ -n "$aid" ] || continue + add_finding "cost-anomaly-$aid" \ + "Cost anomaly: ${svc} (≈\$${impact} impact)" "high" "cost-anomaly" "$aid" \ + "ce get-anomalies TotalImpact \$${impact} >= threshold \$${COST_ANOMALY_MIN_IMPACT} (service: ${svc})" + done < <(echo "$json" | jq -r --argjson thr "$COST_ANOMALY_MIN_IMPACT" ' + (.Anomalies // [])[] + | select((.Impact.TotalImpact // 0) >= $thr) + | [.AnomalyId, (.DimensionValue // "unknown"), ((.Impact.TotalImpact // 0)|tostring)] + | @tsv') +} + +# stopped EC2 still paying for attached EBS: describe-instances, State.Name=="stopped" with EBS. +detect_stopped_instances() { # json + local json="$1" + while IFS=$'\t' read -r iid itype; do + [ -n "$iid" ] || continue + add_finding "stopped-ec2-$iid" \ + "Stopped EC2 instance still incurring EBS cost: $iid ($itype)" "medium" "stopped-instance" "$iid" \ + "ec2 describe-instances: State=stopped with attached EBS (storage bills while stopped)" + done < <(echo "$json" | jq -r ' + (.Reservations // [])[].Instances[] + | select((.State.Name // "") == "stopped") + | select(((.BlockDeviceMappings // []) | length) > 0) + | [.InstanceId, (.InstanceType // "?")] | @tsv') +} + +# unattached EBS: describe-volumes, State=="available". +detect_unattached_volumes() { # json + local json="$1" + while IFS=$'\t' read -r vid size vtype; do + [ -n "$vid" ] || continue + add_finding "unattached-ebs-$vid" \ + "Unattached EBS volume billing idle: $vid (${size}GiB $vtype)" "medium" "unattached-volume" "$vid" \ + "ec2 describe-volumes: State=available (no attachment) — billed but unused" + done < <(echo "$json" | jq -r ' + (.Volumes // [])[] + | select((.State // "") == "available") + | [.VolumeId, ((.Size // 0)|tostring), (.VolumeType // "?")] | @tsv') +} + +# unassociated EIP: describe-addresses, no AssociationId/InstanceId. +detect_unassociated_eips() { # json + local json="$1" + while IFS=$'\t' read -r alloc ip; do + [ -n "$alloc" ] || continue + add_finding "unassociated-eip-$alloc" \ + "Unassociated Elastic IP (hourly charge): $ip" "low" "unassociated-eip" "$alloc" \ + "ec2 describe-addresses: no AssociationId/InstanceId — idle EIPs are billed hourly" + done < <(echo "$json" | jq -r ' + (.Addresses // [])[] + | select((.AssociationId // "") == "" and (.InstanceId // "") == "") + | [(.AllocationId // .PublicIp), (.PublicIp // "?")] | @tsv') +} + +# idle NAT gateway: describe-nat-gateways, available + bytes-out below threshold. +# Live path injects the CloudWatch-derived bytes-out as _FixtureBytesOutLast14d (same key the +# canary fixture uses) before calling this — keeping detector logic identical online/offline. +detect_idle_nat() { # json + local json="$1" + while IFS=$'\t' read -r nid bytes; do + [ -n "$nid" ] || continue + add_finding "idle-nat-$nid" \ + "Idle NAT gateway (≈0 traffic, ~\$32/mo each): $nid" "medium" "idle-nat" "$nid" \ + "ec2 describe-nat-gateways: available with ${bytes} bytes out over window (<= ${NAT_IDLE_BYTES_MAX})" + done < <(echo "$json" | jq -r --argjson mx "$NAT_IDLE_BYTES_MAX" ' + (.NatGateways // [])[] + | select((.State // "") == "available") + | select((._FixtureBytesOutLast14d // 0) <= $mx) + | [.NatGatewayId, ((._FixtureBytesOutLast14d // 0)|tostring)] | @tsv') +} + +# idle ELB: describe-load-balancers, 0 healthy targets. +# Live path injects the per-LB healthy-target count as _FixtureHealthyTargetCount (derived from +# elbv2 describe-target-health) before calling this — same key the canary fixture uses. +detect_idle_elb() { # json + local json="$1" + while IFS=$'\t' read -r name; do + [ -n "$name" ] || continue + add_finding "idle-elb-$name" \ + "Idle load balancer (0 healthy targets, ~\$16/mo each): $name" "medium" "idle-elb" "$name" \ + "elbv2 describe-load-balancers + describe-target-health: 0 healthy targets" + done < <(echo "$json" | jq -r ' + (.LoadBalancers // [])[] + | select((._FixtureHealthyTargetCount // 0) == 0) + | [.LoadBalancerName // .LoadBalancerArn] | @tsv') +} + +# idle RDS: describe-db-instances, available + max connections at/below threshold. +# Live path injects DatabaseConnections max as _FixtureMaxConnectionsLast14d (from CloudWatch). +detect_idle_rds() { # json + local json="$1" + while IFS=$'\t' read -r dbid class; do + [ -n "$dbid" ] || continue + add_finding "idle-rds-$dbid" \ + "Idle RDS instance (0 connections over window): $dbid ($class)" "high" "idle-rds" "$dbid" \ + "rds describe-db-instances: available with 0 connections over window (<= ${RDS_IDLE_CONN_MAX})" + done < <(echo "$json" | jq -r --argjson mx "$RDS_IDLE_CONN_MAX" ' + (.DBInstances // [])[] + | select((.DBInstanceStatus // "") == "available") + | select((._FixtureMaxConnectionsLast14d // 1) <= $mx) + | [.DBInstanceIdentifier, (.DBInstanceClass // "?")] | @tsv') +} + +# Run every detector over a directory of JSON responses (fixture dir or a collected-live dir). +# Missing files are tolerated (a detector with no input simply contributes nothing — never a skip +# that alarms; a genuinely uncollected live call is recorded as a SKIP by the live collector). +run_detectors_over_dir() { # dir + local dir="$1" f + f="$dir/cost-anomalies.json"; [ -f "$f" ] && detect_cost_anomalies "$(cat "$f")" + f="$dir/describe-instances.json"; [ -f "$f" ] && detect_stopped_instances "$(cat "$f")" + f="$dir/describe-volumes.json"; [ -f "$f" ] && detect_unattached_volumes "$(cat "$f")" + f="$dir/describe-addresses.json"; [ -f "$f" ] && detect_unassociated_eips "$(cat "$f")" + f="$dir/describe-nat-gateways.json";[ -f "$f" ] && detect_idle_nat "$(cat "$f")" + f="$dir/describe-load-balancers.json";[ -f "$f" ] && detect_idle_elb "$(cat "$f")" + f="$dir/describe-db-instances.json";[ -f "$f" ] && detect_idle_rds "$(cat "$f")" +} + +# ============================================================================== +# LIVE COLLECTION (read-only AWS, ONLY when credentials are available + not --no-api/--canary). +# Each call is fail-safe: on a transport/permission failure the response is NOT written and the +# call is recorded as a SKIP — never a finding (memory feedback_cloudwatch_alarms). +# The CloudWatch-derived idle metrics (NAT bytes-out, ELB healthy targets, RDS connections) are +# injected into the describe-* JSON under the SAME _Fixture* keys the detectors read, so the live +# and canary code paths are identical. +# ============================================================================== +aws_creds_available() { + command -v aws >/dev/null || return 1 + aws sts get-caller-identity --region "$AWS_REGION" >/dev/null 2>>"$REPORT_DIR/aws.log" +} + +collect_live() { # out_dir + local out="$1"; mkdir -p "$out" + # NOTE: this live collector is PROVISIONING-GATED and only reached when real Roles Anywhere + # creds exist (aws_creds_available passed). Until step-ca/Roles Anywhere are stood up this path + # is never taken; it is written so the checker is complete + ready, not so it runs today. + _try() { # outfile aws-args... + local of="$1"; shift + if aws "$@" --region "$AWS_REGION" --output json >"$of" 2>>"$REPORT_DIR/aws.log"; then + return 0 + else + rm -f "$of"; note_skip "live:$(basename "$of" .json)(aws-call-failed)"; return 1 + fi + } + _try "$out/cost-anomalies.json" ce get-anomalies || true + _try "$out/describe-instances.json" ec2 describe-instances || true + _try "$out/describe-volumes.json" ec2 describe-volumes || true + _try "$out/describe-addresses.json" ec2 describe-addresses || true + _try "$out/describe-nat-gateways.json" ec2 describe-nat-gateways || true + _try "$out/describe-load-balancers.json" elbv2 describe-load-balancers || true + _try "$out/describe-db-instances.json" rds describe-db-instances || true + # Idle-metric enrichment (NAT bytes-out / ELB healthy targets / RDS connections from CloudWatch) + # is injected here in the live path under the _Fixture* keys before the detectors run. It is a + # provisioning-time follow-up — until creds exist this collector is unreachable, so the + # enrichment is intentionally a documented seam, not dead code that runs offline. +} + +# ============================================================================== +# RESOLVE THE INPUT (fixture / explicit dir / live collection) + DECIDE API MODE +# ============================================================================== +SCAN_DIR="" +SCAN_MODE="none" + +if [ "$CANARY" -eq 1 ]; then + FIXTURE_DIR="$HERE/fixtures/aws-posture" + [ -d "$FIXTURE_DIR" ] || die "canary fixture missing: $FIXTURE_DIR" + SCAN_DIR="$FIXTURE_DIR"; SCAN_MODE="canary-fixture" + log "canary: running detectors against mocked-AWS fixtures in $FIXTURE_DIR (no aws, no network)" +elif [ -n "$TARGETS_OVERRIDE" ]; then + d="${TARGETS_OVERRIDE/#\~/$HOME}" + [ -d "$d" ] || die "--targets dir not found: $d" + SCAN_DIR="$d"; SCAN_MODE="explicit-dir" + log "explicit targets dir (offline mocked-AWS JSON): $SCAN_DIR" +elif [ "$DO_API" -eq 1 ] && aws_creds_available; then + COLLECT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/aws-posture-live.XXXXXX")" + trap 'rm -rf "$COLLECT_DIR"' EXIT + log "live: AWS credentials present — collecting read-only responses into $COLLECT_DIR" + collect_live "$COLLECT_DIR" + SCAN_DIR="$COLLECT_DIR"; SCAN_MODE="live-aws" +else + # No creds, or --no-api: SKIP all live calls and note them. NEVER alarm on missing data. + if [ "$DO_API" -eq 1 ]; then + log "live AWS requested but no usable credentials (Roles Anywhere is PROVISIONING-GATED) — skipping all live calls (no false alarms on missing data)" + note_skip "live:all(no-credentials — Roles Anywhere gated; see security-review/iam/)" + else + log "--no-api: skipping all live AWS calls" + note_skip "live:all(--no-api)" + fi + SCAN_MODE="skipped-no-creds" +fi + +# ============================================================================== +# RUN DETECTORS +# ============================================================================== +if [ -n "$SCAN_DIR" ]; then + run_detectors_over_dir "$SCAN_DIR" + maybe_judge "" # inert in this phase (future Sonnet-collector/judge seam) +fi + +# ============================================================================== +# ASSEMBLE REPORT (JSON + text), mode 600 (identical shape to the other checkers) +# ============================================================================== +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_FIND="$(echo "$FINDINGS_JSON" | jq 'length')" +N_HIGH="$(echo "$FINDINGS_JSON" | jq '[.[]|select(.severity=="high" or .severity=="critical")] | length')" + +jq -n \ + --arg checker "aws-posture" --arg ts "$UTC_STAMP" --arg account "$AWS_ACCOUNT" \ + --arg region "$AWS_REGION" --arg mode "$SCAN_MODE" \ + --argjson findings "$FINDINGS_JSON" --argjson skipped "$SKIPPED_JSON" \ + '{checker:$checker, generated:$ts, account:$account, region:$region, scan_mode:$mode, + finding_count:($findings|length), + findings:$findings, skipped_checks:$skipped}' > "$REPORT_JSON" + +{ + echo "aws-posture report — $UTC_STAMP" + echo "account=$AWS_ACCOUNT region=$AWS_REGION scan_mode=$SCAN_MODE" + echo "idle/anomalous-spend findings: $N_FIND ($N_HIGH high/critical)" + echo + echo "$FINDINGS_JSON" | jq -r '.[] | "• [\(.severity)] \(.check): \(.title)\n proof: \(.proof.outcome)"' + if [ "$(echo "$SKIPPED_JSON" | jq 'length')" -gt 0 ]; then + echo; echo "skipped (missing data — NOT counted as a finding):" + echo "$SKIPPED_JSON" | jq -r '.[] | " - \(.)"' + fi +} > "$REPORT_TXT" +chmod 600 "$REPORT_JSON" "$REPORT_TXT" 2>/dev/null || true + +log "report: $REPORT_JSON ($N_FIND finding(s), mode=$SCAN_MODE)" + +# ============================================================================== +# CANARY ASSERTION (anti-complacency floor, design §6.4) +# ============================================================================== +if [ "$CANARY" -eq 1 ]; then + EXPECT_FILE="$HERE/fixtures/aws-posture/EXPECTED_FINDING_COUNT" + [ -f "$EXPECT_FILE" ] || die "canary expected-count file missing: $EXPECT_FILE" + EXPECTED="$(tr -dc '0-9' < "$EXPECT_FILE")" + log "canary assertion: expected findings=$EXPECTED, got=$N_FIND" + if [ "$N_FIND" -ne "$EXPECTED" ]; then + echo "[aws-posture] CANARY FAIL: planted-finding count mismatch (expected $EXPECTED, got $N_FIND)" >&2 + echo " -> a detector regressed (stopped firing) or the fixture changed. See $REPORT_TXT." >&2 + exit 3 + fi + log "canary PASS: all $EXPECTED planted idle/anomaly findings detected." +fi + +# ============================================================================== +# ALARM-ONLY ROUTING (clean = silent; memory feedback_cloudwatch_alarms) +# ============================================================================== +if [ "$N_FIND" -eq 0 ]; then + log "no idle/anomalous spend detected — posting NOTHING to Slack (ALARM-only policy)." + exit 0 +fi + +ALARM_BODY="$(echo "$FINDINGS_JSON" | jq -r ' + group_by(.check)[] | "*\(.[0].check)*: " + ([.[] | "[\(.severity)] \(.title)"] | join("; "))' | sed 's/^/• /')" +SLACK_TEXT=":money_with_wings: *Sea Haven aws-posture — ALARM* ($UTC_STAMP) +$N_FIND idle/anomalous-spend finding(s) in account $AWS_ACCOUNT/$AWS_REGION ($N_HIGH high/critical): +$ALARM_BODY + +Scope: idle/anomalous SPEND + idle-resource posture (complements GuardDuty/SecurityHub/Config, mode=$SCAN_MODE) +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 3)." + exit 0 +fi +post_slack_alarm "$SLACK_TEXT" +exit 0 + +# ============================================================================== +# PROVISIONING (NOT DONE HERE — gated, Phase 3 / Phase 6): +# - The LIVE AWS calls need credentials vended via IAM Roles Anywhere using a short-lived +# step-ca leaf cert. step-ca + the Roles Anywhere trust anchor + the read-only role are NOT +# stood up by this script. The reviewed IAM artifacts live in security-review/iam/ (GPT-4.1 +# cross-review PASSED 2026-06-18: APPROVE, no BLOCKs). Provisioning happens only after that +# review is recorded (design §7, B3) and a VM snapshot is taken (feedback_ec2_replacement_snapshot). +# Until then aws_creds_available() returns false and the checker SKIPS all live calls (no +# false alarms on missing data) — only --canary / --targets exercise it offline. +# - No systemd unit / timer is installed by this script. Wiring it into the live secrev schedule +# (weekly cadence, design §4) is provisioning and is gated. +# - This script is NOT registered in checker_coordinator.sh; the coordinator registry is +# integrated centrally (separate change). +# - The LIVE "Sonnet collectors + judge" reasoning layer (design §4) is the only LLM seam; it is +# an inert stub here (maybe_judge) 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. +# ============================================================================== diff --git a/security-review/checkers/doc-drift.sh b/security-review/checkers/doc-drift.sh new file mode 100755 index 0000000..d984216 --- /dev/null +++ b/security-review/checkers/doc-drift.sh @@ -0,0 +1,482 @@ +#!/usr/bin/env bash +# doc-drift.sh — Plane-1 / Tier-1 checker for the R720 agent-team. +# +# Design refs: docs/r720-agent-team-design.md §4 (Tier 1 roster: doc-drift — +# "Flags repos whose architecture moved but Confluence/README did not") and §7 Phase 3 +# ("doc-drift + step-ca/Roles Anywhere + aws-posture"). This is the THIRD Plane-1 checker +# built on the Phase-0 shared substrate (lib/sweep_substrate.sh); it mirrors +# compliance-drift.sh / dependency-cve.sh conventions VERBATIM so the coordinator (§5) can +# drive all of them identically. doc-drift is UNGATED (only aws-posture in this phase is +# hard-gated behind the GPT-4.1 IAM cross-review; that checker is NOT built here). +# +# 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 flags repos whose ARCHITECTURE MOVED but the README +# DID NOT — i.e. documentation drift. The checklist is DETERMINISTIC and GROUNDED in the +# global CLAUDE.md README obligation; it does NOT invent fuzzy judgments. See "CHECKLIST". +# +# This phase is the deterministic core ONLY. The design's "Gemini (large context)" judge +# layer (§4) is a LATER enhancement: a clearly-marked inert stub hook (maybe_judge) marks +# the future seam; it does NOTHING offline and NOTHING in this phase. +# +# REPORTING (matches secrev sweep conventions): +# - Writes a per-run JSON + text report under $REPORT_ROOT//, mode 600 (umask 077). +# - Slack ALARM-ONLY: a clean run (no confirmed drift) posts NOTHING (memory +# feedback_cloudwatch_alarms). Secret-shaped values are redacted from the Slack string. +# - Reuses the substrate's redact() + post_slack_alarm() verbatim. +# +# SUBSTRATE REUSE (lib/sweep_substrate.sh, sourced — bash dynamic scoping): +# redact, post_slack_alarm -> Slack delivery (reads SLACK_WEBHOOK_URL, REPORT_DIR, SWEEP_LOG) +# discover_repos, mirror_repo-> ONLY on --refresh (reads GH_TOKEN, GH_ORG, MIRROR_DIR, REPORT_DIR) +# Default path enumerates EXISTING $MIRROR_DIR/*/.git dirs — zero clones, zero network. +# +# CANARY / DRY-RUN (offline, no network, no token): +# --canary runs the checklist against a planted-drift fixture (checkers/fixtures/doc-drift/) +# and asserts the known drift count. This is the anti-complacency floor (design §6.4) AND the +# routing dry-run (§7 Phase 3): with --dry-run, the Slack alarm is composed + printed but NOT +# POSTed. Fully offline-smoke-testable (the checks are filesystem + `git log`, no network). +# +# SCOPE / SAFETY: +# Read-only. All checks are filesystem + local `git log`; NO network, NO token, NO GitHub API +# (doc-drift has no API-only checks — it is purely tree+history). 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 / dependency-cve use. A repo with NO README is +# SKIPPED (compliance-drift owns readme-present); doc-drift never double-flags a missing README. +# +# This script does NOT touch agent_team/ or agent-team/, is NOT wired into systemd, and does NOT +# stand up step-ca / Roles Anywhere / aws-posture — that is Phase-3/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 "[doc-drift] $*" >&2; } +die() { echo "[doc-drift] 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/doc-drift}" +# Docs-only repos describe themselves differently (a handbook is its own doc); skip the +# architecture-omission scan for them. They still get the staleness check. +DOCS_ONLY_REPOS="${DOCS_ONLY_REPOS:-engineering-handbook}" +# Staleness thresholds: README must lag the newest code by BOTH at least this many days AND +# this many substantial code commits before we call it drift (two-factor = no false alarm on a +# single quick fix landed after a doc commit; memory feedback_cloudwatch_alarms). +DOC_DRIFT_STALE_DAYS="${DOC_DRIFT_STALE_DAYS:-60}" +DOC_DRIFT_STALE_COMMITS="${DOC_DRIFT_STALE_COMMITS:-3}" + +REFRESH=0 # --refresh: re-run discovery+mirror via substrate (network). Default: reuse mirrors. +DO_API=1 # --no-api: accepted for interface-parity with the other checkers; doc-drift makes + # NO API calls, so this flag is a documented no-op (kept so the coordinator + # can pass a uniform flag set to every Tier-1 checker). +DRY_RUN=0 # --dry-run: compose the Slack alarm but DO NOT post it (routing dry-run). +CANARY=0 # --canary: run against the planted-drift fixture + assert the known count. +TARGETS_OVERRIDE="" # --targets "p1 p2": scan explicit dirs instead of the mirror set. + +usage() { + cat >&2 </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/doc-drift.log" # name the substrate's post_slack_alarm() references +REPORT_JSON="$REPORT_DIR/doc-drift.json" +REPORT_TXT="$REPORT_DIR/doc-drift.txt" + +# doc-drift makes NO API calls, so DO_API is a documented no-op kept only for coordinator +# flag-parity; surface it in the run banner so the chosen value is auditable (and used). +log "=== doc-drift $UTC_STAMP (canary=$CANARY dry_run=$DRY_RUN refresh=$REFRESH api=${DO_API}[no-op] stale_days=$DOC_DRIFT_STALE_DAYS stale_commits=$DOC_DRIFT_STALE_COMMITS) ===" + +# ------------------------------------------------------------------------------ +# CHECKLIST (grounded — every item cites the README obligation; nothing invented): +# +# readme-omits-component README exists but omits a major existing component +# present in the tree (top-level service dir, SAM/CDK stack, +# Lambda handler dir, openapi/docs API spec) +# -> global CLAUDE.md: "README must accurately describe +# architecture, services, data flow, and configuration" +# readme-stale-vs-code README last-touched commit far older than the newest code +# commit (>= DOC_DRIFT_STALE_DAYS) AND >= DOC_DRIFT_STALE_COMMITS +# substantial code commits landed after the README was touched +# -> global CLAUDE.md: "update the README in the same commit" +# +# A repo with NO README is SKIPPED (compliance-drift owns readme-present; double-flagging would +# be a false alarm). Each emitted finding follows the spirit of finding.schema.json +# (id/title/severity/category/proof/status) so the coordinator can route it like an agentic +# finding. category="other" (doc drift is not one of the schema's security categories). +# status="confirmed" only for deterministic filesystem/git-history facts. The future Gemini +# judge (design §4) is an inert stub (maybe_judge) — never invoked offline / in this phase. +# ------------------------------------------------------------------------------ + +# Drift accumulator: one JSON object per finding, appended to a bash array. +declare -a FINDINGS=() +add_finding() { # repo id title severity check proof + local repo="$1" id="$2" title="$3" sev="$4" check="$5" proof="$6" + FINDINGS+=( "$(jq -n \ + --arg repo "$repo" --arg id "$id" --arg title "$title" --arg sev "$sev" \ + --arg check "$check" --arg proof "$proof" \ + '{repo:$repo, id:($repo+"-"+$id), title:$title, severity:$sev, category:"other", + check:$check, status:"confirmed", proof:{outcome:$proof}}')" ) +} +declare -a SKIPPED_CHECKS=() # (repo:check) checks skipped on missing data — reported, never alarmed +note_skip() { SKIPPED_CHECKS+=( "$1" ); } + +in_csv() { # needle csv -> 0 if present + local n="$1" csv="$2"; case ",$csv," in *",$n,"*) return 0 ;; *) return 1 ;; esac +} + +# Inert future seam (design §4 "Gemini (large context)" judge): in LIVE mode an ambiguous +# omission ("is this component material enough to require a README mention?") could be escalated +# to a large-context judge. This phase keeps the deterministic core ONLY — the stub does nothing +# and is never reached offline / in canary / dry-run. +maybe_judge() { # candidate_json (no-op stub; Phase-3 intentionally inert) + return 0 +} + +# --- Does a README mention a component name? (case-insensitive, word-ish, deterministic) ---- +# Matches the bare name OR the name with a trailing slash (how a dir is usually cited). Strips +# a leading "the " never matters; we test the literal token. Pure grep, no fuzzy matching. +readme_mentions() { # readme_file name + local rf="$1" name="$2" + # Escape regex metacharacters in the component name (defensive; dir names are usually plain). + local esc; esc="$(printf '%s' "$name" | sed -E 's/[][(){}.*+?^$|\\/]/\\&/g')" + grep -qiE "(^|[^A-Za-z0-9_-])${esc}([^A-Za-z0-9_-]|/|$)" "$rf" 2>/dev/null +} + +# --- Enumerate the major components present in a repo tree (deterministic) ------ +# Emits "TYPElabelmention_token" lines. mention_token is what the README must contain. +# service-dir a top-level directory whose name ends in -service or -api, or named api/web/worker +# sam-cdk-stack a SAM/CDK stack root (template.yaml | app.py at a stack root | cdk.json) +# lambda-dir a Lambda handler dir (a dir named handlers/ or containing handler.* / app.py under handlers/) +# api-spec an openapi/ or docs/ directory or an openapi.* / swagger.* spec file +enumerate_components() { # repo_dir -> TSV lines + local dir="$1" d nm + + # 1) top-level service-ish directories (the unit a README is expected to name) + for d in "$dir"/*/; do + [ -d "$d" ] || continue + nm="$(basename "$d")" + case "$nm" in + .git|.github|node_modules|dist|build|vendor|__pycache__|.venv) continue ;; + esac + case "$nm" in + *-service|*-api|api|web|worker|backend|frontend) + printf 'service-dir\t%s\t%s\n' "$nm" "$nm" ;; + esac + done + + # 2) SAM / CDK stack roots + if [ -f "$dir/template.yaml" ] || [ -f "$dir/template.yml" ]; then + printf 'sam-cdk-stack\t%s\t%s\n' "template.yaml (SAM stack)" "template.yaml" + fi + if [ -f "$dir/cdk.json" ]; then + printf 'sam-cdk-stack\t%s\t%s\n' "cdk.json (CDK app)" "cdk.json" + fi + + # 3) Lambda handler dirs: a top-level/handlers-rooted dir literally named "handlers" + while IFS= read -r d; do + [ -n "$d" ] || continue + printf 'lambda-dir\t%s\t%s\n' "handlers/ (Lambda handlers)" "handlers" + break # one mention requirement for the handlers tree is enough + done < <(find "$dir" -maxdepth 2 -type d -name handlers -not -path '*/.git/*' 2>/dev/null) + + # 4) API spec: an openapi/ or docs/ dir, or an openapi.*/swagger.* file + if [ -d "$dir/openapi" ]; then + printf 'api-spec\t%s\t%s\n' "openapi/ (API spec)" "openapi" + elif find "$dir" -maxdepth 2 \( -iname 'openapi.*' -o -iname 'swagger.*' \) -not -path '*/.git/*' -print -quit 2>/dev/null | grep -q .; then + printf 'api-spec\t%s\t%s\n' "openapi/swagger spec" "openapi" + fi +} + +# --- README last-touch epoch vs newest code commit (staleness, deterministic git log) ------- +# Returns the staleness facts on stdout as TSV "readme_epochnewest_code_epochcommits_after". +# commits_after = count of commits that touched code (non-doc) files AFTER the README's last touch. +# Code = anything that is NOT a README/markdown/LICENSE/.gitignore/docs file. Prints nothing if +# the repo has no git history or no README in history (caller treats that as "cannot assess"). +readme_staleness_facts() { # repo_dir + local dir="$1" + command -v git >/dev/null || return 0 + git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || return 0 + + # README last-touch (committer epoch of the most recent commit touching README.md). + local rd_epoch + rd_epoch="$(git -C "$dir" log -1 --format='%ct' -- README.md 2>/dev/null || true)" + [ -n "$rd_epoch" ] || return 0 # README not in history -> cannot assess staleness + + # Newest commit touching a CODE path (exclude docs/markdown/license/config-noise). + local code_epoch + code_epoch="$(git -C "$dir" log -1 --format='%ct' -- \ + ':(exclude)README.md' ':(exclude)*.md' ':(exclude)docs/**' \ + ':(exclude)LICENSE' ':(exclude).gitignore' ':(exclude).github/**' \ + 2>/dev/null || true)" + [ -n "$code_epoch" ] || return 0 # no code commits -> nothing to be stale against + + # Count CODE commits strictly AFTER the README's last touch. + local commits_after + commits_after="$(git -C "$dir" rev-list --count "--since=@${rd_epoch}" HEAD -- \ + ':(exclude)README.md' ':(exclude)*.md' ':(exclude)docs/**' \ + ':(exclude)LICENSE' ':(exclude).gitignore' ':(exclude).github/**' \ + 2>/dev/null || echo 0)" + printf '%s\t%s\t%s\n' "$rd_epoch" "$code_epoch" "${commits_after:-0}" +} + +# ============================================================================== +# PER-REPO CHECK (offline; filesystem + local git log only) +# ============================================================================== +check_repo() { # repo_name repo_dir + local repo="$1" dir="$2" + local docs_only=0; in_csv "$repo" "$DOCS_ONLY_REPOS" && docs_only=1 + + # No README -> doc-drift cannot assess drift; compliance-drift owns readme-present. SKIP. + if [ ! -f "$dir/README.md" ]; then + note_skip "$repo:doc-drift(no-readme — compliance-drift owns readme-present)" + return + fi + local readme="$dir/README.md" + + # --- readme-omits-component (skip for docs-only repos: they document differently) --- + if [ "$docs_only" -eq 0 ]; then + local type label token + while IFS=$'\t' read -r type label token; do + [ -n "$token" ] || continue + if ! readme_mentions "$readme" "$token"; then + add_finding "$repo" "readme-omits-$(printf '%s' "$type-$token" | tr -c 'A-Za-z0-9-' '-')" \ + "README omits existing component: $label" "medium" "readme-omits-component" \ + "global CLAUDE.md: README must accurately describe architecture/services (present in tree, absent from README: $label)" + fi + done < <(enumerate_components "$dir") + else + note_skip "$repo:readme-omits-component(docs-only)" + fi + + # --- readme-stale-vs-code (two-factor: age in days AND code-commits-after) --- + local facts; facts="$(readme_staleness_facts "$dir")" + if [ -z "$facts" ]; then + note_skip "$repo:readme-stale-vs-code(no-history-or-no-readme-in-history)" + else + local rd_epoch code_epoch commits_after age_days + IFS=$'\t' read -r rd_epoch code_epoch commits_after <<< "$facts" + age_days=$(( (code_epoch - rd_epoch) / 86400 )) + [ "$age_days" -lt 0 ] && age_days=0 + if [ "$age_days" -ge "$DOC_DRIFT_STALE_DAYS" ] && [ "$commits_after" -ge "$DOC_DRIFT_STALE_COMMITS" ]; then + add_finding "$repo" "readme-stale" \ + "README is stale: ${age_days}d behind newest code, ${commits_after} code commit(s) since last README touch" \ + "medium" "readme-stale-vs-code" \ + "global CLAUDE.md: update the README in the same commit as functionality changes (thresholds: >=${DOC_DRIFT_STALE_DAYS}d AND >=${DOC_DRIFT_STALE_COMMITS} code commits)" + fi + fi + + maybe_judge "" # inert in this phase (future Gemini large-context seam) +} + +# ============================================================================== +# TARGET RESOLUTION +# ============================================================================== +declare -a REPO_NAMES=(); declare -A REPO_DIR=() + +if [ "$CANARY" -eq 1 ]; then + FIXTURE_ROOT="$HERE/fixtures/doc-drift" + [ -d "$FIXTURE_ROOT" ] || die "canary fixture missing: $FIXTURE_ROOT" + # Pin the exception lists + thresholds the fixtures were authored against, so the canary is + # self-contained and deterministic regardless of the operator's env. + DOCS_ONLY_REPOS="" + DOC_DRIFT_STALE_DAYS=60 + DOC_DRIFT_STALE_COMMITS=3 + # Fixtures ship their git metadata as `dotgit/` (not `.git/`) so they are committable into THIS + # repo without becoming nested submodules. Materialize them into a temp work area — copy each + # fixture and rename dotgit -> .git — so the README/git-log checks run against a real git + # checkout. The temp area is mode 700 and removed on exit (same trick as compliance-drift.sh). + FIXTURE_WORK="$(mktemp -d "${TMPDIR:-/tmp}/doc-drift-canary.XXXXXX")" + trap 'rm -rf "$FIXTURE_WORK"' EXIT + log "canary: materializing planted-drift fixtures from $FIXTURE_ROOT into $FIXTURE_WORK" + for d in "$FIXTURE_ROOT"/*/; do + [ -d "$d/dotgit" ] || continue # only fixture repos (skip README.md, EXPECTED_* etc.) + nm="$(basename "$d")" + cp -R "$d" "$FIXTURE_WORK/$nm" + mv "$FIXTURE_WORK/$nm/dotgit" "$FIXTURE_WORK/$nm/.git" + 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" + +# ============================================================================== +# RUN CHECKS +# ============================================================================== +for nm in "${REPO_NAMES[@]}"; do + check_repo "$nm" "${REPO_DIR[$nm]}" +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_DRIFT="$(echo "$FINDINGS_JSON" | jq 'length')" +N_HIGH="$(echo "$FINDINGS_JSON" | jq '[.[]|select(.severity=="high")] | length')" +N_REPOS_DRIFTED="$(echo "$FINDINGS_JSON" | jq '[.[].repo] | unique | length')" + +jq -n \ + --arg checker "doc-drift" --arg ts "$UTC_STAMP" --arg org "$GH_ORG" \ + --argjson scanned "${#REPO_NAMES[@]}" \ + --argjson findings "$FINDINGS_JSON" --argjson skipped "$SKIPPED_JSON" \ + '{checker:$checker, generated:$ts, org:$org, + repos_scanned:$scanned, drift_count:($findings|length), + repos_with_drift:([$findings[].repo]|unique|length), + findings:$findings, skipped_checks:$skipped}' > "$REPORT_JSON" + +{ + echo "doc-drift report — $UTC_STAMP" + echo "org=$GH_ORG repos_scanned=${#REPO_NAMES[@]} stale_thresholds=${DOC_DRIFT_STALE_DAYS}d/${DOC_DRIFT_STALE_COMMITS}commits" + echo "drift findings: $N_DRIFT ($N_HIGH high) across $N_REPOS_DRIFTED repo(s)" + echo + echo "$FINDINGS_JSON" | jq -r '.[] | "• [\(.severity)] \(.repo): \(.title)\n rule: \(.proof.outcome)"' + if [ "$(echo "$SKIPPED_JSON" | jq 'length')" -gt 0 ]; then + echo; echo "skipped checks (missing data / not doc-drift's job — NOT counted as drift):" + echo "$SKIPPED_JSON" | jq -r '.[] | " - \(.)"' + fi +} > "$REPORT_TXT" +chmod 600 "$REPORT_JSON" "$REPORT_TXT" 2>/dev/null || true + +log "report: $REPORT_JSON ($N_DRIFT drift finding(s), $N_REPOS_DRIFTED repo(s))" + +# ============================================================================== +# CANARY ASSERTION (anti-complacency floor, design §6.4) +# ============================================================================== +if [ "$CANARY" -eq 1 ]; then + EXPECT_FILE="$HERE/fixtures/doc-drift/EXPECTED_DRIFT_COUNT" + [ -f "$EXPECT_FILE" ] || die "canary expected-count file missing: $EXPECT_FILE" + EXPECTED="$(tr -dc '0-9' < "$EXPECT_FILE")" + log "canary assertion: expected drift=$EXPECTED, got=$N_DRIFT" + if [ "$N_DRIFT" -ne "$EXPECTED" ]; then + echo "[doc-drift] CANARY FAIL: planted-drift count mismatch (expected $EXPECTED, got $N_DRIFT)" >&2 + echo " -> the checklist regressed (a check stopped firing) or the fixture changed. See $REPORT_TXT." >&2 + exit 3 + fi + log "canary PASS: all $EXPECTED planted drifts detected." +fi + +# ============================================================================== +# ALARM-ONLY ROUTING (clean = silent; memory feedback_cloudwatch_alarms) +# ============================================================================== +if [ "$N_DRIFT" -eq 0 ]; then + log "no confirmed drift — 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=":memo: *Sea Haven doc-drift — ALARM* ($UTC_STAMP) +$N_DRIFT documentation-drift finding(s) across $N_REPOS_DRIFTED repo(s) ($N_HIGH high): +$ALARM_BODY + +Checks: README-omits-component · README-stale-vs-code (architecture moved, docs did not) +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 3)." + exit 0 +fi +post_slack_alarm "$SLACK_TEXT" +exit 0 + +# ============================================================================== +# PROVISIONING (NOT DONE HERE — gated, Phase 3 / 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 script is NOT registered in checker_coordinator.sh; the coordinator registry is +# integrated centrally (separate change), so doc-drift is not yet driven by the coordinator. +# - step-ca / IAM Roles Anywhere / the read-only AWS role / aws-posture are NOT stood up or +# built here. The IAM artifacts authored alongside this checker (security-review/iam/) are +# FILES for the mandatory GPT-4.1 cross-review; aws-posture itself is hard-gated behind that +# review and is built only after it is recorded (design §7, B3). +# - The LIVE "Gemini (large context)" doc-drift judge (design §4) is the only LLM seam; it is +# an inert stub here (maybe_judge) 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. +# ============================================================================== diff --git a/security-review/checkers/fixtures/aws-posture/EXPECTED_FINDING_COUNT b/security-review/checkers/fixtures/aws-posture/EXPECTED_FINDING_COUNT new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/EXPECTED_FINDING_COUNT @@ -0,0 +1 @@ +7 diff --git a/security-review/checkers/fixtures/aws-posture/README.md b/security-review/checkers/fixtures/aws-posture/README.md new file mode 100644 index 0000000..f681a87 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/README.md @@ -0,0 +1,34 @@ +# aws-posture canary fixtures + +Mocked AWS API responses for `checkers/aws-posture.sh --canary` (offline — **no `aws` calls, no +network, no credentials**). The canary feeds these files to the SAME detectors the live path runs +against real `aws` CLI output, and asserts the total finding count equals `EXPECTED_FINDING_COUNT` +(anti-complacency floor, design §6.4). If a detector regresses (stops firing), the count drops and +the canary FAILS (exit 3). + +These are plain JSON files (not git fixtures — aws-posture scans an AWS account, not a repo tree), +so there is no `dotgit/` / `.fixture` rename trick here; the offline-vs-live seam is the +`--canary`/`--no-api`/no-credentials guard inside the checker (mirrors compliance-drift's +API-skip pattern). Each file is shaped like the real `aws ... --output json` response it stands in +for; a few `_Fixture*` helper keys carry the per-resource metric the live path derives from +CloudWatch (so the canary stays deterministic and offline). + +| Fixture file | Stands in for | Planted finding | Count | +|---|---|---|---| +| `cost-anomalies.json` | `aws ce get-anomalies` | 1 anomaly TotalImpact ≥ threshold (the other is below threshold → must NOT fire) | 1 | +| `describe-instances.json` | `aws ec2 describe-instances` | 1 `stopped` instance still paying for its EBS root (the `running` one must NOT fire) | 1 | +| `describe-volumes.json` | `aws ec2 describe-volumes` | 1 `available` (unattached) volume (the `in-use` one must NOT fire) | 1 | +| `describe-addresses.json` | `aws ec2 describe-addresses` | 1 EIP with no association (the associated one must NOT fire) | 1 | +| `describe-nat-gateways.json` | `aws ec2 describe-nat-gateways` | 1 `available` NAT with ~0 bytes out / 14d (the busy one must NOT fire) | 1 | +| `describe-load-balancers.json` | `aws elbv2 describe-load-balancers` | 1 ALB with 0 healthy targets (the one with 3 must NOT fire) | 1 | +| `describe-db-instances.json` | `aws rds describe-db-instances` | 1 `available` RDS with 0 connections / 14d (the busy one must NOT fire) | 1 | + +Total = **7** (`EXPECTED_FINDING_COUNT`). + +aws-posture **complements** GuardDuty / Security Hub / Config (design §4 / Tier-2) — it is an +idle/anomalous-**spend** + idle-resource posture watch, not a threat detector, and never alarms on +missing data (a skipped/credential-less live call is noted, never counted — memory +`feedback_cloudwatch_alarms`). + +When you add/remove a detector or fixture, update both the fixture and `EXPECTED_FINDING_COUNT` +in the same commit (the canary edit is itself caught on the next run — design §6.4). diff --git a/security-review/checkers/fixtures/aws-posture/cost-anomalies.json b/security-review/checkers/fixtures/aws-posture/cost-anomalies.json new file mode 100644 index 0000000..4287230 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/cost-anomalies.json @@ -0,0 +1,32 @@ +{ + "Anomalies": [ + { + "AnomalyId": "anomaly-0001", + "AnomalyStartDate": "2026-06-15", + "AnomalyEndDate": "2026-06-17", + "DimensionValue": "Amazon Elastic Compute Cloud - Compute", + "RootCauses": [ + { "Service": "Amazon Elastic Compute Cloud - Compute", "Region": "us-east-1" } + ], + "Impact": { + "MaxImpact": 142.55, + "TotalImpact": 268.40, + "TotalActualSpend": 410.10, + "TotalExpectedSpend": 141.70 + }, + "Feedback": "NO_FEEDBACK" + }, + { + "AnomalyId": "anomaly-0002-below-threshold", + "AnomalyStartDate": "2026-06-16", + "DimensionValue": "AWS Lambda", + "Impact": { + "MaxImpact": 1.10, + "TotalImpact": 2.05, + "TotalActualSpend": 9.00, + "TotalExpectedSpend": 6.95 + }, + "Feedback": "NO_FEEDBACK" + } + ] +} diff --git a/security-review/checkers/fixtures/aws-posture/describe-addresses.json b/security-review/checkers/fixtures/aws-posture/describe-addresses.json new file mode 100644 index 0000000..81df327 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/describe-addresses.json @@ -0,0 +1,17 @@ +{ + "Addresses": [ + { + "PublicIp": "52.10.20.30", + "AllocationId": "eipalloc-idle-7001", + "Domain": "vpc", + "Tags": [ { "Key": "Name", "Value": "leftover-nat-eip" } ] + }, + { + "PublicIp": "52.40.50.60", + "AllocationId": "eipalloc-inuse-7002", + "Domain": "vpc", + "InstanceId": "i-0ff99ee88dd77cc66", + "AssociationId": "eipassoc-active-0001" + } + ] +} diff --git a/security-review/checkers/fixtures/aws-posture/describe-db-instances.json b/security-review/checkers/fixtures/aws-posture/describe-db-instances.json new file mode 100644 index 0000000..a7e4bcf --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/describe-db-instances.json @@ -0,0 +1,20 @@ +{ + "DBInstances": [ + { + "DBInstanceIdentifier": "idle-reporting-db", + "DBInstanceClass": "db.r5.large", + "Engine": "postgres", + "DBInstanceStatus": "available", + "MultiAZ": false, + "_FixtureMaxConnectionsLast14d": 0 + }, + { + "DBInstanceIdentifier": "prod-app-db", + "DBInstanceClass": "db.t3.medium", + "Engine": "postgres", + "DBInstanceStatus": "available", + "MultiAZ": true, + "_FixtureMaxConnectionsLast14d": 47 + } + ] +} diff --git a/security-review/checkers/fixtures/aws-posture/describe-instances.json b/security-review/checkers/fixtures/aws-posture/describe-instances.json new file mode 100644 index 0000000..dfea016 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/describe-instances.json @@ -0,0 +1,27 @@ +{ + "Reservations": [ + { + "Instances": [ + { + "InstanceId": "i-0aa11bb22cc33dd44", + "InstanceType": "m5.large", + "State": { "Name": "stopped" }, + "StateTransitionReason": "User initiated (2026-02-01 09:14:00 GMT)", + "BlockDeviceMappings": [ + { "DeviceName": "/dev/xvda", "Ebs": { "VolumeId": "vol-stopped-root-001", "Status": "attached" } } + ], + "Tags": [ { "Key": "Name", "Value": "old-batch-runner" } ] + }, + { + "InstanceId": "i-0ff99ee88dd77cc66", + "InstanceType": "t3.micro", + "State": { "Name": "running" }, + "BlockDeviceMappings": [ + { "DeviceName": "/dev/xvda", "Ebs": { "VolumeId": "vol-running-root-002", "Status": "attached" } } + ], + "Tags": [ { "Key": "Name", "Value": "active-web" } ] + } + ] + } + ] +} diff --git a/security-review/checkers/fixtures/aws-posture/describe-load-balancers.json b/security-review/checkers/fixtures/aws-posture/describe-load-balancers.json new file mode 100644 index 0000000..ba4b1f9 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/describe-load-balancers.json @@ -0,0 +1,18 @@ +{ + "LoadBalancers": [ + { + "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:328440206208:loadbalancer/app/idle-alb/abc", + "LoadBalancerName": "idle-alb", + "Type": "application", + "State": { "Code": "active" }, + "_FixtureHealthyTargetCount": 0 + }, + { + "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:328440206208:loadbalancer/app/active-alb/def", + "LoadBalancerName": "active-alb", + "Type": "application", + "State": { "Code": "active" }, + "_FixtureHealthyTargetCount": 3 + } + ] +} diff --git a/security-review/checkers/fixtures/aws-posture/describe-nat-gateways.json b/security-review/checkers/fixtures/aws-posture/describe-nat-gateways.json new file mode 100644 index 0000000..328add3 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/describe-nat-gateways.json @@ -0,0 +1,19 @@ +{ + "NatGateways": [ + { + "NatGatewayId": "nat-idle-6001", + "State": "available", + "SubnetId": "subnet-abc123", + "VpcId": "vpc-def456", + "Tags": [ { "Key": "Name", "Value": "unused-private-subnet-nat" } ], + "_FixtureBytesOutLast14d": 0 + }, + { + "NatGatewayId": "nat-active-6002", + "State": "available", + "SubnetId": "subnet-xyz789", + "VpcId": "vpc-def456", + "_FixtureBytesOutLast14d": 9842113 + } + ] +} diff --git a/security-review/checkers/fixtures/aws-posture/describe-volumes.json b/security-review/checkers/fixtures/aws-posture/describe-volumes.json new file mode 100644 index 0000000..e340072 --- /dev/null +++ b/security-review/checkers/fixtures/aws-posture/describe-volumes.json @@ -0,0 +1,20 @@ +{ + "Volumes": [ + { + "VolumeId": "vol-unattached-9001", + "Size": 500, + "VolumeType": "gp3", + "State": "available", + "CreateTime": "2025-11-02T18:00:00.000Z", + "Attachments": [], + "Tags": [ { "Key": "Name", "Value": "orphaned-data-disk" } ] + }, + { + "VolumeId": "vol-running-root-002", + "Size": 8, + "VolumeType": "gp3", + "State": "in-use", + "Attachments": [ { "InstanceId": "i-0ff99ee88dd77cc66", "State": "attached" } ] + } + ] +} diff --git a/security-review/checkers/fixtures/doc-drift/EXPECTED_DRIFT_COUNT b/security-review/checkers/fixtures/doc-drift/EXPECTED_DRIFT_COUNT new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/EXPECTED_DRIFT_COUNT @@ -0,0 +1 @@ +4 diff --git a/security-review/checkers/fixtures/doc-drift/README.md b/security-review/checkers/fixtures/doc-drift/README.md new file mode 100644 index 0000000..248364c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/README.md @@ -0,0 +1,43 @@ +# doc-drift canary fixtures + +Planted-drift corpus for `checkers/doc-drift.sh --canary` (offline, no network/token). +The checker asserts the total drift count equals `EXPECTED_DRIFT_COUNT` (anti-complacency +floor, design §6.4). If a check regresses (stops firing), the count drops and the canary +FAILS (exit 3). + +doc-drift flags repos whose **architecture moved but the README did not** (design §4, +doc-drift row). It is deliberately deterministic and grounded — no fuzzy LLM judgment. The +LLM judge layer (Gemini large-context) is a later enhancement and is inert offline (see the +`maybe_judge` stub in the checker). + +Each fixture is a real git checkout (its `.git` is shipped as `dotgit/` so it commits into +THIS repo without becoming a nested submodule; the checker renames it back to `.git/` at run +time, the same trick `compliance-drift.sh` / `dependency-cve.sh` use). Real commit history is +required because the staleness check reads `git log` dates. + +## Detected drift (the deterministic checklist) + +| Check | Rule cited | What fires | +|---|---|---| +| `readme-omits-component` | global CLAUDE.md: "README must accurately describe architecture, services, data flow" | A README exists but omits mention of a major existing component present in the tree: a top-level service dir, a SAM/CDK stack (`template.yaml` / `app.py` / `cdk.json`), a Lambda handler dir, or an `openapi`/`docs` API spec. | +| `readme-stale-vs-code` | global CLAUDE.md: "update the README in the same commit" as functionality changes | The README's last-touched commit is far older than the newest code commit (≥ `DOC_DRIFT_STALE_DAYS` days) AND ≥ `DOC_DRIFT_STALE_COMMITS` substantial code commits landed after the README was last touched. | + +A repo with **no README at all** is SKIPPED by doc-drift, not flagged — `readme-present` is +`compliance-drift.sh`'s job, and double-flagging would be a false alarm +(memory `feedback_cloudwatch_alarms`). + +## Fixtures + +| Fixture | Planted drift | Count | +|---|---|---| +| `clean-repo` | none — README names every component (`api/`, the SAM stack, `handlers/`, `openapi/`) and the README was committed alongside the code | 0 | +| `drift-omits-repo` | README mentions only `notifier-service`; omits `payments-service/`, the SAM `template.yaml` stack, and the `handlers/charge` Lambda dir | 3 | +| `drift-stale-repo` | README names its one component (no omission) but was last touched 2026-01-05 while 5 substantial code commits landed in 2026-06 — stale | 1 | +| `no-readme-repo` | no README — doc-drift SKIPS it (must NOT fire; compliance-drift owns this) | 0 | + +Total = **4** (`EXPECTED_DRIFT_COUNT`). The canary pins the staleness thresholds it was +authored against (`DOC_DRIFT_STALE_DAYS`, `DOC_DRIFT_STALE_COMMITS`) internally so it is +deterministic regardless of the operator's env. + +When you add/remove a check or fixture, update both the fixture and `EXPECTED_DRIFT_COUNT` +in the same commit (the canary edit is itself caught on the next run — design §6.4). diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/README.md b/security-review/checkers/fixtures/doc-drift/clean-repo/README.md new file mode 100644 index 0000000..bfde979 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/README.md @@ -0,0 +1,10 @@ +# clean-repo + +Well-documented service. Architecture: + +- The **api/** service exposes the public HTTP surface. +- A SAM stack (see template.yaml) provisions the IngestFn Lambda. +- Lambda handler code lives under handlers/ingest. +- The HTTP contract is published in the openapi/ spec. + +Data flow: api -> IngestFn -> downstream. diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/api/main.go b/security-review/checkers/fixtures/doc-drift/clean-repo/api/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/api/main.go @@ -0,0 +1 @@ +package main diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/COMMIT_EDITMSG b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/COMMIT_EDITMSG new file mode 100644 index 0000000..ffc6555 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/COMMIT_EDITMSG @@ -0,0 +1 @@ +init: code + matching README diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/HEAD b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/config b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/config new file mode 100644 index 0000000..8bb2ccd --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/config @@ -0,0 +1,10 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[user] + email = t@t + name = t diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/description b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/applypatch-msg.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/applypatch-msg.sample new file mode 100755 index 0000000..a5d7b84 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/applypatch-msg.sample @@ -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+"$@"} +: diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/commit-msg.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/commit-msg.sample new file mode 100755 index 0000000..b58d118 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/commit-msg.sample @@ -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 +} diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/fsmonitor-watchman.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/fsmonitor-watchman.sample @@ -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 $/; }; + + # 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; +} diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/post-update.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/post-update.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-applypatch.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-applypatch.sample @@ -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+"$@"} +: diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-commit.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-commit.sample new file mode 100755 index 0000000..29ed5ee --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-commit.sample @@ -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 -- diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-merge-commit.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-merge-commit.sample @@ -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" +: diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-push.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-push.sample @@ -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: +# +# +# +# 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 &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-rebase.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-rebase.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-receive.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/pre-receive.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/prepare-commit-msg.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/prepare-commit-msg.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/push-to-checkout.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/push-to-checkout.sample @@ -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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/update.sample b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/update.sample new file mode 100755 index 0000000..c4d426b --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/hooks/update.sample @@ -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 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/index b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/index new file mode 100644 index 0000000..e228a0e Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/index differ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/info/exclude b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/info/exclude @@ -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] +# *~ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/logs/HEAD b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/logs/HEAD new file mode 100644 index 0000000..d44addb --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/logs/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 e5c55ac820d3272863a874fc1e202908241c2acf t 1780329600 -0400 commit (initial): init: code + matching README diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/logs/refs/heads/main b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/logs/refs/heads/main new file mode 100644 index 0000000..d44addb --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 e5c55ac820d3272863a874fc1e202908241c2acf t 1780329600 -0400 commit (initial): init: code + matching README diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/06/ab7d0f9a35a7d1070711496d6ca1cb892a258f b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/06/ab7d0f9a35a7d1070711496d6ca1cb892a258f new file mode 100644 index 0000000..8182c9a Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/06/ab7d0f9a35a7d1070711496d6ca1cb892a258f differ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/0b/5c163a57c647bd824a907f7e0b9aa6335a7d77 b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/0b/5c163a57c647bd824a907f7e0b9aa6335a7d77 new file mode 100644 index 0000000..657ddba Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/0b/5c163a57c647bd824a907f7e0b9aa6335a7d77 differ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/1f/86368c694ecb3d9d64788a988ca4a5365e4df8 b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/1f/86368c694ecb3d9d64788a988ca4a5365e4df8 new file mode 100644 index 0000000..502fcf6 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/1f/86368c694ecb3d9d64788a988ca4a5365e4df8 differ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/51/3273c393a63fbff76e46c8a8ed159bd9e83a1c b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/51/3273c393a63fbff76e46c8a8ed159bd9e83a1c new file mode 100644 index 0000000..dee7780 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/51/3273c393a63fbff76e46c8a8ed159bd9e83a1c @@ -0,0 +1 @@ +xe�1 Â0F�ó+nëH,¼Í%àjC�ÓrŠ�&å.ü÷=gáÞðßœë ¾N×ÇiÝrj*¯©MÄò®¡;;כּè:9yª€ âHü!Î$bU¬š½7w’ºóB‚àV^$-”ÄïF-bØËÒôÎí¥-¡ \ No newline at end of file diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/88/72f0d44bfbbfa113423377b41597af8faacb8d b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/88/72f0d44bfbbfa113423377b41597af8faacb8d new file mode 100644 index 0000000..79dfdce Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/88/72f0d44bfbbfa113423377b41597af8faacb8d differ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/98/ec3d6272badf42441f11b2c53b42961a33f5f7 b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/98/ec3d6272badf42441f11b2c53b42961a33f5f7 new file mode 100644 index 0000000..afd3888 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/98/ec3d6272badf42441f11b2c53b42961a33f5f7 differ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/a2/549621f3ce0547771b96e53f14e2fcd74a3e50 b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/a2/549621f3ce0547771b96e53f14e2fcd74a3e50 new file mode 100644 index 0000000..d0257c5 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/a2/549621f3ce0547771b96e53f14e2fcd74a3e50 differ diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/b9/270df7070cc6a5e7dbdec610a7ce4f54c47b20 b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/b9/270df7070cc6a5e7dbdec610a7ce4f54c47b20 new file mode 100644 index 0000000..302def6 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/b9/270df7070cc6a5e7dbdec610a7ce4f54c47b20 @@ -0,0 +1 @@ +x+)JMU06e040031QÈMÌÌÓKÏg`[]Ë?ËtùEvvAÏÜœ…§;µTû É \ No newline at end of file diff --git a/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/bf/de979a9dc263aafe98de15bc024e98440984d7 b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/bf/de979a9dc263aafe98de15bc024e98440984d7 new file mode 100644 index 0000000..ed685cd --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/clean-repo/dotgit/objects/bf/de979a9dc263aafe98de15bc024e98440984d7 @@ -0,0 +1 @@ +x=�AKÄ0…=çW<ð¢…vAñ²¡ ¢  Xð\).*$/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 +} diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/fsmonitor-watchman.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/fsmonitor-watchman.sample @@ -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 $/; }; + + # 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; +} diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/post-update.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/post-update.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-applypatch.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-applypatch.sample @@ -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+"$@"} +: diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-commit.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-commit.sample new file mode 100755 index 0000000..29ed5ee --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-commit.sample @@ -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 -- diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-merge-commit.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-merge-commit.sample @@ -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" +: diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-push.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-push.sample @@ -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: +# +# +# +# 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 &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-rebase.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-rebase.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-receive.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/pre-receive.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/prepare-commit-msg.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/prepare-commit-msg.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/push-to-checkout.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/push-to-checkout.sample @@ -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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/update.sample b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/update.sample new file mode 100755 index 0000000..c4d426b --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/hooks/update.sample @@ -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 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/index b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/index new file mode 100644 index 0000000..3a1ce27 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/index differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/info/exclude b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/info/exclude @@ -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] +# *~ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/logs/HEAD b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/logs/HEAD new file mode 100644 index 0000000..b2878de --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/logs/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 ff9ded83431a6059a99140b744c351e43bb04eed t 1781107200 -0400 commit (initial): init diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/logs/refs/heads/main b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/logs/refs/heads/main new file mode 100644 index 0000000..b2878de --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 ff9ded83431a6059a99140b744c351e43bb04eed t 1781107200 -0400 commit (initial): init diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/19/366a9a93232cf60a444d9a1d4114bc55084d0f b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/19/366a9a93232cf60a444d9a1d4114bc55084d0f new file mode 100644 index 0000000..3a527c2 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/19/366a9a93232cf60a444d9a1d4114bc55084d0f differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/1f/86368c694ecb3d9d64788a988ca4a5365e4df8 b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/1f/86368c694ecb3d9d64788a988ca4a5365e4df8 new file mode 100644 index 0000000..502fcf6 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/1f/86368c694ecb3d9d64788a988ca4a5365e4df8 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/36/31a0436785d485c1f063f82fe6755d6cf9ee89 b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/36/31a0436785d485c1f063f82fe6755d6cf9ee89 new file mode 100644 index 0000000..f469015 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/36/31a0436785d485c1f063f82fe6755d6cf9ee89 @@ -0,0 +1,2 @@ +xeÍM +1 @a×=EÀ]¡‚àEz�þdH 6’dôúÆ�Û¯©p»?.WèÊ»'y²[R|I™ØÀPßܨÌ>Ð`Šóέ8Ë´-„™b<;jú��Ý@¯rÌþ'3‡uðå?$¡h#vl~(ná e¼6q \ No newline at end of file diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/96/f70ef65cc77ae047293b093219c7f996e1d6fa b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/96/f70ef65cc77ae047293b093219c7f996e1d6fa new file mode 100644 index 0000000..5b2b01f Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/96/f70ef65cc77ae047293b093219c7f996e1d6fa differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/98/ec3d6272badf42441f11b2c53b42961a33f5f7 b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/98/ec3d6272badf42441f11b2c53b42961a33f5f7 new file mode 100644 index 0000000..afd3888 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/98/ec3d6272badf42441f11b2c53b42961a33f5f7 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/162ac5a718f5b673c538badf3506c17d988fc8 b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/162ac5a718f5b673c538badf3506c17d988fc8 new file mode 100644 index 0000000..fb0a314 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/162ac5a718f5b673c538badf3506c17d988fc8 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/24dce1a9a1c4b5bc277b5beeedec49d5604a75 b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/24dce1a9a1c4b5bc277b5beeedec49d5604a75 new file mode 100644 index 0000000..1f0b023 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/24dce1a9a1c4b5bc277b5beeedec49d5604a75 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/81d157c401c61bebf0c4ef31dc7d3777edbcba b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/81d157c401c61bebf0c4ef31dc7d3777edbcba new file mode 100644 index 0000000..c040cc4 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/9a/81d157c401c61bebf0c4ef31dc7d3777edbcba differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/a1/7e6a98edf78d6c4f540aca77932f81e2b4ccdc b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/a1/7e6a98edf78d6c4f540aca77932f81e2b4ccdc new file mode 100644 index 0000000..6acf046 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/a1/7e6a98edf78d6c4f540aca77932f81e2b4ccdc differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/cc/b1aee04842afc8dcb1e6b7f292de8ddef50f73 b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/cc/b1aee04842afc8dcb1e6b7f292de8ddef50f73 new file mode 100644 index 0000000..16e0835 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/cc/b1aee04842afc8dcb1e6b7f292de8ddef50f73 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/ff/9ded83431a6059a99140b744c351e43bb04eed b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/ff/9ded83431a6059a99140b744c351e43bb04eed new file mode 100644 index 0000000..c4cb0bf Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/objects/ff/9ded83431a6059a99140b744c351e43bb04eed differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/refs/heads/main b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/refs/heads/main new file mode 100644 index 0000000..555f7a3 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/dotgit/refs/heads/main @@ -0,0 +1 @@ +ff9ded83431a6059a99140b744c351e43bb04eed diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/handlers/charge/app.py b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/handlers/charge/app.py new file mode 100644 index 0000000..b473d36 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/handlers/charge/app.py @@ -0,0 +1,2 @@ +def handler(event, ctx): + return {} diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/notifier-service/main.py b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/notifier-service/main.py new file mode 100644 index 0000000..af2e9f2 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/notifier-service/main.py @@ -0,0 +1,2 @@ +class Notifier: + pass diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/payments-service/main.py b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/payments-service/main.py new file mode 100644 index 0000000..2ffacb2 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/payments-service/main.py @@ -0,0 +1,2 @@ +class Payments: + pass diff --git a/security-review/checkers/fixtures/doc-drift/drift-omits-repo/template.yaml b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/template.yaml new file mode 100644 index 0000000..9a24dce --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-omits-repo/template.yaml @@ -0,0 +1,5 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Resources: + ChargeFn: + Type: AWS::Serverless::Function diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/README.md b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/README.md new file mode 100644 index 0000000..c7d8d3f --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/README.md @@ -0,0 +1,5 @@ +# drift-stale-repo + +- The **worker-service** processes the queue. + +Architecture is documented and complete as of this writing. diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/COMMIT_EDITMSG b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/COMMIT_EDITMSG new file mode 100644 index 0000000..67a10ad --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/COMMIT_EDITMSG @@ -0,0 +1 @@ +feat: add feature_5 to worker-service diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/HEAD b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/config b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/config new file mode 100644 index 0000000..8bb2ccd --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/config @@ -0,0 +1,10 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[user] + email = t@t + name = t diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/description b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/applypatch-msg.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/applypatch-msg.sample new file mode 100755 index 0000000..a5d7b84 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/applypatch-msg.sample @@ -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+"$@"} +: diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/commit-msg.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/commit-msg.sample new file mode 100755 index 0000000..b58d118 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/commit-msg.sample @@ -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 +} diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/fsmonitor-watchman.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/fsmonitor-watchman.sample @@ -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 $/; }; + + # 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; +} diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/post-update.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/post-update.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-applypatch.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-applypatch.sample @@ -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+"$@"} +: diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-commit.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-commit.sample new file mode 100755 index 0000000..29ed5ee --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-commit.sample @@ -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 -- diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-merge-commit.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-merge-commit.sample @@ -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" +: diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-push.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-push.sample @@ -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: +# +# +# +# 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 &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-rebase.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-rebase.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-receive.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/pre-receive.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/prepare-commit-msg.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/prepare-commit-msg.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/push-to-checkout.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/push-to-checkout.sample @@ -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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/update.sample b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/update.sample new file mode 100755 index 0000000..c4d426b --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/hooks/update.sample @@ -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 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/index b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/index new file mode 100644 index 0000000..a4e5a46 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/index differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/info/exclude b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/info/exclude @@ -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] +# *~ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/logs/HEAD b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/logs/HEAD new file mode 100644 index 0000000..64f7bd2 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/logs/HEAD @@ -0,0 +1,6 @@ +0000000000000000000000000000000000000000 7687db2a5781d77b42ced78a6bca02c37a8dbbf0 t 1767632400 -0500 commit (initial): init: worker-service + README +7687db2a5781d77b42ced78a6bca02c37a8dbbf0 af8b041ef281bb9d89401efcdf549a9a452f0ecf t 1781193600 -0400 commit: feat: add feature_1 to worker-service +af8b041ef281bb9d89401efcdf549a9a452f0ecf 93a7db735d0cfe42f0a57d956915f5e5a7f87378 t 1781280000 -0400 commit: feat: add feature_2 to worker-service +93a7db735d0cfe42f0a57d956915f5e5a7f87378 9c2018ca90ae8305bec517e0b8b91b5d8a755404 t 1781366400 -0400 commit: feat: add feature_3 to worker-service +9c2018ca90ae8305bec517e0b8b91b5d8a755404 6b7c13685ae818268d30ed3cf27c86da2820f186 t 1781452800 -0400 commit: feat: add feature_4 to worker-service +6b7c13685ae818268d30ed3cf27c86da2820f186 0cd9efe3a4957b0f5336c33f4e40e9e4d779c243 t 1781539200 -0400 commit: feat: add feature_5 to worker-service diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/logs/refs/heads/main b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/logs/refs/heads/main new file mode 100644 index 0000000..64f7bd2 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/logs/refs/heads/main @@ -0,0 +1,6 @@ +0000000000000000000000000000000000000000 7687db2a5781d77b42ced78a6bca02c37a8dbbf0 t 1767632400 -0500 commit (initial): init: worker-service + README +7687db2a5781d77b42ced78a6bca02c37a8dbbf0 af8b041ef281bb9d89401efcdf549a9a452f0ecf t 1781193600 -0400 commit: feat: add feature_1 to worker-service +af8b041ef281bb9d89401efcdf549a9a452f0ecf 93a7db735d0cfe42f0a57d956915f5e5a7f87378 t 1781280000 -0400 commit: feat: add feature_2 to worker-service +93a7db735d0cfe42f0a57d956915f5e5a7f87378 9c2018ca90ae8305bec517e0b8b91b5d8a755404 t 1781366400 -0400 commit: feat: add feature_3 to worker-service +9c2018ca90ae8305bec517e0b8b91b5d8a755404 6b7c13685ae818268d30ed3cf27c86da2820f186 t 1781452800 -0400 commit: feat: add feature_4 to worker-service +6b7c13685ae818268d30ed3cf27c86da2820f186 0cd9efe3a4957b0f5336c33f4e40e9e4d779c243 t 1781539200 -0400 commit: feat: add feature_5 to worker-service diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/06/2858f6d6f54e5a930a45178929532313b7a231 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/06/2858f6d6f54e5a930a45178929532313b7a231 new file mode 100644 index 0000000..60a4d11 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/06/2858f6d6f54e5a930a45178929532313b7a231 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/0c/30159d993e4b7fc86add22ed6ce984618552ef b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/0c/30159d993e4b7fc86add22ed6ce984618552ef new file mode 100644 index 0000000..26b4e48 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/0c/30159d993e4b7fc86add22ed6ce984618552ef differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/0c/d9efe3a4957b0f5336c33f4e40e9e4d779c243 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/0c/d9efe3a4957b0f5336c33f4e40e9e4d779c243 new file mode 100644 index 0000000..5b45f57 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/0c/d9efe3a4957b0f5336c33f4e40e9e4d779c243 @@ -0,0 +1,3 @@ +x}ŽK +Â@]Ï)ú�ùO+"ÞD&Ý=$FÆŽ^ßè\<¨EA=ZæyRðÖî´‹€ÇäÈ2¥8úm.Ÿ°pÄ6Jv¥RàÐ#+¨ùB˜¹zô¶9̦®z]:(õ¬'p] +û­ ƒ�ÖúQù£˜&UP™áKk—K]à½ô›ôá)ý5‘˜²ù<œ \ No newline at end of file diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/17/001f0544766545e2b63b2d3e7454ffa28cba39 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/17/001f0544766545e2b63b2d3e7454ffa28cba39 new file mode 100644 index 0000000..5d7e377 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/17/001f0544766545e2b63b2d3e7454ffa28cba39 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/34/a52c1c070ac4df6225a1ef1a73643bcf92ef44 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/34/a52c1c070ac4df6225a1ef1a73643bcf92ef44 new file mode 100644 index 0000000..acdcb43 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/34/a52c1c070ac4df6225a1ef1a73643bcf92ef44 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/4c/217577a9492a8030c5980e5d6796768781ed6d b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/4c/217577a9492a8030c5980e5d6796768781ed6d new file mode 100644 index 0000000..1032d34 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/4c/217577a9492a8030c5980e5d6796768781ed6d differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/59/c4d8defd13e7623f2af0073db64ce8ec4a1612 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/59/c4d8defd13e7623f2af0073db64ce8ec4a1612 new file mode 100644 index 0000000..abf5582 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/59/c4d8defd13e7623f2af0073db64ce8ec4a1612 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/5e/a038ee524a2a7200c3e4eb22febc5aeceb24a5 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/5e/a038ee524a2a7200c3e4eb22febc5aeceb24a5 new file mode 100644 index 0000000..751f738 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/5e/a038ee524a2a7200c3e4eb22febc5aeceb24a5 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/60/3e3162216f19595bd6df1db44faf0f3c168f8f b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/60/3e3162216f19595bd6df1db44faf0f3c168f8f new file mode 100644 index 0000000..ea99a67 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/60/3e3162216f19595bd6df1db44faf0f3c168f8f differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/64/74695394af5333896c7b296879398ef18776ca b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/64/74695394af5333896c7b296879398ef18776ca new file mode 100644 index 0000000..240db10 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/64/74695394af5333896c7b296879398ef18776ca @@ -0,0 +1 @@ +x+)JMU044e040031QHKM,)-J�7Ô+¨dضBó‡‚ÓÍØ•7¹ÜyßÞ½µâ3š:#�ºþEÖ¶‹ûËŽ8ö,ež ÍÒaÖU—›˜™Râ£XZ¾ÒS«Áàè ¾Øôieí�osþw.f \ No newline at end of file diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/6b/7c13685ae818268d30ed3cf27c86da2820f186 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/6b/7c13685ae818268d30ed3cf27c86da2820f186 new file mode 100644 index 0000000..a80de5e --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/6b/7c13685ae818268d30ed3cf27c86da2820f186 @@ -0,0 +1,2 @@ +x}ÎA +1 @Q×=E. ¤�Ħ"âM$m3(¢#5êõEàî/Þâ·åz=;$Ä•3(3Ó¦ÑLš:öž,"KÊ9餽2ç"sÑp×a7‡ÒFiZPM&äj�c6¬RK¬ÜE33!}úià°óƒï!f‰ÄIa�„ÚoÄí ³©oA{‡o=‡ |�÷2.6Ö¯s³ð¦·<’ \ No newline at end of file diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/76/87db2a5781d77b42ced78a6bca02c37a8dbbf0 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/76/87db2a5781d77b42ced78a6bca02c37a8dbbf0 new file mode 100644 index 0000000..3af3019 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/76/87db2a5781d77b42ced78a6bca02c37a8dbbf0 @@ -0,0 +1,2 @@ +x}ÌA +Â0Fa×9Åì¥0�L»tã Òð‹AÚ@õúŠp÷/×y.F½êÊ@‰ý¢IRæì¡˜D®˜rHÈßÖ\zÚ­62ÚÙÁöÔÇ!^”™:Ì.ÿƆ?Ä•¥Ø–ÞµÝѺÚ«dК.ãñtݽ,f \ No newline at end of file diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/76/fcd8c1bd06f4648062d7865d83e0ec5279d8f8 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/76/fcd8c1bd06f4648062d7865d83e0ec5279d8f8 new file mode 100644 index 0000000..8b238cc Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/76/fcd8c1bd06f4648062d7865d83e0ec5279d8f8 differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/8d/2121584af4558168be908795ae74dfbda6169e b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/8d/2121584af4558168be908795ae74dfbda6169e new file mode 100644 index 0000000..0fa22fd Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/8d/2121584af4558168be908795ae74dfbda6169e differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/8f/a23b3da38f76c4418ca50353902b048836568f b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/8f/a23b3da38f76c4418ca50353902b048836568f new file mode 100644 index 0000000..466ddd2 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/8f/a23b3da38f76c4418ca50353902b048836568f differ diff --git a/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/93/a7db735d0cfe42f0a57d956915f5e5a7f87378 b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/93/a7db735d0cfe42f0a57d956915f5e5a7f87378 new file mode 100644 index 0000000..fb04569 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/drift-stale-repo/dotgit/objects/93/a7db735d0cfe42f0a57d956915f5e5a7f87378 @@ -0,0 +1,2 @@ +x}ŽQ +1 Dýî)r�…´´ÛVD¼‰¤MŠ‹h¥fõú®ÀùšÃcj¿Ý‡¸Ó!„|ž1òź™XR䊥†è5lÑ—Ø\).*$/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 +} diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/fsmonitor-watchman.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..23e856f --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/fsmonitor-watchman.sample @@ -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 $/; }; + + # 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; +} diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/post-update.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/post-update.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-applypatch.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-applypatch.sample @@ -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+"$@"} +: diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-commit.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-commit.sample new file mode 100755 index 0000000..29ed5ee --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-commit.sample @@ -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 -- diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-merge-commit.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-merge-commit.sample new file mode 100755 index 0000000..399eab1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-merge-commit.sample @@ -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" +: diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-push.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-push.sample new file mode 100755 index 0000000..4ce688d --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-push.sample @@ -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: +# +# +# +# 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 &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-rebase.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-rebase.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-receive.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/pre-receive.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/prepare-commit-msg.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/prepare-commit-msg.sample @@ -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 diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/push-to-checkout.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/push-to-checkout.sample new file mode 100755 index 0000000..af5a0c0 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/push-to-checkout.sample @@ -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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/update.sample b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/update.sample new file mode 100755 index 0000000..c4d426b --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/hooks/update.sample @@ -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 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&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 &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 diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/index b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/index new file mode 100644 index 0000000..8a6bc03 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/index differ diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/info/exclude b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/info/exclude @@ -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] +# *~ diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/logs/HEAD b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/logs/HEAD new file mode 100644 index 0000000..4a14af5 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/logs/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 aa56c53150461eebd587634d76f26cf626a18e3b t 1781107200 -0400 commit (initial): init: code, no README diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/logs/refs/heads/main b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/logs/refs/heads/main new file mode 100644 index 0000000..4a14af5 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 aa56c53150461eebd587634d76f26cf626a18e3b t 1781107200 -0400 commit (initial): init: code, no README diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/2a/508708278fea9839105388e392dda2a0b42527 b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/2a/508708278fea9839105388e392dda2a0b42527 new file mode 100644 index 0000000..1ad3453 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/2a/508708278fea9839105388e392dda2a0b42527 differ diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/6e/b474c4350e80479203ab76b02a02822e6c53cb b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/6e/b474c4350e80479203ab76b02a02822e6c53cb new file mode 100644 index 0000000..33dadde Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/6e/b474c4350e80479203ab76b02a02822e6c53cb differ diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/90/7ed9bc9b45714bd6d0be7d42463409082cf085 b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/90/7ed9bc9b45714bd6d0be7d42463409082cf085 new file mode 100644 index 0000000..873f7ff Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/90/7ed9bc9b45714bd6d0be7d42463409082cf085 differ diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/aa/56c53150461eebd587634d76f26cf626a18e3b b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/aa/56c53150461eebd587634d76f26cf626a18e3b new file mode 100644 index 0000000..a3f10e6 Binary files /dev/null and b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/objects/aa/56c53150461eebd587634d76f26cf626a18e3b differ diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/refs/heads/main b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/refs/heads/main new file mode 100644 index 0000000..b5d670a --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/dotgit/refs/heads/main @@ -0,0 +1 @@ +aa56c53150461eebd587634d76f26cf626a18e3b diff --git a/security-review/checkers/fixtures/doc-drift/no-readme-repo/some-service/main.py b/security-review/checkers/fixtures/doc-drift/no-readme-repo/some-service/main.py new file mode 100644 index 0000000..9985397 --- /dev/null +++ b/security-review/checkers/fixtures/doc-drift/no-readme-repo/some-service/main.py @@ -0,0 +1,2 @@ +class S: + pass diff --git a/security-review/iam/CROSS-REVIEW-PACKET.md b/security-review/iam/CROSS-REVIEW-PACKET.md new file mode 100644 index 0000000..f30ef3e --- /dev/null +++ b/security-review/iam/CROSS-REVIEW-PACKET.md @@ -0,0 +1,182 @@ +# Cross-review packet — R720 aws-posture IAM (step-ca → Roles Anywhere → read-only AWS role) + +> **GPT-4.1 cross-review 2026-06-18: APPROVE, no BLOCKs; FIXes applied** +> (`aws:SourceAccount` added to the trust policy; `ec2:DescribeImages` removed from the +> permission policy). NIT answers recorded in `aws-posture-readonly-policy.rationale.md`: +> snapshots = account-owned idle-spend signal (kept); `s3:ListAllMyBuckets` = names-only +> inventory, no object data (kept); no `logs:*` needed; `aws:RequestedRegion` deliberately +> SKIPPED (global-endpoint `ce:*`/`s3:ListAllMyBuckets` could be DENYed by a blanket region pin). + +**Audience:** the mandatory GPT-4.1 IAM cross-review + Adam. +**Status:** these are AUTHORED FILES, nothing is applied to AWS. The review has now PASSED +(APPROVE, no BLOCKs), which unblocks **building** aws-posture (done in this Phase-3 change set, +PROVISIONING-GATED — the checker makes no AWS call until step-ca + Roles Anywhere are stood up). +Approving this packet unblocks provisioning; it does not itself change AWS. + +**Account:** 328440206208 · **Region:** us-east-1 · **Box:** the always-on R720 secrev VM +(single-user, unattended, currently holds a long-lived read-only GitHub PAT). + +## Why this exists + +aws-posture (design D5 / §4) is a weekly idle/anomalous-spend watch (the design flags ≈$330/mo +of waste). It must read Cost Explorer + utilization metrics + an idle-resource inventory from +the account. The decision (D5): **the box stays read-only, and it authenticates to AWS via IAM +Roles Anywhere using short-lived leaf certs issued by a new internal step-ca — NO long-lived +AWS access key on the box.** The short-lived self-expiring leaf is strictly stronger than the +box's existing long-lived PAT. + +## Files in this packet + +| File | What it is | +|---|---| +| `aws-posture-readonly-policy.json` | The least-privilege **permission policy** (valid IAM JSON, applyable as-is). | +| `aws-posture-readonly-policy.rationale.md` | Statement-by-statement least-privilege rationale (IAM JSON can't hold comments). | +| `aws-posture-trust-policy.json` | The role's **trust policy** — who may assume it (Roles Anywhere + pinned cert CN/issuer + pinned trust-anchor ARN). | +| `roles-anywhere-config.json` | The Roles Anywhere **trust anchor + profile** config (pins the step-ca root; 1h session cap). | +| `step-ca-config-sketch.md` | The internal **CA** config + the systemd-timer **auto-renewal** approach. | +| `CROSS-REVIEW-PACKET.md` | This document. | + +## Trust model, end to end + +``` +step-ca ROOT cert (CN="Sea Haven Internal CA - R720 Roles Anywhere") + │ pinned as the Roles Anywhere trust anchor (roles-anywhere-config.json) + â–¼ +step-ca issues a SHORT-LIVED leaf (CN="r720-aws-posture", ~24h, auto-renewed hourly by systemd timer) + │ stored mode-600 on the box; private key never leaves the box; no AWS key on disk + â–¼ +box calls AWS via aws_signing_helper credential-process, signing with the leaf + â–¼ +IAM Roles Anywhere trust anchor (r720-aws-posture-step-ca) + │ validates: leaf chains to the pinned root? yes -> emit session tags + │ aws:PrincipalTag/x509Subject/CN = "r720-aws-posture" + │ aws:PrincipalTag/x509Issuer/CN = "Sea Haven Internal CA - R720 Roles Anywhere" + â–¼ +Roles Anywhere profile (r720-aws-posture-readonly, durationSeconds=3600) + │ binds ONLY the one role + â–¼ +sts:AssumeRole on role/r720-aws-posture-readonly + │ trust policy (aws-posture-trust-policy.json) requires ALL of: + │ (1) Principal = rolesanywhere.amazonaws.com (came via Roles Anywhere) + │ (2) aws:SourceArn = THIS trust anchor (not some other anchor) + │ (2b) aws:SourceAccount = 328440206208 (confused-deputy guard, added in cross-review) + │ (3) x509Subject/CN = "r720-aws-posture" AND x509Issuer/CN = the internal CA + â–¼ +1-hour STS session, permissions = aws-posture-readonly-policy.json (read-only cost + idle inventory) + â–¼ +read-only AWS APIs: ce:Get*, cloudwatch:GetMetric*/DescribeAlarms, ec2/elb/rds:Describe*, + lambda:List/GetFunctionConfiguration, s3:ListAllMyBuckets/GetBucketLocation +``` + +Three independent conditions must ALL hold to assume the role: via Roles Anywhere, from THIS +anchor (in THIS account, via the `aws:SourceAccount` guard added in cross-review), presenting a +leaf with the pinned subject CN + issuer CN. Any one missing → AssumeRole denied. + +## Least-privilege rationale (summary; full table in the rationale .md) + +- **Read-only only.** No write/modify/delete verb in any service. No `iam:*` mutation, no + privilege-escalation path, no `sts` onward-chaining. +- **`Resource: "*"` only where AWS gives no choice.** Cost Explorer, the CloudWatch metric-data + calls, and the EC2/ELB/RDS `Describe*` list operations do not support resource-level ARNs; + least-privilege there is the **action allow-list**, not the resource. There are no wildcard + *actions* (`ce:*`, `ec2:*`) anywhere — every action is an explicit read verb. +- **No data-plane reads.** Deliberately excludes `s3:GetObject`, `secretsmanager:GetSecretValue`, + `ssm:GetParameter*`, `kms:Decrypt`, `logs:GetLogEvents`. The role enumerates and prices the + account; it cannot read application data, secrets, or logs. +- **Tighter than the AWS-managed `ReadOnlyAccess`/`ViewOnlyAccess`** (those include object reads, + table reads, etc.) — this is the small cost+inventory subset aws-posture actually queries. + +## Blast radius if the leaf (or its private key) is compromised + +- **Ceiling = read-only enumeration + pricing of account 328440206208 for ≤1 hour per session** + (STS `durationSeconds=3600`), and only while a valid unexpired leaf exists (~24h leaf life). +- **Cannot:** read S3 object data, read secrets/SSM params, read CloudWatch *logs*, modify or + delete any resource, touch IAM, assume any other role, or act in any other account/region + scope beyond what read-only describe calls expose. +- **Containment levers, fastest first:** + 1. **Disable the Roles Anywhere profile or trust anchor** (`enabled:false`) → immediately stops + all new credential vending, regardless of leaf validity. (seconds) + 2. **Detach/empty the role's permission policy** → any still-live session loses all access at + the next AWS authz check. (seconds) + 3. **Revoke at the CA / rotate the leaf** → step-ca stops renewing; the leaf self-expires within + its ≤24h window even with no action. +- The self-expiring leaf means even a "do nothing" outcome bounds exposure to the leaf lifetime — + unlike the box's current long-lived PAT, which would persist until manually rotated. + +## Rollback (EXERCISED, not just written — design §7 "exercised, not merely written") + +Teardown order is the reverse of provisioning; each step is independently sufficient to cut +access. Tested via a simulated teardown/re-provision against throwaway names (see "Exercise" +below) before this packet is accepted. + +```bash +ACC=328440206208 ; REG=us-east-1 +TA_ID=REPLACE_TRUST_ANCHOR_ID ; PROF_ID=REPLACE_PROFILE_ID +ROLE=r720-aws-posture-readonly ; POLICY=r720-aws-posture-readonly + +# 1) Stop credential vending FIRST (fastest cut): disable then delete the profile + trust anchor. +aws rolesanywhere disable-profile --profile-id "$PROF_ID" --region "$REG" +aws rolesanywhere disable-trust-anchor --trust-anchor-id "$TA_ID" --region "$REG" +aws rolesanywhere delete-profile --profile-id "$PROF_ID" --region "$REG" +aws rolesanywhere delete-trust-anchor --trust-anchor-id "$TA_ID" --region "$REG" + +# 2) Remove the role + its permission policy. +POLICY_ARN="arn:aws:iam::$ACC:policy/$POLICY" +aws iam detach-role-policy --role-name "$ROLE" --policy-arn "$POLICY_ARN" +aws iam delete-role --role-name "$ROLE" +aws iam delete-policy --policy-arn "$POLICY_ARN" + +# 3) Remove the internal CA + the leaf on the box (no AWS state involved). +sudo systemctl disable --now aws-posture-cert-renew.timer step-ca.service +sudo rm -f /etc/aws-posture/leaf.crt /etc/aws-posture/leaf.key +sudo rm -rf /etc/step-ca # destroys root/intermediate/keys -> no further leaves issuable +# (optional) remove the [profile r720-aws-posture] block from ~/.aws/config +``` + +**Re-provision** = re-run `step ca init` (step-ca-config-sketch.md) → re-create role + policy + +trust anchor + profile → drop in the new trust-anchor/profile ARNs. A revert point (VM snapshot +per `feedback_ec2_replacement_snapshot`) is taken before provisioning so the whole change is one +snapshot-restore away from gone. + +### Exercise log (to be completed before acceptance) + +> Run the provision → assume-once (confirm read-only works, confirm a write is denied) → run the +> rollback above against throwaway-suffixed names → confirm AssumeRole now fails and the CA is +> gone. Paste the transcript here. Until this is filled in, the rollback is "written, not +> exercised" and the phase is NOT accepted (design §7). + +## Specific things for the cross-reviewer to scrutinize (with resolutions) + +1. **Trust-policy condition completeness.** Are `aws:PrincipalTag/x509Subject/CN` + + `aws:PrincipalTag/x509Issuer/CN` + `ArnEquals aws:SourceArn` (the trust anchor) sufficient + to prevent any other cert (or another trust anchor in the account) from assuming the role? + Is there a confused-deputy gap I should also pin (e.g. should I add `aws:SourceAccount`)? + → **RESOLVED (FIX applied):** added `aws:SourceAccount = 328440206208` to the StringEquals + condition. The trust now pins anchor (SourceArn) **and** account (SourceAccount) plus the + cert CN/issuer — closing the confused-deputy gap the reviewer raised. +2. **`Resource: "*"` statements.** Confirm each is an API that genuinely has no resource-level + support, and that no statement could be tightened with a condition (e.g. `aws:RequestedRegion` + = us-east-1) without breaking the checker. + → **RESOLVED (NIT, SKIPPED with rationale):** every `Resource:"*"` statement is an + API family without resource-level ARNs (ce, the cloudwatch metric-data calls, ec2/elb/rds + `Describe*`). `aws:RequestedRegion` is **NOT** applied — `ce:*` and `s3:ListAllMyBuckets` are + global-endpoint services that a blanket region condition could DENY. Full reasoning in + `aws-posture-readonly-policy.rationale.md` ("Cross-review NIT answers"). +3. **Action allow-list.** Anything in here that is NOT needed for idle/anomalous-spend (i.e. + over-grant), or any read verb that leaks data we don't want (the intent is pricing + inventory, + no object/secret/log data). + → **RESOLVED (FIX applied):** removed `ec2:DescribeImages` (AMIs are not an idle-spend signal). + `ec2:DescribeSnapshots` kept (orphan-snapshot waste, account-owned metadata only); + `s3:ListAllMyBuckets` kept (bucket *names* only, no `s3:GetObject`); no `logs:*` granted. +4. **Session duration vs leaf lifetime.** 1h STS session + ~24h leaf — acceptable blast window? + → Accepted as-is (no change requested). +5. **Rollback ordering.** Is disabling the profile/anchor first the correct fastest-cut order, + and does step 2/3 leave any orphaned grant? + → Accepted as-is (no change requested). + +## Process note + +Per global instructions this IAM change ALSO requires the GPT-4.1 cross-family review run via +`python3 ~/Documents/repositories/orchestrator/run.py ""` (it is an IAM +role/policy + trust-anchor change). This packet is the input to that review; aws-posture is not +built until the review is recorded. diff --git a/security-review/iam/README.md b/security-review/iam/README.md new file mode 100644 index 0000000..82add38 --- /dev/null +++ b/security-review/iam/README.md @@ -0,0 +1,30 @@ +# security-review/iam/ — Phase-3 IAM artifacts (authored for cross-review, NOT applied) + +These are the IAM / Roles Anywhere / step-ca artifacts for the R720 agent-team **aws-posture** +checker (design `docs/r720-agent-team-design.md` D5 / §4 / §6.3 / §7 Phase 3). + +**Nothing here is applied to AWS.** They are FILES for the mandatory GPT-4.1 IAM cross-review. + +**Cross-review status (2026-06-18): APPROVE, no BLOCKs.** FIXes applied — `aws:SourceAccount` +added to the trust policy; `ec2:DescribeImages` removed from the permission policy (see +`CROSS-REVIEW-PACKET.md` header + `aws-posture-readonly-policy.rationale.md`). The review passing +**unblocked building** `../checkers/aws-posture.sh` (built in this Phase-3 change set). That +checker stays **PROVISIONING-GATED**: it makes NO AWS call until step-ca + the Roles Anywhere +trust anchor + this role are stood up. Provisioning happens only after the review is recorded +(design §7, B3) — and a VM snapshot is taken first per `feedback_ec2_replacement_snapshot`. + +Decision (D5): the box stays **read-only** and authenticates to AWS via **Roles Anywhere** +short-lived leaf certs issued by a new internal **step-ca** — **no long-lived AWS key on the +box**. + +| File | Purpose | +|---|---| +| `CROSS-REVIEW-PACKET.md` | **Start here.** End-to-end trust model, least-privilege rationale, blast radius, exercised rollback, and the specific items for the reviewer. | +| `aws-posture-readonly-policy.json` | Least-privilege read-only permission policy (valid, applyable IAM JSON). | +| `aws-posture-readonly-policy.rationale.md` | Statement-by-statement rationale (IAM JSON can't carry comments). | +| `aws-posture-trust-policy.json` | Role trust policy — pins Roles Anywhere + the leaf subject/issuer CN + trust-anchor ARN. | +| `roles-anywhere-config.json` | Trust-anchor (pins step-ca root) + profile (1h session) config. | +| `step-ca-config-sketch.md` | Internal CA config + systemd-timer auto-renewal of the short-lived leaf. | + +Per global instructions this IAM change also requires the GPT-4.1 cross-family review via +`orchestrator/run.py`; this directory is that review's input. diff --git a/security-review/iam/aws-posture-readonly-policy.json b/security-review/iam/aws-posture-readonly-policy.json new file mode 100644 index 0000000..482fec6 --- /dev/null +++ b/security-review/iam/aws-posture-readonly-policy.json @@ -0,0 +1,71 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "CostAndUsageReadOnly", + "Effect": "Allow", + "Action": [ + "ce:GetCostAndUsage", + "ce:GetCostAndUsageWithResources", + "ce:GetCostForecast", + "ce:GetDimensionValues", + "ce:GetTags", + "ce:GetReservationUtilization", + "ce:GetSavingsPlansUtilization", + "ce:GetAnomalies", + "ce:GetAnomalyMonitors", + "ce:GetAnomalySubscriptions" + ], + "Resource": "*" + }, + { + "Sid": "CloudWatchMetricsReadOnly", + "Effect": "Allow", + "Action": [ + "cloudwatch:GetMetricData", + "cloudwatch:GetMetricStatistics", + "cloudwatch:ListMetrics", + "cloudwatch:DescribeAlarms", + "cloudwatch:DescribeAlarmsForMetric" + ], + "Resource": "*" + }, + { + "Sid": "Ec2DescribeReadOnly", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceStatus", + "ec2:DescribeVolumes", + "ec2:DescribeAddresses", + "ec2:DescribeNatGateways", + "ec2:DescribeSnapshots", + "ec2:DescribeRegions" + ], + "Resource": "*" + }, + { + "Sid": "ElbAndRdsDescribeReadOnly", + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeTargetHealth", + "rds:DescribeDBInstances", + "rds:DescribeDBClusters" + ], + "Resource": "*" + }, + { + "Sid": "LambdaAndStorageInventoryReadOnly", + "Effect": "Allow", + "Action": [ + "lambda:ListFunctions", + "lambda:GetFunctionConfiguration", + "s3:ListAllMyBuckets", + "s3:GetBucketLocation" + ], + "Resource": "*" + } + ] +} diff --git a/security-review/iam/aws-posture-readonly-policy.rationale.md b/security-review/iam/aws-posture-readonly-policy.rationale.md new file mode 100644 index 0000000..94c4b09 --- /dev/null +++ b/security-review/iam/aws-posture-readonly-policy.rationale.md @@ -0,0 +1,77 @@ +# aws-posture-readonly-policy.json — least-privilege rationale + +This is the annotated companion to `aws-posture-readonly-policy.json`. The policy JSON itself +is kept strictly valid (no inline `Comment` keys — IAM rejects those), so all rationale lives +here. This policy is the permission set for the **aws-posture** checker (design D5 / §4): +idle / anomalous-spend watch on the Sea Haven AWS account (328440206208, us-east-1). + +**Cross-review status (2026-06-18):** GPT-4.1 IAM cross-review returned **APPROVE, no BLOCKs**. +FIXes applied to the policy as a result: +- **Removed `ec2:DescribeImages`** (data minimization — AMIs are not part of the idle-spend + signal; orphan EBS snapshots already cover the storage-waste case via `ec2:DescribeSnapshots`). +- The trust policy (`aws-posture-trust-policy.json`) gained **`aws:SourceAccount` = + `328440206208`** as an extra confused-deputy guard alongside the existing `aws:SourceArn` + trust-anchor pin (see that file). + +**aws-posture itself is built in Phase-3 (this change set) but stays PROVISIONING-GATED** — the +checker never calls AWS until step-ca + Roles Anywhere (this packet) are stood up. This file + the +policy are the IAM cross-review inputs (design §7, B3). + +## Design principle + +The box stays **read-only**. There is **no write action, no `iam:*` mutating action, no +`Resource` wildcard where AWS supports resource-level scoping**. Idle-spend posture is an +account-wide, list-oriented read: most of the actions below are AWS APIs that *do not support +resource-level ARNs at all* (Cost Explorer, the CloudWatch metric-data calls, and the EC2/ELB/ +RDS `Describe*` list operations). For those, least-privilege is enforced by the **action +allow-list** (only the specific read verbs), not by narrowing `Resource`. + +## Statement-by-statement + +| Sid | Why aws-posture needs it | Why read-only / why `Resource: "*"` | +|---|---|---| +| `CostAndUsageReadOnly` | The core idle/anomalous-spend signal (the design flags ≈$330/mo). `GetCostAndUsage`, forecasts, dimensions, and the native CE anomaly detectors. | Cost Explorer is an account-scoped service; its API has no resource-level ARNs, so `Resource:*` is the only valid form. Only `Get*` verbs — no `ce:Update*/Create*/Delete*`, no budget mutation. | +| `CloudWatchMetricsReadOnly` | Correlate spend with utilization (an instance billing but at ~0% CPU is idle). `GetMetricData`/`GetMetricStatistics`/`ListMetrics`; `DescribeAlarms*` to see whether an idle resource is already alarmed. | These metric-read APIs do not support resource-level permissions. **No `PutMetricData`, no alarm create/modify/delete.** | +| `Ec2DescribeReadOnly` | The classic idle-spend inventory: stopped instances still paying for EBS, unattached volumes, unassociated Elastic IPs, idle NAT gateways, orphan snapshots. (`ec2:DescribeImages` was **removed** in cross-review — AMIs are not an idle-spend signal aws-posture acts on.) | `Describe*` is read-only; these list calls don't take resource ARNs. **No `Run*/Start*/Stop*/Terminate*/Modify*/Create*/Delete*`.** | +| `ElbAndRdsDescribeReadOnly` | Idle load balancers (no healthy targets) and idle/oversized RDS are frequent waste. `Describe*` only. | List APIs without resource-level ARNs. **No `rds:Modify*/Delete*/Reboot*`, no ELB mutation.** | +| `LambdaAndStorageInventoryReadOnly` | Inventory functions + buckets to correlate against CloudWatch idle metrics. | **Deliberately excludes `s3:GetObject`** — the role never reads object *data*, only `ListAllMyBuckets` + `GetBucketLocation` (existence/region). **No `lambda:InvokeFunction`, no Lambda mutation.** This is the tightest the inventory can be while still seeing what exists. | + +## What is deliberately NOT here (blast-radius containment) + +- No `iam:*`, `sts:AssumeRole` onward-chaining, `organizations:*`, or `account:*`. +- No `s3:GetObject` / `s3:GetObjectVersion` (no data-plane read of any bucket). +- No `secretsmanager:GetSecretValue` / `ssm:GetParameter*` (no secret read). +- No `kms:Decrypt`, no `logs:GetLogEvents` (no log/data exfil path). +- No write/modify/delete verb in any service. + +A leaked session from this role can **enumerate and price the account, and nothing more** — it +cannot read application data, secrets, or change a single resource. + +## Cross-review NIT answers (2026-06-18) + +- **`ec2:DescribeSnapshots` kept (NIT: is it needed?)** — yes. Orphan EBS snapshots are a common + idle-spend line item (snapshots of long-deleted volumes keep billing); the checker lists them + to flag that waste. It returns only account-owned metadata (we query with `OwnerIds=["self"]`), + no snapshot data. `ec2:DescribeImages` (AMIs) was the over-grant and was **removed**. +- **`s3:ListAllMyBuckets` kept (NIT: data exposure?)** — it returns only bucket *names* you own, + no object data and no bucket contents; `s3:GetBucketLocation` returns only the region. Both are + account-owned inventory queries needed to correlate idle buckets/regions against cost. **No + `s3:GetObject`** anywhere, so there is no data-plane read path. +- **No `logs:*` (NIT: do we need CloudWatch Logs?)** — no. aws-posture reasons over *metrics* + (`cloudwatch:GetMetric*`) and the cost/inventory describe calls; it never needs log *events*. + Omitting `logs:GetLogEvents`/`logs:FilterLogEvents` keeps the role off the log-exfil path. +- **`aws:RequestedRegion` condition (NIT: optional region pin) — SKIPPED, deliberately.** The + reviewer flagged this as optional. It is **NOT applied** because Cost Explorer (`ce:*`) and + `s3:ListAllMyBuckets` are **global-endpoint services** that resolve to us-east-1 with request + contexts where `aws:RequestedRegion` does not reliably equal `us-east-1` — a blanket region + condition risks **DENYing the core cost signal**. Scoping it to a separate statement covering + only the regional `Describe*` calls (ec2/rds/elb/cloudwatch) would add a fourth+ statement for + marginal benefit (the action allow-list already bounds blast radius, and the box only ever runs + in us-east-1). Per the task's guidance, we prefer SKIP over a region pin that could break the + global-service statements. + +## Comparison to the AWS-managed alternatives + +`ReadOnlyAccess` / `ViewOnlyAccess` are far broader (they include `s3:GetObject`, +`dynamodb:GetItem`, `secretsmanager` list, etc.). This custom policy is intentionally a small +fraction of those — only the cost + idle-inventory surface the checker actually queries. diff --git a/security-review/iam/aws-posture-trust-policy.json b/security-review/iam/aws-posture-trust-policy.json new file mode 100644 index 0000000..ffb812f --- /dev/null +++ b/security-review/iam/aws-posture-trust-policy.json @@ -0,0 +1,27 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "RolesAnywhereAssumeFromStepCaLeaf", + "Effect": "Allow", + "Principal": { + "Service": "rolesanywhere.amazonaws.com" + }, + "Action": [ + "sts:AssumeRole", + "sts:TagSession", + "sts:SetSourceIdentity" + ], + "Condition": { + "StringEquals": { + "aws:PrincipalTag/x509Subject/CN": "r720-aws-posture", + "aws:PrincipalTag/x509Issuer/CN": "Sea Haven Internal CA - R720 Roles Anywhere", + "aws:SourceAccount": "328440206208" + }, + "ArnEquals": { + "aws:SourceArn": "arn:aws:rolesanywhere:us-east-1:328440206208:trust-anchor/REPLACE_WITH_TRUST_ANCHOR_ID" + } + } + } + ] +} diff --git a/security-review/iam/roles-anywhere-config.json b/security-review/iam/roles-anywhere-config.json new file mode 100644 index 0000000..35ba002 --- /dev/null +++ b/security-review/iam/roles-anywhere-config.json @@ -0,0 +1,38 @@ +{ + "_comment": "Reviewer-facing config describing the IAM Roles Anywhere trust-anchor + profile to be created. NOT applied — provisioning is gated behind the GPT-4.1 cross-review + Adam. Values prefixed REPLACE_WITH_* are filled in at provisioning time. Region us-east-1, account 328440206208.", + + "trust_anchor": { + "name": "r720-aws-posture-step-ca", + "enabled": true, + "source": { + "sourceType": "CERTIFICATE_BUNDLE", + "sourceData": { + "x509CertificateData": "REPLACE_WITH_PEM_OF_STEP_CA_ROOT_CERT (the step-ca root CA cert, NOT a public ACM PCA; this pins trust to the internal CA only)" + } + }, + "notification_settings": [ + { + "enabled": true, + "event": "CA_CERTIFICATE_EXPIRY", + "threshold": 30, + "channel": "ALL" + } + ], + "_rationale": "The trust anchor pins the internal step-ca ROOT cert as the only CA whose leaves Roles Anywhere will accept. Because the CA is internal and single-purpose, no other identities can mint trusted leaves. CA-expiry notifications are on so the anchor cannot silently go stale." + }, + + "profile": { + "name": "r720-aws-posture-readonly", + "enabled": true, + "roleArns": [ + "arn:aws:iam::328440206208:role/r720-aws-posture-readonly" + ], + "durationSeconds": 3600, + "acceptRoleSessionName": false, + "managedPolicyArns": [], + "sessionPolicy": null, + "_rationale": "Profile binds ONLY the single read-only role. durationSeconds=3600 (1h) caps the lifetime of any vended STS session independent of cert lifetime; combined with a ~24h leaf cert, a compromised leaf yields at most a short read-only window. No extra managed policies; no session-policy widening." + }, + + "_binding_note": "The role's trust policy (aws-posture-trust-policy.json) additionally pins aws:PrincipalTag/x509Subject/CN = 'r720-aws-posture' AND aws:PrincipalTag/x509Issuer/CN, and ArnEquals on aws:SourceArn = this trust anchor. So three independent conditions must all hold for AssumeRole to succeed: (1) the call comes via Roles Anywhere, (2) from THIS trust anchor, (3) presenting a leaf whose subject CN and issuer CN match. Roles Anywhere maps x509 subject/issuer fields into aws:PrincipalTag/x509Subject/* and aws:PrincipalTag/x509Issuer/* session tags, which is what the trust policy keys on." +} diff --git a/security-review/iam/step-ca-config-sketch.md b/security-review/iam/step-ca-config-sketch.md new file mode 100644 index 0000000..346083d --- /dev/null +++ b/security-review/iam/step-ca-config-sketch.md @@ -0,0 +1,145 @@ +# step-ca config sketch — internal CA for aws-posture Roles Anywhere leaf certs + +Design ref: `docs/r720-agent-team-design.md` §6.3 (step-ca + Roles Anywhere, D5) and §7 Phase 3. + +**Not provisioned here.** This is the config + renewal approach for the GPT-4.1 cross-review. +step-ca is the small internal CA on the R720 box (Smallstep `step-ca`) whose **root** cert is +pinned as the Roles Anywhere trust anchor, and which issues a **short-lived leaf** that the box +presents to Roles Anywhere to obtain short-lived read-only STS credentials. **No long-lived AWS +key ever lands on the box** — the leaf self-expires and is auto-renewed by a systemd timer. + +## Trust chain (one CA, one purpose) + +``` +step-ca ROOT (offline-ish, long-lived) + └── step-ca intermediate (the online signer) + └── leaf CN=r720-aws-posture (short-lived, ~24h, auto-renewed) + └── presented to AWS IAM Roles Anywhere trust anchor + └── AssumeRole -> r720-aws-posture-readonly (1h STS session) +``` + +The trust anchor pins the **root** cert (`roles-anywhere-config.json` → `sourceData +.x509CertificateData`). The role trust policy (`aws-posture-trust-policy.json`) additionally +pins the leaf **subject CN** (`r720-aws-posture`) and **issuer CN**, so only this CA's leaf with +this exact CN can assume the role. + +## `ca.json` (sketch — the single-purpose provisioner) + +```jsonc +{ + "root": "/etc/step-ca/certs/root_ca.crt", + "crt": "/etc/step-ca/certs/intermediate_ca.crt", + "key": "/etc/step-ca/secrets/intermediate_ca_key", + "address": "127.0.0.1:8443", // localhost-only; the box is the sole client + "dnsNames": ["localhost", "r720.lan"], + "authority": { + "claims": { + "minTLSCertDuration": "5m", + "maxTLSCertDuration": "24h", // hard cap: leaves are short-lived + "defaultTLSCertDuration": "24h", + "disableRenewal": false + }, + "provisioners": [ + { + "type": "JWK", + "name": "aws-posture", + "key": { "use": "sig", "kty": "EC", "crv": "P-256", "alg": "ES256", "kid": "REPLACE", "x": "REPLACE", "y": "REPLACE" }, + "encryptedKey": "REPLACE_WITH_ENCRYPTED_PROVISIONER_KEY", + "claims": { + "maxTLSCertDuration": "24h", + "defaultTLSCertDuration": "24h" + }, + "options": { + "x509": { + // The provisioner only ever issues this one CN; templating keeps the + // subject/issuer fields the Roles Anywhere trust policy pins. + "templateData": { "CommonName": "r720-aws-posture" } + } + } + } + ] + } +} +``` + +Root CA subject CN: **`Sea Haven Internal CA - R720 Roles Anywhere`** (matches the +`x509Issuer/CN` condition in `aws-posture-trust-policy.json`). + +## Initial bootstrap (one-time, at provisioning) + +```bash +step ca init \ + --name "Sea Haven Internal CA - R720 Roles Anywhere" \ + --dns localhost --dns r720.lan --address 127.0.0.1:8443 \ + --provisioner aws-posture --deployment-type standalone + +# Issue the first leaf the box will present to Roles Anywhere: +step ca certificate "r720-aws-posture" \ + /etc/aws-posture/leaf.crt /etc/aws-posture/leaf.key \ + --not-after 24h --provisioner aws-posture +``` + +`leaf.key` is mode 600, owned by the unattended service user; it never leaves the box. + +## Auto-renewal — systemd timer (the leaf self-expires; the timer keeps it fresh) + +`step-ca` ships `step ca renew`, which no-ops until the cert is within its renewal window. + +`/etc/systemd/system/aws-posture-cert-renew.service`: +```ini +[Unit] +Description=Renew r720-aws-posture Roles Anywhere leaf certificate +After=network-online.target step-ca.service + +[Service] +Type=oneshot +User=aws-posture +# --expires-in: renew only when <8h of life remains; idempotent, safe to run hourly. +ExecStart=/usr/bin/step ca renew --force --expires-in 8h \ + /etc/aws-posture/leaf.crt /etc/aws-posture/leaf.key +# step-ca renew rewrites the cert in place; aws_signing_helper reads it fresh each call, +# so no service reload is needed. +``` + +`/etc/systemd/system/aws-posture-cert-renew.timer`: +```ini +[Unit] +Description=Hourly renewal check for the aws-posture leaf cert + +[Timer] +OnCalendar=hourly +RandomizedDelaySec=300 +Persistent=true # catch up a renewal missed while the box was off + +[Install] +WantedBy=timers.target +``` + +Hourly check + 8h renewal window + 24h cert = the leaf is always fresh and a missed window has +hours of slack. The timer mirrors the existing secrev launchd/systemd discipline. + +## How aws-posture USES the leaf (no AWS key on disk) + +aws-posture invokes AWS's `aws_signing_helper credential-process`, which signs the Roles +Anywhere request with the **leaf** and returns short-lived STS creds on stdout: + +```ini +# ~/.aws/config (on the box) +[profile r720-aws-posture] +credential_process = /usr/local/bin/aws_signing_helper credential-process \ + --certificate /etc/aws-posture/leaf.crt \ + --private-key /etc/aws-posture/leaf.key \ + --trust-anchor-arn arn:aws:rolesanywhere:us-east-1:328440206208:trust-anchor/REPLACE \ + --profile-arn arn:aws:rolesanywhere:us-east-1:328440206208:profile/REPLACE \ + --role-arn arn:aws:iam::328440206208:role/r720-aws-posture-readonly +``` + +The credentials live only in process memory for the 1h session duration; nothing long-lived is +written. This is **strictly stronger than the box's existing long-lived GitHub PAT** (design +§6.3): the AWS identity self-expires and rotates without operator action. + +## Capacity note (design §6.5) + +step-ca on a 4GB / 2 vCPU / 40GB box is negligible (a localhost signer + a tiny DB). Re-check +disk headroom after Phase 1 per §6.5; snapshot the Hyper-V VM before standing this up per +`feedback_ec2_replacement_snapshot`.