agentshield/ux/
explain.rs1use 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}