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.
- package/LICENSE +190 -0
- package/NOTICE +7 -0
- package/README.md +369 -0
- package/dist/audit.d.ts +46 -0
- package/dist/audit.js +127 -0
- package/dist/canary.d.ts +31 -0
- package/dist/canary.js +73 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +123 -0
- package/dist/detectors/base.d.ts +8 -0
- package/dist/detectors/base.js +2 -0
- package/dist/detectors/code.d.ts +25 -0
- package/dist/detectors/code.js +144 -0
- package/dist/detectors/context.d.ts +31 -0
- package/dist/detectors/context.js +357 -0
- package/dist/detectors/patterns.d.ts +15 -0
- package/dist/detectors/patterns.js +58 -0
- package/dist/detectors/regex.d.ts +28 -0
- package/dist/detectors/regex.js +955 -0
- package/dist/generators/base.d.ts +6 -0
- package/dist/generators/base.js +2 -0
- package/dist/generators/codes.d.ts +20 -0
- package/dist/generators/codes.js +231 -0
- package/dist/generators/names.d.ts +29 -0
- package/dist/generators/names.js +194 -0
- package/dist/generators/network.d.ts +86 -0
- package/dist/generators/network.js +477 -0
- package/dist/hooks.d.ts +27 -0
- package/dist/hooks.js +457 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +58 -0
- package/dist/mapping.d.ts +33 -0
- package/dist/mapping.js +72 -0
- package/dist/obfuscator.d.ts +78 -0
- package/dist/obfuscator.js +603 -0
- package/dist/redaction.d.ts +26 -0
- package/dist/redaction.js +76 -0
- package/dist/store.d.ts +40 -0
- package/dist/store.js +79 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +35 -0
- package/ncg_adapter.py +530 -0
- package/openclaw.plugin.json +72 -0
- package/package.json +56 -0
- 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
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -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;
|