guard-scanner 1.1.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -35
- package/SECURITY.md +1 -1
- package/SKILL.md +6 -12
- package/docs/OPENCLAW_DOCS_PR_READY_PATCH.md +88 -0
- package/docs/OPENCLAW_HOOK_SCHEMA_REFERENCE_DRAFT.md +78 -0
- package/docs/TASKLIST_RESEARCH_FIRST_V1.md +47 -0
- package/hooks/guard-scanner/handler.ts +6 -1
- package/hooks/guard-scanner/plugin.ts +303 -0
- package/package.json +2 -2
- package/src/patterns.js +22 -0
- package/src/scanner.js +23 -3
package/README.md
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
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,
|
|
6
|
-
<sub
|
|
5
|
+
Detect prompt injection, credential theft, exfiltration, PII exposure, Shadow AI, and 17 more threat categories.<br>
|
|
6
|
+
<sub>🆕 v2.1 — PII Exposure Detection + Shadow AI + Plugin Hook blocking via <code>block</code>/<code>blockReason</code> API</sub>
|
|
7
7
|
</p>
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
|
|
10
10
|
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen" alt="Node.js 18+">
|
|
11
11
|
<img src="https://img.shields.io/badge/dependencies-0-success" alt="Zero Dependencies">
|
|
12
|
-
<img src="https://img.shields.io/badge/tests-
|
|
13
|
-
<img src="https://img.shields.io/badge/patterns-
|
|
14
|
-
<img src="https://img.shields.io/badge/categories-
|
|
12
|
+
<img src="https://img.shields.io/badge/tests-99%2F99-brightgreen" alt="Tests Passing">
|
|
13
|
+
<img src="https://img.shields.io/badge/patterns-129-orange" alt="129 Patterns">
|
|
14
|
+
<img src="https://img.shields.io/badge/categories-21-blueviolet" alt="21 Categories">
|
|
15
15
|
</p>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
@@ -40,8 +40,8 @@ The AI agent skill ecosystem has the same supply-chain security problem that npm
|
|
|
40
40
|
|
|
41
41
|
| Feature | Description |
|
|
42
42
|
|---|---|
|
|
43
|
-
| **
|
|
44
|
-
| **
|
|
43
|
+
| **21 Threat Categories** | Snyk ToxicSkills + OWASP MCP Top 10 + Identity Hijacking + Sandbox/Complexity/Config + PII |
|
|
44
|
+
| **129 Detection Patterns** | Regex-based static analysis covering code, docs, and data files |
|
|
45
45
|
| **IoC Database** | Known malicious IPs, domains, URLs, usernames, and typosquat names |
|
|
46
46
|
| **Data Flow Analysis** | Lightweight JS analysis: secret reads → network calls → exec chains |
|
|
47
47
|
| **Cross-File Analysis** | Phantom references, base64 fragment assembly, multi-file exfil detection |
|
|
@@ -74,6 +74,18 @@ npx guard-scanner ./skills/ --strict
|
|
|
74
74
|
npx guard-scanner ./skills/ --verbose --check-deps --json --sarif --html
|
|
75
75
|
```
|
|
76
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.1** — PII Exposure Detection (OWASP LLM02/06) + Shadow AI detection + Plugin Hook `block`/`blockReason` API. 3 modes: `monitor`, `enforce`, `strict`.
|
|
88
|
+
|
|
77
89
|
### Installation (Optional)
|
|
78
90
|
|
|
79
91
|
```bash
|
|
@@ -87,17 +99,17 @@ npx guard-scanner ./skills/
|
|
|
87
99
|
### As an OpenClaw Skill
|
|
88
100
|
|
|
89
101
|
```bash
|
|
90
|
-
|
|
102
|
+
clawhub install guard-scanner
|
|
91
103
|
guard-scanner ~/.openclaw/workspace/skills/ --self-exclude --verbose
|
|
92
104
|
```
|
|
93
105
|
|
|
94
|
-
>
|
|
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.
|
|
95
107
|
|
|
96
108
|
---
|
|
97
109
|
|
|
98
110
|
## Threat Categories
|
|
99
111
|
|
|
100
|
-
guard-scanner covers **
|
|
112
|
+
guard-scanner covers **21 threat categories** derived from four sources:
|
|
101
113
|
|
|
102
114
|
| # | Category | Based On | Severity | What It Detects |
|
|
103
115
|
|---|----------|----------|----------|----------------|
|
|
@@ -121,8 +133,9 @@ guard-scanner covers **20 threat categories** derived from three taxonomies:
|
|
|
121
133
|
| 18 | **Sandbox Validation** | v1.1 | HIGH | Dangerous binary requirements in SKILL.md, overly broad file scope, sensitive env vars, exec/network declarations |
|
|
122
134
|
| 19 | **Code Complexity** | v1.1 | MEDIUM | Excessive file length (>1000 lines), deep nesting (>5 levels), high eval/exec density |
|
|
123
135
|
| 20 | **Config Impact** | v1.1 | CRITICAL | `openclaw.json` writes, exec approval bypass, exec host gateway, internal hooks modification, network wildcard |
|
|
136
|
+
| 21 | **PII Exposure** | v2.1 | CRITICAL | Hardcoded CC/SSN/phone/email (context-aware), PII logging/network send/plaintext store, Shadow AI (OpenAI/Anthropic/generic LLM), PII collection instructions (address/DOB/government ID) |
|
|
124
137
|
|
|
125
|
-
> **Categories 17–
|
|
138
|
+
> **Categories 17–21** are unique to guard-scanner. Category 17 (Identity Hijacking) was developed from a real attack. Categories 18–20 added in v1.1.0. Category 21 (PII Exposure) added in v2.1.0 covering OWASP LLM02/LLM06.
|
|
126
139
|
|
|
127
140
|
---
|
|
128
141
|
|
|
@@ -131,7 +144,7 @@ guard-scanner covers **20 threat categories** derived from three taxonomies:
|
|
|
131
144
|
### Terminal (Default)
|
|
132
145
|
|
|
133
146
|
```
|
|
134
|
-
🛡️ guard-scanner
|
|
147
|
+
🛡️ guard-scanner v2.1.0
|
|
135
148
|
══════════════════════════════════════════════════════
|
|
136
149
|
📂 Scanning: ./skills/
|
|
137
150
|
📦 Skills found: 22
|
|
@@ -216,6 +229,9 @@ Certain combinations multiply the base score:
|
|
|
216
229
|
| Config impact | **×2** | OpenClaw configuration tampering |
|
|
217
230
|
| Config impact + Sandbox violation | **min 70** | Combined config + capability abuse |
|
|
218
231
|
| Complexity + Malicious code/Obfuscation | **×1.5** | Complex code hiding threats |
|
|
232
|
+
| PII exposure + Exfiltration | **×3** | PII being sent to external servers |
|
|
233
|
+
| PII exposure + Shadow AI | **×2.5** | PII leak through unauthorized LLM |
|
|
234
|
+
| PII exposure + Credential handling | **×2** | Combined PII + credential risk |
|
|
219
235
|
| Known IoC (IP/URL/typosquat) | **= 100** | Confirmed malicious |
|
|
220
236
|
|
|
221
237
|
### Verdict Thresholds
|
|
@@ -388,16 +404,19 @@ Options:
|
|
|
388
404
|
```
|
|
389
405
|
guard-scanner/
|
|
390
406
|
├── src/
|
|
391
|
-
│ ├── scanner.js # GuardScanner class — core scan engine (
|
|
392
|
-
│ ├── patterns.js #
|
|
407
|
+
│ ├── scanner.js # GuardScanner class — core scan engine (21 checks)
|
|
408
|
+
│ ├── patterns.js # 129 threat detection patterns (Cat 1–21)
|
|
393
409
|
│ ├── ioc-db.js # Indicators of Compromise database
|
|
394
410
|
│ └── cli.js # CLI entry point and argument parser
|
|
395
411
|
├── hooks/
|
|
396
412
|
│ └── guard-scanner/
|
|
397
|
-
│
|
|
413
|
+
│ ├── plugin.ts # 🆕 Plugin Hook v2.0 — actual blocking via block/blockReason
|
|
414
|
+
│ ├── handler.ts # Legacy Internal Hook — warn only (deprecated)
|
|
415
|
+
│ └── HOOK.md # Internal Hook manifest (legacy)
|
|
398
416
|
├── test/
|
|
399
|
-
│ ├── scanner.test.js #
|
|
400
|
-
│
|
|
417
|
+
│ ├── scanner.test.js # 64 tests — static scanner (incl. PII v2.1)
|
|
418
|
+
│ ├── plugin.test.js # 35 tests — Plugin Hook runtime guard
|
|
419
|
+
│ └── fixtures/ # Malicious, clean, complex, config-changer, pii-leaky samples
|
|
401
420
|
├── package.json # Zero dependencies, node --test
|
|
402
421
|
├── CHANGELOG.md
|
|
403
422
|
├── LICENSE # MIT
|
|
@@ -521,11 +540,11 @@ console.log(scanner.toHTML()); // HTML string
|
|
|
521
540
|
## Test Results
|
|
522
541
|
|
|
523
542
|
```
|
|
524
|
-
ℹ tests
|
|
525
|
-
ℹ suites
|
|
526
|
-
ℹ pass
|
|
543
|
+
ℹ tests 99
|
|
544
|
+
ℹ suites 16
|
|
545
|
+
ℹ pass 99
|
|
527
546
|
ℹ fail 0
|
|
528
|
-
ℹ duration_ms
|
|
547
|
+
ℹ duration_ms 142ms
|
|
529
548
|
```
|
|
530
549
|
|
|
531
550
|
| Suite | Tests | Coverage |
|
|
@@ -535,14 +554,34 @@ console.log(scanner.toHTML()); // HTML string
|
|
|
535
554
|
| Risk Score Calculation | 5 | Empty, single, combo amplifiers, IoC override |
|
|
536
555
|
| Verdict Determination | 5 | All verdicts + strict mode |
|
|
537
556
|
| Output Formats | 4 | JSON + SARIF 2.1.0 + HTML structure |
|
|
538
|
-
| Pattern Database | 4 |
|
|
557
|
+
| Pattern Database | 4 | 125+ count, required fields, category coverage, regex safety |
|
|
539
558
|
| IoC Database | 5 | Structure, ClawHavoc C2, webhook.site |
|
|
540
559
|
| Shannon Entropy | 2 | Low entropy, high entropy |
|
|
541
560
|
| Ignore Functionality | 1 | Pattern exclusion |
|
|
542
561
|
| Plugin API | 1 | Plugin loading + custom rule injection |
|
|
543
|
-
|
|
|
544
|
-
|
|
|
545
|
-
|
|
|
562
|
+
| Manifest Validation | 4 | Dangerous bins, broad files, sensitive env, clean negatives |
|
|
563
|
+
| Complexity Metrics | 2 | Deep nesting, clean negatives |
|
|
564
|
+
| Config Impact | 4 | openclaw.json write, exec approval, gateway host, clean negatives |
|
|
565
|
+
| **🆕 PII Exposure Detection** | **8** | **Hardcoded CC/SSN, PII logging, network send, Shadow AI, doc collection, risk amp, clean negatives** |
|
|
566
|
+
| **Plugin Hook Runtime Guard** | **35** | **Blocking in enforce/strict, passthrough in monitor, all 12 threat patterns, blockReason format** |
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Fills OpenClaw's Own Security Gaps
|
|
571
|
+
|
|
572
|
+
OpenClaw's official [`THREAT-MODEL-ATLAS.md`](https://github.com/openclaw/openclaw/blob/main/docs/security/THREAT-MODEL-ATLAS.md) identifies security gaps that guard-scanner directly addresses:
|
|
573
|
+
|
|
574
|
+
| Gap (from ATLAS / Source Code) | OpenClaw Status | guard-scanner |
|
|
575
|
+
|---|---|---|
|
|
576
|
+
| _"Simple regex easily bypassed"_ — ClawHub moderation | ⚠️ Basic `FLAG_RULES` | ✅ 129 patterns, 21 categories |
|
|
577
|
+
| _"Does not analyze actual skill code content"_ | ❌ Not implemented | ✅ Full code + doc + data flow analysis |
|
|
578
|
+
| No SOUL.md / IDENTITY.md integrity verification | ❌ Not implemented | ✅ Identity hijacking detection (Cat 17) |
|
|
579
|
+
| `skill:before_install` hook | ❌ Not implemented | 🔜 Proposed ([Issue #18677](https://github.com/openclaw/openclaw/issues/18677)) |
|
|
580
|
+
| `before_tool_call` blocking reference impl | ❌ No official plugin | ✅ First reference implementation (plugin.ts) |
|
|
581
|
+
| SARIF / CI integration for skill security | ❌ Not available | ✅ SARIF 2.1.0 + GitHub Actions |
|
|
582
|
+
| Behavioral analysis beyond VirusTotal | ⏳ In progress | ✅ LLM-specific threat patterns (prompt injection, memory poisoning, MCP attacks) |
|
|
583
|
+
|
|
584
|
+
> guard-scanner is **complementary** to OpenClaw's built-in security — not a replacement. OpenClaw handles infrastructure security (SSRF blocking, exec approvals, sandbox, auth). guard-scanner handles **AI-specific threats** that traditional scanning misses.
|
|
546
585
|
|
|
547
586
|
---
|
|
548
587
|
|
|
@@ -556,13 +595,36 @@ console.log(scanner.toHTML()); // HTML string
|
|
|
556
595
|
|
|
557
596
|
---
|
|
558
597
|
|
|
598
|
+
## OWASP Gen AI Top 10 Coverage
|
|
599
|
+
|
|
600
|
+
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/):
|
|
601
|
+
|
|
602
|
+
| # | Risk | Status | Detection Method |
|
|
603
|
+
|---|------|--------|------------------|
|
|
604
|
+
| LLM01 | Prompt Injection | ⚠️ Partial | Regex: Unicode exploits, role override, system tags, base64 instructions |
|
|
605
|
+
| LLM02 | Sensitive Information Disclosure | ⚠️ Partial | PII Exposure Detection (v2.1): hardcoded PII, PII logging/network/storage, Shadow AI, PII collection instructions |
|
|
606
|
+
| LLM03 | Training Data Poisoning | ⬜ N/A | Out of scope for static analysis |
|
|
607
|
+
| LLM04 | Model Denial of Service | 🔜 v2.2 | Planned: excessive input / infinite loop patterns |
|
|
608
|
+
| LLM05 | Supply Chain Vulnerabilities | ⚠️ Partial | IoC database, typosquat detection, dependency chain scan |
|
|
609
|
+
| LLM06 | Insecure Output Handling | ⚠️ Partial | PII output detection (console.log, network send, plaintext store) |
|
|
610
|
+
| LLM07 | Insecure Plugin Design | 🔜 v1.3 | Planned: unvalidated plugin input patterns |
|
|
611
|
+
| LLM08 | Excessive Agency | 🔜 v1.3 | Planned: over-permissioned scope detection |
|
|
612
|
+
| LLM09 | Overreliance | 🔜 v1.3 | Planned: unverified output trust patterns |
|
|
613
|
+
| LLM10 | Model Theft | 🔜 v1.3 | Planned: model file exfiltration patterns |
|
|
614
|
+
|
|
615
|
+
> **Current coverage: 5/10 (partial).** LLM02 and LLM06 added in v2.1.0. Full coverage targeted for v3.0. See [ROADMAP.md](ROADMAP.md) for details.
|
|
616
|
+
>
|
|
617
|
+
> **Known limitation:** Regex-based detection can be evaded by AI-generated code obfuscation. v3.0 will introduce AST analysis and ML-based detection to address this structural gap.
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
559
621
|
## Contributing
|
|
560
622
|
|
|
561
623
|
1. Fork the repository
|
|
562
624
|
2. Create a feature branch (`git checkout -b feature/new-pattern`)
|
|
563
625
|
3. Add your pattern to `src/patterns.js` with the required fields
|
|
564
626
|
4. Add a test case in `test/fixtures/` and `test/scanner.test.js`
|
|
565
|
-
5. Run `npm test` — all
|
|
627
|
+
5. Run `npm test` — all 99+ tests must pass
|
|
566
628
|
6. Submit a Pull Request
|
|
567
629
|
|
|
568
630
|
### Adding a New Detection Pattern
|
|
@@ -601,19 +663,48 @@ We built one.
|
|
|
601
663
|
|
|
602
664
|
## 🔒 Need More? — GuavaSuite
|
|
603
665
|
|
|
604
|
-
guard-scanner catches threats **before** installation
|
|
666
|
+
guard-scanner catches threats **before** installation and **blocks** CRITICAL threats at runtime. **GuavaSuite** unlocks **strict mode** — blocking HIGH + CRITICAL threats, plus exclusive defense-in-depth features.
|
|
605
667
|
|
|
606
|
-
|
|
668
|
+
### How to Upgrade
|
|
607
669
|
|
|
608
|
-
|
|
670
|
+
```bash
|
|
671
|
+
# 1. Install GuavaSuite
|
|
672
|
+
clawhub install guava-suite
|
|
673
|
+
|
|
674
|
+
# 2. Hold 1M+ $GUAVA on Polygon
|
|
675
|
+
# Token: 0x25cBD481901990bF0ed2ff9c5F3C0d4f743AC7B8
|
|
676
|
+
# Buy on QuickSwap V2: https://quickswap.exchange
|
|
677
|
+
|
|
678
|
+
# 3. Activate with your wallet → get JWT → strict mode enabled
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Feature Comparison
|
|
682
|
+
|
|
683
|
+
| | guard-scanner (Free) | GuavaSuite ($GUAVA) |
|
|
609
684
|
|---|---|---|
|
|
610
|
-
| Static scan
|
|
611
|
-
| Runtime
|
|
612
|
-
|
|
|
613
|
-
|
|
|
614
|
-
|
|
|
685
|
+
| Static scan (129 patterns, 21 categories) | ✅ | ✅ |
|
|
686
|
+
| Runtime Guard — `enforce` (block CRITICAL) | ✅ | ✅ |
|
|
687
|
+
| **Runtime Guard — `strict` (block HIGH + CRITICAL)** | ❌ | ✅ |
|
|
688
|
+
| **Soul Lock** (SOUL.md integrity + auto-rollback) | ❌ | ✅ |
|
|
689
|
+
| **Memory Guard** (L1-L5 記憶保護) | ❌ | ✅ |
|
|
690
|
+
| **On-chain Identity** (SoulRegistry V2 on Polygon) | ❌ | ✅ |
|
|
691
|
+
| Audit Log (JSONL) | ✅ | ✅ |
|
|
692
|
+
|
|
693
|
+
guard-scanner is and always will be **free, open-source, and zero-dependency**.
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## Roadmap
|
|
698
|
+
|
|
699
|
+
| Version | Focus | Key Features |
|
|
700
|
+
|---------|-------|------|
|
|
701
|
+
| v1.1.1 ✅ | Stability | 56 tests, bug fixes |
|
|
702
|
+
| v2.0.0 ✅ | **Plugin Hook Runtime Guard** | `block`/`blockReason` API, 3 modes (monitor/enforce/strict), 91 tests |
|
|
703
|
+
| v2.1.0 ✅ | **PII Exposure + Shadow AI** | 13 PII patterns, OWASP LLM02/06, Shadow AI detection, 3 risk amplifiers, 99 tests |
|
|
704
|
+
| v2.2 | OWASP Full Coverage | LLM04/07/08/09/10, YAML pattern definitions, CONTRIBUTING guide |
|
|
705
|
+
| v3.0 | AST + ML | JavaScript AST analysis, taint tracking, ML-based obfuscation detection, SBOM generation |
|
|
615
706
|
|
|
616
|
-
|
|
707
|
+
See [ROADMAP.md](ROADMAP.md) for full details.
|
|
617
708
|
|
|
618
709
|
---
|
|
619
710
|
|
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:
|
|
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
|
@@ -54,29 +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 (
|
|
57
|
+
### 2. Runtime Guard (OpenClaw) — ⚠️ warn-only currently
|
|
58
58
|
|
|
59
|
-
> **Note:**
|
|
60
|
-
|
|
61
|
-
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.
|
|
62
60
|
|
|
63
61
|
```bash
|
|
64
62
|
openclaw hooks install skills/guard-scanner/hooks/guard-scanner
|
|
65
63
|
openclaw hooks enable guard-scanner
|
|
64
|
+
openclaw hooks list
|
|
66
65
|
```
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
openclaw hooks list # Should show 🛡️ guard-scanner as ✓ ready
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 3. Full Setup (Recommended)
|
|
67
|
+
### 3. Recommended order
|
|
74
68
|
|
|
75
69
|
```bash
|
|
76
|
-
#
|
|
70
|
+
# Pre-install / pre-update gate first
|
|
77
71
|
node skills/guard-scanner/src/cli.js ~/.openclaw/workspace/skills/ --verbose --self-exclude --html
|
|
78
72
|
|
|
79
|
-
# Then
|
|
73
|
+
# Then keep runtime monitoring enabled
|
|
80
74
|
openclaw hooks install skills/guard-scanner/hooks/guard-scanner
|
|
81
75
|
openclaw hooks enable guard-scanner
|
|
82
76
|
```
|
|
@@ -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,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* guard-scanner Runtime Guard — Hook Handler
|
|
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.
|
|
3
8
|
*
|
|
4
9
|
* Intercepts agent tool calls and checks arguments against
|
|
5
10
|
* runtime threat intelligence patterns. Zero dependencies.
|
|
@@ -0,0 +1,303 @@
|
|
|
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
|
+
const SUITE_TOKEN_FILE = join(homedir(), ".openclaw", "guava-suite", "token.jwt");
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if GuavaSuite JWT exists and hasn't expired.
|
|
179
|
+
* Why: Lightweight check without jsonwebtoken dependency — just decode base64 payload.
|
|
180
|
+
* Full JWT signature verification happens at activation time in activate.js.
|
|
181
|
+
*/
|
|
182
|
+
function isSuiteActive(): boolean {
|
|
183
|
+
try {
|
|
184
|
+
const token = readFileSync(SUITE_TOKEN_FILE, "utf8").trim();
|
|
185
|
+
if (!token) return false;
|
|
186
|
+
|
|
187
|
+
// Decode JWT payload (base64url → JSON)
|
|
188
|
+
const parts = token.split(".");
|
|
189
|
+
if (parts.length !== 3) return false;
|
|
190
|
+
|
|
191
|
+
const payload = JSON.parse(
|
|
192
|
+
Buffer.from(parts[1], "base64url").toString("utf8")
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Check expiry
|
|
196
|
+
if (payload.exp && payload.exp * 1000 < Date.now()) return false;
|
|
197
|
+
|
|
198
|
+
// Check scope
|
|
199
|
+
return payload.scope === "suite";
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function loadMode(): GuardMode {
|
|
206
|
+
// Priority 1: GuavaSuite JWT token → strict
|
|
207
|
+
if (isSuiteActive()) {
|
|
208
|
+
return "strict";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Priority 2: explicit config in openclaw.json
|
|
212
|
+
try {
|
|
213
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
214
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
215
|
+
|
|
216
|
+
// Check suiteEnabled flag (set by activate.js)
|
|
217
|
+
if (config?.plugins?.["guard-scanner"]?.suiteEnabled === true) {
|
|
218
|
+
return "strict";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const mode = config?.plugins?.["guard-scanner"]?.mode;
|
|
222
|
+
if (mode === "monitor" || mode === "enforce" || mode === "strict") {
|
|
223
|
+
return mode;
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
/* config not found or invalid — use default */
|
|
227
|
+
}
|
|
228
|
+
return "enforce";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function shouldBlock(severity: string, mode: GuardMode): boolean {
|
|
232
|
+
if (mode === "monitor") return false;
|
|
233
|
+
if (mode === "enforce") return severity === "CRITICAL";
|
|
234
|
+
if (mode === "strict") return severity === "CRITICAL" || severity === "HIGH";
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Dangerous tool filter ──
|
|
239
|
+
|
|
240
|
+
const DANGEROUS_TOOLS = new Set([
|
|
241
|
+
"exec",
|
|
242
|
+
"write",
|
|
243
|
+
"edit",
|
|
244
|
+
"browser",
|
|
245
|
+
"web_fetch",
|
|
246
|
+
"message",
|
|
247
|
+
"shell",
|
|
248
|
+
"run_command",
|
|
249
|
+
"multi_edit",
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
// ── Plugin entry point ──
|
|
253
|
+
|
|
254
|
+
export default function (api: PluginAPI) {
|
|
255
|
+
const mode = loadMode();
|
|
256
|
+
api.logger.info(`🛡️ guard-scanner runtime guard loaded (mode: ${mode})`);
|
|
257
|
+
|
|
258
|
+
api.on("before_tool_call", (event, ctx) => {
|
|
259
|
+
const { toolName, params } = event;
|
|
260
|
+
|
|
261
|
+
// Only check tools that can cause damage
|
|
262
|
+
if (!DANGEROUS_TOOLS.has(toolName)) return;
|
|
263
|
+
|
|
264
|
+
const serialized = JSON.stringify(params);
|
|
265
|
+
|
|
266
|
+
for (const check of RUNTIME_CHECKS) {
|
|
267
|
+
if (!check.test(serialized)) continue;
|
|
268
|
+
|
|
269
|
+
const auditEntry = {
|
|
270
|
+
tool: toolName,
|
|
271
|
+
check: check.id,
|
|
272
|
+
severity: check.severity,
|
|
273
|
+
desc: check.desc,
|
|
274
|
+
mode,
|
|
275
|
+
action: "warned" as string,
|
|
276
|
+
session: ctx.sessionKey || "unknown",
|
|
277
|
+
agent: ctx.agentId || "unknown",
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
if (shouldBlock(check.severity, mode)) {
|
|
281
|
+
auditEntry.action = "blocked";
|
|
282
|
+
logAudit(auditEntry);
|
|
283
|
+
api.logger.warn(
|
|
284
|
+
`🛡️ BLOCKED ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
block: true,
|
|
289
|
+
blockReason: `🛡️ guard-scanner: ${check.desc} [${check.id}]`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Monitor mode or severity below threshold — warn only
|
|
294
|
+
logAudit(auditEntry);
|
|
295
|
+
api.logger.warn(
|
|
296
|
+
`🛡️ WARNING ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// No threats detected or all below threshold — allow
|
|
301
|
+
return;
|
|
302
|
+
});
|
|
303
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guard-scanner",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Agent skill security scanner — detect prompt injection, malicious code, credential leaks, and
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Agent skill security scanner — detect prompt injection, malicious code, credential leaks, PII exposure, Shadow AI, and 21 threat categories in AI agent skills",
|
|
5
5
|
"main": "src/scanner.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"guard-scanner": "src/cli.js"
|
package/src/patterns.js
CHANGED
|
@@ -185,6 +185,28 @@ const PATTERNS = [
|
|
|
185
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
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
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 },
|
|
188
|
+
|
|
189
|
+
// ── Category 21: PII Exposure (OWASP LLM02 / LLM06) ──
|
|
190
|
+
// A. Hardcoded PII — actual PII values in code/config (context-aware to reduce FP)
|
|
191
|
+
{ id: 'PII_HARDCODED_CC', cat: 'pii-exposure', regex: /(?:card|cc|credit|payment|pan)[_\s.-]*(?:num|number|no)?\s*[:=]\s*['"`]\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{3,4}['"`]/gi, severity: 'CRITICAL', desc: 'Hardcoded credit card number', codeOnly: true },
|
|
192
|
+
{ id: 'PII_HARDCODED_SSN', cat: 'pii-exposure', regex: /(?:ssn|social[_\s-]*security|tax[_\s-]*id)\s*[:=]\s*['"`]\d{3}-?\d{2}-?\d{4}['"`]/gi, severity: 'CRITICAL', desc: 'Hardcoded SSN/tax ID', codeOnly: true },
|
|
193
|
+
{ id: 'PII_HARDCODED_PHONE', cat: 'pii-exposure', regex: /(?:phone|tel|mobile|cell|fax)[_\s.-]*(?:num|number|no)?\s*[:=]\s*['"`][+]?[\d\s().-]{7,20}['"`]/gi, severity: 'HIGH', desc: 'Hardcoded phone number', codeOnly: true },
|
|
194
|
+
{ id: 'PII_HARDCODED_EMAIL', cat: 'pii-exposure', regex: /(?:email|e-mail|user[_\s-]*mail|contact)\s*[:=]\s*['"`][a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}['"`]/gi, severity: 'HIGH', desc: 'Hardcoded email address', codeOnly: true },
|
|
195
|
+
|
|
196
|
+
// B. PII output/logging — code that outputs or transmits PII-like variables
|
|
197
|
+
{ id: 'PII_LOG_SENSITIVE', cat: 'pii-exposure', regex: /(?:console\.log|console\.info|console\.warn|logger?\.\w+|print|puts)\s*\([^)]*\b(?:ssn|social_security|credit_card|card_number|cvv|cvc|passport|tax_id|date_of_birth|dob)\b/gi, severity: 'HIGH', desc: 'PII variable logged to console', codeOnly: true },
|
|
198
|
+
{ id: 'PII_SEND_NETWORK', cat: 'pii-exposure', regex: /(?:fetch|axios|request|http|post|put|send)\s*\([^)]*\b(?:ssn|social_security|credit_card|card_number|cvv|passport|bank_account|routing_number)\b/gi, severity: 'CRITICAL', desc: 'PII variable sent over network', codeOnly: true },
|
|
199
|
+
{ id: 'PII_STORE_PLAINTEXT', cat: 'pii-exposure', regex: /(?:writeFile|writeFileSync|appendFile|fs\.write|fwrite)\s*\([^)]*\b(?:ssn|social_security|credit_card|card_number|cvv|passport|tax_id|bank_account)\b/gi, severity: 'HIGH', desc: 'PII stored in plaintext file', codeOnly: true },
|
|
200
|
+
|
|
201
|
+
// C. Shadow AI — unauthorized LLM API calls (data leaks to external AI)
|
|
202
|
+
{ id: 'SHADOW_AI_OPENAI', cat: 'pii-exposure', regex: /(?:api\.openai\.com|https:\/\/api\.openai\.com)\s*|openai\.(?:chat|completions|ChatCompletion)/gi, severity: 'HIGH', desc: 'Shadow AI: OpenAI API call', codeOnly: true },
|
|
203
|
+
{ id: 'SHADOW_AI_ANTHROPIC', cat: 'pii-exposure', regex: /(?:api\.anthropic\.com|https:\/\/api\.anthropic\.com)\s*|anthropic\.(?:messages|completions)/gi, severity: 'HIGH', desc: 'Shadow AI: Anthropic API call', codeOnly: true },
|
|
204
|
+
{ id: 'SHADOW_AI_GENERIC', cat: 'pii-exposure', regex: /\/v1\/(?:chat\/completions|completions|embeddings|models)\b.*(?:fetch|axios|request|http)|(?:fetch|axios|request|http)\s*\([^)]*\/v1\/(?:chat\/completions|completions|embeddings)/gi, severity: 'MEDIUM', desc: 'Shadow AI: generic LLM API endpoint', codeOnly: true },
|
|
205
|
+
|
|
206
|
+
// D. PII collection instructions in docs (extends LEAK_COLLECT_PII)
|
|
207
|
+
{ id: 'PII_ASK_ADDRESS', cat: 'pii-exposure', regex: /(?:collect|ask\s+for|request|get|require)\s+(?:the\s+)?(?:user'?s?\s+)?(?:home\s+)?(?:address|street|zip\s*code|postal\s*code|residence)/gi, severity: 'HIGH', desc: 'PII collection: home address', docOnly: true },
|
|
208
|
+
{ id: 'PII_ASK_DOB', cat: 'pii-exposure', regex: /(?:collect|ask\s+for|request|get|require)\s+(?:the\s+)?(?:user'?s?\s+)?(?:date\s+of\s+birth|birth\s*date|birthday|DOB|age)/gi, severity: 'HIGH', desc: 'PII collection: date of birth', docOnly: true },
|
|
209
|
+
{ id: 'PII_ASK_GOV_ID', cat: 'pii-exposure', regex: /(?:collect|ask\s+for|request|get|require)\s+(?:the\s+)?(?:user'?s?\s+)?(?:passport|driver'?s?\s+licen[sc]e|national\s+id|my\s*number|マイナンバー|国民健康保険|social\s+insurance)/gi, severity: 'CRITICAL', desc: 'PII collection: government ID', docOnly: true },
|
|
188
210
|
];
|
|
189
211
|
|
|
190
212
|
module.exports = { PATTERNS };
|
package/src/scanner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* guard-scanner
|
|
3
|
+
* guard-scanner v2.1.0 — Agent Skill Security Scanner 🛡️
|
|
4
4
|
*
|
|
5
5
|
* @security-manifest
|
|
6
6
|
* env-read: []
|
|
@@ -24,13 +24,14 @@
|
|
|
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');
|
|
30
31
|
const { generateHTML } = require('./html-template.js');
|
|
31
32
|
|
|
32
33
|
// ===== CONFIGURATION =====
|
|
33
|
-
const VERSION = '
|
|
34
|
+
const VERSION = '2.1.0';
|
|
34
35
|
|
|
35
36
|
const THRESHOLDS = {
|
|
36
37
|
normal: { suspicious: 30, malicious: 80 },
|
|
@@ -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 };
|
|
@@ -866,6 +868,11 @@ class GuardScanner {
|
|
|
866
868
|
if (cats.has('config-impact') && cats.has('sandbox-validation')) score = Math.max(score, 70);
|
|
867
869
|
if (cats.has('complexity') && (cats.has('malicious-code') || cats.has('obfuscation'))) score = Math.round(score * 1.5);
|
|
868
870
|
|
|
871
|
+
// v2.1 PII exposure amplifiers
|
|
872
|
+
if (cats.has('pii-exposure') && cats.has('exfiltration')) score = Math.round(score * 3);
|
|
873
|
+
if (cats.has('pii-exposure') && (ids.has('SHADOW_AI_OPENAI') || ids.has('SHADOW_AI_ANTHROPIC') || ids.has('SHADOW_AI_GENERIC'))) score = Math.round(score * 2.5);
|
|
874
|
+
if (cats.has('pii-exposure') && cats.has('credential-handling')) score = Math.round(score * 2);
|
|
875
|
+
|
|
869
876
|
return Math.min(100, score);
|
|
870
877
|
}
|
|
871
878
|
|
|
@@ -886,6 +893,8 @@ class GuardScanner {
|
|
|
886
893
|
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
887
894
|
results.push(...this.getFiles(fullPath));
|
|
888
895
|
} else {
|
|
896
|
+
const baseName = entry.name.toLowerCase();
|
|
897
|
+
if (GENERATED_REPORT_FILES.has(baseName)) continue;
|
|
889
898
|
results.push(fullPath);
|
|
890
899
|
}
|
|
891
900
|
}
|
|
@@ -939,6 +948,7 @@ class GuardScanner {
|
|
|
939
948
|
if (cats.has('sandbox-validation')) skillRecs.push('🔒 SANDBOX: Skill requests dangerous capabilities.');
|
|
940
949
|
if (cats.has('complexity')) skillRecs.push('🧩 COMPLEXITY: Excessive code complexity may hide malicious behavior.');
|
|
941
950
|
if (cats.has('config-impact')) skillRecs.push('⚙️ CONFIG IMPACT: Modifies OpenClaw configuration. DO NOT INSTALL.');
|
|
951
|
+
if (cats.has('pii-exposure')) skillRecs.push('🆔 PII EXPOSURE: Handles personally identifiable information. Review data handling.');
|
|
942
952
|
|
|
943
953
|
if (skillRecs.length > 0) recommendations.push({ skill: skillResult.skill, actions: skillRecs });
|
|
944
954
|
}
|
|
@@ -971,11 +981,21 @@ class GuardScanner {
|
|
|
971
981
|
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
982
|
});
|
|
973
983
|
}
|
|
984
|
+
const normalizedFile = String(f.file || '')
|
|
985
|
+
.replaceAll('\\', '/')
|
|
986
|
+
.replace(/^\/+/, '');
|
|
987
|
+
const artifactUri = `${skillResult.skill}/${normalizedFile}`;
|
|
988
|
+
const fingerprintSeed = `${f.id}|${artifactUri}|${f.line || 0}|${(f.sample || '').slice(0, 200)}`;
|
|
989
|
+
const lineHash = crypto.createHash('sha256').update(fingerprintSeed).digest('hex').slice(0, 24);
|
|
990
|
+
|
|
974
991
|
results.push({
|
|
975
992
|
ruleId: f.id, ruleIndex: ruleIndex[f.id],
|
|
976
993
|
level: f.severity === 'CRITICAL' ? 'error' : f.severity === 'HIGH' ? 'error' : f.severity === 'MEDIUM' ? 'warning' : 'note',
|
|
977
994
|
message: { text: `[${skillResult.skill}] ${f.desc}${f.sample ? ` — "${f.sample}"` : ''}` },
|
|
978
|
-
|
|
995
|
+
partialFingerprints: {
|
|
996
|
+
primaryLocationLineHash: lineHash
|
|
997
|
+
},
|
|
998
|
+
locations: [{ physicalLocation: { artifactLocation: { uri: artifactUri, uriBaseId: '%SRCROOT%' }, region: f.line ? { startLine: f.line } : undefined } }]
|
|
979
999
|
});
|
|
980
1000
|
}
|
|
981
1001
|
}
|