diff --git a/.github/scripts/product-dco.js b/.github/scripts/product-dco.js new file mode 100644 index 00000000000..b60a2b71d90 --- /dev/null +++ b/.github/scripts/product-dco.js @@ -0,0 +1,104 @@ +"use strict"; + +const { execFileSync } = require("node:child_process"); + +const SHA = /^[0-9a-f]{40}$/; +const MAX_COMMITS = 250; // GitHub's pull-request commits endpoint is capped here. + +function sameIdentity(actual, expected, repository, number) { + if ( + actual.number !== number || + actual.base?.repo?.full_name !== repository || + actual.base?.sha !== expected.base?.sha || + actual.head?.sha !== expected.head?.sha || + actual.base?.ref !== expected.base?.ref || + actual.state !== "open" + ) { + throw new Error("Pull request identity changed; rerun for its current base and head"); + } +} + +/** Require a Git-parsed sign-off matching the immutable commit author. */ +function hasAuthorSignoff(commit) { + const { message, author } = commit; + if ( + typeof message !== "string" || Buffer.byteLength(message) > 1024 * 1024 || + typeof author?.name !== "string" || !author.name.trim() || + typeof author?.email !== "string" || !author.email.trim() + ) { + return false; + } + const gitEnvironment = { ...process.env, GIT_CONFIG_NOSYSTEM: "1", + GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_COUNT: "0", GIT_DIR: "/dev/null" }; + delete gitEnvironment.GIT_CONFIG_PARAMETERS; + const trailers = execFileSync("git", ["interpret-trailers", "--parse"], { + input: message, + encoding: "utf8", + timeout: 5000, + maxBuffer: 1024 * 1024, + // Repository configuration must not redefine what counts as a sign-off. + env: gitEnvironment, + }); + return trailers.split("\n").some((line) => { + const match = /^Signed-off-by:\s+([^<>\r\n]+)\s+<([^<>\s]+)>\s*$/i.exec(line); + return match && + match[1].trim().toLowerCase() === author.name.trim().toLowerCase() && + match[2].toLowerCase() === author.email.trim().toLowerCase(); + }); +} + +/** Verify every PR commit using read-only API metadata, fenced by base and head. */ +async function verify({ github, context }) { + if (context.eventName !== "pull_request_target") { + throw new Error("Product DCO requires a trusted pull_request_target event"); + } + const repository = `${context.repo.owner}/${context.repo.repo}`; + const expected = context.payload.pull_request; + const number = context.payload.number; + if ( + !Number.isSafeInteger(number) || number < 1 || + context.payload.repository?.full_name !== repository || + !SHA.test(expected?.base?.sha) || !SHA.test(expected?.head?.sha) || + !SHA.test(context.evaluatorSha) || context.evaluatorSha !== expected.base.sha + ) { + throw new Error("Missing exact repository, PR, base, head, or trusted evaluator SHA"); + } + const request = { ...context.repo, pull_number: number }; + const { data: pull } = await github.rest.pulls.get(request); + sameIdentity(pull, expected, repository, number); + if (!Number.isInteger(pull.commits) || pull.commits < 1 || pull.commits > MAX_COMMITS) { + throw new Error("Cannot prove the complete commit list: PR must contain 1–250 commits"); + } + const commits = await github.paginate(github.rest.pulls.listCommits, { + ...request, per_page: 100, + }); + if ( + commits.length !== pull.commits || + new Set(commits.map((item) => item.sha)).size !== pull.commits || + commits.some((item) => !SHA.test(item.sha)) || + commits.at(-1)?.sha !== expected.head.sha + ) { + throw new Error("Incomplete, duplicate, or stale pull-request commit list"); + } + const failures = commits.filter((item) => !hasAuthorSignoff(item.commit)).map((item) => item.sha); + // A force-push or base update while paginating must invalidate this result. + const { data: latest } = await github.rest.pulls.get(request); + sameIdentity(latest, expected, repository, number); + if (latest.commits !== pull.commits) { + throw new Error("Pull request commit count changed during verification"); + } + return { + schema_version: 1, + kind: "dco_qualification", + repository, + pull_request: number, + base_sha: expected.base.sha, + head_sha: expected.head.sha, + evaluator_sha: context.evaluatorSha, + commits: commits.map((item) => item.sha), + qualified: failures.length === 0, + missing_author_signoff: failures, + }; +} + +module.exports = { hasAuthorSignoff, verify }; diff --git a/.github/scripts/product-dco.test.js b/.github/scripts/product-dco.test.js new file mode 100644 index 00000000000..e289e7aff77 --- /dev/null +++ b/.github/scripts/product-dco.test.js @@ -0,0 +1,160 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); +const { hasAuthorSignoff, verify } = require("./product-dco.js"); + +const BASE = "a".repeat(40); +const HEAD = "b".repeat(40); +const TESTED = "c".repeat(40); +const author = { name: "Test Author", email: "author@example.com" }; +const signed = "Example change\n\nSigned-off-by: Test Author \n"; + +function harness(count = 1) { + const context = { + repo: { owner: "mfethe1", repo: "buzz" }, + sha: BASE, + evaluatorSha: BASE, + eventName: "pull_request_target", + payload: { + repository: { full_name: "mfethe1/buzz" }, + number: 19, + pull_request: { + number: 19, state: "open", commits: count, + base: { sha: BASE, ref: "product/main", repo: { full_name: "mfethe1/buzz" } }, + head: { sha: HEAD }, + }, + }, + }; + const initial = structuredClone(context.payload.pull_request); + const latest = structuredClone(initial); + const commits = Array.from({ length: count }, (_, index) => ({ + sha: index === count - 1 ? HEAD : (index + 1).toString(16).padStart(40, "0"), + commit: { author, message: signed }, + })); + let reads = 0; + const calls = []; + const listCommits = () => { throw new Error("Must paginate listCommits"); }; + const github = { + rest: { pulls: { + get: async (request) => { + calls.push(request); + return { data: reads++ === 0 ? initial : latest }; + }, + listCommits, + } }, + paginate: async (method, request) => { + assert.equal(method, listCommits); + assert.equal(request.per_page, 100); + calls.push(request); + return commits; + }, + }; + return { github, context, initial, latest, commits, calls }; +} + +test("Git parses a real matching author sign-off with additional trailers", () => { + assert.ok(hasAuthorSignoff({ author, message: `${signed}Reviewed-by: Another Person \n` })); + assert.ok(hasAuthorSignoff({ author, message: signed.replace("Test Author", "test author") })); +}); + +test("body text, non-author sign-offs, malformed emails, and missing metadata fail", () => { + for (const message of [ + "Example change", signed.replace("author@example.com", "other@example.com"), + signed.replace("Test Author", "Different Author"), + signed.replace("", "author@example.com"), + `${signed}\nThat was a quoted example, not my sign-off.`, + "> Signed-off-by: Test Author \n", + ]) { + assert.equal(Boolean(hasAuthorSignoff({ author, message })), false, message); + } + assert.equal(hasAuthorSignoff({ author: {}, message: signed }), false); + assert.equal(hasAuthorSignoff({ author, message: "x".repeat(1024 * 1024 + 1) }), false); +}); + +test("complete paginated verification binds repo, PR, base, head, and trusted evaluator", async () => { + const data = harness(101); + const receipt = await verify(data); + assert.equal(receipt.qualified, true); + assert.equal(receipt.commits.length, 101); + assert.equal(receipt.repository, "mfethe1/buzz"); + assert.equal(receipt.pull_request, 19); + assert.equal(receipt.base_sha, BASE); + assert.equal(receipt.head_sha, HEAD); + assert.equal(receipt.evaluator_sha, BASE); + assert.equal(data.calls.length, 3); + assert.ok(data.calls.every((call) => call.owner === "mfethe1" && call.repo === "buzz" && call.pull_number === 19)); +}); + +test("one unsigned middle commit denies the whole PR, including bots and merges", async () => { + const data = harness(101); + data.commits[50].commit.message = "Unsigned change"; + data.commits[50].author = { type: "Bot" }; + data.commits[50].parents = [{ sha: BASE }, { sha: TESTED }]; + const receipt = await verify(data); + assert.equal(receipt.qualified, false); + assert.deepEqual(receipt.missing_author_signoff, [data.commits[50].sha]); +}); + +test("a caller cannot authorize the wrong repository or an unpinned commit", async () => { + for (const mutate of [ + (data) => { data.context.payload.repository.full_name = "other/repo"; }, + (data) => { data.context.eventName = "workflow_dispatch"; }, + (data) => { data.context.evaluatorSha = "product/main"; }, + (data) => { data.context.payload.pull_request.head.sha = "main"; }, + ]) { + const data = harness(); mutate(data); + await assert.rejects(verify(data)); + assert.equal(data.calls.length, 0); + } +}); + +test("stale head, base, target branch, repository or closed PR deny before listing commits", async () => { + for (const mutate of [ + (pull) => { pull.head.sha = TESTED; }, + (pull) => { pull.base.sha = TESTED; }, + (pull) => { pull.base.ref = "other-branch"; }, + (pull) => { pull.base.repo.full_name = "other/repo"; }, + (pull) => { pull.state = "closed"; }, + (pull) => { pull.number = 20; }, + ]) { + const data = harness(); mutate(data.initial); + await assert.rejects(verify(data), /identity changed/); + assert.equal(data.calls.length, 1); + } +}); + +test("head or base movement during pagination invalidates previously valid sign-offs", async () => { + for (const side of ["head", "base"]) { + const data = harness(); data.latest[side].sha = TESTED; + await assert.rejects(verify(data), /identity changed/); + } + const data = harness(); data.latest.commits += 1; + await assert.rejects(verify(data), /count changed/); +}); + +test("partial lists, duplicate entries, wrong last commit, and malformed SHAs fail closed", async () => { + for (const mutate of [ + (data) => { data.commits.splice(1, 1); }, + (data) => { data.commits[0].sha = data.commits[1].sha; }, + (data) => { data.commits.at(-1).sha = TESTED; }, + (data) => { data.commits[0].sha = "main"; }, + ]) { + const data = harness(3); mutate(data); + await assert.rejects(verify(data), /Incomplete, duplicate, or stale/); + } +}); + +test("API truncation limit and missing count do not become a partial pass", async () => { + for (const count of [0, 251, undefined, "1"]) { + const data = harness(); data.initial.commits = count; + await assert.rejects(verify(data), /complete commit list/); + assert.equal(data.calls.length, 1); + } +}); + +test("provider errors propagate rather than authorize an empty result", async () => { + const data = harness(); + data.github.paginate = async () => { throw new Error("API unavailable"); }; + await assert.rejects(verify(data), /API unavailable/); +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a82d1a16920..bb67cf4e58d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,10 @@ jobs: scripts/test-rust-cache-contract-regressions.sh - name: CI required-context isolation contract run: scripts/test-ci-required-context-isolation.sh + - name: Product DCO regressions + run: node --test .github/scripts/product-dco.test.js + - name: Product qualification regressions + run: python3 scripts/test-product-qualification.py - name: File size policy run: just file-size-check diff --git a/scripts/product-qualification.py b/scripts/product-qualification.py new file mode 100644 index 00000000000..5df8e8dcc2d --- /dev/null +++ b/scripts/product-qualification.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Fail closed on selected CI jobs and record the exact tested change. + +The receipt is CI evidence, not independent review or deployment authorization. +It aggregates CI results; protected review of the workflow definition remains +required. Its base-pinned evaluator cannot itself attest hostile PR workflow code. +A copied JSON receipt has no authority. +""" + +import argparse +import json +import os +from pathlib import Path +import re +import sys + + +def qualify(needs, event, env): + """Return a qualification receipt for GitHub's job results and event.""" + failures = [] + repository = env["GITHUB_REPOSITORY"] + if event["repository"]["full_name"] != repository: + raise ValueError("event repository differs from workflow repository") + event_name = env["GITHUB_EVENT_NAME"] + if event_name == "pull_request": + pr = event["pull_request"] + if pr["base"]["repo"]["full_name"] != repository: + raise ValueError("pull request targets a different repository") + identity = { + "repository": repository, + "pull_request": event["number"], + "base_sha": pr["base"]["sha"], + "head_sha": pr["head"]["sha"], + "base_ref": pr["base"]["ref"], + } + elif event_name == "push": + identity = { + "repository": repository, + "pull_request": None, + "base_sha": event["before"], + "head_sha": event["after"], + "base_ref": event["ref"].removeprefix("refs/heads/"), + } + if event["after"] != env["GITHUB_SHA"]: + raise ValueError("push event does not match tested commit") + else: + raise ValueError(f"unsupported event: {event_name}") + identity["tested_sha"] = env["GITHUB_SHA"] + identity["evaluator_sha"] = env["QUALIFICATION_EVALUATOR_SHA"] + if identity["evaluator_sha"] != identity["base_sha"]: + raise ValueError("qualification evaluator must come from the frozen base") + for key in ("base_sha", "head_sha", "tested_sha"): + if not re.fullmatch(r"[0-9a-f]{40}", identity[key]) or identity[key] == "0" * 40: + raise ValueError(f"missing or invalid {key}") + + changes = needs.get("changes", {}) + flags = changes.get("outputs", {}) + selected = {} + for key in ("rust", "desktop", "desktop-rust", "web", "mobile"): + value = flags.get(key) + if value not in ("true", "false"): + failures.append(f"changes.{key}: missing boolean path result") + selected[key] = value == "true" or event_name == "push" + rust = selected["rust"] + desktop_rust = selected["desktop-rust"] + desktop = rust or desktop_rust or selected["desktop"] + + # Read actual job results exported by reusable workflows. Their Results job + # only transports outputs and can succeed after a test job failed. + lanes = [ + ("changes", None, True), + ("dead-token-guard", None, True), + ("rust", "rust_lint_result", rust or desktop_rust), + ("rust", "unit_tests_result", rust), + ("rust", "windows_rust_result", rust or desktop_rust), + ("rust-cross-compile-domain", "server_cross_compile_result", rust), + ("desktop-domain", "desktop_result", desktop), + ("desktop-domain", "desktop_windows_result", desktop), + ("desktop-macos-domain", "desktop_macos_result", desktop), + ("relay-artifacts-domain", "desktop_e2e_relay_result", desktop), + ("relay-domain", "desktop_e2e_integration_result", desktop), + ("relay-domain", "backend_integration_result", rust), + ("relay-domain", "relay_e2e_result", rust), + ("postgres-domain", "postgres_tests_result", rust), + ("clients", "web_result", selected["web"]), + ("clients", "mobile_result", selected["mobile"]), + ("mobile-swift-domain", "mobile_swift_result", selected["mobile"]), + ("security-domain", "security_result", rust), + ] + evidence = [] + for job, output, required in lanes: + state = needs.get(job, {}) + result = state.get("outputs", {}).get(output) if output else state.get("result") + name = f"{job}.{output}" if output else job + evidence.append({"lane": name, "required": required, "result": result}) + if required and (state.get("result") != "success" or result != "success"): + failures.append(f"{name}: required success; got {result!r}, wrapper {state.get('result')!r}") + elif result not in (None, "", "skipped", "success"): + failures.append(f"{name}: unexpected {result!r}") + # Never lose a failed/cancelled wrapper merely because the path selector says + # that domain should not have run. + for job, state in needs.items(): + if state.get("result") not in ("success", "skipped"): + failures.append(f"{job}: wrapper {state.get('result')!r}") + return { + "schema_version": 1, + "kind": "ci_qualification", + **identity, + "run_id": env["GITHUB_RUN_ID"], + "run_attempt": env["GITHUB_RUN_ATTEMPT"], + "qualified": not failures, + "evidence": evidence, + "failures": failures, + } + + +def main(): + """Consume GitHub's runtime environment and write evidence even on denial.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + needs = json.loads(os.environ["QUALIFICATION_NEEDS"]) + event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text()) + receipt = qualify(needs, event, os.environ) + except (KeyError, TypeError, ValueError, OSError) as error: + receipt = {"qualified": False, "failures": [f"invalid qualification input: {error}"]} + args.output.write_text(json.dumps(receipt, indent=2) + "\n") + for failure in receipt["failures"]: + print(f"FAIL: {failure}", file=sys.stderr) + if receipt["qualified"]: + print(f"PASS: CI qualified {receipt['repository']} {receipt['head_sha']} tested as {receipt['tested_sha']}") + return 0 if receipt["qualified"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test-product-qualification.py b/scripts/test-product-qualification.py new file mode 100644 index 00000000000..a099e55474d --- /dev/null +++ b/scripts/test-product-qualification.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Exercise the production qualification CLI, including deceptive green wrappers.""" + +import copy +import importlib.util +import itertools +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +SCRIPT = Path(__file__).with_name("product-qualification.py") +spec = importlib.util.spec_from_file_location("qualification", SCRIPT) +qualification = importlib.util.module_from_spec(spec) +spec.loader.exec_module(qualification) + + +def fixture(): + """Build a full-CI result set using the exported production lane inventory.""" + env = { + "GITHUB_REPOSITORY": "mfethe1/buzz", + "GITHUB_EVENT_NAME": "pull_request", + "GITHUB_SHA": "c" * 40, + "QUALIFICATION_EVALUATOR_SHA": "a" * 40, + "GITHUB_RUN_ID": "100", + "GITHUB_RUN_ATTEMPT": "2", + } + event = { + "repository": {"full_name": "mfethe1/buzz"}, + "number": 18, + "pull_request": { + "base": {"sha": "a" * 40, "ref": "product/main", "repo": {"full_name": "mfethe1/buzz"}}, + "head": {"sha": "b" * 40}, + }, + } + needs = {"changes": {"result": "success", "outputs": { + key: "true" for key in ("rust", "desktop", "desktop-rust", "web", "mobile") + }}} + for evidence in qualification.qualify(needs, event, env)["evidence"]: + job, _, output = evidence["lane"].partition(".") + state = needs.setdefault(job, {"result": "success", "outputs": {}}) + if output: + state["outputs"][output] = "success" + return needs, event, env + + +class QualificationTest(unittest.TestCase): + def run_cli(self, needs, event, env): + with tempfile.TemporaryDirectory() as directory: + event_file = Path(directory) / "event.json" + output = Path(directory) / "receipt.json" + event_file.write_text(json.dumps(event)) + process = subprocess.run( + [sys.executable, str(SCRIPT), "--output", str(output)], + env={**os.environ, **env, "GITHUB_EVENT_PATH": str(event_file), + "QUALIFICATION_NEEDS": json.dumps(needs)}, + capture_output=True, text=True, timeout=10, + ) + return process, json.loads(output.read_text()) + + def test_cli_binds_change_and_tested_merge_commit(self): + process, receipt = self.run_cli(*fixture()) + self.assertEqual(process.returncode, 0, process.stderr) + self.assertTrue(receipt["qualified"]) + self.assertEqual(receipt["base_sha"], "a" * 40) + self.assertEqual(receipt["head_sha"], "b" * 40) + self.assertEqual(receipt["tested_sha"], "c" * 40) + self.assertEqual(receipt["run_attempt"], "2") + + def test_every_actual_lane_fails_closed_under_green_wrapper(self): + baseline, event, env = fixture() + evidence = qualification.qualify(baseline, event, env)["evidence"] + # A lane deleted from the evaluator must also fail this regression. + self.assertEqual({lane["lane"] for lane in evidence}, { + "changes", "dead-token-guard", "rust.rust_lint_result", + "rust.unit_tests_result", "rust.windows_rust_result", + "rust-cross-compile-domain.server_cross_compile_result", + "desktop-domain.desktop_result", "desktop-domain.desktop_windows_result", + "desktop-macos-domain.desktop_macos_result", + "relay-artifacts-domain.desktop_e2e_relay_result", + "relay-domain.desktop_e2e_integration_result", + "relay-domain.backend_integration_result", "relay-domain.relay_e2e_result", + "postgres-domain.postgres_tests_result", "clients.web_result", + "clients.mobile_result", "mobile-swift-domain.mobile_swift_result", + "security-domain.security_result", + }) + for lane in evidence: + job, _, output = lane["lane"].partition(".") + for bad in ("failure", "cancelled", "skipped", "", None): + with self.subTest(lane=lane["lane"], result=bad): + needs = copy.deepcopy(baseline) + if output: + needs[job]["outputs"][output] = bad + else: + needs[job]["result"] = bad + self.assertFalse(qualification.qualify(needs, event, env)["qualified"]) + + def test_cli_rejects_skipped_required_lane_and_keeps_failure_receipt(self): + needs, event, env = fixture() + needs["relay-domain"]["outputs"]["desktop_e2e_integration_result"] = "skipped" + process, receipt = self.run_cli(needs, event, env) + self.assertEqual(process.returncode, 1) + self.assertFalse(receipt["qualified"]) + self.assertIn("desktop_e2e_integration_result", process.stderr) + + def test_documentation_only_change_allows_explicit_out_of_scope_skips(self): + needs, event, env = fixture() + for key in needs["changes"]["outputs"]: + needs["changes"]["outputs"][key] = "false" + for job, state in needs.items(): + if job not in ("changes", "dead-token-guard"): + state.update(result="skipped", outputs={}) + process, receipt = self.run_cli(needs, event, env) + self.assertEqual(process.returncode, 0, process.stderr) + self.assertTrue(receipt["qualified"]) + needs["changes"]["outputs"].pop("rust") + self.assertFalse(qualification.qualify(needs, event, env)["qualified"]) + + def test_out_of_scope_failure_is_not_hidden(self): + needs, event, env = fixture() + needs["changes"]["outputs"]["web"] = "false" + needs["clients"]["outputs"]["web_result"] = "failure" + self.assertFalse(qualification.qualify(needs, event, env)["qualified"]) + + def test_missing_wrapper_denies(self): + needs, event, env = fixture() + needs.pop("postgres-domain") + self.assertFalse(qualification.qualify(needs, event, env)["qualified"]) + + def test_bad_identity_and_unsupported_event_deny_at_cli(self): + for field, value in (("GITHUB_REPOSITORY", "other/repo"), + ("GITHUB_EVENT_NAME", "workflow_dispatch"), + ("GITHUB_SHA", "main"), + ("QUALIFICATION_EVALUATOR_SHA", "c" * 40)): + with self.subTest(field=field): + needs, event, env = fixture() + env[field] = value + process, receipt = self.run_cli(needs, event, env) + self.assertEqual(process.returncode, 1) + self.assertFalse(receipt["qualified"]) + + def test_push_requires_all_lanes_even_when_paths_are_false(self): + needs, _, env = fixture() + env["GITHUB_EVENT_NAME"] = "push" + env["GITHUB_SHA"] = "b" * 40 + event = {"repository": {"full_name": "mfethe1/buzz"}, + "before": "a" * 40, "after": "b" * 40, "ref": "refs/heads/product/main"} + needs["changes"]["outputs"] = {key: "false" for key in needs["changes"]["outputs"]} + process, _ = self.run_cli(needs, event, env) + self.assertEqual(process.returncode, 0, process.stderr) + wrong_repository = {**env, "GITHUB_REPOSITORY": "other/repo"} + process, receipt = self.run_cli(needs, event, wrong_repository) + self.assertEqual(process.returncode, 1) + self.assertFalse(receipt["qualified"]) + needs["mobile-swift-domain"]["outputs"]["mobile_swift_result"] = "skipped" + self.assertFalse(qualification.qualify(needs, event, env)["qualified"]) + + def test_all_path_combinations_require_their_native_surfaces(self): + keys = ("rust", "desktop", "desktop-rust", "web", "mobile") + for values in itertools.product((False, True), repeat=len(keys)): + needs, event, env = fixture() + flags = dict(zip(keys, values)) + needs["changes"]["outputs"] = {key: str(value).lower() for key, value in flags.items()} + evidence = {item["lane"]: item["required"] for item in qualification.qualify(needs, event, env)["evidence"]} + with self.subTest(paths=flags): + self.assertEqual(evidence["clients.web_result"], flags["web"]) + self.assertEqual(evidence["mobile-swift-domain.mobile_swift_result"], flags["mobile"]) + self.assertEqual(evidence["postgres-domain.postgres_tests_result"], flags["rust"]) + self.assertEqual(evidence["desktop-domain.desktop_windows_result"], any(flags[key] for key in ("rust", "desktop", "desktop-rust"))) + + +if __name__ == "__main__": + unittest.main(verbosity=2)