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 +69 -14
- package/SECURITY.md +1 -1
- package/SKILL.md +18 -17
- 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 +103 -46
- package/hooks/guard-scanner/plugin.ts +261 -0
- package/package.json +1 -1
- package/src/scanner.js +15 -1
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
|
|
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-
|
|
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
|
|
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.
|
|
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
|
-
│
|
|
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 #
|
|
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
|
|
539
|
+
ℹ tests 56
|
|
522
540
|
ℹ suites 13
|
|
523
|
-
ℹ pass
|
|
541
|
+
ℹ pass 56
|
|
524
542
|
ℹ fail 0
|
|
525
|
-
ℹ duration_ms
|
|
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
|
|
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 |
|
|
609
|
-
| SOUL.md integrity | Pattern detection only |
|
|
610
|
-
| On-chain verification | — |
|
|
611
|
-
| Identity recovery | — |
|
|
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:
|
|
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
|
-
**
|
|
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 (
|
|
57
|
+
### 2. Runtime Guard (OpenClaw) — ⚠️ warn-only currently
|
|
58
58
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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**:
|
|
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 —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
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
|
-
|
|
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 } =
|
|
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
|
|
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
|
|
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: '
|
|
117
|
-
session:
|
|
169
|
+
action: 'warned' as string,
|
|
170
|
+
session: event.sessionKey,
|
|
118
171
|
};
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
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
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
|
-
|
|
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
|
}
|