soth-mitm 0.3.3

Rust intercepting proxy crate with deterministic handler/event contracts for SOTH.
Documentation
use std::collections::BTreeMap;

use soth_mitm::test_engine::{MitmConfig, MitmEngine};
use soth_mitm::test_observe::{EventType, VecEventConsumer};
use soth_mitm::test_policy::DefaultPolicyEngine;
use soth_mitm::test_protocol::ApplicationProtocol;
use soth_mitm::test_server::{
    MitmproxyTlsCallback, MitmproxyTlsHook, SidecarConfig, SidecarServer,
};
use soth_mitm::FlowId;

fn build_engine(
    config: MitmConfig,
    sink: VecEventConsumer,
) -> MitmEngine<DefaultPolicyEngine, VecEventConsumer> {
    let policy =
        DefaultPolicyEngine::new(config.ignore_hosts.clone(), config.blocked_hosts.clone());
    MitmEngine::new_checked(config, policy, sink).expect("valid test config")
}

fn build_sidecar(sink: VecEventConsumer) -> SidecarServer<DefaultPolicyEngine, VecEventConsumer> {
    let engine = build_engine(MitmConfig::default(), sink);
    SidecarServer::new(SidecarConfig::default(), engine).expect("build sidecar")
}

fn metadata_for_flow(
    events: &[soth_mitm::test_observe::Event],
    flow_id: FlowId,
) -> BTreeMap<String, String> {
    events
        .iter()
        .find(|event| {
            event.kind == EventType::TlsHandshakeFailed && event.context.flow_id == flow_id
        })
        .expect("missing tls failed event")
        .attributes
        .clone()
}

#[test]
fn upstream_revocation_metadata_matrix_emits_stable_fields() {
    let sink = VecEventConsumer::default();
    let server = build_sidecar(sink.clone());

    let fixtures = vec![
        (
            FlowId(201),
            "OCSP response required but missing",
            "false",
            "missing",
            "signal_missing_staple",
        ),
        (
            FlowId(202),
            "upstream rejected malformed OCSP stapling parse error",
            "true",
            "invalid",
            "signal_invalid_staple",
        ),
        (
            FlowId(203),
            "certificate revoked by OCSP responder",
            "unknown",
            "revoked",
            "signal_revoked",
        ),
        (
            FlowId(204),
            "OCSP stapling status: good",
            "true",
            "present",
            "signal_present",
        ),
        (
            FlowId(205),
            "OCSP response expired while validating chain",
            "true",
            "invalid",
            "signal_invalid_staple",
        ),
    ];

    for (flow_id, detail, _, _, _) in &fixtures {
        server.ingest_mitmproxy_tls_callback(MitmproxyTlsCallback {
            flow_id: *flow_id,
            client_addr: "127.0.0.1:50000".to_string(),
            server_host: "api.example.com".to_string(),
            server_port: 443,
            protocol: ApplicationProtocol::Http1,
            hook: MitmproxyTlsHook::TlsFailedServer,
            error: Some((*detail).to_string()),
            provider_error_class: Some("TlsException".to_string()),
            provider_error_code: Some(format!("ERR_{flow_id}")),
            provider_error_detail: Some((*detail).to_string()),
        });
    }

    let events = sink.snapshot();
    for (flow_id, _, expected_present, expected_status, expected_decision) in fixtures {
        let attrs = metadata_for_flow(&events, flow_id);
        assert_eq!(
            attrs
                .get("upstream_ocsp_staple_present")
                .map(String::as_str),
            Some(expected_present)
        );
        assert_eq!(
            attrs.get("upstream_ocsp_staple_status").map(String::as_str),
            Some(expected_status)
        );
        assert_eq!(
            attrs.get("revocation_policy_mode").map(String::as_str),
            Some("passive_observe")
        );
        assert_eq!(
            attrs.get("revocation_decision").map(String::as_str),
            Some(expected_decision)
        );
    }
}

#[test]
fn downstream_tls_failure_marks_revocation_not_applicable() {
    let sink = VecEventConsumer::default();
    let server = build_sidecar(sink.clone());

    server.ingest_mitmproxy_tls_callback(MitmproxyTlsCallback {
        flow_id: FlowId(301),
        client_addr: "127.0.0.1:50001".to_string(),
        server_host: "api.example.com".to_string(),
        server_port: 443,
        protocol: ApplicationProtocol::Http1,
        hook: MitmproxyTlsHook::TlsFailedClient,
        error: Some("certificate verify failed: unknown ca".to_string()),
        provider_error_class: Some("TlsException".to_string()),
        provider_error_code: Some("ERR_301".to_string()),
        provider_error_detail: Some("certificate verify failed: unknown ca".to_string()),
    });

    let events = sink.snapshot();
    let attrs = metadata_for_flow(&events, FlowId(301));
    assert_eq!(
        attrs
            .get("upstream_ocsp_staple_present")
            .map(String::as_str),
        Some("not_applicable")
    );
    assert_eq!(
        attrs.get("upstream_ocsp_staple_status").map(String::as_str),
        Some("not_applicable")
    );
    assert_eq!(
        attrs.get("revocation_policy_mode").map(String::as_str),
        Some("passive_observe")
    );
    assert_eq!(
        attrs.get("revocation_decision").map(String::as_str),
        Some("not_applicable")
    );
}