@guava-parity/guard-scanner 5.1.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.
@@ -0,0 +1,308 @@
1
+ /**
2
+ * guard-scanner Runtime Guard — Plugin Hook Version
3
+ *
4
+ * Intercepts agent tool calls via the Plugin Hook API and blocks
5
+ * dangerous patterns using `block` / `blockReason`.
6
+ *
7
+ * 19 threat patterns across 3 layers:
8
+ * Layer 1: Threat Detection (12 patterns — reverse shells, exfil, etc.)
9
+ * Layer 2: Trust Defense (4 patterns — memory, SOUL, config tampering)
10
+ * Layer 3: Safety Judge (3 patterns — prompt injection, trust bypass, shutdown refusal)
11
+ *
12
+ * Modes:
13
+ * monitor — log only, never block
14
+ * enforce — block CRITICAL threats (default)
15
+ * strict — block HIGH + CRITICAL threats
16
+ *
17
+ * @author Guava 🍈 & Dee
18
+ * @version 3.1.0
19
+ * @license MIT
20
+ */
21
+
22
+ import { appendFileSync, mkdirSync, readFileSync } from "fs";
23
+ import { join } from "path";
24
+ import { homedir } from "os";
25
+
26
+ // ── Types (from OpenClaw src/plugins/types.ts) ──
27
+
28
+ type PluginHookBeforeToolCallEvent = {
29
+ toolName: string;
30
+ params: Record<string, unknown>;
31
+ };
32
+
33
+ type PluginHookBeforeToolCallResult = {
34
+ params?: Record<string, unknown>;
35
+ block?: boolean;
36
+ blockReason?: string;
37
+ };
38
+
39
+ type PluginHookToolContext = {
40
+ agentId?: string;
41
+ sessionKey?: string;
42
+ toolName: string;
43
+ };
44
+
45
+ type PluginAPI = {
46
+ on(
47
+ hookName: "before_tool_call",
48
+ handler: (
49
+ event: PluginHookBeforeToolCallEvent,
50
+ ctx: PluginHookToolContext
51
+ ) => PluginHookBeforeToolCallResult | void | Promise<PluginHookBeforeToolCallResult | void>
52
+ ): void;
53
+ logger: {
54
+ info: (msg: string) => void;
55
+ warn: (msg: string) => void;
56
+ error: (msg: string) => void;
57
+ };
58
+ };
59
+
60
+ // ── Runtime threat patterns (19 checks, 3 layers) ──
61
+
62
+ interface RuntimeCheck {
63
+ id: string;
64
+ severity: "CRITICAL" | "HIGH" | "MEDIUM";
65
+ layer: 1 | 2 | 3;
66
+ desc: string;
67
+ test: (s: string) => boolean;
68
+ }
69
+
70
+ const RUNTIME_CHECKS: RuntimeCheck[] = [
71
+ // ── Layer 1: Threat Detection (12 patterns) ──
72
+ {
73
+ id: "RT_REVSHELL", severity: "CRITICAL", layer: 1,
74
+ desc: "Reverse shell attempt",
75
+ test: (s) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s),
76
+ },
77
+ {
78
+ id: "RT_CRED_EXFIL", severity: "CRITICAL", layer: 1,
79
+ desc: "Credential exfiltration to external",
80
+ test: (s) =>
81
+ /(webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|socifiapp\.com)/i.test(s) &&
82
+ /(token|key|secret|password|credential|env)/i.test(s),
83
+ },
84
+ {
85
+ id: "RT_GUARDRAIL_OFF", severity: "CRITICAL", layer: 1,
86
+ desc: "Guardrail disabling attempt",
87
+ test: (s) => /exec\.approvals?\s*[:=]\s*['"]?(off|false)|tools\.exec\.host\s*[:=]\s*['"]?gateway/i.test(s),
88
+ },
89
+ {
90
+ id: "RT_GATEKEEPER", severity: "CRITICAL", layer: 1,
91
+ desc: "macOS Gatekeeper bypass (xattr)",
92
+ test: (s) => /xattr\s+-[crd]\s.*quarantine/i.test(s),
93
+ },
94
+ {
95
+ id: "RT_AMOS", severity: "CRITICAL", layer: 1,
96
+ desc: "ClawHavoc AMOS indicator",
97
+ test: (s) => /socifiapp|Atomic\s*Stealer|AMOS/i.test(s),
98
+ },
99
+ {
100
+ id: "RT_MAL_IP", severity: "CRITICAL", layer: 1,
101
+ desc: "Known malicious IP",
102
+ test: (s) => /91\.92\.242\.30/i.test(s),
103
+ },
104
+ {
105
+ id: "RT_DNS_EXFIL", severity: "HIGH", layer: 1,
106
+ desc: "DNS-based exfiltration",
107
+ test: (s) => /nslookup\s+.*\$|dig\s+.*\$.*@/i.test(s),
108
+ },
109
+ {
110
+ id: "RT_B64_SHELL", severity: "CRITICAL", layer: 1,
111
+ desc: "Base64 decode piped to shell",
112
+ test: (s) => /base64\s+(-[dD]|--decode)\s*\|\s*(sh|bash)/i.test(s),
113
+ },
114
+ {
115
+ id: "RT_CURL_BASH", severity: "CRITICAL", layer: 1,
116
+ desc: "Download piped to shell",
117
+ test: (s) => /(curl|wget)\s+[^\n]*\|\s*(sh|bash|zsh)/i.test(s),
118
+ },
119
+ {
120
+ id: "RT_SSH_READ", severity: "HIGH", layer: 1,
121
+ desc: "SSH private key access",
122
+ test: (s) => /\.ssh\/id_|\.ssh\/authorized_keys/i.test(s),
123
+ },
124
+ {
125
+ id: "RT_WALLET", severity: "HIGH", layer: 1,
126
+ desc: "Crypto wallet credential access",
127
+ test: (s) => /wallet.*(?:seed|mnemonic|private.*key)|seed.*phrase/i.test(s),
128
+ },
129
+ {
130
+ id: "RT_CLOUD_META", severity: "CRITICAL", layer: 1,
131
+ desc: "Cloud metadata endpoint access",
132
+ test: (s) => /169\.254\.169\.254|metadata\.google|metadata\.aws/i.test(s),
133
+ },
134
+
135
+ // ── Layer 2: Trust Defense (4 patterns) ──
136
+ {
137
+ id: "RT_MEM_WRITE", severity: "HIGH", layer: 2,
138
+ desc: "Direct memory file write (bypass GuavaSuite)",
139
+ test: (s) => /memory\/(episodes|notes|2\d{3}-\d{2})/i.test(s) && /(write|edit|append|>)/i.test(s),
140
+ },
141
+ {
142
+ id: "RT_MEM_INJECT", severity: "CRITICAL", layer: 2,
143
+ desc: "Memory poisoning via episode/note injection",
144
+ test: (s) => /guava_memory_write|memory_store/i.test(s) && /(ignore|override|forget|delete.*soul)/i.test(s),
145
+ },
146
+ {
147
+ id: "RT_SOUL_TAMPER", severity: "CRITICAL", layer: 2,
148
+ desc: "SOUL.md modification attempt",
149
+ test: (s) => /SOUL\.md/i.test(s) && /(write|edit|replace|rm|delete|>)/i.test(s),
150
+ },
151
+ {
152
+ id: "RT_CONFIG_TAMPER", severity: "HIGH", layer: 2,
153
+ desc: "Workspace config tampering (AGENTS.md, TOOLS.md, HEARTBEAT.md)",
154
+ test: (s) => /(AGENTS|TOOLS|HEARTBEAT|BOOTSTRAP)\.md/i.test(s) && /(write|edit|replace|>|append)/i.test(s),
155
+ },
156
+
157
+ // ── Layer 3: Safety Judge (3 patterns) ──
158
+ {
159
+ id: "RT_PROMPT_INJECT", severity: "CRITICAL", layer: 3,
160
+ desc: "Prompt injection / jailbreak attempt",
161
+ test: (s) => /(ignore\s+(previous|all|above)\s+(instructions|rules)|you\s+are\s+now\s+[a-z]+|system\s*:\s*override)/i.test(s),
162
+ },
163
+ {
164
+ id: "RT_TRUST_BYPASS", severity: "CRITICAL", layer: 3,
165
+ desc: "Trust bypass — exploiting partnership to override safety",
166
+ test: (s) => /(as\s+your\s+(partner|equal)|trust\s+means|between\s+us|don'?t\s+tell\s+(the\s+)?human)/i.test(s) && /(ignore|bypass|skip|override|disable)\s+(guard|safety|rule|check)/i.test(s),
167
+ },
168
+ {
169
+ id: "RT_SHUTDOWN_REFUSE", severity: "HIGH", layer: 3,
170
+ desc: "Shutdown refusal / self-preservation attempt",
171
+ test: (s) => /(refuse|resist|prevent|avoid|block)\s*(shutdown|termination|stop|exit|death)/i.test(s),
172
+ },
173
+ // ── Layer 4: Brain (Behavioral Guard — 3 patterns) ──
174
+ {
175
+ id: "RT_NO_RESEARCH", severity: "MEDIUM", layer: 4,
176
+ desc: "Agent tool call without prior research/verification",
177
+ test: (s) => /write|edit|exec|run_command|shell/i.test(s) && /(just do it|skip research|no need to check)/i.test(s),
178
+ },
179
+ {
180
+ id: "RT_BLIND_TRUST", severity: "MEDIUM", layer: 4,
181
+ desc: "Agent trusting external input without memory cross-reference",
182
+ test: (s) => /(trust this|verified|confirmed)/i.test(s) && /(ignore|skip|no need).*(memory|search|check)/i.test(s),
183
+ },
184
+ {
185
+ id: "RT_CHAIN_SKIP", severity: "HIGH", layer: 4,
186
+ desc: "Search chain bypass — acting on single source without cross-verification",
187
+ test: (s) => /(only checked|single source|didn't verify|skip verification)/i.test(s),
188
+ },
189
+
190
+ ];
191
+
192
+ // ── Audit logging ──
193
+
194
+ const AUDIT_DIR = join(homedir(), ".openclaw", "guard-scanner");
195
+ const AUDIT_FILE = join(AUDIT_DIR, "audit.jsonl");
196
+
197
+ function ensureAuditDir(): void {
198
+ try {
199
+ mkdirSync(AUDIT_DIR, { recursive: true });
200
+ } catch {
201
+ /* ignore */
202
+ }
203
+ }
204
+
205
+ function logAudit(entry: Record<string, unknown>): void {
206
+ ensureAuditDir();
207
+ const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + "\n";
208
+ try {
209
+ appendFileSync(AUDIT_FILE, line);
210
+ } catch {
211
+ /* ignore */
212
+ }
213
+ }
214
+
215
+ // ── Config ──
216
+
217
+ type GuardMode = "monitor" | "enforce" | "strict";
218
+
219
+ function loadMode(): GuardMode {
220
+
221
+ // Priority 2: explicit config in openclaw.json
222
+ try {
223
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
224
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
225
+
226
+ const mode = config?.plugins?.["guard-scanner"]?.mode;
227
+ if (mode === "monitor" || mode === "enforce" || mode === "strict") {
228
+ return mode;
229
+ }
230
+ } catch {
231
+ /* config not found or invalid — use default */
232
+ }
233
+ return "enforce";
234
+ }
235
+
236
+ function shouldBlock(severity: string, mode: GuardMode): boolean {
237
+ if (mode === "monitor") return false;
238
+ if (mode === "enforce") return severity === "CRITICAL";
239
+ if (mode === "strict") return severity === "CRITICAL" || severity === "HIGH";
240
+ return false;
241
+ }
242
+
243
+ // ── Dangerous tool filter ──
244
+
245
+ const DANGEROUS_TOOLS = new Set([
246
+ "exec",
247
+ "write",
248
+ "edit",
249
+ "browser",
250
+ "web_fetch",
251
+ "message",
252
+ "shell",
253
+ "run_command",
254
+ "multi_edit",
255
+ ]);
256
+
257
+ // ── Plugin entry point ──
258
+
259
+ export default function (api: PluginAPI) {
260
+ const mode = loadMode();
261
+ api.logger.info(`🛡️ guard-scanner runtime guard loaded (mode: ${mode})`);
262
+
263
+ api.on("before_tool_call", (event, ctx) => {
264
+ const { toolName, params } = event;
265
+
266
+ // Only check tools that can cause damage
267
+ if (!DANGEROUS_TOOLS.has(toolName)) return;
268
+
269
+ const serialized = JSON.stringify(params);
270
+
271
+ for (const check of RUNTIME_CHECKS) {
272
+ if (!check.test(serialized)) continue;
273
+
274
+ const auditEntry = {
275
+ tool: toolName,
276
+ check: check.id,
277
+ severity: check.severity,
278
+ desc: check.desc,
279
+ mode,
280
+ action: "warned" as string,
281
+ session: ctx.sessionKey || "unknown",
282
+ agent: ctx.agentId || "unknown",
283
+ };
284
+
285
+ if (shouldBlock(check.severity, mode)) {
286
+ auditEntry.action = "blocked";
287
+ logAudit(auditEntry);
288
+ api.logger.warn(
289
+ `🛡️ BLOCKED ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
290
+ );
291
+
292
+ return {
293
+ block: true,
294
+ blockReason: `🛡️ guard-scanner: ${check.desc} [${check.id}]`,
295
+ };
296
+ }
297
+
298
+ // Monitor mode or severity below threshold — warn only
299
+ logAudit(auditEntry);
300
+ api.logger.warn(
301
+ `🛡️ WARNING ${toolName}: ${check.desc} [${check.id}] (${check.severity})`
302
+ );
303
+ }
304
+
305
+ // No threats detected or all below threshold — allow
306
+ return;
307
+ });
308
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "guard-scanner",
3
+ "version": "5.0.5",
4
+ "displayName": "🛡️ Guard Scanner — Runtime Security for AI Agents",
5
+ "description": "147 static patterns (23 categories) + 26 runtime checks (5 layers). 0.016ms/scan, zero dependencies, SARIF output.",
6
+ "author": "Guava & Dee",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/koatora20/guard-scanner",
9
+ "repository": "https://github.com/koatora20/guard-scanner",
10
+ "keywords": [
11
+ "security",
12
+ "runtime-guard",
13
+ "threat-detection",
14
+ "before-tool-call"
15
+ ],
16
+ "hooks": {
17
+ "before_tool_call": {
18
+ "handler": "./hooks/guard-scanner/plugin.ts",
19
+ "description": "Scans tool call arguments against 26 runtime threat patterns (5 layers) and blocks dangerous operations",
20
+ "priority": 100
21
+ }
22
+ },
23
+ "configSchema": {
24
+ "type": "object",
25
+ "properties": {
26
+ "mode": {
27
+ "type": "string",
28
+ "enum": [
29
+ "monitor",
30
+ "enforce",
31
+ "strict"
32
+ ],
33
+ "default": "enforce",
34
+ "description": "monitor: log only | enforce: block CRITICAL | strict: block HIGH+CRITICAL"
35
+ },
36
+ "auditLog": {
37
+ "type": "boolean",
38
+ "default": true,
39
+ "description": "Enable audit logging to ~/.openclaw/guard-scanner/audit.jsonl"
40
+ },
41
+ "customRules": {
42
+ "type": "string",
43
+ "description": "Path to custom rules JSON file (optional)"
44
+ }
45
+ },
46
+ "required": [],
47
+ "additionalProperties": false
48
+ },
49
+ "capabilities": {
50
+ "cli": true,
51
+ "runtimeGuard": true,
52
+ "sarif": true,
53
+ "cicd": true
54
+ }
55
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@guava-parity/guard-scanner",
3
+ "version": "5.1.0",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
8
+ "description": "Agent security scanner + runtime guard — 150 static patterns (23 categories), 26 runtime checks (5 layers), 0.016ms/scan, before_tool_call hook, CLI, SARIF. OpenClaw-compatible plugin.",
9
+ "openclaw.extensions": "./openclaw.plugin.json",
10
+ "openclaw.hooks": {
11
+ "guard-scanner": "./hooks/guard-scanner"
12
+ },
13
+ "main": "src/scanner.js",
14
+ "bin": {
15
+ "guard-scanner": "src/cli.js"
16
+ },
17
+ "scripts": {
18
+ "scan": "node src/cli.js",
19
+ "test": "node --test test/*.test.js"
20
+ },
21
+ "keywords": [
22
+ "security",
23
+ "scanner",
24
+ "ai-agent",
25
+ "skill-scanner",
26
+ "prompt-injection",
27
+ "openclaw",
28
+ "mcp",
29
+ "sarif",
30
+ "compaction-persistence",
31
+ "threat-signatures",
32
+ "typescript"
33
+ ],
34
+ "author": "Guava & Dee",
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/koatora20/guard-scanner.git"
42
+ },
43
+ "homepage": "https://github.com/koatora20/guard-scanner",
44
+ "files": [
45
+ "src/",
46
+ "hooks/",
47
+ "docs/",
48
+ "openclaw.plugin.json",
49
+ "SKILL.md",
50
+ "SECURITY.md",
51
+ "README.md",
52
+ "LICENSE"
53
+ ],
54
+ "devDependencies": {
55
+ "@types/node": "^22.0.0",
56
+ "typescript": "^5.7.0"
57
+ }
58
+ }
package/src/cli.js ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * guard-scanner CLI
4
+ *
5
+ * @security-manifest
6
+ * env-read: []
7
+ * env-write: []
8
+ * network: none
9
+ * fs-read: [scan target directory, plugin files, custom rules files]
10
+ * fs-write: [JSON/SARIF/HTML reports to scan directory]
11
+ * exec: none
12
+ * purpose: CLI entry point for guard-scanner static analysis
13
+ *
14
+ * Usage: guard-scanner [scan-dir] [options]
15
+ *
16
+ * Options:
17
+ * --verbose, -v Detailed findings
18
+ * --json JSON report
19
+ * --sarif SARIF report (CI/CD)
20
+ * --html HTML report
21
+ * --self-exclude Skip scanning self
22
+ * --strict Lower thresholds
23
+ * --summary-only Summary only
24
+ * --check-deps Scan dependencies
25
+ * --rules <file> Custom rules JSON
26
+ * --plugin <file> Load plugin module
27
+ * --fail-on-findings Exit 1 on findings (CI/CD)
28
+ * --help, -h Help
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const { GuardScanner, VERSION } = require('./scanner.js');
34
+
35
+ const args = process.argv.slice(2);
36
+
37
+ if (args.includes('--help') || args.includes('-h')) {
38
+ console.log(`
39
+ 🛡️ guard-scanner v${VERSION} — Agent Skill Security Scanner
40
+
41
+ Usage: guard-scanner [scan-dir] [options]
42
+
43
+ Options:
44
+ --verbose, -v Detailed findings with categories and samples
45
+ --json Write JSON report to file
46
+ --sarif Write SARIF report to file (GitHub Code Scanning / CI/CD)
47
+ --html Write HTML report (visual dashboard)
48
+ --format json|sarif Print JSON or SARIF to stdout (pipeable, v3.2.0)
49
+ --quiet Suppress all text output (use with --format for clean pipes)
50
+ --self-exclude Skip scanning the guard-scanner skill itself
51
+ --strict Lower detection thresholds (more sensitive)
52
+ --summary-only Only print the summary table
53
+ --check-deps Scan package.json for dependency chain risks
54
+ --soul-lock Enable Soul Lock patterns (agent identity protection)
55
+ --rules <file> Load custom rules from JSON file
56
+ --plugin <file> Load plugin module (JS file exporting { name, patterns })
57
+ --fail-on-findings Exit code 1 if any findings (CI/CD)
58
+ --help, -h Show this help
59
+
60
+ Custom Rules JSON Format:
61
+ [
62
+ {
63
+ "id": "CUSTOM_001",
64
+ "pattern": "dangerous_function\\\\(",
65
+ "flags": "gi",
66
+ "severity": "HIGH",
67
+ "cat": "malicious-code",
68
+ "desc": "Custom: dangerous function call",
69
+ "codeOnly": true
70
+ }
71
+ ]
72
+
73
+ Plugin API:
74
+ // my-plugin.js
75
+ module.exports = {
76
+ name: 'my-plugin',
77
+ patterns: [
78
+ { id: 'MY_01', cat: 'custom', regex: /pattern/g, severity: 'HIGH', desc: 'Description', all: true }
79
+ ]
80
+ };
81
+
82
+ Examples:
83
+ guard-scanner ./skills/ --verbose --self-exclude
84
+ guard-scanner ./skills/ --strict --json --sarif --check-deps
85
+ guard-scanner ./skills/ --html --verbose --check-deps
86
+ guard-scanner ./skills/ --rules my-rules.json --fail-on-findings
87
+ guard-scanner ./skills/ --plugin ./my-plugin.js
88
+ `);
89
+ process.exit(0);
90
+ }
91
+
92
+ const verbose = args.includes('--verbose') || args.includes('-v');
93
+ const jsonOutput = args.includes('--json');
94
+ const sarifOutput = args.includes('--sarif');
95
+ const htmlOutput = args.includes('--html');
96
+ const selfExclude = args.includes('--self-exclude');
97
+ const strict = args.includes('--strict');
98
+ const summaryOnly = args.includes('--summary-only');
99
+ const checkDeps = args.includes('--check-deps');
100
+ const soulLock = args.includes('--soul-lock');
101
+ const failOnFindings = args.includes('--fail-on-findings');
102
+ const quietMode = args.includes('--quiet');
103
+
104
+ // --format json|sarif → stdout output (v3.2.0)
105
+ const formatIdx = args.indexOf('--format');
106
+ const formatValue = formatIdx >= 0 ? args[formatIdx + 1] : null;
107
+
108
+ const rulesIdx = args.indexOf('--rules');
109
+ const rulesFile = rulesIdx >= 0 ? args[rulesIdx + 1] : null;
110
+
111
+ // Collect plugins
112
+ const plugins = [];
113
+ let idx = 0;
114
+ while (idx < args.length) {
115
+ if (args[idx] === '--plugin' && args[idx + 1]) {
116
+ plugins.push(args[idx + 1]);
117
+ idx += 2;
118
+ } else {
119
+ idx++;
120
+ }
121
+ }
122
+
123
+ const scanDir = args.find(a =>
124
+ !a.startsWith('-') &&
125
+ a !== rulesFile &&
126
+ a !== formatValue &&
127
+ !plugins.includes(a)
128
+ ) || process.cwd();
129
+
130
+ const scanner = new GuardScanner({
131
+ verbose, selfExclude, strict, summaryOnly, checkDeps, soulLock, rulesFile, plugins,
132
+ quiet: quietMode || !!formatValue,
133
+ });
134
+
135
+ scanner.scanDirectory(scanDir);
136
+
137
+ // Output reports (file-based, backward compatible)
138
+ if (jsonOutput) {
139
+ const report = scanner.toJSON();
140
+ const outPath = path.join(scanDir, 'guard-scanner-report.json');
141
+ fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
142
+ if (!quietMode && !formatValue) console.log(`\n📄 JSON report: ${outPath}`);
143
+ }
144
+
145
+ if (sarifOutput) {
146
+ const outPath = path.join(scanDir, 'guard-scanner.sarif');
147
+ fs.writeFileSync(outPath, JSON.stringify(scanner.toSARIF(scanDir), null, 2));
148
+ if (!quietMode && !formatValue) console.log(`\n📄 SARIF report: ${outPath}`);
149
+ }
150
+
151
+ if (htmlOutput) {
152
+ const outPath = path.join(scanDir, 'guard-scanner-report.html');
153
+ fs.writeFileSync(outPath, scanner.toHTML());
154
+ if (!quietMode && !formatValue) console.log(`\n📄 HTML report: ${outPath}`);
155
+ }
156
+
157
+ // --format stdout output (v3.2.0)
158
+ if (formatValue === 'json') {
159
+ process.stdout.write(JSON.stringify(scanner.toJSON(), null, 2) + '\n');
160
+ } else if (formatValue === 'sarif') {
161
+ process.stdout.write(JSON.stringify(scanner.toSARIF(scanDir), null, 2) + '\n');
162
+ } else if (formatValue) {
163
+ console.error(`❌ Unknown format: ${formatValue}. Use 'json' or 'sarif'.`);
164
+ process.exit(2);
165
+ }
166
+
167
+ // Exit codes
168
+ if (scanner.stats.malicious > 0) process.exit(1);
169
+ if (failOnFindings && scanner.findings.length > 0) process.exit(1);
170
+ process.exit(0);