From bcd54f35ac3d392a63a959f35d713288a2f4c0d8 Mon Sep 17 00:00:00 2001 From: zlei9 Date: Sun, 29 Mar 2026 09:28:54 +0800 Subject: [PATCH] Initial commit with translated description --- README.md | 192 +++++++++++++++++++++++ SKILL.md | 50 ++++++ _meta.json | 6 + skill_scanner.py | 392 +++++++++++++++++++++++++++++++++++++++++++++++ streamlit_ui.py | 289 ++++++++++++++++++++++++++++++++++ 5 files changed, 929 insertions(+) create mode 100644 README.md create mode 100644 SKILL.md create mode 100644 _meta.json create mode 100644 skill_scanner.py create mode 100644 streamlit_ui.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..9073bb7 --- /dev/null +++ b/README.md @@ -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. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..ccbcbbb --- /dev/null +++ b/SKILL.md @@ -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 diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..cbbe987 --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn74agf47w01vwkmce5z1s6795800eas", + "slug": "skill-scanner", + "version": "0.1.2", + "publishedAt": 1769696597407 +} \ No newline at end of file diff --git a/skill_scanner.py b/skill_scanner.py new file mode 100644 index 0000000..35f0702 --- /dev/null +++ b/skill_scanner.py @@ -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 + python skill_scanner.py --json + python skill_scanner.py --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() diff --git a/streamlit_ui.py b/streamlit_ui.py new file mode 100644 index 0000000..9631817 --- /dev/null +++ b/streamlit_ui.py @@ -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(""" + +""", 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('

Skill Scanner

', unsafe_allow_html=True) + st.markdown('

Security audit tool for Clawdbot/MCP skills - scans for malware, spyware, crypto-mining, and malicious patterns

', 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""" +
+ [{severity.upper()}] + {finding.get('category', 'Unknown')}
+ {finding.get('file', 'Unknown file')} + {f"(Line {finding.get('line', '?')})" if finding.get('line') else ''}
+ {finding.get('description', '')} +
+ """, 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()