perimeterx-fastly-enforcer 2.2.2

PerimeterX Fastly Compute@Edge Rust Enforcer
Documentation
use regex::Regex;
use serde_json::json;

use crate::handlers::pxgraphql::*;
use crate::pxconfig::PXConfig;

fn test_conf() -> PXConfig {
    PXConfig::default()
}

fn conf_with_keywords(patterns: &[&str]) -> PXConfig {
    let mut conf = test_conf();
    conf.graphql_keywords = patterns.iter().filter_map(|p| Regex::new(p).ok()).collect();
    conf
}

fn extract_one(v: &serde_json::Value) -> PXGraphQLExtractedItem {
    graphql_parse_and_extract(v, &test_conf()).unwrap_or_default()
}

#[test]
fn extract_from_object_with_query_field() {
    let v = json!({ "query": "query Test { id }" });
    let items = px_graphql_extract(&v, &test_conf());
    assert_eq!(items.len(), 1);
    assert_eq!(items.first().map(|i| i.op_type.as_str()), Some("query"));
    assert_eq!(items.first().map(|i| i.name.as_str()), Some("Test"));
}

#[test]
fn extract_from_raw_string_body() {
    let v = json!("query Test { id }");
    let items = px_graphql_extract(&v, &test_conf());
    assert_eq!(items.len(), 1);
    assert_eq!(items.first().map(|i| i.op_type.as_str()), Some("query"));
    assert_eq!(items.first().map(|i| i.name.as_str()), Some("Test"));
}

#[test]
fn object_without_query_field_returns_empty() {
    let v = json!({ "variables": { "id": 1 } });
    let item = extract_one(&v);

    assert!(item.name.is_empty());
    assert_eq!(item.op_type, "query");
    assert_eq!(item.variables, vec!["id"]);
}

#[test]
fn object_with_null_query_returns_empty() {
    let v = json!({ "query": null });
    let item = extract_one(&v);

    assert!(item.name.is_empty());
    assert!(item.op_type.is_empty());
    assert!(item.variables.is_empty());
}

#[test]
fn persisted_query_uses_operation_name_without_query_document() {
    let v = json!({
        "operationName": "test123",
        "variables": {
            "objectIds": ["83159105"],
            "input": {
                "cguid": "26d41c0e8f7ffe672ad97469b9a28117",
                "appCode": "DESKTOP"
            },
            "filters": null
        },
        "extensions": {
            "persistedQuery": {
                "version": 1,
                "sha256Hash": "bcba1c2fe2f11e68a3672bde013387220bf0f98c88efedb533d977a836eb1af0"
            }
        }
    });
    let item = extract_one(&v);

    assert_eq!(item.name, "test123");
    assert_eq!(item.op_type, "query");
    let mut variables = item.variables;
    variables.sort();
    assert_eq!(
        variables,
        vec!["filters", "input.appCode", "input.cguid", "objectIds"]
    );
}

#[test]
fn variable_object_keys_are_omitted_leaf_paths_reported() {
    let v = json!({
        "query": "query Test { id }",
        "variables": {
            "var1": "value1",
            "var2": {
                "var3": "value3"
            }
        }
    });
    let item = extract_one(&v);

    let mut variables = item.variables;
    variables.sort();
    assert_eq!(variables, vec!["var1", "var2.var3"]);
}

#[test]
fn non_string_query_falls_back_to_operation_name() {
    let v = json!({
        "query": 123,
        "operationName": "FallbackName",
        "variables": {
            "id": "abc"
        }
    });
    let item = extract_one(&v);

    assert_eq!(item.name, "FallbackName");
    assert!(item.op_type.is_empty());
    assert_eq!(item.variables, vec!["id"]);
}

#[test]
fn malformed_query_does_not_prevent_variable_extraction() {
    let v = json!({
        "query": "query Broken(",
        "variables": {
            "top": {
                "nested": true
            }
        }
    });
    let item = extract_one(&v);

    assert_eq!(item.variables, vec!["top.nested"]);
}

#[test]
fn scalar_json_values_return_default_item() {
    for v in [json!(null), json!(true), json!(42), json!("not graphql")] {
        let item = extract_one(&v);
        assert!(item.name.is_empty());
        assert!(item.op_type.is_empty());
        assert!(item.variables.is_empty());
        assert!(!item.sensitive);
    }
}

#[test]
fn variables_that_are_not_objects_do_not_add_variable_paths() {
    for variables in [json!(null), json!(["id"]), json!("id"), json!(123)] {
        let v = json!({
            "query": "query Test { id }",
            "variables": variables
        });
        let item = extract_one(&v);

        assert_eq!(item.name, "Test");
        assert_eq!(item.op_type, "query");
        assert!(item.variables.is_empty());
    }
}

#[test]
fn batch_with_mixed_values_returns_one_item_per_payload() {
    let v = json!([
        { "query": "query First { id }" },
        { "operationName": "PersistedOnly" },
        null,
        { "query": null }
    ]);
    let items = px_graphql_extract(&v, &test_conf());

    assert_eq!(items.len(), 4);
    assert_eq!(items.first().map(|i| i.name.as_str()), Some("First"));
    assert_eq!(items.get(1).map(|i| i.name.as_str()), Some("PersistedOnly"));
    assert_eq!(items.get(2).map(|i| i.name.as_str()), Some(""));
    assert_eq!(items.get(3).map(|i| i.name.as_str()), Some(""));
}

#[test]
fn extract_keywords_preserves_duplicates_and_order() {
    let query = r#"query GetUser {
        user {
            id
            name
            friends {
                id
            }
        }
    }"#;
    let conf = conf_with_keywords(&["user", r"friends?", "id"]);
    let v = json!({ "query": query });
    let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();

    let mut keywords = item.keywords;
    keywords.sort();
    assert_eq!(keywords, vec!["friends", "id", "id", "user"]);
}

#[test]
fn extract_keywords_empty_when_config_empty() {
    let v = json!({ "query": "query Test { id id }" });
    let item = graphql_parse_and_extract(&v, &test_conf()).unwrap_or_default();
    assert!(item.keywords.is_empty());
    assert_eq!(item.name, "Test");
}

#[test]
fn extract_keywords_truncates_per_keyword_length() {
    let long = "x".repeat(150);
    let query = format!("query Test {{ {long} }}");
    let conf = conf_with_keywords(&["x+"]);
    let v = json!({ "query": query });
    let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();

    assert_eq!(item.keywords.len(), 1);
    assert_eq!(item.keywords.first().map(String::len), Some(100));
}

#[test]
fn extract_keywords_caps_total_count() {
    let query = "query Test { ".to_owned() + &"id ".repeat(600) + "}";
    let conf = conf_with_keywords(&["id"]);
    let v = json!({ "query": query });
    let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();

    assert_eq!(item.keywords.len(), 500);
}

#[test]
fn extract_keywords_trims_whitespace() {
    let conf = conf_with_keywords(&[r"\s+user\s+"]);
    let v = json!({ "query": "query Test {   user   { id } }" });
    let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();

    assert_eq!(item.keywords, vec!["user"]);
}

#[test]
fn operation_name_fallback_honors_sensitive_operation_config() {
    let mut conf = test_conf();
    conf.sensitive_graphql_operation_names = vec!["SensitivePersisted".to_string()];
    let v = json!({
        "operationName": "SensitivePersisted",
        "extensions": {
            "persistedQuery": {
                "version": 1
            }
        }
    });

    let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();

    assert_eq!(item.name, "SensitivePersisted");
    assert!(item.sensitive);
}