@pwddd/skills-scanner 1.0.0-beta.1
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 +255 -0
- package/index.ts +647 -0
- package/openclaw.plugin.json +122 -0
- package/package.json +64 -0
- package/skills/skills-scanner/SKILL.md +281 -0
- package/src/api-client.ts +245 -0
- package/src/before-install-hook.ts +241 -0
- package/src/cache.ts +138 -0
- package/src/commands.ts +289 -0
- package/src/config-validator.ts +110 -0
- package/src/config.ts +230 -0
- package/src/cron-manager.ts +210 -0
- package/src/debug.ts +40 -0
- package/src/error-handler.ts +103 -0
- package/src/high-risk-operation-guard.ts +62 -0
- package/src/metrics.ts +140 -0
- package/src/prompt-guidance.ts +80 -0
- package/src/prompt-injection-guard.ts +56 -0
- package/src/rate-limiter.ts +102 -0
- package/src/report.ts +128 -0
- package/src/scanner.ts +230 -0
- package/src/state.ts +136 -0
- package/src/structured-logger.ts +97 -0
- package/src/types.ts +76 -0
- package/src/watcher.ts +178 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Scanner 类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ScannerConfig {
|
|
6
|
+
apiUrl?: string;
|
|
7
|
+
scanDirs?: string[];
|
|
8
|
+
behavioral?: boolean;
|
|
9
|
+
useLLM?: boolean;
|
|
10
|
+
policy?: "strict" | "balanced" | "permissive";
|
|
11
|
+
preInstallScan?: "on" | "off";
|
|
12
|
+
onUnsafe?: "quarantine" | "delete" | "warn";
|
|
13
|
+
injectSecurityGuidance?: boolean;
|
|
14
|
+
enablePromptInjectionGuard?: boolean;
|
|
15
|
+
enableHighRiskOperationGuard?: boolean;
|
|
16
|
+
enableBeforeInstallHook?: boolean;
|
|
17
|
+
scanTimeoutMs?: number; // Scan timeout in milliseconds (default: 180000)
|
|
18
|
+
reportDir?: string; // Custom report directory
|
|
19
|
+
quarantineDir?: string; // Custom quarantine directory
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ScanState {
|
|
23
|
+
version?: number; // State schema version
|
|
24
|
+
lastScanAt?: string;
|
|
25
|
+
lastShutdownAt?: string;
|
|
26
|
+
lastUninstallAt?: string; // Track when plugin was uninstalled
|
|
27
|
+
lastUnsafeSkills?: string[];
|
|
28
|
+
configReviewed?: boolean;
|
|
29
|
+
cronJobId?: string;
|
|
30
|
+
pendingAlerts?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScanOptions {
|
|
34
|
+
detailed?: boolean;
|
|
35
|
+
behavioral?: boolean;
|
|
36
|
+
recursive?: boolean;
|
|
37
|
+
jsonOut?: string;
|
|
38
|
+
apiUrl?: string;
|
|
39
|
+
useLLM?: boolean;
|
|
40
|
+
policy?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ScanResult {
|
|
44
|
+
exitCode: number;
|
|
45
|
+
output: string;
|
|
46
|
+
data?: ScanResultData;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ScanResultData {
|
|
50
|
+
is_safe?: boolean;
|
|
51
|
+
max_severity?: string;
|
|
52
|
+
findings?: number;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ScanRecord {
|
|
57
|
+
name?: string;
|
|
58
|
+
path?: string;
|
|
59
|
+
is_safe?: boolean;
|
|
60
|
+
error?: string;
|
|
61
|
+
max_severity?: string;
|
|
62
|
+
findings?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type OnUnsafeAction = "quarantine" | "delete" | "warn";
|
|
66
|
+
|
|
67
|
+
export interface CommandResponse {
|
|
68
|
+
text: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PluginLogger {
|
|
72
|
+
debug(message: string, data?: any): void;
|
|
73
|
+
info(message: string, data?: any): void;
|
|
74
|
+
warn(message: string, data?: any): void;
|
|
75
|
+
error(message: string, data?: any): void;
|
|
76
|
+
}
|
package/src/watcher.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watcher module for pre-installation scanning
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { watch as fsWatch, existsSync, renameSync, rmSync } from "node:fs";
|
|
6
|
+
import { join, basename } from "node:path";
|
|
7
|
+
import { mkdirSync } from "node:fs";
|
|
8
|
+
import { runScan } from "./scanner.js";
|
|
9
|
+
import type { OnUnsafeAction, PluginLogger } from "./types.js";
|
|
10
|
+
|
|
11
|
+
// Debounce delay in milliseconds
|
|
12
|
+
const DEBOUNCE_DELAY = 1000; // Increased from 500ms to 1000ms for better stability
|
|
13
|
+
|
|
14
|
+
export async function handleNewSkill(
|
|
15
|
+
skillPath: string,
|
|
16
|
+
onUnsafe: OnUnsafeAction,
|
|
17
|
+
behavioral: boolean,
|
|
18
|
+
apiUrl: string,
|
|
19
|
+
useLLM: boolean,
|
|
20
|
+
policy: string,
|
|
21
|
+
notifyFn: (msg: string) => void,
|
|
22
|
+
logger: PluginLogger,
|
|
23
|
+
quarantineDir: string
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
if (!existsSync(join(skillPath, "SKILL.md"))) return;
|
|
26
|
+
|
|
27
|
+
const name = basename(skillPath);
|
|
28
|
+
logger.info(`[skills-scanner] 🔍 检测到新 Skill,开始安装前扫描:${name}`);
|
|
29
|
+
notifyFn(`🔍 检测到新 Skill \`${name}\`,正在安全扫描...`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const res = await runScan("scan", skillPath, {
|
|
33
|
+
behavioral,
|
|
34
|
+
detailed: true,
|
|
35
|
+
apiUrl,
|
|
36
|
+
useLLM,
|
|
37
|
+
policy,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (res.exitCode === 0) {
|
|
41
|
+
notifyFn(`✅ \`${name}\` 安全检查通过,可以正常使用。`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let action = "";
|
|
46
|
+
try {
|
|
47
|
+
if (onUnsafe === "quarantine") {
|
|
48
|
+
mkdirSync(quarantineDir, { recursive: true });
|
|
49
|
+
const dest = join(quarantineDir, `${name}-${Date.now()}`);
|
|
50
|
+
renameSync(skillPath, dest);
|
|
51
|
+
action = `已移入隔离目录:\`${dest}\``;
|
|
52
|
+
} else if (onUnsafe === "delete") {
|
|
53
|
+
rmSync(skillPath, { recursive: true, force: true });
|
|
54
|
+
action = "已自动删除";
|
|
55
|
+
} else {
|
|
56
|
+
action = "仅警告,Skill 已保留(请谨慎使用)";
|
|
57
|
+
}
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
action = `处置失败:${e.message}`;
|
|
60
|
+
logger.error("[skills-scanner] Failed to handle unsafe skill", {
|
|
61
|
+
skill: name,
|
|
62
|
+
error: e.message,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
notifyFn(
|
|
67
|
+
[
|
|
68
|
+
`❌ *安全警告:\`${name}\` 未通过扫描*`,
|
|
69
|
+
`处置:${action}`,
|
|
70
|
+
"```",
|
|
71
|
+
res.output.slice(0, 600),
|
|
72
|
+
"```",
|
|
73
|
+
].join("\n")
|
|
74
|
+
);
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
logger.error("[skills-scanner] Scan failed for new skill", {
|
|
77
|
+
skill: name,
|
|
78
|
+
error: err.message,
|
|
79
|
+
stack: err.stack,
|
|
80
|
+
});
|
|
81
|
+
notifyFn(`⚠️ \`${name}\` 扫描失败:${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function startWatcher(
|
|
86
|
+
dirs: string[],
|
|
87
|
+
onUnsafe: OnUnsafeAction,
|
|
88
|
+
behavioral: boolean,
|
|
89
|
+
apiUrl: string,
|
|
90
|
+
useLLM: boolean,
|
|
91
|
+
policy: string,
|
|
92
|
+
notifyFn: (msg: string) => void,
|
|
93
|
+
logger: PluginLogger,
|
|
94
|
+
quarantineDir: string
|
|
95
|
+
): () => void {
|
|
96
|
+
const timers = new Map<string, NodeJS.Timeout>();
|
|
97
|
+
const processing = new Set<string>(); // Track files being processed
|
|
98
|
+
|
|
99
|
+
const watchers = dirs.map((dir) => {
|
|
100
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
101
|
+
logger.info(`[skills-scanner] 👁 监听目录:${dir}`);
|
|
102
|
+
|
|
103
|
+
const watcher = fsWatch(dir, { persistent: false }, (_evt, filename) => {
|
|
104
|
+
if (!filename) return;
|
|
105
|
+
const skillPath = join(dir, filename);
|
|
106
|
+
|
|
107
|
+
// Skip if file doesn't exist or is already being processed
|
|
108
|
+
if (!existsSync(skillPath) || processing.has(skillPath)) return;
|
|
109
|
+
|
|
110
|
+
// Clear existing timer for this path (debounce)
|
|
111
|
+
const prev = timers.get(skillPath);
|
|
112
|
+
if (prev) clearTimeout(prev);
|
|
113
|
+
|
|
114
|
+
// Set new timer with debounce delay
|
|
115
|
+
timers.set(
|
|
116
|
+
skillPath,
|
|
117
|
+
setTimeout(async () => {
|
|
118
|
+
timers.delete(skillPath);
|
|
119
|
+
processing.add(skillPath);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await handleNewSkill(
|
|
123
|
+
skillPath,
|
|
124
|
+
onUnsafe,
|
|
125
|
+
behavioral,
|
|
126
|
+
apiUrl,
|
|
127
|
+
useLLM,
|
|
128
|
+
policy,
|
|
129
|
+
notifyFn,
|
|
130
|
+
logger,
|
|
131
|
+
quarantineDir
|
|
132
|
+
);
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
logger.error("[skills-scanner] Watcher handler failed", {
|
|
135
|
+
path: skillPath,
|
|
136
|
+
error: err.message,
|
|
137
|
+
});
|
|
138
|
+
} finally {
|
|
139
|
+
processing.delete(skillPath);
|
|
140
|
+
}
|
|
141
|
+
}, DEBOUNCE_DELAY)
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Add error handler for watcher
|
|
146
|
+
watcher.on("error", (error: Error) => {
|
|
147
|
+
logger.error("[skills-scanner] Watcher error", {
|
|
148
|
+
directory: dir,
|
|
149
|
+
error: error.message,
|
|
150
|
+
stack: error.stack,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return watcher;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return () => {
|
|
158
|
+
try {
|
|
159
|
+
watchers.forEach((w) => {
|
|
160
|
+
try {
|
|
161
|
+
w.close();
|
|
162
|
+
} catch (err: any) {
|
|
163
|
+
logger.warn("[skills-scanner] Failed to close watcher", {
|
|
164
|
+
error: err.message,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
timers.forEach((t) => clearTimeout(t));
|
|
169
|
+
timers.clear();
|
|
170
|
+
processing.clear();
|
|
171
|
+
logger.info("[skills-scanner] 目录监听已停止");
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
logger.error("[skills-scanner] Error stopping watcher", {
|
|
174
|
+
error: err.message,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|