@pwddd/skills-scanner 1.0.0-beta.2 → 1.0.0-beta.21
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 +3 -45
- package/index.ts +13 -266
- package/openclaw.plugin.json +1 -11
- package/package.json +1 -1
- package/skills/skills-scanner/SKILL.md +15 -51
- package/src/api-client.ts +32 -2
- package/src/before-install-hook.ts +42 -9
- package/src/commands.ts +21 -98
- package/src/config-validator.ts +0 -16
- package/src/config.ts +8 -50
- package/src/cron-manager.ts +117 -169
- package/src/prompt-guidance.ts +8 -18
- package/src/state.ts +0 -17
- package/src/types.ts +0 -4
- package/src/high-risk-operation-guard.ts +0 -62
- package/src/prompt-injection-guard.ts +0 -56
- package/src/report.ts +0 -128
- package/src/watcher.ts +0 -178
package/src/report.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Report generation module
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
6
|
-
import { join, basename } from "node:path";
|
|
7
|
-
import { runScan } from "./scanner.js";
|
|
8
|
-
import { loadState, saveState, STATE_DIR } from "./state.js";
|
|
9
|
-
import type { ScanRecord, PluginLogger } from "./types.js";
|
|
10
|
-
|
|
11
|
-
export async function buildDailyReport(
|
|
12
|
-
dirs: string[],
|
|
13
|
-
behavioral: boolean,
|
|
14
|
-
apiUrl: string,
|
|
15
|
-
useLLM: boolean,
|
|
16
|
-
policy: string,
|
|
17
|
-
logger: PluginLogger
|
|
18
|
-
): Promise<string> {
|
|
19
|
-
const now = new Date();
|
|
20
|
-
const dateStr = now.toLocaleDateString("en-US", {
|
|
21
|
-
year: "numeric",
|
|
22
|
-
month: "2-digit",
|
|
23
|
-
day: "2-digit",
|
|
24
|
-
});
|
|
25
|
-
const timeStr = now.toLocaleTimeString("en-US", {
|
|
26
|
-
hour: "2-digit",
|
|
27
|
-
minute: "2-digit",
|
|
28
|
-
});
|
|
29
|
-
const jsonOut = join(STATE_DIR, `report-${now.toISOString().slice(0, 10)}.json`);
|
|
30
|
-
mkdirSync(STATE_DIR, { recursive: true });
|
|
31
|
-
|
|
32
|
-
let total = 0;
|
|
33
|
-
let safe = 0;
|
|
34
|
-
let unsafe = 0;
|
|
35
|
-
let errors = 0;
|
|
36
|
-
const unsafeList: string[] = [];
|
|
37
|
-
const allResults: ScanRecord[] = [];
|
|
38
|
-
|
|
39
|
-
for (const dir of dirs) {
|
|
40
|
-
if (!existsSync(dir)) continue;
|
|
41
|
-
|
|
42
|
-
// Find all skills in directory
|
|
43
|
-
const skills: string[] = [];
|
|
44
|
-
try {
|
|
45
|
-
const entries = readdirSync(dir);
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
const skillPath = join(dir, entry);
|
|
48
|
-
if (statSync(skillPath).isDirectory() && existsSync(join(skillPath, "SKILL.md"))) {
|
|
49
|
-
skills.push(skillPath);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
} catch {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
for (const skillPath of skills) {
|
|
57
|
-
try {
|
|
58
|
-
const res = await runScan("scan", skillPath, {
|
|
59
|
-
behavioral,
|
|
60
|
-
detailed: false,
|
|
61
|
-
apiUrl,
|
|
62
|
-
useLLM,
|
|
63
|
-
policy,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const name = basename(skillPath);
|
|
67
|
-
total++;
|
|
68
|
-
|
|
69
|
-
if (res.exitCode === 0) {
|
|
70
|
-
safe++;
|
|
71
|
-
allResults.push({
|
|
72
|
-
name,
|
|
73
|
-
path: skillPath,
|
|
74
|
-
is_safe: true,
|
|
75
|
-
max_severity: "NONE",
|
|
76
|
-
findings: 0,
|
|
77
|
-
});
|
|
78
|
-
} else {
|
|
79
|
-
unsafe++;
|
|
80
|
-
unsafeList.push(name);
|
|
81
|
-
allResults.push({
|
|
82
|
-
name,
|
|
83
|
-
path: skillPath,
|
|
84
|
-
is_safe: false,
|
|
85
|
-
max_severity: res.data?.max_severity || "UNKNOWN",
|
|
86
|
-
findings: res.data?.findings_count || 0,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
} catch (err: any) {
|
|
90
|
-
errors++;
|
|
91
|
-
allResults.push({
|
|
92
|
-
name: basename(skillPath),
|
|
93
|
-
path: skillPath,
|
|
94
|
-
error: err.message,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
writeFileSync(jsonOut, JSON.stringify(allResults, null, 2));
|
|
101
|
-
saveState({
|
|
102
|
-
...loadState(),
|
|
103
|
-
lastScanAt: now.toISOString(),
|
|
104
|
-
lastUnsafeSkills: unsafeList,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const lines = [`🔍 *Skills 安全日报* — ${dateStr} ${timeStr}`, "─".repeat(36)];
|
|
108
|
-
if (total === 0) {
|
|
109
|
-
lines.push("📭 未找到任何 Skill,请检查扫描目录。");
|
|
110
|
-
} else {
|
|
111
|
-
lines.push(`📊 扫描总计:${total} 个 Skill`);
|
|
112
|
-
lines.push(`✅ 安全:${safe} 个`);
|
|
113
|
-
lines.push(`❌ 问题:${unsafe} 个`);
|
|
114
|
-
if (errors) lines.push(`⚠️ 错误:${errors} 个`);
|
|
115
|
-
if (unsafe > 0) {
|
|
116
|
-
lines.push("", "🚨 *需要关注的 Skills:*");
|
|
117
|
-
for (const name of unsafeList) {
|
|
118
|
-
const r = allResults.find((x) => x.name === name);
|
|
119
|
-
lines.push(` • ${name} [${r?.max_severity ?? "?"}] — ${r?.findings ?? "?"} 条发现`);
|
|
120
|
-
}
|
|
121
|
-
lines.push("", "💡 运行 `/skills-scanner scan <路径> --detailed` 查看详情");
|
|
122
|
-
} else {
|
|
123
|
-
lines.push("", "🎉 所有 Skills 安全,未发现威胁。");
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
lines.push("", `📁 完整报告:${jsonOut}`);
|
|
127
|
-
return lines.join("\n");
|
|
128
|
-
}
|
package/src/watcher.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
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
|
-
}
|