|
| 1 | +//! SBPL preflight compiler for host-side diagnostics. |
| 2 | +//! |
| 3 | +//! This tool parses a PolicyWitness request JSON, extracts the SBPL policy, |
| 4 | +//! and runs sandbox_compile_string to catch syntax and parameter errors. |
| 5 | +
|
| 6 | +#[path = "../json_contract.rs"] |
| 7 | +#[allow(dead_code)] |
| 8 | +mod json_contract; |
| 9 | + |
| 10 | +use serde::Deserialize; |
| 11 | +use serde::Serialize; |
| 12 | +use sha2::{Digest, Sha256}; |
| 13 | +use std::collections::BTreeMap; |
| 14 | +use std::ffi::{CStr, CString}; |
| 15 | +use std::os::raw::{c_char, c_int, c_void}; |
| 16 | +use std::path::PathBuf; |
| 17 | + |
| 18 | +#[link(name = "sandbox")] |
| 19 | +unsafe extern "C" { |
| 20 | + fn sandbox_compile_string( |
| 21 | + profile: *const c_char, |
| 22 | + params: *const c_void, |
| 23 | + errorbuf: *mut *mut c_char, |
| 24 | + ) -> *mut c_void; |
| 25 | + fn sandbox_free_error(errorbuf: *mut c_char); |
| 26 | + fn sandbox_free_profile(profile: *mut c_void); |
| 27 | + fn sandbox_create_params() -> *mut c_void; |
| 28 | + fn sandbox_set_param(params: *mut c_void, key: *const c_char, value: *const c_char) |
| 29 | + -> c_int; |
| 30 | + fn sandbox_free_params(params: *mut c_void); |
| 31 | +} |
| 32 | + |
| 33 | +#[derive(Deserialize)] |
| 34 | +struct PreflightRequest { |
| 35 | + policy: PreflightPolicy, |
| 36 | +} |
| 37 | + |
| 38 | +#[derive(Deserialize)] |
| 39 | +struct PreflightPolicy { |
| 40 | + format: String, |
| 41 | + sbpl_source: Option<String>, |
| 42 | + params: Option<BTreeMap<String, String>>, |
| 43 | +} |
| 44 | + |
| 45 | +#[derive(Serialize)] |
| 46 | +struct PreflightData { |
| 47 | + policy_format: String, |
| 48 | + policy_sha256: Option<String>, |
| 49 | + params_present: bool, |
| 50 | + params_count: usize, |
| 51 | + compiled: bool, |
| 52 | + compile_error: Option<String>, |
| 53 | +} |
| 54 | + |
| 55 | +fn usage() -> String { |
| 56 | + "\ |
| 57 | +usage: |
| 58 | + sbpl-preflight --request <request.json> |
| 59 | +
|
| 60 | +notes: |
| 61 | + - reads request.json and compiles policy.sbpl_source |
| 62 | + - prints a JSON envelope with compile status and error details" |
| 63 | + .to_string() |
| 64 | +} |
| 65 | + |
| 66 | +fn sha256_hex(data: &str) -> String { |
| 67 | + let digest = Sha256::digest(data.as_bytes()); |
| 68 | + digest.iter().map(|b| format!("{b:02x}")).collect() |
| 69 | +} |
| 70 | + |
| 71 | +fn compile_sbpl(source: &str, params: Option<&BTreeMap<String, String>>) -> Result<(), String> { |
| 72 | + let mut param_cstrings: Vec<CString> = Vec::new(); |
| 73 | + let params_obj: *mut c_void = if let Some(params) = params { |
| 74 | + if params.is_empty() { |
| 75 | + std::ptr::null_mut() |
| 76 | + } else { |
| 77 | + let obj = unsafe { sandbox_create_params() }; |
| 78 | + if obj.is_null() { |
| 79 | + return Err("sandbox_create_params returned NULL".to_string()); |
| 80 | + } |
| 81 | + for (key, value) in params.iter() { |
| 82 | + let key_c = CString::new(key.as_str()) |
| 83 | + .map_err(|_| format!("invalid param key (NUL): {key}"))?; |
| 84 | + let value_c = CString::new(value.as_str()) |
| 85 | + .map_err(|_| format!("invalid param value for {key} (NUL)"))?; |
| 86 | + let rc = unsafe { |
| 87 | + sandbox_set_param(obj, key_c.as_ptr(), value_c.as_ptr()) |
| 88 | + }; |
| 89 | + if rc != 0 { |
| 90 | + unsafe { sandbox_free_params(obj) }; |
| 91 | + return Err(format!("sandbox_set_param failed for {key}: rc={rc}")); |
| 92 | + } |
| 93 | + param_cstrings.push(key_c); |
| 94 | + param_cstrings.push(value_c); |
| 95 | + } |
| 96 | + obj |
| 97 | + } |
| 98 | + } else { |
| 99 | + std::ptr::null_mut() |
| 100 | + }; |
| 101 | + |
| 102 | + let mut err_buf: *mut c_char = std::ptr::null_mut(); |
| 103 | + let profile = unsafe { |
| 104 | + let cstr = CString::new(source).map_err(|_| "sbpl_source contains NUL".to_string())?; |
| 105 | + sandbox_compile_string(cstr.as_ptr(), params_obj, &mut err_buf) |
| 106 | + }; |
| 107 | + |
| 108 | + if !err_buf.is_null() { |
| 109 | + let message = unsafe { |
| 110 | + let msg = CStr::from_ptr(err_buf).to_string_lossy().to_string(); |
| 111 | + sandbox_free_error(err_buf); |
| 112 | + msg |
| 113 | + }; |
| 114 | + if !profile.is_null() { |
| 115 | + unsafe { sandbox_free_profile(profile) }; |
| 116 | + } |
| 117 | + if !params_obj.is_null() { |
| 118 | + unsafe { sandbox_free_params(params_obj) }; |
| 119 | + } |
| 120 | + return Err(format!("sandbox_compile_string failed: {message}")); |
| 121 | + } |
| 122 | + |
| 123 | + if profile.is_null() { |
| 124 | + if !params_obj.is_null() { |
| 125 | + unsafe { sandbox_free_params(params_obj) }; |
| 126 | + } |
| 127 | + return Err("sandbox_compile_string failed (no profile and no error)".to_string()); |
| 128 | + } |
| 129 | + |
| 130 | + unsafe { sandbox_free_profile(profile) }; |
| 131 | + if !params_obj.is_null() { |
| 132 | + unsafe { sandbox_free_params(params_obj) }; |
| 133 | + } |
| 134 | + Ok(()) |
| 135 | +} |
| 136 | + |
| 137 | +fn main() { |
| 138 | + let args: Vec<String> = std::env::args().skip(1).collect(); |
| 139 | + if args.is_empty() { |
| 140 | + eprintln!("{}", usage()); |
| 141 | + std::process::exit(2); |
| 142 | + } |
| 143 | + |
| 144 | + let mut request_path: Option<PathBuf> = None; |
| 145 | + let mut idx = 0usize; |
| 146 | + while idx < args.len() { |
| 147 | + match args[idx].as_str() { |
| 148 | + "-h" | "--help" => { |
| 149 | + println!("{}", usage()); |
| 150 | + return; |
| 151 | + } |
| 152 | + "--request" => { |
| 153 | + if let Some(path) = args.get(idx + 1) { |
| 154 | + request_path = Some(PathBuf::from(path)); |
| 155 | + idx += 2; |
| 156 | + } else { |
| 157 | + eprintln!("missing value for --request"); |
| 158 | + eprintln!("{}", usage()); |
| 159 | + std::process::exit(2); |
| 160 | + } |
| 161 | + } |
| 162 | + other => { |
| 163 | + eprintln!("unknown argument: {other}\n\n{}", usage()); |
| 164 | + std::process::exit(2); |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + let request_path = match request_path { |
| 170 | + Some(path) => path, |
| 171 | + None => { |
| 172 | + eprintln!("missing --request\n\n{}", usage()); |
| 173 | + std::process::exit(2); |
| 174 | + } |
| 175 | + }; |
| 176 | + |
| 177 | + let text = match std::fs::read_to_string(&request_path) { |
| 178 | + Ok(text) => text, |
| 179 | + Err(err) => { |
| 180 | + eprintln!("failed to read request: {err}"); |
| 181 | + std::process::exit(2); |
| 182 | + } |
| 183 | + }; |
| 184 | + |
| 185 | + let parsed: PreflightRequest = match serde_json::from_str(&text) { |
| 186 | + Ok(req) => req, |
| 187 | + Err(err) => { |
| 188 | + eprintln!("failed to parse request.json: {err}"); |
| 189 | + std::process::exit(2); |
| 190 | + } |
| 191 | + }; |
| 192 | + |
| 193 | + let format = parsed.policy.format; |
| 194 | + let params = parsed.policy.params.as_ref(); |
| 195 | + |
| 196 | + if format != "sbpl" { |
| 197 | + let data = PreflightData { |
| 198 | + policy_format: format, |
| 199 | + policy_sha256: None, |
| 200 | + params_present: params.is_some(), |
| 201 | + params_count: params.map(|p| p.len()).unwrap_or(0), |
| 202 | + compiled: false, |
| 203 | + compile_error: Some("unsupported policy.format (expected sbpl)".to_string()), |
| 204 | + }; |
| 205 | + let result = json_contract::JsonResult { |
| 206 | + ok: false, |
| 207 | + rc: None, |
| 208 | + exit_code: Some(1), |
| 209 | + normalized_outcome: Some("unsupported_format".to_string()), |
| 210 | + errno: None, |
| 211 | + error: Some("unsupported policy.format (expected sbpl)".to_string()), |
| 212 | + stderr: None, |
| 213 | + stdout: None, |
| 214 | + }; |
| 215 | + let _ = json_contract::print_envelope("sbpl_preflight", result, &data); |
| 216 | + std::process::exit(1); |
| 217 | + } |
| 218 | + |
| 219 | + let source = match parsed.policy.sbpl_source.as_ref() { |
| 220 | + Some(src) => src, |
| 221 | + None => { |
| 222 | + let data = PreflightData { |
| 223 | + policy_format: format, |
| 224 | + policy_sha256: None, |
| 225 | + params_present: params.is_some(), |
| 226 | + params_count: params.map(|p| p.len()).unwrap_or(0), |
| 227 | + compiled: false, |
| 228 | + compile_error: Some("missing policy.sbpl_source".to_string()), |
| 229 | + }; |
| 230 | + let result = json_contract::JsonResult { |
| 231 | + ok: false, |
| 232 | + rc: None, |
| 233 | + exit_code: Some(1), |
| 234 | + normalized_outcome: Some("bad_policy".to_string()), |
| 235 | + errno: None, |
| 236 | + error: Some("missing policy.sbpl_source".to_string()), |
| 237 | + stderr: None, |
| 238 | + stdout: None, |
| 239 | + }; |
| 240 | + let _ = json_contract::print_envelope("sbpl_preflight", result, &data); |
| 241 | + std::process::exit(1); |
| 242 | + } |
| 243 | + }; |
| 244 | + |
| 245 | + let policy_sha = sha256_hex(source); |
| 246 | + let compile_result = compile_sbpl(source, params); |
| 247 | + match compile_result { |
| 248 | + Ok(()) => { |
| 249 | + let data = PreflightData { |
| 250 | + policy_format: format, |
| 251 | + policy_sha256: Some(policy_sha), |
| 252 | + params_present: params.is_some(), |
| 253 | + params_count: params.map(|p| p.len()).unwrap_or(0), |
| 254 | + compiled: true, |
| 255 | + compile_error: None, |
| 256 | + }; |
| 257 | + let result = json_contract::JsonResult { |
| 258 | + ok: true, |
| 259 | + rc: None, |
| 260 | + exit_code: Some(0), |
| 261 | + normalized_outcome: Some("ok".to_string()), |
| 262 | + errno: None, |
| 263 | + error: None, |
| 264 | + stderr: None, |
| 265 | + stdout: None, |
| 266 | + }; |
| 267 | + let _ = json_contract::print_envelope("sbpl_preflight", result, &data); |
| 268 | + std::process::exit(0); |
| 269 | + } |
| 270 | + Err(err) => { |
| 271 | + let data = PreflightData { |
| 272 | + policy_format: format, |
| 273 | + policy_sha256: Some(policy_sha), |
| 274 | + params_present: params.is_some(), |
| 275 | + params_count: params.map(|p| p.len()).unwrap_or(0), |
| 276 | + compiled: false, |
| 277 | + compile_error: Some(err.clone()), |
| 278 | + }; |
| 279 | + let result = json_contract::JsonResult { |
| 280 | + ok: false, |
| 281 | + rc: None, |
| 282 | + exit_code: Some(1), |
| 283 | + normalized_outcome: Some("compile_error".to_string()), |
| 284 | + errno: None, |
| 285 | + error: Some(err), |
| 286 | + stderr: None, |
| 287 | + stdout: None, |
| 288 | + }; |
| 289 | + let _ = json_contract::print_envelope("sbpl_preflight", result, &data); |
| 290 | + std::process::exit(1); |
| 291 | + } |
| 292 | + } |
| 293 | +} |
0 commit comments