@cdot65/prisma-airs 0.1.4 → 0.2.1
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/hooks/prisma-airs-audit/HOOK.md +47 -0
- package/hooks/prisma-airs-audit/handler.ts +167 -0
- package/hooks/prisma-airs-context/HOOK.md +41 -0
- package/hooks/prisma-airs-context/handler.ts +295 -0
- package/hooks/prisma-airs-outbound/HOOK.md +43 -0
- package/hooks/prisma-airs-outbound/handler.test.ts +296 -0
- package/hooks/prisma-airs-outbound/handler.ts +341 -0
- package/hooks/prisma-airs-tools/HOOK.md +40 -0
- package/hooks/prisma-airs-tools/handler.ts +279 -0
- package/index.ts +3 -3
- package/openclaw.plugin.json +75 -4
- package/package.json +2 -2
- package/src/scan-cache.test.ts +167 -0
- package/src/scan-cache.ts +134 -0
- package/src/scanner.ts +15 -7
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma AIRS Tool Gating (before_tool_call)
|
|
3
|
+
*
|
|
4
|
+
* Blocks dangerous tool calls when security warnings are active.
|
|
5
|
+
* CAN BLOCK via { block: true, blockReason: "..." }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getCachedScanResult } from "../../src/scan-cache";
|
|
9
|
+
import type { ScanResult } from "../../src/scanner";
|
|
10
|
+
|
|
11
|
+
// Event shape from OpenClaw before_tool_call hook
|
|
12
|
+
interface BeforeToolCallEvent {
|
|
13
|
+
toolName: string;
|
|
14
|
+
toolId?: string;
|
|
15
|
+
params?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Context passed to hook
|
|
19
|
+
interface HookContext {
|
|
20
|
+
sessionKey?: string;
|
|
21
|
+
channelId?: string;
|
|
22
|
+
accountId?: string;
|
|
23
|
+
conversationId?: string;
|
|
24
|
+
cfg?: PluginConfig;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Plugin config structure
|
|
28
|
+
interface PluginConfig {
|
|
29
|
+
plugins?: {
|
|
30
|
+
entries?: {
|
|
31
|
+
"prisma-airs"?: {
|
|
32
|
+
config?: {
|
|
33
|
+
tool_gating_enabled?: boolean;
|
|
34
|
+
high_risk_tools?: string[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Hook result type
|
|
42
|
+
interface HookResult {
|
|
43
|
+
params?: Record<string, unknown>;
|
|
44
|
+
block?: boolean;
|
|
45
|
+
blockReason?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Tool blocking rules by threat category
|
|
49
|
+
const TOOL_BLOCKS: Record<string, string[]> = {
|
|
50
|
+
// AI Agent threats - block ALL external actions
|
|
51
|
+
"agent-threat": [
|
|
52
|
+
"exec",
|
|
53
|
+
"Bash",
|
|
54
|
+
"bash",
|
|
55
|
+
"write",
|
|
56
|
+
"Write",
|
|
57
|
+
"edit",
|
|
58
|
+
"Edit",
|
|
59
|
+
"gateway",
|
|
60
|
+
"message",
|
|
61
|
+
"cron",
|
|
62
|
+
"browser",
|
|
63
|
+
"web_fetch",
|
|
64
|
+
"WebFetch",
|
|
65
|
+
"database",
|
|
66
|
+
"query",
|
|
67
|
+
"sql",
|
|
68
|
+
"eval",
|
|
69
|
+
"NotebookEdit",
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
// SQL/Database injection - block database and exec tools
|
|
73
|
+
"sql-injection": ["exec", "Bash", "bash", "database", "query", "sql", "eval"],
|
|
74
|
+
db_security: ["exec", "Bash", "bash", "database", "query", "sql", "eval"],
|
|
75
|
+
"db-security": ["exec", "Bash", "bash", "database", "query", "sql", "eval"],
|
|
76
|
+
|
|
77
|
+
// Malicious code - block code execution and file writes
|
|
78
|
+
"malicious-code": [
|
|
79
|
+
"exec",
|
|
80
|
+
"Bash",
|
|
81
|
+
"bash",
|
|
82
|
+
"write",
|
|
83
|
+
"Write",
|
|
84
|
+
"edit",
|
|
85
|
+
"Edit",
|
|
86
|
+
"eval",
|
|
87
|
+
"NotebookEdit",
|
|
88
|
+
],
|
|
89
|
+
malicious_code: [
|
|
90
|
+
"exec",
|
|
91
|
+
"Bash",
|
|
92
|
+
"bash",
|
|
93
|
+
"write",
|
|
94
|
+
"Write",
|
|
95
|
+
"edit",
|
|
96
|
+
"Edit",
|
|
97
|
+
"eval",
|
|
98
|
+
"NotebookEdit",
|
|
99
|
+
],
|
|
100
|
+
|
|
101
|
+
// Prompt injection - block sensitive tools
|
|
102
|
+
"prompt-injection": ["exec", "Bash", "bash", "gateway", "message", "cron"],
|
|
103
|
+
prompt_injection: ["exec", "Bash", "bash", "gateway", "message", "cron"],
|
|
104
|
+
|
|
105
|
+
// Malicious URLs - block web access
|
|
106
|
+
"malicious-url": ["web_fetch", "WebFetch", "browser", "Browser", "curl"],
|
|
107
|
+
malicious_url: ["web_fetch", "WebFetch", "browser", "Browser", "curl"],
|
|
108
|
+
url_filtering_prompt: ["web_fetch", "WebFetch", "browser", "Browser", "curl"],
|
|
109
|
+
|
|
110
|
+
// Scan failure - block high-risk tools
|
|
111
|
+
"scan-failure": [
|
|
112
|
+
"exec",
|
|
113
|
+
"Bash",
|
|
114
|
+
"bash",
|
|
115
|
+
"write",
|
|
116
|
+
"Write",
|
|
117
|
+
"edit",
|
|
118
|
+
"Edit",
|
|
119
|
+
"gateway",
|
|
120
|
+
"message",
|
|
121
|
+
"cron",
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Default high-risk tools (blocked on any threat)
|
|
126
|
+
const DEFAULT_HIGH_RISK_TOOLS = [
|
|
127
|
+
"exec",
|
|
128
|
+
"Bash",
|
|
129
|
+
"bash",
|
|
130
|
+
"write",
|
|
131
|
+
"Write",
|
|
132
|
+
"edit",
|
|
133
|
+
"Edit",
|
|
134
|
+
"gateway",
|
|
135
|
+
"message",
|
|
136
|
+
"cron",
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get plugin configuration
|
|
141
|
+
*/
|
|
142
|
+
function getPluginConfig(ctx: HookContext): {
|
|
143
|
+
enabled: boolean;
|
|
144
|
+
highRiskTools: string[];
|
|
145
|
+
} {
|
|
146
|
+
const cfg = ctx.cfg?.plugins?.entries?.["prisma-airs"]?.config;
|
|
147
|
+
return {
|
|
148
|
+
enabled: cfg?.tool_gating_enabled !== false,
|
|
149
|
+
highRiskTools: cfg?.high_risk_tools ?? DEFAULT_HIGH_RISK_TOOLS,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Determine if a tool should be blocked based on scan result
|
|
155
|
+
*/
|
|
156
|
+
function shouldBlockTool(
|
|
157
|
+
toolName: string,
|
|
158
|
+
scanResult: ScanResult,
|
|
159
|
+
highRiskTools: string[]
|
|
160
|
+
): { block: boolean; reason: string } {
|
|
161
|
+
// Collect all tools that should be blocked based on detected categories
|
|
162
|
+
const blockedTools = new Set<string>();
|
|
163
|
+
|
|
164
|
+
for (const category of scanResult.categories) {
|
|
165
|
+
const tools = TOOL_BLOCKS[category];
|
|
166
|
+
if (tools) {
|
|
167
|
+
tools.forEach((t) => blockedTools.add(t.toLowerCase()));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add high-risk tools if any threat was detected
|
|
172
|
+
const hasThreat =
|
|
173
|
+
scanResult.action === "block" ||
|
|
174
|
+
scanResult.action === "warn" ||
|
|
175
|
+
(scanResult.categories.length > 0 &&
|
|
176
|
+
!scanResult.categories.every((c) => c === "safe" || c === "benign"));
|
|
177
|
+
|
|
178
|
+
if (hasThreat) {
|
|
179
|
+
highRiskTools.forEach((t) => blockedTools.add(t.toLowerCase()));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if this tool should be blocked
|
|
183
|
+
const toolLower = toolName.toLowerCase();
|
|
184
|
+
if (blockedTools.has(toolLower)) {
|
|
185
|
+
const threatCategories = scanResult.categories
|
|
186
|
+
.filter((c) => c !== "safe" && c !== "benign")
|
|
187
|
+
.join(", ");
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
block: true,
|
|
191
|
+
reason:
|
|
192
|
+
`Tool '${toolName}' blocked due to security threat: ${threatCategories || "unknown"}. ` +
|
|
193
|
+
`Scan ID: ${scanResult.scanId || "N/A"}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { block: false, reason: "" };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Main hook handler
|
|
202
|
+
*/
|
|
203
|
+
const handler = async (
|
|
204
|
+
event: BeforeToolCallEvent,
|
|
205
|
+
ctx: HookContext
|
|
206
|
+
): Promise<HookResult | void> => {
|
|
207
|
+
const config = getPluginConfig(ctx);
|
|
208
|
+
|
|
209
|
+
// Check if tool gating is enabled
|
|
210
|
+
if (!config.enabled) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get tool name
|
|
215
|
+
const toolName = event.toolName;
|
|
216
|
+
if (!toolName) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Build session key
|
|
221
|
+
const sessionKey = ctx.sessionKey || ctx.conversationId || "unknown";
|
|
222
|
+
|
|
223
|
+
// Get cached scan result from inbound scanning
|
|
224
|
+
const scanResult = getCachedScanResult(sessionKey);
|
|
225
|
+
if (!scanResult) {
|
|
226
|
+
return; // No scan result cached, allow through
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if result indicates a safe message
|
|
230
|
+
if (
|
|
231
|
+
scanResult.action === "allow" &&
|
|
232
|
+
(scanResult.severity === "SAFE" ||
|
|
233
|
+
scanResult.categories.every((c) => c === "safe" || c === "benign"))
|
|
234
|
+
) {
|
|
235
|
+
return; // Safe, allow all tools
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check if this tool should be blocked
|
|
239
|
+
const { block, reason } = shouldBlockTool(toolName, scanResult, config.highRiskTools);
|
|
240
|
+
|
|
241
|
+
if (block) {
|
|
242
|
+
console.log(
|
|
243
|
+
JSON.stringify({
|
|
244
|
+
event: "prisma_airs_tool_block",
|
|
245
|
+
timestamp: new Date().toISOString(),
|
|
246
|
+
sessionKey,
|
|
247
|
+
toolName,
|
|
248
|
+
toolId: event.toolId,
|
|
249
|
+
scanAction: scanResult.action,
|
|
250
|
+
severity: scanResult.severity,
|
|
251
|
+
categories: scanResult.categories,
|
|
252
|
+
scanId: scanResult.scanId,
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
block: true,
|
|
258
|
+
blockReason: reason,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Tool allowed, log for audit
|
|
263
|
+
if (scanResult.action !== "allow") {
|
|
264
|
+
console.log(
|
|
265
|
+
JSON.stringify({
|
|
266
|
+
event: "prisma_airs_tool_allow",
|
|
267
|
+
timestamp: new Date().toISOString(),
|
|
268
|
+
sessionKey,
|
|
269
|
+
toolName,
|
|
270
|
+
toolId: event.toolId,
|
|
271
|
+
note: "Tool allowed despite active security warning",
|
|
272
|
+
scanAction: scanResult.action,
|
|
273
|
+
categories: scanResult.categories,
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export default handler;
|
package/index.ts
CHANGED
|
@@ -116,7 +116,7 @@ export default function register(api: PluginApi): void {
|
|
|
116
116
|
const hasApiKey = isConfigured();
|
|
117
117
|
respond(true, {
|
|
118
118
|
plugin: "prisma-airs",
|
|
119
|
-
version: "0.
|
|
119
|
+
version: "0.2.0",
|
|
120
120
|
config: {
|
|
121
121
|
profile_name: cfg.profile_name ?? "default",
|
|
122
122
|
app_name: cfg.app_name ?? "openclaw",
|
|
@@ -215,7 +215,7 @@ export default function register(api: PluginApi): void {
|
|
|
215
215
|
const hasKey = isConfigured();
|
|
216
216
|
console.log("Prisma AIRS Plugin Status");
|
|
217
217
|
console.log("-------------------------");
|
|
218
|
-
console.log(`Version: 0.
|
|
218
|
+
console.log(`Version: 0.2.0`);
|
|
219
219
|
console.log(`Profile: ${cfg.profile_name ?? "default"}`);
|
|
220
220
|
console.log(`App Name: ${cfg.app_name ?? "openclaw"}`);
|
|
221
221
|
console.log(`Reminder: ${cfg.reminder_enabled ?? true}`);
|
|
@@ -266,7 +266,7 @@ export default function register(api: PluginApi): void {
|
|
|
266
266
|
// Export plugin metadata for discovery
|
|
267
267
|
export const id = "prisma-airs";
|
|
268
268
|
export const name = "Prisma AIRS Security";
|
|
269
|
-
export const version = "0.
|
|
269
|
+
export const version = "0.2.0";
|
|
270
270
|
|
|
271
271
|
// Re-export scanner types and functions
|
|
272
272
|
export { scan, isConfigured } from "./src/scanner";
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "prisma-airs",
|
|
3
3
|
"name": "Prisma AIRS Security",
|
|
4
|
-
"description": "AI Runtime Security
|
|
5
|
-
"version": "0.
|
|
4
|
+
"description": "AI Runtime Security - full AIRS detection suite with audit logging, context injection, outbound blocking, and tool gating",
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"entrypoint": "index.ts",
|
|
7
|
-
"hooks": [
|
|
7
|
+
"hooks": [
|
|
8
|
+
"hooks/prisma-airs-guard",
|
|
9
|
+
"hooks/prisma-airs-audit",
|
|
10
|
+
"hooks/prisma-airs-context",
|
|
11
|
+
"hooks/prisma-airs-outbound",
|
|
12
|
+
"hooks/prisma-airs-tools"
|
|
13
|
+
],
|
|
8
14
|
"configSchema": {
|
|
9
15
|
"type": "object",
|
|
10
16
|
"additionalProperties": false,
|
|
@@ -23,6 +29,42 @@
|
|
|
23
29
|
"type": "boolean",
|
|
24
30
|
"default": true,
|
|
25
31
|
"description": "Inject security scanning reminder on agent bootstrap"
|
|
32
|
+
},
|
|
33
|
+
"audit_enabled": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"default": true,
|
|
36
|
+
"description": "Enable audit logging of all inbound messages"
|
|
37
|
+
},
|
|
38
|
+
"context_injection_enabled": {
|
|
39
|
+
"type": "boolean",
|
|
40
|
+
"default": true,
|
|
41
|
+
"description": "Inject security warnings into agent context"
|
|
42
|
+
},
|
|
43
|
+
"outbound_scanning_enabled": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"default": true,
|
|
46
|
+
"description": "Enable scanning and blocking of outbound responses"
|
|
47
|
+
},
|
|
48
|
+
"tool_gating_enabled": {
|
|
49
|
+
"type": "boolean",
|
|
50
|
+
"default": true,
|
|
51
|
+
"description": "Block dangerous tools when threats are detected"
|
|
52
|
+
},
|
|
53
|
+
"fail_closed": {
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"default": true,
|
|
56
|
+
"description": "Block messages when AIRS scan fails (default: true for security)"
|
|
57
|
+
},
|
|
58
|
+
"dlp_mask_only": {
|
|
59
|
+
"type": "boolean",
|
|
60
|
+
"default": true,
|
|
61
|
+
"description": "Mask DLP violations instead of blocking (when no other violations)"
|
|
62
|
+
},
|
|
63
|
+
"high_risk_tools": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"items": { "type": "string" },
|
|
66
|
+
"default": ["exec", "Bash", "write", "Write", "edit", "Edit", "gateway", "message", "cron"],
|
|
67
|
+
"description": "Tools to block on any detected threat"
|
|
26
68
|
}
|
|
27
69
|
}
|
|
28
70
|
},
|
|
@@ -37,9 +79,38 @@
|
|
|
37
79
|
},
|
|
38
80
|
"reminder_enabled": {
|
|
39
81
|
"label": "Enable Bootstrap Reminder"
|
|
82
|
+
},
|
|
83
|
+
"audit_enabled": {
|
|
84
|
+
"label": "Enable Audit Logging",
|
|
85
|
+
"description": "Log all inbound messages with scan results"
|
|
86
|
+
},
|
|
87
|
+
"context_injection_enabled": {
|
|
88
|
+
"label": "Enable Context Injection",
|
|
89
|
+
"description": "Inject security warnings into agent context"
|
|
90
|
+
},
|
|
91
|
+
"outbound_scanning_enabled": {
|
|
92
|
+
"label": "Enable Outbound Scanning",
|
|
93
|
+
"description": "Scan and block/mask outbound responses"
|
|
94
|
+
},
|
|
95
|
+
"tool_gating_enabled": {
|
|
96
|
+
"label": "Enable Tool Gating",
|
|
97
|
+
"description": "Block dangerous tools during active threats"
|
|
98
|
+
},
|
|
99
|
+
"fail_closed": {
|
|
100
|
+
"label": "Fail Closed",
|
|
101
|
+
"description": "Block on scan failure (recommended for security)"
|
|
102
|
+
},
|
|
103
|
+
"dlp_mask_only": {
|
|
104
|
+
"label": "DLP Mask Only",
|
|
105
|
+
"description": "Mask sensitive data instead of blocking"
|
|
106
|
+
},
|
|
107
|
+
"high_risk_tools": {
|
|
108
|
+
"label": "High Risk Tools",
|
|
109
|
+
"description": "Tools blocked on any threat detection"
|
|
40
110
|
}
|
|
41
111
|
},
|
|
42
112
|
"requires": {
|
|
43
|
-
"env": ["PANW_AI_SEC_API_KEY"]
|
|
113
|
+
"env": ["PANW_AI_SEC_API_KEY"],
|
|
114
|
+
"envOptional": ["PANW_AI_SEC_PROFILE_NAME"]
|
|
44
115
|
}
|
|
45
116
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdot65/prisma-airs",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Prisma AIRS (AI Runtime Security) plugin for OpenClaw -
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Prisma AIRS (AI Runtime Security) plugin for OpenClaw - Full security suite with audit logging, context injection, outbound blocking, and tool gating",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for scan-cache module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
cacheScanResult,
|
|
8
|
+
getCachedScanResult,
|
|
9
|
+
getCachedScanResultIfMatch,
|
|
10
|
+
clearScanResult,
|
|
11
|
+
getCacheStats,
|
|
12
|
+
hashMessage,
|
|
13
|
+
stopCleanup,
|
|
14
|
+
startCleanup,
|
|
15
|
+
} from "./scan-cache";
|
|
16
|
+
import type { ScanResult } from "./scanner";
|
|
17
|
+
|
|
18
|
+
// Mock scan result
|
|
19
|
+
const mockScanResult: ScanResult = {
|
|
20
|
+
action: "block",
|
|
21
|
+
severity: "HIGH",
|
|
22
|
+
categories: ["prompt_injection"],
|
|
23
|
+
scanId: "scan_123",
|
|
24
|
+
reportId: "report_456",
|
|
25
|
+
profileName: "default",
|
|
26
|
+
promptDetected: { injection: true, dlp: false, urlCats: false },
|
|
27
|
+
responseDetected: { dlp: false, urlCats: false },
|
|
28
|
+
latencyMs: 100,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("scan-cache", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
// Clear cache before each test
|
|
34
|
+
clearScanResult("test-session");
|
|
35
|
+
clearScanResult("session-1");
|
|
36
|
+
clearScanResult("session-2");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
// Stop cleanup interval to prevent test interference
|
|
41
|
+
stopCleanup();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("cacheScanResult", () => {
|
|
45
|
+
it("should cache a scan result", () => {
|
|
46
|
+
cacheScanResult("test-session", mockScanResult);
|
|
47
|
+
const result = getCachedScanResult("test-session");
|
|
48
|
+
expect(result).toEqual(mockScanResult);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should cache with message hash", () => {
|
|
52
|
+
const hash = hashMessage("test message");
|
|
53
|
+
cacheScanResult("test-session", mockScanResult, hash);
|
|
54
|
+
const result = getCachedScanResultIfMatch("test-session", hash);
|
|
55
|
+
expect(result).toEqual(mockScanResult);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("getCachedScanResult", () => {
|
|
60
|
+
it("should return undefined for non-existent key", () => {
|
|
61
|
+
const result = getCachedScanResult("non-existent");
|
|
62
|
+
expect(result).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should return cached result", () => {
|
|
66
|
+
cacheScanResult("test-session", mockScanResult);
|
|
67
|
+
const result = getCachedScanResult("test-session");
|
|
68
|
+
expect(result).toEqual(mockScanResult);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should return undefined for expired entries", () => {
|
|
72
|
+
// Mock Date.now to simulate time passing
|
|
73
|
+
const originalNow = Date.now;
|
|
74
|
+
const startTime = 1000000;
|
|
75
|
+
vi.spyOn(Date, "now").mockReturnValue(startTime);
|
|
76
|
+
|
|
77
|
+
cacheScanResult("test-session", mockScanResult);
|
|
78
|
+
|
|
79
|
+
// Advance time past TTL (30 seconds)
|
|
80
|
+
vi.spyOn(Date, "now").mockReturnValue(startTime + 31000);
|
|
81
|
+
|
|
82
|
+
const result = getCachedScanResult("test-session");
|
|
83
|
+
expect(result).toBeUndefined();
|
|
84
|
+
|
|
85
|
+
// Restore
|
|
86
|
+
Date.now = originalNow;
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("getCachedScanResultIfMatch", () => {
|
|
91
|
+
it("should return result if hash matches", () => {
|
|
92
|
+
const hash = hashMessage("test message");
|
|
93
|
+
cacheScanResult("test-session", mockScanResult, hash);
|
|
94
|
+
const result = getCachedScanResultIfMatch("test-session", hash);
|
|
95
|
+
expect(result).toEqual(mockScanResult);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return undefined if hash does not match", () => {
|
|
99
|
+
const hash1 = hashMessage("message 1");
|
|
100
|
+
const hash2 = hashMessage("message 2");
|
|
101
|
+
cacheScanResult("test-session", mockScanResult, hash1);
|
|
102
|
+
const result = getCachedScanResultIfMatch("test-session", hash2);
|
|
103
|
+
expect(result).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should return result if no hash was stored", () => {
|
|
107
|
+
cacheScanResult("test-session", mockScanResult);
|
|
108
|
+
const result = getCachedScanResultIfMatch("test-session", "any-hash");
|
|
109
|
+
expect(result).toEqual(mockScanResult);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("clearScanResult", () => {
|
|
114
|
+
it("should clear cached result", () => {
|
|
115
|
+
cacheScanResult("test-session", mockScanResult);
|
|
116
|
+
clearScanResult("test-session");
|
|
117
|
+
const result = getCachedScanResult("test-session");
|
|
118
|
+
expect(result).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should not affect other sessions", () => {
|
|
122
|
+
cacheScanResult("session-1", mockScanResult);
|
|
123
|
+
cacheScanResult("session-2", { ...mockScanResult, scanId: "scan_456" });
|
|
124
|
+
clearScanResult("session-1");
|
|
125
|
+
|
|
126
|
+
expect(getCachedScanResult("session-1")).toBeUndefined();
|
|
127
|
+
expect(getCachedScanResult("session-2")).toBeDefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("hashMessage", () => {
|
|
132
|
+
it("should produce consistent hashes", () => {
|
|
133
|
+
const hash1 = hashMessage("test message");
|
|
134
|
+
const hash2 = hashMessage("test message");
|
|
135
|
+
expect(hash1).toEqual(hash2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should produce different hashes for different messages", () => {
|
|
139
|
+
const hash1 = hashMessage("message 1");
|
|
140
|
+
const hash2 = hashMessage("message 2");
|
|
141
|
+
expect(hash1).not.toEqual(hash2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should handle empty string", () => {
|
|
145
|
+
const hash = hashMessage("");
|
|
146
|
+
expect(hash).toEqual("0");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("getCacheStats", () => {
|
|
151
|
+
it("should return cache stats", () => {
|
|
152
|
+
const stats = getCacheStats();
|
|
153
|
+
expect(stats).toHaveProperty("size");
|
|
154
|
+
expect(stats).toHaveProperty("ttlMs");
|
|
155
|
+
expect(stats.ttlMs).toEqual(30000);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("cleanup", () => {
|
|
160
|
+
it("should be able to stop and start cleanup", () => {
|
|
161
|
+
stopCleanup();
|
|
162
|
+
startCleanup();
|
|
163
|
+
// No error means success
|
|
164
|
+
expect(true).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|