ci-required-drift / drift (pull_request) Has been cancelled
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 18s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 16s
CI / Python Lint & Test (pull_request) Successful in 20s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 44s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 30s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 29s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 29s
E2E Ephemeral CP Happy Path / detect (pull_request) Successful in 1m16s
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Successful in 25s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (pull_request) Successful in 33s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (pull_request) Successful in 27s
CI / Detect changes (pull_request) Successful in 2m3s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 1m20s
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (pull_request) Successful in 32s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (pull_request) Successful in 33s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (pull_request) Successful in 32s
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle (pull_request) Successful in 35s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 24s
E2E API Smoke Test / detect-changes (pull_request) Successful in 2m20s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 16s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 33s
E2E Chat / detect-changes (pull_request) Successful in 2m28s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 51s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 36s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 37s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m4s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 33s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 49s
lint-phantom-gate / lint-phantom-gate (pull_request) Successful in 42s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 25s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 38s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 41s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 33s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 40s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m17s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 25s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 39s
reserved-path-review / reserved-path-review (pull_request_target) Failing after 22s
template-delivery-e2e / detect-changes (pull_request) Successful in 33s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 1m6s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 2m21s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m58s
PR Diff Guard / PR diff guard (pull_request_target) Successful in 1m40s
E2E Staging SaaS (full lifecycle) / Prune stale e2e DNS records (pull_request) Successful in 15s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 17s
CI / Platform (Go) (pull_request) Successful in 9s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
CI / Canvas (Next.js) (pull_request) Successful in 10s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 8s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 10s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 8s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 9s
E2E Chat / E2E Chat (pull_request) Successful in 9s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 1m15s
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (pull_request) Successful in 9m43s
CI / Canvas Deploy Status (pull_request) Successful in 7s
CI / all-required (pull_request) Successful in 15s
audit-force-merge / audit (pull_request_target) Has been skipped
`scan-tenant-token-write` in .gitea/workflows/lint-forbidden-env-keys.yml
built its candidate set by grepping SURFACE_PATTERN across the tree, then:
if [ "${#CANDIDATES[@]}" -eq 0 ]; then
echo "OK No tenant-writer-surface files found in tree (unexpected, but not a lint failure)."
exit 0
fi
Zero candidates is exactly what a BROKEN input produces — a moved,
renamed or refactored package, or a typo in SURFACE_PATTERN. The lint
would scan nothing, prove nothing, and report PASS. Its own comment said
"unexpected", which is the tell. Under main's status_check_contexts=['*']
that green is merge-authorising.
Fail closed instead: zero candidates now goes RED naming the mechanism
and the remedy (update SURFACE_PATTERN if the code genuinely moved).
Also fixes the RED arm's diagnostic, found while mutation-proving: HITS
always begins with "--- <file> ---", so bare `printf "$HITS"` parsed the
"--" as an option ("printf: --: invalid option") and died at exit 2 under
`set -e` before printing WHICH file tripped the lint. `printf '%b'` fixes
it — the guard fired but could never name its finding. Pre-existing on
main; not introduced here.
Mutation-proof (verbatim in PR body): baseline GREEN with 28 candidates;
surface pattern mutated to match nothing -> RED exit 1; real violation
injected -> RED exit 1 and now names file:line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
378 lines
19 KiB
YAML
378 lines
19 KiB
YAML
name: Lint forbidden tenant-env keys
|
|
|
|
# RFC#523 Layer 3 (task #146): scan workspace_secrets-writer Go code
|
|
# under workspace-server/ for new code that hardcodes a forbidden
|
|
# operator-scope env var NAME (GITEA_TOKEN, CP_ADMIN_API_TOKEN,
|
|
# RAILWAY_TOKEN, INFISICAL_OPERATOR_TOKEN, MOLECULE_OPERATOR_*, …).
|
|
#
|
|
# Catches the class "a new writer accidentally widens the propagation
|
|
# set" — e.g. a future env-mutator plugin that sets envVars["GITEA_TOKEN"]
|
|
# directly. Today the L1 runtime guard would abort the provision, but
|
|
# this lint surfaces the offending code at PR review time instead of
|
|
# at first provision attempt.
|
|
#
|
|
# Companion layers:
|
|
# - L1: workspace-server/internal/handlers/workspace_provision_forbidden_env.go
|
|
# (fail-closed abort at provision time)
|
|
# - L2: workspace/entrypoint.sh top-of-file env-grep + exit 1
|
|
#
|
|
# Open-source-template-friendly: the deny pattern is generic. A fork
|
|
# can copy this workflow and replace OPERATOR_KEY_PATTERN with its
|
|
# own operator-scope key names.
|
|
#
|
|
# Path-filter discipline:
|
|
# This workflow runs on every PR (no paths: filter — see
|
|
# feedback_path_filtered_workflow_cant_be_required). The scan itself
|
|
# targets workspace_secrets-writer paths via grep -r; it's fast
|
|
# (sub-second) so unconditional run is fine.
|
|
#
|
|
# ── 2026-06-01 CI-scheduler-fanout consolidation (fix/ci-scheduler-fanout) ──
|
|
# The RFC#523 sibling lint formerly in its own file
|
|
# `lint-no-tenant-gitea-token.yml` (the broader "no repo-host token into
|
|
# any tenant-writer surface" scan) is now a SECOND job in THIS workflow
|
|
# (`scan-tenant-token-write`). Both are sub-second Go-source greps that
|
|
# fired as two separate workflow runs on every PR — pure scheduler
|
|
# fan-out. Folding the sibling in here drops one workflow run + one
|
|
# checkout per PR while keeping BOTH scans firing unconditionally on
|
|
# every PR (the no-paths discipline above is preserved — neither job is
|
|
# paths-filtered). The moved job keeps its exact `name:` so its emitted
|
|
# status context is unchanged in substance; its `# bp-exempt:` directive
|
|
# moves with it (Tier 2g). The old `Lint no tenant GITEA or GITHUB token
|
|
# write / …` context is retired (a disappearing context needs no
|
|
# directive; only NEW emitters do).
|
|
|
|
concurrency:
|
|
# Auto-cancel a superseded run: a newer commit/event supersedes the old
|
|
# one instead of leaving it queued forever (root fix for waiting-run pileup).
|
|
# PR events group by PR number; push events group by ref.
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
push:
|
|
branches: [main, staging]
|
|
|
|
env:
|
|
GITHUB_SERVER_URL: https://git.moleculesai.app
|
|
|
|
jobs:
|
|
scan:
|
|
name: Scan workspace_secrets writers for forbidden env keys
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Scan for forbidden operator-scope env key NAMES in writer paths
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Forbidden EXACT-MATCH env var names. Kept in lockstep with
|
|
# workspace-server/internal/handlers/workspace_provision_forbidden_env.go
|
|
# forbiddenTenantEnvKeys. The Go-side test
|
|
# TestIsForbiddenTenantEnvKey_ExactMatches is the source of
|
|
# truth — if Go-side adds a key, also add it here (and
|
|
# vice-versa). Drift between the two is the failure mode this
|
|
# entire 3-layer guardrail is designed to catch.
|
|
FORBIDDEN_KEYS=(
|
|
"GITEA_TOKEN" "GITEA_PAT"
|
|
"GITHUB_TOKEN" "GITHUB_PAT" "GH_TOKEN"
|
|
"GITLAB_TOKEN" "GL_TOKEN"
|
|
"BITBUCKET_TOKEN"
|
|
"CP_ADMIN_API_TOKEN" "CP_ADMIN_TOKEN"
|
|
"INFISICAL_OPERATOR_TOKEN" "INFISICAL_BOOTSTRAP_TOKEN"
|
|
"RAILWAY_TOKEN" "RAILWAY_PERSONAL_API_TOKEN"
|
|
"HETZNER_TOKEN" "HETZNER_API_TOKEN"
|
|
)
|
|
|
|
# Forbidden PREFIX patterns — operator-scope families.
|
|
FORBIDDEN_PREFIXES=(
|
|
"MOLECULE_OPERATOR_"
|
|
)
|
|
|
|
# Writer paths: Go source under workspace-server/ that
|
|
# writes to the env-vars map or to workspace_secrets DB rows.
|
|
# Tests, the forbidden-env source itself, and the silent-
|
|
# strip denylist are exempt (they LIST the keys by design).
|
|
SCAN_ROOT="workspace-server/internal"
|
|
# Exempt paths fall in two classes:
|
|
# 1. The deny-set definitions + the silent-strip denylist:
|
|
# they LIST the forbidden names by design.
|
|
# 2. Pre-RFC#523 persona-merge / config-read paths that
|
|
# already handle these names correctly (the silent-
|
|
# strip downstream + the new L1 fail-closed cover the
|
|
# runtime risk; these reads are unchanged).
|
|
# New code MUST NOT be added to this list without reviewer
|
|
# signoff and a one-line justification in this diff.
|
|
EXEMPT_PATHS=(
|
|
# Class 1 — deny-set definitions
|
|
"workspace-server/internal/handlers/workspace_provision_forbidden_env.go"
|
|
"workspace-server/internal/handlers/workspace_provision_forbidden_env_test.go"
|
|
"workspace-server/internal/provisioner/provisioner.go"
|
|
"workspace-server/internal/provisioner/provisioner_test.go"
|
|
# Class 3 — secret redaction table: the quoted forbidden names here
|
|
# are category labels for regexps that *strip* secrets from memory
|
|
# content, not env-var injection sinks. core#2918.
|
|
"workspace-server/internal/handlers/memories.go"
|
|
# Class 2 — pre-existing persona-fallback / org-helper paths
|
|
# that set the GITEA_TOKEN fallback lane (stripped downstream
|
|
# by provisioner.buildContainerEnv per forensic #145). The
|
|
# new L1 fail-closed runs BEFORE these writers, so any
|
|
# operator-scope leak via global/workspace_secrets is
|
|
# already caught. See applyAgentGitHTTPCreds doc-comment.
|
|
"workspace-server/internal/handlers/agent_git_identity.go"
|
|
"workspace-server/internal/handlers/org_helpers.go"
|
|
"workspace-server/internal/handlers/org.go"
|
|
# Class 2 — CP→platform admin auth (NOT a tenant env write;
|
|
# this is the control-plane HTTP auth header source).
|
|
"workspace-server/internal/provisioner/cp_provisioner.go"
|
|
)
|
|
|
|
# Build a single grep -F pattern: every forbidden key wrapped
|
|
# in quotes (Go string-literal form, which is how env-map
|
|
# writes appear). e.g. envVars["GITEA_TOKEN"] = ... or
|
|
# `"GITEA_TOKEN":` in a literal-map declaration.
|
|
#
|
|
# We deliberately match the quoted form so a comment that
|
|
# happens to spell the name without quotes (e.g. "see
|
|
# GITEA_TOKEN below") doesn't trip the lint.
|
|
PATTERN=""
|
|
for k in "${FORBIDDEN_KEYS[@]}"; do
|
|
PATTERN="${PATTERN}\"${k}\"\n"
|
|
done
|
|
for p in "${FORBIDDEN_PREFIXES[@]}"; do
|
|
# Prefix match needs a regex; switch to grep -E below for
|
|
# this slice. Kept conceptually here so the deny set lives
|
|
# in one place; scan is run twice (literal + prefix).
|
|
true
|
|
done
|
|
|
|
# Build exempt-paths grep filter — `grep -v -f` style.
|
|
EXEMPT_FILTER=$(mktemp)
|
|
trap 'rm -f "$EXEMPT_FILTER"' EXIT
|
|
for p in "${EXEMPT_PATHS[@]}"; do
|
|
echo "$p" >> "$EXEMPT_FILTER"
|
|
done
|
|
|
|
# --- Exact-match scan ---
|
|
HITS=""
|
|
for k in "${FORBIDDEN_KEYS[@]}"; do
|
|
# Only .go files; skip _test.go for the writer-path scan
|
|
# since tests legitimately reference the names. The
|
|
# writer-path lint targets PRODUCTION code only.
|
|
found=$(grep -rn --include='*.go' --exclude='*_test.go' "\"${k}\"" "$SCAN_ROOT" 2>/dev/null \
|
|
| grep -v -F -f "$EXEMPT_FILTER" || true)
|
|
if [ -n "$found" ]; then
|
|
# Filter out the LABEL side of a redaction regexp-tuple —
|
|
# {regexp.MustCompile(`<re>`), "<KEY>"} — which is a
|
|
# security CONTROL (the label is just the canonical
|
|
# deny-listed env-var name used to tag what the redaction
|
|
# strips), not an actual env-injection. The pattern is
|
|
# anchored on the per-iteration $k; lines that don't
|
|
# contain the regexp.MustCompile(...), "<k>" shape
|
|
# (envVars["<k>"] = ..., os.Getenv("<k>"), etc.) pass
|
|
# through unchanged. core#2918.
|
|
found=$(echo "$found" | grep -vE 'regexp\.MustCompile\([^)]+\)\s*,\s*"'"${k}"'"' || true)
|
|
fi
|
|
if [ -n "$found" ]; then
|
|
HITS="${HITS}${found}\n"
|
|
fi
|
|
done
|
|
|
|
# --- Prefix scan ---
|
|
for prefix in "${FORBIDDEN_PREFIXES[@]}"; do
|
|
found=$(grep -rnE --include='*.go' --exclude='*_test.go' "\"${prefix}[A-Z0-9_]+\"" "$SCAN_ROOT" 2>/dev/null \
|
|
| grep -v -F -f "$EXEMPT_FILTER" || true)
|
|
if [ -n "$found" ]; then
|
|
# Same redaction-tuple-LABEL filter as the exact-match scan
|
|
# (the deny-prefix family can show up in redaction labels too).
|
|
found=$(echo "$found" | grep -vE 'regexp\.MustCompile\([^)]+\)\s*,\s*"'"${prefix}"'[A-Z0-9_]+"' || true)
|
|
fi
|
|
if [ -n "$found" ]; then
|
|
HITS="${HITS}${found}\n"
|
|
fi
|
|
done
|
|
|
|
if [ -n "$HITS" ]; then
|
|
echo "::error::RFC#523 Layer 3: forbidden operator-scope env var name(s) hardcoded in tenant-workspace writer paths:"
|
|
printf "$HITS"
|
|
echo ""
|
|
echo "These env-var NAMES are on the operator-scope deny list (see"
|
|
echo "workspace-server/internal/handlers/workspace_provision_forbidden_env.go)."
|
|
echo "If your code legitimately needs to inject one of these for a"
|
|
echo "non-tenant code path, add the file to EXEMPT_PATHS in this"
|
|
echo "workflow with a one-line justification — reviewer signoff required."
|
|
exit 1
|
|
fi
|
|
|
|
echo "OK No forbidden operator-scope env key names hardcoded in writer paths."
|
|
|
|
# bp-exempt: advisory RFC#523 lint; PR review gate is review-driven, not BP-driven.
|
|
# (Carried with the workflow-name rename in PR mc#1593 so the renamed
|
|
# context emission satisfies lint_required_context_exists_in_bp Tier 2g.)
|
|
scan-tenant-token-write:
|
|
name: Scan for repo-host token write into tenant workspace surface
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Find Go files referencing a tenant-writer surface AND a repo-host token
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Repo-host token NAMES — the threat-model subset. Operator-fleet
|
|
# tokens (CP_ADMIN_API_TOKEN, RAILWAY_TOKEN, INFISICAL_*) are
|
|
# caught by lint-forbidden-env-keys.yml's broader deny set; this
|
|
# lint focuses on the git-host class so a single co-occurrence
|
|
# match has a low false-positive rate.
|
|
FORBIDDEN_KEYS=(
|
|
"GITEA_TOKEN"
|
|
"GITEA_PAT"
|
|
"GITHUB_TOKEN"
|
|
"GITHUB_PAT"
|
|
"GH_TOKEN"
|
|
)
|
|
|
|
# Tenant-writer surface markers. A file matches the surface set
|
|
# if it references ANY of these strings. This is the "is this
|
|
# code path writing into a tenant workspace?" heuristic.
|
|
# Curated to catch the actual code shapes used in this repo
|
|
# (verified by grep against current main 2026-05-19):
|
|
# - "workspace_secrets" / "global_secrets" → DB table writes
|
|
# - "seedAllowList" → CP-side seed table
|
|
# - "/settings/secrets" → tenant HTTP API write
|
|
# - "envVars[" → in-memory env map write
|
|
# - "containerEnv" → docker-run env-set
|
|
# - "userData" → EC2 user-data script
|
|
# - "provisionPayload" / "provisionContext" → provision-request shape
|
|
SURFACE_PATTERN='workspace_secrets|global_secrets|seedAllowList|/settings/secrets|envVars\[|containerEnv|userData|provisionPayload|provisionContext'
|
|
|
|
# Files that legitimately reference these names AND a surface
|
|
# marker, but do so for guard / strip / test / doc-comment
|
|
# reasons. New entries require reviewer signoff and a one-line
|
|
# justification in the diff.
|
|
EXEMPT_FILES=(
|
|
# RFC#523 L1 deny-set source-of-truth + tests
|
|
"workspace-server/internal/handlers/workspace_provision_forbidden_env.go"
|
|
"workspace-server/internal/handlers/workspace_provision_forbidden_env_test.go"
|
|
# Forensic-#145 silent-strip denylist (defense-in-depth, by design lists the names)
|
|
"workspace-server/internal/provisioner/provisioner.go"
|
|
"workspace-server/internal/provisioner/provisioner_test.go"
|
|
# Pre-RFC#523 persona-fallback / org-helper paths. The L1
|
|
# fail-closed runs BEFORE these writers; downstream silent-strip
|
|
# also covers them. See applyAgentGitHTTPCreds doc-comment.
|
|
"workspace-server/internal/handlers/agent_git_identity.go"
|
|
"workspace-server/internal/handlers/org_helpers.go"
|
|
"workspace-server/internal/handlers/org.go"
|
|
# CP→platform admin auth (NOT a tenant env write).
|
|
"workspace-server/internal/provisioner/cp_provisioner.go"
|
|
)
|
|
|
|
# Build an extended-regex alternation of forbidden keys.
|
|
KEY_ALT="$(IFS='|'; echo "${FORBIDDEN_KEYS[*]}")"
|
|
|
|
# Find candidate files: Go non-test sources that contain a
|
|
# tenant-writer surface marker.
|
|
mapfile -t CANDIDATES < <(
|
|
grep -rlE --include='*.go' --exclude='*_test.go' \
|
|
"${SURFACE_PATTERN}" . 2>/dev/null \
|
|
| sed 's|^\./||' \
|
|
| sort -u
|
|
)
|
|
|
|
# FAIL CLOSED on an empty candidate set.
|
|
#
|
|
# This lint's only subject is "the set of Go files that touch a
|
|
# tenant-writer surface". If SURFACE_PATTERN matches nothing, the
|
|
# lint has lost sight of its subject: it cannot prove the absence
|
|
# of a repo-host-token write, it can only report that it looked
|
|
# nowhere. Passing here would make the PASS arm identical to what
|
|
# a BROKEN input (a moved/renamed/refactored package, a typo in
|
|
# SURFACE_PATTERN) produces — i.e. a vacuous gate.
|
|
#
|
|
# On main this scan finds 28 candidate files, so an empty set is
|
|
# never the normal state; it means the pattern has drifted from
|
|
# the code.
|
|
if [ "${#CANDIDATES[@]}" -eq 0 ]; then
|
|
echo "::error::Task #146 lint has lost sight of its subject: SURFACE_PATTERN matched ZERO Go files."
|
|
echo ""
|
|
echo "SURFACE_PATTERN: ${SURFACE_PATTERN}"
|
|
echo ""
|
|
echo "This lint proves 'no tenant-writer-surface file quotes a repo-host"
|
|
echo "token name'. With zero candidate files it has scanned NOTHING and"
|
|
echo "can prove nothing, so it fails closed rather than reporting a"
|
|
echo "vacuous PASS."
|
|
echo ""
|
|
echo "Remedy: the tenant-writer code has almost certainly moved, been"
|
|
echo "renamed, or been refactored. Update SURFACE_PATTERN in"
|
|
echo ".gitea/workflows/lint-forbidden-env-keys.yml so it once again"
|
|
echo "matches the code shapes that write into a tenant workspace"
|
|
echo "(env-var map writes, workspace_secrets/global_secrets DB rows,"
|
|
echo "container env, EC2 user-data, provision payloads)."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Tenant-writer-surface candidate files: ${#CANDIDATES[@]}"
|
|
|
|
HITS=""
|
|
for f in "${CANDIDATES[@]}"; do
|
|
# Skip exempt files.
|
|
skip=0
|
|
for ex in "${EXEMPT_FILES[@]}"; do
|
|
if [ "$f" = "$ex" ]; then skip=1; break; fi
|
|
done
|
|
[ "$skip" = "1" ] && continue
|
|
|
|
# File contains a surface marker; now grep for a forbidden
|
|
# key NAME. We require a QUOTED-literal match to avoid
|
|
# firing on a comment like "// also handle GITEA_TOKEN".
|
|
#
|
|
# The literal form catches:
|
|
# - os.Getenv("GITEA_TOKEN")
|
|
# - envVars["GITEA_TOKEN"] = ...
|
|
# - {envKey: "GITEA_TOKEN", tenantKey: "GITEA_TOKEN"}
|
|
# but not:
|
|
# - // see GITEA_TOKEN below (no quotes)
|
|
found=$(grep -nE "\"(${KEY_ALT})\"" "$f" 2>/dev/null || true)
|
|
# Filter out the LABEL side of a redaction regexp-tuple —
|
|
# {regexp.MustCompile(`<re>`), "<KEY>"} — which is a
|
|
# security CONTROL, not an actual tenant-writer surface
|
|
# injection. Same rationale as the scan job's
|
|
# LABEL_EXCLUDE filter (core#2918). The per-iteration
|
|
# KEY_ALT is the alternation; the LABEL_EXCLUDE alternation
|
|
# matches any of them in the redaction-tuple-LABEL slot.
|
|
if [ -n "$found" ]; then
|
|
found=$(echo "$found" | grep -vE 'regexp\.MustCompile\([^)]+\)\s*,\s*"('"${KEY_ALT}"')"' || true)
|
|
fi
|
|
if [ -n "$found" ]; then
|
|
HITS="${HITS}--- ${f} ---\n${found}\n"
|
|
fi
|
|
done
|
|
|
|
if [ -n "$HITS" ]; then
|
|
echo "::error::Task #146 lint: repo-host token name(s) quoted in a tenant-writer-surface file:"
|
|
# printf '%b' — NOT printf "$HITS". HITS always begins with
|
|
# "--- <file> ---", which bare printf parses as an option
|
|
# terminator ("printf: --: invalid option"), so under `set -e`
|
|
# the step died at exit 2 and the operator never saw WHICH file
|
|
# or line tripped the lint. The RED arm fired but named nothing.
|
|
printf '%b' "$HITS"
|
|
echo ""
|
|
echo "These files reference a tenant-writer surface (workspace_secrets,"
|
|
echo "seedAllowList, /settings/secrets, containerEnv, userData, etc.)"
|
|
echo "AND quote a repo-host token name (GITEA_TOKEN/GITHUB_TOKEN/…)."
|
|
echo "Per RFC#523 threat model, tenant workspaces MUST NOT receive"
|
|
echo "operator-scope repo-host tokens. If your code legitimately needs"
|
|
echo "to reference one of these names in a tenant-writer file (e.g."
|
|
echo "a deny-set definition or silent-strip list), add the file to"
|
|
echo "EXEMPT_FILES with a one-line justification — reviewer signoff"
|
|
echo "required."
|
|
exit 1
|
|
fi
|
|
|
|
echo "OK No tenant-writer-surface file co-mentions a repo-host token literal."
|