shroud-privacy 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +7 -0
  3. package/README.md +369 -0
  4. package/dist/audit.d.ts +46 -0
  5. package/dist/audit.js +127 -0
  6. package/dist/canary.d.ts +31 -0
  7. package/dist/canary.js +73 -0
  8. package/dist/config.d.ts +27 -0
  9. package/dist/config.js +123 -0
  10. package/dist/detectors/base.d.ts +8 -0
  11. package/dist/detectors/base.js +2 -0
  12. package/dist/detectors/code.d.ts +25 -0
  13. package/dist/detectors/code.js +144 -0
  14. package/dist/detectors/context.d.ts +31 -0
  15. package/dist/detectors/context.js +357 -0
  16. package/dist/detectors/patterns.d.ts +15 -0
  17. package/dist/detectors/patterns.js +58 -0
  18. package/dist/detectors/regex.d.ts +28 -0
  19. package/dist/detectors/regex.js +955 -0
  20. package/dist/generators/base.d.ts +6 -0
  21. package/dist/generators/base.js +2 -0
  22. package/dist/generators/codes.d.ts +20 -0
  23. package/dist/generators/codes.js +231 -0
  24. package/dist/generators/names.d.ts +29 -0
  25. package/dist/generators/names.js +194 -0
  26. package/dist/generators/network.d.ts +86 -0
  27. package/dist/generators/network.js +477 -0
  28. package/dist/hooks.d.ts +27 -0
  29. package/dist/hooks.js +457 -0
  30. package/dist/index.d.ts +12 -0
  31. package/dist/index.js +58 -0
  32. package/dist/mapping.d.ts +33 -0
  33. package/dist/mapping.js +72 -0
  34. package/dist/obfuscator.d.ts +78 -0
  35. package/dist/obfuscator.js +603 -0
  36. package/dist/redaction.d.ts +26 -0
  37. package/dist/redaction.js +76 -0
  38. package/dist/store.d.ts +40 -0
  39. package/dist/store.js +79 -0
  40. package/dist/types.d.ts +101 -0
  41. package/dist/types.js +35 -0
  42. package/ncg_adapter.py +530 -0
  43. package/openclaw.plugin.json +72 -0
  44. package/package.json +56 -0
  45. package/shroud_bridge.mjs +225 -0
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Fake network entity generators: IPs, emails, URLs, domains, MACs, BGP ASNs.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import { Category } from "../types.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Domain pools
8
+ // ---------------------------------------------------------------------------
9
+ export const DOMAINS = [
10
+ "nexus.dev", "vertex.io", "prism.net", "atlas.org", "cipher.co",
11
+ "beacon.tech", "forge.dev", "crest.io", "pulse.net", "apex.org",
12
+ "echo.co", "nova.dev", "summit.io", "core.net", "bridge.org",
13
+ "spark.co", "tide.dev", "haven.io", "peak.net", "drift.org",
14
+ "flux.co", "orbit.dev", "zenith.io", "pine.net", "reef.org",
15
+ "slate.co", "wave.dev", "terra.io", "lunar.net", "solar.org",
16
+ ];
17
+ // TLD-grouped domains for format-preserving generation
18
+ export const DOMAINS_BY_TLD = {};
19
+ for (const d of DOMAINS) {
20
+ const tld = d.split(".").pop();
21
+ if (!DOMAINS_BY_TLD[tld])
22
+ DOMAINS_BY_TLD[tld] = [];
23
+ DOMAINS_BY_TLD[tld].push(d);
24
+ }
25
+ export const EMAIL_PREFIXES = [
26
+ "contact", "info", "admin", "support", "hello", "team", "ops",
27
+ "dev", "eng", "data", "sec", "cloud", "mail", "notify", "alerts",
28
+ "user", "agent", "bot", "service", "api",
29
+ ];
30
+ // Short/medium/long prefixes for length matching
31
+ export const EMAIL_PREFIXES_SHORT = EMAIL_PREFIXES.filter((p) => p.length <= 4);
32
+ export const EMAIL_PREFIXES_MEDIUM = EMAIL_PREFIXES.filter((p) => p.length > 4 && p.length <= 7);
33
+ export const EMAIL_PREFIXES_LONG = EMAIL_PREFIXES.filter((p) => p.length > 7);
34
+ export const SNMP_COMMUNITIES = [
35
+ "COMMUNITY_RO", "COMMUNITY_RW", "SNMP_STR_001", "SNMP_STR_002",
36
+ "MGMT_READ", "MGMT_WRITE", "MON_STRING", "NET_COMMUNITY",
37
+ ];
38
+ export const HOSTNAME_ROLES = [
39
+ "SW", "RTR", "FW", "AP", "SRV", "LB", "DC", "NAS",
40
+ ];
41
+ export const HOSTNAME_SITES = [
42
+ "SITE-A", "SITE-B", "SITE-C", "SITE-D", "SITE-E",
43
+ ];
44
+ export const PATH_SEGMENTS = [
45
+ "app", "api", "docs", "dashboard", "portal", "v2", "status", "health",
46
+ ];
47
+ // ---------------------------------------------------------------------------
48
+ // CGNAT constants
49
+ // ---------------------------------------------------------------------------
50
+ /** 100.64.0.0 as unsigned 32-bit int */
51
+ export const CGNAT_BASE = ((100 << 24) | (64 << 16)) >>> 0;
52
+ /** Mask for CGNAT /10 range */
53
+ export const CGNAT_MASK_10 = 0xffc00000;
54
+ const CGNAT_SIZE = 1 << 22; // 100.64.0.0 - 100.127.255.255
55
+ // ---------------------------------------------------------------------------
56
+ // IP / int helpers
57
+ // ---------------------------------------------------------------------------
58
+ export function ipToInt(ip) {
59
+ const parts = ip.split(".");
60
+ return ((((parseInt(parts[0], 10) << 24) |
61
+ (parseInt(parts[1], 10) << 16) |
62
+ (parseInt(parts[2], 10) << 8) |
63
+ parseInt(parts[3], 10)) >>>
64
+ 0));
65
+ }
66
+ export function intToIp(n) {
67
+ return [
68
+ (n >>> 24) & 0xff,
69
+ (n >>> 16) & 0xff,
70
+ (n >>> 8) & 0xff,
71
+ n & 0xff,
72
+ ].join(".");
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // SubnetMapper class
76
+ // ---------------------------------------------------------------------------
77
+ export class SubnetMapper {
78
+ /** Forward mapping: serialized key "netInt,prefixLen" -> fake network int */
79
+ subnetFwd = new Map();
80
+ /** Reverse mapping: fake network int -> serialized key */
81
+ subnetRev = new Map();
82
+ /** Next CGNAT slot to allocate */
83
+ subnetNextSlot = 0;
84
+ /** Learned subnets from text context */
85
+ knownSubnets = [];
86
+ /**
87
+ * Scan text for CIDR notation and subnet masks to learn subnet boundaries.
88
+ * Call this before obfuscating IPs so the generator knows the correct
89
+ * prefix length for each address.
90
+ */
91
+ learnSubnetsFromText(text) {
92
+ // CIDR notation: 10.0.0.0/22
93
+ const cidrRe = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})\b/g;
94
+ let m;
95
+ while ((m = cidrRe.exec(text)) !== null) {
96
+ try {
97
+ const prefixLen = parseInt(m[2], 10);
98
+ if (prefixLen < 0 || prefixLen > 32)
99
+ continue;
100
+ const ipInt = ipToInt(m[1]);
101
+ const mask = prefixLen === 0 ? 0 : ((0xffffffff << (32 - prefixLen)) >>> 0);
102
+ const netInt = (ipInt & mask) >>> 0;
103
+ this.knownSubnets.push({ networkInt: netInt, prefixLen });
104
+ }
105
+ catch {
106
+ // skip invalid
107
+ }
108
+ }
109
+ // Subnet masks near IPs: "10.130.24.0 mask 255.255.252.0" etc.
110
+ const maskPat = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?:mask\s+|netmask\s+)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
111
+ while ((m = maskPat.exec(text)) !== null) {
112
+ try {
113
+ const ipStr = m[1];
114
+ const maskStr = m[2];
115
+ let maskInt = ipToInt(maskStr);
116
+ // Determine if it's a subnet mask or wildcard mask
117
+ if (maskStr.startsWith("0.") && !maskStr.startsWith("0.0.0.0")) {
118
+ // Wildcard mask -> invert to get subnet mask
119
+ maskInt = (maskInt ^ 0xffffffff) >>> 0;
120
+ }
121
+ // Validate it's a valid mask (contiguous 1s then 0s)
122
+ const inverted = (maskInt ^ 0xffffffff) >>> 0;
123
+ if ((inverted & ((inverted + 1) >>> 0)) === 0) {
124
+ let prefixLen = 0;
125
+ let tmp = maskInt;
126
+ while (tmp) {
127
+ prefixLen += tmp & 1;
128
+ tmp >>>= 1;
129
+ }
130
+ if (prefixLen >= 8 && prefixLen <= 30) {
131
+ const ipInt = ipToInt(ipStr);
132
+ const netMask = (0xffffffff << (32 - prefixLen)) >>> 0;
133
+ const netInt = (ipInt & netMask) >>> 0;
134
+ this.knownSubnets.push({ networkInt: netInt, prefixLen });
135
+ }
136
+ }
137
+ }
138
+ catch {
139
+ // skip invalid
140
+ }
141
+ }
142
+ }
143
+ /** Find the best matching prefix length for an IP from learned subnets. */
144
+ findPrefixLen(ipInt) {
145
+ // Try most specific (longest prefix) first
146
+ const sorted = [...this.knownSubnets].sort((a, b) => b.prefixLen - a.prefixLen);
147
+ for (const { networkInt, prefixLen } of sorted) {
148
+ const mask = prefixLen === 0 ? 0 : ((0xffffffff << (32 - prefixLen)) >>> 0);
149
+ const net = (ipInt & mask) >>> 0;
150
+ if (net === networkInt) {
151
+ return prefixLen;
152
+ }
153
+ }
154
+ return 24; // Default
155
+ }
156
+ /** Map a real network address to a fake CGNAT network address. */
157
+ mapSubnet(netInt, prefixLen) {
158
+ const key = `${netInt},${prefixLen}`;
159
+ const existing = this.subnetFwd.get(key);
160
+ if (existing !== undefined)
161
+ return existing;
162
+ // Allocate a slot in CGNAT space, aligned to the subnet size
163
+ const hostBits = 32 - prefixLen;
164
+ const subnetSize = 1 << hostBits;
165
+ // Place fake networks sequentially in CGNAT space
166
+ let fakeNet = (CGNAT_BASE + this.subnetNextSlot * subnetSize) >>> 0;
167
+ this.subnetNextSlot += 1;
168
+ // Wrap around if we exceed CGNAT space
169
+ if ((fakeNet + subnetSize) >>> 0 > (CGNAT_BASE + CGNAT_SIZE) >>> 0) {
170
+ this.subnetNextSlot = 0;
171
+ fakeNet = CGNAT_BASE;
172
+ }
173
+ this.subnetFwd.set(key, fakeNet);
174
+ this.subnetRev.set(fakeNet, key);
175
+ return fakeNet;
176
+ }
177
+ /** Reset all subnet mapping state. */
178
+ reset() {
179
+ this.subnetFwd.clear();
180
+ this.subnetRev.clear();
181
+ this.subnetNextSlot = 0;
182
+ this.knownSubnets = [];
183
+ }
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // NetworkGenerator
187
+ // ---------------------------------------------------------------------------
188
+ export const VLAN_NAMES = [
189
+ "MGMT", "USERS", "SERVERS", "PRINTERS", "VOIP", "GUEST",
190
+ "DMZ", "BACKUP", "IOT", "SECURITY", "WIRELESS", "STORAGE",
191
+ ];
192
+ export const INTERFACE_DESCS = [
193
+ "Uplink to Core", "Server Farm Link", "WAN Circuit", "Management VLAN",
194
+ "User Access Port", "Trunk to Distribution", "Backup Link", "DMZ Segment",
195
+ "VoIP VLAN", "Guest Network", "Storage Network", "Monitoring Port",
196
+ ];
197
+ export const ROUTE_MAP_NAMES = [
198
+ "RM-PEER-IN", "RM-PEER-OUT", "RM-TRANSIT", "RM-LOCAL",
199
+ "RM-DEFAULT", "RM-EXPORT", "RM-IMPORT", "RM-BACKUP",
200
+ "RM-PRIMARY", "RM-SECONDARY", "RM-FILTER", "RM-REDISTRIBUTE",
201
+ ];
202
+ export const ACL_NAMES = [
203
+ "ACL-MGMT", "ACL-USERS", "ACL-VPN", "ACL-OUTSIDE",
204
+ "ACL-INSIDE", "ACL-DMZ", "ACL-SERVERS", "ACL-MONITOR",
205
+ "ACL-DENY-ALL", "ACL-PERMIT-RFC1918", "ACL-EDGE", "ACL-CORE",
206
+ ];
207
+ export class NetworkGenerator {
208
+ categories = [
209
+ Category.IP_ADDRESS,
210
+ Category.EMAIL,
211
+ Category.URL,
212
+ Category.MAC_ADDRESS,
213
+ Category.BGP_ASN,
214
+ Category.SNMP_COMMUNITY,
215
+ Category.NETWORK_CREDENTIAL,
216
+ Category.HOSTNAME,
217
+ Category.VLAN_ID,
218
+ Category.INTERFACE_DESC,
219
+ Category.ROUTE_MAP,
220
+ Category.OSPF_ID,
221
+ Category.ACL_NAME,
222
+ ];
223
+ subnetMapper;
224
+ constructor(subnetMapper) {
225
+ this.subnetMapper = subnetMapper;
226
+ }
227
+ generate(category, seed, original = "") {
228
+ if (category === Category.IP_ADDRESS) {
229
+ return this._fakeIp(seed, original, this.subnetMapper);
230
+ }
231
+ else if (category === Category.EMAIL) {
232
+ return this._fakeEmail(seed, original);
233
+ }
234
+ else if (category === Category.URL) {
235
+ return this._fakeUrl(seed, original);
236
+ }
237
+ else if (category === Category.MAC_ADDRESS) {
238
+ return this._fakeMac(seed, original);
239
+ }
240
+ else if (category === Category.BGP_ASN) {
241
+ return this._fakeAsn(seed);
242
+ }
243
+ else if (category === Category.SNMP_COMMUNITY) {
244
+ return this._fakeSnmpCommunity(seed);
245
+ }
246
+ else if (category === Category.NETWORK_CREDENTIAL) {
247
+ return this._fakeNetworkCredential(seed, original);
248
+ }
249
+ else if (category === Category.HOSTNAME) {
250
+ return this._fakeHostname(seed);
251
+ }
252
+ else if (category === Category.VLAN_ID) {
253
+ return this._fakeVlanId(seed, original);
254
+ }
255
+ else if (category === Category.INTERFACE_DESC) {
256
+ return this._fakeInterfaceDesc(seed, original);
257
+ }
258
+ else if (category === Category.ROUTE_MAP) {
259
+ return this._fakeRouteMap(seed, original);
260
+ }
261
+ else if (category === Category.OSPF_ID) {
262
+ return this._fakeOspfId(seed, original);
263
+ }
264
+ else if (category === Category.ACL_NAME) {
265
+ return this._fakeAclName(seed, original);
266
+ }
267
+ return `net-${String(seed % 10000).padStart(4, "0")}`;
268
+ }
269
+ /**
270
+ * Subnet-preserving IP obfuscation for any prefix length.
271
+ *
272
+ * Network bits are mapped to the CGNAT range (100.64.0.0/10),
273
+ * host bits are preserved exactly. Defaults to /24 when no
274
+ * subnet context is available.
275
+ */
276
+ _fakeIp(seed, original, subnetMapper) {
277
+ // IPv6: use fd00::/8 (unique local)
278
+ if (original && original.includes(":")) {
279
+ const buf = Buffer.alloc(8);
280
+ buf.writeUInt32BE((seed >>> 0), 0);
281
+ buf.writeUInt32BE(((seed >>> 16) ^ 0xa5a5a5a5) >>> 0, 4);
282
+ const h = createHash("sha256").update(buf).digest("hex");
283
+ const groups = [];
284
+ for (let i = 0; i < 32; i += 4) {
285
+ groups.push(h.slice(i, i + 4));
286
+ }
287
+ groups[0] = "fd00";
288
+ return groups.join(":");
289
+ }
290
+ // IPv4: bit-level subnet-preserving mapping
291
+ if (original) {
292
+ try {
293
+ const ipInt = ipToInt(original);
294
+ const prefixLen = subnetMapper.findPrefixLen(ipInt);
295
+ const mask = prefixLen === 0 ? 0 : ((0xffffffff << (32 - prefixLen)) >>> 0);
296
+ const netInt = (ipInt & mask) >>> 0;
297
+ const hostInt = (ipInt & (~mask >>> 0)) >>> 0;
298
+ const fakeNet = subnetMapper.mapSubnet(netInt, prefixLen);
299
+ return intToIp((fakeNet | hostInt) >>> 0);
300
+ }
301
+ catch {
302
+ // fall through
303
+ }
304
+ }
305
+ // Fallback for non-standard input
306
+ return intToIp((CGNAT_BASE + (seed & 0x3fffff)) >>> 0);
307
+ }
308
+ _fakeEmail(seed, original) {
309
+ // Analyze original structure
310
+ let origLocal = "";
311
+ let origTld = "";
312
+ if (original && original.includes("@")) {
313
+ const atIdx = original.lastIndexOf("@");
314
+ origLocal = original.slice(0, atIdx);
315
+ const origDomainFull = original.slice(atIdx + 1);
316
+ if (origDomainFull.includes(".")) {
317
+ const dotIdx = origDomainFull.lastIndexOf(".");
318
+ origTld = origDomainFull.slice(dotIdx + 1);
319
+ }
320
+ }
321
+ // Match local part length
322
+ let pool;
323
+ if (origLocal && origLocal.length <= 4) {
324
+ pool = EMAIL_PREFIXES_SHORT.length > 0 ? EMAIL_PREFIXES_SHORT : EMAIL_PREFIXES;
325
+ }
326
+ else if (origLocal && origLocal.length > 7) {
327
+ pool = EMAIL_PREFIXES_LONG.length > 0 ? EMAIL_PREFIXES_LONG : EMAIL_PREFIXES;
328
+ }
329
+ else {
330
+ pool = EMAIL_PREFIXES_MEDIUM.length > 0 ? EMAIL_PREFIXES_MEDIUM : EMAIL_PREFIXES;
331
+ }
332
+ let prefix = pool[seed % pool.length];
333
+ // Try to match TLD
334
+ let domainPool;
335
+ if (origTld && DOMAINS_BY_TLD[origTld]) {
336
+ domainPool = DOMAINS_BY_TLD[origTld];
337
+ }
338
+ else {
339
+ domainPool = DOMAINS;
340
+ }
341
+ const domain = domainPool[Math.floor(seed / pool.length) % domainPool.length];
342
+ // Preserve dots in local part (e.g., "john.doe" -> "dev.ops")
343
+ if (origLocal && origLocal.includes(".")) {
344
+ const extra = EMAIL_PREFIXES[Math.floor(seed / 7) % EMAIL_PREFIXES.length];
345
+ prefix = `${prefix}.${extra}`;
346
+ }
347
+ const num = Math.floor(seed / (pool.length * domainPool.length)) % 100;
348
+ if (num > 0) {
349
+ return `${prefix}${num}@${domain}`;
350
+ }
351
+ return `${prefix}@${domain}`;
352
+ }
353
+ _fakeUrl(seed, original) {
354
+ const domain = DOMAINS[seed % DOMAINS.length];
355
+ // Preserve URL path depth
356
+ if (original) {
357
+ const pathMatch = original.match(/https?:\/\/[^/]+(.*)/);
358
+ if (pathMatch) {
359
+ const origPath = pathMatch[1];
360
+ const segments = origPath.split("/").filter((s) => s);
361
+ const fakeSegments = [];
362
+ for (let i = 0; i < segments.length; i++) {
363
+ fakeSegments.push(PATH_SEGMENTS[(seed + i) % PATH_SEGMENTS.length]);
364
+ }
365
+ if (fakeSegments.length > 0) {
366
+ return `https://${domain}/${fakeSegments.join("/")}`;
367
+ }
368
+ }
369
+ }
370
+ const path = PATH_SEGMENTS[Math.floor(seed / DOMAINS.length) % PATH_SEGMENTS.length];
371
+ return `https://${domain}/${path}`;
372
+ }
373
+ /** Generate a fake MAC address preserving format (colon, dash, or Cisco dot). */
374
+ _fakeMac(seed, original) {
375
+ const buf = Buffer.alloc(8);
376
+ buf.writeUInt32BE(seed >>> 0, 0);
377
+ buf.writeUInt32BE(((seed >>> 16) ^ 0x5a5a5a5a) >>> 0, 4);
378
+ const h = createHash("sha256").update(buf).digest("hex");
379
+ // Use locally-administered bit (second nibble of first octet is even+2)
380
+ const octets = [];
381
+ for (let i = 0; i < 12; i += 2) {
382
+ octets.push(((i === 0
383
+ ? (parseInt(h.slice(i, i + 2), 16) | 0x02) & 0xfe
384
+ : parseInt(h.slice(i, i + 2), 16)) & 0xff)
385
+ .toString(16)
386
+ .padStart(2, "0"));
387
+ }
388
+ if (original && original.includes(".")) {
389
+ // Cisco format: aabb.ccdd.eeff
390
+ const flat = octets.join("");
391
+ return `${flat.slice(0, 4)}.${flat.slice(4, 8)}.${flat.slice(8, 12)}`;
392
+ }
393
+ else if (original && original.includes("-")) {
394
+ return octets.join("-");
395
+ }
396
+ return octets.join(":");
397
+ }
398
+ /** Map BGP AS numbers to the private AS range (64512-65534). */
399
+ _fakeAsn(seed) {
400
+ const base = 64512;
401
+ return String(base + (seed % 1023));
402
+ }
403
+ /** Replace SNMP community strings with generic names. */
404
+ _fakeSnmpCommunity(seed) {
405
+ return SNMP_COMMUNITIES[seed % SNMP_COMMUNITIES.length];
406
+ }
407
+ /** Replace network credentials with seed-derived unique fake values. */
408
+ _fakeNetworkCredential(seed, original) {
409
+ const buf = Buffer.alloc(8);
410
+ buf.writeUInt32BE(seed >>> 0, 0);
411
+ buf.writeUInt32BE(((seed >>> 16) ^ 0xdeadbeef) >>> 0, 4);
412
+ const h = createHash("sha256").update(buf).digest("hex");
413
+ // Preserve hash type prefix for structure hints
414
+ const hashPrefixMatch = original.match(/^(\$\d\$)/);
415
+ if (hashPrefixMatch) {
416
+ // e.g. $1$salt$hash → $1$fakesalt$fakehash
417
+ return `${hashPrefixMatch[1]}${h.slice(0, 8)}$${h.slice(8, 30)}`;
418
+ }
419
+ // Cisco type 7 hex strings
420
+ if (/^[0-9A-Fa-f]{4,}$/.test(original)) {
421
+ return h.slice(0, original.length).toUpperCase();
422
+ }
423
+ // Generic credential
424
+ return `REDACTED_${h.slice(0, 16)}`;
425
+ }
426
+ /** Generate a fake hostname preserving structure. */
427
+ _fakeHostname(seed) {
428
+ const role = HOSTNAME_ROLES[seed % HOSTNAME_ROLES.length];
429
+ const site = HOSTNAME_SITES[Math.floor(seed / HOSTNAME_ROLES.length) % HOSTNAME_SITES.length];
430
+ const num = (seed % 99) + 1;
431
+ return `${site}-${role}-${String(num).padStart(2, "0")}`;
432
+ }
433
+ /** Fake VLAN ID/name. Preserves the keyword structure. */
434
+ _fakeVlanId(seed, original) {
435
+ // If the original is a "vlan <id>" or just a number in a vlan context,
436
+ // the detector captures the full match. Preserve surrounding keywords.
437
+ const nameMatch = original.match(/name\s+(.+)/i);
438
+ if (nameMatch) {
439
+ const fakeName = VLAN_NAMES[seed % VLAN_NAMES.length];
440
+ return `name ${fakeName}`;
441
+ }
442
+ // VLAN range like "100-200" or "100,200,300"
443
+ if (original.includes("-") || original.includes(",")) {
444
+ const fakeBase = 100 + (seed % 900);
445
+ if (original.includes("-")) {
446
+ return `${fakeBase}-${fakeBase + 99}`;
447
+ }
448
+ const parts = original.split(",");
449
+ return parts.map((_, i) => fakeBase + i * 10).join(",");
450
+ }
451
+ // Single VLAN number
452
+ return String(100 + (seed % 3900));
453
+ }
454
+ /** Fake interface description. */
455
+ _fakeInterfaceDesc(seed, _original) {
456
+ return INTERFACE_DESCS[seed % INTERFACE_DESCS.length];
457
+ }
458
+ /** Fake route-map or prefix-list name. */
459
+ _fakeRouteMap(seed, _original) {
460
+ return ROUTE_MAP_NAMES[seed % ROUTE_MAP_NAMES.length];
461
+ }
462
+ /** Fake OSPF identifiers (router-id or area). */
463
+ _fakeOspfId(seed, original) {
464
+ // OSPF area can be a number or dotted-quad
465
+ const areaNumMatch = original.match(/area\s+(\d+)/i);
466
+ if (areaNumMatch) {
467
+ return `area ${seed % 100}`;
468
+ }
469
+ // Router-id is typically an IP-like dotted quad
470
+ const fakeId = `10.${(seed % 256)}.${(Math.floor(seed / 256) % 256)}.${(Math.floor(seed / 65536) % 256)}`;
471
+ return fakeId;
472
+ }
473
+ /** Fake ACL name. */
474
+ _fakeAclName(seed, _original) {
475
+ return ACL_NAMES[seed % ACL_NAMES.length];
476
+ }
477
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * OpenClaw lifecycle hooks for the Shroud privacy plugin.
3
+ *
4
+ * Registers 5 hooks:
5
+ * 1. before_prompt_build (async) -- obfuscate user prompt via prependContext
6
+ * 2. before_llm_send (async) -- obfuscate LLM input messages + return transformResponse for deobfuscation
7
+ * 3. before_tool_call (async) -- deobfuscate tool params (+ depth tracking)
8
+ * 4. tool_result_persist (SYNC) -- obfuscate tool result message
9
+ * 5. message_sending (async) -- deobfuscate outbound message content (fallback)
10
+ */
11
+ import { Obfuscator } from "./obfuscator.js";
12
+ export interface PluginApi {
13
+ on(event: string, handler: (...args: any[]) => any): void;
14
+ registerTool(tool: {
15
+ name: string;
16
+ description: string;
17
+ inputSchema: object;
18
+ handler: (input: any) => Promise<any>;
19
+ }): void;
20
+ pluginConfig?: unknown;
21
+ logger?: {
22
+ info(...args: any[]): void;
23
+ warn(...args: any[]): void;
24
+ error(...args: any[]): void;
25
+ };
26
+ }
27
+ export declare function registerHooks(api: PluginApi, obfuscator: Obfuscator): void;