diff --git a/.github/workflows/audit-central-ruleset.yml b/.github/workflows/audit-central-ruleset.yml index 655a319835..65a054e1d4 100644 --- a/.github/workflows/audit-central-ruleset.yml +++ b/.github/workflows/audit-central-ruleset.yml @@ -26,8 +26,6 @@ jobs: audit: name: Audit organization ruleset 18156473 runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} - env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 @@ -41,12 +39,18 @@ jobs: - name: Read live inherited organization ruleset and repository scope env: + GH_TOKEN: ${{ secrets.CWL_RULESET_AUDIT_TOKEN || '' }} ORG_LOGIN: ContextualWisdomLab + RULESET_WRITE_CREDENTIAL_AVAILABLE: ${{ secrets.CWL_RULESET_AUDIT_TOKEN != '' }} RULESET_ID: "18156473" STACKED_RULESET_ID: "21732164" RULESET_SENTINEL_REPOSITORY: naruon run: | set -euo pipefail + if [ "$RULESET_WRITE_CREDENTIAL_AVAILABLE" = "false" ]; then + echo "::error::Ruleset bypass audit requires CWL_RULESET_AUDIT_TOKEN with ruleset write access; github.token and review/merge credentials are not accepted as substitutes." + exit 1 + fi ruleset_json="$RUNNER_TEMP/central-required-workflow-ruleset.json" ruleset_with_scope_json="$RUNNER_TEMP/central-required-workflow-ruleset-with-scope.json" ruleset_error="$RUNNER_TEMP/central-required-workflow-ruleset.error" @@ -118,6 +122,7 @@ jobs: # unrelated failure. if: always() env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || '' }} ORG_LOGIN: ContextualWisdomLab ORG_WIDE_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index d90fa0c899..4f625b3914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### Central ruleset audit detects standing bypass actors + +- The ruleset audit now fails closed when the visible payload for ruleset + `18156473` exposes a persistent bypass actor or omits/malforms + `bypass_actors`. This closes the detector gap exposed by LineageWeave PR + #1153; it does not claim that the live actor has been removed. Protected + settings cleanup and a subsequent scheduled audit remain required evidence. + ### Intel macOS native archives are bound to x86_64 bytes - The release prescreener now requires every native member in an Intel macOS diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 674a5d0b5a..20ee607f72 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -406,6 +406,17 @@ non-fork inventory snapshot and rollout ledger, not the ruleset target list. ## Risks and follow-up +- Ruleset `18156473` must not carry a persistent bypass actor. A genuine + circular-admission exception requires separate live authorization, exact-head + evidence, and immediate removal after the guarded operation; an `ALWAYS` + actor turns that exception into an unaudited ordinary merge path. The central + audit therefore treats any standing bypass actor, or a missing or malformed + `bypass_actors` collection that cannot prove its absence, as configuration + drift. The ruleset read uses only the dedicated `CWL_RULESET_AUDIT_TOKEN`, + provisioned with ruleset-write access so GitHub returns `bypass_actors`; an + absent credential fails before the read. The repository `github.token` and + review/merge credentials are not ruleset-audit fallbacks. The separate CodeQL + coverage step retains its org-repository read credentials. - Existing open PRs may need a new push or base update before the latest required workflow SHA appears on their current head. - The central OpenCode workflow now routes model-backed review through the canonical contextual-orchestrator contract; model/provider selection and fallback belong to that owner boundary, not workflow-local heuristics or paid fallback. - The central OpenCode config includes a read-only `code-reviewer` subagent for focused review passes. The subagent may read, grep, glob, and run safe local verification commands, but it must not edit files, stage changes, commit, push, install dependencies, mutate branches, or touch production state. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6e5f1c549a..2953004d2e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,6 +7,12 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-10-07 persistent-bypass incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-RULESET-PERSISTENT-BYPASS-01 | **GREEN locally; protected integration, credential provisioning, and live ruleset remediation pending** | LineageWeave #1153 head `f18c736f4c51dea62d2f543134e077a3f9d9eb22` and squash result `03c575013e00fbce3d31bdc73eabd2bd13fa65e1` have the same tree, but the head had failed Tests and no APPROVED review. The active organization ruleset exposed an organization-admin `ALWAYS` bypass actor. RED proved `audit_ruleset()` accepted both that standing actor and a payload that omitted `bypass_actors`; GREEN reports the standing actor plus missing or malformed actor data as drift. The scheduled read now requires the dedicated `CWL_RULESET_AUDIT_TOKEN` capability and never falls back to `github.token`; an absent token or hidden `bypass_actors` fails closed. | `.github` owns `scripts/ci/audit_central_required_workflows.py` and the audit credential boundary. Merge this contract through exact-head Checks and independent review, provision `CWL_RULESET_AUDIT_TOKEN` with ruleset-write access, then remove the persistent actor from live ruleset `18156473` through the protected organization-admin settings path and capture the next scheduled audit success. LineageWeave #1170 preserves the complete Voice delta and retains only the incident record; neither lane retroactively certifies #1153. | + ### 2026-09-19 exact-head incident delta | Gap ID | 상태 | exact-head evidence | causal owner / next gate | diff --git a/scripts/ci/audit_central_required_workflows.py b/scripts/ci/audit_central_required_workflows.py index cc27d5db7c..8bf33020ef 100755 --- a/scripts/ci/audit_central_required_workflows.py +++ b/scripts/ci/audit_central_required_workflows.py @@ -60,6 +60,26 @@ def audit_ruleset(payload: dict[str, Any]) -> list[str]: if payload.get("enforcement") != "active": errors.append("central ruleset enforcement is not active") + bypass_actors = payload.get("bypass_actors") + if "bypass_actors" not in payload: + errors.append("central ruleset bypass actor data is missing") + elif not isinstance(bypass_actors, list): + errors.append("central ruleset bypass actor data is malformed") + elif bypass_actors: + actor_labels = sorted( + ( + f"{actor.get('actor_type', '')}:" + f"{actor.get('actor_id', '')}:" + f"{actor.get('bypass_mode', '')}" + ) + if isinstance(actor, dict) + else "" + for actor in bypass_actors + ) + errors.append( + f"central ruleset has persistent bypass actors: {actor_labels}" + ) + conditions = payload.get("conditions") conditions = conditions if isinstance(conditions, dict) else {} repository_names = conditions.get("repository_name") diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 0ffc8fa749..375958b060 100644 --- a/tests/test_audit_org_codeql_coverage.py +++ b/tests/test_audit_org_codeql_coverage.py @@ -35,9 +35,13 @@ def default_setup_scanning_nothing(name: str) -> dict: } -def covered_by_recent_analysis(name: str, *, days_ago: int = 1) -> dict: +def covered_by_recent_analysis( + name: str, *, days_ago: int = 1, reference_time: datetime = NOW +) -> dict: """Return a repository payload covered by a recent, successful CodeQL analysis.""" - created_at = (NOW - timedelta(days=days_ago)).isoformat().replace("+00:00", "Z") + created_at = (reference_time - timedelta(days=days_ago)).isoformat().replace( + "+00:00", "Z" + ) return { "name": name, "archived": False, @@ -263,7 +267,13 @@ def test_null_latest_analysis_is_not_coverage() -> None: def test_audit_codeql_coverage_defaults_now_to_current_time() -> None: - repositories = [covered_by_recent_analysis("DefaultNowRepo", days_ago=0)] + repositories = [ + covered_by_recent_analysis( + "DefaultNowRepo", + days_ago=0, + reference_time=datetime.now(timezone.utc), + ) + ] assert audit.audit_codeql_coverage(repositories) == [] @@ -302,7 +312,14 @@ def test_main_fail_path_reports_gaps_from_stdin(monkeypatch, capsys) -> None: def test_main_pass_path_reports_from_file_arg(tmp_path, capsys) -> None: payload_path = tmp_path / "repositories.json" payload_path.write_text( - json.dumps([covered_by_default_setup("ELUNVERA"), covered_by_recent_analysis("Orgmetra")]), + json.dumps( + [ + covered_by_default_setup("ELUNVERA"), + covered_by_recent_analysis( + "Orgmetra", reference_time=datetime.now(timezone.utc) + ), + ] + ), encoding="utf-8", ) diff --git a/tests/test_central_required_workflow_exact_inventory.py b/tests/test_central_required_workflow_exact_inventory.py index 6b3e9b650e..057ae7b97a 100644 --- a/tests/test_central_required_workflow_exact_inventory.py +++ b/tests/test_central_required_workflow_exact_inventory.py @@ -23,6 +23,7 @@ def _ruleset_payload() -> dict: "name": audit.RULESET_NAME, "target": "branch", "enforcement": "active", + "bypass_actors": [], "conditions": { "repository_name": { "include": ["~ALL"], diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index cec0d2aead..a8d34010a1 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -23,6 +23,7 @@ def ruleset_payload() -> dict: "name": "CWL Central required workflows", "target": "branch", "enforcement": "active", + "bypass_actors": [], "conditions": { "repository_name": { "include": ["~ALL"], @@ -118,6 +119,53 @@ def test_expected_central_ruleset_passes(monkeypatch, capsys) -> None: ) +def test_central_ruleset_rejects_persistent_bypass_actor() -> None: + """A standing actor cannot bypass the exact-head review and check gates.""" + payload = ruleset_payload() + payload["bypass_actors"] = [ + { + "actor_id": 1, + "actor_type": "OrganizationAdmin", + "bypass_mode": "always", + } + ] + + assert audit.audit_ruleset(payload) == [ + "central ruleset has persistent bypass actors: " + "['OrganizationAdmin:1:always']" + ] + + +def test_central_ruleset_rejects_missing_bypass_actor_data() -> None: + """An incomplete live payload cannot prove that no bypass actor exists.""" + payload = ruleset_payload() + payload.pop("bypass_actors") + + assert audit.audit_ruleset(payload) == [ + "central ruleset bypass actor data is missing" + ] + + +def test_central_ruleset_rejects_malformed_bypass_actor_data() -> None: + """Unreadable bypass configuration fails closed.""" + payload = ruleset_payload() + payload["bypass_actors"] = "invalid" + + assert audit.audit_ruleset(payload) == [ + "central ruleset bypass actor data is malformed" + ] + + +def test_central_ruleset_describes_malformed_bypass_actor_entry() -> None: + """A malformed standing actor remains visible in the drift reason.""" + payload = ruleset_payload() + payload["bypass_actors"] = ["invalid"] + + assert audit.audit_ruleset(payload) == [ + "central ruleset has persistent bypass actors: ['']" + ] + + def test_inherited_ruleset_and_organization_scope_probes_pass() -> None: assert audit.audit_ruleset(inherited_ruleset_payload()) == [] @@ -378,6 +426,7 @@ def test_audit_reports_all_structural_and_protection_drift() -> None: "expected ruleset name CWL Central required workflows", "central ruleset target is not branch", "central ruleset enforcement is not active", + "central ruleset bypass actor data is missing", "central ruleset does not include all repositories", "central ruleset repository exclusions drifted: expected ['.github', 'IRT-bibliography-set', 'noema'], got []", "central ruleset does not target every default branch", @@ -491,6 +540,35 @@ def test_scheduled_audit_and_rollout_document_semgrep_and_noema_requirements() - assert "- `.github/workflows/sast-semgrep.yml`" in rollout +def test_ruleset_audit_requires_dedicated_write_credential() -> None: + """The bypass audit must not confuse hidden fields with live drift.""" + workflow = (REPO_ROOT / ".github/workflows/audit-central-ruleset.yml").read_text( + encoding="utf-8" + ) + ruleset_step = workflow.split( + "- name: Read live inherited organization ruleset and repository scope\n", 1 + )[1].split(" - name: ", 1)[0] + coverage_step = workflow.split( + "- name: Audit organization CodeQL coverage\n", 1 + )[1].split(" - name: ", 1)[0] + + assert ( + "GH_TOKEN: ${{ secrets.CWL_RULESET_AUDIT_TOKEN || '' }}" in ruleset_step + ) + assert ( + "RULESET_WRITE_CREDENTIAL_AVAILABLE: " + "${{ secrets.CWL_RULESET_AUDIT_TOKEN != '' }}" in ruleset_step + ) + assert 'if [ "$RULESET_WRITE_CREDENTIAL_AVAILABLE" = "false" ]; then' in ruleset_step + assert "requires CWL_RULESET_AUDIT_TOKEN with ruleset write access" in ruleset_step + assert "|| github.token" not in ruleset_step + assert "GH_TOKEN: ${{ github.token }}" not in ruleset_step + assert ( + "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN " + "|| secrets.OPENCODE_APPROVE_TOKEN || '' }}" in coverage_step + ) + + def test_audit_organization_codeql_coverage_step_has_freshness_and_credential_guard() -> None: workflow = (REPO_ROOT / ".github/workflows/audit-central-ruleset.yml").read_text( encoding="utf-8"