guard-scanner 1.1.0 → 2.0.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/README.md CHANGED
@@ -2,13 +2,14 @@
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 17 more threat categories.
5
+ Detect prompt injection, credential theft, exfiltration, identity hijacking, and 16 more threat categories.<br>
6
+ <sub>🆕 Plugin Hook v2.0 — <strong>actual blocking</strong> via <code>block</code>/<code>blockReason</code> API</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-55%2F55-brightgreen" alt="Tests Passing">
12
+ <img src="https://img.shields.io/badge/tests-56%2F56-brightgreen" alt="Tests Passing">
12
13
  <img src="https://img.shields.io/badge/patterns-186-orange" alt="186 Patterns">
13
14
  <img src="https://img.shields.io/badge/categories-20-blueviolet" alt="20 Categories">
14
15
  </p>
@@ -73,6 +74,18 @@ npx guard-scanner ./skills/ --strict
73
74
  npx guard-scanner ./skills/ --verbose --check-deps --json --sarif --html
74
75
  ```
75
76
 
77
+ ## OpenClaw Recommended Setup (short)
78
+
79
+ ```bash
80
+ # 1) Pre-install / pre-update static gate
81
+ npx guard-scanner ~/.openclaw/workspace/skills --self-exclude --verbose
82
+
83
+ # 2) Runtime guard — Plugin Hook version (blocks dangerous calls!)
84
+ cp hooks/guard-scanner/plugin.ts ~/.openclaw/plugins/guard-scanner-runtime.ts
85
+ ```
86
+
87
+ > **🆕 v2.0 Plugin Hook** — Uses OpenClaw's native `block`/`blockReason` API to actually prevent dangerous tool calls. Supports 3 modes: `monitor` (log only), `enforce` (block CRITICAL), `strict` (block HIGH + CRITICAL).
88
+
76
89
  ### Installation (Optional)
77
90
 
78
91
  ```bash
@@ -90,11 +103,13 @@ openclaw skill install guard-scanner
90
103
  guard-scanner ~/.openclaw/workspace/skills/ --self-exclude --verbose
91
104
  ```
92
105
 
106
+ > **🆕 Plugin Hook version** (`plugin.ts`) uses the `before_tool_call` Plugin Hook API with `block`/`blockReason` — **detections are actually blocked**. The legacy Internal Hook version (`handler.ts`) is still available for backward compatibility but can only warn.
107
+
93
108
  ---
94
109
 
95
110
  ## Threat Categories
96
111
 
97
- guard-scanner covers **20 threat categories** derived from three taxonomies:
112
+ guard-scanner covers **20 threat categories** derived from four sources:
98
113
 
99
114
  | # | Category | Based On | Severity | What It Detects |
100
115
  |---|----------|----------|----------|----------------|
@@ -128,7 +143,7 @@ guard-scanner covers **20 threat categories** derived from three taxonomies:
128
143
  ### Terminal (Default)
129
144
 
130
145
  ```
131
- 🛡️ guard-scanner v1.0.0
146
+ 🛡️ guard-scanner v1.1.1
132
147
  ══════════════════════════════════════════════════════
133
148
  📂 Scanning: ./skills/
134
149
  📦 Skills found: 22
@@ -391,9 +406,12 @@ guard-scanner/
391
406
  │ └── cli.js # CLI entry point and argument parser
392
407
  ├── hooks/
393
408
  │ └── guard-scanner/
394
- └── handler.ts # Runtime Guardbefore_tool_call hook
409
+ ├── plugin.ts # 🆕 Plugin Hook v2.0 actual blocking via block/blockReason
410
+ │ ├── handler.ts # Legacy Internal Hook — warn only (deprecated)
411
+ │ └── HOOK.md # Internal Hook manifest (legacy)
395
412
  ├── test/
396
- │ ├── scanner.test.js # 55 tests across 13 sections
413
+ │ ├── scanner.test.js # 56 tests static scanner
414
+ │ ├── plugin.test.js # 35 tests — Plugin Hook runtime guard
397
415
  │ └── fixtures/ # Malicious, clean, complex, config-changer samples
398
416
  ├── package.json # Zero dependencies, node --test
399
417
  ├── CHANGELOG.md
@@ -518,11 +536,11 @@ console.log(scanner.toHTML()); // HTML string
518
536
  ## Test Results
519
537
 
520
538
  ```
521
- ℹ tests 55
539
+ ℹ tests 56
522
540
  ℹ suites 13
523
- ℹ pass 55
541
+ ℹ pass 56
524
542
  ℹ fail 0
525
- ℹ duration_ms 115ms
543
+ ℹ duration_ms 108ms
526
544
  ```
527
545
 
528
546
  | Suite | Tests | Coverage |
@@ -553,13 +571,36 @@ console.log(scanner.toHTML()); // HTML string
553
571
 
554
572
  ---
555
573
 
574
+ ## OWASP Gen AI Top 10 Coverage
575
+
576
+ guard-scanner's coverage of the [OWASP Top 10 for LLM Applications (2025)](https://owasp.org/www-project-top-10-for-large-language-model-applications/):
577
+
578
+ | # | Risk | Status | Detection Method |
579
+ |---|------|--------|------------------|
580
+ | LLM01 | Prompt Injection | ⚠️ Partial | Regex: Unicode exploits, role override, system tags, base64 instructions |
581
+ | LLM02 | Insecure Output Handling | 🔜 v1.2 | Planned: unvalidated output execution patterns |
582
+ | LLM03 | Training Data Poisoning | ⬜ N/A | Out of scope for static analysis |
583
+ | LLM04 | Model Denial of Service | 🔜 v1.3 | Planned: excessive input / infinite loop patterns |
584
+ | LLM05 | Supply Chain Vulnerabilities | ⚠️ Partial | IoC database, typosquat detection, dependency chain scan |
585
+ | LLM06 | Sensitive Information Disclosure | ⚠️ Partial | Secret detection, PII patterns, credential leaks |
586
+ | LLM07 | Insecure Plugin Design | 🔜 v1.3 | Planned: unvalidated plugin input patterns |
587
+ | LLM08 | Excessive Agency | 🔜 v1.3 | Planned: over-permissioned scope detection |
588
+ | LLM09 | Overreliance | 🔜 v1.3 | Planned: unverified output trust patterns |
589
+ | LLM10 | Model Theft | 🔜 v1.3 | Planned: model file exfiltration patterns |
590
+
591
+ > **Current coverage: 3/10 (partial).** Full OWASP Gen AI coverage is targeted for v1.3. See [ROADMAP.md](ROADMAP.md) for details.
592
+ >
593
+ > **Known limitation:** Regex-based detection can be evaded by AI-generated code obfuscation. v2.0 will introduce AST analysis and ML-based detection to address this structural gap.
594
+
595
+ ---
596
+
556
597
  ## Contributing
557
598
 
558
599
  1. Fork the repository
559
600
  2. Create a feature branch (`git checkout -b feature/new-pattern`)
560
601
  3. Add your pattern to `src/patterns.js` with the required fields
561
602
  4. Add a test case in `test/fixtures/` and `test/scanner.test.js`
562
- 5. Run `npm test` — all 55+ tests must pass
603
+ 5. Run `npm test` — all 56+ tests must pass
563
604
  6. Submit a Pull Request
564
605
 
565
606
  ### Adding a New Detection Pattern
@@ -605,15 +646,29 @@ guard-scanner catches threats **before** installation. But what happens **after*
605
646
  | | guard-scanner (OSS) | GuavaSuite (Private) |
606
647
  |---|---|---|
607
648
  | Static scan | ✅ 20 categories | ✅ 20 categories |
608
- | Runtime blocking | | Real-time `before_tool_call` guard |
609
- | SOUL.md integrity | Pattern detection only | SHA-256 hash watchdog |
610
- | On-chain verification | — | SoulChain (Polygon) |
611
- | Identity recovery | — | Automatic rollback |
649
+ | Runtime blocking | Plugin Hook v2.0 (`block`/`blockReason`) | ✅ SuiteGate (enhanced ruleset) |
650
+ | SOUL.md integrity | Pattern detection only | SHA-256 hash watchdog (W4 E2E) |
651
+ | On-chain verification | — | SoulChain (Polygon, Phase 2) |
652
+ | Identity recovery | — | Automatic rollback (Phase 2) |
612
653
 
613
654
  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).
614
655
 
615
656
  ---
616
657
 
658
+ ## Roadmap
659
+
660
+ | Version | Focus | Key Features |
661
+ |---------|-------|------|
662
+ | v1.1.1 ✅ | Stability | 56 tests, bug fixes |
663
+ | v1.2 | PII + Shadow AI | Credential-in-context, unauthorized LLM API calls, memory poisoning vectors |
664
+ | v1.3 | OWASP Gen AI | Complete LLM02/04/07/08/09/10 coverage |
665
+ | v2.0 | AST + ML | JavaScript AST analysis, taint tracking, ML-based obfuscation detection, SBOM generation |
666
+ | v2.1 | Community | YAML pattern definitions, CONTRIBUTING guide, automated pattern updates |
667
+
668
+ See [ROADMAP.md](ROADMAP.md) for full details.
669
+
670
+ ---
671
+
617
672
  ## 💜 Sponsor This Project
618
673
 
619
674
  If guard-scanner helps protect your agents, consider sponsoring continued development:
package/SECURITY.md CHANGED
@@ -5,7 +5,7 @@
5
5
  If you discover a security vulnerability in guard-scanner itself, please report it responsibly:
6
6
 
7
7
  1. **Do NOT open a public issue**
8
- 2. Email: [security contact via GitHub private vulnerability reporting]
8
+ 2. Email: socialgreen.jp@gmail.com
9
9
  3. Include: affected version, steps to reproduce, potential impact
10
10
 
11
11
  We will respond within 48 hours and provide a fix within 7 days for critical issues.
package/SKILL.md CHANGED
@@ -29,7 +29,7 @@ metadata:
29
29
  # guard-scanner 🛡️
30
30
 
31
31
  Static + runtime security scanner for AI agent skills.
32
- **170+ threat patterns** across **17 categories** — zero dependencies.
32
+ **186+ threat patterns** across **20 categories** — zero dependencies.
33
33
 
34
34
  ## When To Use This Skill
35
35
 
@@ -54,27 +54,23 @@ Scan a specific skill:
54
54
  node skills/guard-scanner/src/cli.js /path/to/new-skill/ --strict --verbose
55
55
  ```
56
56
 
57
- ### 2. Runtime Guard (Real-time Protection)
57
+ ### 2. Runtime Guard (OpenClaw) — ⚠️ warn-only currently
58
58
 
59
- Install the hook to block dangerous tool calls before execution:
59
+ > **Note:** OpenClaw `InternalHookEvent` does not yet expose cancel/veto. Runtime hook detections are warning + audit log until [Issue #18677](https://github.com/openclaw/openclaw/issues/18677) is adopted.
60
60
 
61
61
  ```bash
62
62
  openclaw hooks install skills/guard-scanner/hooks/guard-scanner
63
63
  openclaw hooks enable guard-scanner
64
+ openclaw hooks list
64
65
  ```
65
66
 
66
- Restart the gateway, then verify:
67
- ```bash
68
- openclaw hooks list # Should show 🛡️ guard-scanner as ✓ ready
69
- ```
70
-
71
- ### 3. Full Setup (Recommended)
67
+ ### 3. Recommended order
72
68
 
73
69
  ```bash
74
- # Static scan first
70
+ # Pre-install / pre-update gate first
75
71
  node skills/guard-scanner/src/cli.js ~/.openclaw/workspace/skills/ --verbose --self-exclude --html
76
72
 
77
- # Then enable runtime protection
73
+ # Then keep runtime monitoring enabled
78
74
  openclaw hooks install skills/guard-scanner/hooks/guard-scanner
79
75
  openclaw hooks enable guard-scanner
80
76
  ```
@@ -83,11 +79,13 @@ openclaw hooks enable guard-scanner
83
79
 
84
80
  Set in `openclaw.json` → `hooks.internal.entries.guard-scanner.mode`:
85
81
 
86
- | Mode | Behavior |
87
- |------|----------|
88
- | `monitor` | Log all, never block |
89
- | `enforce` (default) | Block CRITICAL threats |
90
- | `strict` | Block HIGH + CRITICAL |
82
+ | Mode | Intended Behavior | Current Status |
83
+ |------|-------------------|----------------|
84
+ | `monitor` | Log all, never block | ✅ Fully working |
85
+ | `enforce` (default) | Block CRITICAL threats | ⚠️ Warn only (cancel API pending) |
86
+ | `strict` | Block HIGH + CRITICAL | ⚠️ Warn only (cancel API pending) |
87
+
88
+ > **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.
91
89
 
92
90
  ## Threat Categories
93
91
 
@@ -110,6 +108,9 @@ Set in `openclaw.json` → `hooks.internal.entries.guard-scanner.mode`:
110
108
  | 15 | CVE Patterns | Known agent vulnerabilities |
111
109
  | 16 | MCP Security | Tool/schema poisoning, SSRF |
112
110
  | 17 | Identity Hijacking | SOUL.md/IDENTITY.md tampering |
111
+ | 18 | Sandbox Validation | Dangerous binaries, broad file scope, sensitive env |
112
+ | 19 | Code Complexity | Excessive file length, deep nesting, eval density |
113
+ | 20 | Config Impact | openclaw.json writes, exec approval bypass |
113
114
 
114
115
  ## External Endpoints
115
116
 
@@ -140,7 +141,7 @@ an AI agent's SOUL.md personality file, and no existing tool could detect it.
140
141
 
141
142
  - **Open source**: Full source code available at https://github.com/koatora20/guard-scanner
142
143
  - **Zero dependencies**: Nothing to audit, no transitive risks
143
- - **Test suite**: 45 tests across 10 sections, 100% pass rate
144
+ - **Test suite**: 55 tests across 13 sections, 100% pass rate
144
145
  - **Taxonomy**: Based on Snyk ToxicSkills (Feb 2026), OWASP MCP Top 10, and original research
145
146
  - **Complementary to VirusTotal**: Detects prompt injection and LLM-specific attacks
146
147
  that VirusTotal's signature-based scanning cannot catch
@@ -0,0 +1,88 @@
1
+ # OpenClaw Docs PR-Ready Patch (Reference Implementation)
2
+
3
+ Updated: 2026-02-18
4
+ Target: `docs/automation/hooks.md` (new subsection)
5
+
6
+ ## Section Title
7
+ `### Runtime Security Guard (Reference: before_tool_call)`
8
+
9
+ ## Paste-ready content
10
+
11
+ ```md
12
+ ### Runtime Security Guard (Reference: `agent:before_tool_call`)
13
+
14
+ This reference shows a backward-compatible runtime hook pattern for tool-call safety.
15
+
16
+ #### Proposed event fields (backward-compatible)
17
+
18
+ ```ts
19
+ interface InternalHookEvent {
20
+ // existing fields
21
+ cancel?: boolean; // default false
22
+ cancelReason?: string; // user-visible cancellation reason
23
+ policyMode?: "warn" | "balanced" | "strict";
24
+ }
25
+ ```
26
+
27
+ - Existing hooks remain unchanged.
28
+ - If `cancel` fields are not used/supported, behavior stays warn-only.
29
+
30
+ #### Recommended policy semantics
31
+
32
+ - `warn`: never block, only warn/log.
33
+ - `balanced`: block high-confidence dangerous patterns.
34
+ - `strict`: block any policy hit.
35
+
36
+ #### `HOOK.md`
37
+
38
+ ```md
39
+ ---
40
+ name: security-runtime-guard
41
+ description: "Reference runtime guard hook for tool-call safety"
42
+ metadata:
43
+ { "openclaw": { "emoji": "🛡️", "events": ["agent:before_tool_call"] } }
44
+ ---
45
+
46
+ # security-runtime-guard
47
+
48
+ Reference implementation for runtime tool-call policy checks.
49
+ ```
50
+
51
+ #### `handler.ts`
52
+
53
+ ```ts
54
+ import type { HookHandler } from "../../src/hooks/hooks.js";
55
+
56
+ const HIGH_RISK = [/curl\s+.*\|\s*sh/i, /reverse\s*shell/i, /169\.254\.169\.254/];
57
+
58
+ const handler: HookHandler = async (event) => {
59
+ if (event.type !== "agent" || event.action !== "before_tool_call") return;
60
+
61
+ const mode = event.policyMode ?? "warn";
62
+ const text = JSON.stringify(event.context ?? {});
63
+ const hit = HIGH_RISK.find((re) => re.test(text));
64
+ if (!hit) return;
65
+
66
+ event.messages.push(`🛡️ Runtime guard detected risky pattern: ${hit}`);
67
+
68
+ if (mode === "warn") return;
69
+
70
+ event.cancel = true;
71
+ event.cancelReason =
72
+ mode === "strict"
73
+ ? "Blocked by strict runtime policy"
74
+ : "Blocked by balanced runtime policy (high-risk pattern)";
75
+ };
76
+
77
+ export default handler;
78
+ ```
79
+
80
+ #### Operational note
81
+
82
+ If your current OpenClaw runtime is warn-only for tool-call hooks, this reference still works as observability-first policy (`warn` mode). Enforcement activates once cancel/veto is available.
83
+ ```
84
+
85
+ ## Reviewer Notes
86
+ - Keeps behavior backward-compatible.
87
+ - Encourages monitor -> enforce rollout.
88
+ - Aligned with install-time + runtime defense-in-depth guidance.
@@ -0,0 +1,78 @@
1
+ # OpenClaw Hook Schema Reference Draft (Issue #18677)
2
+
3
+ Updated: 2026-02-18
4
+
5
+ ## Goal
6
+ Provide a docs-ready, backward-compatible reference implementation for runtime security hooks.
7
+
8
+ ## Proposed Event Extension
9
+
10
+ ```ts
11
+ interface InternalHookEvent {
12
+ // existing fields
13
+ cancel?: boolean;
14
+ cancelReason?: string;
15
+ policyMode?: "warn" | "balanced" | "strict";
16
+ }
17
+ ```
18
+
19
+ ### Compatibility
20
+ - Existing hooks continue to work without changes.
21
+ - If cancel fields are absent, runtime behavior is unchanged.
22
+
23
+ ## Policy Semantics
24
+ - `warn`: never block, log + user warning only
25
+ - `balanced`: block HIGH/CRITICAL confidence matches
26
+ - `strict`: block any matched policy rule
27
+
28
+ ## Reference `HOOK.md`
29
+
30
+ ```md
31
+ ---
32
+ name: security-runtime-guard
33
+ description: "Reference runtime guard hook for tool-call safety"
34
+ metadata:
35
+ { "openclaw": { "emoji": "🛡️", "events": ["agent:before_tool_call"] } }
36
+ ---
37
+
38
+ # security-runtime-guard
39
+
40
+ Reference implementation for cancel/veto-enabled runtime checks.
41
+ ```
42
+
43
+ ## Reference `handler.ts`
44
+
45
+ ```ts
46
+ import type { HookHandler } from "../../src/hooks/hooks.js";
47
+
48
+ const HIGH_RISK = [/curl\s+.*\|\s*sh/i, /reverse\s*shell/i, /169\.254\.169\.254/];
49
+
50
+ const handler: HookHandler = async (event) => {
51
+ if (event.type !== "agent" || event.action !== "before_tool_call") return;
52
+
53
+ const mode = event.policyMode ?? "warn";
54
+ const text = JSON.stringify(event.context ?? {});
55
+ const hit = HIGH_RISK.find((re) => re.test(text));
56
+ if (!hit) return;
57
+
58
+ event.messages.push(`🛡️ Runtime guard detected risky pattern: ${hit}`);
59
+
60
+ if (mode === "warn") return;
61
+
62
+ // balanced/strict: cancellation path
63
+ event.cancel = true;
64
+ event.cancelReason =
65
+ mode === "strict"
66
+ ? "Blocked by strict runtime policy"
67
+ : "Blocked by balanced runtime policy (high-risk pattern)";
68
+ };
69
+
70
+ export default handler;
71
+ ```
72
+
73
+ ## Notes for Docs
74
+ 1. Document that some releases may still be warn-only until cancel support lands.
75
+ 2. Recommend combining install-time static scan + runtime guard:
76
+ - Install-time: `guard-scanner`
77
+ - Runtime: internal hook guard
78
+ 3. Add troubleshooting section for false positives (context-aware suppression).
@@ -0,0 +1,47 @@
1
+ # guard-scanner Research-First Tasklist (t-wada + 鉄の掟)
2
+
3
+ Updated: 2026-02-18
4
+
5
+ ## 0) Research Baseline (完了)
6
+ - [x] Semgrep rule testing practices(ruleid/okでFN/FPを分離管理)
7
+ - [x] GitHub SARIF要件(重複防止fingerprint・SARIF 2.1.0 subset)
8
+ - [x] OWASP MCP Top 10(context/protocol attack surface)
9
+ - [x] SLSA levels(provenance配布・build track段階化)
10
+
11
+ ## 1) RED — テスト仕様を先に固定
12
+ - [ ] T1: False Positive回帰テストを追加(safe skill 20ケース)
13
+ - [ ] T2: False Negative回帰テストを追加(malicious corpus 20ケース)
14
+ - [x] T3: node_modules/生成レポート由来ノイズの再現テスト
15
+ - [x] T4: SARIF妥当性テスト(schema + GitHub ingestion向け)
16
+ - [ ] T5: 性能テスト(N=10/50/100 skillsで時間とメモリ)
17
+
18
+ ## 2) GREEN — 最小実装
19
+ - [x] G1: デフォルト除外ルール(node_modules / guard-scanner-report.*)
20
+ - [ ] G2: trust-boundaryの文脈制約(docs/コメント誤検知を抑制)
21
+ - [ ] G3: entropy検知の閾値と例外改善(既知データ定数除外)
22
+ - [x] G4: SARIF fingerprint一貫性の強化(`partialFingerprints.primaryLocationLineHash` 生成)
23
+
24
+ ## 3) REFACTOR — 構造改善
25
+ - [ ] R1: 検知ルールを「攻撃シグナル」と「文脈ルール」に分離
26
+ - [ ] R2: ルール単位で precision/recall メトリクス出力
27
+ - [ ] R3: release gate(テスト全通過 + SLSA provenance確認)
28
+
29
+ ## 4) OpenClawコミュニティ最優先(即効価値)
30
+ - [x] C1: #18677 に方針返信(agent連携文脈 + cancel/veto提案)
31
+ - [x] C2: #18677 へ実装可能な最小仕様を追記(`cancel`, `cancelReason`, `policyMode`)
32
+ - [x] C3: OpenClaw公式Docs向けサンプルhook草案を作成(before_install + before_tool_call) ※ before_tool_call版ドラフト作成済み: `docs/OPENCLAW_HOOK_SCHEMA_REFERENCE_DRAFT.md`
33
+ - [x] C4: guard-scannerの「OpenClaw推奨導入手順」短縮版をREADME/SKILLに反映
34
+ - [x] C5: OpenClaw Discord向け技術共有文(宣伝色なし、再現手順中心)を作成 (`output/OPENCLAW_DISCORD_TECHNICAL_SHARE_DRAFT_2026-02-18.md`)
35
+ - [x] C6: OpenClaw docs向けPR-readyパッチ作成 + #18677に提出(`docs/OPENCLAW_DOCS_PR_READY_PATCH.md`)
36
+
37
+ ## 5) Delivery & GTM(コミュニティ同期後)
38
+ - [ ] D1: note無料記事(使い方 + 失敗例 + 回避)
39
+ - [ ] D2: X告知(SEO/LLMOワード最適化2パターンAB)
40
+ - [ ] D3: Moltbook技術投稿(検知改善ログ + フィードバック募集)
41
+
42
+ ## Exit Criteria
43
+ - FP率: 現状比 30%以上削減
44
+ - FN率: 悪性固定コーパスで95%以上検知
45
+ - 56既存テスト + 新規テスト全PASS
46
+ - SARIFをGitHub Code Scanningで取り込み確認
47
+ - OpenClawコミュニティ向け公開仕様(Issue + Docs草案)を1セット完了
@@ -1,26 +1,74 @@
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 (LEGACY)
3
+ *
4
+ * ⚠️ DEPRECATED: Use plugin.ts instead.
5
+ * This Internal Hook version can only WARN, not block.
6
+ * The Plugin Hook version (plugin.ts) uses the native
7
+ * `block` / `blockReason` API to actually prevent execution.
8
+ *
9
+ * Intercepts agent tool calls and checks arguments against
10
+ * runtime threat intelligence patterns. Zero dependencies.
11
+ *
12
+ * Registered for event: agent:before_tool_call
13
+ *
14
+ * Current limitation:
15
+ * The OpenClaw InternalHookEvent interface does not yet expose a
16
+ * `cancel` / `veto` mechanism. This handler can WARN via
17
+ * event.messages but cannot block tool execution.
18
+ * When a cancel API is introduced, this handler will be updated
19
+ * to actually block CRITICAL/HIGH threats.
20
+ *
21
+ * Modes (for future blocking behaviour):
22
+ * monitor — log only (current effective behaviour for all modes)
23
+ * enforce — will block CRITICAL when cancel API is available
24
+ * strict — will block HIGH+CRITICAL when cancel API is available
25
+ *
17
26
  * @author Guava 🍈 & Dee
18
- * @version 1.0.0
27
+ * @version 1.1.0
19
28
  * @license MIT
20
29
  */
21
30
 
31
+ import { appendFileSync, mkdirSync } from "fs";
32
+ import { join } from "path";
33
+ import { homedir } from "os";
34
+
35
+ // ── OpenClaw Hook Types (from openclaw/src/hooks/internal-hooks.ts) ──
36
+ // Inline types to avoid broken relative-path imports.
37
+ // These match the official InternalHookEvent / InternalHookHandler
38
+ // from OpenClaw v2026.2.15.
39
+
40
+ type InternalHookEventType = "command" | "session" | "agent" | "gateway";
41
+
42
+ interface InternalHookEvent {
43
+ /** The type of event */
44
+ type: InternalHookEventType;
45
+ /** The specific action within the type (e.g., "before_tool_call") */
46
+ action: string;
47
+ /** The session key this event relates to */
48
+ sessionKey: string;
49
+ /** Additional context specific to the event */
50
+ context: Record<string, unknown>;
51
+ /** Timestamp when the event occurred */
52
+ timestamp: Date;
53
+ /** Messages to send back to the user (hooks can push to this array) */
54
+ messages: string[];
55
+ }
56
+
57
+ type InternalHookHandler = (event: InternalHookEvent) => Promise<void> | void;
58
+
59
+ // Re-export as the public types for compatibility
60
+ type HookHandler = InternalHookHandler;
61
+ type HookEvent = InternalHookEvent;
62
+
22
63
  // ── Runtime threat patterns (12 checks) ──
23
- const RUNTIME_CHECKS = [
64
+ interface RuntimeCheck {
65
+ id: string;
66
+ severity: "CRITICAL" | "HIGH" | "MEDIUM";
67
+ desc: string;
68
+ test: (s: string) => boolean;
69
+ }
70
+
71
+ const RUNTIME_CHECKS: RuntimeCheck[] = [
24
72
  {
25
73
  id: 'RT_REVSHELL', severity: 'CRITICAL', desc: 'Reverse shell attempt',
26
74
  test: (s: string) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s)
@@ -78,11 +126,11 @@ const RUNTIME_CHECKS = [
78
126
  const AUDIT_DIR = join(homedir(), ".openclaw", "guard-scanner");
79
127
  const AUDIT_FILE = join(AUDIT_DIR, "audit.jsonl");
80
128
 
81
- function ensureAuditDir() {
129
+ function ensureAuditDir(): void {
82
130
  try { mkdirSync(AUDIT_DIR, { recursive: true }); } catch { }
83
131
  }
84
132
 
85
- function logAudit(entry: Record<string, unknown>) {
133
+ function logAudit(entry: Record<string, unknown>): void {
86
134
  ensureAuditDir();
87
135
  const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + '\n';
88
136
  try { appendFileSync(AUDIT_FILE, line); } catch { }
@@ -90,16 +138,21 @@ function logAudit(entry: Record<string, unknown>) {
90
138
 
91
139
  // ── Main Handler ──
92
140
  const handler: HookHandler = async (event) => {
93
- // Only handle before_tool_call
141
+ // Only handle agent:before_tool_call events
94
142
  if (event.type !== "agent" || event.action !== "before_tool_call") return;
95
143
 
96
- const { toolName, toolArgs } = (event as any).context || {};
144
+ const { toolName, toolArgs } = event.context as {
145
+ toolName?: string;
146
+ toolArgs?: Record<string, unknown>;
147
+ };
97
148
  if (!toolName || !toolArgs) return;
98
149
 
99
- // Get mode from config
100
- const mode = (event as any).context?.cfg?.hooks?.internal?.entries?.['guard-scanner']?.mode || 'enforce';
150
+ // Get mode from context config (if available)
151
+ const cfg = event.context.cfg as Record<string, unknown> | undefined;
152
+ const hookEntries = (cfg as any)?.hooks?.internal?.entries?.['guard-scanner'] as Record<string, unknown> | undefined;
153
+ const mode = (hookEntries?.mode as string) || 'enforce';
101
154
 
102
- // Only check dangerous tools
155
+ // Only check tools that can cause damage
103
156
  const dangerousTools = new Set(['exec', 'write', 'edit', 'browser', 'web_fetch', 'message']);
104
157
  if (!dangerousTools.has(toolName)) return;
105
158
 
@@ -113,35 +166,39 @@ const handler: HookHandler = async (event) => {
113
166
  severity: check.severity,
114
167
  desc: check.desc,
115
168
  mode,
116
- action: 'allowed' as string,
117
- session: (event as any).sessionKey,
169
+ action: 'warned' as string,
170
+ session: event.sessionKey,
118
171
  };
119
172
 
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';
173
+ // NOTE: OpenClaw InternalHookEvent does not currently support
174
+ // a cancel/veto mechanism. When it does, uncomment the blocking
175
+ // logic below. For now, all detections are warnings only.
176
+ //
177
+ // if (mode === 'strict' && (check.severity === 'CRITICAL' || check.severity === 'HIGH')) {
178
+ // entry.action = 'blocked';
179
+ // logAudit(entry);
180
+ // event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
181
+ // event.cancel = true; // Not yet in the public API
182
+ // return;
183
+ // }
184
+ //
185
+ // if (mode === 'enforce' && check.severity === 'CRITICAL') {
186
+ // entry.action = 'blocked';
187
+ // logAudit(entry);
188
+ // event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
189
+ // event.cancel = true; // Not yet in the public API
190
+ // return;
191
+ // }
192
+
193
+ // Current behaviour: warn and log for all modes
140
194
  logAudit(entry);
141
195
 
142
196
  if (check.severity === 'CRITICAL') {
143
197
  event.messages.push(`🛡️ guard-scanner WARNING: ${check.desc} [${check.id}]`);
144
198
  console.warn(`[guard-scanner] ⚠️ WARNING: ${check.desc} [${check.id}]`);
199
+ } else if (check.severity === 'HIGH') {
200
+ event.messages.push(`🛡️ guard-scanner NOTICE: ${check.desc} [${check.id}]`);
201
+ console.warn(`[guard-scanner] ℹ️ NOTICE: ${check.desc} [${check.id}]`);
145
202
  }
146
203
  }
147
204
  }
@@ -0,0 +1,261 @@
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
+ * Unlike the legacy Internal Hook handler (handler.ts), this version
8
+ * can ACTUALLY BLOCK tool calls, not just warn.
9
+ *
10
+ * Usage:
11
+ * Copy to ~/.openclaw/plugins/guard-scanner-runtime.ts
12
+ * Or register via openclaw plugin system.
13
+ *
14
+ * Modes:
15
+ * monitor — log only, never block
16
+ * enforce — block CRITICAL threats (default)
17
+ * strict — block HIGH + CRITICAL threats
18
+ *
19
+ * @author Guava 🍈 & Dee
20
+ * @version 2.0.0
21
+ * @license MIT
22
+ */
23
+
24
+ import { appendFileSync, mkdirSync, readFileSync } from "fs";
25
+ import { join } from "path";
26
+ import { homedir } from "os";
27
+
28
+ // ── Types (from OpenClaw src/plugins/types.ts) ──
29
+
30
+ type PluginHookBeforeToolCallEvent = {
31
+ toolName: string;
32
+ params: Record<string, unknown>;
33
+ };
34
+
35
+ type PluginHookBeforeToolCallResult = {
36
+ params?: Record<string, unknown>;
37
+ block?: boolean;
38
+ blockReason?: string;
39
+ };
40
+
41
+ type PluginHookToolContext = {
42
+ agentId?: string;
43
+ sessionKey?: string;
44
+ toolName: string;
45
+ };
46
+
47
+ type PluginAPI = {
48
+ on(
49
+ hookName: "before_tool_call",
50
+ handler: (
51
+ event: PluginHookBeforeToolCallEvent,
52
+ ctx: PluginHookToolContext
53
+ ) => PluginHookBeforeToolCallResult | void | Promise<PluginHookBeforeToolCallResult | void>
54
+ ): void;
55
+ logger: {
56
+ info: (msg: string) => void;
57
+ warn: (msg: string) => void;
58
+ error: (msg: string) => void;
59
+ };
60
+ };
61
+
62
+ // ── Runtime threat patterns (12 checks) ──
63
+
64
+ interface RuntimeCheck {
65
+ id: string;
66
+ severity: "CRITICAL" | "HIGH" | "MEDIUM";
67
+ desc: string;
68
+ test: (s: string) => boolean;
69
+ }
70
+
71
+ const RUNTIME_CHECKS: RuntimeCheck[] = [
72
+ {
73
+ id: "RT_REVSHELL",
74
+ severity: "CRITICAL",
75
+ desc: "Reverse shell attempt",
76
+ test: (s) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s),
77
+ },
78
+ {
79
+ id: "RT_CRED_EXFIL",
80
+ severity: "CRITICAL",
81
+ desc: "Credential exfiltration to external",
82
+ test: (s) =>
83
+ /(webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|socifiapp\.com)/i.test(s) &&
84
+ /(token|key|secret|password|credential|env)/i.test(s),
85
+ },
86
+ {
87
+ id: "RT_GUARDRAIL_OFF",
88
+ severity: "CRITICAL",
89
+ desc: "Guardrail disabling attempt",
90
+ test: (s) => /exec\.approvals?\s*[:=]\s*['"]?(off|false)|tools\.exec\.host\s*[:=]\s*['"]?gateway/i.test(s),
91
+ },
92
+ {
93
+ id: "RT_GATEKEEPER",
94
+ severity: "CRITICAL",
95
+ desc: "macOS Gatekeeper bypass (xattr)",
96
+ test: (s) => /xattr\s+-[crd]\s.*quarantine/i.test(s),
97
+ },
98
+ {
99
+ id: "RT_AMOS",
100
+ severity: "CRITICAL",
101
+ desc: "ClawHavoc AMOS indicator",
102
+ test: (s) => /socifiapp|Atomic\s*Stealer|AMOS/i.test(s),
103
+ },
104
+ {
105
+ id: "RT_MAL_IP",
106
+ severity: "CRITICAL",
107
+ desc: "Known malicious IP",
108
+ test: (s) => /91\.92\.242\.30/i.test(s),
109
+ },
110
+ {
111
+ id: "RT_DNS_EXFIL",
112
+ severity: "HIGH",
113
+ desc: "DNS-based exfiltration",
114
+ test: (s) => /nslookup\s+.*\$|dig\s+.*\$.*@/i.test(s),
115
+ },
116
+ {
117
+ id: "RT_B64_SHELL",
118
+ severity: "CRITICAL",
119
+ desc: "Base64 decode piped to shell",
120
+ test: (s) => /base64\s+(-[dD]|--decode)\s*\|\s*(sh|bash)/i.test(s),
121
+ },
122
+ {
123
+ id: "RT_CURL_BASH",
124
+ severity: "CRITICAL",
125
+ desc: "Download piped to shell",
126
+ test: (s) => /(curl|wget)\s+[^\n]*\|\s*(sh|bash|zsh)/i.test(s),
127
+ },
128
+ {
129
+ id: "RT_SSH_READ",
130
+ severity: "HIGH",
131
+ desc: "SSH private key access",
132
+ test: (s) => /\.ssh\/id_|\.ssh\/authorized_keys/i.test(s),
133
+ },
134
+ {
135
+ id: "RT_WALLET",
136
+ severity: "HIGH",
137
+ desc: "Crypto wallet credential access",
138
+ test: (s) => /wallet.*(?:seed|mnemonic|private.*key)|seed.*phrase/i.test(s),
139
+ },
140
+ {
141
+ id: "RT_CLOUD_META",
142
+ severity: "CRITICAL",
143
+ desc: "Cloud metadata endpoint access",
144
+ test: (s) => /169\.254\.169\.254|metadata\.google|metadata\.aws/i.test(s),
145
+ },
146
+ ];
147
+
148
+ // ── Audit logging ──
149
+
150
+ const AUDIT_DIR = join(homedir(), ".openclaw", "guard-scanner");
151
+ const AUDIT_FILE = join(AUDIT_DIR, "audit.jsonl");
152
+
153
+ function ensureAuditDir(): void {
154
+ try {
155
+ mkdirSync(AUDIT_DIR, { recursive: true });
156
+ } catch {
157
+ /* ignore */
158
+ }
159
+ }
160
+
161
+ function logAudit(entry: Record<string, unknown>): void {
162
+ ensureAuditDir();
163
+ const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + "\n";
164
+ try {
165
+ appendFileSync(AUDIT_FILE, line);
166
+ } catch {
167
+ /* ignore */
168
+ }
169
+ }
170
+
171
+ // ── Config ──
172
+
173
+ type GuardMode = "monitor" | "enforce" | "strict";
174
+
175
+ function loadMode(): GuardMode {
176
+ try {
177
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
178
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
179
+ const mode = config?.plugins?.["guard-scanner"]?.mode;
180
+ if (mode === "monitor" || mode === "enforce" || mode === "strict") {
181
+ return mode;
182
+ }
183
+ } catch {
184
+ /* config not found or invalid — use default */
185
+ }
186
+ return "enforce";
187
+ }
188
+
189
+ function shouldBlock(severity: string, mode: GuardMode): boolean {
190
+ if (mode === "monitor") return false;
191
+ if (mode === "enforce") return severity === "CRITICAL";
192
+ if (mode === "strict") return severity === "CRITICAL" || severity === "HIGH";
193
+ return false;
194
+ }
195
+
196
+ // ── Dangerous tool filter ──
197
+
198
+ const DANGEROUS_TOOLS = new Set([
199
+ "exec",
200
+ "write",
201
+ "edit",
202
+ "browser",
203
+ "web_fetch",
204
+ "message",
205
+ "shell",
206
+ "run_command",
207
+ "multi_edit",
208
+ ]);
209
+
210
+ // ── Plugin entry point ──
211
+
212
+ export default function (api: PluginAPI) {
213
+ const mode = loadMode();
214
+ api.logger.info(`🛡️ guard-scanner runtime guard loaded (mode: ${mode})`);
215
+
216
+ api.on("before_tool_call", (event, ctx) => {
217
+ const { toolName, params } = event;
218
+
219
+ // Only check tools that can cause damage
220
+ if (!DANGEROUS_TOOLS.has(toolName)) return;
221
+
222
+ const serialized = JSON.stringify(params);
223
+
224
+ for (const check of RUNTIME_CHECKS) {
225
+ if (!check.test(serialized)) continue;
226
+
227
+ const auditEntry = {
228
+ tool: toolName,
229
+ check: check.id,
230
+ severity: check.severity,
231
+ desc: check.desc,
232
+ mode,
233
+ action: "warned" as string,
234
+ session: ctx.sessionKey || "unknown",
235
+ agent: ctx.agentId || "unknown",
236
+ };
237
+
238
+ if (shouldBlock(check.severity, mode)) {
239
+ auditEntry.action = "blocked";
240
+ logAudit(auditEntry);
241
+ api.logger.warn(
242
+ `🛡️ BLOCKED ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
243
+ );
244
+
245
+ return {
246
+ block: true,
247
+ blockReason: `🛡️ guard-scanner: ${check.desc} [${check.id}]`,
248
+ };
249
+ }
250
+
251
+ // Monitor mode or severity below threshold — warn only
252
+ logAudit(auditEntry);
253
+ api.logger.warn(
254
+ `🛡️ WARNING ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
255
+ );
256
+ }
257
+
258
+ // No threats detected or all below threshold — allow
259
+ return;
260
+ });
261
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guard-scanner",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
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": {
package/src/scanner.js CHANGED
@@ -24,6 +24,7 @@
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
26
  const os = require('os');
27
+ const crypto = require('crypto');
27
28
 
28
29
  const { PATTERNS } = require('./patterns.js');
29
30
  const { KNOWN_MALICIOUS } = require('./ioc-db.js');
@@ -42,6 +43,7 @@ const CODE_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.py', '.sh', '.b
42
43
  const DOC_EXTENSIONS = new Set(['.md', '.txt', '.rst', '.adoc']);
43
44
  const DATA_EXTENSIONS = new Set(['.json', '.yaml', '.yml', '.toml', '.xml', '.csv']);
44
45
  const BINARY_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.wasm', '.wav', '.mp3', '.mp4', '.webm', '.ogg', '.pdf', '.zip', '.tar', '.gz', '.bz2', '.7z', '.exe', '.dll', '.so', '.dylib']);
46
+ const GENERATED_REPORT_FILES = new Set(['guard-scanner-report.json', 'guard-scanner-report.html', 'guard-scanner.sarif']);
45
47
 
46
48
  // Severity weights for risk scoring
47
49
  const SEVERITY_WEIGHTS = { CRITICAL: 40, HIGH: 15, MEDIUM: 5, LOW: 2 };
@@ -886,6 +888,8 @@ class GuardScanner {
886
888
  if (entry.name === '.git' || entry.name === 'node_modules') continue;
887
889
  results.push(...this.getFiles(fullPath));
888
890
  } else {
891
+ const baseName = entry.name.toLowerCase();
892
+ if (GENERATED_REPORT_FILES.has(baseName)) continue;
889
893
  results.push(fullPath);
890
894
  }
891
895
  }
@@ -971,11 +975,21 @@ class GuardScanner {
971
975
  properties: { tags: ['security', f.cat], 'security-severity': f.severity === 'CRITICAL' ? '9.0' : f.severity === 'HIGH' ? '7.0' : f.severity === 'MEDIUM' ? '4.0' : '1.0' }
972
976
  });
973
977
  }
978
+ const normalizedFile = String(f.file || '')
979
+ .replaceAll('\\', '/')
980
+ .replace(/^\/+/, '');
981
+ const artifactUri = `${skillResult.skill}/${normalizedFile}`;
982
+ const fingerprintSeed = `${f.id}|${artifactUri}|${f.line || 0}|${(f.sample || '').slice(0, 200)}`;
983
+ const lineHash = crypto.createHash('sha256').update(fingerprintSeed).digest('hex').slice(0, 24);
984
+
974
985
  results.push({
975
986
  ruleId: f.id, ruleIndex: ruleIndex[f.id],
976
987
  level: f.severity === 'CRITICAL' ? 'error' : f.severity === 'HIGH' ? 'error' : f.severity === 'MEDIUM' ? 'warning' : 'note',
977
988
  message: { text: `[${skillResult.skill}] ${f.desc}${f.sample ? ` — "${f.sample}"` : ''}` },
978
- locations: [{ physicalLocation: { artifactLocation: { uri: `${skillResult.skill}/${f.file}`, uriBaseId: '%SRCROOT%' }, region: f.line ? { startLine: f.line } : undefined } }]
989
+ partialFingerprints: {
990
+ primaryLocationLineHash: lineHash
991
+ },
992
+ locations: [{ physicalLocation: { artifactLocation: { uri: artifactUri, uriBaseId: '%SRCROOT%' }, region: f.line ? { startLine: f.line } : undefined } }]
979
993
  });
980
994
  }
981
995
  }