shroud-privacy 2.2.20 → 2.3.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/README.md +66 -6
- package/dist/config-manager.d.ts +79 -0
- package/dist/config-manager.js +419 -0
- package/dist/config.js +21 -0
- package/dist/detectors/regex.d.ts +10 -1
- package/dist/detectors/regex.js +72 -1
- package/dist/index.js +16 -0
- package/dist/obfuscator.d.ts +8 -1
- package/dist/obfuscator.js +30 -2
- package/dist/types.d.ts +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,18 +233,78 @@ Out of the box, Shroud:
|
|
|
233
233
|
|
|
234
234
|
> **Env var overrides:** `SHROUD_SECRET_KEY` and `SHROUD_PERSISTENT_SALT` override their respective config keys (priority: env var > plugin config > default).
|
|
235
235
|
|
|
236
|
-
###
|
|
236
|
+
### Detection rules as code (hot-reload)
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
Shroud auto-generates a JSONC config file on first run containing every built-in detection rule:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
~/.shroud/shroud.config.json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The file is fully editable. Changes hot-reload within 2 seconds — no gateway restart needed.
|
|
245
|
+
|
|
246
|
+
**Priority:** env vars > config file > plugin config > defaults.
|
|
247
|
+
|
|
248
|
+
#### Override a built-in rule
|
|
249
|
+
|
|
250
|
+
Change the regex, confidence, or category of any rule:
|
|
251
|
+
|
|
252
|
+
```jsonc
|
|
253
|
+
{
|
|
254
|
+
"rules": {
|
|
255
|
+
"email": { "pattern": "\\b[\\w.+-]+@[\\w-]+\\.[a-z]{2,}\\b", "confidence": 0.99 }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Disable a rule
|
|
239
261
|
|
|
240
262
|
```jsonc
|
|
241
|
-
|
|
242
|
-
"
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
{
|
|
264
|
+
"rules": {
|
|
265
|
+
"phone_us": { "enabled": false },
|
|
266
|
+
"gps_coordinate": { "enabled": false }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Add a custom rule
|
|
272
|
+
|
|
273
|
+
```jsonc
|
|
274
|
+
{
|
|
275
|
+
"rules": {
|
|
276
|
+
"internal_ticket": {
|
|
277
|
+
"pattern": "\\bTICK-\\d{6}\\b",
|
|
278
|
+
"category": "custom",
|
|
279
|
+
"confidence": 0.9
|
|
280
|
+
}
|
|
281
|
+
}
|
|
245
282
|
}
|
|
246
283
|
```
|
|
247
284
|
|
|
285
|
+
#### Rule format
|
|
286
|
+
|
|
287
|
+
Each rule in the `rules` object supports:
|
|
288
|
+
|
|
289
|
+
| Field | Type | Description |
|
|
290
|
+
|-------|------|-------------|
|
|
291
|
+
| `pattern` | string | Regex pattern (required for new rules, optional for overrides) |
|
|
292
|
+
| `category` | string | Entity category: `email`, `ip_address`, `phone`, `hostname`, `network_credential`, `custom`, etc. |
|
|
293
|
+
| `confidence` | number | Detection confidence 0.0-1.0 (filtered by `minConfidence`) |
|
|
294
|
+
| `enabled` | boolean | Set to `false` to disable a rule |
|
|
295
|
+
|
|
296
|
+
#### Config manager features
|
|
297
|
+
|
|
298
|
+
| Feature | Detail |
|
|
299
|
+
|---------|--------|
|
|
300
|
+
| Format | JSONC (JSON with `//` and `/* */` comments) |
|
|
301
|
+
| Auto-create | Config file generated on first run with all built-in rules |
|
|
302
|
+
| Watch interval | 2 seconds |
|
|
303
|
+
| History depth | 50 versions (commit/rollback via dashboard API) |
|
|
304
|
+
| Restart-only fields | `secretKey`, `persistentSalt`, `dashboardEnabled`, `dashboardPort`, `maxStoreMappings` — logged as warnings, not applied until restart |
|
|
305
|
+
|
|
306
|
+
> **Legacy:** `detectorOverrides` and `customPatterns` in `openclaw.json` still work. The `rules` config is the preferred way — it replaces both.
|
|
307
|
+
|
|
248
308
|
---
|
|
249
309
|
|
|
250
310
|
## URL handling
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config-as-code manager with hot-reload.
|
|
3
|
+
*
|
|
4
|
+
* Watches a JSONC config file (`~/.shroud/shroud.config.json` by default)
|
|
5
|
+
* and merges it with the base config from `resolveConfig(pluginConfig)`.
|
|
6
|
+
*
|
|
7
|
+
* Priority: env vars > config file > plugin config > defaults.
|
|
8
|
+
*
|
|
9
|
+
* Supports:
|
|
10
|
+
* - JSONC (JSON with // and /* comments)
|
|
11
|
+
* - Keyed field-change callbacks (only fires when watched fields change)
|
|
12
|
+
* - Generic reload callbacks
|
|
13
|
+
* - Commit/rollback with 50-version history
|
|
14
|
+
* - Dashboard read/write via setFields()
|
|
15
|
+
*/
|
|
16
|
+
import type { ShroudConfig } from "./types.js";
|
|
17
|
+
import { type ConfigIssue } from "./config.js";
|
|
18
|
+
/** A versioned config snapshot. */
|
|
19
|
+
export interface ConfigCommit {
|
|
20
|
+
version: number;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
description: string;
|
|
23
|
+
config: Partial<ShroudConfig>;
|
|
24
|
+
}
|
|
25
|
+
export declare class ConfigManager {
|
|
26
|
+
private _configPath;
|
|
27
|
+
private _historyPath;
|
|
28
|
+
private _base;
|
|
29
|
+
private _fileOverrides;
|
|
30
|
+
private _effective;
|
|
31
|
+
private _fieldListeners;
|
|
32
|
+
private _reloadListeners;
|
|
33
|
+
private _history;
|
|
34
|
+
private _version;
|
|
35
|
+
private _watching;
|
|
36
|
+
constructor(configPath: string, baseConfig: ShroudConfig);
|
|
37
|
+
/** Current effective config (merged). */
|
|
38
|
+
getEffective(): ShroudConfig;
|
|
39
|
+
/** Raw overrides from the config file. */
|
|
40
|
+
getFileOverrides(): Partial<ShroudConfig>;
|
|
41
|
+
/** Path to the config file. */
|
|
42
|
+
getConfigPath(): string;
|
|
43
|
+
/** Current version number (increments on each commit). */
|
|
44
|
+
getCurrentVersion(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Register a callback that fires when any of the specified fields change.
|
|
47
|
+
* The callback is deduplicated: even if multiple watched fields change in
|
|
48
|
+
* one reload, the callback fires exactly once.
|
|
49
|
+
*/
|
|
50
|
+
onFieldChange(fields: string[], callback: () => void): void;
|
|
51
|
+
/** Register a callback that fires on every successful reload. */
|
|
52
|
+
onReload(callback: (config: ShroudConfig) => void): void;
|
|
53
|
+
startWatching(): void;
|
|
54
|
+
stopWatching(): void;
|
|
55
|
+
/** Merge partial config into the file and trigger reload. */
|
|
56
|
+
setFields(partial: Partial<ShroudConfig>): {
|
|
57
|
+
changedFields: string[];
|
|
58
|
+
warnings: string[];
|
|
59
|
+
};
|
|
60
|
+
/** Validate a partial config without applying it. */
|
|
61
|
+
validate(partial: Partial<ShroudConfig>): ConfigIssue[];
|
|
62
|
+
commit(description: string): ConfigCommit;
|
|
63
|
+
rollback(version: number): ConfigCommit | null;
|
|
64
|
+
getHistory(): ConfigCommit[];
|
|
65
|
+
/** Generate default config file with all built-in detection rules. */
|
|
66
|
+
private _writeDefaultConfig;
|
|
67
|
+
private _reload;
|
|
68
|
+
private _loadFile;
|
|
69
|
+
private _saveFile;
|
|
70
|
+
private _merge;
|
|
71
|
+
private _diff;
|
|
72
|
+
private _loadHistory;
|
|
73
|
+
private _saveHistory;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Strip // and /* comments from JSONC text.
|
|
77
|
+
* Handles strings correctly (doesn't strip inside quoted values).
|
|
78
|
+
*/
|
|
79
|
+
export declare function stripComments(jsonc: string): string;
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config-as-code manager with hot-reload.
|
|
3
|
+
*
|
|
4
|
+
* Watches a JSONC config file (`~/.shroud/shroud.config.json` by default)
|
|
5
|
+
* and merges it with the base config from `resolveConfig(pluginConfig)`.
|
|
6
|
+
*
|
|
7
|
+
* Priority: env vars > config file > plugin config > defaults.
|
|
8
|
+
*
|
|
9
|
+
* Supports:
|
|
10
|
+
* - JSONC (JSON with // and /* comments)
|
|
11
|
+
* - Keyed field-change callbacks (only fires when watched fields change)
|
|
12
|
+
* - Generic reload callbacks
|
|
13
|
+
* - Commit/rollback with 50-version history
|
|
14
|
+
* - Dashboard read/write via setFields()
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, mkdirSync } from "node:fs";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
|
+
import { validateConfig } from "./config.js";
|
|
19
|
+
import { BUILTIN_PATTERNS } from "./detectors/regex.js";
|
|
20
|
+
/** Fields that cannot be hot-reloaded — require gateway restart. */
|
|
21
|
+
const RESTART_ONLY = new Set([
|
|
22
|
+
"secretKey",
|
|
23
|
+
"persistentSalt",
|
|
24
|
+
"dashboardEnabled",
|
|
25
|
+
"dashboardPort",
|
|
26
|
+
"maxStoreMappings",
|
|
27
|
+
]);
|
|
28
|
+
export class ConfigManager {
|
|
29
|
+
_configPath;
|
|
30
|
+
_historyPath;
|
|
31
|
+
_base;
|
|
32
|
+
_fileOverrides;
|
|
33
|
+
_effective;
|
|
34
|
+
_fieldListeners = [];
|
|
35
|
+
_reloadListeners = [];
|
|
36
|
+
_history = [];
|
|
37
|
+
_version = 0;
|
|
38
|
+
_watching = false;
|
|
39
|
+
constructor(configPath, baseConfig) {
|
|
40
|
+
this._configPath = configPath;
|
|
41
|
+
this._historyPath = configPath + ".history.json";
|
|
42
|
+
this._base = baseConfig;
|
|
43
|
+
// Auto-create config file with built-in rules if it doesn't exist
|
|
44
|
+
if (!existsSync(configPath)) {
|
|
45
|
+
this._writeDefaultConfig();
|
|
46
|
+
}
|
|
47
|
+
this._fileOverrides = this._loadFile();
|
|
48
|
+
this._effective = this._merge(this._base, this._fileOverrides);
|
|
49
|
+
this._loadHistory();
|
|
50
|
+
}
|
|
51
|
+
/** Current effective config (merged). */
|
|
52
|
+
getEffective() {
|
|
53
|
+
return this._effective;
|
|
54
|
+
}
|
|
55
|
+
/** Raw overrides from the config file. */
|
|
56
|
+
getFileOverrides() {
|
|
57
|
+
return { ...this._fileOverrides };
|
|
58
|
+
}
|
|
59
|
+
/** Path to the config file. */
|
|
60
|
+
getConfigPath() {
|
|
61
|
+
return this._configPath;
|
|
62
|
+
}
|
|
63
|
+
/** Current version number (increments on each commit). */
|
|
64
|
+
getCurrentVersion() {
|
|
65
|
+
return this._version;
|
|
66
|
+
}
|
|
67
|
+
// ── Subscriptions ──────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* Register a callback that fires when any of the specified fields change.
|
|
70
|
+
* The callback is deduplicated: even if multiple watched fields change in
|
|
71
|
+
* one reload, the callback fires exactly once.
|
|
72
|
+
*/
|
|
73
|
+
onFieldChange(fields, callback) {
|
|
74
|
+
this._fieldListeners.push({ fields: new Set(fields), callback });
|
|
75
|
+
}
|
|
76
|
+
/** Register a callback that fires on every successful reload. */
|
|
77
|
+
onReload(callback) {
|
|
78
|
+
this._reloadListeners.push(callback);
|
|
79
|
+
}
|
|
80
|
+
// ── File watching ──────────────────────────────────────
|
|
81
|
+
startWatching() {
|
|
82
|
+
if (this._watching)
|
|
83
|
+
return;
|
|
84
|
+
this._watching = true;
|
|
85
|
+
// Ensure parent directory exists so watchFile can stat the path
|
|
86
|
+
mkdirSync(dirname(this._configPath), { recursive: true });
|
|
87
|
+
watchFile(this._configPath, { interval: 2000 }, () => {
|
|
88
|
+
this._reload();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
stopWatching() {
|
|
92
|
+
if (!this._watching)
|
|
93
|
+
return;
|
|
94
|
+
this._watching = false;
|
|
95
|
+
unwatchFile(this._configPath);
|
|
96
|
+
}
|
|
97
|
+
// ── Programmatic writes (dashboard) ────────────────────
|
|
98
|
+
/** Merge partial config into the file and trigger reload. */
|
|
99
|
+
setFields(partial) {
|
|
100
|
+
const warnings = [];
|
|
101
|
+
const cleaned = {};
|
|
102
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
103
|
+
if (RESTART_ONLY.has(key)) {
|
|
104
|
+
warnings.push(`"${key}" requires gateway restart — skipped`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
cleaned[key] = value;
|
|
108
|
+
}
|
|
109
|
+
// Merge with existing file overrides
|
|
110
|
+
const merged = { ...this._fileOverrides, ...cleaned };
|
|
111
|
+
this._saveFile(merged);
|
|
112
|
+
const changedFields = this._reload();
|
|
113
|
+
return { changedFields, warnings };
|
|
114
|
+
}
|
|
115
|
+
/** Validate a partial config without applying it. */
|
|
116
|
+
validate(partial) {
|
|
117
|
+
const testConfig = this._merge(this._base, { ...this._fileOverrides, ...partial });
|
|
118
|
+
return validateConfig(testConfig);
|
|
119
|
+
}
|
|
120
|
+
// ── Commit / rollback ──────────────────────────────────
|
|
121
|
+
commit(description) {
|
|
122
|
+
this._version++;
|
|
123
|
+
const entry = {
|
|
124
|
+
version: this._version,
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
description,
|
|
127
|
+
config: { ...this._fileOverrides },
|
|
128
|
+
};
|
|
129
|
+
this._history.push(entry);
|
|
130
|
+
// Cap at 50
|
|
131
|
+
if (this._history.length > 50) {
|
|
132
|
+
this._history = this._history.slice(-50);
|
|
133
|
+
}
|
|
134
|
+
this._saveHistory();
|
|
135
|
+
return entry;
|
|
136
|
+
}
|
|
137
|
+
rollback(version) {
|
|
138
|
+
const entry = this._history.find(h => h.version === version);
|
|
139
|
+
if (!entry)
|
|
140
|
+
return null;
|
|
141
|
+
this._saveFile(entry.config);
|
|
142
|
+
this._reload();
|
|
143
|
+
return entry;
|
|
144
|
+
}
|
|
145
|
+
getHistory() {
|
|
146
|
+
return [...this._history];
|
|
147
|
+
}
|
|
148
|
+
// ── Internal ───────────────────────────────────────────
|
|
149
|
+
/** Generate default config file with all built-in detection rules. */
|
|
150
|
+
_writeDefaultConfig() {
|
|
151
|
+
const lines = [
|
|
152
|
+
"{",
|
|
153
|
+
" // Shroud config-as-code — auto-generated with built-in detection rules.",
|
|
154
|
+
" // Edit rules here. Changes hot-reload within 2 seconds (no restart needed).",
|
|
155
|
+
" // Priority: env vars > this file > plugin config > defaults.",
|
|
156
|
+
" //",
|
|
157
|
+
" // Rule format:",
|
|
158
|
+
' // "rule_name": {',
|
|
159
|
+
' // "pattern": "regex string", // override or define the detection regex',
|
|
160
|
+
' // "category": "email", // entity category (email, ip_address, phone, etc.)',
|
|
161
|
+
' // "confidence": 0.95, // detection confidence (0.0-1.0)',
|
|
162
|
+
' // "enabled": false // set to false to disable a rule',
|
|
163
|
+
" // }",
|
|
164
|
+
" //",
|
|
165
|
+
' "rules": {',
|
|
166
|
+
];
|
|
167
|
+
for (let i = 0; i < BUILTIN_PATTERNS.length; i++) {
|
|
168
|
+
const p = BUILTIN_PATTERNS[i];
|
|
169
|
+
const comma = i < BUILTIN_PATTERNS.length - 1 ? "," : "";
|
|
170
|
+
// Convert RegExp to source string + flags
|
|
171
|
+
const flags = p.pattern.flags.replace("g", "") || undefined; // "g" is always added; only store extra flags (i, m, etc.)
|
|
172
|
+
const flagsPart = flags ? `, "flags": "${flags}"` : "";
|
|
173
|
+
lines.push(` "${p.name}": { "pattern": ${JSON.stringify(p.pattern.source)}, "category": "${p.category}", "confidence": ${p.confidence}${flagsPart} }${comma}`);
|
|
174
|
+
}
|
|
175
|
+
lines.push(" }");
|
|
176
|
+
lines.push("}");
|
|
177
|
+
lines.push("");
|
|
178
|
+
try {
|
|
179
|
+
mkdirSync(dirname(this._configPath), { recursive: true });
|
|
180
|
+
writeFileSync(this._configPath, lines.join("\n"), "utf-8");
|
|
181
|
+
}
|
|
182
|
+
catch { /* non-fatal — config file is optional */ }
|
|
183
|
+
}
|
|
184
|
+
_reload() {
|
|
185
|
+
const oldEffective = this._effective;
|
|
186
|
+
try {
|
|
187
|
+
this._fileOverrides = this._loadFile();
|
|
188
|
+
this._effective = this._merge(this._base, this._fileOverrides);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Corrupt file — keep previous config
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const changed = this._diff(oldEffective, this._effective);
|
|
195
|
+
// Filter out restart-only fields — log warning but don't apply
|
|
196
|
+
const restartChanged = changed.filter(f => RESTART_ONLY.has(f));
|
|
197
|
+
if (restartChanged.length > 0) {
|
|
198
|
+
console.warn(`[shroud][config] Fields require restart (not applied): ${restartChanged.join(", ")}`);
|
|
199
|
+
// Revert restart-only fields to base values
|
|
200
|
+
for (const field of restartChanged) {
|
|
201
|
+
this._effective[field] =
|
|
202
|
+
oldEffective[field];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const effectiveChanged = changed.filter(f => !RESTART_ONLY.has(f));
|
|
206
|
+
if (effectiveChanged.length > 0) {
|
|
207
|
+
// Fire field-specific listeners
|
|
208
|
+
const firedCallbacks = new Set();
|
|
209
|
+
for (const listener of this._fieldListeners) {
|
|
210
|
+
if (effectiveChanged.some(f => listener.fields.has(f)) && !firedCallbacks.has(listener.callback)) {
|
|
211
|
+
firedCallbacks.add(listener.callback);
|
|
212
|
+
try {
|
|
213
|
+
listener.callback();
|
|
214
|
+
}
|
|
215
|
+
catch { /* non-fatal */ }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Fire generic reload listeners
|
|
219
|
+
for (const cb of this._reloadListeners) {
|
|
220
|
+
try {
|
|
221
|
+
cb(this._effective);
|
|
222
|
+
}
|
|
223
|
+
catch { /* non-fatal */ }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return effectiveChanged;
|
|
227
|
+
}
|
|
228
|
+
_loadFile() {
|
|
229
|
+
if (!existsSync(this._configPath))
|
|
230
|
+
return {};
|
|
231
|
+
try {
|
|
232
|
+
const raw = readFileSync(this._configPath, "utf-8");
|
|
233
|
+
const stripped = stripComments(raw);
|
|
234
|
+
return JSON.parse(stripped);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return {};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
_saveFile(config) {
|
|
241
|
+
try {
|
|
242
|
+
mkdirSync(dirname(this._configPath), { recursive: true });
|
|
243
|
+
writeFileSync(this._configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.warn(`[shroud][config] Failed to write config file: ${err}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
_merge(base, overlay) {
|
|
250
|
+
const result = { ...base };
|
|
251
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
252
|
+
if (value !== undefined) {
|
|
253
|
+
result[key] = value;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Re-apply env var overrides (env always wins)
|
|
257
|
+
return applyEnvOverrides(result);
|
|
258
|
+
}
|
|
259
|
+
_diff(oldConfig, newConfig) {
|
|
260
|
+
const changed = [];
|
|
261
|
+
const allKeys = Array.from(new Set([
|
|
262
|
+
...Object.keys(oldConfig),
|
|
263
|
+
...Object.keys(newConfig),
|
|
264
|
+
]));
|
|
265
|
+
for (const key of allKeys) {
|
|
266
|
+
const oldVal = oldConfig[key];
|
|
267
|
+
const newVal = newConfig[key];
|
|
268
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
269
|
+
changed.push(key);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return changed;
|
|
273
|
+
}
|
|
274
|
+
_loadHistory() {
|
|
275
|
+
if (!existsSync(this._historyPath))
|
|
276
|
+
return;
|
|
277
|
+
try {
|
|
278
|
+
const raw = readFileSync(this._historyPath, "utf-8");
|
|
279
|
+
const parsed = JSON.parse(raw);
|
|
280
|
+
if (Array.isArray(parsed)) {
|
|
281
|
+
this._history = parsed;
|
|
282
|
+
const maxVersion = this._history.reduce((max, h) => Math.max(max, h.version), 0);
|
|
283
|
+
this._version = maxVersion;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch { /* start fresh */ }
|
|
287
|
+
}
|
|
288
|
+
_saveHistory() {
|
|
289
|
+
try {
|
|
290
|
+
mkdirSync(dirname(this._historyPath), { recursive: true });
|
|
291
|
+
writeFileSync(this._historyPath, JSON.stringify(this._history, null, 2) + "\n", "utf-8");
|
|
292
|
+
}
|
|
293
|
+
catch { /* non-fatal */ }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Strip // and /* comments from JSONC text.
|
|
298
|
+
* Handles strings correctly (doesn't strip inside quoted values).
|
|
299
|
+
*/
|
|
300
|
+
export function stripComments(jsonc) {
|
|
301
|
+
let result = "";
|
|
302
|
+
let i = 0;
|
|
303
|
+
const len = jsonc.length;
|
|
304
|
+
while (i < len) {
|
|
305
|
+
// String literal — copy verbatim
|
|
306
|
+
if (jsonc[i] === '"') {
|
|
307
|
+
const start = i;
|
|
308
|
+
i++; // opening quote
|
|
309
|
+
while (i < len && jsonc[i] !== '"') {
|
|
310
|
+
if (jsonc[i] === "\\")
|
|
311
|
+
i++; // skip escaped char
|
|
312
|
+
i++;
|
|
313
|
+
}
|
|
314
|
+
i++; // closing quote
|
|
315
|
+
result += jsonc.slice(start, i);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// Line comment
|
|
319
|
+
if (jsonc[i] === "/" && jsonc[i + 1] === "/") {
|
|
320
|
+
while (i < len && jsonc[i] !== "\n")
|
|
321
|
+
i++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
// Block comment
|
|
325
|
+
if (jsonc[i] === "/" && jsonc[i + 1] === "*") {
|
|
326
|
+
i += 2;
|
|
327
|
+
while (i < len && !(jsonc[i] === "*" && jsonc[i + 1] === "/"))
|
|
328
|
+
i++;
|
|
329
|
+
i += 2; // skip */
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
result += jsonc[i];
|
|
333
|
+
i++;
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Re-apply environment variable overrides to a config.
|
|
339
|
+
* Env vars always win over file and plugin config.
|
|
340
|
+
*/
|
|
341
|
+
function applyEnvOverrides(config) {
|
|
342
|
+
const result = { ...config };
|
|
343
|
+
const env = process.env;
|
|
344
|
+
// Boolean env overrides
|
|
345
|
+
const boolOverrides = [
|
|
346
|
+
["SHROUD_CANARY_ENABLED", "canaryEnabled"],
|
|
347
|
+
["SHROUD_HONEYPOT_ENABLED", "honeypotEnabled"],
|
|
348
|
+
["SHROUD_PROFILING_ENABLED", "profilingEnabled"],
|
|
349
|
+
["SHROUD_CANARY_SYSTEM", "canarySystemInjection"],
|
|
350
|
+
["SHROUD_CANARY_BEHAVIOURAL", "canaryBehavioural"],
|
|
351
|
+
["SHROUD_INJECTION_SCAN_RESPONSES", "injectionScanResponses"],
|
|
352
|
+
["SHROUD_DRIFT_ENABLED", "driftEnabled"],
|
|
353
|
+
["SHROUD_SHADOW_EXECUTION", "shadowExecutionEnabled"],
|
|
354
|
+
["SHROUD_DASHBOARD", "dashboardEnabled"],
|
|
355
|
+
["SHROUD_COHERENCE_ENABLED", "coherenceEnabled"],
|
|
356
|
+
["SHROUD_VECTOR_STORE_ENABLED", "vectorStoreEnabled"],
|
|
357
|
+
["SHROUD_CLUSTERING_ENABLED", "clusteringEnabled"],
|
|
358
|
+
["SHROUD_URL_CORRELATION_ENABLED", "urlCorrelationEnabled"],
|
|
359
|
+
["SHROUD_INTENT_CHAIN_ENABLED", "intentChainEnabled"],
|
|
360
|
+
["SHROUD_TRANSFORMER_ENABLED", "transformerEnabled"],
|
|
361
|
+
];
|
|
362
|
+
for (const [envKey, field] of boolOverrides) {
|
|
363
|
+
if (env[envKey] === "true")
|
|
364
|
+
result[field] = true;
|
|
365
|
+
else if (env[envKey] === "false")
|
|
366
|
+
result[field] = false;
|
|
367
|
+
}
|
|
368
|
+
// Enum env overrides
|
|
369
|
+
if (env.SHROUD_INJECTION_DETECTION === "flag" || env.SHROUD_INJECTION_DETECTION === "block" || env.SHROUD_INJECTION_DETECTION === "off") {
|
|
370
|
+
result.injectionDetection = env.SHROUD_INJECTION_DETECTION;
|
|
371
|
+
}
|
|
372
|
+
if (env.SHROUD_INJECTION_MIN_SEVERITY === "low" || env.SHROUD_INJECTION_MIN_SEVERITY === "medium" || env.SHROUD_INJECTION_MIN_SEVERITY === "high") {
|
|
373
|
+
result.injectionMinSeverity = env.SHROUD_INJECTION_MIN_SEVERITY;
|
|
374
|
+
}
|
|
375
|
+
if (env.SHROUD_PROFILING_MODE === "learning" || env.SHROUD_PROFILING_MODE === "active" || env.SHROUD_PROFILING_MODE === "strict") {
|
|
376
|
+
result.profilingMode = env.SHROUD_PROFILING_MODE;
|
|
377
|
+
}
|
|
378
|
+
// String env overrides
|
|
379
|
+
const stringOverrides = [
|
|
380
|
+
["SHROUD_SECRET_KEY", "secretKey"],
|
|
381
|
+
["SHROUD_PERSISTENT_SALT", "persistentSalt"],
|
|
382
|
+
["SHROUD_PROFILING_DIR", "profilingProfileDir"],
|
|
383
|
+
["SHROUD_SIGNATURES_URL", "signaturesUrl"],
|
|
384
|
+
["SHROUD_SIGNATURES_FILE", "signaturesFile"],
|
|
385
|
+
["SHROUD_SIEM_WEBHOOK_URL", "siemWebhookUrl"],
|
|
386
|
+
["SHROUD_SIEM_WEBHOOK_AUTH", "siemWebhookAuth"],
|
|
387
|
+
["SHROUD_SIEM_JSONL_PATH", "siemJsonlPath"],
|
|
388
|
+
];
|
|
389
|
+
for (const [envKey, field] of stringOverrides) {
|
|
390
|
+
if (env[envKey])
|
|
391
|
+
result[field] = env[envKey];
|
|
392
|
+
}
|
|
393
|
+
// Numeric env overrides
|
|
394
|
+
const numOverrides = [
|
|
395
|
+
["SHROUD_DRIFT_THRESHOLD", "driftThreshold"],
|
|
396
|
+
["SHROUD_DRIFT_SUDDEN_TURN", "driftSuddenTurnDelta"],
|
|
397
|
+
["SHROUD_COHERENCE_ZSCORE", "coherenceZScore"],
|
|
398
|
+
["SHROUD_COHERENCE_RESULT_LIMIT", "coherenceResultLimit"],
|
|
399
|
+
["SHROUD_TRANSFORMER_THRESHOLD", "transformerThreshold"],
|
|
400
|
+
["SHROUD_TRANSFORMER_WINDOW", "transformerWindowSize"],
|
|
401
|
+
["SHROUD_TRANSFORMER_MIN_SESSIONS", "transformerMinSessions"],
|
|
402
|
+
["SHROUD_TRANSFORMER_TRAIN_INTERVAL", "transformerTrainInterval"],
|
|
403
|
+
["SHROUD_TRANSFORMER_INTENT_ATTENTION_THRESHOLD", "transformerIntentAttentionThreshold"],
|
|
404
|
+
["SHROUD_SIGNATURES_REFRESH", "signaturesRefreshSec"],
|
|
405
|
+
["SHROUD_DASHBOARD_PORT", "dashboardPort"],
|
|
406
|
+
["SHROUD_VECTOR_STORE_MAX", "vectorStoreMax"],
|
|
407
|
+
["SHROUD_DELEGATION_DRIFT_THRESHOLD", "delegationDriftThreshold"],
|
|
408
|
+
["SHROUD_SHADOW_TIMEOUT", "shadowExecutionTimeoutMs"],
|
|
409
|
+
["SHROUD_HONEYPOT_RATE", "honeypotRate"],
|
|
410
|
+
];
|
|
411
|
+
for (const [envKey, field] of numOverrides) {
|
|
412
|
+
if (env[envKey]) {
|
|
413
|
+
const parsed = parseFloat(env[envKey]);
|
|
414
|
+
if (!isNaN(parsed))
|
|
415
|
+
result[field] = parsed;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -71,6 +71,9 @@ export function resolveConfig(pluginConfig) {
|
|
|
71
71
|
detectorOverrides: raw.detectorOverrides != null && typeof raw.detectorOverrides === "object"
|
|
72
72
|
? raw.detectorOverrides
|
|
73
73
|
: {},
|
|
74
|
+
rules: raw.rules != null && typeof raw.rules === "object"
|
|
75
|
+
? raw.rules
|
|
76
|
+
: {},
|
|
74
77
|
// Tool chain depth
|
|
75
78
|
maxToolDepth: typeof raw.maxToolDepth === "number" ? raw.maxToolDepth : 10,
|
|
76
79
|
// Redaction level
|
|
@@ -121,5 +124,23 @@ export function validateConfig(config) {
|
|
|
121
124
|
if (Object.keys(config.detectorOverrides).length > 0) {
|
|
122
125
|
issues.push({ severity: "info", field: "detectorOverrides", message: `${Object.keys(config.detectorOverrides).length} detector override(s) configured.` });
|
|
123
126
|
}
|
|
127
|
+
// Rules as code
|
|
128
|
+
if (config.rules && Object.keys(config.rules).length > 0) {
|
|
129
|
+
const ruleCount = Object.keys(config.rules).length;
|
|
130
|
+
const disabled = Object.values(config.rules).filter(r => r.enabled === false).length;
|
|
131
|
+
const custom = Object.entries(config.rules).filter(([, r]) => r.pattern && r.enabled !== false).length;
|
|
132
|
+
issues.push({ severity: "info", field: "rules", message: `${ruleCount} rule(s) configured (${custom} custom/override, ${disabled} disabled).` });
|
|
133
|
+
// Validate regex patterns
|
|
134
|
+
for (const [name, rule] of Object.entries(config.rules)) {
|
|
135
|
+
if (rule.pattern) {
|
|
136
|
+
try {
|
|
137
|
+
new RegExp(rule.pattern);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
issues.push({ severity: "error", field: "rules", message: `Rule "${name}" has invalid regex pattern.` });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
124
145
|
return issues;
|
|
125
146
|
}
|
|
@@ -19,10 +19,19 @@ export type DetectorOverrides = Record<string, {
|
|
|
19
19
|
enabled?: boolean;
|
|
20
20
|
confidence?: number;
|
|
21
21
|
}>;
|
|
22
|
+
/** Config-as-code rule definition (pattern as string, not RegExp). */
|
|
23
|
+
export type ConfigRule = {
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
pattern?: string;
|
|
26
|
+
/** Extra regex flags beyond "g" (e.g. "i", "m", "im"). Always gets "g" automatically. */
|
|
27
|
+
flags?: string;
|
|
28
|
+
category?: string;
|
|
29
|
+
confidence?: number;
|
|
30
|
+
};
|
|
22
31
|
/** Detects sensitive entities using regex patterns. */
|
|
23
32
|
export declare class RegexDetector implements BaseDetector {
|
|
24
33
|
readonly name = "regex";
|
|
25
34
|
private patterns;
|
|
26
|
-
constructor(extraPatterns?: PatternDef[], overrides?: DetectorOverrides);
|
|
35
|
+
constructor(extraPatterns?: PatternDef[], overrides?: DetectorOverrides, configRules?: Record<string, ConfigRule>);
|
|
27
36
|
detect(text: string): DetectedEntity[];
|
|
28
37
|
}
|
package/dist/detectors/regex.js
CHANGED
|
@@ -1231,15 +1231,86 @@ class SpanTracker {
|
|
|
1231
1231
|
spans.splice(lo, 0, [start, end]);
|
|
1232
1232
|
}
|
|
1233
1233
|
}
|
|
1234
|
+
/** Resolve a category string to a Category enum value. */
|
|
1235
|
+
function resolveCategory(name) {
|
|
1236
|
+
const upper = name.toUpperCase().replace(/[^A-Z_]/g, "_");
|
|
1237
|
+
if (upper in Category)
|
|
1238
|
+
return Category[upper];
|
|
1239
|
+
// Common aliases
|
|
1240
|
+
const aliases = {
|
|
1241
|
+
EMAIL: Category.EMAIL,
|
|
1242
|
+
IP: Category.IP_ADDRESS,
|
|
1243
|
+
IP_ADDRESS: Category.IP_ADDRESS,
|
|
1244
|
+
PHONE: Category.PHONE,
|
|
1245
|
+
CREDIT_CARD: Category.CREDIT_CARD,
|
|
1246
|
+
SSN: Category.SSN,
|
|
1247
|
+
API_KEY: Category.API_KEY,
|
|
1248
|
+
URL: Category.URL,
|
|
1249
|
+
FILE_PATH: Category.FILE_PATH,
|
|
1250
|
+
HOSTNAME: Category.HOSTNAME,
|
|
1251
|
+
MAC_ADDRESS: Category.MAC_ADDRESS,
|
|
1252
|
+
NETWORK_CREDENTIAL: Category.NETWORK_CREDENTIAL,
|
|
1253
|
+
CUSTOM: Category.CUSTOM,
|
|
1254
|
+
};
|
|
1255
|
+
return aliases[upper] ?? Category.CUSTOM;
|
|
1256
|
+
}
|
|
1234
1257
|
/** Detects sensitive entities using regex patterns. */
|
|
1235
1258
|
export class RegexDetector {
|
|
1236
1259
|
name = "regex";
|
|
1237
1260
|
patterns;
|
|
1238
|
-
constructor(extraPatterns, overrides) {
|
|
1261
|
+
constructor(extraPatterns, overrides, configRules) {
|
|
1239
1262
|
let patterns = [...BUILTIN_PATTERNS];
|
|
1240
1263
|
if (extraPatterns) {
|
|
1241
1264
|
patterns.push(...extraPatterns);
|
|
1242
1265
|
}
|
|
1266
|
+
// Config-as-code rules: override built-in properties, disable, or add new
|
|
1267
|
+
if (configRules) {
|
|
1268
|
+
const builtinNames = new Set(patterns.map(p => p.name));
|
|
1269
|
+
// Override existing rules
|
|
1270
|
+
patterns = patterns.map((p) => {
|
|
1271
|
+
const rule = configRules[p.name];
|
|
1272
|
+
if (!rule)
|
|
1273
|
+
return p;
|
|
1274
|
+
const updated = { ...p };
|
|
1275
|
+
if (rule.pattern) {
|
|
1276
|
+
const flags = "g" + (rule.flags || "").replace(/g/g, "");
|
|
1277
|
+
try {
|
|
1278
|
+
updated.pattern = new RegExp(rule.pattern, flags);
|
|
1279
|
+
}
|
|
1280
|
+
catch { /* invalid regex — keep built-in */ }
|
|
1281
|
+
}
|
|
1282
|
+
if (rule.confidence !== undefined)
|
|
1283
|
+
updated.confidence = rule.confidence;
|
|
1284
|
+
if (rule.category)
|
|
1285
|
+
updated.category = resolveCategory(rule.category);
|
|
1286
|
+
return updated;
|
|
1287
|
+
});
|
|
1288
|
+
// Disable rules
|
|
1289
|
+
patterns = patterns.filter((p) => {
|
|
1290
|
+
const rule = configRules[p.name];
|
|
1291
|
+
return rule?.enabled !== false;
|
|
1292
|
+
});
|
|
1293
|
+
// Add new rules (names not in built-ins)
|
|
1294
|
+
for (const [name, rule] of Object.entries(configRules)) {
|
|
1295
|
+
if (builtinNames.has(name))
|
|
1296
|
+
continue; // already handled above
|
|
1297
|
+
if (rule.enabled === false)
|
|
1298
|
+
continue;
|
|
1299
|
+
if (!rule.pattern)
|
|
1300
|
+
continue; // new rules must have a pattern
|
|
1301
|
+
try {
|
|
1302
|
+
const flags = "g" + (rule.flags || "").replace(/g/g, "");
|
|
1303
|
+
patterns.push({
|
|
1304
|
+
name,
|
|
1305
|
+
pattern: new RegExp(rule.pattern, flags),
|
|
1306
|
+
category: rule.category ? resolveCategory(rule.category) : Category.CUSTOM,
|
|
1307
|
+
confidence: rule.confidence ?? 0.9,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
catch { /* invalid regex — skip */ }
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
// Legacy detectorOverrides (applied after config rules for backwards compat)
|
|
1243
1314
|
if (overrides) {
|
|
1244
1315
|
patterns = patterns.filter((p) => {
|
|
1245
1316
|
const ov = overrides[p.name];
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join, dirname } from "node:path";
|
|
|
10
10
|
import { resolveConfig } from "./config.js";
|
|
11
11
|
import { Obfuscator } from "./obfuscator.js";
|
|
12
12
|
import { registerHooks } from "./hooks.js";
|
|
13
|
+
import { ConfigManager } from "./config-manager.js";
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Runtime prototype patch: wrap EventStream.prototype.push() with the
|
|
15
16
|
// Shroud deobfuscation hook. No file reads, no file writes, no cache
|
|
@@ -115,6 +116,21 @@ export default {
|
|
|
115
116
|
patchEventStreamPrototype(api.logger);
|
|
116
117
|
const config = resolveConfig(api.pluginConfig);
|
|
117
118
|
const obfuscator = new Obfuscator(config);
|
|
119
|
+
// Config-as-code: watch ~/.shroud/shroud.config.json for hot-reload.
|
|
120
|
+
// Defer startWatching so watchFile doesn't block plugin install (which
|
|
121
|
+
// loads the plugin to verify it, then expects the process to exit).
|
|
122
|
+
// Resolve config path: prefer OPENCLAW_STATE_DIR, then HOME/.shroud
|
|
123
|
+
const configDir = process.env.OPENCLAW_STATE_DIR
|
|
124
|
+
? join(process.env.OPENCLAW_STATE_DIR, ".shroud")
|
|
125
|
+
: join(process.env.HOME || "/root", ".shroud");
|
|
126
|
+
const configPath = join(configDir, "shroud.config.json");
|
|
127
|
+
const configManager = new ConfigManager(configPath, config);
|
|
128
|
+
configManager.onReload((newConfig) => {
|
|
129
|
+
obfuscator.updateConfig(newConfig);
|
|
130
|
+
api.logger?.info("[shroud] Config hot-reloaded from " + configPath);
|
|
131
|
+
});
|
|
132
|
+
const watchTimer = setTimeout(() => configManager.startWatching(), 5000);
|
|
133
|
+
watchTimer.unref();
|
|
118
134
|
registerHooks(api, obfuscator);
|
|
119
135
|
// Register shroud_status tool
|
|
120
136
|
api.registerTool({
|
package/dist/obfuscator.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { DetectedEntity, ObfuscationResult, ShroudConfig } from "./types.js";
|
|
8
8
|
import { BaseDetector } from "./detectors/base.js";
|
|
9
9
|
export declare class Obfuscator {
|
|
10
|
-
|
|
10
|
+
config: ShroudConfig;
|
|
11
11
|
private _store;
|
|
12
12
|
private _subnetMapper;
|
|
13
13
|
private _mapping;
|
|
@@ -26,6 +26,13 @@ export declare class Obfuscator {
|
|
|
26
26
|
private _toolDepth;
|
|
27
27
|
constructor(config: ShroudConfig);
|
|
28
28
|
private _initDetectors;
|
|
29
|
+
/**
|
|
30
|
+
* Hot-swap config at runtime (called by ConfigManager on reload).
|
|
31
|
+
* Preserves mappings, stores, and stats — only swaps behaviour flags.
|
|
32
|
+
* Fields that require restart (secretKey, persistentSalt, etc.) are
|
|
33
|
+
* already filtered by ConfigManager before this is called.
|
|
34
|
+
*/
|
|
35
|
+
updateConfig(newConfig: ShroudConfig): void;
|
|
29
36
|
/** Add a custom detector at runtime. */
|
|
30
37
|
addDetector(detector: BaseDetector): void;
|
|
31
38
|
/** Track tool call depth. */
|
package/dist/obfuscator.js
CHANGED
|
@@ -230,8 +230,9 @@ export class Obfuscator {
|
|
|
230
230
|
}
|
|
231
231
|
_initDetectors() {
|
|
232
232
|
const overrides = this.config.detectorOverrides;
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
const configRules = this.config.rules;
|
|
234
|
+
// Always enable the regex detector (with config rules + legacy overrides)
|
|
235
|
+
const regexDetector = new RegexDetector(undefined, overrides, configRules);
|
|
235
236
|
// Wrap with ContextDetector for confidence boosting, proximity,
|
|
236
237
|
// hostname propagation, learned entities, and frequency decay
|
|
237
238
|
this._contextDetector = new ContextDetector(regexDetector);
|
|
@@ -243,6 +244,33 @@ export class Obfuscator {
|
|
|
243
244
|
// Code-aware detector shares the same configured regex detector
|
|
244
245
|
this._detectors.push(new CodeDetector(regexDetector));
|
|
245
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Hot-swap config at runtime (called by ConfigManager on reload).
|
|
249
|
+
* Preserves mappings, stores, and stats — only swaps behaviour flags.
|
|
250
|
+
* Fields that require restart (secretKey, persistentSalt, etc.) are
|
|
251
|
+
* already filtered by ConfigManager before this is called.
|
|
252
|
+
*/
|
|
253
|
+
updateConfig(newConfig) {
|
|
254
|
+
this.config = newConfig;
|
|
255
|
+
// Rebuild detectors to pick up new overrides / custom patterns
|
|
256
|
+
this._detectors = [];
|
|
257
|
+
this._contextDetector = null;
|
|
258
|
+
this._initDetectors();
|
|
259
|
+
// Toggle canary
|
|
260
|
+
if (newConfig.canaryEnabled && !this._canary) {
|
|
261
|
+
this._canary = new CanaryInjector(newConfig.canaryPrefix, newConfig.secretKey);
|
|
262
|
+
}
|
|
263
|
+
else if (!newConfig.canaryEnabled) {
|
|
264
|
+
this._canary = null;
|
|
265
|
+
}
|
|
266
|
+
// Toggle audit
|
|
267
|
+
if (newConfig.auditEnabled && !this._audit) {
|
|
268
|
+
this._audit = new AuditLogger(newConfig.secretKey);
|
|
269
|
+
}
|
|
270
|
+
else if (!newConfig.auditEnabled) {
|
|
271
|
+
this._audit = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
246
274
|
/** Add a custom detector at runtime. */
|
|
247
275
|
addDetector(detector) {
|
|
248
276
|
this._detectors.push(detector);
|
package/dist/types.d.ts
CHANGED
|
@@ -90,6 +90,19 @@ export interface ShroudConfig {
|
|
|
90
90
|
enabled?: boolean;
|
|
91
91
|
confidence?: number;
|
|
92
92
|
}>;
|
|
93
|
+
/**
|
|
94
|
+
* Detection rules as code. Each key is a rule name.
|
|
95
|
+
* - Override built-in rules: change pattern, confidence, or category
|
|
96
|
+
* - Disable rules: { "enabled": false }
|
|
97
|
+
* - Add new rules: { "pattern": "regex string", "category": "email", "confidence": 0.9 }
|
|
98
|
+
* Built-in rules from BUILTIN_PATTERNS are the defaults; this merges on top.
|
|
99
|
+
*/
|
|
100
|
+
rules: Record<string, {
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
pattern?: string;
|
|
103
|
+
category?: string;
|
|
104
|
+
confidence?: number;
|
|
105
|
+
}>;
|
|
93
106
|
/** Tool chain depth awareness — max depth before warning. */
|
|
94
107
|
maxToolDepth: number;
|
|
95
108
|
/** Redaction levels — 'full' | 'masked' | 'stats'. */
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shroud-privacy",
|
|
3
3
|
"name": "Shroud",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"description": "Privacy obfuscation with deterministic fake values and deobfuscation — PII never reaches the LLM, tool calls still work",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shroud-privacy",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Privacy and infrastructure protection for AI agents — detects sensitive data (PII, network topology, credentials, OT/SCADA) and replaces with deterministic fakes before anything reaches the LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|