mongo-data-anonymizer 0.2.1 → 0.3.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 (55) hide show
  1. package/README.md +268 -18
  2. package/dist/anonymization/anonymize.d.ts +50 -8
  3. package/dist/anonymization/anonymize.js +747 -96
  4. package/dist/anonymization/anonymize.js.map +1 -1
  5. package/dist/anonymization/collections.d.ts +12 -0
  6. package/dist/anonymization/collections.js +14 -0
  7. package/dist/anonymization/collections.js.map +1 -0
  8. package/dist/anonymization/database.d.ts +69 -9
  9. package/dist/anonymization/database.js +233 -61
  10. package/dist/anonymization/database.js.map +1 -1
  11. package/dist/anonymization/randomizer.d.ts +8 -0
  12. package/dist/anonymization/randomizer.js +32 -0
  13. package/dist/anonymization/randomizer.js.map +1 -0
  14. package/dist/anonymization/rules.d.ts +60 -0
  15. package/dist/anonymization/rules.js +193 -0
  16. package/dist/anonymization/rules.js.map +1 -0
  17. package/dist/cli.js +11 -4
  18. package/dist/cli.js.map +1 -1
  19. package/dist/config.d.ts +7 -0
  20. package/dist/config.js +182 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.js +6 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/logger.d.ts +7 -0
  26. package/dist/logger.js +14 -0
  27. package/dist/logger.js.map +1 -0
  28. package/dist/progress.d.ts +25 -0
  29. package/dist/progress.js +74 -0
  30. package/dist/progress.js.map +1 -0
  31. package/dist/run.d.ts +62 -0
  32. package/dist/run.js +365 -0
  33. package/dist/run.js.map +1 -0
  34. package/package.json +43 -29
  35. package/.eslintignore +0 -1
  36. package/.eslintrc.js +0 -25
  37. package/.github/workflows/release.yml +0 -37
  38. package/.github/workflows/test.yml +0 -22
  39. package/.nvmrc +0 -1
  40. package/.prettierrc +0 -7
  41. package/dist/main.d.ts +0 -1
  42. package/dist/main.js +0 -95
  43. package/dist/main.js.map +0 -1
  44. package/dist/utils/field-utils.d.ts +0 -1
  45. package/dist/utils/field-utils.js +0 -18
  46. package/dist/utils/field-utils.js.map +0 -1
  47. package/jest.config.js +0 -8
  48. package/src/anonymization/anonymize.ts +0 -116
  49. package/src/anonymization/database.ts +0 -50
  50. package/src/cli.ts +0 -4
  51. package/src/main.ts +0 -98
  52. package/src/utils/field-utils.ts +0 -16
  53. package/test/anonymizer/anonymizer.test.ts +0 -164
  54. package/test/utils/field-utils.test.ts +0 -31
  55. package/tsconfig.json +0 -14
package/dist/main.js DELETED
@@ -1,95 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.main = void 0;
13
- const database_1 = require("./anonymization/database");
14
- const anonymize_1 = require("./anonymization/anonymize");
15
- const field_utils_1 = require("./utils/field-utils");
16
- const log = require('bunyan').createLogger({ name: "Main" });
17
- const defaultFields = ['email', 'name', 'description', 'address', 'city', 'country', 'phone', 'comment', 'birthdate', 'firstname', 'lastname', 'fullname'];
18
- function main() {
19
- return __awaiter(this, void 0, void 0, function* () {
20
- const config = parseArgs(process.argv);
21
- const sourceDb = new database_1.Database(config.sourceUri, 'source');
22
- const targetDb = new database_1.Database(config.targetUri, 'target');
23
- const anonymizer = new anonymize_1.Anonymize();
24
- try {
25
- yield sourceDb.connect();
26
- yield targetDb.connect();
27
- const collections = yield sourceDb.getCollections();
28
- for (const collectionName of collections) {
29
- const copyNonAnonymized = config.copyNonAnonymized && (!config.collectionList.includes(collectionName) || config.ignoreCollections.includes(collectionName));
30
- if (config.ignoreCollections.includes(collectionName) && !config.copyNonAnonymized) {
31
- log.info(`Skipping collection anonymization for ${collectionName}. Collection is in ignore list.`);
32
- continue;
33
- }
34
- if (config.collectionList.length > 0 && !config.collectionList.includes(collectionName) && !config.copyNonAnonymized) {
35
- log.info(`Skipping collection anonymization for ${collectionName}. Collection is not in the collection list.`);
36
- continue;
37
- }
38
- if (!copyNonAnonymized) {
39
- log.info(`Anonymizing collection ${collectionName}.`);
40
- }
41
- else {
42
- log.info(`Copying collection ${collectionName} without anonymization.`);
43
- }
44
- const cursor = sourceDb.getCursor(collectionName);
45
- while (yield (cursor === null || cursor === void 0 ? void 0 : cursor.hasNext())) {
46
- const batch = yield getBatch(cursor, config.batchSize);
47
- const anonymizedBatch = !copyNonAnonymized ? anonymizer.anonymizeBatch(batch, collectionName, config.fieldList) : batch;
48
- yield targetDb.insertAnonymizedBatch(collectionName, anonymizedBatch);
49
- }
50
- }
51
- log.info("Anonymization process completed successfully!");
52
- }
53
- catch (error) {
54
- log.error(`An error occurred during the anonymization process: ${error.message}`);
55
- }
56
- finally {
57
- yield sourceDb.close();
58
- yield targetDb.close();
59
- }
60
- });
61
- }
62
- exports.main = main;
63
- function parseArgs(argv) {
64
- var _a, _b;
65
- const yargs = require('yargs/yargs');
66
- const { hideBin } = require('yargs/helpers');
67
- const args = yargs(hideBin(argv))
68
- .option('sourceUri', { type: 'string', demandOption: true })
69
- .option('targetUri', { type: 'string', demandOption: true })
70
- .option('fieldList', { type: 'string', demandOption: true, default: defaultFields.join(',') })
71
- .option('collectionList', { type: 'string' })
72
- .option('ignoreCollections', { type: 'string' })
73
- .option('batchSize', { type: 'number', default: 1000 })
74
- .option('copyNonAnonymized', { type: 'boolean', default: false })
75
- .argv;
76
- return {
77
- sourceUri: args.sourceUri,
78
- targetUri: args.targetUri,
79
- fieldList: (0, field_utils_1.getFieldList)(args.fieldList, defaultFields),
80
- ignoreCollections: ((_a = args.ignoreCollections) === null || _a === void 0 ? void 0 : _a.split(',')) || [],
81
- collectionList: ((_b = args.collectionList) === null || _b === void 0 ? void 0 : _b.split(',')) || [],
82
- batchSize: args.batchSize,
83
- copyNonAnonymized: args.copyNonAnonymized
84
- };
85
- }
86
- function getBatch(cursor, batchSize) {
87
- return __awaiter(this, void 0, void 0, function* () {
88
- const batch = [];
89
- for (let i = 0; i < batchSize && (yield cursor.hasNext()); i++) {
90
- batch.push(yield cursor.next());
91
- }
92
- return batch;
93
- });
94
- }
95
- //# sourceMappingURL=main.js.map
package/dist/main.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,uDAAkD;AAClD,yDAAoD;AACpD,qDAAiD;AAEjD,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC,CAAC;AAY3D,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AAE3J,SAAsB,IAAI;;QACtB,MAAM,MAAM,GAAW,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAE/C,MAAM,QAAQ,GAAG,IAAI,mBAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC1D,MAAM,QAAQ,GAAG,IAAI,mBAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC1D,MAAM,UAAU,GAAG,IAAI,qBAAS,EAAE,CAAC;QAEnC,IAAI;YACA,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;YACzB,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;YAEzB,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,cAAc,EAAE,CAAC;YAEpD,KAAK,MAAM,cAAc,IAAI,WAAW,EAAE;gBACtC,MAAM,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,IAAI,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC;gBAE7J,IAAI,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE;oBAChF,GAAG,CAAC,IAAI,CAAC,yCAAyC,cAAc,iCAAiC,CAAC,CAAC;oBACnG,SAAS;iBACZ;gBAED,IAAI,MAAM,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE;oBAClH,GAAG,CAAC,IAAI,CAAC,yCAAyC,cAAc,6CAA6C,CAAC,CAAC;oBAC/G,SAAS;iBACZ;gBAED,IAAI,CAAC,iBAAiB,EAAE;oBACpB,GAAG,CAAC,IAAI,CAAC,0BAA0B,cAAc,GAAG,CAAC,CAAC;iBACzD;qBAAM;oBACH,GAAG,CAAC,IAAI,CAAC,sBAAsB,cAAc,yBAAyB,CAAC,CAAC;iBAC3E;gBACD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;gBAElD,OAAO,MAAM,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,OAAO,EAAE,CAAA,EAAE;oBAC5B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;oBACvD,MAAM,eAAe,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,UAAU,CAAC,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;oBACxH,MAAM,QAAQ,CAAC,qBAAqB,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;iBACzE;aACJ;YAED,GAAG,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;SAC7D;QAAC,OAAO,KAAK,EAAE;YACZ,GAAG,CAAC,KAAK,CAAC,uDAAwD,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;SAChG;gBAAS;YACN,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;YACvB,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;SAC1B;IACL,CAAC;CAAA;AA/CD,oBA+CC;AAED,SAAS,SAAS,CAAC,IAAc;;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrC,MAAM,EAAC,OAAO,EAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;SAC5B,MAAM,CAAC,WAAW,EAAE,EAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAC,CAAC;SACzD,MAAM,CAAC,WAAW,EAAE,EAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAC,CAAC;SACzD,MAAM,CAAC,WAAW,EAAE,EAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAC,CAAC;SAC3F,MAAM,CAAC,gBAAgB,EAAE,EAAC,IAAI,EAAE,QAAQ,EAAC,CAAC;SAC1C,MAAM,CAAC,mBAAmB,EAAE,EAAC,IAAI,EAAE,QAAQ,EAAC,CAAC;SAC7C,MAAM,CAAC,WAAW,EAAE,EAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAC,CAAC;SACpD,MAAM,CAAC,mBAAmB,EAAE,EAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAC,CAAC;SAC9D,IAAI,CAAC;IAEV,OAAO;QACH,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,IAAA,0BAAY,EAAC,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC;QACtD,iBAAiB,EAAE,CAAA,MAAA,IAAI,CAAC,iBAAiB,0CAAE,KAAK,CAAC,GAAG,CAAC,KAAI,EAAE;QAC3D,cAAc,EAAE,CAAA,MAAA,IAAI,CAAC,cAAc,0CAAE,KAAK,CAAC,GAAG,CAAC,KAAI,EAAE;QACrD,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;KAC5C,CAAC;AACN,CAAC;AAED,SAAe,QAAQ,CAAC,MAAW,EAAE,SAAiB;;QAClD,MAAM,KAAK,GAAG,EAAE,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,KAAI,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA,EAAE,CAAC,EAAE,EAAE;YAC1D,KAAK,CAAC,IAAI,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;SACnC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CAAA"}
@@ -1 +0,0 @@
1
- export declare function getFieldList(fieldListOption: string, defaultFields?: string[]): string[];
@@ -1,18 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getFieldList = void 0;
4
- function getFieldList(fieldListOption, defaultFields = []) {
5
- if (fieldListOption.length) {
6
- if (fieldListOption.startsWith('+')) {
7
- return defaultFields.concat(fieldListOption.slice(1).split(','));
8
- }
9
- if (fieldListOption.startsWith('-')) {
10
- const fieldsToRemove = fieldListOption.slice(1).split(',');
11
- return defaultFields.filter(field => !fieldsToRemove.includes(field));
12
- }
13
- return fieldListOption.split(',');
14
- }
15
- return [...defaultFields];
16
- }
17
- exports.getFieldList = getFieldList;
18
- //# sourceMappingURL=field-utils.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"field-utils.js","sourceRoot":"","sources":["../../src/utils/field-utils.ts"],"names":[],"mappings":";;;AAAA,SAAgB,YAAY,CAAC,eAAuB,EAAE,gBAA0B,EAAE;IAC9E,IAAI,eAAe,CAAC,MAAM,EAAE;QACxB,IAAI,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;YACjC,OAAO,aAAa,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;SACpE;QAED,IAAI,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;YACjC,MAAM,cAAc,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC3D,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;SACzE;QAED,OAAO,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;KACrC;IAED,OAAO,CAAC,GAAG,aAAa,CAAC,CAAC;AAC9B,CAAC;AAfD,oCAeC"}
package/jest.config.js DELETED
@@ -1,8 +0,0 @@
1
- module.exports = {
2
- preset: 'ts-jest',
3
- testEnvironment: 'node',
4
- roots: ['<rootDir>/test'],
5
- transform: {
6
- '^.+\\.ts?$': 'ts-jest',
7
- },
8
- };
@@ -1,116 +0,0 @@
1
- import { faker } from '@faker-js/faker';
2
-
3
- export class Anonymize {
4
-
5
- anonymizeBatch(batch: any[], collectionName: string, list: string[]): any[] {
6
- const keysToAnonymize = this.getKeysToAnonymize(list, collectionName);
7
- const fieldsToAnonymize = keysToAnonymize.map((item) => item.field);
8
-
9
- return batch.map((document) => this.anonymizeDocument(document, fieldsToAnonymize, keysToAnonymize));
10
- }
11
-
12
- private getKeysToAnonymize(list: string[], collectionName: string) {
13
- return list
14
- .filter((item) => !item.match(/^[a-z_]+\./gi) || item.startsWith(`${collectionName}.`))
15
- .map((item) => ({
16
- field: item.replace(`${collectionName}.`, "").replace(/:(?:.*)$/, "").toLowerCase(),
17
- replacement: item.includes(":") ? item.replace(/^(?:.*):/, "") : null,
18
- }));
19
- }
20
-
21
- private anonymizeDocument(document: any, fieldsToAnonymize: string[], keysToAnonymize: any[]) {
22
- const anonymizedDocument: Record<string, any> = {};
23
-
24
- for (const key in document) {
25
- if (!document.hasOwnProperty(key)) continue;
26
-
27
- if (fieldsToAnonymize.includes(key.toLowerCase())) {
28
- if (typeof document[key] === 'object') {
29
- if (Array.isArray(document[key]) && document[key].length > 0) {
30
- anonymizedDocument[key] = document[key].map((item: any) => this.anonymizeDocument(item, fieldsToAnonymize, keysToAnonymize));
31
- continue;
32
- }
33
-
34
- anonymizedDocument[key] = this.anonymizeDocument(document[key], fieldsToAnonymize, keysToAnonymize);
35
- continue;
36
- }
37
-
38
- anonymizedDocument[key] = this.anonymizeValue(
39
- key.toLowerCase(),
40
- keysToAnonymize.find((item) => item.field === key.toLowerCase())?.replacement
41
- );
42
- } else {
43
- anonymizedDocument[key] = document[key];
44
- }
45
- }
46
- return anonymizedDocument;
47
- }
48
-
49
- private anonymizeValue(key: string, replacement: string | null) {
50
- if (replacement) {
51
- return this.applyReplacement(replacement);
52
- }
53
- return this.getFakerValueForField(key);
54
- }
55
-
56
- private applyReplacement(replacement: string) {
57
- if (replacement.startsWith("faker")) {
58
- return this.getFakerValue(replacement);
59
- }
60
- switch (replacement) {
61
- case "[]": return [];
62
- case "{}": return {};
63
- case "null": return null;
64
- default: {
65
- if (replacement.startsWith("[") || replacement.startsWith("{")) {
66
- try {
67
- return JSON.parse(decodeURIComponent(replacement));
68
- } catch (error) {
69
- throw new Error(`Failed to parse replacement JSON: ${(error as Error)?.message}`);
70
- }
71
- }
72
- return replacement;
73
- }
74
- }
75
- }
76
-
77
- private getFakerValue(replacement: string): any {
78
- const parts = replacement.split(".");
79
-
80
- if (parts.length !== 3) {
81
- throw new Error(`Invalid format for replacement: ${replacement}. Expected format 'faker.category.method'`);
82
- }
83
-
84
- const [, category, method] = parts;
85
- const fakerCategory = (faker as any)[category];
86
-
87
- if (!fakerCategory) {
88
- throw new Error(`Invalid faker category: ${category}`);
89
- }
90
-
91
- const fakerMethod = fakerCategory[method];
92
-
93
- if (typeof fakerMethod !== 'function') {
94
- throw new Error(`Invalid faker method: ${method} in category ${category}`);
95
- }
96
-
97
- return fakerMethod();
98
- }
99
-
100
-
101
- private getFakerValueForField(key: string) {
102
- if (key.includes("email")) return faker.internet.email().toLowerCase();
103
- if (key.includes("firstname")) return faker.person.firstName();
104
- if (key.includes("lastname")) return faker.person.lastName();
105
- if (key === "description") return faker.lorem.sentence();
106
- if (key.endsWith("address")) return faker.location.streetAddress();
107
- if (key.endsWith("city")) return faker.location.city();
108
- if (key.endsWith("country")) return faker.location.country();
109
- if (key.endsWith("phone")) return faker.phone.number();
110
- if (key.endsWith("comment")) return faker.lorem.sentence();
111
- if (key.endsWith("date")) return faker.date.past();
112
- if (key.endsWith("name")) return faker.person.fullName();
113
-
114
- return faker.word.sample();
115
- }
116
- }
@@ -1,50 +0,0 @@
1
- import { MongoClient, Db } from 'mongodb';
2
- import * as bunyan from 'bunyan';
3
-
4
- const log = bunyan.createLogger({ name: "DatabaseHandler" });
5
-
6
- export class Database {
7
- private client: MongoClient | null = null;
8
- private db: Db | null = null;
9
-
10
- constructor(private uri: string, private name: string) {}
11
-
12
- async connect() {
13
- try {
14
- log.info(`Connecting to ${this.name} database...`);
15
- this.client = new MongoClient(this.uri);
16
- await this.client.connect();
17
- this.db = this.client.db();
18
- log.info(`Successfully connected to ${this.name} database`);
19
- } catch (error) {
20
- throw new Error(`Failed to connect to ${this.name} database: ${(error as Error).message}`);
21
- }
22
- }
23
-
24
- async close() {
25
- await this?.client?.close();
26
- log.info(`Closed database connection`);
27
- }
28
-
29
- async getCollections(): Promise<string[]> {
30
- const collections = await this?.db?.listCollections().toArray();
31
- return collections ? collections.map((item) => item.name) : [];
32
- }
33
-
34
- getCursor(collectionName: string) {
35
- return this.db ? this.db.collection(collectionName).find() : null;
36
- }
37
-
38
- async insertAnonymizedBatch(collectionName: string, anonymizedBatch: any[]) {
39
- if (this.db) {
40
- await this.db.collection(collectionName).insertMany(anonymizedBatch);
41
- }
42
- }
43
-
44
- async dropCollectionIfExists(collectionName: string) {
45
- if (this.db && await this.db.collection(collectionName).countDocuments() > 0) {
46
- await this.db.collection(collectionName).drop();
47
- log.info(`Dropped existing collection: ${collectionName}`);
48
- }
49
- }
50
- }
package/src/cli.ts DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import { main } from './main';
3
-
4
- main().catch((error) => {console.log(error.message)});
package/src/main.ts DELETED
@@ -1,98 +0,0 @@
1
- import {Database} from './anonymization/database';
2
- import {Anonymize} from './anonymization/anonymize';
3
- import {getFieldList} from './utils/field-utils';
4
-
5
- const log = require('bunyan').createLogger({name: "Main"});
6
-
7
- interface Config {
8
- sourceUri: string;
9
- targetUri: string;
10
- fieldList: string[];
11
- ignoreCollections: string[];
12
- collectionList: string[];
13
- batchSize: number;
14
- copyNonAnonymized: boolean;
15
- }
16
-
17
- const defaultFields = ['email', 'name', 'description', 'address', 'city', 'country', 'phone', 'comment', 'birthdate', 'firstname', 'lastname', 'fullname'];
18
-
19
- export async function main() {
20
- const config: Config = parseArgs(process.argv);
21
-
22
- const sourceDb = new Database(config.sourceUri, 'source');
23
- const targetDb = new Database(config.targetUri, 'target');
24
- const anonymizer = new Anonymize();
25
-
26
- try {
27
- await sourceDb.connect();
28
- await targetDb.connect();
29
-
30
- const collections = await sourceDb.getCollections();
31
-
32
- for (const collectionName of collections) {
33
- const copyNonAnonymized = config.copyNonAnonymized && (!config.collectionList.includes(collectionName) || config.ignoreCollections.includes(collectionName));
34
-
35
- if (config.ignoreCollections.includes(collectionName) && !config.copyNonAnonymized) {
36
- log.info(`Skipping collection anonymization for ${collectionName}. Collection is in ignore list.`);
37
- continue;
38
- }
39
-
40
- if (config.collectionList.length > 0 && !config.collectionList.includes(collectionName) && !config.copyNonAnonymized) {
41
- log.info(`Skipping collection anonymization for ${collectionName}. Collection is not in the collection list.`);
42
- continue;
43
- }
44
-
45
- if (!copyNonAnonymized) {
46
- log.info(`Anonymizing collection ${collectionName}.`);
47
- } else {
48
- log.info(`Copying collection ${collectionName} without anonymization.`);
49
- }
50
- const cursor = sourceDb.getCursor(collectionName);
51
-
52
- while (await cursor?.hasNext()) {
53
- const batch = await getBatch(cursor, config.batchSize);
54
- const anonymizedBatch = !copyNonAnonymized ? anonymizer.anonymizeBatch(batch, collectionName, config.fieldList) : batch;
55
- await targetDb.insertAnonymizedBatch(collectionName, anonymizedBatch);
56
- }
57
- }
58
-
59
- log.info("Anonymization process completed successfully!");
60
- } catch (error) {
61
- log.error(`An error occurred during the anonymization process: ${(error as Error).message}`);
62
- } finally {
63
- await sourceDb.close();
64
- await targetDb.close();
65
- }
66
- }
67
-
68
- function parseArgs(argv: string[]): Config {
69
- const yargs = require('yargs/yargs');
70
- const {hideBin} = require('yargs/helpers');
71
- const args = yargs(hideBin(argv))
72
- .option('sourceUri', {type: 'string', demandOption: true})
73
- .option('targetUri', {type: 'string', demandOption: true})
74
- .option('fieldList', {type: 'string', demandOption: true, default: defaultFields.join(',')})
75
- .option('collectionList', {type: 'string'})
76
- .option('ignoreCollections', {type: 'string'})
77
- .option('batchSize', {type: 'number', default: 1000})
78
- .option('copyNonAnonymized', {type: 'boolean', default: false})
79
- .argv;
80
-
81
- return {
82
- sourceUri: args.sourceUri,
83
- targetUri: args.targetUri,
84
- fieldList: getFieldList(args.fieldList, defaultFields),
85
- ignoreCollections: args.ignoreCollections?.split(',') || [],
86
- collectionList: args.collectionList?.split(',') || [],
87
- batchSize: args.batchSize,
88
- copyNonAnonymized: args.copyNonAnonymized
89
- };
90
- }
91
-
92
- async function getBatch(cursor: any, batchSize: number) {
93
- const batch = [];
94
- for (let i = 0; i < batchSize && await cursor.hasNext(); i++) {
95
- batch.push(await cursor.next());
96
- }
97
- return batch;
98
- }
@@ -1,16 +0,0 @@
1
- export function getFieldList(fieldListOption: string, defaultFields: string[] = []) {
2
- if (fieldListOption.length) {
3
- if (fieldListOption.startsWith('+')) {
4
- return defaultFields.concat(fieldListOption.slice(1).split(','));
5
- }
6
-
7
- if (fieldListOption.startsWith('-')) {
8
- const fieldsToRemove = fieldListOption.slice(1).split(',');
9
- return defaultFields.filter(field => !fieldsToRemove.includes(field));
10
- }
11
-
12
- return fieldListOption.split(',');
13
- }
14
-
15
- return [...defaultFields];
16
- }
@@ -1,164 +0,0 @@
1
- import { Anonymize } from '../../src/anonymization/anonymize';
2
-
3
- describe('Anonymize', () => {
4
- let anonymize: Anonymize;
5
-
6
- beforeEach(() => {
7
- anonymize = new Anonymize();
8
- });
9
-
10
- describe('anonymizeBatch', () => {
11
- it('should anonymize fields in a batch of documents', () => {
12
- const batch = [
13
- { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' },
14
- { firstName: 'Jane', lastName: 'Smith', email: 'jane.smith@example.com' }
15
- ];
16
- const collectionName = 'users';
17
- const list = ['users.firstName', 'users.lastName', 'users.email'];
18
-
19
- const result = anonymize.anonymizeBatch(batch, collectionName, list);
20
-
21
- expect(result).toHaveLength(2);
22
- expect(result[0].firstName).not.toBe('John');
23
- expect(result[0].lastName).not.toBe('Doe');
24
- expect(result[0].email).not.toBe('john.doe@example.com');
25
- expect(result[1].firstName).not.toBe('Jane');
26
- expect(result[1].lastName).not.toBe('Smith');
27
- expect(result[1].email).not.toBe('jane.smith@example.com');
28
- });
29
- });
30
-
31
- describe('getKeysToAnonymize', () => {
32
- it('should filter and map the list of fields to anonymize', () => {
33
- const list = ['users.firstName', 'users.lastName', 'users.email:faker.internet.email'];
34
- const collectionName = 'users';
35
-
36
- // @ts-ignore: private method
37
- const result = anonymize.getKeysToAnonymize(list, collectionName);
38
-
39
- expect(result).toEqual([
40
- { field: 'firstname', replacement: null },
41
- { field: 'lastname', replacement: null },
42
- { field: 'email', replacement: 'faker.internet.email' }
43
- ]);
44
- });
45
- });
46
-
47
- describe('anonymizeDocument', () => {
48
- it('should anonymize specified fields in a document', () => {
49
- const document = { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' };
50
- const fieldsToAnonymize = ['firstname', 'lastname', 'email'];
51
- const keysToAnonymize = [
52
- { field: 'firstname', replacement: null },
53
- { field: 'lastname', replacement: null },
54
- { field: 'email', replacement: 'faker.internet.email' }
55
- ];
56
-
57
- // @ts-ignore: private method
58
- const result = anonymize.anonymizeDocument(document, fieldsToAnonymize, keysToAnonymize);
59
-
60
- expect(result.firstName).not.toBe('John');
61
- expect(result.lastName).not.toBe('Doe');
62
- expect(result.email).not.toBe('john.doe@example.com');
63
- });
64
- });
65
-
66
- describe('anonymizeValue', () => {
67
- it('should apply replacement if provided', () => {
68
- // @ts-ignore: private method
69
- const result = anonymize.anonymizeValue('email', 'faker.internet.email');
70
- expect(result).not.toBe(null);
71
- });
72
-
73
- it('should use faker value if no replacement is provided', () => {
74
- // @ts-ignore: private method
75
- const result = anonymize.anonymizeValue('email', null);
76
- expect(result).not.toBe(null);
77
- });
78
- });
79
-
80
- describe('applyReplacement', () => {
81
- it('should handle faker replacements', () => {
82
- // @ts-ignore: private method
83
- const result = anonymize.applyReplacement('faker.internet.email');
84
- expect(result).not.toBe(null);
85
- });
86
-
87
- it('should return empty array for [] replacement', () => {
88
- // @ts-ignore: private method
89
- const result = anonymize.applyReplacement('[]');
90
- expect(result).toEqual([]);
91
- });
92
-
93
- it('should return empty object for {} replacement', () => {
94
- // @ts-ignore: private method
95
- const result = anonymize.applyReplacement('{}');
96
- expect(result).toEqual({});
97
- });
98
-
99
- it('should return null for null replacement', () => {
100
- // @ts-ignore: private method
101
- const result = anonymize.applyReplacement('null');
102
- expect(result).toBeNull();
103
- });
104
-
105
- it('should parse and return JSON for valid JSON replacements', () => {
106
- // @ts-ignore: private method
107
- const result = anonymize.applyReplacement('{"key":"value"}'); // {"key":"value"}
108
- expect(result).toEqual({ key: 'value' });
109
- });
110
-
111
- it('should throw an error for invalid JSON replacements', () => {
112
- expect(() => {
113
- // @ts-ignore: private method
114
- anonymize.applyReplacement('[invalid-json');
115
- }).toThrowError( 'Failed to parse replacement JSON: Unexpected token i in JSON at position 1');
116
- });
117
- });
118
-
119
- describe('getFakerValue', () => {
120
- it('should return a faker value for valid replacement', () => {
121
- // @ts-ignore: private method
122
- const result = anonymize.getFakerValue('faker.internet.email');
123
- expect(result).not.toBe(null);
124
- });
125
-
126
- it('should throw an error for invalid faker category', () => {
127
- expect(() => {
128
- // @ts-ignore: private method
129
- anonymize.getFakerValue('faker.invalidCategory.method');
130
- }).toThrowError('Invalid faker category: invalidCategory');
131
- });
132
-
133
- it('should throw an error for invalid faker method', () => {
134
- expect(() => {
135
- // @ts-ignore: private method
136
- anonymize.getFakerValue('faker.internet.invalidMethod');
137
- }).toThrowError('Invalid faker method: invalidMethod in category internet');
138
- });
139
-
140
- it('should throw an error for incorrect format', () => {
141
- expect(() => {
142
- // @ts-ignore: private method
143
- anonymize.getFakerValue('faker.internet');
144
- }).toThrowError('Invalid format for replacement: faker.internet. Expected format \'faker.category.method\'');
145
- });
146
- });
147
-
148
- describe('getFakerValueForField', () => {
149
- it('should return correct faker value for recognized keys', () => {
150
- // @ts-ignore: private method
151
- expect(anonymize.getFakerValueForField('email')).not.toBe(null);
152
- // @ts-ignore: private method
153
- expect(anonymize.getFakerValueForField('firstname')).not.toBe(null);
154
- // @ts-ignore: private method
155
- expect(anonymize.getFakerValueForField('lastname')).not.toBe(null);
156
- });
157
-
158
- it('should return a sample word for unrecognized keys', () => {
159
- // @ts-ignore: private method
160
- const result = anonymize.getFakerValueForField('unrecognizedKey');
161
- expect(result).not.toBe(null);
162
- });
163
- });
164
- });
@@ -1,31 +0,0 @@
1
- import {getFieldList} from '../../src/utils/field-utils';
2
-
3
- describe('getFieldList', () => {
4
- it('should return the default field list if no field list option is provided', () => {
5
- const result = getFieldList('', ['field1', 'field2', 'field3']);
6
- expect(result).toEqual(['field1', 'field2', 'field3']);
7
- });
8
-
9
- it('should return an empty array if no filed list is provided and no default list is provided', () => {
10
- const result = getFieldList('');
11
- expect(result).toEqual([]);
12
- });
13
-
14
- it('should return the correct field list if a field list is provided without modifiers', () => {
15
- const result = getFieldList('field1,field2,field3', ['field4', 'field5', 'field3']);
16
-
17
- expect(result).toEqual(['field1', 'field2', 'field3']);
18
- });
19
-
20
- it('should return the correct field list if a field list is provided with a + modifier', () => {
21
- const result = getFieldList('+field1,field2', ['field3', 'field4']);
22
-
23
- expect(result).toEqual(['field3', 'field4', 'field1', 'field2']);
24
- });
25
-
26
- it('should return the correct field list if a field list is provided with a - modifier', () => {
27
- const result = getFieldList('-field1,field2', ['field1', 'field2', 'field3']);
28
-
29
- expect(result).toEqual(['field3']);
30
- });
31
- });
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "commonjs",
4
- "target": "es6",
5
- "declaration": true,
6
- "sourceMap": true,
7
- "outDir": "./dist",
8
- "experimentalDecorators": true,
9
- "emitDecoratorMetadata": true,
10
- "strict": true
11
- },
12
- "include": ["src/**/*.ts"],
13
- "exclude": ["node_modules"]
14
- }