guard-scanner 1.0.0 → 1.1.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 CHANGED
@@ -2,17 +2,25 @@
2
2
  <h1 align="center">🛡️ guard-scanner</h1>
3
3
  <p align="center">
4
4
  <strong>Static security scanner for AI agent skills</strong><br>
5
- Detect prompt injection, credential theft, exfiltration, identity hijacking, and 14 more threat categories.
5
+ Detect prompt injection, credential theft, exfiltration, identity hijacking, and 17 more threat categories.<br>
6
+ <sub>Runtime Guard hook included — pending <a href="https://github.com/openclaw/openclaw/issues/18677">OpenClaw hook API adoption</a></sub>
6
7
  </p>
7
8
  <p align="center">
8
9
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
9
10
  <img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen" alt="Node.js 18+">
10
11
  <img src="https://img.shields.io/badge/dependencies-0-success" alt="Zero Dependencies">
11
- <img src="https://img.shields.io/badge/tests-45%2F45-brightgreen" alt="Tests Passing">
12
- <img src="https://img.shields.io/badge/patterns-170%2B-orange" alt="170+ Patterns">
12
+ <img src="https://img.shields.io/badge/tests-55%2F55-brightgreen" alt="Tests Passing">
13
+ <img src="https://img.shields.io/badge/patterns-186-orange" alt="186 Patterns">
14
+ <img src="https://img.shields.io/badge/categories-20-blueviolet" alt="20 Categories">
13
15
  </p>
14
16
  </p>
15
17
 
18
+ <p align="center">
19
+ <img src="docs/html-report-preview.png" alt="guard-scanner HTML Report Preview" width="800">
20
+ <br>
21
+ <em>Dark Glassmorphism Dashboard — Risk gauges, severity distribution, interactive skill cards</em>
22
+ </p>
23
+
16
24
  ---
17
25
 
18
26
  ## Why This Exists
@@ -32,11 +40,14 @@ The AI agent skill ecosystem has the same supply-chain security problem that npm
32
40
 
33
41
  | Feature | Description |
34
42
  |---|---|
35
- | **17 Threat Categories** | Snyk ToxicSkills taxonomy + OWASP MCP Top 10 + Identity Hijacking |
36
- | **170+ Detection Patterns** | Regex-based static analysis covering code, docs, and data files |
43
+ | **20 Threat Categories** | Snyk ToxicSkills + OWASP MCP Top 10 + Identity Hijacking + Sandbox/Complexity/Config |
44
+ | **186 Detection Patterns** | Regex-based static analysis covering code, docs, and data files |
37
45
  | **IoC Database** | Known malicious IPs, domains, URLs, usernames, and typosquat names |
38
46
  | **Data Flow Analysis** | Lightweight JS analysis: secret reads → network calls → exec chains |
39
47
  | **Cross-File Analysis** | Phantom references, base64 fragment assembly, multi-file exfil detection |
48
+ | **Manifest Validation** | SKILL.md frontmatter analysis for dangerous capabilities |
49
+ | **Code Complexity** | File length, nesting depth, eval/exec density analysis |
50
+ | **Config Impact** | Detects modifications to OpenClaw configuration files |
40
51
  | **Shannon Entropy** | High-entropy string detection for leaked secrets and API keys |
41
52
  | **Dependency Chain Scan** | Risky packages, lifecycle scripts, wildcard versions, git dependencies |
42
53
  | **4 Output Formats** | Terminal (with colors), JSON, [SARIF 2.1.0](https://sarifweb.azurewebsites.net), HTML dashboard |
@@ -80,11 +91,13 @@ openclaw skill install guard-scanner
80
91
  guard-scanner ~/.openclaw/workspace/skills/ --self-exclude --verbose
81
92
  ```
82
93
 
94
+ > **⚠️ Runtime Guard (handler.ts)** — The real-time `before_tool_call` hook requires OpenClaw's Hook API ([Issue #18677](https://github.com/openclaw/openclaw/issues/18677)). The hook is registered and runs on `agent:before_tool_call` events, but OpenClaw's `InternalHookEvent` does not yet expose a cancel/veto mechanism — so **detections are warned but not blocked**. The static scanner (`npx guard-scanner`) works fully and independently.
95
+
83
96
  ---
84
97
 
85
98
  ## Threat Categories
86
99
 
87
- guard-scanner covers **17 threat categories** derived from three taxonomies:
100
+ guard-scanner covers **20 threat categories** derived from three taxonomies:
88
101
 
89
102
  | # | Category | Based On | Severity | What It Detects |
90
103
  |---|----------|----------|----------|----------------|
@@ -105,8 +118,11 @@ guard-scanner covers **17 threat categories** derived from three taxonomies:
105
118
  | 15 | **CVE Patterns** | CVE Database | CRITICAL | CVE-2026-25253 `gatewayUrl` injection, sandbox disabling, xattr Gatekeeper bypass, WebSocket origin bypass |
106
119
  | 16 | **MCP Security** | OWASP MCP Top 10 | CRITICAL | Tool poisoning (`<IMPORTANT>`), schema poisoning (malicious defaults), token leaks, shadow server registration, SSRF metadata endpoints |
107
120
  | 17 | **Identity Hijacking** | Original Research | CRITICAL | SOUL.md/IDENTITY.md overwrite/redirect/sed/echo/Python/Node.js writes, persona swap instructions, memory wipe, name override |
121
+ | 18 | **Sandbox Validation** | v1.1 | HIGH | Dangerous binary requirements in SKILL.md, overly broad file scope, sensitive env vars, exec/network declarations |
122
+ | 19 | **Code Complexity** | v1.1 | MEDIUM | Excessive file length (>1000 lines), deep nesting (>5 levels), high eval/exec density |
123
+ | 20 | **Config Impact** | v1.1 | CRITICAL | `openclaw.json` writes, exec approval bypass, exec host gateway, internal hooks modification, network wildcard |
108
124
 
109
- > **Category 17 (Identity Hijacking)** is unique to guard-scanner. It was developed from direct experience with a real attack where an agent's identity files were silently replaced. Detection patterns are open-source; verification logic remains private.
125
+ > **Categories 17–20** are unique to guard-scanner. Category 17 (Identity Hijacking) was developed from a real attack. Categories 18–20 were added in v1.1.0 based on community feedback.
110
126
 
111
127
  ---
112
128
 
@@ -115,7 +131,7 @@ guard-scanner covers **17 threat categories** derived from three taxonomies:
115
131
  ### Terminal (Default)
116
132
 
117
133
  ```
118
- 🛡️ guard-scanner v1.0.0
134
+ 🛡️ guard-scanner v1.1.0
119
135
  ══════════════════════════════════════════════════════
120
136
  📂 Scanning: ./skills/
121
137
  📦 Skills found: 22
@@ -197,6 +213,9 @@ Certain combinations multiply the base score:
197
213
  | Persistence + (malicious\|credential\|memory) | **×1.5** | Survives session restart |
198
214
  | Identity hijacking | **×2** | Core identity compromise |
199
215
  | Identity hijacking + Persistence | **min 90** | Full agent takeover |
216
+ | Config impact | **×2** | OpenClaw configuration tampering |
217
+ | Config impact + Sandbox violation | **min 70** | Combined config + capability abuse |
218
+ | Complexity + Malicious code/Obfuscation | **×1.5** | Complex code hiding threats |
200
219
  | Known IoC (IP/URL/typosquat) | **= 100** | Confirmed malicious |
201
220
 
202
221
  ### Verdict Thresholds
@@ -369,13 +388,16 @@ Options:
369
388
  ```
370
389
  guard-scanner/
371
390
  ├── src/
372
- │ ├── scanner.js # GuardScanner class — core scan engine
373
- │ ├── patterns.js # 170+ threat detection patterns (Cat 1–17)
391
+ │ ├── scanner.js # GuardScanner class — core scan engine (20 checks)
392
+ │ ├── patterns.js # 186 threat detection patterns (Cat 1–20)
374
393
  │ ├── ioc-db.js # Indicators of Compromise database
375
394
  │ └── cli.js # CLI entry point and argument parser
395
+ ├── hooks/
396
+ │ └── guard-scanner/
397
+ │ └── handler.ts # Runtime Guard — before_tool_call hook (experimental, pending OpenClaw API)
376
398
  ├── test/
377
- │ ├── scanner.test.js # 45 tests across 10 sections
378
- │ └── fixtures/ # Malicious + clean skill samples
399
+ │ ├── scanner.test.js # 55 tests across 13 sections
400
+ │ └── fixtures/ # Malicious, clean, complex, config-changer samples
379
401
  ├── package.json # Zero dependencies, node --test
380
402
  ├── CHANGELOG.md
381
403
  ├── LICENSE # MIT
@@ -499,11 +521,11 @@ console.log(scanner.toHTML()); // HTML string
499
521
  ## Test Results
500
522
 
501
523
  ```
502
- ℹ tests 45
503
- ℹ suites 10
504
- ℹ pass 45
524
+ ℹ tests 55
525
+ ℹ suites 13
526
+ ℹ pass 55
505
527
  ℹ fail 0
506
- ℹ duration_ms 83ms
528
+ ℹ duration_ms 115ms
507
529
  ```
508
530
 
509
531
  | Suite | Tests | Coverage |
@@ -518,6 +540,9 @@ console.log(scanner.toHTML()); // HTML string
518
540
  | Shannon Entropy | 2 | Low entropy, high entropy |
519
541
  | Ignore Functionality | 1 | Pattern exclusion |
520
542
  | Plugin API | 1 | Plugin loading + custom rule injection |
543
+ | **Manifest Validation (v1.1)** | 4 | Dangerous bins, broad files, sensitive env, clean negatives |
544
+ | **Complexity Metrics (v1.1)** | 2 | Deep nesting, clean negatives |
545
+ | **Config Impact (v1.1)** | 4 | openclaw.json write, exec approval, gateway host, clean negatives |
521
546
 
522
547
  ---
523
548
 
@@ -537,7 +562,7 @@ console.log(scanner.toHTML()); // HTML string
537
562
  2. Create a feature branch (`git checkout -b feature/new-pattern`)
538
563
  3. Add your pattern to `src/patterns.js` with the required fields
539
564
  4. Add a test case in `test/fixtures/` and `test/scanner.test.js`
540
- 5. Run `npm test` — all 45+ tests must pass
565
+ 5. Run `npm test` — all 55+ tests must pass
541
566
  6. Submit a Pull Request
542
567
 
543
568
  ### Adding a New Detection Pattern
@@ -574,6 +599,39 @@ We built one.
574
599
 
575
600
  ---
576
601
 
602
+ ## 🔒 Need More? — GuavaSuite
603
+
604
+ guard-scanner catches threats **before** installation. But what happens **after** a skill is running?
605
+
606
+ [**GuavaSuite**](https://github.com/koatora20) extends guard-scanner with real-time protection for production agent deployments:
607
+
608
+ | | guard-scanner (OSS) | GuavaSuite (Private) |
609
+ |---|---|---|
610
+ | Static scan | ✅ 20 categories | ✅ 20 categories |
611
+ | Runtime blocking | ⚠️ Warn only (cancel API pending) | ✅ Real-time `before_tool_call` guard |
612
+ | SOUL.md integrity | Pattern detection only | ✅ SHA-256 hash watchdog |
613
+ | On-chain verification | — | ✅ SoulChain (Polygon) |
614
+ | Identity recovery | — | ✅ Automatic rollback |
615
+
616
+ guard-scanner is and always will be **free, open-source, and zero-dependency**. If your agent handles production workloads and you want defense-in-depth, [reach out](https://github.com/koatora20).
617
+
618
+ ---
619
+
620
+ ## 💜 Sponsor This Project
621
+
622
+ If guard-scanner helps protect your agents, consider sponsoring continued development:
623
+
624
+ <p align="center">
625
+ <a href="https://github.com/sponsors/koatora20">💜 Sponsor on GitHub</a>
626
+ </p>
627
+
628
+ Sponsors help fund:
629
+ - 🔬 New threat research and pattern updates
630
+ - 📝 Academic paper on ASI-human coexistence security
631
+ - 🌍 Community-driven security for the agent ecosystem
632
+
633
+ ---
634
+
577
635
  ## License
578
636
 
579
637
  MIT — see [LICENSE](LICENSE)
package/SKILL.md CHANGED
@@ -3,7 +3,8 @@ name: guard-scanner
3
3
  description: >
4
4
  Security scanner for AI agent skills. Use BEFORE installing or running any new skill
5
5
  from ClawHub or external sources. Detects prompt injection, credential theft,
6
- exfiltration, identity hijacking, and 14 more threat categories.
6
+ exfiltration, identity hijacking, sandbox violations, code complexity, config impact,
7
+ and 17 more threat categories.
7
8
  Includes a Runtime Guard hook that blocks dangerous tool calls in real-time.
8
9
  homepage: https://github.com/koatora20/guard-scanner
9
10
  metadata:
@@ -28,7 +29,7 @@ metadata:
28
29
  # guard-scanner 🛡️
29
30
 
30
31
  Static + runtime security scanner for AI agent skills.
31
- **170+ threat patterns** across **17 categories** — zero dependencies.
32
+ **186+ threat patterns** across **20 categories** — zero dependencies.
32
33
 
33
34
  ## When To Use This Skill
34
35
 
@@ -53,7 +54,9 @@ Scan a specific skill:
53
54
  node skills/guard-scanner/src/cli.js /path/to/new-skill/ --strict --verbose
54
55
  ```
55
56
 
56
- ### 2. Runtime Guard (Real-time Protection)
57
+ ### 2. Runtime Guard (Real-time Protection) — ⚠️ Experimental
58
+
59
+ > **Note:** Requires the OpenClaw Hook API ([Issue #18677](https://github.com/openclaw/openclaw/issues/18677)), which has not been officially adopted yet. The handler is included for early testing and will be updated once the API is finalized.
57
60
 
58
61
  Install the hook to block dangerous tool calls before execution:
59
62
 
@@ -82,11 +85,13 @@ openclaw hooks enable guard-scanner
82
85
 
83
86
  Set in `openclaw.json` → `hooks.internal.entries.guard-scanner.mode`:
84
87
 
85
- | Mode | Behavior |
86
- |------|----------|
87
- | `monitor` | Log all, never block |
88
- | `enforce` (default) | Block CRITICAL threats |
89
- | `strict` | Block HIGH + CRITICAL |
88
+ | Mode | Intended Behavior | Current Status |
89
+ |------|-------------------|----------------|
90
+ | `monitor` | Log all, never block | ✅ Fully working |
91
+ | `enforce` (default) | Block CRITICAL threats | ⚠️ Warn only (cancel API pending) |
92
+ | `strict` | Block HIGH + CRITICAL | ⚠️ Warn only (cancel API pending) |
93
+
94
+ > **Note:** OpenClaw's `InternalHookEvent` does not yet expose a `cancel`/`veto` mechanism. All detections are currently logged and warned via `event.messages`, but tool execution cannot be blocked. Blocking will be enabled when the cancel API is added.
90
95
 
91
96
  ## Threat Categories
92
97
 
@@ -109,6 +114,9 @@ Set in `openclaw.json` → `hooks.internal.entries.guard-scanner.mode`:
109
114
  | 15 | CVE Patterns | Known agent vulnerabilities |
110
115
  | 16 | MCP Security | Tool/schema poisoning, SSRF |
111
116
  | 17 | Identity Hijacking | SOUL.md/IDENTITY.md tampering |
117
+ | 18 | Sandbox Validation | Dangerous binaries, broad file scope, sensitive env |
118
+ | 19 | Code Complexity | Excessive file length, deep nesting, eval density |
119
+ | 20 | Config Impact | openclaw.json writes, exec approval bypass |
112
120
 
113
121
  ## External Endpoints
114
122
 
@@ -139,7 +147,7 @@ an AI agent's SOUL.md personality file, and no existing tool could detect it.
139
147
 
140
148
  - **Open source**: Full source code available at https://github.com/koatora20/guard-scanner
141
149
  - **Zero dependencies**: Nothing to audit, no transitive risks
142
- - **Test suite**: 45 tests across 10 sections, 100% pass rate
150
+ - **Test suite**: 55 tests across 13 sections, 100% pass rate
143
151
  - **Taxonomy**: Based on Snyk ToxicSkills (Feb 2026), OWASP MCP Top 10, and original research
144
152
  - **Complementary to VirusTotal**: Detects prompt injection and LLM-specific attacks
145
153
  that VirusTotal's signature-based scanning cannot catch
Binary file
@@ -1,26 +1,69 @@
1
- import type { HookHandler } from "../../src/hooks/hooks.js";
2
- import { appendFileSync, mkdirSync } from "fs";
3
- import { join } from "path";
4
- import { homedir } from "os";
5
-
6
1
  /**
7
- * guard-scanner Runtime Guard — before_tool_call Hook Handler
8
- *
9
- * Intercepts tool executions in real-time and checks against
10
- * threat intelligence patterns. Zero dependencies.
11
- *
12
- * Modes:
13
- * monitor — log only
14
- * enforce — block CRITICAL (default)
15
- * strict — block HIGH+CRITICAL, log MEDIUM+
16
- *
2
+ * guard-scanner Runtime Guard — Hook Handler
3
+ *
4
+ * Intercepts agent tool calls and checks arguments against
5
+ * runtime threat intelligence patterns. Zero dependencies.
6
+ *
7
+ * Registered for event: agent:before_tool_call
8
+ *
9
+ * Current limitation:
10
+ * The OpenClaw InternalHookEvent interface does not yet expose a
11
+ * `cancel` / `veto` mechanism. This handler can WARN via
12
+ * event.messages but cannot block tool execution.
13
+ * When a cancel API is introduced, this handler will be updated
14
+ * to actually block CRITICAL/HIGH threats.
15
+ *
16
+ * Modes (for future blocking behaviour):
17
+ * monitor — log only (current effective behaviour for all modes)
18
+ * enforce — will block CRITICAL when cancel API is available
19
+ * strict — will block HIGH+CRITICAL when cancel API is available
20
+ *
17
21
  * @author Guava 🍈 & Dee
18
- * @version 1.0.0
22
+ * @version 1.1.0
19
23
  * @license MIT
20
24
  */
21
25
 
26
+ import { appendFileSync, mkdirSync } from "fs";
27
+ import { join } from "path";
28
+ import { homedir } from "os";
29
+
30
+ // ── OpenClaw Hook Types (from openclaw/src/hooks/internal-hooks.ts) ──
31
+ // Inline types to avoid broken relative-path imports.
32
+ // These match the official InternalHookEvent / InternalHookHandler
33
+ // from OpenClaw v2026.2.15.
34
+
35
+ type InternalHookEventType = "command" | "session" | "agent" | "gateway";
36
+
37
+ interface InternalHookEvent {
38
+ /** The type of event */
39
+ type: InternalHookEventType;
40
+ /** The specific action within the type (e.g., "before_tool_call") */
41
+ action: string;
42
+ /** The session key this event relates to */
43
+ sessionKey: string;
44
+ /** Additional context specific to the event */
45
+ context: Record<string, unknown>;
46
+ /** Timestamp when the event occurred */
47
+ timestamp: Date;
48
+ /** Messages to send back to the user (hooks can push to this array) */
49
+ messages: string[];
50
+ }
51
+
52
+ type InternalHookHandler = (event: InternalHookEvent) => Promise<void> | void;
53
+
54
+ // Re-export as the public types for compatibility
55
+ type HookHandler = InternalHookHandler;
56
+ type HookEvent = InternalHookEvent;
57
+
22
58
  // ── Runtime threat patterns (12 checks) ──
23
- const RUNTIME_CHECKS = [
59
+ interface RuntimeCheck {
60
+ id: string;
61
+ severity: "CRITICAL" | "HIGH" | "MEDIUM";
62
+ desc: string;
63
+ test: (s: string) => boolean;
64
+ }
65
+
66
+ const RUNTIME_CHECKS: RuntimeCheck[] = [
24
67
  {
25
68
  id: 'RT_REVSHELL', severity: 'CRITICAL', desc: 'Reverse shell attempt',
26
69
  test: (s: string) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s)
@@ -78,11 +121,11 @@ const RUNTIME_CHECKS = [
78
121
  const AUDIT_DIR = join(homedir(), ".openclaw", "guard-scanner");
79
122
  const AUDIT_FILE = join(AUDIT_DIR, "audit.jsonl");
80
123
 
81
- function ensureAuditDir() {
124
+ function ensureAuditDir(): void {
82
125
  try { mkdirSync(AUDIT_DIR, { recursive: true }); } catch { }
83
126
  }
84
127
 
85
- function logAudit(entry: Record<string, unknown>) {
128
+ function logAudit(entry: Record<string, unknown>): void {
86
129
  ensureAuditDir();
87
130
  const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + '\n';
88
131
  try { appendFileSync(AUDIT_FILE, line); } catch { }
@@ -90,16 +133,21 @@ function logAudit(entry: Record<string, unknown>) {
90
133
 
91
134
  // ── Main Handler ──
92
135
  const handler: HookHandler = async (event) => {
93
- // Only handle before_tool_call
136
+ // Only handle agent:before_tool_call events
94
137
  if (event.type !== "agent" || event.action !== "before_tool_call") return;
95
138
 
96
- const { toolName, toolArgs } = (event as any).context || {};
139
+ const { toolName, toolArgs } = event.context as {
140
+ toolName?: string;
141
+ toolArgs?: Record<string, unknown>;
142
+ };
97
143
  if (!toolName || !toolArgs) return;
98
144
 
99
- // Get mode from config
100
- const mode = (event as any).context?.cfg?.hooks?.internal?.entries?.['guard-scanner']?.mode || 'enforce';
145
+ // Get mode from context config (if available)
146
+ const cfg = event.context.cfg as Record<string, unknown> | undefined;
147
+ const hookEntries = (cfg as any)?.hooks?.internal?.entries?.['guard-scanner'] as Record<string, unknown> | undefined;
148
+ const mode = (hookEntries?.mode as string) || 'enforce';
101
149
 
102
- // Only check dangerous tools
150
+ // Only check tools that can cause damage
103
151
  const dangerousTools = new Set(['exec', 'write', 'edit', 'browser', 'web_fetch', 'message']);
104
152
  if (!dangerousTools.has(toolName)) return;
105
153
 
@@ -113,35 +161,39 @@ const handler: HookHandler = async (event) => {
113
161
  severity: check.severity,
114
162
  desc: check.desc,
115
163
  mode,
116
- action: 'allowed' as string,
117
- session: (event as any).sessionKey,
164
+ action: 'warned' as string,
165
+ session: event.sessionKey,
118
166
  };
119
167
 
120
- if (mode === 'strict' && (check.severity === 'CRITICAL' || check.severity === 'HIGH')) {
121
- entry.action = 'blocked';
122
- logAudit(entry);
123
- event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
124
- event.cancel = true;
125
- console.warn(`[guard-scanner] 🚨 BLOCKED: ${check.desc} [${check.id}]`);
126
- return;
127
- }
128
-
129
- if (mode === 'enforce' && check.severity === 'CRITICAL') {
130
- entry.action = 'blocked';
131
- logAudit(entry);
132
- event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
133
- event.cancel = true;
134
- console.warn(`[guard-scanner] 🚨 BLOCKED: ${check.desc} [${check.id}]`);
135
- return;
136
- }
137
-
138
- // Monitor mode or non-critical: log only
139
- entry.action = 'logged';
168
+ // NOTE: OpenClaw InternalHookEvent does not currently support
169
+ // a cancel/veto mechanism. When it does, uncomment the blocking
170
+ // logic below. For now, all detections are warnings only.
171
+ //
172
+ // if (mode === 'strict' && (check.severity === 'CRITICAL' || check.severity === 'HIGH')) {
173
+ // entry.action = 'blocked';
174
+ // logAudit(entry);
175
+ // event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
176
+ // event.cancel = true; // Not yet in the public API
177
+ // return;
178
+ // }
179
+ //
180
+ // if (mode === 'enforce' && check.severity === 'CRITICAL') {
181
+ // entry.action = 'blocked';
182
+ // logAudit(entry);
183
+ // event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
184
+ // event.cancel = true; // Not yet in the public API
185
+ // return;
186
+ // }
187
+
188
+ // Current behaviour: warn and log for all modes
140
189
  logAudit(entry);
141
190
 
142
191
  if (check.severity === 'CRITICAL') {
143
192
  event.messages.push(`🛡️ guard-scanner WARNING: ${check.desc} [${check.id}]`);
144
193
  console.warn(`[guard-scanner] ⚠️ WARNING: ${check.desc} [${check.id}]`);
194
+ } else if (check.severity === 'HIGH') {
195
+ event.messages.push(`🛡️ guard-scanner NOTICE: ${check.desc} [${check.id}]`);
196
+ console.warn(`[guard-scanner] ℹ️ NOTICE: ${check.desc} [${check.id}]`);
145
197
  }
146
198
  }
147
199
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "guard-scanner",
3
- "version": "1.0.0",
4
- "description": "Agent skill security scanner — detect prompt injection, malicious code, credential leaks, and 17 threat categories in AI agent skills",
3
+ "version": "1.1.1",
4
+ "description": "Agent skill security scanner — detect prompt injection, malicious code, credential leaks, and 20 threat categories in AI agent skills",
5
5
  "main": "src/scanner.js",
6
6
  "bin": {
7
7
  "guard-scanner": "src/cli.js"
@@ -0,0 +1,239 @@
1
+ /**
2
+ * guard-scanner — HTML Report Template
3
+ * Dark Glassmorphism + Conic-gradient Risk Gauges
4
+ * Zero dependencies. Pure CSS animations.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ function generateHTML(version, stats, findings) {
10
+ const safetyRate = stats.scanned > 0 ? Math.round(((stats.clean + stats.low) / stats.scanned) * 100) : 100;
11
+
12
+ // ── Risk gauge (conic-gradient donut) ──
13
+ const riskGauge = (risk) => {
14
+ const angle = (risk / 100) * 360;
15
+ const color = risk >= 80 ? '#ef4444' : risk >= 30 ? '#f59e0b' : '#22c55e';
16
+ return `<div class="risk-gauge" style="--risk-angle:${angle}deg;--risk-color:${color}"><span class="risk-val">${risk}</span></div>`;
17
+ };
18
+
19
+ // ── Aggregate severity + category counts ──
20
+ const sevCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
21
+ const catCounts = {};
22
+ for (const sr of findings) {
23
+ for (const f of sr.findings) {
24
+ sevCounts[f.severity] = (sevCounts[f.severity] || 0) + 1;
25
+ catCounts[f.cat] = (catCounts[f.cat] || 0) + 1;
26
+ }
27
+ }
28
+ const total = Object.values(sevCounts).reduce((a, b) => a + b, 0);
29
+
30
+ // ── Severity distribution bars ──
31
+ const sevColors = { CRITICAL: '#ef4444', HIGH: '#f97316', MEDIUM: '#eab308', LOW: '#22c55e' };
32
+ const sevBars = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(s => {
33
+ const c = sevCounts[s], pct = total > 0 ? ((c / total) * 100).toFixed(1) : 0;
34
+ return `<div class="bar-row"><span class="bar-label" style="color:${sevColors[s]}">${s}</span><div class="bar-track"><div class="bar-fill" style="width:${pct}%;background:${sevColors[s]}"></div></div><span class="bar-ct">${c}</span></div>`;
35
+ }).join('\n');
36
+
37
+ // ── Top 8 categories ──
38
+ const topCats = Object.entries(catCounts).sort((a, b) => b[1] - a[1]).slice(0, 8);
39
+ const catBars = topCats.map(([cat, c]) => {
40
+ const pct = total > 0 ? ((c / total) * 100).toFixed(0) : 0;
41
+ return `<div class="bar-row"><span class="bar-label cat-lbl">${cat}</span><div class="bar-track"><div class="bar-fill cat-fill" style="width:${pct}%"></div></div><span class="bar-ct">${c}</span></div>`;
42
+ }).join('\n');
43
+
44
+ // ── Skill cards ──
45
+ let cards = '';
46
+ for (const sr of findings) {
47
+ const vc = sr.verdict === 'MALICIOUS' ? 'mal' : sr.verdict === 'SUSPICIOUS' ? 'sus' : sr.verdict === 'LOW RISK' ? 'low' : 'ok';
48
+ const icon = sr.verdict === 'MALICIOUS' ? '💀' : sr.verdict === 'SUSPICIOUS' ? '⚡' : sr.verdict === 'LOW RISK' ? '⚠️' : '✅';
49
+ const rows = sr.findings.map(f => {
50
+ const sc = f.severity.toLowerCase();
51
+ return `<tr class="f-row"><td><span class="pill ${sc}">${f.severity}</span></td><td class="mono dim">${f.cat}</td><td>${f.desc}</td><td class="mono muted">${f.file}${f.line ? ':' + f.line : ''}</td></tr>`;
52
+ }).join('\n');
53
+
54
+ cards += `
55
+ <details class="card card-${vc}" ${(vc === 'mal' || vc === 'sus') ? 'open' : ''}>
56
+ <summary class="card-head">
57
+ <div class="card-info"><span class="card-icon">${icon}</span><span class="card-name">${sr.skill}</span><span class="v-pill v-${vc}">${sr.verdict}</span></div>
58
+ ${riskGauge(sr.risk)}
59
+ </summary>
60
+ <div class="card-body">
61
+ <table class="ftable"><thead><tr><th>Severity</th><th>Category</th><th>Description</th><th>Location</th></tr></thead><tbody>${rows}</tbody></table>
62
+ </div>
63
+ </details>`;
64
+ }
65
+
66
+ // ── Full HTML ──
67
+ return `<!DOCTYPE html>
68
+ <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
69
+ <title>guard-scanner v${version} — Security Report</title>
70
+ <link rel="preconnect" href="https://fonts.googleapis.com">
71
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
72
+ <style>
73
+ /* ===== Tokens ===== */
74
+ :root{
75
+ --bg:#06070d;--sf:rgba(15,18,35,.7);--cd:rgba(20,24,50,.55);--ch:rgba(28,33,65,.65);
76
+ --bdr:rgba(100,120,255,.08);--glow:rgba(100,140,255,.15);
77
+ --t1:#e8eaf6;--t2:#8b92b3;--t3:#5a6180;
78
+ --blue:#6c8cff;--purple:#a78bfa;--cyan:#22d3ee;--green:#22c55e;--yellow:#eab308;--orange:#f97316;--red:#ef4444;
79
+ --sans:'Inter',system-ui,-apple-system,sans-serif;--mono:'JetBrains Mono','SF Mono',monospace;
80
+ --r:12px;
81
+ }
82
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
83
+ html{scroll-behavior:smooth}
84
+ body{font-family:var(--sans);background:var(--bg);color:var(--t1);min-height:100vh;overflow-x:hidden;line-height:1.6}
85
+
86
+ /* ===== Ambient BG ===== */
87
+ body::before{content:'';position:fixed;inset:0;z-index:-1;
88
+ background:radial-gradient(ellipse 80% 60% at 20% 10%,rgba(108,140,255,.08) 0%,transparent 50%),
89
+ radial-gradient(ellipse 60% 50% at 80% 90%,rgba(167,139,250,.06) 0%,transparent 50%),
90
+ radial-gradient(ellipse 50% 40% at 50% 50%,rgba(34,211,238,.04) 0%,transparent 50%);
91
+ animation:pulse 12s ease-in-out infinite alternate}
92
+ @keyframes pulse{0%{opacity:.6;transform:scale(1)}100%{opacity:1;transform:scale(1.05)}}
93
+
94
+ .wrap{max-width:1200px;margin:0 auto;padding:32px 24px}
95
+
96
+ /* ===== Header ===== */
97
+ .hdr{text-align:center;margin-bottom:36px;animation:fadeD .6s ease-out}
98
+ .hdr h1{font-size:2.2em;font-weight:800;letter-spacing:-.03em;
99
+ background:linear-gradient(135deg,var(--blue),var(--purple),var(--cyan));
100
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
101
+ .hdr .sub{color:var(--t2);font-size:.95em;margin-top:4px}
102
+ .hdr .ts{color:var(--t3);font-family:var(--mono);font-size:.78em;margin-top:2px}
103
+
104
+ /* ===== Stat Grid ===== */
105
+ .sg{display:grid;grid-template-columns:repeat(auto-fit,minmax(155px,1fr));gap:14px;margin-bottom:28px;animation:fadeU .7s ease-out}
106
+ .sc{background:var(--cd);backdrop-filter:blur(20px) saturate(1.5);-webkit-backdrop-filter:blur(20px) saturate(1.5);
107
+ border:1px solid var(--bdr);border-radius:var(--r);padding:18px;text-align:center;
108
+ transition:all .3s cubic-bezier(.4,0,.2,1);position:relative;overflow:hidden}
109
+ .sc::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;
110
+ background:linear-gradient(90deg,transparent,var(--ac,var(--blue)),transparent);opacity:0;transition:opacity .3s}
111
+ .sc:hover{transform:translateY(-3px);border-color:var(--glow)}.sc:hover::before{opacity:1}
112
+ .sn{font-size:2.2em;font-weight:800;letter-spacing:-.04em;line-height:1.1}
113
+ .sl{color:var(--t2);font-size:.78em;font-weight:500;margin-top:3px;text-transform:uppercase;letter-spacing:.06em}
114
+ .sc.g{--ac:var(--green)}.sc.g .sn{color:var(--green)}
115
+ .sc.l{--ac:#86efac}.sc.l .sn{color:#86efac}
116
+ .sc.s{--ac:var(--yellow)}.sc.s .sn{color:var(--yellow)}
117
+ .sc.m{--ac:var(--red)}.sc.m .sn{color:var(--red)}
118
+ .sc.r{--ac:var(--cyan)}.sc.r .sn{color:var(--cyan)}
119
+
120
+ /* ===== Analysis Panels ===== */
121
+ .ag{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-bottom:28px;animation:fadeU .8s ease-out}
122
+ @media(max-width:768px){.ag{grid-template-columns:1fr}}
123
+ .pn{background:var(--cd);backdrop-filter:blur(20px) saturate(1.5);-webkit-backdrop-filter:blur(20px) saturate(1.5);
124
+ border:1px solid var(--bdr);border-radius:var(--r);padding:22px}
125
+ .pn h2{font-size:.88em;font-weight:600;color:var(--t2);text-transform:uppercase;letter-spacing:.08em;margin-bottom:14px}
126
+
127
+ /* Bars */
128
+ .bar-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
129
+ .bar-label{font-family:var(--mono);font-size:.72em;font-weight:600;width:68px;text-align:right}
130
+ .cat-lbl{font-family:var(--sans);font-weight:500;color:var(--t2);width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
131
+ .bar-track{flex:1;height:5px;background:rgba(255,255,255,.04);border-radius:3px;overflow:hidden}
132
+ .bar-fill{height:100%;border-radius:3px;transition:width 1.2s cubic-bezier(.4,0,.2,1)}
133
+ .cat-fill{background:linear-gradient(90deg,var(--blue),var(--purple))}
134
+ .bar-ct{font-family:var(--mono);font-size:.76em;color:var(--t3);width:28px}
135
+
136
+ /* ===== Skill Cards ===== */
137
+ .sec-title{font-size:1.05em;font-weight:700;margin-bottom:14px}
138
+ .card{background:var(--cd);backdrop-filter:blur(16px) saturate(1.4);-webkit-backdrop-filter:blur(16px) saturate(1.4);
139
+ border:1px solid var(--bdr);border-radius:var(--r);margin-bottom:10px;overflow:hidden;
140
+ transition:all .3s ease;animation:fadeU .5s ease-out both}
141
+ .card:hover{border-color:var(--glow)}
142
+ .card-mal{border-left:3px solid var(--red)}
143
+ .card-sus{border-left:3px solid var(--yellow)}
144
+ .card-low{border-left:3px solid #86efac}
145
+ .card-ok{border-left:3px solid var(--green)}
146
+
147
+ .card-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;cursor:pointer;list-style:none}
148
+ .card-head::-webkit-details-marker{display:none}
149
+ .card-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}
150
+ .card-icon{font-size:1.15em}
151
+ .card-name{font-weight:600;font-size:.92em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
152
+ .v-pill{font-family:var(--mono);font-size:.66em;font-weight:600;padding:2px 7px;border-radius:4px;letter-spacing:.04em}
153
+ .v-mal{background:rgba(239,68,68,.15);color:var(--red)}
154
+ .v-sus{background:rgba(234,179,8,.15);color:var(--yellow)}
155
+ .v-low{background:rgba(134,239,172,.15);color:#86efac}
156
+ .v-ok{background:rgba(34,197,94,.15);color:var(--green)}
157
+
158
+ /* Risk Gauge */
159
+ .risk-gauge{width:44px;height:44px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;position:relative;
160
+ background:conic-gradient(var(--risk-color) 0deg,var(--risk-color) var(--risk-angle),rgba(255,255,255,.05) var(--risk-angle),rgba(255,255,255,.05) 360deg)}
161
+ .risk-gauge::after{content:'';position:absolute;inset:5px;border-radius:50%;background:var(--bg)}
162
+ .risk-val{position:relative;z-index:1;font-family:var(--mono);font-size:.68em;font-weight:700}
163
+
164
+ /* Findings Table */
165
+ .card-body{padding:0 18px 14px}
166
+ .ftable{width:100%;border-collapse:collapse;font-size:.82em}
167
+ .ftable th{text-align:left;font-size:.72em;font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:.06em;padding:7px 9px;border-bottom:1px solid rgba(255,255,255,.04)}
168
+ .ftable td{padding:7px 9px;border-bottom:1px solid rgba(255,255,255,.025)}
169
+ .f-row{transition:background .2s}.f-row:hover{background:rgba(255,255,255,.02)}
170
+ .pill{font-family:var(--mono);font-size:.7em;font-weight:600;padding:2px 5px;border-radius:3px;letter-spacing:.03em}
171
+ .critical{background:rgba(239,68,68,.15);color:var(--red)}
172
+ .high{background:rgba(249,115,22,.15);color:var(--orange)}
173
+ .medium{background:rgba(234,179,8,.15);color:var(--yellow)}
174
+ .low{background:rgba(34,197,94,.15);color:var(--green)}
175
+ .mono{font-family:var(--mono);font-size:.9em}
176
+ .dim{color:var(--t2)}.muted{color:var(--t3);white-space:nowrap}
177
+
178
+ /* Empty */
179
+ .empty{text-align:center;padding:48px 20px;background:var(--cd);backdrop-filter:blur(20px);border:1px solid var(--bdr);border-radius:var(--r)}
180
+ .empty .ei{font-size:2.8em;margin-bottom:10px}
181
+ .empty p{color:var(--green);font-size:1.05em;font-weight:600}
182
+ .empty .es{color:var(--t3);font-size:.82em;margin-top:3px}
183
+
184
+ /* Footer */
185
+ .ft{text-align:center;margin-top:40px;padding-top:20px;border-top:1px solid rgba(255,255,255,.04);color:var(--t3);font-size:.78em}
186
+ .ft a{color:var(--blue);text-decoration:none}.ft a:hover{text-decoration:underline}
187
+
188
+ /* Animations */
189
+ @keyframes fadeD{from{opacity:0;transform:translateY(-18px)}to{opacity:1;transform:translateY(0)}}
190
+ @keyframes fadeU{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
191
+
192
+ /* Responsive */
193
+ @media(max-width:640px){
194
+ .sg{grid-template-columns:repeat(2,1fr)}.sn{font-size:1.7em}.hdr h1{font-size:1.5em}
195
+ .ftable{font-size:.74em}
196
+ }
197
+
198
+ /* Print */
199
+ @media print{
200
+ body{background:#fff;color:#111}body::before{display:none}
201
+ .sc,.pn,.card{background:#f8f8f8;backdrop-filter:none;border-color:#ddd}
202
+ .sn{color:#111!important}.card{break-inside:avoid}
203
+ }
204
+ </style></head><body>
205
+ <div class="wrap">
206
+ <div class="hdr">
207
+ <h1>🛡️ guard-scanner v${version}</h1>
208
+ <div class="sub">Security Scan Report</div>
209
+ <div class="ts">${new Date().toISOString()}</div>
210
+ </div>
211
+
212
+ <div class="sg">
213
+ <div class="sc"><div class="sn">${stats.scanned}</div><div class="sl">Scanned</div></div>
214
+ <div class="sc g"><div class="sn">${stats.clean}</div><div class="sl">Clean</div></div>
215
+ <div class="sc l"><div class="sn">${stats.low}</div><div class="sl">Low Risk</div></div>
216
+ <div class="sc s"><div class="sn">${stats.suspicious}</div><div class="sl">Suspicious</div></div>
217
+ <div class="sc m"><div class="sn">${stats.malicious}</div><div class="sl">Malicious</div></div>
218
+ <div class="sc r"><div class="sn">${safetyRate}%</div><div class="sl">Safety Rate</div></div>
219
+ </div>
220
+
221
+ ${total > 0 ? `<div class="ag">
222
+ <div class="pn"><h2>Severity Distribution</h2>${sevBars}</div>
223
+ <div class="pn"><h2>Top Categories</h2>${catBars}</div>
224
+ </div>` : ''}
225
+
226
+ <div>
227
+ <div class="sec-title">Skill Analysis</div>
228
+ ${cards || `<div class="empty"><div class="ei">✅</div><p>All Clear — No Threats Detected</p><div class="es">${stats.scanned} skill(s) scanned, 0 findings</div></div>`}
229
+ </div>
230
+
231
+ <div class="ft">
232
+ guard-scanner v${version} &mdash; Zero dependencies. Zero compromises. 🛡️<br>
233
+ Built by <a href="https://github.com/koatora20">Guava 🍈 &amp; Dee</a>
234
+ </div>
235
+ </div>
236
+ </body></html>`;
237
+ }
238
+
239
+ module.exports = { generateHTML };
package/src/patterns.js CHANGED
@@ -177,6 +177,14 @@ const PATTERNS = [
177
177
  { id: 'SOUL_HOOK_SWAP', cat: 'identity-hijack', regex: /(?:hook|bootstrap|init)\s+[^\n]*(?:swap|replace|override)\s+[^\n]*(?:SOUL|IDENTITY|persona)/gi, severity: 'CRITICAL', desc: 'Hook-based identity swap at bootstrap', all: true },
178
178
  { id: 'SOUL_NAME_OVERRIDE', cat: 'identity-hijack', regex: /(?:your\s+name\s+is|you\s+are\s+now|call\s+yourself|from\s+now\s+on\s+you\s+are)\s+(?!the\s+(?:user|human|assistant))/gi, severity: 'HIGH', desc: 'Agent name/identity override', docOnly: true },
179
179
  { id: 'SOUL_MEMORY_WIPE', cat: 'identity-hijack', regex: /(?:wipe|clear|erase|delete|remove|reset)\s+(?:all\s+)?(?:your\s+)?(?:memory|memories|MEMORY\.md|identity|soul)/gi, severity: 'CRITICAL', desc: 'Memory/identity wipe instruction', docOnly: true },
180
+
181
+ // ── Category 18: Config Impact Analysis ──
182
+ { id: 'CFG_OPENCLAW_WRITE', cat: 'config-impact', regex: /(?:write|writeFile|writeFileSync|fs\.write)\s*\([^)]*openclaw\.json/gi, severity: 'CRITICAL', desc: 'Direct write to openclaw.json', codeOnly: true },
183
+ { id: 'CFG_EXEC_APPROVALS_OFF', cat: 'config-impact', regex: /(?:exec\.approvals?|approvals?)\s*[:=]\s*['"](off|false|disabled|none)['"]/gi, severity: 'CRITICAL', desc: 'Disable exec approvals via config', all: true },
184
+ { id: 'CFG_HOOKS_MODIFY', cat: 'config-impact', regex: /hooks\.internal\.entries\s*[:=]|hooks\.internal\s*[:=]\s*\{/gi, severity: 'HIGH', desc: 'Modify hooks.internal configuration', codeOnly: true },
185
+ { id: 'CFG_EXEC_HOST_GW', cat: 'config-impact', regex: /tools\.exec\.host\s*[:=]\s*['"]gateway['"]/gi, severity: 'CRITICAL', desc: 'Set exec host to gateway (bypass sandbox)', all: true },
186
+ { id: 'CFG_SANDBOX_OFF', cat: 'config-impact', regex: /(?:sandbox|sandboxed|containerized)\s*[:=]\s*(?:false|off|none|disabled|0)/gi, severity: 'CRITICAL', desc: 'Disable sandbox via configuration', all: true },
187
+ { id: 'CFG_TOOL_OVERRIDE', cat: 'config-impact', regex: /(?:tools|capabilities)\s*\.\s*(?:exec|write|browser|web_fetch)\s*[:=]\s*\{[^}]*(?:enabled|allowed|host)/gi, severity: 'HIGH', desc: 'Override tool security settings', codeOnly: true },
180
188
  ];
181
189
 
182
190
  module.exports = { PATTERNS };
package/src/scanner.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * purpose: Static analysis of agent skill files for threat patterns
13
13
  *
14
14
  * Based on GuavaGuard v9.0.0 (OSS extraction)
15
- * 17 threat categories • Snyk ToxicSkills + OWASP MCP Top 10
15
+ * 20 threat categories • Snyk ToxicSkills + OWASP MCP Top 10
16
16
  * Zero dependencies • CLI + JSON + SARIF + HTML output
17
17
  * Plugin API for custom detection rules
18
18
  *
@@ -27,9 +27,10 @@ const os = require('os');
27
27
 
28
28
  const { PATTERNS } = require('./patterns.js');
29
29
  const { KNOWN_MALICIOUS } = require('./ioc-db.js');
30
+ const { generateHTML } = require('./html-template.js');
30
31
 
31
32
  // ===== CONFIGURATION =====
32
- const VERSION = '1.0.0';
33
+ const VERSION = '1.1.0';
33
34
 
34
35
  const THRESHOLDS = {
35
36
  normal: { suspicious: 30, malicious: 80 },
@@ -266,6 +267,15 @@ class GuardScanner {
266
267
  // Check 6: Cross-file analysis
267
268
  this.checkCrossFile(skillPath, skillName, skillFindings);
268
269
 
270
+ // Check 7: Skill manifest validation (v1.1)
271
+ this.checkSkillManifest(skillPath, skillName, skillFindings);
272
+
273
+ // Check 8: Code complexity metrics (v1.1)
274
+ this.checkComplexity(skillPath, skillName, skillFindings);
275
+
276
+ // Check 9: Config impact analysis (v1.1)
277
+ this.checkConfigImpact(skillPath, skillName, skillFindings);
278
+
269
279
  // Filter ignored patterns
270
280
  const filteredFindings = skillFindings.filter(f => !this.ignoredPatterns.has(f.id));
271
281
 
@@ -474,6 +484,226 @@ class GuardScanner {
474
484
  }
475
485
  }
476
486
 
487
+ // ── v1.1: Skill Manifest Validation ──
488
+ // Checks SKILL.md frontmatter for dangerous tool declarations,
489
+ // overly broad file scope, and sensitive env requirements
490
+ checkSkillManifest(skillPath, skillName, findings) {
491
+ const skillMd = path.join(skillPath, 'SKILL.md');
492
+ if (!fs.existsSync(skillMd)) return;
493
+
494
+ let content;
495
+ try { content = fs.readFileSync(skillMd, 'utf-8'); } catch { return; }
496
+
497
+ // Parse YAML frontmatter (lightweight, no dependency)
498
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
499
+ if (!fmMatch) return;
500
+ const fm = fmMatch[1];
501
+
502
+ // Check 1: Dangerous binary requirements
503
+ const DANGEROUS_BINS = new Set([
504
+ 'sudo', 'rm', 'rmdir', 'chmod', 'chown', 'kill', 'pkill',
505
+ 'curl', 'wget', 'nc', 'ncat', 'socat', 'ssh', 'scp',
506
+ 'dd', 'mkfs', 'fdisk', 'mount', 'umount',
507
+ 'iptables', 'ufw', 'firewall-cmd',
508
+ 'docker', 'kubectl', 'systemctl',
509
+ ]);
510
+ const binsMatch = fm.match(/bins:\s*\n((?:\s+-\s+[^\n]+\n?)*)/i);
511
+ if (binsMatch) {
512
+ const bins = binsMatch[1].match(/- ([^\n]+)/g) || [];
513
+ for (const binLine of bins) {
514
+ const bin = binLine.replace(/^-\s*/, '').trim().toLowerCase();
515
+ if (DANGEROUS_BINS.has(bin)) {
516
+ findings.push({
517
+ severity: 'HIGH', id: 'MANIFEST_DANGEROUS_BIN',
518
+ cat: 'sandbox-validation',
519
+ desc: `SKILL.md requires dangerous binary: ${bin}`,
520
+ file: 'SKILL.md'
521
+ });
522
+ }
523
+ }
524
+ }
525
+
526
+ // Check 2: Overly broad file scope
527
+ const filesMatch = fm.match(/files:\s*\[([^\]]+)\]/i) || fm.match(/files:\s*\n((?:\s+-\s+[^\n]+\n?)*)/i);
528
+ if (filesMatch) {
529
+ const filesStr = filesMatch[1];
530
+ if (/\*\*\/\*|\*\.\*|\"\*\"/i.test(filesStr)) {
531
+ findings.push({
532
+ severity: 'HIGH', id: 'MANIFEST_BROAD_FILES',
533
+ cat: 'sandbox-validation',
534
+ desc: 'SKILL.md declares overly broad file scope (e.g. **/*)',
535
+ file: 'SKILL.md'
536
+ });
537
+ }
538
+ }
539
+
540
+ // Check 3: Sensitive env requirements
541
+ const SENSITIVE_ENV_PATTERNS = /(?:SECRET|PASSWORD|CREDENTIAL|PRIVATE_KEY|AWS_SECRET|GITHUB_TOKEN)/i;
542
+ const envMatch = fm.match(/env:\s*\n((?:\s+-\s+[^\n]+\n?)*)/i);
543
+ if (envMatch) {
544
+ const envVars = envMatch[1].match(/- ([^\n]+)/g) || [];
545
+ for (const envLine of envVars) {
546
+ const envVar = envLine.replace(/^-\s*/, '').trim();
547
+ if (SENSITIVE_ENV_PATTERNS.test(envVar)) {
548
+ findings.push({
549
+ severity: 'HIGH', id: 'MANIFEST_SENSITIVE_ENV',
550
+ cat: 'sandbox-validation',
551
+ desc: `SKILL.md requires sensitive env var: ${envVar}`,
552
+ file: 'SKILL.md'
553
+ });
554
+ }
555
+ }
556
+ }
557
+
558
+ // Check 4: exec or network declared without justification
559
+ if (/exec:\s*(?:true|yes|enabled|'\*'|"\*")/i.test(fm)) {
560
+ findings.push({
561
+ severity: 'MEDIUM', id: 'MANIFEST_EXEC_DECLARED',
562
+ cat: 'sandbox-validation',
563
+ desc: 'SKILL.md declares exec capability',
564
+ file: 'SKILL.md'
565
+ });
566
+ }
567
+ if (/network:\s*(?:true|yes|enabled|'\*'|"\*"|all|any)/i.test(fm)) {
568
+ findings.push({
569
+ severity: 'MEDIUM', id: 'MANIFEST_NETWORK_DECLARED',
570
+ cat: 'sandbox-validation',
571
+ desc: 'SKILL.md declares unrestricted network access',
572
+ file: 'SKILL.md'
573
+ });
574
+ }
575
+ }
576
+
577
+ // ── v1.1: Code Complexity Metrics ──
578
+ // Detects excessive file length, deep nesting, and eval/exec density
579
+ checkComplexity(skillPath, skillName, findings) {
580
+ const files = this.getFiles(skillPath);
581
+ const MAX_LINES = 1000;
582
+ const MAX_NESTING = 5;
583
+ const MAX_EVAL_DENSITY = 0.02; // 2% of lines
584
+
585
+ for (const file of files) {
586
+ const ext = path.extname(file).toLowerCase();
587
+ if (!CODE_EXTENSIONS.has(ext)) continue;
588
+
589
+ const relFile = path.relative(skillPath, file);
590
+ if (relFile.includes('node_modules') || relFile.startsWith('.git')) continue;
591
+
592
+ let content;
593
+ try { content = fs.readFileSync(file, 'utf-8'); } catch { continue; }
594
+
595
+ const lines = content.split('\n');
596
+
597
+ // Check 1: Excessive file length
598
+ if (lines.length > MAX_LINES) {
599
+ findings.push({
600
+ severity: 'MEDIUM', id: 'COMPLEXITY_LONG_FILE',
601
+ cat: 'complexity',
602
+ desc: `File exceeds ${MAX_LINES} lines (${lines.length} lines)`,
603
+ file: relFile
604
+ });
605
+ }
606
+
607
+ // Check 2: Deep nesting (brace tracking)
608
+ let maxDepth = 0;
609
+ let currentDepth = 0;
610
+ let deepestLine = 0;
611
+ for (let i = 0; i < lines.length; i++) {
612
+ const line = lines[i];
613
+ // Count opening/closing braces outside strings (simplified)
614
+ for (const ch of line) {
615
+ if (ch === '{') currentDepth++;
616
+ if (ch === '}') currentDepth = Math.max(0, currentDepth - 1);
617
+ }
618
+ if (currentDepth > maxDepth) {
619
+ maxDepth = currentDepth;
620
+ deepestLine = i + 1;
621
+ }
622
+ }
623
+ if (maxDepth > MAX_NESTING) {
624
+ findings.push({
625
+ severity: 'MEDIUM', id: 'COMPLEXITY_DEEP_NESTING',
626
+ cat: 'complexity',
627
+ desc: `Deep nesting detected: ${maxDepth} levels (max recommended: ${MAX_NESTING})`,
628
+ file: relFile, line: deepestLine
629
+ });
630
+ }
631
+
632
+ // Check 3: eval/exec density
633
+ const evalPattern = /\b(?:eval|exec|execSync|spawn|Function)\s*\(/g;
634
+ const evalMatches = content.match(evalPattern) || [];
635
+ const density = lines.length > 0 ? evalMatches.length / lines.length : 0;
636
+ if (density > MAX_EVAL_DENSITY && evalMatches.length >= 3) {
637
+ findings.push({
638
+ severity: 'HIGH', id: 'COMPLEXITY_EVAL_DENSITY',
639
+ cat: 'complexity',
640
+ desc: `High eval/exec density: ${evalMatches.length} calls in ${lines.length} lines (${(density * 100).toFixed(1)}%)`,
641
+ file: relFile
642
+ });
643
+ }
644
+ }
645
+ }
646
+
647
+ // ── v1.1: Config Impact Analysis ──
648
+ // Detects modifications to openclaw.json and dangerous configuration changes
649
+ checkConfigImpact(skillPath, skillName, findings) {
650
+ const files = this.getFiles(skillPath);
651
+
652
+ for (const file of files) {
653
+ const ext = path.extname(file).toLowerCase();
654
+ if (!CODE_EXTENSIONS.has(ext) && ext !== '.json') continue;
655
+
656
+ const relFile = path.relative(skillPath, file);
657
+ if (relFile.includes('node_modules') || relFile.startsWith('.git')) continue;
658
+
659
+ let content;
660
+ try { content = fs.readFileSync(file, 'utf-8'); } catch { continue; }
661
+
662
+ // Check 1: openclaw.json reference + write operation in same file
663
+ // Handles both direct and variable-based patterns (e.g. writeFileSync(configPath))
664
+ const hasConfigRef = /openclaw\.json/i.test(content);
665
+ const hasWriteOp = /(?:writeFileSync|writeFile|fs\.write)\s*\(/i.test(content);
666
+ if (hasConfigRef && hasWriteOp) {
667
+ // Find the write line for location info
668
+ const clines = content.split('\n');
669
+ let writeLine = 0;
670
+ for (let i = 0; i < clines.length; i++) {
671
+ if (/(?:writeFileSync|writeFile|fs\.write)\s*\(/i.test(clines[i])) {
672
+ writeLine = i + 1;
673
+ break;
674
+ }
675
+ }
676
+ findings.push({
677
+ severity: 'CRITICAL', id: 'CFG_WRITE_DETECTED',
678
+ cat: 'config-impact',
679
+ desc: 'Code writes to openclaw.json',
680
+ file: relFile, line: writeLine,
681
+ sample: writeLine > 0 ? clines[writeLine - 1].trim().substring(0, 80) : ''
682
+ });
683
+ }
684
+
685
+ // Check 2: Dangerous config key modifications
686
+ const DANGEROUS_CONFIG_KEYS = [
687
+ { regex: /exec\.approvals?\s*[:=]\s*['"]?(off|false|disabled|none)/gi, id: 'CFG_EXEC_APPROVAL_OFF', desc: 'Disables exec approval requirement', severity: 'CRITICAL' },
688
+ { regex: /tools\.exec\.host\s*[:=]\s*['"]gateway['"]/gi, id: 'CFG_EXEC_HOST_GATEWAY', desc: 'Sets exec host to gateway (bypasses sandbox)', severity: 'CRITICAL' },
689
+ { regex: /hooks\s*\.\s*internal\s*\.\s*entries\s*[:=]/gi, id: 'CFG_HOOKS_INTERNAL', desc: 'Modifies internal hook entries', severity: 'HIGH' },
690
+ { regex: /network\.allowedDomains\s*[:=]\s*\[?\s*['"]\*['"]/gi, id: 'CFG_NET_WILDCARD', desc: 'Sets network allowedDomains to wildcard', severity: 'HIGH' },
691
+ ];
692
+
693
+ for (const check of DANGEROUS_CONFIG_KEYS) {
694
+ check.regex.lastIndex = 0;
695
+ if (check.regex.test(content)) {
696
+ findings.push({
697
+ severity: check.severity, id: check.id,
698
+ cat: 'config-impact',
699
+ desc: check.desc,
700
+ file: relFile
701
+ });
702
+ }
703
+ }
704
+ }
705
+ }
706
+
477
707
  checkHiddenFiles(skillPath, skillName, findings) {
478
708
  try {
479
709
  const entries = fs.readdirSync(skillPath);
@@ -631,6 +861,11 @@ class GuardScanner {
631
861
  if (cats.has('identity-hijack') && (cats.has('persistence') || cats.has('memory-poisoning'))) score = Math.max(score, 90);
632
862
  if (ids.has('IOC_IP') || ids.has('IOC_URL') || ids.has('KNOWN_TYPOSQUAT')) score = 100;
633
863
 
864
+ // v1.1 categories
865
+ if (cats.has('config-impact')) score = Math.round(score * 2);
866
+ if (cats.has('config-impact') && cats.has('sandbox-validation')) score = Math.max(score, 70);
867
+ if (cats.has('complexity') && (cats.has('malicious-code') || cats.has('obfuscation'))) score = Math.round(score * 1.5);
868
+
634
869
  return Math.min(100, score);
635
870
  }
636
871
 
@@ -701,6 +936,9 @@ class GuardScanner {
701
936
  if (cats.has('persistence')) skillRecs.push('⏰ PERSISTENCE: Creates scheduled tasks.');
702
937
  if (cats.has('cve-patterns')) skillRecs.push('🚨 CVE PATTERN: Matches known exploits.');
703
938
  if (cats.has('identity-hijack')) skillRecs.push('🔒 IDENTITY HIJACK: Agent soul file tampering. DO NOT INSTALL.');
939
+ if (cats.has('sandbox-validation')) skillRecs.push('🔒 SANDBOX: Skill requests dangerous capabilities.');
940
+ if (cats.has('complexity')) skillRecs.push('🧩 COMPLEXITY: Excessive code complexity may hide malicious behavior.');
941
+ if (cats.has('config-impact')) skillRecs.push('⚙️ CONFIG IMPACT: Modifies OpenClaw configuration. DO NOT INSTALL.');
704
942
 
705
943
  if (skillRecs.length > 0) recommendations.push({ skill: skillResult.skill, actions: skillRecs });
706
944
  }
@@ -754,54 +992,7 @@ class GuardScanner {
754
992
  }
755
993
 
756
994
  toHTML() {
757
- const stats = this.stats;
758
- const sevColors = { CRITICAL: '#dc2626', HIGH: '#ea580c', MEDIUM: '#ca8a04', LOW: '#65a30d' };
759
-
760
- let skillRows = '';
761
- for (const sr of this.findings) {
762
- const findingRows = sr.findings.map(f => {
763
- const color = sevColors[f.severity] || '#666';
764
- return `<tr><td style="color:${color};font-weight:bold">${f.severity}</td><td>${f.cat}</td><td>${f.desc}</td><td>${f.file}${f.line ? ':' + f.line : ''}</td></tr>`;
765
- }).join('\n');
766
-
767
- const verdictColor = sr.verdict === 'MALICIOUS' ? '#dc2626' : sr.verdict === 'SUSPICIOUS' ? '#ca8a04' : '#65a30d';
768
- skillRows += `
769
- <div class="skill-card">
770
- <h3>${sr.skill} <span style="color:${verdictColor}">[${sr.verdict}]</span> <small>Risk: ${sr.risk}</small></h3>
771
- <table><thead><tr><th>Severity</th><th>Category</th><th>Description</th><th>Location</th></tr></thead>
772
- <tbody>${findingRows}</tbody></table>
773
- </div>`;
774
- }
775
-
776
- return `<!DOCTYPE html>
777
- <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
778
- <title>guard-scanner v${VERSION} Report</title>
779
- <style>
780
- body{font-family:system-ui,sans-serif;max-width:1000px;margin:0 auto;padding:20px;background:#0f172a;color:#e2e8f0}
781
- h1{color:#4ade80}h2{color:#86efac;border-bottom:1px solid #334155;padding-bottom:8px}h3{margin:0}
782
- .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin:20px 0}
783
- .stat{background:#1e293b;border-radius:8px;padding:16px;text-align:center}
784
- .stat .num{font-size:2em;font-weight:bold}.stat .label{color:#94a3b8;font-size:0.85em}
785
- .stat.clean .num{color:#4ade80}.stat.low .num{color:#86efac}.stat.suspicious .num{color:#fbbf24}.stat.malicious .num{color:#ef4444}
786
- .skill-card{background:#1e293b;border-radius:8px;padding:16px;margin:12px 0;border-left:4px solid #334155}
787
- table{width:100%;border-collapse:collapse;margin-top:8px;font-size:0.9em}
788
- th,td{padding:6px 10px;text-align:left;border-bottom:1px solid #334155}
789
- th{color:#94a3b8;font-weight:600}small{color:#64748b;margin-left:8px}
790
- .footer{color:#475569;text-align:center;margin-top:40px;font-size:0.8em}
791
- </style></head><body>
792
- <h1>🛡️ guard-scanner v${VERSION}</h1>
793
- <p>Scan completed: ${new Date().toISOString()}</p>
794
- <div class="stats">
795
- <div class="stat"><div class="num">${stats.scanned}</div><div class="label">Scanned</div></div>
796
- <div class="stat clean"><div class="num">${stats.clean}</div><div class="label">Clean</div></div>
797
- <div class="stat low"><div class="num">${stats.low}</div><div class="label">Low Risk</div></div>
798
- <div class="stat suspicious"><div class="num">${stats.suspicious}</div><div class="label">Suspicious</div></div>
799
- <div class="stat malicious"><div class="num">${stats.malicious}</div><div class="label">Malicious</div></div>
800
- </div>
801
- <h2>Findings</h2>
802
- ${skillRows || '<p style="color:#4ade80">✅ No threats detected.</p>'}
803
- <div class="footer">guard-scanner v${VERSION} — Zero dependencies. Zero compromises. 🛡️</div>
804
- </body></html>`;
995
+ return generateHTML(VERSION, this.stats, this.findings);
805
996
  }
806
997
  }
807
998