Skill: bvinci1-design/skill-scanner (version 0.1.2)

--- name: skill-scanner description: Scan Clawdbot and MCP skills for malware, spyware, crypto-miners, and malicious code patterns before you install them. Security audit tool that detects data exfiltration, system modification attempts, backdoors, and obfuscation techniques. --- # 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

path: README.md
content: # 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.


path: skill_scanner.py
content: #!/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()


path: streamlit_ui.py
content: #!/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()


Security scan verdicts — ClawScan: suspicious | static analysis: malicious | VirusTotal: clean | SkillSpector:  ()