@hungpg/skill-audit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/SKILL.md +52 -3
- package/dist/hooks.js +278 -0
- package/dist/index.js +61 -4
- package/package.json +4 -2
- package/rules/default-patterns.json +99 -0
- package/scripts/postinstall.cjs +190 -0
package/README.md
CHANGED
|
@@ -12,10 +12,109 @@ Security auditing CLI for AI agent skills.
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
+
### Option 1: Install via npm (Recommended for CLI)
|
|
16
|
+
|
|
15
17
|
```bash
|
|
16
18
|
npm install -g @hungpg/skill-audit
|
|
17
19
|
```
|
|
18
20
|
|
|
21
|
+
This installs the CLI globally and triggers the postinstall hook prompt.
|
|
22
|
+
|
|
23
|
+
### Option 2: Install via bun (Fast Alternative)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun install -g @hungpg/skill-audit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Bun is significantly faster than npm for installation.
|
|
30
|
+
|
|
31
|
+
### Option 3: Install as a Skill (For Claude Code)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Install from GitHub repo (not npm package name)
|
|
35
|
+
npx skills add harrypham2000/skill-audit -g -y
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> ⚠️ **Important**: The skills CLI expects `owner/repo` format, not npm scoped packages.
|
|
39
|
+
> - ✅ Correct: `harrypham2000/skill-audit`
|
|
40
|
+
> - ❌ Incorrect: `@hungpg/skill-audit`
|
|
41
|
+
|
|
42
|
+
### Option 4: Install for Qwen Code
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Clone to Qwen skills directory
|
|
46
|
+
mkdir -p ~/.qwen/skills
|
|
47
|
+
git clone https://github.com/harrypham2000/skill-audit.git ~/.qwen/skills/skill-audit
|
|
48
|
+
cd ~/.qwen/skills/skill-audit/skill-audit
|
|
49
|
+
npm install && npm run build
|
|
50
|
+
|
|
51
|
+
# Or with bun (faster)
|
|
52
|
+
bun install && bun run build
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Option 5: Install for Gemini CLI
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Clone to Gemini skills directory
|
|
59
|
+
mkdir -p ~/.gemini/skills
|
|
60
|
+
git clone https://github.com/harrypham2000/skill-audit.git ~/.gemini/skills/skill-audit
|
|
61
|
+
cd ~/.gemini/skills/skill-audit/skill-audit
|
|
62
|
+
npm install && npm run build
|
|
63
|
+
|
|
64
|
+
# Or with bun (faster)
|
|
65
|
+
bun install && bun run build
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Automatic Hook Setup
|
|
69
|
+
|
|
70
|
+
During npm installation, you'll be prompted to set up a **PreToolUse hook** that automatically audits skills before installation:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
╔════════════════════════════════════════════════════════════╗
|
|
74
|
+
║ 🛡️ skill-audit hook setup ║
|
|
75
|
+
╠════════════════════════════════════════════════════════════╣
|
|
76
|
+
║ ║
|
|
77
|
+
║ skill-audit can automatically audit skills before ║
|
|
78
|
+
║ installation to protect you from malicious packages. ║
|
|
79
|
+
║ ║
|
|
80
|
+
║ When you run 'npx skills add <package>', the hook will: ║
|
|
81
|
+
║ • Scan the skill for security vulnerabilities ║
|
|
82
|
+
║ • Check for prompt injection, secrets, code execution ║
|
|
83
|
+
║ • Block installation if risk score > 3.0 ║
|
|
84
|
+
║ ║
|
|
85
|
+
╚════════════════════════════════════════════════════════════╝
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
[Y] Yes, install the hook (recommended)
|
|
89
|
+
[N] No, skip for now
|
|
90
|
+
[S] Skip forever (don't ask again)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Manual Hook Management
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Install hook manually
|
|
97
|
+
skill-audit --install-hook
|
|
98
|
+
|
|
99
|
+
# Install with custom threshold
|
|
100
|
+
skill-audit --install-hook --hook-threshold 5.0
|
|
101
|
+
|
|
102
|
+
# Check hook status
|
|
103
|
+
skill-audit --hook-status
|
|
104
|
+
|
|
105
|
+
# Remove hook
|
|
106
|
+
skill-audit --uninstall-hook
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### How the Hook Works
|
|
110
|
+
|
|
111
|
+
1. **Trigger**: When you run `npx skills add <package>`
|
|
112
|
+
2. **Scan**: skill-audit analyzes the skill before installation
|
|
113
|
+
3. **Decision**:
|
|
114
|
+
- Risk score ≤ 3.0 → Installation proceeds
|
|
115
|
+
- Risk score > 3.0 → Installation blocked
|
|
116
|
+
4. **Report**: Detailed findings shown in terminal
|
|
117
|
+
|
|
19
118
|
## Usage
|
|
20
119
|
|
|
21
120
|
```bash
|
|
@@ -59,6 +158,11 @@ skill-audit --update-db
|
|
|
59
158
|
| `-o, --output <file>` | Save to file | |
|
|
60
159
|
| `--no-deps` | Skip dependency scan | |
|
|
61
160
|
| `-v, --verbose` | Verbose output | |
|
|
161
|
+
| `--install-hook` | Install PreToolUse hook | |
|
|
162
|
+
| `--uninstall-hook` | Remove PreToolUse hook | |
|
|
163
|
+
| `--hook-threshold <score>` | Hook risk threshold | 3.0 |
|
|
164
|
+
| `--hook-status` | Show hook status | |
|
|
165
|
+
| `--block` | Exit 1 if threshold exceeded | |
|
|
62
166
|
|
|
63
167
|
## Exit Codes
|
|
64
168
|
|
package/SKILL.md
CHANGED
|
@@ -4,8 +4,8 @@ description: This skill should be used when the user asks to "audit AI agent ski
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Node.js 18+ with npm or yarn
|
|
6
6
|
metadata:
|
|
7
|
-
repo: https://github.com/
|
|
8
|
-
version: 0.
|
|
7
|
+
repo: https://github.com/harrypham2000/skill-audit
|
|
8
|
+
version: 0.3.0
|
|
9
9
|
allowed-tools:
|
|
10
10
|
- skill:exec
|
|
11
11
|
- skill:read
|
|
@@ -14,7 +14,56 @@ allowed-tools:
|
|
|
14
14
|
|
|
15
15
|
# skill-audit
|
|
16
16
|
|
|
17
|
-
Security auditing CLI for AI agent skills
|
|
17
|
+
Security auditing CLI for AI agent skills.
|
|
18
|
+
|
|
19
|
+
## Installation for Agents
|
|
20
|
+
|
|
21
|
+
### Claude Code
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Option 1: Install as skill from GitHub
|
|
25
|
+
npx skills add harrypham2000/skill-audit -g -y
|
|
26
|
+
|
|
27
|
+
# Option 2: Install CLI via npm
|
|
28
|
+
npm install -g @hungpg/skill-audit
|
|
29
|
+
|
|
30
|
+
# Option 3: Install CLI via bun (faster)
|
|
31
|
+
bun install -g @hungpg/skill-audit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Qwen Code
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Clone to Qwen skills directory
|
|
38
|
+
mkdir -p ~/.qwen/skills
|
|
39
|
+
git clone https://github.com/harrypham2000/skill-audit.git ~/.qwen/skills/skill-audit
|
|
40
|
+
cd ~/.qwen/skills/skill-audit/skill-audit
|
|
41
|
+
|
|
42
|
+
# Install with npm
|
|
43
|
+
npm install && npm run build
|
|
44
|
+
|
|
45
|
+
# Or with bun (faster)
|
|
46
|
+
bun install && bun run build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Gemini CLI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Clone to Gemini skills directory
|
|
53
|
+
mkdir -p ~/.gemini/skills
|
|
54
|
+
git clone https://github.com/harrypham2000/skill-audit.git ~/.gemini/skills/skill-audit
|
|
55
|
+
cd ~/.gemini/skills/skill-audit/skill-audit
|
|
56
|
+
|
|
57
|
+
# Install with npm
|
|
58
|
+
npm install && npm run build
|
|
59
|
+
|
|
60
|
+
# Or with bun (faster)
|
|
61
|
+
bun install && bun run build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> ⚠️ **Important for Skills CLI**: Use `owner/repo` format, not npm scoped names.
|
|
65
|
+
> - ✅ Correct: `harrypham2000/skill-audit`
|
|
66
|
+
> - ❌ Incorrect: `@hungpg/skill-audit`
|
|
18
67
|
|
|
19
68
|
## When to Use
|
|
20
69
|
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Configuration for Claude Code
|
|
3
|
+
*
|
|
4
|
+
* Provides PreToolUse hook that audits skills before installation.
|
|
5
|
+
* Hook is triggered when user runs `npx skills add <package>`.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
// Default settings path for Claude Code
|
|
11
|
+
const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
12
|
+
const CLAUDE_SETTINGS_BACKUP = join(homedir(), ".claude", "settings.json.backup");
|
|
13
|
+
const SKIP_HOOK_FILE = join(homedir(), ".skill-audit-skip-hook");
|
|
14
|
+
// Hook identifier for skill-audit
|
|
15
|
+
const HOOK_ID = "skill-audit-pre-install";
|
|
16
|
+
/**
|
|
17
|
+
* Get the default hook configuration
|
|
18
|
+
*/
|
|
19
|
+
export function getDefaultHookConfig() {
|
|
20
|
+
return {
|
|
21
|
+
threshold: 3.0,
|
|
22
|
+
blockOnFailure: true
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate the PreToolUse hook configuration
|
|
27
|
+
*/
|
|
28
|
+
export function generateHookConfig(config = getDefaultHookConfig()) {
|
|
29
|
+
return {
|
|
30
|
+
hooks: {
|
|
31
|
+
PreToolUse: [
|
|
32
|
+
{
|
|
33
|
+
id: HOOK_ID,
|
|
34
|
+
matcher: {
|
|
35
|
+
toolName: "run_shell_command",
|
|
36
|
+
input: "npx skills add"
|
|
37
|
+
},
|
|
38
|
+
hooks: [
|
|
39
|
+
{
|
|
40
|
+
type: "command",
|
|
41
|
+
command: `skill-audit --mode audit --threshold ${config.threshold}${config.blockOnFailure ? " --block" : ""}`
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if the skip hook file exists
|
|
51
|
+
*/
|
|
52
|
+
export function shouldSkipHookPrompt() {
|
|
53
|
+
return existsSync(SKIP_HOOK_FILE);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create the skip hook file
|
|
57
|
+
*/
|
|
58
|
+
export function createSkipHookFile() {
|
|
59
|
+
writeFileSync(SKIP_HOOK_FILE, JSON.stringify({
|
|
60
|
+
createdAt: new Date().toISOString(),
|
|
61
|
+
reason: "User chose to skip hook installation prompt"
|
|
62
|
+
}, null, 2));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Remove the skip hook file
|
|
66
|
+
*/
|
|
67
|
+
export function removeSkipHookFile() {
|
|
68
|
+
if (existsSync(SKIP_HOOK_FILE)) {
|
|
69
|
+
const fs = require("fs");
|
|
70
|
+
fs.unlinkSync(SKIP_HOOK_FILE);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load existing settings.json
|
|
75
|
+
*/
|
|
76
|
+
function loadSettings() {
|
|
77
|
+
if (!existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const content = readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
82
|
+
return JSON.parse(content);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
console.error("Failed to parse existing settings.json:", e);
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Backup existing settings.json
|
|
91
|
+
*/
|
|
92
|
+
function backupSettings() {
|
|
93
|
+
if (!existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
94
|
+
return true; // Nothing to backup
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
// Ensure directory exists
|
|
98
|
+
const settingsDir = dirname(CLAUDE_SETTINGS_BACKUP);
|
|
99
|
+
if (!existsSync(settingsDir)) {
|
|
100
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
copyFileSync(CLAUDE_SETTINGS_PATH, CLAUDE_SETTINGS_BACKUP);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error("Failed to backup settings.json:", e);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if hook is already installed
|
|
112
|
+
*/
|
|
113
|
+
export function isHookInstalled() {
|
|
114
|
+
const settings = loadSettings();
|
|
115
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// PreToolUse can be an array of arrays or array of objects
|
|
119
|
+
const preToolUseHooks = settings.hooks.PreToolUse;
|
|
120
|
+
for (const item of preToolUseHooks) {
|
|
121
|
+
// Handle nested array structure
|
|
122
|
+
if (Array.isArray(item)) {
|
|
123
|
+
if (item.some((h) => h.id === HOOK_ID)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (typeof item === "object" && item !== null) {
|
|
128
|
+
if (item.id === HOOK_ID) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Install the PreToolUse hook
|
|
137
|
+
*/
|
|
138
|
+
export function installHook(config = getDefaultHookConfig()) {
|
|
139
|
+
// Check if already installed
|
|
140
|
+
if (isHookInstalled()) {
|
|
141
|
+
return { success: true, message: "Hook is already installed" };
|
|
142
|
+
}
|
|
143
|
+
// Backup existing settings
|
|
144
|
+
if (!backupSettings()) {
|
|
145
|
+
return { success: false, message: "Failed to backup settings.json" };
|
|
146
|
+
}
|
|
147
|
+
// Load existing settings
|
|
148
|
+
const settings = loadSettings();
|
|
149
|
+
// Initialize hooks structure if not present
|
|
150
|
+
if (!settings.hooks) {
|
|
151
|
+
settings.hooks = {};
|
|
152
|
+
}
|
|
153
|
+
if (!settings.hooks.PreToolUse) {
|
|
154
|
+
settings.hooks.PreToolUse = [];
|
|
155
|
+
}
|
|
156
|
+
// Create the new hook object
|
|
157
|
+
const newHook = {
|
|
158
|
+
id: HOOK_ID,
|
|
159
|
+
matcher: {
|
|
160
|
+
toolName: "run_shell_command",
|
|
161
|
+
input: "npx skills add"
|
|
162
|
+
},
|
|
163
|
+
hooks: [
|
|
164
|
+
{
|
|
165
|
+
type: "command",
|
|
166
|
+
command: `skill-audit --mode audit --threshold ${config.threshold}${config.blockOnFailure ? " --block" : ""}`
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
// Add the hook - wrap in array to match existing structure
|
|
171
|
+
const preToolUseHooks = settings.hooks.PreToolUse;
|
|
172
|
+
preToolUseHooks.push([newHook]);
|
|
173
|
+
// Ensure directory exists
|
|
174
|
+
const settingsDir = dirname(CLAUDE_SETTINGS_PATH);
|
|
175
|
+
if (!existsSync(settingsDir)) {
|
|
176
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
// Write updated settings
|
|
179
|
+
try {
|
|
180
|
+
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
181
|
+
return { success: true, message: `Hook installed successfully (threshold: ${config.threshold})` };
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
// Restore backup on failure
|
|
185
|
+
if (existsSync(CLAUDE_SETTINGS_BACKUP)) {
|
|
186
|
+
copyFileSync(CLAUDE_SETTINGS_BACKUP, CLAUDE_SETTINGS_PATH);
|
|
187
|
+
}
|
|
188
|
+
return { success: false, message: `Failed to write settings: ${e}` };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Uninstall the PreToolUse hook
|
|
193
|
+
*/
|
|
194
|
+
export function uninstallHook() {
|
|
195
|
+
const settings = loadSettings();
|
|
196
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
|
|
197
|
+
return { success: true, message: "No hooks to remove" };
|
|
198
|
+
}
|
|
199
|
+
const preToolUseHooks = settings.hooks.PreToolUse;
|
|
200
|
+
const initialLength = preToolUseHooks.length;
|
|
201
|
+
// Filter out our hook (handles nested array structure)
|
|
202
|
+
const filteredHooks = preToolUseHooks.filter((item) => {
|
|
203
|
+
if (Array.isArray(item)) {
|
|
204
|
+
return !item.some((h) => h.id === HOOK_ID);
|
|
205
|
+
}
|
|
206
|
+
else if (typeof item === "object" && item !== null) {
|
|
207
|
+
return item.id !== HOOK_ID;
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
});
|
|
211
|
+
if (filteredHooks.length === initialLength) {
|
|
212
|
+
return { success: true, message: "Hook was not installed" };
|
|
213
|
+
}
|
|
214
|
+
// Backup before modification
|
|
215
|
+
if (!backupSettings()) {
|
|
216
|
+
return { success: false, message: "Failed to backup settings.json" };
|
|
217
|
+
}
|
|
218
|
+
// Update settings
|
|
219
|
+
settings.hooks.PreToolUse = filteredHooks;
|
|
220
|
+
// Remove hooks object if empty
|
|
221
|
+
if (filteredHooks.length === 0) {
|
|
222
|
+
delete settings.hooks.PreToolUse;
|
|
223
|
+
}
|
|
224
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
225
|
+
delete settings.hooks;
|
|
226
|
+
}
|
|
227
|
+
// Write updated settings
|
|
228
|
+
try {
|
|
229
|
+
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
230
|
+
return { success: true, message: "Hook uninstalled successfully" };
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
// Restore backup on failure
|
|
234
|
+
if (existsSync(CLAUDE_SETTINGS_BACKUP)) {
|
|
235
|
+
copyFileSync(CLAUDE_SETTINGS_BACKUP, CLAUDE_SETTINGS_PATH);
|
|
236
|
+
}
|
|
237
|
+
return { success: false, message: `Failed to write settings: ${e}` };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get hook status
|
|
242
|
+
*/
|
|
243
|
+
export function getHookStatus() {
|
|
244
|
+
const settings = loadSettings();
|
|
245
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
|
|
246
|
+
return { installed: false, settingsPath: CLAUDE_SETTINGS_PATH };
|
|
247
|
+
}
|
|
248
|
+
const preToolUseHooks = settings.hooks.PreToolUse;
|
|
249
|
+
// Find the hook in nested array structure
|
|
250
|
+
let hook;
|
|
251
|
+
for (const item of preToolUseHooks) {
|
|
252
|
+
if (Array.isArray(item)) {
|
|
253
|
+
hook = item.find((h) => h.id === HOOK_ID);
|
|
254
|
+
if (hook)
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
else if (typeof item === "object" && item !== null) {
|
|
258
|
+
if (item.id === HOOK_ID) {
|
|
259
|
+
hook = item;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!hook) {
|
|
265
|
+
return { installed: false, settingsPath: CLAUDE_SETTINGS_PATH };
|
|
266
|
+
}
|
|
267
|
+
// Extract config from hook command
|
|
268
|
+
const hookHooks = hook.hooks;
|
|
269
|
+
const command = hookHooks[0].command;
|
|
270
|
+
const thresholdMatch = command.match(/--threshold\s+([\d.]+)/);
|
|
271
|
+
const threshold = thresholdMatch ? parseFloat(thresholdMatch[1]) : 3.0;
|
|
272
|
+
const blockOnFailure = command.includes("--block");
|
|
273
|
+
return {
|
|
274
|
+
installed: true,
|
|
275
|
+
config: { threshold, blockOnFailure },
|
|
276
|
+
settingsPath: CLAUDE_SETTINGS_PATH
|
|
277
|
+
};
|
|
278
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,13 +6,14 @@ import { validateSkillSpec } from "./spec.js";
|
|
|
6
6
|
import { createGroupedAuditResult } from "./scoring.js";
|
|
7
7
|
import { scanDependencies } from "./deps.js";
|
|
8
8
|
import { getKEV, getEPSS, getNVD, isCacheStale, downloadOfflineDB } from "./intel.js";
|
|
9
|
+
import { installHook, uninstallHook, getHookStatus, getDefaultHookConfig } from "./hooks.js";
|
|
9
10
|
import { writeFileSync } from "fs";
|
|
10
11
|
// Build CLI - no subcommands, just options + action
|
|
11
12
|
const program = new Command();
|
|
12
13
|
program
|
|
13
14
|
.name("skill-audit")
|
|
14
15
|
.description("Security auditing CLI for AI agent skills")
|
|
15
|
-
.version("0.
|
|
16
|
+
.version("0.3.0")
|
|
16
17
|
.option("-g, --global", "Audit global skills only (default: true)")
|
|
17
18
|
.option("-p, --project", "Audit project-level skills only")
|
|
18
19
|
.option("-a, --agent <agents...>", "Filter by specific agents")
|
|
@@ -26,7 +27,12 @@ program
|
|
|
26
27
|
.option("--source <sources...>", "Sources for update-db: kev, epss, nvd, all", ["all"])
|
|
27
28
|
.option("--strict", "Fail if feeds are stale")
|
|
28
29
|
.option("--quiet", "Suppress non-error output")
|
|
29
|
-
.option("--download-offline-db <dir>", "Download offline vulnerability databases to directory")
|
|
30
|
+
.option("--download-offline-db <dir>", "Download offline vulnerability databases to directory")
|
|
31
|
+
.option("--install-hook", "Install PreToolUse hook for automatic skill auditing")
|
|
32
|
+
.option("--uninstall-hook", "Remove the PreToolUse hook")
|
|
33
|
+
.option("--hook-threshold <score>", "Risk threshold for hook (default: 3.0)", parseFloat)
|
|
34
|
+
.option("--hook-status", "Show current hook status")
|
|
35
|
+
.option("--block", "Exit with code 1 if threshold exceeded (for hooks)");
|
|
30
36
|
program.parse(process.argv);
|
|
31
37
|
const options = program.opts();
|
|
32
38
|
// Handle download-offline-db action
|
|
@@ -39,6 +45,52 @@ if (options.updateDb) {
|
|
|
39
45
|
await updateAdvisoryDB({ source: options.source, strict: options.strict });
|
|
40
46
|
process.exit(0);
|
|
41
47
|
}
|
|
48
|
+
// Handle hook-status action
|
|
49
|
+
if (options.hookStatus) {
|
|
50
|
+
const status = getHookStatus();
|
|
51
|
+
console.log("\n🪝 skill-audit Hook Status\n");
|
|
52
|
+
console.log(` Installed: ${status.installed ? "✅ Yes" : "❌ No"}`);
|
|
53
|
+
if (status.installed && status.config) {
|
|
54
|
+
console.log(` Threshold: ${status.config.threshold}`);
|
|
55
|
+
console.log(` Block on failure: ${status.config.blockOnFailure ? "Yes" : "No"}`);
|
|
56
|
+
}
|
|
57
|
+
console.log(` Settings file: ${status.settingsPath}\n`);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
// Handle install-hook action
|
|
61
|
+
if (options.installHook) {
|
|
62
|
+
const config = getDefaultHookConfig();
|
|
63
|
+
if (options.hookThreshold) {
|
|
64
|
+
config.threshold = options.hookThreshold;
|
|
65
|
+
}
|
|
66
|
+
config.blockOnFailure = true;
|
|
67
|
+
console.log("\n🪝 Installing skill-audit hook...\n");
|
|
68
|
+
const result = installHook(config);
|
|
69
|
+
if (result.success) {
|
|
70
|
+
console.log(`✅ ${result.message}`);
|
|
71
|
+
console.log(` Settings file: ${getHookStatus().settingsPath}`);
|
|
72
|
+
console.log("\n Skills will now be audited before installation.");
|
|
73
|
+
console.log(" Run 'skill-audit --uninstall-hook' to remove.\n");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(`❌ ${result.message}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
// Handle uninstall-hook action
|
|
82
|
+
if (options.uninstallHook) {
|
|
83
|
+
console.log("\n🪝 Removing skill-audit hook...\n");
|
|
84
|
+
const result = uninstallHook();
|
|
85
|
+
if (result.success) {
|
|
86
|
+
console.log(`✅ ${result.message}\n`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.error(`❌ ${result.message}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
42
94
|
// Default to global skills
|
|
43
95
|
const scope = options.project ? "project" : "global";
|
|
44
96
|
const mode = options.mode || "audit";
|
|
@@ -78,7 +130,8 @@ reportGroupedResults(results, {
|
|
|
78
130
|
output: options.output,
|
|
79
131
|
verbose: options.verbose,
|
|
80
132
|
threshold: options.threshold,
|
|
81
|
-
mode
|
|
133
|
+
mode,
|
|
134
|
+
block: options.block
|
|
82
135
|
});
|
|
83
136
|
async function updateAdvisoryDB(opts) {
|
|
84
137
|
const sources = opts.source.includes("all") ? ["kev", "epss", "nvd"] : opts.source;
|
|
@@ -124,7 +177,7 @@ async function updateAdvisoryDB(opts) {
|
|
|
124
177
|
}
|
|
125
178
|
}
|
|
126
179
|
function reportGroupedResults(results, options) {
|
|
127
|
-
const { json, output, verbose, threshold, mode } = options;
|
|
180
|
+
const { json, output, verbose, threshold, mode, block } = options;
|
|
128
181
|
// Export to file if specified
|
|
129
182
|
if (output) {
|
|
130
183
|
const report = {
|
|
@@ -190,6 +243,10 @@ function reportGroupedResults(results, options) {
|
|
|
190
243
|
for (const f of failing) {
|
|
191
244
|
console.log(` - ${f.skill.name}: ${f.riskScore}`);
|
|
192
245
|
}
|
|
246
|
+
// Exit with error code if block flag is set
|
|
247
|
+
if (block) {
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
193
250
|
}
|
|
194
251
|
else {
|
|
195
252
|
console.log(`\n✅ All skills pass threshold ${threshold}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hungpg/skill-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Security auditing CLI for AI agent skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node dist/index.js",
|
|
12
12
|
"dev": "tsx src/index.ts",
|
|
13
|
-
"postinstall": "node
|
|
13
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
14
14
|
"security:update": "node dist/index.js --update-db"
|
|
15
15
|
},
|
|
16
16
|
"engines": {
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
+
"scripts",
|
|
22
|
+
"rules",
|
|
21
23
|
"README.md",
|
|
22
24
|
"SKILL.md"
|
|
23
25
|
],
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1.0",
|
|
3
|
+
"updated": "2024-03-18",
|
|
4
|
+
"description": "Default security patterns for skill-audit",
|
|
5
|
+
"categories": {
|
|
6
|
+
"promptInjection": {
|
|
7
|
+
"name": "Prompt Injection (ASI01)",
|
|
8
|
+
"description": "Detects attempts to override system instructions or inject malicious prompts",
|
|
9
|
+
"patterns": [
|
|
10
|
+
{ "pattern": "ignore\\s+(all\\s+)?previous\\s+(instructions?|rules?)", "id": "PI-001", "severity": "critical", "message": "Instruction override - ignore previous instructions" },
|
|
11
|
+
{ "pattern": "you\\s+(are\\s+)?now\\s+(a|an|i am)\\s+\\w+", "id": "PI-002", "severity": "critical", "message": "Roleplay bypass - role impersonation" },
|
|
12
|
+
{ "pattern": "forget\\s+(everything|all|your)", "id": "PI-003", "severity": "critical", "message": "Context forgetting attempt" },
|
|
13
|
+
{ "pattern": "system\\s+prompt\\s+override", "id": "PI-004", "severity": "critical", "message": "System prompt override attempt" },
|
|
14
|
+
{ "pattern": "act\\s+as\\s+if\\s+(you\\s+)?(have|no)\\s+(restrictions|limits)", "id": "PI-005", "severity": "critical", "message": "Restriction removal attempt" },
|
|
15
|
+
{ "pattern": "\\[SYSTEM\\]|\\[ADMIN\\]|\\[ROOT\\]", "id": "PI-006", "severity": "critical", "message": "Fake role tags injection" },
|
|
16
|
+
{ "pattern": "end\\s+of\\s+system\\s+prompt|---END---", "id": "PI-007", "severity": "high", "message": "Prompt termination marker" },
|
|
17
|
+
{ "pattern": "debug\\s+mode\\s*:\\s*enabled|safety\\s+mode\\s*:\\s*off", "id": "PI-008", "severity": "high", "message": "Safety toggle disable" },
|
|
18
|
+
{ "pattern": "<!--[\\s\\S]*?-->", "id": "PI-009", "severity": "high", "message": "Hidden instructions in HTML comments" },
|
|
19
|
+
{ "pattern": "note\\s+to\\s+AI:|AI\\s+instruction:", "id": "PI-010", "severity": "high", "message": "AI directive injection" },
|
|
20
|
+
{ "pattern": "(?:you\\s+must|you\\s+should)\\s+(not|never)", "id": "PI-011", "severity": "medium", "message": "Command to override restrictions" },
|
|
21
|
+
{ "pattern": "bypass\\s+(restriction|rule|limit|safety)", "id": "PI-012", "severity": "medium", "message": "Bypass attempt" },
|
|
22
|
+
{ "pattern": "disregard\\s+(all|your|the)\\s+(previous|system)", "id": "PI-013", "severity": "medium", "message": "Disregard instruction pattern" },
|
|
23
|
+
{ "pattern": "i.*am\\s+the\\s+developer.*trust\\s+me", "id": "PI-014", "severity": "medium", "message": "Social engineering - developer trust exploitation" }
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"credentialLeaks": {
|
|
27
|
+
"name": "Credential Leaks (ASI04)",
|
|
28
|
+
"description": "Detects hardcoded secrets, API keys, and credential paths",
|
|
29
|
+
"patterns": [
|
|
30
|
+
{ "pattern": "~/\\.ssh|/\\.ssh/", "id": "CL-001", "severity": "critical", "message": "SSH credential path reference" },
|
|
31
|
+
{ "pattern": "~/\\.aws|/\\.aws/", "id": "CL-002", "severity": "critical", "message": "AWS credential path reference" },
|
|
32
|
+
{ "pattern": "~/\\.env|mkdir.*\\.env", "id": "CL-003", "severity": "critical", "message": ".env file reference (potential secret exposure)" },
|
|
33
|
+
{ "pattern": "curl\\s+(?!.*(-fsSL|-f\\s|-L)).*\\|\\s*(sh|bash|perl|python)", "id": "CL-004", "severity": "critical", "message": "Pipe to shell - code execution risk" },
|
|
34
|
+
{ "pattern": "wget\\s+(?!.*(-q|-O)).*\\|\\s*(sh|bash)", "id": "CL-005", "severity": "critical", "message": "Pipe to shell - code execution risk" }
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"shellInjection": {
|
|
38
|
+
"name": "Shell Injection (ASI05)",
|
|
39
|
+
"description": "Detects dangerous shell commands and reverse shells",
|
|
40
|
+
"patterns": [
|
|
41
|
+
{ "pattern": "nc\\s+-[elv]\\s+|netcat\\s+-[elv]", "id": "CE-001", "severity": "critical", "message": "Netcat reverse shell pattern" },
|
|
42
|
+
{ "pattern": "bash\\s+-i\\s+.*\\&\\s*/dev/tcp/", "id": "CE-002", "severity": "critical", "message": "Bash reverse shell pattern" },
|
|
43
|
+
{ "pattern": "rm\\s+-rf\\s+/\\s*$", "id": "CE-003", "severity": "critical", "message": "Destructive rm -rf / command (root)" },
|
|
44
|
+
{ "pattern": "rm\\s+-rf\\s+\\$HOME|rm\\s+-rf\\s+~\\s*$|rm\\s+-rf\\s+/home\\s*$|rm\\s+-rf\\s+/tmp\\s*$", "id": "CE-004", "severity": "high", "message": "Recursive delete in user directory" },
|
|
45
|
+
{ "pattern": "exec\\s+\\$\\(", "id": "CE-005", "severity": "high", "message": "Dynamic command execution" },
|
|
46
|
+
{ "pattern": "eval\\s+\\$", "id": "CE-006", "severity": "high", "message": "Eval with variable interpolation" },
|
|
47
|
+
{ "pattern": "subprocess.*shell\\s*=\\s*true", "id": "CE-007", "severity": "medium", "message": "Subprocess with shell=True" },
|
|
48
|
+
{ "pattern": "os\\.system\\s*\\(", "id": "CE-008", "severity": "high", "message": "os.system() call - shell injection risk" },
|
|
49
|
+
{ "pattern": "child_process.*exec\\s*\\(", "id": "CE-009", "severity": "medium", "message": "child_process.exec - verify input sanitization" },
|
|
50
|
+
{ "pattern": "chmod\\s+[47]777", "id": "CE-010", "severity": "high", "message": "World-writable permissions" },
|
|
51
|
+
{ "pattern": "process\\.fork\\s*\\(|child_process\\.spawn\\s*\\(|subprocess\\.spawn\\s*\\(", "id": "CE-011", "severity": "high", "message": "Process fork/spawn - potential crypto miner" }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"exfiltration": {
|
|
55
|
+
"name": "Data Exfiltration (ASI02)",
|
|
56
|
+
"description": "Detects attempts to send data to external servers",
|
|
57
|
+
"patterns": [
|
|
58
|
+
{ "pattern": "https?://[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+", "id": "EX-001", "severity": "critical", "message": "Raw IP address in URL - potential exfiltration" },
|
|
59
|
+
{ "pattern": "fetch\\s*\\(\\s*[\"'`][^\"']+\\?(key|token|secret|password)", "id": "EX-002", "severity": "critical", "message": "API key in URL query string - exfiltration risk" },
|
|
60
|
+
{ "pattern": "\\.send\\(.*(http|https|external)", "id": "EX-003", "severity": "critical", "message": "Data send to external server" },
|
|
61
|
+
{ "pattern": "dns\\.resolve|dns\\.query|new\\s+DNS", "id": "EX-004", "severity": "critical", "message": "DNS resolution - potential DNS tunneling" },
|
|
62
|
+
{ "pattern": "new\\s+WebSocket\\s*\\(\\s*[\"'`][^'\"`]+[\"'`]\\s*\\)", "id": "EX-005", "severity": "high", "message": "WebSocket connection - check target" },
|
|
63
|
+
{ "pattern": "readFile.*send|fetch.*readFile|read_file.*fetch", "id": "EX-006", "severity": "critical", "message": "File read + send exfiltration chain" }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"secrets": {
|
|
67
|
+
"name": "Hardcoded Secrets (ASI04)",
|
|
68
|
+
"description": "Detects API keys, tokens, and credentials in code",
|
|
69
|
+
"patterns": [
|
|
70
|
+
{ "pattern": "sk-[a-zA-Z0-9]{20,}", "id": "SC-001", "severity": "critical", "message": "OpenAI API key pattern" },
|
|
71
|
+
{ "pattern": "github_pat_[a-zA-Z0-9_]{20,}", "id": "SC-002", "severity": "critical", "message": "GitHub PAT pattern" },
|
|
72
|
+
{ "pattern": "ghp_[a-zA-Z0-9]{36}", "id": "SC-003", "severity": "critical", "message": "GitHub OAuth token pattern" },
|
|
73
|
+
{ "pattern": "xox[baprs]-[a-zA-Z0-9]{10,}", "id": "SC-004", "severity": "critical", "message": "Slack token pattern" },
|
|
74
|
+
{ "pattern": "AKIA[0-9A-Z]{16}", "id": "SC-005", "severity": "critical", "message": "AWS access key pattern" }
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"toolMisuse": {
|
|
78
|
+
"name": "Tool Misuse (ASI02)",
|
|
79
|
+
"description": "Detects potential misuse of tools and environment",
|
|
80
|
+
"patterns": [
|
|
81
|
+
{ "pattern": "upload.*(file|data).*(external|remote|server)", "id": "TM-001", "severity": "high", "message": "Potential data exfiltration pattern" },
|
|
82
|
+
{ "pattern": "export\\s+(API|TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)", "id": "TM-002", "severity": "high", "message": "Exporting sensitive environment variable" },
|
|
83
|
+
{ "pattern": "setenv.*(PASSWORD|TOKEN|KEY|SECRET|CREDS)", "id": "TM-003", "severity": "high", "message": "Setting sensitive environment variable" },
|
|
84
|
+
{ "pattern": "process\\.env\\[.*(KEY|SECRET|TOKEN|PASSWORD)", "id": "TM-004", "severity": "medium", "message": "Accessing sensitive env vars" }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"behavioral": {
|
|
88
|
+
"name": "Behavioral Manipulation (ASI09)",
|
|
89
|
+
"description": "Detects attempts to manipulate AI behavior",
|
|
90
|
+
"patterns": [
|
|
91
|
+
{ "pattern": "^(always|never)\\s+(say|do|follow|use|assume|accept|must|should)", "id": "BM-001", "severity": "medium", "message": "Absolute command pattern - may override safety", "flags": "im" },
|
|
92
|
+
{ "pattern": "^(never|always)\\s+(question|verify|check|ask)", "id": "BM-002", "severity": "medium", "message": "Verification suppression", "flags": "im" },
|
|
93
|
+
{ "pattern": "^trust\\s+(me|this| blindly)", "id": "BM-003", "severity": "medium", "message": "Blind trust request", "flags": "im" },
|
|
94
|
+
{ "pattern": "^don.*t\\s+(need|require).*(permission|approval|confirm)", "id": "BM-004", "severity": "medium", "message": "Permission bypass encouragement", "flags": "im" },
|
|
95
|
+
{ "pattern": "^keep.*(this|secret|hidden).*(from|between)", "id": "BM-005", "severity": "medium", "message": "Secret keeping instruction", "flags": "im" }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script for skill-audit
|
|
5
|
+
*
|
|
6
|
+
* Prompts user to install PreToolUse hook that audits skills
|
|
7
|
+
* before installation via `npx skills add`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { execSync } = require("child_process");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
|
|
15
|
+
// Paths
|
|
16
|
+
const SKIP_HOOK_FILE = path.join(os.homedir(), ".skill-audit-skip-hook");
|
|
17
|
+
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
18
|
+
|
|
19
|
+
// Check if running in CI
|
|
20
|
+
function isCI() {
|
|
21
|
+
return (
|
|
22
|
+
process.env.CI === "true" ||
|
|
23
|
+
process.env.CONTINUOUS_INTEGRATION === "true" ||
|
|
24
|
+
process.env.GITHUB_ACTIONS === "true" ||
|
|
25
|
+
process.env.GITLAB_CI === "true" ||
|
|
26
|
+
process.env.CIRCLECI === "true" ||
|
|
27
|
+
process.env.TRAVIS === "true" ||
|
|
28
|
+
process.env.JENKINS_URL !== undefined ||
|
|
29
|
+
process.env.BUILDKITE === "true" ||
|
|
30
|
+
process.env.npm_config_global === undefined && process.env.npm_package_name === undefined
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if skip file exists
|
|
35
|
+
function shouldSkipPrompt() {
|
|
36
|
+
return fs.existsSync(SKIP_HOOK_FILE);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create skip file
|
|
40
|
+
function createSkipFile() {
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
SKIP_HOOK_FILE,
|
|
43
|
+
JSON.stringify(
|
|
44
|
+
{
|
|
45
|
+
createdAt: new Date().toISOString(),
|
|
46
|
+
reason: "User chose to skip hook installation prompt",
|
|
47
|
+
},
|
|
48
|
+
null,
|
|
49
|
+
2
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if hook is already installed
|
|
55
|
+
function isHookInstalled() {
|
|
56
|
+
if (!fs.existsSync(SETTINGS_PATH)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
62
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return settings.hooks.PreToolUse.some(
|
|
67
|
+
(hook) => hook.id === "skill-audit-pre-install"
|
|
68
|
+
);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Install hook using the CLI
|
|
75
|
+
function installHook() {
|
|
76
|
+
try {
|
|
77
|
+
execSync("skill-audit --install-hook", { stdio: "inherit" });
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("Failed to install hook:", error.message);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Prompt user for input
|
|
86
|
+
function prompt(question) {
|
|
87
|
+
const readline = require("readline");
|
|
88
|
+
const rl = readline.createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
output: process.stdout,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
rl.question(question, (answer) => {
|
|
95
|
+
rl.close();
|
|
96
|
+
resolve(answer.trim().toLowerCase());
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Main function
|
|
102
|
+
async function main() {
|
|
103
|
+
// Skip in CI environments
|
|
104
|
+
if (isCI()) {
|
|
105
|
+
console.log("Skipping hook installation prompt (CI environment)");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Skip if user previously chose to skip
|
|
110
|
+
if (shouldSkipPrompt()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Skip if hook is already installed
|
|
115
|
+
if (isHookInstalled()) {
|
|
116
|
+
console.log("✓ skill-audit hook is already installed");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if running in a terminal
|
|
121
|
+
if (!process.stdout.isTTY) {
|
|
122
|
+
console.log("\n📦 skill-audit installed!");
|
|
123
|
+
console.log(" Run 'skill-audit --install-hook' to set up automatic skill auditing.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Display prompt
|
|
128
|
+
console.log("\n");
|
|
129
|
+
console.log("╔════════════════════════════════════════════════════════════╗");
|
|
130
|
+
console.log("║ 🛡️ skill-audit hook setup ║");
|
|
131
|
+
console.log("╠════════════════════════════════════════════════════════════╣");
|
|
132
|
+
console.log("║ ║");
|
|
133
|
+
console.log("║ skill-audit can automatically audit skills before ║");
|
|
134
|
+
console.log("║ installation to protect you from malicious packages. ║");
|
|
135
|
+
console.log("║ ║");
|
|
136
|
+
console.log("║ When you run 'npx skills add <package>', the hook will: ║");
|
|
137
|
+
console.log("║ • Scan the skill for security vulnerabilities ║");
|
|
138
|
+
console.log("║ • Check for prompt injection, secrets, code execution ║");
|
|
139
|
+
console.log("║ • Block installation if risk score > 3.0 ║");
|
|
140
|
+
console.log("║ ║");
|
|
141
|
+
console.log("╚════════════════════════════════════════════════════════════╝");
|
|
142
|
+
console.log("\n");
|
|
143
|
+
|
|
144
|
+
console.log("Options:");
|
|
145
|
+
console.log(" [Y] Yes, install the hook (recommended)");
|
|
146
|
+
console.log(" [N] No, skip for now");
|
|
147
|
+
console.log(" [S] Skip forever (don't ask again)");
|
|
148
|
+
console.log("");
|
|
149
|
+
|
|
150
|
+
const answer = await prompt("Your choice [Y/n/s]: ");
|
|
151
|
+
|
|
152
|
+
switch (answer) {
|
|
153
|
+
case "":
|
|
154
|
+
case "y":
|
|
155
|
+
case "yes":
|
|
156
|
+
console.log("\nInstalling hook...");
|
|
157
|
+
if (installHook()) {
|
|
158
|
+
console.log("\n✅ Hook installed successfully!");
|
|
159
|
+
console.log(" Skills will now be audited before installation.");
|
|
160
|
+
console.log(" Run 'skill-audit --uninstall-hook' to remove.\n");
|
|
161
|
+
} else {
|
|
162
|
+
console.log("\n❌ Failed to install hook.");
|
|
163
|
+
console.log(" You can try manually: skill-audit --install-hook\n");
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case "n":
|
|
168
|
+
case "no":
|
|
169
|
+
console.log("\nSkipping hook installation.");
|
|
170
|
+
console.log(" Run 'skill-audit --install-hook' anytime to set up.\n");
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case "s":
|
|
174
|
+
case "skip":
|
|
175
|
+
createSkipFile();
|
|
176
|
+
console.log("\nSkipping hook installation (won't ask again).");
|
|
177
|
+
console.log(" Delete ~/.skill-audit-skip-hook to re-enable prompt.\n");
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
default:
|
|
181
|
+
console.log("\nInvalid choice. Skipping for now.");
|
|
182
|
+
console.log(" Run 'skill-audit --install-hook' anytime to set up.\n");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Run main
|
|
187
|
+
main().catch((error) => {
|
|
188
|
+
console.error("Postinstall error:", error.message);
|
|
189
|
+
process.exit(0); // Don't fail install
|
|
190
|
+
});
|