') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - dataanon/data-anon: Data Anonymization implementation in Kotiln · GitHub
Skip to content

Latest commit

History

120 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Data::Anonymization

Data Anonymization tool helps build anonymized production data dumps, which you can use for performance testing, security testing, debugging and development. Tool is implemented in Kotlin, and works with Java 8 & Kotlin.

Build Status

Getting started

Kotlin

funmain(args:Array<String>) {
// define your database connection settings val source =DbConfig("jdbc:h2:tcp://localhost/~/movies_source", "sa", "")
val dest =DbConfig("jdbc:h2:tcp://localhost/~/movies_dest", "sa", "")
// choose Whitelist or Blacklist strategy for anonymizationWhitelist(source, dest)
.table("MOVIES") { // start with table 
where("GENRE = 'Drama'") // allows to select only desired rows (optional)
limit(1_00_000) // useful for testing (optional)// pass through fields, leave it as is (do not anonymize)
whitelist("MOVIE_ID")
// field by field decide the anonymization strategy
anonymize("GENRE").using(PickFromDatabase<String>(source,"SELECT DISTINCT GENRE FROM MOVIES"))
anonymize("RELEASE_DATE").using(DateRandomDelta(10))
// write your own in-line strategy
anonymize("TITLE").using(object:AnonymizationStrategy<String>{
overridefunanonymize(field:Field<String>, record:Record): String="MY MOVIE ${record.rowNum}"
})
}
// continue with multiple tables
.table("RATINGS") {
whitelist("MOVIE_ID","USER_ID","CREATED_AT")
anonymize("RATING").using(FixedDouble(4.3))
}
.execute()
}

Java

publicclassAnonymizer {
publicstaticvoidmain(String[] args) {
// define your database connection settingsDbConfigsource = newDbConfig("jdbc:h2:tcp://localhost/~/movies_source", "sa", "");
DbConfigdest = newDbConfig("jdbc:h2:tcp://localhost/~/movies_dest", "sa", "");
// choose Whitelist or Blacklist strategy for anonymizationnewWhitelist(source, dest)
// start with table
.table("MOVIES", table -> {
table.where("GENRE = 'Drama'"); // allows to select only desired rows (optional)table.limit(1_00); // useful for testing (optional)// pass through fields, leave it as is (do not anonymize)table.whitelist("MOVIE_ID");
// field by field decide the anonymization strategytable.anonymize("GENRE").using(newPickFromDatabase<String>(source,"SELECT DISTINCT GENRE FROM MOVIES"));
table.anonymize("RELEASE_DATE").using(newDateRandomDelta(10));
// write your own in-line strategytable.anonymize("TITLE").using((AnonymizationStrategy<String>) (field, record) -> "MY MOVIE " + record.getRowNum());
// return just to cover up for Kotlin, copy as isreturnUnit.INSTANCE;
})
// continue with multiple tables
.table("RATINGS", table -> {
table.whitelist("MOVIE_ID", "USER_ID", "CREATED_AT");
table.anonymize("RATING").using(newFixedDouble(4.3));
returnUnit.INSTANCE;
})
.execute();
}
}

Running

$ mvn compile exec:java

Or

$ mvn package
$ java -jar target/data-anon.jar

Start with samples...


Notes/tips on usage of tool

  1. In Whitelist approach provide source database connection user with READONLY access.
  2. All messages (incl. errors) are logged in dataaonn.log file. Using logging.properties control log messages.
  3. Use where and limit to limit the number of rows during anonymization. Very useful for testing purpose.
  4. Extend DbConfig and implement connection method for special handling while creating database connection.
  5. Write your own anonymization strategy for specific cases.
  6. null values are kept null after anonymization.
  7. Use PickFromDatabase strategy for picking values for existing values and enum type values.

Roadmap

  1. Support default strategy based on data type. As of now you need to specify anonymization strategy for each field.
  2. MongoDB database.

Share feedback

Please use Github issues to share feedback, feature suggestions and report issues.

Changelog

0.9.3 (Mar 214, 2018)

  1. Error handling using logs. All messages logged in dataaonn.log file. Using logging.properties control log messages.
  2. Added date and timestamp related anonymization strategies - DateRandomDelta & DateTimeRandomDelta
  3. PickFromDatabase strategy added to support enum types fields picked up from database column. Useful for names type fields as well.

0.9.1 (Feb 26, 2018)

  1. First initial release with RDBMS support
  2. First class support for table parallelization providing better performance
  3. Easy to use DSL built using Kotlin

What is data anonymization?

For almost all projects there is a need for production data dump in order to run performance tests, rehearse production releases and debug production issues. However, getting production data and using it is not feasible due to multiple reasons, primary being privacy concerns for user data. And thus the need for data anonymization. This tool helps you to get anonymized production data dump using either Blacklist or Whitelist strategies.

Read more about data anonymization here

Anonymization Strategies

Blacklist

Blacklist approach essentially leaves all fields unchanged with the exception of those specified by the user, which are scrambled/anonymized. For Blacklist create a copy of prod database and chooses the fields to be anonymized like e.g. username, password, email, name, geo location etc. based on user specification. Most of the fields have different rules e.g. password should be set to same value for all users, email needs to be valid.

The problem with this approach is that when new fields are added they will not be anonymized by default. Human error in omitting users personal data could be damaging.

Blacklist(database)
.table("RATINGS") {
anonymize("RATING").using(FixedDouble(4.3))
}
.execute()

Whitelist

Whitelist approach, by default scrambles/anonymizes all fields except a list of fields which are allowed to copied as is. By default all data needs to be anonymized. So from production database data is sanitized record by record and inserted as anonymized data into destination database. Source database needs to be readonly. All fields would be anonymized using default anonymization strategy which is based on the datatype, unless a special anonymization strategy is specified. For instance special strategies could be used for emails, passwords, usernames etc. A whitelisted field implies that it's okay to copy the data as is and anonymization isn't required. This way any new field will be anonymized by default and if we need them as is, add it to the whitelist explicitly. This prevents any human error and protects sensitive information.

Whitelist(source, dest)
.table("RATINGS") {
whitelist("MOVIE_ID","USER_ID","CREATED_AT")
anonymize("RATING").using(FixedDouble(4.3))
}
.execute()

Read more about blacklist and whitelist here


Anonymization Strategies

DataTypeStratergyDescription
BooleanRandomBooleanTrueFalserandom selection of boolean true and false value
BooleanRandomBooleanOneZerorandom selection of 1 and 0 value representing boolean
BooleanRandomBooleanYNrandom selection of Y and N value representing boolean
String (Email)RandomEmailgenerates emailId using one of random picked values defined in specified file (default file is first_names.dat) appended with row number with given host and tld
String (First Name)RandomFirstNamegenerates first name using one of random picked values from specified file. default file is (first_names.dat)
String (Last Name)RandomLastNamegenerates last name using one of random picked values from specified file. default file is (last_names.dat)
IntegerFixedIntreplace all records with the same specified fixed integer value (default to 100)
IntegerRandomIntgenerate random integer value between specified range (default range from 0 to 100)
IntegerRandomIntDeltagenerate new integer value within random delta value (default is 10) on existing value
FloatFixedFloatreplace all records with the same specified fixed float value (default to 100.0f)
FloatRandomFloatgenerate random float value between specified range (default range from 0.0f to 100.0f)
FloatRandomFloatDeltagenerate new float value within random delta value (default is 10.0f) on existing value
DoubleFixedDoublereplace all records with the same specified fixed double value (default to 100.0)
DoubleRandomDoublegenerate random double value between specified range (default range from 0.0 to 100.0)
DoubleRandomDoubleDeltagenerate new double value within random delta value (default is 10.0) on existing value
StringFixedStringreplace all records with the same specified fixed string value
StringLoremIpsumreplace with same length (size) Lorem Ipsum string
StringPickStringFromFilereplace with randomly picked string (line) from file
StringRandomAlphabeticreplace with random alphabets char set only creating string
StringRandomAlphaNumericreplace with random alphabets + numbers (alphanumeric char set) creating string
StringRandomFormattedStringreplace with string build with exactly same format (number replacing number, lowercase replacing lowercase alphabets & uppercase replacing uppercase alphabets
StringRandomStringreplace with random generation of string of any char set
StringStringTemplatereplace with string generated using template specified
DateDateRandomDeltadate field is changed randomly within given range of days
TimestampDateTimeRandomDeltatimestamp (datetime) field is changed randomly within given duration
AnyPickFromListreplaces value with specified type and randomly picked from list of specified values
AnyPickFromDatabasereplaces value with specified type and randomly picked from list of specified values fetched from database

Write your own Anonymization strategy

Implement AnonymizationStrategy interface override method to write your own strategy.

classRandomString: AnonymizationStrategy<String> {
overridefunanonymize(field:Field<String>, record:Record): String="Record Number ${record.rowNum}"
}

Field class represents data related to the field getting processed for anonymization

classField<T:Any>(valname:String, valoldValue:T, varnewValue:T = oldValue)

Record class represents the current record getting processed with row number and all the fields of the record. Other fields data is useful in case if there is any dependent field value which needs to be derived or calculated.

classRecord(privatevalfields:List<Field<Any>>, valrowNum:Int) {
funfind(name:String): Field<Any> = fields.first {name.equals(it.name, true)}
}

It is very easy to write inline strategy as well. See examples

Want to contribute?

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Credits

About

Data Anonymization implementation in Kotiln

Topics

Resources

Stars

38 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages