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
68 changes: 66 additions & 2 deletions scripts/qualification_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,70 @@
)


def _merge_queue_sonar_workflow(document: dict[str, Any], target: str, path: str) -> bool:

Check failure on line 1025 in scripts/qualification_policy.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 27 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_.github&issues=AaF5YtxMtQ1PVhg_KCq4&open=AaF5YtxMtQ1PVhg_KCq4&pullRequest=266
"""Allow one default-branch static scanner, with no candidate execution."""
if target != "server" or Path(path).name != "queue-sonar.yml":
return False
if set(document) != {"name", "on", "permissions", "concurrency", "jobs"}:
return False
if document["on"] != {"workflow_run": {"workflows": ["PHPUnit Feature"], "types": ["completed"]}}:
return False
if document["permissions"] != {"contents": "read"} or document["concurrency"] != {
"group": "queue-sonar-${{ github.event.workflow_run.head_sha }}", "cancel-in-progress": "false",
}:
return False
jobs = document["jobs"]
if not isinstance(jobs, dict) or set(jobs) != {"queue-analysis"}:
return False
job = jobs["queue-analysis"]
if not isinstance(job, dict) or set(job) != {"name", "if", "runs-on", "timeout-minutes", "steps"}:
return False
if job["runs-on"] != "ubuntu-latest" or job["timeout-minutes"] != "20" or job["if"] != (
"github.event_name == 'workflow_run' && "
"github.event.workflow_run.event == 'merge_group' && "
"github.event.workflow_run.conclusion == 'success'"
):
return False
steps = job["steps"]
if not isinstance(steps, list) or len(steps) != 5:
return False
normalized = []
for name in [document["name"], job["name"], *[step.get("name", "") for step in steps if isinstance(step, dict)]]:
if not isinstance(name, str) or "${{" in name:
return False
for step in steps:
if not isinstance(step, dict):
return False
value = {key: content for key, content in step.items() if key != "name"}
if "uses" in value:
reference = _split_action_reference(value["uses"])
if reference is None or reference[1]:
return False
# Immutable action approval is checked before this trust profile.
value["uses"] = reference[0]
normalized.append(value)
return normalized == [
{"uses": "actions/checkout", "with": {
"ref": "${{ github.workflow_sha }}", "path": ".sonar-control", "persist-credentials": "false",
}},
{"id": "candidate", "run": "node .sonar-control/scripts/ci/merge-queue-sonar.mjs validate"},
{"uses": "actions/checkout", "with": {
"ref": "${{ steps.candidate.outputs.sha }}", "path": ".sonar-candidate", "fetch-depth": "0",
"persist-credentials": "false", "submodules": "false", "lfs": "false",
}},
{"run": "node .sonar-control/scripts/ci/merge-queue-sonar.mjs prepare"},
{"uses": "sonarsource/sonarqube-scan-action", "env": {"SONAR_TOKEN": "${{ secrets.SONAR_TOKEN }}"},
"with": {"projectBaseDir": ".sonar-candidate", "args": (
"-Dsonar.branch.name=${{ steps.candidate.outputs.branch }} "
"-Dsonar.scm.revision=${{ steps.candidate.outputs.sha }} "
"-Dsonar.scm.exclusions.disabled=true "
"-Dsonar.rust.clippy.enabled=false "
"-Dsonar.working.directory=${{ runner.temp }}/queue-sonar-work "
"-Dsonar.qualitygate.wait=true -Dsonar.qualitygate.timeout=300"
)}},
]


def _sonar_coverage_handoff(jobs: dict[str, Any], job: dict[str, Any], download_index: int) -> bool:
"""Accept only Clover data from the same build's named coverage producer."""
if download_index != 1 or not _main_sonar_coverage_condition(job) or not _isolated_sonar_job(job):
Expand Down Expand Up @@ -1632,7 +1696,7 @@
"immutable artifact identity, and pre-use digest validation"
)

if workflow_run is not None:
if workflow_run is not None and not _merge_queue_sonar_workflow(document, target_name, workflow_path):
if not isinstance(workflow_run, dict) or consumer_policy is None:
raise PolicyError(f"{label} has no reviewed privileged workflow_run trust binding")
if workflow_run.get("workflows") != [consumer_policy["workflow"]]:
Expand Down Expand Up @@ -1693,7 +1757,7 @@
raise PolicyError(
f"{label} privileged workflow_run job {job_name!r} does not select an exact run"
)
elif consumer_policy is not None:
elif workflow_run is None and consumer_policy is not None:
raise PolicyError(f"{label} is policy-bound as a workflow_run consumer but has no workflow_run trigger")

return {
Expand Down
64 changes: 64 additions & 0 deletions tests/fixtures/merge-queue-sonar.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Merge queue Sonar

# workflow_run uses the default-branch controller, never candidate workflow source.
on:
workflow_run:
workflows: [PHPUnit Feature]
types: [completed]

permissions:
contents: read

concurrency:
group: queue-sonar-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false

jobs:
queue-analysis:
name: SonarQube merge queue analysis
if: >-
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'merge_group' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out trusted scanner controls
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.workflow_sha }}
path: .sonar-control
persist-credentials: false

- name: Validate the merge queue source identity
id: candidate
run: node .sonar-control/scripts/ci/merge-queue-sonar.mjs validate

- name: Check out the synthetic commit as analysis data
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ steps.candidate.outputs.sha }}
path: .sonar-candidate
fetch-depth: 0
persist-credentials: false
submodules: false
lfs: false

- name: Apply trusted scanner configuration
run: node .sonar-control/scripts/ci/merge-queue-sonar.mjs prepare

# No candidate command, dependency installation or scanner configuration
# executes with this token. Only the pinned static scanner receives it.
- uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
projectBaseDir: .sonar-candidate
args: >-
-Dsonar.branch.name=${{ steps.candidate.outputs.branch }}
-Dsonar.scm.revision=${{ steps.candidate.outputs.sha }}
-Dsonar.scm.exclusions.disabled=true
-Dsonar.rust.clippy.enabled=false
-Dsonar.working.directory=${{ runner.temp }}/queue-sonar-work
-Dsonar.qualitygate.wait=true
-Dsonar.qualitygate.timeout=300
40 changes: 40 additions & 0 deletions tests/test_qualification_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,46 @@ def sonar_workflow(self) -> str:
def test_isolated_same_repository_sonar_analysis_is_accepted(self) -> None:
self.scan(self.sonar_workflow())

def test_merge_queue_sonar_accepts_only_the_trusted_static_scanner(self) -> None:
source = (ROOT / "tests/fixtures/merge-queue-sonar.yml").read_text(encoding="utf-8")
scan_workflow_sources(self.policy, "server", {".github/workflows/queue-sonar.yml": source})
with self.assertRaises(PolicyError):
scan_workflow_sources(self.policy, "sdk-php", {".github/workflows/queue-sonar.yml": source})
with self.assertRaises(PolicyError):
scan_workflow_sources(self.policy, "server", {".github/workflows/another-scanner.yml": source})

def test_merge_queue_sonar_refuses_candidate_execution_and_privilege_expansion(self) -> None:
source = (ROOT / "tests/fixtures/merge-queue-sonar.yml").read_text(encoding="utf-8")
mutations = [
source.replace("workflows: [PHPUnit Feature]", "workflows: [Other workflow]"),
source.replace("types: [completed]", "types: [requested]"),
source.replace(" workflow_run:", " pull_request:\n workflow_run:"),
source.replace("event == 'merge_group'", "event == 'pull_request'"),
source.replace("conclusion == 'success'", "conclusion != 'cancelled'"),
source.replace("github.workflow_sha", "github.event.workflow_run.head_sha"),
source.replace("steps.candidate.outputs.sha", "github.event.workflow_run.head_sha"),
source.replace(".sonar-control/scripts/ci/merge-queue-sonar.mjs prepare", "./.sonar-candidate/install.sh"),
source.replace(".sonar-control/scripts/ci/merge-queue-sonar.mjs validate", "echo trusted"),
source.replace("persist-credentials: false", "persist-credentials: true"),
source.replace("submodules: false", "submodules: true"),
source.replace("lfs: false", "lfs: true"),
source.replace("runs-on: ubuntu-latest", "runs-on: self-hosted"),
source.replace("timeout-minutes: 20", "timeout-minutes: 120"),
source.replace("SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}", "SONAR_TOKEN: ${{ secrets.PRODUCTION_TOKEN }}"),
source.replace("steps:\n", "env:\n TOKEN: ${{ secrets.SONAR_TOKEN }}\n steps:\n"),
source.replace("name: Check out trusted scanner controls", "name: ${{ secrets.SONAR_TOKEN }}"),
source.replace("queue-sonar-work", ".sonar-candidate/.scannerwork"),
source.replace("projectBaseDir: .sonar-candidate", "projectBaseDir: ."),
source.replace("sonar.rust.clippy.enabled=false", "sonar.rust.clippy.enabled=true"),
source.replace("qualitygate.timeout=300", "qualitygate.timeout=300 -Dsonar.host.url=https://example.invalid"),
source + " - run: ./.sonar-candidate/after-scan.sh\n",
source + " - uses: ./candidate-action # local\n",
]
for changed in mutations:
self.assertNotEqual(source, changed)
with self.subTest(source=changed), self.assertRaises(PolicyError):
scan_workflow_sources(self.policy, "server", {".github/workflows/queue-sonar.yml": changed})

def test_sonar_coverage_preparation_does_not_receive_analysis_credential(self) -> None:
source = self.sonar_workflow().replace(
" - uses: SonarSource/",
Expand Down
Loading