skill-veil-core 0.1.0

Core library for skill-veil behavioral analysis
Documentation
use crate::findings::{
    derive_package_verdict, ArtifactKind, FindingSummary, RecommendedAction, Severity, Verdict,
};
use crate::policy::{
    context_label, severity_to_sarif_level, ContextPolicy, JsonReport, PolicyGenerator,
    SarifArtifactLocation, SarifConfiguration, SarifDriver, SarifLocation, SarifMessage,
    SarifPhysicalLocation, SarifRegion, SarifReport, SarifResult, SarifRule, SarifRun, SarifTool,
    ShieldPolicy,
};
use std::collections::HashMap;

pub(crate) fn generate_shield_md(generator: &PolicyGenerator) -> String {
    let mut output = String::new();
    output.push_str("# SHIELD Policy\n\n");
    output.push_str(&format!("Generated for: `{}`\n\n", generator.skill_name));
    output.push_str("---\n\n");

    let policies = generator.generate_policies();
    let context_policies = generator.generate_context_policies();

    for policy in &policies {
        append_shield_policy(&mut output, policy);
    }

    if !context_policies.is_empty() {
        output.push_str("## Context Policies\n\n");
        for policy in &context_policies {
            append_context_policy(&mut output, policy);
        }
        output.push('\n');
    }

    output.push_str("## Policy Precedence\n\n");
    for stage in &generator.policy_audit.precedence_order {
        output.push_str(&format!("- {}\n", stage));
    }
    output.push('\n');

    if !generator.policy_audit.applied_overrides.is_empty() {
        output.push_str("## Applied Overrides\n\n");
        for applied in &generator.policy_audit.applied_overrides {
            output.push_str(&format!(
                "- {}: {} -> {} ({})\n",
                applied.rule_id, applied.original_action, applied.effective_action, applied.reason
            ));
        }
        output.push('\n');
    }

    output
}

pub(crate) fn generate_json(generator: &PolicyGenerator) -> JsonReport {
    let summary =
        FindingSummary::from_findings_and_graph(&generator.findings, &generator.artifact_graph);
    let primary_findings = split_primary_findings(generator);
    let supporting_findings = split_supporting_findings(generator);
    let primary_summary = FindingSummary::from_findings(&primary_findings);
    let supporting_summary = FindingSummary::from_findings(&supporting_findings);
    let verdict_report = derive_package_verdict(
        &generator.findings,
        &primary_summary,
        &supporting_summary,
        &summary,
    );
    let policies = generator.generate_policies();
    let context_policies = generator.generate_context_policies();

    JsonReport {
        skill_name: generator.skill_name.clone(),
        skill_path: generator.skill_path.clone(),
        extension_kind: generator.extension_kind,
        classification: generator.classification,
        package_id: generator.package_id.clone(),
        identity_source: generator.identity_source,
        structural_validity: generator.structural_validity,
        heuristic_score: generator.heuristic_score,
        timestamp: chrono::Utc::now(),
        findings: generator.findings.clone(),
        primary_findings,
        supporting_findings,
        summary,
        primary_summary,
        supporting_summary,
        verdict: verdict_report.verdict,
        verdict_report,
        artifact_graph: generator.artifact_graph.clone(),
        policies,
        context_policies,
        profile: generator.profile,
        suppression_summary: generator.suppression_summary.clone(),
        policy_audit: generator.policy_audit.clone(),
    }
}

pub(crate) fn generate_sarif(generator: &PolicyGenerator) -> SarifReport {
    let mut rules_map: HashMap<String, _> = HashMap::new();
    for finding in &generator.findings {
        rules_map.entry(finding.rule_id.clone()).or_insert(finding);
    }

    let mut rules: Vec<SarifRule> = rules_map
        .iter()
        .map(|(id, finding)| SarifRule {
            id: id.clone(),
            name: id.clone(),
            short_description: SarifMessage {
                text: finding.reason.clone(),
            },
            full_description: SarifMessage {
                text: format!("{} (Category: {})", finding.reason, finding.category),
            },
            default_configuration: SarifConfiguration {
                level: severity_to_sarif_level(finding.severity),
            },
        })
        .collect();

    let summary =
        FindingSummary::from_findings_and_graph(&generator.findings, &generator.artifact_graph);
    if !summary.action_triggers.is_empty() {
        rules.push(SarifRule {
            id: "SKILL_VEIL_ACTION_TRIGGER".to_string(),
            name: "SKILL_VEIL_ACTION_TRIGGER".to_string(),
            short_description: SarifMessage {
                text: "Contextual policy escalation".to_string(),
            },
            full_description: SarifMessage {
                text:
                    "Explains why contextual artifact capabilities escalated the recommended action"
                        .to_string(),
            },
            default_configuration: SarifConfiguration {
                level: severity_to_sarif_level(match summary.recommended_action {
                    RecommendedAction::Block => Severity::High,
                    RecommendedAction::RequireApproval => Severity::Medium,
                    RecommendedAction::Log => Severity::Low,
                }),
            },
        });
    }

    let report = generate_json(generator);
    rules.push(SarifRule {
        id: "SKILL_VEIL_PACKAGE_VERDICT".to_string(),
        name: "SKILL_VEIL_PACKAGE_VERDICT".to_string(),
        short_description: SarifMessage {
            text: "Final package verdict".to_string(),
        },
        full_description: SarifMessage {
            text: "Explains the final benign/suspicious/malicious package judgment".to_string(),
        },
        default_configuration: SarifConfiguration {
            level: severity_to_sarif_level(match report.verdict {
                Verdict::Malicious => Severity::High,
                Verdict::Suspicious => Severity::Medium,
                Verdict::Benign => Severity::Low,
            }),
        },
    });

    let mut results: Vec<SarifResult> = generator
        .findings
        .iter()
        .map(|finding| SarifResult {
            rule_id: finding.rule_id.clone(),
            level: severity_to_sarif_level(finding.severity),
            message: SarifMessage {
                text: format!("{}: {}", finding.reason, finding.match_value),
            },
            locations: vec![SarifLocation {
                physical_location: SarifPhysicalLocation {
                    artifact_location: SarifArtifactLocation {
                        uri: finding
                            .artifact_path
                            .clone()
                            .unwrap_or_else(|| generator.skill_path.clone()),
                    },
                    region: finding
                        .line_number
                        .map(|line| SarifRegion { start_line: line }),
                },
            }],
            properties: Some(serde_json::json!({
                "artifact_kind": finding.artifact_kind,
                "artifact_scope": finding.artifact_scope,
                "signal_class": finding.signal_class,
                "evidence_kind": finding.evidence_kind,
                "recommended_action": finding.recommended_action,
                "package_verdict": report.verdict,
            })),
        })
        .collect();

    results.extend(summary.action_triggers.iter().map(|trigger| SarifResult {
        rule_id: "SKILL_VEIL_ACTION_TRIGGER".to_string(),
        level: severity_to_sarif_level(match trigger.action {
            RecommendedAction::Block => Severity::High,
            RecommendedAction::RequireApproval => Severity::Medium,
            RecommendedAction::Log => Severity::Low,
        }),
        message: SarifMessage {
            text: trigger.rationale.clone(),
        },
        locations: vec![SarifLocation {
            physical_location: SarifPhysicalLocation {
                artifact_location: SarifArtifactLocation {
                    uri: generator.skill_path.clone(),
                },
                region: None,
            },
        }],
        properties: Some(serde_json::json!({
            "recommended_action": trigger.action,
            "trigger_factor": trigger.factor,
            "package_verdict": report.verdict,
        })),
    }));

    results.push(SarifResult {
        rule_id: "SKILL_VEIL_PACKAGE_VERDICT".to_string(),
        level: severity_to_sarif_level(match report.verdict {
            Verdict::Malicious => Severity::High,
            Verdict::Suspicious => Severity::Medium,
            Verdict::Benign => Severity::Low,
        }),
        message: SarifMessage {
            text: format!("Final package verdict: {}", report.verdict),
        },
        locations: vec![SarifLocation {
            physical_location: SarifPhysicalLocation {
                artifact_location: SarifArtifactLocation {
                    uri: generator.skill_path.clone(),
                },
                region: None,
            },
        }],
        properties: Some(serde_json::json!({
            "verdict": report.verdict,
            "verdict_reasons": report.verdict_report.verdict_reasons,
            "root_cause_groups": report.verdict_report.root_cause_groups,
            "top_risk_drivers": report.verdict_report.top_risk_drivers,
            "heuristic_score": report.heuristic_score,
            "artifact_scope": "package",
        })),
    });

    SarifReport {
        schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json".to_string(),
        version: "2.1.0".to_string(),
        runs: vec![SarifRun {
            tool: SarifTool {
                driver: SarifDriver {
                    name: "skill-veil".to_string(),
                    version: env!("CARGO_PKG_VERSION").to_string(),
                    information_uri: "https://github.com/seifreed/skill-veil".to_string(),
                    rules,
                },
            },
            results,
        }],
    }
}

fn split_primary_findings(generator: &PolicyGenerator) -> Vec<crate::findings::Finding> {
    generator
        .findings
        .iter()
        .filter(|finding| {
            finding
                .artifact_path
                .as_deref()
                .is_none_or(|artifact_path| artifact_path == generator.skill_path)
                && matches!(
                    finding.artifact_kind,
                    ArtifactKind::SkillDocument
                        | ArtifactKind::AgentInstruction
                        | ArtifactKind::PromptPackDocument
                        | ArtifactKind::McpServerManifest
                        | ArtifactKind::PackageManifest
                )
        })
        .cloned()
        .collect()
}

fn split_supporting_findings(generator: &PolicyGenerator) -> Vec<crate::findings::Finding> {
    generator
        .findings
        .iter()
        .filter(|finding| {
            !(finding
                .artifact_path
                .as_deref()
                .is_none_or(|artifact_path| artifact_path == generator.skill_path)
                && matches!(
                    finding.artifact_kind,
                    ArtifactKind::SkillDocument
                        | ArtifactKind::AgentInstruction
                        | ArtifactKind::PromptPackDocument
                        | ArtifactKind::McpServerManifest
                        | ArtifactKind::PackageManifest
                ))
        })
        .cloned()
        .collect()
}

fn append_shield_policy(output: &mut String, policy: &ShieldPolicy) {
    output.push_str(&format!("## {}\n\n", policy.id));
    output.push_str("```yaml\n");
    output.push_str(&format!("id: {}\n", policy.id));
    output.push_str(&format!("category: {}\n", policy.category));
    output.push_str(&format!("severity: {}\n", policy.severity));
    output.push_str(&format!("confidence: {:.2}\n", policy.confidence));
    output.push_str(&format!("action: {}\n", policy.action));
    output.push_str("recommendation_agent:\n");
    for rec in &policy.recommendation_agent {
        output.push_str(&format!("  - {}\n", rec));
    }
    if let Some(expires) = &policy.expires_at {
        output.push_str(&format!("expires_at: {}\n", expires.format("%Y-%m-%d")));
    }
    output.push_str(&format!("revoked: {}\n", policy.revoked));
    output.push_str("```\n\n");
}

fn append_context_policy(output: &mut String, policy: &ContextPolicy) {
    output.push_str(&format!(
        "- context: {}\n  action: {}\n",
        context_label(policy.context),
        policy.action
    ));
    for rationale in &policy.rationale {
        output.push_str(&format!("  rationale: {}\n", rationale));
    }
}