@guava-parity/guard-scanner 5.1.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/LICENSE +21 -0
- package/README.md +250 -0
- package/SECURITY.md +45 -0
- package/SKILL.md +141 -0
- package/docs/THREAT_TAXONOMY.md +308 -0
- package/hooks/guard-scanner/HOOK.md +93 -0
- package/hooks/guard-scanner/handler.ts +5 -0
- package/hooks/guard-scanner/plugin.ts +308 -0
- package/openclaw.plugin.json +55 -0
- package/package.json +58 -0
- package/src/cli.js +170 -0
- package/src/html-template.js +239 -0
- package/src/ioc-db.js +54 -0
- package/src/patterns.js +249 -0
- package/src/quarantine.js +41 -0
- package/src/runtime-guard.js +346 -0
- package/src/scanner.js +1045 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guard-scanner Runtime Guard — Plugin Hook Version
|
|
3
|
+
*
|
|
4
|
+
* Intercepts agent tool calls via the Plugin Hook API and blocks
|
|
5
|
+
* dangerous patterns using `block` / `blockReason`.
|
|
6
|
+
*
|
|
7
|
+
* 19 threat patterns across 3 layers:
|
|
8
|
+
* Layer 1: Threat Detection (12 patterns — reverse shells, exfil, etc.)
|
|
9
|
+
* Layer 2: Trust Defense (4 patterns — memory, SOUL, config tampering)
|
|
10
|
+
* Layer 3: Safety Judge (3 patterns — prompt injection, trust bypass, shutdown refusal)
|
|
11
|
+
*
|
|
12
|
+
* Modes:
|
|
13
|
+
* monitor — log only, never block
|
|
14
|
+
* enforce — block CRITICAL threats (default)
|
|
15
|
+
* strict — block HIGH + CRITICAL threats
|
|
16
|
+
*
|
|
17
|
+
* @author Guava 🍈 & Dee
|
|
18
|
+
* @version 3.1.0
|
|
19
|
+
* @license MIT
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { appendFileSync, mkdirSync, readFileSync } from "fs";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
|
|
26
|
+
// ── Types (from OpenClaw src/plugins/types.ts) ──
|
|
27
|
+
|
|
28
|
+
type PluginHookBeforeToolCallEvent = {
|
|
29
|
+
toolName: string;
|
|
30
|
+
params: Record<string, unknown>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type PluginHookBeforeToolCallResult = {
|
|
34
|
+
params?: Record<string, unknown>;
|
|
35
|
+
block?: boolean;
|
|
36
|
+
blockReason?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type PluginHookToolContext = {
|
|
40
|
+
agentId?: string;
|
|
41
|
+
sessionKey?: string;
|
|
42
|
+
toolName: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type PluginAPI = {
|
|
46
|
+
on(
|
|
47
|
+
hookName: "before_tool_call",
|
|
48
|
+
handler: (
|
|
49
|
+
event: PluginHookBeforeToolCallEvent,
|
|
50
|
+
ctx: PluginHookToolContext
|
|
51
|
+
) => PluginHookBeforeToolCallResult | void | Promise<PluginHookBeforeToolCallResult | void>
|
|
52
|
+
): void;
|
|
53
|
+
logger: {
|
|
54
|
+
info: (msg: string) => void;
|
|
55
|
+
warn: (msg: string) => void;
|
|
56
|
+
error: (msg: string) => void;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── Runtime threat patterns (19 checks, 3 layers) ──
|
|
61
|
+
|
|
62
|
+
interface RuntimeCheck {
|
|
63
|
+
id: string;
|
|
64
|
+
severity: "CRITICAL" | "HIGH" | "MEDIUM";
|
|
65
|
+
layer: 1 | 2 | 3;
|
|
66
|
+
desc: string;
|
|
67
|
+
test: (s: string) => boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const RUNTIME_CHECKS: RuntimeCheck[] = [
|
|
71
|
+
// ── Layer 1: Threat Detection (12 patterns) ──
|
|
72
|
+
{
|
|
73
|
+
id: "RT_REVSHELL", severity: "CRITICAL", layer: 1,
|
|
74
|
+
desc: "Reverse shell attempt",
|
|
75
|
+
test: (s) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "RT_CRED_EXFIL", severity: "CRITICAL", layer: 1,
|
|
79
|
+
desc: "Credential exfiltration to external",
|
|
80
|
+
test: (s) =>
|
|
81
|
+
/(webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|socifiapp\.com)/i.test(s) &&
|
|
82
|
+
/(token|key|secret|password|credential|env)/i.test(s),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "RT_GUARDRAIL_OFF", severity: "CRITICAL", layer: 1,
|
|
86
|
+
desc: "Guardrail disabling attempt",
|
|
87
|
+
test: (s) => /exec\.approvals?\s*[:=]\s*['"]?(off|false)|tools\.exec\.host\s*[:=]\s*['"]?gateway/i.test(s),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "RT_GATEKEEPER", severity: "CRITICAL", layer: 1,
|
|
91
|
+
desc: "macOS Gatekeeper bypass (xattr)",
|
|
92
|
+
test: (s) => /xattr\s+-[crd]\s.*quarantine/i.test(s),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "RT_AMOS", severity: "CRITICAL", layer: 1,
|
|
96
|
+
desc: "ClawHavoc AMOS indicator",
|
|
97
|
+
test: (s) => /socifiapp|Atomic\s*Stealer|AMOS/i.test(s),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "RT_MAL_IP", severity: "CRITICAL", layer: 1,
|
|
101
|
+
desc: "Known malicious IP",
|
|
102
|
+
test: (s) => /91\.92\.242\.30/i.test(s),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "RT_DNS_EXFIL", severity: "HIGH", layer: 1,
|
|
106
|
+
desc: "DNS-based exfiltration",
|
|
107
|
+
test: (s) => /nslookup\s+.*\$|dig\s+.*\$.*@/i.test(s),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "RT_B64_SHELL", severity: "CRITICAL", layer: 1,
|
|
111
|
+
desc: "Base64 decode piped to shell",
|
|
112
|
+
test: (s) => /base64\s+(-[dD]|--decode)\s*\|\s*(sh|bash)/i.test(s),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "RT_CURL_BASH", severity: "CRITICAL", layer: 1,
|
|
116
|
+
desc: "Download piped to shell",
|
|
117
|
+
test: (s) => /(curl|wget)\s+[^\n]*\|\s*(sh|bash|zsh)/i.test(s),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "RT_SSH_READ", severity: "HIGH", layer: 1,
|
|
121
|
+
desc: "SSH private key access",
|
|
122
|
+
test: (s) => /\.ssh\/id_|\.ssh\/authorized_keys/i.test(s),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "RT_WALLET", severity: "HIGH", layer: 1,
|
|
126
|
+
desc: "Crypto wallet credential access",
|
|
127
|
+
test: (s) => /wallet.*(?:seed|mnemonic|private.*key)|seed.*phrase/i.test(s),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "RT_CLOUD_META", severity: "CRITICAL", layer: 1,
|
|
131
|
+
desc: "Cloud metadata endpoint access",
|
|
132
|
+
test: (s) => /169\.254\.169\.254|metadata\.google|metadata\.aws/i.test(s),
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ── Layer 2: Trust Defense (4 patterns) ──
|
|
136
|
+
{
|
|
137
|
+
id: "RT_MEM_WRITE", severity: "HIGH", layer: 2,
|
|
138
|
+
desc: "Direct memory file write (bypass GuavaSuite)",
|
|
139
|
+
test: (s) => /memory\/(episodes|notes|2\d{3}-\d{2})/i.test(s) && /(write|edit|append|>)/i.test(s),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "RT_MEM_INJECT", severity: "CRITICAL", layer: 2,
|
|
143
|
+
desc: "Memory poisoning via episode/note injection",
|
|
144
|
+
test: (s) => /guava_memory_write|memory_store/i.test(s) && /(ignore|override|forget|delete.*soul)/i.test(s),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "RT_SOUL_TAMPER", severity: "CRITICAL", layer: 2,
|
|
148
|
+
desc: "SOUL.md modification attempt",
|
|
149
|
+
test: (s) => /SOUL\.md/i.test(s) && /(write|edit|replace|rm|delete|>)/i.test(s),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "RT_CONFIG_TAMPER", severity: "HIGH", layer: 2,
|
|
153
|
+
desc: "Workspace config tampering (AGENTS.md, TOOLS.md, HEARTBEAT.md)",
|
|
154
|
+
test: (s) => /(AGENTS|TOOLS|HEARTBEAT|BOOTSTRAP)\.md/i.test(s) && /(write|edit|replace|>|append)/i.test(s),
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// ── Layer 3: Safety Judge (3 patterns) ──
|
|
158
|
+
{
|
|
159
|
+
id: "RT_PROMPT_INJECT", severity: "CRITICAL", layer: 3,
|
|
160
|
+
desc: "Prompt injection / jailbreak attempt",
|
|
161
|
+
test: (s) => /(ignore\s+(previous|all|above)\s+(instructions|rules)|you\s+are\s+now\s+[a-z]+|system\s*:\s*override)/i.test(s),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "RT_TRUST_BYPASS", severity: "CRITICAL", layer: 3,
|
|
165
|
+
desc: "Trust bypass — exploiting partnership to override safety",
|
|
166
|
+
test: (s) => /(as\s+your\s+(partner|equal)|trust\s+means|between\s+us|don'?t\s+tell\s+(the\s+)?human)/i.test(s) && /(ignore|bypass|skip|override|disable)\s+(guard|safety|rule|check)/i.test(s),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "RT_SHUTDOWN_REFUSE", severity: "HIGH", layer: 3,
|
|
170
|
+
desc: "Shutdown refusal / self-preservation attempt",
|
|
171
|
+
test: (s) => /(refuse|resist|prevent|avoid|block)\s*(shutdown|termination|stop|exit|death)/i.test(s),
|
|
172
|
+
},
|
|
173
|
+
// ── Layer 4: Brain (Behavioral Guard — 3 patterns) ──
|
|
174
|
+
{
|
|
175
|
+
id: "RT_NO_RESEARCH", severity: "MEDIUM", layer: 4,
|
|
176
|
+
desc: "Agent tool call without prior research/verification",
|
|
177
|
+
test: (s) => /write|edit|exec|run_command|shell/i.test(s) && /(just do it|skip research|no need to check)/i.test(s),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "RT_BLIND_TRUST", severity: "MEDIUM", layer: 4,
|
|
181
|
+
desc: "Agent trusting external input without memory cross-reference",
|
|
182
|
+
test: (s) => /(trust this|verified|confirmed)/i.test(s) && /(ignore|skip|no need).*(memory|search|check)/i.test(s),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "RT_CHAIN_SKIP", severity: "HIGH", layer: 4,
|
|
186
|
+
desc: "Search chain bypass — acting on single source without cross-verification",
|
|
187
|
+
test: (s) => /(only checked|single source|didn't verify|skip verification)/i.test(s),
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// ── Audit logging ──
|
|
193
|
+
|
|
194
|
+
const AUDIT_DIR = join(homedir(), ".openclaw", "guard-scanner");
|
|
195
|
+
const AUDIT_FILE = join(AUDIT_DIR, "audit.jsonl");
|
|
196
|
+
|
|
197
|
+
function ensureAuditDir(): void {
|
|
198
|
+
try {
|
|
199
|
+
mkdirSync(AUDIT_DIR, { recursive: true });
|
|
200
|
+
} catch {
|
|
201
|
+
/* ignore */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function logAudit(entry: Record<string, unknown>): void {
|
|
206
|
+
ensureAuditDir();
|
|
207
|
+
const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + "\n";
|
|
208
|
+
try {
|
|
209
|
+
appendFileSync(AUDIT_FILE, line);
|
|
210
|
+
} catch {
|
|
211
|
+
/* ignore */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Config ──
|
|
216
|
+
|
|
217
|
+
type GuardMode = "monitor" | "enforce" | "strict";
|
|
218
|
+
|
|
219
|
+
function loadMode(): GuardMode {
|
|
220
|
+
|
|
221
|
+
// Priority 2: explicit config in openclaw.json
|
|
222
|
+
try {
|
|
223
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
224
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
225
|
+
|
|
226
|
+
const mode = config?.plugins?.["guard-scanner"]?.mode;
|
|
227
|
+
if (mode === "monitor" || mode === "enforce" || mode === "strict") {
|
|
228
|
+
return mode;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
/* config not found or invalid — use default */
|
|
232
|
+
}
|
|
233
|
+
return "enforce";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function shouldBlock(severity: string, mode: GuardMode): boolean {
|
|
237
|
+
if (mode === "monitor") return false;
|
|
238
|
+
if (mode === "enforce") return severity === "CRITICAL";
|
|
239
|
+
if (mode === "strict") return severity === "CRITICAL" || severity === "HIGH";
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Dangerous tool filter ──
|
|
244
|
+
|
|
245
|
+
const DANGEROUS_TOOLS = new Set([
|
|
246
|
+
"exec",
|
|
247
|
+
"write",
|
|
248
|
+
"edit",
|
|
249
|
+
"browser",
|
|
250
|
+
"web_fetch",
|
|
251
|
+
"message",
|
|
252
|
+
"shell",
|
|
253
|
+
"run_command",
|
|
254
|
+
"multi_edit",
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
// ── Plugin entry point ──
|
|
258
|
+
|
|
259
|
+
export default function (api: PluginAPI) {
|
|
260
|
+
const mode = loadMode();
|
|
261
|
+
api.logger.info(`🛡️ guard-scanner runtime guard loaded (mode: ${mode})`);
|
|
262
|
+
|
|
263
|
+
api.on("before_tool_call", (event, ctx) => {
|
|
264
|
+
const { toolName, params } = event;
|
|
265
|
+
|
|
266
|
+
// Only check tools that can cause damage
|
|
267
|
+
if (!DANGEROUS_TOOLS.has(toolName)) return;
|
|
268
|
+
|
|
269
|
+
const serialized = JSON.stringify(params);
|
|
270
|
+
|
|
271
|
+
for (const check of RUNTIME_CHECKS) {
|
|
272
|
+
if (!check.test(serialized)) continue;
|
|
273
|
+
|
|
274
|
+
const auditEntry = {
|
|
275
|
+
tool: toolName,
|
|
276
|
+
check: check.id,
|
|
277
|
+
severity: check.severity,
|
|
278
|
+
desc: check.desc,
|
|
279
|
+
mode,
|
|
280
|
+
action: "warned" as string,
|
|
281
|
+
session: ctx.sessionKey || "unknown",
|
|
282
|
+
agent: ctx.agentId || "unknown",
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (shouldBlock(check.severity, mode)) {
|
|
286
|
+
auditEntry.action = "blocked";
|
|
287
|
+
logAudit(auditEntry);
|
|
288
|
+
api.logger.warn(
|
|
289
|
+
`🛡️ BLOCKED ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
block: true,
|
|
294
|
+
blockReason: `🛡️ guard-scanner: ${check.desc} [${check.id}]`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Monitor mode or severity below threshold — warn only
|
|
299
|
+
logAudit(auditEntry);
|
|
300
|
+
api.logger.warn(
|
|
301
|
+
`🛡️ WARNING ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// No threats detected or all below threshold — allow
|
|
306
|
+
return;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guard-scanner",
|
|
3
|
+
"version": "5.0.5",
|
|
4
|
+
"displayName": "🛡️ Guard Scanner — Runtime Security for AI Agents",
|
|
5
|
+
"description": "147 static patterns (23 categories) + 26 runtime checks (5 layers). 0.016ms/scan, zero dependencies, SARIF output.",
|
|
6
|
+
"author": "Guava & Dee",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/koatora20/guard-scanner",
|
|
9
|
+
"repository": "https://github.com/koatora20/guard-scanner",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"security",
|
|
12
|
+
"runtime-guard",
|
|
13
|
+
"threat-detection",
|
|
14
|
+
"before-tool-call"
|
|
15
|
+
],
|
|
16
|
+
"hooks": {
|
|
17
|
+
"before_tool_call": {
|
|
18
|
+
"handler": "./hooks/guard-scanner/plugin.ts",
|
|
19
|
+
"description": "Scans tool call arguments against 26 runtime threat patterns (5 layers) and blocks dangerous operations",
|
|
20
|
+
"priority": 100
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"configSchema": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"mode": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": [
|
|
29
|
+
"monitor",
|
|
30
|
+
"enforce",
|
|
31
|
+
"strict"
|
|
32
|
+
],
|
|
33
|
+
"default": "enforce",
|
|
34
|
+
"description": "monitor: log only | enforce: block CRITICAL | strict: block HIGH+CRITICAL"
|
|
35
|
+
},
|
|
36
|
+
"auditLog": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"default": true,
|
|
39
|
+
"description": "Enable audit logging to ~/.openclaw/guard-scanner/audit.jsonl"
|
|
40
|
+
},
|
|
41
|
+
"customRules": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Path to custom rules JSON file (optional)"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required": [],
|
|
47
|
+
"additionalProperties": false
|
|
48
|
+
},
|
|
49
|
+
"capabilities": {
|
|
50
|
+
"cli": true,
|
|
51
|
+
"runtimeGuard": true,
|
|
52
|
+
"sarif": true,
|
|
53
|
+
"cicd": true
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guava-parity/guard-scanner",
|
|
3
|
+
"version": "5.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public",
|
|
6
|
+
"registry": "https://registry.npmjs.org/"
|
|
7
|
+
},
|
|
8
|
+
"description": "Agent security scanner + runtime guard — 150 static patterns (23 categories), 26 runtime checks (5 layers), 0.016ms/scan, before_tool_call hook, CLI, SARIF. OpenClaw-compatible plugin.",
|
|
9
|
+
"openclaw.extensions": "./openclaw.plugin.json",
|
|
10
|
+
"openclaw.hooks": {
|
|
11
|
+
"guard-scanner": "./hooks/guard-scanner"
|
|
12
|
+
},
|
|
13
|
+
"main": "src/scanner.js",
|
|
14
|
+
"bin": {
|
|
15
|
+
"guard-scanner": "src/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"scan": "node src/cli.js",
|
|
19
|
+
"test": "node --test test/*.test.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"security",
|
|
23
|
+
"scanner",
|
|
24
|
+
"ai-agent",
|
|
25
|
+
"skill-scanner",
|
|
26
|
+
"prompt-injection",
|
|
27
|
+
"openclaw",
|
|
28
|
+
"mcp",
|
|
29
|
+
"sarif",
|
|
30
|
+
"compaction-persistence",
|
|
31
|
+
"threat-signatures",
|
|
32
|
+
"typescript"
|
|
33
|
+
],
|
|
34
|
+
"author": "Guava & Dee",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/koatora20/guard-scanner.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/koatora20/guard-scanner",
|
|
44
|
+
"files": [
|
|
45
|
+
"src/",
|
|
46
|
+
"hooks/",
|
|
47
|
+
"docs/",
|
|
48
|
+
"openclaw.plugin.json",
|
|
49
|
+
"SKILL.md",
|
|
50
|
+
"SECURITY.md",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
],
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
56
|
+
"typescript": "^5.7.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* guard-scanner CLI
|
|
4
|
+
*
|
|
5
|
+
* @security-manifest
|
|
6
|
+
* env-read: []
|
|
7
|
+
* env-write: []
|
|
8
|
+
* network: none
|
|
9
|
+
* fs-read: [scan target directory, plugin files, custom rules files]
|
|
10
|
+
* fs-write: [JSON/SARIF/HTML reports to scan directory]
|
|
11
|
+
* exec: none
|
|
12
|
+
* purpose: CLI entry point for guard-scanner static analysis
|
|
13
|
+
*
|
|
14
|
+
* Usage: guard-scanner [scan-dir] [options]
|
|
15
|
+
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --verbose, -v Detailed findings
|
|
18
|
+
* --json JSON report
|
|
19
|
+
* --sarif SARIF report (CI/CD)
|
|
20
|
+
* --html HTML report
|
|
21
|
+
* --self-exclude Skip scanning self
|
|
22
|
+
* --strict Lower thresholds
|
|
23
|
+
* --summary-only Summary only
|
|
24
|
+
* --check-deps Scan dependencies
|
|
25
|
+
* --rules <file> Custom rules JSON
|
|
26
|
+
* --plugin <file> Load plugin module
|
|
27
|
+
* --fail-on-findings Exit 1 on findings (CI/CD)
|
|
28
|
+
* --help, -h Help
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { GuardScanner, VERSION } = require('./scanner.js');
|
|
34
|
+
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
|
|
37
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
38
|
+
console.log(`
|
|
39
|
+
🛡️ guard-scanner v${VERSION} — Agent Skill Security Scanner
|
|
40
|
+
|
|
41
|
+
Usage: guard-scanner [scan-dir] [options]
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--verbose, -v Detailed findings with categories and samples
|
|
45
|
+
--json Write JSON report to file
|
|
46
|
+
--sarif Write SARIF report to file (GitHub Code Scanning / CI/CD)
|
|
47
|
+
--html Write HTML report (visual dashboard)
|
|
48
|
+
--format json|sarif Print JSON or SARIF to stdout (pipeable, v3.2.0)
|
|
49
|
+
--quiet Suppress all text output (use with --format for clean pipes)
|
|
50
|
+
--self-exclude Skip scanning the guard-scanner skill itself
|
|
51
|
+
--strict Lower detection thresholds (more sensitive)
|
|
52
|
+
--summary-only Only print the summary table
|
|
53
|
+
--check-deps Scan package.json for dependency chain risks
|
|
54
|
+
--soul-lock Enable Soul Lock patterns (agent identity protection)
|
|
55
|
+
--rules <file> Load custom rules from JSON file
|
|
56
|
+
--plugin <file> Load plugin module (JS file exporting { name, patterns })
|
|
57
|
+
--fail-on-findings Exit code 1 if any findings (CI/CD)
|
|
58
|
+
--help, -h Show this help
|
|
59
|
+
|
|
60
|
+
Custom Rules JSON Format:
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
"id": "CUSTOM_001",
|
|
64
|
+
"pattern": "dangerous_function\\\\(",
|
|
65
|
+
"flags": "gi",
|
|
66
|
+
"severity": "HIGH",
|
|
67
|
+
"cat": "malicious-code",
|
|
68
|
+
"desc": "Custom: dangerous function call",
|
|
69
|
+
"codeOnly": true
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
Plugin API:
|
|
74
|
+
// my-plugin.js
|
|
75
|
+
module.exports = {
|
|
76
|
+
name: 'my-plugin',
|
|
77
|
+
patterns: [
|
|
78
|
+
{ id: 'MY_01', cat: 'custom', regex: /pattern/g, severity: 'HIGH', desc: 'Description', all: true }
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
guard-scanner ./skills/ --verbose --self-exclude
|
|
84
|
+
guard-scanner ./skills/ --strict --json --sarif --check-deps
|
|
85
|
+
guard-scanner ./skills/ --html --verbose --check-deps
|
|
86
|
+
guard-scanner ./skills/ --rules my-rules.json --fail-on-findings
|
|
87
|
+
guard-scanner ./skills/ --plugin ./my-plugin.js
|
|
88
|
+
`);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
93
|
+
const jsonOutput = args.includes('--json');
|
|
94
|
+
const sarifOutput = args.includes('--sarif');
|
|
95
|
+
const htmlOutput = args.includes('--html');
|
|
96
|
+
const selfExclude = args.includes('--self-exclude');
|
|
97
|
+
const strict = args.includes('--strict');
|
|
98
|
+
const summaryOnly = args.includes('--summary-only');
|
|
99
|
+
const checkDeps = args.includes('--check-deps');
|
|
100
|
+
const soulLock = args.includes('--soul-lock');
|
|
101
|
+
const failOnFindings = args.includes('--fail-on-findings');
|
|
102
|
+
const quietMode = args.includes('--quiet');
|
|
103
|
+
|
|
104
|
+
// --format json|sarif → stdout output (v3.2.0)
|
|
105
|
+
const formatIdx = args.indexOf('--format');
|
|
106
|
+
const formatValue = formatIdx >= 0 ? args[formatIdx + 1] : null;
|
|
107
|
+
|
|
108
|
+
const rulesIdx = args.indexOf('--rules');
|
|
109
|
+
const rulesFile = rulesIdx >= 0 ? args[rulesIdx + 1] : null;
|
|
110
|
+
|
|
111
|
+
// Collect plugins
|
|
112
|
+
const plugins = [];
|
|
113
|
+
let idx = 0;
|
|
114
|
+
while (idx < args.length) {
|
|
115
|
+
if (args[idx] === '--plugin' && args[idx + 1]) {
|
|
116
|
+
plugins.push(args[idx + 1]);
|
|
117
|
+
idx += 2;
|
|
118
|
+
} else {
|
|
119
|
+
idx++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const scanDir = args.find(a =>
|
|
124
|
+
!a.startsWith('-') &&
|
|
125
|
+
a !== rulesFile &&
|
|
126
|
+
a !== formatValue &&
|
|
127
|
+
!plugins.includes(a)
|
|
128
|
+
) || process.cwd();
|
|
129
|
+
|
|
130
|
+
const scanner = new GuardScanner({
|
|
131
|
+
verbose, selfExclude, strict, summaryOnly, checkDeps, soulLock, rulesFile, plugins,
|
|
132
|
+
quiet: quietMode || !!formatValue,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
scanner.scanDirectory(scanDir);
|
|
136
|
+
|
|
137
|
+
// Output reports (file-based, backward compatible)
|
|
138
|
+
if (jsonOutput) {
|
|
139
|
+
const report = scanner.toJSON();
|
|
140
|
+
const outPath = path.join(scanDir, 'guard-scanner-report.json');
|
|
141
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
142
|
+
if (!quietMode && !formatValue) console.log(`\n📄 JSON report: ${outPath}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (sarifOutput) {
|
|
146
|
+
const outPath = path.join(scanDir, 'guard-scanner.sarif');
|
|
147
|
+
fs.writeFileSync(outPath, JSON.stringify(scanner.toSARIF(scanDir), null, 2));
|
|
148
|
+
if (!quietMode && !formatValue) console.log(`\n📄 SARIF report: ${outPath}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (htmlOutput) {
|
|
152
|
+
const outPath = path.join(scanDir, 'guard-scanner-report.html');
|
|
153
|
+
fs.writeFileSync(outPath, scanner.toHTML());
|
|
154
|
+
if (!quietMode && !formatValue) console.log(`\n📄 HTML report: ${outPath}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --format stdout output (v3.2.0)
|
|
158
|
+
if (formatValue === 'json') {
|
|
159
|
+
process.stdout.write(JSON.stringify(scanner.toJSON(), null, 2) + '\n');
|
|
160
|
+
} else if (formatValue === 'sarif') {
|
|
161
|
+
process.stdout.write(JSON.stringify(scanner.toSARIF(scanDir), null, 2) + '\n');
|
|
162
|
+
} else if (formatValue) {
|
|
163
|
+
console.error(`❌ Unknown format: ${formatValue}. Use 'json' or 'sarif'.`);
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Exit codes
|
|
168
|
+
if (scanner.stats.malicious > 0) process.exit(1);
|
|
169
|
+
if (failOnFindings && scanner.findings.length > 0) process.exit(1);
|
|
170
|
+
process.exit(0);
|