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, extract_verbose_error_hint, post_graphql_ext, typo_variant,
};
use crate::transport::Transport;
use crate::types::{AffectedLocation, Confidence, EvidenceLevel, Finding, FindingStatus, GqlSchema, Severity};
use reqwest::Client;

pub async fn probe_verbose_error_disclosure(
    schema: &GqlSchema,
    url: &str,
    client: &Client,
    extra_headers: &[String],
    rate_limit_ms: u64,
    evasion_level: u8,
    _batch_probes: bool,
    _batch_size: u32,
    transport: Transport,
    confirmed: &mut Vec<Finding>,
    unconfirmed: &mut Vec<Finding>,
) -> Result<(), String> {
    let query_name = schema.query_type.as_ref().map(|q| q.name.as_str());
    let binding = schema.fields_for_type(query_name);
    let Some(first_field) = binding.first() else {
        return Ok(());
    };

    let typo = typo_variant(&first_field.name);
    let typo = if typo == first_field.name {
        format!("{}_typo", first_field.name)
    } else {
        typo
    };

    let query = format!("query {{ {} }}", typo);
    let resp = post_graphql_ext(
        client,
        url,
        &effective_headers(extra_headers, None, false),
        &query,
        None,
        rate_limit_ms,
        evasion_level,
        transport,
        false,
    )

    .await?;

    let hint_source = if resp.errors_text.is_empty() {
        &resp.raw_text
    } else {
        &resp.errors_text
    };

    if let Some(hint) = extract_verbose_error_hint(hint_source) {
        confirmed.push(Finding {
            id: "verbose-errors",
            severity: Severity::Low,
            title: "Verbose GraphQL Error Disclosure",
            description: "Endpoint returned field/argument suggestions (for example 'Did you mean ...') for invalid queries, which can aid schema enumeration.".to_string(),
            affected: vec![AffectedLocation::Field("Query".into(), typo)],
            remediation: "Use generic production validation errors and disable suggestion hints outside development environments.",
            first_step: Some(format!("Execute an invalid query like '{}' and check if the 'errors' array contains suggestions.", query)),
            references: vec!["CWE-200: Information Exposure"],
            status: FindingStatus::Confirmed,
            confidence: Confidence::Confirmed,
            evidence_level: EvidenceLevel::Executed,
            poc: Some(hint),
        });
    } else {
        unconfirmed.push(Finding {
            id: "verbose-errors",
            severity: Severity::Low,
            title: "Verbose GraphQL Error Disclosure Probe Inconclusive",
            description: "No suggestion-style verbose validation hint was observed for the typo probe. The server may already sanitize errors or require different malformed payloads.".to_string(),
            affected: vec![AffectedLocation::Field("Query".into(), typo)],
            remediation: "Confirm production error policy by testing multiple malformed field and argument variants across query and mutation roots.",
            first_step: Some("Manually test different malformed queries (e.g., wrong argument types) to see if the server ever leaks schema hints.".into()),
            references: vec!["CWE-200: Information Exposure"],
            status: FindingStatus::Possible,
            confidence: Confidence::Theoretical,
            evidence_level: EvidenceLevel::Inconclusive,
            poc: None,
        });
    }

    Ok(())
}