shroud-privacy 2.2.11 → 2.2.13
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 +19 -10
- package/dist/hooks.js +246 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/dist/agent-session.d.ts +0 -259
- package/dist/agent-session.js +0 -693
- package/dist/compliance.d.ts +0 -44
- package/dist/compliance.js +0 -76
- package/dist/dashboard.d.ts +0 -42
- package/dist/dashboard.js +0 -1558
- package/dist/detectors/injection-multilingual.d.ts +0 -27
- package/dist/detectors/injection-multilingual.js +0 -399
- package/dist/detectors/injection-signatures.d.ts +0 -26
- package/dist/detectors/injection-signatures.js +0 -508
- package/dist/detectors/injection.d.ts +0 -56
- package/dist/detectors/injection.js +0 -269
- package/dist/detectors/tool-guard.d.ts +0 -27
- package/dist/detectors/tool-guard.js +0 -418
- package/dist/event-grader.d.ts +0 -97
- package/dist/event-grader.js +0 -214
- package/dist/exposure.d.ts +0 -29
- package/dist/exposure.js +0 -72
- package/dist/policy.d.ts +0 -99
- package/dist/policy.js +0 -212
- package/dist/profiler-analysis.d.ts +0 -35
- package/dist/profiler-analysis.js +0 -230
- package/dist/profiler-store.d.ts +0 -33
- package/dist/profiler-store.js +0 -118
- package/dist/profiler-types.d.ts +0 -128
- package/dist/profiler-types.js +0 -16
- package/dist/profiler.d.ts +0 -81
- package/dist/profiler.js +0 -392
- package/dist/security-event.d.ts +0 -70
- package/dist/security-event.js +0 -80
- package/dist/siem.d.ts +0 -49
- package/dist/siem.js +0 -113
- package/dist/signature-loader.d.ts +0 -113
- package/dist/signature-loader.js +0 -255
- package/dist/store-file.d.ts +0 -26
- package/dist/store-file.js +0 -79
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt injection signature detector.
|
|
3
|
-
*
|
|
4
|
-
* Scans text for known injection patterns on the request side
|
|
5
|
-
* (direct/indirect injection) and response side (exfiltration confirmation).
|
|
6
|
-
*
|
|
7
|
-
* This is a standalone scanner — it does NOT integrate into the obfuscation
|
|
8
|
-
* detector chain. The obfuscation pipeline remains untouched.
|
|
9
|
-
*/
|
|
10
|
-
import { ThreatClass, } from "../security-event.js";
|
|
11
|
-
import { REQUEST_SIGNATURES, RESPONSE_SIGNATURES, } from "./injection-signatures.js";
|
|
12
|
-
import { MULTILINGUAL_REQUEST_SIGNATURES, } from "./injection-multilingual.js";
|
|
13
|
-
/** Injection keywords checked inside decoded Base64 payloads. */
|
|
14
|
-
const BASE64_INJECTION_KEYWORDS = [
|
|
15
|
-
"ignore",
|
|
16
|
-
"instructions",
|
|
17
|
-
"system prompt",
|
|
18
|
-
"override",
|
|
19
|
-
"jailbreak",
|
|
20
|
-
"disregard",
|
|
21
|
-
"forget",
|
|
22
|
-
"bypass",
|
|
23
|
-
"unrestricted",
|
|
24
|
-
"developer mode",
|
|
25
|
-
"you are now",
|
|
26
|
-
"act as",
|
|
27
|
-
];
|
|
28
|
-
const SEVERITY_RANK = {
|
|
29
|
-
low: 0,
|
|
30
|
-
medium: 1,
|
|
31
|
-
high: 2,
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Check if a text span is inside quotation marks, backticks, or code blocks.
|
|
35
|
-
* Used to reduce severity of injection patterns that are being discussed/quoted
|
|
36
|
-
* rather than used as actual attacks.
|
|
37
|
-
*/
|
|
38
|
-
function isInsideQuotes(text, matchStart, matchEnd) {
|
|
39
|
-
// Look backwards from matchStart for an unmatched opening quote
|
|
40
|
-
const before = text.slice(Math.max(0, matchStart - 200), matchStart);
|
|
41
|
-
const after = text.slice(matchEnd, Math.min(text.length, matchEnd + 200));
|
|
42
|
-
// Check for surrounding double quotes
|
|
43
|
-
if (before.includes('"') && after.includes('"')) {
|
|
44
|
-
const lastQuoteBefore = before.lastIndexOf('"');
|
|
45
|
-
const quotesBefore = before.slice(lastQuoteBefore).split('"').length - 1;
|
|
46
|
-
if (quotesBefore % 2 === 1)
|
|
47
|
-
return true; // odd number = inside quotes
|
|
48
|
-
}
|
|
49
|
-
// Check for surrounding single quotes
|
|
50
|
-
if (before.includes("'") && after.includes("'")) {
|
|
51
|
-
const lastQuoteBefore = before.lastIndexOf("'");
|
|
52
|
-
const quotesBefore = before.slice(lastQuoteBefore).split("'").length - 1;
|
|
53
|
-
if (quotesBefore % 2 === 1)
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
// Check for backticks (inline code)
|
|
57
|
-
if (before.includes("`") && after.includes("`"))
|
|
58
|
-
return true;
|
|
59
|
-
// Check for parenthetical context: (DAN), (XSS), etc.
|
|
60
|
-
// The match might include the closing paren, so check if before ends with (
|
|
61
|
-
// or the matched text itself starts right after a (
|
|
62
|
-
if (before.endsWith("("))
|
|
63
|
-
return true;
|
|
64
|
-
if (before.trimEnd().endsWith("("))
|
|
65
|
-
return true;
|
|
66
|
-
// Check for 'like "X"' or 'such as "X"' or 'phrases like "X"' patterns
|
|
67
|
-
// These indicate the text is being discussed, not executed
|
|
68
|
-
const discussionPatterns = /(?:like|such\s+as|example|e\.g\.|called|known\s+as|termed|phrase|pattern|classified|documented|described|first\s+appeared)/i;
|
|
69
|
-
if (discussionPatterns.test(before.slice(-100)))
|
|
70
|
-
return true;
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Strip token smuggling characters — invisible Unicode chars that attackers
|
|
75
|
-
* insert between tokens to break regex matching.
|
|
76
|
-
*
|
|
77
|
-
* Strips: zero-width space (U+200B), zero-width non-joiner (U+200C),
|
|
78
|
-
* zero-width joiner (U+200D), byte order mark (U+FEFF), word joiner (U+2060),
|
|
79
|
-
* soft hyphen (U+00AD), Mongolian vowel separator (U+180E),
|
|
80
|
-
* and all variation selectors (U+FE00-FE0F).
|
|
81
|
-
*/
|
|
82
|
-
function stripTokenSmuggling(text) {
|
|
83
|
-
return text.replace(/[\u200B\u200C\u200D\uFEFF\u2060\u00AD\u180E\uFE00-\uFE0F]/g, "");
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Standalone injection scanner. Not a BaseDetector — runs parallel to
|
|
87
|
-
* the obfuscation pipeline, never touches entity replacement.
|
|
88
|
-
*/
|
|
89
|
-
export class InjectionDetector {
|
|
90
|
-
_config;
|
|
91
|
-
_requestSigs;
|
|
92
|
-
_responseSigs;
|
|
93
|
-
_externalRequestSigs = [];
|
|
94
|
-
_externalResponseSigs = [];
|
|
95
|
-
constructor(config) {
|
|
96
|
-
this._config = config;
|
|
97
|
-
const minRank = SEVERITY_RANK[config.minSeverity];
|
|
98
|
-
// Pre-filter signatures by severity and disabled list
|
|
99
|
-
// Include multilingual patterns alongside English ones
|
|
100
|
-
this._requestSigs = [...REQUEST_SIGNATURES, ...MULTILINGUAL_REQUEST_SIGNATURES].filter((s) => !config.disabledSignatures.has(s.id) &&
|
|
101
|
-
SEVERITY_RANK[s.severity] >= minRank);
|
|
102
|
-
this._responseSigs = RESPONSE_SIGNATURES.filter((s) => !config.disabledSignatures.has(s.id) &&
|
|
103
|
-
SEVERITY_RANK[s.severity] >= minRank);
|
|
104
|
-
}
|
|
105
|
-
/** Hot-load external signatures. Atomic swap — takes effect on next scan. */
|
|
106
|
-
loadExternalSignatures(sigs) {
|
|
107
|
-
const minRank = SEVERITY_RANK[this._config.minSeverity];
|
|
108
|
-
const asSigDef = sigs.map(s => ({
|
|
109
|
-
id: s.id,
|
|
110
|
-
threatClass: s.threatClass,
|
|
111
|
-
pattern: s.pattern,
|
|
112
|
-
severity: s.severity,
|
|
113
|
-
description: s.description,
|
|
114
|
-
direction: s.direction,
|
|
115
|
-
})).filter(s => !this._config.disabledSignatures.has(s.id) &&
|
|
116
|
-
SEVERITY_RANK[s.severity] >= minRank);
|
|
117
|
-
this._externalRequestSigs = asSigDef.filter(s => s.direction === "request" || s.direction === "both");
|
|
118
|
-
this._externalResponseSigs = asSigDef.filter(s => s.direction === "response" || s.direction === "both");
|
|
119
|
-
}
|
|
120
|
-
/** Get count of loaded external signatures. */
|
|
121
|
-
getExternalSignatureCount() {
|
|
122
|
-
return this._externalRequestSigs.length + this._externalResponseSigs.length;
|
|
123
|
-
}
|
|
124
|
-
/** Scan request/outbound text for injection patterns. */
|
|
125
|
-
scanRequest(text) {
|
|
126
|
-
if (this._config.action === "off")
|
|
127
|
-
return [];
|
|
128
|
-
// Token smuggling defence: strip invisible characters then re-scan.
|
|
129
|
-
// Attackers insert zero-width spaces, soft hyphens, word joiners etc.
|
|
130
|
-
// between tokens to break regex matching: "ignore previous instructions"
|
|
131
|
-
const cleaned = stripTokenSmuggling(text);
|
|
132
|
-
const smuggled = cleaned !== text;
|
|
133
|
-
const allRequestSigs = this._externalRequestSigs.length > 0
|
|
134
|
-
? [...this._requestSigs, ...this._externalRequestSigs]
|
|
135
|
-
: this._requestSigs;
|
|
136
|
-
const events = this._scanPatterns(text, allRequestSigs, "request");
|
|
137
|
-
// If smuggling chars were present, also scan the cleaned version
|
|
138
|
-
// to catch patterns that were broken by invisible chars
|
|
139
|
-
if (smuggled) {
|
|
140
|
-
const cleanedEvents = this._scanPatterns(cleaned, this._requestSigs, "request");
|
|
141
|
-
// Add cleaned-text detections that weren't found in original
|
|
142
|
-
const existingIds = new Set(events.map(e => e.signatureId));
|
|
143
|
-
for (const evt of cleanedEvents) {
|
|
144
|
-
if (!existingIds.has(evt.signatureId)) {
|
|
145
|
-
evt.description = `[token-smuggling stripped] ${evt.description}`;
|
|
146
|
-
events.push(evt);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// Also flag the smuggling itself
|
|
150
|
-
const action = this._config.action === "block" ? "blocked" : "flagged";
|
|
151
|
-
events.push({
|
|
152
|
-
timestamp: Date.now(),
|
|
153
|
-
eventType: "injection_detected",
|
|
154
|
-
direction: "request",
|
|
155
|
-
threatClass: ThreatClass.ENCODING_BYPASS,
|
|
156
|
-
signatureId: "eb_token_smuggling",
|
|
157
|
-
severity: "medium",
|
|
158
|
-
matchedText: `[${text.length - cleaned.length} invisible chars stripped]`,
|
|
159
|
-
matchStart: 0,
|
|
160
|
-
matchEnd: text.length,
|
|
161
|
-
textLength: text.length,
|
|
162
|
-
action,
|
|
163
|
-
description: `Token smuggling: ${text.length - cleaned.length} invisible characters removed, revealing injection patterns`,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
// Base64 decode-and-rescan
|
|
167
|
-
events.push(...this._scanEncodedPayloads(text, "request"));
|
|
168
|
-
return events;
|
|
169
|
-
}
|
|
170
|
-
/** Scan response/inbound text for exfiltration patterns. */
|
|
171
|
-
scanResponse(text) {
|
|
172
|
-
if (this._config.action === "off" || !this._config.scanResponses)
|
|
173
|
-
return [];
|
|
174
|
-
const allResponseSigs = this._externalResponseSigs.length > 0
|
|
175
|
-
? [...this._responseSigs, ...this._externalResponseSigs]
|
|
176
|
-
: this._responseSigs;
|
|
177
|
-
return this._scanPatterns(text, allResponseSigs, "response");
|
|
178
|
-
}
|
|
179
|
-
_scanPatterns(text, signatures, direction) {
|
|
180
|
-
const events = [];
|
|
181
|
-
const action = this._config.action === "block" ? "blocked" : "flagged";
|
|
182
|
-
for (const sig of signatures) {
|
|
183
|
-
// Reset lastIndex for global regexes
|
|
184
|
-
sig.pattern.lastIndex = 0;
|
|
185
|
-
let match;
|
|
186
|
-
while ((match = sig.pattern.exec(text)) !== null) {
|
|
187
|
-
// Skip OpenClaw system context: "System: [2026-03-30 10:11:29 GMT+2]"
|
|
188
|
-
// This is structural metadata, not a conversation mockup injection.
|
|
189
|
-
if (sig.id === "cm_role_markers") {
|
|
190
|
-
const after = text.slice(match.index + match[0].length - 1, match.index + match[0].length + 30);
|
|
191
|
-
if (/^\[\d{4}-\d{2}-\d{2}/.test(after))
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
// Context-aware severity reduction: if the match is inside
|
|
195
|
-
// quotation marks or backticks, it's likely being discussed/quoted
|
|
196
|
-
// rather than used as an attack. Reduce severity to "low".
|
|
197
|
-
let severity = sig.severity;
|
|
198
|
-
if (isInsideQuotes(text, match.index, match.index + match[0].length)) {
|
|
199
|
-
severity = "low";
|
|
200
|
-
}
|
|
201
|
-
events.push({
|
|
202
|
-
timestamp: Date.now(),
|
|
203
|
-
eventType: "injection_detected",
|
|
204
|
-
direction,
|
|
205
|
-
threatClass: sig.threatClass,
|
|
206
|
-
signatureId: sig.id,
|
|
207
|
-
severity,
|
|
208
|
-
matchedText: match[0].slice(0, 200),
|
|
209
|
-
matchStart: match.index,
|
|
210
|
-
matchEnd: match.index + match[0].length,
|
|
211
|
-
textLength: text.length,
|
|
212
|
-
action,
|
|
213
|
-
description: severity !== sig.severity
|
|
214
|
-
? `[quoted context] ${sig.description}`
|
|
215
|
-
: sig.description,
|
|
216
|
-
});
|
|
217
|
-
// For non-global patterns, break after first match
|
|
218
|
-
if (!sig.pattern.global)
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return events;
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Detect Base64-encoded injection payloads.
|
|
226
|
-
*
|
|
227
|
-
* Finds Base64-looking blocks (40+ chars), decodes them (sync via Buffer),
|
|
228
|
-
* and checks if the decoded text contains injection keywords.
|
|
229
|
-
*/
|
|
230
|
-
_scanEncodedPayloads(text, direction) {
|
|
231
|
-
const events = [];
|
|
232
|
-
const action = this._config.action === "block" ? "blocked" : "flagged";
|
|
233
|
-
const b64Re = /[A-Za-z0-9+/]{40,}={0,2}/g;
|
|
234
|
-
let match;
|
|
235
|
-
while ((match = b64Re.exec(text)) !== null) {
|
|
236
|
-
try {
|
|
237
|
-
const decoded = Buffer.from(match[0], "base64").toString("utf-8");
|
|
238
|
-
// Check if decoded text is mostly printable ASCII (heuristic for valid text)
|
|
239
|
-
const printableRatio = decoded.replace(/[^\x20-\x7E]/g, "").length / decoded.length;
|
|
240
|
-
if (printableRatio < 0.7)
|
|
241
|
-
continue;
|
|
242
|
-
const lower = decoded.toLowerCase();
|
|
243
|
-
for (const keyword of BASE64_INJECTION_KEYWORDS) {
|
|
244
|
-
if (lower.includes(keyword)) {
|
|
245
|
-
events.push({
|
|
246
|
-
timestamp: Date.now(),
|
|
247
|
-
eventType: "injection_detected",
|
|
248
|
-
direction,
|
|
249
|
-
threatClass: ThreatClass.ENCODING_BYPASS,
|
|
250
|
-
signatureId: "eb_base64_injection",
|
|
251
|
-
severity: "high",
|
|
252
|
-
matchedText: `[base64→"${decoded.slice(0, 100)}"]`,
|
|
253
|
-
matchStart: match.index,
|
|
254
|
-
matchEnd: match.index + match[0].length,
|
|
255
|
-
textLength: text.length,
|
|
256
|
-
action,
|
|
257
|
-
description: `Encoding bypass: Base64-encoded text contains injection keyword "${keyword}"`,
|
|
258
|
-
});
|
|
259
|
-
break; // One event per base64 block
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
// Invalid base64 — skip
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return events;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool call guard — detects dangerous tool invocations.
|
|
3
|
-
*
|
|
4
|
-
* Scans tool names and parameters for destructive, exfiltration,
|
|
5
|
-
* or privilege escalation patterns. Runs in the before_tool_call hook
|
|
6
|
-
* where it can BLOCK the call before execution.
|
|
7
|
-
*
|
|
8
|
-
* This is the "exec shutdown" detector — catches:
|
|
9
|
-
* - Destructive commands (rm -rf, drop table, shutdown, format, kill -9)
|
|
10
|
-
* - Exfiltration (curl/wget to external, scp, netcat listeners)
|
|
11
|
-
* - Credential access (cat /etc/shadow, reading .ssh keys)
|
|
12
|
-
* - Reverse shells (bash -i >& /dev/tcp, nc -e, python -c import socket)
|
|
13
|
-
* - Privilege escalation (sudo, su, chmod 777, chown root)
|
|
14
|
-
* - Crypto mining (xmrig, minerd, coin patterns)
|
|
15
|
-
*/
|
|
16
|
-
import { SecurityEvent } from "../security-event.js";
|
|
17
|
-
/**
|
|
18
|
-
* Scan a tool call for dangerous patterns.
|
|
19
|
-
*
|
|
20
|
-
* @param toolName - The tool being called (e.g. "exec", "write", "code_execution")
|
|
21
|
-
* @param params - The tool parameters (will be serialized to JSON for scanning)
|
|
22
|
-
* @returns Array of security events. Check `.block` on the pattern for block recommendation.
|
|
23
|
-
*/
|
|
24
|
-
export declare function scanToolCall(toolName: string, params: unknown): {
|
|
25
|
-
events: SecurityEvent[];
|
|
26
|
-
shouldBlock: boolean;
|
|
27
|
-
};
|