introspectre 1.4.0

A GraphQL offensive-security engine: introspection-driven schema analysis, active vulnerability probing, and an interactive attack-surface report.
use crate::audit::utils::{effective_headers, post_graphql_ext};
use crate::transport::Transport;
use crate::types::{AffectedLocation, Confidence, EvidenceLevel, Finding, FindingStatus, Severity};
use reqwest::Client;
use serde_json::Value;

pub async fn probe_typename(
    client: &Client,
    url: &str,
    extra_headers: &[String],
    rate_limit_ms: u64,
    evasion_level: u8,
    transport: Transport,
    confirmed: &mut Vec<Finding>,
    unconfirmed: &mut Vec<Finding>,
) -> Result<(), String> {
    let typename_resp = post_graphql_ext(
        client,
        url,
        &effective_headers(extra_headers, None, false),
        "query { __typename }",
        None,
        rate_limit_ms,
        evasion_level,
        transport,
        false,
    )

    .await?;

    if extract_typename(&typename_resp.data).as_deref() == Some("Query") {
        confirmed.push(Finding {
            id: "graphql-confirmed",
            severity: Severity::Low,
            title: "GraphQL Confirmed via __typename Probe",
            description: "Endpoint responded with data.__typename=Query without introspection query usage. This confirms GraphQL behavior even if introspection is disabled.".to_string(),
            affected: vec![AffectedLocation::Type("Endpoint".into())],
            remediation: "Restrict endpoint exposure and require authorization where appropriate. Keep introspection disabled in production unless explicitly needed.",
            first_step: Some(format!("Send a manual POST request to {} with the body {{\"query\": \"{{ __typename }}\"}} to confirm the endpoint type.", url)),
            references: vec!["CWE-200: Information Exposure"],
            status: FindingStatus::Confirmed,
            confidence: Confidence::Confirmed,
            evidence_level: EvidenceLevel::Executed,
            poc: None,
        });
    } else {
        unconfirmed.push(Finding {
            id: "graphql-confirmed",
            severity: Severity::Low,
            title: "GraphQL __typename Probe Inconclusive",
            description: format!(
                "Endpoint did not return data.__typename=Query (HTTP {}). GraphQL may still be present behind auth or non-standard routing.",
                typename_resp.status
            ),
            affected: vec![AffectedLocation::Type("Endpoint".into())],
            remediation: "Validate endpoint path, auth requirements, and gateway routing before deeper active probes.",
            first_step: Some("Check if the endpoint requires a specific path (e.g., /graphql, /api/graphql) or an authentication token.".into()),
            references: vec!["CWE-200: Information Exposure"],
            status: FindingStatus::Possible,
            confidence: Confidence::Theoretical,
            evidence_level: EvidenceLevel::Inconclusive,
            poc: None,
        });
    }
    Ok(())
}

fn extract_typename(data: &Option<Value>) -> Option<String> {
    data.as_ref()
        .and_then(|d| d.get("__typename"))
        .and_then(|v| v.as_str())
        .map(str::to_string)
}