mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(ci): make the code-scanning gate able to see alerts, and fail closed
The codeql-gate has never checked anything. It declared no permissions block,
so it inherited the workflow's contents:read and the code-scanning alert API
answered 403. gh writes the API error BODY to stdout, so
ALERTS=$(gh api '.../code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
left ALERTS holding
{"message":"Resource not accessible by integration",...,"status":"403"}0
every [ ... -gt ... ] then failed as a non-integer comparison, the if took its
false branch, and the step printed "CodeQL gate passed (0 alerts)" and exited 0.
Observed in the log of a GREEN run (34700514102, job 103571322660) while the
repository had an open high-severity alert. The `|| echo "0"` reads as defensive
and is the exact opposite: it converts "I cannot see the alerts" into "there are
no alerts", which is the one direction a security gate must never fail in.
Grant the job security-events:read (and actions:read, which the CodeQL-run poll
needs for the same reason), and replace the fallback with a helper that treats a
non-zero gh exit or a non-numeric body as a hard failure. The helper returns
rather than exits because it runs inside a command substitution, where an exit
would end only the subshell and let the caller continue with an empty count --
the same silent pass in a new costume; both call sites check the status.
tests/test_security_gate_fail_closed.sh pins both halves and fails with six
assertions against the previous workflow.
Also bumps graph-ui's vitest to ^4.1.11 (GHSA-82fw-gwwq-j7x9, path traversal via
@vitest/mocker, CVSS 5.9). Development scope, and graph-ui ships in no release
artifact, so nothing released was ever exposed -- but it was the sole open alert
and the gate above is worthless while it stands unresolved. Raising the floor
past the vulnerable range also stops a fresh install resolving back into it.
graph-ui: npm ci clean, 47 tests in 12 files pass on 4.1.11.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
9da4d66df2
commit
1ad52f5fb1
7 files changed
+241
-51
No files matched your search
@@ -41,6 +41,14 @@ jobs:
|
||||
codeql-gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 240
|
||||
# Without security-events:read this job inherits the workflow's
|
||||
# contents:read, the code-scanning alert API answers 403, and the gate
|
||||
# below reported "0 alerts" on every run it has ever made. actions:read is
|
||||
# what the CodeQL-run poll above needs for the same reason.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
actions: read
|
||||
steps:
|
||||
- name: Wait for CodeQL on current commit (max 150 min)
|
||||
env:
|
||||
@@ -77,11 +85,43 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# FAIL CLOSED. The previous form was `gh api ... 2>/dev/null || echo "0"`,
|
||||
# which looks defensive and is the opposite. gh writes the API error
|
||||
# BODY to stdout, so a 403 made ALERTS the string
|
||||
# {"message":"Resource not accessible by integration",...,"status":"403"}0
|
||||
# every `[` comparison then failed as non-integer, the `if` took its
|
||||
# false branch, and the step printed "CodeQL gate passed (0 alerts)"
|
||||
# and exited 0. That is exactly how a security gate should NEVER fail:
|
||||
# silently, in the direction of "everything is fine". An unreadable
|
||||
# alert list is now an error, not a zero.
|
||||
# NOTE: this runs in a command substitution, so it must `return`, not
|
||||
# `exit` -- an `exit` here would only end the subshell and let the
|
||||
# caller continue with an empty count, recreating the very bug this
|
||||
# replaces. The callers below check the status explicitly.
|
||||
count_open_alerts() {
|
||||
local body status
|
||||
body=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
|
||||
--jq 'length' 2>/dev/null)
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "BLOCKED: cannot read code scanning alerts (gh exit $status)." >&2
|
||||
echo " Refusing to treat an unreadable alert list as zero." >&2
|
||||
echo " Most likely cause: the job lacks security-events:read." >&2
|
||||
return 1
|
||||
fi
|
||||
case "$body" in
|
||||
''|*[!0-9]*)
|
||||
echo "BLOCKED: alert count was not a number: ${body:-<empty>}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$body"
|
||||
}
|
||||
echo "Waiting 60s for alert API to settle..."
|
||||
sleep 60
|
||||
ALERTS=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
|
||||
ALERTS=$(count_open_alerts) || exit 1
|
||||
sleep 15
|
||||
ALERTS2=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
|
||||
ALERTS2=$(count_open_alerts) || exit 1
|
||||
[ "$ALERTS" -lt "$ALERTS2" ] && ALERTS=$ALERTS2
|
||||
if [ "$ALERTS" -gt 0 ]; then
|
||||
echo "BLOCKED: $ALERTS open alert(s)"
|
||||
|
||||
@@ -22,6 +22,13 @@ concurrency:
|
||||
jobs:
|
||||
security:
|
||||
uses: ./.github/workflows/_security.yml
|
||||
# The called job requests security-events:read to count code-scanning
|
||||
# alerts; a reusable workflow cannot request more than its caller grants,
|
||||
# so withholding this here fails the whole run at startup.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
actions: read
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
|
||||
@@ -44,6 +44,13 @@ jobs:
|
||||
# ── Security (starts immediately, blocks only verify) ───────────
|
||||
security:
|
||||
uses: ./.github/workflows/_security.yml
|
||||
# The called job requests security-events:read to count code-scanning
|
||||
# alerts; a reusable workflow cannot request more than its caller grants,
|
||||
# so withholding this here fails the whole run at startup.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
actions: read
|
||||
secrets: inherit
|
||||
|
||||
# ── 0. Preflight: refuse malformed dispatch inputs ──────────────
|
||||
|
||||
Generated
+48
-48
@@ -33,7 +33,7 @@
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.4.3",
|
||||
"vitest": "^4.1.0"
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
@@ -3603,16 +3603,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
|
||||
"integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
|
||||
"integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.1.8",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/spy": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
@@ -3621,13 +3621,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
|
||||
"integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
|
||||
"integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.1.8",
|
||||
"@vitest/spy": "4.1.11",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
@@ -3648,9 +3648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
|
||||
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
|
||||
"integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3661,13 +3661,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
|
||||
"integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
|
||||
"integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3675,14 +3675,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
|
||||
"integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
|
||||
"integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.8",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -3691,9 +3691,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
|
||||
"integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
|
||||
"integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -3701,13 +3701,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
|
||||
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
|
||||
"integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.8",
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
@@ -5831,9 +5831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
|
||||
"integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6141,19 +6141,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
|
||||
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
|
||||
"integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.8",
|
||||
"@vitest/mocker": "4.1.8",
|
||||
"@vitest/pretty-format": "4.1.8",
|
||||
"@vitest/runner": "4.1.8",
|
||||
"@vitest/snapshot": "4.1.8",
|
||||
"@vitest/spy": "4.1.8",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/expect": "4.1.11",
|
||||
"@vitest/mocker": "4.1.11",
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"@vitest/runner": "4.1.11",
|
||||
"@vitest/snapshot": "4.1.11",
|
||||
"@vitest/spy": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -6181,12 +6181,12 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.1.8",
|
||||
"@vitest/browser-preview": "4.1.8",
|
||||
"@vitest/browser-webdriverio": "4.1.8",
|
||||
"@vitest/coverage-istanbul": "4.1.8",
|
||||
"@vitest/coverage-v8": "4.1.8",
|
||||
"@vitest/ui": "4.1.8",
|
||||
"@vitest/browser-playwright": "4.1.11",
|
||||
"@vitest/browser-preview": "4.1.11",
|
||||
"@vitest/browser-webdriverio": "4.1.11",
|
||||
"@vitest/coverage-istanbul": "4.1.11",
|
||||
"@vitest/coverage-v8": "4.1.11",
|
||||
"@vitest/ui": "4.1.11",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.4.3",
|
||||
"vitest": "^4.1.0"
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"overrides": {
|
||||
"form-data": ">=4.0.6",
|
||||
|
||||
@@ -218,6 +218,9 @@ bash "$ROOT/tests/test_windows_bundle_contract.sh"
|
||||
echo "=== Step 0e2: activation refusal diagnostic contract ==="
|
||||
bash "$ROOT/tests/test_activation_diagnostic_contract.sh"
|
||||
|
||||
echo "=== Step 0e3: security gate fail-closed contract ==="
|
||||
bash "$ROOT/tests/test_security_gate_fail_closed.sh"
|
||||
|
||||
echo "=== Step 0f: tree-sitter runtime Makefile dependencies ==="
|
||||
bash "$ROOT/tests/test_makefile_ts_runtime_dependencies.sh"
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# The code-scanning gate must FAIL CLOSED.
|
||||
#
|
||||
# History. `codeql-gate` in .github/workflows/_security.yml counted open alerts
|
||||
# with:
|
||||
#
|
||||
# ALERTS=$(gh api '.../code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
|
||||
#
|
||||
# That reads as defensive and is the exact opposite. The job declared no
|
||||
# `permissions:` block, so it inherited the workflow's `contents: read` and the
|
||||
# alert API answered 403. `gh` writes the API error BODY to stdout, so `ALERTS`
|
||||
# became the string
|
||||
#
|
||||
# {"message":"Resource not accessible by integration",...,"status":"403"}0
|
||||
#
|
||||
# every `[ ... -gt ... ]` then failed as a non-integer comparison, the `if` took
|
||||
# its false branch, and the step printed "CodeQL gate passed (0 alerts)" and
|
||||
# exited 0. Observed live in a GREEN run (34700514102 / job 103571322660), while
|
||||
# the repository had an open high-severity alert.
|
||||
#
|
||||
# A security gate that cannot read its input must stop the build, not wave it
|
||||
# through. This test pins both halves of that: the permission that makes the
|
||||
# read possible, and the fail-closed handling for when it still is not.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
python3 - "$ROOT" <<'PY'
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
failures: list[str] = []
|
||||
|
||||
|
||||
def require(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
workflow = (root / ".github" / "workflows" / "_security.yml").read_text(encoding="utf-8")
|
||||
|
||||
# Isolate the codeql-gate job so a permission granted to some OTHER job cannot
|
||||
# satisfy this test.
|
||||
gate = re.search(r"\n codeql-gate:\n(?P<body>(?: .*\n|\n)*)", workflow)
|
||||
require(gate is not None, "_security.yml no longer defines a codeql-gate job")
|
||||
body = gate.group("body") if gate else ""
|
||||
|
||||
# 1. The read has to be possible at all.
|
||||
require(
|
||||
re.search(r"^ permissions:\s*$", body, re.M) is not None,
|
||||
"codeql-gate must declare its own permissions block -- inheriting the "
|
||||
"workflow default leaves it without security-events access and the alert "
|
||||
"API answers 403",
|
||||
)
|
||||
require(
|
||||
re.search(r"^ security-events:\s*read\s*$", body, re.M) is not None,
|
||||
"codeql-gate must request security-events: read, or the code-scanning "
|
||||
"alert query 403s and the gate cannot see any alert",
|
||||
)
|
||||
|
||||
# 2. The failure direction. `|| echo "0"` on the alert query is the specific
|
||||
# construct that turned an API error into a clean pass.
|
||||
require(
|
||||
re.search(r"code-scanning/alerts[^\n]*\|\|\s*echo", body) is None,
|
||||
"the alert count must not fall back to a literal on error -- an unreadable "
|
||||
"alert list is not zero alerts",
|
||||
)
|
||||
require(
|
||||
"BLOCKED: cannot read code scanning alerts" in body,
|
||||
"codeql-gate must fail with a named error when the alert API cannot be read",
|
||||
)
|
||||
require(
|
||||
re.search(r"alert count was not a number", body) is not None,
|
||||
"codeql-gate must reject a non-numeric alert count instead of comparing it",
|
||||
)
|
||||
|
||||
# 3. The count is produced in a command substitution, so a bare `exit` inside
|
||||
# the helper would end only the subshell and let the caller proceed with an
|
||||
# empty value -- the same class of silent pass. The callers must check.
|
||||
assignments = re.findall(r"^\s*ALERTS2?=\$\(count_open_alerts\)(.*)$", body, re.M)
|
||||
require(
|
||||
len(assignments) >= 2,
|
||||
"expected both alert samples to go through the checked helper",
|
||||
)
|
||||
require(
|
||||
all("|| exit 1" in tail for tail in assignments),
|
||||
"every count_open_alerts call must be followed by || exit 1 -- a failure "
|
||||
"inside a command substitution does not stop the caller on its own",
|
||||
)
|
||||
|
||||
# 4. Every caller must GRANT what the job requests. A reusable workflow cannot
|
||||
# request more permission than its caller holds: asking for
|
||||
# security-events:read from a caller that grants only contents:read does not
|
||||
# downgrade, it fails the entire run with a startup_failure before a single
|
||||
# job executes. Checking only the called workflow is how this test passed
|
||||
# while the PR pipeline never started.
|
||||
for caller_name in ("pr.yml", "release.yml"):
|
||||
caller = (root / ".github" / "workflows" / caller_name).read_text(encoding="utf-8")
|
||||
# The next sibling may be a comment line, not a key, so the lookahead has to
|
||||
# accept any 2-space-indented non-space -- matching only ` \w` silently
|
||||
# failed to find release.yml's job at all.
|
||||
call = re.search(
|
||||
r"\n security:\n(?P<body>(?: .*\n|\n)*?)(?=\n? \S|\Z)", caller
|
||||
)
|
||||
require(call is not None, f"{caller_name} no longer has a security: job")
|
||||
call_body = call.group("body") if call else ""
|
||||
require(
|
||||
"uses: ./.github/workflows/_security.yml" in call_body,
|
||||
f"{caller_name}'s security job must call _security.yml",
|
||||
)
|
||||
require(
|
||||
re.search(r"^ security-events:\s*read\s*$", call_body, re.M) is not None,
|
||||
f"{caller_name} must grant security-events: read to the security job -- "
|
||||
"a called workflow cannot request more than its caller grants, and the "
|
||||
"run fails at startup rather than degrading",
|
||||
)
|
||||
require(
|
||||
re.search(r"^ actions:\s*read\s*$", call_body, re.M) is not None,
|
||||
f"{caller_name} must grant actions: read to the security job",
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("Security gate fail-closed contract FAILED:")
|
||||
for failure in failures:
|
||||
print(f" - {failure}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Security gate fail-closed contract passed")
|
||||
PY
|
||||
Reference in new issue
Block a user