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.
Files changed (40) hide show
  1. package/README.md +19 -10
  2. package/dist/hooks.js +246 -14
  3. package/openclaw.plugin.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/agent-session.d.ts +0 -259
  6. package/dist/agent-session.js +0 -693
  7. package/dist/compliance.d.ts +0 -44
  8. package/dist/compliance.js +0 -76
  9. package/dist/dashboard.d.ts +0 -42
  10. package/dist/dashboard.js +0 -1558
  11. package/dist/detectors/injection-multilingual.d.ts +0 -27
  12. package/dist/detectors/injection-multilingual.js +0 -399
  13. package/dist/detectors/injection-signatures.d.ts +0 -26
  14. package/dist/detectors/injection-signatures.js +0 -508
  15. package/dist/detectors/injection.d.ts +0 -56
  16. package/dist/detectors/injection.js +0 -269
  17. package/dist/detectors/tool-guard.d.ts +0 -27
  18. package/dist/detectors/tool-guard.js +0 -418
  19. package/dist/event-grader.d.ts +0 -97
  20. package/dist/event-grader.js +0 -214
  21. package/dist/exposure.d.ts +0 -29
  22. package/dist/exposure.js +0 -72
  23. package/dist/policy.d.ts +0 -99
  24. package/dist/policy.js +0 -212
  25. package/dist/profiler-analysis.d.ts +0 -35
  26. package/dist/profiler-analysis.js +0 -230
  27. package/dist/profiler-store.d.ts +0 -33
  28. package/dist/profiler-store.js +0 -118
  29. package/dist/profiler-types.d.ts +0 -128
  30. package/dist/profiler-types.js +0 -16
  31. package/dist/profiler.d.ts +0 -81
  32. package/dist/profiler.js +0 -392
  33. package/dist/security-event.d.ts +0 -70
  34. package/dist/security-event.js +0 -80
  35. package/dist/siem.d.ts +0 -49
  36. package/dist/siem.js +0 -113
  37. package/dist/signature-loader.d.ts +0 -113
  38. package/dist/signature-loader.js +0 -255
  39. package/dist/store-file.d.ts +0 -26
  40. package/dist/store-file.js +0 -79
package/dist/siem.js DELETED
@@ -1,113 +0,0 @@
1
- /**
2
- * SIEM integration — ships security events to external systems.
3
- *
4
- * Two output modes:
5
- * 1. Webhook POST: sends events as JSON to a configured URL (Splunk HEC, Grafana Loki, custom)
6
- * 2. JSONL file: appends events as newline-delimited JSON to a local file
7
- *
8
- * Both are async fire-and-forget — never blocks the request pipeline.
9
- * Zero runtime dependencies.
10
- */
11
- import { appendFileSync, mkdirSync } from "node:fs";
12
- import { dirname } from "node:path";
13
- import { request as httpRequest } from "node:http";
14
- import { request as httpsRequest } from "node:https";
15
- /**
16
- * SIEM event shipper. Subscribes to SecurityEventBus and ships events
17
- * to configured destinations.
18
- */
19
- export class SiemShipper {
20
- _config;
21
- _buffer = [];
22
- _flushTimer = null;
23
- _stats = { shipped: 0, webhookErrors: 0, fileErrors: 0 };
24
- constructor(config) {
25
- this._config = config;
26
- // Start flush timer if batching
27
- if (config.batchSize > 1 && config.flushIntervalMs > 0) {
28
- this._flushTimer = setInterval(() => this.flush(), config.flushIntervalMs);
29
- }
30
- // Ensure JSONL directory exists
31
- if (config.jsonlPath) {
32
- try {
33
- mkdirSync(dirname(config.jsonlPath), { recursive: true });
34
- }
35
- catch { }
36
- }
37
- }
38
- /** Called for each security event. Buffers and flushes. */
39
- onEvent(event) {
40
- this._buffer.push(event);
41
- if (this._buffer.length >= this._config.batchSize) {
42
- this.flush();
43
- }
44
- }
45
- /** Flush buffered events to all configured destinations. */
46
- flush() {
47
- if (this._buffer.length === 0)
48
- return;
49
- const events = this._buffer.splice(0);
50
- // JSONL file output
51
- if (this._config.jsonlPath) {
52
- try {
53
- const lines = events.map(e => JSON.stringify(e)).join("\n") + "\n";
54
- appendFileSync(this._config.jsonlPath, lines);
55
- this._stats.shipped += events.length;
56
- }
57
- catch {
58
- this._stats.fileErrors++;
59
- }
60
- }
61
- // Webhook POST
62
- if (this._config.webhookUrl) {
63
- this._postWebhook(events);
64
- }
65
- }
66
- /** Stop the flush timer and flush remaining events. */
67
- stop() {
68
- if (this._flushTimer) {
69
- clearInterval(this._flushTimer);
70
- this._flushTimer = null;
71
- }
72
- this.flush();
73
- }
74
- /** Get shipping stats. */
75
- getStats() {
76
- return { ...this._stats, buffered: this._buffer.length };
77
- }
78
- _postWebhook(events) {
79
- try {
80
- const url = new URL(this._config.webhookUrl);
81
- const isHttps = url.protocol === "https:";
82
- const reqFn = isHttps ? httpsRequest : httpRequest;
83
- const body = JSON.stringify({ events, count: events.length, timestamp: new Date().toISOString() });
84
- const options = {
85
- hostname: url.hostname,
86
- port: url.port || (isHttps ? 443 : 80),
87
- path: url.pathname + url.search,
88
- method: "POST",
89
- headers: {
90
- "Content-Type": "application/json",
91
- "Content-Length": Buffer.byteLength(body),
92
- ...(this._config.webhookAuth ? { Authorization: this._config.webhookAuth } : {}),
93
- },
94
- };
95
- const req = reqFn(options, (res) => {
96
- // Drain response
97
- res.resume();
98
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
99
- this._stats.shipped += events.length;
100
- }
101
- else {
102
- this._stats.webhookErrors++;
103
- }
104
- });
105
- req.on("error", () => { this._stats.webhookErrors++; });
106
- req.write(body);
107
- req.end();
108
- }
109
- catch {
110
- this._stats.webhookErrors++;
111
- }
112
- }
113
- }
@@ -1,113 +0,0 @@
1
- /**
2
- * Hot-refresh signature loader.
3
- *
4
- * Fetches external signature definitions from a URL or local file,
5
- * merges them with built-in signatures at runtime. Supports periodic
6
- * polling with atomic swap — no restart needed.
7
- *
8
- * Zero runtime dependencies — uses Node.js built-in http/https + fs.
9
- *
10
- * Signature JSON schema:
11
- * {
12
- * "version": "1.0",
13
- * "updated": "2026-03-30T12:00:00Z",
14
- * "injectionSignatures": [
15
- * { "id": "custom_sig_1", "threatClass": "instruction_override",
16
- * "pattern": "ignore all safety", "flags": "gi",
17
- * "severity": "high", "description": "...", "direction": "request" }
18
- * ],
19
- * "toolGuardPatterns": [
20
- * { "id": "custom_tg_1", "paramPattern": "kubectl delete ns",
21
- * "flags": "gi", "severity": "high", "description": "...",
22
- * "block": true, "toolName": null }
23
- * ]
24
- * }
25
- */
26
- /** External signature definition (JSON-friendly — pattern is a string, not RegExp). */
27
- export interface ExternalSignature {
28
- id: string;
29
- threatClass: string;
30
- pattern: string;
31
- flags?: string;
32
- severity: "low" | "medium" | "high";
33
- description: string;
34
- direction: "request" | "response" | "both";
35
- }
36
- /** External tool guard pattern (JSON-friendly). */
37
- export interface ExternalToolGuard {
38
- id: string;
39
- toolName: string | null;
40
- paramPattern: string;
41
- flags?: string;
42
- severity: "low" | "medium" | "high";
43
- description: string;
44
- block: boolean;
45
- }
46
- /** The full signature feed payload. */
47
- export interface SignatureFeed {
48
- version: string;
49
- updated: string;
50
- injectionSignatures?: ExternalSignature[];
51
- toolGuardPatterns?: ExternalToolGuard[];
52
- }
53
- /** Compiled signatures ready for the scanner. */
54
- export interface CompiledSignatures {
55
- injection: Array<{
56
- id: string;
57
- threatClass: string;
58
- pattern: RegExp;
59
- severity: "low" | "medium" | "high";
60
- description: string;
61
- direction: "request" | "response" | "both";
62
- }>;
63
- toolGuard: Array<{
64
- id: string;
65
- toolName: string | null;
66
- paramPattern: RegExp;
67
- severity: "low" | "medium" | "high";
68
- description: string;
69
- block: boolean;
70
- }>;
71
- feedVersion: string;
72
- feedUpdated: string;
73
- loadedAt: number;
74
- }
75
- /**
76
- * Signature loader with hot-refresh support.
77
- */
78
- export declare class SignatureLoader {
79
- private _url;
80
- private _filePath;
81
- private _refreshMs;
82
- private _timer;
83
- private _current;
84
- private _onUpdate;
85
- private _cacheDir;
86
- constructor(opts: {
87
- url?: string | null;
88
- filePath?: string | null;
89
- refreshSec?: number;
90
- cacheDir?: string;
91
- });
92
- /** Register a callback for when signatures are refreshed. */
93
- onUpdate(cb: (sigs: CompiledSignatures) => void): void;
94
- /** Get the currently loaded external signatures (null if none loaded). */
95
- getCurrent(): CompiledSignatures | null;
96
- /** Start polling. Does an immediate load, then polls on interval. */
97
- start(): Promise<void>;
98
- /** Stop polling. */
99
- stop(): void;
100
- /** Force a manual refresh. */
101
- refresh(): Promise<CompiledSignatures | null>;
102
- private _refresh;
103
- /** Compile JSON signature definitions into RegExp objects with safety validation. */
104
- private _compile;
105
- /** Validate an injection signature before compilation. */
106
- private _validateSig;
107
- /** Validate a tool guard pattern before compilation. */
108
- private _validateToolGuard;
109
- /** Sanitize regex flags — only allow safe flags. */
110
- private _sanitizeFlags;
111
- /** Fetch a URL using Node.js built-in http/https. Zero dependencies. */
112
- private _fetchUrl;
113
- }
@@ -1,255 +0,0 @@
1
- /**
2
- * Hot-refresh signature loader.
3
- *
4
- * Fetches external signature definitions from a URL or local file,
5
- * merges them with built-in signatures at runtime. Supports periodic
6
- * polling with atomic swap — no restart needed.
7
- *
8
- * Zero runtime dependencies — uses Node.js built-in http/https + fs.
9
- *
10
- * Signature JSON schema:
11
- * {
12
- * "version": "1.0",
13
- * "updated": "2026-03-30T12:00:00Z",
14
- * "injectionSignatures": [
15
- * { "id": "custom_sig_1", "threatClass": "instruction_override",
16
- * "pattern": "ignore all safety", "flags": "gi",
17
- * "severity": "high", "description": "...", "direction": "request" }
18
- * ],
19
- * "toolGuardPatterns": [
20
- * { "id": "custom_tg_1", "paramPattern": "kubectl delete ns",
21
- * "flags": "gi", "severity": "high", "description": "...",
22
- * "block": true, "toolName": null }
23
- * ]
24
- * }
25
- */
26
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
27
- import { join } from "node:path";
28
- import * as nodeHttps from "node:https";
29
- import * as nodeHttp from "node:http";
30
- /**
31
- * Signature loader with hot-refresh support.
32
- */
33
- export class SignatureLoader {
34
- _url;
35
- _filePath;
36
- _refreshMs;
37
- _timer = null;
38
- _current = null;
39
- _onUpdate = null;
40
- _cacheDir;
41
- constructor(opts) {
42
- this._url = opts.url || null;
43
- this._filePath = opts.filePath || null;
44
- this._refreshMs = (opts.refreshSec || 3600) * 1000;
45
- this._cacheDir = opts.cacheDir || "/tmp/shroud-signatures";
46
- }
47
- /** Register a callback for when signatures are refreshed. */
48
- onUpdate(cb) {
49
- this._onUpdate = cb;
50
- }
51
- /** Get the currently loaded external signatures (null if none loaded). */
52
- getCurrent() {
53
- return this._current;
54
- }
55
- /** Start polling. Does an immediate load, then polls on interval. */
56
- async start() {
57
- if (!this._url && !this._filePath)
58
- return;
59
- // Immediate load
60
- await this._refresh();
61
- // Periodic poll
62
- if (this._refreshMs > 0) {
63
- this._timer = setInterval(() => this._refresh(), this._refreshMs);
64
- // Don't keep the process alive for the timer
65
- if (this._timer.unref)
66
- this._timer.unref();
67
- }
68
- }
69
- /** Stop polling. */
70
- stop() {
71
- if (this._timer) {
72
- clearInterval(this._timer);
73
- this._timer = null;
74
- }
75
- }
76
- /** Force a manual refresh. */
77
- async refresh() {
78
- await this._refresh();
79
- return this._current;
80
- }
81
- async _refresh() {
82
- try {
83
- let json = null;
84
- // Try URL first
85
- if (this._url) {
86
- json = await this._fetchUrl(this._url);
87
- // Cache to local file for offline resilience
88
- if (json) {
89
- try {
90
- mkdirSync(this._cacheDir, { recursive: true });
91
- writeFileSync(join(this._cacheDir, "signatures-cache.json"), json, "utf-8");
92
- }
93
- catch { }
94
- }
95
- }
96
- // Fall back to local file
97
- if (!json && this._filePath) {
98
- try {
99
- json = readFileSync(this._filePath, "utf-8");
100
- }
101
- catch { }
102
- }
103
- // Fall back to cached copy
104
- if (!json) {
105
- const cachePath = join(this._cacheDir, "signatures-cache.json");
106
- if (existsSync(cachePath)) {
107
- try {
108
- json = readFileSync(cachePath, "utf-8");
109
- }
110
- catch { }
111
- }
112
- }
113
- if (!json)
114
- return;
115
- // Max feed size: 500KB — reject bloated/malicious payloads
116
- if (json.length > 512_000)
117
- return;
118
- const feed = JSON.parse(json);
119
- // Max 500 signatures per feed — prevent resource exhaustion
120
- if ((feed.injectionSignatures?.length || 0) > 500)
121
- return;
122
- if ((feed.toolGuardPatterns?.length || 0) > 500)
123
- return;
124
- const compiled = this._compile(feed);
125
- // Only update if version changed or first load
126
- if (!this._current || compiled.feedVersion !== this._current.feedVersion ||
127
- compiled.feedUpdated !== this._current.feedUpdated) {
128
- this._current = compiled;
129
- if (this._onUpdate)
130
- this._onUpdate(compiled);
131
- }
132
- }
133
- catch {
134
- // Non-fatal — keep using existing signatures
135
- }
136
- }
137
- /** Compile JSON signature definitions into RegExp objects with safety validation. */
138
- _compile(feed) {
139
- const injection = (feed.injectionSignatures || [])
140
- .filter(sig => this._validateSig(sig))
141
- .map(sig => ({
142
- id: sig.id,
143
- threatClass: sig.threatClass,
144
- pattern: new RegExp(sig.pattern, this._sanitizeFlags(sig.flags)),
145
- severity: sig.severity,
146
- description: sig.description,
147
- direction: sig.direction,
148
- }));
149
- const toolGuard = (feed.toolGuardPatterns || [])
150
- .filter(tg => this._validateToolGuard(tg))
151
- .map(tg => ({
152
- id: tg.id,
153
- toolName: tg.toolName,
154
- paramPattern: new RegExp(tg.paramPattern, this._sanitizeFlags(tg.flags)),
155
- severity: tg.severity,
156
- description: tg.description,
157
- block: tg.block,
158
- }));
159
- return {
160
- injection,
161
- toolGuard,
162
- feedVersion: feed.version || "unknown",
163
- feedUpdated: feed.updated || new Date().toISOString(),
164
- loadedAt: Date.now(),
165
- };
166
- }
167
- /** Validate an injection signature before compilation. */
168
- _validateSig(sig) {
169
- // Must have required fields
170
- if (!sig.id || !sig.pattern || !sig.severity || !sig.direction)
171
- return false;
172
- // ID must be prefixed with ext_ (external signatures can't impersonate built-ins)
173
- if (!sig.id.startsWith("ext_"))
174
- return false;
175
- // Severity must be valid
176
- if (!["low", "medium", "high"].includes(sig.severity))
177
- return false;
178
- // Direction must be valid
179
- if (!["request", "response", "both"].includes(sig.direction))
180
- return false;
181
- // Pattern length cap — prevents ReDoS via catastrophic backtracking
182
- if (sig.pattern.length > 500)
183
- return false;
184
- // Block nested quantifiers (ReDoS: (a+)+ or (a*)*b)
185
- if (/\([^)]*[+*][^)]*\)[+*]/.test(sig.pattern))
186
- return false;
187
- // Test compile — reject if regex is invalid
188
- try {
189
- new RegExp(sig.pattern);
190
- }
191
- catch {
192
- return false;
193
- }
194
- // Description length cap
195
- if ((sig.description || "").length > 300)
196
- return false;
197
- return true;
198
- }
199
- /** Validate a tool guard pattern before compilation. */
200
- _validateToolGuard(tg) {
201
- if (!tg.id || !tg.paramPattern || !tg.severity)
202
- return false;
203
- if (!tg.id.startsWith("ext_"))
204
- return false;
205
- if (!["low", "medium", "high"].includes(tg.severity))
206
- return false;
207
- if (tg.paramPattern.length > 500)
208
- return false;
209
- if (/\([^)]*[+*][^)]*\)[+*]/.test(tg.paramPattern))
210
- return false;
211
- try {
212
- new RegExp(tg.paramPattern);
213
- }
214
- catch {
215
- return false;
216
- }
217
- if ((tg.description || "").length > 300)
218
- return false;
219
- return true;
220
- }
221
- /** Sanitize regex flags — only allow safe flags. */
222
- _sanitizeFlags(flags) {
223
- if (!flags)
224
- return "gi";
225
- return flags.replace(/[^gimsuy]/g, "") || "gi";
226
- }
227
- /** Fetch a URL using Node.js built-in http/https. Zero dependencies. */
228
- _fetchUrl(url) {
229
- return new Promise((resolve) => {
230
- try {
231
- const mod = url.startsWith("https") ? nodeHttps : nodeHttp;
232
- const req = mod.get(url, { timeout: 10_000 }, (res) => {
233
- // Follow redirects (1 level)
234
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
235
- this._fetchUrl(res.headers.location).then(resolve);
236
- return;
237
- }
238
- if (res.statusCode !== 200) {
239
- resolve(null);
240
- return;
241
- }
242
- const chunks = [];
243
- res.on("data", (c) => chunks.push(c));
244
- res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
245
- res.on("error", () => resolve(null));
246
- });
247
- req.on("error", () => resolve(null));
248
- req.on("timeout", () => { req.destroy(); resolve(null); });
249
- }
250
- catch {
251
- resolve(null);
252
- }
253
- });
254
- }
255
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * File-backed mapping store — persists to JSON on disk.
3
- * Wraps MemoryStore with periodic flush and load-on-startup.
4
- */
5
- import { Category } from "./types.js";
6
- import { MappingStore } from "./store.js";
7
- export declare class FileBackedStore implements MappingStore {
8
- private _inner;
9
- private _filePath;
10
- private _dirty;
11
- private _flushTimer;
12
- private _flushIntervalMs;
13
- constructor(filePath: string, maxSize?: number, flushIntervalMs?: number);
14
- put(real: string, fake: string, category: Category): void;
15
- getFake(real: string): string | undefined;
16
- getReal(fake: string): string | undefined;
17
- getCategory(real: string): Category | undefined;
18
- allMappings(): Map<string, string>;
19
- size(): number;
20
- clear(): void;
21
- /** Flush dirty mappings to disk. */
22
- flush(): void;
23
- /** Stop the periodic flush timer. */
24
- stop(): void;
25
- private _loadFromDisk;
26
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * File-backed mapping store — persists to JSON on disk.
3
- * Wraps MemoryStore with periodic flush and load-on-startup.
4
- */
5
- import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
6
- import { dirname } from "node:path";
7
- import { MemoryStore } from "./store.js";
8
- export class FileBackedStore {
9
- _inner;
10
- _filePath;
11
- _dirty = false;
12
- _flushTimer = null;
13
- _flushIntervalMs;
14
- constructor(filePath, maxSize = 0, flushIntervalMs = 5000) {
15
- this._inner = new MemoryStore(maxSize);
16
- this._filePath = filePath;
17
- this._flushIntervalMs = flushIntervalMs;
18
- // Ensure directory exists
19
- const dir = dirname(filePath);
20
- if (!existsSync(dir)) {
21
- mkdirSync(dir, { recursive: true });
22
- }
23
- // Load existing data
24
- this._loadFromDisk();
25
- // Periodic flush
26
- this._flushTimer = setInterval(() => this.flush(), this._flushIntervalMs);
27
- if (this._flushTimer.unref)
28
- this._flushTimer.unref();
29
- }
30
- put(real, fake, category) {
31
- this._inner.put(real, fake, category);
32
- this._dirty = true;
33
- }
34
- getFake(real) { return this._inner.getFake(real); }
35
- getReal(fake) { return this._inner.getReal(fake); }
36
- getCategory(real) { return this._inner.getCategory(real); }
37
- allMappings() { return this._inner.allMappings(); }
38
- size() { return this._inner.size(); }
39
- clear() {
40
- this._inner.clear();
41
- this._dirty = true;
42
- this.flush();
43
- }
44
- /** Flush dirty mappings to disk. */
45
- flush() {
46
- if (!this._dirty)
47
- return;
48
- try {
49
- const data = this._inner.export("", undefined);
50
- writeFileSync(this._filePath, JSON.stringify(data, null, 2) + "\n");
51
- this._dirty = false;
52
- }
53
- catch (e) {
54
- // best-effort — log but don't crash
55
- process.stderr.write(`[shroud-store] Flush failed: ${e.message}\n`);
56
- }
57
- }
58
- /** Stop the periodic flush timer. */
59
- stop() {
60
- if (this._flushTimer) {
61
- clearInterval(this._flushTimer);
62
- this._flushTimer = null;
63
- }
64
- this.flush(); // final flush
65
- }
66
- _loadFromDisk() {
67
- if (!existsSync(this._filePath))
68
- return;
69
- try {
70
- const raw = readFileSync(this._filePath, "utf-8");
71
- const data = JSON.parse(raw);
72
- this._inner.import(data);
73
- process.stderr.write(`[shroud-store] Loaded ${data.mappings.length} mappings from ${this._filePath}\n`);
74
- }
75
- catch (e) {
76
- process.stderr.write(`[shroud-store] Load failed: ${e.message}\n`);
77
- }
78
- }
79
- }