|
| 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 | +} |
0 commit comments