Skip to main content

perimeterx_fastly_enforcer/
pxcontext.rs

1use super::pxconfig::{PXConfig, PXCustomParams};
2pub use crate::handlers::pxagentic_trust::AgenticTrustData;
3pub use crate::handlers::pxcredentials_intelligence::PXCredentialIntelligenceData;
4use crate::handlers::pxcrypto;
5use crate::handlers::pxgraphql::PXGraphQLExtractedItem;
6use crate::modules::{pxconstants::*, pxutils};
7use crate::px_debug;
8use base64::{Engine as _, engine::general_purpose};
9use fastly::Request;
10use fastly::http::header::COOKIE;
11use serde::Deserialize;
12use serde::Serialize;
13use std::collections::HashMap;
14use std::fmt;
15use strum_macros::{AsRefStr, Display, EnumString};
16use uuid::Uuid;
17
18#[derive(Debug, PartialEq, Default)]
19pub enum CallReason {
20    #[default]
21    None,
22    NoCookie,
23    NoCookieWVid,
24    CookieDecryptionFailed,
25    CookieValidationFailed,
26    CookieExpired,
27    SensitiveRoute,
28    MobileSdkConnectionError,
29    MobileError1,
30    MobileError2,
31    MobileError3,
32    MobileError4,
33}
34impl fmt::Display for CallReason {
35    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36        match self {
37            CallReason::None => {
38                write!(f, "none")
39            }
40            CallReason::NoCookie => {
41                write!(f, "no_cookie")
42            }
43            CallReason::NoCookieWVid => {
44                write!(f, "no_cookie_w_vid")
45            }
46            CallReason::CookieDecryptionFailed => {
47                write!(f, "cookie_decryption_failed")
48            }
49            CallReason::CookieValidationFailed => {
50                write!(f, "cookie_validation_failed")
51            }
52            CallReason::CookieExpired => {
53                write!(f, "cookie_expired")
54            }
55            CallReason::SensitiveRoute => {
56                write!(f, "sensitive_route")
57            }
58            CallReason::MobileSdkConnectionError => {
59                write!(f, "mobile_sdk_connection_error")
60            }
61            CallReason::MobileError1 => {
62                write!(f, "mobile_error_1")
63            }
64            CallReason::MobileError2 => {
65                write!(f, "mobile_error_2")
66            }
67            CallReason::MobileError3 => {
68                write!(f, "mobile_error_3")
69            }
70            CallReason::MobileError4 => {
71                write!(f, "mobile_error_4")
72            }
73        }
74    }
75}
76
77impl CallReason {
78    pub(crate) fn is_mobile_sdk_error(&self) -> bool {
79        matches!(
80            self,
81            CallReason::MobileError1
82                | CallReason::MobileError2
83                | CallReason::MobileError3
84                | CallReason::MobileError4
85                | CallReason::MobileSdkConnectionError
86        )
87    }
88}
89
90#[derive(PartialEq, Default)]
91pub enum PassReason {
92    #[default]
93    None,
94    Cookie,
95    Error,
96    S2s,
97}
98
99impl fmt::Display for PassReason {
100    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
101        match self {
102            PassReason::None => {
103                write!(f, "none")
104            }
105            PassReason::Cookie => {
106                write!(f, "cookie")
107            }
108            PassReason::Error => {
109                write!(f, "s2s_error")
110            }
111            PassReason::S2s => {
112                write!(f, "s2s")
113            }
114        }
115    }
116}
117
118#[derive(PartialEq, Default)]
119pub enum BlockReason {
120    #[default]
121    None,
122    CookieScore,
123    ServerScore,
124    Challenge,
125}
126impl fmt::Display for BlockReason {
127    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
128        match self {
129            BlockReason::None => {
130                write!(f, "none")
131            }
132            BlockReason::CookieScore => {
133                write!(f, "cookie_high_score")
134            }
135            BlockReason::ServerScore => {
136                write!(f, "s2s_high_score")
137            }
138            BlockReason::Challenge => {
139                write!(f, "challenge")
140            }
141        }
142    }
143}
144
145#[derive(Default, PartialEq)]
146pub enum S2sErrorReason {
147    #[default]
148    None,
149    FailedOnServer,
150    InvalidResponse,
151    BadRequest,
152    ServerError,
153    Unknown,
154}
155impl fmt::Display for S2sErrorReason {
156    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157        match self {
158            S2sErrorReason::None => {
159                write!(f, "none")
160            }
161            S2sErrorReason::FailedOnServer => {
162                write!(f, "request_failed_on_server")
163            }
164            S2sErrorReason::InvalidResponse => {
165                write!(f, "invalid_response")
166            }
167            S2sErrorReason::BadRequest => {
168                write!(f, "bad_request")
169            }
170            S2sErrorReason::ServerError => {
171                write!(f, "server_error")
172            }
173            S2sErrorReason::Unknown => {
174                write!(f, "unknown_error")
175            }
176        }
177    }
178}
179
180/// Working mode of the Enforcer
181#[derive(Debug, Default, PartialEq, Clone, Copy, AsRefStr, Display, EnumString)]
182pub enum PXModuleMode {
183    #[default]
184    #[strum(serialize = "monitor")]
185    Monitor,
186    #[strum(
187        serialize = "active_blocking",
188        serialize = "blocking",
189        serialize = "blocked"
190    )]
191    Blocking,
192}
193
194#[derive(Debug, Default, PartialEq, Clone, Copy, AsRefStr, Display, EnumString)]
195pub enum TokenVersion {
196    #[strum(serialize = "2")]
197    V2,
198    #[default]
199    #[strum(serialize = "3")]
200    V3,
201}
202
203#[derive(Debug, Default, PartialEq)]
204pub enum CookieOrigin {
205    #[default]
206    Cookie,
207    Header,
208}
209
210#[derive(Default, PartialEq)]
211pub enum CookieVersion {
212    V2,
213    #[default]
214    V3,
215}
216
217/// How the visitor ID (`vid`) was obtained for Risk API / activity payloads.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRefStr, Display, EnumString)]
219pub enum VidSource {
220    #[strum(serialize = "vid_cookie")]
221    VidCookie,
222    #[strum(serialize = "risk_cookie")]
223    RiskCookie,
224}
225
226#[derive(Debug, Serialize)]
227pub(crate) struct RiskHeader {
228    pub name: String,
229    pub value: serde_json::Value,
230}
231
232/// Data Enrichment, available after calling `PXEnforcer::enforce()`
233#[derive(Deserialize, Debug, Default)]
234pub struct PXDataEnrichment {
235    pub(crate) timestamp: Option<i64>,
236    pub(crate) f_kb: Option<i8>,
237    pub(crate) f_type: Option<String>,
238    pub(crate) f_id: Option<String>,
239    pub(crate) f_origin: Option<String>,
240    pub(crate) ipc_id: Option<Vec<i32>>,
241    pub(crate) breached_account: Option<i8>,
242    pub(crate) f_access_token: Option<String>,
243    pub(crate) inc_id: Option<Vec<i32>>,
244}
245
246/// Enforcer context for the current request.
247/// Some fields are available after calling `PXEnforcer::enforce()`
248/// use corresponding getter methods to access the fields
249#[allow(dead_code)]
250#[derive(Default)]
251pub struct PXContext {
252    pub(crate) http_method: String,
253    /// Inbound HTTP version string sent on Risk API and async activities
254    pub(crate) http_version: String,
255    /// Client request headers (sensitive headers stripped) for Risk API and activities
256    pub(crate) headers: serde_json::value::Value,
257    /// Parsed request cookies merged from `Cookie` and the custom cookie header
258    pub(crate) cookies: HashMap<String, String>,
259    /// Names of all cookies on the request, sent on Risk API and async activities
260    pub(crate) request_cookie_names: Vec<String>,
261    /// Specially forwarded access cookies (e.g. `_pxac`) included on Risk API when present
262    pub(crate) access_cookies: HashMap<String, String>,
263    /// Request hostname derived from the HTTP Host header
264    pub(crate) hostname: String,
265    /// Full request URL after decoding/normalization for Risk API context
266    pub(crate) full_url: String,
267    /// Client User-Agent string, subject to max-length alignment with cookie signing rules
268    pub(crate) user_agent: String,
269    /// Real client IP from configured trusted headers or the platform's direct peer address
270    pub(crate) ip: String,
271    /// Unique ID for this request, carried on Risk API and telemetry
272    pub(crate) request_id: Uuid,
273
274    /// Whether this request targets a sensitive route requiring a Risk API call even with a valid cookie
275    pub(crate) is_sensitive_route: bool,
276    /// Whether this request is enforced (full blocking workflow even in monitor mode)
277    pub(crate) is_enforced_request: bool,
278    /// Whether this request is monitored (simulated blocks in active-blocking mode)
279    pub(crate) is_monitored_request: bool,
280    /// Effective risk mode sent on Risk API and async activities (`monitor` or `active_blocking`)
281    pub(crate) risk_mode: PXModuleMode,
282    /// Whether a Block activity represents a simulated block (monitor mode or monitored route)
283    pub(crate) is_simulated_block: bool,
284
285    /// Visitor ID: from the risk cookie `v` field, or from the `_pxvid` cookie
286    pub(crate) vid: Option<String>,
287    /// HUMAN user UUID from Risk API or cookie-derived identity, used in enforcement and templates
288    pub(crate) uuid: Option<String>,
289    /// Client-supplied `_pxhd` cookie linking early risk traffic to later sensor activity
290    pub(crate) pxhd_cookie: Option<String>,
291    /// PXHD value returned by the Risk API response to refresh client `_pxhd` tracking
292    pub(crate) pxhd_risk: Option<String>,
293    /// Optional cookie `Domain` attribute from Risk API `pxhdDomain`
294    pub(crate) pxhd_domain: Option<String>,
295    /// How `vid` was obtained; `None` if unknown
296    pub(crate) vid_source: Option<VidSource>,
297    /// Raw `_pxvid` cookie value when present but failing UUID validation
298    pub(crate) orig_cookie_vid: Option<String>,
299
300    /// HTTP status code from a failed Risk API transport/response; `None` when there was no HTTP error
301    pub(crate) s2s_error_http_status: Option<u16>,
302    /// Human-readable error detail for a failed Risk API outcome
303    pub(crate) s2s_error_message: Option<String>,
304    /// Milliseconds spent on the synchronous Risk API round-trip (only set when a call was made)
305    pub(crate) risk_rtt: Option<i64>,
306    /// Single-character enforcement action from Risk API or cookie (`c`=captcha, `b`=block, `r`=ratelimit)
307    pub(crate) block_action: Option<String>,
308    /// Numeric bot-likelihood score (0–100) compared to `px_blocking_score` to decide pass vs block
309    pub(crate) score: Option<u8>,
310    /// Customer-defined `custom_param1`…`custom_param10` merged into Risk API and async activity payloads
311    pub(crate) custom_params: PXCustomParams,
312    /// Timestamp when the enforcer process started, included on Risk API and async activities
313    pub(crate) enforcer_start_time: Option<std::time::SystemTime>,
314    /// Opaque string from Risk API `additional_risk_info`, forwarded into async activities
315    pub(crate) additional_risk_info: Option<String>,
316    /// Extra token payload fragment from advanced cookie/mobile token parsing (cookie `add` field)
317    pub(crate) additional_token_info: Option<String>,
318    /// UTF-8 JSON string of the decrypted v3 risk cookie, sent as `px_cookie` on Risk API
319    pub(crate) cookie_json: Option<String>,
320
321    /// Risk-cookie wire format for this request (`_px2` = V2, `_px3` = V3); `None` until cookie verification runs
322    pub(crate) cookie_version: Option<CookieVersion>,
323    /// Whether the risk token came from browser cookies or a mobile SDK header; `None` if unknown
324    pub(crate) cookie_origin: Option<CookieOrigin>,
325    /// Inbound HTTP method sent on Risk API and async activities
326
327    /// Raw `x-px-original-token` value from the Mobile SDK when `x-px-authorization` reports an error
328    pub(crate) original_token: Option<String>,
329    /// Reason the original mobile token could not be decrypted/validated; `None` when not applicable
330    pub(crate) original_token_error: Option<CallReason>,
331
332    /// HMAC integrity material from the decrypted v2 risk cookie
333    pub(crate) v2_cookie_hash: Option<String>,
334    /// Decrypted/decoded v2 risk-cookie JSON payload used for validation before trusting cookie-based scoring
335    pub(crate) decoded_v2_cookie: Option<String>,
336
337    /// Reason the Risk API was invoked (e.g. `no_cookie`, `cookie_expired`, `sensitive_route`); `None` when unset
338    pub(crate) s2s_call_reason: Option<CallReason>,
339    /// Taxonomy for a failed Risk API outcome; `None` when there was no S2S error classification
340    pub(crate) s2s_error_reason: Option<S2sErrorReason>,
341    /// Why the request was allowed; `None` when unset
342    pub(crate) pass_reason: Option<PassReason>,
343    /// Why the request was blocked; `None` when unset
344    pub(crate) block_reason: Option<BlockReason>,
345
346    /// Parsed GraphQL operations (type, name, sensitivity) sent as `graphql_operations` on activities
347    pub(crate) graphql_extracted_items: Vec<PXGraphQLExtractedItem>,
348
349    /// MCP request metadata captured by Agentic Trust enrichment; `None` when the request
350    /// did not match the configured MCP endpoint.
351    pub(crate) agentic_trust_data: Option<AgenticTrustData>,
352
353    /// Risk API `data_enrichment` object with collector-side enrichment fields
354    pub(crate) data_enrichment: Option<PXDataEnrichment>,
355    /// Parsed PXDE JSON from `_pxde` cookie or Risk API `data_enrichment`, after base64/HMAC handling
356    pub(crate) pxde: Option<String>,
357    /// Whether PXDE was HMAC-verified or trusted from an authenticated Risk API response
358    pub(crate) pxde_verified: bool,
359
360    /// Raw `pxcts` Cross Tab Session token for `cross_tab_session` on Risk/activity payloads
361    pub(crate) pxcts_cookie: Option<String>,
362    /// App user ID extracted from a configured JWT cookie/header payload field
363    pub(crate) app_user_id: Option<String>,
364    /// Additional JWT payload fields keyed by their configured dot-notated paths
365    pub(crate) jwt_additional_fields: Option<serde_json::Map<String, serde_json::Value>>,
366
367    /// Risk response flag requesting the enforcer to send an `enforcer_telemetry` activity
368    pub(crate) telemetry_requested: bool,
369
370    /// Whether to postpone sending the async activity until a later point in the request lifecycle (e.g. after the response is sent)
371    pub(crate) postpone_activities: bool,
372
373    /// Credentials Intelligence extraction/hashing result for this login request.
374    pub(crate) credential_intelligence: Option<PXCredentialIntelligenceData>,
375}
376
377impl PXContext {
378    pub fn new(req: &Request, conf: &PXConfig) -> Self {
379        let mut cookie_origin = CookieOrigin::Cookie;
380        let mut cookies = HashMap::new();
381        let mut access_cookies = HashMap::new();
382        let mut request_cookie_names: Vec<String> = vec![];
383        let mut original_token = String::new();
384        let mut s2s_call_reason = None;
385        let mut risk_mode = conf.module_mode;
386
387        // check if the request has the bypass monitor header set to "1" and the module mode is monitor
388        let should_bypass_monitor = conf.module_mode == PXModuleMode::Monitor
389            && !conf.bypass_monitor_header.is_empty()
390            && req
391                .get_header_str_lossy(&conf.bypass_monitor_header)
392                .map(|v| v.into_owned())
393                .unwrap_or_default()
394                == "1";
395
396        if conf.module_mode == PXModuleMode::Monitor && should_bypass_monitor {
397            risk_mode = PXModuleMode::Blocking;
398            px_debug!("Bypass monitor header set in monitor mode, forcing risk mode to blocking");
399        }
400
401        let is_enforced_request = risk_mode == PXModuleMode::Monitor
402            && (pxutils::verify_route(&conf.enforced_routes, req.get_path())
403                || conf
404                    .is_enforced_request_fn
405                    .map(|f| f(req, conf))
406                    .unwrap_or(false));
407
408        if conf.module_mode == PXModuleMode::Monitor && is_enforced_request {
409            px_debug!("Enforced request detected in monitor mode, forcing risk mode to blocking");
410        }
411
412        let is_monitored_request = risk_mode == PXModuleMode::Blocking
413            && (pxutils::verify_route(&conf.monitored_routes, req.get_path())
414                || conf
415                    .is_monitored_request_fn
416                    .map(|f| f(req, conf))
417                    .unwrap_or(false));
418
419        let risk_mode = if (risk_mode == PXModuleMode::Monitor && !is_enforced_request)
420            || is_monitored_request
421        {
422            PXModuleMode::Monitor
423        } else {
424            PXModuleMode::Blocking
425        };
426
427        if let Some(mobile_sdk_header) = req.get_header_str_lossy(MOBILE_SDK_HEADER) {
428            px_debug!("Mobile SDK token detected");
429            cookie_origin = CookieOrigin::Header;
430            let orig_token_header = req
431                .get_header_str_lossy(MOBILE_SDK_ORIGINAL_TOKEN_HEADER)
432                .map(|v| v.into_owned())
433                .unwrap_or_default();
434
435            let authorization_header = mobile_sdk_header.trim();
436
437            if pxutils::is_mobile_sdk_error_code(authorization_header) {
438                match authorization_header {
439                    "1" => s2s_call_reason = Some(CallReason::MobileError1),
440                    "2" => s2s_call_reason = Some(CallReason::MobileError2),
441                    "3" => s2s_call_reason = Some(CallReason::MobileError3),
442                    "4" => s2s_call_reason = Some(CallReason::MobileError4),
443                    _ => {
444                        s2s_call_reason = Some(CallReason::MobileSdkConnectionError);
445                    }
446                }
447            } else {
448                if let Some((cookie_name, cookie_contents)) =
449                    pxutils::parse_versioned_mobile_token(authorization_header)
450                {
451                    cookies.insert(cookie_name, cookie_contents);
452                }
453            }
454
455            original_token = orig_token_header.trim().to_owned();
456        } else {
457            let cookie_header_value = req
458                .get_header_str_lossy(COOKIE)
459                .map(|v| v.into_owned())
460                .unwrap_or_default();
461
462            let custom_cookie_header_value = {
463                let custom_header_name = conf.custom_cookie_header.as_str();
464                if custom_header_name.is_empty() {
465                    String::new()
466                } else {
467                    req.get_header_str_lossy(custom_header_name)
468                        .map(|v| v.into_owned())
469                        .unwrap_or_default()
470                }
471            };
472
473            cookies = pxutils::build_merged_request_cookies(
474                cookie_header_value.as_str(),
475                custom_cookie_header_value.as_str(),
476            );
477        }
478        request_cookie_names = cookies.keys().cloned().collect();
479
480        let pxvid = pxutils::extract_cookie_value(&cookies, "_pxvid");
481        let (vid, vid_source, orig_cookie_vid) = if pxutils::is_valid_uuid(&pxvid) {
482            (Some(pxvid), Some(VidSource::VidCookie), None)
483        } else if !pxvid.is_empty() {
484            (None, None, Some(pxvid))
485        } else {
486            (None, None, None)
487        };
488        let pxhd_cookie = pxutils::extract_cookie_value(&cookies, "_pxhd");
489        let pxcts_cookie = pxutils::extract_cookie_value(&cookies, "pxcts");
490
491        let access_cookie = pxutils::extract_cookie_value(&cookies, "_pxac");
492        if !access_cookie.is_empty() {
493            access_cookies.insert("access_cookie".to_string(), access_cookie);
494        }
495        for key in &conf.extracted_cookies {
496            let value = pxutils::extract_cookie_value(&cookies, key);
497            if !value.is_empty() {
498                access_cookies.insert(key.clone(), value);
499            }
500        }
501
502        PXContext {
503            cookie_origin: Some(cookie_origin),
504            user_agent: req
505                .get_header_str_lossy("user-agent")
506                .map(|v| v.into_owned())
507                .unwrap_or_default(),
508            http_method: req.get_method_str().to_string(),
509            http_version: pxutils::get_fastly_version_str(req).into(),
510            cookies,
511            access_cookies,
512            headers: pxutils::get_headers_as_json(req),
513            hostname: req.get_url().host_str().unwrap_or_default().to_string(),
514            full_url: req.get_url_str().to_string(),
515            is_sensitive_route: pxutils::verify_route(&conf.sensitive_routes, req.get_path()),
516            is_enforced_request,
517            is_monitored_request,
518            risk_mode,
519            original_token: if original_token.is_empty() {
520                None
521            } else {
522                Some(original_token)
523            },
524            s2s_call_reason,
525            request_cookie_names,
526            vid_source,
527            vid,
528            orig_cookie_vid,
529            pxhd_cookie: if pxhd_cookie.is_empty() {
530                None
531            } else {
532                Some(pxhd_cookie)
533            },
534            ip: pxutils::extract_ip_from_configured_headers(req, &conf.ip_headers)
535                .or_else(|| req.get_client_ip_addr().map(|ip| ip.to_string()))
536                .unwrap_or_default(),
537            block_action: Some("c".to_string()),
538            request_id: Uuid::new_v4(),
539            pxde_verified: false,
540            pxcts_cookie: if pxcts_cookie.is_empty() {
541                None
542            } else {
543                Some(pxcts_cookie)
544            },
545            enforcer_start_time: Some(std::time::SystemTime::now()),
546            postpone_activities: false,
547            ..Default::default()
548        }
549    }
550
551    pub fn extract_pxde_cookie(&mut self, conf: &PXConfig) {
552        if let Some(pxde) = self.cookies.get("_pxde") {
553            let fields = pxde.split(':').collect::<Vec<&str>>();
554            if fields.len() != 2 {
555                px_debug!("_pxde cookie validation failed");
556                return;
557            }
558
559            let ehmac = match fields.first() {
560                Some(h) => h,
561                None => {
562                    px_debug!("_pxde cookie validation failed");
563                    return;
564                }
565            };
566            let pxde_val = fields.get(1..).map(|s| s.join(":")).unwrap_or_default();
567            let expected_hmac = pxcrypto::create_hmac(&pxde_val, &conf.cookie_secret);
568
569            if ehmac.to_lowercase() != expected_hmac.unwrap_or_default().to_lowercase() {
570                px_debug!("_pxde cookie HMAC validation failed");
571                return;
572            }
573
574            let pxde_val = match general_purpose::STANDARD.decode(pxde_val) {
575                Ok(s) => s,
576                Err(e) => {
577                    px_debug!("_pxde cookie validation failed: {}", e);
578                    return;
579                }
580            };
581            let pxde_val = match std::str::from_utf8(&pxde_val) {
582                Ok(s) => s,
583                Err(_) => {
584                    px_debug!("_pxde cookie validation failed: invalid UTF-8");
585                    return;
586                }
587            };
588
589            let data_enrichment: PXDataEnrichment =
590                serde_json::from_str::<PXDataEnrichment>(pxde_val).unwrap_or_default();
591            self.data_enrichment = Some(data_enrichment);
592            self.pxde = Some(pxde_val.to_string());
593            self.pxde_verified = true;
594        }
595    }
596
597    // Public API
598
599    /// Whether this request targets a sensitive route requiring a Risk API call even with a valid cookie.
600    pub fn get_is_sensitive_route(&self) -> bool {
601        self.is_sensitive_route
602    }
603
604    /// Names of all cookies on the request, sent on Risk API and async activity payloads.
605    pub fn get_request_cookie_names(&self) -> &Vec<String> {
606        &self.request_cookie_names
607    }
608
609    /// Visitor ID from the risk cookie `v` field or from the `_pxvid` cookie, when available.
610    pub fn get_vid(&self) -> Option<&str> {
611        self.vid.as_deref()
612    }
613
614    /// HUMAN user UUID from Risk API or cookie-derived identity, used in enforcement and templates.
615    pub fn get_uuid(&self) -> Option<&str> {
616        self.uuid.as_deref()
617    }
618
619    /// Client-supplied `_pxhd` cookie linking early risk traffic to later sensor activity.
620    pub fn get_pxhd_cookie(&self) -> Option<&str> {
621        self.pxhd_cookie.as_deref()
622    }
623
624    /// PXHD value returned by the Risk API response to refresh client `_pxhd` tracking.
625    pub fn get_pxhd_risk(&self) -> Option<&str> {
626        self.pxhd_risk.as_deref()
627    }
628
629    /// Cookie `Domain` attribute from Risk API `pxhdDomain`, when present.
630    pub fn get_pxhd_domain(&self) -> Option<&str> {
631        self.pxhd_domain.as_deref()
632    }
633
634    /// PXHD value for outbound payloads: Risk API pxhd value, else client `_pxhd` cookie.
635    pub fn get_pxhd(&self) -> Option<&str> {
636        if let Some(pxhd) = self.pxhd_risk.as_deref() {
637            return Some(pxhd);
638        }
639        if let Some(pxhd) = self.pxhd_cookie.as_deref() {
640            return Some(pxhd);
641        }
642        None
643    }
644
645    /// How `vid` was obtained; `None` if unknown.
646    pub fn get_vid_source(&self) -> Option<&VidSource> {
647        self.vid_source.as_ref()
648    }
649
650    /// Client User-Agent string, subject to max-length alignment with cookie signing rules.
651    pub fn get_user_agent(&self) -> &str {
652        &self.user_agent
653    }
654
655    /// HTTP status code from a failed Risk API transport or response.
656    pub fn get_s2s_error_http_status(&self) -> Option<u16> {
657        self.s2s_error_http_status
658    }
659
660    /// Human-readable error detail for a failed Risk API outcome.
661    pub fn get_s2s_error_message(&self) -> Option<&str> {
662        self.s2s_error_message.as_deref()
663    }
664
665    /// Real client IP from configured trusted headers or the platform's direct peer address.
666    pub fn get_ip(&self) -> &str {
667        &self.ip
668    }
669
670    /// HMAC integrity material from the decrypted v2 risk cookie.
671    pub fn get_v2_cookie_hash(&self) -> Option<&str> {
672        self.v2_cookie_hash.as_deref()
673    }
674
675    /// Decrypted and decoded v2 risk-cookie JSON payload used for validation before trusting cookie-based scoring.
676    pub fn get_decoded_v2_cookie(&self) -> Option<&str> {
677        self.decoded_v2_cookie.as_deref()
678    }
679
680    /// Milliseconds spent on the synchronous Risk API round-trip, if a call was made.
681    pub fn get_risk_rtt(&self) -> Option<i64> {
682        self.risk_rtt
683    }
684
685    /// Single-character enforcement action from Risk API or cookie (`c` = captcha, `b` = block, `r` = ratelimit).
686    pub fn get_block_action(&self) -> Option<&str> {
687        self.block_action.as_deref()
688    }
689
690    /// Numeric bot-likelihood score (0-100) compared to `px_blocking_score` to decide pass vs block.
691    pub fn get_score(&self) -> Option<u8> {
692        self.score
693    }
694
695    /// Reason the Risk API was invoked, such as `no_cookie`, `cookie_expired`, or `sensitive_route`.
696    pub fn get_s2s_call_reason(&self) -> Option<&CallReason> {
697        self.s2s_call_reason.as_ref()
698    }
699
700    /// Taxonomy for a failed Risk API outcome.
701    pub fn get_s2s_error_reason(&self) -> Option<&S2sErrorReason> {
702        self.s2s_error_reason.as_ref()
703    }
704
705    /// Why the request was allowed.
706    pub fn get_pass_reason(&self) -> Option<&PassReason> {
707        self.pass_reason.as_ref()
708    }
709
710    /// Why the request was blocked.
711    pub fn get_block_reason(&self) -> Option<&BlockReason> {
712        self.block_reason.as_ref()
713    }
714
715    /// Unique ID for this request, carried on Risk API and telemetry.
716    pub fn get_request_id(&self) -> &Uuid {
717        &self.request_id
718    }
719
720    /// Risk API `data_enrichment` object with collector-side enrichment fields.
721    pub fn get_data_enrichment(&self) -> Option<&PXDataEnrichment> {
722        self.data_enrichment.as_ref()
723    }
724
725    /// Raw `pxcts` Cross Tab Session token for `cross_tab_session` on Risk and activity payloads.
726    pub fn get_pxcts_cookie(&self) -> Option<&str> {
727        self.pxcts_cookie.as_deref()
728    }
729
730    /// MCP metadata extracted for Agentic Trust when the request matched the configured endpoint.
731    pub fn get_agentic_trust_data(&self) -> Option<&AgenticTrustData> {
732        self.agentic_trust_data.as_ref()
733    }
734}
735
736static EMPTY_VEC: Vec<i32> = Vec::new();
737impl PXDataEnrichment {
738    /// the creation time
739    pub fn get_timestamp(&self) -> i64 {
740        self.timestamp.unwrap_or_default()
741    }
742
743    /// specifies if the request is made by a known bot or not (0: other, 1: known bot)
744    pub fn get_f_kb(&self) -> i8 {
745        self.f_kb.unwrap_or_default()
746    }
747
748    /// the access control rule type (w: whitelist, b: blacklist)
749    pub fn get_f_type(&self) -> &str {
750        self.f_type.as_deref().unwrap_or("")
751    }
752
753    /// the access control rule ID
754    pub fn get_f_id(&self) -> &str {
755        self.f_id.as_deref().unwrap_or("")
756    }
757
758    /// the data is defined either as a Custom Rule or as a HUMAN rule
759    pub fn get_f_origin(&self) -> &str {
760        self.f_origin.as_deref().unwrap_or("")
761    }
762
763    /// an array of IP Categorization IDs
764    pub fn get_ipc_id(&self) -> &Vec<i32> {
765        self.ipc_id.as_ref().unwrap_or(&EMPTY_VEC)
766    }
767
768    /// Indicates if the credentials on the activity are identified as compromised (1: breached)
769    pub fn get_breached_account(&self) -> i8 {
770        self.breached_account.unwrap_or_default()
771    }
772
773    /// the access token name
774    pub fn get_f_access_token(&self) -> &str {
775        self.f_access_token.as_deref().unwrap_or("")
776    }
777
778    /// an array of incident types
779    pub fn get_inc_id(&self) -> &Vec<i32> {
780        self.inc_id.as_ref().unwrap_or(&EMPTY_VEC)
781    }
782}