@liuhange/dsh-data-masking 2.0.1 → 2.0.2
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/lib/index.js
CHANGED
|
@@ -1,491 +1,115 @@
|
|
|
1
|
-
import { defineTool } from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as path from
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
encryptGeneric(plaintext, key) {
|
|
115
|
-
const keyHash = crypto.createHash("sha256").update(key).digest();
|
|
116
|
-
const result = [];
|
|
117
|
-
for (let i = 0; i < plaintext.length; i++) {
|
|
118
|
-
const charCode = plaintext.charCodeAt(i);
|
|
119
|
-
const offset = crypto.createHmac("sha256", keyHash).update(Buffer.from([i & 255])).digest()[0] % 256;
|
|
120
|
-
result.push(String.fromCharCode((charCode + offset) % 256));
|
|
121
|
-
}
|
|
122
|
-
return result.join("");
|
|
123
|
-
}
|
|
124
|
-
decryptGeneric(ciphertext, key) {
|
|
125
|
-
const keyHash = crypto.createHash("sha256").update(key).digest();
|
|
126
|
-
const result = [];
|
|
127
|
-
for (let i = 0; i < ciphertext.length; i++) {
|
|
128
|
-
const charCode = ciphertext.charCodeAt(i);
|
|
129
|
-
const offset = crypto.createHmac("sha256", keyHash).update(Buffer.from([i & 255])).digest()[0] % 256;
|
|
130
|
-
result.push(String.fromCharCode((charCode - offset + 256) % 256));
|
|
131
|
-
}
|
|
132
|
-
return result.join("");
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region lib/types/algorithms/kAnonymityAlgorithm.js
|
|
137
|
-
var KAnonymityAlgorithm = class {
|
|
138
|
-
anonymize(records, kValue, quasiIdentifiers) {
|
|
139
|
-
if (records.length === 0 || quasiIdentifiers.length === 0) return {
|
|
140
|
-
anonymizedRecords: records,
|
|
141
|
-
actualMinEquivalenceClass: 0,
|
|
142
|
-
suppressedCount: 0
|
|
143
|
-
};
|
|
144
|
-
let workingRecords = [...records];
|
|
145
|
-
let generalized = true;
|
|
146
|
-
let generalizationLevel = 0;
|
|
147
|
-
while (generalized) {
|
|
148
|
-
generalized = false;
|
|
149
|
-
const groups = this.groupByQuasiIdentifiers(workingRecords, quasiIdentifiers);
|
|
150
|
-
if (Math.min(...groups.map((g) => g.length)) >= kValue) {
|
|
151
|
-
const minClass = Math.min(...groups.map((g) => g.length));
|
|
152
|
-
return {
|
|
153
|
-
anonymizedRecords: workingRecords,
|
|
154
|
-
actualMinEquivalenceClass: minClass,
|
|
155
|
-
suppressedCount: 0
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
if (generalizationLevel < 3) {
|
|
159
|
-
workingRecords = this.generalize(workingRecords, quasiIdentifiers, generalizationLevel);
|
|
160
|
-
generalizationLevel++;
|
|
161
|
-
generalized = true;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
const groups = this.groupByQuasiIdentifiers(workingRecords, quasiIdentifiers);
|
|
165
|
-
const validGroups = groups.filter((g) => g.length >= kValue);
|
|
166
|
-
const suppressedRecords = groups.filter((g) => g.length < kValue).flat();
|
|
167
|
-
const anonymizedRecords = validGroups.flat();
|
|
168
|
-
const allGroups = [...validGroups, ...groups.filter((g) => g.length < kValue)];
|
|
169
|
-
return {
|
|
170
|
-
anonymizedRecords,
|
|
171
|
-
actualMinEquivalenceClass: allGroups.length > 0 ? Math.min(...allGroups.map((g) => g.length)) : 0,
|
|
172
|
-
suppressedCount: suppressedRecords.length
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
groupByQuasiIdentifiers(records, quasiIdentifiers) {
|
|
176
|
-
const groups = /* @__PURE__ */ new Map();
|
|
177
|
-
for (const record of records) {
|
|
178
|
-
const key = quasiIdentifiers.map((qi) => String(record[qi] ?? "")).join("|");
|
|
179
|
-
const group = groups.get(key);
|
|
180
|
-
if (group) group.push(record);
|
|
181
|
-
else groups.set(key, [record]);
|
|
182
|
-
}
|
|
183
|
-
return Array.from(groups.values());
|
|
184
|
-
}
|
|
185
|
-
generalize(records, quasiIdentifiers, level) {
|
|
186
|
-
return records.map((record) => {
|
|
187
|
-
const generalized = { ...record };
|
|
188
|
-
for (const qi of quasiIdentifiers) {
|
|
189
|
-
const value = String(generalized[qi] ?? "");
|
|
190
|
-
if (value.length > level + 1) generalized[qi] = value.substring(0, value.length - level - 1) + "*".repeat(level + 1);
|
|
191
|
-
else generalized[qi] = "*".repeat(value.length);
|
|
192
|
-
}
|
|
193
|
-
return generalized;
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
//#endregion
|
|
198
|
-
//#region lib/types/algorithms/differentialPrivacyAlgorithm.js
|
|
199
|
-
var DifferentialPrivacyAlgorithm = class {
|
|
200
|
-
addNoise(trueValue, epsilon, sensitivity, budgetTracker, totalBudget) {
|
|
201
|
-
if (!budgetTracker.canConsume(epsilon, totalBudget)) throw new Error("隐私预算已耗尽,拒绝查询");
|
|
202
|
-
const scale = sensitivity / epsilon;
|
|
203
|
-
const noise = this.generateLaplaceNoise(scale);
|
|
204
|
-
budgetTracker.consume(epsilon);
|
|
205
|
-
return trueValue + noise;
|
|
206
|
-
}
|
|
207
|
-
generateLaplaceNoise(scale) {
|
|
208
|
-
const adjustedU = this.generateSecureUniform() - .5;
|
|
209
|
-
return -scale * Math.sign(adjustedU) * Math.log(1 - 2 * Math.abs(adjustedU));
|
|
210
|
-
}
|
|
211
|
-
generateSecureUniform() {
|
|
212
|
-
const uint64 = crypto.randomBytes(8).readBigUInt64BE(0);
|
|
213
|
-
const maxUint64 = BigInt(2) ** BigInt(64) - BigInt(1);
|
|
214
|
-
return Number(uint64) / Number(maxUint64);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
//#endregion
|
|
218
|
-
//#region lib/types/algorithms/hashAlgorithm.js
|
|
219
|
-
var HashAlgorithm = class {
|
|
220
|
-
hash(plaintext, hashAlgorithm, salt) {
|
|
221
|
-
const input = salt ? salt + plaintext : plaintext;
|
|
222
|
-
const algorithm = hashAlgorithm === "SHA-512" ? "SHA-512" : "SHA-256";
|
|
223
|
-
const hash = crypto.createHash(algorithm);
|
|
224
|
-
hash.update(input, "utf-8");
|
|
225
|
-
const digest = hash.digest("hex");
|
|
226
|
-
return {
|
|
227
|
-
digest,
|
|
228
|
-
salted: salt !== void 0 && salt !== "",
|
|
229
|
-
digestLength: digest.length
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
//#endregion
|
|
234
|
-
//#region lib/types/budgetTracker.js
|
|
235
|
-
var BudgetTracker = class {
|
|
236
|
-
consumed = 0;
|
|
237
|
-
queryCount = 0;
|
|
238
|
-
totalBudget;
|
|
239
|
-
constructor(totalBudget) {
|
|
240
|
-
this.totalBudget = totalBudget;
|
|
241
|
-
}
|
|
242
|
-
consume(epsilon) {
|
|
243
|
-
this.consumed += epsilon;
|
|
244
|
-
this.queryCount++;
|
|
245
|
-
}
|
|
246
|
-
canConsume(epsilon, totalBudget) {
|
|
247
|
-
return this.consumed + epsilon <= totalBudget;
|
|
248
|
-
}
|
|
249
|
-
getState() {
|
|
250
|
-
return {
|
|
251
|
-
totalBudget: this.totalBudget,
|
|
252
|
-
consumed: this.consumed,
|
|
253
|
-
remaining: this.totalBudget - this.consumed,
|
|
254
|
-
queryCount: this.queryCount
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
//#endregion
|
|
259
|
-
//#region lib/types/advancedMaskingExecutor.js
|
|
260
|
-
var AdvancedMaskingExecutor = class {
|
|
261
|
-
fpeAlgorithm = new FpeAlgorithm();
|
|
262
|
-
kAnonymityAlgorithm = new KAnonymityAlgorithm();
|
|
263
|
-
differentialPrivacyAlgorithm = new DifferentialPrivacyAlgorithm();
|
|
264
|
-
hashAlgorithm = new HashAlgorithm();
|
|
265
|
-
execute(data, algorithm, fieldName, config) {
|
|
266
|
-
switch ((fieldName ? config.fieldAlgorithms[fieldName] : void 0)?.algorithm ?? algorithm) {
|
|
267
|
-
case "FPE": return this.executeFpe(data, config);
|
|
268
|
-
case "k-anonymity": return this.executeKAnonymity(data, config);
|
|
269
|
-
case "differential-privacy": return this.executeDifferentialPrivacy(data, config);
|
|
270
|
-
case "hash": return this.executeHash(data, config);
|
|
271
|
-
default: return this.executeHash(data, config);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
executeFpe(data, config) {
|
|
275
|
-
const key = this.resolveEnvVar(config.fpe.key);
|
|
276
|
-
return {
|
|
277
|
-
maskedData: this.fpeAlgorithm.encrypt(data, key),
|
|
278
|
-
algorithm: "FPE",
|
|
279
|
-
details: {
|
|
280
|
-
algorithm: "FPE",
|
|
281
|
-
reversible: true
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
executeKAnonymity(data, config) {
|
|
286
|
-
const kValue = config.kAnonymity.kValue;
|
|
287
|
-
const quasiIdentifiers = config.kAnonymity.quasiIdentifiers;
|
|
288
|
-
let records;
|
|
289
|
-
try {
|
|
290
|
-
records = JSON.parse(data);
|
|
291
|
-
} catch {
|
|
292
|
-
records = [{ value: data }];
|
|
293
|
-
}
|
|
294
|
-
const result = this.kAnonymityAlgorithm.anonymize(records, kValue, quasiIdentifiers);
|
|
295
|
-
const details = {
|
|
296
|
-
algorithm: "k-anonymity",
|
|
297
|
-
kValue,
|
|
298
|
-
actualMinEquivalenceClass: result.actualMinEquivalenceClass,
|
|
299
|
-
suppressedCount: result.suppressedCount
|
|
300
|
-
};
|
|
301
|
-
return {
|
|
302
|
-
maskedData: JSON.stringify(result.anonymizedRecords),
|
|
303
|
-
algorithm: "k-anonymity",
|
|
304
|
-
details
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
executeDifferentialPrivacy(data, config) {
|
|
308
|
-
const epsilon = config.differentialPrivacy.epsilon;
|
|
309
|
-
const totalBudget = config.differentialPrivacy.totalBudget;
|
|
310
|
-
const sensitivity = config.differentialPrivacy.sensitivity;
|
|
311
|
-
const budgetTracker = new BudgetTracker(totalBudget);
|
|
312
|
-
const trueValue = Number(data);
|
|
313
|
-
if (isNaN(trueValue)) throw new Error("差分隐私要求输入为数字");
|
|
314
|
-
const noisyValue = this.differentialPrivacyAlgorithm.addNoise(trueValue, epsilon, sensitivity, budgetTracker, totalBudget);
|
|
315
|
-
const budgetState = budgetTracker.getState();
|
|
316
|
-
const details = {
|
|
317
|
-
algorithm: "differential-privacy",
|
|
318
|
-
epsilon,
|
|
319
|
-
budgetConsumed: budgetState.consumed,
|
|
320
|
-
budgetRemaining: budgetState.remaining
|
|
321
|
-
};
|
|
322
|
-
return {
|
|
323
|
-
maskedData: String(noisyValue),
|
|
324
|
-
algorithm: "differential-privacy",
|
|
325
|
-
details
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
executeHash(data, config) {
|
|
329
|
-
const hashAlgo = config.hash.algorithm;
|
|
330
|
-
const salt = this.resolveEnvVar(config.hash.salt);
|
|
331
|
-
const result = this.hashAlgorithm.hash(data, hashAlgo, salt || void 0);
|
|
332
|
-
const details = {
|
|
333
|
-
algorithm: "hash",
|
|
334
|
-
hashAlgorithm: hashAlgo,
|
|
335
|
-
salted: result.salted,
|
|
336
|
-
digestLength: result.digestLength
|
|
337
|
-
};
|
|
338
|
-
return {
|
|
339
|
-
maskedData: result.digest,
|
|
340
|
-
algorithm: "hash",
|
|
341
|
-
details
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
resolveEnvVar(value) {
|
|
345
|
-
if (value.startsWith("${") && value.endsWith("}")) {
|
|
346
|
-
const envVar = value.slice(2, -1);
|
|
347
|
-
return process.env[envVar] ?? "";
|
|
348
|
-
}
|
|
349
|
-
return value;
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
//#endregion
|
|
353
|
-
//#region lib/types/defaultAdvancedMaskingConfig.js
|
|
354
|
-
const defaultAdvancedMaskingConfig = {
|
|
355
|
-
defaultAlgorithm: "hash",
|
|
356
|
-
fieldAlgorithms: {},
|
|
357
|
-
fpe: {
|
|
358
|
-
key: "",
|
|
359
|
-
radix: 10
|
|
360
|
-
},
|
|
361
|
-
kAnonymity: {
|
|
362
|
-
kValue: 2,
|
|
363
|
-
quasiIdentifiers: [
|
|
364
|
-
"zipCode",
|
|
365
|
-
"age",
|
|
366
|
-
"gender"
|
|
367
|
-
]
|
|
368
|
-
},
|
|
369
|
-
differentialPrivacy: {
|
|
370
|
-
epsilon: 1,
|
|
371
|
-
totalBudget: 5,
|
|
372
|
-
sensitivity: 1
|
|
373
|
-
},
|
|
374
|
-
hash: {
|
|
375
|
-
algorithm: "SHA-256",
|
|
376
|
-
salt: ""
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
//#endregion
|
|
380
|
-
//#region lib/types/index.js
|
|
381
|
-
const name = "data-masking";
|
|
382
|
-
const inject = ["tools"];
|
|
383
|
-
function apply(ctx) {
|
|
384
|
-
const loader = new BusinessRulesLoader();
|
|
385
|
-
const formatAdapter = new FileFormatAdapter();
|
|
386
|
-
const reportGenerator = new ReportGenerator();
|
|
387
|
-
const pathValidator = new PathValidator();
|
|
388
|
-
const auditLogger = new AuditLogger();
|
|
389
|
-
const scanner = new SensitiveFieldScanner();
|
|
390
|
-
const strategyExecutor = new MaskingStrategyExecutor();
|
|
391
|
-
const advancedExecutor = new AdvancedMaskingExecutor();
|
|
392
|
-
ctx.tools.register(defineTool({
|
|
393
|
-
name: "mask_sensitive_data",
|
|
394
|
-
description: "识别并脱敏数据中的敏感字段(身份证、手机号、银行卡、邮箱),支持高级算法(FPE/k-匿名/差分隐私/哈希)",
|
|
395
|
-
parameters: {
|
|
396
|
-
filePath: {
|
|
397
|
-
type: "string",
|
|
398
|
-
required: true,
|
|
399
|
-
description: "待处理文件的路径"
|
|
400
|
-
},
|
|
401
|
-
strategy: {
|
|
402
|
-
type: "string",
|
|
403
|
-
description: "脱敏策略: FULL | PARTIAL | GENERALIZE"
|
|
404
|
-
},
|
|
405
|
-
algorithm: {
|
|
406
|
-
type: "string",
|
|
407
|
-
description: "高级脱敏算法: FPE | k-anonymity | differential-privacy | hash"
|
|
408
|
-
},
|
|
409
|
-
fieldName: {
|
|
410
|
-
type: "string",
|
|
411
|
-
description: "字段名(用于字段级算法配置覆盖)"
|
|
412
|
-
}
|
|
413
|
-
},
|
|
414
|
-
output: {
|
|
415
|
-
schema: { type: "string" },
|
|
416
|
-
render: (_args, value) => [{
|
|
417
|
-
type: "text",
|
|
418
|
-
text: value
|
|
419
|
-
}]
|
|
420
|
-
},
|
|
421
|
-
async execute(args) {
|
|
422
|
-
const { config } = loader.load();
|
|
423
|
-
const workingDir = process.cwd();
|
|
424
|
-
const filePath = args.filePath;
|
|
425
|
-
const strategy = args.strategy ?? "PARTIAL";
|
|
426
|
-
const algorithm = args.algorithm;
|
|
427
|
-
const fieldName = args.fieldName;
|
|
428
|
-
try {
|
|
429
|
-
pathValidator.validate(filePath, workingDir);
|
|
430
|
-
} catch {
|
|
431
|
-
return `错误:路径不合法 - ${filePath}`;
|
|
432
|
-
}
|
|
433
|
-
const fullPath = path.resolve(filePath);
|
|
434
|
-
if (!fs.existsSync(fullPath)) return `错误:文件不存在 - ${fullPath}`;
|
|
435
|
-
if (algorithm) {
|
|
436
|
-
const advancedConfig = config.advancedMasking ?? defaultAdvancedMaskingConfig;
|
|
437
|
-
const readResult = await formatAdapter.read(fullPath);
|
|
438
|
-
const lines = readResult.lines;
|
|
439
|
-
const maskedLines = [];
|
|
440
|
-
for (const line of lines) {
|
|
441
|
-
const result = advancedExecutor.execute(line, algorithm, fieldName, advancedConfig);
|
|
442
|
-
maskedLines.push(typeof result.maskedData === "string" ? result.maskedData : JSON.stringify(result.maskedData));
|
|
443
|
-
}
|
|
444
|
-
const ext = path.extname(fullPath);
|
|
445
|
-
const baseName = path.basename(fullPath, ext);
|
|
446
|
-
const outputPath = path.join(path.dirname(fullPath), `${baseName}_masked${ext}`);
|
|
447
|
-
await formatAdapter.write(outputPath, maskedLines, readResult.format);
|
|
448
|
-
auditLogger.log({
|
|
449
|
-
pluginName: "data-masking",
|
|
450
|
-
operation: "mask_sensitive_data",
|
|
451
|
-
inputPath: fullPath,
|
|
452
|
-
outputPath,
|
|
453
|
-
result: "SUCCESS"
|
|
454
|
-
});
|
|
455
|
-
return `高级脱敏完成(算法: ${algorithm})\n输入: ${fullPath}\n输出: ${outputPath}\n处理行数: ${maskedLines.length}`;
|
|
456
|
-
}
|
|
457
|
-
const readResult = await formatAdapter.read(fullPath);
|
|
458
|
-
const lines = readResult.lines;
|
|
459
|
-
const scanResult = scanner.scan(lines, config.sensitivePatterns);
|
|
460
|
-
let maskedLines = [...lines];
|
|
461
|
-
for (const field of scanResult.fields) {
|
|
462
|
-
const fieldType = field.type;
|
|
463
|
-
const fieldStrategy = config.sensitivePatterns[fieldType].level ?? strategy;
|
|
464
|
-
const maskedValue = strategyExecutor.execute(field.value, fieldType, fieldStrategy);
|
|
465
|
-
maskedLines = maskedLines.map((line) => line.replace(field.value, maskedValue));
|
|
466
|
-
}
|
|
467
|
-
const ext = path.extname(fullPath);
|
|
468
|
-
const baseName = path.basename(fullPath, ext);
|
|
469
|
-
const outputPath = path.join(path.dirname(fullPath), `${baseName}_masked${ext}`);
|
|
470
|
-
await formatAdapter.write(outputPath, maskedLines, readResult.format);
|
|
471
|
-
const report = reportGenerator.generateMaskingReport({
|
|
472
|
-
inputPath: fullPath,
|
|
473
|
-
outputPath,
|
|
474
|
-
strategy: strategyExecutor.resolveStrategy(strategy),
|
|
475
|
-
findings: scanResult.fields,
|
|
476
|
-
fieldTypeCounts: scanResult.typeCounts
|
|
477
|
-
});
|
|
478
|
-
auditLogger.log({
|
|
479
|
-
pluginName: "data-masking",
|
|
480
|
-
operation: "mask_sensitive_data",
|
|
481
|
-
inputPath: fullPath,
|
|
482
|
-
outputPath,
|
|
483
|
-
result: "SUCCESS"
|
|
484
|
-
});
|
|
485
|
-
return report;
|
|
486
|
-
}
|
|
487
|
-
}));
|
|
488
|
-
console.log("[data-masking] 脱敏插件已加载");
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { BusinessRulesLoader, FileFormatAdapter, ReportGenerator, PathValidator, AuditLogger, } from '@liuhange/dsh-data-asset-shared';
|
|
5
|
+
import { SensitiveFieldScanner } from './sensitiveFieldScanner.js';
|
|
6
|
+
import { MaskingStrategyExecutor } from './maskingStrategyExecutor.js';
|
|
7
|
+
import { AdvancedMaskingExecutor } from './advancedMaskingExecutor.js';
|
|
8
|
+
import { defaultAdvancedMaskingConfig } from './defaultAdvancedMaskingConfig.js';
|
|
9
|
+
export const name = 'data-masking';
|
|
10
|
+
export const inject = ['tools'];
|
|
11
|
+
export function apply(ctx) {
|
|
12
|
+
const loader = new BusinessRulesLoader();
|
|
13
|
+
const formatAdapter = new FileFormatAdapter();
|
|
14
|
+
const reportGenerator = new ReportGenerator();
|
|
15
|
+
const pathValidator = new PathValidator();
|
|
16
|
+
const auditLogger = new AuditLogger();
|
|
17
|
+
const scanner = new SensitiveFieldScanner();
|
|
18
|
+
const strategyExecutor = new MaskingStrategyExecutor();
|
|
19
|
+
const advancedExecutor = new AdvancedMaskingExecutor();
|
|
20
|
+
ctx.tools.register(defineTool({
|
|
21
|
+
name: 'mask_sensitive_data',
|
|
22
|
+
description: '识别并脱敏数据中的敏感字段(身份证、手机号、银行卡、邮箱),支持高级算法(FPE/k-匿名/差分隐私/哈希)',
|
|
23
|
+
parameters: {
|
|
24
|
+
filePath: { type: 'string', required: true, description: '待处理文件的路径' },
|
|
25
|
+
strategy: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: '脱敏策略: FULL | PARTIAL | GENERALIZE',
|
|
28
|
+
},
|
|
29
|
+
algorithm: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: '高级脱敏算法: FPE | k-anonymity | differential-privacy | hash',
|
|
32
|
+
},
|
|
33
|
+
fieldName: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: '字段名(用于字段级算法配置覆盖)',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
output: {
|
|
39
|
+
schema: { type: 'string' },
|
|
40
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
41
|
+
},
|
|
42
|
+
async execute(args) {
|
|
43
|
+
const { config } = loader.load();
|
|
44
|
+
const workingDir = process.cwd();
|
|
45
|
+
const filePath = args.filePath;
|
|
46
|
+
const strategy = args.strategy ?? 'PARTIAL';
|
|
47
|
+
const algorithm = args.algorithm;
|
|
48
|
+
const fieldName = args.fieldName;
|
|
49
|
+
try {
|
|
50
|
+
pathValidator.validate(filePath, workingDir);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return `错误:路径不合法 - ${filePath}`;
|
|
54
|
+
}
|
|
55
|
+
const fullPath = path.resolve(filePath);
|
|
56
|
+
if (!fs.existsSync(fullPath)) {
|
|
57
|
+
return `错误:文件不存在 - ${fullPath}`;
|
|
58
|
+
}
|
|
59
|
+
if (algorithm) {
|
|
60
|
+
const advancedConfig = config.advancedMasking ?? defaultAdvancedMaskingConfig;
|
|
61
|
+
const readResult = await formatAdapter.read(fullPath);
|
|
62
|
+
const lines = readResult.lines;
|
|
63
|
+
const maskedLines = [];
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const result = advancedExecutor.execute(line, algorithm, fieldName, advancedConfig);
|
|
66
|
+
maskedLines.push(typeof result.maskedData === 'string' ? result.maskedData : JSON.stringify(result.maskedData));
|
|
67
|
+
}
|
|
68
|
+
const ext = path.extname(fullPath);
|
|
69
|
+
const baseName = path.basename(fullPath, ext);
|
|
70
|
+
const outputPath = path.join(path.dirname(fullPath), `${baseName}_masked${ext}`);
|
|
71
|
+
await formatAdapter.write(outputPath, maskedLines, readResult.format);
|
|
72
|
+
auditLogger.log({
|
|
73
|
+
pluginName: 'data-masking',
|
|
74
|
+
operation: 'mask_sensitive_data',
|
|
75
|
+
inputPath: fullPath,
|
|
76
|
+
outputPath,
|
|
77
|
+
result: 'SUCCESS',
|
|
78
|
+
});
|
|
79
|
+
return `高级脱敏完成(算法: ${algorithm})\n输入: ${fullPath}\n输出: ${outputPath}\n处理行数: ${maskedLines.length}`;
|
|
80
|
+
}
|
|
81
|
+
const readResult = await formatAdapter.read(fullPath);
|
|
82
|
+
const lines = readResult.lines;
|
|
83
|
+
const scanResult = scanner.scan(lines, config.sensitivePatterns);
|
|
84
|
+
let maskedLines = [...lines];
|
|
85
|
+
for (const field of scanResult.fields) {
|
|
86
|
+
const fieldType = field.type;
|
|
87
|
+
const patternConfig = config.sensitivePatterns[fieldType];
|
|
88
|
+
const fieldStrategy = patternConfig.level ?? strategy;
|
|
89
|
+
const maskedValue = strategyExecutor.execute(field.value, fieldType, fieldStrategy);
|
|
90
|
+
maskedLines = maskedLines.map(line => line.replace(field.value, maskedValue));
|
|
91
|
+
}
|
|
92
|
+
const ext = path.extname(fullPath);
|
|
93
|
+
const baseName = path.basename(fullPath, ext);
|
|
94
|
+
const outputPath = path.join(path.dirname(fullPath), `${baseName}_masked${ext}`);
|
|
95
|
+
await formatAdapter.write(outputPath, maskedLines, readResult.format);
|
|
96
|
+
const report = reportGenerator.generateMaskingReport({
|
|
97
|
+
inputPath: fullPath,
|
|
98
|
+
outputPath,
|
|
99
|
+
strategy: strategyExecutor.resolveStrategy(strategy),
|
|
100
|
+
findings: scanResult.fields,
|
|
101
|
+
fieldTypeCounts: scanResult.typeCounts,
|
|
102
|
+
});
|
|
103
|
+
auditLogger.log({
|
|
104
|
+
pluginName: 'data-masking',
|
|
105
|
+
operation: 'mask_sensitive_data',
|
|
106
|
+
inputPath: fullPath,
|
|
107
|
+
outputPath,
|
|
108
|
+
result: 'SUCCESS',
|
|
109
|
+
});
|
|
110
|
+
return report;
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
console.log('[data-masking] 脱敏插件已加载');
|
|
489
114
|
}
|
|
490
|
-
//#
|
|
491
|
-
export { apply, inject, name };
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
package/lib/invariant.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//#
|
|
5
|
-
export { install, invariant };
|
|
1
|
+
export const invariant = 'data-masking';
|
|
2
|
+
export function install() {
|
|
3
|
+
}
|
|
4
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MaskingStrategy, SensitiveFieldType } from '@
|
|
1
|
+
import type { MaskingStrategy, SensitiveFieldType } from '@liuhange/dsh-data-asset-shared';
|
|
2
2
|
export declare class MaskingStrategyExecutor {
|
|
3
3
|
execute(value: string, _type: SensitiveFieldType, strategy: MaskingStrategy): string;
|
|
4
4
|
resolveStrategy(strategy: MaskingStrategy): MaskingStrategy;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SensitiveField, SensitivePatterns } from '@
|
|
1
|
+
import type { SensitiveField, SensitivePatterns } from '@liuhange/dsh-data-asset-shared';
|
|
2
2
|
export interface ScanResult {
|
|
3
3
|
fields: SensitiveField[];
|
|
4
4
|
typeCounts: Record<string, number>;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liuhange/dsh-data-masking",
|
|
3
3
|
"description": "Data masking plugin: sensitive field identification and graded masking (FULL/PARTIAL/GENERALIZE)",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/liuhange789/data-asset-inspector.git",
|
|
11
11
|
"directory": "packages/data-asset/data-masking"
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
|
@@ -31,9 +31,14 @@
|
|
|
31
31
|
"lib/types/**/*.d.ts"
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"build": "tsc"
|
|
38
|
+
},
|
|
34
39
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/cordis":
|
|
36
|
-
"@deepseek-ai/dsh-tools":
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.0",
|
|
41
|
+
"@deepseek-ai/dsh-tools": "0.0.1-rc.1",
|
|
37
42
|
"@liuhange/dsh-data-asset-shared": "^2.0.0"
|
|
38
43
|
}
|
|
39
44
|
}
|