├── .travis.yml ├── src ├── main │ ├── java │ │ ├── com │ │ │ └── rolfje │ │ │ │ └── anonimatron │ │ │ │ ├── version.txt │ │ │ │ ├── anonymizer │ │ │ │ ├── Prefetcher.java │ │ │ │ ├── roman-names.syl │ │ │ │ ├── BankAccountAnonymizer.java │ │ │ │ ├── ElvenNameGenerator.java │ │ │ │ ├── RomanNameGenerator.java │ │ │ │ ├── elven-names.syl │ │ │ │ ├── EmailAddressAnonymizer.java │ │ │ │ ├── UUIDAnonymizer.java │ │ │ │ ├── DateAnonymizer.java │ │ │ │ ├── StringAnonymizer.java │ │ │ │ ├── IPAddressV4Anonymizer.java │ │ │ │ ├── CharacterStringPrefetchAnonymizer.java │ │ │ │ ├── CountryCodeAnonymizer.java │ │ │ │ ├── DutchZipCodeAnonymizer.java │ │ │ │ ├── CharacterStringAnonymizer.java │ │ │ │ ├── Hasher.java │ │ │ │ ├── Anonymizer.java │ │ │ │ ├── AbstractElevenProofAnonymizer.java │ │ │ │ ├── IbanAnonymizer.java │ │ │ │ ├── DigitStringAnonymizer.java │ │ │ │ ├── SynonymCache.java │ │ │ │ └── UkPostCodeAnonymizer.java │ │ │ │ ├── file │ │ │ │ ├── RecordWriter.java │ │ │ │ ├── RecordReader.java │ │ │ │ ├── CsvFileWriter.java │ │ │ │ ├── Record.java │ │ │ │ └── CsvFileReader.java │ │ │ │ ├── synonyms │ │ │ │ ├── NullSynonym.java │ │ │ │ ├── HashedFromSynonym.java │ │ │ │ ├── StringDateFieldHandler.java │ │ │ │ ├── DateSynonym.java │ │ │ │ ├── Synonym.java │ │ │ │ ├── Base64StringFieldHandler.java │ │ │ │ ├── NumberSynonym.java │ │ │ │ ├── StringSynonym.java │ │ │ │ ├── castor-synonym-mapping.xml │ │ │ │ └── SynonymMapper.java │ │ │ │ ├── configuration │ │ │ │ ├── Discriminator.java │ │ │ │ ├── ColumnShortLivedFieldHandler.java │ │ │ │ ├── Table.java │ │ │ │ ├── DataFile.java │ │ │ │ ├── Column.java │ │ │ │ └── castor-config-mapping.xml │ │ │ │ ├── jdbc │ │ │ │ └── ColumnWorker.java │ │ │ │ ├── progress │ │ │ │ ├── Progress.java │ │ │ │ └── ProgressPrinter.java │ │ │ │ └── commandline │ │ │ │ └── CommandLine.java │ │ ├── castor.properties │ │ └── log4j.xml │ └── assembly │ │ └── anonimatronbin.xml └── test │ └── java │ └── com │ ├── rolfje │ ├── anonimatron │ │ ├── integrationtests │ │ │ ├── integrationconfig.xml │ │ │ └── IntegrationTest.java │ │ ├── file │ │ │ ├── AcceptAllFilter.java │ │ │ ├── RecordTest.java │ │ │ ├── CsvFileWriterTest.java │ │ │ └── CsvFileReaderTest.java │ │ ├── synonyms │ │ │ ├── SynonymTest.java │ │ │ ├── Base64StringFieldHandlerTest.java │ │ │ └── SynonymMapperTest.java │ │ ├── anonymizer │ │ │ ├── ToLowerAnonymizer.java │ │ │ ├── FixedValueAnonymizer.java │ │ │ ├── CharacterStringAnonymizerTest.java │ │ │ ├── AbstractElevenProofAnonymizerTest.java │ │ │ ├── StringAnonymizerTest.java │ │ │ ├── DutchZipCodeAnonymizerTest.java │ │ │ ├── RomanNameGeneratorTest.java │ │ │ ├── IPAddressV4AnonymizerTest.java │ │ │ ├── HasherTest.java │ │ │ ├── SynonymCacheTest.java │ │ │ ├── CountryCodeAnonymizerTest.java │ │ │ ├── IbanAnonymizerTest.java │ │ │ ├── CharacterStringPrefetchAnonymizerTest.java │ │ │ ├── UkPostCodeAnonymizerTest.java │ │ │ ├── AnonymizerServiceTest.java │ │ │ ├── DutchBSNAnononymizerTest.java │ │ │ └── DigitStringAnonymizerTest.java │ │ ├── progress │ │ │ ├── ProgressPrinterTest.java │ │ │ └── ProgressTest.java │ │ ├── LoremIpsumTest.java │ │ ├── commandline │ │ │ └── CommandLineTest.java │ │ ├── configuration │ │ │ ├── ConfigurationTest.java │ │ │ └── ColumnShortLivedFieldHandlerTest.java │ │ └── AnonimatronTest.java │ └── junit │ │ └── Asserts.java │ └── javamonitor │ └── tools │ ├── ThreadLocalStopwatchNano.java │ ├── Stopwatch.java │ └── StopwatchNano.java ├── docs ├── assets │ ├── favicon.ico │ └── css │ │ ├── zip-icon.png │ │ ├── book-flat-30.png │ │ ├── octocat-icon.png │ │ ├── tar-gz-icon.png │ │ ├── anonimatron-logo-a.png │ │ └── style.scss ├── images │ ├── logo-csv.png │ ├── logo-db2.png │ ├── logo-xml.png │ ├── no-brain.png │ ├── logo-derby.png │ ├── logo-mssql.png │ ├── logo-mysql.png │ ├── logo-sybase.png │ ├── logo-informix.png │ ├── logo-interbase.png │ ├── logo-mariadb.png │ ├── logo-oracledb.png │ ├── logo-pointbase.png │ ├── logo-firebirdsql.png │ └── logo-postgresql.png ├── documentation │ ├── images │ │ ├── 1-dataindb.png │ │ ├── 2-anonimatron.png │ │ └── 3-datainsynonims.png │ └── anonymizerlist.md ├── Gemfile ├── serve_local_jekyll_site.sh ├── faq.md ├── _config.yml ├── _layouts │ └── default.html └── index.md ├── resources ├── images │ ├── download-now.png │ ├── anonimatron-logo-a.png │ ├── anonimatron-logo-a.pxm │ ├── anonimatron-logo-a.xcf │ ├── anonimatron-logo-full.png │ ├── github-social-preview.png │ └── github-social-preview.pxm ├── libraries │ ├── jtds-1.3.1.jar │ ├── classes12-0.0.jar │ ├── mssql-jdbc-7.2.2.jre8.jar │ ├── postgresql-9.0-801.jdbc4.jar │ └── mysql-connector-java-8.0.21.jar ├── scripts │ ├── anonimatron.bat │ └── anonimatron.sh ├── integration │ ├── mssql │ │ ├── config.xml │ │ └── README.md │ └── mysql │ │ ├── config.xml │ │ └── README.md ├── documentation │ ├── README.md │ └── VERSION_INFO.TXT └── anonymizers │ └── README.md ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── issue_template.md ├── CODE_OF_CONDUCT.md ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── CONTRIBUTING.md ├── .gitignore ├── README.md ├── LICENSE.md └── create_release.sh /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/version.txt: -------------------------------------------------------------------------------- 1 | 1.16-SNAPSHOT 2 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/images/logo-csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-csv.png -------------------------------------------------------------------------------- /docs/images/logo-db2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-db2.png -------------------------------------------------------------------------------- /docs/images/logo-xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-xml.png -------------------------------------------------------------------------------- /docs/images/no-brain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/no-brain.png -------------------------------------------------------------------------------- /docs/images/logo-derby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-derby.png -------------------------------------------------------------------------------- /docs/images/logo-mssql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-mssql.png -------------------------------------------------------------------------------- /docs/images/logo-mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-mysql.png -------------------------------------------------------------------------------- /docs/images/logo-sybase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-sybase.png -------------------------------------------------------------------------------- /docs/assets/css/zip-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/assets/css/zip-icon.png -------------------------------------------------------------------------------- /docs/images/logo-informix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-informix.png -------------------------------------------------------------------------------- /docs/images/logo-interbase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-interbase.png -------------------------------------------------------------------------------- /docs/images/logo-mariadb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-mariadb.png -------------------------------------------------------------------------------- /docs/images/logo-oracledb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-oracledb.png -------------------------------------------------------------------------------- /docs/images/logo-pointbase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-pointbase.png -------------------------------------------------------------------------------- /docs/assets/css/book-flat-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/assets/css/book-flat-30.png -------------------------------------------------------------------------------- /docs/assets/css/octocat-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/assets/css/octocat-icon.png -------------------------------------------------------------------------------- /docs/assets/css/tar-gz-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/assets/css/tar-gz-icon.png -------------------------------------------------------------------------------- /docs/images/logo-firebirdsql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-firebirdsql.png -------------------------------------------------------------------------------- /docs/images/logo-postgresql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/images/logo-postgresql.png -------------------------------------------------------------------------------- /resources/images/download-now.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/images/download-now.png -------------------------------------------------------------------------------- /resources/libraries/jtds-1.3.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/libraries/jtds-1.3.1.jar -------------------------------------------------------------------------------- /src/main/java/castor.properties: -------------------------------------------------------------------------------- 1 | # True if all documents should be indented on output by default 2 | org.exolab.castor.indent=true -------------------------------------------------------------------------------- /resources/libraries/classes12-0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/libraries/classes12-0.0.jar -------------------------------------------------------------------------------- /docs/assets/css/anonimatron-logo-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/assets/css/anonimatron-logo-a.png -------------------------------------------------------------------------------- /docs/documentation/images/1-dataindb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/documentation/images/1-dataindb.png -------------------------------------------------------------------------------- /resources/images/anonimatron-logo-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/images/anonimatron-logo-a.png -------------------------------------------------------------------------------- /resources/images/anonimatron-logo-a.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/images/anonimatron-logo-a.pxm -------------------------------------------------------------------------------- /resources/images/anonimatron-logo-a.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/images/anonimatron-logo-a.xcf -------------------------------------------------------------------------------- /resources/images/anonimatron-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/images/anonimatron-logo-full.png -------------------------------------------------------------------------------- /resources/images/github-social-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/images/github-social-preview.png -------------------------------------------------------------------------------- /resources/images/github-social-preview.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/images/github-social-preview.pxm -------------------------------------------------------------------------------- /docs/documentation/images/2-anonimatron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/documentation/images/2-anonimatron.png -------------------------------------------------------------------------------- /resources/libraries/mssql-jdbc-7.2.2.jre8.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/libraries/mssql-jdbc-7.2.2.jre8.jar -------------------------------------------------------------------------------- /docs/documentation/images/3-datainsynonims.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/docs/documentation/images/3-datainsynonims.png -------------------------------------------------------------------------------- /resources/libraries/postgresql-9.0-801.jdbc4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/libraries/postgresql-9.0-801.jdbc4.jar -------------------------------------------------------------------------------- /resources/libraries/mysql-connector-java-8.0.21.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realrolfje/anonimatron/HEAD/resources/libraries/mysql-connector-java-8.0.21.jar -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Implementation of solution for [issue XXX](https://github.com/realrolfje/anonimatron/issues/XXX) 2 | 3 | ... additional info which helps reviewing this request ... -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/Prefetcher.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | public interface Prefetcher { 4 | 5 | void prefetch(Object sourceData); 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | anonimatron.log 4 | anonimatron.iml 5 | target 6 | /docs/ruby/ 7 | /docs/.bundle/ 8 | /docs/Gemfile.lock 9 | /docs/.env 10 | /docs/_site/ 11 | -------------------------------------------------------------------------------- /resources/scripts/anonimatron.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -Xmx2G --add-opens java.xml/com.sun.org.apache.xml.internal.serialize=ALL-UNNAMED -classpath *;./libraries/*;./jdbcdrivers/*;./anonymizers/* com.rolfje.anonimatron.Anonimatron %* 3 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | Anonimatron version: 2 | Operating system and version: 3 | Java runtime (`java -version`): 4 | 5 | #### Executed commands or actions: 6 | 7 | #### Expected outcome or behavior: 8 | 9 | #### Actual outcome or behavior: 10 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | When contributing code, reporting issues or communicating with other members or users of this project I kindly ask you to behave responsible, polite, respectful and professional. 4 | 5 | Thank you for understanding. -------------------------------------------------------------------------------- /resources/scripts/anonimatron.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | cd -P "$(dirname $0)" 4 | java ${JAVA_OPTS:='-Xmx2G'} --add-opens java.xml/com.sun.org.apache.xml.internal.serialize=ALL-UNNAMED -classpath *:./libraries/*:./jdbcdrivers/*:./anonymizers/* com.rolfje.anonimatron.Anonimatron $* 5 | cd - -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/file/RecordWriter.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import java.io.Closeable; 4 | 5 | /** 6 | * Consumes records which were anonymized by {@link FileAnonymizerService}. 7 | */ 8 | public interface RecordWriter extends Closeable { 9 | void write(Record record); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/file/RecordReader.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import java.io.Closeable; 4 | 5 | /** 6 | * Provides records to be anonymized the {@link FileAnonymizerService}. 7 | */ 8 | public interface RecordReader extends Closeable{ 9 | boolean hasRecords(); 10 | Record read(); 11 | } 12 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Two lines below are a workaround for an octokit/faraday issue, 4 | # see https://github.com/github/pages-gem/issues/665 5 | gem "faraday", "~> 0.17" 6 | gem "octokit", github: "octokit/octokit.rb", ref: "ae5838a" 7 | 8 | gem 'dotenv-rails', groups: [:development, :test] 9 | gem 'github-pages', group: :jekyll_plugins -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/roman-names.syl: -------------------------------------------------------------------------------- 1 | -a 2 | -al 3 | -au +c 4 | -an 5 | -ba 6 | -be 7 | -bi 8 | -br +v 9 | -da 10 | -di 11 | -do 12 | -du 13 | -e 14 | -eu +c 15 | -fa 16 | bi 17 | be 18 | bo 19 | bu 20 | nul +v 21 | gu 22 | da 23 | au +c -c 24 | fri 25 | gus 26 | +tus 27 | +lus 28 | +lius 29 | +nus 30 | +es 31 | +ius -c 32 | +cus 33 | +tor 34 | +cio 35 | +tin -------------------------------------------------------------------------------- /resources/integration/mssql/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 |
7 | 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/integrationtests/integrationconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 |
7 | 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /docs/serve_local_jekyll_site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # From https://help.github.com/en/articles/setting-up-your-github-pages-site-locally-with-jekyll#requirements 4 | # 5 | # Install the ruby bundler: 6 | # sudo ruby install bundler 7 | # 8 | # Install jekyll locally (as local user, in the current directory): 9 | # bundle install --path ruby/bundle 10 | # 11 | # Serve jekyll pages locally: 12 | bundle exec jekyll serve 13 | open http://127.0.0.1:4000 14 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/file/AcceptAllFilter.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import java.io.File; 4 | import java.io.FileFilter; 5 | 6 | public class AcceptAllFilter implements FileFilter { 7 | 8 | private static int acceptcount = 0; 9 | 10 | @Override 11 | public boolean accept(File pathname) { 12 | acceptcount += 1; 13 | return true; 14 | } 15 | 16 | public static int getAcceptCount(){ 17 | return acceptcount; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ### Why does Anonimatron use pseudorandom generators? 4 | This is a speed/safetly tradeoff. Anonimatron is typically used with large datasets, where processing the dataset takes 5 | time. A SecureRandom generator would add a performance hit, while adding only little to no extra security. The reason for 6 | this is that the random generators are not used for cryptographic functions. Most anonymizers generate a new, unrelated 7 | piece of data which is typically not (entirely) based on the input. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "07:00" 8 | open-pull-requests-limit: 10 9 | target-branch: develop 10 | ignore: 11 | - dependency-name: com.microsoft.sqlserver:mssql-jdbc 12 | versions: 13 | - "> 7.2.2.jre8, < 8" 14 | - dependency-name: com.microsoft.sqlserver:mssql-jdbc 15 | versions: 16 | - 9.1.1.jre15-preview 17 | - 9.2.0.jre15 18 | - 9.2.1.jre15 19 | - 9.3.0.jre15-preview 20 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Anonimatron 2 | description: "The free, extendable, open source data anonymization tool." 3 | google_analytics: 4 | show_downloads: true 5 | # https://github.com/pages-themes/cayman 6 | theme: jekyll-theme-cayman 7 | exclude: [ruby, Gemfile, Gemfile.lock, serve_local_jekyll_site.sh] 8 | 9 | github: 10 | zip_url: https://github.com/realrolfje/anonimatron/releases/latest 11 | repository_url: https://github.com/realrolfje/anonimatron/tree/master 12 | documentation_url: /anonimatron/documentation 13 | 14 | plugins: 15 | - jekyll-mentions 16 | 17 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/file/RecordTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import junit.framework.TestCase; 4 | 5 | public class RecordTest extends TestCase { 6 | 7 | public void testToString() { 8 | String[] names = {"name1", "name2", "name3"}; 9 | Object[] values = {"value1", null, new Object()}; 10 | 11 | Record record = new Record(names, values); 12 | 13 | String toString = record.toString(); 14 | System.out.println(toString); 15 | 16 | assertTrue(toString.startsWith("[Record: name1:'value1', name2:null, name3:'java.lang.Object")); 17 | assertTrue(toString.endsWith("']")); 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/synonyms/SynonymTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import junit.framework.TestCase; 4 | 5 | public class SynonymTest extends TestCase { 6 | 7 | public void testEqualsHashcode() { 8 | 9 | StringSynonym a = new StringSynonym( 10 | "Bunk", 11 | "Fpp", 12 | "Bar", 13 | false 14 | ); 15 | 16 | StringSynonym b = new StringSynonym( 17 | "Bunk", 18 | "Fpp", 19 | "Bar", 20 | false 21 | ); 22 | 23 | assertNotSame(a, b); 24 | assertEquals(a, b); 25 | 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /resources/integration/mysql/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 |
17 |
-------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/ToLowerAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | public class ToLowerAnonymizer implements Anonymizer { 7 | 8 | @Override 9 | public String getType() { 10 | return "TO_LOWER_CASE"; 11 | } 12 | 13 | @Override 14 | public Synonym anonymize(Object from, int size, boolean shortlived) { 15 | return new StringSynonym( 16 | getType(), 17 | (String) from, 18 | ((String) from).toLowerCase(), 19 | shortlived 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/BankAccountAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | /** 4 | * An {@linkplain Anonymizer} for Bank Accounts. 5 | * 6 | * @author Erik-Berndt Scheper 7 | */ 8 | interface BankAccountAnonymizer extends Anonymizer { 9 | 10 | /** 11 | * Generate a bank account using the given number of digits. 12 | * 13 | * @param numberOfDigits the number of digits in the account number. 14 | * @return the bank account 15 | */ 16 | String generateBankAccount(int numberOfDigits); 17 | 18 | /** 19 | * Generate a valid bank code. 20 | * 21 | * @return a valid bank code 22 | */ 23 | String generateBankCode(); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/progress/ProgressPrinterTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.progress; 2 | 3 | import junit.framework.TestCase; 4 | 5 | public class ProgressPrinterTest extends TestCase { 6 | 7 | public void testPrintProgress() throws Exception { 8 | 9 | Progress p = new Progress(); 10 | ProgressPrinter printer = new ProgressPrinter(p); 11 | printer.setPrintIntervalMillis(10); 12 | 13 | p.setTotalitemstodo(10000); 14 | p.setTotalitemscompleted(0); 15 | 16 | printer.start(); 17 | 18 | Thread.sleep(30); 19 | p.incItemsCompleted(5000); 20 | 21 | Thread.sleep(30); 22 | p.incItemsCompleted(3000); 23 | 24 | Thread.sleep(30); 25 | p.incItemsCompleted(2000); 26 | 27 | Thread.sleep(30); 28 | 29 | printer.stop(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/ElvenNameGenerator.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import java.io.IOException; 4 | import java.util.Random; 5 | 6 | public class ElvenNameGenerator extends AbstractNameGenerator { 7 | private static final String TYPE = "ELVEN_NAME"; 8 | private static final String SYLABLE_FILE = "elven-names.syl"; 9 | 10 | private static final Random r = new Random(); 11 | 12 | public ElvenNameGenerator() throws IOException { 13 | super(SYLABLE_FILE); 14 | } 15 | 16 | @Override 17 | public String getType() { 18 | return TYPE; 19 | } 20 | 21 | @Override 22 | String getName() { 23 | // Generate an Elven name of 2 to 5 sylables 24 | // This will result well over 1 million unique names. 25 | int syls = Math.round((r.nextFloat() * 5) + 2); 26 | return compose(syls); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/RomanNameGenerator.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import java.io.IOException; 4 | import java.util.Random; 5 | 6 | public class RomanNameGenerator extends AbstractNameGenerator { 7 | private static final String TYPE = "ROMAN_NAME"; 8 | private static final String SYLABLE_FILE = "roman-names.syl"; 9 | 10 | private static final Random r = new Random(); 11 | 12 | @Override 13 | public String getType() { 14 | return TYPE; 15 | } 16 | 17 | public RomanNameGenerator() throws IOException { 18 | super(SYLABLE_FILE); 19 | } 20 | 21 | @Override 22 | String getName() { 23 | // Generate a Roman name of 2 to 5 sylables 24 | // This will result in approx. 897046 unique names. 25 | int syls = Math.round((r.nextFloat() * 5) + 2); 26 | return compose(syls); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/elven-names.syl: -------------------------------------------------------------------------------- 1 | -Ael 2 | -Aer 3 | -af 4 | -ah 5 | -am 6 | -ama 7 | -an 8 | -ang +v 9 | -ansr +v 10 | -cael 11 | -dae +c 12 | -dho 13 | -eir 14 | -fi 15 | -fir 16 | -la 17 | -seh 18 | -sel 19 | -ev 20 | -fis 21 | -hu 22 | -ha 23 | -gar 24 | -gil 25 | -ka 26 | -kan 27 | -ya 28 | -za 29 | -zy 30 | -mara 31 | -mai +c 32 | -lue +c 33 | -ny 34 | -she 35 | -sum 36 | -syl 37 | ae +c -c 38 | ael -c 39 | dar 40 | deth +v 41 | dre -v 42 | drim -v 43 | dul 44 | ean -c 45 | el 46 | emar 47 | hal 48 | iat -c 49 | mah 50 | ten 51 | que -v +c 52 | ria 53 | rail 54 | ther 55 | thus 56 | thi 57 | san 58 | +ael -c 59 | +dar 60 | +deth 61 | +dre 62 | +drim 63 | +dul 64 | +ean -c 65 | +el 66 | +emar 67 | +nes 68 | +nin 69 | +oth 70 | +hal 71 | +iat 72 | +mah 73 | +ten 74 | +ther 75 | +thus 76 | +thi 77 | +ran 78 | +ath 79 | +ess 80 | +san 81 | +yth 82 | +las 83 | +lian 84 | +evar -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/NullSynonym.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | 4 | /** 5 | * Represents a transient synonym for a null value of any type. 6 | * 7 | */ 8 | public class NullSynonym implements Synonym { 9 | private final String type; 10 | 11 | public NullSynonym(String type) { 12 | this.type=type; 13 | } 14 | 15 | public String getType() { 16 | return type; 17 | } 18 | 19 | public Object getFrom() { 20 | return null; 21 | } 22 | 23 | public Object getTo() { 24 | return null; 25 | } 26 | 27 | @Override 28 | public boolean isShortLived() { 29 | return true; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object obj) { 34 | return (obj != null) && (this.getClass() == obj.getClass()) && (this.hashCode() == obj.hashCode()); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return type.hashCode(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/file/CsvFileWriterTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import junit.framework.TestCase; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.File; 7 | import java.io.FileReader; 8 | 9 | public class CsvFileWriterTest extends TestCase { 10 | public void testWrite() throws Exception { 11 | File tempFile = File.createTempFile(CsvFileWriter.class.getSimpleName(), ".csv"); 12 | assertTrue("Could not delete " + tempFile, tempFile.delete()); 13 | 14 | CsvFileWriter csvFileWriter = new CsvFileWriter(tempFile); 15 | csvFileWriter.write(new Record( 16 | new String[]{"name1", "name2"}, 17 | new String[]{"value1", "value2"} 18 | )); 19 | csvFileWriter.close(); 20 | 21 | BufferedReader bufferedReader = new BufferedReader(new FileReader(tempFile)); 22 | String line = bufferedReader.readLine(); 23 | assertEquals("value1,value2", line); 24 | } 25 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Create a database with the following contents '...' 16 | 2. Configure Anonimatron as follows '...' 17 | 3. Start with the following command '...' 18 | 4. See error in log: [...] 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Logs, screenshots** 24 | If applicable, add logs or screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS 10.5.2, Windows 10] 28 | - Java version [e.g. OpenJDK 1.8, Oracle Java 1.10] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anonimatron [![Maven version](https://img.shields.io/maven-central/v/com.rolfje.anonimatron/anonimatron.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.rolfje.anonimatron%22%20AND%20a:%22anonimatron%22) [![Build Status](https://travis-ci.org/realrolfje/anonimatron.svg?branch=master)](https://travis-ci.org/realrolfje/anonimatron) 2 | 3 | Did you ever have that problem where you needed "production data" to find a bug 4 | or do performance tests outside of the client’s production environment? Are 5 | you worried about protecting that data? Do you add screenshots to your bug 6 | reports? Can you live with surrogate data with the same properties? Then this 7 | tool is for you. 8 | 9 | Running Anonimatron is as simple as downloading, unzipping and running 10 | `anonimatron.sh` or `anonimatron.bat`. 11 | 12 | Please find all documentation, downloads and quickstarts at the 13 | [Anonimatron official homepage](https://realrolfje.github.io/anonimatron/). 14 | 15 | -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | h1 { font-size: 1.5em; margin: 0.67em 0; } 7 | 8 | .btn.github { 9 | background-image: url(octocat-icon.png); 10 | background-repeat: no-repeat; 11 | background-position: 10px; 12 | padding-left: 44px; 13 | } 14 | 15 | .btn.zip { 16 | background-image: url(zip-icon.png); 17 | background-repeat: no-repeat; 18 | background-position: 10px; 19 | padding-left: 47px; 20 | } 21 | 22 | .btn.documentation { 23 | background-image: url(book-flat-30.png); 24 | background-repeat: no-repeat; 25 | background-position: 10px; 26 | padding-left: 47px; 27 | } 28 | 29 | .logo { 30 | background-image: url(anonimatron-logo-a.png); 31 | background-repeat: no-repeat; 32 | background-position: top right; 33 | background-size: 100px; 34 | background-clip: border-box; 35 | padding-right: 100px; 36 | } 37 | 38 | .page-header { 39 | padding: 1rem 2rem; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/configuration/Discriminator.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.configuration; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Provides a way to apply anonymization of certain columns based on the value 7 | * of another column. Discriminators override the default column confgurations 8 | * in case of collision. 9 | * 10 | * @author rolf 11 | */ 12 | public class Discriminator { 13 | String columnName; 14 | String value; 15 | List columns; 16 | 17 | public List getColumns() { 18 | return columns; 19 | } 20 | 21 | public void setColumns(List columns) { 22 | this.columns = columns; 23 | } 24 | 25 | public String getColumnName() { 26 | return columnName; 27 | } 28 | 29 | public void setColumnName(String columnName) { 30 | this.columnName = columnName; 31 | } 32 | 33 | public String getValue() { 34 | return value; 35 | } 36 | 37 | public void setValue(String value) { 38 | this.value = value; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/integration/mysql/README.md: -------------------------------------------------------------------------------- 1 | # MySQL test scripts 2 | 3 | This directory contains scripts for manually testing against an ms-sql or 4 | sql server database. It makes use of a MySQL server running inside a 5 | docker container. 6 | 7 | ## Docker commands 8 | 9 | Start mysql container in docker 10 | ```shell 11 | docker run --name anonimatron-mysql \ 12 | -p3306:3306 \ 13 | -e MYSQL_ROOT_PASSWORD=anonimatron \ 14 | -d mysql:8 15 | ``` 16 | 17 | Start the mysql command line tool in the started container (the password is listed in the line above): 18 | 19 | ```shell 20 | docker exec -it anonimatron-mysql mysql -uroot -p 21 | ``` 22 | 23 | ## Create database and tables 24 | 25 | Create a test database as described at https://realrolfje.github.io/anonimatron/documentation/ 26 | 27 | ## Configure and run Anonimatron 28 | 29 | Run anonimatron with the [config.xml](config.xml) configuration file. 30 | A run configuration in IntelliJ may be better for debugging and analysis. 31 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/synonyms/Base64StringFieldHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import junit.framework.TestCase; 4 | 5 | public class Base64StringFieldHandlerTest extends TestCase { 6 | 7 | Base64StringFieldHandler handler = new Base64StringFieldHandler(); 8 | 9 | public void testStringHandling() { 10 | testConversion("\t0123456789 The quick brown fox jumped over the lazy dog.\n\r"); 11 | testConversion(String.valueOf(SynonymMapperTest.ILLEGALSTRINGCHARACTERS)); 12 | testConversion(null); 13 | testConversion(""); 14 | testConversion(" "); 15 | } 16 | 17 | private void testConversion(String testString) { 18 | Object convertedToBase64 = handler.convertUponGet(testString); 19 | Object convertedToString = handler.convertUponSet(convertedToBase64); 20 | assertEquals(testString, convertedToString); 21 | } 22 | 23 | public void testNull() { 24 | assertNull(handler.convertUponGet(null)); 25 | assertNull(handler.convertUponSet(null)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/LoremIpsumTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron; 2 | 3 | import java.util.StringTokenizer; 4 | 5 | import junit.framework.TestCase; 6 | 7 | import static org.junit.Assert.assertNotEquals; 8 | 9 | public class LoremIpsumTest extends TestCase { 10 | 11 | public void testGetParagraphs() { 12 | String test = LoremIpsum.getParagraphs(3); 13 | 14 | assertFalse(test.startsWith(" ")); 15 | assertFalse(test.startsWith("\n")); 16 | assertTrue(test.contains(" ")); 17 | 18 | StringTokenizer t = new StringTokenizer(test, "\n"); 19 | assertEquals(3, t.countTokens()); 20 | } 21 | 22 | public void testGetWords() { 23 | String testText = LoremIpsum.getWords(50); 24 | StringTokenizer tokenizer = new StringTokenizer(testText); 25 | assertEquals(50, tokenizer.countTokens()); 26 | 27 | String testText2 = LoremIpsum.getWords(50); 28 | StringTokenizer tokenizer2 = new StringTokenizer(testText2); 29 | assertEquals(50, tokenizer2.countTokens()); 30 | 31 | assertNotEquals(testText, testText2); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/commandline/CommandLineTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.commandline; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertTrue; 7 | 8 | /** 9 | * @author schrader 10 | */ 11 | public class CommandLineTest { 12 | 13 | @Test 14 | public void parse_With_Options() throws Exception { 15 | String[] args = { 16 | "-config", "/Volumne/Data/config.xml", 17 | "-jdbcurl", "jdbc:postgresql://localhost:5433/", 18 | "-password", "lorem", 19 | "-userid", "ipsum", 20 | "-configexample", 21 | "-dryrun", 22 | }; 23 | 24 | CommandLine cmdl = new CommandLine(args); 25 | assertEquals("jdbc:postgresql://localhost:5433/", cmdl.getJdbcurl()); 26 | assertEquals("lorem", cmdl.getPassword()); 27 | assertEquals("ipsum", cmdl.getUserid()); 28 | assertTrue(cmdl.isConfigExample()); 29 | assertTrue(cmdl.isDryrun()); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/HashedFromSynonym.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import com.rolfje.anonimatron.anonymizer.Hasher; 4 | 5 | public class HashedFromSynonym implements Synonym { 6 | 7 | private String from; 8 | private Object to; 9 | private String type; 10 | private boolean shortLived = false; 11 | 12 | public HashedFromSynonym() { 13 | } 14 | 15 | public HashedFromSynonym(Hasher hasher, Synonym synonym) { 16 | from = hasher.base64Hash(synonym.getFrom()); 17 | to = synonym.getTo(); 18 | type = synonym.getType(); 19 | } 20 | 21 | @Override 22 | public String getType() { 23 | return type; 24 | } 25 | 26 | @Override 27 | public Object getFrom() { 28 | return from; 29 | } 30 | 31 | @Override 32 | public Object getTo() { 33 | return to; 34 | } 35 | 36 | @Override 37 | public boolean isShortLived() { 38 | return shortLived; 39 | } 40 | 41 | public void setFrom(String from) { 42 | this.from = from; 43 | } 44 | 45 | public void setTo(Object to) { 46 | this.to = to; 47 | } 48 | 49 | public void setType(String type) { 50 | this.type = type; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 www.rolfje.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/FixedValueAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.rolfje.anonimatron.synonyms.StringSynonym; 7 | import com.rolfje.anonimatron.synonyms.Synonym; 8 | 9 | public class FixedValueAnonymizer implements Anonymizer { 10 | private static final String TYPE = "FIXED"; 11 | 12 | @Override 13 | public Synonym anonymize(Object from, int size, boolean shortlived) { 14 | return anonymize(from, size, shortlived, new HashMap<>()); 15 | } 16 | 17 | @Override 18 | public Synonym anonymize(Object from, int size, boolean shortlived, Map parameters) { 19 | if (parameters == null || !parameters.containsKey("value")) { 20 | throw new UnsupportedOperationException("no value"); 21 | } 22 | return new StringSynonym(getType(), 23 | (String) from, 24 | parameters.get("value"), 25 | shortlived); 26 | } 27 | 28 | @Override 29 | public String getType() { 30 | return TYPE; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/StringDateFieldHandler.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import org.exolab.castor.mapping.GeneralizedFieldHandler; 4 | 5 | import java.sql.Date; 6 | 7 | /** 8 | * Converts date to epoch String representation. 9 | */ 10 | public class StringDateFieldHandler extends GeneralizedFieldHandler { 11 | 12 | /** 13 | * @param objectValue the value fetched from the Java object 14 | * @return the converted XML value. 15 | */ 16 | @Override 17 | public Object convertUponGet(Object objectValue) { 18 | if (objectValue == null || objectValue.equals("")) { 19 | return objectValue; 20 | } 21 | 22 | long epoch = ((Date) objectValue).getTime(); 23 | return String.valueOf(epoch); 24 | } 25 | 26 | /** 27 | * @param xmlValue the value fetched from XML 28 | * @return the value to set in the Java object. 29 | */ 30 | @Override 31 | public Object convertUponSet(Object xmlValue) { 32 | if (xmlValue == null || xmlValue.equals("")) { 33 | return xmlValue; 34 | } 35 | 36 | long epoch = Long.valueOf((String) xmlValue).longValue(); 37 | return new Date(epoch); 38 | } 39 | 40 | @SuppressWarnings("rawtypes") 41 | @Override 42 | public Class getFieldType() { 43 | return String.class; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/EmailAddressAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | public class EmailAddressAnonymizer implements Anonymizer { 7 | private static final String EMAIL_DOMAIN = "@example.com"; 8 | private static final String TYPE = "EMAIL_ADDRESS"; 9 | 10 | @Override 11 | public Synonym anonymize(Object from, int size, boolean shortlived) { 12 | String randomHexString = null; 13 | if (from instanceof String) { 14 | randomHexString = Long.toHexString(Double 15 | .doubleToLongBits(Math.random())); 16 | randomHexString += EMAIL_DOMAIN; 17 | 18 | if (randomHexString.length() > size) { 19 | throw new UnsupportedOperationException( 20 | "Can not generate email address with length " + size 21 | + "."); 22 | } 23 | 24 | } else { 25 | throw new UnsupportedOperationException( 26 | "Can not anonymize objects of type " + from.getClass()); 27 | } 28 | 29 | return new StringSynonym( 30 | getType(), 31 | (String) from, 32 | randomHexString, 33 | shortlived 34 | ); 35 | } 36 | 37 | @Override 38 | public String getType() { 39 | return TYPE; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/file/CsvFileWriter.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | 8 | public class CsvFileWriter implements RecordWriter { 9 | 10 | private BufferedWriter writer; 11 | private File file; 12 | 13 | public CsvFileWriter(String fileName) throws IOException { 14 | this(new File(fileName)); 15 | } 16 | 17 | public CsvFileWriter(File file) throws IOException { 18 | this.file = file; 19 | writer = new BufferedWriter(new FileWriter(file)); 20 | } 21 | 22 | @Override 23 | public void write(Record record) { 24 | StringBuilder line = new StringBuilder(); 25 | 26 | Object[] values = record.getValues(); 27 | for (int i = 0; i < values.length; i++) { 28 | String value = values[i].toString(); 29 | line.append(value); 30 | if (i < values.length - 1) { 31 | line.append(","); 32 | } 33 | } 34 | 35 | try { 36 | writer.write(line.toString() + "\n"); 37 | } catch (IOException e) { 38 | throw new RuntimeException("Problem writing file " + file.getAbsolutePath() + ".", e); 39 | } 40 | 41 | } 42 | 43 | @Override 44 | public void close() throws IOException { 45 | writer.close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/javamonitor/tools/ThreadLocalStopwatchNano.java: -------------------------------------------------------------------------------- 1 | package com.javamonitor.tools; 2 | 3 | import org.apache.log4j.Level; 4 | 5 | /** 6 | * Provides a {@link ThreadLocal} implementation of {@link StopwatchNano} to facilitate cross-object 7 | * timing without passing Stopwatch as an argument. 8 | *

9 | * Example: 10 | * ThreadLocalStopwatchNano.init("work on thread 1"); 11 | * ThreadLocalStopwatchNano.aboutTo("do other work"); 12 | * ThreadLocalStopwatchNano.aboutTo("finish up"); 13 | * ThreadLocalStopwatchNano.stop(1000); 14 | *

15 | * The example above logs a Warning when the work in the current thread took more than 1000 milliseconds, and will 16 | * seperate stopwatches between threads. Running the same code in parallel results in two warnings in the log. 17 | * 18 | */ 19 | public class ThreadLocalStopwatchNano { 20 | private static ThreadLocal stopwatch; 21 | 22 | public static void init(String name) { 23 | stopwatch.set(new StopwatchNano(name)); 24 | } 25 | 26 | public static void aboutTo(String operation) { 27 | stopwatch.get().aboutTo(operation); 28 | } 29 | 30 | public static void stop(final long thresholdMillis) { 31 | stopwatch.get().stop(thresholdMillis); 32 | } 33 | 34 | public static void setLoglevel(Level loglevel) { 35 | stopwatch.get().setLoglevel(loglevel); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/file/CsvFileReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import junit.framework.TestCase; 4 | 5 | import java.io.*; 6 | 7 | public class CsvFileReaderTest extends TestCase { 8 | 9 | public void testHappy() throws IOException { 10 | File tempFile = File.createTempFile(CsvFileReaderTest.class.getSimpleName(), ".csv"); 11 | Writer writer = new BufferedWriter(new FileWriter(tempFile)); 12 | writer.write("\"Testfield1\";\"Testfield2\""); 13 | writer.close(); 14 | 15 | CsvFileReader csvFileReader = new CsvFileReader(tempFile); 16 | assertTrue(csvFileReader.hasRecords()); 17 | Record read = csvFileReader.read(); 18 | 19 | assertEquals("Testfield1", read.getValues()[0]); 20 | assertEquals("Testfield2", read.getValues()[1]); 21 | } 22 | 23 | public void testNoQuotes() throws IOException { 24 | File tempFile = File.createTempFile(CsvFileReaderTest.class.getSimpleName(), ".csv"); 25 | Writer writer = new BufferedWriter(new FileWriter(tempFile)); 26 | writer.write("Testfield1;Testfield2"); 27 | writer.close(); 28 | 29 | CsvFileReader csvFileReader = new CsvFileReader(tempFile); 30 | assertTrue(csvFileReader.hasRecords()); 31 | Record read = csvFileReader.read(); 32 | 33 | assertEquals("Testfield1", read.getValues()[0]); 34 | assertEquals("Testfield2", read.getValues()[1]); 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/CharacterStringAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | public class CharacterStringAnonymizerTest { 11 | 12 | CharacterStringAnonymizer characterStrAnon = new CharacterStringAnonymizer(); 13 | 14 | @Test 15 | public void testIncorrectParamter() { 16 | try { 17 | characterStrAnon.anonymize("dummy", 0, false, new HashMap() {{ 18 | put("PaRaMeTeR", "any"); 19 | }}); 20 | fail("Should fail with unsupported operation exception."); 21 | } catch (UnsupportedOperationException e) { 22 | assertEquals( 23 | "Please provide '" + CharacterStringAnonymizer.PARAMETER + "' as configuration parameter.", 24 | e.getMessage()); 25 | } 26 | } 27 | 28 | @Test 29 | public void testCorrectParameter() { 30 | Synonym anonymize = characterStrAnon.anonymize("dummy", 0, false, new HashMap() {{ 31 | put(CharacterStringAnonymizer.PARAMETER, "any"); 32 | }}); 33 | 34 | // Actual anonymization tests done elsewhere 35 | assertNotNull(anonymize); 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/AbstractElevenProofAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.util.Map; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | public class AbstractElevenProofAnonymizerTest { 12 | 13 | AbstractElevenProofAnonymizer testdummy; 14 | 15 | @Before 16 | public void setUp() { 17 | testdummy = new AbstractElevenProofAnonymizer() { 18 | 19 | @Override 20 | public String getType() { 21 | return null; 22 | } 23 | 24 | @Override 25 | public Synonym anonymize(Object from, int size, boolean shortlived) { 26 | return null; 27 | } 28 | 29 | @Override 30 | public Synonym anonymize(Object from, int size, boolean shortlived, Map parameters) { 31 | return null; 32 | } 33 | }; 34 | } 35 | 36 | @Test 37 | public void digitsAsInteger() { 38 | assertEquals(10, testdummy.digitsAsInteger(new int[]{1,0})); 39 | assertEquals(1234, testdummy.digitsAsInteger(new int[]{1,2,3,4})); 40 | assertEquals(1, testdummy.digitsAsInteger(new int[]{0,0,0,1})); 41 | assertEquals(0, testdummy.digitsAsInteger(new int[]{})); 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/UUIDAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | import java.util.UUID; 7 | 8 | public class UUIDAnonymizer implements Anonymizer { 9 | private static final String TYPE = "UUID"; 10 | 11 | @Override 12 | public Synonym anonymize(Object from, int size, boolean shortlived) { 13 | String to = getString(from, size); 14 | return new StringSynonym( 15 | getType(), 16 | (String) from, 17 | to, 18 | shortlived 19 | ); 20 | } 21 | 22 | private String getString(Object from, int size) { 23 | if (from == null) { 24 | return null; 25 | } 26 | 27 | if (from instanceof String) { 28 | String to = UUID.randomUUID().toString(); 29 | 30 | if (to.length() > size) { 31 | throw new UnsupportedOperationException( 32 | "Can not generate a UUID smaller than " + size + " characters."); 33 | } 34 | return to; 35 | 36 | } 37 | 38 | throw new UnsupportedOperationException("Can not anonymize objects of type " + from.getClass()); 39 | } 40 | 41 | @Override 42 | public String getType() { 43 | return TYPE; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/StringAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertFalse; 9 | import static org.junit.Assert.assertNotEquals; 10 | import static org.junit.Assert.assertNull; 11 | 12 | public class StringAnonymizerTest { 13 | 14 | private StringAnonymizer stringAnonymizer; 15 | 16 | @Before 17 | public void setUp() { 18 | stringAnonymizer = new StringAnonymizer(); 19 | } 20 | 21 | @Test 22 | public void testHappyFlow() { 23 | Synonym synonym = stringAnonymizer.anonymize("FROM", Integer.MAX_VALUE, false); 24 | 25 | assertNotEquals(synonym.getFrom(), synonym.getTo()); 26 | assertEquals(stringAnonymizer.getType(), synonym.getType()); 27 | assertFalse(synonym.isShortLived()); 28 | } 29 | 30 | @Test 31 | public void testNullInput() { 32 | Synonym synonym = stringAnonymizer.anonymize(null, Integer.MAX_VALUE, true); 33 | assertNull(synonym.getFrom()); 34 | assertNull(synonym.getTo()); 35 | } 36 | 37 | @Test(expected = UnsupportedOperationException.class) 38 | public void testIncorrectInputType() { 39 | stringAnonymizer.anonymize(new Long(0), Integer.MAX_VALUE, true); 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/DateSynonym.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | 4 | import java.sql.Date; 5 | 6 | /** 7 | * Represents a synonym for a {@link java.sql.Date}. 8 | */ 9 | public class DateSynonym implements Synonym { 10 | private String type; 11 | private Date from; 12 | private Date to; 13 | private boolean shortlived = false; 14 | 15 | public String getType() { 16 | return type; 17 | } 18 | 19 | public Object getFrom() { 20 | return from; 21 | } 22 | 23 | public Object getTo() { 24 | return to; 25 | } 26 | 27 | public void setType(String type) { 28 | this.type = type; 29 | } 30 | 31 | public void setTo(Object to) { 32 | this.to = (Date) to; 33 | } 34 | 35 | public void setFrom(Object from) { 36 | this.from = (Date) from; 37 | } 38 | 39 | public void setShortlived(boolean shortlived) { 40 | this.shortlived = shortlived; 41 | } 42 | 43 | @Override 44 | public boolean isShortLived() { 45 | return shortlived; 46 | } 47 | 48 | @Override 49 | public boolean equals(Object obj) { 50 | return (obj != null) && (this.getClass() == obj.getClass()) && (this.hashCode() == obj.hashCode()); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return from.hashCode() + to.hashCode() + type.hashCode(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/file/Record.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | public class Record { 4 | private final String[] names; 5 | private final Object[] values; 6 | 7 | public Record(String[] names, Object[] values) { 8 | if (names.length != values.length) { 9 | throw new IllegalArgumentException("Argument Arrays need to be the same size."); 10 | } 11 | this.names = names; 12 | this.values = values; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | StringBuilder stringBuilder = new StringBuilder("[Record: "); 18 | if (names != null) { 19 | for (int i = 0; i < names.length; i++) { 20 | stringBuilder.append(names[i]); 21 | stringBuilder.append(":"); 22 | 23 | if (values[i] != null) { 24 | stringBuilder.append("'" + (values[i].toString()) + "'"); 25 | } else { 26 | stringBuilder.append("null"); 27 | } 28 | 29 | if (i < names.length - 1) { 30 | stringBuilder.append(", "); 31 | } 32 | 33 | } 34 | } 35 | 36 | stringBuilder.append("]"); 37 | return stringBuilder.toString(); 38 | } 39 | 40 | public String[] getNames() { 41 | return names; 42 | } 43 | 44 | public Object[] getValues() { 45 | return values; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/DutchZipCodeAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import org.junit.Test; 5 | 6 | import java.util.regex.Pattern; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | /** 11 | * Tests for {@link DutchZipCodeAnonymizer}. 12 | * 13 | * @author Erik-Berndt Scheper 14 | */ 15 | public class DutchZipCodeAnonymizerTest { 16 | 17 | private DutchZipCodeAnonymizer anonymizer = new DutchZipCodeAnonymizer(); 18 | private Pattern pattern = Pattern.compile("[1-9][0-9]{3} ?(?!SA|SD|SS)[A-Z]{2}$"); 19 | 20 | @Test 21 | public void anonymize() { 22 | for (int i = 0; i < 1000000; i++) { 23 | String from = anonymizer.buildZipCode(); 24 | assertTrue(isValidZipCode(from)); 25 | assertEquals(6, from.length()); 26 | 27 | internalAnonymize(6, from); 28 | } 29 | } 30 | 31 | private void internalAnonymize(int size, String from) { 32 | Synonym synonym = anonymizer.anonymize(from, size, false); 33 | assertEquals(anonymizer.getType(), synonym.getType()); 34 | 35 | String value = (String) synonym.getTo(); 36 | assertTrue(isValidZipCode(value)); 37 | assertFalse(synonym.isShortLived()); 38 | } 39 | 40 | private boolean isValidZipCode(String value) { 41 | return pattern.matcher(value).matches(); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/Synonym.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import com.rolfje.anonimatron.anonymizer.AnonymizerService; 4 | 5 | /** 6 | * Provides a way to connect source and target data, and identify the type of 7 | * data. Usually the data type is something more semantic, like "NAME" or 8 | * "STREET". Synonyms are produced by the {@link AnonymizerService}. 9 | * 10 | */ 11 | public interface Synonym { 12 | 13 | /** 14 | * Indicates if this Synonym is short-lived or transient, meaning that 15 | * it should not be stored in the synonyms file. Transient Synonyms 16 | * are not stored, need to be calculated each run time. 17 | * 18 | * Transient synonyms are: 19 | * 20 | *

    21 | *
  1. Not stored in memory.
  2. 22 | *
  3. Not stored in the synonym file.
  4. 23 | *
24 | * 25 | * @return true if the synonym should be thrown away 26 | * after use (not stored in the synonym file). 27 | */ 28 | boolean isShortLived(); 29 | 30 | /** 31 | * @return The semantic data type of this Synonym, usually something 32 | * descriptive as "NAME" or "STREET". 33 | */ 34 | String getType(); 35 | 36 | /** 37 | * @return The data which was in the original database for this Synonym 38 | */ 39 | Object getFrom(); 40 | 41 | /** 42 | * 43 | * @return The data with which the original data in the database will be or 44 | * is replaced when the Anonymizer runs. 45 | */ 46 | Object getTo(); 47 | } -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/DateAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.DateSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | import java.sql.Date; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | class DateAnonymizer implements Anonymizer { 11 | private static final String TYPE = "DATE"; 12 | private static final long RANDOMIZATION_MILLIS = 1000L * 60L * 60L * 24L * 31L; 13 | 14 | private Set generatedDates = new HashSet<>(); 15 | 16 | @Override 17 | public Synonym anonymize(Object from, int size, boolean shortlived) { 18 | DateSynonym s = new DateSynonym(); 19 | s.setType(TYPE); 20 | s.setShortlived(shortlived); 21 | 22 | if (from == null) { 23 | s.setFrom(null); 24 | s.setTo(null); 25 | } else if (from instanceof Date) { 26 | s.setFrom(from); 27 | 28 | long originalepoch = ((Date) from).getTime(); 29 | 30 | Date newDate; 31 | do { 32 | long deviation = Math.round(2 * RANDOMIZATION_MILLIS 33 | * Math.random()) 34 | - RANDOMIZATION_MILLIS; 35 | newDate = new Date(originalepoch + deviation); 36 | } while (generatedDates.contains(newDate)); 37 | 38 | generatedDates.add(newDate); 39 | s.setTo(newDate); 40 | } else { 41 | throw new UnsupportedOperationException( 42 | "Can not anonymize objects of type " + from.getClass()); 43 | } 44 | 45 | return s; 46 | } 47 | 48 | @Override 49 | public String getType() { 50 | return TYPE; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/StringAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | public class StringAnonymizer implements Anonymizer { 7 | private static final String TYPE = "STRING"; 8 | 9 | @Override 10 | public Synonym anonymize(Object from, int size, boolean shortlived) { 11 | String randomHexString = getString(from, size); 12 | 13 | return new StringSynonym( 14 | getType(), 15 | (String) from, 16 | randomHexString, 17 | shortlived 18 | ); 19 | } 20 | 21 | private String getString(Object from, int size) { 22 | if (from == null) { 23 | return null; 24 | } 25 | 26 | if (from instanceof String) { 27 | String randomHexString = Long.toHexString(Double.doubleToLongBits(Math.random())); 28 | 29 | if (randomHexString.length() > size) { 30 | throw new UnsupportedOperationException("Can not generate a random hex string with length " + size 31 | + ". Generated String size is " + randomHexString.length() + " characters."); 32 | } 33 | return randomHexString; 34 | } 35 | 36 | throw new UnsupportedOperationException("Can not anonymize objects of type " + from.getClass()); 37 | } 38 | 39 | @Override 40 | public String getType() { 41 | return TYPE; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/jdbc/ColumnWorker.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.jdbc; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | import com.rolfje.anonimatron.configuration.Column; 7 | 8 | /** 9 | * Provides functionality for processing a single column value. By implementing 10 | * this interface, we can provide different behavior to the loop which runs 11 | * through all table/column values. This enables us to do a two-pass on the same 12 | * table with the same code, only replacing the implementation of this 13 | * interface. 14 | * 15 | * @author rolf 16 | */ 17 | public interface ColumnWorker { 18 | 19 | /** 20 | * @param results the resultset in which this worker runs. Can be used to 21 | * update the column value when anonymizing. 22 | * @param column The column which was fetched from the configuration. 23 | * @param databaseColumnValue The current value for this column. 24 | * this column. 25 | * @return true if the columnworker is ready to process the 26 | * next value after this one, or false if the column 27 | * worker does not want to see more data for this column. If all 28 | * workers report false for all columns in a row, the 29 | * AnonimizerService will stop processing the rest of the resultset. 30 | * @throws SQLException On database access or SQL errors. 31 | */ 32 | boolean processColumn(ResultSet results, Column column, 33 | Object databaseColumnValue) throws SQLException; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/Base64StringFieldHandler.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import org.castor.core.util.Base64Decoder; 4 | import org.castor.core.util.Base64Encoder; 5 | import org.exolab.castor.mapping.GeneralizedFieldHandler; 6 | 7 | /** 8 | * Converts object values to a Base64 String and back, see 9 | * "Writing a GeneralizedFieldHandler" at 10 | * http://castor.org/xml-fieldhandlers.html 11 | */ 12 | public class Base64StringFieldHandler extends GeneralizedFieldHandler { 13 | 14 | /** 15 | * @param objectValue the value fetched from the Java object 16 | * @return the converted XML value. 17 | */ 18 | @Override 19 | public Object convertUponGet(Object objectValue) { 20 | if (objectValue == null || objectValue.equals("")) { 21 | return objectValue; 22 | } 23 | 24 | // TODO enforce encoding here. 25 | byte[] stringBytes = ((String)objectValue).getBytes(); 26 | char[] base64EncodedChars = Base64Encoder.encode(stringBytes); 27 | return String.copyValueOf(base64EncodedChars); 28 | } 29 | 30 | /** 31 | * @param xmlValue the value fetched from XML 32 | * @return the value to set in the Java object. 33 | */ 34 | @Override 35 | public Object convertUponSet(Object xmlValue) { 36 | if (xmlValue == null || xmlValue.equals("")) { 37 | return xmlValue; 38 | } 39 | 40 | byte[] decodedBytes = Base64Decoder.decode((String)xmlValue); 41 | return new String(decodedBytes); 42 | } 43 | 44 | @SuppressWarnings("rawtypes") 45 | @Override 46 | public Class getFieldType() { 47 | return String.class; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/NumberSynonym.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | /** 4 | * Represents a synonym for a {@link Number}. 5 | */ 6 | public class NumberSynonym implements Synonym { 7 | private String type; 8 | private Number from; 9 | private Number to; 10 | private boolean shortLived = false; 11 | 12 | public NumberSynonym() { 13 | } 14 | 15 | public NumberSynonym(String type, Number from, Number to, boolean shortLived) { 16 | this.type = type; 17 | this.from = from; 18 | this.to = to; 19 | this.shortLived = shortLived; 20 | } 21 | 22 | @Override 23 | public boolean isShortLived() { 24 | return shortLived; 25 | } 26 | 27 | @Override 28 | public String getType() { 29 | return type; 30 | } 31 | 32 | @Override 33 | public Number getFrom() { 34 | return from; 35 | } 36 | 37 | @Override 38 | public Number getTo() { 39 | return to; 40 | } 41 | 42 | 43 | public void setType(String type) { 44 | this.type = type; 45 | } 46 | 47 | public void setTo(Number to) { 48 | this.to = to; 49 | } 50 | 51 | public void setFrom(Number from) { 52 | this.from = from; 53 | } 54 | 55 | public void setShortLived(boolean shortLived) { 56 | this.shortLived = shortLived; 57 | } 58 | 59 | @Override 60 | public boolean equals(Object obj) { 61 | if (this == obj) 62 | return true; 63 | if (obj == null || getClass() != obj.getClass()) 64 | return false; 65 | return this.hashCode() == obj.hashCode(); 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | return from.hashCode() + to.hashCode() + type.hashCode(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/configuration/ConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.configuration; 2 | 3 | import java.util.Set; 4 | 5 | import junit.framework.TestCase; 6 | 7 | import com.rolfje.anonimatron.anonymizer.AnonymizerService; 8 | 9 | /** 10 | * Prints out an XML to see what the Castor mapping makes and expects. 11 | * 12 | */ 13 | public class ConfigurationTest extends TestCase { 14 | 15 | /** 16 | * Prints an example configuration 17 | * 18 | * @throws Exception 19 | */ 20 | public void testPrintMappedConfig() throws Exception { 21 | String demoxml = Configuration.getDemoConfiguration(); 22 | assertNotNull(demoxml); 23 | assertTrue(demoxml.length()>10); 24 | 25 | // For convenience and visual checking. 26 | System.out.println(demoxml); 27 | 28 | // See if all anonymizer types are represented in the demo xml 29 | AnonymizerService as = new AnonymizerService(); 30 | Set customtypes = as.getCustomAnonymizerTypes(); 31 | for (String type : customtypes) { 32 | assertTrue("Demo xml does not contain "+type,demoxml.indexOf("type=\""+type+"\"")>0); 33 | } 34 | 35 | Set defaulttypes = as.getDefaultAnonymizerTypes(); 36 | for (String type : defaulttypes) { 37 | assertTrue("Demo xml does not contain "+type,demoxml.indexOf(type.toUpperCase().replace('.','_'))>0); 38 | } 39 | 40 | assertTrue(demoxml.indexOf("A_SHORTLIVED_COLUMN") > 0); 41 | assertTrue(demoxml.indexOf("shortlived") > 0); 42 | 43 | // See if we have File records in the configuration. 44 | assertTrue(demoxml.contains(" size) { 35 | throw new UnsupportedOperationException( 36 | "Can not reliably generate a version 4 IP address smaller than " + size + " characters."); 37 | } 38 | return to; 39 | 40 | } 41 | 42 | throw new UnsupportedOperationException("Can not anonymize objects of type " + from.getClass()); 43 | } 44 | 45 | @Override 46 | public String getType() { 47 | return TYPE; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/CharacterStringPrefetchAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import org.apache.log4j.Logger; 4 | 5 | import com.rolfje.anonimatron.synonyms.Synonym; 6 | 7 | /** 8 | * Provides the same functionality as {@link CharacterStringAnonymizer}, but 9 | * uses the prefetch cycle to collect its output character set. This causes 10 | * the anonymized dataset to contain the same characters as used the input set, 11 | * enabling debugging/reproduction of strange character set issues. 12 | * 13 | * @author rolf 14 | */ 15 | public class CharacterStringPrefetchAnonymizer extends CharacterStringAnonymizer implements Prefetcher { 16 | private static final Logger LOG = Logger.getLogger(CharacterStringPrefetchAnonymizer.class); 17 | 18 | public CharacterStringPrefetchAnonymizer() { 19 | CHARS = ""; 20 | } 21 | 22 | @Override 23 | public void prefetch(Object sourceData) { 24 | if (sourceData == null) { 25 | return; 26 | } 27 | 28 | char[] sourcechars = sourceData.toString().toCharArray(); 29 | for (char c : sourcechars) { 30 | if (CHARS.indexOf(c) == -1) { 31 | CHARS += c; 32 | } 33 | } 34 | } 35 | 36 | @Override 37 | public Synonym anonymize(Object from, int size, boolean shortlived) { 38 | if (CHARS.length() < 1) { 39 | LOG.warn("No characters were collected during prefetch. Using the default set '" + getDefaultCharacterString() + "'."); 40 | CHARS = getDefaultCharacterString(); 41 | } 42 | return super.anonymize(from, size, shortlived); 43 | } 44 | 45 | @Override 46 | public String getType() { 47 | return "PREFETCHCHARACTERS"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /resources/documentation/README.md: -------------------------------------------------------------------------------- 1 | Welcome to Anonimatron. So you've downloaded this 2 | distribution to anonymize your data, now what? 3 | 4 | Installation 5 | ------------ 6 | Since you're reading this, we assume that you unpacked 7 | the release. That means you're done. There is no 8 | "installation" of anonimatron. It will run out of any folder, 9 | as long as you keep the folder structure in here identical 10 | so anonimatron can find it's JDBC drivers and jar files. 11 | 12 | Usage 13 | ----- 14 | Anonimatron is a command line tool, with a few command line 15 | options. Running "anonimatron.bat", or "./anonimatron.sh" will 16 | present you with a list of possible command line options. 17 | 18 | If you've never used anonimatron before, please check out 19 | the -configexample command line option, which will show 20 | you what drivers are supported, and prints out a demo XML 21 | file which should give you a few good pointers at how 22 | to set things up. All data types known by anonimatron 23 | at the time of running the command are automatically listed. 24 | 25 | Have fun playing with anonimatron, and don't forget to 26 | check the documentation, post bugs or request features at 27 | [https://github.com/realrolfje/anonimatron] 28 | 29 | Kind regards, 30 | The anonimatron team. 31 | 32 | 33 | FAQ 34 | === 35 | 36 | What's with the spelling? 37 | ------------------------- 38 | Anonimatron is deliberately mis-spelled, so as to be able to 39 | use it as a product- and brand name without colliding with 40 | other products. Within the software, we use American English 41 | so the correct spelling is: 42 | - anonymize 43 | - anonymizer 44 | - anonymous 45 | - anonymization 46 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/RomanNameGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import junit.framework.TestCase; 7 | 8 | import org.apache.log4j.Logger; 9 | 10 | public class RomanNameGeneratorTest extends TestCase { 11 | Logger LOG = Logger.getLogger(RomanNameGenerator.class); 12 | 13 | public void testUniqueness() throws Exception { 14 | RomanNameGenerator r = new RomanNameGenerator(); 15 | Set names = new HashSet<>(); 16 | 17 | for (int i = 0; i < 10000; i++) { 18 | String name = (String) r.anonymize("fakename", 100, false).getTo(); 19 | assertFalse("The name " + name 20 | + " was already generated. This is iteration " + i, 21 | names.contains(name)); 22 | names.add(name); 23 | } 24 | } 25 | 26 | public void testNullFrom() throws Exception { 27 | RomanNameGenerator r = new RomanNameGenerator(); 28 | assertNull(r.anonymize(null, 100, false).getTo()); 29 | } 30 | 31 | // commented out for speed reasons 32 | public void xxxtestMaxNames() throws Exception { 33 | 34 | RomanNameGenerator r = new RomanNameGenerator(); 35 | Set names = new HashSet<>(); 36 | 37 | try { 38 | String name; 39 | boolean wasInSet = false; 40 | do { 41 | name = (String) r.anonymize("fakename", 100, false).getTo(); 42 | wasInSet = names.contains(name); 43 | names.add(name); 44 | } while (!wasInSet); 45 | } catch (UnsupportedOperationException e) { 46 | // expected, generator crashes when it runs out of names. 47 | } 48 | 49 | LOG.info("RomanNameGenerator can generate " + names.size() 50 | + " unique names."); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/IPAddressV4AnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertFalse; 9 | import static org.junit.Assert.assertTrue; 10 | import static org.junit.Assert.assertNotEquals; 11 | import static org.junit.Assert.assertNull; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | public class IPAddressV4AnonymizerTest { 16 | 17 | private IPAddressV4Anonymizer ipAnonymizer; 18 | 19 | @Before 20 | public void setUp() { 21 | ipAnonymizer = new IPAddressV4Anonymizer(); 22 | } 23 | 24 | @Test 25 | public void testHappyFlow() { 26 | Synonym synonym = ipAnonymizer.anonymize("192.168.1.1", Integer.MAX_VALUE, false); 27 | 28 | assertNotEquals(synonym.getFrom(), synonym.getTo()); 29 | assertEquals(ipAnonymizer.getType(), synonym.getType()); 30 | assertFalse(synonym.isShortLived()); 31 | // Example from anonymizer is 127.243.0.15 32 | assertTrue(Pattern.matches("^127\\.[\\d]{1,3}\\.[\\d]{1,3}\\.[\\d]{1,3}$", synonym.getTo().toString())); 33 | } 34 | 35 | @Test 36 | public void testNullInput() { 37 | Synonym synonym = ipAnonymizer.anonymize(null, Integer.MAX_VALUE, true); 38 | assertNull(synonym.getFrom()); 39 | assertNull(synonym.getTo()); 40 | } 41 | 42 | @Test(expected = UnsupportedOperationException.class) 43 | public void testIncorrectInputType() { 44 | ipAnonymizer.anonymize(new Long(0), Integer.MAX_VALUE, true); 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/StringSynonym.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | 4 | /** 5 | * Represents a synonym for a {@link String}. 6 | */ 7 | public class StringSynonym implements Synonym { 8 | private String type; 9 | private String from; 10 | private String to; 11 | private boolean shortlived = false; 12 | 13 | public StringSynonym() { 14 | } 15 | 16 | public StringSynonym(String type, String from, String to, boolean shortlived) { 17 | this.type = type; 18 | this.from = from; 19 | this.to = to; 20 | this.shortlived = shortlived; 21 | } 22 | 23 | @Override 24 | public String getType() { 25 | return type; 26 | } 27 | 28 | @Override 29 | public Object getFrom() { 30 | return from; 31 | } 32 | 33 | @Override 34 | public Object getTo() { 35 | return to; 36 | } 37 | 38 | public void setType(String type) { 39 | this.type = type; 40 | } 41 | 42 | public void setFrom(String from) { 43 | this.from = from; 44 | } 45 | 46 | public void setTo(String to) { 47 | this.to = to; 48 | } 49 | 50 | public void setShortlived(boolean shortlived) { 51 | this.shortlived = shortlived; 52 | } 53 | 54 | @Override 55 | public boolean isShortLived() { 56 | return shortlived; 57 | } 58 | 59 | @Override 60 | public boolean equals(Object obj) { 61 | return (obj != null) && (this.getClass() == obj.getClass()) && (this.hashCode() == obj.hashCode()); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return from.hashCode() + to.hashCode() + type.hashCode(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/junit/Asserts.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.junit; 2 | 3 | import java.util.Arrays; 4 | import java.util.stream.Collectors; 5 | 6 | import static org.junit.Assert.assertTrue; 7 | import static org.junit.Assert.fail; 8 | 9 | public class Asserts { 10 | 11 | public static void assertAnyOf(Object expected, Object... values) { 12 | for (Object value : values) { 13 | if (expected.equals(value)) { 14 | return; 15 | } 16 | } 17 | fail(expected.toString() + " did not match any of " 18 | + Arrays.stream(values).map(Object::toString).collect(Collectors.joining(", ")) 19 | + "."); 20 | } 21 | 22 | public static void assertInstanceOf(Class expectedClass, Object actualClass) { 23 | Class matchable = matchableClass(expectedClass); 24 | assertTrue("Class " + actualClass.getClass().getSimpleName() + " is not an instance of " + expectedClass.getSimpleName(), 25 | matchable.isInstance(actualClass)); 26 | } 27 | 28 | private static Class matchableClass(Class expectedClass) { 29 | if (boolean.class.equals(expectedClass)) return Boolean.class; 30 | if (byte.class.equals(expectedClass)) return Byte.class; 31 | if (char.class.equals(expectedClass)) return Character.class; 32 | if (double.class.equals(expectedClass)) return Double.class; 33 | if (float.class.equals(expectedClass)) return Float.class; 34 | if (int.class.equals(expectedClass)) return Integer.class; 35 | if (long.class.equals(expectedClass)) return Long.class; 36 | if (short.class.equals(expectedClass)) return Short.class; 37 | return expectedClass; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/configuration/ColumnShortLivedFieldHandler.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.configuration; 2 | 3 | import org.exolab.castor.mapping.FieldHandler; 4 | import org.exolab.castor.mapping.ValidityException; 5 | 6 | /** 7 | * Field handler which makes sure that if {@link Column#isShortLived()} returns false, 8 | * this handler returns null so that Castor will not put it in the configuration file. 9 | */ 10 | public class ColumnShortLivedFieldHandler implements FieldHandler { 11 | 12 | @Override 13 | public Boolean getValue(Object o) throws IllegalStateException { 14 | if (o instanceof Column) { 15 | return ((Column) o).isShortLived() 16 | ? Boolean.TRUE 17 | : null; 18 | } 19 | return null; 20 | } 21 | 22 | @Override 23 | public void setValue(Object o, Boolean b) throws IllegalStateException, IllegalArgumentException { 24 | 25 | boolean shortlived = (b != null && b.booleanValue()); 26 | 27 | if (o instanceof Column) { 28 | ((Column) o).setShortlived(shortlived); 29 | } else { 30 | throw new UnsupportedOperationException("Can not set shortlived boolean on object of type " + o.getClass()); 31 | } 32 | } 33 | 34 | @Override 35 | public void resetValue(Object o) throws IllegalStateException, IllegalArgumentException { 36 | setValue(o, false); 37 | } 38 | 39 | @Override 40 | public void checkValidity(Object o) throws ValidityException, IllegalStateException { 41 | // not much to check on a boolean 42 | } 43 | 44 | @Override 45 | public Boolean newInstance(Object o) throws IllegalStateException { 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/progress/Progress.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.progress; 2 | 3 | import java.util.Date; 4 | 5 | public class Progress { 6 | private long starttime = 0; 7 | private long totalitemstodo = 0; 8 | private long totalitemscompleted = 0; 9 | 10 | public Progress() { 11 | } 12 | 13 | public void startTimer() { 14 | starttime = System.currentTimeMillis(); 15 | } 16 | 17 | public void reset(){ 18 | setTotalitemscompleted(0); 19 | startTimer(); 20 | } 21 | 22 | public void incItemsCompleted(long increase) { 23 | totalitemscompleted += increase; 24 | } 25 | 26 | public void setTotalitemscompleted(long totalitemscompleted) { 27 | this.totalitemscompleted = totalitemscompleted; 28 | } 29 | 30 | public void setTotalitemstodo(long totalitemstodo) { 31 | this.totalitemstodo = totalitemstodo; 32 | } 33 | 34 | public int getCompletePercentage() { 35 | double total = totalitemstodo; 36 | double done = totalitemscompleted; 37 | return (int) Math.round((done / total) * 100); 38 | } 39 | 40 | public Date getETA() { 41 | if (starttime <= 0){ 42 | throw new UnsupportedOperationException("Can not calculate ETA if starttime is not set."); 43 | } 44 | 45 | long now = System.currentTimeMillis(); 46 | long elapsed = now - starttime; 47 | long itemstogo = totalitemstodo - totalitemscompleted; 48 | long timetogo = Math.round(((float) elapsed * itemstogo) / Math.max(1,totalitemscompleted)) ; 49 | return new Date(now + timetogo); 50 | } 51 | 52 | public Date getStartTime(){ 53 | return new Date(starttime); 54 | } 55 | 56 | 57 | public long getTotalitemstodo() { 58 | return totalitemstodo; 59 | } 60 | 61 | public long getTotalitemscompleted() { 62 | return totalitemscompleted; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/configuration/Table.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.configuration; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | public class Table { 8 | private String name; 9 | private Integer fetchSize; 10 | private List columns; 11 | private List discriminators; 12 | 13 | // Used for progress monitoring 14 | private long numberOfRows; 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | public Integer getFetchSize() { 25 | return fetchSize; 26 | } 27 | 28 | public void setFetchSize(Integer fetchSize) { 29 | this.fetchSize = fetchSize; 30 | } 31 | 32 | public List getColumns() { 33 | return columns; 34 | } 35 | 36 | public static Map getColumnsAsMap(List columns) { 37 | Map columnMap = new HashMap<>(); 38 | if (columns != null) { 39 | for (Column column : columns) { 40 | columnMap.put(column.getName(), column); 41 | } 42 | } 43 | return columnMap; 44 | } 45 | 46 | public void setColumns(List columns) { 47 | this.columns = columns; 48 | } 49 | 50 | public List getDiscriminators() { 51 | return discriminators; 52 | } 53 | 54 | public void setDiscriminators(List discriminators) { 55 | this.discriminators = discriminators; 56 | } 57 | 58 | public long getNumberOfRows() { 59 | return numberOfRows; 60 | } 61 | 62 | public void setNumberOfRows(long numberOfRows) { 63 | this.numberOfRows = numberOfRows; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/HasherTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import junit.framework.TestCase; 4 | import org.apache.log4j.Logger; 5 | 6 | import java.sql.Date; 7 | 8 | import static org.junit.Assert.assertNotEquals; 9 | 10 | public class HasherTest extends TestCase { 11 | private final Logger LOG = Logger.getLogger(HasherTest.class); 12 | 13 | public void testBase64HashHappy() { 14 | assertEquals( 15 | new Hasher("salt").base64Hash("piep"), 16 | new Hasher("salt").base64Hash("piep") 17 | ); 18 | } 19 | 20 | public void testBase64HashHappyDate() { 21 | assertEquals( 22 | new Hasher("salt").base64Hash(new Date(12345)), 23 | new Hasher("salt").base64Hash(new Date(12345)) 24 | ); 25 | } 26 | 27 | public void testWrongSalt() { 28 | String input = "piep"; 29 | String salt1 = new Hasher("salt1").base64Hash(input); 30 | String salt2 = new Hasher("salt2").base64Hash(input); 31 | 32 | assertNotEquals(salt1, salt2); 33 | } 34 | 35 | public void testWrongInput() { 36 | String salt = "piep"; 37 | String input1 = new Hasher(salt).base64Hash("piep1"); 38 | String input2 = new Hasher(salt).base64Hash("piep2"); 39 | 40 | assertNotEquals(input1, input2); 41 | } 42 | 43 | public void testHashSpeed() { 44 | long start = System.nanoTime(); 45 | long iterations = 2000; 46 | long size = 0; 47 | for (int i = 0; i < iterations; i++) { 48 | size += new Hasher("salt").base64Hash("piep").length(); 49 | } 50 | 51 | long durationMillis = (System.nanoTime() - start) / 1_000_000; 52 | LOG.info(String.format("%d hashes took %d milliseconds.", iterations, durationMillis)); 53 | assertTrue("Hashing "+iterations+ " times took " + durationMillis 54 | + " ms and generated " + size + " characters.", durationMillis < 1000); 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/assembly/anonimatronbin.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | zip 6 | 7 | 8 | 9 | resources/documentation 10 | 11 | README* 12 | LICENSE* 13 | NOTICE* 14 | VERSION* 15 | 16 | / 17 | 18 | 19 | resources/libraries 20 | jdbcdrivers 21 | 22 | *.jar 23 | 24 | 25 | 26 | resources 27 | / 28 | 29 | anonymizers/* 30 | 31 | 32 | 33 | resources/scripts 34 | / 35 | 36 | *.bat 37 | 38 | 39 | 40 | resources/scripts 41 | / 42 | 43 | *.sh 44 | 45 | 0755 46 | 47 | 48 | 49 | 50 | runtime 51 | libraries 52 | 53 | 54 | -------------------------------------------------------------------------------- /resources/integration/mssql/README.md: -------------------------------------------------------------------------------- 1 | # MS-SQL test scripts 2 | 3 | This directory contains scripts for manually testing against an ms-sql or 4 | sql server database. It makes use of an sql server running inside a 5 | docker container. 6 | 7 | ## Docker commands 8 | 9 | See https://hub.docker.com/_/microsoft-mssql-server 10 | 11 | Get the docker image 12 | ``` 13 | docker pull mcr.microsoft.com/mssql/server:2017-latest-ubuntu 14 | ``` 15 | 16 | Start the mssqql docker image listening on port 1433 17 | ```shell 18 | docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Anon!matron' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest 19 | ``` 20 | 21 | Start the sqlcmd command line tool (focused_proskuriakova is the container name in this case) 22 | ```shell 23 | docker exec -it focused_proskuriakova /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Anon!matron' 24 | ``` 25 | 26 | ## Create database and tables 27 | 28 | Create a contained database (for local user management), create a schema, and 29 | two tables, one in the non-default schema. 30 | 31 | ```roomsql 32 | EXEC sp_configure 'CONTAINED DATABASE AUTHENTICATION' 33 | go 34 | EXEC sp_configure 'CONTAINED DATABASE AUTHENTICATION', 1 35 | create database mydb containment = partial 36 | go 37 | 38 | use mydb 39 | go 40 | 41 | CREATE USER test WITH PASSWORD = 'Test.1234' 42 | GRANT SELECT to test 43 | grant update to test 44 | go 45 | 46 | CREATE TABLE TABLE1 (ID int primary key IDENTITY(1,1) NOT NULL, COL1 VARCHAR(200)) 47 | CREATE SCHEMA SCHEMA2 48 | CREATE TABLE SCHEMA2.TABLE2 (ID int primary key IDENTITY(1,1) NOT NULL, COL1 VARCHAR(200)) 49 | go 50 | 51 | INSERT INTO table1 (col1) VALUES ('testmail@example.com'); 52 | INSERT INTO schema2.table1 (col1) VALUES ('testmail@example.com'); 53 | go 54 | ``` 55 | 56 | ## Configure and run Anonimatron 57 | 58 | Run anonimatron with the [config.xml](config.xml) configuration file. 59 | A run configuration in IntelliJ may be better for debugging and analysis. 60 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/configuration/DataFile.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.configuration; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | public class DataFile { 8 | private String inFile; 9 | private String reader; 10 | 11 | private String outFile; 12 | private String writer; 13 | 14 | private List columns; 15 | 16 | private List discriminators; 17 | private long numberOfRecords; 18 | 19 | public String getInFile() { 20 | return inFile; 21 | } 22 | 23 | public void setInFile(String inFile) { 24 | this.inFile = inFile; 25 | } 26 | 27 | public void setReader(String reader) { 28 | this.reader = reader; 29 | } 30 | 31 | public String getReader() { 32 | return reader; 33 | } 34 | 35 | public String getOutFile() { 36 | return outFile; 37 | } 38 | 39 | public void setOutFile(String outFile) { 40 | this.outFile = outFile; 41 | } 42 | 43 | public String getWriter() { 44 | return writer; 45 | } 46 | 47 | public void setWriter(String writer) { 48 | this.writer = writer; 49 | } 50 | 51 | public List getColumns() { 52 | return columns; 53 | } 54 | 55 | public static Map getColumnsAsMap(List columns) { 56 | Map columnMap = new HashMap<>(); 57 | for (Column column : columns) { 58 | columnMap.put(column.getName(), column); 59 | } 60 | return columnMap; 61 | } 62 | 63 | public void setColumns(List columns) { 64 | this.columns = columns; 65 | } 66 | 67 | public List getDiscriminators() { 68 | return discriminators; 69 | } 70 | 71 | public void setDiscriminators(List discriminators) { 72 | this.discriminators = discriminators; 73 | } 74 | 75 | public long getNumberOfRecords() { 76 | return numberOfRecords; 77 | } 78 | 79 | public void setNumberOfRecords(long numberOfRecords) { 80 | this.numberOfRecords = numberOfRecords; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/configuration/Column.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.configuration; 2 | 3 | import java.util.Map; 4 | 5 | public class Column { 6 | private String name; 7 | private String type; 8 | private int size = -1; 9 | private boolean shortlived; 10 | private Map parameters; 11 | 12 | public Column() { 13 | 14 | } 15 | 16 | public Column(String name, String type) { 17 | this.name = name; 18 | this.type = type; 19 | } 20 | 21 | public Column(String name, String type, int size) { 22 | this(name, type); 23 | this.size = size; 24 | } 25 | 26 | public Column(String name, String type, int size, boolean shortlived) { 27 | this(name, type); 28 | this.size = size; 29 | this.shortlived = shortlived; 30 | } 31 | 32 | public Column(String name, String type, int size, boolean shortlived, Map parameters) { 33 | this(name, type, size, shortlived); 34 | this.parameters = parameters; 35 | } 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public void setName(String name) { 42 | this.name = name; 43 | } 44 | 45 | public String getType() { 46 | return type; 47 | } 48 | 49 | public void setType(String type) { 50 | this.type = type; 51 | } 52 | 53 | public int getSize() { 54 | return size; 55 | } 56 | 57 | public void setSize(int size) { 58 | this.size = size; 59 | } 60 | 61 | public boolean isShortLived() { 62 | return shortlived; 63 | } 64 | 65 | public void setShortlived(boolean shortlived) { 66 | this.shortlived = shortlived; 67 | } 68 | 69 | public Map getParameters() { 70 | return parameters; 71 | } 72 | 73 | public void setParameters(Map parameters) { 74 | this.parameters = parameters; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/SynonymCacheTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.NullSynonym; 4 | import com.rolfje.anonimatron.synonyms.StringSynonym; 5 | import com.rolfje.anonimatron.synonyms.Synonym; 6 | import junit.framework.TestCase; 7 | 8 | import static org.junit.Assert.assertNotEquals; 9 | 10 | public class SynonymCacheTest extends TestCase { 11 | 12 | public void testNoStorageOfShortLivedSynonyms() { 13 | SynonymCache synonymCache = new SynonymCache(); 14 | 15 | NullSynonym n = new NullSynonym("null"); 16 | synonymCache.put(n); 17 | 18 | assertNull(synonymCache.get(n.getType(), n.getFrom())); 19 | } 20 | 21 | public void testNoHashing() { 22 | SynonymCache synonymCache = new SynonymCache(); 23 | 24 | StringSynonym originalSynonym = new StringSynonym("type", "from", "to", false); 25 | 26 | // Store and retrieve 27 | synonymCache.put(originalSynonym); 28 | Synonym storedSynonym = synonymCache.get(originalSynonym.getType(), originalSynonym.getFrom()); 29 | 30 | // Should not be hashed 31 | assertEquals(originalSynonym.getType(), storedSynonym.getType()); 32 | assertEquals(originalSynonym.getFrom(), storedSynonym.getFrom()); 33 | assertEquals(originalSynonym.getTo(), storedSynonym.getTo()); 34 | } 35 | 36 | public void testHashing() { 37 | SynonymCache synonymCache = new SynonymCache(); 38 | synonymCache.setHasher(new Hasher("testhash")); 39 | 40 | StringSynonym originalSynonym = new StringSynonym("type", "from", "to", false); 41 | 42 | // Store and retrieve 43 | synonymCache.put(originalSynonym); 44 | Synonym storedSynonym = synonymCache.get(originalSynonym.getType(), originalSynonym.getFrom()); 45 | 46 | // Should not be same 47 | assertNotEquals(originalSynonym.getFrom(), storedSynonym.getFrom()); 48 | 49 | // Should be same 50 | assertEquals(originalSynonym.getType(), storedSynonym.getType()); 51 | assertEquals(originalSynonym.getTo(), storedSynonym.getTo()); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/CountryCodeAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | import org.springframework.util.StringUtils; 6 | 7 | import java.security.SecureRandom; 8 | import java.text.SimpleDateFormat; 9 | import java.util.Locale; 10 | import java.util.MissingResourceException; 11 | 12 | /** 13 | * Produces valid 2 or 3 character country codes. 14 | */ 15 | public class CountryCodeAnonymizer implements Anonymizer { 16 | 17 | private static final String TYPE = "COUNTRY_CODE"; 18 | protected static final Locale[] AVAILABLE_LOCALES = SimpleDateFormat.getAvailableLocales(); 19 | SecureRandom r = new SecureRandom(); 20 | 21 | @Override 22 | public String getType() { 23 | return TYPE; 24 | } 25 | 26 | @Override 27 | public Synonym anonymize(Object from, int size, boolean shortlived) { 28 | 29 | if (size < 2) { 30 | throw new UnsupportedOperationException("Can not produce country codes of one character."); 31 | } 32 | 33 | String country = null; 34 | while (country == null || country.length() < 1) { 35 | Locale l = AVAILABLE_LOCALES[r.nextInt(AVAILABLE_LOCALES.length)]; 36 | 37 | try { 38 | if (size > 2) { 39 | country = l.getISO3Country(); 40 | if(StringUtils.isEmpty(country)) { 41 | continue; 42 | } 43 | country = padRight(country, size); 44 | } 45 | else if (size == 2) { 46 | country = l.getCountry(); 47 | } 48 | } catch (MissingResourceException e) { 49 | // Locale.getISOCountries() has inconsistent behaviour for "AN", "BU" and "CS" country codes 50 | // See https://bugs.openjdk.java.net/browse/JDK-8071929 51 | } 52 | } 53 | 54 | return new StringSynonym( 55 | getType(), 56 | from.toString(), 57 | country, 58 | shortlived 59 | ); 60 | } 61 | 62 | public static String padRight(String s, int n) { 63 | return String.format("%1$-" + n + "s", s); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/file/CsvFileReader.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.file; 2 | 3 | import java.io.*; 4 | import java.util.ArrayList; 5 | import java.util.StringTokenizer; 6 | 7 | public class CsvFileReader implements RecordReader, Closeable { 8 | 9 | private BufferedReader reader; 10 | private File file; 11 | 12 | public CsvFileReader(String fileName) throws IOException { 13 | this(new File(fileName)); 14 | } 15 | 16 | public CsvFileReader(File file) throws IOException { 17 | this.file = file; 18 | try { 19 | reader = new BufferedReader(new FileReader(file)); 20 | } catch (FileNotFoundException e) { 21 | throw new RuntimeException("Problem while reading file " + file.getAbsolutePath() + ".", e); 22 | } 23 | } 24 | 25 | @Override 26 | public boolean hasRecords() { 27 | try { 28 | return reader.ready(); 29 | } catch (IOException e) { 30 | throw new RuntimeException("Problem while reading file " + file.getAbsolutePath() + ".", e); 31 | } 32 | } 33 | 34 | @Override 35 | public Record read() { 36 | String s = null; 37 | try { 38 | s = reader.readLine(); 39 | } catch (IOException e) { 40 | throw new RuntimeException("Problem reading file " + file.getAbsolutePath() + ".", e); 41 | } 42 | 43 | // Super simple implementation, not taking quotes into account. 44 | // The CSVReader library is no longer supporting Java 1.6, we need 45 | // to figure out if we want to switch to a newer Java. 46 | StringTokenizer stringTokenizer = new StringTokenizer(s, ",;\t"); 47 | 48 | ArrayList names = new ArrayList<>(); 49 | ArrayList strings = new ArrayList<>(); 50 | int i = 1; 51 | while (stringTokenizer.hasMoreTokens()) { 52 | names.add(String.valueOf(i)); 53 | strings.add(stringTokenizer.nextToken().replaceAll("^\"|\"$", "")); 54 | i++; 55 | } 56 | 57 | return new Record( 58 | names.toArray(new String[]{}), 59 | strings.toArray() 60 | ); 61 | } 62 | 63 | @Override 64 | public void close() throws IOException { 65 | reader.close(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/castor-synonym-mapping.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Mapping for the different Synonym types. Please note that 8 | synonyms which are implemented externally may not be serialized 9 | because they are missing from this mapping. 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 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/DutchZipCodeAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * Generates valid Dutch zip codes. 10 | *

11 | * The generated zip code has the following characteristics: 12 | *

    13 | *
  • Starts with [1-9]
  • 14 | *
  • Followed by [0-9]{3}
  • 15 | *
  • Ends with two uppercase letters, that may NOT include: 16 | *
      17 | *
    • SA
    • 18 | *
    • SD
    • 19 | *
    • SS
    • 20 | *
  • 21 | *
22 | *

23 | * 24 | * @author Erik-Berndt Scheper 25 | * @see https://nl.wikipedia.org/wiki/Postcodes_in_Nederland 26 | */ 27 | public class DutchZipCodeAnonymizer implements Anonymizer { 28 | 29 | private static final String TYPE = "DUTCH_ZIP_CODE"; 30 | 31 | private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 32 | private static final String CHARS_WITHOUT_ADS = "BCEFGHIJKLMNOPQRTUVWXYZ"; 33 | protected static final Random RANDOM = new Random(); 34 | 35 | @Override 36 | public String getType() { 37 | return TYPE; 38 | } 39 | 40 | @Override 41 | public Synonym anonymize(Object from, int size, boolean shortlived) { 42 | String result = buildZipCode(); 43 | return new StringSynonym(TYPE, (String) from, result, shortlived); 44 | } 45 | 46 | String buildZipCode() { 47 | 48 | // generate a random integer from 0000 to 8999, then add 1000 49 | Integer pcNum = RANDOM.nextInt(9000) + 1000; 50 | return pcNum + buildPcAlpha(); 51 | } 52 | 53 | private String buildPcAlpha() { 54 | 55 | char a1 = getCharacter(CHARS); 56 | char a2; 57 | 58 | if (a1 == 'S') { 59 | a2 = getCharacter(CHARS_WITHOUT_ADS); 60 | } else { 61 | a2 = getCharacter(CHARS); 62 | } 63 | 64 | return new StringBuilder().append(a1).append(a2).toString(); 65 | } 66 | 67 | private char getCharacter(String chars) { 68 | return chars.charAt(RANDOM.nextInt(chars.length())); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/CharacterStringAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | import java.util.Map; 7 | import java.util.Random; 8 | 9 | /** 10 | * Generates an output string based on the configured characters 11 | * to use. 12 | */ 13 | public class CharacterStringAnonymizer implements Anonymizer { 14 | 15 | public static final String PARAMETER = "characters"; 16 | 17 | protected String CHARS; 18 | 19 | protected static final Random RANDOM = new Random(); 20 | 21 | public CharacterStringAnonymizer() { 22 | CHARS = getDefaultCharacterString(); 23 | } 24 | 25 | protected String getDefaultCharacterString() { 26 | return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 27 | } 28 | 29 | @Override 30 | public String getType() { 31 | return "RANDOMCHARACTERS"; 32 | } 33 | 34 | @Override 35 | public Synonym anonymize(Object from, int size, boolean shortlived, Map parameters) { 36 | if (parameters == null || !parameters.containsKey(PARAMETER)) { 37 | throw new UnsupportedOperationException("Please provide '" + PARAMETER + "' as configuration parameter."); 38 | } 39 | return anonymize(from, size, shortlived, parameters.get(PARAMETER)); 40 | } 41 | 42 | @Override 43 | public Synonym anonymize(Object from, int size, boolean shortlived) { 44 | return anonymize(from, size, shortlived, CHARS); 45 | } 46 | 47 | private Synonym anonymize(Object from, int size, boolean shortlived, String characters) { 48 | String fromString = from.toString(); 49 | 50 | int length = fromString.length(); 51 | StringBuilder sb = new StringBuilder(); 52 | for (int i = 0; i < length; i++) { 53 | sb.append(characters.charAt(RANDOM.nextInt(characters.length()))); 54 | } 55 | 56 | String to = sb.toString(); 57 | 58 | return new StringSynonym( 59 | getType(), 60 | fromString, 61 | to, 62 | shortlived 63 | ); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /create_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Usage of this script: 4 | # 1. Make sure develop branch build is up to date and has no failing tests. 5 | # 2. Switch to master and make sure it up to date and has no failing tests. 6 | # 3. Run this script to: 7 | # - Merge develop in master (without fetch) 8 | # - Creates a release on master branch 9 | # - Merge master into develop (without fetch) 10 | # - Push master and develop branch to remote. 11 | # 12 | 13 | if [ "$#" -ne 2 ] 14 | then 15 | echo "Please specify the release version number and the next snapshot for this release:" 16 | echo " ./create_release.sh 1.8 1.9-SNAPSHOT" 17 | echo "" 18 | echo "The current version recorded in VERSION.TXT is " 19 | cat src/main/java/com/rolfje/anonimatron/version.txt 20 | exit 21 | fi 22 | 23 | # Exit when a command fails. 24 | set -e 25 | 26 | # ------------------------------------------ Update master and create release 27 | git checkout master 28 | git merge develop 29 | 30 | # Set the new versions 31 | echo $1 > src/main/java/com/rolfje/anonimatron/version.txt 32 | mvn versions:set -DnewVersion=$1 33 | 34 | # Deploy the release to mavenrepo 35 | mvn clean deploy -P release 36 | 37 | # Commit the release and tag it. 38 | mvn versions:commit 39 | git add pom.xml src/main/java/com/rolfje/anonimatron/version.txt 40 | git commit -m "Release $1" 41 | git tag "v$1" 42 | git push origin "v$1" 43 | 44 | # ------------------------------------------------- Update develop 45 | git checkout develop 46 | git merge master 47 | 48 | # Set the version to the new SNAPSHOT version 49 | echo $2 > src/main/java/com/rolfje/anonimatron/version.txt 50 | mvn versions:set -DnewVersion=$2 51 | mvn versions:commit 52 | 53 | # Commit the SNAPSHOT version to git 54 | git add pom.xml src/main/java/com/rolfje/anonimatron/version.txt 55 | git commit -m "Update version to $2" 56 | git push 57 | 58 | # Sign the zip file 59 | gpg -ab --default-key 45E2A5E085182DC26EFEF6E796BB2760490D54DD target/anonimatron*.zip 60 | 61 | echo "The files to upload for this release are:" 62 | echo 63 | ls -l target/anonimatron*.zip* 64 | echo 65 | echo "When creating a release, github automatically adds src zip" 66 | echo "files, you only need to upload the binary." 67 | echo 68 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/synonyms/SynonymMapper.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import org.exolab.castor.mapping.Mapping; 4 | import org.exolab.castor.mapping.MappingException; 5 | import org.exolab.castor.xml.*; 6 | 7 | import java.io.*; 8 | import java.net.URL; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | /** 14 | * Provides functionality for reading and writing {@link Synonym}s to an XML 15 | * file for later reference. 16 | * 17 | */ 18 | public class SynonymMapper { 19 | 20 | @SuppressWarnings("unchecked") 21 | public static List readFromFile(String filename) throws IOException, XMLException, MappingException { 22 | Mapping mapping = getMapping(); 23 | Unmarshaller unmarshaller = new Unmarshaller(ArrayList.class); 24 | unmarshaller.setMapping(mapping); 25 | 26 | File file = new File(filename); 27 | 28 | if (file.exists() && !file.isFile()) { 29 | throw new IOException("File " + file.getAbsolutePath() + " exists but is not a file."); 30 | } 31 | 32 | if (!file.exists() || file.length() == 0) { 33 | return Collections.emptyList(); 34 | } 35 | 36 | Reader reader = new FileReader(file); 37 | return (List) unmarshaller.unmarshal(reader); 38 | } 39 | 40 | public static void writeToFile(List synonyms, String filename) throws IOException, MappingException, XMLException { 41 | Mapping mapping = getMapping(); 42 | 43 | Writer writer = new FileWriter(new File(filename)); 44 | Marshaller marshaller = new Marshaller(writer); 45 | 46 | // I have no idea why this does not work, so I added a castor.propeties 47 | // file in the root as workaround. 48 | // marshaller.setProperty("org.exolab.castor.indent", "true"); 49 | 50 | marshaller.setRootElement("synonyms"); 51 | marshaller.setMapping(mapping); 52 | marshaller.setSuppressXSIType(true); 53 | marshaller.marshal(synonyms); 54 | writer.close(); 55 | } 56 | 57 | private static Mapping getMapping() throws IOException, MappingException { 58 | URL url = SynonymMapper.class.getResource("castor-synonym-mapping.xml"); 59 | Mapping mapping = new Mapping(); 60 | mapping.loadMapping(url); 61 | return mapping; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/configuration/ColumnShortLivedFieldHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.configuration; 2 | 3 | import org.exolab.castor.mapping.ValidityException; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.*; 8 | 9 | public class ColumnShortLivedFieldHandlerTest { 10 | 11 | private ColumnShortLivedFieldHandler handler; 12 | 13 | @Before 14 | public void setUp() { 15 | handler = new ColumnShortLivedFieldHandler(); 16 | } 17 | 18 | @Test 19 | public void getValue() { 20 | assertTrue(handler.getValue(shortLivedColumn())); 21 | assertNull(handler.getValue(column())); 22 | assertNull(handler.getValue(new Object())); 23 | } 24 | 25 | @Test 26 | public void setValue() { 27 | Column column = column(); 28 | handler.setValue(column, true); 29 | assertTrue(column.isShortLived()); 30 | 31 | handler.setValue(column, false); 32 | assertFalse(column.isShortLived()); 33 | 34 | try { 35 | handler.setValue(new Object(), true); 36 | fail("Can not set boolean on non-column object."); 37 | } catch (UnsupportedOperationException e) { 38 | // ok 39 | } 40 | } 41 | 42 | @Test 43 | public void resetValue() { 44 | Column column = shortLivedColumn(); 45 | handler.resetValue(column); 46 | assertFalse(column.isShortLived()); 47 | } 48 | 49 | @Test 50 | public void checkValidity() throws ValidityException { 51 | boolean[] all = new boolean[]{true, false}; 52 | for (boolean b : all) { 53 | // Should not do anything 54 | Column column = column(); 55 | column.setShortlived(b); 56 | handler.checkValidity(column); 57 | assertEquals(b, column.isShortLived()); 58 | } 59 | } 60 | 61 | @Test 62 | public void newInstance() { 63 | assertNull(handler.newInstance(new Object())); 64 | } 65 | 66 | private Column shortLivedColumn() { 67 | Column column = new Column(); 68 | column.setShortlived(true); 69 | return column; 70 | } 71 | 72 | private Column column() { 73 | return new Column(); 74 | } 75 | } -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/CountryCodeAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import junit.framework.TestCase; 5 | 6 | import java.util.*; 7 | 8 | import static org.junit.Assert.assertNotEquals; 9 | 10 | public class CountryCodeAnonymizerTest extends TestCase { 11 | 12 | private static final Set ISO_3_COUNTRY_CODES = getISO3CountryCodes(); 13 | 14 | public void testAnonymize() { 15 | 16 | testInternal(2, "EN"); 17 | testInternal(3, "NLD"); 18 | testInternal(4, "BLR"); 19 | } 20 | 21 | private void testInternal(int size, String from) { 22 | CountryCodeAnonymizer countryCodeAnonymizer = new CountryCodeAnonymizer(); 23 | Synonym nld = countryCodeAnonymizer.anonymize(from, size, false); 24 | assertEquals(countryCodeAnonymizer.getType(), nld.getType()); 25 | assertEquals(from, nld.getFrom()); 26 | assertNotEquals(from, nld.getTo()); 27 | assertEquals("String length check failed, should be " + size + ". From: " + nld.getFrom() + " To: " + nld.getTo(), 28 | size, ((String) nld.getTo()).length()); 29 | assertFalse(nld.isShortLived()); 30 | } 31 | 32 | public void testThreeDigitCountryCodes_shouldAnonymizeToThreeDigitCountryCodes() { 33 | CountryCodeAnonymizer anonymizer = new CountryCodeAnonymizer(); 34 | String countryCode = "NLD"; 35 | for (int i = 0; i < 100; i++) { 36 | Synonym synonym = anonymizer.anonymize(countryCode, 3, false); 37 | assertTrue(synonym.getTo() + " is not a valid country code.", 38 | ISO_3_COUNTRY_CODES.contains(String.valueOf(synonym.getTo()))); 39 | } 40 | } 41 | 42 | private static Set getISO3CountryCodes() { 43 | Locale[] availableLocales = Locale.getAvailableLocales(); 44 | Set iso3CountryCodes = new HashSet<>(availableLocales.length); 45 | for (Locale locale : availableLocales) { 46 | try { 47 | iso3CountryCodes.add(locale.getISO3Country()); 48 | } catch (MissingResourceException e) { 49 | // don't add 50 | } 51 | } 52 | iso3CountryCodes.remove(""); 53 | return iso3CountryCodes; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Anonimatron 2 | 3 | Thank you for your interest and time in contributing to Anonimatron. I'm sure you've already found the [code of conduct](CODE_OF_CONDUCT.md) by now so lets cover your options: 4 | 5 | ## Reporting an issue or feature request 6 | 7 | If you noticed a problem, or have a great idea for a feature, you are welcome to [open an issue]()https://github.com/realrolfje/anonimatron/issues) so we can help you out. 8 | 9 | ## Contributing code 10 | 11 | If you already figured out how to fix the bug you've found, or added a great feature, I'd like to hear from you. This being GitHub, you are welcome to [fork this repository](https://help.github.com/articles/fork-a-repo/). I'm happy to review your pull requests or look at your branch. 12 | 13 | In order to build the codebase you'll need Maven installed. Run the following command in the root of the project to install the dependencies we'll need and run the tests to ensure your environment is healthy: 14 | 15 | ```console 16 | $ mvn install 17 | ``` 18 | 19 | At this point you can contribute your code changes. Please add unit tests for anything new / changed. You can verify your changes work with: 20 | 21 | ```console 22 | $ mvn test 23 | ``` 24 | 25 | To build the code locally so you can use your release as you'd use the one from GitHub Releases, run: 26 | 27 | ```console 28 | $ mvn package 29 | ``` 30 | 31 | The built version is now in `target/anonimatron-[version].zip`. 32 | 33 | ## Publishing to mavenrepo (experimental) 34 | 35 | The `pom.xml` file adheres to http://central.sonatype.org/pages/requirements.html. Release to mavenrepo is based on https://medium.com/pleo/deploying-to-mavens-central-repository-835253a119db 36 | 37 | In order to run `mvn clean deploy` to release to the sonatype Maven Repository, please create a security token at your [sonatype Nexus profile page](https://oss.sonatype.org/#profile;User%20Token) and add it to your settings xml: 38 | 39 | ```xml 40 | 41 | 42 | ossrh 43 | YOUR_SONATYPE_TOKEN 44 | YOUR_SONATYPE_TOKENPASS 45 | 46 | 47 | 48 | ``` 49 | 50 | After doing this, running `maven deploy` on a SNAPSHOT release should release to the snapshot repository, and on a non-snapshot release it should promote to the central maven repo. Be careful with your powers. -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/synonyms/SynonymMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.synonyms; 2 | 3 | import junit.framework.TestCase; 4 | 5 | import java.io.File; 6 | import java.sql.Date; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class SynonymMapperTest extends TestCase { 11 | 12 | public static final char[] ILLEGALSTRINGCHARACTERS = new char[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 13 | 0x07, 0x08, 0x0B, 0x0C, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 14 | 0x15, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x16, 0x17, 0x18, 0x19, 0x7F}; 15 | 16 | public void testSynonymMapper() throws Exception { 17 | List synonyms = new ArrayList<>(); 18 | 19 | StringSynonym s = new StringSynonym( 20 | "Bunk", 21 | "Foo", 22 | "Bar", 23 | false 24 | ); 25 | synonyms.add(s); 26 | 27 | s = new StringSynonym( 28 | "XMLCHARS", 29 | "<>!@#$%^&*()", 30 | "<>!@#$%^&*()", 31 | false 32 | ); 33 | synonyms.add(s); 34 | 35 | s = new StringSynonym( 36 | "", 37 | "", 38 | "", 39 | false 40 | ); 41 | synonyms.add(s); 42 | 43 | String illegalcharString = String.copyValueOf(ILLEGALSTRINGCHARACTERS); 44 | s = new StringSynonym( 45 | "", 46 | illegalcharString, 47 | "", 48 | false 49 | ); 50 | synonyms.add(s); 51 | 52 | DateSynonym d = new DateSynonym(); 53 | d.setFrom(new Date(System.currentTimeMillis())); 54 | d.setTo(new Date(System.currentTimeMillis() - 1000)); 55 | d.setType(""); 56 | synonyms.add(d); 57 | 58 | File tempFile = File.createTempFile("Anonimatron-SynonymMapperTest-", ".xml"); 59 | tempFile.deleteOnExit(); 60 | 61 | SynonymMapper.writeToFile(synonyms, tempFile.getAbsolutePath()); 62 | List synonymsFromFile = SynonymMapper.readFromFile(tempFile.getAbsolutePath()); 63 | 64 | assertEquals("Not all synonyms were serialized.", synonyms.size(), synonymsFromFile.size()); 65 | 66 | synonyms.removeAll(synonymsFromFile); 67 | assertEquals("Some synonyms were serialized incorrectly.", 0, 68 | synonyms.size()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/IbanAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import com.rolfje.junit.Asserts; 5 | import org.iban4j.CountryCode; 6 | import org.iban4j.Iban; 7 | import org.iban4j.bban.BbanStructure; 8 | import org.junit.Test; 9 | 10 | import java.util.EnumSet; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | import static org.junit.Assert.fail; 14 | 15 | /** 16 | * Tests for {@link IbanAnonymizer}. 17 | * 18 | * @author Erik-Berndt Scheper 19 | */ 20 | public class IbanAnonymizerTest { 21 | 22 | private IbanAnonymizer anonymizer = new IbanAnonymizer(); 23 | private EnumSet countriesWithBankAccountAnonymizer = EnumSet 24 | .copyOf(IbanAnonymizer.BANK_ACCOUNT_ANONYMIZERS.keySet()); 25 | private EnumSet countriesWithoutBankAccountAnonymizer = EnumSet.complementOf(countriesWithBankAccountAnonymizer); 26 | 27 | @Test 28 | public void testAnonymizeForCountriesWithBankAccountAnonymizer() { 29 | for (CountryCode countryCode : countriesWithBankAccountAnonymizer) { 30 | testAnonymize(countryCode); 31 | } 32 | } 33 | 34 | @Test 35 | public void testAnonymizeForOtherCountries() { 36 | for (CountryCode countryCode : countriesWithoutBankAccountAnonymizer) { 37 | if (BbanStructure.forCountry(countryCode) != null) { 38 | testAnonymize(countryCode); 39 | } 40 | } 41 | } 42 | 43 | private void testAnonymize(CountryCode countryCode) { 44 | for (int i = 0; i < 1000; i++) { 45 | String from = anonymizer.generateIban(countryCode); 46 | 47 | Asserts.assertAnyOf(Iban.valueOf(from).getCountryCode(), 48 | countryCode, 49 | IbanAnonymizer.DEFAULT_COUNTRY_CODE 50 | ); 51 | 52 | testInternal(from.length(), from, countryCode); 53 | } 54 | } 55 | 56 | private void testInternal(int size, String from, CountryCode countryCode) { 57 | Synonym synonym = anonymizer.anonymize(from, size, false); 58 | assertEquals(anonymizer.getType(), synonym.getType()); 59 | 60 | Asserts.assertAnyOf(Iban.valueOf((String) synonym.getTo()).getCountryCode(), 61 | countryCode, 62 | IbanAnonymizer.DEFAULT_COUNTRY_CODE 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if site.google_analytics %} 6 | 7 | 13 | {% endif %} 14 | 15 | 16 | {% seo %} 17 | 18 | 19 | 20 | 21 | 22 | 23 |

38 | 39 |
40 | {{ content }} 41 | 42 | 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/progress/ProgressTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.progress; 2 | 3 | import java.util.Date; 4 | 5 | import junit.framework.TestCase; 6 | 7 | public class ProgressTest extends TestCase { 8 | private Progress progress; 9 | 10 | @Override 11 | protected void setUp() throws Exception { 12 | super.setUp(); 13 | progress = new Progress(); 14 | } 15 | 16 | public void testPercentageCalculations() { 17 | progress.setTotalitemstodo(1000); 18 | progress.setTotalitemscompleted(0); 19 | 20 | assertEquals(0, progress.getCompletePercentage()); 21 | 22 | progress.setTotalitemscompleted(333); 23 | assertEquals(33, progress.getCompletePercentage()); 24 | 25 | progress.setTotalitemscompleted(495); 26 | assertEquals(50, progress.getCompletePercentage()); 27 | 28 | progress.setTotalitemscompleted(504); 29 | assertEquals(50, progress.getCompletePercentage()); 30 | 31 | progress.setTotalitemscompleted(666); 32 | assertEquals(67, progress.getCompletePercentage()); 33 | 34 | progress.setTotalitemscompleted(754); 35 | assertEquals(75, progress.getCompletePercentage()); 36 | 37 | progress.setTotalitemscompleted(999); 38 | assertEquals(100, progress.getCompletePercentage()); 39 | } 40 | 41 | public void testETAfifty() throws Exception { 42 | progress.setTotalitemstodo(100); 43 | progress.startTimer(); 44 | 45 | Thread.sleep(100); 46 | progress.setTotalitemscompleted(50); 47 | 48 | long now = System.currentTimeMillis(); 49 | long eta = progress.getETA().getTime(); 50 | long start = progress.getStartTime().getTime(); 51 | 52 | long expected = now + (now - start); 53 | 54 | assertETA(expected, eta); 55 | 56 | } 57 | 58 | private void assertETA(long expected, long eta) { 59 | int maxerror = 1; 60 | 61 | long error = Math.abs(Math.round(100F * (eta - expected) / expected)); 62 | assertTrue("ETA is more than " + maxerror + "% off, should be " 63 | + expected + "(" + new Date(expected) + ") but was " + eta 64 | + "(" + new Date(eta) + ").", error < maxerror); 65 | } 66 | 67 | public void testETAWeek() throws Exception { 68 | long millisInWeek = 7L * 24 * 60 * 60 * 100; 69 | 70 | progress.setTotalitemstodo(millisInWeek); 71 | progress.startTimer(); 72 | 73 | Thread.sleep(100); 74 | progress.setTotalitemscompleted(100); 75 | 76 | long start = progress.getStartTime().getTime(); 77 | long eta = progress.getETA().getTime(); 78 | long expected = start + millisInWeek; 79 | 80 | assertETA(expected, eta); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/progress/ProgressPrinter.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.progress; 2 | 3 | import org.apache.log4j.Logger; 4 | 5 | import java.text.DateFormat; 6 | 7 | public class ProgressPrinter implements Runnable { 8 | private static final Logger LOG = Logger.getLogger(ProgressPrinter.class); 9 | 10 | private int printIntervalMillis = 4000; 11 | private Progress progress; 12 | private boolean printing = false; 13 | private Thread thread; 14 | private String message = ""; 15 | private String lastMessage = ""; 16 | 17 | private final DateFormat timeformat = DateFormat 18 | .getTimeInstance(DateFormat.MEDIUM); 19 | 20 | public ProgressPrinter(Progress p) { 21 | progress = p; 22 | } 23 | 24 | public void setMessage(String message) { 25 | this.message = message; 26 | } 27 | 28 | public void setPrintIntervalMillis(int printIntervalMillis) { 29 | this.printIntervalMillis = printIntervalMillis; 30 | } 31 | 32 | @Override 33 | public void run() { 34 | if (progress.getStartTime().getTime() <= 0) { 35 | LOG.debug("Progress timer was not started by caller, starting it now to get sensible ETA figures."); 36 | progress.startTimer(); 37 | } 38 | 39 | while (printing) { 40 | print(); 41 | sleep(); 42 | } 43 | } 44 | 45 | private void print() { 46 | String eta = timeformat.format(progress.getETA()); 47 | String toprint = message + " [" + progress.getCompletePercentage() 48 | + "%, ETA " + eta + "]"; 49 | toprint = toprint.trim(); 50 | 51 | if (!toprint.equals(lastMessage)) { 52 | // Only print if information changed. 53 | for (int i = 0; i < lastMessage.length(); i++) { 54 | // Clear old message with backspaces (does not work in some consoles) 55 | System.out.print('\b'); 56 | } 57 | System.out.print(toprint); 58 | lastMessage = toprint; 59 | } 60 | } 61 | 62 | private void sleep() { 63 | try { 64 | Thread.sleep(printIntervalMillis); 65 | } catch (InterruptedException e) { 66 | // ignore and continue 67 | Thread.currentThread().interrupt(); 68 | } 69 | } 70 | 71 | public void start() { 72 | printing = true; 73 | thread = new Thread(this); 74 | thread.start(); 75 | } 76 | 77 | public void stop() { 78 | if (!printing) { 79 | return; 80 | } 81 | 82 | printing = false; 83 | 84 | while (thread != null && thread.isAlive()) { 85 | try { 86 | thread.join(); 87 | } catch (InterruptedException e) { 88 | // ignore, retry 89 | Thread.currentThread().interrupt(); 90 | } 91 | } 92 | print(); // Make sure 100% is printed 93 | thread = null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/Hasher.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import org.castor.core.util.Base64Encoder; 4 | 5 | import javax.crypto.SecretKeyFactory; 6 | import javax.crypto.spec.PBEKeySpec; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.IOException; 9 | import java.io.ObjectOutputStream; 10 | import java.nio.charset.Charset; 11 | import java.nio.charset.StandardCharsets; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.security.spec.InvalidKeySpecException; 14 | import java.security.spec.KeySpec; 15 | 16 | 17 | public class Hasher { 18 | 19 | // Ideally more iterations mean better protection against brute 20 | // force attacks. Since synonyms are accross the while dataset, 21 | // an attacker would only know the generated synonym type based 22 | // on the synonym file, but not the source date type or location. 23 | private static final int ITERATIONS = 64; 24 | 25 | // Roughly same space as version 4 UUID, more than 5 undecillion combinations, 26 | // very unlikely to generate collisions. 27 | private static final int SIZE = 128; 28 | 29 | private static final String ALGORITHM = "PBKDF2WithHmacSHA1"; 30 | public static final Charset CHARSET = StandardCharsets.UTF_8; 31 | private byte[] salt; 32 | 33 | public Hasher(String salt) { 34 | this.salt = salt.getBytes(); 35 | } 36 | 37 | public String base64Hash(Object object) { 38 | byte[] serialize = serialize(object); 39 | char[] chars = toCharArray(serialize); 40 | return new String(Base64Encoder.encode(pbkdf2(chars))); 41 | } 42 | 43 | public String base64Hash(String object) { 44 | byte[] hash = pbkdf2(object.toCharArray()); 45 | return new String(Base64Encoder.encode(hash)); 46 | } 47 | 48 | private char[] toCharArray(byte[] bytes) { 49 | return new String(bytes, CHARSET).toCharArray(); 50 | } 51 | 52 | private byte[] serialize(Object object) { 53 | try { 54 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 55 | ObjectOutputStream objectOutputStream = new ObjectOutputStream(out); 56 | objectOutputStream.writeObject(object); 57 | objectOutputStream.close(); 58 | return out.toByteArray(); 59 | } catch (IOException e) { 60 | throw new RuntimeException("Unexpected problem serializing object.", e); 61 | } 62 | } 63 | 64 | private byte[] pbkdf2(char[] password) { 65 | KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, SIZE); 66 | try { 67 | SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM); 68 | return f.generateSecret(spec).getEncoded(); 69 | } catch (NoSuchAlgorithmException ex) { 70 | throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex); 71 | } catch (InvalidKeySpecException ex) { 72 | throw new IllegalStateException("Invalid SecretKeyFactory", ex); 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/Anonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | 5 | import java.sql.Date; 6 | import java.util.Map; 7 | 8 | /** 9 | * Provides functionality for consitently anonymizing a piece of data. 10 | *

11 | * Implementations of this interface must make sure that anonymization is done 12 | * in a reproducable manner. That is, if A transforms into B, it has to 13 | * consistently do so on each and every call. 14 | *

15 | * By doing this, anonimatron can guarantee that data is transformed 16 | * consistently accross all tables of the database, and referential constraints 17 | * can be re-enforced after anonymization. 18 | */ 19 | public interface Anonymizer { 20 | 21 | /** 22 | * @return The ID or name of this anonymizer, as used in the XML 23 | * configuration file. This is generally something along the lines 24 | * of "LASTNAME" or "UNEVENNUMBER". Please see the output of the 25 | * -configexample command line option when running Anonimatron. 26 | */ 27 | String getType(); 28 | 29 | /** 30 | * Anonymizes the given data into a non-tracable, non-reversible synonym, 31 | * and does it consistently, so that A always translates to B. 32 | * 33 | * @param from the data to be anonymized, usually passed in as a 34 | * {@link String}, {@link Integer}, {@link Date} or other classes 35 | * which can be stored in a single JDBC database column. 36 | * @param size the optional maximum size of the generated value 37 | * @param shortlived indicates that the generated synonym must have the 38 | * {@link Synonym#isShortLived()} boolean set 39 | * @return a {@link Synonym} 40 | */ 41 | Synonym anonymize(Object from, int size, boolean shortlived); 42 | 43 | /** 44 | * Anonymizes the given data into a non-tracable, non-reversible synonym 45 | * with the provided parameters, and does it consistently, so that A 46 | * always translates to B. 47 | * @param from the data to be anonymized, usually passed in as a 48 | * {@link String}, {@link Integer}, {@link java.sql.Date} or other classes 49 | * which can be stored in a single JDBC database column. 50 | * @param size the optional maximum size of the generated value 51 | * @param shortlived indicates that the generated synonym must have the 52 | * {@link Synonym#isShortLived()} boolean set 53 | * @param parameters the parameters to be used by the anonymizer 54 | * @return a {@link Synonym} 55 | */ 56 | default Synonym anonymize(Object from, int size, boolean shortlived, Map parameters) { 57 | return anonymize(from, size, shortlived); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/CharacterStringPrefetchAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import junit.framework.TestCase; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class CharacterStringPrefetchAnonymizerTest extends TestCase { 10 | 11 | CharacterStringPrefetchAnonymizer anonimyzer; 12 | 13 | @Override 14 | protected void setUp() throws Exception { 15 | super.setUp(); 16 | anonimyzer = new CharacterStringPrefetchAnonymizer(); 17 | } 18 | 19 | public void testPrefetch() { 20 | String sourceData = "ABC"; 21 | anonimyzer.prefetch(sourceData); 22 | 23 | String from = "TEST1"; 24 | Synonym synonym = anonimyzer.anonymize(from, 5, false); 25 | 26 | String to = (String) synonym.getTo(); 27 | assertEquals("from and to string lengths did not match", from.length(), to.length()); 28 | 29 | for (int i = 0; i < to.length(); i++) { 30 | assertTrue("'to' string contained characters which were not in the source data." 31 | , sourceData.indexOf(to.charAt(i)) > -1); 32 | } 33 | } 34 | 35 | public void testParameterizedCharacterString() { 36 | String from = "TEST1"; 37 | String characters = "#$%"; 38 | 39 | Map m = new HashMap<>(); 40 | m.put(CharacterStringAnonymizer.PARAMETER, characters); 41 | Synonym synonym = anonimyzer.anonymize(from, 5, false, m); 42 | 43 | String to = (String) synonym.getTo(); 44 | for (int i = 0; i < to.length(); i++) { 45 | assertTrue("'to' string contained characters which were not in the parameter." 46 | , characters.indexOf(to.charAt(i)) > -1); 47 | } 48 | } 49 | 50 | public void testWrongParameter() { 51 | try { 52 | anonimyzer.anonymize("any", 10, false, null); 53 | 54 | anonimyzer.anonymize("any", 10, false, new HashMap<>()); 55 | 56 | anonimyzer.anonymize("any", 10, false, new HashMap() {{ 57 | put("PaRaMeTeR", "any"); 58 | }}); 59 | 60 | fail("Using the wrong parameters should throw an exception."); 61 | } catch (UnsupportedOperationException e) { 62 | assertTrue("Exception should point to the character parameter", 63 | e.getMessage().contains(CharacterStringAnonymizer.PARAMETER)); 64 | } 65 | } 66 | 67 | public void testPrefetchNull() { 68 | anonimyzer.prefetch(null); 69 | String from = "DUMMY"; 70 | Synonym synonym = anonimyzer.anonymize(from, 5, false); 71 | 72 | String to = (String) synonym.getTo(); 73 | assertEquals("from and to string lengths did not match", from.length(), to.length()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/documentation/anonymizerlist.md: -------------------------------------------------------------------------------- 1 | # Available Anonymizers 2 | 3 | Anonimatron comes with the following default anonymizers. Please start Anonimatron with the `-configexample` 4 | parameter to see how these are configured. For more information on how Anonimatron works and runs, check our [quickstart](index.md). 5 | 6 | | Name | Type | Input | Output | 7 | |:----------------------------------|:--------------------|:-----------------|:-------------------------------------------------------------------------| 8 | | CharacterStringAnonymizer | RANDOMCHARACTERS | Any string | A-Z, same length | 9 | | CharacterStringPrefetchAnonymizer | RANDOMCHARACTERS | Any string | Characters from all input data, same length | 10 | | CountryCodeAnonymizer | COUNTRY_CODE | Any string | ISO 3166-1 alpha 2 or alpha 3 code | 11 | | DateAnonymizer | DATE | Valid date | Date between 31 days before and 32 days after the input date | 12 | | DigitStringAnonymizer | RANDOMDIGITS | Any string | 0-9, same length, optional mask | 13 | | DutchBankAccountAnononymizer | DUTCHBANKACCOUNT | Any string | 11 proof number, minimal 9 digits | 14 | | DutchBSNAnononymizer | BURGERSERVICENUMMER | Number or string | Valid Dutch "Burger Service Nummer" or "SOFI Nummer" as number or string | 15 | | DutchZipCodeAnonymizer | DUTCH_ZIP_CODE | Any string | Valid Dutch zip/postal code | 16 | | ElvenNameGenerator | ELVEN_NAME | Any string | Pronounceable elven name, 2 to 5 syllables | 17 | | EmailAddressAnonymizer | EMAIL_ADDRESS | Any string | Valid email address in the domain "@example.com" | 18 | | IbanAnonymizer | IBAN | Any string | Valid International Bank Account Number | 19 | | RomanNameGenerator | ROMAN_NAME | Any string | Pronounceable Roman name, 2 to 5 syllables | 20 | | StringAnonymizer | STRING | Any string | Random hexadecimal string | 21 | | UkPostCodeAnonymizer | UK_POST_CODE | Any string | Valid Uk Post code | 22 | | UUIDAnonymizer | UUID | Any string | A random UUID | -------------------------------------------------------------------------------- /src/test/java/com/javamonitor/tools/Stopwatch.java: -------------------------------------------------------------------------------- 1 | package com.javamonitor.tools; 2 | 3 | import org.apache.log4j.Level; 4 | import org.apache.log4j.Logger; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * A simple stopwatch timer to trace slow method calls. 10 | * This is a faster and leaner alternative to {@link org.springframework.util.StopWatch}, 11 | * please note the differences in usage and output. 12 | * 13 | * @author Kees Jan Koster <kjkoster@kjkoster.org> 14 | */ 15 | public final class Stopwatch implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | private static final Logger log = Logger.getLogger(Stopwatch.class); 19 | 20 | private final long start; 21 | private long lastTime; 22 | private Level loglevel = Level.WARN; 23 | 24 | private final StringBuilder message = new StringBuilder(); 25 | 26 | /** 27 | * Start a new stopwatch, specifying the class we work for. 28 | * 29 | * @param clazz The class we work for. 30 | */ 31 | public Stopwatch(final Class clazz) { 32 | this(clazz.getName()); 33 | } 34 | 35 | /** 36 | * Start a new stopwatch, with a custom name. 37 | * 38 | * @param name The name of this Stopwatch 39 | */ 40 | public Stopwatch(final String name) { 41 | super(); 42 | 43 | start = System.currentTimeMillis(); 44 | lastTime = start; 45 | 46 | message.append("entering ").append(name).append(" took "); 47 | } 48 | 49 | /** 50 | * Mark the time of the operation that we are about to perform. 51 | * 52 | * @param operation The operation we are about to perform. 53 | */ 54 | public void aboutTo(final String operation) { 55 | final long now = System.currentTimeMillis(); 56 | final long timeDiff = now - lastTime; 57 | lastTime = now; 58 | 59 | message.append(timeDiff).append("; ").append(operation) 60 | .append(" took "); 61 | } 62 | 63 | /** 64 | * Stop the stopwatch, logging the events in case the time was longer than 65 | * the specified threshold time value. This method is typically invoked in a 66 | * finally block. 67 | * 68 | * @param thresholdMillis The threshold above which we print the events. 69 | * @return true if the operation completes within the specified time 70 | */ 71 | public boolean stop(final long thresholdMillis) { 72 | final long now = System.currentTimeMillis(); 73 | final long timeDiff = now - lastTime; 74 | lastTime = now; 75 | 76 | long total = now - start; 77 | message.append(timeDiff).append(". Total: ").append(total).append(" ms."); 78 | 79 | if ((total) > thresholdMillis) { 80 | log.log(loglevel, message); 81 | return false; 82 | } 83 | return true; 84 | } 85 | 86 | public String getMessage(){ 87 | return message.toString(); 88 | } 89 | 90 | public Stopwatch setLoglevel(Level loglevel) { 91 | this.loglevel = loglevel; 92 | return this; 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/UkPostCodeAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.util.regex.Pattern; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | public class UkPostCodeAnonymizerTest { 12 | 13 | private UkPostCodeAnonymizer ukPostCodeAnonymizer; 14 | 15 | @Before 16 | public void setUp() { 17 | ukPostCodeAnonymizer = new UkPostCodeAnonymizer(); 18 | } 19 | 20 | @Test 21 | public void testValidFormatOfGeneratedCodes() { 22 | int numberOfCodes = 10_000; 23 | long max_time_ms = 1000; // Be forgiving here; not all machines perform equally 24 | 25 | long start = System.currentTimeMillis(); 26 | for (int i = 0; i < 10_000; i++) { 27 | assertValidPostalCode(ukPostCodeAnonymizer.buildZipCode()); 28 | } 29 | long stop = System.currentTimeMillis(); 30 | long duration = stop - start; 31 | 32 | assertTrue("Generating " + numberOfCodes + " took " + duration + ".", duration < max_time_ms); 33 | } 34 | 35 | @Test 36 | public void testPostalCodeFlavor1() { 37 | assertValidPostalCode(ukPostCodeAnonymizer.buildZipCodeFlavor1()); 38 | } 39 | 40 | @Test 41 | public void testPostalCodeFlavor2() { 42 | assertValidPostalCode(ukPostCodeAnonymizer.buildZipCodeFlavor2()); 43 | } 44 | 45 | 46 | private void assertValidPostalCode(String s) { 47 | 48 | if (s.length() > 8) { 49 | fail("length longer than 8"); 50 | } 51 | 52 | // See https://stackoverflow.com/a/164994 53 | String regex = "^([Gg][Ii][Rr] ?0[Aa]{2})|" + 54 | "((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))" + 55 | " ?[0-9][A-Za-z]{2})$"; 56 | 57 | Pattern regexPattern = Pattern.compile(regex); 58 | assertTrue("Invalid postal code generated: '" + s + "'", regexPattern.matcher(s).matches()); 59 | } 60 | 61 | @Test 62 | public void testGenerateCorrectSynonym() { 63 | Synonym s1 = ukPostCodeAnonymizer.anonymize("x", 10, false); 64 | assertEquals(ukPostCodeAnonymizer.getType(), s1.getType()); 65 | assertNotEquals(s1.getFrom(), s1.getTo()); 66 | assertFalse(s1.isShortLived()); 67 | 68 | Synonym s2 = ukPostCodeAnonymizer.anonymize("x", 10, true); 69 | assertEquals(ukPostCodeAnonymizer.getType(), s2.getType()); 70 | assertNotEquals(s2.getFrom(), s2.getTo()); 71 | assertTrue(s2.isShortLived()); 72 | 73 | assertNotEquals(s1.getTo(), s2.getTo()); 74 | } 75 | 76 | @Test 77 | public void testGenerateErrorForShortCodes() { 78 | try { 79 | ukPostCodeAnonymizer.anonymize("x", 5, false); 80 | fail("Should throw a configuration error."); 81 | } catch (UnsupportedOperationException e) { 82 | assertTrue(e.getMessage().contains("will not always fit")); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/com/javamonitor/tools/StopwatchNano.java: -------------------------------------------------------------------------------- 1 | package com.javamonitor.tools; 2 | 3 | import org.apache.log4j.Level; 4 | import org.apache.log4j.Logger; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * A simple stopwatch timer to trace slow method calls. 10 | * This is a faster and leaner alternative to {@link org.springframework.util.StopWatch}, 11 | * please note the differences in usage and output. 12 | * 13 | * @author Kees Jan Koster <kjkoster@kjkoster.org> 14 | */ 15 | public final class StopwatchNano implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | private static final Logger log = Logger.getLogger(StopwatchNano.class); 19 | 20 | private final long startNanos; 21 | private long lastTimeNanos; 22 | private Level loglevel = Level.WARN; 23 | 24 | private final StringBuilder message = new StringBuilder(); 25 | 26 | /** 27 | * Start a new stopwatch, specifying the class we work for. 28 | * 29 | * @param clazz The class we work for. 30 | */ 31 | public StopwatchNano(final Class clazz) { 32 | this(clazz.getName()); 33 | } 34 | 35 | /** 36 | * Start a new stopwatch, with a custom name. 37 | * 38 | * @param name The name of this Stopwatch 39 | */ 40 | public StopwatchNano(final String name) { 41 | super(); 42 | 43 | startNanos = System.nanoTime(); 44 | lastTimeNanos = startNanos; 45 | 46 | message.append("entering ").append(name).append(" took "); 47 | } 48 | 49 | /** 50 | * Mark the time of the operation that we are about to perform. 51 | * 52 | * @param operation The operation we are about to perform. 53 | */ 54 | public void aboutTo(final String operation) { 55 | final long now = System.nanoTime(); 56 | final long timeDiff = now - lastTimeNanos; 57 | lastTimeNanos = now; 58 | 59 | message.append(timeDiff).append("; ").append(operation) 60 | .append(" took "); 61 | } 62 | 63 | /** 64 | * Stop the stopwatch, logging the events in case the time was longer than 65 | * the specified threshold time value. This method is typically invoked in a 66 | * finally block. 67 | * 68 | * @param thresholdMillis The threshold above which we print the events. 69 | * @return true if the operation completes within the specified time 70 | */ 71 | public boolean stop(final long thresholdMillis) { 72 | final long now = System.nanoTime(); 73 | final long timeDiff = now - lastTimeNanos; 74 | lastTimeNanos = now; 75 | 76 | long total = now - startNanos; 77 | message.append(timeDiff).append(". Total: ").append(total).append(" ns."); 78 | 79 | if ((total) > (thresholdMillis * 1_000_000)) { 80 | log.log(loglevel, message); 81 | return false; 82 | } 83 | return true; 84 | } 85 | 86 | public String getMessage() { 87 | return message.toString(); 88 | } 89 | 90 | public StopwatchNano setLoglevel(Level loglevel) { 91 | this.loglevel = loglevel; 92 | return this; 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /resources/anonymizers/README.md: -------------------------------------------------------------------------------- 1 | In this directory, you can put jar files containing your own custom 2 | anonymizers. When you do so, don't forget to register them in your config.xml 3 | so that Anonimatron can instantiate them. Examples of an Anonymizer 4 | and a config below: 5 | 6 | This is an example of an anonymizer which returns lower case Strings for 7 | each String passed in. In the root of the unzipped anonimatron project, create a file called `my/package/ToLowerAnonymizer.java`: 8 | 9 | ```java 10 | package my.package; 11 | 12 | import com.rolfje.anonimatron.synonyms.StringSynonym; 13 | import com.rolfje.anonimatron.synonyms.Synonym; 14 | import com.rolfje.anonimatron.anonymizer.Anonymizer; 15 | 16 | public class ToLowerAnonymizer implements Anonymizer { 17 | 18 | @Override 19 | public String getType() { 20 | return "TO_LOWER_CASE"; 21 | } 22 | 23 | @Override 24 | public Synonym anonymize(Object from, int size) { 25 | StringSynonym s = new StringSynonym(); 26 | s.setFrom(from); 27 | s.setTo(((String)from).toLowerCase()); 28 | return s; 29 | } 30 | } 31 | ``` 32 | 33 | Now create a `.class` file with the command: `javac -classpath ./libraries/anonimatron-[version].jar my/package/ToLowerAnonymizer.java`, and a `.jar` file with `jar cvf toloweranonymizer.jar`. Move this `.jar` file into the `anonymizers` folder, and you are ready to user `TO_LOWER_CASE` in your config.xml as seen below. 34 | 35 | If you need an anonymizer with parameters, you can define it like so: 36 | 37 | ```java 38 | package my.packager; 39 | 40 | import java.util.HashMap; 41 | import java.util.Map; 42 | 43 | import com.rolfje.anonimatron.synonyms.StringSynonym; 44 | import com.rolfje.anonimatron.synonyms.Synonym; 45 | import com.rolfje.anonimatron.anonymizer.Anonymizer; 46 | 47 | public class FixedValueAnonymizer implements Anonymizer { 48 | @Override 49 | public Synonym anonymize(Object from, int size, boolean shortlived) { 50 | return anonymize(from, size, shortlived, new HashMap<>()); 51 | } 52 | 53 | @Override 54 | public Synonym anonymize(Object from, int size, boolean shortlived, Map parameters) { 55 | if (parameters == null || !parameters.containsKey("value")) { 56 | throw new UnsupportedOperationException("no value"); 57 | } 58 | return new StringSynonym(getType(), 59 | (String) from, 60 | parameters.get("value"), 61 | shortlived); 62 | } 63 | 64 | @Override 65 | public String getType() { 66 | return "FIXED"; 67 | } 68 | } 69 | 70 | ``` 71 | 72 | This is how you add it to your config.xml: 73 | 74 | ```xml 75 | 76 | 78 | my.package.ToLowerAnonymizer 79 | my.package.FixedValueAnonymizer 80 | 81 | 82 | 83 | testValue 84 | 85 |
86 |
87 | ``` 88 | 89 | Have fun experimenting! 90 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # GDPR compliant testing. 2 | 3 | Did you ever have that problem where you needed "production data" to find a bug 4 | or do performance tests outside of the client’s production environment? Are 5 | you worried about protecting that data? Do you add screenshots to your bug 6 | reports? Can you live with surrogate data with the same properties? Then this 7 | tool is for you. 8 | 9 |

10 | DB2 11 | Derby 12 | Firebird 13 | Informix 14 | Interbase 15 | MariaDB 16 | SQL Server/MsSQL 17 | MySQL 18 | Oracle 8+ 19 | PointBase 20 | PostgreSQL 21 | Sybase 22 | csv files 23 | xml files 24 |
25 | 26 | 27 | # Features 28 | 29 | - Anonymize data in databases and files. 30 | - Generates fake email addresses, fake Roman names, and UUID’s out of the box. 31 | - Easy to configure, automatically generates example config file. 32 | - Anonymized data is consistent between runs. No need to re-write your tests 33 | to handle random data. 34 | - Extendable, easily implement and add your own anonymization handlers 35 | - 100% Java 1.8, multi platform, runs on Windows, Mac OSX, Linux derivatives. 36 | - Multi database, uses SQL92 standards and supports Oracle, PostgreSQL and 37 | MySQL out of the box. Anonimatron will autodetect the following JDBC drivers: 38 | DB2, MsSQL, Cloudscape, Pointbase, Firebird, IDS, Informix, Enhydra, 39 | Interbase, Hypersonic, jTurbo, SQLServer and Sybase. 40 | - Available as library at 41 | [Maven Central](https://search.maven.org/search?q=g:%22com.rolfje.anonimatron%22%20AND%20a:%22anonimatron%22). 42 | - 100% free of charge 43 | 44 | 45 | # Open Source 46 | 47 | Anonimatron is an open source project, and gets better with your help. Do you 48 | think you have written an interesting extension, do you want to write useful 49 | documentation, or do you have other suggestions? Please let us now by 50 | [filing a feature request, a bug report](https://github.com/realrolfje/anonimatron/issues), or [join the project at github.com](https://github.com/realrolfje/anonimatron). 51 | 52 | # DISCLAIMER 53 | Even if properly 54 | configured, the output of the Anonimatron anonymization process may contain 55 | certain (statistical) properties of the input dataset. That means that even after 56 | Anonymization, you should take reasonable care in protecting the Anonymized 57 | data to prevent leaks. 58 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/AnonymizerServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.configuration.Column; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | import junit.framework.TestCase; 6 | 7 | import java.sql.Date; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | public class AnonymizerServiceTest extends TestCase { 15 | AnonymizerService anonService; 16 | 17 | protected void setUp() throws Exception { 18 | anonService = new AnonymizerService(); 19 | anonService.registerAnonymizers( 20 | Collections.singletonList( 21 | FixedValueAnonymizer.class.getName())); 22 | } 23 | 24 | public void testStringAnonymizer() { 25 | List fromList = new ArrayList<>(); 26 | fromList.add("String 1"); 27 | fromList.add("String 2"); 28 | fromList.add("String 3"); 29 | 30 | String type = new StringAnonymizer().getType(); 31 | 32 | testAnonymizer(fromList, type, type); 33 | } 34 | 35 | public void testUUIDAnonymizer() { 36 | List fromList = new ArrayList<>(); 37 | fromList.add("String 1"); 38 | fromList.add("String 2"); 39 | fromList.add("String 3"); 40 | 41 | String type = new UUIDAnonymizer().getType(); 42 | 43 | testAnonymizer(fromList, type, type); 44 | } 45 | 46 | public void testDateAnonymizer() { 47 | 48 | assertEquals(new Date(0), new Date(0)); 49 | 50 | List fromList = new ArrayList<>(); 51 | fromList.add(new Date(0)); 52 | fromList.add(new Date(86400000L)); 53 | fromList.add(new Date(172800000L)); 54 | 55 | String type = Date.class.getName(); 56 | 57 | testAnonymizer(fromList, type, "DATE"); 58 | } 59 | 60 | public void testParameterizedAnonymizer() { 61 | List fromList = new ArrayList<>(); 62 | fromList.add("String 1"); 63 | fromList.add("String 2"); 64 | fromList.add("String 3"); 65 | 66 | String type = new FixedValueAnonymizer().getType(); 67 | 68 | testAnonymizer(fromList, type, type); 69 | } 70 | 71 | private void testAnonymizer(List fromList, String lookupType, String synonymType) { 72 | List toList = new ArrayList<>(); 73 | 74 | Map parameters = new HashMap<>(); 75 | parameters.put("value", "testValue"); 76 | 77 | Column column = new Column("Testcolumn", lookupType, 100, false, parameters); 78 | 79 | // First pass 80 | for (Object from : fromList) { 81 | Synonym s = anonService.anonymize(column, from); 82 | 83 | assertEquals(from, s.getFrom()); 84 | assertEquals(synonymType, s.getType()); 85 | assertNotNull(s.getTo()); 86 | 87 | toList.add(s.getTo()); 88 | } 89 | 90 | // Second pass (consistency check) 91 | for (int i = 0; i < fromList.size(); i++) { 92 | Synonym s = anonService.anonymize(column, fromList.get(i)); 93 | assertEquals(toList.get(i), s.getTo()); 94 | } 95 | 96 | // Test passing in null 97 | assertNull(anonService.anonymize(column, null).getTo()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/AbstractElevenProofAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | 4 | public abstract class AbstractElevenProofAnonymizer implements Anonymizer { 5 | 6 | protected int[] add(int[] digits, int number) { 7 | String numberString = digitsAsString(digits); 8 | int intValue = Integer.parseInt(numberString) + number; 9 | return numberAsDigits(intValue, digits.length); 10 | } 11 | 12 | 13 | /** 14 | * @deprecated Use the more correctly named {@link #digitsAsString(int[])} 15 | */ 16 | @Deprecated 17 | protected String digitsAsNumber(int[] digits) { 18 | return digitsAsString(digits); 19 | } 20 | 21 | protected String digitsAsString(int[] digits) { 22 | String result = ""; 23 | for (int i : digits) { 24 | result += String.valueOf(i); 25 | } 26 | return result; 27 | } 28 | 29 | protected int digitsAsInteger(int[] digits) { 30 | int result = 0; 31 | for (int i = 0; i < digits.length; i++) { 32 | result += (int) Math.pow(10, (digits.length - i - 1)) * digits[i]; 33 | } 34 | return result; 35 | } 36 | 37 | private int[] numberAsDigits(int number, int digits) { 38 | String s = String.valueOf(number); 39 | 40 | while (s.length() < digits) { 41 | s = "0" + s; 42 | } 43 | 44 | int[] digitarray = new int[digits]; 45 | for (int i = 0; i < digitarray.length; i++) { 46 | digitarray[i] = Integer.parseInt("" + s.charAt(i)); 47 | } 48 | 49 | return digitarray; 50 | } 51 | 52 | /** 53 | * For the given digits in the array, calculate the 54 | * seperate 11-proof values. 55 | * 56 | * @param elevennumber an account, sofi or bsn number, with bsnnumber[0] being the 57 | * most significant digit. 58 | * @return An array of equal size, where each number represents the 11 proof 59 | * result for the corresponding digit in the bsnnumber paramter. 60 | */ 61 | protected int[] calculate11proofdigits(int[] elevennumber) { 62 | int[] elevenValues = new int[elevennumber.length]; 63 | for (int i = 0; i < elevennumber.length; i++) { 64 | elevenValues[i] = elevennumber[i] * (elevennumber.length - i); 65 | } 66 | return elevenValues; 67 | } 68 | 69 | protected int sum(int[] digits) { 70 | int sum = 0; 71 | for (int i : digits) { 72 | sum += i; 73 | } 74 | return sum; 75 | } 76 | 77 | protected int[] generate11ProofNumber(int numberOfDigits) { 78 | int[] elevenProof = getRandomDigits(numberOfDigits); 79 | int sum11values = sum(calculate11proofdigits(elevenProof)); 80 | int correctedSum = Math.round((float) sum11values / 11) * 11; 81 | int correction = correctedSum < sum11values ? -1 : +1; 82 | do { 83 | // Correct number and see if we corrected completely. 84 | elevenProof = add(elevenProof, correction); 85 | } while (sum(calculate11proofdigits(elevenProof)) != correctedSum); 86 | return elevenProof; 87 | } 88 | 89 | protected int[] getRandomDigits(int numberOfDigits) { 90 | int[] randomDigits = new int[numberOfDigits]; 91 | for (int i = 0; i < randomDigits.length; i++) { 92 | randomDigits[i] = (int) Math.floor(Math.random() * 10); 93 | } 94 | return randomDigits; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/IbanAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import static org.apache.log4j.Logger.getLogger; 4 | 5 | import java.util.EnumMap; 6 | import java.util.Locale; 7 | import java.util.Map; 8 | 9 | import org.apache.log4j.Logger; 10 | import org.iban4j.CountryCode; 11 | import org.iban4j.Iban; 12 | import org.iban4j.Iban4jException; 13 | import org.iban4j.bban.BbanStructure; 14 | 15 | import com.rolfje.anonimatron.synonyms.StringSynonym; 16 | import com.rolfje.anonimatron.synonyms.Synonym; 17 | 18 | /** 19 | * Generates valid International Bank Account Numbers, or {@code IBAN}'s. 20 | * Tries to use the {@link BankAccountAnonymizer} to generate valid {@code BBAN}'s for the system default country code. 21 | * If no such anonymizer exists, a random account is generated. 22 | *

23 | * 24 | * @author Erik-Berndt Scheper 25 | * @see https://en.wikipedia.org/wiki/International_Bank_Account_Number 26 | */ 27 | public class IbanAnonymizer implements Anonymizer { 28 | 29 | private static final Logger LOGGER = getLogger(IbanAnonymizer.class); 30 | 31 | private static final String TYPE = "IBAN"; 32 | 33 | static final Map BANK_ACCOUNT_ANONYMIZERS; 34 | static final CountryCode DEFAULT_COUNTRY_CODE; 35 | 36 | static { 37 | // initialize bank account anonymizers 38 | BANK_ACCOUNT_ANONYMIZERS = new EnumMap<>(CountryCode.class); 39 | BANK_ACCOUNT_ANONYMIZERS.put(CountryCode.NL, new DutchBankAccountAnononymizer()); 40 | 41 | // set a default country code to be used when the original iban cannot be parsed 42 | CountryCode countryCode = CountryCode.getByCode(Locale.getDefault().getCountry()); 43 | 44 | if (countryCode != null && BbanStructure.forCountry(countryCode) != null) { 45 | DEFAULT_COUNTRY_CODE = countryCode; 46 | } else { 47 | DEFAULT_COUNTRY_CODE = CountryCode.NL; 48 | } 49 | } 50 | 51 | @Override 52 | public String getType() { 53 | return TYPE; 54 | } 55 | 56 | @Override 57 | public Synonym anonymize(Object from, int size, boolean shortlived) { 58 | 59 | CountryCode countryCode = getCountryCode(from); 60 | String result = generateIban(countryCode); 61 | 62 | return new StringSynonym( 63 | getType(), 64 | (String) from, 65 | result, 66 | shortlived 67 | ); 68 | } 69 | 70 | String generateIban(CountryCode countryCode) { 71 | BankAccountAnonymizer bankAccountAnonymizer = BANK_ACCOUNT_ANONYMIZERS.get(countryCode); 72 | 73 | String accountNumber = null; 74 | String bankCode = null; 75 | if (bankAccountAnonymizer != null) { 76 | accountNumber = "0" + bankAccountAnonymizer.generateBankAccount(9); 77 | bankCode = bankAccountAnonymizer.generateBankCode(); 78 | } 79 | 80 | Iban iban = new Iban.Builder(). 81 | countryCode(countryCode). 82 | accountNumber(accountNumber). 83 | bankCode(bankCode). 84 | buildRandom(); 85 | 86 | return iban.toString(); 87 | } 88 | 89 | private CountryCode getCountryCode(Object from) { 90 | CountryCode countryCode = DEFAULT_COUNTRY_CODE; 91 | 92 | if (from instanceof String) { 93 | try { 94 | Iban iban = Iban.valueOf((String) from); 95 | 96 | // verify this country code is supported for BBAN randomisation 97 | if (BbanStructure.forCountry(iban.getCountryCode()) == null) { 98 | countryCode = iban.getCountryCode(); 99 | } 100 | 101 | } catch (Iban4jException ibex) { 102 | // ignore 103 | LOGGER.trace("Ignoring IBAN value " + from, ibex); 104 | } 105 | } 106 | 107 | return countryCode; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/DigitStringAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | import java.util.Map; 7 | 8 | public class DigitStringAnonymizer extends AbstractElevenProofAnonymizer { 9 | 10 | final static String PARAMETER = "mask"; 11 | 12 | @Override 13 | public String getType() { 14 | return "RANDOMDIGITS"; 15 | } 16 | 17 | @Override 18 | public Synonym anonymize(Object from, int size, boolean shortlived, Map parameters) { 19 | if (parameters == null || parameters.isEmpty()) { 20 | return anonymize(from, size, shortlived); 21 | } else if (!parameters.containsKey(PARAMETER)) { 22 | throw new UnsupportedOperationException("Please provide '" + PARAMETER + "' with a digit mask in the form 111******, where only stars are replaced with random characters."); 23 | } 24 | return anonymizeMasked(from, size, shortlived, parameters.get(PARAMETER)); 25 | } 26 | 27 | @Override 28 | public Synonym anonymize(Object from, int size, boolean shortlived) { 29 | if (from == null) { 30 | return new StringSynonym( 31 | getType(), 32 | null, 33 | null, 34 | shortlived 35 | ); 36 | } 37 | 38 | int length = from.toString().length(); 39 | 40 | int[] digits = getRandomDigits(length); 41 | String to = digitsAsString(digits); 42 | 43 | return new StringSynonym( 44 | getType(), 45 | (String) from, 46 | to, 47 | shortlived 48 | ); 49 | } 50 | 51 | /** 52 | * Anonymizes a string with digits into a new string of digits where only a part 53 | * of the original digits are changed. You can use this to mask out a certain part 54 | * of the original string, like an area code of a phone number, or the bank number 55 | * of a credit card number. 56 | *

57 | * A mask looks like "111***", where digits remain original, and stars (or other 58 | * characters) are replaced with random digits. An example: 59 | *

60 | * Original number: 555-1234 61 | * Mask: 1111**** (note that the '-' is also untouched by placing a digit there) 62 | * New number : 555-9876 63 | *

64 | * Note that if the mask is shorter than the input string, the output string will 65 | * have random digits where there is no mask. In short: only the locations where 66 | * there is a digit in the mask are left untouched and will be in the output. 67 | * 68 | * @param from 69 | * @param size 70 | * @param shortlived 71 | * @param mask 72 | * @return 73 | */ 74 | private Synonym anonymizeMasked(Object from, int size, boolean shortlived, String mask) { 75 | if (from == null) { 76 | return new StringSynonym( 77 | getType(), 78 | null, 79 | null, 80 | shortlived 81 | ); 82 | } 83 | 84 | char[] fromChars = from.toString().toCharArray(); 85 | char[] toChars = new char[fromChars.length]; 86 | 87 | for (int i = 0; i < fromChars.length; i++) { 88 | if (i < mask.length() && Character.isDigit(mask.charAt(i))) { 89 | toChars[i] = fromChars[i]; 90 | } else { 91 | toChars[i] = Character.forDigit((int) Math.floor(Math.random() * 10), 10); 92 | } 93 | } 94 | 95 | return new StringSynonym( 96 | getType(), 97 | String.valueOf(fromChars), 98 | String.valueOf(toChars), 99 | shortlived 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/integrationtests/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.integrationtests; 2 | 3 | import com.rolfje.anonimatron.Anonimatron; 4 | import com.rolfje.anonimatron.jdbc.AbstractInMemoryHsqlDbTest; 5 | import org.apache.log4j.Logger; 6 | 7 | import java.io.*; 8 | import java.nio.charset.StandardCharsets; 9 | import java.sql.PreparedStatement; 10 | import java.sql.ResultSet; 11 | 12 | public class IntegrationTest extends AbstractInMemoryHsqlDbTest { 13 | Logger LOG = Logger.getLogger(IntegrationTest.class); 14 | 15 | private File configFile; 16 | private File synonymFile; 17 | 18 | @Override 19 | protected void setUp() throws Exception { 20 | super.setUp(); 21 | configFile = createConfigurationFile(); 22 | synonymFile = File.createTempFile("anonimatron-synonyms", ".xml"); 23 | 24 | createDatabase(); 25 | } 26 | 27 | @Override 28 | protected void tearDown() throws Exception { 29 | assertTrue("Could not delete temporary configuration.", configFile.delete()); 30 | assertTrue("Could not delete temporary synonym file.", synonymFile.delete()); 31 | super.tearDown(); 32 | } 33 | 34 | public void testAnonimatron() throws Exception { 35 | runAnonimatron(configFile.getAbsolutePath(), synonymFile.getAbsolutePath()); 36 | resultSetIsAnonymized("TABLE1", "COL1"); 37 | resultSetIsAnonymized("TESTSCHEMA.TABLE2", "COL1"); 38 | } 39 | 40 | private void createDatabase() throws Exception { 41 | executeSql("create table TABLE1 (COL1 VARCHAR(200), ID IDENTITY)"); 42 | PreparedStatement p = connection 43 | .prepareStatement("insert into TABLE1 (COL1) values (?)"); 44 | for (int i = 0; i < 100; i++) { 45 | p.setString(1, "varcharstring-" + i); 46 | p.execute(); 47 | } 48 | 49 | executeSql("create schema TESTSCHEMA authorization DBA"); 50 | executeSql("create table TESTSCHEMA.TABLE2 (COL1 VARCHAR(200), ID IDENTITY)"); 51 | p = connection 52 | .prepareStatement("insert into TESTSCHEMA.TABLE2 (COL1) values (?)"); 53 | for (int i = 0; i < 100; i++) { 54 | p.setString(1, "varcharstring-" + i); 55 | p.execute(); 56 | } 57 | 58 | LOG.info("Created test database."); 59 | } 60 | 61 | private void resultSetIsAnonymized(String table, String column) throws Exception { 62 | PreparedStatement preparedStatement = connection.prepareStatement("select * from " + table); 63 | preparedStatement.execute(); 64 | ResultSet resultSet = preparedStatement.getResultSet(); 65 | int rowcount = 0; 66 | while (resultSet.next()) { 67 | rowcount++; 68 | String value = resultSet.getString(column); 69 | assertFalse(value.startsWith("varcharstring-")); 70 | assertTrue(value.endsWith("@example.com")); 71 | } 72 | assertEquals("Not all rows accounted for", 100, rowcount); 73 | } 74 | 75 | /** 76 | * Copies the integrationconfig.xml file from the classpath into a 77 | * temportary system file. 78 | * 79 | * @return 80 | * @throws Exception 81 | */ 82 | private File createConfigurationFile() throws Exception { 83 | LOG.debug("Copying " + IntegrationTest.class.getResource("integrationconfig.xml") + " to a tempfile."); 84 | InputStream stream = IntegrationTest.class.getResourceAsStream("integrationconfig.xml"); 85 | assertNotNull(stream); 86 | 87 | BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); 88 | 89 | File tempConfig = File.createTempFile("anonimatron-config", ".xml"); 90 | BufferedWriter writer = new BufferedWriter(new FileWriter(tempConfig)); 91 | 92 | while (reader.ready()) { 93 | writer.write(reader.readLine()); 94 | } 95 | 96 | reader.close(); 97 | writer.flush(); 98 | writer.close(); 99 | 100 | return tempConfig; 101 | } 102 | 103 | private void runAnonimatron(String configFile, String synonymFile) 104 | throws Exception { 105 | String[] arguments = new String[] { "-config", configFile, "-synonyms", 106 | synonymFile }; 107 | 108 | Anonimatron.main(arguments); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/DutchBSNAnononymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.Synonym; 4 | import com.rolfje.junit.Asserts; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.math.BigDecimal; 9 | import java.math.BigInteger; 10 | 11 | import static com.rolfje.anonimatron.anonymizer.DutchBSNAnononymizer.isValidBSN; 12 | import static org.junit.Assert.*; 13 | 14 | public class DutchBSNAnononymizerTest { 15 | 16 | private DutchBSNAnononymizer bsnAnonymizer = new DutchBSNAnononymizer(); 17 | 18 | private String original; 19 | boolean shortlived = true; 20 | 21 | 22 | @Before 23 | public void setUp() { 24 | original = bsnAnonymizer.generateBSN(9); 25 | } 26 | 27 | @Test 28 | public void testAnonymizeNumbers() { 29 | Object[] originals = { 30 | Integer.valueOf(original), 31 | Long.valueOf(original), 32 | new BigDecimal(original), 33 | new Integer(original) 34 | }; 35 | 36 | for (Object originalAsNumber : originals) { 37 | Synonym synonym = bsnAnonymizer.anonymize(originalAsNumber, 9, shortlived); 38 | assertEquals(originalAsNumber, synonym.getFrom()); 39 | assertNotEquals(originalAsNumber, synonym.getTo()); 40 | Class expectedClass = originalAsNumber.getClass(); 41 | Object actualClass = synonym.getTo(); 42 | Asserts.assertInstanceOf(expectedClass, actualClass); 43 | assertEquals(shortlived, synonym.isShortLived()); 44 | validate(synonym); 45 | } 46 | } 47 | 48 | 49 | @Test 50 | public void testLength() { 51 | bsnAnonymizer.anonymize("dummy", 10000, false); 52 | bsnAnonymizer.anonymize("dummy", 9, false); 53 | 54 | try { 55 | bsnAnonymizer.anonymize("dummy", 8, false); 56 | fail("should throw exception"); 57 | } catch (UnsupportedOperationException e) { 58 | // ok 59 | } 60 | } 61 | 62 | @Test 63 | public void testGenerateBSN() { 64 | boolean hadNonZeroLastDigit = false; 65 | for (int i = 0; i < 1000; i++) { 66 | String bsn = bsnAnonymizer.generateBSN(9); 67 | assertTrue("Incorrect BSN " + bsn, isValidBSN(bsn)); 68 | hadNonZeroLastDigit = hadNonZeroLastDigit || !bsn.endsWith("0"); 69 | } 70 | assertTrue("BSN ending in 0 can pass both BSN and regular 11 proof test.", hadNonZeroLastDigit); 71 | } 72 | 73 | @Test 74 | public void testKnownFailingBSNs() { 75 | String[] invalidBSNs = new String[]{"815098", "9815098", "920006450", "529790203", "223118818",}; 76 | for (String invalidBSN : invalidBSNs) { 77 | assertFalse("Valid BSN reported as invalid: " + invalidBSN, isValidBSN(invalidBSN)); 78 | } 79 | } 80 | 81 | @Test 82 | public void testKnownCorrectBSNs() { 83 | String[] validBSNs = new String[]{"111222333", "123456782"}; 84 | for (String validBSN : validBSNs) { 85 | assertTrue("Correct BSN reported as incorrect: " + validBSN, isValidBSN(validBSN)); 86 | } 87 | } 88 | 89 | private void validate(Synonym synonym) { 90 | assertNotEquals(synonym.getFrom(), synonym.getTo()); 91 | assertNotNull(synonym.getType()); 92 | String burgerServiceNummer = toString(synonym.getTo()); 93 | assertTrue(isValidBSN(burgerServiceNummer)); 94 | } 95 | 96 | private String toString(Object value) { 97 | String toString; 98 | if (value instanceof Integer) { 99 | toString = String.format("%09d", (Integer) value); 100 | } else if (value instanceof Long) { 101 | toString = String.format("%09d", (Long) value); 102 | } else if (value instanceof BigInteger) { 103 | toString = String.format("%09d", ((BigInteger) value).longValue()); 104 | } else if (value instanceof BigDecimal) { 105 | toString = String.format("%09d", ((BigDecimal) value).longValue()); 106 | } else { 107 | toString = value.toString(); 108 | } 109 | return toString; 110 | } 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/commandline/CommandLine.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.commandline; 2 | 3 | import org.apache.commons.cli.CommandLineParser; 4 | import org.apache.commons.cli.DefaultParser; 5 | import org.apache.commons.cli.HelpFormatter; 6 | import org.apache.commons.cli.Options; 7 | import org.apache.commons.cli.ParseException; 8 | 9 | import static com.rolfje.anonimatron.Anonimatron.VERSION; 10 | 11 | public class CommandLine { 12 | 13 | private static final String OPT_JDBCURL = "jdbcurl"; 14 | private static final String OPT_USERID = "userid"; 15 | private static final String OPT_PASSWORD = "password"; 16 | private static final String OPT_CONFIGFILE = "config"; 17 | private static final String OPT_SYNONYMFILE = "synonyms"; 18 | private static final String OPT_DRYRUN = "dryrun"; 19 | private static final String OPT_CONFIGEXAMPLE = "configexample"; 20 | 21 | private static final Options options = new Options() 22 | .addOption(OPT_CONFIGFILE, true, 23 | "The XML Configuration file describing what to anonymize.") 24 | .addOption(OPT_SYNONYMFILE, true, 25 | "The XML file to read/write synonyms to. " 26 | + "If the file does not exist it will be created.") 27 | .addOption(OPT_CONFIGEXAMPLE, false, 28 | "Prints out a demo/template configuration file.") 29 | .addOption(OPT_DRYRUN, false, "Do not make changes to the database.") 30 | .addOption(OPT_JDBCURL, true, 31 | "The JDBC URL to connect to. " + 32 | "If provided, overrides the value in the config file.") 33 | .addOption(OPT_USERID, true, 34 | "The user id for the database connection. " + 35 | "If provided, overrides the value in the config file.") 36 | .addOption(OPT_PASSWORD, true, 37 | "The password for the database connection. " + 38 | "If provided, overrides the value in the config file."); 39 | 40 | private final org.apache.commons.cli.CommandLine clicommandLine; 41 | 42 | public CommandLine(String[] arguments) throws ParseException { 43 | CommandLineParser parser = new DefaultParser(); 44 | clicommandLine = parser.parse(options, arguments); 45 | } 46 | 47 | public static void printHelp() { 48 | System.out 49 | .println("\nThis is Anonimatron " + VERSION + ", a command line tool to consistently \n" 50 | + "replace live data in your database or data files with data which \n" 51 | + "can not easily be traced back to the original data.\n" 52 | + "You can use this tool to transform a dump from a production \n" 53 | + "database into a large representative dataset you can \n" 54 | + "share with your development and test team.\n" 55 | + "The tool can also read files with sensitive data and write\n" 56 | + "consistently anonymized versions of those files to a different location.\n" 57 | + "Use the -configexample command line option to get an idea of\n" 58 | + "what your configuration file needs to look like.\n\n"); 59 | 60 | HelpFormatter helpFormatter = new HelpFormatter(); 61 | helpFormatter.printHelp("java -jar anonimatron.jar", options); 62 | } 63 | 64 | public String getJdbcurl() { 65 | return clicommandLine.getOptionValue(CommandLine.OPT_JDBCURL); 66 | } 67 | 68 | public String getUserid() { 69 | return clicommandLine.getOptionValue(CommandLine.OPT_USERID); 70 | } 71 | 72 | public String getPassword() { 73 | return clicommandLine.getOptionValue(CommandLine.OPT_PASSWORD); 74 | } 75 | 76 | public String getConfigfileName() { 77 | return clicommandLine.getOptionValue(CommandLine.OPT_CONFIGFILE); 78 | } 79 | 80 | public String getSynonymfileName() { 81 | return clicommandLine.getOptionValue(CommandLine.OPT_SYNONYMFILE); 82 | } 83 | 84 | public boolean isDryrun() { 85 | return clicommandLine.hasOption(CommandLine.OPT_DRYRUN); 86 | } 87 | 88 | public boolean isConfigExample() { 89 | return clicommandLine.hasOption(CommandLine.OPT_CONFIGEXAMPLE); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/SynonymCache.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.HashedFromSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | import com.rolfje.anonimatron.synonyms.SynonymMapper; 6 | import org.exolab.castor.mapping.MappingException; 7 | import org.exolab.castor.xml.XMLException; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | public class SynonymCache { 18 | 19 | private Map> synonymCache = new HashMap<>(); 20 | private Hasher hasher; 21 | private long size = 0; 22 | 23 | public SynonymCache() { 24 | } 25 | 26 | /** 27 | * Reads the synonyms from the specified file and (re-)initializes the 28 | * {@link #synonymCache} with it. 29 | * 30 | * @param synonymXMLfile the xml file containing the synonyms, as written by 31 | * {@link #toFile(File)} 32 | * @return Synonyms as the were stored on last run. 33 | * @throws MappingException When synonyms can not be read from file. 34 | * @throws IOException When synonyms can not be read from file. 35 | * @throws XMLException When synonyms can not be read from file. 36 | */ 37 | public static SynonymCache fromFile(File synonymXMLfile) throws MappingException, IOException, XMLException { 38 | 39 | SynonymCache synonymCache = new SynonymCache(); 40 | List synonymsFromFile = SynonymMapper 41 | .readFromFile(synonymXMLfile.getAbsolutePath()); 42 | 43 | for (Synonym synonym : synonymsFromFile) { 44 | synonymCache.put(synonym); 45 | } 46 | 47 | return synonymCache; 48 | } 49 | 50 | /** 51 | * Writes all known {@link Synonym}s in the cache out to the specified file 52 | * in XML format. 53 | * 54 | * @param synonymXMLfile an empty writeable xml file to write the synonyms 55 | * to. 56 | * @throws XMLException When there is a problem writing the synonyms to file. 57 | * @throws IOException When there is a problem writing the synonyms to file. 58 | * @throws MappingException When there is a problem writing the synonyms to file. 59 | */ 60 | public void toFile(File synonymXMLfile) throws XMLException, IOException, MappingException { 61 | List allSynonyms = new ArrayList<>(); 62 | 63 | // Flatten the type -> From -> Synonym map. 64 | Collection> allObjectMaps = synonymCache.values(); 65 | for (Map typeMap : allObjectMaps) { 66 | allSynonyms.addAll(typeMap.values()); 67 | } 68 | 69 | SynonymMapper 70 | .writeToFile(allSynonyms, synonymXMLfile.getAbsolutePath()); 71 | } 72 | 73 | /** 74 | * Stores the given {@link Synonym} in the synonym cache, except when the 75 | * given synonym is short-lived. Short-lived synonyms are not stored or 76 | * re-used. 77 | * 78 | * @param synonym to store 79 | */ 80 | public void put(Synonym synonym) { 81 | if (synonym.isShortLived()) { 82 | return; 83 | } 84 | 85 | Map map = synonymCache.computeIfAbsent(synonym.getType(), k -> new HashMap<>()); 86 | 87 | if (hasher != null) { 88 | // Hash sensitive data before storing 89 | Synonym hashed = new HashedFromSynonym(hasher, synonym); 90 | if (map.put(hashed.getFrom(), hashed) == null) { 91 | size++; 92 | } 93 | } else { 94 | // Store as-is 95 | if (map.put(synonym.getFrom(), synonym) == null) { 96 | size++; 97 | } 98 | } 99 | } 100 | 101 | public Synonym get(String type, Object from) { 102 | Map typemap = synonymCache.get(type); 103 | if (typemap == null) { 104 | return null; 105 | } 106 | 107 | if (hasher != null) { 108 | return typemap.get(hasher.base64Hash(from)); 109 | } else { 110 | return typemap.get(from); 111 | } 112 | } 113 | 114 | public void setHasher(Hasher hasher) { 115 | this.hasher = hasher; 116 | } 117 | 118 | public long size() { 119 | return size; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/AnonimatronTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron; 2 | 3 | import com.rolfje.anonimatron.anonymizer.Hasher; 4 | import com.rolfje.anonimatron.anonymizer.SynonymCache; 5 | import com.rolfje.anonimatron.file.AcceptAllFilter; 6 | import com.rolfje.anonimatron.file.CsvFileWriter; 7 | import com.rolfje.anonimatron.file.Record; 8 | import junit.framework.TestCase; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.File; 12 | import java.io.FileReader; 13 | import java.io.PrintWriter; 14 | 15 | /** 16 | * Unit test for simple App. 17 | */ 18 | public class AnonimatronTest extends TestCase { 19 | 20 | public void testVersion() throws Exception { 21 | File pom = new File("pom.xml"); 22 | assertTrue("Could not find the project pom file.", pom.exists()); 23 | 24 | String versionString = "" + Anonimatron.VERSION + ""; 25 | 26 | try (BufferedReader reader = new BufferedReader(new FileReader(pom))) { 27 | while (reader.ready()) { 28 | String line = reader.readLine(); 29 | if (line.contains(versionString)) { 30 | // Version is ok, return 31 | return; 32 | } 33 | 34 | if (line.contains("")) { 35 | fail("Incorrect version, pom.xml does not match version info in Anonimatron.VERSION."); 36 | } 37 | } 38 | } 39 | fail("Incorrect version, pom.xml does not match version info in Anonimatron.VERSION."); 40 | } 41 | 42 | 43 | public void testIntegrationFileReader() throws Exception { 44 | // Create fake input file 45 | File inFile = File.createTempFile(this.getClass().getSimpleName(), ".input.csv"); 46 | CsvFileWriter csvFileWriter = new CsvFileWriter(inFile); 47 | Record inputRecords = new Record( 48 | new String[]{"colname1", "colname2"}, 49 | new String[]{"value1", "value2"} 50 | ); 51 | csvFileWriter.write(inputRecords); 52 | csvFileWriter.close(); 53 | 54 | File outFile = File.createTempFile(this.getClass().getSimpleName(), ".output.csv"); 55 | assertTrue("Could not delete " + outFile, outFile.delete()); 56 | 57 | File synonymFile = File.createTempFile(this.getClass().getSimpleName(), "synonyms.xml"); 58 | assertTrue("Could not delete " + synonymFile, synonymFile.delete()); 59 | 60 | File configFile = File.createTempFile(this.getClass().getSimpleName(), ".config.xml)"); 61 | PrintWriter printWriter = new PrintWriter(configFile); 62 | printWriter.write("\n"); 63 | printWriter.write("\n"); 64 | printWriter.write("com.rolfje.anonimatron.file.AcceptAllFilter"); 65 | printWriter.write("\n"); 69 | printWriter.write("\n"); 70 | printWriter.write("\n"); 71 | printWriter.write("\n"); 72 | printWriter.close(); 73 | 74 | String[] arguments = new String[]{ 75 | "-config", configFile.getAbsolutePath(), 76 | "-synonyms", synonymFile.getAbsolutePath() 77 | }; 78 | 79 | Anonimatron.main(arguments); 80 | 81 | assertEquals(1, AcceptAllFilter.getAcceptCount()); 82 | 83 | // Check that original data is not present in the synonym file. 84 | SynonymCache synonymCache = SynonymCache.fromFile(synonymFile); 85 | Object[] inputValues = inputRecords.getValues(); 86 | for (Object inputValue : inputValues) { 87 | assertNull(synonymCache.get("ROMAN_NAME", inputValue)); 88 | } 89 | 90 | // Check that we can find the hashed first column value 91 | synonymCache.setHasher(new Hasher("testsalt")); 92 | assertNotNull("Hashed value not found.", synonymCache.get("ROMAN_NAME", inputValues[0])); 93 | assertNull("Hashed value found.", synonymCache.get("ROMAN_NAME", inputValues[1])); 94 | } 95 | 96 | public void testDemoConfiguration() throws Exception { 97 | Anonimatron.main(new String[]{"-configexample"}); 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /resources/documentation/VERSION_INFO.TXT: -------------------------------------------------------------------------------- 1 | *** Version 1.16-SNAPSHOT 2 | 3 | *** Version 1.15 4 | - Fixed a problem where tables from other catalogs could prevent 5 | fetching primary key information from the correct table, 6 | fixes issues #105 and #106 7 | - Updated maven plugin versions 8 | - Updated several libraries to the latest 1.8 supporting version 9 | 10 | *** Version 1.14 11 | - Fixed a problem where generated Dutch BSN numbers could be 12 | syntactically invalid, issue #99 13 | - Removed references to deprecated Hamcrest matchers in the 14 | unittests, issue #94 15 | - Fixed a problem where RANDOMDIGITS was not downwards compatible 16 | and required a digit mask, issue #95 17 | - Upgraded code to make better use of Java 8 features. 18 | - Upgraded bundled mysql-connector-j to 8.0.21, also requested in issue #101 19 | - Solved a problem where UkPostCodeAnonymizer would not set the Synonym 20 | type and ignored the "shortlived" synonym configuration. 21 | - Solved a problem where discriminator configuration was parsed 22 | incorrectly, see #86 23 | 24 | *** Version 1.13 25 | - Fixed OutOfMemoryError for tables with BLOBs by adding fetch size, 26 | see pull request #83 27 | - Moved documentation to the Jekyll folder in /docs/documentation 28 | 29 | *** Version 1.12 30 | - Added mask parameter to DigitStringAnonymizer 31 | (fixes issue #74, thanks to Balogh Tamás and Bartlomiej Komendarczuk) 32 | - Added UK Postalcode Anonymizer 33 | - Optimized Random number generators (less instantiations) 34 | - Fixed override problem with password, userid and jdbcurl 35 | (pull request #69, thanks to Stephan Schrader) 36 | - Security, performance and readability fixes, as suggested by SonarCube, 37 | see https://sonarcloud.io/dashboard?id=realrolfje_anonimatron) 38 | - Integrated documentation in the main development branch 39 | 40 | *** Version 1.11 41 | - Add numeric BSN support, closes issue #31 42 | - Ability to provide database connection parameters through 43 | the command line, closes issue #53 44 | - Ability to pass parameters to Anonymizers through the 45 | column configuration. Closes issue #54 46 | - Bumped up the versions of some maven plugins and dependencies 47 | thanks to dependabot. 48 | 49 | *** Version 1.10.1 50 | - Implements (rudamentary) support for MSSQL Schemas 51 | 52 | *** Version 1.10 53 | - Warning: Breaks code which relies on 1.9.3 and earlier as a library. 54 | - New feature: Short lived synonyms. These values are not stored, 55 | and not consistent between runs. Used to fill unimportant fields 56 | and reduce the number of stored synonyms. Use with care. 57 | 58 | *** Version 1.9.3 59 | - Anonimatron now available as library in maven central, see 60 | https://search.maven.org/search?q=g:com.rolfje.anonimatron 61 | 62 | *** Version 1.9.2 63 | - New Synonym: HashedSynonym, where the "from" is hashed so that your 64 | synonym file no longer contains source data. 65 | 66 | *** Version 1.9 67 | - New feature: Anonymize files. 68 | - Bumped to Java 1.7 69 | - Implemented suggestions, bugfixes and improvements. 70 | 71 | *** Version 1.8 72 | - Moved to Github 73 | - Added release scripts 74 | - Changed versioning to support the new release scripts 75 | - Added SyBase drivers 76 | 77 | *** Version 1.7 78 | - Implemented "Dryrun", feature request #3. 79 | There is now a new command line option "-dryrun" which will make 80 | Anonimatron go through all it's paces, but without any writes to your 81 | database. It will even create a synonym file if you want to. 82 | 83 | - Implemented "Prefetching", feature request #5. 84 | If your Anonymizer implements the "Prefetcher" interface, Anonimatron will 85 | make a pass through the table and feed every value for your column to your 86 | Anonymizer. This will enable you to collect the characters used in the 87 | source data, so you can base your generated Synonyms on that. Any strange 88 | UTF-8 character in your source database can be in your output set now, 89 | while still being Anonymized. 90 | 91 | - Added "CharacterStringPrefetchAnonymizer" for feature #5. 92 | 93 | - Added "Base64StringFieldHandler" to handle writing binary data from and 94 | to the Synonym XML file. 95 | 96 | - Changed the update strategy, Anonimatron now uses a cursor to go through 97 | a resultset, instead of generating update statements. 98 | 99 | - Reduced memory usage and improved performance. 100 | 101 | *** Version 1.6 (and earlier) 102 | The original Anonimatron versions 1.0 to 1.6 were released in rapid 103 | succession. New features and bugfixes were rapidly implemented to support 104 | some people who started using Anonimatron on production systems. This 105 | usage made Anonimatron what it is today: 106 | - 5 Star rating on Sourceforge 107 | - Featured on Softpedia 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/configuration/castor-config-mapping.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Mapping for the the Configuration classes 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | 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 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/main/java/com/rolfje/anonimatron/anonymizer/UkPostCodeAnonymizer.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.rolfje.anonimatron.synonyms.StringSynonym; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * Generates format valid Uk Post codes. 10 | * See https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom 11 | * and See https://stackoverflow.com/a/164994 12 | *

13 | * Let me know if this isn't correct in practice; UK postal codes seem 14 | * to have a very strange organically grown format. 15 | */ 16 | public class UkPostCodeAnonymizer implements Anonymizer { 17 | private final Random random = new Random(); 18 | 19 | private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 20 | private static final String DIGITS = "0123456789"; 21 | 22 | @Override 23 | public String getType() { 24 | return "UK_POST_CODE"; 25 | } 26 | 27 | @Override 28 | public Synonym anonymize(Object from, int size, boolean shortlived) { 29 | if (from == null) { 30 | return new StringSynonym(getType(), null, null, shortlived); 31 | } 32 | 33 | if (size < 8) { 34 | throw new UnsupportedOperationException( 35 | "Random UK postal codes can be 9 characters in length and will " 36 | + "not always fit configured size of " + size + "."); 37 | } 38 | 39 | String to = buildZipCode(); 40 | if (to.length() > size) { 41 | throw new UnsupportedOperationException( 42 | "Generated postalcode was " + to.length() + " characters and does not fit " 43 | + size + " characters."); 44 | } 45 | 46 | return new StringSynonym( 47 | getType(), 48 | (String) from, 49 | to, 50 | shortlived 51 | ); 52 | } 53 | 54 | String buildZipCode() { 55 | if (random.nextInt(1_673_945_000) == 0) { 56 | return buildZipCodeFlavor1(); 57 | } else { 58 | return buildZipCodeFlavor2(); 59 | } 60 | } 61 | 62 | // Length 7 63 | // ^([Gg][Ii][Rr] 0[Aa]{2})$ 64 | String buildZipCodeFlavor1() { 65 | if (random.nextBoolean()) { 66 | return "GIR0AA"; 67 | } else { 68 | return "GIR 0AA"; 69 | } 70 | } 71 | 72 | String buildZipCodeFlavor2() { 73 | // Total 247.625 * 10 * 26 * 26 = 1.673.945.000 combinations 74 | StringBuilder b = new StringBuilder(); 75 | 76 | // Prefix: Total prefix set is 247.625 combinations 77 | if (random.nextInt(96) == 0) { // one in 96 is a p1 78 | b.append(p1()); // 2.600 combinations 79 | } else { 80 | if (random.nextInt(4) == 0) { // if not a p1, one in 4 is a p2a 81 | b.append(p2a()); // 62.400 combinations 82 | } else { 83 | if (random.nextInt(27) == 0) { // if not a p2a, one in 27 is a p2ba 84 | b.append(p2ba()); // 6.760 combinations 85 | } else { 86 | b.append(p2bb()); // 178.464 combinations 87 | } 88 | } 89 | } 90 | 91 | if (random.nextBoolean()) { 92 | b.append(' '); 93 | } 94 | 95 | b.append(getRandomCharacter(DIGITS)); 96 | b.append(getRandomCharacter(CHARACTERS)); 97 | b.append(getRandomCharacter(CHARACTERS)); 98 | return b.toString(); 99 | } 100 | 101 | // Length 3 102 | // 26 * 100 = 2.600 combinations 103 | // ^((([A-Za-z][0-9]{1,2})$ 104 | String p1() { 105 | StringBuilder b = new StringBuilder(); 106 | b.append(getRandomCharacter(CHARACTERS)); 107 | b.append(getRandomCharacter(DIGITS)); 108 | 109 | if (random.nextBoolean()) { 110 | b.append(getRandomCharacter(DIGITS)); 111 | } 112 | 113 | return b.toString(); 114 | } 115 | 116 | // Length 4 117 | // 26 * 24 * 100 = 62.400 combinations 118 | // ([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2}) 119 | String p2a() { 120 | StringBuilder b = new StringBuilder(); 121 | b.append(getRandomCharacter(CHARACTERS)); 122 | b.append(getRandomCharacter("ABCDEFGHJKLMNOPQRSTUVWXY")); 123 | b.append(getRandomCharacter(DIGITS)); 124 | if (random.nextBoolean()) { 125 | b.append(getRandomCharacter(DIGITS)); 126 | } 127 | return b.toString(); 128 | } 129 | 130 | // Length 3 131 | // 26 * 10 * 26 = 6.760 combinations 132 | // ([A-Za-z][0-9][A-Za-z]) 133 | String p2ba() { 134 | return String.valueOf(getRandomCharacter(CHARACTERS)) + 135 | getRandomCharacter(DIGITS) + 136 | getRandomCharacter(CHARACTERS); 137 | } 138 | 139 | // Length 3 to 4 140 | // 26 * 24 * 11 * 26 = 178.464 combinations 141 | //([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]) 142 | String p2bb() { 143 | StringBuilder b = new StringBuilder(); 144 | b.append(getRandomCharacter(CHARACTERS)); 145 | b.append(getRandomCharacter("ABCDEFGHJKLMNOPQRSTUVWXY")); 146 | if (random.nextBoolean()) { 147 | b.append(getRandomCharacter(DIGITS)); 148 | } 149 | b.append(getRandomCharacter(CHARACTERS)); 150 | return b.toString(); 151 | } 152 | 153 | private char getRandomCharacter(String characters) { 154 | return characters.charAt(random.nextInt(characters.length())); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/com/rolfje/anonimatron/anonymizer/DigitStringAnonymizerTest.java: -------------------------------------------------------------------------------- 1 | package com.rolfje.anonimatron.anonymizer; 2 | 3 | import com.javamonitor.tools.Stopwatch; 4 | import com.rolfje.anonimatron.synonyms.Synonym; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | public class DigitStringAnonymizerTest { 14 | 15 | private DigitStringAnonymizer anonymizer; 16 | 17 | @Before 18 | public void setUp() { 19 | anonymizer = new DigitStringAnonymizer(); 20 | } 21 | 22 | @Test 23 | public void anonymize() { 24 | String original = "ORIGINAL"; 25 | 26 | Synonym synonym = anonymizer.anonymize(original, Integer.MAX_VALUE, false); 27 | 28 | assertNotNull(synonym); 29 | assertNotEquals(synonym.getFrom(), synonym.getTo()); 30 | assertEquals(anonymizer.getType(), synonym.getType()); 31 | assertEquals(original.length(), synonym.getTo().toString().length()); 32 | assertFalse(synonym.isShortLived()); 33 | } 34 | 35 | @Test 36 | public void testNull() { 37 | Synonym synonym = anonymizer.anonymize(null, Integer.MAX_VALUE, false); 38 | assertNull(synonym.getFrom()); 39 | assertNull(synonym.getTo()); 40 | } 41 | 42 | @Test 43 | public void testNullMasked() { 44 | Synonym synonym = 45 | anonymizer.anonymize(null, Integer.MAX_VALUE, 46 | false, getMaskParameters("11**")); 47 | 48 | assertNull(synonym.getFrom()); 49 | assertNull(synonym.getTo()); 50 | } 51 | 52 | @Test 53 | public void testMaskedAnonymize() { 54 | String original = "ABCDEFGH"; 55 | String mask = "1,1-1*99"; 56 | String expected = "AxCxExGH"; 57 | 58 | Synonym synonym = anonymizer.anonymize( 59 | original, Integer.MAX_VALUE, false, 60 | getMaskParameters(mask) 61 | ); 62 | 63 | String toString = synonym.getTo().toString(); 64 | 65 | assertEquals(original, synonym.getFrom()); 66 | assertNotEquals(synonym.getFrom(), synonym.getTo()); 67 | assertEquals(synonym.getFrom().toString().length(), toString.length()); 68 | 69 | char[] toChars = toString.toCharArray(); 70 | char[] expectedChars = expected.toCharArray(); 71 | 72 | for (int i = 0; i < expectedChars.length; i++) { 73 | assertEquals("Character at position " + i + " not what we expected. String is '" + toString + "'", (expectedChars[i] == 'x'), (Character.isDigit(toChars[i]))); 74 | } 75 | } 76 | 77 | @Test 78 | public void testMaskedDigitsPerformance() { 79 | String original = "ABCDEFGH"; 80 | String mask = "1,1-1*99"; 81 | Map maskParameters = getMaskParameters(mask); 82 | 83 | Stopwatch stopwatchNano = new Stopwatch("Masked performance"); 84 | for (int i = 0; i < 1_000_000; i++) { 85 | Synonym synonym = anonymizer.anonymize( 86 | original, Integer.MAX_VALUE, false, 87 | maskParameters 88 | ); 89 | } 90 | 91 | // On a 2013 MacBook this takes less than 200 ms. 92 | // On the Travis build server this takes roughly 750 ms. 93 | boolean fastEnough = stopwatchNano.stop(1000); 94 | assertTrue(stopwatchNano.getMessage(), fastEnough); 95 | } 96 | 97 | private Map getMaskParameters(String mask) { 98 | Map parameters = new HashMap<>(); 99 | parameters.put(DigitStringAnonymizer.PARAMETER, mask); 100 | return parameters; 101 | } 102 | 103 | @Test 104 | public void testIncorrectParamter() { 105 | try { 106 | anonymizer.anonymize("dummy", 0, false, new HashMap() {{ 107 | put("PaRaMeTeR", "any"); 108 | }}); 109 | fail("Should fail with unsupported operation exception."); 110 | } catch (UnsupportedOperationException e) { 111 | assertEquals( 112 | "Please provide '" + DigitStringAnonymizer.PARAMETER + "' with a digit mask in the form 111******, where only stars are replaced with random characters.", 113 | e.getMessage()); 114 | } 115 | } 116 | 117 | @Test 118 | public void testCorrectParameter() { 119 | Synonym anonymize = anonymizer.anonymize("dummy", 0, false, new HashMap() {{ 120 | put(DigitStringAnonymizer.PARAMETER, "any"); 121 | }}); 122 | 123 | // Actual anonymization tests done elsewhere 124 | assertNotNull(anonymize); 125 | } 126 | 127 | @Test 128 | public void testNoParameter() { 129 | // Actual anonymization tests done elsewhere 130 | assertNotNull( 131 | anonymizer.anonymize("dummy", 0, false, new HashMap<>()) 132 | ); 133 | 134 | assertNotNull( 135 | anonymizer.anonymize("dummy", 0, false, null) 136 | ); 137 | } 138 | 139 | @Test 140 | public void testInCorrectParameter() { 141 | try { 142 | Synonym anonymize = anonymizer.anonymize("dummy", 0, false, new HashMap() {{ 143 | put("dummy", "any"); 144 | }}); 145 | fail("Should result in UnsupportedOperationException with useful message."); 146 | } catch (UnsupportedOperationException e) { 147 | assertEquals( 148 | "Please provide 'mask' with a digit mask in the form 111******, where only stars are replaced with random characters.", 149 | e.getMessage() 150 | ); 151 | } 152 | } 153 | } --------------------------------------------------------------------------------