use crate::px_debug;
use crate::pxconfig::PXConfig;
use apollo_parser::{
Parser,
cst::{self, CstNode},
};
use regex::Regex;
use serde_json;
const GRAPHQL_KEYWORD_MAX_LEN: usize = 100;
const GRAPHQL_KEYWORDS_MAX_COUNT: usize = 500;
#[derive(Default)]
pub struct PXGraphQLExtractedItem {
pub op_type: String,
pub name: String,
pub sensitive: bool,
pub variables: Vec<String>,
pub keywords: Vec<String>,
}
fn is_sensitive_graphql_operation_name(names: &[String], name: &str) -> bool {
let name = name.trim();
if name.is_empty() {
return false;
}
names.iter().any(|n| n.trim() == name)
}
fn clean_keyword(kw: &str) -> String {
let trimmed = kw.trim();
trimmed.chars().take(GRAPHQL_KEYWORD_MAX_LEN).collect()
}
fn extract_query_keywords(query: &str, patterns: &[Regex]) -> Vec<String> {
if patterns.is_empty() {
return Vec::new();
}
let mut keywords = Vec::new();
for pattern in patterns {
for m in pattern.find_iter(query) {
let kw = clean_keyword(m.as_str());
if kw.is_empty() {
continue;
}
keywords.push(kw);
if keywords.len() >= GRAPHQL_KEYWORDS_MAX_COUNT {
return keywords;
}
}
}
keywords
}
fn traverse_json_for_keys(value: &serde_json::Value, keys: &mut Vec<String>, current_key: &str) {
let Some(map) = value.as_object() else {
return;
};
for (key, val) in map {
let mut new_key = current_key.to_owned();
if !new_key.is_empty() {
new_key.push('.');
}
new_key.push_str(key);
if val.is_object() {
traverse_json_for_keys(val, keys, &new_key);
} else {
keys.push(new_key);
}
}
}
fn graphql_query_from_value(v: &serde_json::Value) -> String {
if let Some(obj) = v.as_object() {
match obj.get("query") {
Some(query_val) => query_val.as_str().unwrap_or_default().to_string(),
None => v.to_string(),
}
} else {
v.as_str().unwrap_or_default().to_string()
}
}
pub(crate) fn graphql_parse_and_extract(
v: &serde_json::Value,
conf: &PXConfig,
) -> Option<PXGraphQLExtractedItem> {
let query = graphql_query_from_value(v);
let mut item: PXGraphQLExtractedItem = Default::default();
if let Some(var_obj) = v.get("variables") {
if !var_obj.is_null() {
traverse_json_for_keys(var_obj, &mut item.variables, "");
}
}
let parser = Parser::new(query.as_str());
let cst = parser.parse();
let doc = cst.document();
for def in doc.definitions() {
if let cst::Definition::OperationDefinition(op) = def {
if let Some(n) = op.name() {
item.name = n.text().to_string();
if is_sensitive_graphql_operation_name(
&conf.sensitive_graphql_operation_names,
&item.name,
) {
item.sensitive = true;
}
};
if let Some(t) = op.operation_type() {
item.op_type = t.source_string().to_string();
} else {
item.op_type = "query".to_string();
}
if conf
.sensitive_graphql_operation_types
.contains(&item.op_type)
{
item.sensitive = true;
}
}
}
if item.name.is_empty() {
if let Some(op_name) = v.get("operationName").and_then(|n| n.as_str()) {
item.name = op_name.to_string();
if is_sensitive_graphql_operation_name(
&conf.sensitive_graphql_operation_names,
&item.name,
) {
item.sensitive = true;
}
}
}
if !conf.graphql_keywords.is_empty() && !query.is_empty() {
item.keywords = extract_query_keywords(&query, &conf.graphql_keywords);
}
if !item.sensitive
&& !conf.sensitive_graphql_operation_names.is_empty()
&& !item.keywords.is_empty()
{
item.sensitive = true;
}
Some(item)
}
pub fn px_graphql_extract(v: &serde_json::Value, conf: &PXConfig) -> Vec<PXGraphQLExtractedItem> {
let mut extracted_items = Vec::new();
px_debug!("Extracting GraphQL items from JSON body");
if let Some(array) = v.as_array() {
for i in array {
if let Some(item) = graphql_parse_and_extract(i, conf) {
extracted_items.push(item);
}
}
} else if let Some(item) = graphql_parse_and_extract(v, conf) {
extracted_items.push(item);
}
extracted_items
}
pub fn px_graphql_is_sensitive(items: &Vec<PXGraphQLExtractedItem>) -> bool {
for item in items {
if item.sensitive {
return true;
}
}
false
}