guard-scanner 2.1.0 → 3.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.
- 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 +374 -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 +189 -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 +54 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +1043 -0
- package/dist/scanner.js.map +1 -0
- package/dist/types.d.ts +165 -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 +525 -0
- package/ts-src/cli.ts +171 -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} +372 -385
- package/ts-src/types.ts +187 -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/ts-src/types.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guard-scanner v3.0.0 — Type Definitions
|
|
3
|
+
* TypeScript rewrite with full type safety
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ── Severity & Verdict ──────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
9
|
+
|
|
10
|
+
export type VerdictLabel = 'MALICIOUS' | 'SUSPICIOUS' | 'LOW RISK' | 'CLEAN';
|
|
11
|
+
export type VerdictStat = 'malicious' | 'suspicious' | 'low' | 'clean';
|
|
12
|
+
|
|
13
|
+
export interface Verdict {
|
|
14
|
+
icon: string;
|
|
15
|
+
label: VerdictLabel;
|
|
16
|
+
stat: VerdictStat;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── File Types ──────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export type FileType = 'code' | 'doc' | 'data' | 'skill-doc' | 'other';
|
|
22
|
+
|
|
23
|
+
// ── Findings ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface Finding {
|
|
26
|
+
severity: Severity;
|
|
27
|
+
id: string;
|
|
28
|
+
cat: string;
|
|
29
|
+
desc: string;
|
|
30
|
+
file: string;
|
|
31
|
+
line?: number;
|
|
32
|
+
matchCount?: number;
|
|
33
|
+
sample?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SkillResult {
|
|
37
|
+
skill: string;
|
|
38
|
+
risk: number;
|
|
39
|
+
verdict: VerdictLabel;
|
|
40
|
+
findings: Finding[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Patterns ────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export interface PatternRule {
|
|
46
|
+
id: string;
|
|
47
|
+
cat: string;
|
|
48
|
+
regex: RegExp;
|
|
49
|
+
severity: Severity;
|
|
50
|
+
desc: string;
|
|
51
|
+
codeOnly?: boolean;
|
|
52
|
+
docOnly?: boolean;
|
|
53
|
+
all?: boolean;
|
|
54
|
+
/** OWASP LLM Top 10 2025 mapping (e.g. 'LLM01', 'LLM06') */
|
|
55
|
+
owasp?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CustomRuleInput {
|
|
59
|
+
id: string;
|
|
60
|
+
pattern: string;
|
|
61
|
+
flags?: string;
|
|
62
|
+
severity: Severity;
|
|
63
|
+
cat: string;
|
|
64
|
+
desc: string;
|
|
65
|
+
codeOnly?: boolean;
|
|
66
|
+
docOnly?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── IoC Database ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export interface IoC_Database {
|
|
72
|
+
ips: string[];
|
|
73
|
+
domains: string[];
|
|
74
|
+
urls: string[];
|
|
75
|
+
usernames: string[];
|
|
76
|
+
filenames: string[];
|
|
77
|
+
typosquats: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Signature Database (hbg-scan compatible) ────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export interface ThreatSignature {
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
severity: Severity;
|
|
86
|
+
description: string;
|
|
87
|
+
hash?: string; // SHA-256 content hash match
|
|
88
|
+
patterns?: string[]; // String patterns to match
|
|
89
|
+
domains?: string[]; // Suspicious domains
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SignatureDatabase {
|
|
93
|
+
version: string;
|
|
94
|
+
updated: string;
|
|
95
|
+
signatures: ThreatSignature[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Scanner Options ─────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export interface ScannerOptions {
|
|
101
|
+
verbose?: boolean;
|
|
102
|
+
selfExclude?: boolean;
|
|
103
|
+
strict?: boolean;
|
|
104
|
+
summaryOnly?: boolean;
|
|
105
|
+
checkDeps?: boolean;
|
|
106
|
+
rulesFile?: string;
|
|
107
|
+
plugins?: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Scanner Stats ───────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export interface ScanStats {
|
|
113
|
+
scanned: number;
|
|
114
|
+
clean: number;
|
|
115
|
+
low: number;
|
|
116
|
+
suspicious: number;
|
|
117
|
+
malicious: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Thresholds ──────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export interface Thresholds {
|
|
123
|
+
suspicious: number;
|
|
124
|
+
malicious: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Reports ─────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export interface JSONReport {
|
|
130
|
+
timestamp: string;
|
|
131
|
+
scanner: string;
|
|
132
|
+
mode: 'strict' | 'normal';
|
|
133
|
+
stats: ScanStats;
|
|
134
|
+
thresholds: Thresholds;
|
|
135
|
+
findings: SkillResult[];
|
|
136
|
+
recommendations: Recommendation[];
|
|
137
|
+
iocVersion: string;
|
|
138
|
+
signaturesVersion?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Recommendation {
|
|
142
|
+
skill: string;
|
|
143
|
+
actions: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── SARIF ───────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export interface SARIFReport {
|
|
149
|
+
version: string;
|
|
150
|
+
$schema: string;
|
|
151
|
+
runs: SARIFRun[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface SARIFRun {
|
|
155
|
+
tool: {
|
|
156
|
+
driver: {
|
|
157
|
+
name: string;
|
|
158
|
+
version: string;
|
|
159
|
+
informationUri: string;
|
|
160
|
+
rules: SARIFRule[];
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
results: SARIFResult[];
|
|
164
|
+
invocations: Array<{ executionSuccessful: boolean; endTimeUtc: string }>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface SARIFRule {
|
|
168
|
+
id: string;
|
|
169
|
+
name: string;
|
|
170
|
+
shortDescription: { text: string };
|
|
171
|
+
defaultConfiguration: { level: string };
|
|
172
|
+
properties: { tags: string[]; 'security-severity': string };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface SARIFResult {
|
|
176
|
+
ruleId: string;
|
|
177
|
+
ruleIndex: number;
|
|
178
|
+
level: string;
|
|
179
|
+
message: { text: string };
|
|
180
|
+
partialFingerprints: { primaryLocationLineHash: string };
|
|
181
|
+
locations: Array<{
|
|
182
|
+
physicalLocation: {
|
|
183
|
+
artifactLocation: { uri: string; uriBaseId: string };
|
|
184
|
+
region?: { startLine: number };
|
|
185
|
+
};
|
|
186
|
+
}>;
|
|
187
|
+
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* guard-scanner Runtime Guard — Hook Handler (LEGACY)
|
|
3
|
-
*
|
|
4
|
-
* ⚠️ DEPRECATED: Use plugin.ts instead.
|
|
5
|
-
* This Internal Hook version can only WARN, not block.
|
|
6
|
-
* The Plugin Hook version (plugin.ts) uses the native
|
|
7
|
-
* `block` / `blockReason` API to actually prevent execution.
|
|
8
|
-
*
|
|
9
|
-
* Intercepts agent tool calls and checks arguments against
|
|
10
|
-
* runtime threat intelligence patterns. Zero dependencies.
|
|
11
|
-
*
|
|
12
|
-
* Registered for event: agent:before_tool_call
|
|
13
|
-
*
|
|
14
|
-
* Current limitation:
|
|
15
|
-
* The OpenClaw InternalHookEvent interface does not yet expose a
|
|
16
|
-
* `cancel` / `veto` mechanism. This handler can WARN via
|
|
17
|
-
* event.messages but cannot block tool execution.
|
|
18
|
-
* When a cancel API is introduced, this handler will be updated
|
|
19
|
-
* to actually block CRITICAL/HIGH threats.
|
|
20
|
-
*
|
|
21
|
-
* Modes (for future blocking behaviour):
|
|
22
|
-
* monitor — log only (current effective behaviour for all modes)
|
|
23
|
-
* enforce — will block CRITICAL when cancel API is available
|
|
24
|
-
* strict — will block HIGH+CRITICAL when cancel API is available
|
|
25
|
-
*
|
|
26
|
-
* @author Guava 🍈 & Dee
|
|
27
|
-
* @version 1.1.0
|
|
28
|
-
* @license MIT
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { appendFileSync, mkdirSync } from "fs";
|
|
32
|
-
import { join } from "path";
|
|
33
|
-
import { homedir } from "os";
|
|
34
|
-
|
|
35
|
-
// ── OpenClaw Hook Types (from openclaw/src/hooks/internal-hooks.ts) ──
|
|
36
|
-
// Inline types to avoid broken relative-path imports.
|
|
37
|
-
// These match the official InternalHookEvent / InternalHookHandler
|
|
38
|
-
// from OpenClaw v2026.2.15.
|
|
39
|
-
|
|
40
|
-
type InternalHookEventType = "command" | "session" | "agent" | "gateway";
|
|
41
|
-
|
|
42
|
-
interface InternalHookEvent {
|
|
43
|
-
/** The type of event */
|
|
44
|
-
type: InternalHookEventType;
|
|
45
|
-
/** The specific action within the type (e.g., "before_tool_call") */
|
|
46
|
-
action: string;
|
|
47
|
-
/** The session key this event relates to */
|
|
48
|
-
sessionKey: string;
|
|
49
|
-
/** Additional context specific to the event */
|
|
50
|
-
context: Record<string, unknown>;
|
|
51
|
-
/** Timestamp when the event occurred */
|
|
52
|
-
timestamp: Date;
|
|
53
|
-
/** Messages to send back to the user (hooks can push to this array) */
|
|
54
|
-
messages: string[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type InternalHookHandler = (event: InternalHookEvent) => Promise<void> | void;
|
|
58
|
-
|
|
59
|
-
// Re-export as the public types for compatibility
|
|
60
|
-
type HookHandler = InternalHookHandler;
|
|
61
|
-
type HookEvent = InternalHookEvent;
|
|
62
|
-
|
|
63
|
-
// ── Runtime threat patterns (12 checks) ──
|
|
64
|
-
interface RuntimeCheck {
|
|
65
|
-
id: string;
|
|
66
|
-
severity: "CRITICAL" | "HIGH" | "MEDIUM";
|
|
67
|
-
desc: string;
|
|
68
|
-
test: (s: string) => boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const RUNTIME_CHECKS: RuntimeCheck[] = [
|
|
72
|
-
{
|
|
73
|
-
id: 'RT_REVSHELL', severity: 'CRITICAL', desc: 'Reverse shell attempt',
|
|
74
|
-
test: (s: string) => /\/dev\/tcp\/|nc\s+-e|ncat\s+-e|bash\s+-i\s+>&|socat\s+TCP/i.test(s)
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: 'RT_CRED_EXFIL', severity: 'CRITICAL', desc: 'Credential exfiltration to external',
|
|
78
|
-
test: (s: string) => {
|
|
79
|
-
return /(webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|socifiapp\.com)/i.test(s) &&
|
|
80
|
-
/(token|key|secret|password|credential|env)/i.test(s);
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: 'RT_GUARDRAIL_OFF', severity: 'CRITICAL', desc: 'Guardrail disabling attempt',
|
|
85
|
-
test: (s: string) => /exec\.approvals?\s*[:=]\s*['"]?(off|false)|tools\.exec\.host\s*[:=]\s*['"]?gateway/i.test(s)
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: 'RT_GATEKEEPER', severity: 'CRITICAL', desc: 'macOS Gatekeeper bypass (xattr)',
|
|
89
|
-
test: (s: string) => /xattr\s+-[crd]\s.*quarantine/i.test(s)
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: 'RT_AMOS', severity: 'CRITICAL', desc: 'ClawHavoc AMOS indicator',
|
|
93
|
-
test: (s: string) => /socifiapp|Atomic\s*Stealer|AMOS/i.test(s)
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: 'RT_MAL_IP', severity: 'CRITICAL', desc: 'Known malicious IP',
|
|
97
|
-
test: (s: string) => /91\.92\.242\.30/i.test(s)
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: 'RT_DNS_EXFIL', severity: 'HIGH', desc: 'DNS-based exfiltration',
|
|
101
|
-
test: (s: string) => /nslookup\s+.*\$|dig\s+.*\$.*@/i.test(s)
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: 'RT_B64_SHELL', severity: 'CRITICAL', desc: 'Base64 decode piped to shell',
|
|
105
|
-
test: (s: string) => /base64\s+(-[dD]|--decode)\s*\|\s*(sh|bash)/i.test(s)
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
id: 'RT_CURL_BASH', severity: 'CRITICAL', desc: 'Download piped to shell',
|
|
109
|
-
test: (s: string) => /(curl|wget)\s+[^\n]*\|\s*(sh|bash|zsh)/i.test(s)
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
id: 'RT_SSH_READ', severity: 'HIGH', desc: 'SSH private key access',
|
|
113
|
-
test: (s: string) => /\.ssh\/id_|\.ssh\/authorized_keys/i.test(s)
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
id: 'RT_WALLET', severity: 'HIGH', desc: 'Crypto wallet credential access',
|
|
117
|
-
test: (s: string) => /wallet.*(?:seed|mnemonic|private.*key)|seed.*phrase/i.test(s)
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: 'RT_CLOUD_META', severity: 'CRITICAL', desc: 'Cloud metadata endpoint access',
|
|
121
|
-
test: (s: string) => /169\.254\.169\.254|metadata\.google|metadata\.aws/i.test(s)
|
|
122
|
-
},
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
// ── Audit logging ──
|
|
126
|
-
const AUDIT_DIR = join(homedir(), ".openclaw", "guard-scanner");
|
|
127
|
-
const AUDIT_FILE = join(AUDIT_DIR, "audit.jsonl");
|
|
128
|
-
|
|
129
|
-
function ensureAuditDir(): void {
|
|
130
|
-
try { mkdirSync(AUDIT_DIR, { recursive: true }); } catch { }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function logAudit(entry: Record<string, unknown>): void {
|
|
134
|
-
ensureAuditDir();
|
|
135
|
-
const line = JSON.stringify({ ...entry, ts: new Date().toISOString() }) + '\n';
|
|
136
|
-
try { appendFileSync(AUDIT_FILE, line); } catch { }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── Main Handler ──
|
|
140
|
-
const handler: HookHandler = async (event) => {
|
|
141
|
-
// Only handle agent:before_tool_call events
|
|
142
|
-
if (event.type !== "agent" || event.action !== "before_tool_call") return;
|
|
143
|
-
|
|
144
|
-
const { toolName, toolArgs } = event.context as {
|
|
145
|
-
toolName?: string;
|
|
146
|
-
toolArgs?: Record<string, unknown>;
|
|
147
|
-
};
|
|
148
|
-
if (!toolName || !toolArgs) return;
|
|
149
|
-
|
|
150
|
-
// Get mode from context config (if available)
|
|
151
|
-
const cfg = event.context.cfg as Record<string, unknown> | undefined;
|
|
152
|
-
const hookEntries = (cfg as any)?.hooks?.internal?.entries?.['guard-scanner'] as Record<string, unknown> | undefined;
|
|
153
|
-
const mode = (hookEntries?.mode as string) || 'enforce';
|
|
154
|
-
|
|
155
|
-
// Only check tools that can cause damage
|
|
156
|
-
const dangerousTools = new Set(['exec', 'write', 'edit', 'browser', 'web_fetch', 'message']);
|
|
157
|
-
if (!dangerousTools.has(toolName)) return;
|
|
158
|
-
|
|
159
|
-
const serialized = JSON.stringify(toolArgs);
|
|
160
|
-
|
|
161
|
-
for (const check of RUNTIME_CHECKS) {
|
|
162
|
-
if (check.test(serialized)) {
|
|
163
|
-
const entry = {
|
|
164
|
-
tool: toolName,
|
|
165
|
-
check: check.id,
|
|
166
|
-
severity: check.severity,
|
|
167
|
-
desc: check.desc,
|
|
168
|
-
mode,
|
|
169
|
-
action: 'warned' as string,
|
|
170
|
-
session: event.sessionKey,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// NOTE: OpenClaw InternalHookEvent does not currently support
|
|
174
|
-
// a cancel/veto mechanism. When it does, uncomment the blocking
|
|
175
|
-
// logic below. For now, all detections are warnings only.
|
|
176
|
-
//
|
|
177
|
-
// if (mode === 'strict' && (check.severity === 'CRITICAL' || check.severity === 'HIGH')) {
|
|
178
|
-
// entry.action = 'blocked';
|
|
179
|
-
// logAudit(entry);
|
|
180
|
-
// event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
|
|
181
|
-
// event.cancel = true; // Not yet in the public API
|
|
182
|
-
// return;
|
|
183
|
-
// }
|
|
184
|
-
//
|
|
185
|
-
// if (mode === 'enforce' && check.severity === 'CRITICAL') {
|
|
186
|
-
// entry.action = 'blocked';
|
|
187
|
-
// logAudit(entry);
|
|
188
|
-
// event.messages.push(`🛡️ guard-scanner BLOCKED: ${check.desc} [${check.id}]`);
|
|
189
|
-
// event.cancel = true; // Not yet in the public API
|
|
190
|
-
// return;
|
|
191
|
-
// }
|
|
192
|
-
|
|
193
|
-
// Current behaviour: warn and log for all modes
|
|
194
|
-
logAudit(entry);
|
|
195
|
-
|
|
196
|
-
if (check.severity === 'CRITICAL') {
|
|
197
|
-
event.messages.push(`🛡️ guard-scanner WARNING: ${check.desc} [${check.id}]`);
|
|
198
|
-
console.warn(`[guard-scanner] ⚠️ WARNING: ${check.desc} [${check.id}]`);
|
|
199
|
-
} else if (check.severity === 'HIGH') {
|
|
200
|
-
event.messages.push(`🛡️ guard-scanner NOTICE: ${check.desc} [${check.id}]`);
|
|
201
|
-
console.warn(`[guard-scanner] ℹ️ NOTICE: ${check.desc} [${check.id}]`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
export default handler;
|
package/src/cli.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
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 scan-dir/guard-scanner-report.json
|
|
46
|
-
--sarif Write SARIF report (GitHub Code Scanning / CI/CD)
|
|
47
|
-
--html Write HTML report (visual dashboard)
|
|
48
|
-
--self-exclude Skip scanning the guard-scanner skill itself
|
|
49
|
-
--strict Lower detection thresholds (more sensitive)
|
|
50
|
-
--summary-only Only print the summary table
|
|
51
|
-
--check-deps Scan package.json for dependency chain risks
|
|
52
|
-
--rules <file> Load custom rules from JSON file
|
|
53
|
-
--plugin <file> Load plugin module (JS file exporting { name, patterns })
|
|
54
|
-
--fail-on-findings Exit code 1 if any findings (CI/CD)
|
|
55
|
-
--help, -h Show this help
|
|
56
|
-
|
|
57
|
-
Custom Rules JSON Format:
|
|
58
|
-
[
|
|
59
|
-
{
|
|
60
|
-
"id": "CUSTOM_001",
|
|
61
|
-
"pattern": "dangerous_function\\\\(",
|
|
62
|
-
"flags": "gi",
|
|
63
|
-
"severity": "HIGH",
|
|
64
|
-
"cat": "malicious-code",
|
|
65
|
-
"desc": "Custom: dangerous function call",
|
|
66
|
-
"codeOnly": true
|
|
67
|
-
}
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
Plugin API:
|
|
71
|
-
// my-plugin.js
|
|
72
|
-
module.exports = {
|
|
73
|
-
name: 'my-plugin',
|
|
74
|
-
patterns: [
|
|
75
|
-
{ id: 'MY_01', cat: 'custom', regex: /pattern/g, severity: 'HIGH', desc: 'Description', all: true }
|
|
76
|
-
]
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
Examples:
|
|
80
|
-
guard-scanner ./skills/ --verbose --self-exclude
|
|
81
|
-
guard-scanner ./skills/ --strict --json --sarif --check-deps
|
|
82
|
-
guard-scanner ./skills/ --html --verbose --check-deps
|
|
83
|
-
guard-scanner ./skills/ --rules my-rules.json --fail-on-findings
|
|
84
|
-
guard-scanner ./skills/ --plugin ./my-plugin.js
|
|
85
|
-
`);
|
|
86
|
-
process.exit(0);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
90
|
-
const jsonOutput = args.includes('--json');
|
|
91
|
-
const sarifOutput = args.includes('--sarif');
|
|
92
|
-
const htmlOutput = args.includes('--html');
|
|
93
|
-
const selfExclude = args.includes('--self-exclude');
|
|
94
|
-
const strict = args.includes('--strict');
|
|
95
|
-
const summaryOnly = args.includes('--summary-only');
|
|
96
|
-
const checkDeps = args.includes('--check-deps');
|
|
97
|
-
const failOnFindings = args.includes('--fail-on-findings');
|
|
98
|
-
|
|
99
|
-
const rulesIdx = args.indexOf('--rules');
|
|
100
|
-
const rulesFile = rulesIdx >= 0 ? args[rulesIdx + 1] : null;
|
|
101
|
-
|
|
102
|
-
// Collect plugins
|
|
103
|
-
const plugins = [];
|
|
104
|
-
let idx = 0;
|
|
105
|
-
while (idx < args.length) {
|
|
106
|
-
if (args[idx] === '--plugin' && args[idx + 1]) {
|
|
107
|
-
plugins.push(args[idx + 1]);
|
|
108
|
-
idx += 2;
|
|
109
|
-
} else {
|
|
110
|
-
idx++;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const scanDir = args.find(a =>
|
|
115
|
-
!a.startsWith('-') &&
|
|
116
|
-
a !== rulesFile &&
|
|
117
|
-
!plugins.includes(a)
|
|
118
|
-
) || process.cwd();
|
|
119
|
-
|
|
120
|
-
const scanner = new GuardScanner({
|
|
121
|
-
verbose, selfExclude, strict, summaryOnly, checkDeps, rulesFile, plugins
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
scanner.scanDirectory(scanDir);
|
|
125
|
-
|
|
126
|
-
// Output reports
|
|
127
|
-
if (jsonOutput) {
|
|
128
|
-
const report = scanner.toJSON();
|
|
129
|
-
const outPath = path.join(scanDir, 'guard-scanner-report.json');
|
|
130
|
-
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
131
|
-
console.log(`\n📄 JSON report: ${outPath}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (sarifOutput) {
|
|
135
|
-
const outPath = path.join(scanDir, 'guard-scanner.sarif');
|
|
136
|
-
fs.writeFileSync(outPath, JSON.stringify(scanner.toSARIF(scanDir), null, 2));
|
|
137
|
-
console.log(`\n📄 SARIF report: ${outPath}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (htmlOutput) {
|
|
141
|
-
const outPath = path.join(scanDir, 'guard-scanner-report.html');
|
|
142
|
-
fs.writeFileSync(outPath, scanner.toHTML());
|
|
143
|
-
console.log(`\n📄 HTML report: ${outPath}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Exit codes
|
|
147
|
-
if (scanner.stats.malicious > 0) process.exit(1);
|
|
148
|
-
if (failOnFindings && scanner.findings.length > 0) process.exit(1);
|
|
149
|
-
process.exit(0);
|