├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── cd.yaml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── CONTRIBUTING.md ├── Jenkinsfile ├── LICENSE.txt ├── README.md ├── docs ├── CHANGELOG.old.md ├── README.md └── images │ ├── add.svg │ ├── config-after.png │ ├── config-before.png │ ├── console-after.png │ ├── console-before.png │ ├── error.svg │ ├── global-settings.png │ └── information.svg ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── michelin │ │ └── cio │ │ └── hudson │ │ └── plugins │ │ ├── maskpasswords │ │ ├── MaskPasswordsBuildWrapper.java │ │ ├── MaskPasswordsConfig.java │ │ ├── MaskPasswordsConsoleLogFilter.java │ │ └── MaskPasswordsOutputStream.java │ │ ├── passwordparam │ │ ├── PasswordParameterDefinition.java │ │ └── PasswordParameterValue.java │ │ └── util │ │ └── MaskPasswordsUtil.java └── resources │ ├── com │ └── michelin │ │ └── cio │ │ └── hudson │ │ └── plugins │ │ ├── maskpasswords │ │ ├── MaskPasswordsBuildWrapper.properties │ │ └── MaskPasswordsBuildWrapper │ │ │ ├── config.jelly │ │ │ ├── config.properties │ │ │ ├── global.jelly │ │ │ ├── global.properties │ │ │ ├── help-globalVarMaskEnabledGlobally.html │ │ │ ├── help-globalVarMaskRegexes.html │ │ │ ├── help-globalVarPasswordPairs.html │ │ │ └── help.html │ │ └── passwordparam │ │ ├── PasswordParameterDefinition.properties │ │ ├── PasswordParameterDefinition │ │ ├── config.jelly │ │ └── index.jelly │ │ └── PasswordParameterValue │ │ └── value.jelly │ └── index.jelly └── test ├── java └── com │ └── michelin │ └── cio │ └── hudson │ └── plugins │ ├── integrations │ └── CorePasswordParameterTest.java │ ├── maskpasswords │ ├── MaskPasswordConfigTests.java │ ├── MaskPasswordsURLEncodingTest.java │ └── MaskPasswordsWorkflowTest.java │ ├── passwordparam │ └── PasswordParameterTest.java │ └── util │ └── MaskPasswordsUtilTest.java └── resources └── com └── michelin └── cio └── hudson └── plugins └── util ├── echoAwsJson.txt └── echoAwsJsonMasked.txt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/mask-passwords-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: maven 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | work 3 | 4 | # Eclipse project files 5 | .settings 6 | .classpath 7 | .project 8 | 9 | # Intellij project files 10 | .idea 11 | *.iml 12 | 13 | **/.DS_Store 14 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the mask passwords plugin 2 | 3 | Thank you for your interest in contributing to the Jenkins "mask passwords plugin"! This guide will provide you with the necessary information to contribute to the plugin. 4 | 5 | ## Getting Started 6 | 7 | Before you begin contributing to the plugin, you should familiarize yourself with the plugin's source code and build process. You can find the plugin's source code on GitHub at https://github.com/jenkinsci/mask-passwords-plugin. The plugin is built using Maven, so you will need to have Maven installed on your system to build the plugin. 8 | 9 | ## Newcomers 10 | 11 | If you are a newcomer contributor and have any questions, please do not hesitate to ask in the [Newcomers Gitter channel](https://app.gitter.im/#/room/#jenkinsci_newcomer-contributors:gitter.im). 12 | 13 | ## Contributing Code 14 | 15 | If you would like to contribute code to the plugin, please follow these steps: 16 | 17 | * Fork the plugin's repository on GitHub. 18 | * Clone your forked repository to your local machine. 19 | * Make your changes to the plugin's source code. 20 | * Test your changes to ensure that they do not introduce any new issues. 21 | * Commit your changes and push them to your forked repository. 22 | * Open a pull request from your forked repository to the main repository. Please ensure that your pull request includes a clear description of the changes you have made and the reason for making them. 23 | 24 | ## Run Locally 25 | 26 | * Ensure Java 11 or 17 is available. 27 | ```console 28 | $ java -version 29 | openjdk version "11.0.18" 2023-01-17 30 | OpenJDK Runtime Environment Temurin-11.0.18+10 (build 11.0.18+10) 31 | OpenJDK 64-Bit Server VM Temurin-11.0.18+10 (build 11.0.18+10, mixed mode) 32 | ``` 33 | 34 | * Ensure Maven > 3.8.4 or newer is installed and included in the PATH environment variable. 35 | ```console 36 | $ mvn --version 37 | Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8) 38 | Maven home: /opt/apache-maven-3.9.1 39 | Java version: 11.0.18, vendor: Eclipse Adoptium, runtime: /opt/jdk-11 40 | Default locale: en_US, platform encoding: UTF-8 41 | OS name: "linux", version: "4.18.0-425.13.1.el8_7.x86_64", arch: "amd64", family: "unix" 42 | ``` 43 | 44 | ## CLI 45 | 46 | - Use the following command 47 | ```console 48 | $ mvn hpi:run 49 | ... 50 | INFO: Jenkins is fully up and running 51 | ``` 52 | - Open http://localhost:8080/jenkins/ to test the plugin locally. 53 | 54 | ## Reporting Issues 55 | 56 | If you encounter any issues with the plugin, please report them on the [Jira issue tracker](https://www.jenkins.io/participate/report-issue/redirect/#15761) . When reporting an issue, please include as much information as possible, including a description of the issue, steps to reproduce the issue, and any relevant logs or error messages. ["How to report an issue"](https://www.jenkins.io/participate/report-issue/) provides more details on the information needed for a better bug report. 57 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | https://github.com/jenkins-infra/pipeline-library/ 4 | */ 5 | buildPlugin( 6 | forkCount: '1C', // Run parallel tests on ci.jenkins.io for lower costs, faster feedback 7 | useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests 8 | configurations: [ 9 | [platform: 'linux', jdk: 21], 10 | [platform: 'windows', jdk: 17], 11 | ]) 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2011, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mask Passwords plugin for Jenkins 2 | 3 | This plugin allows masking passwords that may appear in the console. 4 | 5 | ## About this plugin 6 | 7 | This plugin allows masking passwords that may appear in the console, 8 | including the ones defined as build parameters. 9 | This often happens, for example, when you use build steps which *can't* handle passwords properly. 10 | Take a look at the following example. 11 | 12 | ### Before 13 | 14 | Consider you're using an **Invoke Ant** build step to run an Ant target. 15 | This target requires a password to achieve its goal. 16 | You would end up having a job configuration like this: 17 | 18 | ![](docs/images/config-before.png) 19 | 20 | Of course, you could have created a variable to store the password and use this variable in the build step configuration so that it doesn't appear as plain text. 21 | But you would still end with a console output like this: 22 | 23 | ![](docs/images/console-before.png) 24 | 25 | ### After 26 | 27 | When activating the **Mask passwords** option in a job, the builds' **Password Parameters** 28 | (or any other type of build parameters selected for masking in **Manage Jenkins** \> **Configure System**) are automatically masked from the console. 29 | Furthermore, you can also safely define a list of static passwords to be masked 30 | (you can also define a list of static password shared by all jobs in Jenkins' main configuration screen). 31 | As such, the passwords don't appear anymore as plain text in the job configuration 32 | (plus it is ciphered in the job configuration file): 33 | 34 | ![](docs/images/config-after.png) 35 | 36 | Once done, new builds will have the passwords masked from the console output: 37 | 38 | ![](docs/images/console-after.png) 39 | 40 | ## User guide 41 | 42 | First, go to Jenkins' main configuration screen (**Manage Jenkins** \> **Configure System**) and select, 43 | in the **Mask Passwords - Configuration** section, which kind of build parameters have to be automatically masked from the console output: 44 | 45 | ![](docs/images/global-settings.png) 46 | 47 | Notice that, as of version 2.7, you can also define global passwords (defined as pairs of name/password) that can be accessed across all jobs. 48 | 49 | Then, for a specific job, activate the **Mask passwords** option in the **Build Environment** section to mask passwords from the console: 50 | 51 | 1. All the password parameters defined for the job will be automatically hidden. 52 | 2. For each other kind of password (that is, static ones) that may appear in the console output, 53 | add an entry (by clicking on the **Add** button) and set the **Password** field. 54 | You may additionally set the **Name** field. 55 | If you do so, the password will then be available as a standard variable. 56 | It is then possible to refer to this password using this variable rather than keying it in a field which is not ciphered. 57 | Take a look at the screenshots above for an example. 58 | 59 | ## Release Notes 60 | 61 | * See [GitHub Releases](https://github.com/jenkinsci/mask-passwords-plugin/releases) for recent releases 62 | * See the [Changelog archive](./docs/CHANGELOG.old.md) for 2.12.0 and before 63 | -------------------------------------------------------------------------------- /docs/CHANGELOG.old.md: -------------------------------------------------------------------------------- 1 | # Version history (archive) 2 | 3 | ## New releases 4 | 5 | See [GitHub Releases](https://github.com/jenkinsci/mask-passwords-plugin/releases) 6 | 7 | ## Version 2.12.0 8 | 9 | Release Date: (Jun 01, 2018) 10 | 11 | - [![(plus)](images/add.svg) PR 12 | \#18](https://github.com/jenkinsci/mask-passwords-plugin/pull/18) - 13 | Mask Passwords Console Log filter can be now applied to all Run 14 | types 15 | - It should allow filtering Pipeline jobs 16 | once [JENKINS-45693](http://issues.jenkins-ci.org/browse/JENKINS-45693) 17 | is implemented 18 | - ![(info)](images/information.svg) Update 19 | minimal core requirement to 1.625.3 20 | 21 | ## Version 2.11.0 22 | 23 | Release Date: (Mar 13, 2018) 24 | 25 | - ![(info)](images/information.svg) Update 26 | minimal core requirement to 1.625.3 27 | - ![(info)](images/information.svg) Developer: 28 | Update Plugin POm to the latest version 29 | 30 | ## Version 2.10.1 31 | 32 | Release Date: (Apr 11, 2017) 33 | 34 | - ![(error)](images/error.svg) Prevent 35 | NullPointerException when loading configurations from the disk 36 | ([JENKINS-43504](https://issues.jenkins-ci.org/browse/JENKINS-43504)) 37 | 38 | ## Version 2.10 39 | 40 | Release Date: (Apr 08, 2017) 41 | 42 | - ![(plus)](images/add.svg) Rework 43 | the Parameter Definition processing engine, improve the reliability 44 | of Sensitive parameter discovery 45 | - ![(error)](images/error.svg) Fix 46 | a number of issues with parameter masking reported to the plugin. 47 | Full list will be published later 48 | 49 | ## Version 2.9 50 | 51 | Release Date: (30/11/2016) 52 | 53 | - ![(plus)](images/add.svg) 54 | Add option to mask output strings by a regular expression, also with 55 | a global setting ([PR 56 | \#6](https://github.com/jenkinsci/mask-passwords-plugin/pull/6)) 57 | - ![(error)](images/error.svg) 58 | Properly invoke flush/close operations for the logger in 59 | MaskPasswordOutputStream ([PR 60 | \#8](https://github.com/jenkinsci/mask-passwords-plugin/pull/8)) 61 | - ![(error)](images/error.svg) 62 | Fix issues reported by FindBugs 63 | - ![(info)](images/information.svg) 64 | Update to the new Parent POM 65 | 66 | ## Version 2.8 67 | 68 | Release Date: (18/10/2015) 69 | 70 | - ![(plus)](images/add.svg) 71 | Implement SimpleBuildWrapper in order to support Workflow project 72 | type 73 | ([JENKINS-27392](https://issues.jenkins-ci.org/browse/JENKINS-27392)) 74 | 75 | ## Version 2.7.4 76 | 77 | Release Date: (29/07/2015) 78 | 79 | - ![(error)](images/error.svg) 80 | Password parameters were insensitive 81 | - ![(error)](images/error.svg) 82 | "Mask passwords" build wrapper was generating insensitive 83 | environment variables 84 | 85 | Fixed issues (to be investigated and updated): 86 | 87 | - Masking of global password parameters in EnvInject 88 | ([JENKINS-25821](https://issues.jenkins-ci.org/browse/JENKINS-25821)) 89 | - Masked Passwords are shown as input parameters in Build pipeline 90 | plugin 91 | ([JENKINS-16516](https://issues.jenkins-ci.org/browse/JENKINS-16516)) 92 | 93 | ## Version 2.7.3 94 | 95 | Release Date: (29/04/2015) 96 | 97 | - Fixed 98 | [JENKINS-12161](https://issues.jenkins-ci.org/browse/JENKINS-12161): 99 | EnvInject vars could have been not masked because of plugins loading order 100 | - Fixed 101 | [JENKINS-14687](https://issues.jenkins-ci.org/browse/JENKINS-14687): 102 | password exposed unencrypted in HTML source 103 | 104 | ## Version 2.7.2 105 | 106 | Release Date: (12/07/2011) 107 | 108 | - Fixed 109 | [JENKINS-11934](https://issues.jenkins-ci.org/browse/JENKINS-11934): 110 | Once a job config was submitted, new/updated global passwords were 111 | not masked 112 | - Implemented 113 | [JENKINS-11924](https://issues.jenkins-ci.org/browse/JENKINS-11924): 114 | Improved global passwords-related labels 115 | 116 | ## Version 2.7.1 117 | 118 | Release Date: (10/27/2011) 119 | 120 | - Fixed 121 | [JENKINS-11514](https://issues.jenkins-ci.org/browse/JENKINS-11514): 122 | When migrating from an older version of the plugin, 123 | `NullPointerException`s were preventing the jobs using Mask 124 | Passwords to load 125 | - Fixed 126 | [JENKINS-11515](https://issues.jenkins-ci.org/browse/JENKINS-11515): 127 | Mask Passwords global config was not actually saved when no global 128 | passwords were defined 129 | 130 | ## Version 2.7 131 | 132 | Release Date: (10/20/2011) 133 | 134 | - Implemented 135 | [JENKINS-11399](https://issues.jenkins-ci.org/browse/JENKINS-11399): 136 | It is now possible to define name/password pairs in Jenkins' main 137 | configuration screen (**Manage Jenkins** \> **Configure System**) 138 | 139 | ## Version 2.6.1 140 | 141 | Release Date: (05/26/2011) 142 | 143 | - Fixed a bug which was emptying the console output if there was no 144 | password to actually mask 145 | 146 | ## Version 2.6 147 | 148 | Release Date: (04/29/2011) 149 | 150 | - Added a new type of build parameter: **Non-Stored Password 151 | Parameter** 152 | - Blank passwords are no more masked, avoiding overcrowding the 153 | console with stars 154 | 155 | ## Version 2.5 156 | 157 | Release Date: (03/11/2011) 158 | 159 | - New configuration screen (in **Manage Jenkins** \> **Configure 160 | System**) allowing to select which build parameters have to be 161 | masked (**Password Parameter** are selected by default) 162 | - Fixed a bug which was preventing to mask passwords containing 163 | regular expressions' meta-characters or escape sequences 164 | 165 | ## Version 2.0 166 | 167 | Release Date: (02/23/2011) 168 | 169 | - Builds' **Password Parameter**s are now automatically masked. 170 | 171 | ## Version 1.0 172 | 173 | Release Date: (09/01/2010) 174 | 175 | - Initial release 176 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This plugin allows masking passwords that may appear in the console 2 | 3 | The current version of this plugin may not be safe to use. Please review 4 | the following warnings before use: 5 | 6 | - [Plain text passwords shown in global configuration form 7 | fields](https://jenkins.io/security/advisory/2019-08-07/#SECURITY-157) 8 | 9 | **This plugin is up for adoption.** Want to help improve this plugin? 10 | [Click here to learn 11 | more](https://wiki.jenkins.io/display/JENKINS/Adopt+a+Plugin "Adopt a Plugin")! 12 | 13 | # About this plugin 14 | 15 | This plugin allows masking passwords that may appear in the console, 16 | including the ones defined as build parameters. This often happens, for 17 | example, when you use build steps which *can't* handle passwords 18 | properly. Take a look at the following example. 19 | 20 | ## Before 21 | 22 | Consider you're using an **Invoke Ant** build step to run an Ant target. 23 | This target requires a password to achieve its goal. You would end up 24 | having a job configuration like this: 25 | 26 | ![](docs/images/config-before.png) 27 | 28 | Of course, you could have created a variable to store the password and 29 | use this variable in the build step configuration so that it doesn't 30 | appear as plain text. But you would still end with a console output like 31 | this: 32 | 33 | ![](docs/images/console-before.png) 34 | 35 | ## After 36 | 37 | When activating the **Mask passwords** option in a job, the builds' 38 | **Password Parameters** (or any other type of build parameters selected 39 | for masking in **Manage Jenkins** \> **Configure System**) are 40 | automatically masked from the console. Furthermore, you can also safely 41 | define a list of static passwords to be masked (you can also define a 42 | list of static password shared by all jobs in Jenkins' main 43 | configuration screen). As such, the passwords don't appear anymore as 44 | plain text in the job configuration (plus it is ciphered in the job 45 | configuration file): 46 | 47 | ![](docs/images/config-after.png) 48 | 49 | Once done, new builds will have the passwords masked from the console 50 | output: 51 | 52 | ![](docs/images/console-after.png) 53 | 54 | # User guide 55 | 56 | First, go to Jenkins' main configuration screen (**Manage Jenkins** \> 57 | **Configure System**) and select, in the **Mask Passwords - 58 | Configuration** section, which kind of build parameters have to be 59 | automatically masked from the console output: 60 | 61 | ![](docs/images/global-settings.png) 62 | 63 | Notice that, as of version 2.7, you can also define global passwords 64 | (defined as pairs of name/password) that can be accessed across all 65 | jobs. 66 | 67 | Then, for a specific job, activate the **Mask passwords** option in the 68 | **Build Environment** section to mask passwords from the console: 69 | 70 | 1. All the password parameters defined for the job will be 71 | automatically hidden. 72 | 2. For each other kind of password (that is, static ones) that may 73 | appear in the console output, add an entry (by clicking on the 74 | **Add** button) and set the **Password** field. 75 | You may additionally set the **Name** field. If you do so, the 76 | password will then be available as a standard variable. It is then 77 | possible to refer to this password using this variable rather than 78 | keying it in a field which is not ciphered. Take a look at the 79 | screenshots above for an example. 80 | 81 | -------------------------------------------------------------------------------- /docs/images/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/config-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/mask-passwords-plugin/900cdfb39bc23a49f952ae5202bdd66f5f474ead/docs/images/config-after.png -------------------------------------------------------------------------------- /docs/images/config-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/mask-passwords-plugin/900cdfb39bc23a49f952ae5202bdd66f5f474ead/docs/images/config-before.png -------------------------------------------------------------------------------- /docs/images/console-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/mask-passwords-plugin/900cdfb39bc23a49f952ae5202bdd66f5f474ead/docs/images/console-after.png -------------------------------------------------------------------------------- /docs/images/console-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/mask-passwords-plugin/900cdfb39bc23a49f952ae5202bdd66f5f474ead/docs/images/console-before.png -------------------------------------------------------------------------------- /docs/images/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/global-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/mask-passwords-plugin/900cdfb39bc23a49f952ae5202bdd66f5f474ead/docs/images/global-settings.png -------------------------------------------------------------------------------- /docs/images/information.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 5.17 8 | 9 | 10 | 11 | mask-passwords 12 | hpi 13 | Mask Passwords Plugin 14 | Masks passwords that may appear in the console. Provides non-stored password parameter. 15 | https://github.com/jenkinsci/mask-passwords-plugin 16 | ${changelist} 17 | 18 | 19 | 2.479 20 | ${jenkins.baseline}.3 21 | Max 22 | 999999-SNAPSHOT 23 | jenkinsci/mask-passwords-plugin 24 | true 25 | 26 | 27 | 28 | scm:git:https://github.com/${gitHubRepo}.git 29 | scm:git:git@github.com:${gitHubRepo}.git 30 | https://github.com/${gitHubRepo} 31 | ${scmTag} 32 | 33 | 34 | 35 | 36 | MIT License 37 | https://opensource.org/licenses/MIT 38 | 39 | 40 | 41 | 42 | 43 | repo.jenkins-ci.org 44 | https://repo.jenkins-ci.org/public/ 45 | 46 | 47 | 48 | 49 | 50 | repo.jenkins-ci.org 51 | https://repo.jenkins-ci.org/public/ 52 | 53 | 54 | 55 | 56 | 57 | 58 | io.jenkins.tools.bom 59 | bom-${jenkins.baseline}.x 60 | 4845.v9163d3278e4f 61 | import 62 | pom 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.jenkins-ci.plugins.workflow 70 | workflow-job 71 | test 72 | 73 | 74 | org.jenkins-ci.plugins 75 | structs 76 | 77 | 78 | org.jenkins-ci.plugins.workflow 79 | workflow-basic-steps 80 | test 81 | 82 | 83 | org.jenkins-ci.plugins.workflow 84 | workflow-cps 85 | test 86 | 87 | 88 | org.jenkins-ci.plugins.workflow 89 | workflow-durable-task-step 90 | test 91 | 92 | 93 | org.jenkins-ci.plugins.workflow 94 | workflow-step-api 95 | tests 96 | test 97 | 98 | 99 | org.jenkins-ci.plugins.workflow 100 | workflow-support 101 | tests 102 | test 103 | 104 | 105 | org.awaitility 106 | awaitility 107 | 4.3.0 108 | test 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/main/java/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010-2012, Manufacture Francaise des Pneumatiques Michelin, 5 | * Romain Seguy 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | package com.michelin.cio.hudson.plugins.maskpasswords; 27 | 28 | import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsConfig.VarMaskRegexEntry; 29 | import com.thoughtworks.xstream.converters.Converter; 30 | import com.thoughtworks.xstream.converters.MarshallingContext; 31 | import com.thoughtworks.xstream.converters.UnmarshallingContext; 32 | import com.thoughtworks.xstream.io.HierarchicalStreamReader; 33 | import com.thoughtworks.xstream.io.HierarchicalStreamWriter; 34 | import edu.umd.cs.findbugs.annotations.CheckForNull; 35 | import edu.umd.cs.findbugs.annotations.NonNull; 36 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 37 | import hudson.EnvVars; 38 | import hudson.Extension; 39 | import hudson.console.ConsoleLogFilter; 40 | import hudson.model.AbstractBuild; 41 | import hudson.model.AbstractDescribableImpl; 42 | import hudson.model.AbstractProject; 43 | import hudson.model.Descriptor; 44 | import hudson.model.ParameterValue; 45 | import hudson.model.ParametersAction; 46 | import hudson.model.Run; 47 | import hudson.model.TaskListener; 48 | import hudson.tasks.BuildWrapperDescriptor; 49 | import hudson.util.Secret; 50 | import jenkins.tasks.SimpleBuildWrapper; 51 | import net.sf.json.JSONArray; 52 | import net.sf.json.JSONObject; 53 | import org.apache.commons.lang.StringUtils; 54 | import org.jenkinsci.Symbol; 55 | import org.jenkinsci.plugins.structs.describable.CustomDescribableModel; 56 | import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable; 57 | import org.jvnet.localizer.Localizable; 58 | import org.jvnet.localizer.ResourceBundleHolder; 59 | import org.kohsuke.stapler.DataBoundConstructor; 60 | import org.kohsuke.stapler.StaplerRequest2; 61 | 62 | import java.io.IOException; 63 | import java.io.OutputStream; 64 | import java.io.Serializable; 65 | import java.util.ArrayList; 66 | import java.util.HashMap; 67 | import java.util.List; 68 | import java.util.Map; 69 | import java.util.Objects; 70 | import java.util.Set; 71 | import java.util.TreeMap; 72 | import java.util.logging.Level; 73 | import java.util.logging.Logger; 74 | 75 | /** 76 | * Build wrapper that alters the console so that passwords don't get displayed. 77 | * 78 | * @author Romain Seguy (http://openromain.blogspot.com) 79 | */ 80 | public final class MaskPasswordsBuildWrapper extends SimpleBuildWrapper { 81 | 82 | private final List varPasswordPairs; 83 | private final List varMaskRegexes; 84 | 85 | @DataBoundConstructor 86 | public MaskPasswordsBuildWrapper(List varPasswordPairs, List varMaskRegexes) { 87 | this.varPasswordPairs = varPasswordPairs; 88 | this.varMaskRegexes = varMaskRegexes; 89 | } 90 | 91 | public MaskPasswordsBuildWrapper(List varPasswordPairs) { 92 | this.varPasswordPairs = varPasswordPairs; 93 | this.varMaskRegexes = new ArrayList<>(); 94 | } 95 | 96 | @Override 97 | public ConsoleLogFilter createLoggerDecorator(Run build) { 98 | List allPasswords = new ArrayList<>(); // all passwords to be masked 99 | List allRegexes = new ArrayList<>(); // all regexes to be masked 100 | MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); 101 | 102 | // global passwords 103 | List globalVarPasswordPairs = config.getGlobalVarPasswordPairs(); 104 | for(VarPasswordPair globalVarPasswordPair: globalVarPasswordPairs) { 105 | allPasswords.add(globalVarPasswordPair.getPlainTextPassword()); 106 | } 107 | 108 | // global regexes 109 | List globalVarMaskRegexes = config.getGlobalVarMaskRegexesU(); 110 | for(MaskPasswordsConfig.VarMaskRegexEntry globalVarMaskRegex: globalVarMaskRegexes) { 111 | allRegexes.add(globalVarMaskRegex.getValue()); 112 | } 113 | 114 | // job's passwords 115 | if(varPasswordPairs != null) { 116 | for(VarPasswordPair varPasswordPair: varPasswordPairs) { 117 | String password = varPasswordPair.getPlainTextPassword(); 118 | if(StringUtils.isNotBlank(password)) { 119 | allPasswords.add(password); 120 | } 121 | } 122 | } 123 | 124 | // job's regexes 125 | if(varMaskRegexes != null) { 126 | for(VarMaskRegexEntry entry: varMaskRegexes) { 127 | String regex = entry.getRegexString(); 128 | if(StringUtils.isNotBlank(regex)) { 129 | allRegexes.add(regex); 130 | } 131 | } 132 | } 133 | 134 | // find build parameters which are passwords (PasswordParameterValue) 135 | ParametersAction params = build.getAction(ParametersAction.class); 136 | if(params != null) { 137 | for(ParameterValue param : params) { 138 | if(config.isMasked(param, param.getClass().getName())) { 139 | EnvVars env = new EnvVars(); 140 | param.buildEnvironment(build, env); 141 | String password = env.get(param.getName()); 142 | if(StringUtils.isNotBlank(password)) { 143 | allPasswords.add(password); 144 | } 145 | } 146 | } 147 | } 148 | 149 | return new FilterImpl(allPasswords, allRegexes); 150 | } 151 | 152 | @Override 153 | public boolean requiresWorkspace() { 154 | return false; 155 | } 156 | 157 | private static final class FilterImpl extends ConsoleLogFilter implements Serializable { 158 | 159 | private static final long serialVersionUID = 1L; 160 | 161 | private final List allPasswords; 162 | private final List allRegexes; 163 | 164 | FilterImpl(List allPasswords, List allRegexes) { 165 | this.allPasswords = new ArrayList<>(); 166 | this.allRegexes = new ArrayList<>(); 167 | for (String password : allPasswords) { 168 | this.allPasswords.add(Secret.fromString(password)); 169 | } 170 | this.allRegexes.addAll(allRegexes); 171 | } 172 | 173 | @Override 174 | public OutputStream decorateLogger(Run run, OutputStream logger) { 175 | List passwords = new ArrayList<>(); 176 | for (Secret password : allPasswords) { 177 | passwords.add(password.getPlainText()); 178 | } 179 | List regexes = new ArrayList<>(allRegexes); 180 | String runName = run != null ? run.getFullDisplayName() : ""; 181 | return new MaskPasswordsOutputStream(logger, passwords, regexes, runName); 182 | } 183 | 184 | } 185 | 186 | /** 187 | * Contributes the passwords defined by the user as variables that can be reused 188 | * from build steps (and other places). 189 | */ 190 | @Override 191 | public void makeBuildVariables(AbstractBuild build, Map variables) { 192 | // global var/password pairs 193 | MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); 194 | List globalVarPasswordPairs = config.getGlobalVarPasswordPairs(); 195 | // we can't use variables.putAll() since passwords are ciphered when in varPasswordPairs 196 | for(VarPasswordPair globalVarPasswordPair: globalVarPasswordPairs) { 197 | variables.put(globalVarPasswordPair.getVar(), globalVarPasswordPair.getPlainTextPassword()); 198 | } 199 | 200 | // job's var/password pairs 201 | if(varPasswordPairs != null) { 202 | // cf. comment above 203 | for(VarPasswordPair varPasswordPair: varPasswordPairs) { 204 | if(StringUtils.isNotBlank(varPasswordPair.getVar())) { 205 | variables.put(varPasswordPair.getVar(), varPasswordPair.getPlainTextPassword()); 206 | } 207 | } 208 | } 209 | } 210 | 211 | @Override 212 | public void makeSensitiveBuildVariables(AbstractBuild build, Set sensitiveVariables) { 213 | final Map variables = new TreeMap<>(); 214 | makeBuildVariables(build, variables); 215 | sensitiveVariables.addAll(variables.keySet()); 216 | } 217 | 218 | @Override 219 | public void setUp(Context context, Run build, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException { 220 | // nothing to do here 221 | } 222 | 223 | public List getVarPasswordPairs() { 224 | return varPasswordPairs; 225 | } 226 | 227 | public List getVarMaskRegexes() { 228 | return varMaskRegexes; 229 | } 230 | 231 | /** 232 | * Represents name/password entries defined by users in their jobs. 233 | * Equality and hashcode are based on {@code var} only, not {@code password}. 234 | * If the class gets extended, a clone() method must be implemented without super.clone() calls. 235 | */ 236 | public static class VarPasswordPair extends AbstractDescribableImpl implements Cloneable { 237 | 238 | private final String var; 239 | private final Secret password; 240 | 241 | @DataBoundConstructor 242 | public VarPasswordPair(String var, Secret password) { 243 | this.var = var; 244 | this.password = password; 245 | } 246 | 247 | @Override 248 | @SuppressFBWarnings(value = "CN_IDIOM_NO_SUPER_CALL", justification = "We do not expect anybody to use this class." 249 | + "If they do, they must override clone() as well") 250 | public Object clone() { 251 | return new VarPasswordPair(getVar(), password); 252 | } 253 | 254 | @Override 255 | public boolean equals(Object obj) { 256 | if(obj == null) { 257 | return false; 258 | } 259 | if(getClass() != obj.getClass()) { 260 | return false; 261 | } 262 | final VarPasswordPair other = (VarPasswordPair) obj; 263 | return Objects.equals(this.var, other.var); 264 | } 265 | 266 | public String getVar() { 267 | return var; 268 | } 269 | 270 | public Secret getPassword() { 271 | return password; 272 | } 273 | 274 | public String getPlainTextPassword() { 275 | if (password == null || StringUtils.isBlank(password.getPlainText())) { 276 | return null; 277 | } 278 | 279 | return password.getPlainText(); 280 | } 281 | 282 | @Override 283 | public int hashCode() { 284 | int hash = 3; 285 | hash = 67 * hash + (this.var != null ? this.var.hashCode() : 0); 286 | return hash; 287 | } 288 | 289 | @Extension 290 | /** 291 | * {@link CustomDescribableModel} is needed because pipeline doesn't natively support the {@link Secret} class 292 | * but we need Secret so that data-binding works correctly. 293 | */ 294 | public static class DescriptorImpl extends Descriptor implements CustomDescribableModel { 295 | @NonNull 296 | @Override 297 | public UninstantiatedDescribable customUninstantiate(@NonNull UninstantiatedDescribable step) { 298 | Map arguments = step.getArguments(); 299 | Map newMap1 = new HashMap<>(); 300 | newMap1.put("var", arguments.get("var")); 301 | newMap1.put("password", ((Secret) arguments.get("password")).getPlainText()); 302 | return step.withArguments(newMap1); 303 | } 304 | 305 | @NonNull 306 | @Override 307 | public Map customInstantiate(@NonNull Map arguments) { 308 | Map newMap = new HashMap<>(); 309 | newMap.put("var", arguments.get("var")); 310 | Object password = arguments.get("password"); 311 | if (password instanceof String) { 312 | password = Secret.fromString((String) password); 313 | } 314 | newMap.put("password", password); 315 | return newMap; 316 | } 317 | } 318 | 319 | } 320 | 321 | /** 322 | * Represents regexes defined by users in their jobs. 323 | * If the class gets extended, a clone() method must be implemented without super.clone() calls. 324 | */ 325 | public static class VarMaskRegex extends AbstractDescribableImpl implements Cloneable { 326 | 327 | private final String regex; 328 | 329 | @DataBoundConstructor 330 | public VarMaskRegex(String regex) { 331 | this.regex = regex; 332 | } 333 | 334 | @Override 335 | @SuppressFBWarnings(value = "CN_IDIOM_NO_SUPER_CALL", justification = "We do not expect anybody to use this class." 336 | + "If they do, they must override clone() as well") 337 | public Object clone() { 338 | return new VarMaskRegex(getRegex()); 339 | } 340 | 341 | @Override 342 | public boolean equals(Object obj) { 343 | if(obj == null) { 344 | return false; 345 | } 346 | if(getClass() != obj.getClass()) { 347 | return false; 348 | } 349 | final VarMaskRegex other = (VarMaskRegex) obj; 350 | return Objects.equals(this.regex, other.regex); 351 | } 352 | 353 | @CheckForNull 354 | public String getRegex() { 355 | return regex; 356 | } 357 | 358 | @Override 359 | public int hashCode() { 360 | int hash = 3; 361 | hash = 67 * hash + (this.regex != null ? this.regex.hashCode() : 0); 362 | return hash; 363 | } 364 | 365 | public String toString() { 366 | return regex; 367 | } 368 | 369 | @Extension 370 | public static class DescriptorImpl extends Descriptor {} 371 | 372 | } 373 | 374 | @Symbol("maskPasswords") 375 | @Extension(ordinal = 100) // JENKINS-12161, was previously 1000 but that made the system configuration page look weird 376 | public static final class DescriptorImpl extends BuildWrapperDescriptor { 377 | 378 | public DescriptorImpl() { 379 | super(MaskPasswordsBuildWrapper.class); 380 | } 381 | 382 | /** 383 | * @since 2.5 384 | */ 385 | @Override 386 | public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException { 387 | try { 388 | getConfig().clear(); 389 | 390 | LOGGER.fine("Processing the maskedParamDefs and selectedMaskedParamDefs JSON objects"); 391 | JSONObject submittedForm = req.getSubmittedForm(); 392 | 393 | // parameter definitions to be automatically masked 394 | JSONArray paramDefinitions = submittedForm.getJSONArray("maskedParamDefs"); 395 | JSONArray selectedParamDefinitions = submittedForm.getJSONArray("selectedMaskedParamDefs"); 396 | for(int i = 0; i < selectedParamDefinitions.size(); i++) { 397 | if(selectedParamDefinitions.getBoolean(i)) { 398 | getConfig().addMaskedPasswordParameterDefinition(paramDefinitions.getString(i)); 399 | } 400 | } 401 | 402 | // global var/password pairs 403 | if(submittedForm.has("globalVarPasswordPairs")) { 404 | Object o = submittedForm.get("globalVarPasswordPairs"); 405 | 406 | if(o instanceof JSONArray) { 407 | JSONArray jsonArray = submittedForm.getJSONArray("globalVarPasswordPairs"); 408 | for(int i = 0; i < jsonArray.size(); i++) { 409 | getConfig().addGlobalVarPasswordPair(new VarPasswordPair( 410 | jsonArray.getJSONObject(i).getString("var"), 411 | Secret.fromString(jsonArray.getJSONObject(i).getString("password")))); 412 | } 413 | } 414 | else if(o instanceof JSONObject) { 415 | JSONObject jsonObject = submittedForm.getJSONObject("globalVarPasswordPairs"); 416 | getConfig().addGlobalVarPasswordPair(new VarPasswordPair( 417 | jsonObject.getString("var"), 418 | Secret.fromString(jsonObject.getString("password")))); 419 | } 420 | } 421 | 422 | // global regexes 423 | if(submittedForm.has("globalVarMaskRegexesU")) { 424 | Object o = submittedForm.get("globalVarMaskRegexesU"); 425 | 426 | if(o instanceof JSONArray) { 427 | JSONArray jsonArray = submittedForm.getJSONArray("globalVarMaskRegexesU"); 428 | for(int i = 0; i < jsonArray.size(); i++) { 429 | getConfig().addGlobalVarMaskRegex( 430 | jsonArray.getJSONObject(i).getString("key"), 431 | new VarMaskRegex(jsonArray.getJSONObject(i).getString("value"))); 432 | } 433 | } 434 | else if(o instanceof JSONObject) { 435 | JSONObject jsonObject = submittedForm.getJSONObject("globalVarMaskRegexesU"); 436 | getConfig().addGlobalVarMaskRegex( 437 | jsonObject.getString("key"), 438 | new VarMaskRegex(jsonObject.getString("value"))); 439 | } 440 | } 441 | 442 | // global enable 443 | if(submittedForm.has("globalVarMaskEnabledGlobally")) { 444 | boolean b = submittedForm.getBoolean("globalVarMaskEnabledGlobally"); 445 | if(b) { 446 | getConfig().setGlobalVarEnabledGlobally(true); 447 | } 448 | } 449 | 450 | MaskPasswordsConfig.save(getConfig()); 451 | 452 | return true; 453 | } 454 | catch (Exception e) { 455 | LOGGER.log(Level.SEVERE, "Failed to save Mask Passwords plugin configuration", e); 456 | return false; 457 | } 458 | } 459 | 460 | public List getGlobalVarPasswordPairs() { 461 | return getConfig().getGlobalVarPasswordPairs(); 462 | } 463 | 464 | public List getGlobalVarMaskRegexesU() { 465 | return getConfig().getGlobalVarMaskRegexesU(); 466 | } 467 | 468 | /** 469 | * @since 2.5 470 | */ 471 | public MaskPasswordsConfig getConfig() { 472 | return MaskPasswordsConfig.getInstance(); 473 | } 474 | 475 | @Override 476 | public String getDisplayName() { 477 | return new Localizable(ResourceBundleHolder.get(MaskPasswordsBuildWrapper.class), "DisplayName").toString(); 478 | } 479 | 480 | @Override 481 | public boolean isApplicable(AbstractProject item) { 482 | return true; 483 | } 484 | 485 | } 486 | 487 | /** 488 | * We need this converter to handle marshalling/unmarshalling of the build 489 | * wrapper data: Relying on the default mechanism doesn't make it (because 490 | * {@link Secret} doesn't have the {@code DataBoundConstructor} annotation). 491 | */ 492 | public static final class ConverterImpl implements Converter { 493 | 494 | private final static String VAR_PASSWORD_PAIRS_NODE = "varPasswordPairs"; 495 | private final static String VAR_PASSWORD_PAIR_NODE = "varPasswordPair"; 496 | private final static String VAR_MASK_REGEXES_NODE = "varMaskRegexes"; 497 | private final static String VAR_MASK_REGEX_NODE = "varMaskRegex"; 498 | private final static String VAR_ATT = "var"; 499 | private final static String PASSWORD_ATT = "password"; 500 | private final static String REGEX_ATT = "regex"; 501 | private final static String REGEX_NAME = "name"; 502 | 503 | public boolean canConvert(Class clazz) { 504 | return clazz.equals(MaskPasswordsBuildWrapper.class); 505 | } 506 | 507 | public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext mc) { 508 | MaskPasswordsBuildWrapper maskPasswordsBuildWrapper = (MaskPasswordsBuildWrapper) o; 509 | 510 | // varPasswordPairs 511 | if(maskPasswordsBuildWrapper.getVarPasswordPairs() != null) { 512 | writer.startNode(VAR_PASSWORD_PAIRS_NODE); 513 | for(VarPasswordPair varPasswordPair: maskPasswordsBuildWrapper.getVarPasswordPairs()) { 514 | // blank passwords are skipped 515 | if(varPasswordPair.getPlainTextPassword() == null) { 516 | continue; 517 | } 518 | writer.startNode(VAR_PASSWORD_PAIR_NODE); 519 | writer.addAttribute(VAR_ATT, varPasswordPair.getVar()); 520 | writer.addAttribute(PASSWORD_ATT, varPasswordPair.getPassword().getEncryptedValue()); 521 | writer.endNode(); 522 | } 523 | writer.endNode(); 524 | } 525 | // varMaskRegexes 526 | if(maskPasswordsBuildWrapper.getVarMaskRegexes() != null) { 527 | writer.startNode(VAR_MASK_REGEXES_NODE); 528 | for(VarMaskRegexEntry varMaskRegex: maskPasswordsBuildWrapper.getVarMaskRegexes()) { 529 | // blank passwords are skipped 530 | if(StringUtils.isBlank(varMaskRegex.getRegexString())) { 531 | continue; 532 | } 533 | writer.startNode(VAR_MASK_REGEX_NODE); 534 | writer.addAttribute(REGEX_NAME, varMaskRegex.getKey()); 535 | writer.addAttribute(REGEX_ATT, varMaskRegex.getRegexString()); 536 | writer.endNode(); 537 | } 538 | writer.endNode(); 539 | } 540 | } 541 | 542 | public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext uc) { 543 | List varPasswordPairs = new ArrayList<>(); 544 | List varMaskRegexes = new ArrayList<>(); 545 | 546 | while(reader.hasMoreChildren()) { 547 | reader.moveDown(); 548 | if(reader.getNodeName().equals(VAR_PASSWORD_PAIRS_NODE)) { 549 | while(reader.hasMoreChildren()) { 550 | reader.moveDown(); 551 | if(reader.getNodeName().equals(VAR_PASSWORD_PAIR_NODE)) { 552 | varPasswordPairs.add(new VarPasswordPair( 553 | reader.getAttribute(VAR_ATT), 554 | Secret.fromString(reader.getAttribute(PASSWORD_ATT)))); 555 | } 556 | else { 557 | LOGGER.log(Level.WARNING, 558 | "Encountered incorrect node name: Expected \"" + VAR_PASSWORD_PAIR_NODE + "\", got \"{0}\"", 559 | reader.getNodeName()); 560 | } 561 | reader.moveUp(); 562 | } 563 | reader.moveUp(); 564 | } 565 | else if(reader.getNodeName().equals(VAR_MASK_REGEXES_NODE)) { 566 | while(reader.hasMoreChildren()) { 567 | reader.moveDown(); 568 | if(reader.getNodeName().equals(VAR_MASK_REGEX_NODE)) { 569 | varMaskRegexes.add(new VarMaskRegexEntry( 570 | reader.getAttribute(REGEX_NAME), 571 | reader.getAttribute(REGEX_ATT))); 572 | } 573 | else { 574 | LOGGER.log(Level.WARNING, 575 | "Encountered incorrect node name: Expected \"" + VAR_MASK_REGEX_NODE + "\", got \"{0}\"", 576 | reader.getNodeName()); 577 | } 578 | reader.moveUp(); 579 | } 580 | reader.moveUp(); 581 | } 582 | else { 583 | LOGGER.log(Level.WARNING, 584 | "Encountered incorrect node name: \"{0}\"", reader.getNodeName()); 585 | } 586 | } 587 | 588 | return new MaskPasswordsBuildWrapper(varPasswordPairs, varMaskRegexes); 589 | } 590 | 591 | } 592 | 593 | private static final Logger LOGGER = Logger.getLogger(MaskPasswordsBuildWrapper.class.getName()); 594 | 595 | } 596 | -------------------------------------------------------------------------------- /src/main/java/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package com.michelin.cio.hudson.plugins.maskpasswords; 26 | 27 | import com.google.common.annotations.VisibleForTesting; 28 | import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper.VarMaskRegex; 29 | import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper.VarPasswordPair; 30 | import edu.umd.cs.findbugs.annotations.CheckForNull; 31 | import edu.umd.cs.findbugs.annotations.NonNull; 32 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 33 | import hudson.Extension; 34 | import hudson.ExtensionList; 35 | import hudson.XmlFile; 36 | import hudson.cli.CLICommand; 37 | import hudson.model.AbstractDescribableImpl; 38 | import hudson.model.Descriptor; 39 | import hudson.model.ParameterDefinition; 40 | import hudson.model.ParameterDefinition.ParameterDescriptor; 41 | import hudson.model.ParameterValue; 42 | import jenkins.model.Jenkins; 43 | import net.sf.json.JSONObject; 44 | import org.apache.commons.lang.StringUtils; 45 | import org.jenkinsci.plugins.structs.describable.CustomDescribableModel; 46 | import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable; 47 | import org.kohsuke.accmod.Restricted; 48 | import org.kohsuke.accmod.restrictions.NoExternalUse; 49 | import org.kohsuke.stapler.DataBoundConstructor; 50 | import org.kohsuke.stapler.StaplerRequest2; 51 | 52 | import net.jcip.annotations.GuardedBy; 53 | import java.io.File; 54 | import java.io.FileNotFoundException; 55 | import java.io.IOException; 56 | import java.lang.reflect.Method; 57 | import java.nio.file.NoSuchFileException; 58 | import java.util.ArrayList; 59 | import java.util.HashMap; 60 | import java.util.HashSet; 61 | import java.util.LinkedHashSet; 62 | import java.util.List; 63 | import java.util.Map; 64 | import java.util.Set; 65 | import java.util.logging.Level; 66 | import java.util.logging.Logger; 67 | 68 | /** 69 | * Singleton class to manage Mask Passwords global settings. 70 | * 71 | * @author Romain Seguy (http://openromain.blogspot.com) 72 | * @since 2.5 73 | */ 74 | public class MaskPasswordsConfig { 75 | 76 | private final static String CONFIG_FILE = "com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsConfig.xml"; 77 | private static Object CONFIG_FILE_LOCK = new Object(); 78 | @GuardedBy("CONFIG_FILE_LOCK") 79 | private static MaskPasswordsConfig config; 80 | 81 | /** 82 | * Contains the set of {@link ParameterDefinition}s whose value must be 83 | * masked in builds' console. 84 | */ 85 | @GuardedBy("this") 86 | private Set maskPasswordsParamDefClasses; 87 | /** 88 | * Contains the set of {@link ParameterValue}s whose value must be masked in 89 | * builds' console. 90 | */ 91 | @NonNull 92 | @GuardedBy("this") 93 | private transient Set paramValueCache_maskedClasses = new HashSet<>(); 94 | 95 | /** 96 | * Cache of values, which are not subjects for masking. 97 | */ 98 | @NonNull 99 | @GuardedBy("this") 100 | private transient Set paramValueCache_nonMaskedClasses = new HashSet<>(); 101 | 102 | /** 103 | * Users can define key/password pairs at the global level to share common 104 | * passwords with several jobs. 105 | * 106 | *

Never ever use this attribute directly: Use {@link #getGlobalVarPasswordPairsList} to avoid 107 | * potential NPEs.

108 | * 109 | * @since 2.7 110 | */ 111 | private List globalVarPasswordPairs; 112 | /** 113 | * Users can define regexes at the global level to mask in jobs. 114 | * 115 | *

Never ever use this attribute directly: Use {@link #getGlobalVarMaskRegexesMap} to avoid 116 | * potential NPEs.

117 | * 118 | * @since 2.9 119 | * 120 | * Deprecated in favor of globalVarMaskRegexesMap which has label names mapped to value's for better identification 121 | */ 122 | @Deprecated 123 | @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Retain API compatibility") 124 | public List globalVarMaskRegexes; 125 | @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Retain API compatibility") 126 | public List globalVarMaskRegexesU; 127 | @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Retain API compatibility") 128 | public HashMap globalVarMaskRegexesMap; 129 | /** 130 | * Whether or not to enable the plugin globally on ALL BUILDS. 131 | * 132 | * @since 2.9 133 | */ 134 | private boolean globalVarEnableGlobally; 135 | 136 | public MaskPasswordsConfig() { 137 | maskPasswordsParamDefClasses = new LinkedHashSet<>(); 138 | reset(); 139 | } 140 | 141 | @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "readResolve()") 142 | private Object readResolve() { 143 | // Reinit caches 144 | synchronized(this) { 145 | if (paramValueCache_maskedClasses == null) { 146 | paramValueCache_maskedClasses = new HashSet<>(); 147 | } 148 | if (paramValueCache_nonMaskedClasses == null) { 149 | paramValueCache_nonMaskedClasses = new HashSet<>(); 150 | } 151 | } 152 | 153 | return this; 154 | } 155 | 156 | private boolean isGlobalVarMaskRegexesNull() { 157 | return (this.globalVarMaskRegexesMap == null) || (this.globalVarMaskRegexesU == null); 158 | } 159 | 160 | /** 161 | * Adds a key/password pair at the global level. 162 | * 163 | *

If either key or password is blank (as defined per the Commons Lang 164 | * library), then the pair is not added.

165 | * 166 | * @since 2.7 167 | */ 168 | public void addGlobalVarPasswordPair(VarPasswordPair varPasswordPair) { 169 | // blank values are forbidden 170 | if(StringUtils.isBlank(varPasswordPair.getVar()) || varPasswordPair.getPlainTextPassword() == null) { 171 | LOGGER.fine("addGlobalVarPasswordPair NOT adding pair with null var or password"); 172 | return; 173 | } 174 | getGlobalVarPasswordPairsList().add(varPasswordPair); 175 | } 176 | 177 | /** 178 | * Adds a regex at the global level. 179 | * 180 | *

If regex is blank (as defined per the Commons Lang 181 | * library), then the pair is not added.

182 | * 183 | * @since 2.9 184 | */ 185 | public void addGlobalVarMaskRegex(VarMaskRegex varMaskRegex) { 186 | addGlobalVarMaskRegex("", varMaskRegex); 187 | } 188 | 189 | public void addGlobalVarMaskRegex(String name, String regex) { 190 | addGlobalVarMaskRegex(name, new VarMaskRegex(regex)); 191 | } 192 | 193 | public void addGlobalVarMaskRegex(String name, VarMaskRegex varMaskRegex) { 194 | // blank values are forbidden 195 | if(StringUtils.isBlank(varMaskRegex.getRegex())) { 196 | LOGGER.fine("addGlobalVarMaskRegex NOT adding null regex"); 197 | return; 198 | } 199 | // blank values are forbidden, will give default numbered key name 200 | if(StringUtils.isBlank(name)) { 201 | LOGGER.fine("Generating default numbered key for VarMaskRegex"); 202 | name = "VarMaskRegex" + getGlobalVarMaskRegexesMap().size(); 203 | } 204 | HashMap regexMap = getGlobalVarMaskRegexesMap(); 205 | regexMap.put(name, varMaskRegex); 206 | getGlobalVarMaskRegexesUList().clear(); 207 | for (Map.Entry entry: getGlobalVarMaskRegexesMap().entrySet()) { 208 | getGlobalVarMaskRegexesUList().add(new VarMaskRegexEntry(entry.getKey(), entry.getValue())); 209 | } 210 | 211 | saveSafeIO(this); 212 | } 213 | 214 | public void removeGlobalVarMaskRegexByName(@NonNull String name) { 215 | if (!isGlobalVarMaskRegexesNull()) { 216 | VarMaskRegex r = getGlobalVarMaskRegexesMap().get(name); 217 | if (r != null) { 218 | VarMaskRegexEntry e = new VarMaskRegexEntry(name, r); 219 | getGlobalVarMaskRegexesMap().remove(name); 220 | getGlobalVarMaskRegexesUList().remove(e); 221 | } 222 | } 223 | saveSafeIO(this); 224 | } 225 | 226 | public void removeGlobalVarMaskRegex(String name, String regex) { 227 | if (!isGlobalVarMaskRegexesNull()) { 228 | VarMaskRegexEntry e = new VarMaskRegexEntry(name, regex); 229 | if (getGlobalVarMaskRegexesUList().remove(e)) { 230 | HashMap map = getGlobalVarMaskRegexesMap(); 231 | VarMaskRegex r = map.get(name); 232 | if (r != null && r.getRegex() != null && regex != null && r.getRegex().equals(regex)) { 233 | map.remove(name); 234 | } 235 | } 236 | 237 | } 238 | saveSafeIO(this); 239 | } 240 | 241 | /** 242 | * @param className The class key of a {@link ParameterDescriptor} to be added 243 | * to the list of parameters which will prevent the rebuild 244 | * action to be enabled for a build 245 | */ 246 | public synchronized void addMaskedPasswordParameterDefinition(String className) { 247 | maskPasswordsParamDefClasses.add(className); 248 | // Maybe is it masked now 249 | paramValueCache_nonMaskedClasses.clear(); 250 | } 251 | 252 | public void setGlobalVarEnabledGlobally(boolean state) { 253 | globalVarEnableGlobally = state; 254 | } 255 | 256 | /** 257 | * Resets configuration to the default state. 258 | */ 259 | @Restricted(NoExternalUse.class) 260 | @VisibleForTesting 261 | public final synchronized void reset() { 262 | // Wipe the data 263 | clear(); 264 | 265 | // default values for the first time the config is created 266 | addMaskedPasswordParameterDefinition(hudson.model.PasswordParameterDefinition.class.getName()); 267 | addMaskedPasswordParameterDefinition(com.michelin.cio.hudson.plugins.passwordparam.PasswordParameterDefinition.class.getName()); 268 | } 269 | 270 | public synchronized void clear() { 271 | maskPasswordsParamDefClasses.clear(); 272 | getGlobalVarPasswordPairsList().clear(); 273 | getGlobalVarMaskRegexesList().clear(); 274 | getGlobalVarMaskRegexesUList().clear(); 275 | getGlobalVarMaskRegexesMap().clear(); 276 | globalVarEnableGlobally = false; 277 | 278 | // Drop caches 279 | invalidatePasswordValueClassCaches(); 280 | } 281 | 282 | public synchronized void clear(boolean doSave) { 283 | clear(); 284 | if (doSave) { 285 | saveSafeIO(this); 286 | } 287 | } 288 | 289 | /*package*/ synchronized void invalidatePasswordValueClassCaches() { 290 | paramValueCache_maskedClasses.clear(); 291 | paramValueCache_nonMaskedClasses.clear(); 292 | } 293 | 294 | public static MaskPasswordsConfig getInstance() { 295 | synchronized(CONFIG_FILE_LOCK) { 296 | if(config == null) { 297 | config = load(); 298 | } 299 | return config; 300 | } 301 | } 302 | 303 | private static XmlFile getConfigFile() { 304 | return new XmlFile(new File(Jenkins.get().getRootDir(), CONFIG_FILE)); 305 | } 306 | 307 | /** 308 | * Returns the list of key/password pairs defined at the global level. 309 | * 310 | *

Modifications broughts to the returned list has no impact on this 311 | * configuration (the returned value is a copy). Also, the list can be 312 | * empty but never {@code null}.

313 | * 314 | * @since 2.7 315 | */ 316 | public List getGlobalVarPasswordPairs() { 317 | List r = new ArrayList<>(getGlobalVarPasswordPairsList().size()); 318 | 319 | // deep copy 320 | for(VarPasswordPair varPasswordPair: getGlobalVarPasswordPairsList()) { 321 | r.add((VarPasswordPair) varPasswordPair.clone()); 322 | } 323 | return r; 324 | } 325 | 326 | /** 327 | * Returns the list of regexes defined at the global level. 328 | * 329 | *

Modifications broughts to the returned list has no impact on this 330 | * configuration (the returned value is a copy). Also, the list can be 331 | * empty but never {@code null}.

332 | * 333 | * @since 2.9 334 | */ 335 | public List getGlobalVarMaskRegexesU() { 336 | List r = new ArrayList<>(getGlobalVarMaskRegexesMap().size()); 337 | 338 | // deep copy 339 | for(Map.Entry entry: getGlobalVarMaskRegexesMap().entrySet()) { 340 | r.add(new VarMaskRegexEntry(entry.getKey(), (VarMaskRegex) entry.getValue().clone())); 341 | } 342 | 343 | return r; 344 | } 345 | 346 | 347 | /** 348 | * Fixes JENKINS-11514: When {@code MaskPasswordsConfig.xml} is there but was created from 349 | * version 2.6.1 (or older) of the plugin, {@link #globalVarPasswordPairs} can actually be 350 | * {@code null} ==> Always use this getter to avoid NPEs. 351 | * 352 | * @since 2.7.1 353 | */ 354 | private List getGlobalVarPasswordPairsList() { 355 | if(globalVarPasswordPairs == null) { 356 | globalVarPasswordPairs = new ArrayList<>(); 357 | } 358 | return globalVarPasswordPairs; 359 | } 360 | 361 | /** 362 | * Fixes JENKINS-11514: When {@code MaskPasswordsConfig.xml} is there but was created from 363 | * version 2.8 (or older) of the plugin, {@link #globalVarPasswordPairs} can actually be 364 | * {@code null} ==> Always use this getter to avoid NPEs. 365 | * 366 | * @since 2.9 367 | */ 368 | @Deprecated 369 | public List getGlobalVarMaskRegexesList() { 370 | if(globalVarMaskRegexes == null) { 371 | globalVarMaskRegexes = new ArrayList<>(); 372 | } 373 | return globalVarMaskRegexes; 374 | } 375 | 376 | public List getGlobalVarMaskRegexesUList() { 377 | if (this.globalVarMaskRegexesU == null) { 378 | globalVarMaskRegexesU = new ArrayList<>(); 379 | } 380 | return globalVarMaskRegexesU; 381 | } 382 | 383 | public HashMap getGlobalVarMaskRegexesMap() { 384 | if (globalVarMaskRegexesMap == null) { 385 | globalVarMaskRegexesMap = new HashMap<>(); 386 | /* upon initialization, create entries from globalVarMaskRegex (List) */ 387 | LOGGER.info("Initializing global var mask regexes map"); 388 | if (getGlobalVarMaskRegexesList().size() > 0) { 389 | for (int i = 0 ; i < getGlobalVarMaskRegexesList().size(); i++) { 390 | globalVarMaskRegexesMap.put("Regex_" + String.valueOf(i), getGlobalVarMaskRegexesList().get(i)); 391 | } 392 | getGlobalVarMaskRegexesList().clear(); 393 | try { 394 | save(this); 395 | } catch (IOException e) { 396 | LOGGER.info("IO Exception when trying to initialize global var mask value map from list:\n" + e.getMessage()); 397 | } 398 | } 399 | } 400 | return globalVarMaskRegexesMap; 401 | } 402 | 403 | 404 | /** 405 | * Returns a map of all {@link ParameterDefinition}s that can be used in 406 | * jobs. 407 | * 408 | *

The key is the class key of the {@link ParameterDefinition}, the value 409 | * is its display key.

410 | */ 411 | public static Map getParameterDefinitions() { 412 | Map params = new HashMap<>(); 413 | 414 | ExtensionList paramExtensions = 415 | Jenkins.get().getExtensionList(ParameterDefinition.ParameterDescriptor.class); 416 | for(ParameterDefinition.ParameterDescriptor paramExtension: paramExtensions) { 417 | // we need the getEnclosingClass() to drop the inner ParameterDescriptor 418 | // and work directly with the ParameterDefinition 419 | params.put(paramExtension.getClass().getEnclosingClass().getName(), paramExtension.getDisplayName()); 420 | } 421 | 422 | return params; 423 | } 424 | 425 | /** 426 | * Returns whether the plugin is enabled globally for ALL BUILDS. 427 | */ 428 | public boolean isEnabledGlobally() { 429 | return globalVarEnableGlobally; 430 | } 431 | 432 | /** 433 | * Check if the parameter value class needs to be masked 434 | * @deprecated There is a high risk of false-negatives. Use {@link #isMasked(hudson.model.ParameterValue, java.lang.String)} at least 435 | * @param paramValueClassName Class key of the {@link ParameterValue} 436 | * @return {@code true} if the parameter value should be masked. 437 | * {@code false} if the plugin is not sure, may be false-negative 438 | */ 439 | @Deprecated 440 | public synchronized boolean isMasked(final @NonNull String paramValueClassName) { 441 | return isMasked(null, paramValueClassName); 442 | } 443 | 444 | /** 445 | * Returns true if the specified parameter value class key corresponds to 446 | * a parameter definition class key selected in Jenkins' main 447 | * configuration screen. 448 | * @param value Parameter value. Without it there is a high risk of false negatives. 449 | * @param paramValueClassName Class key of the {@link ParameterValue} class implementation 450 | * @return {@code true} if the parameter value should be masked. 451 | * {@code false} if the plugin is not sure, may be false-negative especially if the value is {@code null}. 452 | * @since 2.10 453 | */ 454 | public boolean isMasked(final @CheckForNull ParameterValue value, 455 | final @NonNull String paramValueClassName) { 456 | 457 | // We always mask sensitive variables, the configuration does not matter in such case 458 | if (value != null && value.isSensitive()) { 459 | return true; 460 | } 461 | 462 | synchronized(this) { 463 | // Check if the value is in the cache 464 | if (paramValueCache_maskedClasses.contains(paramValueClassName)) { 465 | return true; 466 | } 467 | if (paramValueCache_nonMaskedClasses.contains(paramValueClassName)) { 468 | return false; 469 | } 470 | 471 | // Now guess 472 | boolean guessSo = guessIfShouldMask(paramValueClassName); 473 | if (guessSo) { 474 | // We are pretty sure it requires masking 475 | paramValueCache_maskedClasses.add(paramValueClassName); 476 | return true; 477 | } else { 478 | // It does not require masking, but we are not so sure 479 | // The warning will be printed each time the cache is invalidated due to whatever reason 480 | LOGGER.log(Level.WARNING, "Identified the {0} class as a ParameterValue class, which does not require masking. It may be false-negative", paramValueClassName); 481 | paramValueCache_nonMaskedClasses.add(paramValueClassName); 482 | return false; 483 | } 484 | } 485 | } 486 | 487 | //TODO: add support of specifying masked parameter values byt the... parameter value classs key. So obvious, yeah? 488 | /** 489 | * Tries to guess if the parameter value class should be masked. 490 | * @param paramValueClassName Parameter value class key 491 | * @return {@code true} if we are sure that the class has to be masked 492 | * {@code false} otherwise, there is a risk of false negative due to the presumptions. 493 | */ 494 | /*package*/ synchronized boolean guessIfShouldMask(final @NonNull String paramValueClassName) { 495 | // The only way to find parameter definition/parameter value 496 | // couples is to reflect the methods of parameter definition 497 | // classes which instantiate the parameter value. 498 | // This means that this algorithm expects that the developers do 499 | // clearly redefine the return type when implementing parameter 500 | // definitions/values. 501 | for(String paramDefClassName: maskPasswordsParamDefClasses) { 502 | final Class paramDefClass; 503 | try { 504 | paramDefClass = Jenkins.get().getPluginManager().uberClassLoader.loadClass(paramDefClassName); 505 | } catch (ClassNotFoundException ex) { 506 | LOGGER.log(Level.WARNING, "Cannot check ParamDef for masking " + paramDefClassName, ex); 507 | continue; 508 | } 509 | 510 | tryProcessMethod(paramDefClass, "getDefaultParameterValue", true); 511 | tryProcessMethod(paramDefClass, "createValue", true, StaplerRequest2.class, JSONObject.class); 512 | tryProcessMethod(paramDefClass, "createValue", true, StaplerRequest2.class); 513 | tryProcessMethod(paramDefClass, "createValue", true, CLICommand.class, String.class); 514 | // This custom implementation is not a part of the API, but let's try it 515 | tryProcessMethod(paramDefClass, "createValue", false, String.class); 516 | 517 | // If the parameter value class has been added to the cache, exit 518 | if (paramValueCache_maskedClasses.contains(paramValueClassName)) { 519 | return true; 520 | } 521 | } 522 | 523 | // Always mask the hudson.model.PasswordParameterValue class and its overrides 524 | // This class does not comply with the criteria above, but it is sensitive starting from 1.378 525 | final Class valueClass; 526 | try { 527 | valueClass = Jenkins.get().getPluginManager().uberClassLoader.loadClass(paramValueClassName); 528 | } catch (Exception ex) { 529 | // Move on. Whatever happens here, it will blow up somewhere else 530 | LOGGER.log(Level.FINE, "Failed to load class for the ParameterValue " + paramValueClassName, ex); 531 | return false; 532 | } 533 | 534 | return hudson.model.PasswordParameterValue.class.isAssignableFrom(valueClass); 535 | } 536 | 537 | /** 538 | * Processes the methods in the {@link ParameterValue} class and caches all ParameterValue implementations as ones requiring masking. 539 | * @param clazz Class 540 | * @param methodName Method key 541 | * @param parameterTypes Parameters 542 | */ 543 | private synchronized void tryProcessMethod(Class clazz, String methodName, boolean expectedToExist, Class ... parameterTypes) { 544 | 545 | final Method method; 546 | try { 547 | method = clazz.getMethod(methodName, parameterTypes); 548 | } catch (NoSuchMethodException ex) { 549 | Level logLevel = expectedToExist ? Level.INFO : Level.CONFIG; 550 | if (LOGGER.isLoggable(logLevel)) { 551 | String methodSpec = String.format("%s(%s)", methodName, StringUtils.join(parameterTypes, ",")); 552 | LOGGER.log(logLevel, "No method {0} for class {1}", new Object[] {methodSpec, clazz}); 553 | } 554 | return; 555 | } catch (RuntimeException ex) { 556 | Level logLevel = expectedToExist ? Level.INFO : Level.CONFIG; 557 | if (LOGGER.isLoggable(logLevel)) { 558 | String methodSpec = String.format("%s(%s)", methodName, StringUtils.join(parameterTypes, ",")); 559 | LOGGER.log(logLevel, "Failed to retrieve the method {0} for class {1}", new Object[] {methodSpec, clazz}); 560 | } 561 | return; 562 | } 563 | 564 | Class returnType = method.getReturnType(); 565 | // We do not veto the the root class 566 | if (ParameterValue.class.isAssignableFrom(returnType)) { 567 | if (!ParameterValue.class.equals(returnType)) { 568 | // Add this class to the cache 569 | paramValueCache_maskedClasses.add(returnType.getName()); 570 | } 571 | } 572 | } 573 | 574 | /** 575 | * Returns true if the specified parameter definition class key has been 576 | * selected in Jenkins main configuration screen. 577 | */ 578 | public synchronized boolean isSelected(String paramDefClassName) { 579 | return maskPasswordsParamDefClasses.contains(paramDefClassName); 580 | } 581 | 582 | public static MaskPasswordsConfig load() { 583 | LOGGER.entering(CLASS_NAME, "load"); 584 | try { 585 | MaskPasswordsConfig file = (MaskPasswordsConfig) getConfigFile().read(); 586 | return (MaskPasswordsConfig) getConfigFile().read(); 587 | } 588 | catch(FileNotFoundException | NoSuchFileException e) { 589 | LOGGER.log(Level.WARNING, "No configuration found for Mask Passwords plugin"); 590 | } 591 | catch(Exception e) { 592 | LOGGER.log(Level.WARNING, "Unable to load Mask Passwords plugin configuration from " + CONFIG_FILE, e); 593 | } 594 | LOGGER.log(Level.FINE, "No Mask Passwords config file loaded; using defaults"); 595 | return new MaskPasswordsConfig(); 596 | } 597 | 598 | public static void save(MaskPasswordsConfig config) throws IOException { 599 | LOGGER.entering(CLASS_NAME, "save"); 600 | getConfigFile().write(config); 601 | LOGGER.exiting(CLASS_NAME, "save"); 602 | } 603 | 604 | static void saveSafeIO(MaskPasswordsConfig config) { 605 | try { 606 | save(config); 607 | } catch(IOException e) { 608 | LOGGER.warning("Failed to save MaskPasswordsConfig due to IOException: " + e.getMessage()); 609 | } 610 | } 611 | 612 | public String toString() { 613 | StringBuilder sb = new StringBuilder("MaskPasswordsConfigFile Regexes:[\n"); 614 | for (Map.Entry entry : this.getGlobalVarMaskRegexesMap().entrySet()) { 615 | sb.append(entry.getKey() + " : " + entry.getValue().getRegex() + "\n"); 616 | } 617 | sb.append("]"); 618 | return sb.toString(); 619 | } 620 | 621 | private final static String CLASS_NAME = MaskPasswordsConfig.class.getName(); 622 | private final static Logger LOGGER = Logger.getLogger(CLASS_NAME); 623 | 624 | public static class VarMaskRegexEntry extends AbstractDescribableImpl implements Cloneable{ 625 | private String key; 626 | private VarMaskRegex value; 627 | 628 | @DataBoundConstructor 629 | public VarMaskRegexEntry(String key, String value) { 630 | this.key = key; 631 | this.value = new VarMaskRegex(value); 632 | } 633 | 634 | public VarMaskRegexEntry(String key, VarMaskRegex value) { 635 | this.key = key; 636 | this.value = value; 637 | } 638 | 639 | public VarMaskRegexEntry(VarMaskRegex value) { 640 | key = ""; 641 | this.value = value; 642 | } 643 | 644 | public String getName() { 645 | return key; 646 | } 647 | 648 | public void setName(String name) { 649 | this.key = name; 650 | } 651 | 652 | public VarMaskRegex getRegex() { 653 | return value; 654 | } 655 | 656 | public void setRegex(VarMaskRegex regex) { 657 | this.value = regex; 658 | } 659 | 660 | public String getKey() { 661 | return this.key; 662 | } 663 | 664 | public void setKey(String key) { 665 | this.key = key; 666 | } 667 | 668 | public String getValue() { 669 | return this.getRegex().getRegex(); 670 | } 671 | public void setValue(String regex) { 672 | this.setRegex(new VarMaskRegex(regex)); 673 | } 674 | 675 | public String getRegexString() { 676 | if (this.value == null) { 677 | return ""; 678 | } 679 | return this.value.getRegex(); 680 | } 681 | 682 | public String toString() { 683 | return this.key + ":" + this.value; 684 | } 685 | 686 | @Override 687 | @SuppressFBWarnings(value = "CN_IDIOM_NO_SUPER_CALL", justification = "We do not expect anybody to use this class." 688 | + "If they do, they must override clone() as well") 689 | public Object clone() { 690 | return new VarMaskRegexEntry(this.getKey(), this.getRegex()); 691 | } 692 | 693 | @Override 694 | public int hashCode() { 695 | int hash = 3; 696 | hash = 67 * hash + (this.key != null ? this.key.hashCode() : 0); 697 | return hash; 698 | } 699 | 700 | @Override 701 | public boolean equals(Object other) { 702 | if (other == null) { 703 | return false; 704 | } else if (!this.getClass().equals(other.getClass())) { 705 | return false; 706 | } else { 707 | VarMaskRegexEntry otherE = (VarMaskRegexEntry) other; 708 | return (this.getKey().equals(otherE.getKey())) && this.getRegex().equals(otherE.getRegex()); 709 | } 710 | } 711 | 712 | @Extension 713 | public static class DescriptorImpl extends Descriptor implements CustomDescribableModel { 714 | public String getDisplayName() { 715 | return VarMaskRegexEntry.class.getName(); 716 | } 717 | 718 | @NonNull 719 | @Override 720 | public UninstantiatedDescribable customUninstantiate(@NonNull UninstantiatedDescribable step) { 721 | Map arguments = step.getArguments(); 722 | Map newMap1 = new HashMap<>(); 723 | newMap1.put("key", arguments.get("key")); 724 | newMap1.put("value", arguments.get("value")); 725 | return step.withArguments(newMap1); 726 | } 727 | 728 | @NonNull 729 | @Override 730 | public Map customInstantiate(@NonNull Map arguments) { 731 | Map newMap = new HashMap<>(); 732 | newMap.put("key", arguments.get("key")); 733 | newMap.put("value", String.valueOf(arguments.get("value"))); 734 | return newMap; 735 | } 736 | } 737 | } 738 | 739 | } 740 | -------------------------------------------------------------------------------- /src/main/java/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsConsoleLogFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2016 Cox Automotive, Inc./Manheim, Jason Antman. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package com.michelin.cio.hudson.plugins.maskpasswords; 26 | 27 | import hudson.Extension; 28 | import hudson.console.ConsoleLogFilter; 29 | import hudson.model.Run; 30 | 31 | import java.io.IOException; 32 | import java.io.OutputStream; 33 | import java.io.Serializable; 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | import java.util.logging.Level; 37 | import java.util.logging.Logger; 38 | 39 | /** 40 | * GLOBAL Console Log Filter that alters the console so that passwords don't 41 | * get displayed. 42 | * 43 | * @author Jason Antman jason@jasonantman.com 44 | */ 45 | @Extension 46 | public class MaskPasswordsConsoleLogFilter extends ConsoleLogFilter implements Serializable { 47 | 48 | private static final long serialVersionUID = 1L; 49 | 50 | public MaskPasswordsConsoleLogFilter() { 51 | // nothing to do here; this object lives for the lifetime of Jenkins, 52 | // so if we don't want to have to restart to detect config changes, 53 | // we need to get the config in each run. 54 | } 55 | 56 | @SuppressWarnings("rawtypes") 57 | @Override 58 | public OutputStream decorateLogger(Run _ignore, OutputStream logger) throws IOException, InterruptedException { 59 | // check the config 60 | MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); 61 | if(! config.isEnabledGlobally()) { 62 | LOGGER.log(Level.FINE, "MaskPasswords not enabled globally; not decorating logger"); 63 | return logger; 64 | } 65 | LOGGER.log(Level.FINE, "MaskPasswords IS enabled globally; decorating logger"); 66 | 67 | // build our config 68 | List passwords = new ArrayList<>(); 69 | List regexes = new ArrayList<>(); 70 | 71 | // global passwords 72 | List globalVarPasswordPairs = config.getGlobalVarPasswordPairs(); 73 | for(MaskPasswordsBuildWrapper.VarPasswordPair globalVarPasswordPair: globalVarPasswordPairs) { 74 | passwords.add(globalVarPasswordPair.getPlainTextPassword()); 75 | } 76 | 77 | // global regexes 78 | List globalVarMaskRegexes = config.getGlobalVarMaskRegexesU(); 79 | for(MaskPasswordsConfig.VarMaskRegexEntry globalVarMaskRegex: globalVarMaskRegexes) { 80 | regexes.add(globalVarMaskRegex.getValue()); 81 | } 82 | return new MaskPasswordsOutputStream(logger, passwords, regexes); 83 | } 84 | 85 | private static final Logger LOGGER = Logger.getLogger(MaskPasswordsConsoleLogFilter.class.getName()); 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010-2011, Manufacture Francaise des Pneumatiques Michelin, 5 | * Romain Seguy 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | package com.michelin.cio.hudson.plugins.maskpasswords; 27 | 28 | import edu.umd.cs.findbugs.annotations.CheckForNull; 29 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 30 | import hudson.console.LineTransformationOutputStream; 31 | import org.apache.commons.lang.StringUtils; 32 | 33 | import java.io.IOException; 34 | import java.io.OutputStream; 35 | import java.io.UnsupportedEncodingException; 36 | import java.net.URLEncoder; 37 | import java.util.ArrayList; 38 | import java.util.Collection; 39 | import java.util.List; 40 | import java.util.regex.Pattern; 41 | 42 | import static com.michelin.cio.hudson.plugins.util.MaskPasswordsUtil.secretsMaskPatterns; 43 | 44 | //TODO: UTF-8 hardcoding is not a perfect solution 45 | /** 46 | * Custom output stream which masks a predefined set of passwords. 47 | * 48 | * @author Romain Seguy (http://openromain.blogspot.com) 49 | */ 50 | public class MaskPasswordsOutputStream extends LineTransformationOutputStream { 51 | 52 | private final OutputStream logger; 53 | private final List passwordsAsPatterns; 54 | private final String runName; 55 | 56 | /** 57 | * @param logger The output stream to which this {@link MaskPasswordsOutputStream} 58 | * will write to 59 | * @param passwords A collection of {@link String}s to be masked 60 | * @param regexes A collection of Regular Expression {@link String}s to be masked 61 | * @param runName A string representation of the Run/Build the output stream logger is associated with. Used for logging purposes. 62 | */ 63 | public MaskPasswordsOutputStream(OutputStream logger, @CheckForNull Collection passwords, @CheckForNull Collection regexes, String runName) { 64 | this.logger = logger; 65 | this.runName = (runName != null) ? runName : ""; 66 | passwordsAsPatterns = new ArrayList<>(); 67 | 68 | if (passwords != null) { 69 | // Passwords aggregated into single regex which is compiled as a pattern for efficiency 70 | StringBuilder pwRegex = new StringBuilder().append('('); 71 | int pwCount = 0; 72 | for (String pw : passwords) { 73 | if (StringUtils.isNotEmpty(pw)) { 74 | pwCount++; 75 | pwRegex.append(Pattern.quote(pw)); 76 | pwRegex.append('|'); 77 | try { 78 | String encodedPassword = URLEncoder.encode(pw, "UTF-8"); 79 | if (!encodedPassword.equals(pw)) { 80 | pwRegex.append(Pattern.quote(encodedPassword)); 81 | pwRegex.append('|'); 82 | } 83 | } catch (UnsupportedEncodingException e) { 84 | // ignore any encoding problem => status quo 85 | } 86 | } 87 | } 88 | if (pwCount > 0) { 89 | pwRegex.deleteCharAt(pwRegex.length()-1); // removes the last unuseful pipe 90 | pwRegex.append(')'); 91 | passwordsAsPatterns.add(Pattern.compile(pwRegex.toString())); 92 | } 93 | } 94 | if (regexes != null) { 95 | for (String r: regexes) { 96 | passwordsAsPatterns.add(Pattern.compile(r)); 97 | } 98 | } 99 | 100 | } 101 | 102 | /** 103 | * @param logger The output stream to which this {@link MaskPasswordsOutputStream} 104 | * will write to 105 | * @param passwords A collection of {@link String}s to be masked 106 | */ 107 | public MaskPasswordsOutputStream(OutputStream logger, @CheckForNull Collection passwords) { 108 | this(logger, passwords, null); 109 | } 110 | 111 | public MaskPasswordsOutputStream(OutputStream logger, @CheckForNull Collection passwords, @CheckForNull Collection regexes) { 112 | this(logger, passwords, regexes, ""); 113 | } 114 | 115 | // TODO: The logic relies on the default encoding, which may cause issues when master and agent have different encodings 116 | @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "Open TODO item for wider rework") 117 | @Override 118 | protected void eol(byte[] bytes, int len) throws IOException { 119 | String line = new String(bytes, 0, len); 120 | if(passwordsAsPatterns != null && line != null) { 121 | line = secretsMaskPatterns(passwordsAsPatterns, line, runName); 122 | } 123 | logger.write(line.getBytes()); 124 | } 125 | 126 | /** 127 | * {@inheritDoc} 128 | * @throws IOException on error 129 | */ 130 | @Override 131 | public void close() throws IOException { 132 | super.close(); 133 | logger.close(); 134 | } 135 | 136 | /** 137 | * {@inheritDoc} 138 | * @throws IOException on error 139 | */ 140 | @Override 141 | public void flush() throws IOException { 142 | super.flush(); 143 | logger.flush(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/michelin/cio/hudson/plugins/passwordparam/PasswordParameterDefinition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2004-2009, Sun Microsystems, Inc. 5 | * Copyright (c) 2011, Manufacture Française des Pneumatiques Michelin, Romain Seguy 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | package com.michelin.cio.hudson.plugins.passwordparam; 26 | 27 | import hudson.model.ParameterValue; 28 | import net.sf.json.JSONObject; 29 | import org.kohsuke.stapler.StaplerRequest2; 30 | import org.kohsuke.stapler.DataBoundConstructor; 31 | import hudson.Extension; 32 | import hudson.model.ParameterDefinition; 33 | import hudson.util.Secret; 34 | import org.jvnet.localizer.ResourceBundleHolder; 35 | 36 | public class PasswordParameterDefinition extends ParameterDefinition { 37 | 38 | @DataBoundConstructor 39 | public PasswordParameterDefinition(String name, String description) { 40 | super(name, description); 41 | } 42 | 43 | public ParameterValue createValue(Secret password) { 44 | PasswordParameterValue value = new PasswordParameterValue(getName(), password, getDescription()); 45 | value.setDescription(getDescription()); 46 | return value; 47 | } 48 | 49 | @Override 50 | public ParameterValue createValue(StaplerRequest2 req) { 51 | String[] value = req.getParameterValues(getName()); 52 | if(value == null) { 53 | return getDefaultParameterValue(); 54 | } 55 | else if (value.length != 1) { 56 | throw new IllegalArgumentException("Illegal number of parameter values for " + getName() + ": " + value.length); 57 | } 58 | else { 59 | return createValue(Secret.fromString(value[0])); 60 | } 61 | } 62 | 63 | @Override 64 | public PasswordParameterValue createValue(StaplerRequest2 req, JSONObject formData) { 65 | PasswordParameterValue value = req.bindJSON(PasswordParameterValue.class, formData); 66 | value.setDescription(getDescription()); 67 | return value; 68 | } 69 | 70 | @Extension 71 | public final static class ParameterDescriptorImpl extends ParameterDescriptor { 72 | 73 | @Override 74 | public String getDisplayName() { 75 | return ResourceBundleHolder.get(PasswordParameterDefinition.class).format("DisplayName"); 76 | } 77 | 78 | @Override 79 | public String getHelpFile() { 80 | return "/help/parameter/string.html"; 81 | } 82 | 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/michelin/cio/hudson/plugins/passwordparam/PasswordParameterValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2004-2009, Sun Microsystems, Inc. 5 | * Copyright (c) 2011-2012, Manufacture Francaise des Pneumatiques Michelin, 6 | * Romain Seguy 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | package com.michelin.cio.hudson.plugins.passwordparam; 27 | 28 | import edu.umd.cs.findbugs.annotations.CheckForNull; 29 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 30 | import hudson.EnvVars; 31 | import hudson.model.AbstractBuild; 32 | import hudson.model.ParameterValue; 33 | import hudson.model.Run; 34 | import hudson.util.Secret; 35 | import hudson.util.VariableResolver; 36 | import org.kohsuke.stapler.DataBoundConstructor; 37 | 38 | @SuppressFBWarnings(value = "SE_NO_SERIALVERSIONID", justification = "XStream does not need Serial version ID") 39 | public class PasswordParameterValue extends ParameterValue { 40 | 41 | //TODO: Can it even work with Pipeline? And it should be fine to write secrets 42 | @CheckForNull 43 | @SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "The secret must not be stored, so the att has to become transient") 44 | private final transient Secret value; 45 | 46 | public PasswordParameterValue(String name, Secret value, String description) { 47 | super(name, description); 48 | this.value = value; 49 | } 50 | 51 | @DataBoundConstructor 52 | public PasswordParameterValue(String name, String value, String description) { 53 | super(name, description); 54 | this.value = Secret.fromString(value); 55 | } 56 | 57 | @Override 58 | public void buildEnvironment(Run build, EnvVars env) { 59 | env.put(name.toUpperCase(), value != null ? Secret.toString(value) : null); 60 | } 61 | 62 | @Override 63 | public VariableResolver createVariableResolver(AbstractBuild build) { 64 | return new VariableResolver() { 65 | public String resolve(String name) { 66 | return PasswordParameterValue.this.name.equals(name) ? (value != null ? Secret.toString(value) : null) : null; 67 | } 68 | }; 69 | } 70 | 71 | @Override 72 | public final boolean isSensitive() { 73 | return true; 74 | } 75 | 76 | public String getValue() { 77 | return value != null ? Secret.toString(value) : null; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/michelin/cio/hudson/plugins/util/MaskPasswordsUtil.java: -------------------------------------------------------------------------------- 1 | package com.michelin.cio.hudson.plugins.util; 2 | 3 | import edu.umd.cs.findbugs.annotations.CheckForNull; 4 | import org.apache.commons.lang.StringUtils; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.logging.Logger; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | public class MaskPasswordsUtil { 15 | private static final Logger LOGGER = Logger.getLogger(MaskPasswordsUtil.class.getName()); 16 | public final static String MASKED_STRING = "********"; 17 | 18 | public static List patternMatch(List ps, String s) { 19 | List ret = new ArrayList<>(); 20 | for (Pattern p: ps) { 21 | Matcher m = p.matcher(s); 22 | while (m.find()) { // Regex matches 23 | if (m.groupCount() > 0) { // Regex contains group(s) 24 | for (int i = 1; i <= m.groupCount(); i++) { 25 | String toAdd = m.group(i); 26 | if (toAdd != null) { 27 | ret.add(toAdd); 28 | } 29 | } 30 | } else { // Regex doesn't contain groups, match entire Regex string 31 | ret.add(m.group(0)); 32 | } 33 | } 34 | } 35 | return ret; 36 | } 37 | 38 | public static List patternMatch(Pattern p, String s) { 39 | return patternMatch(Collections.singletonList(p), s); 40 | } 41 | 42 | public static String secretsMask(List secrets, String s, String runName) { 43 | if (secrets != null && secrets.size() > 0) { 44 | for (String secret: secrets) { 45 | s = s.replaceAll(Pattern.quote(secret), MASKED_STRING); 46 | } 47 | LOGGER.info(String.format("Masking Run[%s]'s line: %s", runName, StringUtils.strip(s))); 48 | } 49 | return s; 50 | } 51 | 52 | public static String secretsMaskPattern(Pattern p, String s) { 53 | return StringUtils.isNotBlank(s) ? secretsMask(patternMatch(p, s), s, "") : s; 54 | } 55 | 56 | public static String secretsMaskPatterns(List ps, String s, String runName) { 57 | return StringUtils.isNotBlank(s) ? secretsMask(patternMatch(ps, s), s, runName) : s; 58 | } 59 | 60 | public static List passwordRegexCombiner(@CheckForNull Collection passwords, @CheckForNull Collection regexes) { 61 | List passwordsAsPatterns = new ArrayList<>(); 62 | 63 | if (passwords != null) { 64 | for (String pw : passwords) { 65 | passwordsAsPatterns.add(Pattern.compile(pw)); 66 | } 67 | } 68 | if (regexes != null) { 69 | for (String r: regexes) { 70 | passwordsAsPatterns.add(Pattern.compile(r)); 71 | } 72 | } 73 | 74 | return passwordsAsPatterns; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper.properties: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | # 3 | # Copyright (c) 2010-2011, Manufacture Francaise des Pneumatiques Michelin, 4 | # Romain Seguy 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | DisplayName=Mask passwords and regexes (and enable global passwords) 25 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/config.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 |
28 | ${%HowToConfigureThisPlugin} 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/config.properties: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | # 3 | # Copyright (c) 2011, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | HowToConfigureThisPlugin=Password Parameters, or any other \ 24 | type of build parameters or regexes selected for masking in Jenkins'' main \ 25 | configuration screen (Manage Jenkins > Configure \ 26 | System), will be automatically masked. 27 | PasswordPairTitle=Name/Password Pairs 28 | RegexTitle=Regular Expressions 29 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/global.jelly: -------------------------------------------------------------------------------- 1 | 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 | 50 | 51 | 52 | 53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 |
64 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/global.properties: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | # 3 | # Copyright (c) 2011, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | GlobalVarPasswordPairs=Mask Passwords - Global name/password pairs 24 | ParameterDefinitions=Mask Passwords - Parameters to automatically mask 25 | GlobalVarMaskRegexes=Mask Passwords - Global Regexes 26 | Regex=Regex to automatically mask 27 | EnabledGlobally=Mask Passwords - Enable Globally 28 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/help-globalVarMaskEnabledGlobally.html: -------------------------------------------------------------------------------- 1 |
2 | If checked, the a MaskPasswordsConsoleLogFilter will be injected into EVERY Build, regardless of the Project's settings. 3 | This is generally bad practice, but if your foremost concern is having secrets hidden, it may be worth trying. Note that 4 | this should be tested with your jobs before being relied on; it's possible that custom Job types will not honor this. 5 | Pipeline jobs are not currently supported, vote for JENKINS-30777) 6 | if you really need this functionality. 7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/help-globalVarMaskRegexes.html: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |

Define a list of regular expression patterns 27 | to be masked in build output. If the Mask passwords option is enabled on these 28 | jobs or the Mask Passwords - Enable Globally global option is enabled, 29 | then the defined regexes will be automatically masked from the console.

30 |

Blank values are not accepted. Patterns will be compiled via 31 | java.util.regex.Pattern. 32 | Regexes and patterns for defined passwords will be combined with pipes ("|") 33 | within one large parenthesized pattern to mask from output (i.e. the final pattern to mask 34 | for two passwords ("Password1" and "Password2") and two regexes ("Regex1" and "Regex2") 35 | will be: "(Password1|Password2|Regex1|Regex2)"). 36 |

37 |
38 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/help-globalVarPasswordPairs.html: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |

Define a list of Name/Password pairs to be used from jobs 27 | as build variables. If the Mask passwords option is enabled on these 28 | jobs or the Mask Passwords - Enable Globally global option is enabled, 29 | then the defined passwords will be automatically masked from the console.

30 |

Blank values are not accepted (for both key and password).

31 |
32 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsBuildWrapper/help.html: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 |

When enabled, allows masking passwords that may appear in the console.

28 |

Passwords or regexes to be masked can be defined at three levels.

29 |

First, it is possible to select in Jenkins configuration screen 30 | the build parameters whose value must be masked. For example, selecting the 31 | Password Parameter type is a good idea.

32 |

Second, still in Jenkins' configuration screen, it is possible 33 | to define passwords to be masked as global Name/Password 34 | pairs, or regexes to be masked.

35 |

Third, on a per job basis (that is, in the current configuration screen), 36 | passwords to be masked can be defined as local Name/Password 37 | pairs, or regexes can be masked:

    38 |
  • Password is aimed at containing a password to be masked from the 39 | console. Empty and blank values are not allowed (e.g. " ", 40 | etc.). This field is ciphered. This field can not contain variables, they 41 | won't be expanded.
  • 42 |
  • If Name is set, then a build variable will be defined accordingly. 43 | This allows accessing the password by using the variable rather than hard-coding 44 | it in a field which is not ciphered (e.g. the ones of the Invoke Ant or 45 | Execute shell build steps).
  • 46 |
  • If Regex is set, then any text matching that regex will be masked 47 | in the console output. The regex should not contain start- or end-of-string 48 | markers and will be internally and-ed (piped) together with all other regexes 49 | and regexes matching the specified names and passwords.
  • 50 |
51 |
52 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/passwordparam/PasswordParameterDefinition.properties: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | # 3 | # Copyright (c) 2011, Manufacture Française des Pneumatiques Michelin, Romain Seguy 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | DisplayName=Non-Stored Password Parameter 24 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/passwordparam/PasswordParameterDefinition/config.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/passwordparam/PasswordParameterDefinition/index.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 29 | 30 | 31 |
32 | 33 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /src/main/resources/com/michelin/cio/hudson/plugins/passwordparam/PasswordParameterValue/value.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | This plugin allows masking passwords that may appear in the console. 28 | The plugin also provides a non-stored password parameter. 29 |
30 | -------------------------------------------------------------------------------- /src/test/java/com/michelin/cio/hudson/plugins/integrations/CorePasswordParameterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017 Jenkins contributors. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.michelin.cio.hudson.plugins.integrations; 25 | 26 | import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper; 27 | import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsConfig; 28 | import hudson.Launcher; 29 | import hudson.model.AbstractBuild; 30 | import hudson.model.BuildListener; 31 | import hudson.model.FreeStyleBuild; 32 | import hudson.model.FreeStyleProject; 33 | import hudson.model.ParameterValue; 34 | import hudson.model.ParametersDefinitionProperty; 35 | import hudson.model.PasswordParameterDefinition; 36 | import hudson.model.PasswordParameterValue; 37 | import hudson.model.Result; 38 | import org.junit.jupiter.api.BeforeEach; 39 | import org.junit.jupiter.api.Test; 40 | import org.jvnet.hudson.test.Issue; 41 | import org.jvnet.hudson.test.JenkinsRule; 42 | import org.jvnet.hudson.test.TestBuilder; 43 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 44 | 45 | import java.io.Serial; 46 | import java.util.Collections; 47 | 48 | import static org.junit.jupiter.api.Assertions.assertTrue; 49 | 50 | /** 51 | * Tests of {@link PasswordParameterValue} and {@link PasswordParameterDefinition}. 52 | * 53 | * @author bpmarinho 54 | */ 55 | @WithJenkins 56 | class CorePasswordParameterTest { 57 | 58 | private JenkinsRule j; 59 | 60 | @BeforeEach 61 | void dropCache(JenkinsRule j) { 62 | this.j = j; 63 | MaskPasswordsConfig.getInstance().reset(); 64 | } 65 | 66 | @Test 67 | void shouldMaskPasswordParameterClassByDefault() { 68 | assertTrue(MaskPasswordsConfig.getInstance().isMasked(PasswordParameterValue.class.getName()), 69 | PasswordParameterValue.class + " must be masked by default"); 70 | } 71 | 72 | @Test 73 | void shouldMaskPasswordParameterValueByDefault() { 74 | PasswordParameterDefinition d = new PasswordParameterDefinition("FOO", "myPassword", "BAR"); 75 | ParameterValue created = d.createValue("hello"); 76 | 77 | // We pass the non-existent class name in order to ensure that the Value metadata check is enough 78 | assertTrue(MaskPasswordsConfig.getInstance().isMasked(created, "nonExistent"), 79 | PasswordParameterValue.class + " must be masked by default"); 80 | } 81 | 82 | @Test 83 | void shouldMaskPasswordParameterChildrenValueByValue() { 84 | ParameterValue created = new MyPasswordParameter(); 85 | 86 | // We pass the non-existent class name in order to ensure that the Value metadata check is enough 87 | assertTrue(MaskPasswordsConfig.getInstance().isMasked(created, "nonExistent"), 88 | PasswordParameterValue.class + " must be masked by default"); 89 | } 90 | 91 | @Test 92 | void shouldMaskPasswordParameterChildrenValueByClass() { 93 | // We pass the non-existent class name in order to ensure that the Value metadata check is enough 94 | assertTrue(MaskPasswordsConfig.getInstance().isMasked(MyPasswordParameter.class.getName()), 95 | PasswordParameterValue.class + " must be masked by the class name"); 96 | } 97 | 98 | @Test 99 | @Issue("JENKINS-41955") 100 | void passwordParameterShouldBeMaskedInFreestyleProject() throws Exception { 101 | final String clearTextPassword = "myClearTextPassword"; 102 | final String logWithClearTextPassword = "printed " + clearTextPassword + " oops"; 103 | final String logWithHiddenPassword = "printed ******** oops"; 104 | 105 | FreeStyleProject project 106 | = j.jenkins.createProject(FreeStyleProject.class, "testPasswordParameter"); 107 | 108 | PasswordParameterDefinition passwordParameterDefinition 109 | = new hudson.model.PasswordParameterDefinition("Password1", clearTextPassword, null); 110 | ParametersDefinitionProperty parametersDefinitionProperty 111 | = new ParametersDefinitionProperty(passwordParameterDefinition); 112 | project.addProperty(parametersDefinitionProperty); 113 | 114 | MaskPasswordsBuildWrapper maskPasswordsBuildWrapper 115 | = new MaskPasswordsBuildWrapper(Collections.emptyList()); 116 | project.getBuildWrappersList().add(maskPasswordsBuildWrapper); 117 | 118 | project.getBuildersList().add(new TestBuilder() { 119 | @Override 120 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { 121 | listener.getLogger().println(logWithClearTextPassword); 122 | build.setResult(Result.SUCCESS); 123 | return true; 124 | } 125 | }); 126 | 127 | FreeStyleBuild build = j.buildAndAssertSuccess(project); 128 | j.assertLogContains(logWithHiddenPassword, build); 129 | j.assertLogNotContains(logWithClearTextPassword, build); 130 | } 131 | 132 | private static final class MyPasswordParameter extends hudson.model.PasswordParameterValue { 133 | 134 | @Serial 135 | private static final long serialVersionUID = 1L; 136 | 137 | public MyPasswordParameter() { 138 | super("MYPASSWORD", "qwerty123"); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test/java/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordConfigTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.michelin.cio.hudson.plugins.maskpasswords; 25 | 26 | import org.junit.jupiter.api.Test; 27 | import org.jvnet.hudson.test.Issue; 28 | import org.jvnet.hudson.test.JenkinsRule; 29 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 30 | 31 | import static org.junit.jupiter.api.Assertions.assertFalse; 32 | import static org.junit.jupiter.api.Assertions.assertTrue; 33 | 34 | /** 35 | * Tests of {@link MaskPasswordsConfig}. 36 | * These tests do not depend on the caching being done in the Jenkins instance. 37 | * 38 | * @author Oleg Nenashev 39 | */ 40 | @WithJenkins 41 | class MaskPasswordConfigTests { 42 | 43 | @Test 44 | void shouldConsiderAsMasked_cmchppPasswordParameterValue(JenkinsRule j) { 45 | assertIsMasked(com.michelin.cio.hudson.plugins.passwordparam.PasswordParameterValue.class); 46 | } 47 | 48 | @Test 49 | void shouldConsiderAsMasked_hmPasswordParameterValue(JenkinsRule j) { 50 | assertIsMasked(hudson.model.PasswordParameterValue.class); 51 | } 52 | 53 | @Test 54 | void shouldNotMaskTheBaseClass(JenkinsRule j) { 55 | assertIsNotMasked(hudson.model.ParameterValue.class); 56 | } 57 | 58 | @Test 59 | void shouldNotMaskTheBasicParameterTypes(JenkinsRule j) { 60 | assertIsNotMasked(hudson.model.StringParameterValue.class); 61 | assertIsNotMasked(hudson.model.FileParameterValue.class); 62 | } 63 | 64 | @Test 65 | void shouldReloadCorrectly_fromMissingFile(JenkinsRule j) { 66 | // Initialize caches 67 | MaskPasswordsConfig instance = MaskPasswordsConfig.getInstance(); 68 | assertIsNotMasked(instance, hudson.model.StringParameterValue.class); 69 | assertIsNotMasked(instance, hudson.model.FileParameterValue.class); 70 | 71 | MaskPasswordsConfig loaded = MaskPasswordsConfig.load(); 72 | assertIsNotMasked(loaded, hudson.model.StringParameterValue.class); 73 | assertIsNotMasked(loaded, hudson.model.FileParameterValue.class); 74 | } 75 | 76 | @Test 77 | @Issue("JENKINS-43504") 78 | void shouldReloadCorrectly_fromFile(JenkinsRule j) throws Exception { 79 | // Initialize caches 80 | MaskPasswordsConfig instance = MaskPasswordsConfig.getInstance(); 81 | assertIsNotMasked(instance, hudson.model.StringParameterValue.class); 82 | assertIsNotMasked(instance, hudson.model.FileParameterValue.class); 83 | MaskPasswordsConfig.save(instance); 84 | 85 | MaskPasswordsConfig loaded = MaskPasswordsConfig.load(); 86 | assertIsNotMasked(loaded, hudson.model.StringParameterValue.class); 87 | assertIsNotMasked(loaded, hudson.model.FileParameterValue.class); 88 | } 89 | 90 | private static void assertIsMasked(Class clazz) { 91 | MaskPasswordsConfig instance = MaskPasswordsConfig.getInstance(); 92 | assertIsMasked(instance, clazz); 93 | } 94 | 95 | private static void assertIsMasked(MaskPasswordsConfig config, Class clazz) { 96 | config.invalidatePasswordValueClassCaches(); 97 | assertTrue(config.guessIfShouldMask(clazz.getName()), "Expected that the class is masked: " + clazz); 98 | } 99 | 100 | private static void assertIsNotMasked(Class clazz) { 101 | MaskPasswordsConfig instance = MaskPasswordsConfig.getInstance(); 102 | assertIsNotMasked(instance, clazz); 103 | } 104 | 105 | private static void assertIsNotMasked(MaskPasswordsConfig config, Class clazz) { 106 | config.invalidatePasswordValueClassCaches(); 107 | assertFalse(config.guessIfShouldMask(clazz.getName()), "Expected that the class is not masked: " + clazz); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsURLEncodingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 Jesse Glick. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package com.michelin.cio.hudson.plugins.maskpasswords; 26 | 27 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 28 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 29 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 30 | import org.junit.jupiter.api.BeforeEach; 31 | import org.junit.jupiter.api.Test; 32 | import org.jvnet.hudson.test.Issue; 33 | import org.jvnet.hudson.test.JenkinsRule; 34 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 35 | 36 | import java.net.URLEncoder; 37 | import java.nio.charset.StandardCharsets; 38 | 39 | @Issue("JENKINS-34908") 40 | @WithJenkins 41 | class MaskPasswordsURLEncodingTest { 42 | 43 | private static final String THE_SECRET = "#s3cr3t"; 44 | 45 | private JenkinsRule j; 46 | 47 | @BeforeEach 48 | void dropCache(JenkinsRule j) { 49 | this.j = j; 50 | MaskPasswordsConfig.getInstance().reset(); 51 | } 52 | 53 | @Test 54 | void passwordMaskedEncoded() throws Throwable { 55 | WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); 56 | p.setDefinition(new CpsFlowDefinition("node {wrap([$class: 'MaskPasswordsBuildWrapper', varPasswordPairs: [[var: 'PASSWORD', password: '" + THE_SECRET + "']]]) {echo 'printed unencoded " + THE_SECRET + " oops'; echo 'printed encoded " + URLEncoder.encode(THE_SECRET, StandardCharsets.UTF_8) + " oops'}}", true)); 57 | 58 | WorkflowRun b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); 59 | j.assertLogContains("printed unencoded ******** oops", b); 60 | j.assertLogContains("printed encoded ******** oops", b); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/michelin/cio/hudson/plugins/maskpasswords/MaskPasswordsWorkflowTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2015 Jesse Glick. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package com.michelin.cio.hudson.plugins.maskpasswords; 26 | 27 | import hudson.Functions; 28 | import hudson.Launcher; 29 | import hudson.model.AbstractBuild; 30 | import hudson.model.BuildListener; 31 | import hudson.model.Cause; 32 | import hudson.model.FreeStyleBuild; 33 | import hudson.model.FreeStyleProject; 34 | import hudson.model.Result; 35 | import hudson.util.Secret; 36 | import org.apache.commons.io.FileUtils; 37 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 38 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 39 | import org.jenkinsci.plugins.workflow.job.WorkflowRun; 40 | import org.jenkinsci.plugins.workflow.steps.CoreWrapperStep; 41 | import org.jenkinsci.plugins.workflow.steps.StepConfigTester; 42 | import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; 43 | import org.junit.jupiter.api.Test; 44 | import org.jvnet.hudson.test.Issue; 45 | import org.jvnet.hudson.test.JenkinsRule; 46 | import org.jvnet.hudson.test.TestBuilder; 47 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 48 | 49 | import java.io.File; 50 | import java.io.IOException; 51 | import java.nio.charset.StandardCharsets; 52 | import java.util.Arrays; 53 | import java.util.Collections; 54 | import java.util.HashSet; 55 | import java.util.List; 56 | import java.util.Set; 57 | import java.util.TreeSet; 58 | import java.util.concurrent.TimeUnit; 59 | 60 | import static org.awaitility.Awaitility.await; 61 | import static org.hamcrest.Matchers.equalTo; 62 | import static org.junit.jupiter.api.Assertions.assertEquals; 63 | 64 | @Issue("JENKINS-27392") 65 | @WithJenkins 66 | class MaskPasswordsWorkflowTest { 67 | 68 | @Test 69 | void configRoundTrip(JenkinsRule j) throws Exception { 70 | MaskPasswordsBuildWrapper bw1 = new MaskPasswordsBuildWrapper( 71 | Collections.singletonList(new MaskPasswordsBuildWrapper.VarPasswordPair("PASSWORD", Secret.fromString("s3cr3t"))) 72 | ); 73 | CoreWrapperStep step1 = new CoreWrapperStep(bw1); 74 | CoreWrapperStep step2 = new StepConfigTester(j).configRoundTrip(step1); 75 | MaskPasswordsBuildWrapper bw2 = (MaskPasswordsBuildWrapper) step2.getDelegate(); 76 | List pairs = bw2.getVarPasswordPairs(); 77 | assertEquals(1, pairs.size()); 78 | MaskPasswordsBuildWrapper.VarPasswordPair pair = pairs.get(0); 79 | assertEquals("PASSWORD", pair.getVar()); 80 | assertEquals("s3cr3t", pair.getPassword().getPlainText()); 81 | } 82 | 83 | @Test 84 | void regexConfigRoundTrip(JenkinsRule j) throws Exception { 85 | MaskPasswordsBuildWrapper bw1 = new MaskPasswordsBuildWrapper( 86 | null, 87 | Collections.singletonList(new MaskPasswordsConfig.VarMaskRegexEntry("test", "foobar")) 88 | ); 89 | CoreWrapperStep step1 = new CoreWrapperStep(bw1); 90 | CoreWrapperStep step2 = new StepConfigTester(j).configRoundTrip(step1); 91 | MaskPasswordsBuildWrapper bw2 = (MaskPasswordsBuildWrapper) step2.getDelegate(); 92 | List regexes = bw2.getVarMaskRegexes(); 93 | assertEquals(1, regexes.size()); 94 | MaskPasswordsConfig.VarMaskRegexEntry regex = regexes.get(0); 95 | assertEquals("foobar", regex.getRegexString()); 96 | assertEquals("test", regex.getKey()); 97 | } 98 | 99 | @Test 100 | void basics(JenkinsRule j) throws Throwable { 101 | WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); 102 | p.setDefinition(new CpsFlowDefinition("node {wrap([$class: 'MaskPasswordsBuildWrapper', varPasswordPairs: [[var: 'PASSWORD', password: 's3cr3t']]]) {semaphore 'waiting'; echo 'printed s3cr3t oops'}}", true)); 103 | WorkflowRun b = p.scheduleBuild2(0).waitForStart(); 104 | SemaphoreStep.waitForStart("waiting/1", b); 105 | Set expected = new HashSet<>(Arrays.asList("build.xml", "program.dat", "workflow/5.xml")); 106 | if (!Functions.isWindows()) { 107 | // Skip assertion on Windows, temporary files contaminate content frequently 108 | await().atMost(5, TimeUnit.SECONDS) 109 | .alias("TODO cannot keep it out of the closure block, but at least outside users cannot see this; withCredentials does better") 110 | .until(() -> grep(b.getRootDir(), "s3cr3t"), equalTo(expected)); 111 | } 112 | SemaphoreStep.success("waiting/1", null); 113 | j.assertBuildStatusSuccess(j.waitForCompletion(b)); 114 | j.assertLogContains("printed ******** oops", b); 115 | j.assertLogNotContains("printed s3cr3t oops", b); 116 | expected = new HashSet<>(Arrays.asList("build.xml", "workflow-completed/flowNodeStore.xml")); 117 | if (!Functions.isWindows()) { 118 | // Skip assertion on Windows, temporary files contaminate content frequently 119 | await().atMost(5, TimeUnit.SECONDS) 120 | .alias("in build.xml only because it was literally in program text") 121 | .until(() -> grep(b.getRootDir(), "s3cr3t"), equalTo(expected)); 122 | } 123 | } 124 | 125 | @Test 126 | void basicsRegex(JenkinsRule j) throws Throwable { 127 | WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); 128 | p.setDefinition(new CpsFlowDefinition("node {wrap([$class: 'MaskPasswordsBuildWrapper', varMaskRegexes: [[key: 'REGEX', value: 's3cr3t']]]) {semaphore 'waiting'; echo 'printed s3cr3t oops'}}", true)); 129 | WorkflowRun b = p.scheduleBuild2(0).waitForStart(); 130 | SemaphoreStep.waitForStart("waiting/1", b); 131 | Set expected = new HashSet<>(Arrays.asList("build.xml", "program.dat", "workflow/5.xml")); 132 | if (!Functions.isWindows()) { 133 | // Skip assertion on Windows, temporary files contaminate content frequently 134 | await().atMost(5, TimeUnit.SECONDS) 135 | .alias("TODO cannot keep it out of the closure block, but at least outside users cannot see this; withCredentials does better") 136 | .until(() -> grep(b.getRootDir(), "s3cr3t"), equalTo(expected)); 137 | } 138 | SemaphoreStep.success("waiting/1", null); 139 | j.assertBuildStatusSuccess(j.waitForCompletion(b)); 140 | j.assertLogContains("printed ******** oops", b); 141 | j.assertLogNotContains("printed s3cr3t oops", b); 142 | expected = new HashSet<>(Arrays.asList("build.xml", "workflow-completed/flowNodeStore.xml")); 143 | if (!Functions.isWindows()) { 144 | // Skip assertion on Windows, temporary files contaminate content frequently 145 | await().atMost(5, TimeUnit.SECONDS) 146 | .alias("in build.xml only because it was literally in program text") 147 | .until(() -> grep(b.getRootDir(), "s3cr3t"), equalTo(expected)); 148 | } 149 | } 150 | 151 | @Test 152 | void noWorkspaceRequired(JenkinsRule j) throws Exception { 153 | WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); 154 | p.setDefinition(new CpsFlowDefinition("maskPasswords(varPasswordPairs: [[var: 'PASSWORD', password: 's3cr3t']]) {echo 'printed s3cr3t oops'}", true)); 155 | WorkflowRun b = p.scheduleBuild2(0).waitForStart(); 156 | j.assertBuildStatusSuccess(j.waitForCompletion(b)); 157 | j.assertLogContains("printed ******** oops", b); 158 | j.assertLogNotContains("printed s3cr3t oops", b); 159 | } 160 | 161 | @Test 162 | void noWorkspaceRequiredRegex(JenkinsRule j) throws Exception { 163 | WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); 164 | p.setDefinition(new CpsFlowDefinition("maskPasswords(varMaskRegexes: [[key: 'REGEX', value: 's3cr3t']]) {echo 'printed s3cr3t oops'}", true)); 165 | WorkflowRun b = p.scheduleBuild2(0).waitForStart(); 166 | j.assertBuildStatusSuccess(j.waitForCompletion(b)); 167 | j.assertLogContains("printed ******** oops", b); 168 | j.assertLogNotContains("printed s3cr3t oops", b); 169 | } 170 | 171 | // test to ensure that when the ConsoleLogFilter isn't enabled globally, 172 | // it doesn't change the output (i.e. it respects the config setting). 173 | // Note that per JENKINS-30777, this does not work with Pipeline jobs 174 | // we would need to implement a TaskListenerDecorator for that to work 175 | @Test 176 | void notEnabledGlobally(JenkinsRule j) throws Exception { 177 | MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); 178 | config.setGlobalVarEnabledGlobally(false); 179 | config.addGlobalVarMaskRegex(new MaskPasswordsBuildWrapper.VarMaskRegex("s\\dcr[0-9]t")); 180 | MaskPasswordsConfig.save(config); 181 | FreeStyleProject p = j.jenkins.createProject(FreeStyleProject.class, "p2"); 182 | p.getBuildersList().add(new TestBuilder() { 183 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { 184 | listener.getLogger().println("printed s3cr3t oops"); 185 | build.setResult(Result.SUCCESS); 186 | return true; 187 | } 188 | }); 189 | FreeStyleBuild b = p.scheduleBuild2(0, new Cause.UserIdCause()).get(); 190 | j.assertBuildStatusSuccess(j.waitForCompletion(b)); 191 | j.assertLogContains("printed s3cr3t oops", b); 192 | } 193 | 194 | // Test to ensure that when the plugin/ConsoleLogFilter **is** enabled globally, 195 | // it actually suppresses the log output. Note that per JENKINS-30777, 196 | // this does not work with Pipeline jobs 197 | // we would need to implement a TaskListenerDecorator for that to work 198 | @Test 199 | void enabledGlobally(JenkinsRule j) throws Exception { 200 | MaskPasswordsConfig config = MaskPasswordsConfig.getInstance(); 201 | config.setGlobalVarEnabledGlobally(true); 202 | config.addGlobalVarMaskRegex(new MaskPasswordsBuildWrapper.VarMaskRegex("s\\dcr[0-9]t")); 203 | MaskPasswordsConfig.save(config); 204 | FreeStyleProject p = j.jenkins.createProject(FreeStyleProject.class, "p2"); 205 | p.getBuildersList().add(new TestBuilder() { 206 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { 207 | listener.getLogger().println("printed s3cr3t oops"); 208 | build.setResult(Result.SUCCESS); 209 | return true; 210 | } 211 | }); 212 | FreeStyleBuild b = p.scheduleBuild2(0, new Cause.UserIdCause()).get(); 213 | j.assertBuildStatusSuccess(j.waitForCompletion(b)); 214 | j.assertLogContains("printed ******** oops", b); 215 | j.assertLogNotContains("printed s3cr3t oops", b); 216 | } 217 | 218 | 219 | // Copied from credentials-binding-plugin; perhaps belongs in JenkinsRule? 220 | private static Set grep(File dir, String text) throws IOException { 221 | Set matches = new TreeSet<>(); 222 | grep(dir, text, "", matches); 223 | return matches; 224 | } 225 | 226 | private static void grep(File dir, String text, String prefix, Set matches) throws IOException { 227 | File[] kids = dir.listFiles(); 228 | if (kids == null) { 229 | return; 230 | } 231 | for (File kid : kids) { 232 | String qualifiedName = prefix + kid.getName(); 233 | if (kid.isDirectory()) { 234 | grep(kid, text, qualifiedName + "/", matches); 235 | } else if (kid.isFile() && FileUtils.readFileToString(kid, StandardCharsets.UTF_8).contains(text)) { 236 | matches.add(qualifiedName); 237 | } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/test/java/com/michelin/cio/hudson/plugins/passwordparam/PasswordParameterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2017 Jenkins contributors. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.michelin.cio.hudson.plugins.passwordparam; 25 | 26 | import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper; 27 | import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsConfig; 28 | import hudson.Launcher; 29 | import hudson.model.AbstractBuild; 30 | import hudson.model.BuildListener; 31 | import hudson.model.Cause; 32 | import hudson.model.FreeStyleBuild; 33 | import hudson.model.FreeStyleProject; 34 | import hudson.model.ParameterValue; 35 | import hudson.model.ParametersAction; 36 | import hudson.model.ParametersDefinitionProperty; 37 | import hudson.model.Result; 38 | import hudson.util.Secret; 39 | import org.junit.jupiter.api.BeforeEach; 40 | import org.junit.jupiter.api.Test; 41 | import org.jvnet.hudson.test.Issue; 42 | import org.jvnet.hudson.test.JenkinsRule; 43 | import org.jvnet.hudson.test.TestBuilder; 44 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 45 | 46 | import java.util.Collections; 47 | 48 | import static org.junit.jupiter.api.Assertions.assertTrue; 49 | 50 | /** 51 | * Tests of {@link PasswordParameterValue} and {@link PasswordParameterDefinition}. 52 | * 53 | * @author bpmarinho 54 | */ 55 | @WithJenkins 56 | class PasswordParameterTest { 57 | 58 | private JenkinsRule j; 59 | 60 | @BeforeEach 61 | void dropCache(JenkinsRule j) { 62 | this.j = j; 63 | MaskPasswordsConfig.getInstance().reset(); 64 | } 65 | 66 | @Test 67 | @Issue("JENKINS-41955") 68 | void shouldMaskPasswordParameterClassByDefault() { 69 | assertTrue(MaskPasswordsConfig.getInstance().isMasked(PasswordParameterValue.class.getName()), 70 | PasswordParameterValue.class + " must be masked by default"); 71 | } 72 | 73 | @Test 74 | @Issue("JENKINS-41955") 75 | void shouldMaskPasswordParameterValueByDefault() { 76 | PasswordParameterDefinition d = new PasswordParameterDefinition("FOO", "BAR"); 77 | ParameterValue created = d.createValue(Secret.fromString("hello")); 78 | 79 | // We pass the non-existent class name in order to ensure that the Value metadata check is enough 80 | assertTrue(MaskPasswordsConfig.getInstance().isMasked(created, "nonExistent"), 81 | PasswordParameterValue.class + " must be masked by default"); 82 | } 83 | 84 | @Test 85 | @Issue("JENKINS-41955") 86 | void passwordParameterShouldBeMaskedInFreestyleProject() throws Exception { 87 | final String clearTextPassword = "myClearTextPassword"; 88 | final String logWithClearTextPassword = "printed " + clearTextPassword + " oops"; 89 | final String logWithHiddenPassword = "printed ******** oops"; 90 | 91 | FreeStyleProject project 92 | = j.jenkins.createProject(FreeStyleProject.class, "testPasswordParameter"); 93 | 94 | PasswordParameterDefinition passwordParameterDefinition = new PasswordParameterDefinition("Password1", null); 95 | ParametersDefinitionProperty parametersDefinitionProperty 96 | = new ParametersDefinitionProperty(passwordParameterDefinition); 97 | project.addProperty(parametersDefinitionProperty); 98 | 99 | MaskPasswordsBuildWrapper maskPasswordsBuildWrapper 100 | = new MaskPasswordsBuildWrapper(Collections.emptyList()); 101 | project.getBuildWrappersList().add(maskPasswordsBuildWrapper); 102 | 103 | project.getBuildersList().add(new TestBuilder() { 104 | @Override 105 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { 106 | listener.getLogger().println(logWithClearTextPassword); 107 | build.setResult(Result.SUCCESS); 108 | return true; 109 | } 110 | }); 111 | 112 | FreeStyleBuild build = project.scheduleBuild2(0, new Cause.UserIdCause(), 113 | new ParametersAction(passwordParameterDefinition.createValue(Secret.fromString(clearTextPassword)))) 114 | .get(); 115 | j.assertBuildStatusSuccess(j.waitForCompletion(build)); 116 | j.assertLogContains(logWithHiddenPassword, build); 117 | j.assertLogNotContains(logWithClearTextPassword, build); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/test/java/com/michelin/cio/hudson/plugins/util/MaskPasswordsUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.michelin.cio.hudson.plugins.util; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.regex.Pattern; 12 | 13 | import static com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper.VarMaskRegex; 14 | import static com.michelin.cio.hudson.plugins.util.MaskPasswordsUtil.passwordRegexCombiner; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | 17 | class MaskPasswordsUtilTest { 18 | 19 | @Test 20 | void testAwsMaskJson() throws IOException { 21 | String input = FileUtils.readFileToString(new File(getClass().getResource("echoAwsJson.txt").getFile()), "UTF-8"); 22 | String maskedString = FileUtils.readFileToString(new File(getClass().getResource("echoAwsJsonMasked.txt").getFile()), "UTF-8"); 23 | 24 | List regexes = new ArrayList<>(); 25 | List rs = new ArrayList<>(); 26 | rs.add(new VarMaskRegex("['\"]+(?:(?i:SecretAccessKey)|(?i:AccessKeyId)|(?i:SessionToken))['\"]+:[']?\\s*['\"]+\\s*([a-zA-Z0-9\\/=+]*)['\"]?")); 27 | for (VarMaskRegex r : rs) { 28 | regexes.add(r.getRegex()); 29 | } 30 | 31 | String output = MaskPasswordsUtil.secretsMaskPatterns(passwordRegexCombiner(null, regexes), input, ""); 32 | 33 | assertEquals(maskedString, output); 34 | } 35 | 36 | @Test 37 | void testSimpleString() { 38 | String input = "test1 too12 test123 too1234 secret12345 secret1234"; 39 | String expected = "t********1 t********12 t********123 t********1234 ********12345 ********1234"; 40 | 41 | List regexes = List.of("(?:t)(est|oo)(?:1)"); 42 | List passwords = List.of("secret"); 43 | String output = MaskPasswordsUtil.secretsMaskPatterns(passwordRegexCombiner(passwords, regexes), input, ""); 44 | 45 | assertEquals(expected, output); 46 | } 47 | 48 | // Test usecase where Regex doesn't contain grouping and want to match entire string 49 | @Test 50 | void testEntireRegex() { 51 | String expectStr = "AWS_SECRET_ACCESS_KEY=4KJOMHUs8BHcILmZ4KlLfKLjuIuSINfExPy4oZIC"; 52 | String input = "export " + expectStr; 53 | List expect = new ArrayList<>(List.of(expectStr)); 54 | Pattern p = Pattern.compile("AWS_SECRET_ACCESS_KEY=[\\S]+"); 55 | assertEquals(expect, MaskPasswordsUtil.patternMatch(p, input)); 56 | } 57 | 58 | // 59 | // Test where Regex pattern has multiple matches in a single String 60 | @Test 61 | void testMultipleMatches() { 62 | String expect1 = "1234"; 63 | String expect2 = "5678"; 64 | List expect = new ArrayList<>(Arrays.asList(expect1, expect2)); 65 | String input = String.format("Secret = %s, Secret = %s", expect1, expect2); 66 | Pattern p = Pattern.compile("Secret = ([(0-9]*)"); 67 | assertEquals(expect, MaskPasswordsUtil.patternMatch(p, input)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/resources/com/michelin/cio/hudson/plugins/util/echoAwsJson.txt: -------------------------------------------------------------------------------- 1 | + echo { "Credentials": { "Expiration": "2021-02-06T01:39:01Z", "SecretAccessKey": "DYj8LoxSgA1MW6rz8bd0jgzxmYfUNX57+RsUxEwE", "AccessKeyId": "ASIA2VSRKG7RDUPV4FWZ", "SessionToken": "FwoGZXIvYXdzEFIaDB+/m9zeHuRiekrwhCKyAf5XZMLTy+wwfYNORYNc8H14GbcMTe7a9t28IKKPstvF2PGJjPwZDS6EsAKFD79ZQ8A53W4Fu7HfDMuoeXuDZCCj5s9B934gFFfAzRV+95j1gUBhfxCL2t+ShHPx3xEAr1ixrL5gyaD2OSjyaJUCmdPlOch6s2L4QYWFaZrkvmxmRv989WF6ttZzj4ns9Oyfoflt3gztOQlH5fLt/zur7xlU/rKaddQQqQxqpsfk3JqRKxcopcr3gAYyLcrHBuzU3XGXZ6i9VFKUEaw9ueWF35A4V4F4i8eY0UteQeF3Hqk00pu9FNgkEA==" }, "AssumedRoleUser": { "AssumedRoleId": "AROA2VSRKG7RO6QH2IY4T:AWSCLI-Session", "Arn": "arn:aws:sts::733536204770:assumed-role/some-fake-role-permissions/AWSCLI-Session" } } -------------------------------------------------------------------------------- /src/test/resources/com/michelin/cio/hudson/plugins/util/echoAwsJsonMasked.txt: -------------------------------------------------------------------------------- 1 | + echo { "Credentials": { "Expiration": "2021-02-06T01:39:01Z", "SecretAccessKey": "********", "AccessKeyId": "********", "SessionToken": "********" }, "AssumedRoleUser": { "AssumedRoleId": "AROA2VSRKG7RO6QH2IY4T:AWSCLI-Session", "Arn": "arn:aws:sts::733536204770:assumed-role/some-fake-role-permissions/AWSCLI-Session" } } --------------------------------------------------------------------------------