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