Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ permissions:
contents: read

env:
GCP_PROJECT_ID: binancequant
GCP_WORKLOAD_IDENTITY_PROVIDER: projects/677468735457/locations/global/workloadIdentityPools/github-actions/providers/github-main
GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: binance-platform-runtime@binancequant.iam.gserviceaccount.com
GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ vars.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}

concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
Expand All @@ -42,6 +42,29 @@ jobs:
contents: write
id-token: write
steps:
- name: 0. Validate deployment identity configuration
shell: bash
run: |
set -euo pipefail
for name in GCP_PROJECT_ID GCP_WORKLOAD_IDENTITY_PROVIDER GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT; do
if [ -z "${!name:-}" ]; then
echo "::error::Required repository variable ${name} is not configured."
exit 1
fi
done
readonly EXPECTED_OIDC_IDENTITY_SHA256="68e87a5dc1bbe2e41af33d526514034246c6487fb7b716596cc75fe1c739a6b9"
actual_oidc_identity_sha256="$(
printf '%s\0%s\0%s' \
"$GCP_PROJECT_ID" \
"$GCP_WORKLOAD_IDENTITY_PROVIDER" \
"$GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT" |
sha256sum |
awk '{print $1}'
)"
if [ "$actual_oidc_identity_sha256" != "$EXPECTED_OIDC_IDENTITY_SHA256" ]; then
echo "::error::Repository OIDC identity variables do not match the reviewed identity contract."
exit 1
fi
- name: 1. Checkout latest code
uses: actions/checkout@v6
with:
Expand Down
29 changes: 26 additions & 3 deletions .github/workflows/watchdog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,39 @@ permissions:
contents: read

env:
GCP_PROJECT_ID: binancequant
GCP_WORKLOAD_IDENTITY_PROVIDER: projects/677468735457/locations/global/workloadIdentityPools/github-actions/providers/github-main
GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: binance-platform-runtime@binancequant.iam.gserviceaccount.com
GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ vars.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}
WATCHDOG_MAX_AGE_SECONDS: ${{ vars.WATCHDOG_MAX_AGE_SECONDS || '4500' }}

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Validate deployment identity configuration
shell: bash
run: |
set -euo pipefail
for name in GCP_PROJECT_ID GCP_WORKLOAD_IDENTITY_PROVIDER GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT; do
if [ -z "${!name:-}" ]; then
echo "::error::Required repository variable ${name} is not configured."
exit 1
fi
done
readonly EXPECTED_OIDC_IDENTITY_SHA256="68e87a5dc1bbe2e41af33d526514034246c6487fb7b716596cc75fe1c739a6b9"
actual_oidc_identity_sha256="$(
printf '%s\0%s\0%s' \
"$GCP_PROJECT_ID" \
"$GCP_WORKLOAD_IDENTITY_PROVIDER" \
"$GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT" |
sha256sum |
awk '{print $1}'
)"
if [ "$actual_oidc_identity_sha256" != "$EXPECTED_OIDC_IDENTITY_SHA256" ]; then
echo "::error::Repository OIDC identity variables do not match the reviewed identity contract."
exit 1
fi
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
Expand Down
12 changes: 12 additions & 0 deletions tests/test_runtime_workflow_shared_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ grep -Fq "STRATEGY_PROFILE: \${{ vars.STRATEGY_PROFILE || 'crypto_live_pool_rota
grep -Fq 'id-token: write' "$workflow_file"
grep -Fq 'workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}' "$workflow_file"
grep -Fq 'service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}' "$workflow_file"
grep -Fq 'GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}' "$workflow_file"
grep -Fq 'GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}' "$workflow_file"
grep -Fq 'GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ${{ vars.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}' "$workflow_file"
grep -Fq 'for name in GCP_PROJECT_ID GCP_WORKLOAD_IDENTITY_PROVIDER GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT; do' "$workflow_file"
grep -Fq 'echo "::error::Required repository variable ${name} is not configured."' "$workflow_file"
grep -Eq 'readonly EXPECTED_OIDC_IDENTITY_SHA256="[0-9a-f]{64}"' "$workflow_file"
grep -Fq "printf '%s\\0%s\\0%s'" "$workflow_file"
preflight_line="$(grep -nF -- '- name: 0. Validate deployment identity configuration' "$workflow_file" | cut -d: -f1)"
checkout_line="$(grep -nF -- '- name: 1. Checkout latest code' "$workflow_file" | cut -d: -f1)"
auth_line="$(grep -nF -- '- name: 2. Authenticate to Google Cloud' "$workflow_file" | cut -d: -f1)"
test "$preflight_line" -lt "$checkout_line"
test "$preflight_line" -lt "$auth_line"
grep -Fq '7. Notify Telegram on runtime workflow failure' "$workflow_file"
grep -Fq "if: \${{ failure() && github.event.inputs.validate_only != 'true' }}" "$workflow_file"
grep -Fq 'reports/execution_report.json' "$workflow_file"
Expand Down
143 changes: 138 additions & 5 deletions tests/test_watchdog_workflow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import annotations

import hashlib
import os
import re
import subprocess
import tomllib
import unittest
from pathlib import Path
Expand All @@ -11,6 +15,46 @@
PYPROJECT = ROOT / "pyproject.toml"
LOCK = ROOT / "uv.lock"
QSL = ROOT / "qsl.toml"
IDENTITY_NAMES = (
"GCP_PROJECT_ID",
"GCP_WORKLOAD_IDENTITY_PROVIDER",
"GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT",
)
EXPECTED_DIGEST_PATTERN = re.compile(r'EXPECTED_OIDC_IDENTITY_SHA256="([0-9a-f]{64})"')


def _identity_digest(values: dict[str, str]) -> str:
canonical = b"\0".join(values[name].encode("utf-8") for name in IDENTITY_NAMES)
return hashlib.sha256(canonical).hexdigest()


def _preflight_script(workflow_text: str) -> str:
step = workflow_text.index("Validate deployment identity configuration")
run_marker = " run: |\n"
start = workflow_text.index(run_marker, step) + len(run_marker)
lines: list[str] = []
for line in workflow_text[start:].splitlines():
if line.startswith(" - "):
break
if line.startswith(" "):
lines.append(line[10:])
elif not line:
lines.append("")
else:
break
return "\n".join(lines)


def _run_preflight(script: str, values: dict[str, str]) -> subprocess.CompletedProcess[str]:
env = {"PATH": os.environ.get("PATH", "")}
env.update(values)
return subprocess.run(
["bash", "-c", script],
env=env,
capture_output=True,
text=True,
check=False,
)


def _project_dependencies() -> list[str]:
Expand All @@ -30,25 +74,114 @@ class WatchdogWorkflowTests(unittest.TestCase):
def setUpClass(cls) -> None:
cls.workflow_text = WORKFLOW.read_text(encoding="utf-8")

def test_watchdog_uses_binance_runtime_identity(self) -> None:
def test_watchdog_uses_repository_variables_before_remote_actions(self) -> None:
text = self.workflow_text

self.assertIn("id-token: write", text)
for name in (
"GCP_PROJECT_ID",
"GCP_WORKLOAD_IDENTITY_PROVIDER",
"GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT",
):
self.assertIn(f"{name}: ${{{{ vars.{name} }}}}", text)
self.assertIn(
"GCP_WORKLOAD_IDENTITY_PROVIDER: "
"projects/677468735457/locations/global/workloadIdentityPools/github-actions/providers/github-main",
"for name in GCP_PROJECT_ID GCP_WORKLOAD_IDENTITY_PROVIDER "
"GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT; do",
text,
)
self.assertIn(
"GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: "
"binance-platform-runtime@binancequant.iam.gserviceaccount.com",
'echo "::error::Required repository variable ${name} is not configured."',
text,
)
preflight = text.index("- name: Validate deployment identity configuration")
checkout = text.index("- uses: actions/checkout@v6")
auth = text.index("- name: Authenticate to Google Cloud")
self.assertLess(preflight, checkout)
self.assertLess(preflight, auth)
self.assertIn("uses: google-github-actions/auth@v3", text)
self.assertIn("workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}", text)
self.assertIn("service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}", text)
self.assertIn("WATCHDOG_MAX_AGE_SECONDS: ${{ vars.WATCHDOG_MAX_AGE_SECONDS || '4500' }}", text)

def test_oidc_identity_digest_is_fixed_and_shared(self) -> None:
scripts = (
_preflight_script(RUNTIME_WORKFLOW.read_text(encoding="utf-8")),
_preflight_script(self.workflow_text),
)
digests: list[str] = []

for script in scripts:
matches = EXPECTED_DIGEST_PATTERN.findall(script)
self.assertEqual(len(matches), 1)
digests.extend(matches)
self.assertIn("readonly EXPECTED_OIDC_IDENTITY_SHA256=", script)
self.assertIn("set -euo pipefail", script)
self.assertNotIn("set -x", script)
self.assertIn(r"printf '%s\0%s\0%s'", script)
self.assertLess(script.index('"$GCP_PROJECT_ID"'), script.index('"$GCP_WORKLOAD_IDENTITY_PROVIDER"'))
self.assertLess(
script.index('"$GCP_WORKLOAD_IDENTITY_PROVIDER"'),
script.index('"$GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT"'),
)

self.assertEqual(len(set(digests)), 1)

def test_oidc_identity_digest_fails_closed_without_value_output(self) -> None:
baseline = {
"GCP_PROJECT_ID": "synthetic-project",
"GCP_WORKLOAD_IDENTITY_PROVIDER": "synthetic-provider",
"GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT": "synthetic-service-account",
}
expected = _identity_digest(baseline)

for workflow in (RUNTIME_WORKFLOW, WORKFLOW):
script = _preflight_script(workflow.read_text(encoding="utf-8"))
script, replacements = EXPECTED_DIGEST_PATTERN.subn(
f'EXPECTED_OIDC_IDENTITY_SHA256="{expected}"',
script,
)
self.assertEqual(replacements, 1)
baseline_result = _run_preflight(script, baseline)
self.assertEqual(baseline_result.returncode, 0)
baseline_output = baseline_result.stdout + baseline_result.stderr
self.assertNotIn(expected, baseline_output)
for value in baseline.values():
self.assertNotIn(value, baseline_output)

candidates: list[dict[str, str]] = []
for name in IDENTITY_NAMES:
empty = dict(baseline)
empty[name] = ""
candidates.append(empty)

modified = dict(baseline)
modified[name] = f"{modified[name]}-modified"
candidates.append(modified)

for left, right in ((0, 1), (0, 2), (1, 2)):
swapped = dict(baseline)
swapped[IDENTITY_NAMES[left]], swapped[IDENTITY_NAMES[right]] = (
swapped[IDENTITY_NAMES[right]],
swapped[IDENTITY_NAMES[left]],
)
candidates.append(swapped)
candidates.append(
{
IDENTITY_NAMES[0]: baseline[IDENTITY_NAMES[0]] + baseline[IDENTITY_NAMES[1]],
IDENTITY_NAMES[1]: baseline[IDENTITY_NAMES[2]],
IDENTITY_NAMES[2]: baseline[IDENTITY_NAMES[0]],
}
)

for candidate in candidates:
result = _run_preflight(script, candidate)
self.assertNotEqual(result.returncode, 0)
output = result.stdout + result.stderr
self.assertNotIn(expected, output)
for value in candidate.values():
if value:
self.assertNotIn(value, output)

def test_watchdog_installs_locked_internal_dependency(self) -> None:
text = self.workflow_text

Expand Down
Loading