From 175514f177f0923718c6c0a33aeb3a88bdb02299 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Sun, 20 Sep 2026 17:56:35 +0200 Subject: [PATCH 1/3] add support for capturing and tracing HTTP request content and bodies --- Cargo.lock | 1 + crates/machine/Cargo.toml | 1 + crates/machine/src/trace/http.rs | 360 +++++++++++++++++-- crates/machine/src/trace/mod.rs | 17 +- crates/machine/src/virtio/slirp/mod.rs | 118 ++++++ crates/machine/src/virtio/tls_proxy/tests.rs | 43 +++ crates/wasi-component/src/api/session.rs | 1 + crates/wasi-component/vpod.wit | 1 + sdks/python/vpod/trace.py | 20 +- sdks/typescript/src/trace.ts | 23 +- 10 files changed, 552 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb67b220..0ace7996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1758,6 +1758,7 @@ dependencies = [ name = "machine" version = "0.0.0" dependencies = [ + "base64 0.22.1", "const-oid", "der", "dirs 6.0.0", diff --git a/crates/machine/Cargo.toml b/crates/machine/Cargo.toml index a7a0efe7..d0167de0 100644 --- a/crates/machine/Cargo.toml +++ b/crates/machine/Cargo.toml @@ -8,6 +8,7 @@ riscv-core = { path = "../riscv-core" } log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" +base64 = "0.22" dirs = "6" rustls = { version = "0.23", default-features = false, features = ["std", "tls12"] } rustls-rustcrypto = "0.0.2-alpha" diff --git a/crates/machine/src/trace/http.rs b/crates/machine/src/trace/http.rs index 66049867..5b7ef553 100644 --- a/crates/machine/src/trace/http.rs +++ b/crates/machine/src/trace/http.rs @@ -4,10 +4,26 @@ use super::Tracer; const MAX_HEAD_BYTES: usize = 16 * 1024; +pub const MAX_BODY_BYTES: usize = 64 * 1024; + #[derive(Debug, PartialEq, Eq)] pub struct HttpRequest { pub method: String, pub url: String, + pub headers: Vec<(String, String)>, + pub body_bytes: Option, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct HttpBody { + pub content: Vec, + pub truncated: bool, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum HttpEvent { + Head(HttpRequest), + Body(HttpBody), } pub struct HttpRequests { @@ -16,6 +32,9 @@ pub struct HttpRequests { head: Vec, body_bytes_left: u64, stopped: bool, + capture_bodies: bool, + body: Vec, + body_truncated: bool, } impl HttpRequests { @@ -26,24 +45,35 @@ impl HttpRequests { head: Vec::new(), body_bytes_left: 0, stopped: false, + capture_bodies: false, + body: Vec::new(), + body_truncated: false, } } + pub fn capture_bodies(&mut self) { + self.capture_bodies = true; + } + pub fn set_default_host(&mut self, host: &str) { if self.default_host.is_none() { self.default_host = Some(host.to_string()); } } - pub fn observe(&mut self, bytes: &[u8]) -> Vec { - let mut requests = Vec::new(); + pub fn observe(&mut self, bytes: &[u8]) -> Vec { + let mut events = Vec::new(); if self.stopped { - return requests; + return events; } - let skipped = self.body_bytes_left.min(bytes.len() as u64); - self.body_bytes_left -= skipped; - let bytes = &bytes[skipped as usize..]; + let taken = self.body_bytes_left.min(bytes.len() as u64); + self.keep_body(&bytes[..taken as usize]); + self.body_bytes_left -= taken; + if taken > 0 && self.body_bytes_left == 0 { + events.extend(self.finish_body()); + } + let bytes = &bytes[taken as usize..]; let mut searched_to = self.head.len().saturating_sub(3); self.head.extend_from_slice(bytes); @@ -62,20 +92,54 @@ impl HttpRequests { match parsed { Some((request, Body::Length(length))) => { - requests.push(request); - let skipped = length.min(self.head.len() as u64); - self.head.drain(..skipped as usize); - self.body_bytes_left = length - skipped; + events.push(HttpEvent::Head(request)); + + let taken = length.min(self.head.len() as u64); + let body: Vec = self.head.drain(..taken as usize).collect(); + self.keep_body(&body); + self.body_bytes_left = length - taken; + + if self.body_bytes_left == 0 { + events.extend(self.finish_body()); + } } Some((request, Body::Undelimited)) => { - requests.push(request); + // No declared length, so there is no way to know where this + // body ends or the next head begins. The head says so with a + // null length and parsing stops here. + events.push(HttpEvent::Head(request)); self.stop(); } None => self.stop(), } } - requests + events + } + + fn keep_body(&mut self, bytes: &[u8]) { + if !self.capture_bodies || bytes.is_empty() { + return; + } + + let room = MAX_BODY_BYTES.saturating_sub(self.body.len()); + if bytes.len() > room { + self.body_truncated = true; + } + self.body.extend_from_slice(&bytes[..room.min(bytes.len())]); + } + + fn finish_body(&mut self) -> Option { + if !self.capture_bodies || (self.body.is_empty() && !self.body_truncated) { + return None; + } + + let body = HttpBody { + content: std::mem::take(&mut self.body), + truncated: std::mem::take(&mut self.body_truncated), + }; + + Some(HttpEvent::Body(body)) } fn stop(&mut self) { @@ -101,11 +165,25 @@ impl HttpRequests { let mut host = None; let mut body = Body::Length(0); + let mut headers: Vec<(String, String)> = Vec::new(); + for line in lines { let Some((name, value)) = line.split_once(':') else { continue; }; let value = value.trim(); + + match headers + .iter_mut() + .find(|(seen, _)| seen.eq_ignore_ascii_case(name)) + { + Some((_, existing)) => { + existing.push_str(", "); + existing.push_str(value); + } + None => headers.push((name.to_string(), value.to_string())), + } + if name.eq_ignore_ascii_case("host") { host = Some(value.to_string()); } else if name.eq_ignore_ascii_case("content-length") { @@ -131,6 +209,11 @@ impl HttpRequests { HttpRequest { method: method.to_string(), url, + headers, + body_bytes: match body { + Body::Length(length) => Some(length), + Body::Undelimited => None, + }, }, body, )) @@ -153,15 +236,27 @@ pub struct HttpObserver { requests: HttpRequests, address: [u8; 4], port: u16, + content: bool, + head_seq: Option, + head_url: Option, } impl HttpObserver { pub fn new(tracer: Tracer, scheme: &'static str, address: [u8; 4], port: u16) -> Self { + let content = tracer.traces_request_content(); + let mut requests = HttpRequests::new(scheme, None); + if content { + requests.capture_bodies(); + } + Self { tracer, - requests: HttpRequests::new(scheme, None), + requests, address, port, + content, + head_seq: None, + head_url: None, } } @@ -170,17 +265,67 @@ impl HttpObserver { } pub fn observe(&mut self, bytes: &[u8]) { - for request in self.requests.observe(bytes) { - self.tracer.record( - "net.http", - &[ - ("protocol", Value::from(self.requests.scheme)), - ("method", request.method.into()), - ("url", request.url.into()), - ("address", super::format_address(self.address).into()), - ("port", self.port.into()), - ], - ); + for event in self.requests.observe(bytes) { + match event { + HttpEvent::Head(request) => { + let url = request.url.clone(); + let mut fields = vec![ + ("protocol", Value::from(self.requests.scheme)), + ("method", request.method.into()), + ("url", request.url.into()), + ("address", super::format_address(self.address).into()), + ("port", self.port.into()), + ]; + + if self.content { + let headers: serde_json::Map = request + .headers + .into_iter() + .map(|(name, value)| (name, Value::from(value))) + .collect(); + + fields.push(("headers", Value::Object(headers))); + fields.push(( + "body_bytes", + match request.body_bytes { + Some(length) => Value::from(length), + None => Value::Null, + }, + )); + } + + self.head_seq = self.tracer.record_seq("net.http", &fields); + self.head_url = Some(url); + } + + HttpEvent::Body(body) => { + let (Some(head_seq), Some(url)) = (self.head_seq, self.head_url.clone()) else { + continue; + }; + + let (encoding, content) = match String::from_utf8(body.content) { + Ok(text) => ("utf8", text), + Err(raw) => ( + "base64", + base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + raw.as_bytes(), + ), + ), + }; + + self.tracer.record( + "net.http.body", + &[ + ("url", url.into()), + ("request_seq", head_seq.into()), + ("encoding", encoding.into()), + ("truncated", body.truncated.into()), + ("content", content.into()), + ], + ); + } + } } } } @@ -189,13 +334,176 @@ impl HttpObserver { mod tests { use super::*; - fn urls(requests: Vec) -> Vec { - requests + fn urls(events: Vec) -> Vec { + heads(events) .into_iter() .map(|request| format!("{} {}", request.method, request.url)) .collect() } + fn heads(events: Vec) -> Vec { + events + .into_iter() + .filter_map(|event| match event { + HttpEvent::Head(request) => Some(request), + HttpEvent::Body(_) => None, + }) + .collect() + } + + fn bodies(events: Vec) -> Vec { + events + .into_iter() + .filter_map(|event| match event { + HttpEvent::Body(body) => Some(body), + HttpEvent::Head(_) => None, + }) + .collect() + } + + fn capturing() -> HttpRequests { + let mut requests = HttpRequests::new("https", Some("api.example.com".to_string())); + requests.capture_bodies(); + requests + } + + fn post(body: &str) -> Vec { + format!( + "POST /v1/messages HTTP/1.1\r\nHost: api.example.com\r\n\ + x-api-key: vpod-secret-key-a1b2c3d4\r\nContent-Type: application/json\r\n\ + Content-Length: {}\r\n\r\n{body}", + body.len() + ) + .into_bytes() + } + + #[test] + fn the_headers_a_request_carried_are_kept_in_order() { + let mut requests = capturing(); + let head = heads(requests.observe(&post("{}"))).remove(0); + + assert_eq!( + head.headers, + vec![ + ("Host".to_string(), "api.example.com".to_string()), + ( + "x-api-key".to_string(), + "vpod-secret-key-a1b2c3d4".to_string() + ), + ("Content-Type".to_string(), "application/json".to_string()), + ("Content-Length".to_string(), "2".to_string()), + ] + ); + } + + #[test] + fn a_header_sent_twice_is_joined_the_way_http_says_it_means() { + let mut requests = capturing(); + let wire = + b"GET /x HTTP/1.1\r\nHost: h\r\nAccept: a\r\nAccept: b\r\nContent-Length: 0\r\n\r\n"; + + let head = heads(requests.observe(wire)).remove(0); + let accept = head + .headers + .iter() + .find(|(name, _)| name == "Accept") + .expect("the header was dropped"); + + assert_eq!(accept.1, "a, b"); + } + + #[test] + fn a_body_under_the_cap_arrives_whole() { + let mut requests = capturing(); + let events = requests.observe(&post(r#"{"model":"claude"}"#)); + let body = bodies(events).remove(0); + + assert_eq!(body.content, br#"{"model":"claude"}"#); + assert!(!body.truncated); + } + + #[test] + fn a_body_over_the_cap_is_cut_and_says_so() { + let mut requests = capturing(); + let long = "x".repeat(MAX_BODY_BYTES + 100); + let body = bodies(requests.observe(&post(&long))).remove(0); + + assert_eq!(body.content.len(), MAX_BODY_BYTES); + assert!(body.truncated, "an oversized body did not say it was cut"); + } + + #[test] + fn a_body_split_across_writes_is_reassembled() { + let mut requests = capturing(); + let wire = post(r#"{"a":1,"b":2}"#); + + let mut events = Vec::new(); + for byte in &wire { + events.extend(requests.observe(std::slice::from_ref(byte))); + } + + let body = bodies(events).remove(0); + assert_eq!(body.content, br#"{"a":1,"b":2}"#); + } + + #[test] + fn the_head_is_emitted_before_its_body() { + let mut requests = capturing(); + let events = requests.observe(&post("{}")); + + assert!(matches!(events[0], HttpEvent::Head(_))); + assert!(matches!(events[1], HttpEvent::Body(_))); + assert_eq!(events.len(), 2); + } + + #[test] + fn a_request_with_no_body_produces_no_body_event() { + let mut requests = capturing(); + let events = requests.observe(b"GET /x HTTP/1.1\r\nHost: h\r\nContent-Length: 0\r\n\r\n"); + + assert_eq!(bodies(events).len(), 0); + } + + #[test] + fn a_second_request_on_one_connection_gets_its_own_pair() { + let mut requests = capturing(); + let mut wire = post(r#"{"n":1}"#); + wire.extend(post(r#"{"n":2}"#)); + + let events = requests.observe(&wire); + let captured = bodies(events); + + assert_eq!(captured.len(), 2, "keep-alive lost the second body"); + assert_eq!(captured[0].content, br#"{"n":1}"#); + assert_eq!(captured[1].content, br#"{"n":2}"#); + } + + #[test] + fn a_body_with_no_declared_length_is_not_captured() { + let mut requests = capturing(); + let wire = b"POST /x HTTP/1.1\r\nHost: h\r\nTransfer-Encoding: chunked\r\n\r\n7\r\nsecret\n\r\n0\r\n\r\n"; + + let events = requests.observe(wire); + let heads_seen = heads(events); + + assert_eq!(heads_seen.len(), 1); + assert_eq!( + heads_seen[0].body_bytes, None, + "an undelimited body claimed a length" + ); + } + + #[test] + fn nothing_is_kept_unless_capture_was_asked_for() { + let mut requests = HttpRequests::new("https", Some("api.example.com".to_string())); + let events = requests.observe(&post(r#"{"secret":"value"}"#)); + + assert_eq!( + bodies(events).len(), + 0, + "a body was kept without being asked for" + ); + } #[test] fn a_request_split_across_writes_is_seen_once() { let mut requests = HttpRequests::new("https", None); diff --git a/crates/machine/src/trace/mod.rs b/crates/machine/src/trace/mod.rs index 48861ad7..6920f569 100644 --- a/crates/machine/src/trace/mod.rs +++ b/crates/machine/src/trace/mod.rs @@ -23,6 +23,7 @@ pub struct TraceOptions { pub files: bool, pub network: bool, pub mounts: bool, + pub request_content: bool, pub buffer_bytes: usize, } @@ -33,6 +34,7 @@ impl Default for TraceOptions { files: true, network: true, mounts: true, + request_content: false, buffer_bytes: DEFAULT_BUFFER_BYTES, } } @@ -84,6 +86,10 @@ impl Tracer { self.options.network } + pub fn traces_request_content(&self) -> bool { + self.options.network && self.options.request_content + } + pub fn traces_mounts(&self) -> bool { self.options.mounts } @@ -103,6 +109,10 @@ impl Tracer { } pub fn record(&self, kind: &str, fields: &[(&str, Value)]) { + self.record_seq(kind, fields); + } + + pub fn record_seq(&self, kind: &str, fields: &[(&str, Value)]) -> Option { let wall_ms = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|elapsed| elapsed.as_millis() as u64) @@ -117,18 +127,21 @@ impl Tracer { if recorder.buffered_bytes + marker.len() > capacity { recorder.dropped += 1; - return; + return None; } recorder.dropped = 0; recorder.push(marker); } + let seq = recorder.next_seq; let line = recorder.line(kind, wall_ms, fields); if recorder.buffered_bytes + line.len() > capacity { recorder.dropped += 1; - return; + return None; } recorder.push(line); + + Some(seq) } pub fn drain(&self, max_bytes: usize) -> Vec { diff --git a/crates/machine/src/virtio/slirp/mod.rs b/crates/machine/src/virtio/slirp/mod.rs index fcde91e7..a2f6181e 100644 --- a/crates/machine/src/virtio/slirp/mod.rs +++ b/crates/machine/src/virtio/slirp/mod.rs @@ -369,6 +369,124 @@ mod tests { assert_eq!(events[1]["failed"], false); } + #[test] + fn a_traced_request_can_carry_its_headers_and_body() { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + + let guest_mac = [0x02, 0, 0, 0, 0, 0x1a]; + let mut slirp = SlirpBackend::new(guest_mac); + let tracer = crate::trace::Tracer::new(crate::trace::TraceOptions { + request_content: true, + ..crate::trace::TraceOptions::default() + }); + tracer.remember_name([127, 0, 0, 1], "local.test"); + slirp.set_tracer(Some(tracer.clone())); + + let ends = from_guest(guest_mac, [127, 0, 0, 1], 45006, port); + let guest_isn = 5000u32; + let (mut upstream, ack) = connect(&mut slirp, &listener, &ends, guest_isn); + + let body = r#"{"model":"claude","prompt":"hello"}"#; + let request = format!( + "POST /v1/messages HTTP/1.1\r\nHost: local.test\r\n\ + x-api-key: vpod-secret-key-a1b2c3d4\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + slirp.send(&make_tcp_frame( + &ends, + guest_isn.wrapping_add(1), + ack, + ACK, + request.as_bytes(), + )); + assert_eq!( + read_upstream(&mut upstream, &mut slirp, request.len()), + request.as_bytes() + ); + + slirp.send(&make_tcp_frame( + &ends, + guest_isn.wrapping_add(1), + ack, + RST, + &[], + )); + + let events: Vec = String::from_utf8(tracer.drain(usize::MAX)) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect(); + let kinds: Vec<&str> = events + .iter() + .map(|event| event["kind"].as_str().unwrap()) + .collect(); + assert_eq!(kinds, ["net.http", "net.http.body", "net.flow"]); + + let head = &events[0]; + assert_eq!(head["method"], "POST"); + assert_eq!(head["headers"]["Host"], "local.test"); + assert_eq!(head["headers"]["x-api-key"], "vpod-secret-key-a1b2c3d4"); + assert_eq!(head["body_bytes"], body.len()); + + let captured = &events[1]; + assert_eq!(captured["request_seq"], head["seq"]); + assert_eq!(captured["url"], head["url"]); + assert_eq!(captured["encoding"], "utf8"); + assert_eq!(captured["truncated"], false); + assert_eq!(captured["content"], body); + } + + #[test] + fn a_traced_request_says_nothing_of_its_content_unless_asked() { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + + let guest_mac = [0x02, 0, 0, 0, 0, 0x1b]; + let mut slirp = SlirpBackend::new(guest_mac); + let tracer = crate::trace::Tracer::new(crate::trace::TraceOptions::default()); + tracer.remember_name([127, 0, 0, 1], "local.test"); + slirp.set_tracer(Some(tracer.clone())); + + let ends = from_guest(guest_mac, [127, 0, 0, 1], 45007, port); + let guest_isn = 5000u32; + let (mut upstream, ack) = connect(&mut slirp, &listener, &ends, guest_isn); + + let body = r#"{"secret":"do-not-record"}"#; + let request = format!( + "POST /v1/messages HTTP/1.1\r\nHost: local.test\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + slirp.send(&make_tcp_frame( + &ends, + guest_isn.wrapping_add(1), + ack, + ACK, + request.as_bytes(), + )); + read_upstream(&mut upstream, &mut slirp, request.len()); + + slirp.send(&make_tcp_frame( + &ends, + guest_isn.wrapping_add(1), + ack, + RST, + &[], + )); + + let drained = String::from_utf8(tracer.drain(usize::MAX)).unwrap(); + + assert!( + !drained.contains("net.http.body"), + "a body event appeared uninvited" + ); + assert!( + !drained.contains("do-not-record"), + "the body was recorded anyway" + ); + assert!(!drained.contains("headers"), "headers were recorded anyway"); + } #[test] fn a_guest_fin_becomes_an_upstream_eof() { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/crates/machine/src/virtio/tls_proxy/tests.rs b/crates/machine/src/virtio/tls_proxy/tests.rs index 26e8d102..b5e1b1b9 100644 --- a/crates/machine/src/virtio/tls_proxy/tests.rs +++ b/crates/machine/src/virtio/tls_proxy/tests.rs @@ -1040,3 +1040,46 @@ fn a_request_without_a_credential_is_unchanged_by_the_substitution() { assert_eq!(received, wire); } + +#[test] +fn a_traced_header_holds_the_stand_in_and_never_the_credential() { + let (port, up_ca, seen, host) = spawn_capturing_upstream(UPSTREAM_REPLY); + let ctx = TlsContext::new().unwrap(); + let mut proxy = proxy_carrying(&ctx, &up_ca, port, &["localhost"]); + + let tracer = crate::trace::Tracer::new(crate::trace::TraceOptions { + request_content: true, + ..crate::trace::TraceOptions::default() + }); + proxy.observe_http(crate::trace::HttpObserver::new( + tracer.clone(), + "https", + [127, 0, 0, 1], + 443, + )); + + let received = request_through_proxy( + &mut proxy, + &up_ca, + &seen, + "GET /v1/messages?key=vpod-secret-key-a1b2c3d4 HTTP/1.1\r\nHost: localhost\r\n\ + x-api-key: vpod-secret-key-a1b2c3d4\r\nContent-Length: 0\r\n\r\n", + ) + .expect("the upstream never saw a request"); + + let _ = host.join(); + let drained = String::from_utf8(tracer.drain(usize::MAX)).unwrap(); + + assert!( + received.contains("sk-ant-the-real-thing"), + "the substitution did not happen at all: {received}" + ); + assert!( + !drained.contains("sk-ant-the-real-thing"), + "the trace recorded the real credential: {drained}" + ); + assert!( + drained.contains("vpod-secret-key-a1b2c3d4"), + "the trace recorded neither the stand-in nor anything else useful: {drained}" + ); +} diff --git a/crates/wasi-component/src/api/session.rs b/crates/wasi-component/src/api/session.rs index d75fc6ea..e4b393ff 100644 --- a/crates/wasi-component/src/api/session.rs +++ b/crates/wasi-component/src/api/session.rs @@ -1007,6 +1007,7 @@ impl SessionManager { files: options.files, network: options.network, mounts: options.mounts, + request_content: options.request_content, buffer_bytes: if options.buffer_bytes == 0 { DEFAULT_BUFFER_BYTES } else { diff --git a/crates/wasi-component/vpod.wit b/crates/wasi-component/vpod.wit index 4faf5231..18b7509a 100644 --- a/crates/wasi-component/vpod.wit +++ b/crates/wasi-component/vpod.wit @@ -42,6 +42,7 @@ interface executor { files: bool, network: bool, mounts: bool, + request-content: bool, buffer-bytes: u32, } diff --git a/sdks/python/vpod/trace.py b/sdks/python/vpod/trace.py index 7d646752..4adaa5c1 100644 --- a/sdks/python/vpod/trace.py +++ b/sdks/python/vpod/trace.py @@ -33,15 +33,28 @@ def trace_options(trace) -> Optional[dict]: if trace is None or trace is False: return None if trace is True: - return {**{source: True for source in SOURCES}, "buffer_bytes": 0} + return { + **{source: True for source in SOURCES}, + "request_content": False, + "buffer_bytes": 0, + } if isinstance(trace, dict): - unknown = set(trace) - set(SOURCES) - {"buffer_bytes"} + unknown = set(trace) - set(SOURCES) - {"buffer_bytes", "request_content"} if unknown: raise ValueError( - f"unknown trace options {sorted(unknown)}, expected {list(SOURCES) + ['buffer_bytes']}" + f"unknown trace options {sorted(unknown)}, expected " + f"{list(SOURCES) + ['request_content', 'buffer_bytes']}" ) + request_content = bool(trace.get("request_content", False)) + if request_content and not trace.get("network", False): + raise ValueError( + "trace request_content records what each traced request carried, " + "so it needs network tracing on as well" + ) + return { **{source: bool(trace.get(source, False)) for source in SOURCES}, + "request_content": request_content, "buffer_bytes": int(trace.get("buffer_bytes", 0)), } raise TypeError(f"trace must be True or a dict of sources, got {trace!r}") @@ -376,6 +389,7 @@ def _start(self, exports, session_id: int) -> None: record: object = object.__new__(type("TraceOptions", (), {})) for source in SOURCES: object.__setattr__(record, source, self._options[source]) + object.__setattr__(record, "request-content", self._options["request_content"]) object.__setattr__(record, "buffer-bytes", self._options["buffer_bytes"]) unwrap_result(exports["session-trace-start"](session_id, record)) diff --git a/sdks/typescript/src/trace.ts b/sdks/typescript/src/trace.ts index 674b60de..565b6c5a 100644 --- a/sdks/typescript/src/trace.ts +++ b/sdks/typescript/src/trace.ts @@ -3,6 +3,8 @@ export interface TraceSources { files?: boolean; network?: boolean; mounts?: boolean; + /** Keep the headers and body of each traced request. Needs `network`. */ + requestContent?: boolean; bufferBytes?: number; } @@ -13,6 +15,7 @@ export interface WireTraceOptions { files: boolean; network: boolean; mounts: boolean; + requestContent: boolean; bufferBytes: number; } @@ -86,13 +89,20 @@ export function traceOptions(setting: TraceSetting | undefined): WireTraceOption return null; } if (setting === true) { - return { processes: true, files: true, network: true, mounts: true, bufferBytes: 0 }; + return { + processes: true, + files: true, + network: true, + mounts: true, + requestContent: false, + bufferBytes: 0, + }; } if (typeof setting !== "object" || setting === null) { throw new Error(`vpod: trace must be true or an object of sources, got ${JSON.stringify(setting)}`); } - const known = new Set([...SOURCES, "bufferBytes"]); + const known = new Set([...SOURCES, "requestContent", "bufferBytes"]); const unknown = Object.keys(setting).filter((key) => !known.has(key)); if (unknown.length > 0) { throw new Error( @@ -100,11 +110,20 @@ export function traceOptions(setting: TraceSetting | undefined): WireTraceOption ); } + const requestContent = setting.requestContent === true; + if (requestContent && setting.network !== true) { + throw new Error( + "vpod: trace requestContent records what each traced request carried, " + + "so it needs network tracing on as well", + ); + } + return { processes: setting.processes === true, files: setting.files === true, network: setting.network === true, mounts: setting.mounts === true, + requestContent, bufferBytes: setting.bufferBytes ?? 0, }; } From 6c726e93130e701593c3563ee8e2713973a3e3f0 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Sun, 20 Sep 2026 18:52:27 +0200 Subject: [PATCH 2/3] remove request_content option and always capture HTTP request bodies --- crates/machine/src/trace/http.rs | 65 +- crates/machine/src/trace/mod.rs | 6 - crates/machine/src/virtio/slirp/mod.rs | 57 +- crates/machine/src/virtio/tls_proxy/tests.rs | 9 +- crates/wasi-component/src/api/session.rs | 1 - crates/wasi-component/vpod.wit | 1 - sdks/python/tests/test_integration.py | 30 + sdks/python/vpod/trace.py | 43 +- sdks/trace-summaries.json | 1914 +++++++++++++++-- sdks/typescript/src/trace.ts | 55 +- .../integration/request-content.test.mjs | 39 + 11 files changed, 1935 insertions(+), 285 deletions(-) create mode 100644 sdks/typescript/tests/integration/request-content.test.mjs diff --git a/crates/machine/src/trace/http.rs b/crates/machine/src/trace/http.rs index 5b7ef553..1810e67c 100644 --- a/crates/machine/src/trace/http.rs +++ b/crates/machine/src/trace/http.rs @@ -32,7 +32,6 @@ pub struct HttpRequests { head: Vec, body_bytes_left: u64, stopped: bool, - capture_bodies: bool, body: Vec, body_truncated: bool, } @@ -45,16 +44,11 @@ impl HttpRequests { head: Vec::new(), body_bytes_left: 0, stopped: false, - capture_bodies: false, body: Vec::new(), body_truncated: false, } } - pub fn capture_bodies(&mut self) { - self.capture_bodies = true; - } - pub fn set_default_host(&mut self, host: &str) { if self.default_host.is_none() { self.default_host = Some(host.to_string()); @@ -118,7 +112,7 @@ impl HttpRequests { } fn keep_body(&mut self, bytes: &[u8]) { - if !self.capture_bodies || bytes.is_empty() { + if bytes.is_empty() { return; } @@ -130,7 +124,7 @@ impl HttpRequests { } fn finish_body(&mut self) -> Option { - if !self.capture_bodies || (self.body.is_empty() && !self.body_truncated) { + if self.body.is_empty() && !self.body_truncated { return None; } @@ -236,25 +230,17 @@ pub struct HttpObserver { requests: HttpRequests, address: [u8; 4], port: u16, - content: bool, head_seq: Option, head_url: Option, } impl HttpObserver { pub fn new(tracer: Tracer, scheme: &'static str, address: [u8; 4], port: u16) -> Self { - let content = tracer.traces_request_content(); - let mut requests = HttpRequests::new(scheme, None); - if content { - requests.capture_bodies(); - } - Self { tracer, - requests, + requests: HttpRequests::new(scheme, None), address, port, - content, head_seq: None, head_url: None, } @@ -277,22 +263,20 @@ impl HttpObserver { ("port", self.port.into()), ]; - if self.content { - let headers: serde_json::Map = request - .headers - .into_iter() - .map(|(name, value)| (name, Value::from(value))) - .collect(); - - fields.push(("headers", Value::Object(headers))); - fields.push(( - "body_bytes", - match request.body_bytes { - Some(length) => Value::from(length), - None => Value::Null, - }, - )); - } + let headers: serde_json::Map = request + .headers + .into_iter() + .map(|(name, value)| (name, Value::from(value))) + .collect(); + + fields.push(("headers", Value::Object(headers))); + fields.push(( + "body_bytes", + match request.body_bytes { + Some(length) => Value::from(length), + None => Value::Null, + }, + )); self.head_seq = self.tracer.record_seq("net.http", &fields); self.head_url = Some(url); @@ -362,9 +346,7 @@ mod tests { } fn capturing() -> HttpRequests { - let mut requests = HttpRequests::new("https", Some("api.example.com".to_string())); - requests.capture_bodies(); - requests + HttpRequests::new("https", Some("api.example.com".to_string())) } fn post(body: &str) -> Vec { @@ -493,17 +475,6 @@ mod tests { ); } - #[test] - fn nothing_is_kept_unless_capture_was_asked_for() { - let mut requests = HttpRequests::new("https", Some("api.example.com".to_string())); - let events = requests.observe(&post(r#"{"secret":"value"}"#)); - - assert_eq!( - bodies(events).len(), - 0, - "a body was kept without being asked for" - ); - } #[test] fn a_request_split_across_writes_is_seen_once() { let mut requests = HttpRequests::new("https", None); diff --git a/crates/machine/src/trace/mod.rs b/crates/machine/src/trace/mod.rs index 6920f569..6308ae84 100644 --- a/crates/machine/src/trace/mod.rs +++ b/crates/machine/src/trace/mod.rs @@ -23,7 +23,6 @@ pub struct TraceOptions { pub files: bool, pub network: bool, pub mounts: bool, - pub request_content: bool, pub buffer_bytes: usize, } @@ -34,7 +33,6 @@ impl Default for TraceOptions { files: true, network: true, mounts: true, - request_content: false, buffer_bytes: DEFAULT_BUFFER_BYTES, } } @@ -86,10 +84,6 @@ impl Tracer { self.options.network } - pub fn traces_request_content(&self) -> bool { - self.options.network && self.options.request_content - } - pub fn traces_mounts(&self) -> bool { self.options.mounts } diff --git a/crates/machine/src/virtio/slirp/mod.rs b/crates/machine/src/virtio/slirp/mod.rs index a2f6181e..d3b9e8d5 100644 --- a/crates/machine/src/virtio/slirp/mod.rs +++ b/crates/machine/src/virtio/slirp/mod.rs @@ -189,6 +189,7 @@ impl NetworkBackend for SlirpBackend { mod tests { use super::frames::{ACK, Endpoints, FIN, GUEST_IP, RST, SYN, make_tcp_frame}; use super::*; + use crate::trace::TraceOptions; use std::io::{Read, Write}; use std::time::{Duration, Instant}; @@ -321,7 +322,7 @@ mod tests { let guest_mac = [0x02, 0, 0, 0, 0, 0x19]; let mut slirp = SlirpBackend::new(guest_mac); - let tracer = crate::trace::Tracer::new(crate::trace::TraceOptions::default()); + let tracer = Tracer::new(TraceOptions::default()); tracer.remember_name([127, 0, 0, 1], "local.test"); slirp.set_tracer(Some(tracer.clone())); @@ -376,10 +377,7 @@ mod tests { let guest_mac = [0x02, 0, 0, 0, 0, 0x1a]; let mut slirp = SlirpBackend::new(guest_mac); - let tracer = crate::trace::Tracer::new(crate::trace::TraceOptions { - request_content: true, - ..crate::trace::TraceOptions::default() - }); + let tracer = Tracer::new(TraceOptions::default()); tracer.remember_name([127, 0, 0, 1], "local.test"); slirp.set_tracer(Some(tracer.clone())); @@ -438,55 +436,6 @@ mod tests { assert_eq!(captured["content"], body); } - #[test] - fn a_traced_request_says_nothing_of_its_content_unless_asked() { - let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - - let guest_mac = [0x02, 0, 0, 0, 0, 0x1b]; - let mut slirp = SlirpBackend::new(guest_mac); - let tracer = crate::trace::Tracer::new(crate::trace::TraceOptions::default()); - tracer.remember_name([127, 0, 0, 1], "local.test"); - slirp.set_tracer(Some(tracer.clone())); - - let ends = from_guest(guest_mac, [127, 0, 0, 1], 45007, port); - let guest_isn = 5000u32; - let (mut upstream, ack) = connect(&mut slirp, &listener, &ends, guest_isn); - - let body = r#"{"secret":"do-not-record"}"#; - let request = format!( - "POST /v1/messages HTTP/1.1\r\nHost: local.test\r\nContent-Length: {}\r\n\r\n{body}", - body.len() - ); - slirp.send(&make_tcp_frame( - &ends, - guest_isn.wrapping_add(1), - ack, - ACK, - request.as_bytes(), - )); - read_upstream(&mut upstream, &mut slirp, request.len()); - - slirp.send(&make_tcp_frame( - &ends, - guest_isn.wrapping_add(1), - ack, - RST, - &[], - )); - - let drained = String::from_utf8(tracer.drain(usize::MAX)).unwrap(); - - assert!( - !drained.contains("net.http.body"), - "a body event appeared uninvited" - ); - assert!( - !drained.contains("do-not-record"), - "the body was recorded anyway" - ); - assert!(!drained.contains("headers"), "headers were recorded anyway"); - } #[test] fn a_guest_fin_becomes_an_upstream_eof() { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/crates/machine/src/virtio/tls_proxy/tests.rs b/crates/machine/src/virtio/tls_proxy/tests.rs index b5e1b1b9..8a3d6777 100644 --- a/crates/machine/src/virtio/tls_proxy/tests.rs +++ b/crates/machine/src/virtio/tls_proxy/tests.rs @@ -8,6 +8,8 @@ use rustls::pki_types::ServerName; use std::net::TcpListener; use std::thread; +use crate::trace::{Tracer, TraceOptions}; + const UPSTREAM_REPLY: &[u8] = b"HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nhello"; fn provider() -> Arc { @@ -891,8 +893,6 @@ fn large_response_delivered_in_full_without_truncation() { assert_eq!(body, Some(BODY), "large body truncated: {body:?} of {BODY}"); } -/// Drive a real guest TLS client through the proxy and return what the upstream -/// received, or `None` when the proxy refused the connection. fn request_through_proxy( proxy: &mut TlsProxy, up_ca: &str, @@ -1047,10 +1047,7 @@ fn a_traced_header_holds_the_stand_in_and_never_the_credential() { let ctx = TlsContext::new().unwrap(); let mut proxy = proxy_carrying(&ctx, &up_ca, port, &["localhost"]); - let tracer = crate::trace::Tracer::new(crate::trace::TraceOptions { - request_content: true, - ..crate::trace::TraceOptions::default() - }); + let tracer = Tracer::new(TraceOptions::default()); proxy.observe_http(crate::trace::HttpObserver::new( tracer.clone(), "https", diff --git a/crates/wasi-component/src/api/session.rs b/crates/wasi-component/src/api/session.rs index e4b393ff..d75fc6ea 100644 --- a/crates/wasi-component/src/api/session.rs +++ b/crates/wasi-component/src/api/session.rs @@ -1007,7 +1007,6 @@ impl SessionManager { files: options.files, network: options.network, mounts: options.mounts, - request_content: options.request_content, buffer_bytes: if options.buffer_bytes == 0 { DEFAULT_BUFFER_BYTES } else { diff --git a/crates/wasi-component/vpod.wit b/crates/wasi-component/vpod.wit index 18b7509a..4faf5231 100644 --- a/crates/wasi-component/vpod.wit +++ b/crates/wasi-component/vpod.wit @@ -42,7 +42,6 @@ interface executor { files: bool, network: bool, mounts: bool, - request-content: bool, buffer-bytes: u32, } diff --git a/sdks/python/tests/test_integration.py b/sdks/python/tests/test_integration.py index 4bbd62fc..9cec24ea 100644 --- a/sdks/python/tests/test_integration.py +++ b/sdks/python/tests/test_integration.py @@ -1276,3 +1276,33 @@ def test_a_secret_without_a_host_is_refused(): def test_a_secret_needs_a_value(): with pytest.raises(ValueError, match="non-empty string value"): Sandbox.create(secrets={"K": {"value": "", "hosts": ["api.example.com"]}}) + + +def test_network_tracing_records_what_a_request_carried(): + body = '{"model":"claude","prompt":"hello"}' + + with Sandbox.create(trace={"network": True}) as sbx: + result = sbx.commands.run( + "wget -q -O- --header='Content-Type: application/json' " + f"--post-data='{body}' https://pypi.org/pypi/six/json > /dev/null 2>&1; true", + timeout=90, + ) + assert result.exit_code == 0 + + requests = [ + request + for activity in sbx.trace.collect().network() + for request in activity.requests + ] + + posted = [request for request in requests if request.method == "POST"] + if not posted: + pytest.skip("no route to the network") + + request = posted[0] + assert request.headers.get("Content-Type") == "application/json" + assert request.headers.get("Host") == "pypi.org" + assert request.body_bytes == len(body) + assert request.body == body + assert request.body_encoding == "utf8" + assert not request.body_truncated diff --git a/sdks/python/vpod/trace.py b/sdks/python/vpod/trace.py index 4adaa5c1..71549296 100644 --- a/sdks/python/vpod/trace.py +++ b/sdks/python/vpod/trace.py @@ -33,28 +33,15 @@ def trace_options(trace) -> Optional[dict]: if trace is None or trace is False: return None if trace is True: - return { - **{source: True for source in SOURCES}, - "request_content": False, - "buffer_bytes": 0, - } + return {**{source: True for source in SOURCES}, "buffer_bytes": 0} if isinstance(trace, dict): - unknown = set(trace) - set(SOURCES) - {"buffer_bytes", "request_content"} + unknown = set(trace) - set(SOURCES) - {"buffer_bytes"} if unknown: raise ValueError( - f"unknown trace options {sorted(unknown)}, expected " - f"{list(SOURCES) + ['request_content', 'buffer_bytes']}" + f"unknown trace options {sorted(unknown)}, expected {list(SOURCES) + ['buffer_bytes']}" ) - request_content = bool(trace.get("request_content", False)) - if request_content and not trace.get("network", False): - raise ValueError( - "trace request_content records what each traced request carried, " - "so it needs network tracing on as well" - ) - return { **{source: bool(trace.get(source, False)) for source in SOURCES}, - "request_content": request_content, "buffer_bytes": int(trace.get("buffer_bytes", 0)), } raise TypeError(f"trace must be True or a dict of sources, got {trace!r}") @@ -77,6 +64,11 @@ class FileActivity: class HttpRequest: method: str url: str + headers: dict[str, str] = field(default_factory=dict) + body_bytes: Optional[int] = None + body: Optional[str] = None + body_encoding: Optional[str] = None + body_truncated: bool = False @dataclass @@ -223,11 +215,20 @@ def activity(event) -> Optional[NetworkActivity]: touching[key].add(pid) return entry + requests_by_seq: dict[int, HttpRequest] = {} + for event in self._events: if event.get("internal") and not internal: continue kind = event["kind"] + if kind == "net.http.body": + if (request := requests_by_seq.get(event.get("request_seq"))) is not None: + request.body = event.get("content") + request.body_encoding = event.get("encoding") + request.body_truncated = bool(event.get("truncated")) + continue + if kind not in ("net.connect", "net.flow", "net.udp", "net.http"): continue if (entry := activity(event)) is None: @@ -246,7 +247,14 @@ def activity(event) -> Optional[NetworkActivity]: entry.failed = False else: entry.host = entry.host or urlsplit(event["url"]).hostname - entry.requests.append(HttpRequest(event["method"], event["url"])) + request = HttpRequest( + event["method"], + event["url"], + headers=event.get("headers") or {}, + body_bytes=event.get("body_bytes"), + ) + entry.requests.append(request) + requests_by_seq[event["seq"]] = request for key, entry in activities.items(): entry.processes = sorted(touching[key]) @@ -389,7 +397,6 @@ def _start(self, exports, session_id: int) -> None: record: object = object.__new__(type("TraceOptions", (), {})) for source in SOURCES: object.__setattr__(record, source, self._options[source]) - object.__setattr__(record, "request-content", self._options["request_content"]) object.__setattr__(record, "buffer-bytes", self._options["buffer_bytes"]) unwrap_result(exports["session-trace-start"](session_id, record)) diff --git a/sdks/trace-summaries.json b/sdks/trace-summaries.json index b3bc91b9..0b08b24f 100644 --- a/sdks/trace-summaries.json +++ b/sdks/trace-summaries.json @@ -3,146 +3,1650 @@ { "name": "an agent command that installs, downloads and edits files", "events": [ - {"v": 1, "seq": 0, "guest_ns": 1000, "wall_ms": 1, "kind": "file.open", "task": "a1", "pid": 1, "path": "/dev/ttyS1", "access": "write", "create": true, "truncate": true, "result": 3, "internal": true}, - {"v": 1, "seq": 1, "guest_ns": 1000, "wall_ms": 1, "kind": "process.fork", "task": "a1", "pid": 1, "child_pid": 563, "thread": false}, - {"v": 1, "seq": 2, "guest_ns": 2000, "wall_ms": 1, "kind": "process.exec", "task": "b2", "pid": 563, "ppid": 1, "path": "/usr/local/bin/sh", "argv": ["sh", "-c", "cd /app && make"], "result": -2}, - {"v": 1, "seq": 3, "guest_ns": 2000, "wall_ms": 1, "kind": "process.exec", "task": "b2", "pid": 563, "ppid": 1, "path": "/bin/sh", "argv": ["sh", "-c", "cd /app && make"]}, - {"v": 1, "seq": 4, "guest_ns": 3000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/etc/ld-musl-riscv64.path", "access": "read", "create": false, "truncate": false, "result": 3}, - {"v": 1, "seq": 5, "guest_ns": 3000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/usr/lib/libz.so.1", "access": "read", "create": false, "truncate": false, "result": 3}, - {"v": 1, "seq": 6, "guest_ns": 3000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/proc/self/stat", "access": "read", "create": false, "truncate": false, "result": 4}, - {"v": 1, "seq": 7, "guest_ns": 4000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/app/Makefile", "access": "read", "create": false, "truncate": false, "result": 3}, - {"v": 1, "seq": 8, "guest_ns": 4000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/app/missing.h", "access": "read", "create": false, "truncate": false, "result": -2}, - {"v": 1, "seq": 9, "guest_ns": 5000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/etc/shadow", "access": "read", "create": false, "truncate": false, "result": -13}, - {"v": 1, "seq": 10, "guest_ns": 5000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/app/build/.out.tmp", "access": "write", "create": true, "truncate": true, "result": 5}, - {"v": 1, "seq": 11, "guest_ns": 6000, "wall_ms": 1, "kind": "file.rename", "task": "b2", "pid": 563, "from": "/app/build/.out.tmp", "to": "/app/build/out", "result": 0}, - {"v": 1, "seq": 12, "guest_ns": 6000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/app/build/ext.so", "access": "write", "create": true, "truncate": true, "result": 6}, - {"v": 1, "seq": 13, "guest_ns": 6000, "wall_ms": 1, "kind": "file.open", "task": "b2", "pid": 563, "path": "/app/db.sqlite", "access": "read-write", "create": false, "truncate": false, "result": 7}, - {"v": 1, "seq": 14, "guest_ns": 7000, "wall_ms": 1, "kind": "dir.create", "task": "b2", "pid": 563, "path": "/app/cache", "result": 0}, - {"v": 1, "seq": 15, "guest_ns": 7000, "wall_ms": 1, "kind": "dir.create", "task": "b2", "pid": 563, "path": "/app/build", "result": -17}, - {"v": 1, "seq": 16, "guest_ns": 7000, "wall_ms": 1, "kind": "file.truncate", "task": "b2", "pid": 563, "path": "/app/log.txt", "size": 0, "result": 0}, - {"v": 1, "seq": 17, "guest_ns": 8000, "wall_ms": 1, "kind": "file.delete", "task": "b2", "pid": 563, "path": "/app/old.o", "directory": false, "result": 0}, - {"v": 1, "seq": 18, "guest_ns": 8000, "wall_ms": 1, "kind": "file.delete", "task": "b2", "pid": 563, "path": "/root/.ssh/authorized_keys", "directory": false, "result": -1}, - {"v": 1, "seq": 19, "guest_ns": 8000, "wall_ms": 1, "kind": "file.delete", "task": "c3", "pid": 700, "path": "/tmp/.vpod_cmd.b64", "directory": false, "result": 0, "internal": true}, - {"v": 1, "seq": 20, "guest_ns": 9000, "wall_ms": 1, "kind": "net.dns", "name": "pypi.org", "type": "A", "answers": ["151.101.192.223"]}, - {"v": 1, "seq": 21, "guest_ns": 9000, "wall_ms": 1, "kind": "net.connect", "task": "b2", "pid": 563, "protocol": "tcp", "address": "151.101.192.223", "port": 443, "host": "pypi.org", "result": 0}, - {"v": 1, "seq": 22, "guest_ns": 9500, "wall_ms": 1, "kind": "net.http", "protocol": "https", "method": "GET", "url": "https://pypi.org/simple/requests/", "address": "151.101.192.223", "port": 443}, - {"v": 1, "seq": 23, "guest_ns": 9600, "wall_ms": 1, "kind": "net.http", "protocol": "https", "method": "GET", "url": "https://pypi.org/simple/urllib3/", "address": "151.101.192.223", "port": 443}, - {"v": 1, "seq": 24, "guest_ns": 9700, "wall_ms": 1, "kind": "net.flow", "protocol": "tcp", "address": "151.101.192.223", "port": 443, "host": "pypi.org", "bytes_out": 252, "bytes_in": 84487, "duration_ns": 300, "failed": false}, - {"v": 1, "seq": 25, "guest_ns": 9800, "wall_ms": 1, "kind": "net.connect", "task": "b2", "pid": 563, "protocol": "tcp", "address": "151.101.192.223", "port": 443, "host": "pypi.org", "result": -115}, - {"v": 1, "seq": 26, "guest_ns": 9900, "wall_ms": 1, "kind": "net.flow", "protocol": "tcp", "address": "151.101.192.223", "port": 443, "host": "pypi.org", "bytes_out": 100, "bytes_in": 1000, "duration_ns": 50, "failed": false}, - {"v": 1, "seq": 27, "guest_ns": 10000, "wall_ms": 1, "kind": "net.connect", "task": "e5", "pid": 564, "protocol": null, "address": "127.0.0.1", "port": 9, "host": null, "result": -111}, - {"v": 1, "seq": 28, "guest_ns": 10000, "wall_ms": 1, "kind": "net.http", "protocol": "http", "method": "POST", "url": "http://example.com/upload", "address": "93.184.215.14", "port": 80}, - {"v": 1, "seq": 29, "guest_ns": 10000, "wall_ms": 1, "kind": "net.flow", "protocol": "tcp", "address": "93.184.215.14", "port": 80, "host": null, "bytes_out": 11, "bytes_in": 0, "duration_ns": 5, "failed": true}, - {"v": 1, "seq": 30, "guest_ns": 10500, "wall_ms": 1, "kind": "net.udp", "address": "8.8.8.8", "port": 123, "host": null}, - {"v": 1, "seq": 31, "guest_ns": 10600, "wall_ms": 1, "kind": "net.connect", "task": "c3", "pid": 700, "protocol": "tcp", "address": "10.0.2.2", "port": 8080, "host": null, "result": 0, "internal": true}, - {"v": 1, "seq": 32, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.open", "pid": 563, "path": "/data/input.csv", "access": "read", "truncate": false, "result": 0}, - {"v": 1, "seq": 33, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.close", "pid": 563, "path": "/data/input.csv", "bytes_read": 4096, "bytes_written": 0}, - {"v": 1, "seq": 34, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.create", "pid": 563, "path": "/data/report.md", "result": 0}, - {"v": 1, "seq": 35, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.close", "pid": 563, "path": "/data/report.md", "bytes_read": 0, "bytes_written": 512}, - {"v": 1, "seq": 36, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.mkdir", "pid": 563, "path": "/data/out", "result": 0}, - {"v": 1, "seq": 37, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.rename", "pid": 563, "from": "/data/draft.md", "to": "/data/out/final.md", "result": 0}, - {"v": 1, "seq": 38, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.delete", "pid": 563, "path": "/data/stale.lock", "directory": false, "result": 0}, - {"v": 1, "seq": 39, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.truncate", "pid": 563, "path": "/data/log.txt", "size": 0, "result": 0}, - {"v": 1, "seq": 40, "guest_ns": 11000, "wall_ms": 1, "kind": "mount.open", "pid": 563, "path": "/data/secret.key", "access": "read", "truncate": false, "result": -13}, - {"v": 1, "seq": 41, "guest_ns": 12000, "wall_ms": 1, "kind": "process.exec", "task": "d4", "pid": 570, "ppid": 1, "path": "/usr/lib/vpod/vpod-seed-entropy", "argv": ["vpod-seed-entropy", "ab12"], "internal": true}, - {"v": 1, "seq": 42, "guest_ns": 12000, "wall_ms": 1, "kind": "process.exit", "task": "d4", "pid": 570, "code": 0, "internal": true}, - {"v": 1, "seq": 43, "guest_ns": 13000, "wall_ms": 1, "kind": "process.exec", "task": "b2", "pid": 563, "ppid": 1, "path": "/usr/bin/make", "argv": ["make"]}, - {"v": 1, "seq": 44, "guest_ns": 13500, "wall_ms": 1, "kind": "process.fork", "task": "b2", "pid": 563, "child_pid": 564, "thread": false}, - {"v": 1, "seq": 45, "guest_ns": 14000, "wall_ms": 1, "kind": "process.exec", "task": "e5", "pid": 564, "ppid": 563, "path": "/usr/bin/cc", "argv": ["cc", "-o", "build/out", "main.c"]}, - {"v": 1, "seq": 46, "guest_ns": 15000, "wall_ms": 1, "kind": "process.exit", "task": "e5", "pid": 564, "code": 1}, - {"v": 1, "seq": 47, "guest_ns": 15200, "wall_ms": 1, "kind": "process.fork", "task": "b2", "pid": 563, "child_pid": 565, "thread": false}, - {"v": 1, "seq": 48, "guest_ns": 15300, "wall_ms": 1, "kind": "process.fork", "task": "f6", "pid": 565, "child_pid": 566, "thread": false}, - {"v": 1, "seq": 49, "guest_ns": 15400, "wall_ms": 1, "kind": "process.exec", "task": "g7", "pid": 566, "ppid": 565, "path": "/bin/echo", "argv": ["echo", "done"]}, - {"v": 1, "seq": 50, "guest_ns": 15500, "wall_ms": 1, "kind": "process.exit", "task": "g7", "pid": 566, "code": 0}, - {"v": 1, "seq": 51, "guest_ns": 16000, "wall_ms": 1, "kind": "process.exit", "task": "b2", "pid": 563, "code": 2}, - {"v": 1, "seq": 52, "guest_ns": 16500, "wall_ms": 1, "kind": "process.fork", "task": "a1", "pid": 1, "child_pid": 571, "thread": false}, - {"v": 1, "seq": 53, "guest_ns": 17000, "wall_ms": 1, "kind": "process.exec", "task": "h8", "pid": 571, "ppid": 1, "path": "/usr/bin/python3", "argv": ["python3", "-c", "print(1)"], "argv_truncated": true}, - {"v": 1, "seq": 54, "guest_ns": 17500, "wall_ms": 1, "kind": "process.fork", "task": "h8", "pid": 571, "child_pid": 572, "thread": true} + { + "v": 1, + "seq": 0, + "guest_ns": 1000, + "wall_ms": 1, + "kind": "file.open", + "task": "a1", + "pid": 1, + "path": "/dev/ttyS1", + "access": "write", + "create": true, + "truncate": true, + "result": 3, + "internal": true + }, + { + "v": 1, + "seq": 1, + "guest_ns": 1000, + "wall_ms": 1, + "kind": "process.fork", + "task": "a1", + "pid": 1, + "child_pid": 563, + "thread": false + }, + { + "v": 1, + "seq": 2, + "guest_ns": 2000, + "wall_ms": 1, + "kind": "process.exec", + "task": "b2", + "pid": 563, + "ppid": 1, + "path": "/usr/local/bin/sh", + "argv": [ + "sh", + "-c", + "cd /app && make" + ], + "result": -2 + }, + { + "v": 1, + "seq": 3, + "guest_ns": 2000, + "wall_ms": 1, + "kind": "process.exec", + "task": "b2", + "pid": 563, + "ppid": 1, + "path": "/bin/sh", + "argv": [ + "sh", + "-c", + "cd /app && make" + ] + }, + { + "v": 1, + "seq": 4, + "guest_ns": 3000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/etc/ld-musl-riscv64.path", + "access": "read", + "create": false, + "truncate": false, + "result": 3 + }, + { + "v": 1, + "seq": 5, + "guest_ns": 3000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/usr/lib/libz.so.1", + "access": "read", + "create": false, + "truncate": false, + "result": 3 + }, + { + "v": 1, + "seq": 6, + "guest_ns": 3000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/proc/self/stat", + "access": "read", + "create": false, + "truncate": false, + "result": 4 + }, + { + "v": 1, + "seq": 7, + "guest_ns": 4000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/app/Makefile", + "access": "read", + "create": false, + "truncate": false, + "result": 3 + }, + { + "v": 1, + "seq": 8, + "guest_ns": 4000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/app/missing.h", + "access": "read", + "create": false, + "truncate": false, + "result": -2 + }, + { + "v": 1, + "seq": 9, + "guest_ns": 5000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/etc/shadow", + "access": "read", + "create": false, + "truncate": false, + "result": -13 + }, + { + "v": 1, + "seq": 10, + "guest_ns": 5000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/app/build/.out.tmp", + "access": "write", + "create": true, + "truncate": true, + "result": 5 + }, + { + "v": 1, + "seq": 11, + "guest_ns": 6000, + "wall_ms": 1, + "kind": "file.rename", + "task": "b2", + "pid": 563, + "from": "/app/build/.out.tmp", + "to": "/app/build/out", + "result": 0 + }, + { + "v": 1, + "seq": 12, + "guest_ns": 6000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/app/build/ext.so", + "access": "write", + "create": true, + "truncate": true, + "result": 6 + }, + { + "v": 1, + "seq": 13, + "guest_ns": 6000, + "wall_ms": 1, + "kind": "file.open", + "task": "b2", + "pid": 563, + "path": "/app/db.sqlite", + "access": "read-write", + "create": false, + "truncate": false, + "result": 7 + }, + { + "v": 1, + "seq": 14, + "guest_ns": 7000, + "wall_ms": 1, + "kind": "dir.create", + "task": "b2", + "pid": 563, + "path": "/app/cache", + "result": 0 + }, + { + "v": 1, + "seq": 15, + "guest_ns": 7000, + "wall_ms": 1, + "kind": "dir.create", + "task": "b2", + "pid": 563, + "path": "/app/build", + "result": -17 + }, + { + "v": 1, + "seq": 16, + "guest_ns": 7000, + "wall_ms": 1, + "kind": "file.truncate", + "task": "b2", + "pid": 563, + "path": "/app/log.txt", + "size": 0, + "result": 0 + }, + { + "v": 1, + "seq": 17, + "guest_ns": 8000, + "wall_ms": 1, + "kind": "file.delete", + "task": "b2", + "pid": 563, + "path": "/app/old.o", + "directory": false, + "result": 0 + }, + { + "v": 1, + "seq": 18, + "guest_ns": 8000, + "wall_ms": 1, + "kind": "file.delete", + "task": "b2", + "pid": 563, + "path": "/root/.ssh/authorized_keys", + "directory": false, + "result": -1 + }, + { + "v": 1, + "seq": 19, + "guest_ns": 8000, + "wall_ms": 1, + "kind": "file.delete", + "task": "c3", + "pid": 700, + "path": "/tmp/.vpod_cmd.b64", + "directory": false, + "result": 0, + "internal": true + }, + { + "v": 1, + "seq": 20, + "guest_ns": 9000, + "wall_ms": 1, + "kind": "net.dns", + "name": "pypi.org", + "type": "A", + "answers": [ + "151.101.192.223" + ] + }, + { + "v": 1, + "seq": 21, + "guest_ns": 9000, + "wall_ms": 1, + "kind": "net.connect", + "task": "b2", + "pid": 563, + "protocol": "tcp", + "address": "151.101.192.223", + "port": 443, + "host": "pypi.org", + "result": 0 + }, + { + "v": 1, + "seq": 22, + "guest_ns": 9500, + "wall_ms": 1, + "kind": "net.http", + "protocol": "https", + "method": "GET", + "url": "https://pypi.org/simple/requests/", + "address": "151.101.192.223", + "port": 443 + }, + { + "v": 1, + "seq": 23, + "guest_ns": 9600, + "wall_ms": 1, + "kind": "net.http", + "protocol": "https", + "method": "GET", + "url": "https://pypi.org/simple/urllib3/", + "address": "151.101.192.223", + "port": 443 + }, + { + "v": 1, + "seq": 24, + "guest_ns": 9700, + "wall_ms": 1, + "kind": "net.flow", + "protocol": "tcp", + "address": "151.101.192.223", + "port": 443, + "host": "pypi.org", + "bytes_out": 252, + "bytes_in": 84487, + "duration_ns": 300, + "failed": false + }, + { + "v": 1, + "seq": 25, + "guest_ns": 9800, + "wall_ms": 1, + "kind": "net.connect", + "task": "b2", + "pid": 563, + "protocol": "tcp", + "address": "151.101.192.223", + "port": 443, + "host": "pypi.org", + "result": -115 + }, + { + "v": 1, + "seq": 26, + "guest_ns": 9900, + "wall_ms": 1, + "kind": "net.flow", + "protocol": "tcp", + "address": "151.101.192.223", + "port": 443, + "host": "pypi.org", + "bytes_out": 100, + "bytes_in": 1000, + "duration_ns": 50, + "failed": false + }, + { + "v": 1, + "seq": 27, + "guest_ns": 10000, + "wall_ms": 1, + "kind": "net.connect", + "task": "e5", + "pid": 564, + "protocol": null, + "address": "127.0.0.1", + "port": 9, + "host": null, + "result": -111 + }, + { + "v": 1, + "seq": 28, + "guest_ns": 10000, + "wall_ms": 1, + "kind": "net.http", + "protocol": "http", + "method": "POST", + "url": "http://example.com/upload", + "address": "93.184.215.14", + "port": 80 + }, + { + "v": 1, + "seq": 29, + "guest_ns": 10000, + "wall_ms": 1, + "kind": "net.flow", + "protocol": "tcp", + "address": "93.184.215.14", + "port": 80, + "host": null, + "bytes_out": 11, + "bytes_in": 0, + "duration_ns": 5, + "failed": true + }, + { + "v": 1, + "seq": 30, + "guest_ns": 10500, + "wall_ms": 1, + "kind": "net.udp", + "address": "8.8.8.8", + "port": 123, + "host": null + }, + { + "v": 1, + "seq": 31, + "guest_ns": 10600, + "wall_ms": 1, + "kind": "net.connect", + "task": "c3", + "pid": 700, + "protocol": "tcp", + "address": "10.0.2.2", + "port": 8080, + "host": null, + "result": 0, + "internal": true + }, + { + "v": 1, + "seq": 32, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.open", + "pid": 563, + "path": "/data/input.csv", + "access": "read", + "truncate": false, + "result": 0 + }, + { + "v": 1, + "seq": 33, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.close", + "pid": 563, + "path": "/data/input.csv", + "bytes_read": 4096, + "bytes_written": 0 + }, + { + "v": 1, + "seq": 34, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.create", + "pid": 563, + "path": "/data/report.md", + "result": 0 + }, + { + "v": 1, + "seq": 35, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.close", + "pid": 563, + "path": "/data/report.md", + "bytes_read": 0, + "bytes_written": 512 + }, + { + "v": 1, + "seq": 36, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.mkdir", + "pid": 563, + "path": "/data/out", + "result": 0 + }, + { + "v": 1, + "seq": 37, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.rename", + "pid": 563, + "from": "/data/draft.md", + "to": "/data/out/final.md", + "result": 0 + }, + { + "v": 1, + "seq": 38, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.delete", + "pid": 563, + "path": "/data/stale.lock", + "directory": false, + "result": 0 + }, + { + "v": 1, + "seq": 39, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.truncate", + "pid": 563, + "path": "/data/log.txt", + "size": 0, + "result": 0 + }, + { + "v": 1, + "seq": 40, + "guest_ns": 11000, + "wall_ms": 1, + "kind": "mount.open", + "pid": 563, + "path": "/data/secret.key", + "access": "read", + "truncate": false, + "result": -13 + }, + { + "v": 1, + "seq": 41, + "guest_ns": 12000, + "wall_ms": 1, + "kind": "process.exec", + "task": "d4", + "pid": 570, + "ppid": 1, + "path": "/usr/lib/vpod/vpod-seed-entropy", + "argv": [ + "vpod-seed-entropy", + "ab12" + ], + "internal": true + }, + { + "v": 1, + "seq": 42, + "guest_ns": 12000, + "wall_ms": 1, + "kind": "process.exit", + "task": "d4", + "pid": 570, + "code": 0, + "internal": true + }, + { + "v": 1, + "seq": 43, + "guest_ns": 13000, + "wall_ms": 1, + "kind": "process.exec", + "task": "b2", + "pid": 563, + "ppid": 1, + "path": "/usr/bin/make", + "argv": [ + "make" + ] + }, + { + "v": 1, + "seq": 44, + "guest_ns": 13500, + "wall_ms": 1, + "kind": "process.fork", + "task": "b2", + "pid": 563, + "child_pid": 564, + "thread": false + }, + { + "v": 1, + "seq": 45, + "guest_ns": 14000, + "wall_ms": 1, + "kind": "process.exec", + "task": "e5", + "pid": 564, + "ppid": 563, + "path": "/usr/bin/cc", + "argv": [ + "cc", + "-o", + "build/out", + "main.c" + ] + }, + { + "v": 1, + "seq": 46, + "guest_ns": 15000, + "wall_ms": 1, + "kind": "process.exit", + "task": "e5", + "pid": 564, + "code": 1 + }, + { + "v": 1, + "seq": 47, + "guest_ns": 15200, + "wall_ms": 1, + "kind": "process.fork", + "task": "b2", + "pid": 563, + "child_pid": 565, + "thread": false + }, + { + "v": 1, + "seq": 48, + "guest_ns": 15300, + "wall_ms": 1, + "kind": "process.fork", + "task": "f6", + "pid": 565, + "child_pid": 566, + "thread": false + }, + { + "v": 1, + "seq": 49, + "guest_ns": 15400, + "wall_ms": 1, + "kind": "process.exec", + "task": "g7", + "pid": 566, + "ppid": 565, + "path": "/bin/echo", + "argv": [ + "echo", + "done" + ] + }, + { + "v": 1, + "seq": 50, + "guest_ns": 15500, + "wall_ms": 1, + "kind": "process.exit", + "task": "g7", + "pid": 566, + "code": 0 + }, + { + "v": 1, + "seq": 51, + "guest_ns": 16000, + "wall_ms": 1, + "kind": "process.exit", + "task": "b2", + "pid": 563, + "code": 2 + }, + { + "v": 1, + "seq": 52, + "guest_ns": 16500, + "wall_ms": 1, + "kind": "process.fork", + "task": "a1", + "pid": 1, + "child_pid": 571, + "thread": false + }, + { + "v": 1, + "seq": 53, + "guest_ns": 17000, + "wall_ms": 1, + "kind": "process.exec", + "task": "h8", + "pid": 571, + "ppid": 1, + "path": "/usr/bin/python3", + "argv": [ + "python3", + "-c", + "print(1)" + ], + "argv_truncated": true + }, + { + "v": 1, + "seq": 54, + "guest_ns": 17500, + "wall_ms": 1, + "kind": "process.fork", + "task": "h8", + "pid": 571, + "child_pid": 572, + "thread": true + } ], "complete": true, "files": [ - {"path": "/app/Makefile", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/etc/shadow", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": true, "processes": [563]}, - {"path": "/app/build/.out.tmp", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": "/app/build/out", "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/build/out", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": "/app/build/.out.tmp", "denied": false, "processes": [563]}, - {"path": "/app/build/ext.so", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/db.sqlite", "read": true, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/cache", "read": false, "written": false, "created": true, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/log.txt", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/old.o", "read": false, "written": false, "created": false, "deleted": true, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/root/.ssh/authorized_keys", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": true, "processes": [563]}, - {"path": "/data/input.csv", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/report.md", "read": false, "written": true, "created": true, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/out", "read": false, "written": false, "created": true, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/draft.md", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": "/data/out/final.md", "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/out/final.md", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": "/data/draft.md", "denied": false, "processes": [563]}, - {"path": "/data/stale.lock", "read": false, "written": false, "created": false, "deleted": true, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/log.txt", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/secret.key", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": true, "processes": [563]} + { + "path": "/app/Makefile", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/etc/shadow", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": true, + "processes": [ + 563 + ] + }, + { + "path": "/app/build/.out.tmp", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": "/app/build/out", + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/build/out", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": "/app/build/.out.tmp", + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/build/ext.so", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/db.sqlite", + "read": true, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/cache", + "read": false, + "written": false, + "created": true, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/log.txt", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/old.o", + "read": false, + "written": false, + "created": false, + "deleted": true, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/root/.ssh/authorized_keys", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": true, + "processes": [ + 563 + ] + }, + { + "path": "/data/input.csv", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/report.md", + "read": false, + "written": true, + "created": true, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/out", + "read": false, + "written": false, + "created": true, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/draft.md", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": "/data/out/final.md", + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/out/final.md", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": "/data/draft.md", + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/stale.lock", + "read": false, + "written": false, + "created": false, + "deleted": true, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/log.txt", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/secret.key", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": true, + "processes": [ + 563 + ] + } ], "files_including_internal_and_noise": [ - {"path": "/dev/ttyS1", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [1]}, - {"path": "/etc/ld-musl-riscv64.path", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/usr/lib/libz.so.1", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/proc/self/stat", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/Makefile", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/etc/shadow", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": true, "processes": [563]}, - {"path": "/app/build/.out.tmp", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": "/app/build/out", "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/build/out", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": "/app/build/.out.tmp", "denied": false, "processes": [563]}, - {"path": "/app/build/ext.so", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/db.sqlite", "read": true, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/cache", "read": false, "written": false, "created": true, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/log.txt", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/app/old.o", "read": false, "written": false, "created": false, "deleted": true, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/root/.ssh/authorized_keys", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": true, "processes": [563]}, - {"path": "/tmp/.vpod_cmd.b64", "read": false, "written": false, "created": false, "deleted": true, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [700]}, - {"path": "/data/input.csv", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/report.md", "read": false, "written": true, "created": true, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/out", "read": false, "written": false, "created": true, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/draft.md", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": "/data/out/final.md", "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/out/final.md", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": "/data/draft.md", "denied": false, "processes": [563]}, - {"path": "/data/stale.lock", "read": false, "written": false, "created": false, "deleted": true, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/log.txt", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/data/secret.key", "read": false, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": true, "processes": [563]} + { + "path": "/dev/ttyS1", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 1 + ] + }, + { + "path": "/etc/ld-musl-riscv64.path", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/usr/lib/libz.so.1", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/proc/self/stat", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/Makefile", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/etc/shadow", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": true, + "processes": [ + 563 + ] + }, + { + "path": "/app/build/.out.tmp", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": "/app/build/out", + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/build/out", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": "/app/build/.out.tmp", + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/build/ext.so", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/db.sqlite", + "read": true, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/cache", + "read": false, + "written": false, + "created": true, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/log.txt", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/app/old.o", + "read": false, + "written": false, + "created": false, + "deleted": true, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/root/.ssh/authorized_keys", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": true, + "processes": [ + 563 + ] + }, + { + "path": "/tmp/.vpod_cmd.b64", + "read": false, + "written": false, + "created": false, + "deleted": true, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 700 + ] + }, + { + "path": "/data/input.csv", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/report.md", + "read": false, + "written": true, + "created": true, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/out", + "read": false, + "written": false, + "created": true, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/draft.md", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": "/data/out/final.md", + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/out/final.md", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": "/data/draft.md", + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/stale.lock", + "read": false, + "written": false, + "created": false, + "deleted": true, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/log.txt", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/data/secret.key", + "read": false, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": true, + "processes": [ + 563 + ] + } ], "network": [ - {"host": "pypi.org", "address": "151.101.192.223", "port": 443, "protocol": "tcp", "requests": [{"method": "GET", "url": "https://pypi.org/simple/requests/"}, {"method": "GET", "url": "https://pypi.org/simple/urllib3/"}], "bytes_out": 352, "bytes_in": 85487, "failed": false, "processes": [563]}, - {"host": null, "address": "127.0.0.1", "port": 9, "protocol": null, "requests": [], "bytes_out": 0, "bytes_in": 0, "failed": true, "processes": [564]}, - {"host": "example.com", "address": "93.184.215.14", "port": 80, "protocol": "tcp", "requests": [{"method": "POST", "url": "http://example.com/upload"}], "bytes_out": 11, "bytes_in": 0, "failed": true, "processes": []}, - {"host": null, "address": "8.8.8.8", "port": 123, "protocol": "udp", "requests": [], "bytes_out": 0, "bytes_in": 0, "failed": false, "processes": []} + { + "host": "pypi.org", + "address": "151.101.192.223", + "port": 443, + "protocol": "tcp", + "requests": [ + { + "method": "GET", + "url": "https://pypi.org/simple/requests/", + "headers": {}, + "body_bytes": null, + "body": null, + "body_encoding": null, + "body_truncated": false + }, + { + "method": "GET", + "url": "https://pypi.org/simple/urllib3/", + "headers": {}, + "body_bytes": null, + "body": null, + "body_encoding": null, + "body_truncated": false + } + ], + "bytes_out": 352, + "bytes_in": 85487, + "failed": false, + "processes": [ + 563 + ] + }, + { + "host": null, + "address": "127.0.0.1", + "port": 9, + "protocol": null, + "requests": [], + "bytes_out": 0, + "bytes_in": 0, + "failed": true, + "processes": [ + 564 + ] + }, + { + "host": "example.com", + "address": "93.184.215.14", + "port": 80, + "protocol": "tcp", + "requests": [ + { + "method": "POST", + "url": "http://example.com/upload", + "headers": {}, + "body_bytes": null, + "body": null, + "body_encoding": null, + "body_truncated": false + } + ], + "bytes_out": 11, + "bytes_in": 0, + "failed": true, + "processes": [] + }, + { + "host": null, + "address": "8.8.8.8", + "port": 123, + "protocol": "udp", + "requests": [], + "bytes_out": 0, + "bytes_in": 0, + "failed": false, + "processes": [] + } ], "network_including_internal": [ - {"host": "pypi.org", "address": "151.101.192.223", "port": 443, "protocol": "tcp", "requests": [{"method": "GET", "url": "https://pypi.org/simple/requests/"}, {"method": "GET", "url": "https://pypi.org/simple/urllib3/"}], "bytes_out": 352, "bytes_in": 85487, "failed": false, "processes": [563]}, - {"host": null, "address": "127.0.0.1", "port": 9, "protocol": null, "requests": [], "bytes_out": 0, "bytes_in": 0, "failed": true, "processes": [564]}, - {"host": "example.com", "address": "93.184.215.14", "port": 80, "protocol": "tcp", "requests": [{"method": "POST", "url": "http://example.com/upload"}], "bytes_out": 11, "bytes_in": 0, "failed": true, "processes": []}, - {"host": null, "address": "8.8.8.8", "port": 123, "protocol": "udp", "requests": [], "bytes_out": 0, "bytes_in": 0, "failed": false, "processes": []}, - {"host": null, "address": "10.0.2.2", "port": 8080, "protocol": "tcp", "requests": [], "bytes_out": 0, "bytes_in": 0, "failed": false, "processes": [700]} + { + "host": "pypi.org", + "address": "151.101.192.223", + "port": 443, + "protocol": "tcp", + "requests": [ + { + "method": "GET", + "url": "https://pypi.org/simple/requests/", + "headers": {}, + "body_bytes": null, + "body": null, + "body_encoding": null, + "body_truncated": false + }, + { + "method": "GET", + "url": "https://pypi.org/simple/urllib3/", + "headers": {}, + "body_bytes": null, + "body": null, + "body_encoding": null, + "body_truncated": false + } + ], + "bytes_out": 352, + "bytes_in": 85487, + "failed": false, + "processes": [ + 563 + ] + }, + { + "host": null, + "address": "127.0.0.1", + "port": 9, + "protocol": null, + "requests": [], + "bytes_out": 0, + "bytes_in": 0, + "failed": true, + "processes": [ + 564 + ] + }, + { + "host": "example.com", + "address": "93.184.215.14", + "port": 80, + "protocol": "tcp", + "requests": [ + { + "method": "POST", + "url": "http://example.com/upload", + "headers": {}, + "body_bytes": null, + "body": null, + "body_encoding": null, + "body_truncated": false + } + ], + "bytes_out": 11, + "bytes_in": 0, + "failed": true, + "processes": [] + }, + { + "host": null, + "address": "8.8.8.8", + "port": 123, + "protocol": "udp", + "requests": [], + "bytes_out": 0, + "bytes_in": 0, + "failed": false, + "processes": [] + }, + { + "host": null, + "address": "10.0.2.2", + "port": 8080, + "protocol": "tcp", + "requests": [], + "bytes_out": 0, + "bytes_in": 0, + "failed": false, + "processes": [ + 700 + ] + } ], "processes": [ - {"pid": 563, "path": "/bin/sh", "argv": ["sh", "-c", "cd /app && make"], "exit_code": null, "started_at": 2000, "children": [{"pid": 563, "path": "/usr/bin/make", "argv": ["make"], "exit_code": 2, "started_at": 13000, "children": [{"pid": 564, "path": "/usr/bin/cc", "argv": ["cc", "-o", "build/out", "main.c"], "exit_code": 1, "started_at": 14000, "children": []}, {"pid": 566, "path": "/bin/echo", "argv": ["echo", "done"], "exit_code": 0, "started_at": 15400, "children": []}]}]}, - {"pid": 571, "path": "/usr/bin/python3", "argv": ["python3", "-c", "print(1)"], "exit_code": null, "started_at": 17000, "children": []} + { + "pid": 563, + "path": "/bin/sh", + "argv": [ + "sh", + "-c", + "cd /app && make" + ], + "exit_code": null, + "started_at": 2000, + "children": [ + { + "pid": 563, + "path": "/usr/bin/make", + "argv": [ + "make" + ], + "exit_code": 2, + "started_at": 13000, + "children": [ + { + "pid": 564, + "path": "/usr/bin/cc", + "argv": [ + "cc", + "-o", + "build/out", + "main.c" + ], + "exit_code": 1, + "started_at": 14000, + "children": [] + }, + { + "pid": 566, + "path": "/bin/echo", + "argv": [ + "echo", + "done" + ], + "exit_code": 0, + "started_at": 15400, + "children": [] + } + ] + } + ] + }, + { + "pid": 571, + "path": "/usr/bin/python3", + "argv": [ + "python3", + "-c", + "print(1)" + ], + "exit_code": null, + "started_at": 17000, + "children": [] + } ], "processes_including_internal": [ - {"pid": 563, "path": "/bin/sh", "argv": ["sh", "-c", "cd /app && make"], "exit_code": null, "started_at": 2000, "children": [{"pid": 563, "path": "/usr/bin/make", "argv": ["make"], "exit_code": 2, "started_at": 13000, "children": [{"pid": 564, "path": "/usr/bin/cc", "argv": ["cc", "-o", "build/out", "main.c"], "exit_code": 1, "started_at": 14000, "children": []}, {"pid": 566, "path": "/bin/echo", "argv": ["echo", "done"], "exit_code": 0, "started_at": 15400, "children": []}]}]}, - {"pid": 570, "path": "/usr/lib/vpod/vpod-seed-entropy", "argv": ["vpod-seed-entropy", "ab12"], "exit_code": 0, "started_at": 12000, "children": []}, - {"pid": 571, "path": "/usr/bin/python3", "argv": ["python3", "-c", "print(1)"], "exit_code": null, "started_at": 17000, "children": []} + { + "pid": 563, + "path": "/bin/sh", + "argv": [ + "sh", + "-c", + "cd /app && make" + ], + "exit_code": null, + "started_at": 2000, + "children": [ + { + "pid": 563, + "path": "/usr/bin/make", + "argv": [ + "make" + ], + "exit_code": 2, + "started_at": 13000, + "children": [ + { + "pid": 564, + "path": "/usr/bin/cc", + "argv": [ + "cc", + "-o", + "build/out", + "main.c" + ], + "exit_code": 1, + "started_at": 14000, + "children": [] + }, + { + "pid": 566, + "path": "/bin/echo", + "argv": [ + "echo", + "done" + ], + "exit_code": 0, + "started_at": 15400, + "children": [] + } + ] + } + ] + }, + { + "pid": 570, + "path": "/usr/lib/vpod/vpod-seed-entropy", + "argv": [ + "vpod-seed-entropy", + "ab12" + ], + "exit_code": 0, + "started_at": 12000, + "children": [] + }, + { + "pid": 571, + "path": "/usr/bin/python3", + "argv": [ + "python3", + "-c", + "print(1)" + ], + "exit_code": null, + "started_at": 17000, + "children": [] + } ] }, { "name": "a buffer that overflowed", "events": [ - {"v": 1, "seq": 0, "guest_ns": 1, "wall_ms": 1, "kind": "file.open", "task": "a1", "pid": 563, "path": "/tmp/a", "access": "write", "create": true, "truncate": true, "result": 3}, - {"v": 1, "seq": 1, "guest_ns": 2, "wall_ms": 1, "kind": "trace.dropped", "count": 912}, - {"v": 1, "seq": 2, "guest_ns": 3, "wall_ms": 1, "kind": "file.open", "task": "a1", "pid": 563, "path": "/tmp/b", "access": "read", "create": false, "truncate": false, "result": 3} + { + "v": 1, + "seq": 0, + "guest_ns": 1, + "wall_ms": 1, + "kind": "file.open", + "task": "a1", + "pid": 563, + "path": "/tmp/a", + "access": "write", + "create": true, + "truncate": true, + "result": 3 + }, + { + "v": 1, + "seq": 1, + "guest_ns": 2, + "wall_ms": 1, + "kind": "trace.dropped", + "count": 912 + }, + { + "v": 1, + "seq": 2, + "guest_ns": 3, + "wall_ms": 1, + "kind": "file.open", + "task": "a1", + "pid": 563, + "path": "/tmp/b", + "access": "read", + "create": false, + "truncate": false, + "result": 3 + } ], "complete": false, "files": [ - {"path": "/tmp/a", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/tmp/b", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]} + { + "path": "/tmp/a", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/tmp/b", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + } ], "files_including_internal_and_noise": [ - {"path": "/tmp/a", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]}, - {"path": "/tmp/b", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [563]} + { + "path": "/tmp/a", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + }, + { + "path": "/tmp/b", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 563 + ] + } ], "network": [], "network_including_internal": [], @@ -152,46 +1656,196 @@ { "name": "a guest whose process ids were never calibrated", "events": [ - {"v": 1, "seq": 0, "guest_ns": 1000, "wall_ms": 1, "kind": "process.exec", "task": "a1", "pid": null, "path": "/bin/sh", "argv": ["sh", "-c", "cat data/notes.txt"]}, - {"v": 1, "seq": 1, "guest_ns": 2000, "wall_ms": 1, "kind": "file.open", "task": "a1", "pid": null, "path": "data/notes.txt", "access": "read", "create": false, "truncate": false, "result": 3, "path_unresolved": true} + { + "v": 1, + "seq": 0, + "guest_ns": 1000, + "wall_ms": 1, + "kind": "process.exec", + "task": "a1", + "pid": null, + "path": "/bin/sh", + "argv": [ + "sh", + "-c", + "cat data/notes.txt" + ] + }, + { + "v": 1, + "seq": 1, + "guest_ns": 2000, + "wall_ms": 1, + "kind": "file.open", + "task": "a1", + "pid": null, + "path": "data/notes.txt", + "access": "read", + "create": false, + "truncate": false, + "result": 3, + "path_unresolved": true + } ], "complete": false, "files": [ - {"path": "data/notes.txt", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": []} + { + "path": "data/notes.txt", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [] + } ], "files_including_internal_and_noise": [ - {"path": "data/notes.txt", "read": true, "written": false, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": []} + { + "path": "data/notes.txt", + "read": true, + "written": false, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [] + } ], "network": [], "network_including_internal": [], "processes": [ - {"pid": null, "path": "/bin/sh", "argv": ["sh", "-c", "cat data/notes.txt"], "exit_code": null, "started_at": 1000, "children": []} + { + "pid": null, + "path": "/bin/sh", + "argv": [ + "sh", + "-c", + "cat data/notes.txt" + ], + "exit_code": null, + "started_at": 1000, + "children": [] + } ], "processes_including_internal": [ - {"pid": null, "path": "/bin/sh", "argv": ["sh", "-c", "cat data/notes.txt"], "exit_code": null, "started_at": 1000, "children": []} + { + "pid": null, + "path": "/bin/sh", + "argv": [ + "sh", + "-c", + "cat data/notes.txt" + ], + "exit_code": null, + "started_at": 1000, + "children": [] + } ] }, { "name": "a guest that got behind the tracer with io_uring", "events": [ - {"v": 1, "seq": 0, "guest_ns": 1000, "wall_ms": 1, "kind": "process.exec", "task": "a1", "pid": 601, "ppid": 1, "path": "/usr/bin/node", "argv": ["node", "build.js"]}, - {"v": 1, "seq": 1, "guest_ns": 2000, "wall_ms": 1, "kind": "trace.blind", "task": "a1", "pid": 601, "reason": "io-uring"}, - {"v": 1, "seq": 2, "guest_ns": 3000, "wall_ms": 1, "kind": "file.open", "task": "a1", "pid": 601, "path": "/app/out.js", "access": "write", "create": true, "truncate": true, "result": 3} + { + "v": 1, + "seq": 0, + "guest_ns": 1000, + "wall_ms": 1, + "kind": "process.exec", + "task": "a1", + "pid": 601, + "ppid": 1, + "path": "/usr/bin/node", + "argv": [ + "node", + "build.js" + ] + }, + { + "v": 1, + "seq": 1, + "guest_ns": 2000, + "wall_ms": 1, + "kind": "trace.blind", + "task": "a1", + "pid": 601, + "reason": "io-uring" + }, + { + "v": 1, + "seq": 2, + "guest_ns": 3000, + "wall_ms": 1, + "kind": "file.open", + "task": "a1", + "pid": 601, + "path": "/app/out.js", + "access": "write", + "create": true, + "truncate": true, + "result": 3 + } ], "complete": false, "files": [ - {"path": "/app/out.js", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [601]} + { + "path": "/app/out.js", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 601 + ] + } ], "files_including_internal_and_noise": [ - {"path": "/app/out.js", "read": false, "written": true, "created": false, "deleted": false, "renamed_to": null, "renamed_from": null, "denied": false, "processes": [601]} + { + "path": "/app/out.js", + "read": false, + "written": true, + "created": false, + "deleted": false, + "renamed_to": null, + "renamed_from": null, + "denied": false, + "processes": [ + 601 + ] + } ], "network": [], "network_including_internal": [], "processes": [ - {"pid": 601, "path": "/usr/bin/node", "argv": ["node", "build.js"], "exit_code": null, "started_at": 1000, "children": []} + { + "pid": 601, + "path": "/usr/bin/node", + "argv": [ + "node", + "build.js" + ], + "exit_code": null, + "started_at": 1000, + "children": [] + } ], "processes_including_internal": [ - {"pid": 601, "path": "/usr/bin/node", "argv": ["node", "build.js"], "exit_code": null, "started_at": 1000, "children": []} + { + "pid": 601, + "path": "/usr/bin/node", + "argv": [ + "node", + "build.js" + ], + "exit_code": null, + "started_at": 1000, + "children": [] + } ] } ] diff --git a/sdks/typescript/src/trace.ts b/sdks/typescript/src/trace.ts index 565b6c5a..55182ec7 100644 --- a/sdks/typescript/src/trace.ts +++ b/sdks/typescript/src/trace.ts @@ -3,8 +3,6 @@ export interface TraceSources { files?: boolean; network?: boolean; mounts?: boolean; - /** Keep the headers and body of each traced request. Needs `network`. */ - requestContent?: boolean; bufferBytes?: number; } @@ -15,7 +13,6 @@ export interface WireTraceOptions { files: boolean; network: boolean; mounts: boolean; - requestContent: boolean; bufferBytes: number; } @@ -43,6 +40,11 @@ export interface FileActivity { export interface HttpRequest { method: string; url: string; + headers: Record; + bodyBytes: number | null; + body: string | null; + bodyEncoding: string | null; + bodyTruncated: boolean; } export interface NetworkActivity { @@ -89,20 +91,13 @@ export function traceOptions(setting: TraceSetting | undefined): WireTraceOption return null; } if (setting === true) { - return { - processes: true, - files: true, - network: true, - mounts: true, - requestContent: false, - bufferBytes: 0, - }; + return { processes: true, files: true, network: true, mounts: true, bufferBytes: 0 }; } if (typeof setting !== "object" || setting === null) { throw new Error(`vpod: trace must be true or an object of sources, got ${JSON.stringify(setting)}`); } - const known = new Set([...SOURCES, "requestContent", "bufferBytes"]); + const known = new Set([...SOURCES, "bufferBytes"]); const unknown = Object.keys(setting).filter((key) => !known.has(key)); if (unknown.length > 0) { throw new Error( @@ -110,20 +105,11 @@ export function traceOptions(setting: TraceSetting | undefined): WireTraceOption ); } - const requestContent = setting.requestContent === true; - if (requestContent && setting.network !== true) { - throw new Error( - "vpod: trace requestContent records what each traced request carried, " + - "so it needs network tracing on as well", - ); - } - return { processes: setting.processes === true, files: setting.files === true, network: setting.network === true, mounts: setting.mounts === true, - requestContent, bufferBytes: setting.bufferBytes ?? 0, }; } @@ -324,10 +310,22 @@ export class Trace { network(options: { internal?: boolean } = {}): NetworkActivity[] { const activities = new Map(); + // A body event names the head it belongs to by sequence number. + const requestsBySeq = new Map(); const touching = new Map>(); for (const event of this.#events) { if (event.internal === true && !options.internal) continue; + if (event.kind === "net.http.body") { + const request = requestsBySeq.get(count(event.request_seq)); + if (request !== undefined) { + request.body = text(event.content); + request.bodyEncoding = text(event.encoding); + request.bodyTruncated = event.truncated === true; + } + continue; + } + if (!["net.connect", "net.flow", "net.udp", "net.http"].includes(event.kind)) continue; if (typeof event.address !== "string" || typeof event.port !== "number") continue; @@ -368,7 +366,20 @@ export class Trace { break; case "net.http": entry.host ??= hostOf(String(event.url)); - entry.requests.push({ method: String(event.method), url: String(event.url) }); + { + const request: HttpRequest = { + method: String(event.method), + url: String(event.url), + headers: (event.headers as Record) ?? {}, + bodyBytes: + typeof event.body_bytes === "number" ? event.body_bytes : null, + body: null, + bodyEncoding: null, + bodyTruncated: false, + }; + entry.requests.push(request); + requestsBySeq.set(count(event.seq), request); + } break; } } diff --git a/sdks/typescript/tests/integration/request-content.test.mjs b/sdks/typescript/tests/integration/request-content.test.mjs new file mode 100644 index 00000000..66d447e0 --- /dev/null +++ b/sdks/typescript/tests/integration/request-content.test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { createTestSandbox, skipReason } from "../helpers.mjs"; + +const BODY = '{"model":"claude","prompt":"hello"}'; + +describe("request content", { skip: skipReason() ?? false }, () => { + it("records the headers and body a request carried", async () => { + const sandbox = await createTestSandbox({ trace: { network: true } }); + + let requests; + try { + await sandbox.commands.run( + `wget -q -O- --header='Content-Type: application/json' ` + + `--post-data='${BODY}' https://pypi.org/pypi/six/json > /dev/null 2>&1; true`, + { timeout: 90 }, + ); + requests = (await sandbox.trace.collect()) + .network() + .flatMap((activity) => activity.requests); + } finally { + await sandbox.close(); + } + + const posted = requests.filter((request) => request.method === "POST"); + if (posted.length === 0) { + return; // no route to the network + } + + const request = posted[0]; + assert.equal(request.headers["Content-Type"], "application/json"); + assert.equal(request.headers.Host, "pypi.org"); + assert.equal(request.bodyBytes, BODY.length); + assert.equal(request.body, BODY); + assert.equal(request.bodyEncoding, "utf8"); + assert.equal(request.bodyTruncated, false); + }); +}); From b0a00186be85ab020b80950fc198f5f54249e045 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Sun, 20 Sep 2026 19:01:31 +0200 Subject: [PATCH 3/3] sort imports in tls_proxy tests --- crates/machine/src/virtio/tls_proxy/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/machine/src/virtio/tls_proxy/tests.rs b/crates/machine/src/virtio/tls_proxy/tests.rs index 8a3d6777..7c041bbd 100644 --- a/crates/machine/src/virtio/tls_proxy/tests.rs +++ b/crates/machine/src/virtio/tls_proxy/tests.rs @@ -8,7 +8,7 @@ use rustls::pki_types::ServerName; use std::net::TcpListener; use std::thread; -use crate::trace::{Tracer, TraceOptions}; +use crate::trace::{TraceOptions, Tracer}; const UPSTREAM_REPLY: &[u8] = b"HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nhello";