guard-scanner 1.1.1 → 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 +68 -16
- 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 +261 -0
- package/package.json +1 -1
- package/src/scanner.js +15 -1
package/README.md
CHANGED
|
@@ -2,14 +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
|
|
6
|
-
<sub
|
|
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>
|
|
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-
|
|
12
|
+
<img src="https://img.shields.io/badge/tests-56%2F56-brightgreen" alt="Tests Passing">
|
|
13
13
|
<img src="https://img.shields.io/badge/patterns-186-orange" alt="186 Patterns">
|
|
14
14
|
<img src="https://img.shields.io/badge/categories-20-blueviolet" alt="20 Categories">
|
|
15
15
|
</p>
|
|
@@ -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.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
|
+
|
|
77
89
|
### Installation (Optional)
|
|
78
90
|
|
|
79
91
|
```bash
|
|
@@ -91,13 +103,13 @@ openclaw skill 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 **20 threat categories** derived from
|
|
112
|
+
guard-scanner covers **20 threat categories** derived from four sources:
|
|
101
113
|
|
|
102
114
|
| # | Category | Based On | Severity | What It Detects |
|
|
103
115
|
|---|----------|----------|----------|----------------|
|
|
@@ -131,7 +143,7 @@ guard-scanner covers **20 threat categories** derived from three taxonomies:
|
|
|
131
143
|
### Terminal (Default)
|
|
132
144
|
|
|
133
145
|
```
|
|
134
|
-
🛡️ guard-scanner v1.1.
|
|
146
|
+
🛡️ guard-scanner v1.1.1
|
|
135
147
|
══════════════════════════════════════════════════════
|
|
136
148
|
📂 Scanning: ./skills/
|
|
137
149
|
📦 Skills found: 22
|
|
@@ -394,9 +406,12 @@ guard-scanner/
|
|
|
394
406
|
│ └── cli.js # CLI entry point and argument parser
|
|
395
407
|
├── hooks/
|
|
396
408
|
│ └── guard-scanner/
|
|
397
|
-
│
|
|
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)
|
|
398
412
|
├── test/
|
|
399
|
-
│ ├── scanner.test.js #
|
|
413
|
+
│ ├── scanner.test.js # 56 tests — static scanner
|
|
414
|
+
│ ├── plugin.test.js # 35 tests — Plugin Hook runtime guard
|
|
400
415
|
│ └── fixtures/ # Malicious, clean, complex, config-changer samples
|
|
401
416
|
├── package.json # Zero dependencies, node --test
|
|
402
417
|
├── CHANGELOG.md
|
|
@@ -521,11 +536,11 @@ console.log(scanner.toHTML()); // HTML string
|
|
|
521
536
|
## Test Results
|
|
522
537
|
|
|
523
538
|
```
|
|
524
|
-
ℹ tests
|
|
539
|
+
ℹ tests 56
|
|
525
540
|
ℹ suites 13
|
|
526
|
-
ℹ pass
|
|
541
|
+
ℹ pass 56
|
|
527
542
|
ℹ fail 0
|
|
528
|
-
ℹ duration_ms
|
|
543
|
+
ℹ duration_ms 108ms
|
|
529
544
|
```
|
|
530
545
|
|
|
531
546
|
| Suite | Tests | Coverage |
|
|
@@ -556,13 +571,36 @@ console.log(scanner.toHTML()); // HTML string
|
|
|
556
571
|
|
|
557
572
|
---
|
|
558
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
|
+
|
|
559
597
|
## Contributing
|
|
560
598
|
|
|
561
599
|
1. Fork the repository
|
|
562
600
|
2. Create a feature branch (`git checkout -b feature/new-pattern`)
|
|
563
601
|
3. Add your pattern to `src/patterns.js` with the required fields
|
|
564
602
|
4. Add a test case in `test/fixtures/` and `test/scanner.test.js`
|
|
565
|
-
5. Run `npm test` — all
|
|
603
|
+
5. Run `npm test` — all 56+ tests must pass
|
|
566
604
|
6. Submit a Pull Request
|
|
567
605
|
|
|
568
606
|
### Adding a New Detection Pattern
|
|
@@ -608,15 +646,29 @@ guard-scanner catches threats **before** installation. But what happens **after*
|
|
|
608
646
|
| | guard-scanner (OSS) | GuavaSuite (Private) |
|
|
609
647
|
|---|---|---|
|
|
610
648
|
| Static scan | ✅ 20 categories | ✅ 20 categories |
|
|
611
|
-
| Runtime blocking |
|
|
612
|
-
| SOUL.md integrity | Pattern detection only |
|
|
613
|
-
| On-chain verification | — |
|
|
614
|
-
| 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) |
|
|
615
653
|
|
|
616
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).
|
|
617
655
|
|
|
618
656
|
---
|
|
619
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
|
+
|
|
620
672
|
## 💜 Sponsor This Project
|
|
621
673
|
|
|
622
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
|
@@ -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,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
|
}
|