@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,91 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const runtimeGuard = require("../src/runtime-guard.js") as {
|
|
6
|
+
scanToolCall: (
|
|
7
|
+
toolName: string,
|
|
8
|
+
params: Record<string, unknown>,
|
|
9
|
+
options?: {
|
|
10
|
+
mode?: "monitor" | "enforce" | "strict";
|
|
11
|
+
auditLog?: boolean;
|
|
12
|
+
sessionKey?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
runId?: string;
|
|
15
|
+
toolCallId?: string;
|
|
16
|
+
agentId?: string;
|
|
17
|
+
},
|
|
18
|
+
) => {
|
|
19
|
+
blocked: boolean;
|
|
20
|
+
blockReason: string | null;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type GuardMode = "monitor" | "enforce" | "strict";
|
|
25
|
+
type PluginHookBeforeToolCallEvent = {
|
|
26
|
+
toolName: string;
|
|
27
|
+
params: Record<string, unknown>;
|
|
28
|
+
runId?: string;
|
|
29
|
+
toolCallId?: string;
|
|
30
|
+
};
|
|
31
|
+
type PluginHookToolContext = {
|
|
32
|
+
agentId?: string;
|
|
33
|
+
sessionKey?: string;
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
runId?: string;
|
|
36
|
+
toolName: string;
|
|
37
|
+
toolCallId?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function resolveMode(pluginConfig?: Record<string, unknown>): GuardMode | undefined {
|
|
41
|
+
const mode = pluginConfig?.mode;
|
|
42
|
+
if (mode === "monitor" || mode === "enforce" || mode === "strict") {
|
|
43
|
+
return mode;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveAuditLog(pluginConfig?: Record<string, unknown>): boolean {
|
|
49
|
+
return pluginConfig?.auditLog !== false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function beforeToolCall(
|
|
53
|
+
event: PluginHookBeforeToolCallEvent,
|
|
54
|
+
ctx: PluginHookToolContext,
|
|
55
|
+
api: OpenClawPluginApi,
|
|
56
|
+
) {
|
|
57
|
+
const result = runtimeGuard.scanToolCall(event.toolName, event.params, {
|
|
58
|
+
mode: resolveMode(api.pluginConfig),
|
|
59
|
+
auditLog: resolveAuditLog(api.pluginConfig),
|
|
60
|
+
sessionKey: ctx.sessionKey,
|
|
61
|
+
sessionId: ctx.sessionId,
|
|
62
|
+
runId: ctx.runId ?? event.runId,
|
|
63
|
+
toolCallId: ctx.toolCallId ?? event.toolCallId,
|
|
64
|
+
agentId: ctx.agentId,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!result.blocked) return;
|
|
68
|
+
return {
|
|
69
|
+
block: true,
|
|
70
|
+
blockReason: result.blockReason ?? "guard-scanner blocked the tool call.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const plugin = {
|
|
75
|
+
id: "guard-scanner",
|
|
76
|
+
name: "guard-scanner",
|
|
77
|
+
version: "15.0.0",
|
|
78
|
+
description: "Runtime guard for OpenClaw before_tool_call hook execution.",
|
|
79
|
+
register(api: OpenClawPluginApi) {
|
|
80
|
+
api.on(
|
|
81
|
+
"before_tool_call",
|
|
82
|
+
(event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext) => beforeToolCall(event, ctx, api),
|
|
83
|
+
{ priority: 90 },
|
|
84
|
+
);
|
|
85
|
+
api.logger.info(
|
|
86
|
+
"guard-scanner registered OpenClaw before_tool_call hook (validated for v2026.3.8).",
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default plugin;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,55 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
"id": "guard-scanner",
|
|
3
|
+
"name": "guard-scanner",
|
|
4
|
+
"description": "Runtime guard plugin for OpenClaw before_tool_call enforcement.",
|
|
5
|
+
"version": "15.0.0",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"mode": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": [
|
|
12
|
+
"monitor",
|
|
13
|
+
"enforce",
|
|
14
|
+
"strict"
|
|
15
|
+
],
|
|
16
|
+
"default": "enforce",
|
|
17
|
+
"description": "monitor: log only | enforce: block CRITICAL | strict: block HIGH+CRITICAL"
|
|
18
|
+
},
|
|
19
|
+
"auditLog": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"default": true,
|
|
22
|
+
"description": "Enable audit logging to ~/.openclaw/guard-scanner/audit.jsonl"
|
|
23
|
+
},
|
|
24
|
+
"customRules": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Path to custom rules JSON file (optional)"
|
|
27
|
+
}
|
|
22
28
|
},
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
}
|
|
29
|
+
"required": [],
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guava-parity/guard-scanner",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "15.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
7
7
|
},
|
|
8
|
-
"description": "Agent Skill Security Scanner -
|
|
9
|
-
"openclaw
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
"description": "Agent Skill Security Scanner - ASI Sanctuary Enforcer (v15)",
|
|
9
|
+
"openclaw": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./dist/openclaw-plugin.mjs"
|
|
12
|
+
]
|
|
12
13
|
},
|
|
13
14
|
"main": "src/scanner.js",
|
|
14
15
|
"bin": {
|
|
15
16
|
"guard-scanner": "src/cli.js"
|
|
16
17
|
},
|
|
17
18
|
"scripts": {
|
|
19
|
+
"build:plugin": "npx tsc -p tsconfig.build.json",
|
|
20
|
+
"check:upstream": "node scripts/check-openclaw-upstream.js",
|
|
18
21
|
"scan": "node src/cli.js",
|
|
19
|
-
"
|
|
22
|
+
"lint": "node scripts/lint.js",
|
|
23
|
+
"typecheck": "npx tsc --noEmit -p tsconfig.json",
|
|
24
|
+
"release:gate": "node scripts/release-gate.js",
|
|
25
|
+
"test": "npm run lint && npm run typecheck && npm run build:plugin && node scripts/generate-capabilities.js && node scripts/generate-readme-metrics.js && node scripts/generate-readme-stats.js && node scripts/verify-capabilities.js && node scripts/test-quality-gate.js && node scripts/corpus-metrics.js --check && node scripts/perf-regression.js && node scripts/release-gate.js && node --test test/*.test.js",
|
|
20
26
|
"test:core": "node --test test/scanner.test.js test/patterns.test.js",
|
|
21
|
-
"test:
|
|
27
|
+
"test:contracts": "npm run build:plugin && node scripts/release-gate.js && node --test test/finding-schema.test.js test/mcp.test.js test/e2e-mcp.test.js test/openclaw-plugin-compat.test.js test/stale-claims.test.js test/openclaw-upstream-check.test.js",
|
|
28
|
+
"test:corpus": "node scripts/corpus-metrics.js --check",
|
|
29
|
+
"test:perf": "node scripts/perf-regression.js",
|
|
30
|
+
"test:quality": "node scripts/test-quality-gate.js",
|
|
31
|
+
"sbom": "node scripts/generate-sbom.js",
|
|
32
|
+
"sync:readme": "node scripts/generate-capabilities.js && node scripts/generate-readme-metrics.js && node scripts/generate-readme-stats.js",
|
|
33
|
+
"prepack": "npm run build:plugin && npm run release:gate"
|
|
22
34
|
},
|
|
23
35
|
"keywords": [
|
|
24
36
|
"security",
|
|
@@ -47,9 +59,11 @@
|
|
|
47
59
|
},
|
|
48
60
|
"homepage": "https://github.com/koatora20/guard-scanner",
|
|
49
61
|
"files": [
|
|
62
|
+
"dist/",
|
|
50
63
|
"src/",
|
|
51
64
|
"hooks/",
|
|
52
65
|
"docs/",
|
|
66
|
+
"openclaw-plugin.mts",
|
|
53
67
|
"openclaw.plugin.json",
|
|
54
68
|
"SKILL.md",
|
|
55
69
|
"SECURITY.md",
|
|
@@ -58,9 +72,10 @@
|
|
|
58
72
|
],
|
|
59
73
|
"devDependencies": {
|
|
60
74
|
"@types/node": "^22.0.0",
|
|
75
|
+
"openclaw": "2026.3.8",
|
|
61
76
|
"typescript": "^5.7.0"
|
|
62
77
|
},
|
|
63
78
|
"dependencies": {
|
|
64
79
|
"ws": "^8.19.0"
|
|
65
80
|
}
|
|
66
|
-
}
|
|
81
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -150,6 +150,141 @@ Examples:
|
|
|
150
150
|
process.exit(2);
|
|
151
151
|
}
|
|
152
152
|
})();
|
|
153
|
+
|
|
154
|
+
// ── crawl subcommand ──────────────────────────────────────────
|
|
155
|
+
} else if (args[0] === 'crawl') {
|
|
156
|
+
const { SkillCrawler } = require('./skill-crawler.js');
|
|
157
|
+
const subCmd = args[1]; // clawhub | github | url
|
|
158
|
+
const target = args[2]; // query or URL
|
|
159
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
160
|
+
const formatIdx = args.indexOf('--format');
|
|
161
|
+
const formatValue = formatIdx >= 0 ? args[formatIdx + 1] : null;
|
|
162
|
+
const quiet = !!formatValue;
|
|
163
|
+
const maxIdx = args.indexOf('--max');
|
|
164
|
+
const maxSkills = maxIdx >= 0 ? parseInt(args[maxIdx + 1], 10) : 50;
|
|
165
|
+
|
|
166
|
+
if (!subCmd || subCmd === '--help' || subCmd === '-h') {
|
|
167
|
+
console.log(`
|
|
168
|
+
🛡️ guard-scanner crawl — Autonomous Skill Scanner
|
|
169
|
+
|
|
170
|
+
Usage:
|
|
171
|
+
guard-scanner crawl clawhub Crawl ClawHub for all SKILL.md
|
|
172
|
+
guard-scanner crawl github <query> Search GitHub for SKILL.md
|
|
173
|
+
guard-scanner crawl url <url> Scan a single SKILL.md URL
|
|
174
|
+
|
|
175
|
+
Options:
|
|
176
|
+
--verbose, -v Show detection details
|
|
177
|
+
--format json Output JSON to stdout
|
|
178
|
+
--max <n> Max skills to scan (default: 50)
|
|
179
|
+
--help, -h Show this help
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
guard-scanner crawl clawhub --verbose
|
|
183
|
+
guard-scanner crawl github "polymarket trader" --format json
|
|
184
|
+
guard-scanner crawl url https://raw.githubusercontent.com/.../SKILL.md
|
|
185
|
+
`);
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const crawler = new SkillCrawler({ verbose, quiet, concurrency: 5 });
|
|
190
|
+
|
|
191
|
+
(async () => {
|
|
192
|
+
try {
|
|
193
|
+
if (subCmd === 'clawhub') {
|
|
194
|
+
await crawler.crawlClawHub({ maxSkills });
|
|
195
|
+
} else if (subCmd === 'github') {
|
|
196
|
+
if (!target) { console.error('❌ Usage: guard-scanner crawl github <query>'); process.exit(2); }
|
|
197
|
+
await crawler.crawlGitHub(target, { maxResults: maxSkills });
|
|
198
|
+
} else if (subCmd === 'url') {
|
|
199
|
+
if (!target) { console.error('❌ Usage: guard-scanner crawl url <url>'); process.exit(2); }
|
|
200
|
+
await crawler.scanUrl(target, target);
|
|
201
|
+
} else {
|
|
202
|
+
console.error(`❌ Unknown crawl target: ${subCmd}. Use: clawhub, github, or url`);
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (formatValue === 'json') {
|
|
207
|
+
process.stdout.write(JSON.stringify(crawler.toJSON(), null, 2) + '\n');
|
|
208
|
+
} else {
|
|
209
|
+
crawler.printSummary();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const summary = crawler.getSummary();
|
|
213
|
+
process.exit(summary.unsafe > 0 ? 1 : 0);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.error(`❌ Crawl error: ${e.message}`);
|
|
216
|
+
process.exit(2);
|
|
217
|
+
}
|
|
218
|
+
})();
|
|
219
|
+
|
|
220
|
+
// ── patrol subcommand ──────────────────────────────────────────
|
|
221
|
+
} else if (args[0] === 'patrol') {
|
|
222
|
+
const { SkillCrawler } = require('./skill-crawler.js');
|
|
223
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
224
|
+
const once = args.includes('--once');
|
|
225
|
+
const intervalIdx = args.indexOf('--interval');
|
|
226
|
+
const intervalSecs = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1], 10) : 3600;
|
|
227
|
+
const formatIdx = args.indexOf('--format');
|
|
228
|
+
const formatValue = formatIdx >= 0 ? args[formatIdx + 1] : null;
|
|
229
|
+
|
|
230
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
231
|
+
console.log(`
|
|
232
|
+
🛡️ guard-scanner patrol — Autonomous Security Patrol
|
|
233
|
+
|
|
234
|
+
Usage:
|
|
235
|
+
guard-scanner patrol --once Run one patrol cycle
|
|
236
|
+
guard-scanner patrol --interval 3600 Patrol every N seconds
|
|
237
|
+
|
|
238
|
+
Options:
|
|
239
|
+
--once Run once and exit
|
|
240
|
+
--interval <secs> Polling interval (default: 3600 = 1 hour)
|
|
241
|
+
--verbose, -v Show detection details
|
|
242
|
+
--format json Output JSON
|
|
243
|
+
--help, -h Show this help
|
|
244
|
+
`);
|
|
245
|
+
process.exit(0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function runPatrol() {
|
|
249
|
+
const timestamp = new Date().toISOString();
|
|
250
|
+
console.log(`\n🚨 Patrol cycle starting at ${timestamp}`);
|
|
251
|
+
console.log('─'.repeat(54));
|
|
252
|
+
|
|
253
|
+
const crawler = new SkillCrawler({ verbose, quiet: !!formatValue, concurrency: 5 });
|
|
254
|
+
await crawler.crawlClawHub({ maxSkills: 50 });
|
|
255
|
+
|
|
256
|
+
if (formatValue === 'json') {
|
|
257
|
+
process.stdout.write(JSON.stringify(crawler.toJSON(), null, 2) + '\n');
|
|
258
|
+
} else {
|
|
259
|
+
crawler.printSummary();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const summary = crawler.getSummary();
|
|
263
|
+
if (summary.unsafe > 0) {
|
|
264
|
+
console.log(`\n⚠️ ${summary.unsafe} unsafe skill(s) detected!`);
|
|
265
|
+
}
|
|
266
|
+
return summary;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
(async () => {
|
|
270
|
+
try {
|
|
271
|
+
if (once) {
|
|
272
|
+
const summary = await runPatrol();
|
|
273
|
+
process.exit(summary.unsafe > 0 ? 1 : 0);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`🛡️ guard-scanner patrol mode — interval: ${intervalSecs}s`);
|
|
276
|
+
console.log(` Press Ctrl+C to stop\n`);
|
|
277
|
+
await runPatrol();
|
|
278
|
+
setInterval(async () => {
|
|
279
|
+
await runPatrol();
|
|
280
|
+
}, intervalSecs * 1000);
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
console.error(`❌ Patrol error: ${e.message}`);
|
|
284
|
+
process.exit(2);
|
|
285
|
+
}
|
|
286
|
+
})();
|
|
287
|
+
|
|
153
288
|
} else {
|
|
154
289
|
// ── existing scan logic (backward compatible) ─────────────────
|
|
155
290
|
|
|
@@ -249,46 +384,51 @@ Examples:
|
|
|
249
384
|
!plugins.includes(a)
|
|
250
385
|
) || process.cwd();
|
|
251
386
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
387
|
+
try {
|
|
388
|
+
const scanner = new GuardScanner({
|
|
389
|
+
verbose, selfExclude, strict, summaryOnly, checkDeps, soulLock, rulesFile, plugins,
|
|
390
|
+
quiet: quietMode || !!formatValue,
|
|
391
|
+
});
|
|
256
392
|
|
|
257
|
-
|
|
393
|
+
scanner.scanDirectory(scanDir);
|
|
258
394
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
395
|
+
// Output reports (file-based, backward compatible)
|
|
396
|
+
if (jsonOutput) {
|
|
397
|
+
const report = scanner.toJSON();
|
|
398
|
+
const outPath = path.join(scanDir, 'guard-scanner-report.json');
|
|
399
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
400
|
+
if (!quietMode && !formatValue) console.log(`\n📄 JSON report: ${outPath}`);
|
|
401
|
+
}
|
|
266
402
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
403
|
+
if (sarifOutput) {
|
|
404
|
+
const outPath = path.join(scanDir, 'guard-scanner.sarif');
|
|
405
|
+
fs.writeFileSync(outPath, JSON.stringify(scanner.toSARIF(scanDir), null, 2));
|
|
406
|
+
if (!quietMode && !formatValue) console.log(`\n📄 SARIF report: ${outPath}`);
|
|
407
|
+
}
|
|
272
408
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
409
|
+
if (htmlOutput) {
|
|
410
|
+
const outPath = path.join(scanDir, 'guard-scanner-report.html');
|
|
411
|
+
fs.writeFileSync(outPath, scanner.toHTML());
|
|
412
|
+
if (!quietMode && !formatValue) console.log(`\n📄 HTML report: ${outPath}`);
|
|
413
|
+
}
|
|
278
414
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
415
|
+
// --format stdout output (v3.2.0)
|
|
416
|
+
if (formatValue === 'json') {
|
|
417
|
+
process.stdout.write(JSON.stringify(scanner.toJSON(), null, 2) + '\n');
|
|
418
|
+
} else if (formatValue === 'sarif') {
|
|
419
|
+
process.stdout.write(JSON.stringify(scanner.toSARIF(scanDir), null, 2) + '\n');
|
|
420
|
+
} else if (formatValue) {
|
|
421
|
+
console.error(`❌ Unknown format: ${formatValue}. Use 'json' or 'sarif'.`);
|
|
422
|
+
process.exit(2);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Exit codes
|
|
426
|
+
if (scanner.stats.malicious > 0) process.exit(1);
|
|
427
|
+
if (failOnFindings && scanner.findings.length > 0) process.exit(1);
|
|
428
|
+
process.exit(0);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(`❌ ${error.message}`);
|
|
286
431
|
process.exit(2);
|
|
287
432
|
}
|
|
288
433
|
|
|
289
|
-
// Exit codes
|
|
290
|
-
if (scanner.stats.malicious > 0) process.exit(1);
|
|
291
|
-
if (failOnFindings && scanner.findings.length > 0) process.exit(1);
|
|
292
|
-
process.exit(0);
|
|
293
|
-
|
|
294
434
|
} // end else (scan mode)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function loadIgnoreFile(scanDir) {
|
|
7
|
+
const ignoredSkills = new Set();
|
|
8
|
+
const ignoredPatterns = new Set();
|
|
9
|
+
const ignorePaths = [
|
|
10
|
+
path.join(scanDir, '.guard-scanner-ignore'),
|
|
11
|
+
path.join(scanDir, '.guava-guard-ignore'),
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (const ignorePath of ignorePaths) {
|
|
15
|
+
if (!fs.existsSync(ignorePath)) continue;
|
|
16
|
+
const lines = fs.readFileSync(ignorePath, 'utf-8').split('\n');
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
20
|
+
if (trimmed.startsWith('pattern:')) ignoredPatterns.add(trimmed.replace('pattern:', '').trim());
|
|
21
|
+
else ignoredSkills.add(trimmed);
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { ignoredSkills, ignoredPatterns };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadTextFile(filePath, maxLength = 500000) {
|
|
30
|
+
try {
|
|
31
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
32
|
+
if (content.length > maxLength) return null;
|
|
33
|
+
return content;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
loadIgnoreFile,
|
|
41
|
+
loadTextFile,
|
|
42
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const CODE_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.py', '.sh', '.bash', '.ps1', '.rb', '.go', '.rs', '.php', '.pl']);
|
|
7
|
+
const DOC_EXTENSIONS = new Set(['.md', '.txt', '.rst', '.adoc']);
|
|
8
|
+
const DATA_EXTENSIONS = new Set(['.json', '.yaml', '.yml', '.toml', '.xml', '.csv']);
|
|
9
|
+
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']);
|
|
10
|
+
const GENERATED_REPORT_FILES = new Set(['guard-scanner-report.json', 'guard-scanner-report.html', 'guard-scanner.sarif']);
|
|
11
|
+
|
|
12
|
+
function classifyFile(ext, relFile) {
|
|
13
|
+
if (CODE_EXTENSIONS.has(ext)) return 'code';
|
|
14
|
+
if (DOC_EXTENSIONS.has(ext)) return 'doc';
|
|
15
|
+
if (DATA_EXTENSIONS.has(ext)) return 'data';
|
|
16
|
+
const base = path.basename(relFile).toLowerCase();
|
|
17
|
+
if (base === 'skill.md' || base === 'readme.md') return 'skill-doc';
|
|
18
|
+
return 'other';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSelfNoisePath(skillName, relFile) {
|
|
22
|
+
if (skillName !== 'guard-scanner') return false;
|
|
23
|
+
return /^test\//.test(relFile)
|
|
24
|
+
|| /^dist\/__tests__\//.test(relFile)
|
|
25
|
+
|| /^ts-src\/__tests__\//.test(relFile)
|
|
26
|
+
|| /^docs\//.test(relFile)
|
|
27
|
+
|| relFile === 'ROADMAP-RESEARCH.md'
|
|
28
|
+
|| relFile === 'CHANGELOG.md';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isSelfThreatCorpus(skillName, relFile) {
|
|
32
|
+
if (skillName !== 'guard-scanner') return false;
|
|
33
|
+
return /(^|\/)(ioc-db|patterns)\.(js|ts)$/.test(relFile);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getFiles(dir) {
|
|
37
|
+
const results = [];
|
|
38
|
+
try {
|
|
39
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
44
|
+
results.push(...getFiles(fullPath));
|
|
45
|
+
} else {
|
|
46
|
+
const baseName = entry.name.toLowerCase();
|
|
47
|
+
if (GENERATED_REPORT_FILES.has(baseName)) continue;
|
|
48
|
+
results.push(fullPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch { }
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function listSkills(dir) {
|
|
56
|
+
return fs.readdirSync(dir).filter((file) => {
|
|
57
|
+
const fullPath = path.join(dir, file);
|
|
58
|
+
return fs.statSync(fullPath).isDirectory();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
CODE_EXTENSIONS,
|
|
64
|
+
DOC_EXTENSIONS,
|
|
65
|
+
DATA_EXTENSIONS,
|
|
66
|
+
BINARY_EXTENSIONS,
|
|
67
|
+
GENERATED_REPORT_FILES,
|
|
68
|
+
classifyFile,
|
|
69
|
+
isSelfNoisePath,
|
|
70
|
+
isSelfThreatCorpus,
|
|
71
|
+
getFiles,
|
|
72
|
+
listSkills,
|
|
73
|
+
};
|