perimeterx-fastly-enforcer 2.2.2

PerimeterX Fastly Compute@Edge Rust Enforcer
Documentation
use crate::handlers::pxcredentials_intelligence;
use crate::modules::pxconstants::*;
use crate::modules::pxutils::{get_risk_headers, set_json_int, set_json_str};
use crate::px_debug;
use crate::pxconfig::PXConfig;
use crate::pxcontext::{
    BlockReason, CallReason, CookieOrigin, PXContext, PassReason, S2sErrorReason,
};
use fastly::Request;
use serde_json::json;

fn post_activity_async(
    activity_type: &str,
    details: &mut serde_json::value::Value,
    ctx: &PXContext,
    conf: &PXConfig,
) {
    let cookie_origin = match ctx.cookie_origin {
        Some(CookieOrigin::Cookie) => "cookie",
        Some(CookieOrigin::Header) => "header",
        None => "",
    };

    set_json_str!(details, "cookie_origin"; cookie_origin);
    set_json_str!(details, "module_version"; PX_MODULE_VERSION);
    set_json_int!(details, "risk_rtt"; ctx.risk_rtt.unwrap_or(0));
    set_json_int!(details, "enforcer_start_time"; ctx.enforcer_start_time
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
    );
    details["is_sensitive_route"] = json!(ctx.is_sensitive_route);

    set_json_str!(details, "http_method"; ctx.http_method);
    set_json_str!(details, "http_version"; ctx.http_version);
    set_json_str!(details, "custom_param1"; ctx.custom_params.custom_param1);
    set_json_str!(details, "custom_param2"; ctx.custom_params.custom_param2);
    set_json_str!(details, "custom_param3"; ctx.custom_params.custom_param3);
    set_json_str!(details, "custom_param4"; ctx.custom_params.custom_param4);
    set_json_str!(details, "custom_param5"; ctx.custom_params.custom_param5);
    set_json_str!(details, "custom_param6"; ctx.custom_params.custom_param6);
    set_json_str!(details, "custom_param7"; ctx.custom_params.custom_param7);
    set_json_str!(details, "custom_param8"; ctx.custom_params.custom_param8);
    set_json_str!(details, "custom_param9"; ctx.custom_params.custom_param9);
    set_json_str!(details, "custom_param10"; ctx.custom_params.custom_param10);

    set_json_str!(details, "risk_mode"; ctx.risk_mode);

    if let Some(ref s2s_call_reason) = ctx.s2s_call_reason {
        if s2s_call_reason != &CallReason::None {
            set_json_str!(details, "s2s_call_reason"; s2s_call_reason);
        }
    }

    if !ctx.request_cookie_names.is_empty() {
        set_json_str!(details, "request_cookie_names"; ctx.request_cookie_names.join(","));
    }

    set_json_str!(details, "cross_tab_session"; ctx.pxcts_cookie.as_deref().unwrap_or_default());
    if let Some(app_user_id) = &ctx.app_user_id {
        set_json_str!(details, "app_user_id"; app_user_id);
    }
    if let Some(jwt_fields) = &ctx.jwt_additional_fields {
        details["jwt_additional_fields"] = serde_json::Value::Object(jwt_fields.clone());
    }
    if let Some(additional_risk_info) = &ctx.additional_risk_info {
        details["additional_risk_info"] = json!(additional_risk_info);
    }
    if let Some(additional_token_info) = &ctx.additional_token_info {
        details["additional_token_info"] = json!(additional_token_info);
    }

    if let Some(orig_cookie_vid) = &ctx.orig_cookie_vid {
        details["orig_cookie_vid"] = json!(orig_cookie_vid);
    }

    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0);

    pxcredentials_intelligence::apply_ci_fields_to_details(details, ctx, true, true);

    if let Some(at) = &ctx.agentic_trust_data {
        if let Some(v) = &at.mcp_method {
            set_json_str!(details, "mcp_method"; v);
        }
        if let Some(v) = &at.mcp_tool_name {
            set_json_str!(details, "mcp_tool_name"; v);
        }
        if let Some(v) = &at.mcp_tool_argument_keys {
            set_json_str!(details, "mcp_tool_argument_keys"; v);
        }
        if let Some(v) = &at.mcp_session_id {
            set_json_str!(details, "mcp_session_id"; v);
        }
        if let Some(v) = &at.mcp_http_method {
            set_json_str!(details, "mcp_http_method"; v);
        }
    }

    let mut request_body = json!({
        "timestamp": timestamp,
        "type": activity_type,
        "headers": get_risk_headers(&ctx.headers, &conf.sensitive_headers),
        "socket_ip": ctx.ip,
        "px_app_id": conf.app_id,
        "url": ctx.full_url,
        "pxhd": ctx.get_pxhd().unwrap_or_default(),
        "details": details
    });

    set_json_str!(&mut request_body, "vid"; ctx.vid.as_deref().unwrap_or_default());

    if !ctx.graphql_extracted_items.is_empty() {
        let mut graphql_items = vec![];
        for item in &ctx.graphql_extracted_items {
            let graphql_item = json!({
                "type": item.op_type,
                "name": item.name,
                "sensitive": item.sensitive,
                "variables": item.variables,
                "keywords": item.keywords
            });
            graphql_items.push(graphql_item);
        }
        if let Some(details_obj) = request_body.get_mut("details") {
            if let Ok(graphql_ops) = serde_json::to_value(graphql_items) {
                details_obj["graphql_operations"] = graphql_ops;
            }
        }
    }

    let body = request_body.to_string();
    px_debug!("activity body: {}", body);
    let body_bytes = body.as_bytes();

    let url = format!("https://{}{}", conf.human_collector_host, ACTIVITY_API);

    let req = Request::post(url)
        .with_header("Authorization", format!("Bearer {}", conf.auth_token))
        .with_header("Content-Type", APPLICATION_JSON)
        .with_body(body_bytes)
        .send_async(&conf.human_collector_backend);
    match req {
        Ok(r) => {
            r.poll();
        }
        Err(e) => px_debug!("Error sending Activity: {}", e),
    }
}

pub fn send_page_requested_activity(ctx: &PXContext, conf: &PXConfig) {
    let mut details = json!({
            "client_uuid": ctx.uuid.as_deref().unwrap_or_default(),
            "pass_reason": ctx
                .pass_reason
                .as_ref()
                .map_or_else(|| PassReason::None.to_string(), ToString::to_string),
            "request_id": ctx.request_id.to_string(),
    });
    if let Some(cookie_json) = &ctx.cookie_json {
        set_json_str!(&mut details, "px_cookie"; cookie_json);
    }
    if let Some(reason) = &ctx.s2s_error_reason {
        if reason != &S2sErrorReason::None {
            set_json_str!(&mut details, "s2s_error_reason"; reason);
        }
    }
    if let Some(status) = ctx.s2s_error_http_status {
        set_json_int!(&mut details, "s2s_error_http_status"; status);
    }
    set_json_str!(&mut details, "s2s_error_message"; ctx.s2s_error_message.as_deref().unwrap_or_default());

    post_activity_async("page_requested", &mut details, ctx, conf);
}

pub fn send_block_activity(ctx: &PXContext, conf: &PXConfig) {
    let simulated_block = ctx.is_simulated_block;
    let mut details = json!({
        "client_uuid": ctx.uuid.as_deref().unwrap_or_default(),
        "block_reason": ctx
            .block_reason
            .as_ref()
            .map_or_else(|| BlockReason::None.to_string(), ToString::to_string),
        "block_action": ctx.block_action.as_deref().unwrap_or("c"),
        "block_score": ctx.score.unwrap_or(0),
        "simulated_block": simulated_block,
        "request_id": ctx.request_id.to_string(),
    });

    post_activity_async("block", &mut details, ctx, conf);
}