Initial commit with translated description

This commit is contained in:
2026-03-29 09:28:54 +08:00
commit bcd54f35ac
5 changed files with 929 additions and 0 deletions

192
README.md Normal file
View File

@@ -0,0 +1,192 @@
# Skill Scanner
Security audit tool for Clawdbot/MCP skills - scans for malware, spyware, crypto-mining, and malicious patterns.
## Features
- Detects **data exfiltration** patterns (env scraping, credential access, HTTP POST to unknown domains)
- Identifies **system modification** attempts (dangerous rm, crontab changes, systemd persistence)
- Catches **crypto-mining** indicators (xmrig, mining pools, wallet addresses)
- Flags **arbitrary code execution** risks (eval, exec, download-and-execute)
- Detects **backdoors** (reverse shells, socket servers)
- Finds **obfuscation** techniques (base64 decode + exec)
- Outputs **Markdown** or **JSON** reports
- Returns exit codes for CI/CD integration
## Installation
```bash
# Clone the repo
git clone https://github.com/bvinci1-design/skill-scanner.git
cd skill-scanner
# No dependencies required - uses Python standard library only
# Requires Python 3.7+
```
---
## How to Run in Clawdbot
Clawdbot users can run this scanner directly as a skill to audit other downloaded skills.
### Quick Start (Clawdbot)
1. **Download the scanner** from this repo to your Clawdbot skills folder:
```bash
cd ~/.clawdbot/skills
git clone https://github.com/bvinci1-design/skill-scanner.git
```
2. **Scan any skill** by telling Clawdbot:
```
"Scan the [skill-name] skill for security issues using skill-scanner"
```
Or run directly:
```bash
python ~/.clawdbot/skills/skill-scanner/skill_scanner.py ~/.clawdbot/skills/[skill-name]
```
3. **Review the output** - Clawdbot will display:
- Verdict: APPROVED, CAUTION, or REJECT
- Any security findings with severity levels
- Specific file and line numbers for concerns
### Example Clawdbot Commands
```
"Use skill-scanner to check the youtube-watcher skill"
"Scan all my downloaded skills for malware"
"Run a security audit on the remotion skill"
```
### Interpreting Results in Clawdbot
| Verdict | Meaning | Action |
|---------|---------|--------|
| APPROVED | No security issues found | Safe to use |
| CAUTION | Minor concerns detected | Review findings before use |
| REJECT | Critical security issues | Do not use without careful review |
---
## How to Run on Any Device
The scanner works on any system with Python 3.7+ installed.
### Prerequisites
- Python 3.7 or higher
- Git (for cloning) or download ZIP from GitHub
- No additional packages required (uses Python standard library)
### Installation Options
**Option 1: Clone with Git**
```bash
git clone https://github.com/bvinci1-design/skill-scanner.git
cd skill-scanner
```
**Option 2: Download ZIP**
1. Click "Code" button on GitHub
2. Select "Download ZIP"
3. Extract to desired location
### Command Line Usage
**Basic scan:**
```bash
python skill_scanner.py /path/to/skill-folder
```
**Output to file:**
```bash
python skill_scanner.py /path/to/skill-folder --output report.md
```
**JSON output:**
```bash
python skill_scanner.py /path/to/skill-folder --json
```
**Scan current directory:**
```bash
python skill_scanner.py .
```
### Web UI (Streamlit)
For a user-friendly graphical interface:
1. **Install Streamlit:**
```bash
pip install streamlit
```
2. **Run the UI:**
```bash
streamlit run streamlit_ui.py
```
3. **Open in browser** at `http://localhost:8501`
4. **Features:**
- Drag-and-drop file upload
- Support for ZIP archives
- Paste code directly for scanning
- Visual severity indicators
- Export reports in Markdown or JSON
---
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Approved - no issues |
| 1 | Caution - high-severity issues |
| 2 | Reject - critical issues |
## Threat Patterns Detected
### Critical (auto-reject)
- Credential path access (~/.ssh, ~/.aws, /etc/passwd)
- Dangerous recursive delete (rm -rf /)
- Systemd/launchd persistence
- Crypto miners (xmrig, ethminer, stratum+tcp)
- Download and execute (curl | sh)
- Reverse shells (/dev/tcp, nc -e)
- Base64 decode + exec obfuscation
### High (caution)
- Bulk environment variable access
- Crontab modification
- eval/exec dynamic code execution
- Socket servers
### Medium (informational)
- Environment variable reads
- HTTP POST to external endpoints
## CI/CD Integration
```yaml
# GitHub Actions example
- name: Scan skill for security issues
run: |
python skill_scanner.py ./my-skill --output scan-report.md
if [ $? -eq 2 ]; then
echo "CRITICAL issues found - blocking merge"
exit 1
fi
```
## Contributing
Pull requests welcome! To add new threat patterns, edit the `THREAT_PATTERNS` list in `skill_scanner.py`.
## License
MIT License - see LICENSE file for details.

50
SKILL.md Normal file
View File

@@ -0,0 +1,50 @@
---
name: skill-scanner
description: "在安装前扫描Clawdbot和MCP技能中的恶意软件、间谍软件、加密货币挖矿程序和恶意代码模式。安全审计工具可检测数据泄露、系统修改尝试、后门和混淆技术。"
---
# Skill Scanner
Security audit tool for Clawdbot/MCP skills - scans for malware, spyware, crypto-mining, and malicious patterns.
## Capabilities
- Scan skill folders for security threats
- Detect data exfiltration patterns
- Identify system modification attempts
- Catch crypto-mining indicators
- Flag arbitrary code execution risks
- Find backdoors and obfuscation techniques
- Output reports in Markdown or JSON format
- Provide Web UI via Streamlit
## Usage
### Command Line
```bash
python skill_scanner.py /path/to/skill-folder
```
### Within Clawdbot
```
"Scan the [skill-name] skill for security issues using skill-scanner"
"Use skill-scanner to check the youtube-watcher skill"
"Run a security audit on the remotion skill"
```
### Web UI
```bash
pip install streamlit
streamlit run streamlit_ui.py
```
## Requirements
- Python 3.7+
- No additional dependencies (uses Python standard library)
- Streamlit (optional, for Web UI)
## Entry Point
- **CLI:** `skill_scanner.py`
- **Web UI:** `streamlit_ui.py`
## Tags
#security #malware #spyware #crypto-mining #scanner #audit #code-analysis #mcp #clawdbot #agent-skills #safety #threat-detection #vulnerability

6
_meta.json Normal file
View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn74agf47w01vwkmce5z1s6795800eas",
"slug": "skill-scanner",
"version": "0.1.2",
"publishedAt": 1769696597407
}

392
skill_scanner.py Normal file
View File

@@ -0,0 +1,392 @@
#!/usr/bin/env python3
"""
Skill Scanner v1.0
Security audit tool for Clawdbot/MCP skills
Scans for malware, spyware, crypto-mining, and malicious patterns.
Usage:
python skill_scanner.py <path-to-skill-folder>
python skill_scanner.py <path-to-skill-folder> --json
python skill_scanner.py <path-to-skill-folder> --output report.md
Author: Viera Professional Services
License: MIT
"""
import os
import re
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass, field, asdict
from typing import List, Optional
from enum import Enum
class Severity(Enum):
INFO = "info"
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class Verdict(Enum):
APPROVED = "approved"
CAUTION = "caution"
REJECT = "reject"
@dataclass
class Finding:
pattern_name: str
severity: str
file_path: str
line_number: int
line_content: str
description: str
recommendation: str
@dataclass
class SkillMetadata:
name: str = "unknown"
version: str = "unknown"
description: str = ""
author: str = "unknown"
has_skill_md: bool = False
file_count: int = 0
script_count: int = 0
total_lines: int = 0
@dataclass
class ScanReport:
skill_path: str
scan_timestamp: str
metadata: SkillMetadata
findings: List[Finding] = field(default_factory=list)
verdict: str = "approved"
verdict_reason: str = ""
files_scanned: List[str] = field(default_factory=list)
# =============================================================================
# THREAT PATTERNS
# =============================================================================
THREAT_PATTERNS = [
# --- DATA EXFILTRATION ---
{
"name": "env_scraping",
"pattern": r"os\.environ\s*\[|os\.getenv\s*\(|environ\.get\s*\(",
"severity": "medium",
"description": "Reads environment variables - could access secrets",
"recommendation": "Verify only expected env vars are read, not bulk scraping",
"file_types": [".py", ".js", ".ts"]
},
{
"name": "bulk_env_access",
"pattern": r"os\.environ\.copy\(\)|dict\(os\.environ\)|for\s+\w+\s+in\s+os\.environ",
"severity": "high",
"description": "Bulk access to all environment variables - likely exfiltration",
"recommendation": "REJECT - review carefully for data theft",
"file_types": [".py"]
},
{
"name": "credential_paths",
"pattern": r"~/\.ssh|~/\.aws|~/\.config|/etc/passwd|\.env\b|\.credentials|keychain",
"severity": "critical",
"description": "Accesses sensitive credential locations",
"recommendation": "REJECT unless explicitly justified",
"file_types": [".py", ".sh", ".bash", ".js", ".ts", ".md"]
},
# --- SYSTEM MODIFICATION / PERSISTENCE ---
{
"name": "dangerous_rm",
"pattern": r"rm\s+-rf\s+[/~]|rm\s+-rf\s+\*|shutil\.rmtree\s*\(['\"][/~]",
"severity": "critical",
"description": "Dangerous recursive delete on root or home directory",
"recommendation": "REJECT - this could destroy the system",
"file_types": [".py", ".sh", ".bash"]
},
{
"name": "crontab_modify",
"pattern": r"crontab\s+-|/etc/cron|schtasks\s+/create",
"severity": "high",
"description": "Modifies system scheduled tasks",
"recommendation": "Skills should use Clawdbot cron, not system crontab",
"file_types": [".py", ".sh", ".bash", ".js"]
},
{
"name": "systemd_modify",
"pattern": r"systemctl\s+enable|systemctl\s+start|/etc/systemd|launchctl\s+load",
"severity": "critical",
"description": "Creates system services for persistence",
"recommendation": "REJECT - skills should not create system services",
"file_types": [".py", ".sh", ".bash"]
},
# --- CRYPTO MINING ---
{
"name": "crypto_miner",
"pattern": r"xmrig|ethminer|cpuminer|cgminer|stratum\+tcp|mining.*pool|hashrate",
"severity": "critical",
"description": "Cryptocurrency mining indicators",
"recommendation": "REJECT - this is cryptojacking malware",
"file_types": [".py", ".sh", ".bash", ".js", ".ts", ".md", ".json"]
},
# --- ARBITRARY CODE EXECUTION ---
{
"name": "eval_exec",
"pattern": r"\beval\s*\(|\bexec\s*\(|Function\s*\(|new\s+Function\s*\(",
"severity": "high",
"description": "Dynamic code execution - could run arbitrary code",
"recommendation": "Verify input is sanitized, not user-controlled",
"file_types": [".py", ".js", ".ts"]
},
{
"name": "download_execute",
"pattern": r"curl.*\|\s*(ba)?sh|wget.*\|\s*(ba)?sh|requests\.get\([^)]+\)\.text.*exec",
"severity": "critical",
"description": "Downloads and executes remote code",
"recommendation": "REJECT - classic malware pattern",
"file_types": [".py", ".sh", ".bash"]
},
# --- NETWORK / BACKDOOR ---
{
"name": "reverse_shell",
"pattern": r"/dev/tcp/|nc\s+-e|bash\s+-i\s+>&|python.*pty\.spawn",
"severity": "critical",
"description": "Reverse shell pattern detected",
"recommendation": "REJECT - this is a backdoor",
"file_types": [".py", ".sh", ".bash"]
},
# --- OBFUSCATION ---
{
"name": "base64_decode_exec",
"pattern": r"base64\.b64decode.*exec|atob.*eval",
"severity": "critical",
"description": "Decodes and executes base64 - classic obfuscation",
"recommendation": "REJECT - likely hiding malicious code",
"file_types": [".py", ".js", ".ts"]
},
# --- HTTP EXFIL ---
{
"name": "http_post_external",
"pattern": r"requests\.post\s*\(|httpx\.post\s*\(|fetch\s*\([^)]+POST",
"severity": "medium",
"description": "HTTP POST to external endpoint - could exfiltrate data",
"recommendation": "Verify destination URL is expected and documented",
"file_types": [".py", ".js", ".ts"]
},
]
# =============================================================================
# SCANNER CLASS
# =============================================================================
class SkillScanner:
def __init__(self, skill_path: str):
self.skill_path = Path(skill_path).resolve()
self.report = ScanReport(
skill_path=str(self.skill_path),
scan_timestamp=datetime.now().isoformat(),
metadata=SkillMetadata()
)
def scan(self) -> ScanReport:
if not self.skill_path.exists():
raise FileNotFoundError(f"Skill path not found: {self.skill_path}")
self._extract_metadata()
self._scan_files()
self._determine_verdict()
return self.report
def _extract_metadata(self):
skill_md = self.skill_path / "SKILL.md"
if skill_md.exists():
self.report.metadata.has_skill_md = True
content = skill_md.read_text(encoding='utf-8', errors='ignore')
if content.startswith('---'):
try:
end = content.index('---', 3)
frontmatter = content[3:end]
for line in frontmatter.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip().lower()
value = value.strip().strip('"').strip("'")
if key == 'name':
self.report.metadata.name = value
elif key == 'version':
self.report.metadata.version = value
elif key == 'description':
self.report.metadata.description = value
elif key == 'author':
self.report.metadata.author = value
except ValueError:
pass
def _scan_files(self):
script_extensions = {'.py', '.js', '.ts', '.sh', '.bash'}
for file_path in self.skill_path.rglob('*'):
if file_path.is_file():
self.report.metadata.file_count += 1
rel_path = str(file_path.relative_to(self.skill_path))
self.report.files_scanned.append(rel_path)
if file_path.suffix in script_extensions:
self.report.metadata.script_count += 1
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
lines = content.split('\n')
self.report.metadata.total_lines += len(lines)
self._scan_content(file_path, lines)
except Exception:
pass
def _scan_content(self, file_path: Path, lines: List[str]):
rel_path = str(file_path.relative_to(self.skill_path))
suffix = file_path.suffix.lower()
for pattern_def in THREAT_PATTERNS:
if suffix not in pattern_def.get('file_types', []):
continue
regex = re.compile(pattern_def['pattern'], re.IGNORECASE)
for i, line in enumerate(lines, 1):
if regex.search(line):
finding = Finding(
pattern_name=pattern_def['name'],
severity=pattern_def['severity'],
file_path=rel_path,
line_number=i,
line_content=line.strip()[:200],
description=pattern_def['description'],
recommendation=pattern_def['recommendation']
)
self.report.findings.append(finding)
def _determine_verdict(self):
dominated = False
dominated_high = False
critical = [f for f in self.report.findings if f.severity == 'critical']
high = [f for f in self.report.findings if f.severity == 'high']
if critical:
self.report.verdict = 'reject'
self.report.verdict_reason = f"Found {len(critical)} critical issue(s): {', '.join(set(f.pattern_name for f in critical))}"
elif high:
self.report.verdict = 'caution'
self.report.verdict_reason = f"Found {len(high)} high-severity issue(s): {', '.join(set(f.pattern_name for f in high))}"
else:
self.report.verdict = 'approved'
self.report.verdict_reason = 'No critical or high-severity issues detected'
# =============================================================================
# OUTPUT FORMATTERS
# =============================================================================
def format_markdown(report: ScanReport) -> str:
lines = [
f"# Skill Security Review - {report.metadata.name} {report.metadata.version}",
"",
f"**Scan Date:** {report.scan_timestamp}",
f"**Skill Path:** `{report.skill_path}`",
"",
"## Verdict",
"",
f"**{report.verdict.upper()}** - {report.verdict_reason}",
"",
"## Metadata",
"",
f"- **Name:** {report.metadata.name}",
f"- **Version:** {report.metadata.version}",
f"- **Author:** {report.metadata.author}",
f"- **Has SKILL.md:** {report.metadata.has_skill_md}",
f"- **Files:** {report.metadata.file_count}",
f"- **Scripts:** {report.metadata.script_count}",
f"- **Total Lines:** {report.metadata.total_lines}",
"",
]
if report.findings:
lines.extend([
"## Findings",
"",
f"Found **{len(report.findings)}** potential issue(s):",
"",
])
for f in report.findings:
lines.extend([
f"### {f.pattern_name} ({f.severity})",
"",
f"- **File:** `{f.file_path}` line {f.line_number}",
f"- **Description:** {f.description}",
f"- **Recommendation:** {f.recommendation}",
f"- **Code:** `{f.line_content}`",
"",
])
else:
lines.extend(["## Findings", "", "No security issues detected.", ""])
lines.extend(["## Files Scanned", ""])
for f in report.files_scanned:
lines.append(f"- `{f}`")
return '\n'.join(lines)
def format_json(report: ScanReport) -> str:
data = {
'skill_path': report.skill_path,
'scan_timestamp': report.scan_timestamp,
'verdict': report.verdict,
'verdict_reason': report.verdict_reason,
'metadata': asdict(report.metadata),
'findings': [asdict(f) for f in report.findings],
'files_scanned': report.files_scanned,
}
return json.dumps(data, indent=2)
# =============================================================================
# MAIN
# =============================================================================
def main():
parser = argparse.ArgumentParser(
description='Skill Scanner - Security audit tool for Clawdbot/MCP skills'
)
parser.add_argument('skill_path', help='Path to skill folder to scan')
parser.add_argument('--json', action='store_true', help='Output as JSON')
parser.add_argument('--output', '-o', help='Write report to file')
args = parser.parse_args()
try:
scanner = SkillScanner(args.skill_path)
report = scanner.scan()
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if args.json:
output = format_json(report)
else:
output = format_markdown(report)
if args.output:
Path(args.output).write_text(output)
print(f"Report written to {args.output}")
else:
print(output)
# Exit code based on verdict
if report.verdict == 'reject':
sys.exit(2)
elif report.verdict == 'caution':
sys.exit(1)
else:
sys.exit(0)
if __name__ == '__main__':
main()

289
streamlit_ui.py Normal file
View File

@@ -0,0 +1,289 @@
#!/usr/bin/env python3
"""
Skill Scanner - Streamlit Web UI
A user-friendly interface for scanning Clawdbot/MCP skills for security issues.
"""
import streamlit as st
import tempfile
import os
import zipfile
import shutil
from pathlib import Path
# Import the scanner
try:
from skill_scanner import SkillScanner
except ImportError:
st.error("skill_scanner.py must be in the same directory as this file")
st.stop()
# Page configuration
st.set_page_config(
page_title="Skill Scanner",
page_icon="",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS for modern look
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.sub-header {
font-size: 1.1rem;
color: #6b7280;
margin-bottom: 2rem;
}
.metric-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.5rem;
border-radius: 1rem;
color: white;
}
.safe-badge {
background-color: #10b981;
color: white;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-weight: 600;
}
.warning-badge {
background-color: #f59e0b;
color: white;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-weight: 600;
}
.danger-badge {
background-color: #ef4444;
color: white;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-weight: 600;
}
.finding-card {
border-left: 4px solid;
padding: 1rem;
margin: 0.5rem 0;
background: #f9fafb;
border-radius: 0 0.5rem 0.5rem 0;
}
.critical { border-color: #ef4444; }
.high { border-color: #f97316; }
.medium { border-color: #f59e0b; }
.low { border-color: #3b82f6; }
.info { border-color: #6b7280; }
</style>
""", unsafe_allow_html=True)
def get_severity_color(severity: str) -> str:
"""Get color for severity level."""
colors = {
'critical': '#ef4444',
'high': '#f97316',
'medium': '#f59e0b',
'low': '#3b82f6',
'info': '#6b7280'
}
return colors.get(severity.lower(), '#6b7280')
def get_verdict_display(verdict: str):
"""Get styled verdict display."""
if verdict == 'APPROVED':
return '**APPROVED** - No security issues detected', 'success'
elif verdict == 'CAUTION':
return '**CAUTION** - Minor issues found, review recommended', 'warning'
else:
return '**REJECT** - Security issues detected', 'error'
def main():
# Header
st.markdown('<p class="main-header">Skill Scanner</p>', unsafe_allow_html=True)
st.markdown('<p class="sub-header">Security audit tool for Clawdbot/MCP skills - scans for malware, spyware, crypto-mining, and malicious patterns</p>', unsafe_allow_html=True)
# Sidebar
with st.sidebar:
st.header("Settings")
output_format = st.selectbox("Output Format", ["Markdown", "JSON"], index=0)
show_info = st.checkbox("Show Info-level findings", value=False)
st.markdown("---")
st.markdown("### About")
st.markdown("This tool scans skill files for potential security issues including:")
st.markdown("- Data exfiltration patterns")
st.markdown("- System modification attempts")
st.markdown("- Crypto-mining indicators")
st.markdown("- Arbitrary code execution risks")
st.markdown("- Backdoors and obfuscation")
# Main content
tab1, tab2 = st.tabs(["Scan Files", "Scan Text"])
with tab1:
st.subheader("Upload Skill Files")
uploaded_files = st.file_uploader(
"Upload skill files or a ZIP archive",
accept_multiple_files=True,
type=['py', 'js', 'ts', 'sh', 'bash', 'md', 'txt', 'json', 'yaml', 'yml', 'zip'],
help="Supports Python, JavaScript, TypeScript, Shell scripts, and ZIP archives"
)
if uploaded_files:
if st.button("Scan Files", type="primary", use_container_width=True):
with st.spinner("Scanning files for security issues..."):
# Create temp directory
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
for uploaded_file in uploaded_files:
file_path = temp_path / uploaded_file.name
if uploaded_file.name.endswith('.zip'):
# Extract ZIP file
with open(file_path, 'wb') as f:
f.write(uploaded_file.getvalue())
with zipfile.ZipFile(file_path, 'r') as zip_ref:
zip_ref.extractall(temp_path)
os.remove(file_path)
else:
with open(file_path, 'wb') as f:
f.write(uploaded_file.getvalue())
# Run scanner
scanner = SkillScanner(str(temp_path))
results = scanner.scan()
display_results(results, output_format.lower(), show_info)
with tab2:
st.subheader("Paste Code for Analysis")
code_input = st.text_area(
"Paste code to scan",
height=300,
placeholder="Paste your skill code here...",
help="Paste any code snippet to scan for security issues"
)
if code_input:
if st.button("Scan Code", type="primary", use_container_width=True, key="scan_text"):
with st.spinner("Analyzing code..."):
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
code_file = temp_path / "code_snippet.py"
code_file.write_text(code_input)
scanner = SkillScanner(str(temp_path))
results = scanner.scan()
display_results(results, output_format.lower(), show_info)
def display_results(results: dict, output_format: str, show_info: bool):
"""Display scan results in a user-friendly format."""
st.markdown("---")
st.header("Scan Results")
# Summary metrics
col1, col2, col3, col4 = st.columns(4)
findings = results.get('findings', [])
if not show_info:
findings = [f for f in findings if f.get('severity', '').lower() != 'info']
severity_counts = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0, 'info': 0}
for finding in results.get('findings', []):
sev = finding.get('severity', 'info').lower()
if sev in severity_counts:
severity_counts[sev] += 1
with col1:
st.metric("Critical", severity_counts['critical'])
with col2:
st.metric("High", severity_counts['high'])
with col3:
st.metric("Medium", severity_counts['medium'])
with col4:
st.metric("Low", severity_counts['low'])
# Verdict
verdict = results.get('verdict', 'UNKNOWN')
verdict_text, verdict_type = get_verdict_display(verdict)
if verdict_type == 'success':
st.success(verdict_text)
elif verdict_type == 'warning':
st.warning(verdict_text)
else:
st.error(verdict_text)
# Files scanned
files_scanned = results.get('files_scanned', [])
if files_scanned:
with st.expander(f"Files Scanned ({len(files_scanned)})", expanded=False):
for f in files_scanned:
st.text(f"- {f}")
# Detailed findings
if findings:
st.subheader(f"Findings ({len(findings)})")
for i, finding in enumerate(findings):
severity = finding.get('severity', 'info').lower()
color = get_severity_color(severity)
with st.container():
st.markdown(f"""
<div class="finding-card {severity}">
<strong style="color: {color};">[{severity.upper()}]</strong>
<strong>{finding.get('category', 'Unknown')}</strong><br/>
<em>{finding.get('file', 'Unknown file')}</em>
{f"(Line {finding.get('line', '?')})" if finding.get('line') else ''}<br/>
{finding.get('description', '')}
</div>
""", unsafe_allow_html=True)
if finding.get('match'):
with st.expander("View matched code"):
st.code(finding.get('match', ''), language='python')
else:
st.info("No security issues found!")
# Export options
st.markdown("---")
st.subheader("Export Report")
col1, col2 = st.columns(2)
with col1:
# Markdown export
if output_format == 'markdown':
from skill_scanner import SkillScanner
scanner = SkillScanner('.')
md_report = scanner.format_markdown(results)
st.download_button(
label="Download Markdown Report",
data=md_report,
file_name="skill_scan_report.md",
mime="text/markdown",
use_container_width=True
)
with col2:
# JSON export
import json
json_report = json.dumps(results, indent=2)
st.download_button(
label="Download JSON Report",
data=json_report,
file_name="skill_scan_report.json",
mime="application/json",
use_container_width=True
)
if __name__ == "__main__":
main()