Skip to main content

agentshield/ux/
explain.rs

1use std::path::Path;
2
3use crate::ScanReport;
4use crate::config::ScanPathFilterSummary;
5use crate::error::ShieldError;
6use crate::rules::{AttackCategory, Finding, Severity};
7
8use super::{hotspots, roots};
9
10mod support;
11#[cfg(test)]
12mod tests;
13
14use support::{
15    confidence_for_report, coverage_summary, display_list, finding_group_summary,
16    format_path_filters, gate_reason, next_actions, severity_counts,
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum CoverageConfidence {
21    High,
22    Medium,
23    Low,
24}
25
26impl CoverageConfidence {
27    fn label(self) -> &'static str {
28        match self {
29            Self::High => "High",
30            Self::Medium => "Medium",
31            Self::Low => "Low",
32        }
33    }
34
35    fn reason(self) -> &'static str {
36        match self {
37            Self::High => "known adapter(s) matched and source files were parsed",
38            Self::Medium => "known adapter(s) matched, but code parsing coverage is limited",
39            Self::Low => "no supported agent extension surface was detected",
40        }
41    }
42}
43
44#[derive(Debug, Clone)]
45pub struct ExplainOptions {
46    pub ignore_tests: bool,
47}
48
49pub fn quickstart_config_toml(fail_on: Severity, ignore_tests: bool) -> String {
50    format!(
51        r#"# AgentShield configuration
52# Generated by `agentshield quickstart`.
53
54[policy]
55fail_on = "{fail_on}"
56
57[scan]
58ignore_tests = {ignore_tests}
59
60[runtime.proxy]
61fail_on = "block"
62"#
63    )
64}
65
66pub fn render_explain(report: &ScanReport, options: &ExplainOptions) -> String {
67    let coverage = coverage_summary(report);
68    let confidence = confidence_for_report(report);
69    let runtime_findings: Vec<&Finding> = report
70        .findings
71        .iter()
72        .filter(|finding| finding.attack_category != AttackCategory::SupplyChain)
73        .collect();
74    let supply_chain_findings: Vec<&Finding> = report
75        .findings
76        .iter()
77        .filter(|finding| finding.attack_category == AttackCategory::SupplyChain)
78        .collect();
79
80    let mut output = String::new();
81    output.push_str("AgentShield explain\n");
82    output.push_str("===================\n\n");
83    output.push_str(&format!(
84        "Gate: {}\n",
85        if report.verdict.pass { "PASS" } else { "FAIL" }
86    ));
87    output.push_str(&format!("Reason: {}\n", gate_reason(report)));
88    output.push_str(&format!(
89        "Security confidence: {} - {}\n\n",
90        confidence.label(),
91        confidence.reason()
92    ));
93
94    output.push_str("Coverage:\n");
95    output.push_str(&format!(
96        "- Adapters: {}\n",
97        display_list(&coverage.frameworks, "none")
98    ));
99    output.push_str(&roots::render(report));
100    output.push_str(&format!("- Targets: {}\n", coverage.targets));
101    output.push_str(&format!(
102        "- Source files parsed: {} ({})\n",
103        coverage.source_files,
104        display_list(&coverage.languages, "no code parser coverage")
105    ));
106    output.push_str(&format!("- Tools discovered: {}\n", coverage.tools));
107    output.push_str(&format!(
108        "- Dependencies checked: {}\n",
109        coverage.dependencies
110    ));
111    output.push_str(&format!("- Lockfiles detected: {}\n", coverage.lockfiles));
112    output.push_str(&format!(
113        "- Test file exclusion: {}\n",
114        if options.ignore_tests {
115            "enabled"
116        } else {
117            "disabled"
118        }
119    ));
120    output.push_str(&format!(
121        "- Path filters: {}\n\n",
122        format_path_filters(&report.path_filter_summary)
123    ));
124
125    output.push_str("Findings:\n");
126    output.push_str(&format!(
127        "- Runtime-risk findings: {}\n",
128        finding_group_summary(&runtime_findings)
129    ));
130    output.push_str(&format!(
131        "- Supply-chain hygiene: {}\n",
132        finding_group_summary(&supply_chain_findings)
133    ));
134    output.push_str(&format!(
135        "- Severity counts: {}\n\n",
136        severity_counts(&report.findings)
137    ));
138
139    output.push_str(&hotspots::render(report));
140
141    output.push_str("Next actions:\n");
142    for action in next_actions(report) {
143        output.push_str(&format!("- {action}\n"));
144    }
145
146    output.push_str("\nWhat this does not prove:\n");
147    output.push_str("- This scan does not execute tools or prove absence of vulnerabilities.\n");
148    output.push_str(
149        "- It checks known risky patterns in supported agent surfaces and dependency metadata.\n",
150    );
151
152    output
153}
154
155pub fn render_no_adapter_explain(
156    path: &Path,
157    ignore_tests: bool,
158    path_filters: &ScanPathFilterSummary,
159) -> String {
160    let mut output = String::new();
161    output.push_str("AgentShield explain\n");
162    output.push_str("===================\n\n");
163    output.push_str("Gate: INCONCLUSIVE\n");
164    output.push_str("Reason: no supported agent extension surface was detected.\n");
165    output.push_str(&format!(
166        "Security confidence: {} - {}\n\n",
167        CoverageConfidence::Low.label(),
168        CoverageConfidence::Low.reason()
169    ));
170    output.push_str("Coverage:\n");
171    output.push_str("- Adapters: none\n");
172    output.push_str(&format!("- Target: {}\n", path.display()));
173    output.push_str(&format!(
174        "- Test file exclusion: {}\n",
175        if ignore_tests { "enabled" } else { "disabled" }
176    ));
177    output.push_str(&format!(
178        "- Path filters: {}\n\n",
179        format_path_filters(path_filters)
180    ));
181    output.push_str("Next actions:\n");
182    output.push_str("- Confirm this repository contains an MCP server, OpenClaw skill, Hermes agent, CrewAI/LangChain tool, GPT Action, or Cursor Rules surface.\n");
183    output.push_str("- If it does, add a framework manifest or dependency metadata that AgentShield can detect.\n");
184    output.push_str("- Run `agentshield doctor .` to inspect adapter detection.\n\n");
185    output.push_str("What this does not prove:\n");
186    output.push_str("- This result does not mean the project is safe; it means AgentShield did not find a supported surface to scan.\n");
187    output
188}
189
190pub fn is_no_adapter(error: &ShieldError) -> bool {
191    matches!(error, ShieldError::NoAdapter(_))
192}