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.
- package/README.md +268 -18
- package/dist/anonymization/anonymize.d.ts +50 -8
- package/dist/anonymization/anonymize.js +747 -96
- package/dist/anonymization/anonymize.js.map +1 -1
- package/dist/anonymization/collections.d.ts +12 -0
- package/dist/anonymization/collections.js +14 -0
- package/dist/anonymization/collections.js.map +1 -0
- package/dist/anonymization/database.d.ts +69 -9
- package/dist/anonymization/database.js +233 -61
- package/dist/anonymization/database.js.map +1 -1
- package/dist/anonymization/randomizer.d.ts +8 -0
- package/dist/anonymization/randomizer.js +32 -0
- package/dist/anonymization/randomizer.js.map +1 -0
- package/dist/anonymization/rules.d.ts +60 -0
- package/dist/anonymization/rules.js +193 -0
- package/dist/anonymization/rules.js.map +1 -0
- package/dist/cli.js +11 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +182 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/progress.d.ts +25 -0
- package/dist/progress.js +74 -0
- package/dist/progress.js.map +1 -0
- package/dist/run.d.ts +62 -0
- package/dist/run.js +365 -0
- package/dist/run.js.map +1 -0
- package/package.json +43 -29
- package/.eslintignore +0 -1
- package/.eslintrc.js +0 -25
- package/.github/workflows/release.yml +0 -37
- package/.github/workflows/test.yml +0 -22
- package/.nvmrc +0 -1
- package/.prettierrc +0 -7
- package/dist/main.d.ts +0 -1
- package/dist/main.js +0 -95
- package/dist/main.js.map +0 -1
- package/dist/utils/field-utils.d.ts +0 -1
- package/dist/utils/field-utils.js +0 -18
- package/dist/utils/field-utils.js.map +0 -1
- package/jest.config.js +0 -8
- package/src/anonymization/anonymize.ts +0 -116
- package/src/anonymization/database.ts +0 -50
- package/src/cli.ts +0 -4
- package/src/main.ts +0 -98
- package/src/utils/field-utils.ts +0 -16
- package/test/anonymizer/anonymizer.test.ts +0 -164
- package/test/utils/field-utils.test.ts +0 -31
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fields anonymized when --fieldList doesn't replace them. Patterns end in
|
|
3
|
+
* the personal word, so `orderEmail` matches `*email` but `emailTemplate`
|
|
4
|
+
* and `emailVerified` don't.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_FIELDS = [
|
|
7
|
+
// Emails (any string value that is an email address is also caught by --scrubEmails).
|
|
8
|
+
'*email',
|
|
9
|
+
'*emails',
|
|
10
|
+
'recipient',
|
|
11
|
+
'recipients',
|
|
12
|
+
'sender',
|
|
13
|
+
// Names.
|
|
14
|
+
'name',
|
|
15
|
+
'firstname',
|
|
16
|
+
'lastname',
|
|
17
|
+
'middlename',
|
|
18
|
+
'fullname',
|
|
19
|
+
'surname',
|
|
20
|
+
'displayname',
|
|
21
|
+
'nickname',
|
|
22
|
+
'username',
|
|
23
|
+
// Phones.
|
|
24
|
+
'*phone',
|
|
25
|
+
'*phones',
|
|
26
|
+
'*phoneno',
|
|
27
|
+
'*phonenumber',
|
|
28
|
+
'*mobile',
|
|
29
|
+
'fax',
|
|
30
|
+
// Addresses and places.
|
|
31
|
+
'*address',
|
|
32
|
+
'street',
|
|
33
|
+
'city',
|
|
34
|
+
'country',
|
|
35
|
+
'*zip',
|
|
36
|
+
'*zipcode',
|
|
37
|
+
'*postcode',
|
|
38
|
+
'*postalcode',
|
|
39
|
+
// Dates of birth.
|
|
40
|
+
'birthdate',
|
|
41
|
+
'birthday',
|
|
42
|
+
'dateofbirth',
|
|
43
|
+
'dob',
|
|
44
|
+
// Free text that tends to contain personal details.
|
|
45
|
+
'description',
|
|
46
|
+
'comment',
|
|
47
|
+
'comments',
|
|
48
|
+
'note',
|
|
49
|
+
'notes',
|
|
50
|
+
// Identifiers and credentials.
|
|
51
|
+
'ip',
|
|
52
|
+
'ssn',
|
|
53
|
+
'iban',
|
|
54
|
+
'passport',
|
|
55
|
+
'*password',
|
|
56
|
+
'password*',
|
|
57
|
+
'*passwordhash',
|
|
58
|
+
'*token',
|
|
59
|
+
'*tokenhash',
|
|
60
|
+
'*secret',
|
|
61
|
+
'*secretkey',
|
|
62
|
+
'*apikey',
|
|
63
|
+
'*privatekey',
|
|
64
|
+
'salt',
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* How keys and rule fields are compared: case-insensitively and ignoring
|
|
68
|
+
* `_` and `-`, so `first_name`, `first-name` and `firstName` are the same key.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeKey(key) {
|
|
71
|
+
return key.toLowerCase().replace(/[_-]/g, '');
|
|
72
|
+
}
|
|
73
|
+
function toPattern(field) {
|
|
74
|
+
if (!field.includes('*'))
|
|
75
|
+
return null;
|
|
76
|
+
const source = field
|
|
77
|
+
.split('*')
|
|
78
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
79
|
+
.join('.*');
|
|
80
|
+
return new RegExp(`^${source}$`);
|
|
81
|
+
}
|
|
82
|
+
export function parseFieldRule(rule) {
|
|
83
|
+
const colon = rule.indexOf(':');
|
|
84
|
+
const spec = colon === -1 ? rule : rule.slice(0, colon);
|
|
85
|
+
const replacement = colon === -1 ? null : rule.slice(colon + 1);
|
|
86
|
+
const dot = spec.lastIndexOf('.');
|
|
87
|
+
const field = normalizeKey((dot === -1 ? spec : spec.slice(dot + 1)).trim());
|
|
88
|
+
if (!field) {
|
|
89
|
+
throw new Error(`Invalid field rule "${rule}": missing field name`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
collection: dot === -1 ? null : spec.slice(0, dot),
|
|
93
|
+
field,
|
|
94
|
+
replacement,
|
|
95
|
+
pattern: toPattern(field),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** The part of a rule that identifies it, i.e. without the replacement. */
|
|
99
|
+
function ruleSpec(rule) {
|
|
100
|
+
const colon = rule.indexOf(':');
|
|
101
|
+
const spec = colon === -1 ? rule : rule.slice(0, colon);
|
|
102
|
+
const dot = spec.lastIndexOf('.');
|
|
103
|
+
return `${spec.slice(0, dot + 1)}${normalizeKey(spec.slice(dot + 1))}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolves the `--fieldList` option against the default fields.
|
|
107
|
+
*
|
|
108
|
+
* Every item may carry its own modifier:
|
|
109
|
+
* - `field` : explicit field; if any are given, they replace the defaults
|
|
110
|
+
* - `+field` : added to the list
|
|
111
|
+
* - `-field` : removed from the list
|
|
112
|
+
*
|
|
113
|
+
* So `+age,-*email` means "the defaults, plus age, minus *email". A removal
|
|
114
|
+
* must name an existing rule exactly; anything else is an error rather than
|
|
115
|
+
* a silent no-op.
|
|
116
|
+
*/
|
|
117
|
+
export function getFieldList(fieldListOption, defaultFields = DEFAULT_FIELDS) {
|
|
118
|
+
const items = (fieldListOption ?? '')
|
|
119
|
+
.split(',')
|
|
120
|
+
.map((item) => item.trim())
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
const explicit = items.filter((item) => !/^[+-]/.test(item));
|
|
123
|
+
const added = items
|
|
124
|
+
.filter((item) => item.startsWith('+'))
|
|
125
|
+
.map((item) => item.slice(1));
|
|
126
|
+
const removed = new Set(items
|
|
127
|
+
.filter((item) => item.startsWith('-'))
|
|
128
|
+
.map((item) => ruleSpec(item.slice(1))));
|
|
129
|
+
const base = explicit.length > 0 ? explicit : defaultFields;
|
|
130
|
+
const all = [...base, ...added];
|
|
131
|
+
const unknown = [...removed].filter((spec) => !all.some((item) => ruleSpec(item) === spec));
|
|
132
|
+
if (unknown.length > 0) {
|
|
133
|
+
throw new Error(`--fieldList removes rules that don't exist: ${unknown.map((spec) => `-${spec}`).join(', ')}. Removals must match a rule exactly, e.g. -*email for the default *email.`);
|
|
134
|
+
}
|
|
135
|
+
return [...new Set(all.filter((item) => !removed.has(ruleSpec(item))))];
|
|
136
|
+
}
|
|
137
|
+
class RuleTier {
|
|
138
|
+
exact = new Map();
|
|
139
|
+
patterns = [];
|
|
140
|
+
/** A later rule wins over an earlier one, so rules added to the defaults take precedence. */
|
|
141
|
+
add(rule) {
|
|
142
|
+
if (rule.pattern) {
|
|
143
|
+
this.patterns.unshift(rule);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.exact.set(rule.field, rule);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
match(lower) {
|
|
150
|
+
return (this.exact.get(lower) ??
|
|
151
|
+
this.patterns.find(({ pattern }) => pattern?.test(lower)));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The rules that apply to one collection. Lookup order: the collection's own
|
|
156
|
+
* rules before global ones, and within each, an exact field name before a
|
|
157
|
+
* pattern. So `users.*name:REDACTED` overrides a global `name`.
|
|
158
|
+
*/
|
|
159
|
+
export class CollectionRules {
|
|
160
|
+
#scoped = new RuleTier();
|
|
161
|
+
#global = new RuleTier();
|
|
162
|
+
constructor(rules) {
|
|
163
|
+
for (const rule of rules) {
|
|
164
|
+
(rule.collection === null ? this.#global : this.#scoped).add(rule);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** The rule for a document key, if any. */
|
|
168
|
+
match(key) {
|
|
169
|
+
const normalized = normalizeKey(key);
|
|
170
|
+
return this.#scoped.match(normalized) ?? this.#global.match(normalized);
|
|
171
|
+
}
|
|
172
|
+
/** Field names and patterns, for reports. */
|
|
173
|
+
get fields() {
|
|
174
|
+
return [
|
|
175
|
+
...new Set([this.#scoped, this.#global].flatMap((tier) => [...tier.exact.values(), ...tier.patterns]
|
|
176
|
+
// Only rules that are in effect: not kept, and not shadowed by a
|
|
177
|
+
// collection rule (e.g. a global `name` under `images.name:keep`).
|
|
178
|
+
.filter((rule) => rule.replacement !== 'keep' && this.match(rule.field) === rule)
|
|
179
|
+
.map(({ field }) => field))),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Rules that apply to a collection: its own rules, then the global ones, so
|
|
185
|
+
* a collection-scoped rule takes precedence over a global rule.
|
|
186
|
+
*/
|
|
187
|
+
export function rulesForCollection(rules, collectionName) {
|
|
188
|
+
return new CollectionRules([
|
|
189
|
+
...rules.filter(({ collection }) => collection === collectionName),
|
|
190
|
+
...rules.filter(({ collection }) => collection === null),
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/anonymization/rules.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import { parseArgs } from "./config.js";
|
|
4
|
+
import { consoleLogger } from "./logger.js";
|
|
5
|
+
import { run } from "./run.js";
|
|
6
|
+
try {
|
|
7
|
+
await run(parseArgs(hideBin(process.argv)), consoleLogger);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
consoleLogger.error(error.message);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
6
13
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,IAAI,CAAC;IACH,MAAM,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;AAC7D,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,aAAa,CAAC,KAAK,CAAE,KAAe,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RunConfig } from './run.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Parses command-line arguments. Every option can also be set through an
|
|
4
|
+
* environment variable prefixed with ANONYMIZER_ (e.g. ANONYMIZER_SOURCE_URI,
|
|
5
|
+
* ANONYMIZER_SECRET), which keeps credentials out of the shell history.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseArgs(args: string[], env?: NodeJS.ProcessEnv): RunConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import { DEFAULT_FIELDS, getFieldList } from "./anonymization/rules.js";
|
|
4
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
5
|
+
function parseCommitQuorum(value) {
|
|
6
|
+
if (/^\d+$/.test(value))
|
|
7
|
+
return Number(value);
|
|
8
|
+
if (value === 'majority' || value === 'votingMembers')
|
|
9
|
+
return value;
|
|
10
|
+
throw new Error(`--indexCommitQuorum must be a number of members, "majority" or "votingMembers", got "${value}"`);
|
|
11
|
+
}
|
|
12
|
+
function list(value) {
|
|
13
|
+
return (value ?? '')
|
|
14
|
+
.split(',')
|
|
15
|
+
.map((item) => item.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
const OPTIONS = [
|
|
19
|
+
'sourceUri',
|
|
20
|
+
'targetUri',
|
|
21
|
+
'fieldList',
|
|
22
|
+
'collectionList',
|
|
23
|
+
'ignoreCollections',
|
|
24
|
+
'copyNonAnonymized',
|
|
25
|
+
'dropTarget',
|
|
26
|
+
'dryRun',
|
|
27
|
+
'strict',
|
|
28
|
+
'assumeDifferentTarget',
|
|
29
|
+
'scrubEmails',
|
|
30
|
+
'sampleSize',
|
|
31
|
+
'indexCommitQuorum',
|
|
32
|
+
'indexTimeout',
|
|
33
|
+
'batchSize',
|
|
34
|
+
'secret',
|
|
35
|
+
];
|
|
36
|
+
/** `sourceUri` → `ANONYMIZER_SOURCE_URI`. */
|
|
37
|
+
function envName(option) {
|
|
38
|
+
return `ANONYMIZER_${option.replace(/([A-Z])/g, '_$1').toUpperCase()}`;
|
|
39
|
+
}
|
|
40
|
+
const BOOLEAN_OPTIONS = new Set([
|
|
41
|
+
'copyNonAnonymized',
|
|
42
|
+
'dropTarget',
|
|
43
|
+
'dryRun',
|
|
44
|
+
'strict',
|
|
45
|
+
'assumeDifferentTarget',
|
|
46
|
+
'scrubEmails',
|
|
47
|
+
]);
|
|
48
|
+
function envBoolean(name, value) {
|
|
49
|
+
const normalized = value.trim().toLowerCase();
|
|
50
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized))
|
|
51
|
+
return true;
|
|
52
|
+
if (['', '0', 'false', 'no', 'off'].includes(normalized))
|
|
53
|
+
return false;
|
|
54
|
+
throw new Error(`${name} must be true or false, got "${value}"`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Options set through the environment, as arguments placed before the real
|
|
58
|
+
* ones (so the command line wins). Only known variables are read: an
|
|
59
|
+
* unrelated ANONYMIZER_* variable must not make the strict parser fail.
|
|
60
|
+
*/
|
|
61
|
+
function envArgs(env) {
|
|
62
|
+
return OPTIONS.flatMap((option) => {
|
|
63
|
+
const name = envName(option);
|
|
64
|
+
const value = env[name];
|
|
65
|
+
if (value === undefined)
|
|
66
|
+
return [];
|
|
67
|
+
if (BOOLEAN_OPTIONS.has(option)) {
|
|
68
|
+
return [envBoolean(name, value) ? `--${option}` : `--no-${option}`];
|
|
69
|
+
}
|
|
70
|
+
return [`--${option}=${value}`];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parses command-line arguments. Every option can also be set through an
|
|
75
|
+
* environment variable prefixed with ANONYMIZER_ (e.g. ANONYMIZER_SOURCE_URI,
|
|
76
|
+
* ANONYMIZER_SECRET), which keeps credentials out of the shell history.
|
|
77
|
+
*/
|
|
78
|
+
export function parseArgs(args, env = process.env) {
|
|
79
|
+
const argv = yargs([...envArgs(env), ...args])
|
|
80
|
+
.scriptName('mongo-data-anonymizer')
|
|
81
|
+
.usage('$0 --sourceUri <uri> --targetUri <uri> [options]')
|
|
82
|
+
.parserConfiguration({ 'duplicate-arguments-array': false })
|
|
83
|
+
.option('sourceUri', {
|
|
84
|
+
type: 'string',
|
|
85
|
+
demandOption: true,
|
|
86
|
+
describe: 'Source MongoDB URI',
|
|
87
|
+
})
|
|
88
|
+
.option('targetUri', {
|
|
89
|
+
type: 'string',
|
|
90
|
+
demandOption: true,
|
|
91
|
+
describe: 'Target MongoDB URI',
|
|
92
|
+
})
|
|
93
|
+
.option('fieldList', {
|
|
94
|
+
type: 'string',
|
|
95
|
+
describe: `Fields to anonymize: [collection.]field[:replacement]. Prefix items with + or - to adjust the defaults (${DEFAULT_FIELDS.join(', ')})`,
|
|
96
|
+
})
|
|
97
|
+
.option('collectionList', {
|
|
98
|
+
type: 'string',
|
|
99
|
+
describe: 'Only anonymize these collections',
|
|
100
|
+
})
|
|
101
|
+
.option('ignoreCollections', {
|
|
102
|
+
type: 'string',
|
|
103
|
+
describe: 'Never anonymize these collections',
|
|
104
|
+
})
|
|
105
|
+
.option('copyNonAnonymized', {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
default: false,
|
|
108
|
+
describe: 'Copy collections that are not anonymized as-is instead of skipping them',
|
|
109
|
+
})
|
|
110
|
+
.option('dropTarget', {
|
|
111
|
+
type: 'boolean',
|
|
112
|
+
default: false,
|
|
113
|
+
describe: 'Drop target collections that already exist',
|
|
114
|
+
})
|
|
115
|
+
.option('dryRun', {
|
|
116
|
+
type: 'boolean',
|
|
117
|
+
default: false,
|
|
118
|
+
describe: 'Show what would be done without writing anything',
|
|
119
|
+
})
|
|
120
|
+
.option('strict', {
|
|
121
|
+
type: 'boolean',
|
|
122
|
+
default: false,
|
|
123
|
+
describe: 'Fail instead of warning when a matched value cannot be anonymized (ObjectId, Binary, Decimal128, ...)',
|
|
124
|
+
})
|
|
125
|
+
.option('assumeDifferentTarget', {
|
|
126
|
+
type: 'boolean',
|
|
127
|
+
default: false,
|
|
128
|
+
describe: 'Allow a target that shares collection UUIDs or hosts with the source (e.g. a restored copy of it); writes are still checked not to reach the source',
|
|
129
|
+
})
|
|
130
|
+
.option('scrubEmails', {
|
|
131
|
+
type: 'boolean',
|
|
132
|
+
default: true,
|
|
133
|
+
describe: 'Replace email addresses found in any string, including free text (--no-scrubEmails to turn off)',
|
|
134
|
+
})
|
|
135
|
+
.option('sampleSize', {
|
|
136
|
+
type: 'number',
|
|
137
|
+
default: 1000,
|
|
138
|
+
describe: 'Documents per collection a dry run checks for unmatched personal data (0 = all)',
|
|
139
|
+
})
|
|
140
|
+
.option('indexCommitQuorum', {
|
|
141
|
+
type: 'string',
|
|
142
|
+
default: '1',
|
|
143
|
+
describe: 'Commit quorum for index builds on a replica set target: a number of members, "majority" or "votingMembers"',
|
|
144
|
+
})
|
|
145
|
+
.option('indexTimeout', {
|
|
146
|
+
type: 'number',
|
|
147
|
+
default: 900,
|
|
148
|
+
describe: "Seconds to wait for a collection's indexes to build before skipping them with a warning (0 = no limit)",
|
|
149
|
+
})
|
|
150
|
+
.option('batchSize', {
|
|
151
|
+
type: 'number',
|
|
152
|
+
default: 1000,
|
|
153
|
+
describe: 'Documents per batch',
|
|
154
|
+
})
|
|
155
|
+
.option('secret', {
|
|
156
|
+
type: 'string',
|
|
157
|
+
describe: 'Secret for deterministic fake values (prefer ANONYMIZER_SECRET)',
|
|
158
|
+
})
|
|
159
|
+
.strict()
|
|
160
|
+
.help()
|
|
161
|
+
.version(version)
|
|
162
|
+
.parseSync();
|
|
163
|
+
return {
|
|
164
|
+
sourceUri: argv.sourceUri,
|
|
165
|
+
targetUri: argv.targetUri,
|
|
166
|
+
fieldList: getFieldList(argv.fieldList),
|
|
167
|
+
collectionList: list(argv.collectionList),
|
|
168
|
+
ignoreCollections: list(argv.ignoreCollections),
|
|
169
|
+
batchSize: argv.batchSize,
|
|
170
|
+
copyNonAnonymized: argv.copyNonAnonymized,
|
|
171
|
+
dropTarget: argv.dropTarget,
|
|
172
|
+
dryRun: argv.dryRun,
|
|
173
|
+
strict: argv.strict,
|
|
174
|
+
assumeDifferentTarget: argv.assumeDifferentTarget,
|
|
175
|
+
scrubEmails: argv.scrubEmails,
|
|
176
|
+
sampleSize: argv.sampleSize,
|
|
177
|
+
indexCommitQuorum: parseCommitQuorum(argv.indexCommitQuorum),
|
|
178
|
+
indexTimeoutSeconds: argv.indexTimeout,
|
|
179
|
+
...(argv.secret ? { secret: argv.secret } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { run, type RunConfig, type RunReport, type CollectionReport, } from './run.ts';
|
|
2
|
+
export { Anonymizer, type AnonymizerOptions, } from './anonymization/anonymize.ts';
|
|
3
|
+
export { CollectionRules, DEFAULT_FIELDS, getFieldList, parseFieldRule, rulesForCollection, type FieldRule, } from './anonymization/rules.ts';
|
|
4
|
+
export { decideCollectionAction, type CollectionAction, } from './anonymization/collections.ts';
|
|
5
|
+
export { consoleLogger, silentLogger, type Logger } from './logger.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { run, } from "./run.js";
|
|
2
|
+
export { Anonymizer, } from "./anonymization/anonymize.js";
|
|
3
|
+
export { CollectionRules, DEFAULT_FIELDS, getFieldList, parseFieldRule, rulesForCollection, } from "./anonymization/rules.js";
|
|
4
|
+
export { decideCollectionAction, } from "./anonymization/collections.js";
|
|
5
|
+
export { consoleLogger, silentLogger } from "./logger.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,GAIJ,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,UAAU,GAEX,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,eAAe,EACf,cAAc,EACd,YAAY,EACZ,cAAc,EACd,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,sBAAsB,GAEvB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAe,MAAM,aAAa,CAAC"}
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function write(level, message) {
|
|
2
|
+
process.stderr.write(`${new Date().toISOString()} ${level} ${message}\n`);
|
|
3
|
+
}
|
|
4
|
+
export const consoleLogger = {
|
|
5
|
+
info: (message) => write('INFO ', message),
|
|
6
|
+
warn: (message) => write('WARN ', message),
|
|
7
|
+
error: (message) => write('ERROR', message),
|
|
8
|
+
};
|
|
9
|
+
export const silentLogger = {
|
|
10
|
+
info: () => { },
|
|
11
|
+
warn: () => { },
|
|
12
|
+
error: () => { },
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAMA,SAAS,KAAK,CAAC,KAAa,EAAE,OAAe;IAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,KAAK,IAAI,OAAO,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAW;IACnC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1C,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1C,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC;CAC5C,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAW;IAClC,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;CAChB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ProgressOptions {
|
|
2
|
+
/** How often a long copy logs its progress. */
|
|
3
|
+
reportEveryMs: number;
|
|
4
|
+
/** Warn when nothing has completed for this long. */
|
|
5
|
+
stallAfterMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_PROGRESS: ProgressOptions;
|
|
8
|
+
/**
|
|
9
|
+
* Tracks the operation in progress and warns when nothing completes for a
|
|
10
|
+
* while, so a stuck server call (e.g. an index build waiting for a replica
|
|
11
|
+
* set's commit quorum) shows up in the log instead of as silence.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Progress {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(options: ProgressOptions, warn: (message: string) => void);
|
|
16
|
+
start(): void;
|
|
17
|
+
stop(): void;
|
|
18
|
+
/** A new server call or step starts. */
|
|
19
|
+
operation(description: string): void;
|
|
20
|
+
/** Something completed (a batch was read or written). */
|
|
21
|
+
activity(): void;
|
|
22
|
+
}
|
|
23
|
+
/** `12,000/35,000 (34%), 9,800 docs/s, ETA 2s` */
|
|
24
|
+
export declare function describeProgress(done: number, total: number, elapsedMs: number): string;
|
|
25
|
+
export declare function formatDuration(ms: number): string;
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const DEFAULT_PROGRESS = {
|
|
2
|
+
reportEveryMs: 5_000,
|
|
3
|
+
stallAfterMs: 30_000,
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Tracks the operation in progress and warns when nothing completes for a
|
|
7
|
+
* while, so a stuck server call (e.g. an index build waiting for a replica
|
|
8
|
+
* set's commit quorum) shows up in the log instead of as silence.
|
|
9
|
+
*/
|
|
10
|
+
export class Progress {
|
|
11
|
+
#options;
|
|
12
|
+
#warn;
|
|
13
|
+
#timer;
|
|
14
|
+
#operation = 'starting';
|
|
15
|
+
#operationStarted = Date.now();
|
|
16
|
+
#lastActivity = Date.now();
|
|
17
|
+
#stallReported = false;
|
|
18
|
+
constructor(options, warn) {
|
|
19
|
+
this.#options = options;
|
|
20
|
+
this.#warn = warn;
|
|
21
|
+
}
|
|
22
|
+
start() {
|
|
23
|
+
const every = Math.max(100, Math.min(1_000, this.#options.stallAfterMs / 2));
|
|
24
|
+
this.#timer = setInterval(() => this.#check(), every);
|
|
25
|
+
this.#timer.unref();
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
clearInterval(this.#timer);
|
|
29
|
+
}
|
|
30
|
+
/** A new server call or step starts. */
|
|
31
|
+
operation(description) {
|
|
32
|
+
this.#operation = description;
|
|
33
|
+
this.#operationStarted = Date.now();
|
|
34
|
+
this.activity();
|
|
35
|
+
}
|
|
36
|
+
/** Something completed (a batch was read or written). */
|
|
37
|
+
activity() {
|
|
38
|
+
this.#lastActivity = Date.now();
|
|
39
|
+
this.#stallReported = false;
|
|
40
|
+
}
|
|
41
|
+
#check() {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const idle = now - this.#lastActivity;
|
|
44
|
+
if (this.#stallReported || idle < this.#options.stallAfterMs)
|
|
45
|
+
return;
|
|
46
|
+
this.#stallReported = true;
|
|
47
|
+
const running = Math.round((now - this.#operationStarted) / 1000);
|
|
48
|
+
const hint = this.#operation.startsWith('createIndexes')
|
|
49
|
+
? ' On a replica set, index builds wait for the commit quorum; check db.currentOp() on the target and see --indexCommitQuorum.'
|
|
50
|
+
: '';
|
|
51
|
+
this.#warn(`No progress for ${Math.round(idle / 1000)}s: ${this.#operation} (running ${running}s).${hint}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** `12,000/35,000 (34%), 9,800 docs/s, ETA 2s` */
|
|
55
|
+
export function describeProgress(done, total, elapsedMs) {
|
|
56
|
+
const rate = elapsedMs > 0 ? (done * 1000) / elapsedMs : 0;
|
|
57
|
+
const format = (n) => Math.round(n).toLocaleString('en-US');
|
|
58
|
+
if (total <= 0 || done > total) {
|
|
59
|
+
return `${format(done)} documents, ${format(rate)} docs/s`;
|
|
60
|
+
}
|
|
61
|
+
const percent = Math.floor((done * 100) / total);
|
|
62
|
+
const eta = rate > 0 ? formatDuration(((total - done) * 1000) / rate) : '?';
|
|
63
|
+
return `${format(done)}/${format(total)} (${percent}%), ${format(rate)} docs/s, ETA ${eta}`;
|
|
64
|
+
}
|
|
65
|
+
export function formatDuration(ms) {
|
|
66
|
+
const seconds = Math.round(ms / 1000);
|
|
67
|
+
if (seconds < 60)
|
|
68
|
+
return `${seconds}s`;
|
|
69
|
+
const minutes = Math.floor(seconds / 60);
|
|
70
|
+
if (minutes < 60)
|
|
71
|
+
return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
72
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.js","sourceRoot":"","sources":["../src/progress.ts"],"names":[],"mappings":"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"}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type CollectionAction } from './anonymization/collections.ts';
|
|
2
|
+
import { type Logger } from './logger.ts';
|
|
3
|
+
import { type ProgressOptions } from './progress.ts';
|
|
4
|
+
export interface RunConfig {
|
|
5
|
+
sourceUri: string;
|
|
6
|
+
targetUri: string;
|
|
7
|
+
/** Field rules, `[collection.]field[:replacement]`. */
|
|
8
|
+
fieldList: string[];
|
|
9
|
+
collectionList: string[];
|
|
10
|
+
ignoreCollections: string[];
|
|
11
|
+
batchSize: number;
|
|
12
|
+
copyNonAnonymized: boolean;
|
|
13
|
+
/** Drop target collections that already exist instead of refusing to run. */
|
|
14
|
+
dropTarget: boolean;
|
|
15
|
+
/** Report what would happen without writing anything. */
|
|
16
|
+
dryRun: boolean;
|
|
17
|
+
/** Secret for deterministic anonymization; random per run when omitted. */
|
|
18
|
+
secret?: string;
|
|
19
|
+
/** Fail instead of warning when a matched value can't be anonymized. */
|
|
20
|
+
strict?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Skip the read-only "looks like the same database" check (shared
|
|
23
|
+
* collection UUIDs or hosts), e.g. when the target is a restored copy of the
|
|
24
|
+
* source. The write probe that proves the target is not the source still runs.
|
|
25
|
+
*/
|
|
26
|
+
assumeDifferentTarget?: boolean;
|
|
27
|
+
/** Replace email addresses found in any string (default true). */
|
|
28
|
+
scrubEmails?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Documents per collection a dry run checks for personal-looking keys that
|
|
31
|
+
* no rule matches (default 1000; 0 checks every document).
|
|
32
|
+
*/
|
|
33
|
+
sampleSize?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Commit quorum for index builds when the target is a replica set
|
|
36
|
+
* (default 1: the primary's vote is enough, so an unavailable or stuck
|
|
37
|
+
* secondary can't hold the build up). Ignored on a standalone.
|
|
38
|
+
*/
|
|
39
|
+
indexCommitQuorum?: number | string;
|
|
40
|
+
/** Give up on a collection's index build after this many seconds (default 900; 0 = no limit). */
|
|
41
|
+
indexTimeoutSeconds?: number;
|
|
42
|
+
/** Progress log and stall warning timings; mainly for tests. */
|
|
43
|
+
progress?: Partial<ProgressOptions>;
|
|
44
|
+
}
|
|
45
|
+
export interface CollectionReport {
|
|
46
|
+
name: string;
|
|
47
|
+
type: string;
|
|
48
|
+
action: CollectionAction;
|
|
49
|
+
documents: number;
|
|
50
|
+
anonymizedFields: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Dry run only: keys in a sample of the collection that look like
|
|
53
|
+
* personal data but that no rule covers (and that will therefore be
|
|
54
|
+
* written unchanged).
|
|
55
|
+
*/
|
|
56
|
+
unmatchedPersonalKeys?: string[];
|
|
57
|
+
}
|
|
58
|
+
export interface RunReport {
|
|
59
|
+
collections: CollectionReport[];
|
|
60
|
+
warnings: string[];
|
|
61
|
+
}
|
|
62
|
+
export declare function run(config: RunConfig, logger?: Logger): Promise<RunReport>;
|