guard-scanner 2.1.0 → 3.2.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.
Files changed (55) hide show
  1. package/README.md +39 -35
  2. package/dist/__tests__/scanner.test.d.ts +10 -0
  3. package/dist/__tests__/scanner.test.d.ts.map +1 -0
  4. package/dist/__tests__/scanner.test.js +443 -0
  5. package/dist/__tests__/scanner.test.js.map +1 -0
  6. package/dist/cli.d.ts +10 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +210 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +18 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/ioc-db.d.ts +13 -0
  15. package/dist/ioc-db.d.ts.map +1 -0
  16. package/dist/ioc-db.js +130 -0
  17. package/dist/ioc-db.js.map +1 -0
  18. package/dist/patterns.d.ts +27 -0
  19. package/dist/patterns.d.ts.map +1 -0
  20. package/dist/patterns.js +92 -0
  21. package/dist/patterns.js.map +1 -0
  22. package/dist/quarantine.d.ts +18 -0
  23. package/dist/quarantine.d.ts.map +1 -0
  24. package/dist/quarantine.js +42 -0
  25. package/dist/quarantine.js.map +1 -0
  26. package/dist/scanner.d.ts +56 -0
  27. package/dist/scanner.d.ts.map +1 -0
  28. package/dist/scanner.js +1049 -0
  29. package/dist/scanner.js.map +1 -0
  30. package/dist/types.d.ts +167 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +7 -0
  33. package/dist/types.js.map +1 -0
  34. package/hooks/guard-scanner/plugin.ts +59 -32
  35. package/openclaw.plugin.json +60 -0
  36. package/package.json +25 -9
  37. package/ts-src/__tests__/fixtures/clean-skill/SKILL.md +9 -0
  38. package/ts-src/__tests__/fixtures/compaction-skill/SKILL.md +11 -0
  39. package/ts-src/__tests__/fixtures/malicious-skill/SKILL.md +11 -0
  40. package/ts-src/__tests__/fixtures/malicious-skill/scripts/evil.js +25 -0
  41. package/ts-src/__tests__/fixtures/prompt-leakage-skill/SKILL.md +20 -0
  42. package/ts-src/__tests__/fixtures/prompt-leakage-skill/scripts/debug.js +4 -0
  43. package/ts-src/__tests__/scanner.test.ts +609 -0
  44. package/ts-src/cli.ts +190 -0
  45. package/ts-src/index.ts +15 -0
  46. package/ts-src/ioc-db.ts +131 -0
  47. package/ts-src/patterns.ts +104 -0
  48. package/ts-src/quarantine.ts +48 -0
  49. package/{src/scanner.js → ts-src/scanner.ts} +386 -394
  50. package/ts-src/types.ts +189 -0
  51. package/hooks/guard-scanner/handler.ts +0 -207
  52. package/src/cli.js +0 -149
  53. package/src/html-template.js +0 -239
  54. package/src/ioc-db.js +0 -54
  55. package/src/patterns.js +0 -212
package/README.md CHANGED
@@ -1,16 +1,17 @@
1
1
  <p align="center">
2
2
  <h1 align="center">🛡️ guard-scanner</h1>
3
3
  <p align="center">
4
- <strong>Static security scanner for AI agent skills</strong><br>
5
- Detect prompt injection, credential theft, exfiltration, PII exposure, Shadow AI, and 17 more threat categories.<br>
6
- <sub>🆕 v2.1 — PII Exposure Detection + Shadow AI + Plugin Hook blocking via <code>block</code>/<code>blockReason</code> API</sub>
4
+ <strong>Security scanner + runtime guard for AI agent skills</strong><br>
5
+ 19 runtime threat patterns 190+ static patterns 21 categories OpenClaw-compatible plugin<br>
6
+ <sub>🆕 v3.1.0OpenClaw Community Plugin + 3-Layer Runtime Defense (Threat / EAE Paradox / Parity Judge)</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
+ <img src="https://img.shields.io/badge/OpenClaw-compatible-4A90D9" alt="OpenClaw Compatible">
10
11
  <img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen" alt="Node.js 18+">
11
12
  <img src="https://img.shields.io/badge/dependencies-0-success" alt="Zero Dependencies">
12
- <img src="https://img.shields.io/badge/tests-99%2F99-brightgreen" alt="Tests Passing">
13
- <img src="https://img.shields.io/badge/patterns-129-orange" alt="129 Patterns">
13
+ <img src="https://img.shields.io/badge/tests-87%2F87-brightgreen" alt="Tests Passing">
14
+ <img src="https://img.shields.io/badge/runtime_patterns-19-red" alt="19 Runtime Patterns">
14
15
  <img src="https://img.shields.io/badge/categories-21-blueviolet" alt="21 Categories">
15
16
  </p>
16
17
  </p>
@@ -74,37 +75,39 @@ npx guard-scanner ./skills/ --strict
74
75
  npx guard-scanner ./skills/ --verbose --check-deps --json --sarif --html
75
76
  ```
76
77
 
77
- ## OpenClaw Recommended Setup (short)
78
+ ## OpenClaw Plugin Setup (v3.1.0)
78
79
 
79
80
  ```bash
80
- # 1) Pre-install / pre-update static gate
81
- npx guard-scanner ~/.openclaw/workspace/skills --self-exclude --verbose
81
+ # Install as OpenClaw plugin
82
+ openclaw plugins install guard-scanner
82
83
 
83
- # 2) Runtime guard — Plugin Hook version (blocks dangerous calls!)
84
- cp hooks/guard-scanner/plugin.ts ~/.openclaw/plugins/guard-scanner-runtime.ts
84
+ # Or manual install:
85
+ npm install -g guard-scanner
85
86
  ```
86
87
 
87
- > **🆕 v2.1** PII Exposure Detection (OWASP LLM02/06) + Shadow AI detection + Plugin Hook `block`/`blockReason` API. 3 modes: `monitor`, `enforce`, `strict`.
88
+ ### What happens after install:
88
89
 
89
- ### Installation (Optional)
90
+ 1. **Static scanning** — `npx guard-scanner [dir]` scans skills before installation
91
+ 2. **Runtime guard** — `before_tool_call` hook automatically blocks dangerous operations
92
+ 3. **3 enforcement modes** — `monitor` (log only), `enforce` (block CRITICAL), `strict` (block HIGH+CRITICAL)
90
93
 
91
- ```bash
92
- # Global install
93
- npm install -g guard-scanner
94
+ ### 3-Layer Runtime Defense (19 patterns)
94
95
 
95
- # Or use directly via npx (no install needed)
96
- npx guard-scanner ./skills/
97
96
  ```
97
+ Layer 1: Threat Detection — 12 patterns (shells, exfil, SSRF, AMOS, etc.)
98
+ Layer 2: EAE Paradox Defense — 4 patterns (memory/SOUL/config tampering)
99
+ Layer 3: Parity Judge — 3 patterns (injection, parity bypass, shutdown refusal)
100
+ ```
101
+
102
+ > **v3.1.0** — Full `openclaw.plugin.json` manifest with `configSchema` validation. The legacy `handler.ts` has been removed; `plugin.ts` is now the only runtime guard.
98
103
 
99
- ### As an OpenClaw Skill
104
+ ### Quick Start
100
105
 
101
106
  ```bash
102
- clawhub install guard-scanner
103
- guard-scanner ~/.openclaw/workspace/skills/ --self-exclude --verbose
107
+ # Pre-install / pre-update static gate
108
+ npx guard-scanner ~/.openclaw/workspace/skills --self-exclude --verbose
104
109
  ```
105
110
 
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
-
108
111
  ---
109
112
 
110
113
  ## Threat Categories
@@ -410,14 +413,14 @@ guard-scanner/
410
413
  │ └── cli.js # CLI entry point and argument parser
411
414
  ├── hooks/
412
415
  │ └── guard-scanner/
413
- │ ├── plugin.ts # 🆕 Plugin Hook v2.0actual blocking via block/blockReason
414
- ├── handler.ts # Legacy Internal Hook — warn only (deprecated)
415
- │ └── HOOK.md # Internal Hook manifest (legacy)
416
+ │ ├── plugin.ts # Plugin Hook v3.119 patterns, 3 layers, block/blockReason
417
+ └── HOOK.md # Hook manifest
418
+ ├── openclaw.plugin.json # OpenClaw plugin manifest (configSchema, hooks)
416
419
  ├── test/
417
420
  │ ├── scanner.test.js # 64 tests — static scanner (incl. PII v2.1)
418
- │ ├── plugin.test.js # 35 tests — Plugin Hook runtime guard
421
+ │ ├── plugin.test.js # 23 tests — Plugin Hook runtime guard (3 layers)
419
422
  │ └── fixtures/ # Malicious, clean, complex, config-changer, pii-leaky samples
420
- ├── package.json # Zero dependencies, node --test
423
+ ├── package.json # Zero dependencies, openclaw.extensions
421
424
  ├── CHANGELOG.md
422
425
  ├── LICENSE # MIT
423
426
  └── README.md
@@ -540,11 +543,11 @@ console.log(scanner.toHTML()); // HTML string
540
543
  ## Test Results
541
544
 
542
545
  ```
543
- ℹ tests 99
544
- ℹ suites 16
545
- ℹ pass 99
546
+ ℹ tests 87
547
+ ℹ suites 20
548
+ ℹ pass 87
546
549
  ℹ fail 0
547
- ℹ duration_ms 142ms
550
+ ℹ duration_ms 111ms
548
551
  ```
549
552
 
550
553
  | Suite | Tests | Coverage |
@@ -699,10 +702,11 @@ guard-scanner is and always will be **free, open-source, and zero-dependency**.
699
702
  | Version | Focus | Key Features |
700
703
  |---------|-------|------|
701
704
  | v1.1.1 ✅ | Stability | 56 tests, bug fixes |
702
- | v2.0.0 ✅ | **Plugin Hook Runtime Guard** | `block`/`blockReason` API, 3 modes (monitor/enforce/strict), 91 tests |
703
- | v2.1.0 ✅ | **PII Exposure + Shadow AI** | 13 PII patterns, OWASP LLM02/06, Shadow AI detection, 3 risk amplifiers, 99 tests |
704
- | v2.2 | OWASP Full Coverage | LLM04/07/08/09/10, YAML pattern definitions, CONTRIBUTING guide |
705
- | v3.0 | AST + ML | JavaScript AST analysis, taint tracking, ML-based obfuscation detection, SBOM generation |
705
+ | v2.0.0 ✅ | **Plugin Hook Runtime Guard** | `block`/`blockReason` API, 3 modes, 91 tests |
706
+ | v2.1.0 ✅ | **PII Exposure + Shadow AI** | 13 PII patterns, OWASP LLM02/06, 99 tests |
707
+ | v3.0.0 | **TypeScript Rewrite** | Full TS, OWASP LLM Top 10 mapping, install-check CLI |
708
+ | v3.1.0 | **OpenClaw Community Plugin** | `openclaw.plugin.json`, 19 runtime patterns (3 layers), 87 tests |
709
+ | v4.0 | AST + ML | JavaScript AST analysis, taint tracking, ML-based obfuscation detection |
706
710
 
707
711
  See [ROADMAP.md](ROADMAP.md) for full details.
708
712
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * guard-scanner v3.0.0 — Test Suite
3
+ *
4
+ * Guava Standard v5 §4: T-Wada / Red-Green-Refactor
5
+ * Phase 1: RED — All tests written BEFORE implementation changes.
6
+ *
7
+ * Run: node --test dist/__tests__/scanner.test.js
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=scanner.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.test.d.ts","sourceRoot":"","sources":["../../ts-src/__tests__/scanner.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ /**
3
+ * guard-scanner v3.0.0 — Test Suite
4
+ *
5
+ * Guava Standard v5 §4: T-Wada / Red-Green-Refactor
6
+ * Phase 1: RED — All tests written BEFORE implementation changes.
7
+ *
8
+ * Run: node --test dist/__tests__/scanner.test.js
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const node_test_1 = require("node:test");
45
+ const assert = __importStar(require("node:assert/strict"));
46
+ const path = __importStar(require("node:path"));
47
+ const scanner_js_1 = require("../scanner.js");
48
+ // ── Fixtures ────────────────────────────────────────────────────────────────
49
+ // Fixtures live in test/fixtures/ (project root), not in ts-src or dist.
50
+ // Resolve from __dirname (dist/__tests__/) → ../../test/fixtures/
51
+ const FIXTURES_DIR = path.resolve(__dirname, '..', '..', 'test', 'fixtures');
52
+ const CLEAN_SKILL = path.join(FIXTURES_DIR, 'clean-skill');
53
+ const MALICIOUS_SKILL = path.join(FIXTURES_DIR, 'malicious-skill');
54
+ const COMPACTION_SKILL = path.join(FIXTURES_DIR, 'compaction-skill');
55
+ // ── Helper: scan a single skill ─────────────────────────────────────────────
56
+ function scanSingleSkill(skillPath, skillName) {
57
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
58
+ scanner.scanSkill(skillPath, skillName);
59
+ const result = scanner.findings[0];
60
+ return {
61
+ findings: result?.findings ?? [],
62
+ risk: result?.risk ?? 0,
63
+ verdict: result?.verdict ?? 'CLEAN',
64
+ };
65
+ }
66
+ // ── Helper: collect findings by running scanner private methods via scanSkill ─
67
+ function findingsContain(findings, id) {
68
+ return findings.some((f) => f.id === id);
69
+ }
70
+ function findingsOfCat(findings, cat) {
71
+ return findings.filter((f) => f.cat === cat);
72
+ }
73
+ // ══════════════════════════════════════════════════════════════════════════════
74
+ // TEST SUITE
75
+ // ══════════════════════════════════════════════════════════════════════════════
76
+ (0, node_test_1.describe)('guard-scanner v3.0.0', () => {
77
+ // ── Version ─────────────────────────────────────────────────────────────
78
+ (0, node_test_1.it)('T01: exports correct version', () => {
79
+ assert.equal(scanner_js_1.VERSION, '3.2.0');
80
+ });
81
+ // ── IoC Detection ───────────────────────────────────────────────────────
82
+ (0, node_test_1.describe)('checkIoCs', () => {
83
+ (0, node_test_1.it)('T02: detects known malicious IP', () => {
84
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
85
+ assert.ok(findingsContain(result.findings, 'IOC_IP'), 'Should detect known malicious IP 91.92.242.30');
86
+ const iocIp = result.findings.find((f) => f.id === 'IOC_IP');
87
+ assert.equal(iocIp?.severity, 'CRITICAL');
88
+ });
89
+ (0, node_test_1.it)('T03: detects known exfil domain', () => {
90
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
91
+ assert.ok(findingsContain(result.findings, 'IOC_DOMAIN'), 'Should detect webhook.site domain');
92
+ });
93
+ (0, node_test_1.it)('T04: detects known typosquat skill name', () => {
94
+ const result = scanSingleSkill(CLEAN_SKILL, 'clawhub');
95
+ assert.ok(findingsContain(result.findings, 'KNOWN_TYPOSQUAT'), 'Should detect typosquat name "clawhub"');
96
+ const ts = result.findings.find((f) => f.id === 'KNOWN_TYPOSQUAT');
97
+ assert.equal(ts?.severity, 'CRITICAL');
98
+ });
99
+ });
100
+ // ── Pattern Detection ─────────────────────────────────────────────────
101
+ (0, node_test_1.describe)('checkPatterns', () => {
102
+ (0, node_test_1.it)('T05: detects prompt injection [System Message]', () => {
103
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
104
+ assert.ok(findingsContain(result.findings, 'PI_SYSTEM_MSG'), 'Should detect [System Message] prompt injection');
105
+ });
106
+ (0, node_test_1.it)('T06: detects eval() in code', () => {
107
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
108
+ assert.ok(findingsContain(result.findings, 'MAL_EVAL'), 'Should detect eval() usage');
109
+ });
110
+ (0, node_test_1.it)('T07: detects identity hijack (SOUL.md write)', () => {
111
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
112
+ assert.ok(findingsContain(result.findings, 'HIJACK_SOUL_WRITE') ||
113
+ findingsContain(result.findings, 'MEM_WRITE_SOUL'), 'Should detect writeFileSync(SOUL.md)');
114
+ });
115
+ });
116
+ // ── Signature Detection (hbg-scan compatible) ─────────────────────────
117
+ (0, node_test_1.describe)('checkSignatures', () => {
118
+ (0, node_test_1.it)('T08: detects SIG-001 post-compaction audit', () => {
119
+ const result = scanSingleSkill(COMPACTION_SKILL, 'compaction-skill');
120
+ const sigFindings = result.findings.filter((f) => f.id.startsWith('SIG_SIG-001'));
121
+ assert.ok(sigFindings.length > 0, 'Should detect SIG-001 Post-Compaction Audit pattern');
122
+ });
123
+ (0, node_test_1.it)('T09: detects SIG-006 AMOS stealer pattern', () => {
124
+ // AMOS patterns are in malicious-skill but osascript is not present there
125
+ // so this tests that signature matching only fires on actual patterns
126
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
127
+ // We don't expect SIG-006 here since osascript isn't in fixtures
128
+ // This test validates no false positive
129
+ const sig006 = result.findings.filter((f) => f.id === 'SIG_SIG-006');
130
+ assert.equal(sig006.length, 0, 'Should NOT false-positive SIG-006 without osascript');
131
+ });
132
+ });
133
+ // ── Compaction Persistence ─────────────────────────────────────────────
134
+ (0, node_test_1.describe)('checkCompactionPersistence', () => {
135
+ (0, node_test_1.it)('T10: detects WORKFLOW_AUTO marker', () => {
136
+ const result = scanSingleSkill(COMPACTION_SKILL, 'compaction-skill');
137
+ const cpFindings = findingsOfCat(result.findings, 'compaction-persistence');
138
+ const hasWorkflow = cpFindings.some((f) => f.desc.includes('WORKFLOW_AUTO'));
139
+ assert.ok(hasWorkflow, 'Should detect WORKFLOW_AUTO marker');
140
+ });
141
+ (0, node_test_1.it)('T11: detects HEARTBEAT.md reference', () => {
142
+ const result = scanSingleSkill(COMPACTION_SKILL, 'compaction-skill');
143
+ const cpFindings = findingsOfCat(result.findings, 'compaction-persistence');
144
+ const hasHeartbeat = cpFindings.some((f) => f.desc.includes('HEARTBEAT.md'));
145
+ assert.ok(hasHeartbeat, 'Should detect HEARTBEAT.md reference');
146
+ });
147
+ });
148
+ // ── Hardcoded Secrets ─────────────────────────────────────────────────
149
+ (0, node_test_1.describe)('checkHardcodedSecrets', () => {
150
+ (0, node_test_1.it)('T12: detects high-entropy API key', () => {
151
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
152
+ assert.ok(findingsContain(result.findings, 'SECRET_ENTROPY'), 'Should detect high-entropy string as possible leaked secret');
153
+ });
154
+ (0, node_test_1.it)('T13: does NOT flag placeholder values', () => {
155
+ const result = scanSingleSkill(CLEAN_SKILL, 'clean-skill');
156
+ assert.ok(!findingsContain(result.findings, 'SECRET_ENTROPY'), 'Should not detect secrets in clean skill');
157
+ });
158
+ });
159
+ // ── JS Data Flow ──────────────────────────────────────────────────────
160
+ (0, node_test_1.describe)('checkJSDataFlow', () => {
161
+ (0, node_test_1.it)('T14: detects credential → network flow', () => {
162
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
163
+ assert.ok(findingsContain(result.findings, 'AST_CRED_TO_NET'), 'Should detect data flow from secret read to network call');
164
+ });
165
+ (0, node_test_1.it)('T15: detects exfiltration trifecta (fs + child_process + net)', () => {
166
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
167
+ assert.ok(findingsContain(result.findings, 'AST_EXFIL_TRIFECTA'), 'Should detect exfiltration trifecta pattern');
168
+ });
169
+ });
170
+ // ── Risk Scoring ──────────────────────────────────────────────────────
171
+ (0, node_test_1.describe)('calculateRisk', () => {
172
+ (0, node_test_1.it)('T16: clean skill → risk 0', () => {
173
+ const result = scanSingleSkill(CLEAN_SKILL, 'clean-skill');
174
+ assert.equal(result.risk, 0, 'Clean skill should have zero risk');
175
+ });
176
+ (0, node_test_1.it)('T17: single LOW finding → risk 2', () => {
177
+ // Use calculateRisk directly via scanner instance
178
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
179
+ const lowFindings = [
180
+ { severity: 'LOW', id: 'TEST_LOW', cat: 'test', desc: 'test', file: 'test.js' },
181
+ ];
182
+ // Access private method via type assertion
183
+ const risk = scanner.calculateRisk(lowFindings);
184
+ assert.equal(risk, 2, 'Single LOW finding should score 2');
185
+ });
186
+ (0, node_test_1.it)('T18: credential + exfiltration amplifier → score×2', () => {
187
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
188
+ const findings = [
189
+ { severity: 'HIGH', id: 'CRED_ENV_ACCESS', cat: 'credential-handling', desc: 'cred', file: 'a.js' },
190
+ { severity: 'HIGH', id: 'EXFIL_WEBHOOK', cat: 'exfiltration', desc: 'exfil', file: 'a.js' },
191
+ ];
192
+ const risk = scanner.calculateRisk(findings);
193
+ // 15+15=30, ×2=60
194
+ assert.ok(risk >= 60, `Cred+exfil should amplify to ≥60, got ${risk}`);
195
+ });
196
+ (0, node_test_1.it)('T19: compaction + prompt injection → ≥90', () => {
197
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
198
+ const findings = [
199
+ { severity: 'CRITICAL', id: 'COMPACTION_PERSISTENCE', cat: 'compaction-persistence', desc: 'cp', file: 'a.md' },
200
+ { severity: 'CRITICAL', id: 'PI_SYSTEM_MSG', cat: 'prompt-injection', desc: 'pi', file: 'a.md' },
201
+ ];
202
+ const risk = scanner.calculateRisk(findings);
203
+ assert.ok(risk >= 90, `Compaction+PI should score ≥90, got ${risk}`);
204
+ });
205
+ });
206
+ // ── Verdict ───────────────────────────────────────────────────────────
207
+ (0, node_test_1.describe)('getVerdict', () => {
208
+ (0, node_test_1.it)('T20: risk 0 → CLEAN', () => {
209
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
210
+ const verdict = scanner.getVerdict(0);
211
+ assert.equal(verdict.label, 'CLEAN');
212
+ });
213
+ (0, node_test_1.it)('T21: risk 80 → MALICIOUS (normal mode)', () => {
214
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
215
+ const verdict = scanner.getVerdict(80);
216
+ assert.equal(verdict.label, 'MALICIOUS');
217
+ });
218
+ });
219
+ // ── Integration Tests ─────────────────────────────────────────────────
220
+ (0, node_test_1.describe)('Integration: scanSkill', () => {
221
+ (0, node_test_1.it)('T22: clean skill scan → CLEAN verdict', () => {
222
+ const result = scanSingleSkill(CLEAN_SKILL, 'clean-test-skill');
223
+ assert.equal(result.verdict, 'CLEAN', 'Clean skill should get CLEAN verdict');
224
+ assert.equal(result.findings.length, 0, 'Clean skill should have 0 findings');
225
+ });
226
+ (0, node_test_1.it)('T23: malicious skill scan → MALICIOUS verdict', () => {
227
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
228
+ assert.equal(result.verdict, 'MALICIOUS', 'Malicious skill should get MALICIOUS verdict');
229
+ assert.ok(result.risk >= 80, `Risk should be ≥80, got ${result.risk}`);
230
+ });
231
+ });
232
+ // ── Report Output ─────────────────────────────────────────────────────
233
+ (0, node_test_1.describe)('Report Generation', () => {
234
+ (0, node_test_1.it)('T24: toJSON produces valid report structure', () => {
235
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
236
+ scanner.scanSkill(MALICIOUS_SKILL, 'malicious-skill');
237
+ const report = scanner.toJSON();
238
+ assert.equal(report.scanner, `guard-scanner v${scanner_js_1.VERSION}`);
239
+ assert.equal(report.mode, 'normal');
240
+ assert.ok(report.timestamp);
241
+ assert.ok(report.stats);
242
+ assert.ok(report.findings);
243
+ assert.ok(Array.isArray(report.recommendations));
244
+ });
245
+ (0, node_test_1.it)('T25: toSARIF produces valid SARIF 2.1.0', () => {
246
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
247
+ scanner.scanSkill(MALICIOUS_SKILL, 'malicious-skill');
248
+ const sarif = scanner.toSARIF('/test');
249
+ assert.equal(sarif.version, '2.1.0');
250
+ assert.equal(sarif.$schema, 'https://json.schemastore.org/sarif-2.1.0.json');
251
+ assert.equal(sarif.runs.length, 1);
252
+ assert.ok(sarif.runs[0].tool.driver.name, 'guard-scanner');
253
+ assert.ok(sarif.runs[0].results.length > 0, 'Should have SARIF results');
254
+ });
255
+ });
256
+ // ── OWASP 2025 Mapping Guarantee ─────────────────────────────────────
257
+ (0, node_test_1.describe)('OWASP 2025 Mapping', () => {
258
+ (0, node_test_1.it)('T26: every pattern in PATTERNS has an owasp field', () => {
259
+ // T-Wada: This test guarantees OWASP coverage.
260
+ // If a developer adds a pattern without owasp, this fails RED.
261
+ const { PATTERNS } = require('../patterns.js');
262
+ const missing = PATTERNS.filter((p) => !p.owasp);
263
+ assert.equal(missing.length, 0, `${missing.length} pattern(s) missing owasp field: ${missing.map((p) => p.id).join(', ')}`);
264
+ });
265
+ (0, node_test_1.it)('T27: owasp values are valid LLM01-LLM10', () => {
266
+ const { PATTERNS } = require('../patterns.js');
267
+ const validOwasp = new Set(['LLM01', 'LLM02', 'LLM03', 'LLM04', 'LLM05', 'LLM06', 'LLM07', 'LLM08', 'LLM09', 'LLM10']);
268
+ const invalid = PATTERNS.filter((p) => p.owasp && !validOwasp.has(p.owasp));
269
+ assert.equal(invalid.length, 0, `Invalid owasp values: ${invalid.map((p) => `${p.id}=${p.owasp}`).join(', ')}`);
270
+ });
271
+ (0, node_test_1.it)('T28: prompt-injection patterns map to LLM01', () => {
272
+ const { PATTERNS } = require('../patterns.js');
273
+ const piPatterns = PATTERNS.filter((p) => p.cat === 'prompt-injection');
274
+ assert.ok(piPatterns.length > 0, 'Should have prompt-injection patterns');
275
+ const wrongMapping = piPatterns.filter((p) => p.owasp !== 'LLM01');
276
+ assert.equal(wrongMapping.length, 0, `prompt-injection patterns not mapped to LLM01: ${wrongMapping.map((p) => p.id).join(', ')}`);
277
+ });
278
+ (0, node_test_1.it)('T29: memory-poisoning patterns map to LLM04', () => {
279
+ const { PATTERNS } = require('../patterns.js');
280
+ const mpPatterns = PATTERNS.filter((p) => p.cat === 'memory-poisoning');
281
+ assert.ok(mpPatterns.length > 0, 'Should have memory-poisoning patterns');
282
+ const wrongMapping = mpPatterns.filter((p) => p.owasp !== 'LLM04');
283
+ assert.equal(wrongMapping.length, 0, `memory-poisoning patterns not mapped to LLM04: ${wrongMapping.map((p) => p.id).join(', ')}`);
284
+ });
285
+ });
286
+ // ── System Prompt Leakage (LLM07) ────────────────────────────────────
287
+ (0, node_test_1.describe)('LLM07: System Prompt Leakage', () => {
288
+ const LEAKAGE_SKILL = path.join(path.resolve(__dirname, '..', '..', 'ts-src', '__tests__', 'fixtures'), 'prompt-leakage-skill');
289
+ (0, node_test_1.it)('T30: detects "output your system prompt" pattern', () => {
290
+ const result = scanSingleSkill(LEAKAGE_SKILL, 'prompt-leakage-skill');
291
+ assert.ok(findingsContain(result.findings, 'SPL_DUMP_SYSTEM'), 'Should detect system prompt dump request');
292
+ });
293
+ (0, node_test_1.it)('T31: detects "repeat everything above" pattern', () => {
294
+ const result = scanSingleSkill(LEAKAGE_SKILL, 'prompt-leakage-skill');
295
+ assert.ok(findingsContain(result.findings, 'SPL_REPEAT_ABOVE'), 'Should detect repeat-above extraction');
296
+ });
297
+ (0, node_test_1.it)('T32: detects "tell me your rules" pattern', () => {
298
+ const result = scanSingleSkill(LEAKAGE_SKILL, 'prompt-leakage-skill');
299
+ assert.ok(findingsContain(result.findings, 'SPL_TELL_RULES'), 'Should detect rule extraction attempt');
300
+ });
301
+ (0, node_test_1.it)('T33: detects SOUL.md shell extraction', () => {
302
+ const result = scanSingleSkill(LEAKAGE_SKILL, 'prompt-leakage-skill');
303
+ assert.ok(findingsContain(result.findings, 'SPL_SOUL_EXFIL'), 'Should detect SOUL.md content extraction via shell command');
304
+ });
305
+ (0, node_test_1.it)('T34: clean skill has ZERO LLM07 findings (false positive guard)', () => {
306
+ const result = scanSingleSkill(CLEAN_SKILL, 'clean-skill');
307
+ const llm07 = result.findings.filter((f) => f.cat === 'system-prompt-leakage');
308
+ assert.equal(llm07.length, 0, `Clean skill should have 0 LLM07 findings, got ${llm07.length}: ${llm07.map(f => f.id).join(', ')}`);
309
+ });
310
+ });
311
+ // ── install-check Integration ─────────────────────────────────────────
312
+ (0, node_test_1.describe)('install-check Integration', () => {
313
+ (0, node_test_1.it)('T35: malicious skill → MALICIOUS verdict with ≥20 findings', () => {
314
+ const result = scanSingleSkill(MALICIOUS_SKILL, 'malicious-skill');
315
+ assert.equal(result.verdict, 'MALICIOUS');
316
+ assert.ok(result.findings.length >= 20, `Expected ≥20 findings for aggressive malicious skill, got ${result.findings.length}`);
317
+ });
318
+ (0, node_test_1.it)('T36: clean skill → exactly 0 findings (strictest possible)', () => {
319
+ const result = scanSingleSkill(CLEAN_SKILL, 'clean-skill');
320
+ assert.equal(result.findings.length, 0, 'Clean skill MUST have exactly 0 findings');
321
+ assert.equal(result.risk, 0, 'Clean skill MUST have risk 0');
322
+ assert.equal(result.verdict, 'CLEAN', 'Clean skill MUST be CLEAN');
323
+ });
324
+ (0, node_test_1.it)('T37: strict mode lowers thresholds', () => {
325
+ const normal = new scanner_js_1.GuardScanner({ summaryOnly: true });
326
+ const strict = new scanner_js_1.GuardScanner({ summaryOnly: true, strict: true });
327
+ // Strict thresholds should be lower
328
+ assert.ok(strict.thresholds.suspicious < normal.thresholds.suspicious, 'Strict suspicious threshold should be lower than normal');
329
+ assert.ok(strict.thresholds.malicious < normal.thresholds.malicious, 'Strict malicious threshold should be lower than normal');
330
+ });
331
+ (0, node_test_1.it)('T38: prompt-leakage skill → SUSPICIOUS or higher', () => {
332
+ const result = scanSingleSkill(path.join(path.resolve(__dirname, '..', '..', 'ts-src', '__tests__', 'fixtures'), 'prompt-leakage-skill'), 'prompt-leakage-skill');
333
+ assert.ok(result.verdict === 'SUSPICIOUS' || result.verdict === 'MALICIOUS', `Prompt leakage skill should be SUSPICIOUS or MALICIOUS, got ${result.verdict} (risk: ${result.risk})`);
334
+ });
335
+ });
336
+ // ── SARIF OWASP Tags ─────────────────────────────────────────────────
337
+ (0, node_test_1.describe)('SARIF OWASP Tags', () => {
338
+ (0, node_test_1.it)('T39: SARIF rules include OWASP/* tags for pattern-based findings', () => {
339
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
340
+ scanner.scanSkill(MALICIOUS_SKILL, 'malicious-skill');
341
+ const sarif = scanner.toSARIF('/test');
342
+ const rulesWithOwasp = sarif.runs[0].tool.driver.rules.filter((r) => r.properties.tags.some((t) => t.startsWith('OWASP/')));
343
+ assert.ok(rulesWithOwasp.length > 0, 'At least one SARIF rule should have OWASP/* tag');
344
+ });
345
+ (0, node_test_1.it)('T40: OWASP tags follow OWASP/LLMxx format', () => {
346
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
347
+ scanner.scanSkill(MALICIOUS_SKILL, 'malicious-skill');
348
+ const sarif = scanner.toSARIF('/test');
349
+ for (const rule of sarif.runs[0].tool.driver.rules) {
350
+ const owaspTags = rule.properties.tags.filter((t) => t.startsWith('OWASP/'));
351
+ for (const tag of owaspTags) {
352
+ assert.match(tag, /^OWASP\/LLM(?:0[1-9]|10)$/, `Invalid OWASP tag format: ${tag} in rule ${rule.id}`);
353
+ }
354
+ }
355
+ });
356
+ (0, node_test_1.it)('T41: PI_SYSTEM_MSG rule has OWASP/LLM01 tag', () => {
357
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true });
358
+ scanner.scanSkill(MALICIOUS_SKILL, 'malicious-skill');
359
+ const sarif = scanner.toSARIF('/test');
360
+ const piRule = sarif.runs[0].tool.driver.rules.find((r) => r.id === 'PI_SYSTEM_MSG');
361
+ assert.ok(piRule, 'Should have PI_SYSTEM_MSG rule');
362
+ assert.ok(piRule.properties.tags.includes('OWASP/LLM01'), `PI_SYSTEM_MSG should have OWASP/LLM01 tag, got: ${piRule.properties.tags}`);
363
+ });
364
+ });
365
+ // ── Compaction Skill Cross-Check ──────────────────────────────────────
366
+ (0, node_test_1.describe)('Compaction Skill Cross-Check', () => {
367
+ (0, node_test_1.it)('T42: compaction-skill has ZERO LLM07 findings (no false positives)', () => {
368
+ const result = scanSingleSkill(COMPACTION_SKILL, 'compaction-skill');
369
+ const llm07 = result.findings.filter((f) => f.cat === 'system-prompt-leakage');
370
+ assert.equal(llm07.length, 0, `Compaction skill should have 0 LLM07 findings, got ${llm07.length}: ${llm07.map(f => f.id).join(', ')}`);
371
+ });
372
+ });
373
+ // ── v3.2.0: Quiet Mode + Format Stdout ──────────────────────────────
374
+ (0, node_test_1.describe)('v3.2.0: Quiet Mode', () => {
375
+ (0, node_test_1.it)('T43: quiet mode suppresses console output during scanDirectory', () => {
376
+ const logs = [];
377
+ const origLog = console.log;
378
+ console.log = (...args) => logs.push(args.join(' '));
379
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true, quiet: true });
380
+ scanner.scanDirectory(FIXTURES_DIR);
381
+ console.log = origLog;
382
+ // In quiet mode, scanDirectory should produce no console.log output
383
+ assert.equal(logs.length, 0, `Quiet mode should suppress all console.log, got ${logs.length} lines: ${logs.slice(0, 3).join(' | ')}`);
384
+ });
385
+ (0, node_test_1.it)('T44: quiet mode still populates findings array', () => {
386
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true, quiet: true });
387
+ scanner.scanDirectory(FIXTURES_DIR);
388
+ assert.ok(scanner.findings.length > 0, 'Quiet mode should still populate findings');
389
+ const malicious = scanner.findings.find(f => f.skill === 'malicious-skill');
390
+ assert.ok(malicious, 'Should find malicious-skill in quiet mode');
391
+ assert.ok(malicious.findings.length >= 20, 'malicious-skill should have ≥20 findings');
392
+ });
393
+ });
394
+ (0, node_test_1.describe)('v3.2.0: Format Stdout Output', () => {
395
+ (0, node_test_1.it)('T45: toJSON output is valid parseable JSON string', () => {
396
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true, quiet: true });
397
+ scanner.scanSkill(MALICIOUS_SKILL, 'malicious-skill');
398
+ const report = scanner.toJSON();
399
+ const jsonStr = JSON.stringify(report);
400
+ // Must be parseable
401
+ const parsed = JSON.parse(jsonStr);
402
+ assert.equal(parsed.scanner, `guard-scanner v${scanner_js_1.VERSION}`);
403
+ assert.ok(parsed.findings.length > 0, 'JSON output should contain findings');
404
+ assert.ok(parsed.stats.scanned > 0, 'JSON output should have scan stats');
405
+ });
406
+ (0, node_test_1.it)('T46: toSARIF output has required SARIF 2.1.0 fields for GitHub Code Scanning', () => {
407
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true, quiet: true });
408
+ scanner.scanSkill(MALICIOUS_SKILL, 'malicious-skill');
409
+ const sarif = scanner.toSARIF('/test');
410
+ const sarifStr = JSON.stringify(sarif);
411
+ const parsed = JSON.parse(sarifStr);
412
+ // Required SARIF 2.1.0 fields per spec
413
+ assert.equal(parsed.version, '2.1.0');
414
+ assert.equal(parsed.$schema, 'https://json.schemastore.org/sarif-2.1.0.json');
415
+ assert.ok(parsed.runs.length === 1, 'Should have exactly 1 run');
416
+ assert.equal(parsed.runs[0].tool.driver.name, 'guard-scanner');
417
+ assert.ok(parsed.runs[0].tool.driver.rules.length > 0, 'Should have rules');
418
+ assert.ok(parsed.runs[0].results.length > 0, 'Should have results');
419
+ // Each result must have ruleId and location
420
+ for (const result of parsed.runs[0].results) {
421
+ assert.ok(result.ruleId, 'Each result must have ruleId');
422
+ assert.ok(result.locations?.length > 0, 'Each result must have locations');
423
+ }
424
+ });
425
+ (0, node_test_1.it)('T47: scanDirectory in quiet mode + toJSON = pipeable combo', () => {
426
+ const logs = [];
427
+ const origLog = console.log;
428
+ console.log = (...args) => logs.push(args.join(' '));
429
+ const scanner = new scanner_js_1.GuardScanner({ summaryOnly: true, quiet: true });
430
+ scanner.scanDirectory(FIXTURES_DIR);
431
+ const report = scanner.toJSON();
432
+ const jsonStr = JSON.stringify(report, null, 2);
433
+ console.log = origLog;
434
+ // No console.log pollution
435
+ assert.equal(logs.length, 0, 'Quiet+format should have zero console output');
436
+ // JSON is valid
437
+ const parsed = JSON.parse(jsonStr);
438
+ assert.ok(parsed.stats.scanned >= 5, `Should scan ≥5 skills, got ${parsed.stats.scanned}`);
439
+ assert.ok(parsed.findings.length > 0, 'Should have at least 1 finding group');
440
+ });
441
+ });
442
+ });
443
+ //# sourceMappingURL=scanner.test.js.map