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