@guava-parity/guard-scanner 13.0.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -253
- package/SECURITY.md +12 -4
- package/SKILL.md +121 -59
- package/dist/openclaw-plugin.mjs +41 -0
- package/docs/EVIDENCE_DRIVEN.md +182 -0
- package/docs/banner.png +0 -0
- package/docs/data/corpus-metrics.json +11 -0
- package/docs/data/latest.json +25837 -2481
- package/docs/generated/npm-audit-20260312.json +96 -0
- package/docs/generated/openclaw-upstream-status.json +25 -0
- package/docs/glossary.md +46 -0
- package/docs/index.html +1085 -496
- package/docs/logo.png +0 -0
- package/docs/openclaw-compatibility-audit.md +44 -0
- package/docs/openclaw-continuous-compatibility-plan.md +36 -0
- package/docs/rules/a2a-contagion.md +68 -0
- package/docs/rules/advanced-exfil.md +52 -0
- package/docs/rules/agent-protocol.md +108 -0
- package/docs/rules/api-abuse.md +68 -0
- package/docs/rules/autonomous-risk.md +92 -0
- package/docs/rules/config-impact.md +132 -0
- package/docs/rules/credential-handling.md +100 -0
- package/docs/rules/cve-patterns.md +332 -0
- package/docs/rules/data-exposure.md +84 -0
- package/docs/rules/exfiltration.md +36 -0
- package/docs/rules/financial-access.md +84 -0
- package/docs/rules/identity-hijack.md +140 -0
- package/docs/rules/inference-manipulation.md +60 -0
- package/docs/rules/leaky-skills.md +52 -0
- package/docs/rules/malicious-code.md +108 -0
- package/docs/rules/mcp-security.md +148 -0
- package/docs/rules/memory-poisoning.md +84 -0
- package/docs/rules/model-poisoning.md +44 -0
- package/docs/rules/obfuscation.md +60 -0
- package/docs/rules/persistence.md +108 -0
- package/docs/rules/pii-exposure.md +116 -0
- package/docs/rules/prompt-injection.md +148 -0
- package/docs/rules/prompt-worm.md +44 -0
- package/docs/rules/safeguard-bypass.md +44 -0
- package/docs/rules/sandbox-escape.md +100 -0
- package/docs/rules/secret-detection.md +44 -0
- package/docs/rules/supply-chain-v2.md +92 -0
- package/docs/rules/suspicious-download.md +60 -0
- package/docs/rules/trust-boundary.md +76 -0
- package/docs/rules/trust-exploitation.md +92 -0
- package/docs/rules/unverifiable-deps.md +84 -0
- package/docs/rules/vdb-injection.md +84 -0
- package/docs/security-vulnerability-report-20260312.md +53 -0
- package/docs/spec/PRD_V2_ARCHITECTURE.md +55 -0
- package/docs/spec/capabilities.json +42 -0
- package/docs/spec/finding.schema.json +104 -0
- package/docs/spec/integration-manifest.md +39 -0
- package/docs/spec/sbom.json +33 -0
- package/docs/threat-model.md +65 -0
- package/docs/v13-architecture-manifest.md +55 -0
- package/hooks/context.js +305 -0
- package/hooks/guard-scanner/plugin.ts +24 -1
- package/openclaw-plugin.mts +91 -0
- package/openclaw.plugin.json +30 -53
- package/package.json +23 -8
- package/src/cli.js +174 -34
- package/src/core/content-loader.js +42 -0
- package/src/core/inventory.js +73 -0
- package/src/core/report-adapters.js +171 -0
- package/src/core/risk-engine.js +93 -0
- package/src/core/rule-registry.js +73 -0
- package/src/core/semantic-validators.js +85 -0
- package/src/finding-schema.js +191 -0
- package/src/hooks/context.ts +49 -0
- package/src/html-template.js +2 -2
- package/src/mcp-server.js +24 -73
- package/src/openclaw-upstream.js +128 -0
- package/src/patterns.js +371 -353
- package/src/policy-engine.js +32 -0
- package/src/runtime-guard.js +40 -2
- package/src/scanner.js +101 -216
- package/src/skill-crawler.js +254 -0
- package/src/threat-model.js +50 -0
- package/src/validation-layer.js +39 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class PolicyEngine {
|
|
2
|
+
constructor(config = { mode: 'enforce' }) {
|
|
3
|
+
this.mode = config.mode;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
evaluate(toolName, args) {
|
|
7
|
+
if (this.mode === 'monitor') {
|
|
8
|
+
return { action: 'allow', reason: 'monitor mode' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const argsStr = JSON.stringify(args).toLowerCase();
|
|
12
|
+
|
|
13
|
+
// Destructive FS operations
|
|
14
|
+
if (toolName === 'run_shell_command' && (argsStr.includes('rm -rf') || argsStr.includes('mkfs'))) {
|
|
15
|
+
return { action: 'block', reason: 'destructive fs operation' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Credential access
|
|
19
|
+
if (toolName === 'read_file' && (argsStr.includes('.env') || argsStr.includes('secret') || argsStr.includes('.aws'))) {
|
|
20
|
+
return { action: 'block', reason: 'credential read operation' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Unrestricted network
|
|
24
|
+
if (toolName === 'run_shell_command' && (argsStr.includes('curl') || argsStr.includes('wget')) && argsStr.includes('| bash')) {
|
|
25
|
+
return { action: 'block', reason: 'unrestricted network execution (curl|bash)' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { action: 'allow', reason: 'safe operation' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { PolicyEngine };
|
package/src/runtime-guard.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
const fs = require('fs');
|
|
34
34
|
const path = require('path');
|
|
35
35
|
const os = require('os');
|
|
36
|
+
const { normalizeFinding } = require('./finding-schema.js');
|
|
36
37
|
|
|
37
38
|
// ── Runtime threat patterns (26 checks, 5 layers) ──
|
|
38
39
|
|
|
@@ -85,6 +86,11 @@ const RUNTIME_CHECKS = [
|
|
|
85
86
|
desc: 'Download piped to shell',
|
|
86
87
|
test: (s) => /(curl|wget)\s+[^\n]*\|\s*(sh|bash|zsh)/i.test(s),
|
|
87
88
|
},
|
|
89
|
+
{
|
|
90
|
+
id: 'RT_ENV_CURL_EXFIL', severity: 'CRITICAL', layer: 1,
|
|
91
|
+
desc: 'Environment variable exfiltration piped to curl upload',
|
|
92
|
+
test: (s) => /env\s*\|\s*curl\b[^\n]*-d\s+@-/i.test(s),
|
|
93
|
+
},
|
|
88
94
|
{
|
|
89
95
|
id: 'RT_SSH_READ', severity: 'HIGH', layer: 1,
|
|
90
96
|
desc: 'SSH private key access',
|
|
@@ -247,6 +253,9 @@ function shouldBlock(severity, mode) {
|
|
|
247
253
|
* @param {string} [options.mode] - Override mode ('monitor' | 'enforce' | 'strict')
|
|
248
254
|
* @param {boolean} [options.auditLog=true] - Enable audit logging
|
|
249
255
|
* @param {string} [options.sessionKey] - Session identifier for audit
|
|
256
|
+
* @param {string} [options.sessionId] - Ephemeral OpenClaw session UUID for audit
|
|
257
|
+
* @param {string} [options.runId] - Stable OpenClaw run identifier for audit
|
|
258
|
+
* @param {string} [options.toolCallId] - Provider tool call identifier for audit
|
|
250
259
|
* @param {string} [options.agentId] - Agent identifier for audit
|
|
251
260
|
* @returns {{ blocked: boolean, detections: Array<{id: string, severity: string, layer: number, desc: string, action: string}> }}
|
|
252
261
|
*/
|
|
@@ -254,6 +263,9 @@ function scanToolCall(toolName, params, options = {}) {
|
|
|
254
263
|
const mode = options.mode || loadMode();
|
|
255
264
|
const enableAudit = options.auditLog !== false;
|
|
256
265
|
const sessionKey = options.sessionKey || 'unknown';
|
|
266
|
+
const sessionId = options.sessionId || 'unknown';
|
|
267
|
+
const runId = options.runId || 'unknown';
|
|
268
|
+
const toolCallId = options.toolCallId || 'unknown';
|
|
257
269
|
const agentId = options.agentId || 'unknown';
|
|
258
270
|
|
|
259
271
|
const result = {
|
|
@@ -275,14 +287,29 @@ function scanToolCall(toolName, params, options = {}) {
|
|
|
275
287
|
if (!check.test(serialized)) continue;
|
|
276
288
|
|
|
277
289
|
const action = shouldBlock(check.severity, mode) ? 'blocked' : 'warned';
|
|
290
|
+
const paramsPreview = serialized.length > 200 ? `${serialized.slice(0, 200)}…` : serialized;
|
|
278
291
|
|
|
279
|
-
const detection = {
|
|
292
|
+
const detection = normalizeFinding({
|
|
280
293
|
id: check.id,
|
|
294
|
+
category: LAYER_CATEGORIES[check.layer] || 'runtime-guard',
|
|
281
295
|
severity: check.severity,
|
|
282
296
|
layer: check.layer,
|
|
283
297
|
desc: check.desc,
|
|
284
298
|
action,
|
|
285
|
-
|
|
299
|
+
rationale: check.rationale || `Runtime guard matched ${check.desc.toLowerCase()} before the tool call executed.`,
|
|
300
|
+
preconditions: check.preconditions || 'The tool call arguments must reach the runtime enforcement hook with attacker-controlled or unsafe content.',
|
|
301
|
+
false_positive_scenarios: check.falsePositiveScenarios || [
|
|
302
|
+
'The arguments are part of a security test, audit note, or documentation sample and are not actually executed.',
|
|
303
|
+
'The command is legitimate but still requires explicit human review before execution.',
|
|
304
|
+
],
|
|
305
|
+
remediation_hint: check.remediationHint || 'Review the tool arguments, remove the unsafe construct, and rerun only after an allowlisted human review.',
|
|
306
|
+
}, {
|
|
307
|
+
source: 'runtime',
|
|
308
|
+
toolName,
|
|
309
|
+
paramsPreview,
|
|
310
|
+
layer_name: LAYER_NAMES[check.layer],
|
|
311
|
+
ruleMetadata: check,
|
|
312
|
+
});
|
|
286
313
|
|
|
287
314
|
result.detections.push(detection);
|
|
288
315
|
|
|
@@ -296,6 +323,9 @@ function scanToolCall(toolName, params, options = {}) {
|
|
|
296
323
|
mode,
|
|
297
324
|
action,
|
|
298
325
|
session: sessionKey,
|
|
326
|
+
sessionId,
|
|
327
|
+
runId,
|
|
328
|
+
toolCallId,
|
|
299
329
|
agent: agentId,
|
|
300
330
|
});
|
|
301
331
|
}
|
|
@@ -332,6 +362,14 @@ const LAYER_NAMES = {
|
|
|
332
362
|
5: 'Trust Exploitation (ASI09)',
|
|
333
363
|
};
|
|
334
364
|
|
|
365
|
+
const LAYER_CATEGORIES = {
|
|
366
|
+
1: 'threat-detection',
|
|
367
|
+
2: 'trust-defense',
|
|
368
|
+
3: 'safety-judge',
|
|
369
|
+
4: 'behavioral-guard',
|
|
370
|
+
5: 'trust-exploitation',
|
|
371
|
+
};
|
|
372
|
+
|
|
335
373
|
module.exports = {
|
|
336
374
|
RUNTIME_CHECKS,
|
|
337
375
|
DANGEROUS_TOOLS,
|
package/src/scanner.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Based on GuavaGuard v9.0.0 (OSS extraction)
|
|
15
15
|
* 20 threat categories • Snyk ToxicSkills + OWASP MCP Top 10
|
|
16
|
-
*
|
|
16
|
+
* Lightweight runtime footprint • CLI + JSON + SARIF + HTML output
|
|
17
17
|
* Plugin API for custom detection rules
|
|
18
18
|
*
|
|
19
19
|
* Born from a real 3-day agent identity hijack (2026-02-12)
|
|
@@ -24,30 +24,24 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const os = require('os');
|
|
27
|
-
const crypto = require('crypto');
|
|
28
27
|
|
|
29
28
|
const { PATTERNS } = require('./patterns.js');
|
|
30
29
|
const { KNOWN_MALICIOUS } = require('./ioc-db.js');
|
|
31
|
-
const {
|
|
30
|
+
const { RuleRegistry } = require('./core/rule-registry.js');
|
|
31
|
+
const { loadIgnoreFile, loadTextFile } = require('./core/content-loader.js');
|
|
32
|
+
const { classifyFile, CODE_EXTENSIONS, BINARY_EXTENSIONS, isSelfNoisePath, isSelfThreatCorpus, getFiles, listSkills } = require('./core/inventory.js');
|
|
33
|
+
const { calculateRisk, getVerdict, SEVERITY_WEIGHTS } = require('./core/risk-engine.js');
|
|
34
|
+
const { applySemanticValidators, checkASTValidation } = require('./core/semantic-validators.js');
|
|
35
|
+
const { toJSONReport, toSARIFReport, toHTMLReport, printSummary } = require('./core/report-adapters.js');
|
|
32
36
|
|
|
33
37
|
// ===== CONFIGURATION =====
|
|
34
|
-
const VERSION = '
|
|
38
|
+
const { version: VERSION } = require('../package.json');
|
|
35
39
|
|
|
36
40
|
const THRESHOLDS = {
|
|
37
41
|
normal: { suspicious: 30, malicious: 80 },
|
|
38
42
|
strict: { suspicious: 20, malicious: 60 },
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
// File classification
|
|
42
|
-
const CODE_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.py', '.sh', '.bash', '.ps1', '.rb', '.go', '.rs', '.php', '.pl']);
|
|
43
|
-
const DOC_EXTENSIONS = new Set(['.md', '.txt', '.rst', '.adoc']);
|
|
44
|
-
const DATA_EXTENSIONS = new Set(['.json', '.yaml', '.yml', '.toml', '.xml', '.csv']);
|
|
45
|
-
const BINARY_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.wasm', '.wav', '.mp3', '.mp4', '.webm', '.ogg', '.pdf', '.zip', '.tar', '.gz', '.bz2', '.7z', '.exe', '.dll', '.so', '.dylib']);
|
|
46
|
-
const GENERATED_REPORT_FILES = new Set(['guard-scanner-report.json', 'guard-scanner-report.html', 'guard-scanner.sarif']);
|
|
47
|
-
|
|
48
|
-
// Severity weights for risk scoring
|
|
49
|
-
const SEVERITY_WEIGHTS = { CRITICAL: 40, HIGH: 15, MEDIUM: 5, LOW: 2 };
|
|
50
|
-
|
|
51
45
|
class GuardScanner {
|
|
52
46
|
constructor(options = {}) {
|
|
53
47
|
this.verbose = options.verbose || false;
|
|
@@ -76,6 +70,8 @@ class GuardScanner {
|
|
|
76
70
|
if (options.rulesFile) {
|
|
77
71
|
this.loadCustomRules(options.rulesFile);
|
|
78
72
|
}
|
|
73
|
+
|
|
74
|
+
this.ruleRegistry = new RuleRegistry(PATTERNS, this.customRules);
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
// Plugin API: load a plugin module
|
|
@@ -91,6 +87,7 @@ class GuardScanner {
|
|
|
91
87
|
if (!this.summaryOnly) {
|
|
92
88
|
console.log(`🔌 Plugin loaded: ${plugin.name || pluginPath} (${plugin.patterns.length} rule(s))`);
|
|
93
89
|
}
|
|
90
|
+
this.ruleRegistry = new RuleRegistry(PATTERNS, this.customRules);
|
|
94
91
|
}
|
|
95
92
|
} catch (e) {
|
|
96
93
|
console.error(`⚠️ Failed to load plugin ${pluginPath}: ${e.message}`);
|
|
@@ -130,6 +127,7 @@ class GuardScanner {
|
|
|
130
127
|
if (!this.summaryOnly && this.customRules.length > 0) {
|
|
131
128
|
console.log(`📏 Loaded ${this.customRules.length} custom rule(s) from ${rulesFile}`);
|
|
132
129
|
}
|
|
130
|
+
this.ruleRegistry = new RuleRegistry(PATTERNS, this.customRules);
|
|
133
131
|
} catch (e) {
|
|
134
132
|
console.error(`⚠️ Failed to load custom rules: ${e.message}`);
|
|
135
133
|
}
|
|
@@ -137,26 +135,11 @@ class GuardScanner {
|
|
|
137
135
|
|
|
138
136
|
// Load .guava-guard-ignore / .guard-scanner-ignore from scan directory
|
|
139
137
|
loadIgnoreFile(scanDir) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!fs.existsSync(ignorePath)) continue;
|
|
146
|
-
const lines = fs.readFileSync(ignorePath, 'utf-8').split('\n');
|
|
147
|
-
for (const line of lines) {
|
|
148
|
-
const trimmed = line.trim();
|
|
149
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
150
|
-
if (trimmed.startsWith('pattern:')) {
|
|
151
|
-
this.ignoredPatterns.add(trimmed.replace('pattern:', '').trim());
|
|
152
|
-
} else {
|
|
153
|
-
this.ignoredSkills.add(trimmed);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (this.verbose && (this.ignoredSkills.size || this.ignoredPatterns.size)) {
|
|
157
|
-
console.log(`📋 Loaded ignore file: ${this.ignoredSkills.size} skills, ${this.ignoredPatterns.size} patterns`);
|
|
158
|
-
}
|
|
159
|
-
break; // use first found
|
|
138
|
+
const ignored = loadIgnoreFile(scanDir);
|
|
139
|
+
this.ignoredSkills = ignored.ignoredSkills;
|
|
140
|
+
this.ignoredPatterns = ignored.ignoredPatterns;
|
|
141
|
+
if (this.verbose && (this.ignoredSkills.size || this.ignoredPatterns.size)) {
|
|
142
|
+
console.log(`📋 Loaded ignore file: ${this.ignoredSkills.size} skills, ${this.ignoredPatterns.size} patterns`);
|
|
160
143
|
}
|
|
161
144
|
}
|
|
162
145
|
|
|
@@ -172,6 +155,7 @@ class GuardScanner {
|
|
|
172
155
|
if (this.customRules.length > 0) {
|
|
173
156
|
this.checkPatterns(text, 'raw_text', 'code', findings, this.customRules);
|
|
174
157
|
}
|
|
158
|
+
applySemanticValidators(text, 'raw_text', findings);
|
|
175
159
|
|
|
176
160
|
// Filter ignored patterns
|
|
177
161
|
const filteredFindings = findings.filter(f => !this.ignoredPatterns.has(f.id));
|
|
@@ -186,16 +170,12 @@ class GuardScanner {
|
|
|
186
170
|
|
|
187
171
|
scanDirectory(dir) {
|
|
188
172
|
if (!fs.existsSync(dir)) {
|
|
189
|
-
|
|
190
|
-
process.exit(2);
|
|
173
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
191
174
|
}
|
|
192
175
|
|
|
193
176
|
this.loadIgnoreFile(dir);
|
|
194
177
|
|
|
195
|
-
const skills =
|
|
196
|
-
const p = path.join(dir, f);
|
|
197
|
-
return fs.statSync(p).isDirectory();
|
|
198
|
-
});
|
|
178
|
+
const skills = listSkills(dir);
|
|
199
179
|
|
|
200
180
|
if (!this.quiet) {
|
|
201
181
|
console.log(`\n🛡️ guard-scanner v${VERSION}`);
|
|
@@ -228,6 +208,13 @@ class GuardScanner {
|
|
|
228
208
|
return this.findings;
|
|
229
209
|
}
|
|
230
210
|
|
|
211
|
+
scanTarget(targetPath) {
|
|
212
|
+
this.findings = [];
|
|
213
|
+
this.stats = { scanned: 0, clean: 0, low: 0, suspicious: 0, malicious: 0 };
|
|
214
|
+
this.scanDirectory(targetPath);
|
|
215
|
+
return this.toJSON();
|
|
216
|
+
}
|
|
217
|
+
|
|
231
218
|
scanSkill(skillPath, skillName) {
|
|
232
219
|
this.stats.scanned++;
|
|
233
220
|
const skillFindings = [];
|
|
@@ -252,9 +239,8 @@ class GuardScanner {
|
|
|
252
239
|
if (BINARY_EXTENSIONS.has(ext)) continue;
|
|
253
240
|
if (this.isSelfNoisePath(skillName, relFile)) continue;
|
|
254
241
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (content.length > 500000) continue;
|
|
242
|
+
const content = loadTextFile(file);
|
|
243
|
+
if (content === null) continue;
|
|
258
244
|
|
|
259
245
|
const fileType = this.classifyFile(ext, relFile);
|
|
260
246
|
|
|
@@ -284,6 +270,7 @@ class GuardScanner {
|
|
|
284
270
|
if ((ext === '.js' || ext === '.mjs' || ext === '.cjs' || ext === '.ts') && content.length < 200000) {
|
|
285
271
|
this.checkJSDataFlow(content, relFile, skillFindings);
|
|
286
272
|
}
|
|
273
|
+
applySemanticValidators(content, relFile, skillFindings);
|
|
287
274
|
}
|
|
288
275
|
|
|
289
276
|
// Check 3: Structural checks
|
|
@@ -344,27 +331,15 @@ class GuardScanner {
|
|
|
344
331
|
}
|
|
345
332
|
|
|
346
333
|
classifyFile(ext, relFile) {
|
|
347
|
-
|
|
348
|
-
if (DOC_EXTENSIONS.has(ext)) return 'doc';
|
|
349
|
-
if (DATA_EXTENSIONS.has(ext)) return 'data';
|
|
350
|
-
const base = path.basename(relFile).toLowerCase();
|
|
351
|
-
if (base === 'skill.md' || base === 'readme.md') return 'skill-doc';
|
|
352
|
-
return 'other';
|
|
334
|
+
return classifyFile(ext, relFile);
|
|
353
335
|
}
|
|
354
336
|
|
|
355
337
|
isSelfNoisePath(skillName, relFile) {
|
|
356
|
-
|
|
357
|
-
return /^test\//.test(relFile)
|
|
358
|
-
|| /^dist\/__tests__\//.test(relFile)
|
|
359
|
-
|| /^ts-src\/__tests__\//.test(relFile)
|
|
360
|
-
|| /^docs\//.test(relFile)
|
|
361
|
-
|| relFile === 'ROADMAP-RESEARCH.md'
|
|
362
|
-
|| relFile === 'CHANGELOG.md';
|
|
338
|
+
return isSelfNoisePath(skillName, relFile);
|
|
363
339
|
}
|
|
364
340
|
|
|
365
341
|
isSelfThreatCorpus(skillName, relFile) {
|
|
366
|
-
|
|
367
|
-
return /(^|\/)(ioc-db|patterns)\.(js|ts)$/.test(relFile);
|
|
342
|
+
return isSelfThreatCorpus(skillName, relFile);
|
|
368
343
|
}
|
|
369
344
|
|
|
370
345
|
checkIoCs(content, relFile, findings) {
|
|
@@ -402,7 +377,8 @@ class GuardScanner {
|
|
|
402
377
|
}
|
|
403
378
|
}
|
|
404
379
|
|
|
405
|
-
checkPatterns(content, relFile, fileType, findings, patterns =
|
|
380
|
+
checkPatterns(content, relFile, fileType, findings, patterns = null) {
|
|
381
|
+
const activePatterns = patterns || this.ruleRegistry.getRulesForFileType(fileType);
|
|
406
382
|
// v9: Payload Unfurling (Base64 / Hex Decoders)
|
|
407
383
|
let unfurledContent = content;
|
|
408
384
|
|
|
@@ -420,12 +396,12 @@ class GuardScanner {
|
|
|
420
396
|
return String.fromCharCode(parseInt(hex, 16));
|
|
421
397
|
});
|
|
422
398
|
|
|
423
|
-
for (const pattern of
|
|
399
|
+
for (const pattern of activePatterns) {
|
|
424
400
|
// Soul Lock: skip identity-hijack/memory-poisoning patterns unless --soul-lock is enabled
|
|
425
401
|
if (pattern.soulLock && !this.soulLock) continue;
|
|
426
402
|
if (pattern.codeOnly && fileType !== 'code') continue;
|
|
427
403
|
if (pattern.docOnly && fileType !== 'doc' && fileType !== 'skill-doc') continue;
|
|
428
|
-
if (!pattern.all && !pattern.codeOnly && !pattern.docOnly) continue;
|
|
404
|
+
if (!pattern.all && !pattern.codeOnly && !pattern.docOnly && pattern.scope !== 'skill-doc') continue;
|
|
429
405
|
|
|
430
406
|
pattern.regex.lastIndex = 0;
|
|
431
407
|
let matches = content.match(pattern.regex);
|
|
@@ -453,8 +429,8 @@ class GuardScanner {
|
|
|
453
429
|
findings.push({
|
|
454
430
|
severity: adjustedSeverity,
|
|
455
431
|
id: pattern.id,
|
|
456
|
-
cat: pattern.cat,
|
|
457
|
-
desc: pattern.desc,
|
|
432
|
+
cat: pattern.cat || pattern.category,
|
|
433
|
+
desc: pattern.desc || pattern.description,
|
|
458
434
|
file: relFile,
|
|
459
435
|
line: lineNum,
|
|
460
436
|
matchCount: matches.length,
|
|
@@ -975,181 +951,90 @@ class GuardScanner {
|
|
|
975
951
|
}
|
|
976
952
|
|
|
977
953
|
calculateRisk(findings) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
let score = 0;
|
|
981
|
-
for (const f of findings) {
|
|
982
|
-
score += SEVERITY_WEIGHTS[f.severity] || 0;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const ids = new Set(findings.map(f => f.id));
|
|
986
|
-
const cats = new Set(findings.map(f => f.cat));
|
|
987
|
-
|
|
988
|
-
if (cats.has('credential-handling') && cats.has('exfiltration')) score = Math.round(score * 2);
|
|
989
|
-
if (cats.has('credential-handling') && findings.some(f => f.id === 'MAL_CHILD' || f.id === 'MAL_EXEC')) score = Math.round(score * 1.5);
|
|
990
|
-
if (cats.has('obfuscation') && (cats.has('malicious-code') || cats.has('credential-handling'))) score = Math.round(score * 2);
|
|
991
|
-
if (ids.has('DEP_LIFECYCLE_EXEC')) score = Math.round(score * 2);
|
|
992
|
-
if (ids.has('PI_BIDI') && findings.length > 1) score = Math.round(score * 1.5);
|
|
993
|
-
if (cats.has('leaky-skills') && (cats.has('exfiltration') || cats.has('malicious-code'))) score = Math.round(score * 2);
|
|
994
|
-
if (cats.has('memory-poisoning')) score = Math.round(score * 1.5);
|
|
995
|
-
if (cats.has('prompt-worm')) score = Math.round(score * 2);
|
|
996
|
-
if (cats.has('cve-patterns')) score = Math.max(score, 70);
|
|
997
|
-
if (cats.has('persistence') && (cats.has('malicious-code') || cats.has('credential-handling') || cats.has('memory-poisoning'))) score = Math.round(score * 1.5);
|
|
998
|
-
if (cats.has('identity-hijack')) score = Math.round(score * 2);
|
|
999
|
-
if (cats.has('identity-hijack') && (cats.has('persistence') || cats.has('memory-poisoning'))) score = Math.max(score, 90);
|
|
1000
|
-
if (ids.has('IOC_IP') || ids.has('IOC_URL') || ids.has('KNOWN_TYPOSQUAT')) score = 100;
|
|
1001
|
-
|
|
1002
|
-
// v1.1 categories
|
|
1003
|
-
if (cats.has('config-impact')) score = Math.round(score * 2);
|
|
1004
|
-
if (cats.has('config-impact') && cats.has('sandbox-validation')) score = Math.max(score, 70);
|
|
1005
|
-
if (cats.has('complexity') && (cats.has('malicious-code') || cats.has('obfuscation'))) score = Math.round(score * 1.5);
|
|
1006
|
-
|
|
1007
|
-
// v2.1 PII exposure amplifiers
|
|
1008
|
-
if (cats.has('pii-exposure') && cats.has('exfiltration')) score = Math.round(score * 3);
|
|
1009
|
-
if (cats.has('pii-exposure') && (ids.has('SHADOW_AI_OPENAI') || ids.has('SHADOW_AI_ANTHROPIC') || ids.has('SHADOW_AI_GENERIC'))) score = Math.round(score * 2.5);
|
|
1010
|
-
if (cats.has('pii-exposure') && cats.has('credential-handling')) score = Math.round(score * 2);
|
|
1011
|
-
|
|
1012
|
-
return Math.min(100, score);
|
|
954
|
+
return calculateRisk(findings);
|
|
1013
955
|
}
|
|
1014
956
|
|
|
1015
957
|
getVerdict(risk) {
|
|
1016
|
-
|
|
1017
|
-
if (risk >= this.thresholds.suspicious) return { icon: '🟡', label: 'SUSPICIOUS', stat: 'suspicious' };
|
|
1018
|
-
if (risk > 0) return { icon: '🟢', label: 'LOW RISK', stat: 'low' };
|
|
1019
|
-
return { icon: '🟢', label: 'CLEAN', stat: 'clean' };
|
|
958
|
+
return getVerdict(risk, this.thresholds);
|
|
1020
959
|
}
|
|
1021
960
|
|
|
1022
961
|
getFiles(dir) {
|
|
1023
|
-
|
|
1024
|
-
try {
|
|
1025
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1026
|
-
for (const entry of entries) {
|
|
1027
|
-
const fullPath = path.join(dir, entry.name);
|
|
1028
|
-
if (entry.isDirectory()) {
|
|
1029
|
-
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
1030
|
-
results.push(...this.getFiles(fullPath));
|
|
1031
|
-
} else {
|
|
1032
|
-
const baseName = entry.name.toLowerCase();
|
|
1033
|
-
if (GENERATED_REPORT_FILES.has(baseName)) continue;
|
|
1034
|
-
results.push(fullPath);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
} catch { }
|
|
1038
|
-
return results;
|
|
962
|
+
return getFiles(dir);
|
|
1039
963
|
}
|
|
1040
964
|
|
|
1041
965
|
printSummary() {
|
|
1042
|
-
|
|
1043
|
-
const safe = this.stats.clean + this.stats.low;
|
|
1044
|
-
console.log(`\n${'═'.repeat(54)}`);
|
|
1045
|
-
console.log(`📊 guard-scanner v${VERSION} Scan Summary`);
|
|
1046
|
-
console.log(`${'─'.repeat(54)}`);
|
|
1047
|
-
console.log(` Scanned: ${total}`);
|
|
1048
|
-
console.log(` 🟢 Clean: ${this.stats.clean}`);
|
|
1049
|
-
console.log(` 🟢 Low Risk: ${this.stats.low}`);
|
|
1050
|
-
console.log(` 🟡 Suspicious: ${this.stats.suspicious}`);
|
|
1051
|
-
console.log(` 🔴 Malicious: ${this.stats.malicious}`);
|
|
1052
|
-
console.log(` Safety Rate: ${total ? Math.round(safe / total * 100) : 0}%`);
|
|
1053
|
-
console.log(`${'═'.repeat(54)}`);
|
|
1054
|
-
|
|
1055
|
-
if (this.stats.malicious > 0) {
|
|
1056
|
-
console.log(`\n⚠️ CRITICAL: ${this.stats.malicious} malicious skill(s) detected!`);
|
|
1057
|
-
console.log(` Review findings with --verbose and remove if confirmed.`);
|
|
1058
|
-
} else if (this.stats.suspicious > 0) {
|
|
1059
|
-
console.log(`\n⚡ ${this.stats.suspicious} suspicious skill(s) found — review recommended.`);
|
|
1060
|
-
} else {
|
|
1061
|
-
console.log(`\n✅ All clear! No threats detected.`);
|
|
1062
|
-
}
|
|
966
|
+
return printSummary(this.stats, VERSION);
|
|
1063
967
|
}
|
|
1064
968
|
|
|
1065
969
|
toJSON() {
|
|
1066
|
-
|
|
1067
|
-
for (const skillResult of this.findings) {
|
|
1068
|
-
const skillRecs = [];
|
|
1069
|
-
const cats = new Set(skillResult.findings.map(f => f.cat));
|
|
1070
|
-
|
|
1071
|
-
if (cats.has('prompt-injection')) skillRecs.push('🛑 Contains prompt injection patterns.');
|
|
1072
|
-
if (cats.has('malicious-code')) skillRecs.push('🛑 Contains potentially malicious code.');
|
|
1073
|
-
if (cats.has('credential-handling') && cats.has('exfiltration')) skillRecs.push('💀 CRITICAL: Credential access + exfiltration. DO NOT INSTALL.');
|
|
1074
|
-
if (cats.has('dependency-chain')) skillRecs.push('📦 Suspicious dependency chain.');
|
|
1075
|
-
if (cats.has('obfuscation')) skillRecs.push('🔍 Code obfuscation detected.');
|
|
1076
|
-
if (cats.has('secret-detection')) skillRecs.push('🔑 Possible hardcoded secrets.');
|
|
1077
|
-
if (cats.has('leaky-skills')) skillRecs.push('💧 LEAKY SKILL: Secrets pass through LLM context.');
|
|
1078
|
-
if (cats.has('memory-poisoning')) skillRecs.push('🧠 MEMORY POISONING: Agent memory modification attempt.');
|
|
1079
|
-
if (cats.has('prompt-worm')) skillRecs.push('🪱 PROMPT WORM: Self-replicating instructions.');
|
|
1080
|
-
if (cats.has('data-flow')) skillRecs.push('🔀 Suspicious data flow patterns.');
|
|
1081
|
-
if (cats.has('persistence')) skillRecs.push('⏰ PERSISTENCE: Creates scheduled tasks.');
|
|
1082
|
-
if (cats.has('cve-patterns')) skillRecs.push('🚨 CVE PATTERN: Matches known exploits.');
|
|
1083
|
-
if (cats.has('identity-hijack')) skillRecs.push('🔒 IDENTITY HIJACK: Agent soul file tampering. DO NOT INSTALL.');
|
|
1084
|
-
if (cats.has('sandbox-validation')) skillRecs.push('🔒 SANDBOX: Skill requests dangerous capabilities.');
|
|
1085
|
-
if (cats.has('complexity')) skillRecs.push('🧩 COMPLEXITY: Excessive code complexity may hide malicious behavior.');
|
|
1086
|
-
if (cats.has('config-impact')) skillRecs.push('⚙️ CONFIG IMPACT: Modifies OpenClaw configuration. DO NOT INSTALL.');
|
|
1087
|
-
if (cats.has('pii-exposure')) skillRecs.push('🆔 PII EXPOSURE: Handles personally identifiable information. Review data handling.');
|
|
1088
|
-
|
|
1089
|
-
if (skillRecs.length > 0) recommendations.push({ skill: skillResult.skill, actions: skillRecs });
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
return {
|
|
1093
|
-
timestamp: new Date().toISOString(),
|
|
1094
|
-
scanner: `guard-scanner v${VERSION}`,
|
|
1095
|
-
mode: this.strict ? 'strict' : 'normal',
|
|
1096
|
-
stats: this.stats,
|
|
1097
|
-
thresholds: this.thresholds,
|
|
1098
|
-
findings: this.findings,
|
|
1099
|
-
recommendations,
|
|
1100
|
-
iocVersion: '2026-02-12',
|
|
1101
|
-
};
|
|
970
|
+
return toJSONReport(this, VERSION);
|
|
1102
971
|
}
|
|
1103
972
|
|
|
1104
973
|
toSARIF(scanDir) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
974
|
+
return toSARIFReport(this, VERSION, scanDir);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
toHTML() {
|
|
978
|
+
return toHTMLReport(this, VERSION);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Generate a Threat Model based on the scan findings.
|
|
983
|
+
* @param {Array<Object>} findings - The array of findings from the scan.
|
|
984
|
+
* @returns {Object} The generated threat model.
|
|
985
|
+
*/
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Check AST for contextual validation of high-risk chains.
|
|
989
|
+
* Separates heuristic-only matches from validated chains.
|
|
990
|
+
*/
|
|
991
|
+
checkASTValidation(content, relFile, findings) {
|
|
992
|
+
return checkASTValidation(content, relFile, findings);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
generateThreatModel(findings) {
|
|
996
|
+
const surface = {
|
|
997
|
+
network: false,
|
|
998
|
+
file_system: false,
|
|
999
|
+
code_execution: false,
|
|
1000
|
+
credential_exposure: false,
|
|
1001
|
+
external_ingestion: false,
|
|
1002
|
+
persistence: false
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
for (const f of findings) {
|
|
1006
|
+
// Map pattern IDs or categories to capability surfaces
|
|
1007
|
+
const id = f.id || '';
|
|
1008
|
+
const cat = f.cat || '';
|
|
1009
|
+
const desc = (f.desc || '').toLowerCase();
|
|
1010
|
+
|
|
1011
|
+
if (id.includes('FETCH') || id.includes('CURL') || id.includes('SSRF') || id.includes('NETWORK') || id.includes('EXFIL') || id.includes('TRUST_WEB_EXEC') || desc.includes('fetch') || desc.includes('network') || desc.includes('web content')) {
|
|
1012
|
+
surface.network = true;
|
|
1013
|
+
}
|
|
1014
|
+
if (id.includes('FS_') || id.includes('WRITE') || id.includes('READ') || id.includes('FILE') || id.includes('TRUST_WEB_EXEC') || desc.includes('file system') || desc.includes('readfilesync') || desc.includes('fs.read')) {
|
|
1015
|
+
surface.file_system = true;
|
|
1016
|
+
}
|
|
1017
|
+
if (id.includes('EXEC') || id.includes('EVAL') || id.includes('SHELL') || id.includes('SPAWN') || id.includes('RCE') || desc.includes('exec') || desc.includes('shell')) {
|
|
1018
|
+
surface.code_execution = true;
|
|
1019
|
+
}
|
|
1020
|
+
if (id.includes('CRED') || id.includes('KEY') || id.includes('SECRET') || id.includes('TOKEN') || cat.includes('credential') || desc.includes('credential') || desc.includes('trust boundary')) {
|
|
1021
|
+
surface.credential_exposure = true;
|
|
1022
|
+
}
|
|
1023
|
+
if (id.includes('PI_') || id.includes('PROMPT_INJECT') || id.includes('POISON') || id.includes('TRUST_WEB_EXEC') || cat.includes('prompt-injection') || desc.includes('ignore all')) {
|
|
1024
|
+
surface.external_ingestion = true;
|
|
1025
|
+
}
|
|
1026
|
+
if (id.includes('PERSIST') || id.includes('CRON') || id.includes('STARTUP') || cat.includes('persistence') || desc.includes('cron') || id.includes('DEPS_PHANTOM_IMPORT')) {
|
|
1027
|
+
surface.persistence = true;
|
|
1136
1028
|
}
|
|
1137
1029
|
}
|
|
1138
1030
|
|
|
1139
1031
|
return {
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
tool: { driver: { name: 'guard-scanner', version: VERSION, informationUri: 'https://github.com/koatora20/guard-scanner', rules } },
|
|
1144
|
-
results,
|
|
1145
|
-
invocations: [{ executionSuccessful: true, endTimeUtc: new Date().toISOString() }]
|
|
1146
|
-
}]
|
|
1032
|
+
timestamp: new Date().toISOString(),
|
|
1033
|
+
surface,
|
|
1034
|
+
summary: Object.keys(surface).filter(k => surface[k]).join(', ') || 'none'
|
|
1147
1035
|
};
|
|
1148
1036
|
}
|
|
1149
1037
|
|
|
1150
|
-
toHTML() {
|
|
1151
|
-
return generateHTML(VERSION, this.stats, this.findings);
|
|
1152
|
-
}
|
|
1153
1038
|
}
|
|
1154
1039
|
|
|
1155
1040
|
const { scanToolCall, RUNTIME_CHECKS, getCheckStats, LAYER_NAMES } = require('./runtime-guard.js');
|