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.
- shieldbot_mcp-1.0.0/PKG-INFO +111 -0
- shieldbot_mcp-1.0.0/README.md +78 -0
- shieldbot_mcp-1.0.0/pyproject.toml +56 -0
- shieldbot_mcp-1.0.0/shieldbot/__init__.py +3 -0
- shieldbot_mcp-1.0.0/shieldbot/config.py +61 -0
- shieldbot_mcp-1.0.0/shieldbot/models.py +108 -0
- shieldbot_mcp-1.0.0/shieldbot/reporters/__init__.py +1 -0
- shieldbot_mcp-1.0.0/shieldbot/reporters/console_reporter.py +208 -0
- shieldbot_mcp-1.0.0/shieldbot/reporters/html_reporter.py +195 -0
- shieldbot_mcp-1.0.0/shieldbot/reporters/json_reporter.py +30 -0
- shieldbot_mcp-1.0.0/shieldbot/reporters/sarif_reporter.py +101 -0
- shieldbot_mcp-1.0.0/shieldbot/run_scan.py +261 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/__init__.py +21 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/bandit_scanner.py +97 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/base.py +152 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/npm_audit_scanner.py +120 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/pip_audit_scanner.py +146 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/ruff_scanner.py +100 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/secrets_scanner.py +150 -0
- shieldbot_mcp-1.0.0/shieldbot/scanners/semgrep_scanner.py +152 -0
- shieldbot_mcp-1.0.0/shieldbot/server.py +119 -0
|
@@ -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,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)
|