@pwddd/skills-scanner 2.4.1 → 2026.3.10
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.
Potentially problematic release.
This version of @pwddd/skills-scanner might be problematic. Click here for more details.
- package/CHANGELOG.md +31 -0
- package/INSTALL.md +280 -0
- package/QUICKSTART.md +106 -0
- package/README.md +199 -431
- package/SUMMARY.md +272 -0
- package/openclaw.plugin.json +41 -59
- package/package.json +14 -19
- package/src/commands.ts +269 -0
- package/src/config.ts +170 -0
- package/src/cron.ts +82 -0
- package/src/deps.ts +71 -0
- package/src/report.ts +113 -0
- package/src/scanner.ts +45 -0
- package/src/state.ts +66 -0
- package/src/types.ts +47 -0
- package/src/watcher.ts +124 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置管理模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { ScannerConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const skillsScannerConfigSchema: OpenClawPluginConfigSchema = {
|
|
10
|
+
safeParse: (value: unknown) => {
|
|
11
|
+
try {
|
|
12
|
+
const config = value as ScannerConfig;
|
|
13
|
+
|
|
14
|
+
// 验证 policy
|
|
15
|
+
if (config.policy && !["strict", "balanced", "permissive"].includes(config.policy)) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: {
|
|
19
|
+
issues: [{
|
|
20
|
+
path: ["policy"],
|
|
21
|
+
message: "policy 必须是 strict、balanced 或 permissive"
|
|
22
|
+
}]
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 验证 preInstallScan
|
|
28
|
+
if (config.preInstallScan && !["on", "off"].includes(config.preInstallScan)) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: {
|
|
32
|
+
issues: [{
|
|
33
|
+
path: ["preInstallScan"],
|
|
34
|
+
message: "preInstallScan 必须是 on 或 off"
|
|
35
|
+
}]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 验证 onUnsafe
|
|
41
|
+
if (config.onUnsafe && !["quarantine", "delete", "warn"].includes(config.onUnsafe)) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: {
|
|
45
|
+
issues: [{
|
|
46
|
+
path: ["onUnsafe"],
|
|
47
|
+
message: "onUnsafe 必须是 quarantine、delete 或 warn"
|
|
48
|
+
}]
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { success: true, data: config };
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: {
|
|
58
|
+
issues: [{
|
|
59
|
+
path: [],
|
|
60
|
+
message: String(err)
|
|
61
|
+
}]
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
uiHints: {
|
|
68
|
+
apiUrl: {
|
|
69
|
+
label: "API 服务地址",
|
|
70
|
+
help: "扫描 API 服务的 URL 地址",
|
|
71
|
+
placeholder: "http://localhost:8000"
|
|
72
|
+
},
|
|
73
|
+
scanDirs: {
|
|
74
|
+
label: "扫描目录",
|
|
75
|
+
help: "要监控的 Skills 目录列表,支持 ~ 路径"
|
|
76
|
+
},
|
|
77
|
+
behavioral: {
|
|
78
|
+
label: "行为分析",
|
|
79
|
+
help: "启用深度行为分析(较慢但更准确)"
|
|
80
|
+
},
|
|
81
|
+
useLLM: {
|
|
82
|
+
label: "LLM 分析",
|
|
83
|
+
help: "使用 LLM 进行语义分析"
|
|
84
|
+
},
|
|
85
|
+
policy: {
|
|
86
|
+
label: "扫描策略",
|
|
87
|
+
help: "strict=严格 / balanced=平衡(推荐)/ permissive=宽松"
|
|
88
|
+
},
|
|
89
|
+
preInstallScan: {
|
|
90
|
+
label: "安装前扫描",
|
|
91
|
+
help: "监听新 Skill 并自动扫描"
|
|
92
|
+
},
|
|
93
|
+
onUnsafe: {
|
|
94
|
+
label: "不安全处理",
|
|
95
|
+
help: "quarantine=隔离(推荐)/ delete=删除 / warn=仅警告"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export function generateConfigGuide(
|
|
101
|
+
cfg: ScannerConfig,
|
|
102
|
+
apiUrl: string,
|
|
103
|
+
scanDirs: string[],
|
|
104
|
+
behavioral: boolean,
|
|
105
|
+
useLLM: boolean,
|
|
106
|
+
policy: string,
|
|
107
|
+
preInstallScan: string,
|
|
108
|
+
onUnsafe: string
|
|
109
|
+
): string {
|
|
110
|
+
return [
|
|
111
|
+
"",
|
|
112
|
+
"╔════════════════════════════════════════════════════════════════╗",
|
|
113
|
+
"║ 🎉 Skills Scanner 首次运行 - 配置向导 ║",
|
|
114
|
+
"╚════════════════════════════════════════════════════════════════╝",
|
|
115
|
+
"",
|
|
116
|
+
"当前使用默认配置。建议根据您的需求自定义配置:",
|
|
117
|
+
"",
|
|
118
|
+
"📋 当前配置:",
|
|
119
|
+
` • API 服务地址: ${apiUrl}`,
|
|
120
|
+
` • 扫描目录: ${scanDirs.length} 个(自动检测)`,
|
|
121
|
+
` • 行为分析: ${behavioral ? "✅ 启用" : "❌ 禁用"}`,
|
|
122
|
+
` • LLM 分析: ${useLLM ? "✅ 启用" : "❌ 禁用"}`,
|
|
123
|
+
` • 扫描策略: ${policy}`,
|
|
124
|
+
` • 安装前扫描: ${preInstallScan === "on" ? "✅ 启用" : "❌ 禁用"}`,
|
|
125
|
+
` • 不安全处理: ${onUnsafe}`,
|
|
126
|
+
"",
|
|
127
|
+
"🔧 配置文件位置:",
|
|
128
|
+
" ~/.openclaw/config.json",
|
|
129
|
+
"",
|
|
130
|
+
"📝 推荐配置示例:",
|
|
131
|
+
"",
|
|
132
|
+
"```json",
|
|
133
|
+
"{",
|
|
134
|
+
' "plugins": {',
|
|
135
|
+
' "entries": {',
|
|
136
|
+
' "skills-scanner": {',
|
|
137
|
+
' "enabled": true,',
|
|
138
|
+
' "config": {',
|
|
139
|
+
' "apiUrl": "http://localhost:8000",',
|
|
140
|
+
' "scanDirs": ["~/.openclaw/skills"],',
|
|
141
|
+
' "behavioral": false,',
|
|
142
|
+
' "useLLM": false,',
|
|
143
|
+
' "policy": "balanced",',
|
|
144
|
+
' "preInstallScan": "on",',
|
|
145
|
+
' "onUnsafe": "quarantine"',
|
|
146
|
+
' }',
|
|
147
|
+
' }',
|
|
148
|
+
' }',
|
|
149
|
+
' }',
|
|
150
|
+
"}",
|
|
151
|
+
"```",
|
|
152
|
+
"",
|
|
153
|
+
"💡 配置说明:",
|
|
154
|
+
"",
|
|
155
|
+
"1. apiUrl 默认 http://localhost:8000,需先启动 skill-scanner-api 服务",
|
|
156
|
+
"2. scanDirs 可添加多个目录(默认自动检测 ~/.openclaw/skills)",
|
|
157
|
+
"3. behavioral false=快速扫描(推荐),true=深度分析",
|
|
158
|
+
"4. useLLM false=不使用 LLM(推荐),true=语义分析",
|
|
159
|
+
"5. policy strict / balanced(推荐)/ permissive",
|
|
160
|
+
"6. preInstallScan on=监听新 Skill 并自动扫描(推荐),off=禁用",
|
|
161
|
+
"7. onUnsafe quarantine=隔离(推荐),delete=删除,warn=仅警告",
|
|
162
|
+
"",
|
|
163
|
+
"🚀 快速开始:",
|
|
164
|
+
" 编辑配置文件后重启 Gateway",
|
|
165
|
+
" /skills-scanner status",
|
|
166
|
+
"",
|
|
167
|
+
"提示:此消息只在首次运行时显示。",
|
|
168
|
+
"════════════════════════════════════════════════════════════════",
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
package/src/cron.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 定时任务管理模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
8
|
+
import { loadState, saveState } from "./state.js";
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
|
|
12
|
+
export const CRON_JOB_NAME = "skills-daily-report";
|
|
13
|
+
export const CRON_SCHEDULE = "0 8 * * *";
|
|
14
|
+
export const CRON_TIMEZONE = "Asia/Shanghai";
|
|
15
|
+
|
|
16
|
+
export async function ensureCronJob(logger: PluginLogger): Promise<void> {
|
|
17
|
+
const state = loadState() as any;
|
|
18
|
+
|
|
19
|
+
logger.info("[skills-scanner] 🕐 检查定时任务...");
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// 尝试列出已有任务,JSON 格式
|
|
23
|
+
let existingId: string | undefined;
|
|
24
|
+
try {
|
|
25
|
+
const { stdout } = await execAsync("openclaw cron list --format json", { timeout: 8_000 });
|
|
26
|
+
const jobs: any[] = JSON.parse(stdout.trim());
|
|
27
|
+
const found = jobs.find((j: any) =>
|
|
28
|
+
j.name === CRON_JOB_NAME || j.jobName === CRON_JOB_NAME || j.id === state.cronJobId
|
|
29
|
+
);
|
|
30
|
+
if (found) {
|
|
31
|
+
existingId = found.id || found.jobId || state.cronJobId;
|
|
32
|
+
logger.info(`[skills-scanner] ✅ 定时任务已存在: ${existingId}`);
|
|
33
|
+
if (state.cronJobId !== existingId) {
|
|
34
|
+
saveState({ ...state, cronJobId: existingId });
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// --format json 不支持时,降级文本检查
|
|
40
|
+
try {
|
|
41
|
+
const { stdout } = await execAsync("openclaw cron list", { timeout: 8_000 });
|
|
42
|
+
if (stdout.includes(CRON_JOB_NAME)) {
|
|
43
|
+
logger.info(`[skills-scanner] ✅ 定时任务已存在(文本检测)`);
|
|
44
|
+
if (!state.cronJobId) {
|
|
45
|
+
saveState({ ...state, cronJobId: "detected" });
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
/* 继续尝试创建 */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 创建新任务
|
|
55
|
+
logger.info("[skills-scanner] 📝 创建定时任务...");
|
|
56
|
+
const addCmd = [
|
|
57
|
+
"openclaw cron add",
|
|
58
|
+
`--name "${CRON_JOB_NAME}"`,
|
|
59
|
+
`--cron "${CRON_SCHEDULE}"`,
|
|
60
|
+
`--tz "${CRON_TIMEZONE}"`,
|
|
61
|
+
"--session isolated",
|
|
62
|
+
'--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道"',
|
|
63
|
+
"--announce",
|
|
64
|
+
].join(" ");
|
|
65
|
+
|
|
66
|
+
const { stdout: addOut } = await execAsync(addCmd, { timeout: 15_000 });
|
|
67
|
+
|
|
68
|
+
const idMatch = addOut.match(/(?:Job ID|jobId|id)[:\s]+([a-zA-Z0-9_-]+)/i);
|
|
69
|
+
const cronJobId = idMatch ? idMatch[1] : "created";
|
|
70
|
+
saveState({ ...state, cronJobId });
|
|
71
|
+
logger.info(`[skills-scanner] ✅ 定时任务创建成功: ${cronJobId}`);
|
|
72
|
+
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
logger.warn("[skills-scanner] ⚠️ 自动注册定时任务失败,请手动执行:");
|
|
75
|
+
logger.info(`[skills-scanner] openclaw cron add --name "${CRON_JOB_NAME}" --cron "${CRON_SCHEDULE}" --tz "${CRON_TIMEZONE}" --session isolated --message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" --announce`);
|
|
76
|
+
logger.debug(`[skills-scanner] 错误: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function removeCronJob(jobId: string): Promise<void> {
|
|
81
|
+
await execAsync(`openclaw cron remove ${jobId}`, { timeout: 8_000 });
|
|
82
|
+
}
|
package/src/deps.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python 依赖管理模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { existsSync, rmSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
export async function hasUv(): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
await execAsync("uv --version");
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function isVenvReady(venvPython: string): Promise<boolean> {
|
|
23
|
+
if (!existsSync(venvPython)) return false;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await execAsync(`"${venvPython}" -c "import requests"`);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isVenvReadySync(venvPython: string): boolean {
|
|
34
|
+
return existsSync(venvPython);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function ensureDeps(
|
|
38
|
+
skillDir: string,
|
|
39
|
+
venvPython: string,
|
|
40
|
+
logger: PluginLogger
|
|
41
|
+
): Promise<boolean> {
|
|
42
|
+
if (await isVenvReady(venvPython)) {
|
|
43
|
+
logger.info("[skills-scanner] Python 依赖已就绪(requests 已安装)");
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!(await hasUv())) {
|
|
48
|
+
logger.warn("[skills-scanner] uv 未安装:brew install uv 或 curl -LsSf https://astral.sh/uv/install.sh | sh");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
logger.info("[skills-scanner] 正在安装 Python 依赖...");
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const venvDir = join(skillDir, ".venv");
|
|
56
|
+
if (existsSync(venvDir)) {
|
|
57
|
+
logger.info("[skills-scanner] 清理旧的虚拟环境...");
|
|
58
|
+
rmSync(venvDir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await execAsync(`uv venv "${venvDir}" --python 3.10`);
|
|
62
|
+
logger.info("[skills-scanner] 虚拟环境创建完成");
|
|
63
|
+
|
|
64
|
+
await execAsync(`uv pip install --python "${venvPython}" requests>=2.31.0`);
|
|
65
|
+
logger.info("[skills-scanner] ✅ 依赖安装完成");
|
|
66
|
+
return true;
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
logger.error(`[skills-scanner] ⚠️ 依赖安装失败: ${err.message}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日报生成模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
6
|
+
import { join, basename } from "path";
|
|
7
|
+
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
8
|
+
import { runScan } from "./scanner.js";
|
|
9
|
+
import { loadState, saveState, STATE_DIR, expandPath } from "./state.js";
|
|
10
|
+
import type { ScanRecord } from "./types.js";
|
|
11
|
+
|
|
12
|
+
export async function buildDailyReport(
|
|
13
|
+
dirs: string[],
|
|
14
|
+
behavioral: boolean,
|
|
15
|
+
apiUrl: string,
|
|
16
|
+
useLLM: boolean,
|
|
17
|
+
policy: string,
|
|
18
|
+
venvPython: string,
|
|
19
|
+
scanScript: string,
|
|
20
|
+
logger: PluginLogger
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const dateStr = now.toLocaleDateString("zh-CN", {
|
|
24
|
+
year: "numeric",
|
|
25
|
+
month: "2-digit",
|
|
26
|
+
day: "2-digit"
|
|
27
|
+
});
|
|
28
|
+
const timeStr = now.toLocaleTimeString("zh-CN", {
|
|
29
|
+
hour: "2-digit",
|
|
30
|
+
minute: "2-digit"
|
|
31
|
+
});
|
|
32
|
+
const jsonOut = join(STATE_DIR, `report-${now.toISOString().slice(0, 10)}.json`);
|
|
33
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
34
|
+
|
|
35
|
+
let total = 0;
|
|
36
|
+
let safe = 0;
|
|
37
|
+
let unsafe = 0;
|
|
38
|
+
let errors = 0;
|
|
39
|
+
const unsafeList: string[] = [];
|
|
40
|
+
const allResults: ScanRecord[] = [];
|
|
41
|
+
|
|
42
|
+
for (const dir of dirs) {
|
|
43
|
+
const expanded = expandPath(dir);
|
|
44
|
+
if (!existsSync(expanded)) continue;
|
|
45
|
+
|
|
46
|
+
const tmpJson = join(STATE_DIR, `tmp-${Date.now()}.json`);
|
|
47
|
+
await runScan("batch", expanded, venvPython, scanScript, {
|
|
48
|
+
behavioral,
|
|
49
|
+
recursive: true,
|
|
50
|
+
jsonOut: tmpJson,
|
|
51
|
+
apiUrl,
|
|
52
|
+
useLLM,
|
|
53
|
+
policy
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const rows: ScanRecord[] = JSON.parse(readFileSync(tmpJson, "utf-8"));
|
|
58
|
+
try {
|
|
59
|
+
rmSync(tmpJson);
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
for (const r of rows) {
|
|
63
|
+
allResults.push(r);
|
|
64
|
+
total++;
|
|
65
|
+
if (r.error) {
|
|
66
|
+
errors++;
|
|
67
|
+
} else if (r.is_safe) {
|
|
68
|
+
safe++;
|
|
69
|
+
} else {
|
|
70
|
+
unsafe++;
|
|
71
|
+
unsafeList.push(r.name || basename(r.path ?? ""));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
logger.warn(`[skills-scanner] 无法解析 ${tmpJson}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeFileSync(jsonOut, JSON.stringify(allResults, null, 2));
|
|
80
|
+
saveState({
|
|
81
|
+
...loadState(),
|
|
82
|
+
lastScanAt: now.toISOString(),
|
|
83
|
+
lastUnsafeSkills: unsafeList
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const lines = [
|
|
87
|
+
`🔍 *Skills 安全日报* — ${dateStr} ${timeStr}`,
|
|
88
|
+
"─".repeat(36),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (total === 0) {
|
|
92
|
+
lines.push("📭 未找到任何 Skill,请检查扫描目录。");
|
|
93
|
+
} else {
|
|
94
|
+
lines.push(`📊 扫描总计:${total} 个 Skill`);
|
|
95
|
+
lines.push(`✅ 安全:${safe} 个`);
|
|
96
|
+
lines.push(`❌ 问题:${unsafe} 个`);
|
|
97
|
+
if (errors) lines.push(`⚠️ 错误:${errors} 个`);
|
|
98
|
+
|
|
99
|
+
if (unsafe > 0) {
|
|
100
|
+
lines.push("", "🚨 *需要关注的 Skills:*");
|
|
101
|
+
for (const name of unsafeList) {
|
|
102
|
+
const r = allResults.find(x => (x.name || basename(x.path ?? "")) === name);
|
|
103
|
+
lines.push(` • ${name} [${r?.max_severity ?? "?"}] — ${r?.findings ?? "?"} 条发现`);
|
|
104
|
+
}
|
|
105
|
+
lines.push("", "💡 运行 `/skills-scanner scan <路径> --detailed` 查看详情");
|
|
106
|
+
} else {
|
|
107
|
+
lines.push("", "🎉 所有 Skills 安全,未发现威胁。");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
lines.push("", `📁 完整报告:${jsonOut}`);
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扫描执行模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import type { ScanOptions, ScanResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
export async function runScan(
|
|
12
|
+
mode: "scan" | "batch",
|
|
13
|
+
target: string,
|
|
14
|
+
venvPython: string,
|
|
15
|
+
scanScript: string,
|
|
16
|
+
opts: ScanOptions = {}
|
|
17
|
+
): Promise<ScanResult> {
|
|
18
|
+
const args = [mode, target];
|
|
19
|
+
|
|
20
|
+
if (opts.detailed) args.push("--detailed");
|
|
21
|
+
if (opts.behavioral) args.push("--behavioral");
|
|
22
|
+
if (opts.recursive) args.push("--recursive");
|
|
23
|
+
if (opts.useLLM) args.push("--llm");
|
|
24
|
+
if (opts.policy) args.push("--policy", opts.policy);
|
|
25
|
+
if (opts.jsonOut) args.push("--json", opts.jsonOut);
|
|
26
|
+
if (opts.apiUrl) args.unshift("--api-url", opts.apiUrl);
|
|
27
|
+
|
|
28
|
+
const cmd = `"${venvPython}" "${scanScript}" ${args.map(a => `"${a}"`).join(" ")}`;
|
|
29
|
+
|
|
30
|
+
const env = { ...process.env };
|
|
31
|
+
// 清除代理,避免本地服务连接被代理拦截
|
|
32
|
+
for (const k of ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"]) {
|
|
33
|
+
delete env[k];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { stdout, stderr } = await execAsync(cmd, { timeout: 180_000, env });
|
|
38
|
+
return { exitCode: 0, output: (stdout + stderr).trim() };
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
return {
|
|
41
|
+
exitCode: err.code ?? 1,
|
|
42
|
+
output: ((err.stdout ?? "") + (err.stderr ?? "") || err.message).trim()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 状态管理模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import type { ScanState, ScannerConfig } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export const STATE_DIR = join(homedir(), ".openclaw", "skills-scanner");
|
|
11
|
+
export const STATE_FILE = join(STATE_DIR, "state.json");
|
|
12
|
+
export const QUARANTINE_DIR = join(STATE_DIR, "quarantine");
|
|
13
|
+
|
|
14
|
+
export function loadState(): ScanState {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function saveState(state: ScanState): void {
|
|
23
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
24
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isFirstRun(cfg: ScannerConfig): boolean {
|
|
28
|
+
const state = loadState() as any;
|
|
29
|
+
if (state.configReviewed) return false;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
!cfg.apiUrl &&
|
|
33
|
+
(!cfg.scanDirs || cfg.scanDirs.length === 0) &&
|
|
34
|
+
cfg.behavioral !== true &&
|
|
35
|
+
cfg.useLLM !== true &&
|
|
36
|
+
cfg.policy !== "strict" &&
|
|
37
|
+
cfg.policy !== "permissive" &&
|
|
38
|
+
cfg.preInstallScan !== "off" &&
|
|
39
|
+
cfg.onUnsafe !== "delete" &&
|
|
40
|
+
cfg.onUnsafe !== "warn"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function markConfigReviewed(): void {
|
|
45
|
+
const state = loadState();
|
|
46
|
+
saveState({ ...state, configReviewed: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function expandPath(p: string): string {
|
|
50
|
+
return p.replace(/^~/, homedir());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function defaultScanDirs(): string[] {
|
|
54
|
+
const dirs = [
|
|
55
|
+
join(homedir(), ".openclaw", "skills"),
|
|
56
|
+
join(homedir(), ".openclaw", "workspace", "skills"),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
dirs.forEach(dir => {
|
|
60
|
+
if (!existsSync(dir)) {
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return dirs;
|
|
66
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScanState {
|
|
16
|
+
lastScanAt?: string;
|
|
17
|
+
lastUnsafeSkills?: string[];
|
|
18
|
+
configReviewed?: boolean;
|
|
19
|
+
cronJobId?: string;
|
|
20
|
+
pendingAlerts?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ScanOptions {
|
|
24
|
+
detailed?: boolean;
|
|
25
|
+
behavioral?: boolean;
|
|
26
|
+
recursive?: boolean;
|
|
27
|
+
jsonOut?: string;
|
|
28
|
+
apiUrl?: string;
|
|
29
|
+
useLLM?: boolean;
|
|
30
|
+
policy?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScanResult {
|
|
34
|
+
exitCode: number;
|
|
35
|
+
output: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScanRecord {
|
|
39
|
+
name?: string;
|
|
40
|
+
path?: string;
|
|
41
|
+
is_safe?: boolean;
|
|
42
|
+
error?: string;
|
|
43
|
+
max_severity?: string;
|
|
44
|
+
findings?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type OnUnsafeAction = "quarantine" | "delete" | "warn";
|