├── .circleci └── config.yml ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── JsonMasking.Tests ├── JsonMasking.Tests.csproj ├── JsonMaskingTests.cs └── Mocks │ └── BlacklistPartialMock.cs ├── JsonMasking.sln ├── JsonMasking ├── JsonMasking.cs └── JsonMasking.csproj ├── LICENSE ├── QA.md ├── README.md ├── appveyor.yml └── azure-pipelines.yml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: /app 5 | docker: 6 | - image: mcr.microsoft.com/dotnet/sdk:6.0 7 | steps: 8 | - checkout 9 | - run: 10 | name: Checking files 11 | command: ls -al 12 | - run: 13 | name: Restoring nuget packages 14 | command: dotnet restore 15 | - run: 16 | name: Publishing app 17 | command: dotnet publish -c Release -o binn 18 | - store_artifacts: 19 | path: /app/JsonMasking/binn 20 | destination: app-artifact 21 | - persist_to_workspace: 22 | root: / 23 | paths: 24 | - app 25 | 26 | unit_tests: 27 | working_directory: / 28 | docker: 29 | - image: mcr.microsoft.com/dotnet/sdk:6.0 30 | steps: 31 | - attach_workspace: 32 | at: / 33 | - run: 34 | name: Checking files 35 | command: ls /app 36 | - run: 37 | name: Install coverlet 38 | command: | 39 | dotnet tool install --global coverlet.console 40 | cd /app/JsonMasking.Tests 41 | dotnet add package coverlet.msbuild 42 | - run: 43 | name: Running unit tests 44 | command: | 45 | dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=/test-result/result /p:Exclude=[xunit.*]* /app/JsonMasking.sln 46 | - store_artifacts: 47 | path: /test-result 48 | destination: test-result 49 | - persist_to_workspace: 50 | root: / 51 | paths: 52 | - test-result 53 | 54 | workflows: 55 | version: 2 56 | build_publish_deploy: 57 | jobs: 58 | - build 59 | - unit_tests: 60 | requires: 61 | - build 62 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for taking the time to contribute! 4 | 5 | ### How can I contribute? 6 | 7 | * Fork this project; 8 | * Make your changes / new implementatios; 9 | * Use the pattern for git commts; 10 | * Make sure that the acceptance criteria are met (tests, docs, etc); 11 | * Create a pull request; 12 | 13 | ### Pull Requests 14 | 15 | Template [PULLREQUEST-TEMPLATE](PULLREQUEST-TEMPLATE.md) 16 | 17 | ### Git Commit Messages 18 | 19 | * Use the present tense ("Adds feature" not "Added feature") 20 | * Limit the first line to 72 characters or less 21 | * Reference issues and pull requests liberally 22 | * Consider starting the commit message with an applicable emoji: 23 | * :art: `:art:` when improving the format/structure of the code 24 | * :racehorse: `:racehorse:` when improving performance 25 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks 26 | * :memo: `:memo:` when writing docs 27 | * :penguin: `:penguin:` when fixing something on Linux 28 | * :apple: `:apple:` when fixing something on Mac OS 29 | * :checkered_flag: `:checkered_flag:` when fixing something on Windows 30 | * :bug: `:bug:` when fixing a bug 31 | * :fire: `:fire:` when removing code or files 32 | * :green_heart: `:green_heart:` when fixing the CI build 33 | * :white_check_mark: `:white_check_mark:` when adding tests 34 | * :lock: `:lock:` when dealing with security 35 | * :arrow_up: `:arrow_up:` when upgrading dependencies 36 | * :arrow_down: `:arrow_down:` when downgrading dependencies 37 | * :shirt: `:shirt:` when removing linter warnings 38 | * :bulb: `:bulb:` new idea 39 | * :construction: `:construction:` work in progress 40 | * :heavy_plus_sign: `:heavy_plus_sign:` when adding features 41 | * :heavy_minus_sign: `:heavy_minus_sign:` when removing features 42 | * :speaker: `:mute:` when adding logging 43 | * :mute: `:mute:` when reducing logging 44 | * :facepunch: `:facepunch:` when resolve conflict 45 | * :wrench: `:wrench:` when modify Web.config 46 | 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ``` 2 | Please use the following template to submit your issue. 3 | Following this template will allow us to quickly investigate and help you with your issue. 4 | Please be aware that issues which do not conform to this template may be closed. 5 | 6 | DO NOT FORGET TO REMOVE THIS BLOCK 7 | ``` 8 | 9 | ### Status 10 | 11 | BUG REPORT / TASK 12 | 13 | ### Checklist 14 | 15 | Add checklist if this is a task 16 | 17 | - [x] Add slack integration 18 | - [_] Support xyz 19 | 20 | ### Steps 21 | 22 | 1. First step 23 | 2. Second step 24 | 3. Third step 25 | 26 | ### Expected behaviour 27 | 28 | How do you think the program should work? Add screenshots and code blocks if necessary. 29 | 30 | ### Actual behaviour 31 | 32 | How does the program work in its current state? 33 | 34 | ### Environment 35 | 36 | You may write here the specifications like the version of the project, services, operating system, or hardware if applicable. 37 | 38 | ### Logs / Stack trace 39 | 40 | ``` 41 | Insert your log/stack trace here 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ``` 2 | This is a guide to use this Pull Request Template. 3 | 4 | # Title 5 | [feature] Implements the crazy powerful transaction search 6 | [hotfix] Fixes login with e-mail address 7 | 8 | Add a gif that expresses your reaction to the implemented code, make it fun 9 | 10 | DO NOT FORGET TO REMOVE THIS BLOCK 11 | ``` 12 | ![Git Merge](https://media.giphy.com/media/cFkiFMDg3iFoI/giphy.gif) 13 | 14 | ### Status 15 | 16 | READY / IN DEVELOPMENT 17 | 18 | ### Whats? 19 | 20 | Describe in an objective way what has been done. 21 | 22 | ### Why? 23 | 24 | Why do you need this implementation/fix? 25 | 26 | ### How? 27 | 28 | How did you solve the problem? What are the main flows? Any technical information regarding infrastructure or architecture? 29 | 30 | ### Attachments (if appropriate) 31 | 32 | Add additional informations like screenshots, issue link, zendesk ticket link, jira task link, etc 33 | 34 | ### Definition of Done: 35 | - [ ] Increases API documentation 36 | - [ ] Implements integration tests 37 | - [ ] Implements unit tests 38 | - [ ] Is there appropriate logging included? 39 | - [ ] Does this add new dependencies? 40 | - [ ] Does need add new version in changelog? 41 | - [ ] Does need update readme, contributing, etc? 42 | - [ ] Does need change in CI server? 43 | - [ ] Will this feature require a new piece of infrastructure be implemented? 44 | - [ ] Does this PR require a blog post? If so, ensure marketing has signed off on content. 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /JsonMasking.Tests/JsonMasking.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /JsonMasking.Tests/JsonMaskingTests.cs: -------------------------------------------------------------------------------- 1 | using JsonMasking.Tests.Mocks; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Collections.Generic; 6 | using Xunit; 7 | 8 | namespace JsonMasking.Tests 9 | { 10 | public static class JsonMaskingTests 11 | { 12 | 13 | [Fact] 14 | public static void MaskFields_Should_Mask_No_Field_With_Empty_Blacklist() 15 | { 16 | // arrange 17 | var obj = new 18 | { 19 | Test = "1", 20 | Password = "somepass#here" 21 | }; 22 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 23 | string[] blacklist = { }; 24 | var mask = "*******"; 25 | 26 | // act 27 | var result = json.MaskFields(blacklist, mask); 28 | 29 | // assert 30 | Assert.Equal("{\n \"Test\": \"1\",\n \"Password\": \"somepass#here\"\n}", result.Replace("\r\n", "\n")); 31 | } 32 | 33 | [Fact] 34 | public static void MaskFields_Should_Mask_No_Field_With_Json_Without_Property() 35 | { 36 | // arrange 37 | var obj = new 38 | { 39 | Test = "1", 40 | OtherField = "somepass#here" 41 | }; 42 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 43 | string[] blacklist = { "password" }; 44 | var mask = "*******"; 45 | 46 | // act 47 | var result = json.MaskFields(blacklist, mask); 48 | 49 | // assert 50 | Assert.Equal("{\n \"Test\": \"1\",\n \"OtherField\": \"somepass#here\"\n}", result.Replace("\r\n", "\n")); 51 | } 52 | 53 | [Fact] 54 | public static void MaskFields_Should_Mask_Single_Field() 55 | { 56 | // arrange 57 | var obj = new 58 | { 59 | Test = "1", 60 | Password = "somepass#here" 61 | }; 62 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 63 | string[] blacklist = { "password" }; 64 | var mask = "----"; 65 | 66 | // act 67 | var result = json.MaskFields(blacklist, mask); 68 | 69 | // assert 70 | Assert.Equal("{\n \"Test\": \"1\",\n \"Password\": \"----\"\n}", result.Replace("\r\n", "\n")); 71 | } 72 | 73 | [Fact] 74 | public static void MaskFields_Should_Mask_Integer_Field() 75 | { 76 | // arrange 77 | var obj = new 78 | { 79 | Test = 1, 80 | Password = 123456 81 | }; 82 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 83 | string[] blacklist = { "*password" }; 84 | var mask = "*******"; 85 | 86 | // act 87 | var result = json.MaskFields(blacklist, mask); 88 | 89 | // assert 90 | Assert.Equal("{\n \"Test\": 1,\n \"Password\": \"*******\"\n}", result.Replace("\r\n", "\n")); 91 | } 92 | 93 | [Fact] 94 | public static void MaskFields_Should_Mask_Depth_Field() 95 | { 96 | // arrange 97 | var obj = new 98 | { 99 | DepthObject = new 100 | { 101 | Test = "1", 102 | Password = "somepass#here" 103 | } 104 | }; 105 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 106 | string[] blacklist = { "*.password" }; 107 | var mask = "*******"; 108 | 109 | // act 110 | var result = json.MaskFields(blacklist, mask); 111 | 112 | // assert 113 | Assert.Equal("{\n \"DepthObject\": {\n \"Test\": \"1\",\n \"Password\": \"*******\"\n }\n}", result.Replace("\r\n", "\n")); 114 | } 115 | 116 | [Fact] 117 | public static void MaskFields_Should_Mask_Multiple_Fields() 118 | { 119 | // arrange 120 | var obj = new 121 | { 122 | Password = "somepass#here", 123 | DepthObject = new 124 | { 125 | Test = "1", 126 | Password = "somepass#here2" 127 | } 128 | }; 129 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 130 | string[] blacklist = { "*password" }; 131 | var mask = "*******"; 132 | 133 | // act 134 | var result = json.MaskFields(blacklist, mask); 135 | 136 | // assert 137 | Assert.Equal("{\n \"Password\": \"*******\",\n \"DepthObject\": {\n \"Test\": \"1\",\n \"Password\": \"*******\"\n }\n}", result.Replace("\r\n", "\n")); 138 | } 139 | 140 | [Fact] 141 | public static void MaskFields_Should_Mask_Multiple_Fields_With_Multiple_Blacklist() 142 | { 143 | // arrange 144 | var obj = new 145 | { 146 | Password = "somepass#here", 147 | DepthObject = new 148 | { 149 | Test = "1", 150 | CreditCardNumber = "5555000011112222" 151 | } 152 | }; 153 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 154 | string[] blacklist = { "password", "*creditcardnumber" }; 155 | var mask = "*******"; 156 | 157 | // act 158 | var result = json.MaskFields(blacklist, mask); 159 | 160 | // assert 161 | Assert.Equal("{\n \"Password\": \"*******\",\n \"DepthObject\": {\n \"Test\": \"1\",\n \"CreditCardNumber\": \"*******\"\n }\n}", result.Replace("\r\n", "\n")); 162 | } 163 | 164 | [Fact] 165 | public static void MaskFields_Should_Mask_With_Null_Property() 166 | { 167 | // arrange 168 | var obj = new 169 | { 170 | DepthObject = new 171 | { 172 | Test = "1", 173 | Password = (string)null, 174 | } 175 | }; 176 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 177 | string[] blacklist = { "*password" }; 178 | var mask = "*******"; 179 | 180 | // act 181 | var result = json.MaskFields(blacklist, mask); 182 | 183 | // assert 184 | Assert.Equal("{\n \"DepthObject\": {\n \"Test\": \"1\",\n \"Password\": \"*******\"\n }\n}", result.Replace("\r\n", "\n")); 185 | } 186 | 187 | [Fact] 188 | public static void MaskFields_Should_Throw_Exception_When_Blacklist_Is_Null() 189 | { 190 | // arrange 191 | var obj = new 192 | { 193 | Test = "1", 194 | Password = "somepass#here" 195 | }; 196 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 197 | string[] blacklist = null; 198 | var mask = "*******"; 199 | 200 | // act 201 | Exception ex = Assert.Throws(() => 202 | json.MaskFields(blacklist, mask)); 203 | 204 | // assert 205 | Assert.Equal("Value cannot be null. (Parameter 'blacklist')", ex.Message); 206 | } 207 | 208 | [Fact] 209 | public static void MaskFields_Should_Throw_Exception_When_Json_Is_Null() 210 | { 211 | // arrange 212 | string json = null; 213 | string[] blacklist = { "password" }; 214 | var mask = "*******"; 215 | 216 | // act 217 | Exception ex = Assert.Throws(() => 218 | json.MaskFields(blacklist, mask)); 219 | 220 | // assert 221 | Assert.Equal("Value cannot be null. (Parameter 'json')", ex.Message); 222 | } 223 | 224 | [Fact] 225 | public static void MaskFields_Should_Throw_Exception_When_Json_String_Is_Empty() 226 | { 227 | // arrange 228 | string json = ""; 229 | string[] blacklist = { "password" }; 230 | var mask = "*******"; 231 | 232 | // act 233 | Exception ex = Assert.Throws(() => 234 | json.MaskFields(blacklist, mask)); 235 | 236 | // assert 237 | Assert.Equal("Value cannot be null. (Parameter 'json')", ex.Message); 238 | } 239 | 240 | [Fact] 241 | public static void MaskFields_Should_Throw_Exception_When_Json_String_Is_Invalid() 242 | { 243 | // arrange 244 | var json = "invalid json"; 245 | string[] blacklist = { "password" }; 246 | string mask = "------"; 247 | 248 | // act 249 | Exception ex = Assert.Throws(() => 250 | json.MaskFields(blacklist, mask)); 251 | 252 | // assert 253 | Assert.StartsWith("Unexpected character encountered while parsing value", ex.Message); 254 | } 255 | 256 | [Fact] 257 | public static void MaskFields_Should_Mask_With_Wildcard() 258 | { 259 | // arrange 260 | var obj = new 261 | { 262 | DepthObject = new 263 | { 264 | Test = "1", 265 | Password = "somepass#here", 266 | DepthObject = new 267 | { 268 | Test = "1", 269 | Password = "somepass#here" 270 | } 271 | }, 272 | DepthObject2 = new 273 | { 274 | Test = "1", 275 | Password = "somepass#here", 276 | DepthObject = new 277 | { 278 | Test = "1", 279 | Password = "somepass#here", 280 | DepthObject = new 281 | { 282 | Test = "1", 283 | Password = new 284 | { 285 | Test1 = "1", 286 | Password2 = "somepass#here" 287 | } 288 | } 289 | } 290 | }, 291 | Password = "somepass#here" 292 | }; 293 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 294 | string[] blacklist = { "*.DepthObject*.Password" }; 295 | var mask = "*******"; 296 | 297 | // act 298 | var result = json.MaskFields(blacklist, mask); 299 | 300 | // assert 301 | Assert.Equal("{\n \"DepthObject\": {\n \"Test\": \"1\",\n \"Password\": \"somepass#here\",\n \"DepthObject\": {\n \"Test\": \"1\",\n \"Password\": \"*******\"\n }\n },\n \"DepthObject2\": {\n \"Test\": \"1\",\n \"Password\": \"somepass#here\",\n \"DepthObject\": {\n \"Test\": \"1\",\n \"Password\": \"*******\",\n \"DepthObject\": {\n \"Test\": \"1\",\n \"Password\": \"*******\"\n }\n }\n },\n \"Password\": \"somepass#here\"\n}", result.Replace("\r\n", "\n")); 302 | } 303 | 304 | [Fact] 305 | public static void MaskFields_Should_Mask_Partial_With_Single_Field() 306 | { 307 | // arrange 308 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"462294*****9865\",\n \"Password\": \"somepass#here2\"\n }\n}"; 309 | 310 | var blacklistPartialMock = BlacklistPartialMock.DefaultBlackListPartial; 311 | 312 | var obj = new 313 | { 314 | Test = "1", 315 | Card = new 316 | { 317 | Number = "4622943127049865", 318 | Password = "somepass#here2" 319 | } 320 | }; 321 | 322 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 323 | string[] blacklist = { "*card.number" }; 324 | var mask = "----"; 325 | 326 | // act 327 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 328 | 329 | // assert 330 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 331 | } 332 | 333 | [Fact] 334 | public static void MaskFields_Should_Mask_Partial_And_Completely_With_Single_Field() 335 | { 336 | // arrange 337 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"462294*****9865\",\n \"Password\": \"----\"\n },\n \"Password\": \"----\"\n}"; 338 | 339 | var blacklistPartialMock = BlacklistPartialMock.DefaultBlackListPartial; 340 | 341 | var obj = new 342 | { 343 | Test = "1", 344 | Card = new 345 | { 346 | Number = "4622943127049865", 347 | Password = "somepass#here2" 348 | }, 349 | Password = "somepass#here2" 350 | }; 351 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 352 | string[] blacklist = { "*card.number", "*password" }; 353 | var mask = "----"; 354 | 355 | // act 356 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 357 | 358 | // assert 359 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 360 | } 361 | 362 | [Fact] 363 | public static void MaskFields_Should_Mask_Partial_With_Multiple_Fields() 364 | { 365 | // arrange 366 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"462294*****9865\",\n \"Password\": \"somepass#here2\",\n \"Card\": {\n \"Number\": \"462294*****9865\"\n },\n \"Teste\": {\n \"Card\": {\n \"Number\": \"462294*****9865\"\n }\n }\n }\n}"; 367 | 368 | var blacklistPartialMock = BlacklistPartialMock.DefaultBlackListPartial; 369 | 370 | var obj = new 371 | { 372 | Test = "1", 373 | Card = new 374 | { 375 | Number = "4622943127049865", 376 | Password = "somepass#here2", 377 | Card = new 378 | { 379 | Number = "4622943127049865", 380 | }, 381 | Teste = new 382 | { 383 | Card = new 384 | { 385 | Number = "4622943127049865", 386 | } 387 | } 388 | 389 | } 390 | }; 391 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 392 | string[] blacklist = { "*card.number" }; 393 | var mask = "----"; 394 | 395 | // act 396 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 397 | 398 | // assert 399 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 400 | } 401 | 402 | [Fact] 403 | public static void MaskFields_Should_Mask_Partial_And_Completely_With_Multiple_Field() 404 | { 405 | // arrange 406 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"462294*****9865\",\n \"Password\": \"----\",\n \"Card\": {\n \"Number\": \"462294*****9865\",\n \"Password\": \"----\"\n },\n \"Teste\": {\n \"Card\": {\n \"Number\": \"462294*****9865\",\n \"Password\": \"----\"\n }\n }\n }\n}"; 407 | 408 | var blacklistPartialMock = BlacklistPartialMock.DefaultBlackListPartial; 409 | 410 | var obj = new 411 | { 412 | Test = "1", 413 | Card = new 414 | { 415 | Number = "4622943127049865", 416 | Password = "somepass#here2", 417 | Card = new 418 | { 419 | Number = "4622943127049865", 420 | Password = "somepass#here2" 421 | }, 422 | Teste = new 423 | { 424 | Card = new 425 | { 426 | Number = "4622943127049865", 427 | Password = "somepass#here2" 428 | } 429 | } 430 | } 431 | }; 432 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 433 | string[] blacklist = { "*card.number", "*password" }; 434 | var mask = "----"; 435 | 436 | // act 437 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 438 | 439 | // assert 440 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 441 | } 442 | 443 | [Fact] 444 | public static void MaskFields_Should_Not_Mask_Partial_If_Property_IsNot_In_Blacklist() 445 | { 446 | // arrange 447 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"4622943127049865\",\n \"Password\": \"somepass#here2\"\n }\n}"; 448 | 449 | var blacklistPartialMock = BlacklistPartialMock.DefaultBlackListPartial; 450 | 451 | var obj = new 452 | { 453 | Test = "1", 454 | Card = new 455 | { 456 | Number = "4622943127049865", 457 | Password = "somepass#here2" 458 | } 459 | }; 460 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 461 | string[] blacklist = { }; 462 | var mask = "----"; 463 | 464 | // act 465 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 466 | 467 | // assert 468 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 469 | } 470 | 471 | [Fact] 472 | public static void MaskFields_Should_Mask_Completely_If_Partial_Blacklist_Is_Empty() 473 | { 474 | // arrange 475 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"----\",\n \"Password\": \"somepass#here2\"\n }\n}"; 476 | 477 | var blacklistPartialMock = new Dictionary>(StringComparer.OrdinalIgnoreCase){ }; 478 | 479 | var obj = new 480 | { 481 | Test = "1", 482 | Card = new 483 | { 484 | Number = "4622943127049865", 485 | Password = "somepass#here2" 486 | } 487 | }; 488 | 489 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 490 | string[] blacklist = { "*card.number" }; 491 | var mask = "----"; 492 | 493 | // act 494 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 495 | 496 | // assert 497 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 498 | } 499 | 500 | [Fact] 501 | public static void MaskFields_Should_Mask_Completely_If_Delegate_Return_Same_Value() 502 | { 503 | // arrange 504 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"----\",\n \"Password\": \"somepass#here2\"\n }\n}"; 505 | 506 | var blacklistPartialMock = new Dictionary>(StringComparer.OrdinalIgnoreCase) 507 | { 508 | { "*card.number", text => text } 509 | }; 510 | 511 | var obj = new 512 | { 513 | Test = "1", 514 | Card = new 515 | { 516 | Number = "4622943127049865", 517 | Password = "somepass#here2" 518 | } 519 | }; 520 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 521 | string[] blacklist = { "*card.number" }; 522 | var mask = "----"; 523 | 524 | // act 525 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 526 | 527 | // assert 528 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 529 | } 530 | 531 | [Fact] 532 | public static void MaskFields_Should_Mask_With_Wildcard_ForJsonArray() 533 | { 534 | // arrange 535 | var dto1 = 536 | new 537 | { 538 | Name = "Test1", 539 | WillbeMasked = "12345" 540 | }; 541 | 542 | var dto2 = 543 | new 544 | { 545 | Name = "Test2", 546 | WillbeMasked = "123" 547 | }; 548 | 549 | List list = new List(); 550 | 551 | list.Add(dto1); 552 | list.Add(dto2); 553 | 554 | var json = JsonConvert.SerializeObject(list, Formatting.Indented); 555 | 556 | string[] blacklist = { "*.WillbeMasked" }; 557 | var mask = "*******"; 558 | 559 | // act 560 | var result = json.MaskFields(blacklist, mask); 561 | 562 | // assert 563 | Assert.Equal(@"[ 564 | { 565 | ""Name"": ""Test1"", 566 | ""WillbeMasked"": ""*******"" 567 | }, 568 | { 569 | ""Name"": ""Test2"", 570 | ""WillbeMasked"": ""*******"" 571 | } 572 | ]", result); 573 | } 574 | 575 | [Fact] 576 | public static void MaskFields_Should_Throw_Exception_Correctly_When_Has_error_In_Delegate() 577 | { 578 | // arrange 579 | const string EXPECTED_ERROR = "An error occurred while executing the function in the dictionary value. startIndex cannot be larger than length of string. (Parameter 'startIndex')"; 580 | 581 | var blacklistPartialMock = new Dictionary>(StringComparer.OrdinalIgnoreCase) 582 | { 583 | { "*card.number", text => text.Substring(100) } 584 | }; 585 | 586 | var obj = new 587 | { 588 | Test = "1", 589 | Card = new 590 | { 591 | Number = "4622943127049865", 592 | Password = "somepass#here2" 593 | } 594 | }; 595 | 596 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 597 | string[] blacklist = { "*card.number" }; 598 | var mask = "----"; 599 | 600 | // act 601 | Exception ex = Assert.Throws(() => 602 | json.MaskFields(blacklist, mask, blacklistPartialMock)); 603 | 604 | // assert 605 | Assert.Equal(EXPECTED_ERROR, ex.Message); 606 | } 607 | 608 | [Theory] 609 | [InlineData("4622943127049865", "462294*****9865")] 610 | [InlineData(" 462 29431270 49865 ", "462294*****9865")] 611 | [InlineData("46@22912704986@5", "----")] 612 | [InlineData("462.29431.2704.9865", "----")] 613 | [InlineData("46|2294312|70498|65", "----")] 614 | [InlineData("123", "----")] 615 | [InlineData("0123456789123456789012345", "012345*****2345")] 616 | public static void MaskFields_Should_Mask_Completely_and_Partially_Correctly(string received, string expected) 617 | { 618 | // arrange 619 | var blacklistPartialMock = BlacklistPartialMock.DefaultBlackListPartial; 620 | 621 | var obj = new 622 | { 623 | Card = new 624 | { 625 | Number = received, 626 | } 627 | }; 628 | 629 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 630 | string[] blacklist = { "*card.number" }; 631 | var mask = "----"; 632 | 633 | // act 634 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 635 | var jsonObject = JObject.Parse(result); 636 | var numberMasked = jsonObject["Card"]["Number"].ToString(); 637 | 638 | // assert 639 | Assert.Equal(numberMasked, expected); 640 | } 641 | 642 | [Fact] 643 | public static void MaskFields_Should_Mask_Completely_If_Delegate_Is_Null() 644 | { 645 | // arrange 646 | const string EXPECTED_VALUE = "{\n \"Test\": \"1\",\n \"Card\": {\n \"Number\": \"----\",\n \"Password\": \"somepass#here2\"\n }\n}"; 647 | 648 | var blacklistPartialMock = new Dictionary>(StringComparer.OrdinalIgnoreCase) 649 | { 650 | { "*card.number", null } 651 | }; 652 | 653 | var obj = new 654 | { 655 | Test = "1", 656 | Card = new 657 | { 658 | Number = "4622943127049865", 659 | Password = "somepass#here2" 660 | } 661 | }; 662 | var json = JsonConvert.SerializeObject(obj, Formatting.Indented); 663 | string[] blacklist = { "*card.number" }; 664 | var mask = "----"; 665 | 666 | // act 667 | var result = json.MaskFields(blacklist, mask, blacklistPartialMock); 668 | 669 | // assert 670 | Assert.Equal(EXPECTED_VALUE, result.Replace("\r\n", "\n")); 671 | } 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /JsonMasking.Tests/Mocks/BlacklistPartialMock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace JsonMasking.Tests.Mocks 6 | { 7 | public static class BlacklistPartialMock 8 | { 9 | private const string CARD_NUMBER_PATTERN = @"(\d{4,5})[ -|]?(\d{3,6})[ -|]?(\d{3,5})[ -|]?(\d{3,4})"; 10 | 11 | private const string EMPTY_SPACE_PATTERN = @"\s+"; 12 | 13 | public static Dictionary> DefaultBlackListPartial = new Dictionary>(StringComparer.OrdinalIgnoreCase) 14 | { 15 | {"*card.number", MaskCardNumber } 16 | }; 17 | 18 | public static string MaskCardNumber(this string text) 19 | { 20 | const int MINIMAL_LENGTH = 10; 21 | 22 | text = Regex.Replace(text, EMPTY_SPACE_PATTERN, ""); 23 | 24 | if (string.IsNullOrEmpty(text) || text.Length < MINIMAL_LENGTH || !ContainsCardNumber(text)) 25 | { 26 | return text; 27 | } 28 | 29 | var firstSix = text[..6]; 30 | var lastFour = text[^4..]; 31 | if (firstSix.Length == 6 && lastFour.Length == 4) 32 | { 33 | return $"{firstSix}*****{lastFour}"; 34 | } 35 | 36 | return text; 37 | } 38 | 39 | public static bool ContainsCardNumber(this string text) 40 | { 41 | if (string.IsNullOrEmpty(text)) 42 | return false; 43 | 44 | return Regex.IsMatch(text, CARD_NUMBER_PATTERN); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /JsonMasking.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonMasking", "JsonMasking\JsonMasking.csproj", "{124D3B3E-3282-4A2D-89F1-8384CFBE4889}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{323DA697-4C17-4AFD-98D0-2C8135AF2001}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonMasking.Tests", "JsonMasking.Tests\JsonMasking.Tests.csproj", "{0F9ABAF8-E1A8-47C3-90BD-53F0A967271C}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "git", "git", "{85DD2CC9-2969-47D9-81D4-2B61D3943151}" 13 | ProjectSection(SolutionItems) = preProject 14 | .gitattributes = .gitattributes 15 | .gitignore = .gitignore 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{E18D0D3B-4D8E-4B89-883B-8DB0DC2E98B5}" 19 | ProjectSection(SolutionItems) = preProject 20 | .github\CONTRIBUTING.md = .github\CONTRIBUTING.md 21 | .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md 22 | LICENSE = LICENSE 23 | .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md 24 | QA.md = QA.md 25 | README.md = README.md 26 | EndProjectSection 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "devops", "devops", "{7C946BE2-D6FE-4831-88A3-63CD9BE030C7}" 29 | ProjectSection(SolutionItems) = preProject 30 | appveyor.yml = appveyor.yml 31 | azure-pipelines.yml = azure-pipelines.yml 32 | EndProjectSection 33 | EndProject 34 | Global 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Release|Any CPU = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {124D3B3E-3282-4A2D-89F1-8384CFBE4889}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {124D3B3E-3282-4A2D-89F1-8384CFBE4889}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {124D3B3E-3282-4A2D-89F1-8384CFBE4889}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {124D3B3E-3282-4A2D-89F1-8384CFBE4889}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {0F9ABAF8-E1A8-47C3-90BD-53F0A967271C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {0F9ABAF8-E1A8-47C3-90BD-53F0A967271C}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {0F9ABAF8-E1A8-47C3-90BD-53F0A967271C}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {0F9ABAF8-E1A8-47C3-90BD-53F0A967271C}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {85DD2CC9-2969-47D9-81D4-2B61D3943151} = {323DA697-4C17-4AFD-98D0-2C8135AF2001} 54 | {E18D0D3B-4D8E-4B89-883B-8DB0DC2E98B5} = {323DA697-4C17-4AFD-98D0-2C8135AF2001} 55 | {7C946BE2-D6FE-4831-88A3-63CD9BE030C7} = {323DA697-4C17-4AFD-98D0-2C8135AF2001} 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {031A14D9-8A4F-4DEF-B742-868DCA99A53E} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /JsonMasking/JsonMasking.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace JsonMasking 9 | { 10 | /// 11 | /// Json Masking Extension 12 | /// 13 | public static class JsonMasking 14 | { 15 | /// 16 | /// Mask fields 17 | /// 18 | /// json to mask properties 19 | /// insensitive property array 20 | /// mask to replace property value 21 | /// Optional. Dictionary where the key is the property to be masked and the value is the function to apply to the property 22 | /// 23 | public static string MaskFields(this string json, string[] blacklist, string mask, Dictionary> blacklistPartial = null) 24 | { 25 | if (string.IsNullOrWhiteSpace(json) == true) 26 | { 27 | throw new ArgumentNullException(nameof(json)); 28 | } 29 | 30 | if (blacklist == null) 31 | { 32 | throw new ArgumentNullException(nameof(blacklist)); 33 | } 34 | 35 | if (blacklist.Any() == false) 36 | { 37 | return json; 38 | } 39 | 40 | var deserilizedObject = JsonConvert.DeserializeObject(json); 41 | 42 | if (deserilizedObject is JArray) 43 | { 44 | foreach (var item in (JArray)deserilizedObject) 45 | { 46 | MaskFieldsFromJToken(item, blacklist, mask); 47 | } 48 | 49 | return deserilizedObject.ToString(); 50 | } 51 | 52 | var jsonObject = (JObject) deserilizedObject; 53 | 54 | if (blacklistPartial != null) 55 | { 56 | MaskFieldsFromJToken(jsonObject, blacklist, mask, blacklistPartial); 57 | } 58 | else 59 | { 60 | MaskFieldsFromJToken(jsonObject, blacklist, mask); 61 | } 62 | 63 | return jsonObject.ToString(); 64 | } 65 | 66 | /// 67 | /// Mask fields from JToken 68 | /// 69 | /// 70 | /// 71 | /// 72 | /// 73 | private static void MaskFieldsFromJToken(JToken token, string[] blacklist, string mask) 74 | { 75 | JContainer container = token as JContainer; 76 | if (container == null) 77 | { 78 | return; // abort recursive 79 | } 80 | 81 | List removeList = new List(); 82 | foreach (JToken jtoken in container.Children()) 83 | { 84 | if (jtoken is JProperty prop) 85 | { 86 | var matching = blacklist.Any(item => 87 | { 88 | return IsMatch(prop.Path, item); 89 | }); 90 | 91 | if (matching) 92 | { 93 | removeList.Add(jtoken); 94 | } 95 | } 96 | 97 | // call recursive 98 | MaskFieldsFromJToken(jtoken, blacklist, mask); 99 | } 100 | 101 | // replace 102 | foreach (JToken el in removeList) 103 | { 104 | var prop = (JProperty)el; 105 | prop.Value = mask; 106 | } 107 | } 108 | 109 | /// 110 | /// Mask fields completely or partially from JToken 111 | /// 112 | /// 113 | /// 114 | /// 115 | /// 116 | /// 117 | private static void MaskFieldsFromJToken(JToken token, string[] blacklist, string mask, Dictionary> blacklistPartial) 118 | { 119 | JContainer container = token as JContainer; 120 | if (container == null) 121 | { 122 | return; // abort recursive 123 | } 124 | 125 | List removeList = new List(); 126 | foreach (JToken jtoken in container.Children()) 127 | { 128 | if (jtoken is JProperty prop) 129 | { 130 | var matching = blacklist.Any(item => 131 | { 132 | return IsMatch(prop.Path, item); 133 | }); 134 | 135 | if (matching) 136 | { 137 | removeList.Add(jtoken); 138 | } 139 | } 140 | 141 | // call recursive 142 | MaskFieldsFromJToken(jtoken, blacklist, mask, blacklistPartial); 143 | } 144 | 145 | foreach (JToken el in removeList) 146 | { 147 | var prop = (JProperty)el; 148 | 149 | if (blacklistPartial.TryGetValue(blacklistPartial.GetKey(prop.Path), out var maskFunc)) 150 | { 151 | var value = prop.Value.ToString(); 152 | try 153 | { 154 | var valueMasked = (maskFunc != null) ? maskFunc(value) : mask; 155 | prop.Value = (valueMasked != value) ? valueMasked : mask; 156 | } 157 | catch (Exception ex) 158 | { 159 | throw new InvalidOperationException( 160 | $"An error occurred while executing the function in the dictionary value. {ex.Message}"); 161 | } 162 | } 163 | else 164 | { 165 | prop.Value = mask; 166 | } 167 | } 168 | } 169 | 170 | private static string GetKey(this Dictionary> blacklistPartial, string key) 171 | { 172 | var result = blacklistPartial.Keys.FirstOrDefault(dictionaryKey => 173 | { 174 | return IsMatch(key, dictionaryKey); 175 | }); 176 | 177 | return result ?? key; 178 | } 179 | 180 | private static bool IsMatch(string key, string value) 181 | { 182 | return Regex.IsMatch(key, WildCardToRegular(value), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); 183 | } 184 | 185 | private static string WildCardToRegular(string value) 186 | { 187 | return "^" + Regex.Escape(value).Replace("\\*", ".*") + "$"; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /JsonMasking/JsonMasking.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Thiago Barradas 6 | Thiago Barradas 7 | Ⓒ Thiago Barradas 8 | https://github.com/ThiagoBarradas/jsonmasking/blob/master/LICENSE 9 | https://github.com/ThiagoBarradas/jsonmasking 10 | https://github.com/ThiagoBarradas/jsonmasking 11 | newtonsoft json masking utility remove mask core standard easy replace wildcard regex 12 | https://i.imgur.com/ZhUJnNd.png 13 | Replace fields in json, replacing by something, don't care if property is in depth objects. Very useful to replace passwords credit card number, etc. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thiago Barradas 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. 22 | -------------------------------------------------------------------------------- /QA.md: -------------------------------------------------------------------------------- 1 | ## Code Climate 2 | 3 | [Script](https://gist.github.com/ThiagoBarradas/b66eeb1271e696a1a76cbdd98fae251d#file-codeclimate-dotnet-send-code-coverage-sh) 4 | 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/b1dd2400d00d1957047b/maintainability)](https://codeclimate.com/github/ThiagoBarradas/jsonmasking/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/b1dd2400d00d1957047b/test_coverage)](https://codeclimate.com/github/ThiagoBarradas/jsonmasking/test_coverage) 7 | 8 | ## Codacy 9 | 10 | [Script](https://gist.github.com/ThiagoBarradas/cf53df59b8d08dbe95e7aab5c2d2dfed#file-codacy-dotnet-send-code-coverage-sh) 11 | 12 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a1c03f81731447218e44c17f2595e496)](https://www.codacy.com/app/ThiagoBarradas/jsonmasking) 13 | [![Test Coverage](https://img.shields.io/codacy/coverage/a1c03f81731447218e44c17f2595e496.svg)](https://www.codacy.com/app/ThiagoBarradas/jsonmasking) 14 | 15 | ## Sonarqube 16 | 17 | [Script](https://gist.github.com/ThiagoBarradas/d59ee9a2dbb4c2b5ce5b96562dc97725#file-sonarqube-dotnet-send-analysis-and-code-coverage-sh) 18 | 19 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ThiagoBarradas_jsonmasking&metric=alert_status)](https://sonarcloud.io/dashboard?id=ThiagoBarradas_jsonmasking) 20 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ThiagoBarradas_jsonmasking&metric=coverage)](https://sonarcloud.io/dashboard?id=ThiagoBarradas_jsonmasking) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://barradas.visualstudio.com/Contributions/_apis/build/status/NugetPackage/JsonMasking?branchName=develop)](https://barradas.visualstudio.com/Contributions/_build/latest?definitionId=1&branchName=develop) 2 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ThiagoBarradas_jsonmasking&metric=alert_status)](https://sonarcloud.io/dashboard?id=ThiagoBarradas_jsonmasking) 3 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ThiagoBarradas_jsonmasking&metric=coverage)](https://sonarcloud.io/dashboard?id=ThiagoBarradas_jsonmasking) 4 | [![NuGet Downloads](https://img.shields.io/nuget/dt/JsonMasking.svg)](https://www.nuget.org/packages/JsonMasking/) 5 | [![NuGet Version](https://img.shields.io/nuget/v/JsonMasking.svg)](https://www.nuget.org/packages/JsonMasking/) 6 | 7 | # Json Masking 8 | 9 | Replace fields in json, replacing by something, don't care if property is in depth objects. Very useful to replace passwords, credit card number, etc. 10 | 11 | This library matching insensitive values with field namespaces. You can use wildcard * to allow any char in pattern; 12 | 13 | # Sample 14 | 15 | ```c# 16 | 17 | - Completely masking some of the properties 18 | 19 | var example = new 20 | { 21 | SomeValue = "Demo", 22 | Password = "SomePasswordHere", 23 | DepthObject = new 24 | { 25 | Password = "SomePasswordHere2", 26 | Card = new 27 | { 28 | Number = "555500022223333" 29 | } 30 | }, 31 | CreditCardNumber = "5555000011112222", 32 | Card = new 33 | { 34 | Number = "555500022223333" 35 | } 36 | }; 37 | 38 | var exampleAsString = JsonConvert.Serialize(example); // value must be a json string to masked 39 | 40 | // note that password is only replaced when is in root path 41 | var blacklist = new string[] { "password", "card.number", "*.card.number" "creditcardnumber" }; 42 | var mask = "******"; 43 | 44 | var maskedExampleAsString = exampleAsString.MaskFields(blacklist, mask); 45 | 46 | Console.WriteLine(maskedExampleAsString); 47 | 48 | ``` 49 | 50 | Output 51 | ```json 52 | { 53 | "SomeValue" : "Demo", 54 | "Password" : "******", 55 | "DepthObject" : { 56 | "Password" : "SomePasswordHere2", 57 | "Card" : { 58 | "Number" : "******" 59 | } 60 | }, 61 | "CreditCardNumber" : "******", 62 | "Card" : { 63 | "Number" : "******" 64 | } 65 | } 66 | ``` 67 | 68 | ```c# 69 | 70 | - Partially masking some of the properties 71 | 72 | var example = new 73 | { 74 | SomeValue = "Demo", 75 | Password = "SomePasswordHere", 76 | DepthObject = new 77 | { 78 | Password = "SomePasswordHere2", 79 | Card = new 80 | { 81 | Number = "555500022223333" 82 | } 83 | }, 84 | CreditCardNumber = "5555000011112222", 85 | Card = new 86 | { 87 | Number = "555500022223333" 88 | } 89 | }; 90 | 91 | var blacklistPartial = new Dictionary>(StringComparer.OrdinalIgnoreCase) // The key is the property to be partially masked and the value is the function to be applied. 92 | { 93 | { "*card.number", text => // Note that the property "*card.number" is also in the blacklist. If the property only exists in the blacklistPartial, it will not be masked. 94 | Regex.Replace( 95 | text, 96 | @"(\d{4,5})[ -|]?(\d{3,6})[ -|]?(\d{3,5})[ -|]?(\d{3,4})", 97 | match => $"{match.Value.Substring(0, 6)}*****{match.Value.Substring(match.Value.Length - 4, 4)}") 98 | } 99 | }; 100 | 101 | var exampleAsString = JsonConvert.Serialize(example); // The value must be a JSON string to be masked. 102 | 103 | // Note that the password is only replaced when it is in the root path. 104 | var blacklist = new string[] { "password", "*card.number", "creditcardnumber" }; 105 | var mask = "******"; 106 | 107 | var maskedExampleAsString = exampleAsString.MaskFields(blacklist, mask, blacklistPartial); // The blacklistPartial is optional. If provided, it will apply the mask only if the property is also in the blacklist. 108 | 109 | Console.WriteLine(maskedExampleAsString); 110 | 111 | ``` 112 | 113 | Output 114 | ```json 115 | { 116 | "SomeValue" : "Demo", 117 | "Password" : "******", 118 | "DepthObject" : { 119 | "Password" : "SomePasswordHere2", 120 | "Card" : { 121 | "Number" : "555500*****3333" 122 | } 123 | }, 124 | "CreditCardNumber" : "******", 125 | "Card" : { 126 | "Number" : "555500*****3333" 127 | } 128 | } 129 | ``` 130 | 131 | ## Install via NuGet 132 | 133 | ``` 134 | PM> Install-Package JsonMasking 135 | ``` 136 | 137 | ## How can I contribute? 138 | Please, refer to [CONTRIBUTING](.github/CONTRIBUTING.md) 139 | 140 | ## Found something strange or need a new feature? 141 | Open a new Issue following our issue template [ISSUE_TEMPLATE](.github/ISSUE_TEMPLATE.md) 142 | 143 | ## Changelog 144 | See in [nuget version history](https://www.nuget.org/packages/JsonMasking) 145 | 146 | ## Did you like it? Please, make a donate :) 147 | 148 | if you liked this project, please make a contribution and help to keep this and other initiatives, send me some Satochis. 149 | 150 | BTC Wallet: `1G535x1rYdMo9CNdTGK3eG6XJddBHdaqfX` 151 | 152 | ![1G535x1rYdMo9CNdTGK3eG6XJddBHdaqfX](https://i.imgur.com/mN7ueoE.png) 153 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: ubuntu 2 | platform: Any CPU 3 | 4 | environment: 5 | version: ${APPVEYOR_BUILD_VERSION} 6 | 7 | install: 8 | # Download codacy test report 9 | - sh: curl -L https://github.com/codacy/codacy-coverage-reporter/releases/download/4.0.5/codacy-coverage-reporter-4.0.5-assembly.jar > ./codacy-test-reporter.jar 10 | - sh: chmod +x ./codacy-test-reporter.jar 11 | # Download codeclimate test report 12 | - sh: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./codeclimate-test-reporter 13 | - sh: chmod +x ./codeclimate-test-reporter 14 | # Download sonarqube scanner / report 15 | - sh: dotnet tool install --global dotnet-sonarscanner 16 | # Install tools for generate 17 | - sh: dotnet tool install --global coverlet.console 18 | - sh: dotnet add JsonMasking.Tests/JsonMasking.Tests.csproj package coverlet.msbuild 19 | - sh: dotnet restore 20 | 21 | configuration: 22 | - Release 23 | 24 | dotnet_csproj: 25 | patch: true 26 | file: '**\*.csproj' 27 | version: '${version}' 28 | package_version: '${version}' 29 | assembly_version: '${version}' 30 | file_version: '${version}' 31 | informational_version: '${version}' 32 | 33 | before_build: 34 | - sh: dotnet build-server shutdown 35 | # Setup sonarqube 36 | - sh: dotnet sonarscanner begin /o:thiagobarradas-github /k:ThiagoBarradas_jsonmasking /v:${version} /d:sonar.host.url=https://sonarcloud.io /d:sonar.login=${SONARQUBE_TOKEN} /d:sonar.cs.opencover.reportsPaths="opencover.xml" 37 | 38 | build: 39 | project: JsonMasking.sln 40 | 41 | test_script: 42 | # Run test and generate result with opencover, cobertura and lcov format 43 | - sh: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=coverage /p:Exclude=[xunit.*]* JsonMasking.sln 44 | - sh: mv ./JsonMasking.Tests/coverage.opencover.xml opencover.xml 45 | - sh: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=coverage /p:Exclude=[xunit.*]* JsonMasking.sln 46 | - sh: mv ./JsonMasking.Tests/coverage.cobertura.xml cobertura.xml 47 | - sh: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=coverage /p:Exclude=[xunit.*]* JsonMasking.sln 48 | - sh: mkdir coverage 49 | - sh: mv ./JsonMasking.Tests/coverage.info coverage/lcov.info 50 | 51 | after_test: 52 | # Send test result to codecov 53 | - sh: bash <(curl -s https://codecov.io/bash) -f opencover.xml -t ${CODECOV_TOKEN} 54 | # Send test result to codeclimate 55 | - sh: ./codeclimate-test-reporter before-build 56 | - sh: ./codeclimate-test-reporter after-build -t lcov -r ${CC_TEST_REPORTER_ID} -p /home/appveyor/projects/newtonsoft-extensions-jsonmasking/ --exit-code $? 57 | # Send test result to codacy 58 | - sh: java -jar ./codacy-test-reporter.jar report -l CSharp -t ${CODACY_PROJECT_TOKEN} -r cobertura.xml 59 | # Send test result to sonarqube 60 | - sh: dotnet sonarscanner end /d:sonar.login=${SONARQUBE_TOKEN} 61 | # Pack to nuget 62 | - dotnet pack --configuration Release /p:Version=${version} 63 | 64 | artifacts: 65 | - path: JsonMasking/bin/Release/netstandard2.0/JsonMasking.dll 66 | name: JsonMasking.dll 67 | - path: JsonMasking/bin/Release/JsonMasking.${version}.nupkg 68 | name: JsonMasking.${version}.nupkg 69 | 70 | #deploy: 71 | # provider: NuGet 72 | # api_key: 73 | # secure: csyDlyeSjDmdYAQ/PK0GYNZ4ofK/yC6vd5ZLPnG2TgujmrAW7fUiQxjDVOWD4XAX 74 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # - requires: 2 | # https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarcloud 3 | # https://marketplace.visualstudio.com/items?itemName=SimondeLang.sonarcloud-buildbreaker 4 | # https://marketplace.visualstudio.com/items?itemName=gittools.gitversion 5 | # 6 | # - CI/CD for git flow 7 | # - for linux environment 8 | 9 | variables: 10 | organization: 'barradas' 11 | solution_path: 'JsonMasking.sln' 12 | sdk_project_name: 'JsonMasking' 13 | sdk_project_path: 'JsonMasking/JsonMasking.csproj' 14 | net_version: 'netstandard2.0' 15 | execute_test: 'true' 16 | test_project_base_folder: 'JsonMasking.Tests' 17 | test_project_path: 'JsonMasking.Tests/JsonMasking.Tests.csproj' 18 | execute_sonarcloud: 'true' 19 | sonarcloud_project: 'ThiagoBarradas_jsonmasking' 20 | sonarcloud_organization: 'thiagobarradas-github' 21 | sonarcloud_account: 'SonarCloudBarradas' 22 | 23 | trigger: 24 | branches: 25 | include: 26 | - release/* 27 | - hotfix/* 28 | - develop 29 | tags: 30 | include: 31 | - '*' 32 | 33 | pr: 34 | branches: 35 | include: 36 | - develop 37 | 38 | pool: 39 | vmImage: 'ubuntu-latest' 40 | 41 | stages: 42 | - stage: BuildApp 43 | jobs: 44 | - job: UpdateBuildVersion 45 | workspace: 46 | clean: all 47 | variables: 48 | current_counter: $[counter(variables['Build.SourceBranchName'], 1)] 49 | steps: 50 | - bash: | 51 | if [[ "$(Build.SourceVersionMessage)" =~ "hotfix/" ]]; 52 | then 53 | echo "##vso[task.setvariable variable=IsHotfix;isOutput=true]true" 54 | echo "##vso[task.setvariable variable=PreviousPrefix;isOutput=true]hotfix" 55 | else 56 | echo "##vso[task.setvariable variable=IsHotfix;isOutput=true]false" 57 | echo "##vso[task.setvariable variable=PreviousPrefix;isOutput=true]release" 58 | fi 59 | if [[ "$(Build.SourceBranch)" =~ "/hotfix/" ]] || 60 | [[ "$(Build.SourceBranch)" =~ "/release/" ]]; 61 | then 62 | echo "Generate Preview Release Version" 63 | echo "Version: $(Build.SourceBranchName)" 64 | echo " $(Build.SourceBranchName)-preview.$(current_counter)" 65 | echo "##vso[build.updatebuildnumber]$(Build.SourceBranchName)-preview.$(current_counter)" 66 | echo "##vso[task.setvariable variable=PureVersion;isOutput=true]$(Build.SourceBranchName)" 67 | elif [[ "$(Build.SourceBranch)" =~ "/tags/" ]]; 68 | then 69 | echo "Generate Release Version" 70 | echo "Version: $(Build.SourceBranchName)" 71 | echo "##vso[build.updatebuildnumber]$(Build.SourceBranchName)" 72 | echo "##vso[task.setvariable variable=SonarMasterWhenTag;isOutput=true]sonar.branch.name=master" 73 | echo "##vso[task.setvariable variable=PureVersion;isOutput=true]$(Build.SourceBranchName)" 74 | else 75 | echo "Generate Development Version" 76 | echo "##vso[build.updatebuildnumber]$(Build.BuildNumber)-develop" 77 | echo "Version: $(Build.BuildNumber)-develop" 78 | fi 79 | displayName: Update Version Number 80 | name: UpdateBuildVersionTask 81 | - bash: | 82 | STAGING_URL="https://dev.azure.com/$(organization)/$(System.TeamProject)/_apis/build/status/$(System.DefinitionId)?branchName=$(UpdateBuildVersionTask.PreviousPrefix)/$(GitVersion.SemVer)&stageName=PublishNugetPackage" 83 | STAGING_RESULT=$`curl --silent $STAGING_URL` 84 | echo "URL: $STAGING_URL" 85 | echo "RESULT: $STAGING_RESULT" 86 | SUCCEEDED=$`echo $STAGING_RESULT | grep -P 'succeeded' -o | head -n 1` 87 | if [[ "$STAGING_RESULT" =~ "succeeded" ]]; 88 | then 89 | echo "$PREVIOUS_PREFIX branch is ok!" 90 | elif [ "$(Build.SourceBranchName)" = "master" ]; 91 | then 92 | echo "$PREVIOUS_PREFIX branch is not ok!" 93 | exit 1 94 | fi 95 | condition: eq(variables['Build.SourceBranchName'], 'master') 96 | displayName: Break if release/hotfix branch is not passing 97 | name: BreakIsReleaseOrHotfixIsNotPassing 98 | 99 | - job: Build 100 | dependsOn: 101 | - UpdateBuildVersion 102 | condition: succeeded() 103 | workspace: 104 | clean: all 105 | steps: 106 | - script: | 107 | pwd && ls -la 108 | dotnet restore "$(solution_path)" 109 | dotnet publish -c Release -o $(System.DefaultWorkingDirectory)/bin "$(sdk_project_path)" 110 | mkdir artifact 111 | cp -r $(System.DefaultWorkingDirectory)/bin artifact/bin 112 | displayName: Build Application 113 | - task: PublishPipelineArtifact@1 114 | displayName: Store Application Artifact 115 | inputs: 116 | path: $(System.DefaultWorkingDirectory)/artifact 117 | artifact: $(sdk_project_name) 118 | 119 | - job: UnitTests 120 | dependsOn: 121 | - UpdateBuildVersion 122 | - Build 123 | workspace: 124 | clean: all 125 | condition: and(succeeded(), eq(variables['execute_test'], 'true')) 126 | steps: 127 | - script: | 128 | dotnet tool install --global coverlet.console 129 | dotnet tool install dotnet-reportgenerator-globaltool --tool-path . 130 | dotnet add $(test_project_path) package coverlet.msbuild 131 | dotnet add $(test_project_path) package XunitXml.TestLogger 132 | dotnet test /p:CollectCoverage=true \ 133 | /p:CoverletOutputFormat=\"opencover,cobertura\" \ 134 | /p:CoverletOutput=results/result \ 135 | /p:Exclude=[xunit.*]* "$(solution_path)" \ 136 | --test-adapter-path:. --logger:"xunit;LogFilePath=results/result.xunit.xml" 137 | ./reportgenerator "-reports:$(test_project_base_folder)/results/result.opencover.xml" \ 138 | "-targetdir:$(test_project_base_folder)/results/Cobertura" \ 139 | "-reporttypes:Cobertura;HTMLInline;HTMLChart" 140 | echo "##vso[task.setvariable variable=OpencoverSonar;isOutput=true]sonar.cs.opencover.reportsPaths=$(Pipeline.Workspace)/result.opencover.xml" 141 | ls $(System.DefaultWorkingDirectory)/$(test_project_base_folder)/results 142 | displayName: Running Unit Tests 143 | name: UnitTestsTask 144 | - task: PublishPipelineArtifact@1 145 | displayName: Store Test Result 146 | inputs: 147 | path: $(System.DefaultWorkingDirectory)/$(test_project_base_folder)/results 148 | artifact: UnitTestResults 149 | - task: PublishCodeCoverageResults@1 150 | displayName: Publish Code Coverage 151 | inputs: 152 | codeCoverageTool: 'Cobertura' 153 | summaryFileLocation: $(System.DefaultWorkingDirectory)/$(test_project_base_folder)/results/result.cobertura.xml 154 | pathToSources: . 155 | failIfCoverageEmpty: true 156 | - task: PublishTestResults@2 157 | displayName: Publish Test Result 158 | inputs: 159 | testResultsFormat: 'XUnit' 160 | testResultsFiles: '$(System.DefaultWorkingDirectory)/$(test_project_base_folder)/results/result.xunit.xml' 161 | testRunTitle: 'Collecting Test Results' 162 | 163 | - job: QualityAnalysis 164 | dependsOn: 165 | - UpdateBuildVersion 166 | - Build 167 | - UnitTests 168 | condition: | 169 | and 170 | ( 171 | in(dependencies.UpdateBuildVersion.result, 'Succeeded'), 172 | in(dependencies.Build.result, 'Succeeded', 'Skipped'), 173 | in(dependencies.UnitTests.result, 'Succeeded', 'Skipped'), 174 | eq(variables['execute_sonarcloud'], 'true') 175 | ) 176 | workspace: 177 | clean: all 178 | variables: 179 | SonarMasterWhenTag: $[ dependencies.UpdateBuildVersion.outputs['UpdateBuildVersionTask.SonarMasterWhenTag'] ] 180 | OpencoverSonar: $[ dependencies.UnitTests.outputs['UnitTestsTask.OpencoverSonar'] ] 181 | steps: 182 | - task: DownloadPipelineArtifact@2 183 | displayName: Get Test Result 184 | condition: eq(variables['execute_test'], 'true') 185 | inputs: 186 | artifact: UnitTestResults 187 | - task: SonarCloudPrepare@1 188 | displayName: Start Sonarqube Analysis 189 | inputs: 190 | SonarCloud: '$(sonarcloud_account)' 191 | organization: '$(sonarcloud_organization)' 192 | scannerMode: 'MSBuild' 193 | projectKey: '$(sonarcloud_project)' 194 | projectName: '$(sdk_project_name)' 195 | extraProperties: | 196 | sonar.sourceEncoding=UTF-8 197 | sonar.scm.forceReloadAll=true 198 | $(OpencoverSonar) 199 | $(SonarMasterWhenTag) 200 | - script: | 201 | dotnet build "$(solution_path)" 202 | displayName: Runnig Build For Analysis 203 | - task: SonarCloudAnalyze@1 204 | displayName: Finish Sonarqube Analysis 205 | - task: SonarCloudPublish@1 206 | displayName: Publish Sonarqube Analysis 207 | inputs: 208 | pollingTimeoutSec: '300' 209 | - task: sonarcloud-buildbreaker@2 210 | inputs: 211 | SonarCloud: '$(sonarcloud_account)' 212 | organization: '$(sonarcloud_organization)' 213 | 214 | - job: PackingNuget 215 | dependsOn: 216 | - UpdateBuildVersion 217 | - Build 218 | - UnitTests 219 | - QualityAnalysis 220 | workspace: 221 | clean: all 222 | condition: | 223 | and 224 | ( 225 | eq(dependencies.UpdateBuildVersion.result, 'Succeeded'), 226 | in(dependencies.Build.result, 'Succeeded', 'Skipped'), 227 | in(dependencies.UnitTests.result, 'Succeeded', 'Skipped'), 228 | in(dependencies.QualityAnalysis.result, 'Succeeded', 'Skipped'), 229 | or 230 | ( 231 | contains(variables['Build.SourceBranch'], '/hotfix/'), 232 | contains(variables['Build.SourceBranch'], '/release/'), 233 | contains(variables['Build.SourceBranch'], '/tags/') 234 | ) 235 | ) 236 | variables: 237 | ReleaseVersion: $(Build.BuildNumber) 238 | PureVersion: $[ dependencies.UpdateBuildVersion.outputs['UpdateBuildVersionTask.PureVersion'] ] 239 | steps: 240 | - task: DotNetCoreCLI@2 241 | displayName: 'Packing Release' 242 | inputs: 243 | command: 'custom' 244 | projects: '$(sdk_project_path)' 245 | custom: 'pack' 246 | arguments: '-p:Configuration=Release -p:PackageVersion=$(ReleaseVersion) -p:Version=$(PureVersion) --output $(Build.ArtifactStagingDirectory)' 247 | - task: PublishPipelineArtifact@1 248 | displayName: Store Nuget Packages 249 | inputs: 250 | path: '$(Build.ArtifactStagingDirectory)' 251 | artifact: NugetPackage 252 | 253 | - stage: DeployPackage 254 | dependsOn: BuildApp 255 | condition: | 256 | and 257 | ( 258 | succeeded(), 259 | or 260 | ( 261 | contains(variables['Build.SourceBranch'], '/hotfix/'), 262 | contains(variables['Build.SourceBranch'], '/release/'), 263 | contains(variables['Build.SourceBranch'], '/tags/') 264 | ) 265 | ) 266 | variables: 267 | - group: NugetCredentials 268 | jobs: 269 | - job: PublishNugetPackage 270 | steps: 271 | - task: DownloadPipelineArtifact@2 272 | displayName: Get Application Artifact 273 | inputs: 274 | artifact: NugetPackage 275 | - task: DotNetCoreCLI@2 276 | displayName: 'Publish package in NuGet' 277 | inputs: 278 | command: custom 279 | custom: nuget 280 | arguments: > 281 | push $(Pipeline.Workspace)/$(sdk_project_name).$(Build.BuildNumber).nupkg 282 | -s $(NugetServerUrl) 283 | -k $(NugetApiKey) --------------------------------------------------------------------------------