use crate::handlers::pxcredentials_intelligence;
use crate::modules::pxconstants::*;
use crate::modules::pxutils::{get_risk_headers, set_json_str};
use crate::pxconfig::PXConfig;
use crate::pxcontext::{
BlockReason, CallReason, CookieOrigin, CookieVersion, PXContext, PXDataEnrichment, PassReason,
S2sErrorReason,
};
use crate::{px_debug, px_error};
use fastly::{Error, Request, Response};
use serde_json::json;
use std::time::Instant;
fn send_risk_api(payload: String, conf: &PXConfig) -> Result<Response, Error> {
let body = payload.as_bytes();
let url = format!("https://{}{}", conf.human_sapi_host, RISK_API);
let req = Request::post(url)
.with_header("Authorization", format!("Bearer {}", conf.auth_token))
.with_header("Content-Type", APPLICATION_JSON)
.with_body(body);
Ok(req.send(&conf.human_sapi_backend)?)
}
fn build_risk_payload(ctx: &mut PXContext, conf: &PXConfig) -> String {
let mut payload = json!({
"request": {
"socket_ip": ctx.ip,
"headers": get_risk_headers(&ctx.headers, &conf.sensitive_headers),
"url": ctx.full_url,
},
"additional": {
"s2s_call_reason": ctx
.s2s_call_reason
.as_ref()
.map_or_else(|| CallReason::None.to_string(), ToString::to_string),
"module_version": PX_MODULE_VERSION,
"http_method": ctx.http_method,
"http_version": ctx.http_version,
"risk_mode": ctx.risk_mode,
"cookie_origin": match ctx.cookie_origin {
Some(CookieOrigin::Cookie) | None => "cookie",
Some(CookieOrigin::Header) => "header",
},
"request_cookie_names": ctx.request_cookie_names,
"is_sensitive_route": ctx.is_sensitive_route,
"enforcer_start_time": ctx.enforcer_start_time
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs()),
"risk_start_time": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).ok()
.map(|d| d.as_secs()),
}
});
set_json_str!(&mut payload, "additional.px_cookie_hmac"; ctx.v2_cookie_hash.as_deref().unwrap_or_default());
set_json_str!(&mut payload, "vid"; ctx.vid.as_deref().unwrap_or_default());
set_json_str!(&mut payload, "pxhd"; ctx.get_pxhd().unwrap_or_default());
set_json_str!(&mut payload, "vid_source"; ctx.vid_source.map(|v| v.to_string()).unwrap_or_default());
set_json_str!(&mut payload, "client_uuid"; ctx.uuid.as_deref().unwrap_or_default());
set_json_str!(&mut payload, "additional.cross_tab_session"; ctx.pxcts_cookie.as_deref().unwrap_or_default());
if let Some(app_user_id) = &ctx.app_user_id {
set_json_str!(&mut payload, "additional.app_user_id"; app_user_id);
}
if let Some(jwt_fields) = &ctx.jwt_additional_fields {
if let Some(additional) = payload.get_mut("additional") {
additional["jwt_additional_fields"] = serde_json::Value::Object(jwt_fields.clone());
}
}
if let Some(cookie_json) = &ctx.cookie_json {
set_json_str!(&mut payload, "additional.px_cookie"; cookie_json);
}
if let Some(orig_cookie_vid) = &ctx.orig_cookie_vid {
set_json_str!(&mut payload, "additional.orig_cookie_vid"; orig_cookie_vid);
}
for (key, value) in &ctx.access_cookies {
set_json_str!(&mut payload, &format!("additional.{}", key); value.to_string());
}
if ctx.s2s_call_reason == Some(CallReason::CookieDecryptionFailed) {
if ctx.cookie_version == Some(CookieVersion::V2) {
if let Some(cookie) = ctx.cookies.get("_px2") {
set_json_str!(&mut payload, "additional.px_orig_cookie"; cookie);
}
} else if ctx.cookie_version == Some(CookieVersion::V3) {
if let Some(cookie) = ctx.cookies.get("_px3") {
set_json_str!(&mut payload, "additional.px_orig_cookie"; cookie);
}
}
} else if ctx.s2s_call_reason == Some(CallReason::CookieExpired)
|| ctx.s2s_call_reason == Some(CallReason::CookieValidationFailed)
{
set_json_str!(&mut payload, "additional.px_cookie"; ctx.decoded_v2_cookie.as_deref().unwrap_or_default());
}
if let Some(err) = &ctx.original_token_error {
set_json_str!(
&mut payload, "additional.original_token_error";
err
);
}
set_json_str!(&mut payload, "additional.original_token"; ctx.original_token.as_deref().unwrap_or_default());
set_json_str!(
&mut payload, "additional.custom_param1";
ctx.custom_params.custom_param1
);
set_json_str!(
&mut payload, "additional.custom_param2";
ctx.custom_params.custom_param2
);
set_json_str!(
&mut payload, "additional.custom_param3";
ctx.custom_params.custom_param3
);
set_json_str!(
&mut payload, "additional.custom_param4";
ctx.custom_params.custom_param4
);
set_json_str!(
&mut payload, "additional.custom_param5";
ctx.custom_params.custom_param5
);
set_json_str!(
&mut payload, "additional.custom_param6";
ctx.custom_params.custom_param6
);
set_json_str!(
&mut payload, "additional.custom_param7";
ctx.custom_params.custom_param7
);
set_json_str!(
&mut payload, "additional.custom_param8";
ctx.custom_params.custom_param8
);
set_json_str!(
&mut payload, "additional.custom_param9";
ctx.custom_params.custom_param9
);
set_json_str!(
&mut payload, "additional.custom_param10";
ctx.custom_params.custom_param10
);
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(additional) = payload.get_mut("additional") {
additional["graphql_operations"] = json!(graphql_items);
}
}
if let Some(at) = &ctx.agentic_trust_data {
if let Some(v) = &at.mcp_method {
set_json_str!(&mut payload, "additional.mcp_method"; v);
}
if let Some(v) = &at.mcp_tool_name {
set_json_str!(&mut payload, "additional.mcp_tool_name"; v);
}
if let Some(v) = &at.mcp_tool_argument_keys {
set_json_str!(&mut payload, "additional.mcp_tool_argument_keys"; v);
}
if let Some(v) = &at.mcp_session_id {
set_json_str!(&mut payload, "additional.mcp_session_id"; v);
}
if let Some(v) = &at.mcp_http_method {
set_json_str!(&mut payload, "additional.mcp_http_method"; v);
}
}
set_json_str!(
&mut payload, "additional.request_id";
ctx.request_id.to_string()
);
if let Some(additional) = payload.get_mut("additional") {
pxcredentials_intelligence::apply_ci_fields_to_details(additional, ctx, true, false);
}
payload.to_string()
}
pub(crate) fn verify_risk_response(
ctx: &mut PXContext,
conf: &PXConfig,
parsed_risk_body: &serde_json::Value,
) -> bool {
let action = parsed_risk_body["action"].as_str().unwrap_or("c");
let uuid = parsed_risk_body["uuid"].as_str().unwrap_or_default();
let score = parsed_risk_body["score"].as_u64().unwrap_or_default() as u8;
ctx.telemetry_requested = parsed_risk_body["telemetry_requested"]
.as_bool()
.unwrap_or(false);
if let Some(pxde) = parsed_risk_body.get("data_enrichment") {
let data_enrichment: PXDataEnrichment =
serde_json::from_value::<PXDataEnrichment>(pxde.clone()).unwrap_or_default();
ctx.data_enrichment = Some(data_enrichment);
ctx.pxde = Some(pxde.to_string());
ctx.pxde_verified = true;
}
ctx.additional_risk_info = parsed_risk_body
.get("additional_risk_info")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(additional_token_info) = parsed_risk_body
.get("additional_token_info")
.and_then(|v| v.as_str())
{
ctx.additional_token_info = Some(additional_token_info.to_string());
}
ctx.pxhd_risk = parsed_risk_body
.get("pxhd")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.filter(|s| !s.is_empty());
ctx.pxhd_domain = parsed_risk_body
.get("pxhdDomain")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.filter(|s| !s.is_empty());
ctx.block_action = Some(action.to_string());
ctx.uuid = Some(uuid.to_string());
ctx.score = Some(score);
if ctx.cookie_version == Some(CookieVersion::V2)
&& parsed_risk_body.get("cookie_cfg_block_result").is_some()
{
let block_result = match parsed_risk_body["cookie_cfg_block_result"].as_str() {
Some(c) => c.parse::<u8>().unwrap_or(0) == 1,
None => false,
};
return block_result;
};
px_debug!(
"score: {:?} blocking_score: {}",
ctx.score,
conf.blocking_score
);
ctx.score.unwrap_or(0) >= conf.blocking_score
}
pub fn verify_request(ctx: &mut PXContext, conf: &PXConfig) {
px_debug!(
"Evaluating Risk API request, call reason: {}",
ctx.s2s_call_reason
.as_ref()
.map_or_else(|| CallReason::None.to_string(), ToString::to_string)
);
let payload = build_risk_payload(ctx, conf);
px_debug!("Risk API payload: {}", payload);
let start_risk_rtt = Instant::now();
let risk_response = send_risk_api(payload, conf);
ctx.risk_rtt = Some(start_risk_rtt.elapsed().as_millis() as i64);
let resp = match risk_response {
Ok(r) => r,
Err(e) => {
px_error!("Risk API failed: {}", e);
ctx.pass_reason = Some(PassReason::Error);
return;
}
};
px_debug!("risk response status is {}", resp.get_status());
if resp.get_status() == 200 {
let parsed_risk_body: serde_json::Value =
match serde_json::from_slice(&resp.into_body_bytes()) {
Ok(body) => body,
Err(e) => {
px_error!("Failed to parse Risk API response: {}", e);
ctx.pass_reason = Some(PassReason::Error);
ctx.s2s_error_reason = Some(S2sErrorReason::InvalidResponse);
return;
}
};
if parsed_risk_body.is_object() && parsed_risk_body.get("status").is_some_and(|v| v == 0) {
let block_result = verify_risk_response(ctx, conf, &parsed_risk_body);
if block_result {
px_debug!("Block based on risk response - {}", parsed_risk_body);
ctx.block_reason = Some(BlockReason::ServerScore);
} else {
px_debug!("Risk response non block - {}", parsed_risk_body);
ctx.pass_reason = Some(PassReason::S2s);
}
} else {
ctx.pass_reason = Some(PassReason::Error);
if let Some(message) = parsed_risk_body.get("message") {
ctx.s2s_error_reason = Some(S2sErrorReason::FailedOnServer);
if let Some(msg_str) = message.as_str() {
ctx.s2s_error_message = Some(String::from(msg_str));
}
} else {
ctx.s2s_error_reason = Some(S2sErrorReason::InvalidResponse);
}
if let Some(uuid) = parsed_risk_body.get("uuid").and_then(|v| v.as_str()) {
ctx.uuid = Some(String::from(uuid));
}
px_error!(
"Unexpected exception in Risk API call: {}",
parsed_risk_body
);
}
} else {
px_error!("Risk API failed with status: {}", resp.get_status());
ctx.pass_reason = Some(PassReason::Error);
ctx.s2s_error_http_status = Some(resp.get_status().as_u16());
if 400 <= resp.get_status().as_u16() && resp.get_status().as_u16() < 500 {
ctx.s2s_error_reason = Some(S2sErrorReason::BadRequest);
} else if 500 <= resp.get_status().as_u16() && resp.get_status().as_u16() < 600 {
ctx.s2s_error_reason = Some(S2sErrorReason::ServerError);
} else {
ctx.s2s_error_reason = Some(S2sErrorReason::Unknown);
}
}
}