From d1f42a1a834634c3f9adf4ebad3b940eb61ca577 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:51:57 +0200 Subject: [PATCH] =?UTF-8?q?feat(agent):=20grounded=20control=20checker=20?= =?UTF-8?q?=E2=80=94=20compose=20judge=20->=20ground=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GroundedControlChecker::check(spec, regions, repo_id) judges each candidate region and keeps only findings that survive the core grounding gate. Generic over the judge so tests drive it with a deterministic stub — the recognize->ground path is proven without an LLM (grounded snippet kept, ungrounded dropped, non-violation yields nothing). 2 lib tests. Co-Authored-By: Claude Opus 4.8 --- compliance-agent/src/controls/checker.rs | 114 +++++++++++++++++++++++ compliance-agent/src/controls/mod.rs | 2 + 2 files changed, 116 insertions(+) create mode 100644 compliance-agent/src/controls/checker.rs diff --git a/compliance-agent/src/controls/checker.rs b/compliance-agent/src/controls/checker.rs new file mode 100644 index 0000000..cd2c9ef --- /dev/null +++ b/compliance-agent/src/controls/checker.rs @@ -0,0 +1,114 @@ +//! The grounded control checker: judge each candidate region for a control, then +//! keep only the verdicts that survive the grounding gate. +//! +//! Generic over [`ControlJudge`] so tests drive it with a deterministic stub — +//! the whole recognize → ground path is then exercised without an LLM. With the +//! real judge, determinism comes from temperature 0 plus the gate. + +use compliance_core::control_check::{ground, CandidateRegion, ControlCheckSpec}; +use compliance_core::models::Finding; + +use super::judge::ControlJudge; + +/// Runs a [`ControlJudge`] over candidate regions and grounds the results. +pub struct GroundedControlChecker { + judge: J, +} + +impl GroundedControlChecker { + pub fn new(judge: J) -> Self { + Self { judge } + } + + /// Judge every candidate region for `spec` and return the grounded findings. + /// A verdict that doesn't quote real code in its region is dropped by + /// [`ground`], so nothing fabricated reaches the caller. + pub async fn check( + &self, + spec: &ControlCheckSpec, + regions: &[CandidateRegion], + repo_id: &str, + ) -> Vec { + let mut findings = Vec::new(); + for region in regions { + let verdict = self.judge.judge(spec, region).await; + if let Some(finding) = ground(spec, region, &verdict, repo_id) { + findings.push(finding); + } + } + findings + } +} + +#[cfg(test)] +mod tests { + use super::*; + use compliance_core::control_check::LlmVerdict; + use compliance_core::models::finding::Severity; + + /// Deterministic stub: returns a fixed verdict for every region, so the + /// recognize → ground composition is tested without an LLM. + struct StubJudge { + verdict: LlmVerdict, + } + + impl ControlJudge for StubJudge { + async fn judge(&self, _spec: &ControlCheckSpec, _region: &CandidateRegion) -> LlmVerdict { + self.verdict.clone() + } + } + + fn spec() -> ControlCheckSpec { + ControlCheckSpec { + control_id: "cra-ai-8".into(), + title: "No default passwords".into(), + requirement: "No default credentials".into(), + default_cwe: Some("CWE-798".into()), + severity: Severity::High, + } + } + + fn region(content: &str) -> CandidateRegion { + CandidateRegion { + file: "src/auth.py".into(), + start_line: 1, + content: content.into(), + } + } + + #[tokio::test] + async fn keeps_grounded_and_drops_ungrounded() { + let checker = GroundedControlChecker::new(StubJudge { + verdict: LlmVerdict { + violates: true, + snippet: "PASSWORD = \"admin\"".into(), + cwe: None, + confidence: 0.9, + }, + }); + let regions = vec![ + region("x = 1\nPASSWORD = \"admin\"\n"), // quotes real code → grounded + region("totally unrelated code\n"), // snippet absent → dropped + ]; + let findings = checker.check(&spec(), ®ions, "repo").await; + assert_eq!(findings.len(), 1); + assert_eq!(findings[0].rule_id.as_deref(), Some("cra-ai-8")); + assert_eq!(findings[0].line_number, Some(2)); + } + + #[tokio::test] + async fn non_violation_yields_nothing() { + let checker = GroundedControlChecker::new(StubJudge { + verdict: LlmVerdict { + violates: false, + snippet: String::new(), + cwe: None, + confidence: 0.0, + }, + }); + let findings = checker + .check(&spec(), &[region("PASSWORD = \"admin\"\n")], "repo") + .await; + assert!(findings.is_empty()); + } +} diff --git a/compliance-agent/src/controls/mod.rs b/compliance-agent/src/controls/mod.rs index 71f6b5a..417adb3 100644 --- a/compliance-agent/src/controls/mod.rs +++ b/compliance-agent/src/controls/mod.rs @@ -5,8 +5,10 @@ //! [`OscalControlsProvider`], which pulls breakpilot-compliance's OSCAL catalog //! and snapshots it locally. +mod checker; mod judge; mod oscal_provider; +pub use checker::GroundedControlChecker; pub use judge::{ControlJudge, LlmControlJudge, PROMPT_VERSION}; pub use oscal_provider::OscalControlsProvider;