diff --git a/scripts/lib/main-governance-audit.mjs b/scripts/lib/main-governance-audit.mjs index 3529ec11a..bdd28011b 100644 --- a/scripts/lib/main-governance-audit.mjs +++ b/scripts/lib/main-governance-audit.mjs @@ -8,7 +8,7 @@ export const REQUIRED_MAIN_CHECK_NAMES = Object.freeze([ ]); function normalized(value) { - return String(value ?? "").trim(); + return typeof value === "string" ? value.trim() : ""; } function positiveInteger(value) { @@ -33,6 +33,25 @@ function rulesOfType(rules, type) { return rules.filter((rule) => normalized(rule?.type) === type); } +function observedWorkflowControls(rules) { + return rulesOfType(rules, "workflows").flatMap((rule) => { + const workflows = ruleParameters(rule).workflows; + if (!Array.isArray(workflows)) { + return []; + } + return workflows.map((workflow) => ({ + repository_id: positiveInteger(workflow?.repository_id) + ? workflow.repository_id + : null, + path: normalized(workflow?.path) || "unknown", + ref: normalized(workflow?.ref) || "unknown", + ruleset_id: positiveInteger(rule?.ruleset_id) ? rule.ruleset_id : null, + ruleset_source_type: normalized(rule?.ruleset_source_type) || "unknown", + ruleset_source: normalized(rule?.ruleset_source) || "unknown", + })); + }); +} + export function evaluateMainGovernanceRules(rules) { const checks = []; const failures = []; @@ -44,7 +63,12 @@ export function evaluateMainGovernanceRules(rules) { false, "Active main rules must be supplied as an array.", ); - return { status: "FAIL", checks, failures }; + return { + status: "FAIL", + checks, + failures, + observed_controls: { required_workflows: [] }, + }; } const pullRequestRules = rulesOfType(rules, "pull_request"); @@ -190,5 +214,8 @@ export function evaluateMainGovernanceRules(rules) { status: failures.length === 0 ? "PASS" : "FAIL", checks, failures, + observed_controls: { + required_workflows: observedWorkflowControls(rules), + }, }; } diff --git a/scripts/main-governance-audit.mjs b/scripts/main-governance-audit.mjs index 3b8b19640..f9c91f580 100644 --- a/scripts/main-governance-audit.mjs +++ b/scripts/main-governance-audit.mjs @@ -182,6 +182,7 @@ function appendSummary(report) { `- Status: **${report.status}**`, `- Active rules: ${report.active_rule_count}`, `- Ruleset sources: ${report.rule_sources.length}`, + `- Observed required workflows: ${report.observed_controls.required_workflows.length}`, `- Failures: ${report.failures.length}`, ]; if (report.failures.length > 0) { @@ -204,6 +205,7 @@ function buildReport(repository, rules, evaluation) { active_rule_count: rules.length, active_rule_types: [...new Set(rules.map((rule) => bound(rule?.type, 100) || "unknown"))].sort(), rule_sources: collectRuleSources(rules), + observed_controls: evaluation.observed_controls, checks: evaluation.checks, failures: evaluation.failures, limitations: [ @@ -239,6 +241,7 @@ export function main() { active_rule_count: 0, active_rule_types: [], rule_sources: [], + observed_controls: { required_workflows: [] }, checks: [], failures: [ { diff --git a/test/main-governance-audit-script.test.ts b/test/main-governance-audit-script.test.ts index 483646fef..2f973cd53 100644 --- a/test/main-governance-audit-script.test.ts +++ b/test/main-governance-audit-script.test.ts @@ -102,6 +102,15 @@ describe("main governance audit GitHub adapter", () => { expect(script).not.toContain("JSON.stringify(process.env"); }); + it("propagates observed required-workflow evidence into the durable report and summary", () => { + const script = readFileSync("scripts/main-governance-audit.mjs", "utf8"); + + expect(script).toContain("observed_controls: evaluation.observed_controls"); + expect(script).toContain("Observed required workflows"); + expect(script).toContain("report.observed_controls.required_workflows.length"); + expect(script).toContain('observed_controls: { required_workflows: [] }'); + }); + it("fails closed when the capability, audit, or collection do not pass", () => { const script = readFileSync("scripts/main-governance-audit.mjs", "utf8"); diff --git a/test/main-governance-observed-workflows.test.ts b/test/main-governance-observed-workflows.test.ts new file mode 100644 index 000000000..70edc92a3 --- /dev/null +++ b/test/main-governance-observed-workflows.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import { evaluateMainGovernanceRules } from "../scripts/lib/main-governance-audit.mjs"; + +describe("observed main governance controls", () => { + it("records an enforced workflow without promoting missing target governance to PASS", () => { + const result = evaluateMainGovernanceRules([ + { + type: "workflows", + ruleset_id: 18_794_436, + ruleset_source_type: "Organization", + ruleset_source: "ContextualWisdomLab", + parameters: { + do_not_enforce_on_create: false, + workflows: [ + { + repository_id: 1_274_066_402, + path: ".github/workflows/security-scan.yml", + ref: "refs/heads/main", + }, + ], + }, + }, + ]); + + expect(result.status).toBe("FAIL"); + expect(result.observed_controls).toEqual({ + required_workflows: [ + { + repository_id: 1_274_066_402, + path: ".github/workflows/security-scan.yml", + ref: "refs/heads/main", + ruleset_id: 18_794_436, + ruleset_source_type: "Organization", + ruleset_source: "ContextualWisdomLab", + }, + ], + }); + expect(result.failures.map((failure) => failure.code)).toContain("pull_request_rule_missing"); + }); + + it("normalizes malformed workflow entries into bounded unknown evidence instead of inventing authority", () => { + const result = evaluateMainGovernanceRules([ + { + type: "workflows", + ruleset_id: 18_794_436, + ruleset_source_type: "Organization", + ruleset_source: "ContextualWisdomLab", + parameters: { workflows: [null, { repository_id: -1, path: "", ref: null }] }, + }, + ]); + + expect(result.observed_controls.required_workflows).toEqual([ + { + repository_id: null, + path: "unknown", + ref: "unknown", + ruleset_id: 18_794_436, + ruleset_source_type: "Organization", + ruleset_source: "ContextualWisdomLab", + }, + { + repository_id: null, + path: "unknown", + ref: "unknown", + ruleset_id: 18_794_436, + ruleset_source_type: "Organization", + ruleset_source: "ContextualWisdomLab", + }, + ]); + expect(result.status).toBe("FAIL"); + }); + + it("rejects object and array workflow identity fields instead of stringifying them", () => { + const result = evaluateMainGovernanceRules([ + { + type: "workflows", + ruleset_id: -1, + ruleset_source_type: { kind: "Organization" }, + ruleset_source: ["ContextualWisdomLab"], + parameters: { + workflows: [ + { + repository_id: -1, + path: { fake: ".github/workflows/security-scan.yml" }, + ref: ["refs/heads/main"], + }, + ], + }, + }, + ]); + + expect(result.observed_controls.required_workflows).toEqual([ + { + repository_id: null, + path: "unknown", + ref: "unknown", + ruleset_id: null, + ruleset_source_type: "unknown", + ruleset_source: "unknown", + }, + ]); + expect(result.status).toBe("FAIL"); + }); +});