Skip to content

Commit ff5504e

Browse files
committed
实现 Gradle 原生模型局部采集并验收 Groovy 与 Kotlin DSL
1 parent 0e5f417 commit ff5504e

14 files changed

Lines changed: 1467 additions & 0 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// 只观察模型,不调用质量任务、不授予检查通过;由Rust固定脚本字节和输出位置。
2+
import groovy.json.JsonOutput
3+
4+
gradle.projectsEvaluated {
5+
def root = gradle.rootProject
6+
def taskName = 'codeguardObserveCheckerModel'
7+
if (root.tasks.findByName(taskName) != null) {
8+
throw new GradleException('codeguard_reserved_task_collision')
9+
}
10+
root.tasks.register(taskName) {
11+
doLast {
12+
def knownPlugins = ['java', 'java-library', 'org.owasp.dependencycheck', 'checkstyle', 'pmd', 'com.github.spotbugs']
13+
def knownTypes = ['org.owasp.dependencycheck.gradle.tasks.Analyze', 'org.owasp.dependencycheck.gradle.tasks.Aggregate', 'org.gradle.api.tasks.javadoc.Javadoc', 'org.gradle.api.plugins.quality.Checkstyle', 'org.gradle.api.plugins.quality.Pmd', 'com.github.spotbugs.snom.SpotBugsTask']
14+
def base = root.projectDir.canonicalFile.toPath()
15+
def relative = { File file ->
16+
def path = file.canonicalFile.toPath()
17+
if (!path.startsWith(base)) { throw new GradleException('codeguard_model_directory_outside_root') }
18+
def value = base.relativize(path).toString().replace(File.separatorChar, '/' as char)
19+
value.isEmpty() ? '.' : value
20+
}
21+
if (root.allprojects.size() > 128) { throw new GradleException('codeguard_model_project_budget') }
22+
def projects = root.allprojects.sort { a, b -> a.path <=> b.path }.collect { project ->
23+
def tasks = []
24+
project.tasks.each { task ->
25+
def implementation = task.class.name
26+
def matched = false
27+
def type = task.class
28+
while (type != null) {
29+
if (knownTypes.contains(type.name)) { implementation = type.name; matched = true; break }
30+
type = type.superclass
31+
}
32+
def relevantName = task.name == 'javadoc' || task.name.startsWith('dependencyCheck') || task.name.startsWith('checkstyle') || task.name.startsWith('pmd') || task.name.startsWith('spotbugs')
33+
if (matched || relevantName) { tasks.add([name: task.name, implementation: implementation, enabled: task.enabled]) }
34+
if (tasks.size() > 64) { throw new GradleException('codeguard_model_task_budget') }
35+
}
36+
[path: project.path, project_dir: relative(project.projectDir), build_dir: relative(project.layout.buildDirectory.get().asFile), plugins: knownPlugins.findAll { project.plugins.hasPlugin(it) }, tasks: tasks.sort { a, b -> a.name <=> b.name }]
37+
}
38+
def model = [schema_version: '0.1.0', report_type: 'gradle_checker_model', gradle_version: gradle.gradleVersion, included_build_count: gradle.includedBuilds.size(), projects: projects]
39+
def bytes = JsonOutput.toJson(model).getBytes('UTF-8')
40+
if (bytes.length > 1024 * 1024) { throw new GradleException('codeguard_model_output_budget') }
41+
def output = new File(System.getProperty('codeguard.model.output'))
42+
if (!output.createNewFile()) { throw new GradleException('codeguard_model_output_already_exists') }
43+
output.bytes = bytes
44+
}
45+
}
46+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//! 调用已有Gradle观察选定构建输入的原生模型;配置执行不等于质量检查。
2+
use std::{
3+
collections::BTreeMap,
4+
ffi::OsString,
5+
fs,
6+
os::unix::fs::PermissionsExt,
7+
path::PathBuf,
8+
sync::atomic::{AtomicBool, AtomicU64, Ordering},
9+
time::{Instant, SystemTime, UNIX_EPOCH},
10+
};
11+
use codeguard_adapters::parse_gradle_checker_model;
12+
use codeguard_runtime::{
13+
ProcessSpec, SourceSnapshot, Termination, read_bounded_regular_file, run_process_recorded,
14+
};
15+
use serde_json::{Value, json};
16+
use sha2::{Digest, Sha256};
17+
use crate::tool_identity::hash_bundle_tree;
18+
pub use crate::gradle_model_probe_request::Request;
19+
const SCRIPT: &str = include_str!("../resources/gradle_checker_model.init.gradle");
20+
static NEXT_SCRATCH: AtomicU64 = AtomicU64::new(0);
21+
struct Scratch(PathBuf);
22+
impl Drop for Scratch {
23+
fn drop(&mut self) {
24+
let _ = fs::remove_dir_all(&self.0);
25+
}
26+
}
27+
28+
/// 在私有副本中离线采集模型,返回脱敏局部观察;取消/失败保持未完成,不生成源码违规。
29+
/// 参数含显式工具和选定文件及取消信号;返回值不授予项目覆盖、漏洞清洁或交付结论。
30+
pub fn observe(request: &Request, cancelled: &AtomicBool) -> Value {
31+
let mut report = json!({"schema_version":"0.1.0","report_type":"gradle_model_probe","native_status":"incomplete","reason":"prerequisites_missing","model":null,"input_scope":"selected_project_files","input_files":[],"source_snapshot_sha256":null,"gradle_bundle_sha256":null,"java_entry_sha256":null,"jdk_release_sha256":null,"init_script_sha256":digest(SCRIPT.as_bytes()),"native_report_sha256":null,"jdk_closure_verified":false,"project_sources_complete":false,"coverage_proven":false,"authority":"local_unverified","delivery_decision":"not_evaluated"});
32+
macro_rules! fail {
33+
($why:expr) => {{
34+
report["reason"] = json!($why);
35+
return report;
36+
}};
37+
}
38+
if cancelled.load(Ordering::Relaxed) || codeguard_runtime::sigint_cancellation_requested() {
39+
fail!("request_cancelled");
40+
}
41+
if Instant::now() >= request.deadline {
42+
fail!("request_deadline_exceeded");
43+
}
44+
let has_settings = ["settings.gradle", "settings.gradle.kts"]
45+
.iter()
46+
.any(|p| request.project_files.contains(&PathBuf::from(p)));
47+
let has_build = ["build.gradle", "build.gradle.kts"]
48+
.iter()
49+
.any(|p| request.project_files.contains(&PathBuf::from(p)));
50+
if !has_settings || !has_build {
51+
fail!("root_build_inputs_missing");
52+
}
53+
let Ok(snapshot) = SourceSnapshot::capture(
54+
&request.project_root,
55+
request.project_files.iter().cloned(),
56+
128,
57+
1024 * 1024,
58+
16 * 1024 * 1024,
59+
) else {
60+
fail!("project_snapshot_unavailable");
61+
};
62+
let files = snapshot
63+
.files()
64+
.iter()
65+
.map(|(path, bytes)| json!({"path":path,"sha256":digest(bytes)}))
66+
.collect::<Vec<_>>();
67+
report["source_snapshot_sha256"] = json!(digest(
68+
&serde_json::to_vec(&files).expect("文件摘要可序列化")
69+
));
70+
report["input_files"] = json!(files);
71+
let (Ok(bundle), Ok(java_home)) = (
72+
request.gradle_bundle.canonicalize(),
73+
request.java_home.canonicalize(),
74+
) else {
75+
fail!("native_prerequisite_unavailable");
76+
};
77+
let tool = bundle.join("bin/gradle");
78+
let java = java_home.join("bin/java");
79+
let release = java_home.join("release");
80+
let (Ok(bundle_hash), Ok(java_bytes), Ok(release_bytes)) = (
81+
hash_bundle_tree(&bundle),
82+
read_bounded_regular_file(&java, 128 * 1024 * 1024),
83+
read_bounded_regular_file(&release, 64 * 1024),
84+
) else {
85+
fail!("native_tool_identity_unavailable");
86+
};
87+
if release_bytes.is_empty() {
88+
fail!("jdk_version_unverified");
89+
}
90+
report["gradle_bundle_sha256"] = json!(bundle_hash);
91+
report["java_entry_sha256"] = json!(digest(&java_bytes));
92+
report["jdk_release_sha256"] = json!(digest(&release_bytes));
93+
if Instant::now() >= request.deadline {
94+
fail!("request_deadline_exceeded");
95+
}
96+
let Ok(scratch) = private_scratch() else {
97+
fail!("private_workspace_unavailable");
98+
};
99+
let project = scratch.0.join("project");
100+
let evidence = scratch.0.join("evidence");
101+
let home = scratch.0.join("home");
102+
let script = scratch.0.join("model.init.gradle");
103+
let output = evidence.join("model.json");
104+
if snapshot.materialize_new(&project).is_err()
105+
|| fs::create_dir(&evidence).is_err()
106+
|| fs::create_dir(&home).is_err()
107+
|| fs::set_permissions(&evidence, fs::Permissions::from_mode(0o700)).is_err()
108+
|| fs::set_permissions(&home, fs::Permissions::from_mode(0o700)).is_err()
109+
|| fs::write(&script, SCRIPT).is_err()
110+
{
111+
fail!("private_workspace_unavailable");
112+
}
113+
let mut env = BTreeMap::new();
114+
env.insert(
115+
OsString::from("JAVA_HOME"),
116+
java_home.as_os_str().to_owned(),
117+
);
118+
env.insert(OsString::from("HOME"), home.as_os_str().to_owned());
119+
env.insert(OsString::from("PATH"), OsString::from("/usr/bin:/bin"));
120+
let spec = ProcessSpec {
121+
executable: tool,
122+
args: vec![
123+
"--offline".into(),
124+
"--no-daemon".into(),
125+
"--console=plain".into(),
126+
"--no-configuration-cache".into(),
127+
"--init-script".into(),
128+
script.clone().into_os_string(),
129+
"--project-dir".into(),
130+
project.clone().into_os_string(),
131+
"--gradle-user-home".into(),
132+
home.join(".gradle").into_os_string(),
133+
format!("-Duser.home={}", home.display()).into(),
134+
format!("-Dcodeguard.model.output={}", output.display()).into(),
135+
":codeguardObserveCheckerModel".into(),
136+
],
137+
cwd: project.clone(),
138+
env,
139+
stdin: None,
140+
deadline: request.deadline,
141+
output_limit_bytes: 2 * 1024 * 1024,
142+
};
143+
let Ok(outcome) = run_process_recorded(&spec, cancelled, &evidence, "gradle.log") else {
144+
if cancelled.load(Ordering::Relaxed) || codeguard_runtime::sigint_cancellation_requested() {
145+
fail!("request_cancelled");
146+
}
147+
if Instant::now() >= request.deadline {
148+
fail!("request_deadline_exceeded");
149+
}
150+
fail!("native_process_incomplete");
151+
};
152+
if cancelled.load(Ordering::Relaxed) || codeguard_runtime::sigint_cancellation_requested() {
153+
fail!("request_cancelled");
154+
}
155+
if Instant::now() >= request.deadline {
156+
fail!("request_deadline_exceeded");
157+
}
158+
if snapshot.verify_unchanged(&project).ok() != Some(true)
159+
|| read_bounded_regular_file(&script, 64 * 1024)
160+
.ok()
161+
.as_deref()
162+
!= Some(SCRIPT.as_bytes())
163+
{
164+
fail!("native_inputs_changed_during_probe");
165+
}
166+
if hash_bundle_tree(&bundle).ok().as_deref() != Some(bundle_hash.as_str())
167+
|| read_bounded_regular_file(&java, 128 * 1024 * 1024)
168+
.ok()
169+
.as_deref()
170+
!= Some(java_bytes.as_slice())
171+
|| read_bounded_regular_file(&release, 64 * 1024)
172+
.ok()
173+
.as_deref()
174+
!= Some(release_bytes.as_slice())
175+
{
176+
fail!("native_identity_changed_during_probe");
177+
}
178+
if !matches!(outcome.termination, Termination::Exited(0)) {
179+
fail!("native_execution_incomplete");
180+
}
181+
let Ok(bytes) = read_bounded_regular_file(&output, 1024 * 1024) else {
182+
fail!("native_model_missing");
183+
};
184+
let Ok(model) = parse_gradle_checker_model(&bytes) else {
185+
fail!("native_model_invalid_or_incomplete");
186+
};
187+
if cancelled.load(Ordering::Relaxed) || codeguard_runtime::sigint_cancellation_requested() {
188+
fail!("request_cancelled");
189+
}
190+
if Instant::now() >= request.deadline {
191+
fail!("request_deadline_exceeded");
192+
}
193+
report["native_status"] = json!("model_observed_unverified");
194+
report["reason"] = json!("selected_inputs_and_jdk_closure_unverified");
195+
report["model"] = json!(model);
196+
report["native_report_sha256"] = json!(digest(&bytes));
197+
report
198+
}
199+
fn digest(bytes: &[u8]) -> String {
200+
format!("{:x}", Sha256::digest(bytes))
201+
}
202+
fn private_scratch() -> Result<Scratch, ()> {
203+
let root = std::env::temp_dir().canonicalize().map_err(|_| ())?;
204+
let nonce = SystemTime::now()
205+
.duration_since(UNIX_EPOCH)
206+
.map_err(|_| ())?
207+
.as_nanos();
208+
let path = root.join(format!(
209+
"codeguard-gradle-model-{}-{nonce}-{}",
210+
std::process::id(),
211+
NEXT_SCRATCH.fetch_add(1, Ordering::Relaxed)
212+
));
213+
fs::create_dir(&path).map_err(|_| ())?;
214+
let scratch = Scratch(path);
215+
fs::set_permissions(&scratch.0, fs::Permissions::from_mode(0o700)).map_err(|_| ())?;
216+
Ok(scratch)
217+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! 有界Gradle原生模型采集请求。
2+
use std::{collections::BTreeSet, path::PathBuf, time::Instant};
3+
/// 调用方显式提供选定项目文件、已有工具和截止时间;不代表完整构建范围。
4+
pub struct Request {
5+
/// 当前受检构建根。
6+
pub project_root: PathBuf,
7+
/// 相对构建根的选定输入,必须包含根settings及build脚本。
8+
pub project_files: BTreeSet<PathBuf>,
9+
/// 已安装Gradle分发目录,使用bin/gradle并核对整个目录字节。
10+
pub gradle_bundle: PathBuf,
11+
/// 已安装JDK目录,核对java入口和release;完整JDK尚未资格验收。
12+
pub java_home: PathBuf,
13+
/// 统一执行截止时间。
14+
pub deadline: Instant,
15+
}

‎crates/codeguard-cli/src/lib.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
//! CLI 共享的 Rust 开发期验收与命令应用服务入口。
22
3+
#[cfg(unix)]
4+
pub mod gradle_model_probe;
5+
#[cfg(unix)]
6+
pub mod gradle_model_probe_request;
7+
38
pub mod agents_block;
49
pub mod language_alias;
510
mod command_catalogue;

0 commit comments

Comments
 (0)