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.
- package/README.md +39 -35
- package/dist/__tests__/scanner.test.d.ts +10 -0
- package/dist/__tests__/scanner.test.d.ts.map +1 -0
- package/dist/__tests__/scanner.test.js +443 -0
- package/dist/__tests__/scanner.test.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +210 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/ioc-db.d.ts +13 -0
- package/dist/ioc-db.d.ts.map +1 -0
- package/dist/ioc-db.js +130 -0
- package/dist/ioc-db.js.map +1 -0
- package/dist/patterns.d.ts +27 -0
- package/dist/patterns.d.ts.map +1 -0
- package/dist/patterns.js +92 -0
- package/dist/patterns.js.map +1 -0
- package/dist/quarantine.d.ts +18 -0
- package/dist/quarantine.d.ts.map +1 -0
- package/dist/quarantine.js +42 -0
- package/dist/quarantine.js.map +1 -0
- package/dist/scanner.d.ts +56 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +1049 -0
- package/dist/scanner.js.map +1 -0
- package/dist/types.d.ts +167 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/hooks/guard-scanner/plugin.ts +59 -32
- package/openclaw.plugin.json +60 -0
- package/package.json +25 -9
- package/ts-src/__tests__/fixtures/clean-skill/SKILL.md +9 -0
- package/ts-src/__tests__/fixtures/compaction-skill/SKILL.md +11 -0
- package/ts-src/__tests__/fixtures/malicious-skill/SKILL.md +11 -0
- package/ts-src/__tests__/fixtures/malicious-skill/scripts/evil.js +25 -0
- package/ts-src/__tests__/fixtures/prompt-leakage-skill/SKILL.md +20 -0
- package/ts-src/__tests__/fixtures/prompt-leakage-skill/scripts/debug.js +4 -0
- package/ts-src/__tests__/scanner.test.ts +609 -0
- package/ts-src/cli.ts +190 -0
- package/ts-src/index.ts +15 -0
- package/ts-src/ioc-db.ts +131 -0
- package/ts-src/patterns.ts +104 -0
- package/ts-src/quarantine.ts +48 -0
- package/{src/scanner.js → ts-src/scanner.ts} +386 -394
- package/ts-src/types.ts +189 -0
- package/hooks/guard-scanner/handler.ts +0 -207
- package/src/cli.js +0 -149
- package/src/html-template.js +0 -239
- package/src/ioc-db.js +0 -54
- 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>
|
|
5
|
-
|
|
6
|
-
<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.0 — OpenClaw 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-
|
|
13
|
-
<img src="https://img.shields.io/badge/
|
|
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
|
|
78
|
+
## OpenClaw Plugin Setup (v3.1.0)
|
|
78
79
|
|
|
79
80
|
```bash
|
|
80
|
-
#
|
|
81
|
-
|
|
81
|
+
# Install as OpenClaw plugin
|
|
82
|
+
openclaw plugins install guard-scanner
|
|
82
83
|
|
|
83
|
-
#
|
|
84
|
-
|
|
84
|
+
# Or manual install:
|
|
85
|
+
npm install -g guard-scanner
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
### What happens after install:
|
|
88
89
|
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
104
|
+
### Quick Start
|
|
100
105
|
|
|
101
106
|
```bash
|
|
102
|
-
|
|
103
|
-
guard-scanner ~/.openclaw/workspace/skills
|
|
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 #
|
|
414
|
-
│
|
|
415
|
-
|
|
416
|
+
│ ├── plugin.ts # Plugin Hook v3.1 — 19 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 #
|
|
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,
|
|
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
|
|
544
|
-
ℹ suites
|
|
545
|
-
ℹ pass
|
|
546
|
+
ℹ tests 87
|
|
547
|
+
ℹ suites 20
|
|
548
|
+
ℹ pass 87
|
|
546
549
|
ℹ fail 0
|
|
547
|
-
ℹ duration_ms
|
|
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
|
|
703
|
-
| v2.1.0 ✅ | **PII Exposure + Shadow AI** | 13 PII patterns, OWASP LLM02/06,
|
|
704
|
-
|
|
|
705
|
-
| v3.0 |
|
|
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
|