Skip to main content

har/analysis/
extract.rs

1use crate::filter::Filter;
2use crate::jsonpath;
3use crate::model::{Capture, Entry};
4use crate::opaque::is_opaque;
5use crate::redact::REDACTED;
6use serde::Serialize;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Target {
10    Req,
11    Resp,
12}
13
14#[derive(Debug, Serialize)]
15pub struct ExtractResult {
16    pub values: Vec<ExtractValue>,
17}
18
19#[derive(Debug, Serialize)]
20pub struct ExtractValue {
21    pub id: String,
22    pub value: String,
23}
24
25fn body_of(e: &Entry, target: Target) -> Option<&str> {
26    let b = match target {
27        Target::Req => &e.req_body,
28        Target::Resp => &e.resp_body,
29    };
30    b.as_deref().filter(|s| !s.is_empty())
31}
32
33fn stringify(v: &serde_json::Value) -> String {
34    match v {
35        serde_json::Value::String(s) => s.clone(),
36        other => other.to_string(),
37    }
38}
39
40/// Extract a JSON-path value from request/response bodies across entries.
41pub fn compute_extract(
42    cap: &Capture,
43    filter: &Filter,
44    path: &str,
45    target: Target,
46    top: usize,
47    unsafe_include: bool,
48) -> ExtractResult {
49    let mut values = Vec::new();
50    for e in cap.entries.iter().filter(|e| filter.matches(e)) {
51        let Some(body) = body_of(e, target) else {
52            continue;
53        };
54        let Ok(json) = serde_json::from_str::<serde_json::Value>(body) else {
55            continue;
56        };
57        for v in jsonpath::eval(&json, path) {
58            let s = stringify(&v);
59            let shown = if !unsafe_include && is_opaque(&s) {
60                REDACTED.to_string()
61            } else {
62                s
63            };
64            values.push(ExtractValue {
65                id: e.id.clone(),
66                value: shown,
67            });
68            if values.len() >= top {
69                return ExtractResult { values };
70            }
71        }
72    }
73    ExtractResult { values }
74}
75
76/// Render extracted values as deterministic terminal text.
77pub fn render_extract_text(r: &ExtractResult) -> String {
78    let mut out = String::new();
79    out.push_str("== wiretrail extract ==\n");
80    for v in &r.values {
81        out.push_str(&format!("{}  {}\n", v.id, v.value));
82    }
83    out
84}
85
86#[cfg(test)]
87mod tests {
88    use super::{Target, compute_extract};
89    use crate::filter::Filter;
90    use crate::model::{Entry, sample_capture, sample_entry};
91
92    fn with_resp(index: usize, body: &str) -> Entry {
93        let mut e = sample_entry(index, "api.x", "GET", "/a", 200);
94        e.resp_body = Some(body.to_string());
95        e
96    }
97
98    #[test]
99    fn extracts_field_from_response_bodies() {
100        let cap = sample_capture(vec![
101            with_resp(0, r#"{"error":{"message":"boom"}}"#),
102            with_resp(1, r#"{"error":{"message":"nope"}}"#),
103        ]);
104        let r = compute_extract(
105            &cap,
106            &Filter::parse(&[]).unwrap(),
107            "$.error.message",
108            Target::Resp,
109            10,
110            false,
111        );
112        let vals: Vec<&str> = r.values.iter().map(|v| v.value.as_str()).collect();
113        assert!(vals.contains(&"boom"));
114        assert!(vals.contains(&"nope"));
115    }
116
117    #[test]
118    fn masks_opaque_value_by_default() {
119        let cap = sample_capture(vec![with_resp(
120            0,
121            r#"{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}"#,
122        )]);
123        let r = compute_extract(
124            &cap,
125            &Filter::parse(&[]).unwrap(),
126            "$.token",
127            Target::Resp,
128            10,
129            false,
130        );
131        assert_eq!(r.values[0].value, "<redacted>");
132        let r2 = compute_extract(
133            &cap,
134            &Filter::parse(&[]).unwrap(),
135            "$.token",
136            Target::Resp,
137            10,
138            true,
139        );
140        assert!(r2.values[0].value.starts_with("eyJ"));
141    }
142}