kingfisher-bin 2.8.0

MongoDB's blazingly fast and accurate secret scanning and validation tool
use serde::Serialize;

use super::*;

#[derive(Serialize)]
struct ToonReportEnvelope {
    schema: &'static str,
    scan: ToonScanMetadata,
    findings: Vec<ToonFindingRecord>,
    #[serde(skip_serializing_if = "Option::is_none")]
    access_map: Option<Vec<AccessMapEntry>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    audit: Option<crate::scan_audit::ScanAuditManifest>,
}

#[derive(Serialize)]
struct ToonScanMetadata {
    generated_at: String,
    scan_timestamp: String,
    target: Option<String>,
    kingfisher_version: String,
    latest_version_available: Option<String>,
    update_check_status: Option<String>,
    summary: ScanReportSummary,
}

#[derive(Serialize)]
struct ToonFindingRecord {
    title: String,
    rule_id: String,
    rule_name: String,
    description: String,
    validation_status: String,
    validation_outcome: kingfisher_core::ValidationOutcome,
    path: String,
    line: u32,
    column_start: u32,
    column_end: u32,
    confidence: String,
    entropy: f32,
    language: String,
    fingerprint: String,
    snippet: String,
    validation_response: Option<String>,
    encoding: Option<String>,
    validate_command: Option<String>,
    revoke_command: Option<String>,
    blast_radius_command: Option<String>,
    git_repository_url: Option<String>,
    git_commit_id: Option<String>,
    git_commit_url: Option<String>,
    git_file_url: Option<String>,
}

impl ToonFindingRecord {
    fn from_record(record: &FindingReporterRecord) -> Self {
        let git = record.finding.git_metadata.as_ref();

        Self {
            title: record.rule.title.clone(),
            rule_id: record.rule.id.clone(),
            rule_name: record.rule.name.clone(),
            description: record.rule.description.clone(),
            validation_status: record.finding.validation.status.clone(),
            validation_outcome: record.finding.validation.outcome,
            path: record.finding.path.clone(),
            line: record.finding.line,
            column_start: record.finding.column_start,
            column_end: record.finding.column_end,
            confidence: record.finding.confidence.clone(),
            entropy: record.finding.entropy.parse().unwrap_or_default(),
            language: record.finding.language.clone(),
            fingerprint: record.finding.fingerprint.clone(),
            snippet: record.finding.snippet.clone(),
            validation_response: non_empty(record.finding.validation.response.clone()),
            encoding: record.finding.encoding.clone(),
            validate_command: record.finding.validate_command.clone(),
            revoke_command: record.finding.revoke_command.clone(),
            blast_radius_command: record.finding.blast_radius_command.clone(),
            git_repository_url: json_string(git, &["repository_url"]),
            git_commit_id: json_string(git, &["commit", "id"]),
            git_commit_url: json_string(git, &["commit", "url"]),
            git_file_url: json_string(git, &["file", "url"]),
        }
    }
}

fn json_string(value: Option<&serde_json::Value>, path: &[&str]) -> Option<String> {
    let mut current = value?;
    for segment in path {
        current = current.get(*segment)?;
    }
    current.as_str().map(str::trim).filter(|value| !value.is_empty()).map(str::to_string)
}

fn non_empty(value: String) -> Option<String> {
    let trimmed = value.trim();
    if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
}

impl DetailsReporter {
    pub fn toon_format<W: std::io::Write>(
        &self,
        mut writer: W,
        args: &cli::commands::scan::ScanArgs,
    ) -> Result<()> {
        let envelope = self.build_report_envelope(args)?;
        let payload = ToonReportEnvelope {
            schema: "kingfisher.toon.v1",
            scan: ToonScanMetadata {
                generated_at: envelope
                    .metadata
                    .as_ref()
                    .map(|metadata| metadata.generated_at.clone())
                    .unwrap_or_default(),
                scan_timestamp: envelope
                    .metadata
                    .as_ref()
                    .map(|metadata| metadata.scan_timestamp.clone())
                    .unwrap_or_default(),
                target: envelope.metadata.as_ref().and_then(|metadata| metadata.target.clone()),
                kingfisher_version: envelope
                    .metadata
                    .as_ref()
                    .map(|metadata| metadata.kingfisher_version.clone())
                    .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()),
                latest_version_available: envelope
                    .metadata
                    .as_ref()
                    .and_then(|metadata| metadata.latest_version_available.clone()),
                update_check_status: envelope
                    .metadata
                    .as_ref()
                    .and_then(|metadata| metadata.update_check_status.clone()),
                summary: envelope.metadata.map(|metadata| metadata.summary).unwrap_or(
                    ScanReportSummary {
                        findings: envelope.findings.len(),
                        active_findings: 0,
                        inactive_findings: 0,
                        locally_derived_findings: 0,
                        invalid_material_findings: 0,
                        unknown_validation_findings: 0,
                        access_map_identities: envelope.access_map.as_ref().map_or(0, Vec::len),
                        rules_applied: None,
                        confidence_level: args.confidence.to_string(),
                        custom_rules_used: !args.rules.rules_path.is_empty()
                            || !args.rules.load_builtins,
                        successful_validations: None,
                        failed_validations: None,
                        skipped_validations: None,
                        blobs_scanned: None,
                        bytes_scanned: None,
                        scan_duration_seconds: None,
                    },
                ),
            },
            findings: envelope.findings.iter().map(ToonFindingRecord::from_record).collect(),
            access_map: envelope.access_map,
            audit: envelope.audit,
        };

        write!(writer, "{}", crate::toon::encode_llm_friendly(&payload)?)?;
        writeln!(writer)?;
        Ok(())
    }
}