shieldbot-mcp 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: shieldbot-mcp
3
+ Version: 1.0.0
4
+ Summary: AI-powered security code review MCP server for Claude Code — combines Semgrep (5,000+ rules), bandit, detect-secrets, pip-audit, and npm-audit
5
+ Project-URL: Homepage, https://github.com/BalaSriharsha/shieldbot
6
+ Project-URL: Repository, https://github.com/BalaSriharsha/shieldbot
7
+ Project-URL: Bug Tracker, https://github.com/BalaSriharsha/shieldbot/issues
8
+ License: MIT
9
+ Keywords: anthropic,claude,code-review,mcp,sast,security,semgrep,vulnerability
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: anyio>=4.0.0
19
+ Requires-Dist: bandit>=1.7.0
20
+ Requires-Dist: detect-secrets>=1.4.0
21
+ Requires-Dist: gitpython>=3.1.0
22
+ Requires-Dist: jinja2>=3.0.0
23
+ Requires-Dist: mcp>=1.0.0
24
+ Requires-Dist: pip-audit>=2.6.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: ruff>=0.1.0
27
+ Requires-Dist: semgrep>=1.50.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: hatchling; extra == 'dev'
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # shieldbot-mcp
35
+
36
+ AI-powered security code review MCP server for Claude Code.
37
+
38
+ Combines **Semgrep (5,000+ rules)**, bandit, ruff, detect-secrets, pip-audit, and npm-audit with Claude's security expertise to deliver prioritized, actionable security reports.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install shieldbot-mcp
44
+ ```
45
+
46
+ Or run directly via `uvx` (recommended for MCP):
47
+ ```bash
48
+ uvx shieldbot-mcp
49
+ ```
50
+
51
+ ## Usage with Claude Code
52
+
53
+ Install the plugin:
54
+ ```
55
+ /plugin install shieldbot
56
+ ```
57
+
58
+ Then ask Claude naturally:
59
+ - *"scan this repo for security issues"*
60
+ - *"check for hardcoded secrets"*
61
+ - *"audit my dependencies for CVEs"*
62
+
63
+ Or use the slash command:
64
+ ```
65
+ /shieldbot-scan .
66
+ /shieldbot-scan /path/to/repo --min-severity high
67
+ /shieldbot-scan . --git-history
68
+ ```
69
+
70
+ ## MCP tools exposed
71
+
72
+ | Tool | Description |
73
+ |------|-------------|
74
+ | `scan_repository` | Full parallel security scan → JSON report |
75
+ | `check_scanner_tools` | Check which scanners are installed |
76
+
77
+ ## Add to any MCP client
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "shieldbot": {
83
+ "command": "uvx",
84
+ "args": ["shieldbot-mcp"]
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Scanners
91
+
92
+ | Scanner | Coverage |
93
+ |---------|---------|
94
+ | Semgrep 5,000+ rules | OWASP Top 10, CWE Top 25, injection, XSS, SSRF, taint |
95
+ | bandit | Python security |
96
+ | ruff | Python quality + security |
97
+ | detect-secrets | API keys, passwords, tokens |
98
+ | pip-audit | Python CVEs (PyPI Advisory DB) |
99
+ | npm audit | Node.js CVEs |
100
+
101
+ ## Publish to PyPI
102
+
103
+ ```bash
104
+ pip install hatchling build twine
105
+ python -m build
106
+ twine upload dist/*
107
+ ```
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,78 @@
1
+ # shieldbot-mcp
2
+
3
+ AI-powered security code review MCP server for Claude Code.
4
+
5
+ Combines **Semgrep (5,000+ rules)**, bandit, ruff, detect-secrets, pip-audit, and npm-audit with Claude's security expertise to deliver prioritized, actionable security reports.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install shieldbot-mcp
11
+ ```
12
+
13
+ Or run directly via `uvx` (recommended for MCP):
14
+ ```bash
15
+ uvx shieldbot-mcp
16
+ ```
17
+
18
+ ## Usage with Claude Code
19
+
20
+ Install the plugin:
21
+ ```
22
+ /plugin install shieldbot
23
+ ```
24
+
25
+ Then ask Claude naturally:
26
+ - *"scan this repo for security issues"*
27
+ - *"check for hardcoded secrets"*
28
+ - *"audit my dependencies for CVEs"*
29
+
30
+ Or use the slash command:
31
+ ```
32
+ /shieldbot-scan .
33
+ /shieldbot-scan /path/to/repo --min-severity high
34
+ /shieldbot-scan . --git-history
35
+ ```
36
+
37
+ ## MCP tools exposed
38
+
39
+ | Tool | Description |
40
+ |------|-------------|
41
+ | `scan_repository` | Full parallel security scan → JSON report |
42
+ | `check_scanner_tools` | Check which scanners are installed |
43
+
44
+ ## Add to any MCP client
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "shieldbot": {
50
+ "command": "uvx",
51
+ "args": ["shieldbot-mcp"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Scanners
58
+
59
+ | Scanner | Coverage |
60
+ |---------|---------|
61
+ | Semgrep 5,000+ rules | OWASP Top 10, CWE Top 25, injection, XSS, SSRF, taint |
62
+ | bandit | Python security |
63
+ | ruff | Python quality + security |
64
+ | detect-secrets | API keys, passwords, tokens |
65
+ | pip-audit | Python CVEs (PyPI Advisory DB) |
66
+ | npm audit | Node.js CVEs |
67
+
68
+ ## Publish to PyPI
69
+
70
+ ```bash
71
+ pip install hatchling build twine
72
+ python -m build
73
+ twine upload dist/*
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "shieldbot-mcp"
7
+ version = "1.0.0"
8
+ description = "AI-powered security code review MCP server for Claude Code — combines Semgrep (5,000+ rules), bandit, detect-secrets, pip-audit, and npm-audit"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ keywords = ["security", "mcp", "code-review", "sast", "claude", "anthropic", "semgrep", "vulnerability"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Security",
17
+ "Topic :: Software Development :: Quality Assurance",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ ]
22
+ dependencies = [
23
+ "mcp>=1.0.0",
24
+ "pydantic>=2.0.0",
25
+ "jinja2>=3.0.0",
26
+ "anyio>=4.0.0",
27
+ "GitPython>=3.1.0",
28
+ # Scanners (installed separately or via extras)
29
+ "semgrep>=1.50.0",
30
+ "bandit>=1.7.0",
31
+ "ruff>=0.1.0",
32
+ "detect-secrets>=1.4.0",
33
+ "pip-audit>=2.6.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["hatchling", "pytest", "pytest-asyncio"]
38
+
39
+ [project.scripts]
40
+ shieldbot-mcp = "shieldbot.server:main"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/BalaSriharsha/shieldbot"
44
+ Repository = "https://github.com/BalaSriharsha/shieldbot"
45
+ "Bug Tracker" = "https://github.com/BalaSriharsha/shieldbot/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["shieldbot"]
49
+
50
+ [tool.hatch.build.targets.sdist]
51
+ include = [
52
+ "/shieldbot",
53
+ "/README.md",
54
+ "/LICENSE",
55
+ "/pyproject.toml",
56
+ ]
@@ -0,0 +1,3 @@
1
+ """Shieldbot - Security scanner for the Claude Code agent."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,61 @@
1
+ """Configuration constants for shieldbot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Claude model to use
6
+ CLAUDE_MODEL = "claude-sonnet-4-6"
7
+
8
+ # Semgrep rulesets always applied regardless of detected language
9
+ SEMGREP_ALWAYS_RULESETS = [
10
+ "p/owasp-top-ten",
11
+ "p/secrets",
12
+ "p/cwe-top-25",
13
+ "p/sql-injection",
14
+ "p/command-injection",
15
+ "p/ssrf",
16
+ ]
17
+
18
+ # Additional rulesets keyed by detected language
19
+ SEMGREP_LANGUAGE_RULESETS: dict[str, list[str]] = {
20
+ "python": ["p/security-audit", "p/python", "p/django", "p/flask", "p/bandit"],
21
+ "javascript": ["p/security-audit", "p/javascript", "p/react", "p/express", "p/xss"],
22
+ "typescript": ["p/security-audit", "p/typescript", "p/react"],
23
+ "java": ["p/security-audit", "p/java"],
24
+ "go": ["p/security-audit", "p/go"],
25
+ "ruby": ["p/security-audit", "p/ruby", "p/rails"],
26
+ "php": ["p/security-audit", "p/php"],
27
+ "kotlin": ["p/security-audit"],
28
+ "scala": ["p/security-audit"],
29
+ "c": ["p/security-audit"],
30
+ "cpp": ["p/security-audit"],
31
+ "csharp": ["p/security-audit"],
32
+ "rust": ["p/security-audit"],
33
+ }
34
+
35
+ # Scanner priority for deduplication (lower = higher priority, keeps its data)
36
+ SCANNER_PRIORITY: dict[str, int] = {
37
+ "semgrep": 0,
38
+ "bandit": 1,
39
+ "ruff": 2,
40
+ "detect-secrets": 3,
41
+ "gitleaks": 3,
42
+ "pip-audit": 4,
43
+ "npm-audit": 4,
44
+ }
45
+
46
+ # Semgrep subprocess settings
47
+ SEMGREP_TIMEOUT_PER_FILE = 300 # seconds
48
+ SEMGREP_MAX_MEMORY_MB = 2000
49
+ SEMGREP_JOBS = 4
50
+ SEMGREP_OVERALL_TIMEOUT = 600 # 10 minutes total
51
+
52
+ # Scanners that are optional (warn but don't fail if missing)
53
+ OPTIONAL_SCANNERS = {"ruff", "gitleaks"}
54
+
55
+ # Max lines of code snippet to store per finding
56
+ MAX_SNIPPET_LINES = 10
57
+
58
+ # Severity thresholds for exit codes
59
+ EXIT_CODE_MEDIUM = 1
60
+ EXIT_CODE_HIGH = 2
61
+ EXIT_CODE_CRITICAL = 3
@@ -0,0 +1,108 @@
1
+ """Pydantic data models for shieldbot security findings and reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from datetime import datetime
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class Severity(str, Enum):
14
+ CRITICAL = "critical"
15
+ HIGH = "high"
16
+ MEDIUM = "medium"
17
+ LOW = "low"
18
+ INFO = "info"
19
+
20
+
21
+ SEVERITY_ORDER = {
22
+ Severity.CRITICAL: 0,
23
+ Severity.HIGH: 1,
24
+ Severity.MEDIUM: 2,
25
+ Severity.LOW: 3,
26
+ Severity.INFO: 4,
27
+ }
28
+
29
+
30
+ class FindingCategory(str, Enum):
31
+ INJECTION = "injection"
32
+ SECRETS = "secrets"
33
+ CRYPTOGRAPHY = "cryptography"
34
+ AUTHENTICATION = "authentication"
35
+ ACCESS_CONTROL = "access_control"
36
+ DEPENDENCY_CVE = "dependency_cve"
37
+ DESERIALIZATION = "deserialization"
38
+ PATH_TRAVERSAL = "path_traversal"
39
+ XSS = "xss"
40
+ SSRF = "ssrf"
41
+ CODE_QUALITY = "code_quality"
42
+ MISCONFIGURATION = "misconfiguration"
43
+ OTHER = "other"
44
+
45
+
46
+ class Finding(BaseModel):
47
+ id: str = ""
48
+ scanner: str
49
+ rule_id: str
50
+ title: str
51
+ description: str
52
+ severity: Severity
53
+ category: FindingCategory
54
+ file_path: str
55
+ line_start: int
56
+ line_end: Optional[int] = None
57
+ column: Optional[int] = None
58
+ code_snippet: Optional[str] = None
59
+ cve_id: Optional[str] = None
60
+ cwe_id: Optional[str] = None
61
+ owasp_category: Optional[str] = None
62
+ remediation: Optional[str] = None
63
+ references: List[str] = Field(default_factory=list)
64
+ confidence: str = "medium"
65
+ is_false_positive: bool = False
66
+ duplicate_of: Optional[str] = None
67
+
68
+ def model_post_init(self, __context: Any) -> None:
69
+ if not self.id:
70
+ raw = f"{self.rule_id}:{self.file_path}:{self.line_start}"
71
+ self.id = hashlib.sha256(raw.encode()).hexdigest()[:16]
72
+
73
+
74
+ class ScanResult(BaseModel):
75
+ scanner: str
76
+ success: bool
77
+ findings: List[Finding] = Field(default_factory=list)
78
+ raw_output: Dict[str, Any] = Field(default_factory=dict)
79
+ error_message: Optional[str] = None
80
+ duration_seconds: float = 0.0
81
+ files_scanned: int = 0
82
+
83
+
84
+ class ClaudeAnalysis(BaseModel):
85
+ executive_summary: str
86
+ risk_score: int = Field(ge=0, le=100)
87
+ risk_label: str
88
+ prioritized_findings: List[str] = Field(default_factory=list)
89
+ false_positive_ids: List[str] = Field(default_factory=list)
90
+ attack_narrative: Optional[str] = None
91
+ top_remediations: List[Dict[str, Any]] = Field(default_factory=list)
92
+ recommended_focus: str = ""
93
+
94
+
95
+ class SecurityReport(BaseModel):
96
+ report_id: str
97
+ repo_path: str
98
+ scan_timestamp: datetime = Field(default_factory=datetime.utcnow)
99
+ scan_duration_seconds: float = 0.0
100
+ languages_detected: List[str] = Field(default_factory=list)
101
+ scanners_run: List[str] = Field(default_factory=list)
102
+ total_findings: int = 0
103
+ findings_by_severity: Dict[str, int] = Field(default_factory=dict)
104
+ findings_by_category: Dict[str, int] = Field(default_factory=dict)
105
+ all_findings: List[Finding] = Field(default_factory=list)
106
+ scan_results: List[ScanResult] = Field(default_factory=list)
107
+ claude_analysis: Optional[ClaudeAnalysis] = None
108
+ report_version: str = "1.0.0"
@@ -0,0 +1 @@
1
+ """Report output formatters."""
@@ -0,0 +1,208 @@
1
+ """Rich terminal reporter for shieldbot security reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.text import Text
9
+ from rich import box
10
+
11
+ from shieldbot.models import Finding, SecurityReport, Severity
12
+
13
+ console = Console()
14
+
15
+ _SEVERITY_COLORS = {
16
+ Severity.CRITICAL: "bold red",
17
+ Severity.HIGH: "red",
18
+ Severity.MEDIUM: "yellow",
19
+ Severity.LOW: "cyan",
20
+ Severity.INFO: "dim",
21
+ }
22
+
23
+ _RISK_LABEL_COLORS = {
24
+ "Critical": "bold red",
25
+ "High": "red",
26
+ "Medium": "yellow",
27
+ "Low": "cyan",
28
+ "Clean": "bold green",
29
+ }
30
+
31
+
32
+ def print_report(report: SecurityReport, min_severity: Severity = Severity.INFO) -> None:
33
+ """Print a full security report to the terminal."""
34
+ console.print()
35
+
36
+ # ── Header ──────────────────────────────────────────────────────────
37
+ console.print(Panel(
38
+ f"[bold white]AUTOBOT SECURITY SCAN REPORT[/bold white]\n"
39
+ f"Repo: [dim]{report.repo_path}[/dim]\n"
40
+ f"Scanned: [dim]{report.scan_timestamp.strftime('%Y-%m-%d %H:%M:%S UTC')}[/dim] "
41
+ f"Duration: [dim]{report.scan_duration_seconds:.1f}s[/dim]",
42
+ border_style="blue",
43
+ expand=False,
44
+ ))
45
+
46
+ # ── Risk score banner ────────────────────────────────────────────────
47
+ if report.claude_analysis:
48
+ risk_color = _RISK_LABEL_COLORS.get(report.claude_analysis.risk_label, "white")
49
+ score = report.claude_analysis.risk_score
50
+ label = report.claude_analysis.risk_label
51
+ console.print(f"\n[bold]RISK SCORE:[/bold] [{risk_color}]{score}/100 [{label.upper()}][/{risk_color}]\n")
52
+ else:
53
+ # Compute naive risk from finding counts
54
+ crit = report.findings_by_severity.get("critical", 0)
55
+ high = report.findings_by_severity.get("high", 0)
56
+ if crit > 0:
57
+ console.print("\n[bold red]RISK: CRITICAL findings present — immediate action required[/bold red]\n")
58
+ elif high > 0:
59
+ console.print("\n[bold red]RISK: HIGH findings detected[/bold red]\n")
60
+ else:
61
+ console.print("\n[bold yellow]RISK: Review findings below[/bold yellow]\n")
62
+
63
+ # ── Scanner summary table ────────────────────────────────────────────
64
+ table = Table(title="Scan Summary", box=box.ROUNDED, show_header=True, header_style="bold blue")
65
+ table.add_column("Scanner", style="cyan")
66
+ table.add_column("Critical", style="bold red", justify="right")
67
+ table.add_column("High", style="red", justify="right")
68
+ table.add_column("Medium", style="yellow", justify="right")
69
+ table.add_column("Low", style="cyan", justify="right")
70
+ table.add_column("Info", style="dim", justify="right")
71
+ table.add_column("Status")
72
+
73
+ for result in report.scan_results:
74
+ # Count by severity for this scanner
75
+ counts: dict[str, int] = {s.value: 0 for s in Severity}
76
+ for f in result.findings:
77
+ if not f.duplicate_of:
78
+ counts[f.severity.value] += 1
79
+
80
+ status = "[green]OK[/green]" if result.success else f"[red]ERROR[/red]"
81
+ if result.error_message and not result.success:
82
+ status = f"[red]{result.error_message[:40]}[/red]"
83
+
84
+ table.add_row(
85
+ result.scanner,
86
+ str(counts["critical"]) if counts["critical"] else "-",
87
+ str(counts["high"]) if counts["high"] else "-",
88
+ str(counts["medium"]) if counts["medium"] else "-",
89
+ str(counts["low"]) if counts["low"] else "-",
90
+ str(counts["info"]) if counts["info"] else "-",
91
+ status,
92
+ )
93
+
94
+ console.print(table)
95
+ console.print()
96
+
97
+ # ── Executive summary ────────────────────────────────────────────────
98
+ if report.claude_analysis and report.claude_analysis.executive_summary:
99
+ console.print(Panel(
100
+ report.claude_analysis.executive_summary,
101
+ title="[bold]Executive Summary (Claude Analysis)[/bold]",
102
+ border_style="blue",
103
+ ))
104
+ console.print()
105
+
106
+ # ── Findings ─────────────────────────────────────────────────────────
107
+ min_order = {
108
+ Severity.CRITICAL: 0, Severity.HIGH: 1,
109
+ Severity.MEDIUM: 2, Severity.LOW: 3, Severity.INFO: 4,
110
+ }
111
+ min_sev_order = min_order[min_severity]
112
+
113
+ canonical = [f for f in report.all_findings if not f.duplicate_of]
114
+ canonical.sort(key=lambda f: min_order.get(f.severity, 9))
115
+
116
+ shown = [f for f in canonical if min_order.get(f.severity, 9) <= min_sev_order]
117
+
118
+ if not shown:
119
+ console.print("[green]No findings at or above the selected severity threshold.[/green]")
120
+ else:
121
+ console.print(f"[bold]Findings ({len(shown)} shown, min severity: {min_severity.value})[/bold]\n")
122
+ for f in shown:
123
+ _print_finding(f, report)
124
+
125
+ # ── Top remediations ─────────────────────────────────────────────────
126
+ if report.claude_analysis and report.claude_analysis.top_remediations:
127
+ console.print(Panel(
128
+ _format_remediations(report.claude_analysis.top_remediations),
129
+ title="[bold]Top Remediation Priorities (Claude)[/bold]",
130
+ border_style="green",
131
+ ))
132
+
133
+ console.print()
134
+ _print_footer(report)
135
+
136
+
137
+ def _print_finding(f: Finding, report: SecurityReport) -> None:
138
+ color = _SEVERITY_COLORS.get(f.severity, "white")
139
+ fp_note = " [dim](possible false positive)[/dim]" if f.is_false_positive else ""
140
+ if (
141
+ report.claude_analysis
142
+ and f.id in report.claude_analysis.false_positive_ids
143
+ ):
144
+ fp_note = " [dim italic](Claude: likely false positive)[/dim italic]"
145
+
146
+ title = f"[{color}][{f.severity.value.upper()}] {f.title}[/{color}]{fp_note}"
147
+
148
+ body_lines = [
149
+ f"[dim]Rule:[/dim] {f.rule_id}",
150
+ f"[dim]File:[/dim] {f.file_path}:{f.line_start}",
151
+ f"[dim]Scanner:[/dim] {f.scanner}",
152
+ ]
153
+ if f.cwe_id:
154
+ body_lines.append(f"[dim]CWE:[/dim] {f.cwe_id}")
155
+ if f.owasp_category:
156
+ body_lines.append(f"[dim]OWASP:[/dim] {f.owasp_category}")
157
+ if f.cve_id:
158
+ body_lines.append(f"[dim]CVE:[/dim] {f.cve_id}")
159
+ if f.code_snippet:
160
+ snippet = f.code_snippet.strip()[:300]
161
+ body_lines.append(f"\n[dim]Code:[/dim]\n[dim]{snippet}[/dim]")
162
+ if f.remediation:
163
+ body_lines.append(f"\n[dim]Fix:[/dim] {f.remediation[:300]}")
164
+
165
+ console.print(Panel("\n".join(body_lines), title=title, border_style=color.replace("bold ", "")))
166
+
167
+
168
+ def _format_remediations(remediations: list) -> str:
169
+ lines = []
170
+ for i, rem in enumerate(remediations[:10], 1):
171
+ effort = rem.get("effort", "?")
172
+ title = rem.get("title", "")
173
+ steps = rem.get("steps", [])
174
+ lines.append(f"{i}. [bold]{title}[/bold] [dim](effort: {effort})[/dim]")
175
+ for step in steps[:3]:
176
+ lines.append(f" • {step}")
177
+ return "\n".join(lines)
178
+
179
+
180
+ def _print_footer(report: SecurityReport) -> None:
181
+ total = report.total_findings
182
+ crit = report.findings_by_severity.get("critical", 0)
183
+ high = report.findings_by_severity.get("high", 0)
184
+ med = report.findings_by_severity.get("medium", 0)
185
+ low = report.findings_by_severity.get("low", 0)
186
+
187
+ console.print(
188
+ f"[dim]Total: {total} findings | "
189
+ f"[bold red]Critical: {crit}[/bold red] "
190
+ f"[red]High: {high}[/red] "
191
+ f"[yellow]Medium: {med}[/yellow] "
192
+ f"[cyan]Low: {low}[/cyan][/dim]"
193
+ )
194
+ console.print()
195
+
196
+
197
+ def print_tool_check(tool_statuses: dict[str, tuple[bool, str]]) -> None:
198
+ """Print tool availability check table."""
199
+ table = Table(title="Scanner Tool Status", box=box.ROUNDED, header_style="bold blue")
200
+ table.add_column("Tool")
201
+ table.add_column("Status")
202
+ table.add_column("Notes")
203
+
204
+ for tool, (available, note) in tool_statuses.items():
205
+ status = "[green]Available[/green]" if available else "[red]Not Found[/red]"
206
+ table.add_row(tool, status, note)
207
+
208
+ console.print(table)