Files
gbrain/scripts/check-source-config-leak.sh
df86ea5f1d v0.40.5.0 Federated Sync v2 — parallel source sync + push triggers + per-source health (#1322)
* wip: federated sync v2 pre-merge snapshot

* v0.40.5.0 Federated Sync v2 — parallel source sync + push triggers + per-source health

Bump VERSION + package.json + CHANGELOG header + migration walkthrough filename
to v0.40.5.0 (claiming the next free slot in the v0.40.x patch series after
master's v0.40.1.0).

What ships (6 components, all behind sync.federated_v2 feature flag default-on):
1. Per-source sync lock — syncLockId(sourceId), phantom-redirect parity
2. Parallel sync --all — pMapAllSettled fan-out, --max-sources N cap
3. embed-backfill minion handler — D2 per-source lock + D6 $10/job budget + D15.1
   fire-and-forget submission + D19 source-level cooldown + 24h $25 rolling cap
4. sync trigger CLI + POST /webhooks/github — HMAC-verified (60 req/min/IP),
   X-GitHub-Event=push + ref filter against tracked_branch
5. sources status + federation_health doctor — batched GROUP BY pipeline
   (4 queries instead of 6×N per-source roundtrips)
6. sources federate/unfederate hook — auto-submit embed-backfill on flip

Correctness fixes (unconditional):
- D21: sync.ts:959 facts backstop now passes sourceId to engine.getPage
- D15.4: redactSourceConfig + CI guard prevent webhook_secret leak
- D15.5: safeHexEqual extracted to src/core/timing-safe.ts

Schema:
- Migration v89 (sources_github_repo_index): partial expression index on
  config->>'github_repo' for fast webhook source-lookup

Tests:
- 14 new test files, 112 cases. 4 IRON-RULE regressions pinned (SYNC_LOCK_ID
  back-compat, phantom per-source lock, embed-backfill kill+resume,
  webhook HMAC prefix-strip). All 9449 unit tests pass.

Caught at test-write time: the webhook handler had a Buffer.from('sha256=...',
'hex') truncation bug — without the prefix-strip, every signature would have
"matched" empty buffers. Pinned by a test/sources-webhook.test.ts IRON-RULE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(check-source-config-leak): tighten regex to source-row patterns only

The v0.40.5.0 wave added scripts/check-source-config-leak.sh with a
too-broad pattern (JSON\.stringify\(.*config) that flagged any variable
named 'config' — catching the GLOBAL gbrain config.json serializers in
src/commands/init.ts (status envelopes) and src/core/config.ts (the
config-file write site). On the CI runner without rg installed, the
grep -rE fallback fired correctly and produced 4 false positives that
broke the `verify` script.

Tightened the patterns to specifically match `(source|src|row|s).config`
property access — the actual risk shape (a sources-table row being
serialized whole). The global gbrain config has a different shape and
threat model (file-mode 0o600 at the write site), so it's safe to
exempt at the regex level rather than per-file whitelist.

Also fixed a latent bug: the rg branch used `--include='*.ts'` (grep's
flag, not rg's). rg silently rejected it and CANDIDATES came back empty,
so the local-dev runs (which have rg) would never have caught a real
leak. Now branches on tool availability: `-g '*.ts'` for rg, `--include`
for grep -rE. Both branches verified against a synthetic leak fixture.

Also added init.ts + config.ts to the whitelist as a belt-and-suspenders
since they handle gbrain-global config (not source rows) and could
otherwise reflect-back via regex iteration.

CI: `bun run verify` exit 0 locally with both the original false-positive
fixture (clean repo) and a synthetic leak fixture (correctly caught,
exit 1).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:21:59 -07:00

111 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# v0.40 D15.4 CI guard — prevent webhook_secret leak through sources.config
# serialization paths.
#
# After v0.40, sources.config can contain secrets (webhook_secret). Any code
# path that returns the raw config object via JSON.stringify / serializer
# without first running it through redactSourceConfig() will leak the secret.
#
# This script greps for risky patterns:
# 1. JSON.stringify on a `config` field where the source is a row from `sources`
# 2. New endpoints / ops that return raw `config` without `redactSourceConfig`
#
# Failure mode is loose-positive on purpose — false positives cost one
# 30-second comment-or-fix; false negatives leak production secrets.
set -euo pipefail
cd "$(dirname "$0")/.."
FOUND=0
# Pattern A: sources.config field referenced in a JSON serializer call site
# without redactSourceConfig nearby. Covers MCP op handlers, admin API
# routes, sources.ts subcommands that print --json output.
#
# Whitelist:
# - src/core/source-config-redact.ts itself (defines the redactor)
# - src/core/sources-load.ts (returns raw rows; callers redact)
# - src/commands/sources.ts runFederate/runWebhook* (mutators write raw)
# - src/core/migrate.ts (DDL data references not serialization)
# - src/core/sources-ops.ts (CLI feedback prints structured fields, not raw config)
# - test/ (tests are allowed to introspect raw config)
# Grep for `r.config\|src.config\|source.config` near JSON.stringify/console.log/res.json
# where redactSourceConfig is NOT used in the same hunk.
RAW_PATTERN='\b(\.config\b|config:[[:space:]]*src\.config)\b'
# Tightened patterns: match serializers that pass a source-row's .config
# field (source.config, src.config, row.config, s.config, or a property
# access like `.config` on an object likely sourced from a `sources` row),
# NOT every variable named "config" (which would catch global gbrain config).
#
# The risk pattern is `JSON.stringify(<srcVar>.config)` where srcVar holds
# a row from the sources table. Variables that hold the GLOBAL gbrain
# config.json are also commonly named `config` — that's a different shape
# and a different threat model (already protected at the file-mode 0o600
# write site in src/core/config.ts).
#
# rg uses `-g` for globs; grep -rE uses `--include`. Branch accordingly so
# CI runners without rg still match cleanly.
if command -v rg >/dev/null 2>&1; then
CANDIDATES=$(rg -n \
-e 'JSON\.stringify\((source|src|row|s)\.config' \
-e 'res\.json\((source|src|row|s)\.config' \
-e 'res\.json\(\{[^}]*\.config[^.]' \
-e 'console\.log\(JSON\.stringify\((source|src|row|s)\.config' \
-g '*.ts' \
src/ 2>/dev/null || true)
else
CANDIDATES=$(grep -rEn \
-e 'JSON\.stringify\((source|src|row|s)\.config' \
-e 'res\.json\((source|src|row|s)\.config' \
-e 'res\.json\(\{[^}]*\.config[^.]' \
-e 'console\.log\(JSON\.stringify\((source|src|row|s)\.config' \
--include='*.ts' \
src/ 2>/dev/null || true)
fi
# Filter out files we trust (handle sources.config redaction themselves OR
# handle the gbrain global config, which is a different object).
FILTERED=$(echo "$CANDIDATES" | \
grep -v 'src/core/source-config-redact.ts' | \
grep -v 'src/core/sources-load.ts' | \
grep -v 'src/commands/sources.ts' | \
grep -v 'src/core/migrate.ts' | \
grep -v 'src/core/sources-ops.ts' | \
grep -v 'src/commands/init.ts' | \
grep -v 'src/core/config.ts' || true)
if [ -n "$FILTERED" ]; then
# For each candidate, check if redactSourceConfig appears within 10 lines above.
while IFS= read -r LINE; do
[ -z "$LINE" ] && continue
FILE=$(echo "$LINE" | cut -d: -f1)
LINENO=$(echo "$LINE" | cut -d: -f2)
# Look in surrounding 20 lines
START=$((LINENO - 10))
[ "$START" -lt 1 ] && START=1
END=$((LINENO + 5))
CONTEXT=$(sed -n "${START},${END}p" "$FILE" 2>/dev/null || true)
if ! echo "$CONTEXT" | grep -q 'redactSourceConfig'; then
echo "POTENTIAL_LEAK: $LINE"
echo " Context lacks redactSourceConfig — verify webhook_secret cannot be serialized."
FOUND=1
fi
done <<< "$FILTERED"
fi
if [ "$FOUND" -eq 1 ]; then
echo ""
echo "v0.40 D15.4 guard: every sources.config serializer MUST go through"
echo "redactSourceConfig() from src/core/source-config-redact.ts."
echo ""
echo "If a flagged site is a known false positive (e.g. CLI command that"
echo "only prints metadata, not the raw object), update the whitelist in"
echo "scripts/check-source-config-leak.sh."
exit 1
fi
exit 0