diff --git a/.github/workflows/grc-product.yml b/.github/workflows/grc-product.yml new file mode 100644 index 0000000000..6e3fbee3ec --- /dev/null +++ b/.github/workflows/grc-product.yml @@ -0,0 +1,81 @@ +# Central execution only. Consumers retain trigger/concurrency declarations. +# Proposed until linux-cluster-ops#326 and QSR#136/#137 admit this caller. +name: GRC Product + +on: + workflow_call: + +permissions: + contents: read + +jobs: + product: + name: Lint, tests, and coverage + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + timeout-minutes: 20 + defaults: + run: + shell: bash --noprofile --norc -e -o pipefail {0} + steps: + - name: Admit GRC caller before checkout + env: + CALLER_REPOSITORY: ${{ github.repository }} + CALLER_EVENT: ${{ github.event_name }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + run: | + test "$CALLER_REPOSITORY" = "ContextualWisdomLab/governance-risk-compliance" + case "$CALLER_EVENT" in + pull_request) + test "$HEAD_REPOSITORY" = "$CALLER_REPOSITORY" + ;; + push|workflow_dispatch) ;; + *) exit 1 ;; + esac + - name: Harden runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + - name: Checkout exact source revision + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Verify exact source head + env: + SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$SOURCE_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + - name: Set up uv without a shared cache + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.5" + enable-cache: false + cache-local-path: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + - name: Install the hash-locked product + run: uv sync --locked --extra dev + - name: Lint + run: uv run ruff check cwl_grc tests + - name: Docstrings + run: uv run interrogate --fail-under 100 cwl_grc + - name: Tests and coverage + run: >- + uv run pytest --cov=cwl_grc --cov-branch + --cov-report=term-missing --cov-fail-under=100 + - name: Compile + run: uv run python -m compileall -q cwl_grc tests + - name: Verify lock freshness + run: uv lock --check + - name: Verify clean product tree + run: | + git diff --check + test -z "$(git status --porcelain)" + - name: Remove job-private uv cache + if: always() + env: + UV_CACHE_DIR: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + run: rm -rf -- "$UV_CACHE_DIR" diff --git a/CHANGELOG.d/20261009-grc-central-quality.md b/CHANGELOG.d/20261009-grc-central-quality.md new file mode 100644 index 0000000000..e2b9fd93b9 --- /dev/null +++ b/CHANGELOG.d/20261009-grc-central-quality.md @@ -0,0 +1,10 @@ +### GRC central quality workflow remains staged behind isolated runner admission + +- Add the central `workflow_call` quality lane, thin-caller template, and + regression contracts for GRC lint/tests/coverage while keeping product policy + in `ContextualWisdomLab/governance-risk-compliance`. +- This is source preparation only. No consumer caller is installed, no runner + access is expanded, and no hosted fallback is introduced. Activation still + requires the authorized disposable isolated-runner and cleanup canary, + exact-head required Checks, independent approval, and ordinary protected + integration. diff --git a/docs/doctoring/grc-product-central-execution.md b/docs/doctoring/grc-product-central-execution.md new file mode 100644 index 0000000000..1998bd2c26 --- /dev/null +++ b/docs/doctoring/grc-product-central-execution.md @@ -0,0 +1,115 @@ +# GRC Product central execution — staged rollout + +Status: **Implemented source / cloud policy-tested / not activated**. + +## Ownership and existing delivery + +The GRC policy/control/evidence kernel remains in +`ContextualWisdomLab/governance-risk-compliance`. This control plane owns only +`.github/workflows/grc-product.yml`, its policy tests, `.github/actionlint.yaml` +(custom label validation), and the thin-caller template. +There is no new identity, risk, audit, scanner, database, or officer UI body here. + +- `ContextualWisdomLab/.github#2565` already owns conversion of all existing + central workflows to self-hosted. Do not duplicate its 73-job change. +- `ContextualWisdomLab/linux-cluster-ops#326` owns S1 placement, registration, + access custody, image provisioning, reset and actual canary. +- `ContextualWisdomLab/quarantine-sandbox-runtime#136` and + `ContextualWisdomLab/quarantine-sandbox-runtime#137` own reachability-attestation + contracts. Limited point probes are not complete CIDR or clean-job evidence. +- GRC privacy #69/#70; Keyverse #38, #55–#58, revision #73 and coverage #74; + PostgreSQL #18; version-one API #53; officer workspace #34/#54; readiness #17; + domain roadmap #27–#30; and baseline refresh #75 retain their existing owners. + All those references are in `ContextualWisdomLab/governance-risk-compliance`. + +A useful commercial GRC must distinguish artifact presence from tested control +operating effectiveness. The existing roadmap's dependency sequence remains: +identity/tenancy, PostgreSQL, evidence recovery, operations and stable API; +then internal controls, obligations/catalog, risk/audit and connected officer +workflows. This CI increment does not close any of those product issues. + +## Central contract + +The workflow accepts no executable caller input or secret inheritance. It runs +only the fixed GRC repository on same-repository PRs, pushes and manual runs. +Foreign callers, external PR heads, missing PR repository identity and unsupported +events fail in an actual pre-checkout shell; they are not skipped into success. +This guard executes after runner assignment and is not pre-lease isolation proof. + +Only `CWL CI isolated` AND `[self-hosted, Linux, X64, cwlab-ci-isolated]` may run +the product job. No hosted fallback, control/scanner pool or existing runner +relabeling is introduced. The original exact-head checkout, immutable Actions, +`contents: read`, no persisted Git credentials, Python 3.12, locked uv install, +Ruff, docstrings 100%, production statement/branch coverage 100%, compile, lock +freshness and dirty-tree gates remain. UV cache is job-private, not shared or +uploaded, and cleanup runs even on failure. VM cancellation/termination reset, +HOME/tool-cache/workspace cleanup and network isolation remain operator gates. + +StepSecurity harden-runner is retained, not silently removed. Its self-hosted +agent and subscription/image acceptance must be established by the operator; +a successful/no-op action is not monitoring or isolation evidence. No subscription +purchase is authorized here. References: GitHub's reusable-workflow runner context +reference and StepSecurity's self-hosted/how-it-works documentation. + +## Consumer rollout, without dangling remote pins + +`docs/templates/grc-product.yml.in` preserves GRC trigger/path exclusions and +workflow-scope PR-only cancellation. ARCHITECTURE.md is deliberately not ignored. +Do not add the same concurrency group to the reusable workflow: it can cancel +its calling run. Changing to a reusable job may change check-context display +names; inspect actual contexts and reconcile through the existing protection owner, +never remove required gates as a shortcut. + +1. Independently review and publish this central source through the ordinary + protected path. This session does not authorize commit, push or merge. +2. Obtain a real accepted isolated group, caller repository/workflow/ref/event/fork + eligibility, disposable image and cleanup proof under ops #326 and QSR. + Runner availability and central workflow availability are separate gates. +3. Replace `__CENTRAL_PRODUCT_COMMIT__` in the template with the exact reviewed, + published 40-hex central commit. Verify that commit contains this workflow. + Copy only the rendered caller to GRC `.github/workflows/product.yml` in an + owner-reviewed PR; do not use a fabricated hash, mutable ref or unresolved pin. +4. Update GRC's dirty-tree workflow regression to validate the released central + contract rather than require execution commands in the caller. Keep its + concurrency regression and all product quality thresholds unchanged. +5. Have existing #18, #34 and #17 owners inventory and migrate PostgreSQL, UI + and readiness executable jobs using independently reviewed central contracts. + Those contracts are not implemented by this initial Product slice. +6. Inspect GitHub-managed dynamic CodeQL/Code Quality configuration through its + current owner. Moving YAML cannot move those platform-managed jobs. +7. On the unchanged caller/central source pair, record actual run/job ID, runner + name/group, tool-image identity, all executed checks, cleanup and qualifying + independent approval. Only that evidence permits a runtime-complete claim. + +## Observed boundary — October 5, 2026 + +The initial GRC branch is `529cf321f134e26c0cd379ee53c06ab5297363b6`. +Central baseline is `37b10243cec3d160ecc9c1be75c71428b160a703`. +The GRC repository runner inventory returned zero. The original GRC worktree is +unchanged. Central preparation is in a dedicated cloud clone on S1, not another +writer's checkout. The operator infrastructure and production containers were +not modified. No remote central approval, protected integration, self-hosted +GitHub Product run, full-organizational migration or release is claimed. + +## Execution evidence + +New central-contract existence test first failed with +`GRC Product execution must be centrally owned`. The implemented workflow then +passed 8 policy tests normally and 8 with `GITHUB_ACTIONS=true` on S1. The +admission regression executes the actual shell for allowed and rejected tuples. +This is cloud source validation, not registered runner execution. + +Additional actual S1 acceptance: actionlint 1.7.12 (official archive digest verified), +new policy-test Ruff E9/F/I and `git diff --check` pass. The unchanged GRC baseline +ran in a resource-bounded, non-root, read-only Python 3.12.15 container with no +host socket, service mounts or credentials. Locked uv 0.12.5 installation, +Ruff, docstrings, compile and lock freshness pass; 48 tests cover all 769 +production statements and 152 branches. The run emitted 36 existing dependency +deprecation warnings (Starlette HTTPX and SQLite datetime adapter), which remain +visible rather than being suppressed. The product tree remains clean and the +container was removed. The bootstrap uv package was fetched by exact version, +not a hash-locked bootstrap; this receipt is not runner-image supply-chain approval. +Full central repository tests, actual GitHub Actions, protected integration and +release remain NOT_RUN. Further gate +results must be reported from their actual command output, not inherited from +central #2565 or any product feature PR. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 25d6c727fd..4e744b9691 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,12 @@ # Product and Technical Gap Baseline +## 2026-10-09 GRC central reusable quality lane + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| CONTROL-GRC-CENTRAL-QUALITY-01 | `.github#2602` predecessor `efb53f7b44d09614993702e4a20d75c06a190a37` carries the GRC reusable workflow, thin-caller template, and contract tests. Canonical runner-policy owner `.github#2565` is integrated at `ef65b2fdc2036ccaca8d19c1db88dc8075ae9164`; the stack repair preserves both source histories and reapplies the current-time CodeQL fixture repair to the owner tree. | Keep the product caller uninstalled until `linux-cluster-ops#326` provisions and proves disposable isolated capacity and cleanup. Re-fetch the published head and require exact-head required Checks, independent approval, and ordinary protected integration. No hosted fallback, borrowed privileged runner, or bypass. | **Draft / Proposed / merge HOLD** — source staged; runtime activation and protected integration pending. | + + ## 2026-10-09 reusable R matrix conservation | Gap | Exact evidence | Action | Status | diff --git a/docs/templates/grc-product.yml.in b/docs/templates/grc-product.yml.in new file mode 100644 index 0000000000..b70f342ec0 --- /dev/null +++ b/docs/templates/grc-product.yml.in @@ -0,0 +1,33 @@ +# Render __CENTRAL_PRODUCT_COMMIT__ to the reviewed published 40-hex commit. +# Do not activate before the dedicated runner/access/isolation canary passes. +name: Product + +on: + push: + branches: + - develop + paths-ignore: + - "docs/**" + - "AGENTS.md" + - "CHANGELOG.md" + - "CLAUDE.md" + - "README.md" + pull_request: + paths-ignore: + - "docs/**" + - "AGENTS.md" + - "CHANGELOG.md" + - "CLAUDE.md" + - "README.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + product: + uses: ContextualWisdomLab/.github/.github/workflows/grc-product.yml@__CENTRAL_PRODUCT_COMMIT__ diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 8d95cdb0b1..5412196ca0 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, @@ -270,7 +274,13 @@ def now(cls, tz=None): return NOW monkeypatch.setattr(audit, "datetime", FixedClock) - 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) == [] @@ -316,7 +326,14 @@ def now(cls, tz=None): monkeypatch.setattr(audit, "datetime", FixedClock) 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_grc_product_reusable_workflow_contract.py b/tests/test_grc_product_reusable_workflow_contract.py new file mode 100644 index 0000000000..27bfdbefea --- /dev/null +++ b/tests/test_grc_product_reusable_workflow_contract.py @@ -0,0 +1,139 @@ +"""Contracts for centrally owned, isolated GRC Product execution.""" + +import os +import re +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = REPO_ROOT / ".github/workflows/grc-product.yml" + + +def workflow_text() -> str: + """Read the central contract, failing explicitly before implementation.""" + assert WORKFLOW.is_file(), "GRC Product execution must be centrally owned" + return WORKFLOW.read_text(encoding="utf-8") + + +def admission_script() -> str: + """Extract the actual pre-checkout shell rather than emulate its decision.""" + source = workflow_text().split( + " - name: Admit GRC caller before checkout\n", 1 + )[1] + block = source.split(" run: |\n", 1)[1].split(" - name:", 1)[0] + return "\n".join(line[10:] for line in block.splitlines()) + + +def test_central_grc_product_contract_exists() -> None: + """Execution is reusable and has no schedule or privileged trigger.""" + source = workflow_text() + assert "on:\n workflow_call:\n" in source + assert "inputs:" not in source + assert "pull_request_target" not in source + assert "secrets:" not in source + + +def test_only_dedicated_self_hosted_group_is_admitted() -> None: + """No hosted fallback or privileged central pool executes product code.""" + source = workflow_text() + assert "group: CWL CI isolated" in source + assert "labels: [self-hosted, Linux, X64, cwlab-ci-isolated]" in source + assert len(re.findall(r"^ runs-on:", source, re.M)) == 1 + assert "ubuntu-24.04" not in source + assert "timeout-minutes: 20" in source + config = REPO_ROOT / ".github/actionlint.yaml" + assert config.is_file(), "Custom runner labels require repository actionlint config" + assert " - cwlab-ci-isolated" in config.read_text(encoding="utf-8") + assert "permissions:\n contents: read" in source + + +def test_actual_precheckout_admission_accepts_only_supported_caller_events() -> None: + """Unsupported callers/forks/events fail, not skip to a false green.""" + script = admission_script() + subprocess.run(["bash", "-n"], input=script, text=True, check=True) + repository = "ContextualWisdomLab/governance-risk-compliance" + for event, caller, head, accepted in ( + ("pull_request", repository, repository, True), + ("push", repository, "", True), + ("workflow_dispatch", repository, "", True), + ("pull_request", repository, "external/grc", False), + ("pull_request", repository, "", False), + ("push", "external/grc", "", False), + ("repository_dispatch", repository, "", False), + ("schedule", repository, "", False), + ): + result = subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], + env={ + **os.environ, + "CALLER_REPOSITORY": caller, + "CALLER_EVENT": event, + "HEAD_REPOSITORY": head, + }, + capture_output=True, + text=True, + check=False, + ) + assert (result.returncode == 0) is accepted, (event, caller, head) + + +def test_exact_head_and_checkout_identity_are_preserved() -> None: + """Checkout uses caller context with no credential persistence.""" + source = workflow_text() + assert source.index("Admit GRC caller before checkout") < source.index( + "actions/checkout@" + ) + assert "ref: ${{ github.event.pull_request.head.sha || github.sha }}" in source + assert "persist-credentials: false" in source + assert "SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}" in source + assert 'test "$(git rev-parse HEAD)" = "$SOURCE_SHA"' in source + for ref in re.findall(r"uses: (\S+)", source): + assert re.fullmatch(r"[^@]+@[0-9a-f]{40}", ref), ref + + +def test_original_quality_and_clean_tree_gates_are_not_lowered() -> None: + """All original commands and thresholds execute centrally.""" + source = workflow_text() + for command in ( + "uv sync --locked --extra dev", + "uv run ruff check cwl_grc tests", + "uv run interrogate --fail-under 100 cwl_grc", + "uv run pytest --cov=cwl_grc --cov-branch", + "--cov-report=term-missing --cov-fail-under=100", + "uv run python -m compileall -q cwl_grc tests", + "uv lock --check", + "git diff --check", + 'test -z "$(git status --porcelain)"', + ): + assert command in source, command + assert "continue-on-error" not in source + assert 'python-version: "3.12"' in source + + +def test_concurrency_lives_only_in_caller() -> None: + """Reusable/caller concurrency cannot cancel its own parent run.""" + assert "concurrency:" not in workflow_text() + + +def test_uv_cache_is_job_private_and_removed_on_failure() -> None: + """No persisted PR cache crosses execution trust contexts.""" + source = workflow_text() + assert "enable-cache: false" in source + assert 'version: "0.12.5"' in source + assert ( + "cache-local-path: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-" + "${{ github.run_attempt }}-${{ github.job }}" + ) in source + assert ( + "UV_CACHE_DIR: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-" + "${{ github.run_attempt }}-${{ github.job }}" + ) in source + assert " - name: Remove job-private uv cache\n if: always()" in source + assert 'rm -rf -- "$UV_CACHE_DIR"' in source + + +def test_existing_hardening_action_is_retained() -> None: + """Self-hosted image-agent acceptance stays an operator prerequisite.""" + source = workflow_text() + assert "step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40" in source + assert "egress-policy: audit" in source