#!/usr/bin/env bash # Phase 14 fail-closed gate (domain jobs and external integrations: JOBS-02, # JOBS-03, SRCH-02, INTG-01, INTG-02, API-08, CLI-05). # # Every stage exits non-zero on a failing command, a go test run that fails, # skips, matches zero tests or prints "no tests to run", a named test that # did not pass, a data race, a parity count other than the expected one, a # coverage floor missed, a corpus secret or an evidence gap. --self-test # proves each detector fails closed on planted inputs. # # --removal is the anchor-exact mutation harness behind the RC rows of # 14-SECURITY-REVIEW.md: it removes one protection at a time, requires its # named test to fail on an assertion, and restores the file byte for byte # (checked with cmp). It refuses a file with uncommitted changes and edits # tracked source while it runs, so it is not part of --all. # # Framework commands run in summercms.go; application commands run in the # sibling repository named by PHASE14_APP (default ../fonoteka.go), whose # workspace also holds the shared plugins sm-user-plugin, sm-golem-plugin # and sm-feedback-plugin. No stage calls a live vendor (D-15): every # Discogs, AI and G15Office exchange replays from a recorded sidecar. set -euo pipefail # A colour-forcing shell variable changes the output some framework tests # compare byte for byte; the gate runs without it. unset FORCE_COLOR ROOT="${PHASE14_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" APP="${PHASE14_APP:-$(cd "$ROOT/../fonoteka.go" && pwd)}" PHASE_DIR="${PHASE14_PHASE_DIR:-$ROOT/.planning/phases/14-domain-jobs-and-external-integrations}" REVIEW="$PHASE_DIR/14-SECURITY-REVIEW.md" VALIDATION="$PHASE_DIR/14-VALIDATION.md" APP_PLUGINS=(./plugins/golem15/fonoteka/... ./plugins/golem15/user/... ./plugins/golem15/golem/... ./plugins/golem15/feedback/...) EXPECTED_ROUTES=175 EXPECTED_PORTED=172 EXPECTED_PENDING=3 COVERAGE_FLOOR=80 FUZZ_CORPUS="plugins/golem15/fonoteka/testdata/fuzz" # D-02: the AI adapters are hand-written JSON over fetchguard. A vendor SDK # in the module graph means the decision was undone (T-14-SC). VENDOR_SDK_RE='^(github\.com/anthropics/|github\.com/openai/|github\.com/sashabaranov/|github\.com/liushuangls/go-anthropic|github\.com/tmc/langchaingo)' usage() { cat >&2 <<'EOF' usage: check-phase14.sh --self-test check-phase14.sh --go check-phase14.sh --parity check-phase14.sh --named check-phase14.sh --removal check-phase14.sh --coverage check-phase14.sh --all EOF exit 2 } # phase14_detect reads go test -json. Exit 1 fail, 2 skip, 3 zero tests or # "no tests to run", 4 non-JSON, 5 a required test did not pass, 6 a data # race was reported. PHASE14_REQUIRE lists tests that must pass. phase14_detect() { python3 - "$1" <<'PY' import json, os, sys path = sys.argv[1] require = set(os.environ.get("PHASE14_REQUIRE", "").split()) passed = set() failed_tests, failed_pkgs = {}, [] build_failed = False with open(path, encoding="utf-8", errors="replace") as fh: for raw in fh: line = raw.strip() if not line.startswith("{"): continue try: ev = json.loads(line) except json.JSONDecodeError: print("refuse: non-json test output", file=sys.stderr) sys.exit(4) action = ev.get("Action") test = ev.get("Test") or "" pkg = ev.get("Package") or "" if action == "build-fail": build_failed = True if action == "output": text = ev.get("Output") or "" if "no tests to run" in text: print(f"refuse: no tests to run in {pkg}", file=sys.stderr) sys.exit(3) if "WARNING: DATA RACE" in text: print(f"refuse: data race in {pkg} {test}", file=sys.stderr) sys.exit(6) if action == "skip" and test: print(f"refuse: skipped {pkg} {test}", file=sys.stderr) sys.exit(2) if action == "fail": if ev.get("FailedBuild"): build_failed = True if test: failed_tests.setdefault(pkg, []).append(test) else: failed_pkgs.append(pkg) if action == "pass" and test: passed.add(test) if build_failed: print("refuse: build failed", file=sys.stderr) sys.exit(1) for pkg, tests in failed_tests.items(): for test in tests: print(f"refuse: failed {pkg} {test}", file=sys.stderr) sys.exit(1) for pkg in failed_pkgs: print(f"refuse: failed {pkg or 'unknown package'}", file=sys.stderr) sys.exit(1) missing = sorted(name for name in require if name not in passed) if missing: print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr) sys.exit(5) if not passed: print("refuse: zero tests", file=sys.stderr) sys.exit(3) PY } # phase14_go DIR ARGS... runs go test -json -count=1 ARGS through the # detector. With PHASE14_KEEP set, the JSON log is copied there. phase14_go() { local dir="$1" shift local log err log="$(mktemp)" err="$(mktemp)" set +e (cd "$dir" && go test -json -count=1 "$@") >"$log" 2>"$err" local rc=$? set -e local dc=0 verdict verdict="$(phase14_detect "$log" 2>&1)" || dc=$? if [[ "$dc" -ne 0 || "$rc" -ne 0 ]]; then cat "$err" >&2 || true tail -n 40 "$log" >&2 || true # The full log stays for diagnosis; its path is printed. local kept="${TMPDIR:-/tmp}/phase14-failed-$$.json" mv "$log" "$kept" 2>/dev/null && echo "phase14: full go test log kept at $kept" >&2 rm -f "$log" "$err" # The detector's verdict comes last, after the log tail, so it is # the line a reader sees. [[ -n "$verdict" ]] && echo "$verdict" >&2 echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2 exit 1 fi if [[ -n "${PHASE14_KEEP:-}" ]]; then cp "$log" "$PHASE14_KEEP" fi rm -f "$log" "$err" } # phase14_tests DIR PKG [-race] TEST... requires every named test to run and # pass, each matched by its exact name. phase14_tests() { local dir="$1" pkg="$2" shift 2 local extra=() if [[ "${1:-}" == "-race" ]]; then extra=(-race) shift fi local names="$*" local regex="^($(tr ' ' '|' <<<"$names"))\$" PHASE14_REQUIRE="$names" phase14_go "$dir" "$pkg" "${extra[@]}" -run "$regex" } expect_detect() { local name="$1" want="$2" payload="$3" local log dc=0 log="$(mktemp)" printf '%s\n' "$payload" >"$log" phase14_detect "$log" 2>/dev/null || dc=$? rm -f "$log" if [[ "$dc" -ne "$want" ]]; then echo "refuse: self-test $name: detector exit $dc, want $want" >&2 exit 1 fi } # module_hygiene MODLIST: no vendor AI SDK anywhere in the module graph # (D-02, T-14-SC). module_hygiene() { local modlist="$1" hits hits="$(grep -E "$VENDOR_SDK_RE" "$modlist" | sort -u || true)" if [[ -n "$hits" ]]; then echo "refuse: hygiene: a vendor SDK is in the module graph: $hits" >&2 return 1 fi return 0 } run_go() { (cd "$ROOT" && go vet ./...) phase14_go "$ROOT" ./... (cd "$APP" && go vet ./... "${APP_PLUGINS[@]}") phase14_go "$APP" -race ./... "${APP_PLUGINS[@]}" local modlist modlist="$(mktemp)" (cd "$ROOT" && go list -m all) >"$modlist" (cd "$APP" && go list -m all) >>"$modlist" if ! module_hygiene "$modlist"; then rm -f "$modlist" exit 1 fi rm -f "$modlist" echo "phase14 go passed" } # corpus_scan DIR: the fuzz seed corpus holds synthetic values only: no # 64-hex token, inv_ personal token, JWT, bearer header or vendor key. corpus_scan() { python3 - "$1" <<'PY' import os, re, sys root = sys.argv[1] if not os.path.isdir(root): print(f"refuse: fuzz corpus {root} is missing", file=sys.stderr) sys.exit(1) patterns = [ ("64-hex value", re.compile(r"(?&2 return 1 fi return 0 } # corpus_coverage LOG: the replay's own coverage line in a go test -json # log ("recorded R/T passing P failing F unrecorded U pending Q") must say # EXPECTED_ROUTES recorded, EXPECTED_PORTED passing, 0 failing, 0 # unrecorded and EXPECTED_PENDING pending. Pending routes are never counted # as passing. corpus_coverage() { python3 - "$1" "$EXPECTED_ROUTES" "$EXPECTED_PORTED" "$EXPECTED_PENDING" <<'PY' import json, re, sys path, routes, ported, pending = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4]) rx = re.compile(r"recorded (\d+)/(\d+) passing (\d+) failing (\d+) unrecorded (\d+) pending (\d+)") found = None for raw in open(path, encoding="utf-8", errors="replace"): raw = raw.strip() if not raw.startswith("{"): continue try: ev = json.loads(raw) except json.JSONDecodeError: continue m = rx.search(ev.get("Output") or "") if m: found = [int(x) for x in m.groups()] if found is None: print("refuse: the corpus replay printed no coverage line", file=sys.stderr) sys.exit(1) recorded, total, passing, failing, unrecorded, pend = found if total != routes or recorded != total or passing != ported or failing != 0 or unrecorded != 0 or pend != pending or passing + pend != total: print(f"refuse: corpus coverage recorded {recorded}/{total} passing {passing} failing {failing} unrecorded {unrecorded} pending {pend}, want {routes}/{routes} recorded, {ported} passing, 0 failing, {pending} pending", file=sys.stderr) sys.exit(1) print(f"phase14 corpus: {recorded}/{total} recorded, {passing} ported and passing, 0 failing, {pend} pending") PY } # flow_subtests FILE TEST: the subtest names of TEST as declared in FILE # (t.Run("name", ...)), each prefixed with TEST/. Every declared subtest is # required, so a new flow cannot be added without the gate running it. flow_subtests() { local file="$1" test="$2" grep -oE 't\.Run\("[^"]+"' "$file" | sed -E "s#t\\.Run\\(\"([^\"]+)\"#$test/\\1#" } PARITY_REQUIRE_FIXED="TestParityCorpus TestParityCorpus/coverage TestBroadcastGoldens TestBroadcastGoldens/created TestBroadcastGoldens/updated TestBroadcastGoldens/deleted TestBroadcastGoldens/bulk TestFonotekaNuxtFlows TestUserAPINuxtFlows TestCheckCorpusPortedCaseStatus TestUpstreamSidecarsAreReplayed TestFeedbackJobSidecarMatchesPlugin" parity_require() { local flows flows="$(flow_subtests "$APP/parity/fonoteka_flows_test.go" TestFonotekaNuxtFlows | tr '\n' ' ')" if [[ -z "${flows// /}" ]]; then echo "refuse: TestFonotekaNuxtFlows declares no subtest" >&2 exit 1 fi echo "$PARITY_REQUIRE_FIXED $flows" } run_parity() { manifest_check "$APP/parity/manifest.yaml" || exit 1 local log require require="$(parity_require)" log="$(mktemp)" PHASE14_KEEP="$log" PHASE14_REQUIRE="$require" \ phase14_go "$APP" ./parity -run '^(TestParityCorpus|TestBroadcastGoldens|TestFonotekaNuxtFlows|TestUserAPINuxtFlows|TestCheckCorpusPortedCaseStatus|TestUpstreamSidecarsAreReplayed|TestFeedbackJobSidecarMatchesPlugin)$' if ! corpus_coverage "$log"; then rm -f "$log" exit 1 fi rm -f "$log" (cd "$APP" && go run ./parity/check_corpus.go --manifest parity/manifest.yaml --require-recorded --check-secrets) corpus_scan "$APP/$FUZZ_CORPUS" PHASE14_REQUIRE="TestDocsTree" phase14_go "$ROOT" ./cmd/summer -run '^TestDocsTree$' (cd "$ROOT" && go run ./cmd/summer docs:build --check) echo "phase14 parity passed ($EXPECTED_ROUTES recorded, $EXPECTED_PORTED ported and passing, 0 failing, $EXPECTED_PENDING pending)" } # The named tests: every test 14-VALIDATION.md and 14-SECURITY-REVIEW.md # name, by package. The evidence stage refuses a validation or review row # naming a test missing here. NAMED_ROOT_FETCHGUARD="TestClientModes TestClientSchemeGuard TestClientMultipart TestClientBodyCap TestClientPutJSONHeaderOrder TestTransportSeamIsCodeOnly TestClientPostJSONThroughUpstreamFake TestClientNeverFollowsRedirects TestIsPrivateAddr" NAMED_ROOT_TIDE="TestUpstreamFakeRejectsMismatchedRequest TestUpstreamFakeHashesBase64Bodies TestWriteUpstreamRefusesUnmaskedCredential TestUpstreamProxyScriptMode TestUpstreamProxyRefusesNonLoopback TestEnsureParityCA TestUpstreamProxyMultipartAndForwardGuard TestUpstreamSidecarRefusals" NAMED_ROOT_SUNSCREEN="TestRedactHandler TestScrub TestInstallDefault" NAMED_ROOT_SURF="TestRecoverHidesPanicDetails TestCORSOptionsMatchesLaravel" NAMED_ROOT_BEACHCOMBER="TestDropIndex" NAMED_ROOT_TYPESENSE="TestEngineDropIndex TestEngineEnsureIndex" NAMED_ROOT_BUILD="TestGenerateMainInstallsRedactingLogger" NAMED_ROOT_SUMMER="TestParityCommandContract TestToolCommandNames" NAMED_APP_FONOTEKA="TestCsvMatchJob TestCsvImportJob TestCsvWR02 TestCsvRowPickSeam TestCsvRowPickResolves TestWishlistDigestJob TestWishlistDigestWorkerRegistered TestFonotekaSchedulePrune TestDiscogsMatchRoute TestDiscogsInboundLimits TestApplyReleaseModes TestDiscogsImportRoute TestCoverPriceRoute TestDiscogsCredentialTestRoute TestAICredentialTestRoute TestAdminVisionTier TestRecognizeRoutes TestResolveAIConfigPrecedence TestRouteTablePhase14 TestRouteTablePhase13 TestRouteTablePhase12 TestPhase14Threats TestPhase14Edges TestPhase14Jobs TestPhase14HandlersFailClosed FuzzWriteEndpoints" NAMED_APP_CLASSES="TestRecognizeAlbums TestPhase14Classes" NAMED_APP_DISCOGS="TestPHPTruthDiscogs TestClientGetRelease TestBucketID TestDiscogsClientStatuses TestRateLimiterBudget TestRateLimiterSyncFromHeaders TestRegisterRetryAfter TestDiscogsDomainVectors TestRateLimiterPostgresAcquire TestDiscogsRateWindowConcurrent TestCoverFetcherHostLock TestNumberFormat4" NAMED_APP_CONSOLE="TestPruneNotifications TestReindexCommand" NAMED_APP_API="TestPhase14ControllerHelpers" NAMED_APP_GOLEM="TestGolemPluginBoot TestImportSettings TestAdminModelsForm TestGolemAdminSchemas TestGolemImportCommand" NAMED_APP_GOLEM_PROVIDERS="TestOpenAIAdapterPayload TestAnthropicAdapterPayload" NAMED_APP_GOLEM_SERVICES="TestAIServiceFailures TestAIServiceStream TestDefaultModelQuirk TestAdminModelTrusted" NAMED_APP_GOLEM_FACTORIES="TestPromptFactory" NAMED_APP_GOLEM_SECURITY="TestSSRFGuard" NAMED_APP_FEEDBACK="TestFeedbackPluginBoot TestFeedbackConfig TestKeyMatches TestURLHost TestAllowedOriginHosts TestFeedbackSubmit TestMeHidden TestFeedbackApiArrayHook TestFeedbackOptionsPreflight TestEmbedJSServed TestFeedbackImportSettings TestFeedbackAdminSchemas TestFeedbackEdges" NAMED_APP_FEEDBACK_CLASSES="TestSyncG15Office TestTaskText TestImageGuardCopy" NAMED_APP_PARITY="TestParityCorpus TestCheckCorpusPortedCaseStatus TestUpstreamSidecarsAreReplayed TestFeedbackJobSidecarMatchesPlugin TestFonotekaNuxtFlows TestCheckCorpusUpstreamCredential TestParityContract TestSchemaMatchesPHPSnapshot" all_named() { echo "$NAMED_ROOT_FETCHGUARD $NAMED_ROOT_TIDE $NAMED_ROOT_SUNSCREEN $NAMED_ROOT_SURF $NAMED_ROOT_BEACHCOMBER $NAMED_ROOT_TYPESENSE $NAMED_ROOT_BUILD $NAMED_ROOT_SUMMER $NAMED_APP_FONOTEKA $NAMED_APP_CLASSES $NAMED_APP_DISCOGS $NAMED_APP_CONSOLE $NAMED_APP_API $NAMED_APP_GOLEM $NAMED_APP_GOLEM_PROVIDERS $NAMED_APP_GOLEM_SERVICES $NAMED_APP_GOLEM_FACTORIES $NAMED_APP_GOLEM_SECURITY $NAMED_APP_FEEDBACK $NAMED_APP_FEEDBACK_CLASSES $NAMED_APP_PARITY" } run_named() { phase14_tests "$ROOT" ./modules/fetchguard $NAMED_ROOT_FETCHGUARD phase14_tests "$ROOT" ./modules/tide $NAMED_ROOT_TIDE phase14_tests "$ROOT" ./modules/sunscreen $NAMED_ROOT_SUNSCREEN phase14_tests "$ROOT" ./modules/surf $NAMED_ROOT_SURF phase14_tests "$ROOT" ./modules/beachcomber $NAMED_ROOT_BEACHCOMBER phase14_tests "$ROOT" ./modules/beachcomber/typesense $NAMED_ROOT_TYPESENSE phase14_tests "$ROOT" ./internal/build $NAMED_ROOT_BUILD phase14_tests "$ROOT" ./cmd/summer $NAMED_ROOT_SUMMER phase14_tests "$APP" ./plugins/golem15/fonoteka -race $NAMED_APP_FONOTEKA phase14_tests "$APP" ./plugins/golem15/fonoteka/classes $NAMED_APP_CLASSES phase14_tests "$APP" ./plugins/golem15/fonoteka/classes/discogs -race $NAMED_APP_DISCOGS phase14_tests "$APP" ./plugins/golem15/fonoteka/console $NAMED_APP_CONSOLE phase14_tests "$APP" ./plugins/golem15/fonoteka/controllers/api $NAMED_APP_API phase14_tests "$APP" ./plugins/golem15/golem -race $NAMED_APP_GOLEM phase14_tests "$APP" ./plugins/golem15/golem/classes/providers $NAMED_APP_GOLEM_PROVIDERS phase14_tests "$APP" ./plugins/golem15/golem/classes/services -race $NAMED_APP_GOLEM_SERVICES phase14_tests "$APP" ./plugins/golem15/golem/classes/factories $NAMED_APP_GOLEM_FACTORIES phase14_tests "$APP" ./plugins/golem15/golem/classes/security $NAMED_APP_GOLEM_SECURITY phase14_tests "$APP" ./plugins/golem15/feedback -race $NAMED_APP_FEEDBACK phase14_tests "$APP" ./plugins/golem15/feedback/classes $NAMED_APP_FEEDBACK_CLASSES phase14_tests "$APP" ./parity $NAMED_APP_PARITY echo "phase14 named passed" } # coverage_report FLOOR PROFILE... prints one line per package of the merged # profiles (a block counts as covered when any profile covered it) and # refuses any package below FLOOR percent. COVERAGE_ONLY limits the report # to packages whose import path ends with one of its words; COVERAGE_SKIP # drops packages whose import path ends with one of its words (each a # test-only helper named, with its reason, in COVERAGE_EXEMPT_PACKAGES). coverage_report() { python3 - "$@" <<'PY' import collections, os, sys floor = float(sys.argv[1]) only = os.environ.get("COVERAGE_ONLY", "").split() skip = os.environ.get("COVERAGE_SKIP", "").split() blocks = {} for path in sys.argv[2:]: for line in open(path): if line.startswith("mode:") or not line.strip(): continue loc, n, c = line.rsplit(" ", 2) n, c = int(n), int(c) prev = blocks.get(loc, (n, 0)) blocks[loc] = (n, max(prev[1], c)) total, covered = collections.Counter(), collections.Counter() for loc, (n, c) in blocks.items(): pkg = loc.split(":")[0].rsplit("/", 1)[0] if only and not any(pkg.endswith(o) for o in only): continue if any(pkg.endswith(s) for s in skip): continue total[pkg] += n if c: covered[pkg] += n if not total: print("refuse: coverage profile is empty", file=sys.stderr) sys.exit(1) low = [] for pkg in sorted(total): pct = 100.0 * covered[pkg] / total[pkg] print(f"coverage {pkg} {pct:.1f}%") if pct < floor: low.append(f"{pkg} {pct:.1f}%") if low: print(f"refuse: below the {floor:.1f}% coverage floor: " + ", ".join(low), file=sys.stderr) sys.exit(1) PY } # PHASE14_FUNCS: every function Phase 14 added or changed in the fonoteka # plugin's root, classes and controllers/api packages (a function whose # lines the 14-02..14-05 diff touched), by file. Each must reach the # function floor by go tool cover -func over every test of the plugin. PHASE14_FUNCS='classes/ai_config_resolver.go SetAdminVisionModel AdminVisionModel ResolveAIConfig UserAIConfig ModelConfig orgAIConfig aiConfigFrom classes/album_recognition.go Error RecognizeAlbums finishReason stripFences decodeRecognition normalizeRecognition sortedJSONKeys coalesce castString nullableString recognitionYear recognitionFormat recognitionTracklist collectionGenreNames recognitionLanguage RecognitionPrompt classes/album_write_service.go SetAlbumField SaveAlbumRow albumFillValue isEmptyFill withTracklistText albumPivotCount FillEmptyFromCsv CreateCsvAlbum ImportCsvCovers csvInputOf ApplyCsvFill ApplyCsvOverwrite CreateCanonicalCsvAlbum ResolveCsvStyleIDs ResolveGenreID SyncCsvRating classes/cover_importer.go NewGuardedCoverImporter allowHosts fetch classes/csv_canonical_matcher.go MatchCanonicalCsvAlbum classes/csv_import_service.go UpdateCsvMapping FetchRelease UpdateCsvRow saveCsvRowLocked lockCsvImport CancelCsvImport classes/gates.go AIAllowed classes/php_values.go PHPIsNumeric PHPFloatString PHPFloatOf controllers/api/album_cover_fetch_controller.go AlbumCoverPriceFetch controllers/api/credentials_controller.go DiscogsCredentialTest AICredentialTest aiTestConfig controllers/api/discogs_import_controller.go DiscogsImport controllers/api/inbound_limits.go NewInboundLimits DiscogsMissing DiscogsImport Recognize controllers/api/recognize_controller.go AlbumRecognize controllers/api/release_match_controller.go discogsLang appConfig writeDiscogsDisabled discogsAllowed discogsGateAndLimit validateDiscogsInput writeDiscogsCallError discogsErrorClass discogsClientFor AlbumReleaseMatch searchCandidates mapValue phpListValues filled yearWithin mediumMatches AlbumReleaseMatchDraft draftAlbum AlbumApplyRelease applyRelease appliedAlbumDTO discogsCoverImporter nonNil controllers/api/wishlist_release_match_controller.go WishlistReleaseMatch WishlistApplyRelease csv_import_job.go importCsv deliverCsvImport setStatus importerCanWrite stopUnauthorized fail writeOptions writeRow markRowWritten isCanonicalCsvRow inputForRow csvFieldsMap rowCellList decodePHPObject csv_match_job.go matchCsv deliverCsvMatch loadUser lostImport fail pause matchRow searchResults saveRow unwrapResults decodeRowCells rowCell nullableString orEmptyMap emptyCandidates discogs_wiring.go FetchRelease discogsErrorClass golem_wiring.go golemVisionModel installGolemWiring jobs.go Jobs mail.go MailTemplates plugin.go inboundLimits Requires Register Boot Commands routes.go Routes wishlist_digest_job.go sendWishlistDigest deliverWishlistDigest mailWishlistDigest' # PHASE14_FUNC_EXEMPT: functions held below the floor, with the reason. # Each still has to appear in the profile. PHASE14_FUNC_EXEMPT='classes/cover_importer.go fetch the one-shot fetchguard.Fetch path ignores the WithTransport seam, so its 2xx and status branches run only against the live Discogs image hosts (deferred-items.md, 14-03) controllers/api/release_match_controller.go validateDiscogsInput lagoon.ValidateRequest fails only on a malformed rule table or a database rule, and the Discogs tables have neither' # COVERAGE_EXEMPT_PACKAGES: packages outside the floor, with the reason. COVERAGE_EXEMPT_PACKAGES='internal/pgtest test-only helper of each shared plugin: its uncovered lines are the container start failures' # funcs_floor FLOOR SPEC EXEMPT reads go tool cover -func output on stdin # and refuses a SPEC function below FLOOR (unless EXEMPT names it with a # reason) or missing from the profile. funcs_floor() { python3 -c ' import sys floor, spec, exempt = float(sys.argv[1]), sys.argv[2], sys.argv[3] want = {} for line in spec.strip().splitlines(): parts = line.split() for name in parts[1:]: want.setdefault((parts[0], name), []) skip = {} for line in exempt.strip().splitlines(): parts = line.split(None, 2) if len(parts) < 3 or not parts[2].strip(): print(f"refuse: exemption without a reason: {line}", file=sys.stderr) sys.exit(1) skip[(parts[0], parts[1])] = parts[2] for line in sys.stdin: parts = line.split() if len(parts) < 3 or parts[0] == "total:": continue path = parts[0].rsplit(":", 2)[0] for (f, name) in want: if parts[1] == name and (path == f or path.endswith("/" + f)): want[(f, name)].append(float(parts[-1].rstrip("%"))) low, missing = [], [] for (f, name), pcts in sorted(want.items()): if not pcts: missing.append(f + " " + name) continue pct = min(pcts) note = "" if (f, name) in skip: note = " (exempt: " + skip[(f, name)] + ")" elif pct < floor: low.append(f"{f} {name} {pct:.1f}%") print(f"coverage {f} {name} {pct:.1f}%{note}") if missing: print("refuse: Phase 14 functions missing from the profile: " + ", ".join(missing), file=sys.stderr) sys.exit(1) if low: print(f"refuse: below the {floor:.0f}% function floor: " + ", ".join(low), file=sys.stderr) sys.exit(1) ' "$@" } # cover_profile DIR OUT ARGS... writes a coverage profile of go test ARGS. cover_profile() { local dir="$1" out="$2" shift 2 local log log="$(mktemp)" if ! (cd "$dir" && go test -count=1 -coverprofile="$out" "$@") >"$log" 2>&1; then tail -n 40 "$log" >&2 rm -f "$log" echo "refuse: go test -coverprofile $* in $dir" >&2 exit 1 fi rm -f "$log" } run_coverage() { local dir dir="$(mktemp -d)" trap 'rm -rf "$dir"' RETURN local pkg i=0 # Framework packages changed in Phase 14: each package's own tests. for pkg in ./modules/fetchguard ./modules/tide ./modules/sunscreen ./modules/beachcomber ./modules/beachcomber/typesense; do i=$((i + 1)) cover_profile "$ROOT" "$dir/root$i.out" "$pkg" done coverage_report "$COVERAGE_FLOOR" "$dir"/root*.out # The application: classes/discogs and console at the package floor, # every Phase 14 function of the root, classes and controllers/api # packages at the function floor. local f=./plugins/golem15/fonoteka cover_profile "$APP" "$dir/app.out" "$f/..." \ -coverpkg="$f,$f/classes,$f/classes/discogs,$f/console,$f/controllers/api" COVERAGE_ONLY="/classes/discogs /console" coverage_report "$COVERAGE_FLOOR" "$dir/app.out" (cd "$APP" && go tool cover -func="$dir/app.out") | funcs_floor "$COVERAGE_FLOOR" "$PHASE14_FUNCS" "$PHASE14_FUNC_EXEMPT" # The two shared plugins: every package at the floor over all the # plugin's tests, the test-only helper excepted. local skip skip="$(awk '{print $1}' <<<"$COVERAGE_EXEMPT_PACKAGES" | tr '\n' ' ')" cover_profile "$APP" "$dir/golem.out" ./plugins/golem15/golem/... -coverpkg=./plugins/golem15/golem/... COVERAGE_SKIP="$skip" coverage_report "$COVERAGE_FLOOR" "$dir/golem.out" cover_profile "$APP" "$dir/feedback.out" ./plugins/golem15/feedback/... -coverpkg=./plugins/golem15/feedback/... COVERAGE_SKIP="$skip" coverage_report "$COVERAGE_FLOOR" "$dir/feedback.out" echo "phase14 coverage passed" } # removal_table: the RC rows of 14-SECURITY-REVIEW.md. Fields: id, threat, # repo (root|app|script), file, anchor, replacement, package, test regex. # Anchors must occur exactly once. app files include the shared plugins' # submodule checkouts; a dirty file there is refused like any other. removal_table() { cat <<'EOF' [ ["RC-01", "T-14-01", "root", "modules/fetchguard/client.go", "\tif scheme != \"https\" {\n\t\treturn &Error{Reason: ReasonScheme}\n\t}\n\tif c.policy.Mode == AllowHostsMode", "\tif false && scheme != \"https\" {\n\t\treturn &Error{Reason: ReasonScheme}\n\t}\n\tif c.policy.Mode == AllowHostsMode", "./modules/fetchguard", "^TestClientSchemeGuard$"], ["RC-02", "T-14-01", "root", "modules/fetchguard/fetch.go", "\t\t\treturn http.ErrUseLastResponse\n", "\t\t\treturn nil\n", "./modules/fetchguard", "^TestClientNeverFollowsRedirects$"], ["RC-03", "T-14-02", "root", "modules/fetchguard/client.go", "type Client struct {\n", "type Client struct {\n\tTransport http.RoundTripper\n", "./modules/fetchguard", "^TestTransportSeamIsCodeOnly$"], ["RC-04", "T-14-03", "root", "modules/sunscreen/sunscreen.go", "\t\"authorization\",\n", "", "./modules/sunscreen", "^TestRedactHandler$"], ["RC-05", "T-14-04", "root", "modules/tide/upstream.go", "if isCredentialHeader(name) && !fullyMasked(v) {", "if false && isCredentialHeader(name) && !fullyMasked(v) {", "./modules/tide", "^TestWriteUpstreamRefusesUnmaskedCredential$"], ["RC-06", "T-14-05", "root", "modules/tide/upstream_proxy.go", "\tif err := requireLoopbackAddr(cfg.Listen); err != nil {", "\tif err := error(nil); err != nil {", "./modules/tide", "^TestUpstreamProxyRefusesNonLoopback$"], ["RC-07", "T-14-06", "root", "modules/fetchguard/client.go", "resp.Body = &cappedBody{rc: resp.Body, max: c.maxBytes}", "resp.Body = &cappedBody{rc: resp.Body, max: 1 << 62}", "./modules/fetchguard", "^TestClientBodyCap$"], ["RC-08", "T-14-07", "root", "modules/beachcomber/typesense/engine.go", "\tcase code == http.StatusNotFound:\n\t\treturn false, nil", "\tcase code == http.StatusNotFound:\n\t\treturn true, nil", "./modules/beachcomber/typesense", "^TestEngineDropIndex$"], ["RC-09", "T-14-08", "app", "plugins/golem15/fonoteka/classes/discogs/rate_limiter.go", "\treturn hex.EncodeToString(m.Sum(nil))[:32]", "\t_ = hex.EncodeToString(m.Sum(nil))[:32]\n\treturn token", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-08$"], ["RC-10", "T-14-09", "app", "plugins/golem15/fonoteka/classes/discogs/rate_store.go", "OR w.hits < ?\nRETURNING", "OR w.hits < ? OR TRUE\nRETURNING", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-09$"], ["RC-11", "T-14-10", "app", "plugins/golem15/fonoteka/classes/csv_import_service.go", "\t\tif !csvBeforeCommit(cur.Status) {\n\t\t\treturn ErrCsvAlreadyCommitted\n\t\t}\n\t\tvar rows []models.CsvImportRow", "\t\tvar rows []models.CsvImportRow", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-10$"], ["RC-12", "T-14-11", "app", "plugins/golem15/fonoteka/classes/csv_import_service.go", "func csvAllowedCandidate(row *models.CsvImportRow, selected string) bool {\n\tif !row.CandidatesJSON.Valid {", "func csvAllowedCandidate(row *models.CsvImportRow, selected string) bool {\n\tif selected != \"\" {\n\t\treturn true\n\t}\n\tif !row.CandidatesJSON.Valid {", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-11$"], ["RC-13", "T-14-12", "app", "plugins/golem15/fonoteka/csv_match_job.go", "\treturn m.d.Jobs.FailJob(ctx, jobID, map[string]any{\"error\": msg})\n}\n\n// pause", "\t_ = m.d.Jobs.FailJob(ctx, jobID, map[string]any{\"error\": msg})\n\treturn cause\n}\n\n// pause", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-12$"], ["RC-14", "T-14-13", "app", "plugins/golem15/fonoteka/csv_import_job.go", "\treturn n > 0, err\n}\n\nfunc (w *csvWriter) stopUnauthorized", "\treturn true, err\n}\n\nfunc (w *csvWriter) stopUnauthorized", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-13$"], ["RC-15", "T-14-14", "app", "plugins/golem15/fonoteka/wishlist_digest_job.go", "wishlists[0].Kind != \"wishlist\" || ", "", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-14$"], ["RC-16", "T-14-15", "app", "plugins/golem15/fonoteka/console/reindex.go", "\t\t\tif tenantless > 0 {\n\t\t\t\treturn fail(reindexTenantless)\n\t\t\t}\n", "", "./plugins/golem15/fonoteka/console", "^TestReindexCommand$"], ["RC-17", "T-14-16", "app", "plugins/golem15/fonoteka/controllers/api/release_match_controller.go", "func AlbumReleaseMatch(app *backpack.App, limits *InboundLimits) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\ts, ok := resolveAlbumScope(w, r, app)\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\talbum, ok := s.findAlbum(w, r)", "func AlbumReleaseMatch(app *backpack.App, limits *InboundLimits) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\ts, ok := resolveAlbumScope(w, r, app)\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\talbum, ok := &models.Album{ID: pathID(r, \"id\")}, true", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-16$"], ["RC-18", "T-14-17", "app", "plugins/golem15/fonoteka/controllers/api/inbound_limits.go", "\tdiscogsMissingMax = 60\n", "\tdiscogsMissingMax = 6000\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-17$"], ["RC-19", "T-14-18", "app", "plugins/golem15/fonoteka/classes/cover_importer.go", "\treturn host == \"discogs.com\" || (ci.HostSuffix != \"\" && strings.HasSuffix(host, strings.ToLower(ci.HostSuffix)))", "\treturn host != \"\"", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-18$"], ["RC-20", "T-14-19", "app", "plugins/golem15/fonoteka/classes/discogs/applicator.go", "\t\t\tif !overwriteAll && !isBlankValue(current) {\n\t\t\t\tcontinue\n\t\t\t}\n", "", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-19$"], ["RC-21", "T-14-19", "app", "plugins/golem15/fonoteka/classes/discogs/applicator.go", "\t\t\tif dryRun && slices.Contains(applicatorDraftFields, field) {", "\t\t\tif false && dryRun && slices.Contains(applicatorDraftFields, field) {", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-19$"], ["RC-22", "T-14-20", "app", "plugins/golem15/fonoteka/routes.go", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:write\", \"throttle:12,1\")", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:read\", \"throttle:12,1\")", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-20$"], ["RC-23", "T-14-20", "app", "plugins/golem15/fonoteka/routes.go", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:write\", \"throttle:12,1\")", "api.AlbumCoverPriceFetch(p.app), \"inv.scope:read\", \"throttle:12,1\")", "./plugins/golem15/fonoteka", "^TestRouteTablePhase14$"], ["RC-24", "T-14-21", "app", "plugins/golem15/fonoteka/controllers/api/credentials_controller.go", "\t\t\twriteJSON(w, http.StatusOK, discogsTestResult{Error: discogsLang(r, app, \"unavailable\", nil)})", "\t\t\twriteJSON(w, http.StatusOK, discogsTestResult{Error: err.Error()})", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-21$"], ["RC-25", "T-14-22", "app", "plugins/golem15/fonoteka/classes/ai_config_resolver.go", "\t\tif err := security.AssertSafeURL(ctx, *baseURL, security.AllowedHosts(cfg)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n", "\t\t_ = security.AllowedHosts(cfg)\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-22$"], ["RC-26", "T-14-23", "app", "plugins/golem15/fonoteka/classes/ai_config_resolver.go", "\tc := &AIConfig{Adapter: \"openai\", APIKey: key, BaseURL: openAIBaseURL}", "\tc := &AIConfig{Adapter: \"openai\", APIKey: key, BaseURL: openAIBaseURL, Trusted: true}", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-23$"], ["RC-27", "T-14-24", "app", "plugins/golem15/golem/models/ai_model.go", "gorm:\"column:api_key\" json:\"-\"", "gorm:\"column:api_key\" json:\"api_key\"", "./plugins/golem15/golem", "^TestGolemAdminSchemas$"], ["RC-28", "T-14-24", "app", "plugins/golem15/golem/models/ai_model.go", "gorm:\"column:api_key\" json:\"-\"", "gorm:\"column:api_key\" json:\"api_key\"", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-24$"], ["RC-29", "T-14-25", "app", "plugins/golem15/fonoteka/classes/album_recognition.go", "\tRecognitionMaxAlbums = 30\n", "\tRecognitionMaxAlbums = 3000\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-25$"], ["RC-30", "T-14-26", "app", "plugins/golem15/fonoteka/controllers/api/inbound_limits.go", "\trecognizeMax = 10\n", "\trecognizeMax = 1000\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-26$"], ["RC-31", "T-14-27", "app", "plugins/golem15/fonoteka/controllers/api/recognize_controller.go", "\t\tif !classes.IsAllowedImage(data) {", "\t\tif false && !classes.IsAllowedImage(data) {", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-27$"], ["RC-32", "T-14-29", "app", "plugins/golem15/golem/README.md", "# golem15.golem\n\n", "# golem15.golem\n\nsk-PLANTEDsecretPLANTEDsecretPLANTED\n\n", "./plugins/golem15/fonoteka", "^TestPhase14Threats$/^T-14-29$"], ["RC-33", "T-14-30", "app", "plugins/golem15/feedback/controllers/api/feedback_api_controller.go", "\treturn subtle.ConstantTimeCompare([]byte(settings.WidgetKey), []byte(key)) == 1", "\t_ = subtle.ConstantTimeCompare([]byte(settings.WidgetKey), []byte(key))\n\treturn true", "./plugins/golem15/feedback", "^TestFeedbackConfig$"], ["RC-43", "T-14-30", "app", "plugins/golem15/feedback/controllers/api/feedback_api_controller.go", "\t\tif a == host {", "\t\tif a == host || a != host {", "./plugins/golem15/feedback", "^TestFeedbackConfig$"], ["RC-34", "T-14-31", "app", "plugins/golem15/feedback/plugin.go", "BucketSubmit: {Max: 10, Decay: time.Minute, Key: byIP},", "BucketSubmit: {Max: 1000, Decay: time.Minute, Key: byIP},", "./plugins/golem15/feedback", "^TestFeedbackEdges$"], ["RC-35", "T-14-32", "app", "plugins/golem15/feedback/controllers/api/feedback_api_controller.go", "\t\t\tif !classes.IsAllowedImage(data) {", "\t\t\tif false && !classes.IsAllowedImage(data) {", "./plugins/golem15/feedback", "^TestFeedbackSubmit$"], ["RC-36", "T-14-33", "app", "plugins/golem15/feedback/classes/g15office_client.go", "\"Authorization\": {fetchguard.Bearer(c.token)},", "\"Authorization\": {fetchguard.Bearer(\"\")},", "./plugins/golem15/feedback/classes", "^TestSyncG15Office$"], ["RC-37", "T-14-34", "app", "plugins/golem15/feedback/classes/g15office_client.go", "\t\tMode: fetchguard.AllowHostsMode,\n\t\tAllowHosts: []string{u.Hostname()},", "\t\tMode: fetchguard.TrustedMode,\n\t\tAllowHosts: []string{u.Hostname()},", "./plugins/golem15/feedback/classes", "^TestSyncG15Office$"], ["RC-38", "T-14-35", "app", "plugins/golem15/feedback/controllers/api/me_hidden_controller.go", "models.SetWidgetHidden(ctx, gdb, user.ID, hidden)", "models.SetWidgetHidden(ctx, gdb, user.ID+1, hidden)", "./plugins/golem15/feedback", "^TestMeHidden$"], ["RC-39", "T-14-36", "app", "plugins/golem15/feedback/models/submission/columns.yaml", " message:\n label: golem15.feedback::lang.submissions.message\n type: text", " message:\n label: golem15.feedback::lang.submissions.message\n type: partial", "./plugins/golem15/feedback", "^TestFeedbackAdminSchemas$"], ["RC-40", "T-14-37", "script", "scripts/check-phase14.sh", "if total != routes or recorded != total or passing != ported or failing != 0 or unrecorded != 0 or pend != pending or passing + pend != total:\n print(", "if False:\n print(", "", "--self-test"], ["RC-41", "T-14-37", "script", "scripts/check-phase14.sh", " if action == \"skip\" and test:", " if False:", "", "--self-test"], ["RC-42", "T-14-38", "script", "scripts/check-phase14.sh", " if dirty:\n", " if False:\n", "", "--self-test"] ] EOF } # removal_harness TABLE_FILE: for each row, refuse a file with uncommitted # changes, save it, apply the anchor-exact mutation, run the named test (or, # for the gate script, its --self-test on a mutated copy) and require it to # fail on an assertion, then restore the file and require cmp to match. # PHASE14_RC limits the run to the listed row ids. removal_harness() { python3 - "$1" "$ROOT" "$APP" <<'PY' import json, os, shutil, signal, subprocess, sys, tempfile table = json.load(open(sys.argv[1])) root, app = sys.argv[2], sys.argv[3] only = set(os.environ.get("PHASE14_RC", "").split()) current = {} def restore(*_): # A signal mid-run still puts the file back. if current: with open(current["path"], "wb") as fh: fh.write(current["original"]) sys.exit(1) signal.signal(signal.SIGINT, restore) signal.signal(signal.SIGTERM, restore) failures = 0 for rc, threat, repo, rel, anchor, repl, pkg, run in table: if only and rc not in only: continue base = {"root": root, "app": app, "script": root}[repo] path = os.path.join(base, rel) tracked = subprocess.run(["git", "-C", os.path.dirname(path), "rev-parse", "--is-inside-work-tree"], capture_output=True, text=True).returncode == 0 if tracked and repo != "script": dirty = subprocess.run(["git", "-C", os.path.dirname(path), "status", "--porcelain", "--", os.path.basename(path)], capture_output=True, text=True).stdout.strip() if dirty: print(f"refuse: {rc}: {rel} is dirty; commit or restore it first", file=sys.stderr) sys.exit(1) original = open(path, "rb").read() text = original.decode() n = text.count(anchor) if n != 1: print(f"refuse: {rc} {threat}: anchor occurs {n} times in {rel}", file=sys.stderr) sys.exit(1) mutated = text.replace(anchor, repl, 1) scratch = tempfile.mkdtemp(prefix="phase14-rc-") saved = os.path.join(scratch, "saved") shutil.copyfile(path, saved) try: if repo == "script": copy = os.path.join(scratch, os.path.basename(rel)) open(copy, "w").write(mutated) env = dict(os.environ, PHASE14_ROOT=root, PHASE14_APP=app) proc = subprocess.run(["bash", copy, run], cwd=root, env=env, capture_output=True, text=True, timeout=900) out = proc.stdout + proc.stderr ok = proc.returncode != 0 and "refuse:" in out evidence = next((l for l in out.splitlines() if l.startswith("refuse:")), "") else: current.update(path=path, original=original) with open(path, "w") as fh: fh.write(mutated) proc = subprocess.run(["go", "test", pkg, "-run", run, "-count=1"], cwd=base, capture_output=True, text=True, timeout=1800) out = proc.stdout + proc.stderr build = "[build failed]" in out or "[setup failed]" in out ok = proc.returncode != 0 and "--- FAIL" in out and not build fails = [l.strip() for l in out.splitlines() if l.strip().startswith("--- FAIL")] names = [l.split()[2] for l in fails if len(l.split()) > 2] evidence = ", ".join(names[:5]) + (f" (+{len(names) - 5} more)" if len(names) > 5 else "") if names else ("build failed" if build else "no failure") finally: with open(path, "wb") as fh: fh.write(original) current.clear() same = subprocess.run(["cmp", "-s", saved, path]).returncode == 0 shutil.rmtree(scratch, ignore_errors=True) if not same: print(f"refuse: {rc}: {rel} was not restored byte for byte", file=sys.stderr) sys.exit(1) status = "fails as required" if ok else "SURVIVED" print(f"{rc} {threat} {rel}: {status}: {evidence}", flush=True) if not ok: failures += 1 if failures: print(f"refuse: {failures} removal check(s) survived", file=sys.stderr) sys.exit(1) PY } run_removal() { local table table="$(mktemp)" removal_table >"$table" if ! removal_harness "$table"; then rm -f "$table" exit 1 fi rm -f "$table" echo "phase14 removal passed" } # removal_harness_in ROOT TABLE runs the harness against another root. removal_harness_in() { local root="$1" table="$2" ( ROOT="$root" APP="$root" export GOWORK=off GOFLAGS=-mod=mod removal_harness "$table" ) } run_self_test() { bash -n "${BASH_SOURCE[0]}" expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestPhase14Threats"}' expect_detect fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p","Test":"TestPhase14Threats/T-14-16"}' expect_detect skip 2 '{"Action":"skip","Package":"p","Test":"TestPhase14Threats"}' expect_detect zero 3 '{"Action":"pass","Package":"p"}' expect_detect no-tests-to-run 3 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"output","Package":"q","Output":"testing: warning: no tests to run\n"}' expect_detect nonjson 4 '{"Action":"pass",' expect_detect build 1 '{"Action":"build-fail","ImportPath":"p"} {"Action":"pass","Package":"q","Test":"TestA"}' expect_detect build-flag 1 '{"Action":"pass","Package":"q","Test":"TestA"} {"Action":"fail","Package":"p","FailedBuild":"p"}' expect_detect package 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p"}' expect_detect race 6 '{"Action":"output","Package":"p","Test":"TestA","Output":"WARNING: DATA RACE\n"} {"Action":"pass","Package":"p","Test":"TestA"}' PHASE14_REQUIRE="TestRouteTablePhase14 TestPhase14Threats" expect_detect required 5 \ '{"Action":"pass","Package":"p","Test":"TestRouteTablePhase14"}' local flag for flag in $(grep -oE '^ check-phase14\.sh --[a-z-]+' "${BASH_SOURCE[0]}" | awk '{print $2}'); do grep -q -- "^$flag)" "${BASH_SOURCE[0]}" || { echo "refuse: usage names $flag but the script has no such mode" >&2 exit 1 } done if [[ -n "${FORCE_COLOR+x}" ]]; then echo "refuse: self-test FORCE_COLOR is still set" >&2 exit 1 fi local scratch scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' RETURN # Module hygiene refuses a vendor SDK and accepts the framework graph. printf 'git.golem15.com/golem15/summercms v0.0.0\ngorm.io/gorm v1.31.2\n' >"$scratch/mods" module_hygiene "$scratch/mods" 2>/dev/null || { echo "refuse: self-test module_hygiene rejected a clean graph" >&2 exit 1 } local plant for plant in 'github.com/anthropics/anthropic-sdk-go v1.2.0' 'github.com/openai/openai-go v1.0.0' 'github.com/sashabaranov/go-openai v1.36.0'; do printf 'gorm.io/gorm v1.31.2\n%s\n' "$plant" >"$scratch/mods" if module_hygiene "$scratch/mods" 2>/dev/null; then echo "refuse: self-test module_hygiene accepted $plant" >&2 exit 1 fi done # The corpus scan refuses each planted secret shape and accepts # synthetic values. mkdir -p "$scratch/corpus" printf 'go test fuzz v1\nstring("POST /x")\nstring("{\\"status\\":\\"imported\\",\\"pad\\":\\"QQQQ\\"}")\n' >"$scratch/corpus/seed" corpus_scan "$scratch/corpus" 2>/dev/null || { echo "refuse: self-test corpus_scan rejected a synthetic seed" >&2 exit 1 } local secret for secret in "$(printf 'a%.0s' $(seq 64))" "inv_ABCDEFGHIJKLMNOPQRST" "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.sig" "Bearer abcdefghijklmnop" "sk-ant-api03-abcdefghijklmnopqrstuv"; do printf 'string("%s")\n' "$secret" >"$scratch/corpus/planted" if corpus_scan "$scratch/corpus" 2>/dev/null; then echo "refuse: self-test corpus_scan accepted a planted secret ${secret:0:12}" >&2 exit 1 fi done rm -f "$scratch/corpus/planted" "$scratch/corpus/seed" if corpus_scan "$scratch/corpus" 2>/dev/null; then echo "refuse: self-test corpus_scan accepted an empty corpus" >&2 exit 1 fi # The manifest counter reads only status lines, and the manifest check # refuses any count but the expected ones: a fourth pending route (one # of the ported routes flipped back) fails --parity. printf 'routes:\n - id: a\n status: ported\n - id: b\n status: pending\n - id: c\n status: ported\n# status: ported\n' >"$scratch/manifest.yaml" if [[ "$(manifest_count "$scratch/manifest.yaml" ported)" -ne 2 || "$(manifest_count "$scratch/manifest.yaml" pending)" -ne 1 ]]; then echo "refuse: self-test manifest_count miscounted" >&2 exit 1 fi plant_manifest() { local ported="$1" pending="$2" other="${3:-0}" i { echo "routes:" for ((i = 0; i < ported; i++)); do printf ' - id: "r%d"\n status: ported\n' "$i"; done for ((i = 0; i < pending; i++)); do printf ' - id: "p%d"\n status: pending\n' "$i"; done for ((i = 0; i < other; i++)); do printf ' - id: "o%d"\n status: skipped\n' "$i"; done } >"$scratch/manifest.yaml" } plant_manifest "$EXPECTED_PORTED" "$EXPECTED_PENDING" manifest_check "$scratch/manifest.yaml" 2>/dev/null || { echo "refuse: self-test manifest_check rejected the expected counts" >&2 exit 1 } local counts for counts in "$((EXPECTED_PORTED - 1)) $((EXPECTED_PENDING + 1)) 0" "$EXPECTED_PORTED $((EXPECTED_PENDING + 1)) 0" "$((EXPECTED_PORTED - 1)) $EXPECTED_PENDING 1" "$((EXPECTED_PORTED + 1)) $((EXPECTED_PENDING - 1)) 0"; do # shellcheck disable=SC2086 plant_manifest $counts if manifest_check "$scratch/manifest.yaml" 2>/dev/null; then echo "refuse: self-test manifest_check accepted ported/pending/other $counts" >&2 exit 1 fi done # The corpus coverage line must show the expected counts. local total=$EXPECTED_ROUTES local line="recorded $total/$total passing $EXPECTED_PORTED failing 0 unrecorded 0 pending $EXPECTED_PENDING" printf '{"Action":"output","Package":"p","Test":"TestParityCorpus/coverage","Output":"%s\\n"}\n' "$line" >"$scratch/cov.json" corpus_coverage "$scratch/cov.json" >/dev/null 2>&1 || { echo "refuse: self-test corpus_coverage rejected the expected counts" >&2 exit 1 } local planted for planted in \ "recorded $total/$total passing $((EXPECTED_PORTED - 1)) failing 1 unrecorded 0 pending $EXPECTED_PENDING" \ "recorded $total/$total passing $((EXPECTED_PORTED - 1)) failing 0 unrecorded 0 pending $((EXPECTED_PENDING + 1))" \ "recorded $((total + 1))/$((total + 1)) passing $EXPECTED_PORTED failing 0 unrecorded 0 pending $((EXPECTED_PENDING + 1))" \ "recorded $((total - 1))/$total passing $EXPECTED_PORTED failing 0 unrecorded 1 pending $((EXPECTED_PENDING - 1))" \ "recorded $total/$total passing $((EXPECTED_PORTED + 1)) failing 0 unrecorded 0 pending $((EXPECTED_PENDING - 1))"; do printf '{"Action":"output","Package":"p","Output":"%s\\n"}\n' "$planted" >"$scratch/cov.json" if corpus_coverage "$scratch/cov.json" >/dev/null 2>&1; then echo "refuse: self-test corpus_coverage accepted: $planted" >&2 exit 1 fi done printf '{"Action":"pass","Package":"p","Test":"TestParityCorpus"}\n' >"$scratch/cov.json" if corpus_coverage "$scratch/cov.json" >/dev/null 2>&1; then echo "refuse: self-test corpus_coverage accepted a log without a coverage line" >&2 exit 1 fi # Every declared flow subtest is required: a run that lacks one fails # the required-test check. printf 'func TestFlows(t *testing.T) {\n\tt.Run("nuxt-a", f)\n\tt.Run("mcp-b", f)\n}\n' >"$scratch/flows_test.go" local subs subs="$(flow_subtests "$scratch/flows_test.go" TestFlows | tr '\n' ' ')" if [[ "$subs" != "TestFlows/nuxt-a TestFlows/mcp-b " ]]; then echo "refuse: self-test flow_subtests read: $subs" >&2 exit 1 fi PHASE14_REQUIRE="TestFlows $subs" expect_detect flow-missing 5 '{"Action":"pass","Package":"p","Test":"TestFlows"} {"Action":"pass","Package":"p","Test":"TestFlows/nuxt-a"}' # The coverage report refuses a package under the floor and accepts one # over it; a block covered by any profile counts once; COVERAGE_ONLY # narrows the report and COVERAGE_SKIP drops an exempt package. printf 'mode: set\nexample.test/a/x.go:1.1,2.2 8 1\nexample.test/a/x.go:3.1,4.2 2 0\n' >"$scratch/p1" printf 'mode: set\nexample.test/a/x.go:3.1,4.2 2 1\nexample.test/b/y.go:1.1,2.2 5 0\nexample.test/b/y.go:3.1,4.2 5 1\n' >"$scratch/p2" local out out="$(coverage_report 80 "$scratch/p1" 2>&1)" || { echo "refuse: self-test coverage_report refused 80% at an 80% floor: $out" >&2 exit 1 } if out="$(coverage_report 80 "$scratch/p1" "$scratch/p2" 2>&1)"; then echo "refuse: self-test coverage_report accepted a 50% package" >&2 exit 1 fi grep -q "coverage example.test/a 100.0%" <<<"$out" || { echo "refuse: self-test coverage_report did not merge profiles: $out" >&2 exit 1 } COVERAGE_ONLY="/a" coverage_report 80 "$scratch/p1" "$scratch/p2" >/dev/null 2>&1 || { echo "refuse: self-test COVERAGE_ONLY did not narrow the report" >&2 exit 1 } COVERAGE_SKIP="/b" coverage_report 80 "$scratch/p1" "$scratch/p2" >/dev/null 2>&1 || { echo "refuse: self-test COVERAGE_SKIP did not drop the exempt package" >&2 exit 1 } printf 'mode: set\n' >"$scratch/empty" if coverage_report 80 "$scratch/empty" 2>/dev/null; then echo "refuse: self-test coverage_report accepted an empty profile" >&2 exit 1 fi # The function floor refuses a listed function below it or missing from # the profile, and an exemption without a reason; an exempt function # with a reason passes. printf 'example.test/p/csv_job.go:10:\tdeliver\t84.4%%\nexample.test/p/csv_job.go:40:\thelper\t79.9%%\nexample.test/p/other.go:5:\tdeliver\t10.0%%\ntotal:\t(statements)\t90.0%%\n' >"$scratch/func" funcs_floor 80 "csv_job.go deliver" "" <"$scratch/func" >/dev/null 2>&1 || { echo "refuse: self-test funcs_floor rejected a function over the floor" >&2 exit 1 } if funcs_floor 80 "csv_job.go deliver helper" "" <"$scratch/func" >/dev/null 2>&1; then echo "refuse: self-test funcs_floor accepted a 79.9% function" >&2 exit 1 fi if funcs_floor 80 "csv_job.go deliver gone" "" <"$scratch/func" >/dev/null 2>&1; then echo "refuse: self-test funcs_floor accepted a function missing from the profile" >&2 exit 1 fi funcs_floor 80 "csv_job.go deliver helper" "csv_job.go helper a recorded reason" <"$scratch/func" >/dev/null 2>&1 || { echo "refuse: self-test funcs_floor refused an exempt function" >&2 exit 1 } if funcs_floor 80 "csv_job.go deliver helper" "csv_job.go helper" <"$scratch/func" >/dev/null 2>&1; then echo "refuse: self-test funcs_floor accepted an exemption without a reason" >&2 exit 1 fi # The removal harness refuses an anchor that is not unique and a dirty # tracked file, restores the file byte for byte, and reports a mutation # whose test passes or does not build. local fake="$scratch/fake" mkdir -p "$fake/modules/acme" printf 'module example.test/acme\n\ngo 1.27\n' >"$fake/go.mod" printf 'package acme\n\nfunc Guard(n int) bool {\n\tif n > 3 {\n\t\treturn false\n\t}\n\treturn true\n}\n' >"$fake/modules/acme/acme.go" printf 'package acme\n\nimport "testing"\n\nfunc TestGuard(t *testing.T) {\n\tif Guard(4) {\n\t\tt.Fatal("guard removed")\n\t}\n}\n\nfunc TestOther(t *testing.T) {}\n' >"$fake/modules/acme/acme_test.go" cp "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" local table="$scratch/table.json" printf '[["RC-T1","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","./modules/acme","^TestGuard$"]]' >"$table" out="$(removal_harness_in "$fake" "$table" 2>&1)" || { echo "refuse: self-test removal harness did not catch a guarded mutation: $out" >&2 exit 1 } grep -q "RC-T1 T-X modules/acme/acme.go: fails as required" <<<"$out" || { echo "refuse: self-test removal harness output: $out" >&2 exit 1 } cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || { echo "refuse: self-test removal harness did not restore the file" >&2 exit 1 } printf '[["RC-T2","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","./modules/acme","^TestOther$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness accepted a mutation whose test passes: $out" >&2 exit 1 fi grep -q "RC-T2 T-X modules/acme/acme.go: SURVIVED" <<<"$out" || { echo "refuse: self-test removal harness refused a surviving mutation for the wrong reason: $out" >&2 exit 1 } printf '[["RC-T3","T-X","root","modules/acme/acme.go","return","x","./modules/acme","^TestGuard$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness accepted a non-unique anchor" >&2 exit 1 fi grep -q "anchor occurs 2 times" <<<"$out" || { echo "refuse: self-test removal harness refused a non-unique anchor for the wrong reason: $out" >&2 exit 1 } printf '[["RC-T5","T-X","root","modules/acme/acme.go","if n > 3 {","if n > 3 {\\n\\tundefinedCall()","./modules/acme","^TestGuard$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness counted a build failure as a failing test" >&2 exit 1 fi cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || { echo "refuse: self-test removal harness did not restore after a build failure" >&2 exit 1 } (cd "$fake" && git init -q && git add -A && git -c user.email=gate@example.test -c user.name=gate commit -qm init) >/dev/null printf '// local edit\n' >>"$fake/modules/acme/acme.go" printf '[["RC-T4","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","./modules/acme","^TestGuard$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness mutated a dirty file" >&2 exit 1 fi grep -q "is dirty" <<<"$out" || { echo "refuse: self-test removal harness refused a dirty file for the wrong reason: $out" >&2 exit 1 } # Every row of the real removal table names a unique anchor in the # current tree (the --removal stage would refuse it otherwise). removal_table >"$table" python3 - "$table" "$ROOT" "$APP" <<'PY' || exit 1 import json, os, sys table, root, app = json.load(open(sys.argv[1])), sys.argv[2], sys.argv[3] ids = set() for rc, threat, repo, rel, anchor, repl, pkg, run in table: if rc in ids: print(f"refuse: self-test duplicate removal row {rc}", file=sys.stderr) sys.exit(1) ids.add(rc) base = {"root": root, "app": app, "script": root}[repo] path = os.path.join(base, rel) if not os.path.isfile(path): print(f"refuse: self-test {rc}: {rel} is missing", file=sys.stderr) sys.exit(1) n = open(path).read().count(anchor) if n != 1: print(f"refuse: self-test {rc}: anchor occurs {n} times in {rel}", file=sys.stderr) sys.exit(1) PY echo "phase14 self-test passed" } case "${1:-}" in --self-test) run_self_test ;; --go) run_go ;; --parity) run_parity ;; --named) run_named ;; --removal) run_removal ;; --coverage) run_coverage ;; --all) # --removal edits tracked source while it runs, so it runs on its own. run_self_test run_go run_parity run_named run_coverage echo "phase14 all passed" ;; *) usage ;; esac