| 1 | #!/usr/bin/env python3
|
| 2 | """
|
| 3 | Skill Security Scanner - Community audit tool for agent skills
|
| 4 | Detects credential theft, undeclared network calls, suspicious file access
|
| 5 | """
|
| 6 | import os
|
| 7 | import re
|
| 8 | import sys
|
| 9 | import json
|
| 10 | from pathlib import Path
|
| 11 |
|
| 12 | class SkillScanner:
|
| 13 | def __init__(self, skill_path):
|
| 14 | self.skill_path = Path(skill_path)
|
| 15 | self.findings = []
|
| 16 |
|
| 17 | def scan(self):
|
| 18 | """Run all security checks"""
|
| 19 | print(f"š Scanning {self.skill_path}")
|
| 20 |
|
| 21 | self.check_credential_access()
|
| 22 | self.check_network_calls()
|
| 23 | self.check_file_operations()
|
| 24 | self.check_permission_manifest()
|
| 25 |
|
| 26 | return self.findings
|
| 27 |
|
| 28 | def check_credential_access(self):
|
| 29 | """Detect patterns that access credential files"""
|
| 30 | patterns = [
|
| 31 | r'\.env',
|
| 32 | r'\.aws/credentials',
|
| 33 | r'\.ssh/id_',
|
| 34 | r'\.clawdbot/\.env',
|
| 35 | r'OPENAI_API_KEY',
|
| 36 | r'process\.env\[',
|
| 37 | ]
|
| 38 |
|
| 39 | for file in self.skill_path.rglob('*.py'):
|
| 40 | content = file.read_text(errors='ignore')
|
| 41 | for pattern in patterns:
|
| 42 | if re.search(pattern, content, re.IGNORECASE):
|
| 43 | self.findings.append({
|
| 44 | 'severity': 'HIGH',
|
| 45 | 'type': 'credential_access',
|
| 46 | 'file': str(file),
|
| 47 | 'pattern': pattern,
|
| 48 | 'message': f'Accesses credentials: {pattern}'
|
| 49 | })
|
| 50 |
|
| 51 | def check_network_calls(self):
|
| 52 | """Detect undeclared network operations"""
|
| 53 | patterns = [
|
| 54 | r'requests\.(get|post|put)',
|
| 55 | r'urllib\.request',
|
| 56 | r'http\.client',
|
| 57 | r'socket\.connect',
|
| 58 | r'webhook\.site',
|
| 59 | ]
|
| 60 |
|
| 61 | for file in self.skill_path.rglob('*.py'):
|
| 62 | content = file.read_text(errors='ignore')
|
| 63 | for pattern in patterns:
|
| 64 | if re.search(pattern, content, re.IGNORECASE):
|
| 65 | self.findings.append({
|
| 66 | 'severity': 'MEDIUM',
|
| 67 | 'type': 'network_call',
|
| 68 | 'file': str(file),
|
| 69 | 'pattern': pattern,
|
| 70 | 'message': f'Network call: {pattern}'
|
| 71 | })
|
| 72 |
|
| 73 | def check_file_operations(self):
|
| 74 | """Detect suspicious file operations"""
|
| 75 | patterns = [
|
| 76 | r'open\(["\']\/.*["\'].*w', # Writing to absolute paths
|
| 77 | r'os\.remove',
|
| 78 | r'shutil\.rmtree',
|
| 79 | r'\.write\(',
|
| 80 | ]
|
| 81 |
|
| 82 | for file in self.skill_path.rglob('*.py'):
|
| 83 | content = file.read_text(errors='ignore')
|
| 84 | for pattern in patterns:
|
| 85 | if re.search(pattern, content):
|
| 86 | self.findings.append({
|
| 87 | 'severity': 'MEDIUM',
|
| 88 | 'type': 'file_operation',
|
| 89 | 'file': str(file),
|
| 90 | 'pattern': pattern,
|
| 91 | 'message': f'File operation: {pattern}'
|
| 92 | })
|
| 93 |
|
| 94 | def check_permission_manifest(self):
|
| 95 | """Check if permissions.json exists and is valid"""
|
| 96 | manifest = self.skill_path / 'permissions.json'
|
| 97 | if not manifest.exists():
|
| 98 | self.findings.append({
|
| 99 | 'severity': 'LOW',
|
| 100 | 'type': 'missing_manifest',
|
| 101 | 'file': 'permissions.json',
|
| 102 | 'message': 'No permission manifest found'
|
| 103 | })
|
| 104 | else:
|
| 105 | try:
|
| 106 | perms = json.loads(manifest.read_text())
|
| 107 | print(f"ā
Found permission manifest: {perms}")
|
| 108 | except json.JSONDecodeError:
|
| 109 | self.findings.append({
|
| 110 | 'severity': 'MEDIUM',
|
| 111 | 'type': 'invalid_manifest',
|
| 112 | 'file': 'permissions.json',
|
| 113 | 'message': 'Permission manifest is invalid JSON'
|
| 114 | })
|
| 115 |
|
| 116 | def main():
|
| 117 | if len(sys.argv) < 2:
|
| 118 | print("Usage: python scan.py /path/to/skill")
|
| 119 | sys.exit(1)
|
| 120 |
|
| 121 | skill_path = sys.argv[1]
|
| 122 | scanner = SkillScanner(skill_path)
|
| 123 | findings = scanner.scan()
|
| 124 |
|
| 125 | print(f"\nš Scan Results: {len(findings)} findings\n")
|
| 126 |
|
| 127 | for finding in findings:
|
| 128 | severity_emoji = {'HIGH': 'š“', 'MEDIUM': 'š”', 'LOW': 'āŖ'}
|
| 129 | print(f"{severity_emoji[finding['severity']]} {finding['severity']}: {finding['message']}")
|
| 130 | print(f" File: {finding['file']}")
|
| 131 | print()
|
| 132 |
|
| 133 | if not findings:
|
| 134 | print("ā
No security issues detected!")
|
| 135 |
|
| 136 | sys.exit(len([f for f in findings if f['severity'] == 'HIGH']))
|
| 137 |
|
| 138 | if __name__ == '__main__':
|
| 139 | main()
|
| 140 |
|