Files
cgalo5758 b245bdb0a9 Fix security audit findings from 2026-09-21 scan
Remediate six confirmed security issues: deployment-only config keys,
bounded provider responses, short-lived registration sessions, private
init file mode, FedWiki workflow authorization, and switch preview
gates.

- Add DeploymentOnly config key declaration; refuse runtime overrides
  for keys that decide where secrets are sent
- Create httplimit package; bound all provider response reads at 8 MiB
- Set fifteen-minute deadline on /register sessions
- Write mc-config.yaml with 0600 permissions
- Derive FedWiki workflow IDs from site IDs; re-authorize sites before
  mutating activities
- Apply switch authorization gates to the proration preview
2026-09-21 13:57:57 -05:00

152 lines
7.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Ralph-style audit loop: for each model, for each scope task, run a FRESH
# auditor container (clean context) and capture its findings. Sequential by
# design — concurrent opencode runs in one project deadlock, and it respects
# provider rate limits.
#
# Usage:
# OUT=<dir> ./run-audit.sh # default models, all tasks
# OUT=<dir> MODELS="deepseek/deepseek-v4-pro" ./run-audit.sh
# OUT=<dir> MODELS="codex:gpt-6-astra" VARIANT=ultra ./run-audit.sh # Codex CLI lane
# OUT=<dir> ./run-audit.sh 02-authorization 05-csp-headers # only these slices
# OUT=<dir> MODELS="codex:gpt-6-astra" VARIANT=ultra TIMEOUT=14400 ./run-audit.sh open/bare
# OUT=<dir> TIMEOUT=1800 ./run-audit.sh
# OUT=<dir> VARIANT=high ./run-audit.sh # reasoning effort; default max, "" to omit
# OUT=<dir> QUOTA_POLL=600 QUOTA_WAIT=43200 ./run-audit.sh # usage-limit resume cadence and ceiling
#
# A task is a slice under scope/ (the default set is all of them) or an open
# brief named as "open/<name>", which hands the model the whole repository and
# no questions. A MODELS entry is "<harness>:<model>"; no prefix means
# opencode. harness.sh holds what differs per harness: the id syntax, the
# credential each lane mounts, how VARIANT is passed, and how a task resumes
# after the plan's usage window runs out.
#
# OUT names the directory that receives findings. Keep it outside this one:
# this directory is tracked, and a run's output is not.
set -euo pipefail
cd "$(dirname "$0")"
HERE="$(pwd)"
PROJECT=member-console-audit
: "${OUT:?set OUT to the directory that receives findings}"
mkdir -p "$OUT"; OUT="$(cd "$OUT" && pwd)"; export OUT
. ./harness.sh
# Reverify current IDs with `opencode models`; these drift. Gemini is in the
# default set
# (google/gemini-3.8-flash); its key is in the mounted auth.json and its host
# is allowlisted for egress. Override the set with MODELS="..." ./run-audit.sh.
DEFAULT_MODELS="deepseek/deepseek-v4-pro google/gemini-3.8-flash zai-coding-plan/glm-5.3 kimi-for-coding/k3-256k"
MODELS="${MODELS:-$DEFAULT_MODELS}"
# 1200s was too tight for Kimi at --variant max: it timed out mid-investigation
# on the larger slices (2026-09-08). Thorough models need real headroom.
TIMEOUT="${TIMEOUT:-2700}"
# Reasoning effort, in each harness's vocabulary (see harness.sh). "max" asks
# each model for its deepest reasoning; set VARIANT="" to omit the flag if a
# provider rejects the value.
VARIANT="${VARIANT:-max}"
if [[ ! -d .code ]]; then echo "Run ./prepare.sh first." >&2; exit 1; fi
harness_check_auth $MODELS
docker compose up -d egress-proxy >/dev/null
# The scanner pass is deterministic and cheap; run it once if it has not been.
if [[ ! -d "$OUT/tools" ]]; then
echo "==> No scanner output yet; running ./run-tools.sh first"
./run-tools.sh
fi
# Which tasks: args override; else every scope file.
if [[ $# -gt 0 ]]; then
TASKS=("$@")
else
mapfile -t TASKS < <(cd scope && ls *.md | sed 's/\.md$//')
fi
RUN_ID="$(date +%Y-%m-%d_%H%M%S)"
RUN_DIR="$OUT/$RUN_ID"
mkdir -p "$RUN_DIR"
{
echo "run: $RUN_ID"
echo "commit: $(cat .code/AUDIT_COMMIT.txt 2>/dev/null || echo '?')"
echo "models: $MODELS"
echo "variant: ${VARIANT:-<default>}"
echo "tasks: ${TASKS[*]}"
echo "timeout-per-attempt: ${TIMEOUT}s"
echo "quota-resume: every ${QUOTA_POLL}s, up to ${QUOTA_WAIT}s of waiting"
echo "execution: ${SERIAL:+serial}${SERIAL:-parallel by model ($(echo $MODELS | wc -w) lanes)}"
} | tee "$RUN_DIR/manifest.txt"
# One lane per model: the models run concurrently, but within a lane the
# slices run serially, so each provider/key sees one request at a time (no
# rate-limit stacking). Set SERIAL=1 to fall back to a single sequential lane.
# Progress interleaves across lanes; each lane also logs to $outdir/_lane.log.
run_lane() {
local model="$1"
local harness id safe_model; harness="$(harness_of "$model")"; id="$(model_of "$model")"; safe_model="$(safe_name "$model")"
local outdir="$RUN_DIR/$safe_model"; mkdir -p "$outdir"
local lane_log="$outdir/_lane.log"
# Lane state (a codex lane's throwaway CODEX_HOME) lives under .run/, which
# git ignores and teardown.sh removes; harness_release removes it sooner.
local state="$HERE/.run/$RUN_ID/$safe_model"; mkdir -p "$state"
local -a LANE_DOCKER; mapfile -t LANE_DOCKER < <(harness_mount "$harness" "$state")
local task file out err prompt env_sh plugins scan_dir
for task in "${TASKS[@]}"; do
task="${task%.md}"
# A bare name is a slice under scope/; "open/<name>" is an open brief;
# "codex-security/<name>" hands method and report to the Codex Security
# plugin (codex harness only).
[[ "$task" == */* ]] && file="$task.md" || file="scope/$task.md"
if [[ ! -f "$file" ]]; then echo " ! [$model] no $file, skipping"; continue; fi
if [[ "$file" == codex-security/* && "$harness" != codex ]]; then
echo " ! [$model] $task needs the codex harness, skipping"; continue
fi
out="$outdir/${task//\//__}.md"; err="$outdir/${task//\//__}.err"
echo "==> [$model] $task (started $(date +%H:%M:%S))"
# The prompt. A slice: how to work, the system context as fact, the
# slice, then what a finding must carry. An open brief: the brief, then
# what a finding must carry; no map, no questions. A Codex Security task:
# the task alone; the plugin owns the method and writes its own artifacts
# (scan-manifest.json, findings.json, coverage.json, report.md) into a
# scan directory under this lane's output, which the two variables name.
env_sh=""; plugins=""
case "$file" in
scope/*) prompt="cat AUDITOR.md; echo; echo '---'; cat CONTEXT.md; echo; echo '---'; cat $file; echo; echo '---'; cat REPORT.md";;
codex-security/*)
scan_dir="$outdir/${task//\//__}"; mkdir -p "$scan_dir/state"
prompt="cat $file; echo; echo 'Scan directory: /out/${scan_dir#"$OUT"/} (it exists; use that absolute path, and no other).'"
env_sh="export CODEX_SECURITY_SCAN_ROOT='/out/${scan_dir#"$OUT"/}' CODEX_SECURITY_STATE_DIR='/out/${scan_dir#"$OUT"/}/state'"
plugins="codex-security";;
*) prompt="cat $file; echo; echo '---'; cat REPORT.md";;
esac
if ! harness_run_task "$harness" "$id" "$VARIANT" "$prompt" "$out" "$err" "$lane_log" "$env_sh" "$plugins"; then
echo " [$model] $task: exit non-zero or timeout — agent exit codes are unreliable; judging by output" >>"$lane_log"
fi
# opencode headless exits 0 even when it did nothing; trust the artifact.
if [[ -s "$out" ]] && grep -qiE 'finding|COMPLETE|summary|report' "$out"; then
echo " ok [$model] $task -> $out ($(wc -l <"$out") lines)"
else
echo " SUSPECT [$model] $task -> EMPTY/SUSPECT; see $err"
fi
done
harness_own_output "$outdir"
harness_release "$harness" "$state"
echo "== lane done: [$model]"
}
if [[ -n "${SERIAL:-}" ]]; then
for model in $MODELS; do run_lane "$model"; done
else
declare -a lane_pids=()
for model in $MODELS; do
run_lane "$model" &
lane_pids+=("$!")
done
echo "==> ${#lane_pids[@]} model lanes running in parallel (pids: ${lane_pids[*]}); one provider/key per lane"
wait "${lane_pids[@]}"
fi
echo
echo "Findings under: $RUN_DIR"
echo "Review, then verify each real finding against a fresh stack before it"
echo "enters the issue ledger in status/issues.md. These models over-report."