Initial commit with translated description
This commit is contained in:
192
README.md
Normal file
192
README.md
Normal 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
50
SKILL.md
Normal 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
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn74agf47w01vwkmce5z1s6795800eas",
|
||||||
|
"slug": "skill-scanner",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"publishedAt": 1769696597407
|
||||||
|
}
|
||||||
392
skill_scanner.py
Normal file
392
skill_scanner.py
Normal 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
289
streamlit_ui.py
Normal 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()
|
||||||
Reference in New Issue
Block a user