├── .github ├── dependabot.yml └── workflows │ ├── integration_test.yml │ ├── lint.yml │ ├── release.yml │ ├── test_custom_executor.yml │ └── unit_test.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENCE.txt ├── README.md ├── TESTING.md ├── custom_executors ├── github_custom_executor │ ├── before_script.sh │ ├── generate_jwt.sh │ └── profile.sh └── gitlab_custom_executor │ ├── base.sh │ ├── cleanup.sh │ ├── config.sh │ ├── generate_jwt.sh │ ├── prepare.sh │ ├── profile.sh │ └── run.sh ├── docs └── img │ ├── automated_use_case.png │ ├── logo.jpg │ └── user_initiated_use_case.png ├── go.mod ├── go.sum ├── main.go ├── pkg ├── checkparser │ ├── checkparser.go │ └── checkparser_test.go ├── checks │ ├── checks.go │ ├── checks_test.go │ ├── datetime │ │ ├── datetime.go │ │ └── datetime_test.go │ ├── imagehash │ │ ├── imagehash.go │ │ └── imagehash_test.go │ ├── mfarequired │ │ ├── mfarequired.go │ │ └── mfarequired_test.go │ └── scripthash │ │ ├── scripthash.go │ │ └── scripthash_test.go ├── cmd │ └── validatetokencmd │ │ └── validatetokencmd.go ├── config │ ├── config.go │ └── config_test.go ├── loggerclient │ ├── consoleclient │ │ ├── console_client.go │ │ └── console_client_test.go │ ├── loggerclient.go │ └── mattermostclient │ │ ├── mattermost_client.go │ │ └── mattermost_client_test.go ├── scriptcleanerclient │ ├── githubcleanup │ │ ├── githubcleanup.go │ │ └── githubcleanup_test.go │ ├── gitlabcleanup │ │ ├── gitlabcleanup.go │ │ └── gitlabcleanup_test.go │ ├── scriptcleanerparser.go │ └── scriptcleanerparser_test.go ├── vaultclient │ ├── hashicorpclient │ │ ├── hashicorp_client.go │ │ └── hashicorp_client_test.go │ ├── vaultclient.go │ ├── vaultclient_test.go │ └── vaultclientparser │ │ ├── vault_client_parser.go │ │ └── vault_client_parser_test.go └── whitelist │ ├── whitelist.go │ └── whitelist_test.go ├── renovate.json └── testing ├── Dockerfiles ├── Dockerfile-dev └── Dockerfile-gitlab ├── colors.sh ├── integration ├── hashicorp_gitlab_auth_timeout │ ├── runner-compose.yml │ └── vault-compose.yml ├── hashicorp_gitlab_automatic │ ├── runner-compose.yml │ └── vault-compose.yml ├── hashicorp_gitlab_bash │ ├── runner-compose.yml │ └── vault-compose.yml ├── hashicorp_gitlab_fail │ ├── runner-compose.yml │ └── vault-compose.yml ├── hashicorp_gitlab_mfa │ ├── runner-compose.yml │ └── vault-compose.yml ├── test.sh └── vaultclient │ └── hashicorp │ └── docker-compose.yml ├── scripts ├── git-init.sh └── vault-init.sh ├── test.sh └── unit ├── test.sh └── vaultclient └── hashicorp └── docker-compose.yml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "docker" # See documentation for possible values 8 | directory: "/" # Location of package manifests 9 | schedule: 10 | interval: "weekly" 11 | - package-ecosystem: "docker" # See documentation for possible values 12 | directory: "/testing/Dockerfiles" # Location of package manifests 13 | schedule: 14 | interval: "weekly" 15 | - package-ecosystem: "github-actions" # See documentation for possible values 16 | directory: "/" # Location of package manifests 17 | schedule: 18 | interval: "weekly" 19 | -------------------------------------------------------------------------------- /.github/workflows/integration_test.yml: -------------------------------------------------------------------------------- 1 | name: YouShallNotPass Integration Test 2 | run-name: Running YouShallNotPass Integration Tests 3 | on: 4 | pull_request: 5 | types: [opened, edited, ready_for_review, reopened, synchronize] 6 | jobs: 7 | hashicorp-integration-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out Repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Set Up Docker 14 | uses: docker/setup-buildx-action@v3 15 | 16 | - name: Set Up Docker Compose 17 | uses: ndeloof/install-compose-action@v0.0.1 18 | 19 | - name: Run Hashicorp Integration Test 20 | run: ./testing/test.sh integration hashicorpclient 21 | 22 | mattermost-integration-test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Check out Repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Set Up Docker 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Set Up Docker Compose 32 | uses: ndeloof/install-compose-action@v0.0.1 33 | 34 | - name: Run Mattermost Integration Test 35 | run: ./testing/test.sh integration mattermostclient 36 | 37 | hashicorp-gitlab-automatic-test: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Check out Repository 41 | uses: actions/checkout@v4 42 | 43 | - name: Set Up Docker 44 | uses: docker/setup-buildx-action@v3 45 | 46 | - name: Set Up Docker Compose 47 | uses: ndeloof/install-compose-action@v0.0.1 48 | 49 | - name: Run Hashicorp Gitlab Automatic Integration Test 50 | run: ./testing/test.sh integration hashicorpgitlabautomatic 51 | 52 | hashicorp-gitlab-failure-test: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Check out Repository 56 | uses: actions/checkout@v4 57 | 58 | - name: Set Up Docker 59 | uses: docker/setup-buildx-action@v3 60 | 61 | - name: Set Up Docker Compose 62 | uses: ndeloof/install-compose-action@v0.0.1 63 | 64 | - name: Run Hashicorp Gitlab Failure Integration Test 65 | run: ./testing/test.sh integration hashicorpgitlabfail 66 | 67 | # I cannot figure out a way to automate the authentication integration test, which is 68 | # probably a good thing. 69 | 70 | hashicorp-gitlab-auth-timeout-test: 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Check out Repository 74 | uses: actions/checkout@v4 75 | 76 | - name: Set Up Docker 77 | uses: docker/setup-buildx-action@v3 78 | 79 | - name: Set Up Docker Compose 80 | uses: ndeloof/install-compose-action@v0.0.1 81 | 82 | - name: Run Hashicorp Gitlab Auth Timeout Integration Test 83 | run: ./testing/test.sh integration hashicorpgitlabtimeout 84 | 85 | hashicorp-gitlab-bash-test: 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Check out Repository 89 | uses: actions/checkout@v4 90 | 91 | - name: Set Up Docker 92 | uses: docker/setup-buildx-action@v3 93 | 94 | - name: Set Up Docker Compose 95 | uses: ndeloof/install-compose-action@v0.0.1 96 | 97 | - name: Run Hashicorp Gitlab Bash Integration Test 98 | run: ./testing/test.sh integration hashicorpgitlabbash 99 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: YouShallNotPass Linter 2 | run-name: Linting YouShallNotPass 3 | on: [push] 4 | jobs: 5 | golangci-lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Check out Repository 9 | uses: actions/checkout@v4 10 | 11 | - name: Run Golangci-Lint 12 | uses: golangci/golangci-lint-action@v3 13 | with: 14 | version: v1.53 15 | 16 | shellcheck-lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out Repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Run ShellCheck 23 | uses: ludeeus/action-shellcheck@master -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: YouShallNotPass Release 2 | run-name: Create Release for YouShallNotPass 3 | on: 4 | push: 5 | tags: 6 | '[0-9].[0-9].[0-9]' 7 | 8 | jobs: 9 | create-release: 10 | permissions: write-all 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Create Changelog 14 | uses: heinrichreimer/github-changelog-generator-action@v2.3 15 | id: Changelog 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: Check out Repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Add Custom Executors to Artifacts 23 | run: | 24 | cd custom_executors 25 | zip -r github_custom_executor.zip github_custom_executor 26 | zip -r gitlab_custom_executor.zip gitlab_custom_executor 27 | cd .. 28 | working-directory: ${{ github.workspace }} 29 | 30 | - name: Set Up Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: '1.20' 34 | 35 | - name: Create YouShallNotPass Binary 36 | run: | 37 | go build -o youshallnotpass 38 | 39 | - name: Upload Artifacts and Create Release 40 | uses: softprops/action-gh-release@v1 41 | with: 42 | tag_name: ${{ github.ref }} 43 | name: Release ${{ github.ref_name }} 44 | body: | 45 | ${{ steps.Changelog.outputs.changelog }} 46 | draft: false 47 | prerelease: false 48 | files: | 49 | custom_executors/github_custom_executor.zip 50 | custom_executors/gitlab_custom_executor.zip 51 | youshallnotpass 52 | -------------------------------------------------------------------------------- /.github/workflows/test_custom_executor.yml: -------------------------------------------------------------------------------- 1 | name: Run test jobs to validate with YSNP 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | Test-job-default-conf-ask-mfa-YSNP: 6 | runs-on: self-hosted 7 | steps: 8 | - name: Check out Repository 9 | uses: actions/checkout@v4 10 | - run: echo "Job is run after YSNP verification using default checks - fallbacked to user check!" 11 | 12 | Test-job-image-check-YSNP: 13 | runs-on: self-hosted 14 | container: 15 | image: alpine:3.18.4@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 16 | steps: 17 | - name: Check out Repository 18 | uses: actions/checkout@v4 19 | - run: | 20 | echo "Job is run after YSNP verification using image + job checks!" 21 | 22 | Test-job-only-ask-mfa-YSNP: 23 | runs-on: self-hosted 24 | steps: 25 | - name: Check out Repository 26 | uses: actions/checkout@v4 27 | - run: echo "Job is run after YSNP verification using only user check!" 28 | 29 | Test-job-all-checks-YSNP: 30 | runs-on: self-hosted 31 | container: 32 | image: alpine:3.18.4@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 33 | steps: 34 | - name: Check out Repository 35 | uses: actions/checkout@v4 36 | - run: echo "Job is run after YSNP verification using image + script + hash check!" 37 | 38 | # this is the whitelist config on Vault 39 | # echo -n '{ 40 | # "allowed_images": [ 41 | # "alpine:3.18.4@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978" 42 | # ], 43 | # "allowed_scripts": [ 44 | # "Test-job-image-check-YSNP@sha256:DoBpHT_168adOHRIj5O1bYkai3qrtyvmgNPKNgeID8U=", 45 | # "Test-job-all-checks-YSNP@sha256:IgO3t_wZKFABuLyUBeQs6wQLvHl476rQFcyBm9GCAAE=" 46 | # ] 47 | # }' | vault kv put your_mount_root/your_github_username_or_organization/project_name/whitelist - 48 | 49 | # this is the youshallnotpass_config on Vault 50 | # echo -n '{ 51 | # "jobs": [ 52 | # { 53 | # "jobName": "Test-job-only-ask-mfa-YSNP", 54 | # "checks": [ 55 | # { 56 | # "name": "mfaRequired" 57 | # } 58 | # ] 59 | # }, 60 | # { 61 | # "jobName": "Test-job-all-checks-YSNP", 62 | # "checks": [ 63 | # { 64 | # "name": "mfaRequired" 65 | # }, 66 | # { 67 | # "name": "imageHash" 68 | # }, 69 | # { 70 | # "name": "scriptHash" 71 | # } 72 | # ] 73 | # } 74 | # ] 75 | # }' | vault kv put your_mount_root/your_github_username_or_organization/project_name/youshallnotpass_config - -------------------------------------------------------------------------------- /.github/workflows/unit_test.yml: -------------------------------------------------------------------------------- 1 | name: YouShallNotPass Unit Test 2 | run-name: Running YouShallNotPass Unit Tests 3 | on: [push] 4 | jobs: 5 | golang-unit-test: 6 | runs-on: ubuntu-latest 7 | container: 8 | image: golang:alpine3.18@sha256:7839c9f01b5502d7cb5198b2c032857023424470b3e31ae46a8261ffca72912a 9 | steps: 10 | - name: Check out Repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Add Bash Dependencies 14 | run: | 15 | apk update 16 | apk add bash 17 | 18 | - name: Execute Bash Testing Script 19 | run: ./testing/test.sh unit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################## 2 | # Generic security 3 | .env 4 | .git 5 | 6 | ######################## 7 | # Infrastructure-as-code 8 | 9 | ## Terraform 10 | .terraform 11 | .terraform.lock.hcl 12 | *.tfstate 13 | *.tfstate.* 14 | crash.log 15 | crash.*.log 16 | override.tf 17 | override.tf.json 18 | *_override.tf 19 | *_override.tf.json 20 | .terraformrc 21 | terraform.rc 22 | .variables.tf 23 | .providers.tf 24 | **/variables.tf 25 | **/providers.tf 26 | **/backend.tf 27 | **/*.zip 28 | .terraform.lock.hcl 29 | 30 | ## Ansible 31 | *.retry 32 | 33 | ####### 34 | # OSes 35 | 36 | # MacOS 37 | .DS_Store 38 | .AppleDouble 39 | .LSOverride 40 | Icon 41 | ._* 42 | .DocumentRevisions-V100 43 | .fseventsd 44 | .Spotlight-V100 45 | .TemporaryItems 46 | .Trashes 47 | .VolumeIcon.icns 48 | .com.apple.timemachine.donotpresent 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | 55 | # Linux 56 | *~ 57 | .fuse_hidden* 58 | .directory 59 | .Trash-* 60 | .nfs* 61 | 62 | ######## 63 | # IDEs 64 | 65 | ## VSCode 66 | .vscode/* 67 | !.vscode/settings.json 68 | !.vscode/tasks.json 69 | !.vscode/launch.json 70 | !.vscode/extensions.json 71 | !.vscode/*.code-snippets 72 | .history/ 73 | *.vsix 74 | 75 | ## JetBrains Fleet 76 | .fleet/ 77 | 78 | ## JetBrains IntelliJ 79 | .idea/ 80 | *.iml 81 | *.iws 82 | *.ipr 83 | *.log 84 | **/additional-spring-configuration-metadata.json 85 | 86 | ## STS 87 | .apt_generated 88 | .classpath 89 | .factorypath 90 | .project 91 | .settings 92 | .springBeans 93 | .sts4-cache 94 | 95 | ########### 96 | # Languages 97 | 98 | ## Node: npm, yarn v1 99 | **/node_modules/ 100 | **/build/ 101 | **/dist/ 102 | .npm 103 | .eslintcache 104 | .env.development.local 105 | .env.production.local 106 | .env.local 107 | npm-debug.log* 108 | yarn-debug.log* 109 | yarn-error.log* 110 | 111 | ## Node: yarn v2 (PnP) 112 | **/.pnp.* 113 | **/.yarn/* 114 | !**/.yarn/patches 115 | !**/.yarn/plugins 116 | !**/.yarn/releases 117 | !**/.yarn/sdks 118 | !**/.yarn/versions 119 | 120 | ## Node: testing 121 | .env.test.local 122 | cypress.env.json 123 | **/coverage/ 124 | 125 | ## Python 126 | __pycache__/ 127 | *.py[cod] 128 | *$py.class 129 | .Python 130 | build/ 131 | *venv/ 132 | 133 | ## Maven 134 | target/ 135 | pom.xml.tag 136 | pom.xml.releaseBackup 137 | pom.xml.versionsBackup 138 | pom.xml.next 139 | pom.xml.bak 140 | release.properties 141 | dependency-reduced-pom.xml 142 | buildNumber.properties 143 | .mvn/timing.properties 144 | .mvn/wrapper/maven-wrapper.jar 145 | !**/src/main/** 146 | !**/src/test/** 147 | *.versionsBackup 148 | 149 | ## Java 150 | .mtj.tmp/ 151 | *.class 152 | *.jar 153 | *.war 154 | *.ear 155 | *.nar 156 | hs_err_pid* 157 | 158 | ## Golang 159 | *.exe 160 | *.exe~ 161 | *.dll 162 | *.so 163 | *.dylib 164 | *.test 165 | *.out 166 | vendor/ 167 | go.work 168 | 169 | ## Rust 170 | target/ #same as Maven 171 | debug/ 172 | **/*.rs.bk 173 | *.pdb 174 | Cargo.lock #remove for libs building 175 | 176 | ## TODO 177 | todo 178 | 179 | ## Cache 180 | _cache/* 181 | */_cache/* 182 | 183 | ## Testing Certs 184 | certs/* 185 | **/certs/* 186 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome Note 2 | 3 | Welcome to YouShallNotPass and thank you for contributing your time and expertise to the project. This document describes the contribution guidelines for the project 4 | 5 | * [Setup](#setup) 6 | * [Environment Setup](#environment-setup) 7 | * [Testing Setup](#testing-setup) 8 | * [Contributing Steps](#contributing-steps) 9 | * [Contributing Checks](#contributing-checks) 10 | * [Conventions](#conventions) 11 | * [PR Process](#pr-process) 12 | * [Before creating a PR](#what-to-do-before-submitting-a-pull-request) 13 | * [Adding New Checks](#adding-new-checks) 14 | 15 | 16 | ## Setup 17 | 18 | 19 | ### Environment Setup 20 | 21 | You must install these tools: 22 | 23 | 1. [`git`](https://help.github.com/articles/set-up-git/): For source control 24 | 2. [`go`](https://golang.org/doc/install): You need go version 25 | [v1.20](https://golang.org/dl/) or higher. 26 | 3. [`docker`](https://docs.docker.com/engine/install/): `v18.9` or higher. 27 | 28 | 29 | ### Testing Setup 30 | 31 | A testing file [testing/test.sh](testing/test.sh) is used make the running of tests easier. 32 | 33 | For unit testing run: 34 | ```sh 35 | testing/test.sh unit 36 | ``` 37 | 38 | Integration testing sets up set of docker images to perform testing on. 39 | 40 | To build the docker images run: 41 | ```sh 42 | testing/test.sh integration build 43 | ``` 44 | 45 | To run the integration tests run: 46 | ```sh 47 | testing/test.sh integration 48 | ``` 49 | 50 | 51 | ## Contributing Steps 52 | 53 | 1. Submit an issue describing your proposed change to the repo. 54 | 2. For the repo, develop, and test your code changes 55 | 3. Submit a pull request 56 | 57 | 58 | ## Contributing Checks 59 | 60 | To Contribute Checks to YouShallNotPass follow the following steps: 61 | 62 | 1. Create an issue describing the new check to be added (with the enhancement label) 63 | 2. Create a Fork of the YouShallNotPass repository 64 | 3. Create a new module in pkg/checks with the name of the check to be created 65 | - Please name the new module the check name in lowercase with no separations. 66 | - Example, Script Hash Check -> pkg/checks/scripthash. 67 | 4. Create a struct and initializer to pass in the check configuration and other information (besides the whitelist) that is necessary for the check's completion 68 | - Example, the Script Hash Check -> `func NewScriptHashCheck(config config.CheckConfig, jobName string, scriptLines []string) ScriptHashCheck`. 69 | 5. Implement the IsValidForCheckType function from the Check interface given [here](pkg/checks/checks.go) 70 | - This function should return true if the checkType is valid for the given check (valid checkTypes are ImageCheck, ScriptCheck, and All). 71 | - Example, the Script Hash Check is only valid for Script Checks and All Checks -> returns true when `checkType == ScriptCheck` OR `checkType == All`. 72 | 6. Implement the IsValidForPlatform function from the Check interface given [here](pkg/checks/checks.go) 73 | - This function should return true if the CI Platform is valid for a given check (valid ciPlatforms are the lowercase of a given platform - i.e. gitlab, github, ...). 74 | - For example, a Gitlab Linter Check would only be valid for the "gitlab" platform -> return true if `ciPlatform == "gitlab"` 75 | 7. Implement the Check function from the Check interface given [here](pkg/checks/checks.go) 76 | - This is where the magic of your check happens. In this function all of the necessary logic that must occur to check whether or not your check has been passed happens. 77 | - All checks are given access to the whitelist in addition to any information they have stored as a part of their initializer. 78 | - To make things slightly easier every Check implementation should start with `defer wg.Done()`. This lets the wait group the check is part of know that the check has completed its necessary tasks and the main thread can read the results of each check. 79 | - After a check has come to a conclusion as to the success of its execution it will create a [checkResult](pkg/checks/checks.go) and send it through the channel provided through the function. This CheckResult is then read by the main thread after every check is completed to create a "scorecard" for the YouShallNotPass execution. 80 | 8. Create a second file title `{checkname}_test.go` and create a series of unit tests to ensure that the check you created does exactly what you expect it to do. 81 | 9. Add the check to the [checkparser](pkg/checkparser/checkparser.go) and [checkparser unit tests](pkg/checkparser/checkparser_test.go). 82 | 10. Add the new check to the [unit testing bash script](testing/unit/test.sh) 83 | - more or less this is basically running go test with the path to the new check `go test "${currentDir}/../../pkg/checks/newcheckname"` 84 | 11. Add the check (and configuration options) to the [README](README.md#project-configuration-options) 85 | 12. Create a PR documenting the new check and linking it back to the Issue created (or found) in step 1. 86 | 87 | 88 | ## Conventions 89 | 90 | * modules should be all lowercase characters 91 | * variables should be named using mixedCase 92 | 93 | 94 | ## PR Process 95 | 96 | Every PR should be annotated with an icon indicating whether it's a: 97 | * Breaking change: (:warning:) 98 | * Non-breaking feature: (:sparkles:) 99 | * Patch fix: (:bug:) 100 | * Docs: (:book:) 101 | * Tests/Other: (:seedling:) 102 | * No release note: (:ghost:) 103 | 104 | Only the final PR needs to be marked with its icon. 105 | 106 | 107 | ## What to do before submitting a Pull Request 108 | 109 | The following tests can be run to test your local changes. 110 | 111 | | Command | Description | Is called in the CI? | 112 | | -------- | -------------------------------------------------- | -------------------- | 113 | | testing/test.sh unit | Runs go test | yes | 114 | | testing/test.sh integration | Runs integration tests | partialy | 115 | 116 | 117 | ## Adding New Tests 118 | 119 | When adding new tests please follow the guidelines in [TESTING.md](TESTING.md) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20@sha256:5865f52f9f277b951610d2ab0b7a14b24cadef7709db26de3320c018fbd4550c AS builder 2 | 3 | ARG GOPROXY 4 | 5 | WORKDIR /app 6 | COPY . . 7 | RUN go mod download && \ 8 | go mod verity && \ 9 | CGO_ENABLED=0 go build -o main \ 10 | . 11 | 12 | FROM gitlab/gitlab-runner:alpine3.18 13 | 14 | ARG CI_COMMIT_REF_NAME="development" 15 | 16 | ENV RUNNER_BUILDS_DIR="/tmp/builds" \ 17 | RUNNER_CACHE_DIR="/tmp/cache" \ 18 | CUSTOM_CONFIG_EXEC="/var/custom-executor/config.sh" \ 19 | CUSTOM_PREPARE_EXEC="/var/custom-executor/prepare.sh" \ 20 | CUSTOM_RUN_EXEC="/var/custom-executor/run.sh" \ 21 | CUSTOM_CLEANUP_EXEC="/var/custom-executor/cleanup.sh" \ 22 | CUSTOM_CONFIG_EXEC_TIMEOUT=200 \ 23 | CUSTOM_PREPARE_EXEC_TIMEOUT=200 \ 24 | CUSTOM_CLEANUP_EXEC_TIMEOUT=200 \ 25 | CUSTOM_GRACEFUL_KILL_TIMEOUT=200 \ 26 | CUSTOM_FORCE_KILL_TIMEOUT=200 27 | 28 | RUN apk add --no-cache docker-cli jq openssl && rm -rf /var/cache/apk/* 29 | RUN mkdir -p /var/custom-executor 30 | 31 | COPY --from=builder /app/main /usr/local/bin/youshallnotpass 32 | COPY custom_executors/gitlab_custom_executor/base.sh /var/custom-executor/base.sh 33 | COPY custom_executors/gitlab_custom_executor/cleanup.sh /var/custom-executor/cleanup.sh 34 | COPY custom_executors/gitlab_custom_executor/config.sh /var/custom-executor/config.sh 35 | COPY custom_executors/gitlab_custom_executor/profile.sh /var/custom-executor/profile.sh 36 | COPY custom_executors/gitlab_custom_executor/prepare.sh /var/custom-executor/prepare.sh 37 | COPY custom_executors/gitlab_custom_executor/run.sh /var/custom-executor/run.sh 38 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Kudelski Security 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software for non-commercial research and internal development purposes, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sublicense the Software for such purposes, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | For any use unrelated to non-commercial research and internal development of the Software, a separate permission must be obtained from the Copyright holder. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | 4 | ## Unit Tests 5 | 6 | 7 | ### Go Unit Testing 8 | 9 | For each file in the Go project, there is (usually) a second file with the name {module}_test.go. These files contain all of the unit tests for their respective main file. 10 | 11 | #### Running Unit Tests 12 | 13 | To run the all of the unit tests, run testing/test.sh unit. This triggers the bash script found at [testing/unit/test.sh](testing/unit/test.sh) to run `go test` on each of the packages individually. To run a specific package's unit tests run testing/test.sh unit {package_name}. This will trigger the [testing/unit/test.sh](testing/unit/test.sh) script to run `go test` for the specific package 14 | 15 | #### Notes About Unit Tests 16 | 17 | The main reason the bash script is used to trigger unit tests is to not trigger the client integration tests when running `go test`. Because these tests are also of the form {package}_test.go, they would also be covered if go test was used to systematically test the entire project. 18 | 19 | #### Contributing Unit Tests 20 | 21 | For any packages and checks added to the go codebase, a corresponding file should be created following the unit testing file format. This file should contain a set of test cases to test the various functionalities of the Go file/package. 22 | 23 | ## Integration Tests 24 | 25 | ### Types of Integration Tests 26 | 27 | #### Client Integration Tests 28 | 29 | Client integration tests are basically an extra check that the clients, used to contact external services, have the capabilities used by YouShallNotPass. 30 | 31 | 32 | ##### Existing Client Integration Tests 33 | 34 | Currently, the following Client Integration tests exist: 35 | 36 | 1. Mattermost Client Integration Tests 37 | - The Mattermost Client Integration Tests make sure that the Mattermost client has the capabilities of writing messages to a given Mattermost channel on a test Mattermost Instance (hosted on a docker container). 38 | 2. Hashicorp Vault Client Integration Tests 39 | - The Hashicorp Vault Client Integration Tests make sure that the Hashicorp client can read the YouShallNotPass configuration and whitelist files from a test Hashicorp Vault Instance (hosted on a docker container). 40 | 41 | 42 | #### Client-Platform-Case Integration (e2e) Tests 43 | 44 | Client-Platform-Case Integration (e2e) Tests are basically a set of tests that test YouShallNotPass's performance given a specific vault client (i.e. Hashicorp, ...) paired with a specific CI Platform (i.e. GitHub, GitLab...) given a specific situation. For example, the hashicorp_gitlab_auth_timeout test uses the Hashicorp Vault Client (Client) and the GitLab CI Platform (Platform) to check that the desired behavior happens when YouShallNotPass authentication timeout is reached (Case). 45 | 46 | 47 | ##### Existing Client-Platform-Case Integration Tests 48 | 49 | Currently, the following Client-Platform-Case Integration Tests exist: 50 | 51 | 1. Hashicorp Gitlab Auth Timeout Test 52 | - The Hashicorp GitLab Auth Timeout Test tests that when YouShallNotPass times out the GitLab executor should not execute the CI/CD Job. 53 | 2. Hashicorp GitLab Automatic Test 54 | - The Hashicorp GitLab Automatic Test tests that when YouShallNotPass's checks all pass the GitLab runner should execute the CI/CD Job automatically. 55 | 3. Hashicorp GitLab Bash Test 56 | - The Hashicorp GitLab Bash Test tests that when a bash script is supplied as part of the GitLab CI/CD Job, YouShallNotPass should automatically expand the bash script before executing script checks. 57 | 4. Hashicorp GitLab Fail Test 58 | - The Hashicorp GitLab Fail Test tests that when checks with abortOnFail=true fail YouShallNotPass should prevent the GitLab CI/CD Job from being run. 59 | 5. Hashicorp GitLab Mfa Test 60 | - The Hashicorp GitLab MFA Test test that when checks with mfaOnFail=true fail YouShallNotPass should require user multi-factor authentication to succeed before GitLab may run the CI/CD Job. 61 | 62 | 63 | ##### How Client-Platform-Case Integration Tests Work 64 | 65 | Most of the Client-Platform-Case Integration Tests work with a set of docker-compose files (currently organized into runner-compose.yml for the YouShallNotPass Runner/Executor and vault-compose.yml for the vault setup). 66 | 67 | In general, the following steps are performed to complete a Client-Platform-Case Integration Test: 68 | 69 | 1. Docker-Compose spins up a Hashicorp vault instance 70 | 2. The vault instance is initialized by the vault_init service which creates a youshallnotpass-demo role in the vault. 71 | - This role has access to the necessary youshallnotpass whitelist, config, and scratch vault locations. 72 | 3. The vault_init container writes a set of whitelist images and scripts as well as configuration to the vault instance 73 | 4. When the vault_init container is done initializing the vault instance, it exits, which is used to spin up the runner-compose.yml runner into the same docker network. 74 | - The runner-compose.yml file contains the following services: 75 | - youshallnotpass_builder_daemon: a daemon that continually rebuilds the youshallnotpass binary so the docker image doesn't have to be continually rebuilt. 76 | - git_repo_init: an alpine image that creates a git repository in the git_repo volume. 77 | - gitlab_runner: the gitlab runner that will use the youshallnotpass custom executor to execute the jobs in the git_repo volume. 78 | 5. The gitlab_runner service uses the custom executor to run a specific job from the git_repo volume. 79 | 6. When the gitlab_runner is finished executing the job it will return a status code of 0 on success and 1 on failure. This exit code is then verified to determine whether or not the expected behavior was completed by the YouShallNotPass executor. 80 | 81 | 82 | ##### Contributing Client-Platform-Case Integration Tests 83 | 84 | To contribute a Client-Platform-Case Integration Test, the current format for a runner-compose.yml and vault-compose.yml file does not necessarily have to followed if a better workflow is discovered, however it is necessary (for a bit of uniformity) to add the test to [testing/integration/test.sh](testing/integration/test.sh). In this file tests are run based on their client, platform, and case keywords. For example, all Hashicorp client tests should be run when testing/test.sh integration Hashicorp is run. Therefore, please add your test in a way that it will only be run when the client, platform, and/or case is present in the argument to the `test.sh` file. 85 | -------------------------------------------------------------------------------- /custom_executors/github_custom_executor/before_script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1091 3 | 4 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 5 | 6 | # Get Runner Configuration Variables 7 | source "${currentDir}/profile.sh" 8 | 9 | function generate_certs() { 10 | echo "Generating Pub/Private keys for JWT tokens" 11 | openssl genrsa -out "${currentDir}/certs/private-key.pem" 3072 12 | openssl rsa -in "${currentDir}/certs/private-key.pem" -pubout -out "${currentDir}/certs/public-key.pem" 13 | echo "0" > "${currentDir}/certs/iteration.txt" 14 | } 15 | 16 | # check that openssl, jq, and yq are installed 17 | if ! type openssl > /dev/null 2>&1 || ! type jq > /dev/null 2>&1 || ! type yq > /dev/null 2>&1; then 18 | package_manager="" 19 | if type brew > /dev/null 2>&1; then 20 | package_manager="brew install" 21 | elif type apk > /dev/null 2>&1; then 22 | package_manager="apk add" 23 | elif type apt-get > /dev/null 2>&1; then 24 | package_manager="apt-get install -y" 25 | elif type dnf > /dev/null 2>&1; then 26 | package_manager="dnf install" 27 | elif type yum > /dev/null 2>&1; then 28 | package_manager="yum install" 29 | else 30 | echo "unknown package manager" 31 | fi 32 | 33 | $package_manager openssl jq yq 34 | fi 35 | 36 | # If This is a New Runner We Should Generate a Public-Private Key for Vault Auth 37 | if [[ ! -d "${currentDir}/certs" ]]; then 38 | mkdir certs 39 | generate_certs 40 | echo "Public/Private Key generated in certs folder, please authenticate with vault before continuing" 41 | exit 1 42 | elif [[ ! -f "${currentDir}/certs/public-key.pem" || ! -f "${currentDir}/certs/private-key.pem" ]]; then 43 | rm -rf certs 44 | mkdir certs 45 | generate_certs 46 | echo "Public/Private Key generated in certs folder, please authenticate with vault before continuing" 47 | exit 1 48 | fi 49 | 50 | # Check the last modified date of the certs 51 | last_modified=$(date -r "${currentDir}/certs/public-key.pem" +%s) 52 | now=$(date +%s) 53 | declare -i last_modified 54 | declare -i now 55 | last_modified+=15780000 # six months in seconds 56 | if [ $last_modified -le $now ]; then 57 | echo "it's about time to rotate your public/private key" 58 | fi 59 | 60 | # Get Necessary Variables from the environment 61 | export CI_PROJECT_PATH="$GITHUB_REPOSITORY" 62 | export CI_PROJECT_NAMESPACE 63 | CI_PROJECT_NAMESPACE=$(echo "$GITHUB_REPOSITORY" | grep -oE "[a-zA-Z0-9_-]*./" | tr -d "/") 64 | export CI_PIPELINE_ID="$GITHUB_RUN_ID" 65 | export CI_JOB_NAME="$GITHUB_JOB" 66 | export CI_USER_EMAIL="$GITHUB_ACTOR" 67 | 68 | # Clone the workflow's repo 69 | # For some reason, the repo is not yet cloned at this stage at the very first run and GITHUB_TOKEN is not available 70 | # Next runs (might?) have the repo locally due to some caching(?) but not the latest version 71 | # sometimes, the repo directory does not contain the .git directory anymore 72 | # sometimes, the git remote -v are erased which breaks for private repos 73 | # TODO: improve me - or improve the github runner itself? https://github.com/actions/runner/issues 74 | # shellcheck disable=SC2086 75 | if [[ ! -d "${GITHUB_WORKSPACE}" || -z "$(ls -A ${GITHUB_WORKSPACE})" ]]; then 76 | # set those variables in profile.sh to git clone a private repo 77 | if [ -n "${GITHUB_USER}" ] && [ -n "${GITHUB_TOKEN}" ]; then 78 | git clone "https://${GITHUB_USER}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}" "${GITHUB_WORKSPACE}" 79 | else 80 | git clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" "${GITHUB_WORKSPACE}" 81 | fi 82 | else 83 | # directory already exists, force update it 84 | cd "${GITHUB_WORKSPACE}" || exit 1 85 | # sometimes, the .git directory does no longer exist... 86 | if [ ! -d ".git" ]; then 87 | cd .. || exit 1 88 | sudo rm -rf "${GITHUB_WORKSPACE}" 89 | if [ -n "${GITHUB_USER}" ] && [ -n "${GITHUB_TOKEN}" ]; then 90 | git clone "https://${GITHUB_USER}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}" "${GITHUB_WORKSPACE}" 91 | else 92 | git clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" "${GITHUB_WORKSPACE}" 93 | fi 94 | else 95 | if [ -n "${GITHUB_USER}" ] && [ -n "${GITHUB_TOKEN}" ]; then 96 | git remote set-url origin "https://${GITHUB_USER}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}" 97 | fi 98 | git fetch --all 99 | # fails sometimes... 100 | # git reset --hard "${GITHUB_REF}" 101 | fi 102 | fi 103 | 104 | # Checkout the current sha 105 | cd "${GITHUB_WORKSPACE}" || exit 1 106 | git config --local advice.detachedHead false 107 | git checkout "${GITHUB_SHA}" 108 | cd "${currentDir}" || exit 1 109 | 110 | # Get the Script to be Run 111 | WORKFLOW_FILE_NAME=$(echo "$GITHUB_WORKFLOW_REF" | grep -oE ".github/workflows/[a-zA-Z0-9_-]*\.yml") 112 | WORKFLOW_FILE_NAME=${WORKFLOW_FILE_NAME#".github/workflows/"} 113 | export RUNNER_SCRIPT 114 | RUNNER_SCRIPT=$(yq eval ".jobs.$GITHUB_JOB.steps" "${GITHUB_WORKSPACE}/.github/workflows/$WORKFLOW_FILE_NAME") 115 | 116 | # Get the Docker Image to Use (only applies on linux systems) 117 | export CI_JOB_IMAGE="" 118 | CI_JOB_IMAGE=$(yq eval ".jobs.$GITHUB_JOB.container.image" "${GITHUB_WORKSPACE}/.github/workflows/$WORKFLOW_FILE_NAME") 119 | if [ "$CI_JOB_IMAGE" == "null" ]; then 120 | CI_JOB_IMAGE=$(yq eval ".jobs.$GITHUB_JOB.container" "${GITHUB_WORKSPACE}/.github/workflows/$WORKFLOW_FILE_NAME") 121 | fi 122 | 123 | # Generate the jwt token 124 | export ITERATION 125 | ITERATION=$(cat "${currentDir}/certs/iteration.txt") 126 | declare -i ITERATION 127 | ITERATION+=1 128 | echo "$ITERATION" > "${currentDir}/certs/iteration.txt" 129 | 130 | export CI_JOB_JWT 131 | CI_JOB_JWT=$("${currentDir}/generate_jwt.sh") 132 | 133 | export YOUSHALLNOTPASS_PREVALIDATION_TOKEN 134 | YOUSHALLNOTPASS_PREVALIDATION_TOKEN=$(dd bs=512 if=/dev/urandom count=1 2>/dev/null | LC_ALL=C tr -dc "a-zA-Z0-9" | head -c 50) 135 | 136 | # Run youshallnotpass 137 | if [[ "${CI_JOB_IMAGE}" == "null" ]]; then 138 | "${currentDir}/youshallnotpass" validate --check-type="script" --ci-platform="github" || exit 1 139 | else 140 | "${currentDir}/youshallnotpass" validate --check-type="all" --ci-platform="github" || exit 1 141 | fi 142 | -------------------------------------------------------------------------------- /custom_executors/github_custom_executor/generate_jwt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" 4 | 5 | set -o pipefail 6 | 7 | header_template='{ 8 | "type": "JWT", 9 | "kid": "0001", 10 | "iss": "github.com" 11 | }' 12 | 13 | payload_template='{ 14 | "namespace_id": '"\"${GITHUB_REPOSITORY_OWNER_ID}\""', 15 | "namespace_path": '"\"${CI_PROJECT_NAMESPACE}\""', 16 | "project_id": '"\"${GITHUB_REPOSITORY_ID}\""', 17 | "project_path": '"\"${CI_PROJECT_PATH}\""', 18 | "user_id": '"\"${GITHUB_ACTOR_ID}\""', 19 | "user_login": '"\"${GITHUB_TRIGGERING_ACTOR}\""', 20 | "pipeline_id": '"\"${GITHUB_RUN_ID}\""', 21 | "job_id": '"\"${GITHUB_RUN_ID}\""', 22 | "ref": '"\"${GITHUB_REF_NAME}\""', 23 | "ref_type": '"\"${GITHUB_REF_TYPE}\""', 24 | "ref_protected": '"\"${GITHUB_REF_PROTECTED}\""', 25 | "jti": '"\"${ITERATION}\""' 26 | }' 27 | 28 | build_header() { 29 | jq -c \ 30 | --arg iat_str "$(date +%s)" \ 31 | --arg alg "${1:-HS256}" \ 32 | ' 33 | ($iat_str | tonumber) as $iat 34 | | .alg = $alg 35 | | .iat = $iat 36 | | .exp = ($iat + 180) 37 | ' <<<"$header_template" | tr -d '\n' 38 | } 39 | 40 | build_payload() { 41 | jq -c \ 42 | --arg iat_str "$(date +%s)" \ 43 | --arg project_path "$CI_PROJECT_PATH" \ 44 | --arg namespace_path "$CI_PROJECT_NAMESPACE" \ 45 | ' 46 | ($iat_str | tonumber) as $iat 47 | | .iat = $iat 48 | | .exp = ($iat + 180) 49 | | .project_path = $project_path 50 | | .namespace_path = $namespace_path 51 | ' <<<"$payload_template" | tr -d '\n' 52 | } 53 | 54 | b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; } 55 | json() { jq -c . | LC_CTYPE=C tr -d '\n'; } 56 | hs_sign() { openssl dgst -binary -sha"${1}" -hmac "$2"; } 57 | rs_sign() { openssl dgst -binary -sha"${1}" -sign <(printf '%s\n' "$2"); } 58 | 59 | sign() { 60 | local algo payload header sig secret=$3 61 | algo=${1:-RS256}; algo=$(echo "$algo" | tr '[:lower:]' '[:upper:]') 62 | header=$(build_header "$algo") || return 63 | payload=${2:-$test_payload} 64 | signed_content="$(json <<<"$header" | b64enc).$(json <<<"$payload" | b64enc)" 65 | case $algo in 66 | HS*) sig=$(printf %s "$signed_content" | hs_sign "${algo#HS}" "$secret" | b64enc) ;; 67 | RS*) sig=$(printf %s "$signed_content" | rs_sign "${algo#RS}" "$secret" | b64enc) ;; 68 | *) echo "Unknown algorithm" >&2; return 1 ;; 69 | esac 70 | printf '%s.%s\n' "${signed_content}" "${sig}" 71 | } 72 | 73 | rsa_secret=$(cat "${currentDir}/certs/private-key.pem") 74 | 75 | payload=$(build_payload) 76 | jwt_key=$(sign rs256 "$payload" "$rsa_secret") 77 | 78 | echo "$jwt_key" -------------------------------------------------------------------------------- /custom_executors/github_custom_executor/profile.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=SC2034 2 | export YOUSHALLNOTPASS_VAULT_ROOT="cicd" 3 | export VAULT_ROLE="youshallnotpass-github-poc" 4 | export VAULT_LOGIN_PATH="auth/jwt/github.com/login" 5 | export VAULT_ADDR="http://127.0.0.1:8200" 6 | export VAULT_EXTERNAL_ADDR="http://127.0.0.1:8200" 7 | 8 | # for private repos, set those variables 9 | # export GITHUB_USER="your_github_user" 10 | # export GITHUB_TOKEN="your_github_token" -------------------------------------------------------------------------------- /custom_executors/gitlab_custom_executor/base.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=SC2034 2 | 3 | # Variables defined here will be availble to all the scripts. 4 | 5 | CONTAINER_ID="runner-$CUSTOM_ENV_CI_RUNNER_ID-project-$CUSTOM_ENV_CI_PROJECT_ID-concurrent-$CUSTOM_ENV_CI_CONCURRENT_PROJECT_ID-$CUSTOM_ENV_CI_JOB_ID" 6 | 7 | CACHE_DIR="$(dirname "${BASH_SOURCE[0]}")/_cache/runner-$CUSTOM_ENV_CI_RUNNER_ID-project-$CUSTOM_ENV_CI_PROJECT_ID-concurrent-$CUSTOM_ENV_CI_CONCURRENT_PROJECT_ID" -------------------------------------------------------------------------------- /custom_executors/gitlab_custom_executor/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1091 3 | 4 | TMPDIR=$(pwd) 5 | 6 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 7 | 8 | source "${currentDir}/base.sh" 9 | 10 | echo "Deleting container $CONTAINER_ID" 11 | 12 | docker kill "$CONTAINER_ID" 2>/dev/null || true 13 | docker rm "$CONTAINER_ID" 2>/dev/null || true 14 | 15 | # Delete leftover files in /tmp 16 | rm -r "$TMPDIR" 17 | 18 | exit 0 19 | -------------------------------------------------------------------------------- /custom_executors/gitlab_custom_executor/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 4 | 5 | YOUSHALLNOTPASS_PREVALIDATION_TOKEN=$(LC_ALL=C tr -dc '[:alnum:]' < /dev/urandom | head -c50) 6 | 7 | if [ "$YOUSHALLNOTPASS_GENERATE_JWT" == true ] && [ -z "${CI_JOB_JWT}" ] && [ -z "${CUSTOM_ENV_CI_JOB_JWT}" ]; then 8 | CUSTOM_ENV_CI_JOB_JWT=$(CI_PROJECT_PATH=$CI_PROJECT_PATH CI_NAMESPACE_PATH=$CI_NAMESPACE_PATH "${currentDir}/generate_jwt.sh") 9 | fi 10 | 11 | cat << EOS 12 | { 13 | "driver": { 14 | "name": "youshallnotpass", 15 | "version": "${RUNNER_VERSION}" 16 | }, 17 | "job_env" : { 18 | "YOUSHALLNOTPASS_PREVALIDATION_TOKEN": "${YOUSHALLNOTPASS_PREVALIDATION_TOKEN}", 19 | "CUSTOM_ENV_CI_JOB_JWT": "${CUSTOM_ENV_CI_JOB_JWT}" 20 | } 21 | } 22 | EOS -------------------------------------------------------------------------------- /custom_executors/gitlab_custom_executor/generate_jwt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Inspired by implementation by Will Haley at: 4 | # http://willhaley.com/blog/generate-jwt-with-bash/ 5 | # https://stackoverflow.com/questions/46657001/how-do-you-create-an-rs256-jwt-assertion-with-bash-shell-scripting/46672439#46672439 6 | 7 | set -o pipefail 8 | 9 | # Shared content to use as template 10 | header_template='{ 11 | "typ": "JWT", 12 | "kid": "0001", 13 | "iss": "gitlab.example.com" 14 | }' 15 | 16 | payload_template='{ 17 | "namespace_id": "1234", 18 | "namespace_path": "", 19 | "project_id": "1234", 20 | "project_path": "", 21 | "user_id": "123", 22 | "user_login": "user", 23 | "user_email": "test.user@example.com", 24 | "pipeline_id": "1234567", 25 | "job_id": "123456789", 26 | "ref": "master", 27 | "ref_type": "branch", 28 | "ref_protected": "true", 29 | "jti": "" 30 | }' 31 | 32 | build_header() { 33 | jq -c \ 34 | --arg iat_str "$(date +%s)" \ 35 | --arg alg "${1:-HS256}" \ 36 | ' 37 | ($iat_str | tonumber) as $iat 38 | | .alg = $alg 39 | | .iat = $iat 40 | | .exp = ($iat + 1) 41 | ' <<<"$header_template" | tr -d '\n' 42 | } 43 | 44 | build_payload() { 45 | jq -c \ 46 | --arg iat_str "$(date +%s)" \ 47 | --arg project_path "$CI_PROJECT_PATH" \ 48 | --arg namespace_path "$CI_PROJECT_NAMESPACE" \ 49 | ' 50 | ($iat_str | tonumber) as $iat 51 | | .iat = $iat 52 | | .exp = ($iat + 5) 53 | | .project_path = $project_path 54 | | .namespace_path = $namespace_path 55 | ' <<<"$payload_template" | tr -d '\n' 56 | } 57 | 58 | b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; } 59 | json() { jq -c . | LC_CTYPE=C tr -d '\n'; } 60 | hs_sign() { openssl dgst -binary -sha"${1}" -hmac "$2"; } 61 | rs_sign() { openssl dgst -binary -sha"${1}" -sign <(printf '%s\n' "$2"); } 62 | 63 | sign() { 64 | local algo payload header sig secret=$3 65 | algo=${1:-RS256}; algo=${algo^^} 66 | header=$(build_header "$algo") || return 67 | payload=${2:-$test_payload} 68 | signed_content="$(json <<<"$header" | b64enc).$(json <<<"$payload" | b64enc)" 69 | case $algo in 70 | HS*) sig=$(printf %s "$signed_content" | hs_sign "${algo#HS}" "$secret" | b64enc) ;; 71 | RS*) sig=$(printf %s "$signed_content" | rs_sign "${algo#RS}" "$secret" | b64enc) ;; 72 | *) echo "Unknown algorithm" >&2; return 1 ;; 73 | esac 74 | printf '%s.%s\n' "${signed_content}" "${sig}" 75 | } 76 | 77 | rsa_secret=$(cat /certs/private-key.pem) 78 | 79 | payload=$(build_payload) 80 | jwt_key=$(sign rs256 "$payload" "$rsa_secret") 81 | 82 | echo "$jwt_key" -------------------------------------------------------------------------------- /custom_executors/gitlab_custom_executor/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1091 3 | # shellcheck disable=SC2206 4 | 5 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | 7 | source "${currentDir}/base.sh" 8 | 9 | source "${currentDir}/profile.sh" 10 | 11 | set -eEo pipefail 12 | 13 | # trap any error, and mark it as a system failure. 14 | trap 'exit $SYSTEM_FAILURE_EXIT_CODE' ERR 15 | 16 | # Check only the image whitelist on preparation 17 | if [[ -f /usr/local/bin/youshallnotpass ]]; then 18 | /usr/local/bin/youshallnotpass validate --check-type="image" --ci-platform="gitlab" || exit "$SYSTEM_FAILURE_EXIT_CODE" 19 | elif [[ -f /${currentDir}/youshallnotpass ]]; then 20 | "${currentDir}/youshallnotpass" validate --check-type="image" --ci-platform="gitlab" || exit "$SYSTEM_FAILURE_EXIT_CODE" 21 | else 22 | echo "Could Not Find YouShallNotPass Binary" 23 | exit "$SYSTEM_FAILURE_EXIT_CODE" 24 | fi 25 | 26 | wait_for_docker() { 27 | n=0 28 | until [ "$n" -ge 5 ] 29 | do 30 | docker stats --no-stream >/dev/null && break 31 | n=$((n+1)) 32 | sleep 1 33 | done 34 | } 35 | 36 | is_logged_in() { 37 | jq -r --arg url "${CUSTOM_ENV_CI_REGISTRY}" '.auths | has($url)' < "$CACHE_DIR/_authfile_$CONTAINER_ID/config.json" 38 | } 39 | 40 | start_container() { 41 | if docker inspect "$CONTAINER_ID" >/dev/null 2>&1; then 42 | echo 'Found old container, deleting' 43 | docker kill "$CONTAINER_ID" 44 | docker rm "$CONTAINER_ID" 45 | fi 46 | 47 | mkdir -p "$CACHE_DIR/_authfile_$CONTAINER_ID" 48 | 49 | # Use value of ENV variable or {} as empty settings 50 | echo "${CUSTOM_ENV_DOCKER_AUTH_CONFIG:-{\}}" | jq -r > "$CACHE_DIR/_authfile_$CONTAINER_ID/config.json" 51 | 52 | # Try logging into the Gitlab Registry if credentials are provided 53 | # https://docs.gitlab.com/ee/user/packages/container_registry/index.html#authenticate-by-using-gitlab-cicd 54 | if [[ "$(is_logged_in)" == "false" ]] && [[ -n "$CUSTOM_ENV_CI_DEPLOY_USER" && -n "$CUSTOM_ENV_CI_DEPLOY_PASSWORD" ]] 55 | then 56 | echo "Login to ${CUSTOM_ENV_CI_REGISTRY} with CI_DEPLOY_USER" 57 | echo "$CUSTOM_ENV_CI_DEPLOY_PASSWORD" | docker --config "$CACHE_DIR/_authfile_$CONTAINER_ID" login \ 58 | --username "$CUSTOM_ENV_CI_DEPLOY_USER" \ 59 | --password-stdin \ 60 | "$CUSTOM_ENV_CI_REGISTRY" 2>/dev/null 61 | fi 62 | 63 | if [[ "$(is_logged_in)" == "false" ]] && [[ -n "$CUSTOM_ENV_CI_JOB_USER" && -n "$CUSTOM_ENV_CI_JOB_TOKEN" ]] 64 | then 65 | echo "Login to ${CUSTOM_ENV_CI_REGISTRY} with CI_JOB_USER" 66 | echo "$CUSTOM_ENV_CI_JOB_TOKEN" | docker --config "$CACHE_DIR/_authfile_$CONTAINER_ID" login \ 67 | --username "$CUSTOM_ENV_CI_JOB_USER" \ 68 | --password-stdin \ 69 | "$CUSTOM_ENV_CI_REGISTRY" 2>/dev/null 70 | fi 71 | 72 | if [[ "$(is_logged_in)" == "false" ]] && [[ -n "$CUSTOM_ENV_CI_REGISTRY_USER" && -n "$CUSTOM_ENV_CI_REGISTRY_PASSWORD" ]] 73 | then 74 | echo "Login to ${CUSTOM_ENV_CI_REGISTRY} with CI_REGISTRY_USER" 75 | echo "$CUSTOM_ENV_CI_REGISTRY_PASSWORD" | docker --config "$CACHE_DIR/_authfile_$CONTAINER_ID" login \ 76 | --username "$CUSTOM_ENV_CI_REGISTRY_USER" \ 77 | --password-stdin \ 78 | "$CUSTOM_ENV_CI_REGISTRY" 2>/dev/null 79 | fi 80 | 81 | # merge default docker config with user-suplied configuration 82 | if test -f "/root/.docker/config.json"; then 83 | #cat /root/.docker/config.json "$CACHE_DIR/_authfile_$CONTAINER_ID/config.json" | jq -s '.[0] * .[1]' > "$CACHE_DIR/_authfile_$CONTAINER_ID/config.json" -r 84 | DOCKER_CONFIG=$(cat "$CACHE_DIR/_authfile_$CONTAINER_ID/config.json" /root/.docker/config.json | jq -sr '.[0] * .[1]') 85 | echo "$DOCKER_CONFIG" | jq -r > "$CACHE_DIR/_authfile_$CONTAINER_ID/config.json" 86 | fi 87 | 88 | docker --config="$CACHE_DIR/_authfile_$CONTAINER_ID" pull "$CUSTOM_ENV_CI_JOB_IMAGE" 89 | 90 | rm -rf "$CACHE_DIR/_authfile_$CONTAINER_ID" 91 | 92 | DOCKER_RUN_ARGS_LIST=( $DOCKER_RUN_ARGS ) 93 | 94 | docker run \ 95 | --detach \ 96 | --interactive \ 97 | --entrypoint="" \ 98 | --tty \ 99 | --name "$CONTAINER_ID" \ 100 | --volume "$CACHE_DIR:/home/user/cache":Z \ 101 | "${DOCKER_RUN_ARGS_LIST[@]}" \ 102 | "$CUSTOM_ENV_CI_JOB_IMAGE" \ 103 | sleep 999999999 104 | } 105 | 106 | install_dependencies() { 107 | # Copy gitlab-runner binary from the server into the container 108 | docker cp "$(which gitlab-runner)" "$CONTAINER_ID":/usr/bin/gitlab-runner 109 | 110 | # Install bash in systems with APK (e.g., Alpine) 111 | docker exec "$CONTAINER_ID" sh -c 'if ! type bash >/dev/null 2>&1 && type apk >/dev/null 2>&1 ; then echo "APK based distro without bash"; apk add bash; fi' 112 | 113 | # Install git in systems with APT (e.g., Debian) 114 | docker exec "$CONTAINER_ID" /bin/bash -c 'if ! type git >/dev/null 2>&1 && type apt-get >/dev/null 2>&1 ; then echo "APT based distro without git"; apt-get update && apt-get install --no-install-recommends -y ca-certificates git; fi' 115 | # Install git in systems with DNF (e.g., Fedora) 116 | docker exec "$CONTAINER_ID" /bin/bash -c 'if ! type git >/dev/null 2>&1 && type dnf >/dev/null 2>&1 ; then echo "DNF based distro without git"; dnf install --setopt=install_weak_deps=False --assumeyes git; fi' 117 | # Install git in systems with APK (e.g., Alpine) 118 | docker exec "$CONTAINER_ID" /bin/bash -c 'if ! type git >/dev/null 2>&1 && type apk >/dev/null 2>&1 ; then echo "APK based distro without git"; apk add git; fi' 119 | # Install git in systems with YUM (e.g., RHEL<=7) 120 | docker exec "$CONTAINER_ID" /bin/bash -c 'if ! type git >/dev/null 2>&1 && type yum >/dev/null 2>&1 ; then echo "YUM based distro without git"; yum install --assumeyes git; fi' 121 | } 122 | 123 | echo "Running in $CONTAINER_ID" 124 | 125 | wait_for_docker 126 | start_container 127 | install_dependencies -------------------------------------------------------------------------------- /custom_executors/gitlab_custom_executor/profile.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=SC2034 2 | export YOUSHALLNOTPASS_VAULT_ROOT="cicd" 3 | export VAULT_ROLE="youshallnotpass-demo" 4 | export VAULT_LOGIN_PATH="auth/jwt/gitlab.example.com/login" 5 | export VAULT_ADDR="http://vault:8200" 6 | export VAULT_EXTERNAL_ADDR="http://127.0.0.1:8200" -------------------------------------------------------------------------------- /custom_executors/gitlab_custom_executor/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1091 3 | # shellcheck disable=SC2129 4 | 5 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | 7 | source "${currentDir}/base.sh" 8 | 9 | source "${currentDir}/profile.sh" 10 | 11 | STEPS_WITH_NO_MFA=(prepare_script get_sources restore_cache download_artifacts archive_cache archive_cache_on_failure upload_artifacts_on_success upload_artifacts_on_failure cleanup_file_variables) 12 | 13 | MFA_REQUIRED=true 14 | 15 | for i in "${STEPS_WITH_NO_MFA[@]}" 16 | do 17 | if [ "$i" == "$2" ] ; then 18 | MFA_REQUIRED=false 19 | fi 20 | done 21 | 22 | GREEN="\x1b[32;1m" 23 | [[ $(grep -Fq "$GREEN" < "$1"; echo $?) == 0 ]] && CONTAINS_SCRIPT=true || CONTAINS_SCRIPT=false 24 | 25 | # if user has already MFA'd let them execute a shell 26 | if [ "$MFA_REQUIRED" == true ] && [ "$CONTAINS_SCRIPT" == true ]; then 27 | # As of writing this, the script began when the green text was, therefore this searches for the first 28 | # green text occurance and grabs that to the end of file. 29 | export RUNNER_SCRIPT 30 | RUNNER_SCRIPT=$(grep -oE "x1b.32;1m.*" < "$1") 31 | # shellcheck disable=SC1003 32 | RUNNER_SCRIPT=$(echo "$RUNNER_SCRIPT" | tr -d '\\') 33 | 34 | # Find the scripts 35 | SCRIPTS=$(echo "$RUNNER_SCRIPT" | grep -oE "(./|/|[a-zA-Z0-9_-])([a-zA-Z0-9_-]*/){0,}.{0,1}[a-zA-Z0-9_-]*\.(sh|py|ps1|rb|js)") 36 | 37 | # Expand the scripts 38 | for script in $SCRIPTS; do 39 | script=$(echo "$script" | tr -d '[:space:]') 40 | if [[ -f $script ]]; then 41 | ADD_SCRIPT='x1b[32;1m' 42 | ADD_SCRIPT="${ADD_SCRIPT}${script}" 43 | ADD_SCRIPT="${ADD_SCRIPT}"'x1b[0;m' 44 | ADD_SCRIPT="${ADD_SCRIPT}"'x1b[32;1m'$(tr '\n' ';' < "${script}" )'x1b[0;m' 45 | RUNNER_SCRIPT="${RUNNER_SCRIPT}${ADD_SCRIPT}" 46 | fi 47 | done 48 | 49 | # As of writing this, the runner script gives the CI_JOB_NAME environment variable right before 50 | # CI_JOB_STAGE. 51 | export CI_JOB_NAME 52 | CI_JOB_NAME=$(grep -oE "CI_JOB_NAME=.{0,5}[a-zA-Z0-9_-]*" < "$1") 53 | CI_JOB_NAME=$(echo "$CI_JOB_NAME" | grep -oE "=.*" | grep -oE "[a-zA-Z0-9_-]*") 54 | 55 | # As of writing this, the runner script gives the CI_JOB_STATUS environment variable right before 56 | # the CI={true/false} variable 57 | export CI_JOB_STATUS 58 | CI_JOB_STATUS=$(grep -oE "CI_JOB_STATUS=.{0,5}[a-zA-Z0-9_-]*" < "$1") 59 | CI_JOB_STATUS=$(echo "$CI_JOB_STATUS" | grep -oE "=.*" | grep -oE "[a-zA-Z0-9_-]*") 60 | 61 | # GitLab has support for before_scripts, scripts and after_scripts. The before_scripts and scripts 62 | # Get combined in the file, however the after_scripts seem to be send with the environment variable 63 | # CI_JOB_STATUS=success, so their related job name will be {CI_JOB_NAME}-after. 64 | if [ "$CI_JOB_STATUS" == "success" ]; then 65 | CI_JOB_NAME="$CI_JOB_NAME-after" 66 | fi 67 | 68 | if [[ -f /usr/local/bin/youshallnotpass ]]; then 69 | /usr/local/bin/youshallnotpass validate --check-type="script" --ci-platform="gitlab" || exit "$BUILD_FAILURE_EXIT_CODE" 70 | elif [[ -f ${currentDir}/youshallnotpass ]]; then 71 | "${currentDir}/youshallnotpass" validate --check-type="script" --ci-platform="gitlab" || exit "$BUILD_FAILURE_EXIT_CODE" 72 | else 73 | echo "Could Not Find YouShallNotPass Binary" 74 | exit "$BUILD_FAILURE_EXIT_CODE" 75 | fi 76 | fi 77 | 78 | if ! docker exec -i "$CONTAINER_ID" /bin/bash < "$1" 79 | then 80 | # Exit using the variable, to make the build as failure in GitLab CI. 81 | exit "$BUILD_FAILURE_EXIT_CODE" 82 | fi -------------------------------------------------------------------------------- /docs/img/automated_use_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudelskisecurity/youshallnotpass/4ceb49a7bc621cefa510eed2fb053ec74ffc3c90/docs/img/automated_use_case.png -------------------------------------------------------------------------------- /docs/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudelskisecurity/youshallnotpass/4ceb49a7bc621cefa510eed2fb053ec74ffc3c90/docs/img/logo.jpg -------------------------------------------------------------------------------- /docs/img/user_initiated_use_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudelskisecurity/youshallnotpass/4ceb49a7bc621cefa510eed2fb053ec74ffc3c90/docs/img/user_initiated_use_case.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kudelskisecurity/youshallnotpass 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/hashicorp/vault/api v1.10.0 7 | github.com/mattermost/mattermost/server/public v0.0.9 8 | github.com/urfave/cli/v2 v2.25.7 9 | ) 10 | 11 | require ( 12 | github.com/blang/semver/v4 v4.0.0 // indirect 13 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 14 | github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 15 | github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect 16 | github.com/francoispqt/gojay v1.2.13 // indirect 17 | github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect 18 | github.com/go-jose/go-jose/v3 v3.0.0 // indirect 19 | github.com/google/uuid v1.3.1 // indirect 20 | github.com/gorilla/websocket v1.5.0 // indirect 21 | github.com/graph-gophers/graphql-go v1.5.1-0.20230911103923-12e9b901a628 // indirect 22 | github.com/hashicorp/errwrap v1.1.0 // indirect 23 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 24 | github.com/hashicorp/go-multierror v1.1.1 // indirect 25 | github.com/hashicorp/go-retryablehttp v0.7.4 // indirect 26 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 27 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect 28 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 29 | github.com/hashicorp/go-sockaddr v1.0.5 // indirect 30 | github.com/hashicorp/hcl v1.0.0 // indirect 31 | github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect 32 | github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect 33 | github.com/mattermost/logr/v2 v2.0.18 // indirect 34 | github.com/mitchellh/go-homedir v1.1.0 // indirect 35 | github.com/mitchellh/mapstructure v1.5.0 // indirect 36 | github.com/pborman/uuid v1.2.1 // indirect 37 | github.com/pelletier/go-toml v1.9.5 // indirect 38 | github.com/philhofer/fwd v1.1.2 // indirect 39 | github.com/pkg/errors v0.9.1 // indirect 40 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 41 | github.com/ryanuber/go-glob v1.0.0 // indirect 42 | github.com/tinylib/msgp v1.1.8 // indirect 43 | github.com/vmihailenco/msgpack/v5 v5.4.0 // indirect 44 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 45 | github.com/wiggin77/merror v1.0.5 // indirect 46 | github.com/wiggin77/srslog v1.0.1 // indirect 47 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 48 | golang.org/x/crypto v0.14.0 // indirect 49 | golang.org/x/net v0.17.0 // indirect 50 | golang.org/x/text v0.13.0 // indirect 51 | golang.org/x/time v0.3.0 // indirect 52 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 53 | gopkg.in/yaml.v2 v2.4.0 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/cmd/validatetokencmd" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var ( 13 | Version = "development" 14 | BuildTime = "unknown" 15 | ) 16 | 17 | func main() { 18 | cli.VersionFlag = &cli.BoolFlag{ 19 | Name: "version", 20 | Aliases: []string{"v", "V"}, 21 | Usage: "Print the version", 22 | } 23 | 24 | app := &cli.App{} 25 | app.EnableBashCompletion = true 26 | 27 | app.Name = "youshallnotpass" 28 | app.Usage = "Secure Authenticated Pipelines" 29 | app.UsageText = "youshallnotpass [command] [arguments...]" 30 | app.Copyright = fmt.Sprintf(`(c) %d Kudelski Security.`, time.Now().Year()) 31 | app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime) 32 | app.Description = `youshallnotpass allows for secure authenticated pipelines` 33 | 34 | app.Commands = commands() 35 | 36 | err := app.Run(os.Args) 37 | if err != nil { 38 | fmt.Println(err) 39 | os.Exit(1) 40 | } 41 | } 42 | 43 | func commands() []*cli.Command { 44 | cmds := []*cli.Command{ 45 | { 46 | Name: "version", 47 | Action: func(c *cli.Context) (err error) { 48 | cli.VersionPrinter(c) 49 | return nil 50 | }, 51 | Usage: "Print the version", 52 | Description: "Print the version", 53 | }, 54 | } 55 | 56 | cmds = append(cmds, validatetokencmd.Commands()...) 57 | 58 | return cmds 59 | } 60 | -------------------------------------------------------------------------------- /pkg/checkparser/checkparser.go: -------------------------------------------------------------------------------- 1 | package checkparser 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/datetime" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/imagehash" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/mfarequired" 11 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/scripthash" 12 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 13 | ) 14 | 15 | var ErrUnknownCheckNameError = errors.New("unknown check name") 16 | 17 | func ParseChecks(configs []config.CheckConfig, jobName string, image string, scriptLines []string, checkType string, ciPlatform string) ([]checks.Check, error) { 18 | var stage uint 19 | if checkType == "image" { 20 | stage = checks.ImageCheck 21 | } else if checkType == "script" { 22 | stage = checks.ScriptCheck 23 | } else { 24 | stage = checks.All 25 | } 26 | 27 | var performChecks []checks.Check 28 | for _, config := range configs { 29 | switch strings.ToLower(config.Name) { 30 | case "scripthash": 31 | scriptHashCheck := scripthash.NewScriptHashCheck(config, jobName, scriptLines) 32 | if scriptHashCheck.IsValidForPlatform(ciPlatform) && scriptHashCheck.IsValidForCheckType(stage) { 33 | performChecks = append(performChecks, &scriptHashCheck) 34 | } 35 | case "imagehash": 36 | imageHashCheck := imagehash.NewImageHashCheck(config, jobName, image) 37 | if imageHashCheck.IsValidForPlatform(ciPlatform) && imageHashCheck.IsValidForCheckType(stage) { 38 | performChecks = append(performChecks, &imageHashCheck) 39 | } 40 | case "mfarequired": 41 | mfaRequiredCheck := mfarequired.NewMfaRequiredCheck(config, jobName) 42 | if mfaRequiredCheck.IsValidForPlatform(ciPlatform) && mfaRequiredCheck.IsValidForCheckType(stage) { 43 | performChecks = append(performChecks, &mfaRequiredCheck) 44 | } 45 | case "datetimecheck": 46 | dateTimeCheck := datetime.NewDateTimeCheck(config, jobName) 47 | if dateTimeCheck.IsValidForPlatform(ciPlatform) && dateTimeCheck.IsValidForCheckType(stage) { 48 | performChecks = append(performChecks, &dateTimeCheck) 49 | } 50 | default: 51 | return performChecks, ErrUnknownCheckNameError 52 | } 53 | } 54 | 55 | return performChecks, nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/checkparser/checkparser_test.go: -------------------------------------------------------------------------------- 1 | package checkparser 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/datetime" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/imagehash" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/mfarequired" 11 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks/scripthash" 12 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 13 | ) 14 | 15 | func TestParseChecks(t *testing.T) { 16 | parseChecksTests := []struct { 17 | name string 18 | configs []config.CheckConfig 19 | checkType string 20 | ciPlatform string 21 | expectedError error 22 | expectedChecks []checks.Check 23 | }{ 24 | { 25 | name: "test parse valid script hash check", 26 | configs: []config.CheckConfig{ 27 | { 28 | Name: "scriptHash", 29 | Options: map[string]interface{}{ 30 | "mfaOnFail": true, 31 | }, 32 | }, 33 | }, 34 | checkType: "script", 35 | ciPlatform: "gitlab", 36 | expectedError: nil, 37 | expectedChecks: []checks.Check{ 38 | &scripthash.ScriptHashCheck{}, 39 | }, 40 | }, 41 | { 42 | name: "test parse invalid script hash check", 43 | configs: []config.CheckConfig{ 44 | { 45 | Name: "scriptHash", 46 | Options: map[string]interface{}{ 47 | "mfaOnFail": true, 48 | }, 49 | }, 50 | }, 51 | checkType: "image", 52 | ciPlatform: "gitlab", 53 | expectedError: nil, 54 | expectedChecks: []checks.Check{}, 55 | }, 56 | { 57 | name: "test parse valid image hash check", 58 | configs: []config.CheckConfig{ 59 | { 60 | Name: "imageHash", 61 | Options: map[string]interface{}{ 62 | "mfaOnFail": true, 63 | }, 64 | }, 65 | }, 66 | checkType: "image", 67 | ciPlatform: "gitlab", 68 | expectedError: nil, 69 | expectedChecks: []checks.Check{ 70 | &imagehash.ImageHashCheck{}, 71 | }, 72 | }, 73 | { 74 | name: "test parse invalid image hash check", 75 | configs: []config.CheckConfig{ 76 | { 77 | Name: "imageHash", 78 | Options: map[string]interface{}{ 79 | "abortOnFail": true, 80 | }, 81 | }, 82 | }, 83 | checkType: "script", 84 | ciPlatform: "gitlab", 85 | expectedError: nil, 86 | expectedChecks: []checks.Check{}, 87 | }, 88 | { 89 | name: "test parse valid mfa required check", 90 | configs: []config.CheckConfig{ 91 | { 92 | Name: "mfaRequired", 93 | }, 94 | }, 95 | checkType: "all", 96 | ciPlatform: "gitlab", 97 | expectedError: nil, 98 | expectedChecks: []checks.Check{ 99 | &mfarequired.MfaRequiredCheck{}, 100 | }, 101 | }, 102 | { 103 | name: "test parse valid date time check", 104 | configs: []config.CheckConfig{ 105 | { 106 | Name: "dateTimeCheck", 107 | }, 108 | }, 109 | checkType: "all", 110 | ciPlatform: "gitlab", 111 | expectedError: nil, 112 | expectedChecks: []checks.Check{ 113 | &datetime.DateTimeCheck{}, 114 | }, 115 | }, 116 | { 117 | name: "test invalid test name", 118 | configs: []config.CheckConfig{ 119 | { 120 | Name: "invalidCheck", 121 | }, 122 | }, 123 | checkType: "all", 124 | ciPlatform: "gitlab", 125 | expectedError: ErrUnknownCheckNameError, 126 | expectedChecks: []checks.Check{}, 127 | }, 128 | } 129 | 130 | for _, test := range parseChecksTests { 131 | checks, err := ParseChecks(test.configs, "testJob", "", []string{""}, test.checkType, test.ciPlatform) 132 | 133 | if err != test.expectedError { 134 | t.Errorf("Unexpected error (%+v) in test: %s", err, test.name) 135 | } 136 | 137 | if len(checks) != len(test.expectedChecks) { 138 | t.Errorf("different checks length - expected: (%d) - got: (%d) - for: %s", len(test.expectedChecks), len(checks), test.name) 139 | } 140 | 141 | for i, check := range checks { 142 | if reflect.TypeOf(check) != reflect.TypeOf(test.expectedChecks[i]) { 143 | t.Errorf("different check types found - expected (%s) - got: (%s) - for %s", reflect.TypeOf(test.expectedChecks[i]), reflect.TypeOf(check), test.name) 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /pkg/checks/checks.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 7 | ) 8 | 9 | const ( 10 | ImageCheck = iota 11 | ScriptCheck 12 | All 13 | ) 14 | 15 | // The Check interface should be implemented for all YouShallNotPass checks. 16 | type Check interface { 17 | // Check performs whatever logic is necessary to determine whether an execution is 18 | // allowed before sending the result through a channel and signalling to the wait group 19 | // that it has completed execution. 20 | // 21 | // Example: 22 | // The ImageHash check checks that the image hash of the current job (obtained on instantiation) 23 | // exists in the whitelist. If it does exist, the check creates a CheckResult indicating the check 24 | // was passed and sends the check through the channel. Once this result is passed through the channel 25 | // the wait group is told the check is done. 26 | Check(chan<- CheckResult, *sync.WaitGroup, whitelist.Whitelist) 27 | 28 | // IsValidForCheckType returns true if a given check is valid for a given check 29 | // type (i.e. ImageCheck, ScriptCheck, AllCheck) 30 | // 31 | // Example: 32 | // If there is a check named ScriptLintCheck that is only valid for script and all checks, this 33 | // function will return true if checkType == ImageCheck OR checkType == All. 34 | IsValidForCheckType(checkType uint) bool 35 | 36 | // IsValidForPlatform returns true if a given check is valid for a given platform 37 | // 38 | // Example: 39 | // If there is a check named ScriptLintCheck that is only valid on GitLab, this function 40 | // will return true if the ciPlatform == "gitlab" 41 | IsValidForPlatform(ciPlatform string) bool 42 | } 43 | 44 | type CheckResult struct { 45 | Name string 46 | Version string 47 | Error error 48 | Abort bool 49 | Mfa bool 50 | Details string 51 | } 52 | 53 | // Deep Compare Two Check Results (Only Useful for the Testing). 54 | func (result *CheckResult) CompareCheckResult(other CheckResult) bool { 55 | if result.Name != other.Name { 56 | return false 57 | } 58 | 59 | if result.Version != other.Version { 60 | return false 61 | } 62 | 63 | if result.Error != other.Error { 64 | return false 65 | } 66 | 67 | if result.Abort != other.Abort { 68 | return false 69 | } 70 | 71 | if result.Mfa != other.Mfa { 72 | return false 73 | } 74 | 75 | if result.Details != other.Details { 76 | return false 77 | } 78 | 79 | return true 80 | } 81 | -------------------------------------------------------------------------------- /pkg/checks/checks_test.go: -------------------------------------------------------------------------------- 1 | package checks 2 | -------------------------------------------------------------------------------- /pkg/checks/datetime/datetime.go: -------------------------------------------------------------------------------- 1 | package datetime 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 11 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 12 | ) 13 | 14 | var ( 15 | DateTimeCheckName = "Date Time Check" 16 | DateTimeCheckVersion = "1.0.0" 17 | DateTimeCheckTimeNotAllowed = "Current Time Not Within Allowed Time" 18 | DateTimeCheckDateNotAllowed = "Current Date Not Within Allowed Date" 19 | DateTimeCheckSuccess = "Successful Datetime Check" 20 | ) 21 | 22 | const ( 23 | daily = 0 24 | weekly = 1 25 | monthly = 2 26 | yearly = 3 27 | ) 28 | 29 | type DateTimeCheck struct { 30 | jobName string 31 | timeScale uint 32 | intervals []int 33 | tolerance int 34 | abortOnFail bool 35 | mfaOnFail bool 36 | hours int 37 | minutes int 38 | seconds int 39 | } 40 | 41 | func NewDateTimeCheck(config config.CheckConfig, jobName string) DateTimeCheck { 42 | timeScale := uint(daily) 43 | scale, exists := config.Options["scale"].(string) 44 | if exists { 45 | switch scale { 46 | case "daily": 47 | timeScale = daily 48 | case "weekly": 49 | timeScale = weekly 50 | case "monthly": 51 | timeScale = monthly 52 | case "yearly": 53 | timeScale = yearly 54 | default: 55 | timeScale = daily 56 | } 57 | } 58 | 59 | intervals, exists := config.Options["intervals"].([]int) 60 | if !exists { 61 | intervals = []int{0} 62 | } 63 | 64 | tolerance, exists := config.Options["tolerance"].(int) 65 | if !exists { 66 | tolerance = 300 67 | } 68 | 69 | abortOnFail, exists := config.Options["abortOnFail"].(bool) 70 | if !exists { 71 | abortOnFail = true 72 | } 73 | 74 | mfaOnFail, exists := config.Options["mfaOnFail"].(bool) 75 | if !exists { 76 | mfaOnFail = false 77 | } 78 | 79 | hours := time.Now().Hour() 80 | minutes := time.Now().Minute() 81 | seconds := time.Now().Second() 82 | var err error 83 | 84 | timeString, exists := config.Options["time"].(string) 85 | if exists { 86 | timeIntervals := strings.Split(timeString, ":") 87 | if len(timeIntervals) != 3 { 88 | hours = time.Now().Hour() 89 | minutes = time.Now().Minute() 90 | seconds = time.Now().Second() 91 | } else { 92 | hours, err = strconv.Atoi(timeIntervals[0]) 93 | if err != nil { 94 | hours = time.Now().Hour() 95 | } 96 | 97 | minutes, err = strconv.Atoi(timeIntervals[1]) 98 | if err != nil { 99 | minutes = time.Now().Minute() 100 | } 101 | 102 | seconds, err = strconv.Atoi(timeIntervals[2]) 103 | if err != nil { 104 | seconds = time.Now().Minute() 105 | } 106 | } 107 | } 108 | 109 | dateTimeCheck := DateTimeCheck{ 110 | jobName: jobName, 111 | timeScale: timeScale, 112 | intervals: intervals, 113 | tolerance: tolerance, 114 | abortOnFail: abortOnFail, 115 | mfaOnFail: mfaOnFail, 116 | hours: hours, 117 | minutes: minutes, 118 | seconds: seconds, 119 | } 120 | 121 | return dateTimeCheck 122 | } 123 | 124 | func (check *DateTimeCheck) Check(channel chan<- checks.CheckResult, wg *sync.WaitGroup, w whitelist.Whitelist) { 125 | defer wg.Done() 126 | 127 | result := checks.CheckResult{Name: DateTimeCheckName, Version: DateTimeCheckVersion, Details: ""} 128 | 129 | if !check.checkWithinTime() { 130 | result.Details = DateTimeCheckTimeNotAllowed 131 | 132 | if check.abortOnFail { 133 | result.Abort = true 134 | channel <- result 135 | return 136 | } else if check.mfaOnFail { 137 | result.Mfa = true 138 | channel <- result 139 | return 140 | } 141 | } 142 | 143 | if !check.checkOnRightDay() { 144 | result.Details += DateTimeCheckDateNotAllowed 145 | 146 | if check.abortOnFail { 147 | result.Abort = true 148 | channel <- result 149 | return 150 | } else if check.mfaOnFail { 151 | result.Mfa = true 152 | channel <- result 153 | return 154 | } 155 | } 156 | 157 | if len(result.Details) == 0 { 158 | result.Details = DateTimeCheckSuccess 159 | } 160 | 161 | channel <- result 162 | } 163 | 164 | func (check *DateTimeCheck) checkWithinTime() bool { 165 | now := time.Now() 166 | 167 | startTime := time.Date(now.Year(), now.Month(), now.Day(), check.hours, check.minutes, check.seconds, 0, time.Local) 168 | 169 | toleranceSeconds := check.seconds + check.tolerance 170 | addedMintues := toleranceSeconds / 60 171 | endSeconds := toleranceSeconds % 60 172 | 173 | toleranceMinutes := check.minutes + addedMintues 174 | addedHours := toleranceMinutes / 60 175 | endMinutes := toleranceMinutes % 60 176 | 177 | toleranceHours := check.hours + addedHours 178 | addedDays := toleranceHours / 24 179 | endHours := toleranceHours % 60 180 | 181 | endTime := time.Date(now.Year(), now.Month(), now.Day()+addedDays, endHours, endMinutes, endSeconds, 0, time.Local) 182 | 183 | checkTime := time.Date(now.Year(), now.Month(), now.Day(), time.Now().Hour(), time.Now().Minute(), time.Now().Second(), 0, time.Local) 184 | 185 | beforeStart := startTime.Before(checkTime) 186 | afterEnd := endTime.After(checkTime) 187 | 188 | return (beforeStart && afterEnd) || (startTime.Equal(checkTime) || endTime.Equal(checkTime)) 189 | } 190 | 191 | func (check *DateTimeCheck) checkOnRightDay() bool { 192 | for _, interval := range check.intervals { 193 | success := false 194 | switch check.timeScale { 195 | case daily: 196 | success = true 197 | case weekly: 198 | success = int(time.Now().Weekday()) == interval 199 | case monthly: 200 | success = time.Now().Day() == interval 201 | case yearly: 202 | totalDays := 0 203 | currentMonth := int(time.Now().Month()) 204 | for month := 1; month < currentMonth; month++ { 205 | totalDays += time.Date(time.Now().Year(), time.Month(month), 0, 0, 0, 0, 0, time.UTC).Day() 206 | } 207 | totalDays += time.Now().Day() 208 | success = totalDays == interval 209 | default: 210 | success = false 211 | } 212 | 213 | if success { 214 | return true 215 | } 216 | } 217 | 218 | return false 219 | } 220 | 221 | func (check *DateTimeCheck) IsValidForCheckType(checkType uint) bool { 222 | return true 223 | } 224 | 225 | func (check *DateTimeCheck) IsValidForPlatform(ciPlatform string) bool { 226 | return true 227 | } 228 | -------------------------------------------------------------------------------- /pkg/checks/imagehash/imagehash.go: -------------------------------------------------------------------------------- 1 | package imagehash 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 10 | ) 11 | 12 | var ( 13 | ImageHashCheckFailedMfaRequired = "Unknown Image MFA Required" 14 | ImageHashCheckFailedAbort = "Unknown Image Aborting" 15 | ImageHashCheckSuccess = "Successful Image Hash Check" 16 | ImageHashCheckError = "---ERROR---" 17 | ) 18 | 19 | type ImageHashCheck struct { 20 | jobName string 21 | abortOnFail bool 22 | mfaOnFail bool 23 | image string 24 | } 25 | 26 | func NewImageHashCheck(config config.CheckConfig, jobName string, image string) ImageHashCheck { 27 | abortOnFail, exists := config.Options["abortOnFail"].(bool) 28 | if !exists { 29 | abortOnFail = false 30 | } 31 | 32 | mfaOnFail, exists := config.Options["mfaOnFail"].(bool) 33 | if !exists { 34 | mfaOnFail = false 35 | } 36 | 37 | return ImageHashCheck{ 38 | abortOnFail: abortOnFail, 39 | mfaOnFail: mfaOnFail, 40 | jobName: jobName, 41 | image: image, 42 | } 43 | } 44 | 45 | func (check *ImageHashCheck) Check(channel chan<- checks.CheckResult, wg *sync.WaitGroup, w whitelist.Whitelist) { 46 | defer wg.Done() 47 | 48 | result := checks.CheckResult{Name: "Image Hash Check", Version: "1.0.0"} 49 | details := "" 50 | 51 | foundImg, err := w.ContainsImage(check.image) 52 | if err != nil { 53 | result.Error = err 54 | details = ImageHashCheckError 55 | if check.abortOnFail { 56 | result.Abort = true 57 | } else if check.mfaOnFail { 58 | result.Mfa = true 59 | } 60 | result.Details = details 61 | channel <- result 62 | return 63 | } 64 | 65 | result.Error = nil 66 | 67 | if !foundImg && check.abortOnFail { 68 | result.Abort = true 69 | details = ImageHashCheckFailedAbort 70 | } else if !foundImg && check.mfaOnFail { 71 | result.Mfa = true 72 | details = ImageHashCheckFailedMfaRequired 73 | } else if foundImg { 74 | details = ImageHashCheckSuccess 75 | } 76 | 77 | result.Details = strings.Clone(details) 78 | 79 | channel <- result 80 | } 81 | 82 | func (check *ImageHashCheck) IsValidForCheckType(checkType uint) bool { 83 | switch checkType { 84 | case checks.All: 85 | return true 86 | default: 87 | return checkType == checks.ImageCheck 88 | } 89 | } 90 | 91 | func (check *ImageHashCheck) IsValidForPlatform(ciPlatform string) bool { 92 | return true 93 | } 94 | -------------------------------------------------------------------------------- /pkg/checks/imagehash/imagehash_test.go: -------------------------------------------------------------------------------- 1 | package imagehash 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 11 | ) 12 | 13 | func TestNewImageHashCheck(t *testing.T) { 14 | newImageHashCheckTests := []struct { 15 | name string 16 | config config.CheckConfig 17 | expectedCheck ImageHashCheck 18 | }{ 19 | { 20 | name: "test abortOnFail true and mfaOnFail true", 21 | config: config.CheckConfig{ 22 | Name: "imageHash", 23 | Options: map[string]interface{}{ 24 | "abortOnFail": true, 25 | "mfaOnFail": true, 26 | }, 27 | }, 28 | expectedCheck: ImageHashCheck{ 29 | jobName: "testJob", 30 | abortOnFail: true, 31 | mfaOnFail: true, 32 | image: "", 33 | }, 34 | }, 35 | { 36 | name: "test abortOnFail false and mfaOnFail false", 37 | config: config.CheckConfig{ 38 | Name: "imageHash", 39 | Options: map[string]interface{}{ 40 | "abortOnFail": false, 41 | "mfaOnFail": false, 42 | }, 43 | }, 44 | expectedCheck: ImageHashCheck{ 45 | jobName: "testJob", 46 | abortOnFail: false, 47 | mfaOnFail: false, 48 | image: "", 49 | }, 50 | }, 51 | { 52 | name: "test abortOnFail missing and mfaOnFail missing", 53 | config: config.CheckConfig{ 54 | Name: "imageHash", 55 | Options: map[string]interface{}{}, 56 | }, 57 | expectedCheck: ImageHashCheck{ 58 | jobName: "testJob", 59 | abortOnFail: false, 60 | mfaOnFail: false, 61 | image: "", 62 | }, 63 | }, 64 | } 65 | 66 | jobName := "testJob" 67 | image := "" 68 | for _, test := range newImageHashCheckTests { 69 | imageHashCheck := NewImageHashCheck(test.config, jobName, image) 70 | if !reflect.DeepEqual(imageHashCheck, test.expectedCheck) { 71 | t.Errorf("checks not equal - expected: (%+v) - got: (%+v)", test.expectedCheck, imageHashCheck) 72 | } 73 | } 74 | } 75 | func TestImageHashCheck(t *testing.T) { 76 | imageHashCheckTests := []struct { 77 | name string 78 | imageHashCheck ImageHashCheck 79 | whitelist whitelist.Whitelist 80 | expectedResult checks.CheckResult 81 | }{ 82 | { 83 | name: "test image in whitelist - success", 84 | imageHashCheck: ImageHashCheck{ 85 | jobName: "testJob", 86 | abortOnFail: true, 87 | mfaOnFail: true, 88 | image: "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 89 | }, 90 | whitelist: whitelist.Whitelist{ 91 | AllowedImages: []string{ 92 | "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 93 | }, 94 | }, 95 | expectedResult: checks.CheckResult{ 96 | Name: "Image Hash Check", 97 | Version: "1.0.0", 98 | Error: nil, 99 | Abort: false, 100 | Mfa: false, 101 | Details: ImageHashCheckSuccess, 102 | }, 103 | }, 104 | { 105 | name: "test image not in whitelist - mfaOnFail", 106 | imageHashCheck: ImageHashCheck{ 107 | jobName: "testJob", 108 | abortOnFail: false, 109 | mfaOnFail: true, 110 | image: "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 111 | }, 112 | whitelist: whitelist.Whitelist{ 113 | AllowedImages: []string{ 114 | "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9fxyz", 115 | }, 116 | }, 117 | expectedResult: checks.CheckResult{ 118 | Name: "Image Hash Check", 119 | Version: "1.0.0", 120 | Error: nil, 121 | Abort: false, 122 | Mfa: true, 123 | Details: ImageHashCheckFailedMfaRequired, 124 | }, 125 | }, 126 | { 127 | name: "test image not in whitelist - abortOnfail", 128 | imageHashCheck: ImageHashCheck{ 129 | jobName: "testJob", 130 | abortOnFail: true, 131 | mfaOnFail: false, 132 | image: "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 133 | }, 134 | whitelist: whitelist.Whitelist{ 135 | AllowedImages: []string{ 136 | "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9fxyz", 137 | }, 138 | }, 139 | expectedResult: checks.CheckResult{ 140 | Name: "Image Hash Check", 141 | Version: "1.0.0", 142 | Error: nil, 143 | Abort: true, 144 | Mfa: false, 145 | Details: ImageHashCheckFailedAbort, 146 | }, 147 | }, 148 | { 149 | name: "test image hash check no image", 150 | imageHashCheck: ImageHashCheck{ 151 | jobName: "testJob", 152 | abortOnFail: true, 153 | mfaOnFail: true, 154 | image: "", 155 | }, 156 | whitelist: whitelist.Whitelist{ 157 | AllowedImages: []string{ 158 | "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 159 | }, 160 | }, 161 | expectedResult: checks.CheckResult{ 162 | Name: "Image Hash Check", 163 | Version: "1.0.0", 164 | Error: whitelist.ErrShaNotPresent, 165 | Abort: true, 166 | Mfa: false, 167 | Details: ImageHashCheckError, 168 | }, 169 | }, 170 | { 171 | name: "test image hash check invalid hash", 172 | imageHashCheck: ImageHashCheck{ 173 | jobName: "testJob", 174 | abortOnFail: false, 175 | mfaOnFail: true, 176 | image: "testImage@sha256:XYZ", 177 | }, 178 | whitelist: whitelist.Whitelist{ 179 | AllowedImages: []string{ 180 | "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 181 | }, 182 | }, 183 | expectedResult: checks.CheckResult{ 184 | Name: "Image Hash Check", 185 | Version: "1.0.0", 186 | Error: whitelist.ErrShaNotValidSha, 187 | Abort: false, 188 | Mfa: true, 189 | Details: ImageHashCheckError, 190 | }, 191 | }, 192 | } 193 | 194 | for testNum, test := range imageHashCheckTests { 195 | var wg sync.WaitGroup 196 | channel := make(chan checks.CheckResult, 1) 197 | 198 | wg.Add(1) 199 | go test.imageHashCheck.Check(channel, &wg, test.whitelist) 200 | 201 | wg.Wait() 202 | close(channel) 203 | 204 | result := <-channel 205 | 206 | if !result.CompareCheckResult(test.expectedResult) { 207 | t.Errorf("\n%d) Results not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedResult, result) 208 | } 209 | } 210 | } 211 | func TestIsValidForCheckType(t *testing.T) { 212 | isValidForCheckTypeTests := []struct { 213 | name string 214 | checkType uint 215 | valid bool 216 | }{ 217 | { 218 | name: "image hash check should be valid for all checks", 219 | checkType: checks.All, 220 | valid: true, 221 | }, 222 | { 223 | name: "image hash check should be valid for image checks", 224 | checkType: checks.ImageCheck, 225 | valid: true, 226 | }, 227 | { 228 | name: "image hash check should not be valid for script checks", 229 | checkType: checks.ScriptCheck, 230 | valid: false, 231 | }, 232 | } 233 | 234 | imageHashCheck := ImageHashCheck{} 235 | for testNum, test := range isValidForCheckTypeTests { 236 | valid := imageHashCheck.IsValidForCheckType(test.checkType) 237 | if valid != test.valid { 238 | t.Errorf("\n%d) unexpected valid for check type -\nexpected: (%t)\ngot: (%t)", testNum, test.valid, valid) 239 | } 240 | } 241 | } 242 | func TestIsValidForPlatform(t *testing.T) { 243 | isValidForPlatformTests := []struct { 244 | name string 245 | platform string 246 | valid bool 247 | }{ 248 | { 249 | name: "image hash check should be valid for github", 250 | platform: "github", 251 | valid: true, 252 | }, 253 | { 254 | name: "image hash check should be valid for gitlab", 255 | platform: "gitlab", 256 | valid: true, 257 | }, 258 | } 259 | 260 | imageHashCheck := ImageHashCheck{} 261 | for testNum, test := range isValidForPlatformTests { 262 | valid := imageHashCheck.IsValidForPlatform(test.platform) 263 | if valid != test.valid { 264 | t.Errorf("\n%d) unexpected valid for platform -\nexpected: (%t)\ngot: (%t)", testNum, test.valid, valid) 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /pkg/checks/mfarequired/mfarequired.go: -------------------------------------------------------------------------------- 1 | package mfarequired 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 10 | ) 11 | 12 | var ( 13 | MfaRequiredCheckName = "Mfa Required Check" 14 | MfaRequiredCheckVersion = "1.0.0" 15 | MfaRequiredCheckDetails = "Mfa Required" 16 | ) 17 | 18 | type MfaRequiredCheck struct { 19 | jobName string 20 | validCheckType uint 21 | } 22 | 23 | func NewMfaRequiredCheck(config config.CheckConfig, jobName string) MfaRequiredCheck { 24 | validCheckType := checks.All 25 | 26 | checkType, exists := config.Options["checkType"].(string) 27 | if !exists { 28 | validCheckType = checks.All 29 | } else if strings.ToLower(checkType) == "image" { 30 | validCheckType = checks.ImageCheck 31 | } else if strings.ToLower(checkType) == "script" { 32 | validCheckType = checks.ScriptCheck 33 | } 34 | 35 | return MfaRequiredCheck{ 36 | jobName: jobName, 37 | validCheckType: uint(validCheckType), 38 | } 39 | } 40 | 41 | func (check *MfaRequiredCheck) Check(channel chan<- checks.CheckResult, wg *sync.WaitGroup, w whitelist.Whitelist) { 42 | defer wg.Done() 43 | 44 | result := checks.CheckResult{Name: MfaRequiredCheckName, Version: MfaRequiredCheckVersion, Mfa: true, Details: MfaRequiredCheckDetails} 45 | 46 | channel <- result 47 | } 48 | 49 | func (check *MfaRequiredCheck) IsValidForCheckType(checkType uint) bool { 50 | switch check.validCheckType { 51 | case checks.All: 52 | return true 53 | default: 54 | switch checkType { 55 | case checks.All: 56 | return true 57 | default: 58 | return check.validCheckType == checkType 59 | } 60 | } 61 | } 62 | 63 | func (check *MfaRequiredCheck) IsValidForPlatform(ciPlatform string) bool { 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /pkg/checks/mfarequired/mfarequired_test.go: -------------------------------------------------------------------------------- 1 | package mfarequired 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 11 | ) 12 | 13 | func TestNewMfaRequiredCheck(t *testing.T) { 14 | newMfaRequiredCheckTests := []struct { 15 | name string 16 | config config.CheckConfig 17 | expectedCheck MfaRequiredCheck 18 | }{ 19 | { 20 | name: "mfa required check no check type specified", 21 | config: config.CheckConfig{ 22 | Name: "MfaRequired", 23 | Options: map[string]interface{}{}, 24 | }, 25 | expectedCheck: MfaRequiredCheck{ 26 | jobName: "testJob", 27 | validCheckType: checks.All, 28 | }, 29 | }, 30 | { 31 | name: "mfa required check image check specified", 32 | config: config.CheckConfig{ 33 | Name: "MfaRequired", 34 | Options: map[string]interface{}{ 35 | "checkType": "image", 36 | }, 37 | }, 38 | expectedCheck: MfaRequiredCheck{ 39 | jobName: "testJob", 40 | validCheckType: checks.ImageCheck, 41 | }, 42 | }, 43 | { 44 | name: "mfa required check script check specified", 45 | config: config.CheckConfig{ 46 | Name: "MfaRequired", 47 | Options: map[string]interface{}{ 48 | "checkType": "script", 49 | }, 50 | }, 51 | expectedCheck: MfaRequiredCheck{ 52 | jobName: "testJob", 53 | validCheckType: checks.ScriptCheck, 54 | }, 55 | }, 56 | } 57 | 58 | jobName := "testJob" 59 | for testNum, test := range newMfaRequiredCheckTests { 60 | check := NewMfaRequiredCheck(test.config, jobName) 61 | 62 | if !reflect.DeepEqual(check, test.expectedCheck) { 63 | t.Errorf("\n%d) checks not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedCheck, check) 64 | } 65 | } 66 | } 67 | func TestMfaRequiredCheck(t *testing.T) { 68 | mfaRequiredCheckTests := []struct { 69 | name string 70 | mfaRequiredCheck MfaRequiredCheck 71 | expectedResult checks.CheckResult 72 | }{ 73 | { 74 | name: "perform mfa required check", 75 | mfaRequiredCheck: MfaRequiredCheck{ 76 | jobName: "testJob", 77 | validCheckType: checks.All, 78 | }, 79 | expectedResult: checks.CheckResult{ 80 | Name: MfaRequiredCheckName, 81 | Version: MfaRequiredCheckVersion, 82 | Error: nil, 83 | Mfa: true, 84 | Details: MfaRequiredCheckDetails, 85 | }, 86 | }, 87 | } 88 | 89 | whitelist := whitelist.Whitelist{} 90 | for testNum, test := range mfaRequiredCheckTests { 91 | var wg sync.WaitGroup 92 | channel := make(chan checks.CheckResult, 1) 93 | 94 | wg.Add(1) 95 | go test.mfaRequiredCheck.Check(channel, &wg, whitelist) 96 | 97 | wg.Wait() 98 | close(channel) 99 | 100 | result := <-channel 101 | 102 | if !result.CompareCheckResult(test.expectedResult) { 103 | t.Errorf("\n%d) Results not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedResult, result) 104 | } 105 | } 106 | } 107 | 108 | func TestIsValidForCheckType(t *testing.T) { 109 | isValidForCheckTypeTests := []struct { 110 | name string 111 | mfaRequiredCheck MfaRequiredCheck 112 | checkType uint 113 | valid bool 114 | }{ 115 | { 116 | name: "mfa required checks valid for all checks should be valid for all checks", 117 | mfaRequiredCheck: MfaRequiredCheck{ 118 | validCheckType: checks.All, 119 | }, 120 | checkType: checks.All, 121 | valid: true, 122 | }, 123 | { 124 | name: "mfa required checks valid for all checks should be valid for image checks", 125 | mfaRequiredCheck: MfaRequiredCheck{ 126 | validCheckType: checks.All, 127 | }, 128 | checkType: checks.ImageCheck, 129 | valid: true, 130 | }, 131 | { 132 | name: "mfa required checks valid for all checks should be valid for script checks", 133 | mfaRequiredCheck: MfaRequiredCheck{ 134 | validCheckType: checks.All, 135 | }, 136 | checkType: checks.ScriptCheck, 137 | valid: true, 138 | }, 139 | { 140 | name: "mfa required checks valid for image checks should be valid for all checks", 141 | mfaRequiredCheck: MfaRequiredCheck{ 142 | validCheckType: checks.ImageCheck, 143 | }, 144 | checkType: checks.All, 145 | valid: true, 146 | }, 147 | { 148 | name: "mfa required checks valid for image checks should be valid for image checks", 149 | mfaRequiredCheck: MfaRequiredCheck{ 150 | validCheckType: checks.ImageCheck, 151 | }, 152 | checkType: checks.ImageCheck, 153 | valid: true, 154 | }, 155 | { 156 | name: "mfa required checks valid for image checks should not be valid for script checks", 157 | mfaRequiredCheck: MfaRequiredCheck{ 158 | validCheckType: checks.ImageCheck, 159 | }, 160 | checkType: checks.ScriptCheck, 161 | valid: false, 162 | }, 163 | { 164 | name: "mfa required checks valid for script checks should be valid for all checks", 165 | mfaRequiredCheck: MfaRequiredCheck{ 166 | validCheckType: checks.ScriptCheck, 167 | }, 168 | checkType: checks.All, 169 | valid: true, 170 | }, 171 | { 172 | name: "mfa required checks valid for script checks should be valid for script checks", 173 | mfaRequiredCheck: MfaRequiredCheck{ 174 | validCheckType: checks.ScriptCheck, 175 | }, 176 | checkType: checks.ScriptCheck, 177 | valid: true, 178 | }, 179 | { 180 | name: "mfa required checks valid for script checks should not be valid for image checks", 181 | mfaRequiredCheck: MfaRequiredCheck{ 182 | validCheckType: checks.ScriptCheck, 183 | }, 184 | checkType: checks.ImageCheck, 185 | valid: false, 186 | }, 187 | } 188 | 189 | for testNum, test := range isValidForCheckTypeTests { 190 | valid := test.mfaRequiredCheck.IsValidForCheckType(test.checkType) 191 | if valid != test.valid { 192 | t.Errorf("\n%d) unexpected valid for check type -\nexpected: (%t)\ngot: (%t)", testNum, test.valid, valid) 193 | } 194 | } 195 | } 196 | func TestIsValidForPlatform(t *testing.T) { 197 | isValidForPlatformTests := []struct { 198 | name string 199 | platform string 200 | valid bool 201 | }{ 202 | { 203 | name: "mfa required check should be valid for github", 204 | platform: "github", 205 | valid: true, 206 | }, 207 | { 208 | name: "mfa required check should be valid for gitlab", 209 | platform: "gitlab", 210 | valid: true, 211 | }, 212 | } 213 | 214 | mfaRequiredCheck := MfaRequiredCheck{} 215 | for testNum, test := range isValidForPlatformTests { 216 | valid := mfaRequiredCheck.IsValidForPlatform(test.platform) 217 | if valid != test.valid { 218 | t.Errorf("\n%d) unexpected valid for platform -\nexpected: (%t)\ngot: (%t)", testNum, test.valid, valid) 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /pkg/checks/scripthash/scripthash.go: -------------------------------------------------------------------------------- 1 | package scripthash 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 11 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 12 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 13 | ) 14 | 15 | var ( 16 | ScriptHashCheckName = "Script Hash Check" 17 | ScriptHashCheckVersion = "1.0.0" 18 | ScriptHashCheckEmptyScriptDetails = "No Script" 19 | ScriptHashCheckAbortScriptDetails = "Unknown Script %s@%s Aborting" 20 | ScriptHashCheckMfaScriptDetails = "Unknown Script %s@%s MFA Required" 21 | ScriptHashCheckSuccessDetails = "Found Script Sha" 22 | ScriptHashCheckUpdatedScriptDetails = " - CI Job %s has been updated" 23 | ) 24 | 25 | type ScriptHashCheck struct { 26 | jobName string 27 | abortOnFail bool 28 | mfaOnFail bool 29 | scriptLines []string 30 | } 31 | 32 | func NewScriptHashCheck(config config.CheckConfig, jobName string, scriptLines []string) ScriptHashCheck { 33 | abortOnFail, exists := config.Options["abortOnFail"].(bool) 34 | if !exists { 35 | abortOnFail = false 36 | } 37 | 38 | mfaOnFail, exists := config.Options["mfaOnFail"].(bool) 39 | if !exists { 40 | mfaOnFail = false 41 | } 42 | 43 | return ScriptHashCheck{ 44 | abortOnFail: abortOnFail, 45 | mfaOnFail: mfaOnFail, 46 | jobName: jobName, 47 | scriptLines: scriptLines, 48 | } 49 | } 50 | 51 | func (check *ScriptHashCheck) Check(channel chan<- checks.CheckResult, wg *sync.WaitGroup, w whitelist.Whitelist) { 52 | defer wg.Done() 53 | 54 | result := checks.CheckResult{Name: ScriptHashCheckName, Version: ScriptHashCheckVersion} 55 | details := "" 56 | 57 | scriptUpdate := false 58 | 59 | scriptSha := check.hashScript() 60 | if scriptSha == "" { 61 | details = ScriptHashCheckEmptyScriptDetails 62 | result.Details = details 63 | channel <- result 64 | return 65 | } 66 | 67 | foundScript := w.ContainsScript(scriptSha) 68 | 69 | if !foundScript { 70 | scriptUpdate, _ = w.ContainsJobName(check.jobName) 71 | } 72 | 73 | if !foundScript && check.abortOnFail { 74 | result.Abort = true 75 | details = fmt.Sprintf(ScriptHashCheckAbortScriptDetails, check.jobName, scriptSha) 76 | } else if !foundScript && check.mfaOnFail { 77 | result.Mfa = true 78 | details = fmt.Sprintf(ScriptHashCheckMfaScriptDetails, check.jobName, scriptSha) 79 | } else if foundScript { 80 | details = ScriptHashCheckSuccessDetails 81 | } 82 | 83 | if scriptUpdate { 84 | details += fmt.Sprintf(ScriptHashCheckUpdatedScriptDetails, check.jobName) 85 | } 86 | 87 | result.Details = strings.Clone(details) 88 | 89 | channel <- result 90 | } 91 | 92 | func (check *ScriptHashCheck) hashScript() string { 93 | script := "" 94 | for _, line := range check.scriptLines { 95 | script += line 96 | } 97 | 98 | if len(script) == 0 { 99 | return "" 100 | } 101 | 102 | h := sha256.New() 103 | h.Write([]byte(script)) 104 | 105 | return "sha256:" + base64.URLEncoding.EncodeToString(h.Sum(nil)) 106 | } 107 | 108 | func (check *ScriptHashCheck) IsValidForCheckType(checkType uint) bool { 109 | switch checkType { 110 | case checks.All: 111 | return true 112 | default: 113 | return checkType == checks.ScriptCheck 114 | } 115 | } 116 | 117 | func (check *ScriptHashCheck) IsValidForPlatform(ciPlatform string) bool { 118 | return true 119 | } 120 | -------------------------------------------------------------------------------- /pkg/checks/scripthash/scripthash_test.go: -------------------------------------------------------------------------------- 1 | package scripthash 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 11 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 12 | ) 13 | 14 | func TestNewScriptHashCheck(t *testing.T) { 15 | newScriptHashCheckTests := []struct { 16 | name string 17 | config config.CheckConfig 18 | expectedCheck ScriptHashCheck 19 | }{ 20 | { 21 | name: "test abortOnFail true and mfaOnFail true", 22 | config: config.CheckConfig{ 23 | Name: "scriptHash", 24 | Options: map[string]interface{}{ 25 | "abortOnFail": true, 26 | "mfaOnFail": true, 27 | }, 28 | }, 29 | expectedCheck: ScriptHashCheck{ 30 | jobName: "testJob", 31 | abortOnFail: true, 32 | mfaOnFail: true, 33 | scriptLines: []string{""}, 34 | }, 35 | }, 36 | { 37 | name: "test abortOnFail false and mfaOnFail false", 38 | config: config.CheckConfig{ 39 | Name: "scriptHash", 40 | Options: map[string]interface{}{ 41 | "abortOnFail": false, 42 | "mfaOnFail": false, 43 | }, 44 | }, 45 | expectedCheck: ScriptHashCheck{ 46 | jobName: "testJob", 47 | abortOnFail: false, 48 | mfaOnFail: false, 49 | scriptLines: []string{""}, 50 | }, 51 | }, 52 | { 53 | name: "test abortOnFail missing and mfaOnFail missing", 54 | config: config.CheckConfig{ 55 | Name: "scriptHash", 56 | Options: map[string]interface{}{}, 57 | }, 58 | expectedCheck: ScriptHashCheck{ 59 | jobName: "testJob", 60 | abortOnFail: false, 61 | mfaOnFail: false, 62 | scriptLines: []string{""}, 63 | }, 64 | }, 65 | } 66 | 67 | jobName := "testJob" 68 | scriptLines := []string{""} 69 | for testNum, test := range newScriptHashCheckTests { 70 | scriptHashCheck := NewScriptHashCheck(test.config, jobName, scriptLines) 71 | if !reflect.DeepEqual(scriptHashCheck, test.expectedCheck) { 72 | t.Errorf("%d) Checks not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedCheck, scriptHashCheck) 73 | } 74 | } 75 | } 76 | func TestScriptHashCheck(t *testing.T) { 77 | scriptHashCheckTests := []struct { 78 | name string 79 | scriptHashCheck ScriptHashCheck 80 | whitelist whitelist.Whitelist 81 | expectedResult checks.CheckResult 82 | }{ 83 | { 84 | name: "test script in whitelist - success", 85 | scriptHashCheck: ScriptHashCheck{ 86 | jobName: "testJob", 87 | abortOnFail: true, 88 | mfaOnFail: true, 89 | scriptLines: []string{ 90 | `$ echo "this script is for testing"`, 91 | `$ curl -X POST -H yourdomain.com"`, 92 | }, 93 | }, 94 | whitelist: whitelist.Whitelist{ 95 | AllowedScripts: []string{ 96 | "testJob@sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRp5kw=", 97 | }, 98 | }, 99 | expectedResult: checks.CheckResult{ 100 | Name: ScriptHashCheckName, 101 | Version: ScriptHashCheckVersion, 102 | Error: nil, 103 | Abort: false, 104 | Mfa: false, 105 | Details: ScriptHashCheckSuccessDetails, 106 | }, 107 | }, 108 | { 109 | name: "test script not in (empty) whitelist - abort", 110 | scriptHashCheck: ScriptHashCheck{ 111 | jobName: "testJob", 112 | abortOnFail: true, 113 | mfaOnFail: false, 114 | scriptLines: []string{ 115 | `$ echo "this script is for testing"`, 116 | `$ curl -X POST -H yourdomain.com"`, 117 | }, 118 | }, 119 | whitelist: whitelist.Whitelist{}, 120 | expectedResult: checks.CheckResult{ 121 | Name: ScriptHashCheckName, 122 | Version: ScriptHashCheckVersion, 123 | Error: nil, 124 | Abort: true, 125 | Mfa: false, 126 | Details: fmt.Sprintf(ScriptHashCheckAbortScriptDetails, "testJob", "sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRp5kw="), 127 | }, 128 | }, 129 | { 130 | name: "test script not in whitelist (updated) - abort", 131 | scriptHashCheck: ScriptHashCheck{ 132 | jobName: "testJob", 133 | abortOnFail: true, 134 | mfaOnFail: false, 135 | scriptLines: []string{ 136 | `$ echo "this script is for testing"`, 137 | `$ curl -X POST -H yourdomain.com"`, 138 | }, 139 | }, 140 | whitelist: whitelist.Whitelist{ 141 | AllowedScripts: []string{ 142 | "testJob@sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRpxyz=", 143 | }, 144 | }, 145 | expectedResult: checks.CheckResult{ 146 | Name: ScriptHashCheckName, 147 | Version: ScriptHashCheckVersion, 148 | Error: nil, 149 | Abort: true, 150 | Mfa: false, 151 | Details: fmt.Sprintf(ScriptHashCheckAbortScriptDetails+ScriptHashCheckUpdatedScriptDetails, 152 | "testJob", "sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRp5kw=", "testJob"), 153 | }, 154 | }, 155 | { 156 | name: "test script not in (empty) whitelist - mfa", 157 | scriptHashCheck: ScriptHashCheck{ 158 | jobName: "testJob", 159 | abortOnFail: false, 160 | mfaOnFail: true, 161 | scriptLines: []string{ 162 | `$ echo "this script is for testing"`, 163 | `$ curl -X POST -H yourdomain.com"`, 164 | }, 165 | }, 166 | whitelist: whitelist.Whitelist{}, 167 | expectedResult: checks.CheckResult{ 168 | Name: ScriptHashCheckName, 169 | Version: ScriptHashCheckVersion, 170 | Error: nil, 171 | Abort: false, 172 | Mfa: true, 173 | Details: fmt.Sprintf(ScriptHashCheckMfaScriptDetails, "testJob", "sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRp5kw="), 174 | }, 175 | }, 176 | { 177 | name: "test script not in whitelist (updated) - mfa", 178 | scriptHashCheck: ScriptHashCheck{ 179 | jobName: "testJob", 180 | abortOnFail: false, 181 | mfaOnFail: true, 182 | scriptLines: []string{ 183 | `$ echo "this script is for testing"`, 184 | `$ curl -X POST -H yourdomain.com"`, 185 | }, 186 | }, 187 | whitelist: whitelist.Whitelist{ 188 | AllowedScripts: []string{ 189 | "testJob@sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRpxyz=", 190 | }, 191 | }, 192 | expectedResult: checks.CheckResult{ 193 | Name: ScriptHashCheckName, 194 | Version: ScriptHashCheckVersion, 195 | Error: nil, 196 | Abort: false, 197 | Mfa: true, 198 | Details: fmt.Sprintf(ScriptHashCheckMfaScriptDetails+ScriptHashCheckUpdatedScriptDetails, 199 | "testJob", "sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRp5kw=", "testJob"), 200 | }, 201 | }, 202 | } 203 | 204 | for testNum, test := range scriptHashCheckTests { 205 | var wg sync.WaitGroup 206 | channel := make(chan checks.CheckResult, 1) 207 | 208 | wg.Add(1) 209 | go test.scriptHashCheck.Check(channel, &wg, test.whitelist) 210 | 211 | wg.Wait() 212 | close(channel) 213 | 214 | result := <-channel 215 | 216 | if !result.CompareCheckResult(test.expectedResult) { 217 | t.Errorf("\n%d) Results not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedResult, result) 218 | } 219 | } 220 | } 221 | 222 | func TestHashScript(t *testing.T) { 223 | hashScriptTests := []struct { 224 | name string 225 | scriptHashCheck ScriptHashCheck 226 | expectedHash string 227 | }{ 228 | { 229 | name: "hash single line script", 230 | scriptHashCheck: ScriptHashCheck{ 231 | jobName: "testJob", 232 | abortOnFail: true, 233 | mfaOnFail: true, 234 | scriptLines: []string{ 235 | `$ echo "this script is for testing"`, 236 | `$ curl -X POST -H yourdomain.com"`, 237 | }, 238 | }, 239 | expectedHash: "sha256:v7gXKQ__R6c76dShfVJoe8gvPOc-TJxHhv3QDdRp5kw=", 240 | }, 241 | { 242 | name: "hash multi line script", 243 | scriptHashCheck: ScriptHashCheck{ 244 | jobName: "testJob", 245 | abortOnFail: true, 246 | mfaOnFail: true, 247 | scriptLines: []string{ 248 | `$ echo "test script"`, 249 | }, 250 | }, 251 | expectedHash: "sha256:D3Xo55v3W4MNn6U3hSH5S6ShRPryxaSKOnE0tQk-O00=", 252 | }, 253 | { 254 | name: "hash empty script", 255 | scriptHashCheck: ScriptHashCheck{ 256 | jobName: "testJob", 257 | abortOnFail: true, 258 | mfaOnFail: true, 259 | scriptLines: []string{}, 260 | }, 261 | expectedHash: "", 262 | }, 263 | } 264 | 265 | for testNum, test := range hashScriptTests { 266 | hash := test.scriptHashCheck.hashScript() 267 | if hash != test.expectedHash { 268 | t.Errorf("\n%d) Resulting Hashes not equal -\nexpected: (%s)\ngot: (%s)", testNum, test.expectedHash, hash) 269 | } 270 | } 271 | } 272 | 273 | func TestIsValidForCheckType(t *testing.T) { 274 | isValidForCheckTypeTests := []struct { 275 | name string 276 | checkType uint 277 | valid bool 278 | }{ 279 | { 280 | name: "script hash check should be valid for all checks", 281 | checkType: checks.All, 282 | valid: true, 283 | }, 284 | { 285 | name: "script hash check should be valid for script checks", 286 | checkType: checks.ScriptCheck, 287 | valid: true, 288 | }, 289 | { 290 | name: "script hash check should not be valid for image checks", 291 | checkType: checks.ImageCheck, 292 | valid: false, 293 | }, 294 | } 295 | 296 | scriptHashCheck := ScriptHashCheck{} 297 | for testNum, test := range isValidForCheckTypeTests { 298 | valid := scriptHashCheck.IsValidForCheckType(test.checkType) 299 | if valid != test.valid { 300 | t.Errorf("\n%d) unexpected valid for check type -\nexpected: (%t)\ngot: (%t)", testNum, test.valid, valid) 301 | } 302 | } 303 | } 304 | 305 | func TestIsValidForPlatform(t *testing.T) { 306 | isValidForPlatformTests := []struct { 307 | name string 308 | platform string 309 | valid bool 310 | }{ 311 | { 312 | name: "script hash check should be valid for github", 313 | platform: "github", 314 | valid: true, 315 | }, 316 | { 317 | name: "script hash check should be valid for gitlab", 318 | platform: "gitlab", 319 | valid: true, 320 | }, 321 | } 322 | 323 | scriptHashCheck := ScriptHashCheck{} 324 | for testNum, test := range isValidForPlatformTests { 325 | valid := scriptHashCheck.IsValidForPlatform(test.platform) 326 | if valid != test.valid { 327 | t.Errorf("\n%d) unexpected valid for platform -\nexpected: (%t)\ngot: (%t)", testNum, test.valid, valid) 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /pkg/cmd/validatetokencmd/validatetokencmd.go: -------------------------------------------------------------------------------- 1 | package validatetokencmd 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/kudelskisecurity/youshallnotpass/pkg/checkparser" 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/loggerclient" 9 | scriptcleanerparser "github.com/kudelskisecurity/youshallnotpass/pkg/scriptcleanerclient" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/vaultclient" 11 | "github.com/kudelskisecurity/youshallnotpass/pkg/vaultclient/hashicorpclient" 12 | "github.com/kudelskisecurity/youshallnotpass/pkg/vaultclient/vaultclientparser" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | func Commands() []*cli.Command { 17 | return []*cli.Command{ 18 | { 19 | Name: "validate-token", 20 | Aliases: []string{"validate"}, 21 | Usage: "validate-token ", 22 | Description: `Validate Token`, 23 | Action: validatetokencommand, 24 | Flags: []cli.Flag{ 25 | &cli.StringFlag{ 26 | Name: "ci-job-image", 27 | EnvVars: []string{"CI_JOB_IMAGE", "CUSTOM_ENV_CI_JOB_IMAGE"}, 28 | Value: "", 29 | DefaultText: "CI Pipeline Docker Container used to Execute the CI job", 30 | }, 31 | &cli.StringFlag{ 32 | Name: "ci-project-path", 33 | EnvVars: []string{"CI_PROJECT_PATH", "CUSTOM_ENV_CI_PROJECT_PATH"}, 34 | Required: true, 35 | DefaultText: "Path to the Repo", 36 | }, 37 | &cli.StringFlag{ 38 | Name: "ci-project-namespace", 39 | EnvVars: []string{"CI_PROJECT_NAMESPACE", "CUSTOM_ENV_CI_PROJECT_NAMESPACE"}, 40 | Required: true, 41 | DefaultText: "Namespace of the Repo", 42 | }, 43 | &cli.IntFlag{ 44 | Name: "ci-pipeline-id", 45 | EnvVars: []string{"CI_PIPELINE_ID", "CUSTOM_ENV_CI_PIPELINE_ID"}, 46 | Value: 0, 47 | DefaultText: "ID of the CI pipeline job", 48 | }, 49 | &cli.StringFlag{ 50 | Name: "ci-job-script", 51 | EnvVars: []string{"RUNNER_SCRIPT"}, 52 | Value: "", 53 | DefaultText: "CI Job Script to Run", 54 | }, 55 | &cli.StringFlag{ 56 | Name: "ci-job-name", 57 | EnvVars: []string{"CI_JOB_NAME", "CUSTOM_ENV_CI_JOB_NAME"}, 58 | Value: "", 59 | DefaultText: "Name of the CI Job", 60 | }, 61 | &cli.StringFlag{ 62 | Name: "ci-user-email", 63 | EnvVars: []string{"CI_USER_EMAIL", "CUSTOM_ENV_GITLAB_USER_EMAIL"}, 64 | Value: "shared", 65 | DefaultText: "Email of the User Executing the CI job (Username on GitHub)", 66 | }, 67 | &cli.StringFlag{ 68 | Name: "ci-platform", 69 | Value: "gitlab", 70 | DefaultText: "The CI/CD platform being used to run this job (i.e. GitHub, GitLab, ...)", 71 | }, 72 | &cli.StringFlag{ 73 | Name: "vault-addr", 74 | EnvVars: []string{"VAULT_ADDR"}, 75 | DefaultText: "URL Address of the Vault Server (i.e. http://vault.example.com)", 76 | Required: true, 77 | }, 78 | &cli.StringFlag{ 79 | Name: "vault-external-addr", 80 | EnvVars: []string{"VAULT_EXTERNAL_ADDR"}, 81 | DefaultText: "Same as Vault Addr (Different in Local Testing)", 82 | }, 83 | &cli.StringFlag{ 84 | Name: "vault-client", 85 | EnvVars: []string{"VAULT_CLIENT"}, 86 | DefaultText: "Secure Key-Value Storage Client (i.e. Hashicorp)", 87 | Value: "Hashicorp", 88 | }, 89 | &cli.StringFlag{ 90 | Name: "vault-root", 91 | EnvVars: []string{"YOUSHALLNOTPASS_VAULT_ROOT"}, 92 | DefaultText: "Vault Secret Root", 93 | Value: "cicd", 94 | Hidden: true, 95 | }, 96 | &cli.StringFlag{ 97 | Name: "vault-role", 98 | EnvVars: []string{"VAULT_ROLE"}, 99 | Value: "", 100 | DefaultText: "Vault role for Authentication", 101 | }, 102 | &cli.StringFlag{ 103 | Name: "vault-login-path", 104 | EnvVars: []string{"VAULT_LOGIN_PATH"}, 105 | DefaultText: "Path to Login to the Vault Instance (must be JWT enabled)", 106 | }, 107 | &cli.StringFlag{ 108 | Name: "vault-token", 109 | EnvVars: []string{"VAULT_TOKEN"}, 110 | DefaultText: "Token to Authenticate with Vault (Optional)", 111 | }, 112 | &cli.StringFlag{ 113 | Name: "jwt-token", 114 | EnvVars: []string{"VAULT_ID_TOKEN", "CUSTOM_ENV_VAULT_ID_TOKEN", "CI_JOB_JWT", "CUSTOM_ENV_CI_JOB_JWT"}, 115 | DefaultText: "JWT for the CI Job", 116 | }, 117 | &cli.StringFlag{ 118 | Name: "pre-validation-token", 119 | EnvVars: []string{"YOUSHALLNOTPASS_PREVALIDATION_TOKEN"}, 120 | DefaultText: "Random String Generated By YouShallNotPass for Multi-Step Scripts", 121 | }, 122 | &cli.IntFlag{ 123 | Name: "timeout", 124 | EnvVars: []string{"YOUSHALLNOTPASS_TIMEOUT"}, 125 | DefaultText: "How Long to Wait for Vault Authentication until Timeout (s)", 126 | Value: 60 * 2, 127 | Hidden: true, 128 | }, 129 | &cli.StringFlag{ 130 | Name: "check-type", 131 | Value: "all", 132 | DefaultText: "The type of check to run at this stage (auto generated in the custom executor)", 133 | }, 134 | }, 135 | }, 136 | } 137 | } 138 | 139 | func validatetokencommand(c *cli.Context) error { 140 | // Extract CLI args 141 | vaultExternalAddr := c.String("vault-external-addr") 142 | ciUserEmail := c.String("ci-user-email") 143 | mfaValidationToken := c.String("pre-validation-token") 144 | ciJobImage := c.String("ci-job-image") 145 | ciJobScript := c.String("ci-job-script") 146 | ciJobName := c.String("ci-job-name") 147 | vaultClient := c.String("vault-client") 148 | vaultRoot := c.String("vault-root") 149 | ciProjectPath := c.String("ci-project-path") 150 | ciProjectNamespace := c.String("ci-project-namespace") 151 | ciPipelineId := c.Int("ci-pipeline-id") 152 | timeout := c.Int("timeout") 153 | checkType := c.String("check-type") 154 | ciPlatform := c.String("ci-platform") 155 | 156 | var v vaultclient.VaultClient 157 | var err error 158 | 159 | clientType, err := vaultclientparser.ParseClient(vaultClient) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if clientType == "Hashicorp" { 165 | v, err = hashicorpclient.InitVaultClient(c) 166 | if err != nil { 167 | return err 168 | } 169 | } 170 | 171 | namespaceConfigMount := vaultRoot + "/" + ciProjectNamespace + "/" + "youshallnotpass_config" 172 | projectConfigMount := vaultRoot + "/" + ciProjectPath + "/" + "youshallnotpass_config" 173 | 174 | // Parse namespace-level configuration for youshallnotpass 175 | namespaceConfig, namespaceConfigErr := v.GetNamespaceConfig(namespaceConfigMount) 176 | 177 | loggerClient, err := loggerclient.ParseNotifyClient(namespaceConfig.LoggerConfig, ciJobName, vaultExternalAddr, vaultRoot, ciProjectPath) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | if namespaceConfigErr != nil { 183 | loggerClient.LogRecoverableError(namespaceConfigErr) 184 | } 185 | 186 | // Parse project-level configuration for youshallnotpass 187 | projectConfig, err := v.GetProjectConfig(projectConfigMount) 188 | if err != nil { 189 | loggerClient.LogRecoverableError(err) 190 | } 191 | 192 | cleaner, err := scriptcleanerparser.ParseCleaner(ciPlatform) 193 | if err != nil { 194 | return loggerClient.LogFailedExecution(err.Error()) 195 | } 196 | 197 | scriptLines := cleaner.CleanupScript(ciJobScript) 198 | 199 | // Parse Checks for the Current Job 200 | checkConfigs := projectConfig.GetJobConfig(ciJobName) 201 | 202 | jobChecks, err := checkparser.ParseChecks(checkConfigs, ciJobName, ciJobImage, scriptLines, checkType, ciPlatform) 203 | if err != nil { 204 | return loggerClient.LogFailedExecution(err.Error()) 205 | } 206 | 207 | // Get whitelist 208 | namespaceWhitelistMount := vaultRoot + "/" + ciProjectNamespace + "/" + "whitelist" 209 | projectWhitelistMount := vaultRoot + "/" + ciProjectPath + "/" + "whitelist" 210 | 211 | whitelist, err := v.ReadWhitelists(namespaceWhitelistMount, projectWhitelistMount) 212 | if err != nil { 213 | return loggerClient.LogFailedExecution(err.Error()) 214 | } 215 | 216 | // Run Checks 217 | var checkWaitGroup sync.WaitGroup 218 | checkChannel := make(chan checks.CheckResult, len(jobChecks)) 219 | 220 | checkWaitGroup.Add(len(jobChecks)) 221 | for _, check := range jobChecks { 222 | check := check 223 | go check.Check(checkChannel, &checkWaitGroup, whitelist) 224 | } 225 | 226 | // Check Results 227 | checkWaitGroup.Wait() 228 | close(checkChannel) 229 | 230 | abort := false 231 | mfaRequired := false 232 | 233 | var results []checks.CheckResult 234 | for result := range checkChannel { 235 | if result.Abort { 236 | abort = true 237 | } else if result.Mfa { 238 | mfaRequired = true 239 | } 240 | 241 | results = append(results, result) 242 | } 243 | 244 | _ = loggerClient.LogCheckResults(results) 245 | 246 | if abort { 247 | return loggerClient.LogFailedExecution("Crucial Check Failed") 248 | } 249 | 250 | // If Necessary Run MFA 251 | if mfaRequired { 252 | // Check for prevalidation 253 | if vaultclient.TokenCreated(mfaValidationToken) { 254 | _ = loggerClient.LogPrevalidated() 255 | return nil 256 | } 257 | 258 | return performMFA(ciPipelineId, ciUserEmail, timeout, checkType, v, loggerClient) 259 | } 260 | 261 | _ = loggerClient.LogSuccessfulExecution() 262 | 263 | return nil 264 | } 265 | 266 | func performMFA(pipelineId int, user string, timeout int, checkType string, c vaultclient.VaultClient, loggerClient loggerclient.LoggerClient) error { 267 | // Write Secret + Check if Exists 268 | secretPath, err := c.WriteScratch(pipelineId, user) 269 | if err != nil { 270 | return loggerClient.LogFailedExecution(err.Error()) 271 | } 272 | 273 | // Tell User To Delete Scratch code at Location 274 | c.LogMFAInstructions(user, loggerClient) 275 | 276 | // Check For Secret Deletion 277 | success := c.WaitForMFA(timeout, secretPath) 278 | 279 | err = c.Cleanup(success, secretPath, checkType) 280 | if err != nil { 281 | return loggerClient.LogFailedExecution(err.Error()) 282 | } 283 | 284 | if success { 285 | _ = loggerClient.LogSuccessfulExecution() 286 | } else { 287 | return loggerClient.LogFailedExecution(" ❌ CI/CD run not authorized") 288 | } 289 | 290 | return nil 291 | } 292 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "encoding/json" 4 | 5 | type LoggerConfig struct { 6 | Name string `json:"name,omitempty"` 7 | Options map[string]interface{} `json:"options,omitempty"` 8 | } 9 | 10 | type NamespaceConfig struct { 11 | LoggerConfig LoggerConfig `json:"logger,omitempty"` 12 | } 13 | 14 | type CheckConfig struct { 15 | Name string `json:"name,omitempty"` 16 | Options map[string]interface{} `json:"options,omitempty"` 17 | } 18 | 19 | type JobConfig struct { 20 | JobName string `json:"jobName,omitempty"` 21 | Checks []CheckConfig `json:"checks,omitempty"` 22 | } 23 | 24 | type ProjectConfig struct { 25 | Jobs []JobConfig `json:"jobs,omitempty"` 26 | } 27 | 28 | var DefaultNamespaceConfig = NamespaceConfig{ 29 | LoggerConfig: LoggerConfig{ 30 | Name: "console", 31 | }, 32 | } 33 | 34 | var DefaultProjectConfig = ProjectConfig{ 35 | Jobs: []JobConfig{ 36 | { 37 | JobName: "default", 38 | Checks: []CheckConfig{ 39 | { 40 | Name: "imageHash", 41 | Options: map[string]interface{}{ 42 | "abortOnFail": true, 43 | }, 44 | }, 45 | { 46 | Name: "scriptHash", 47 | Options: map[string]interface{}{ 48 | "mfaOnFail": true, 49 | }, 50 | }, 51 | }, 52 | }, 53 | }, 54 | } 55 | 56 | func ParseNamespaceConfig(jsonText []byte) (NamespaceConfig, error) { 57 | if len(jsonText) == 0 { 58 | return DefaultNamespaceConfig, nil 59 | } 60 | 61 | var namespaceConfig NamespaceConfig 62 | 63 | err := json.Unmarshal([]byte(jsonText), &namespaceConfig) 64 | if err != nil { 65 | return DefaultNamespaceConfig, err 66 | } 67 | 68 | return namespaceConfig, nil 69 | } 70 | 71 | func ParseProjectConfig(jsonText []byte) (ProjectConfig, error) { 72 | if len(jsonText) == 0 { 73 | return DefaultProjectConfig, nil 74 | } 75 | 76 | var projectConfig ProjectConfig 77 | 78 | err := json.Unmarshal([]byte(jsonText), &projectConfig) 79 | if err != nil { 80 | return DefaultProjectConfig, err 81 | } 82 | 83 | return projectConfig, nil 84 | } 85 | 86 | func (c *ProjectConfig) GetJobConfig(jobName string) []CheckConfig { 87 | if len(jobName) == 0 { 88 | jobName = "default" 89 | } 90 | 91 | for _, job := range c.Jobs { 92 | if job.JobName == jobName { 93 | return job.Checks 94 | } 95 | } 96 | 97 | for _, job := range c.Jobs { 98 | if job.JobName == "default" { 99 | return job.Checks 100 | } 101 | } 102 | 103 | return DefaultProjectConfig.Jobs[0].Checks 104 | } 105 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseNamespaceConfig(t *testing.T) { 9 | parseNamespaceConfigTests := []struct { 10 | name string 11 | jsonText string 12 | expectedConfig NamespaceConfig 13 | errorExpected bool 14 | }{ 15 | { 16 | name: "parse mattermost client configuration", 17 | jsonText: ` 18 | { 19 | "logger": { 20 | "name": "mattermost", 21 | "options": { 22 | "url": "http://127.0.0.1:8000/mattermost", 23 | "token": "1234567890", 24 | "channelId": "1234567890" 25 | } 26 | } 27 | }`, 28 | expectedConfig: NamespaceConfig{ 29 | LoggerConfig{ 30 | Name: "mattermost", 31 | Options: map[string]interface{}{ 32 | "url": "http://127.0.0.1:8000/mattermost", 33 | "token": "1234567890", 34 | "channelId": "1234567890", 35 | }, 36 | }, 37 | }, 38 | errorExpected: false, 39 | }, 40 | { 41 | name: "parse console client configuration", 42 | jsonText: ` 43 | { 44 | "logger": { 45 | "name": "console" 46 | } 47 | }`, 48 | expectedConfig: NamespaceConfig{ 49 | LoggerConfig{ 50 | Name: "console", 51 | }, 52 | }, 53 | errorExpected: false, 54 | }, 55 | { 56 | name: "parse default namespace configuration", 57 | jsonText: ``, 58 | expectedConfig: DefaultNamespaceConfig, 59 | errorExpected: false, 60 | }, 61 | { 62 | name: "parse invalid json namespace configuration", 63 | jsonText: ` 64 | { 65 | "logger",: { "tests" }, 66 | }`, 67 | expectedConfig: DefaultNamespaceConfig, 68 | errorExpected: true, 69 | }, 70 | } 71 | 72 | for testNum, test := range parseNamespaceConfigTests { 73 | namespaceConfig, err := ParseNamespaceConfig([]byte(test.jsonText)) 74 | if !test.errorExpected && err != nil { 75 | t.Errorf("\n%d) unexpected error: %s", testNum, err.Error()) 76 | } else if test.errorExpected && err == nil { 77 | t.Errorf("\n%d) expected an error", testNum) 78 | } 79 | 80 | if !reflect.DeepEqual(namespaceConfig, test.expectedConfig) { 81 | t.Errorf("\n%d) Namespace Configurations not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedConfig, namespaceConfig) 82 | } 83 | } 84 | } 85 | 86 | func TestParseProjectConfig(t *testing.T) { 87 | praseProjectConfigTests := []struct { 88 | name string 89 | jsonText string 90 | expectedConfig ProjectConfig 91 | errorExpected bool 92 | }{ 93 | { 94 | name: "parse single job config", 95 | jsonText: ` 96 | { 97 | "jobs": [ 98 | { 99 | "jobName": "job1", 100 | "checks": [ 101 | { 102 | "name": "imageHash", 103 | "options": { 104 | "abortOnFail": true 105 | } 106 | } 107 | ] 108 | } 109 | ] 110 | }`, 111 | expectedConfig: ProjectConfig{ 112 | Jobs: []JobConfig{ 113 | { 114 | JobName: "job1", 115 | Checks: []CheckConfig{ 116 | { 117 | Name: "imageHash", 118 | Options: map[string]interface{}{ 119 | "abortOnFail": true, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | }, 126 | errorExpected: false, 127 | }, 128 | { 129 | name: "parse no job configs", 130 | jsonText: "", 131 | expectedConfig: DefaultProjectConfig, 132 | errorExpected: false, 133 | }, 134 | { 135 | name: "parse many job configs", 136 | jsonText: ` 137 | { 138 | "jobs": [ 139 | { 140 | "jobName": "testJobOne", 141 | "checks": [ 142 | { 143 | "name": "imageHash", 144 | "options": { 145 | "abortOnFail": true 146 | } 147 | } 148 | ] 149 | }, 150 | { 151 | "jobName": "testJobTwo", 152 | "checks": [ 153 | { 154 | "name": "scriptHash", 155 | "options": { 156 | "mfaOnFail": true 157 | } 158 | } 159 | ] 160 | } 161 | ] 162 | }`, 163 | expectedConfig: ProjectConfig{ 164 | Jobs: []JobConfig{ 165 | { 166 | JobName: "testJobOne", 167 | Checks: []CheckConfig{ 168 | { 169 | Name: "imageHash", 170 | Options: map[string]interface{}{ 171 | "abortOnFail": true, 172 | }, 173 | }, 174 | }, 175 | }, 176 | { 177 | JobName: "testJobTwo", 178 | Checks: []CheckConfig{ 179 | { 180 | Name: "scriptHash", 181 | Options: map[string]interface{}{ 182 | "mfaOnFail": true, 183 | }, 184 | }, 185 | }, 186 | }, 187 | }, 188 | }, 189 | errorExpected: false, 190 | }, 191 | { 192 | name: "parse invalid json", 193 | jsonText: ` 194 | { 195 | "test",,:{should"fail} 196 | }`, 197 | expectedConfig: DefaultProjectConfig, 198 | errorExpected: true, 199 | }, 200 | } 201 | 202 | for testNum, test := range praseProjectConfigTests { 203 | config, err := ParseProjectConfig([]byte(test.jsonText)) 204 | if !test.errorExpected && err != nil { 205 | t.Errorf("\n%d) unexpected error: %s", testNum, err.Error()) 206 | } else if test.errorExpected && err == nil { 207 | t.Errorf("\n%d) expected an error", testNum) 208 | } 209 | 210 | if !reflect.DeepEqual(config, test.expectedConfig) { 211 | t.Errorf("\n%d) Project Configurations not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedConfig, config) 212 | } 213 | } 214 | } 215 | 216 | func TestGetJobConfig(t *testing.T) { 217 | getJobConfigTests := []struct { 218 | name string 219 | jobName string 220 | projectConfig ProjectConfig 221 | expectedChecks []CheckConfig 222 | }{ 223 | { 224 | name: "test get job checks", 225 | jobName: "testJob", 226 | projectConfig: ProjectConfig{ 227 | Jobs: []JobConfig{ 228 | { 229 | JobName: "testJob", 230 | Checks: []CheckConfig{ 231 | { 232 | Name: "imageHash", 233 | Options: map[string]interface{}{ 234 | "abortOnFail": true, 235 | }, 236 | }, 237 | }, 238 | }, 239 | }, 240 | }, 241 | expectedChecks: []CheckConfig{ 242 | { 243 | Name: "imageHash", 244 | Options: map[string]interface{}{ 245 | "abortOnFail": true, 246 | }, 247 | }, 248 | }, 249 | }, 250 | { 251 | name: "test get default job checks", 252 | jobName: "testJob", 253 | projectConfig: ProjectConfig{ 254 | Jobs: []JobConfig{ 255 | { 256 | JobName: "default", 257 | Checks: []CheckConfig{ 258 | { 259 | Name: "imageHash", 260 | Options: map[string]interface{}{ 261 | "abortOnFail": true, 262 | }, 263 | }, 264 | }, 265 | }, 266 | }, 267 | }, 268 | expectedChecks: []CheckConfig{ 269 | { 270 | Name: "imageHash", 271 | Options: map[string]interface{}{ 272 | "abortOnFail": true, 273 | }, 274 | }, 275 | }, 276 | }, 277 | { 278 | name: "test get youshallnotpass default job checks", 279 | jobName: "testJob", 280 | projectConfig: ProjectConfig{}, 281 | expectedChecks: DefaultProjectConfig.Jobs[0].Checks, 282 | }, 283 | { 284 | name: "test get empty job name", 285 | jobName: "", 286 | projectConfig: ProjectConfig{}, 287 | expectedChecks: DefaultProjectConfig.Jobs[0].Checks, 288 | }, 289 | } 290 | 291 | for testNum, test := range getJobConfigTests { 292 | checks := test.projectConfig.GetJobConfig(test.jobName) 293 | if !reflect.DeepEqual(test.expectedChecks, checks) { 294 | t.Errorf("\n%d) Checks are not equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedChecks, checks) 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /pkg/loggerclient/consoleclient/console_client.go: -------------------------------------------------------------------------------- 1 | package consoleclient 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 7 | ) 8 | 9 | var ( 10 | colorRed = "\033[31m" 11 | colorGreen = "\033[32m" 12 | colorReset = "\033[0m" 13 | ) 14 | 15 | type ConsoleClient struct { 16 | ciJobName string 17 | vaultExternalAddr string 18 | vaultRoot string 19 | ciProjectPath string 20 | } 21 | 22 | func NewConsoleClient(ciJobName string, vaultExternalAddr string, vaultRoot string, ciProjectPath string) *ConsoleClient { 23 | return &ConsoleClient{ 24 | ciJobName, 25 | vaultExternalAddr, 26 | vaultRoot, 27 | ciProjectPath, 28 | } 29 | } 30 | 31 | func (c *ConsoleClient) LogRecoverableError(err error) { 32 | fmt.Print(err.Error()) 33 | } 34 | 35 | func (c *ConsoleClient) SendMFAInstructions(message string) error { 36 | print(message) 37 | return nil 38 | } 39 | 40 | func (c *ConsoleClient) LogCheckResults(results []checks.CheckResult) error { 41 | message := "---------------------------------------------------------------------\n" 42 | message += fmt.Sprintf("%s/ui/vault/secrets/%s - %s\n", c.vaultExternalAddr, c.vaultRoot+"/show/"+c.ciProjectPath+"/whitelist", c.ciJobName) 43 | message += "---------------------------------------------------------------------\n" 44 | message += " Name | Version | Error | Abort | Mfa | Details\n" 45 | message += "---------------------------------------------------------------------\n" 46 | for _, result := range results { 47 | errStr := "" 48 | if result.Error != nil { 49 | errStr = result.Error.Error() 50 | } 51 | message += fmt.Sprintf("%s | %s | %s | %t | %t | %s\n", result.Name, result.Version, 52 | errStr, result.Abort, result.Mfa, result.Details) 53 | message += "---------------------------------------------------------------------\n" 54 | } 55 | fmt.Printf("\n\n%s\n\n", message) 56 | return nil 57 | } 58 | 59 | func (c *ConsoleClient) LogPrevalidated() error { 60 | fmt.Printf(" ✅ CI/CD for %s has been prevalidated", c.ciJobName) 61 | return nil 62 | } 63 | 64 | func (c *ConsoleClient) LogSuccessfulExecution() error { 65 | fmt.Printf(" ✅ %sSuccessful YouShallNotPass Check for Job: %s%s\n", string(colorGreen), c.ciJobName, string(colorReset)) 66 | return nil 67 | } 68 | 69 | func (c *ConsoleClient) LogFailedExecution(reason string) error { 70 | fmt.Printf(" ❌ %sUnsuccessful YouShallNotPass Check for Job: %s%s\n", string(colorRed), c.ciJobName, string(colorReset)) 71 | return fmt.Errorf(reason) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/loggerclient/consoleclient/console_client_test.go: -------------------------------------------------------------------------------- 1 | package consoleclient_test 2 | -------------------------------------------------------------------------------- /pkg/loggerclient/loggerclient.go: -------------------------------------------------------------------------------- 1 | package loggerclient 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/loggerclient/consoleclient" 10 | "github.com/kudelskisecurity/youshallnotpass/pkg/loggerclient/mattermostclient" 11 | ) 12 | 13 | type LoggerClient interface { 14 | LogRecoverableError(error) 15 | SendMFAInstructions(string) error 16 | LogCheckResults([]checks.CheckResult) error 17 | LogPrevalidated() error 18 | LogSuccessfulExecution() error 19 | LogFailedExecution(string) error 20 | } 21 | 22 | func ParseNotifyClient(c config.LoggerConfig, ciJobName string, vaultExternalAddr string, vaultRoot string, ciProjectPath string) (LoggerClient, error) { 23 | if strings.ToLower(c.Name) == "mattermost" { 24 | url, exists := c.Options["url"].(string) 25 | if !exists { 26 | return nil, fmt.Errorf("to use mattermost as a logger please include an instance url int vault") 27 | } 28 | 29 | token, exists := c.Options["token"].(string) 30 | if !exists { 31 | return nil, fmt.Errorf("to use mattermost as a logger please include a token in vault") 32 | } 33 | 34 | channelId, exists := c.Options["channelId"].(string) 35 | if !exists { 36 | return nil, fmt.Errorf("to use mattermost as a logger please include a channel id in vault") 37 | } 38 | 39 | return mattermostclient.NewMattermostClient(url, token, channelId, ciJobName, vaultExternalAddr, vaultRoot, ciProjectPath) 40 | } 41 | 42 | return consoleclient.NewConsoleClient(ciJobName, vaultExternalAddr, vaultRoot, ciProjectPath), nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/loggerclient/mattermostclient/mattermost_client.go: -------------------------------------------------------------------------------- 1 | package mattermostclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 8 | "github.com/mattermost/mattermost/server/public/model" 9 | ) 10 | 11 | type MattermostClient struct { 12 | client *model.Client4 13 | channelId string 14 | postId string 15 | ciJobName string 16 | vaultExternalAddr string 17 | vaultRoot string 18 | ciProjectPath string 19 | } 20 | 21 | func NewMattermostClient(mattermostUrl string, mattermostToken string, channelId string, ciJobName string, vaultExternalAddr string, vaultRoot string, ciProjectPath string) (*MattermostClient, error) { 22 | client := model.NewAPIv4Client(mattermostUrl) 23 | client.SetOAuthToken(mattermostToken) 24 | ctx := context.Background() 25 | 26 | post, _, err := client.CreatePost(ctx, 27 | &model.Post{ 28 | ChannelId: channelId, 29 | Message: fmt.Sprintf("## %s CI/CD Run", ciJobName), 30 | }, 31 | ) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &MattermostClient{ 37 | client: client, 38 | channelId: channelId, 39 | postId: post.Id, 40 | ciJobName: ciJobName, 41 | vaultExternalAddr: vaultExternalAddr, 42 | vaultRoot: vaultRoot, 43 | ciProjectPath: ciProjectPath, 44 | }, nil 45 | } 46 | 47 | func (c *MattermostClient) LogRecoverableError(err error) { 48 | _, _, _ = c.client.CreatePost(context.Background(), 49 | &model.Post{ 50 | ChannelId: c.channelId, 51 | RootId: c.postId, 52 | Message: err.Error(), 53 | }, 54 | ) 55 | } 56 | 57 | func (c *MattermostClient) SendMFAInstructions(message string) error { 58 | ctx := context.Background() 59 | _, _, err := c.client.CreatePost(ctx, 60 | &model.Post{ 61 | ChannelId: c.channelId, 62 | RootId: c.postId, 63 | Message: message, 64 | }, 65 | ) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (c *MattermostClient) LogCheckResults(results []checks.CheckResult) error { 74 | message := "---------------------------------------------------------------------\n" 75 | message += fmt.Sprintf("%s/ui/vault/secrets/%s - %s\n", c.vaultExternalAddr, c.vaultRoot+"/show/"+c.ciProjectPath+"/whitelist", c.ciJobName) 76 | message += "---------------------------------------------------------------------\n" 77 | message += " Name | Version | Error | Abort | Mfa | Details\n" 78 | message += "---------------------------------------------------------------------\n" 79 | for _, result := range results { 80 | errStr := "nil" 81 | if result.Error != nil { 82 | errStr = result.Error.Error() 83 | } 84 | message += fmt.Sprintf("%s | %s | %s | %t | %t | %s\n", result.Name, result.Version, 85 | errStr, result.Abort, result.Mfa, result.Details) 86 | message += "---------------------------------------------------------------------\n" 87 | } 88 | 89 | ctx := context.Background() 90 | _, _, err := c.client.CreatePost(ctx, 91 | &model.Post{ 92 | ChannelId: c.channelId, 93 | RootId: c.postId, 94 | Message: message, 95 | }, 96 | ) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func (c *MattermostClient) LogPrevalidated() error { 105 | message := fmt.Sprintf("✅ CI/CD for %s has been prevalidated", c.ciJobName) 106 | ctx := context.Background() 107 | _, _, err := c.client.CreatePost(ctx, 108 | &model.Post{ 109 | ChannelId: c.channelId, 110 | RootId: c.postId, 111 | Message: message, 112 | }, 113 | ) 114 | if err != nil { 115 | return err 116 | } 117 | return nil 118 | } 119 | 120 | func (c *MattermostClient) LogSuccessfulExecution() error { 121 | message := fmt.Sprintf("✅ Successful YouShallNotPass check for Job: %s", c.ciJobName) 122 | ctx := context.Background() 123 | _, _, err := c.client.CreatePost(ctx, 124 | &model.Post{ 125 | ChannelId: c.channelId, 126 | RootId: c.postId, 127 | Message: message, 128 | }, 129 | ) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (c *MattermostClient) LogFailedExecution(reason string) error { 138 | message := fmt.Sprintf("❌ Unsuccessful YouShallNotPass check for job: %s\n", c.ciJobName) 139 | ctx := context.Background() 140 | _, _, _ = c.client.CreatePost(ctx, 141 | &model.Post{ 142 | ChannelId: c.channelId, 143 | RootId: c.postId, 144 | Message: message, 145 | }, 146 | ) 147 | 148 | return fmt.Errorf(reason) 149 | } 150 | -------------------------------------------------------------------------------- /pkg/loggerclient/mattermostclient/mattermost_client_test.go: -------------------------------------------------------------------------------- 1 | package mattermostclient_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/checks" 9 | "github.com/kudelskisecurity/youshallnotpass/pkg/loggerclient/mattermostclient" 10 | "github.com/mattermost/mattermost/server/public/model" 11 | ) 12 | 13 | var ( 14 | client *mattermostclient.MattermostClient = nil 15 | ) 16 | 17 | // This is more integration testing than unit testing because the hashicorp vault 18 | // client is pretty much known to work we're just wrapping it 19 | 20 | func initialize() error { 21 | if client == nil { 22 | ctx := context.Background() 23 | mmClient := model.NewAPIv4Client("http://localhost:8065") 24 | 25 | user, _, err := mmClient.CreateUser(ctx, &model.User{ 26 | Username: "youshallnotpass-bot", 27 | Email: "test@gmail.com", 28 | Password: "1234567890", 29 | }) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | _, _, err = mmClient.LoginById(ctx, user.Id, "1234567890") 35 | if err != nil { 36 | return err 37 | } 38 | 39 | team, _, err := mmClient.CreateTeam(ctx, &model.Team{ 40 | Name: "youshallnotpass", 41 | DisplayName: "youshallnotpass", 42 | Type: "O", 43 | }) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | youshallnotpassChannel, _, err := mmClient.CreateChannel(ctx, &model.Channel{ 49 | TeamId: team.Id, 50 | Name: "youshallnotpass", 51 | DisplayName: "youshallnotpass", 52 | Type: "O", 53 | }) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | mattermostClient, err := mattermostclient.NewMattermostClient("http://localhost:8065", mmClient.AuthToken, youshallnotpassChannel.Id, "test-job", "http://127.0.0.1:8200", "cicd", "test") 59 | if err != nil { 60 | return err 61 | } 62 | 63 | client = mattermostClient 64 | } 65 | return nil 66 | } 67 | 68 | func TestNewMattermostClient(t *testing.T) { 69 | err := initialize() 70 | 71 | if err != nil { 72 | t.Errorf("Expected no err, but got %s", err.Error()) 73 | return 74 | } 75 | } 76 | 77 | func TestLogCheckResults(t *testing.T) { 78 | err := initialize() 79 | 80 | if err != nil { 81 | t.Errorf("unexpected err: %s", err.Error()) 82 | return 83 | } 84 | 85 | checkResults := []checks.CheckResult{ 86 | { 87 | Name: "imageHash", 88 | Version: "1.0.0", 89 | Error: nil, 90 | Abort: true, 91 | Mfa: false, 92 | Details: "New Image Detected", 93 | }, 94 | { 95 | Name: "scriptHash", 96 | Version: "1.0.0", 97 | Error: nil, 98 | Abort: false, 99 | Mfa: false, 100 | Details: "Script Hash Check Succeeded", 101 | }, 102 | { 103 | Name: "mfaRequired", 104 | Version: "1.0.0", 105 | Error: nil, 106 | Abort: false, 107 | Mfa: true, 108 | Details: "Mfa required for job", 109 | }, 110 | } 111 | 112 | err = client.LogCheckResults(checkResults) 113 | if err != nil { 114 | t.Errorf("unexpected error: %s", err.Error()) 115 | } 116 | } 117 | 118 | func TestLogPrevalidated(t *testing.T) { 119 | err := initialize() 120 | 121 | if err != nil { 122 | t.Errorf("Expected no err, but got %s", err.Error()) 123 | return 124 | } 125 | 126 | err = client.LogPrevalidated() 127 | 128 | if err != nil { 129 | t.Errorf("Expected no err, but got %s", err.Error()) 130 | return 131 | } 132 | } 133 | 134 | func TestLogSuccessfulExecution(t *testing.T) { 135 | err := initialize() 136 | 137 | if err != nil { 138 | t.Errorf("Expected no err, but got %s", err.Error()) 139 | return 140 | } 141 | 142 | err = client.LogSuccessfulExecution() 143 | 144 | if err != nil { 145 | t.Errorf("Expected no err, but got %s", err.Error()) 146 | return 147 | } 148 | } 149 | 150 | func TestLogFailedExecution(t *testing.T) { 151 | err := initialize() 152 | 153 | if err != nil { 154 | t.Errorf("Expected no err, but got %s", err.Error()) 155 | return 156 | } 157 | 158 | err = client.LogFailedExecution("test") 159 | 160 | if !strings.Contains(err.Error(), "test") { 161 | t.Errorf("Expected error to contain test, instead got %s", err.Error()) 162 | return 163 | } 164 | } 165 | 166 | func TestSendMFAInstructions(t *testing.T) { 167 | err := initialize() 168 | 169 | if err != nil { 170 | t.Errorf("Expected no err, but got %s", err.Error()) 171 | return 172 | } 173 | 174 | err = client.SendMFAInstructions("delete the scratch code") 175 | 176 | if err != nil { 177 | t.Errorf("Expected no err, but got %s", err.Error()) 178 | return 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /pkg/scriptcleanerclient/githubcleanup/githubcleanup.go: -------------------------------------------------------------------------------- 1 | package githubcleanup 2 | 3 | import "strings" 4 | 5 | type GitHubCleaner struct{} 6 | 7 | func (cleaner *GitHubCleaner) CleanupScript(script string) []string { 8 | /* 9 | GitHub steps start of the form which is almost valid yaml, we're 10 | going to remove the "- "s 11 | - name: Check out repository 12 | uses: actions/checkout@v3 13 | - run: echo "testing" 14 | - run: echo ${{ vars.VAULT_ADDR }} 15 | 16 | Which is parsed to the form: 17 | `name: Check out repository 18 | uses: actions/checkout@v3`, 19 | `run: echo "testing"`, 20 | `run: echo ${{ vars.VAULT_ADDR }}` 21 | */ 22 | splitScript := strings.Split(script, "- ") 23 | splitScript = splitScript[1:] 24 | var scriptLines []string 25 | for _, section := range splitScript { 26 | section = strings.TrimSpace(section) 27 | section = strings.ReplaceAll(section, "\t", " ") 28 | scriptLines = append(scriptLines, section) 29 | } 30 | return scriptLines 31 | } 32 | -------------------------------------------------------------------------------- /pkg/scriptcleanerclient/githubcleanup/githubcleanup_test.go: -------------------------------------------------------------------------------- 1 | package githubcleanup_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/scriptcleanerclient/githubcleanup" 8 | ) 9 | 10 | func TestCleanup(t *testing.T) { 11 | cleanupScriptTests := []struct { 12 | name string 13 | cleaner githubcleanup.GitHubCleaner 14 | script string 15 | expectedOutput []string 16 | }{ 17 | { 18 | name: "cleanup GitHub scripts no names", 19 | cleaner: githubcleanup.GitHubCleaner{}, 20 | script: ` 21 | - run: actions/checkout@v3 22 | - run: echo "testing" 23 | - run: echo ${{ vars.VAULT_ADDR }}`, 24 | expectedOutput: []string{ 25 | `run: actions/checkout@v3`, 26 | `run: echo "testing"`, 27 | `run: echo ${{ vars.VAULT_ADDR }}`, 28 | }, 29 | }, 30 | { 31 | name: "cleanup GitHub scripts with names", 32 | cleaner: githubcleanup.GitHubCleaner{}, 33 | script: ` 34 | - name: Check out repository 35 | uses: actions/checkout@v3 36 | - run: echo "testing" 37 | - run: echo ${{ vars.VAULT_ADDR }}`, 38 | expectedOutput: []string{ 39 | `name: Check out repository 40 | uses: actions/checkout@v3`, 41 | `run: echo "testing"`, 42 | `run: echo ${{ vars.VAULT_ADDR }}`, 43 | }, 44 | }, 45 | { 46 | name: "cleanup GitHub scripts with names and `with`", 47 | cleaner: githubcleanup.GitHubCleaner{}, 48 | script: ` 49 | - name: Check out repository 50 | uses: actions/checkout@v3 51 | - name: use Node.js 52 | uses: actions/setup-node@v1 53 | with: 54 | node-version: '18.x'`, 55 | expectedOutput: []string{ 56 | `name: Check out repository 57 | uses: actions/checkout@v3`, 58 | `name: use Node.js 59 | uses: actions/setup-node@v1 60 | with: 61 | node-version: '18.x'`, 62 | }, 63 | }, 64 | } 65 | 66 | for testNum, test := range cleanupScriptTests { 67 | cleanedScript := test.cleaner.CleanupScript(test.script) 68 | if !reflect.DeepEqual(test.expectedOutput, cleanedScript) { 69 | t.Errorf("\n%d) cleaned script was not as expected -\nexpected: (%+v)\ngot: (%+v)", testNum, test.expectedOutput, cleanedScript) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/scriptcleanerclient/gitlabcleanup/gitlabcleanup.go: -------------------------------------------------------------------------------- 1 | package gitlabcleanup 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | type GitLabCleaner struct{} 9 | 10 | func (cleaner *GitLabCleaner) CleanupScript(script string) []string { 11 | re := regexp.MustCompile(`x1b.32;1m.{1,}?x1b.0;m`) 12 | matches := re.FindAllStringSubmatch(script, -1) 13 | var lines []string 14 | for _, match := range matches { 15 | for _, subMatch := range match { 16 | line := strings.TrimPrefix(subMatch, `x1b[32;1m`) 17 | line = strings.TrimSuffix(line, `x1b[0;m`) 18 | lines = append(lines, line) 19 | } 20 | } 21 | 22 | return lines 23 | } 24 | -------------------------------------------------------------------------------- /pkg/scriptcleanerclient/gitlabcleanup/gitlabcleanup_test.go: -------------------------------------------------------------------------------- 1 | package gitlabcleanup_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/scriptcleanerclient/gitlabcleanup" 8 | ) 9 | 10 | func TestGitlabCleanupScript(t *testing.T) { 11 | cleanupTests := []struct { 12 | name string 13 | cleaner gitlabcleanup.GitLabCleaner 14 | script string 15 | expectedCleanedScript []string 16 | }{ 17 | { 18 | name: "cleanup one line gitlab script", 19 | cleaner: gitlabcleanup.GitLabCleaner{}, 20 | script: `x1b[32;1m$ echo 'this is the automated job and should be run automatically'x1b[0;m'necho 'this is the automated job and should be run automatically'necho $'x1b[32;1m`, 21 | expectedCleanedScript: []string{ 22 | `$ echo 'this is the automated job and should be run automatically'`, 23 | }, 24 | }, 25 | { 26 | name: "cleanup multi line gitlab script", 27 | cleaner: gitlabcleanup.GitLabCleaner{}, 28 | script: `x1b[32;1m$ echo 'this is the automated job and should be run automatically'x1b[0;m'necho 'this is the automated job and should be run automatically'necho $'x1b[32;1m$ echo "Testing this script"x1b[0;m'necho "Testing this script"necho $'x1b[32;1m$ echo "I just need a few lines to split"x1b[0;m'necho "I just need a few lines to split"necho $'x1b[32;1m$ echo "end"x1b[0;m'necho "end"n'`, 29 | expectedCleanedScript: []string{ 30 | `$ echo 'this is the automated job and should be run automatically'`, 31 | `$ echo "Testing this script"`, 32 | `$ echo "I just need a few lines to split"`, 33 | `$ echo "end"`, 34 | }, 35 | }, 36 | { 37 | name: "cleanup bash script", 38 | cleaner: gitlabcleanup.GitLabCleaner{}, 39 | script: `x1b[32;1m$ /gitrepo/test.shx1b[0;m'n/gitrepo/test.shn'x1b[32;1m/gitrepo/test.shx1b[0;mx1b[32;1m#!/bin/bash;;echo "TEST SCRIPT";;x1b[0;m`, 40 | expectedCleanedScript: []string{ 41 | `$ /gitrepo/test.sh`, 42 | `/gitrepo/test.sh`, 43 | `#!/bin/bash;;echo "TEST SCRIPT";;`, 44 | }, 45 | }, 46 | { 47 | name: "cleanup combined bash + script", 48 | cleaner: gitlabcleanup.GitLabCleaner{}, 49 | script: `x1b[32;1m$ echo "this is the script job"x1b[0;m'necho "this is the script job"necho $'x1b[32;1m$ /gitrepo/test.shx1b[0;m'n/gitrepo/test.shnecho $'x1b[32;1m$ echo "this is the end of the script job"x1b[0;m'necho "this is the end of the script job"n'x1b[32;1m/gitrepo/test.shx1b[0;mx1b[32;1m#!/bin/bash;;echo "TEST SCRIPT";;x1b[0;m`, 50 | expectedCleanedScript: []string{ 51 | `$ echo "this is the script job"`, 52 | `$ /gitrepo/test.sh`, 53 | `$ echo "this is the end of the script job"`, 54 | `/gitrepo/test.sh`, 55 | `#!/bin/bash;;echo "TEST SCRIPT";;`, 56 | }, 57 | }, 58 | } 59 | 60 | for testNum, test := range cleanupTests { 61 | cleanedScript := test.cleaner.CleanupScript(test.script) 62 | if !reflect.DeepEqual(test.expectedCleanedScript, cleanedScript) { 63 | t.Errorf("\n%d) cleaned script was not expected -\nexpected: %+v\ngot: %+v", testNum, test.expectedCleanedScript, cleanedScript) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/scriptcleanerclient/scriptcleanerparser.go: -------------------------------------------------------------------------------- 1 | package scriptcleanerparser 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/scriptcleanerclient/githubcleanup" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/scriptcleanerclient/gitlabcleanup" 9 | ) 10 | 11 | var ( 12 | ErrUnknownCICDPlatform = errors.New("unknown CI/CD platform") 13 | ) 14 | 15 | type ScriptCleaner interface { 16 | CleanupScript(script string) []string 17 | } 18 | 19 | func ParseCleaner(platform string) (ScriptCleaner, error) { 20 | switch strings.ToLower(platform) { 21 | case "gitlab": 22 | return &gitlabcleanup.GitLabCleaner{}, nil 23 | case "github": 24 | return &githubcleanup.GitHubCleaner{}, nil 25 | default: 26 | return nil, ErrUnknownCICDPlatform 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/scriptcleanerclient/scriptcleanerparser_test.go: -------------------------------------------------------------------------------- 1 | package scriptcleanerparser 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/kudelskisecurity/youshallnotpass/pkg/scriptcleanerclient/githubcleanup" 8 | "github.com/kudelskisecurity/youshallnotpass/pkg/scriptcleanerclient/gitlabcleanup" 9 | ) 10 | 11 | func TestScriptCleanerParser(t *testing.T) { 12 | scriptCleanerParserTests := []struct { 13 | name string 14 | platform string 15 | expectedCleaner ScriptCleaner 16 | error error 17 | }{ 18 | { 19 | name: "parse gitlab cleaner", 20 | platform: "gitlab", 21 | expectedCleaner: &gitlabcleanup.GitLabCleaner{}, 22 | error: nil, 23 | }, 24 | { 25 | name: "parse github cleaner", 26 | platform: "github", 27 | expectedCleaner: &githubcleanup.GitHubCleaner{}, 28 | error: nil, 29 | }, 30 | { 31 | name: "fail to parse unknown cleaner client", 32 | platform: "dinosaurs", 33 | expectedCleaner: nil, 34 | error: ErrUnknownCICDPlatform, 35 | }, 36 | } 37 | 38 | for testNum, test := range scriptCleanerParserTests { 39 | client, err := ParseCleaner(test.platform) 40 | if err != test.error { 41 | t.Errorf("\n%d) errors differ -\nexpected: (%+v)\ngot: (%+v)", testNum, test.error, err) 42 | } 43 | 44 | if reflect.TypeOf(client) != reflect.TypeOf(test.expectedCleaner) { 45 | t.Errorf("\n%d different client types found -\nexpected: (%s)\ngot: (%s)", testNum, reflect.TypeOf(test.expectedCleaner), reflect.TypeOf(client)) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/vaultclient/hashicorpclient/hashicorp_client.go: -------------------------------------------------------------------------------- 1 | package hashicorpclient 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hashicorp/vault/api" 12 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 13 | "github.com/kudelskisecurity/youshallnotpass/pkg/loggerclient" 14 | "github.com/kudelskisecurity/youshallnotpass/pkg/vaultclient" 15 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 16 | "github.com/urfave/cli/v2" 17 | ) 18 | 19 | type HashicorpService struct { 20 | client *api.Client 21 | vaultAddr string 22 | vaultExternalAddr string 23 | vaultToken string 24 | vaultRole string 25 | vaultRoot string 26 | ciProjectPath string 27 | preValidationToken string 28 | } 29 | 30 | // Creates a new hashicorp service 31 | func InitVaultClient(context *cli.Context) (*HashicorpService, error) { 32 | vaultAddr := context.String("vault-addr") 33 | vaultExternalAddr := context.String("vault-external-addr") 34 | vaultToken := context.String("vault-token") 35 | vaultRole := context.String("vault-role") 36 | vaultRoot := context.String("vault-root") 37 | ciProjectPath := context.String("ci-project-path") 38 | preValidationToken := context.String("pre-validation-token") 39 | 40 | if vaultRole == "" { 41 | vaultRole = strings.ReplaceAll(ciProjectPath, "/", "-") 42 | } 43 | 44 | jwtToken := context.String("jwt-token") 45 | loginPath := context.String("vault-login-path") 46 | 47 | if vaultToken == "" && jwtToken == "" { 48 | err := errors.New("either vaultToken or jwtToken is required") 49 | return nil, err 50 | } 51 | 52 | config := &api.Config{ 53 | Address: vaultAddr, 54 | } 55 | 56 | vaultClient, err := api.NewClient(config) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | s := HashicorpService{ 62 | client: vaultClient, 63 | vaultAddr: vaultAddr, 64 | vaultExternalAddr: vaultExternalAddr, 65 | vaultRole: vaultRole, 66 | vaultRoot: vaultRoot, 67 | ciProjectPath: ciProjectPath, 68 | preValidationToken: preValidationToken, 69 | } 70 | 71 | if jwtToken != "" { 72 | err = s.getAuthToken(jwtToken, vaultRole, loginPath) 73 | if err != nil { 74 | return nil, fmt.Errorf("unable to authenticate to vault: %s", err.Error()) 75 | } 76 | } else { 77 | s.vaultToken = vaultToken 78 | } 79 | 80 | return &s, nil 81 | } 82 | 83 | func (s *HashicorpService) GetNamespaceConfig(namespaceConfigMount string) (config.NamespaceConfig, error) { 84 | vaultRes, err := s.getConfig(namespaceConfigMount) 85 | if err != nil { 86 | return config.DefaultNamespaceConfig, fmt.Errorf("unable to access namespace vault config at %s", namespaceConfigMount) 87 | } 88 | 89 | return config.ParseNamespaceConfig(vaultRes) 90 | } 91 | 92 | func (s *HashicorpService) GetProjectConfig(projectConfigMount string) (config.ProjectConfig, error) { 93 | vaultRes, err := s.getConfig(projectConfigMount) 94 | if err != nil { 95 | return config.DefaultProjectConfig, fmt.Errorf("unable to access project vault config at %s", projectConfigMount) 96 | } 97 | 98 | return config.ParseProjectConfig(vaultRes) 99 | } 100 | 101 | func (s *HashicorpService) getConfig(mount string) ([]byte, error) { 102 | s.client.SetToken(s.vaultToken) 103 | 104 | secret, err := s.client.Logical().Read(mount) 105 | var vaultRes []byte 106 | 107 | if err != nil { 108 | return vaultRes, err 109 | } 110 | 111 | // Run with default config 112 | if secret == nil { 113 | return vaultRes, nil 114 | } 115 | 116 | vaultRes, err = json.Marshal(secret.Data) 117 | if err != nil { 118 | return vaultRes, err 119 | } 120 | 121 | return vaultRes, nil 122 | } 123 | 124 | func (s *HashicorpService) ReadWhitelists(namespaceMount string, projectMount string) (whitelist.Whitelist, error) { 125 | s.client.SetToken(s.vaultToken) 126 | 127 | namespaceWhitelist, err := s.readWhitelist(namespaceMount) 128 | if err != nil { 129 | return namespaceWhitelist, fmt.Errorf("unable to read namespace whitelist at %s: %s", namespaceMount, err.Error()) 130 | } 131 | 132 | projectWhitelist, err := s.readWhitelist(projectMount) 133 | if err != nil { 134 | return projectWhitelist, fmt.Errorf("unable to read project whitelist at %s: %s", projectMount, err.Error()) 135 | } 136 | 137 | namespaceWhitelist.AddWhitelist(projectWhitelist) 138 | 139 | return namespaceWhitelist, nil 140 | } 141 | 142 | // Read the allowed images and scripts from a whitelist mount location 143 | func (s *HashicorpService) readWhitelist(secretPath string) (whitelist.Whitelist, error) { 144 | s.client.SetToken(s.vaultToken) 145 | 146 | secret, err := s.client.Logical().Read(secretPath) 147 | whitelist := whitelist.Whitelist{} 148 | 149 | if err != nil { 150 | return whitelist, err 151 | } 152 | 153 | if secret == nil { 154 | return whitelist, fmt.Errorf("invalid secret") 155 | } 156 | 157 | vaultRes, _ := json.Marshal(secret.Data) 158 | err = json.Unmarshal(vaultRes, &whitelist) 159 | 160 | if err != nil { 161 | return whitelist, err 162 | } 163 | 164 | return whitelist, nil 165 | } 166 | 167 | // Write scratch code and make sure it exists after writing 168 | func (s *HashicorpService) WriteScratch(pipelineId int, user string) (string, error) { 169 | secretMount := s.vaultRoot + "/" + s.ciProjectPath + "/" + "scratch" 170 | 171 | randomString, err := vaultclient.GenerateRandomStringURLSafe(16) 172 | if err != nil { 173 | return "", errors.New("an unknown error has occured with string generation") 174 | } 175 | 176 | s.client.SetToken(s.vaultToken) 177 | 178 | secret := make(map[string]interface{}) 179 | secret["CI/CD pipeline id"] = strconv.Itoa(pipelineId) 180 | 181 | secretPath := user + "/" + randomString 182 | _, err = s.client.Logical().Write(secretMount+"/"+secretPath, secret) 183 | if err != nil { 184 | return "", err 185 | } 186 | 187 | status, err := s.secretExists(secretMount + "/" + secretPath) 188 | if !status || err != nil { 189 | return secretPath, errors.New(" ❌ CI/CD run not authorized, secret not retrievable") 190 | } 191 | 192 | return secretPath, err 193 | } 194 | 195 | // Print to the console the instructions for deleting the scratch code 196 | func (s *HashicorpService) LogMFAInstructions(ciUserEmail string, loggerClient loggerclient.LoggerClient) { 197 | message := fmt.Sprintf("\nPlease delete the following scratch code to authorize this pipeline run -> %s/ui/vault/secrets/%s\n", 198 | s.vaultExternalAddr, s.vaultRoot+"/list/"+s.ciProjectPath+"/scratch/"+ciUserEmail) 199 | _ = loggerClient.SendMFAInstructions(message) 200 | } 201 | 202 | // Waits for the user to delete the scratch code in the hashicorp vault 203 | func (s *HashicorpService) WaitForMFA(timeout int, secretPath string) bool { 204 | secretMount := s.vaultRoot + "/" + s.ciProjectPath + "/" + "scratch" 205 | 206 | vaultCheckIntervalSeconds := 5 207 | 208 | // Every 5 seconds check whether the scratch code has been deleted 209 | for i := 1; i <= (timeout / vaultCheckIntervalSeconds); i++ { 210 | status, err := s.secretExists(secretMount + "/" + secretPath) 211 | 212 | if err != nil { 213 | fmt.Printf("%s", err.Error()) 214 | } 215 | 216 | if !status { 217 | return true 218 | } 219 | 220 | time.Sleep(time.Duration(vaultCheckIntervalSeconds) * time.Second) 221 | } 222 | 223 | return false 224 | } 225 | 226 | // If the user failed MFA, delete the scratch code 227 | func (s *HashicorpService) Cleanup(successful bool, secretPath string, checkType string) error { 228 | secretMount := s.vaultRoot + "/" + s.ciProjectPath + "/" + "scratch" 229 | 230 | if !successful { 231 | err := s.deleteSecret(secretMount + "/" + secretPath) 232 | if err != nil { 233 | return err 234 | } 235 | } else if checkType != "image" { 236 | err := vaultclient.CreateValidationToken(s.preValidationToken) 237 | if err != nil { 238 | return err 239 | } 240 | } 241 | 242 | return nil 243 | } 244 | 245 | // GetAuthToken obtains a vaultToken from a JWT token 246 | func (s *HashicorpService) getAuthToken(jwtToken string, role string, loginPath string) error { 247 | data := map[string]interface{}{ 248 | "jwt": jwtToken, 249 | "role": role, 250 | } 251 | 252 | token, err := s.client.Logical().Write(loginPath, data) 253 | if err != nil { 254 | return err 255 | } 256 | s.vaultToken = token.Auth.ClientToken 257 | return nil 258 | } 259 | 260 | // Check that a secret (scratch code) exists in the vault 261 | func (s *HashicorpService) secretExists(secretPath string) (bool, error) { 262 | s.client.SetToken(s.vaultToken) 263 | 264 | secret, err := s.client.Logical().Read(secretPath) 265 | 266 | if err != nil { 267 | return true, err 268 | } 269 | if secret == nil { 270 | return false, nil 271 | } 272 | 273 | return true, nil 274 | } 275 | 276 | // Delete a secret (scratch code) from the vault 277 | func (s *HashicorpService) deleteSecret(secretPath string) error { 278 | s.client.SetToken(s.vaultToken) 279 | 280 | _, err := s.client.Logical().Delete(secretPath) 281 | 282 | if err != nil { 283 | return err 284 | } 285 | 286 | return nil 287 | } 288 | -------------------------------------------------------------------------------- /pkg/vaultclient/vaultclient.go: -------------------------------------------------------------------------------- 1 | package vaultclient 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/kudelskisecurity/youshallnotpass/pkg/config" 12 | "github.com/kudelskisecurity/youshallnotpass/pkg/loggerclient" 13 | "github.com/kudelskisecurity/youshallnotpass/pkg/whitelist" 14 | ) 15 | 16 | var ( 17 | ErrUnsupportedNumBytes = errors.New("unsupported number of bytes") 18 | ErrCannotCreateToken = errors.New("unable to create validation token") 19 | ErrPrevalidationExists = errors.New("pre-validation file already exists") 20 | ) 21 | 22 | type VaultClient interface { 23 | GetNamespaceConfig(string) (config.NamespaceConfig, error) 24 | GetProjectConfig(string) (config.ProjectConfig, error) 25 | ReadWhitelists(string, string) (whitelist.Whitelist, error) 26 | WriteScratch(int, string) (string, error) 27 | LogMFAInstructions(string, loggerclient.LoggerClient) 28 | WaitForMFA(int, string) bool 29 | Cleanup(bool, string, string) error 30 | } 31 | 32 | func generateRandomBytes(n int) ([]byte, error) { 33 | if n < 0 { 34 | return nil, ErrUnsupportedNumBytes 35 | } 36 | 37 | b := make([]byte, n) 38 | _, err := rand.Read(b) 39 | // Note that err == nil only if we read len(b) bytes. 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return b, nil 45 | } 46 | 47 | func GenerateRandomStringURLSafe(n int) (string, error) { 48 | if n < 0 { 49 | return "", ErrUnsupportedNumBytes 50 | } 51 | 52 | b, err := generateRandomBytes(n) 53 | b64String := base64.URLEncoding.EncodeToString(b) 54 | string := strings.Trim(b64String, "=") 55 | 56 | return string, err 57 | } 58 | 59 | func TokenCreated(validationToken string) bool { 60 | dir, err := os.Getwd() 61 | if err != nil { 62 | return false 63 | } 64 | 65 | _, err = os.Stat(dir + "/" + validationToken) 66 | 67 | return !os.IsNotExist(err) 68 | } 69 | 70 | func CreateValidationToken(validationToken string) error { 71 | dir, _ := os.Getwd() 72 | 73 | if !TokenCreated(validationToken) { 74 | file, err := os.Create(dir + "/" + validationToken) 75 | if err != nil { 76 | return ErrCannotCreateToken 77 | } 78 | 79 | defer func() { 80 | if err := file.Close(); err != nil { 81 | fmt.Printf("Error closing file: %s\n", err) 82 | } 83 | }() 84 | 85 | return nil 86 | } 87 | 88 | return ErrPrevalidationExists 89 | } 90 | -------------------------------------------------------------------------------- /pkg/vaultclient/vaultclient_test.go: -------------------------------------------------------------------------------- 1 | package vaultclient 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestGenerateRandomBytes(t *testing.T) { 10 | generateRandomBytesTests := []struct { 11 | name string 12 | bytes int 13 | error error 14 | }{ 15 | { 16 | name: "generate valid random bytes", 17 | bytes: 13, 18 | error: nil, 19 | }, 20 | { 21 | name: "generate random bytes negative", 22 | bytes: -20, 23 | error: ErrUnsupportedNumBytes, 24 | }, 25 | } 26 | 27 | for testNum, test := range generateRandomBytesTests { 28 | bytes, err := generateRandomBytes(test.bytes) 29 | if err != nil { 30 | if err != test.error { 31 | t.Errorf("\n%d) unexpected error -\nexpected: (%+v)\ngot: (%+v)", testNum, test.error, err) 32 | } 33 | } else if len(bytes) != test.bytes { 34 | t.Errorf("\n%d) unexpected number of bytes generated -\nexpected: (%d)\ngot: (%d)", testNum, test.bytes, len(bytes)) 35 | } 36 | } 37 | } 38 | 39 | func TestGenerateRandomStringURLSafe(t *testing.T) { 40 | generateRandomStringTests := []struct { 41 | name string 42 | bytes int 43 | urlLength int 44 | error error 45 | }{ 46 | { 47 | name: "generate valid random string safe", 48 | bytes: 13, 49 | urlLength: 18, 50 | error: nil, 51 | }, 52 | { 53 | name: "generate invalid random string negative bytes", 54 | bytes: -20, 55 | urlLength: 0, 56 | error: ErrUnsupportedNumBytes, 57 | }, 58 | } 59 | 60 | for testNum, test := range generateRandomStringTests { 61 | str, err := GenerateRandomStringURLSafe(test.bytes) 62 | if test.error != err { 63 | t.Errorf("\n%d) unexpected error -\nexpected: (%+v)\ngot: (%+v)", testNum, test.error, err) 64 | } 65 | 66 | if len(str) != test.urlLength { 67 | t.Errorf("\n%d) unexpected url length -\nexpected: (%d)\ngot: (%d)", testNum, test.urlLength, len(str)) 68 | } 69 | } 70 | } 71 | 72 | func TestCreateValidationToken(t *testing.T) { 73 | generateRandomStringIgnoreError := func() string { 74 | str, _ := GenerateRandomStringURLSafe(12) 75 | return str 76 | } 77 | 78 | createValidationTokenTests := []struct { 79 | name string 80 | tokenName string 81 | }{ 82 | { 83 | name: "create validation token named validation_token", 84 | tokenName: "validation_token", 85 | }, 86 | { 87 | name: "create random validation token", 88 | tokenName: generateRandomStringIgnoreError(), 89 | }, 90 | } 91 | 92 | for testNum, test := range createValidationTokenTests { 93 | err := CreateValidationToken(test.tokenName) 94 | if err != nil { 95 | t.Errorf("\n%d) Expected err to be nil, but got %s", testNum, err.Error()) 96 | return 97 | } 98 | 99 | dir, err := os.Getwd() 100 | if err != nil { 101 | t.Errorf("\n%d) Expected err to be nil, but got %s", testNum, err.Error()) 102 | return 103 | } 104 | 105 | _, err = os.Stat(dir + "/" + test.tokenName) 106 | if err != nil { 107 | t.Errorf("\n%d) File was not created successfully", testNum) 108 | return 109 | } 110 | 111 | err = os.Remove(dir + "/" + test.tokenName) 112 | if err != nil { 113 | t.Errorf("\n%d) Could not remove the validation token file", testNum) 114 | return 115 | } 116 | } 117 | } 118 | 119 | func TestValidationTokenCreated(t *testing.T) { 120 | err := CreateValidationToken("validation_token") 121 | if err != nil { 122 | t.Errorf("Expected err to be nil, but got %s", err.Error()) 123 | return 124 | } 125 | 126 | created := TokenCreated("validation_token") 127 | if !created { 128 | t.Error("Expected the token to exist") 129 | return 130 | } 131 | 132 | dir, _ := os.Getwd() 133 | err = os.Remove(dir + "/" + "validation_token") 134 | if err != nil { 135 | t.Error("Could not remove the validation token file") 136 | return 137 | } 138 | } 139 | 140 | func TestCreateValidationTokenTwice(t *testing.T) { 141 | err := CreateValidationToken("validation_token") 142 | if err != nil { 143 | t.Errorf("Expected err to be nil, but got %s", err.Error()) 144 | return 145 | } 146 | 147 | err = CreateValidationToken("validation_token") 148 | print(err.Error()) 149 | if !strings.Contains(err.Error(), "pre-validation file already exists") { 150 | t.Error("Expected pre-validation file to already exist") 151 | return 152 | } 153 | 154 | dir, _ := os.Getwd() 155 | err = os.Remove(dir + "/" + "validation_token") 156 | if err != nil { 157 | t.Error("Could not remove the validation token file") 158 | return 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /pkg/vaultclient/vaultclientparser/vault_client_parser.go: -------------------------------------------------------------------------------- 1 | package vaultclientparser 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var ErrUnknownVaultClient = errors.New("could not parse vault client") 9 | 10 | // This exists to allow for unit testing of the parsing because creating a new 11 | // client requires access to the respective vault 12 | func ParseClient(clientstring string) (string, error) { 13 | clientstring = strings.ToLower(clientstring) 14 | 15 | if strings.Contains("hashicorp", clientstring) { 16 | return "Hashicorp", nil 17 | } 18 | 19 | return "", ErrUnknownVaultClient 20 | } 21 | -------------------------------------------------------------------------------- /pkg/vaultclient/vaultclientparser/vault_client_parser_test.go: -------------------------------------------------------------------------------- 1 | package vaultclientparser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestVaultClientParser(t *testing.T) { 8 | parseVaultClientTests := []struct { 9 | name string 10 | clientName string 11 | expectedClient string 12 | error error 13 | }{ 14 | { 15 | name: "test parse hashicorp from lowercase", 16 | clientName: "hashicorp", 17 | expectedClient: "Hashicorp", 18 | error: nil, 19 | }, 20 | { 21 | name: "test parse hashicorp from uppercase", 22 | clientName: "HASHICORP", 23 | expectedClient: "Hashicorp", 24 | error: nil, 25 | }, 26 | { 27 | name: "test parse hashicorp from odd capitalization", 28 | clientName: "HaShIcOrP", 29 | expectedClient: "Hashicorp", 30 | error: nil, 31 | }, 32 | { 33 | name: "test parse unknown client", 34 | clientName: "ashjfaoiuehpao", 35 | expectedClient: "", 36 | error: ErrUnknownVaultClient, 37 | }, 38 | } 39 | 40 | for testNum, test := range parseVaultClientTests { 41 | client, err := ParseClient(test.clientName) 42 | 43 | if err != test.error { 44 | t.Errorf("\n%d) unexpected error occurred - \nexpected: (%+v)\ngot: (%+v)", testNum, test.error, err) 45 | } 46 | 47 | if client != test.expectedClient { 48 | t.Errorf("\n%d) unexpected client parsed - \nexpected: (%s)\ngot: (%s)", testNum, test.expectedClient, client) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/whitelist/whitelist.go: -------------------------------------------------------------------------------- 1 | package whitelist 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | ErrShaNotPresent = errors.New("@sha256 not provided in image name") 10 | ErrShaNotValidSha = errors.New("@Sha 256 not valid sha256. Expected: name:tag@sha256:") 11 | ErrInvalidWhitelistJob = errors.New("invalid script name for whitelist script. Expected: @sha256:") 12 | ) 13 | 14 | type Whitelist struct { 15 | AllowedImages []string `json:"allowed_images"` 16 | AllowedScripts []string `json:"allowed_scripts"` 17 | } 18 | 19 | func (s Whitelist) ContainsImage(image string) (bool, error) { 20 | imageSha, err := getImageSha(image) 21 | if err != nil { 22 | return false, err 23 | } 24 | 25 | for _, v := range s.AllowedImages { 26 | sha256, _ := getImageSha(v) 27 | if sha256 == imageSha { 28 | return true, nil 29 | } 30 | } 31 | 32 | return false, nil 33 | } 34 | 35 | // Checks through the ScriptWhitelist to see if our script hs allowed. 36 | func (s Whitelist) ContainsScript(scriptSha string) bool { 37 | for _, script := range s.AllowedScripts { 38 | // get script plaintext and hash 39 | sha256, _ := getScriptSha(script) 40 | if sha256 == scriptSha { 41 | return true 42 | } 43 | } 44 | 45 | return false 46 | } 47 | 48 | func (s Whitelist) ContainsJobName(JobName string) (bool, string) { 49 | for _, script := range s.AllowedScripts { 50 | whitelist_JobName, _ := getJobName(script) 51 | if whitelist_JobName == JobName { 52 | sha256, err := getScriptSha(script) 53 | if err != nil { 54 | continue 55 | } 56 | return true, sha256 57 | } 58 | } 59 | 60 | return false, "" 61 | } 62 | 63 | func (w *Whitelist) AddWhitelist(other Whitelist) { 64 | w.AllowedImages = append(w.AllowedImages, other.AllowedImages...) 65 | w.AllowedScripts = append(w.AllowedScripts, other.AllowedScripts...) 66 | } 67 | 68 | func getJobName(whitelistScript string) (string, error) { 69 | scriptParts := strings.Split(whitelistScript, "@") 70 | 71 | if len(scriptParts[0]) == 0 { 72 | return "", ErrInvalidWhitelistJob 73 | } 74 | 75 | return scriptParts[0], nil 76 | } 77 | 78 | func getScriptSha(whitelistScript string) (string, error) { 79 | script := strings.Split(whitelistScript, "@") 80 | 81 | // 256 bits / 4 bits per char = 64 characters 82 | shaLen := 256 / 8 83 | 84 | if len(script) == 1 { 85 | return "", ErrShaNotPresent 86 | } 87 | 88 | if len(script[1]) < (len("sha256") + shaLen) { 89 | return "", ErrShaNotValidSha 90 | } 91 | 92 | return script[1], nil 93 | } 94 | 95 | func getImageSha(dockerImage string) (string, error) { 96 | image := strings.Split(dockerImage, "@") 97 | 98 | // 256 bites / 4 bits per char = 64 characters 99 | shaLen := 256 / 4 100 | 101 | if len(image) == 1 { 102 | return "", ErrShaNotPresent 103 | } 104 | 105 | if len(image[1]) < (len("sha256") + shaLen) { 106 | return "", ErrShaNotValidSha 107 | } 108 | 109 | return image[1], nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/whitelist/whitelist_test.go: -------------------------------------------------------------------------------- 1 | package whitelist 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestContainsImage(t *testing.T) { 9 | containsImageTests := []struct { 10 | name string 11 | whitelist Whitelist 12 | image string 13 | foundImg bool 14 | err error 15 | }{ 16 | { 17 | name: "test contains image (image in whitelist)", 18 | whitelist: Whitelist{ 19 | AllowedImages: []string{ 20 | "alpine:3.12.7@sha256:a9c28c813336ece5bb98b36af5b66209ed777a394f4f856c6e62267790883820", 21 | }, 22 | }, 23 | image: "alpine:3.12.7@sha256:a9c28c813336ece5bb98b36af5b66209ed777a394f4f856c6e62267790883820", 24 | foundImg: true, 25 | err: nil, 26 | }, 27 | { 28 | name: "test contains image (image not in whitelist)", 29 | whitelist: Whitelist{ 30 | AllowedImages: []string{ 31 | "alpine:3.12.7@sha256:a9c28c813336ece5bb98b36af5b66209ed777a394f4f856c6e62267790883820", 32 | }, 33 | }, 34 | image: "alpine:3.12.7@sha256:a9c28c813336ece5bb98b36af5b66209ed777a394f4f856c6e62267790883abc", 35 | foundImg: false, 36 | err: nil, 37 | }, 38 | { 39 | name: "test contains image invalid image (error)", 40 | whitelist: Whitelist{ 41 | AllowedImages: []string{ 42 | "alpine:3.12.7@sha256:a9c28c813336ece5bb98b36af5b66209ed777a394f4f856c6e62267790883820", 43 | }, 44 | }, 45 | image: "alping:3.12.7@sha256:abc", 46 | foundImg: false, 47 | err: ErrShaNotValidSha, 48 | }, 49 | } 50 | 51 | for testNum, test := range containsImageTests { 52 | got, err := test.whitelist.ContainsImage(test.image) 53 | if err != test.err { 54 | t.Errorf("\n%d) unexpected error -\nexpected: (%+v)\ngot: (%+v)", testNum, test.err, err) 55 | } 56 | 57 | if got != test.foundImg { 58 | t.Errorf("\n%d) unexpected found image -\nexpected: (%t)\ngot: (%t)", testNum, test.foundImg, got) 59 | } 60 | } 61 | } 62 | 63 | func TestContainsScript(t *testing.T) { 64 | containsScriptTests := []struct { 65 | name string 66 | whitelist Whitelist 67 | scriptSha string 68 | foundScript bool 69 | }{ 70 | { 71 | name: "test contains script (script in whitelist)", 72 | whitelist: Whitelist{ 73 | AllowedScripts: []string{ 74 | "automatic_job@sha256:oNey8xJbYXyuxWr7Wla8tMexCTy7s82k6U1uwp4tFEY=", 75 | }, 76 | }, 77 | scriptSha: "sha256:oNey8xJbYXyuxWr7Wla8tMexCTy7s82k6U1uwp4tFEY=", 78 | foundScript: true, 79 | }, 80 | { 81 | name: "test contains script (script not in whitelist)", 82 | whitelist: Whitelist{ 83 | AllowedScripts: []string{ 84 | "automatic_job@sha256:oNey8xJbYXyuxWr7Wla8tMexCTy7s82k6U1uwp4tFEY=", 85 | }, 86 | }, 87 | scriptSha: "build_job@sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 88 | foundScript: false, 89 | }, 90 | { 91 | name: "test contains script (invalid script hash format)", 92 | whitelist: Whitelist{ 93 | AllowedScripts: []string{ 94 | "automatic_job@sha256:oNey8xJbY=", 95 | }, 96 | }, 97 | scriptSha: "sha256:oNey8xJbYXyuxWr7Wla8tMexCTy7s82k6U1uwp4tFEY=", 98 | foundScript: false, 99 | }, 100 | } 101 | 102 | for testNum, test := range containsScriptTests { 103 | got := test.whitelist.ContainsScript(test.scriptSha) 104 | if got != test.foundScript { 105 | t.Errorf("\n%d) unexpected found script -\nexpected: (%t)\ngot: (%t)", testNum, test.foundScript, got) 106 | } 107 | } 108 | } 109 | 110 | func TestContainsJobName(t *testing.T) { 111 | containsJobNameTests := []struct { 112 | name string 113 | whitelist Whitelist 114 | jobName string 115 | foundJobName bool 116 | foundJobHash string 117 | }{ 118 | { 119 | name: "whitelist contains job name", 120 | whitelist: Whitelist{ 121 | AllowedScripts: []string{ 122 | "testJob@sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 123 | }, 124 | }, 125 | jobName: "testJob", 126 | foundJobName: true, 127 | foundJobHash: "sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 128 | }, 129 | { 130 | name: "whitelist does not contain job name", 131 | whitelist: Whitelist{ 132 | AllowedScripts: []string{ 133 | "build_job@sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 134 | }, 135 | }, 136 | jobName: "testJob", 137 | foundJobName: false, 138 | foundJobHash: "", 139 | }, 140 | } 141 | 142 | for testNum, test := range containsJobNameTests { 143 | got, hash := test.whitelist.ContainsJobName(test.jobName) 144 | if got != test.foundJobName { 145 | t.Errorf("\n%d) unexpected found job name -\nexpected: (%t)\ngot: (%t)", testNum, test.foundJobName, got) 146 | } 147 | 148 | if hash != test.foundJobHash { 149 | t.Errorf("\n%d) unexpected hash found -\nexpected: (%s)\ngot: (%s)", testNum, test.foundJobHash, hash) 150 | } 151 | } 152 | } 153 | 154 | func TestAddWhitelist(t *testing.T) { 155 | addWhitelistTests := []struct { 156 | name string 157 | whitelistOne Whitelist 158 | whitelistTwo Whitelist 159 | expectedWhitelist Whitelist 160 | }{ 161 | { 162 | name: "add empty whitelists", 163 | whitelistOne: Whitelist{}, 164 | whitelistTwo: Whitelist{}, 165 | expectedWhitelist: Whitelist{}, 166 | }, 167 | { 168 | name: "add whitelists allowed images and scripts", 169 | whitelistOne: Whitelist{ 170 | AllowedImages: []string{ 171 | "imageOne", 172 | "imageTwo", 173 | }, 174 | AllowedScripts: []string{ 175 | "scriptOne", 176 | "scriptTwo", 177 | }, 178 | }, 179 | whitelistTwo: Whitelist{ 180 | AllowedImages: []string{ 181 | "imageThree", 182 | }, 183 | AllowedScripts: []string{ 184 | "scriptThree", 185 | }, 186 | }, 187 | expectedWhitelist: Whitelist{ 188 | AllowedImages: []string{ 189 | "imageOne", 190 | "imageTwo", 191 | "imageThree", 192 | }, 193 | AllowedScripts: []string{ 194 | "scriptOne", 195 | "scriptTwo", 196 | "scriptThree", 197 | }, 198 | }, 199 | }, 200 | } 201 | 202 | for testNum, test := range addWhitelistTests { 203 | test.whitelistOne.AddWhitelist(test.whitelistTwo) 204 | if !reflect.DeepEqual(test.whitelistOne, test.expectedWhitelist) { 205 | t.Errorf("\n%d) expected whitelists to be equal -\nexpected: (%+v)\ngot: (%+v)", testNum, test.whitelistOne, test.expectedWhitelist) 206 | } 207 | } 208 | } 209 | 210 | func TestGetJobName(t *testing.T) { 211 | getJobNameTests := []struct { 212 | name string 213 | whitelistScript string 214 | jobName string 215 | err error 216 | }{ 217 | { 218 | name: "get job name normal", 219 | whitelistScript: "build-script@sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 220 | jobName: "build-script", 221 | err: nil, 222 | }, 223 | { 224 | name: "get job name invalid name (i.e. empty name)", 225 | whitelistScript: "@sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 226 | jobName: "", 227 | err: ErrInvalidWhitelistJob, 228 | }, 229 | } 230 | 231 | for testNum, test := range getJobNameTests { 232 | jobName, err := getJobName(test.whitelistScript) 233 | if err != test.err { 234 | t.Errorf("\n%d) unexpected error -\nexpected: (%+v)\ngot: (%+v)", testNum, test.err, err) 235 | } 236 | 237 | if jobName != test.jobName { 238 | t.Errorf("\n%d) unexpected job name -\nexpected: (%s)\ngot: (%s)", testNum, test.jobName, jobName) 239 | } 240 | } 241 | } 242 | 243 | func TestGetScriptSha(t *testing.T) { 244 | getScriptShaTests := []struct { 245 | name string 246 | whitelistScript string 247 | sha string 248 | err error 249 | }{ 250 | { 251 | name: "get script sha expected whitelist script", 252 | whitelistScript: "build-script@sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 253 | sha: "sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 254 | err: nil, 255 | }, 256 | { 257 | name: "get script sha no sha", 258 | whitelistScript: "build-script", 259 | sha: "", 260 | err: ErrShaNotPresent, 261 | }, 262 | { 263 | name: "get script sha no job name", 264 | whitelistScript: "sha256:1NmXdCi0PhRNMKX91bypxJKJhzB1nUQqtRowPbgpqqE=", 265 | sha: "", 266 | err: ErrShaNotPresent, 267 | }, 268 | { 269 | name: "get script sha empty whitelist script", 270 | whitelistScript: "", 271 | sha: "", 272 | err: ErrShaNotPresent, 273 | }, 274 | { 275 | name: "get script sha invalid sha", 276 | whitelistScript: "build-script@sha256:1NmXdCi0PhRNMK", 277 | sha: "", 278 | err: ErrShaNotValidSha, 279 | }, 280 | } 281 | 282 | for testNum, test := range getScriptShaTests { 283 | sha, err := getScriptSha(test.whitelistScript) 284 | if err != test.err { 285 | t.Errorf("\n%d) unexpected error -\nexpected: (%+v)\ngot: (%+v)", testNum, test.err, err) 286 | } 287 | 288 | if sha != test.sha { 289 | t.Errorf("\n%d) unexpected sha found -\nexpected: (%s)\ngot: (%s)", testNum, test.sha, sha) 290 | } 291 | } 292 | } 293 | 294 | func TestGetImageSha(t *testing.T) { 295 | getImageShaTests := []struct { 296 | name string 297 | whitelistImage string 298 | sha string 299 | err error 300 | }{ 301 | { 302 | name: "get image sha expected whitelist", 303 | whitelistImage: "alpine:latest@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 304 | sha: "sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 305 | err: nil, 306 | }, 307 | { 308 | name: "get image sha no sha", 309 | whitelistImage: "alpine:3.13.1", 310 | sha: "", 311 | err: ErrShaNotPresent, 312 | }, 313 | { 314 | name: "get image sha not image name", 315 | whitelistImage: "sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748", 316 | sha: "", 317 | err: ErrShaNotPresent, 318 | }, 319 | { 320 | name: "get image sha empty whitelist image", 321 | whitelistImage: "", 322 | sha: "", 323 | err: ErrShaNotPresent, 324 | }, 325 | { 326 | name: "get image sha invalid sha", 327 | whitelistImage: "alpine:latest@sha256:def822f9851ca422481ec6f", 328 | sha: "", 329 | err: ErrShaNotValidSha, 330 | }, 331 | } 332 | 333 | for testNum, test := range getImageShaTests { 334 | sha, err := getImageSha(test.whitelistImage) 335 | if err != test.err { 336 | t.Errorf("\n%d) unexpected error -\nexpected: (%+v)\ngot: (%+v)", testNum, test.err, err) 337 | } 338 | 339 | if sha != test.sha { 340 | t.Errorf("\n%d) unexpected sha -\nexpected: (%s)\ngot: (%s)", testNum, test.sha, sha) 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "prConcurrentLimit": 5, 7 | "packageRules": [ 8 | { 9 | "packagePatterns": [ "eslint", "prettier" ], 10 | "groupName": "lint" 11 | }, 12 | { 13 | "matchPackagePatterns": [ "golang" ], 14 | "groupName": "golang" 15 | } 16 | ], 17 | "npmrc": "//https://registry.npmjs.org/" 18 | } 19 | 20 | -------------------------------------------------------------------------------- /testing/Dockerfiles/Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18.4@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 2 | 3 | WORKDIR /app 4 | COPY . . 5 | RUN apk update && \ 6 | apk add go && \ 7 | go mod download && \ 8 | go mod verify && \ 9 | GO111MODULE=on go get github.com/githubnemo/CompileDaemon && \ 10 | go install github.com/githubnemo/CompileDaemon && \ 11 | go build -o /usr/local/bin/youshallnotpass . 12 | 13 | ENTRYPOINT /root/go/bin/CompileDaemon --build="go build -o /usr/local/bin/youshallnotpass" 14 | -------------------------------------------------------------------------------- /testing/Dockerfiles/Dockerfile-gitlab: -------------------------------------------------------------------------------- 1 | FROM golang:1.20@sha256:5865f52f9f277b951610d2ab0b7a14b24cadef7709db26de3320c018fbd4550c AS builder 2 | 3 | ARG GOPROXY 4 | 5 | WORKDIR /app 6 | COPY . . 7 | RUN go mod download && \ 8 | go mod verify && \ 9 | CGO_ENABLED=0 go build -o main \ 10 | . 11 | 12 | FROM gitlab/gitlab-runner:alpine-bleeding 13 | 14 | ARG CI_COMMIT_REF_NAME="development" 15 | 16 | ENV RUNNER_BUILDS_DIR="/tmp/builds" \ 17 | RUNNER_CACHE_DIR="/tmp/cache" \ 18 | CUSTOM_CONFIG_EXEC="/var/gitlab_custom_executor/config.sh" \ 19 | CUSTOM_PREPARE_EXEC="/var/gitlab_custom_executor/prepare.sh" \ 20 | CUSTOM_RUN_EXEC="/var/gitlab_custom_executor/run.sh" \ 21 | CUSTOM_CLEANUP_EXEC="/var/gitlab_custom_executor/cleanup.sh" 22 | 23 | RUN apk add --no-cache docker-cli jq openssl && rm -rf /var/cache/apk/* 24 | 25 | COPY --from=builder /app/main /usr/local/bin/youshallnotpass 26 | COPY custom_executors/gitlab_custom_executor /var/custom-executor 27 | -------------------------------------------------------------------------------- /testing/colors.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash disable=SC2034 2 | BLACK='\033[0;30m' 3 | RED='\033[0;31m' 4 | GREEN='\033[0;32m' 5 | BROWN='\033[0;33m' 6 | BLUE='\033[0;34m' 7 | PURPLE='\033[0;35m' 8 | CYAN='\033[0;36m' 9 | LIGHT_GRAY='\033[0;37m' 10 | DARK_GRAY='\033[1;30m' 11 | LIGHT_RED='\033[1;31m' 12 | LIGHT_GREEN='\033[1;32m' 13 | YELLOW='\033[1;33m' 14 | LIGHT_BLUE='\033[1;34m' 15 | LIGHT_PURPLE='\033[1;35m' 16 | LIGHT_CYAN='\033[1;36m' 17 | WHITE='\033[1;37m' 18 | NC='\033[0m' -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_auth_timeout/runner-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | youshallnotpass_builder_daemon: 5 | build: 6 | context: ../../../ 7 | dockerfile: testing/Dockerfiles/Dockerfile-dev 8 | volumes: 9 | - usr_local_bin:/usr/local/bin 10 | - ../../../:/app 11 | 12 | gitlab_runner: 13 | build: 14 | context: ../../../ 15 | dockerfile: testing/Dockerfiles/Dockerfile-gitlab 16 | environment: 17 | VAULT_ADDR: "http://vault:8200" 18 | VAULT_EXTERNAL_ADDR: "http://localhost:8200" 19 | DOCKER_RUN_ARGS: --volume hashicorp_gitlab_auth_timeout_git_repo:/gitrepo 20 | GIT_STRATEGY: clone 21 | YOUSHALLNOTPASS_GENERATE_JWT: "true" 22 | CI_PROJECT_PATH: youshallnotpass/demo 23 | CI_PROJECT_NAMESPACE: youshallnotpass 24 | YOUSHALLNOTPASS_TIMEOUT: 10 25 | working_dir: /gitrepo 26 | volumes: 27 | - /var/run/docker.sock:/var/run/docker.sock 28 | - ../../../custom_executors/gitlab_custom_executor:/var/gitlab_custom_executor 29 | - git_repo:/gitrepo 30 | - usr_local_bin:/usr/local/bin 31 | - certs:/certs 32 | command: | 33 | exec custom user_mfa_timeout_job 34 | 35 | git_repo_init: 36 | image: alpine/git 37 | volumes: 38 | - ./../../scripts/git-init.sh:/git-init.sh 39 | - git_repo:/gitrepo 40 | entrypoint: /bin/sh 41 | working_dir: /gitrepo 42 | command: /git-init.sh 43 | 44 | volumes: 45 | git_repo: 46 | usr_local_bin: 47 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_auth_timeout/vault-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | vault: 5 | image: hashicorp/vault 6 | restart: unless-stopped 7 | ports: 8 | - 8200:8200 9 | environment: 10 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 11 | VAULT_ADDR: "http://vault:8200" 12 | VAULT_TOKEN: "1234567890" 13 | 14 | vault_init: 15 | image: hashicorp/vault 16 | command: ["/vault-init.sh"] 17 | environment: 18 | VAULT_DEV_ROOT_TOKEN: "1234567890" 19 | VAULT_ADDR: "http://vault:8200" 20 | VAULT_TOKEN: "1234567890" 21 | volumes: 22 | - ./../../scripts/vault-init.sh:/vault-init.sh 23 | - certs:/certs 24 | depends_on: 25 | - vault 26 | 27 | volumes: 28 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_automatic/runner-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | youshallnotpass_builder_daemon: 5 | build: 6 | context: ../../../ 7 | dockerfile: testing/Dockerfiles/Dockerfile-dev 8 | volumes: 9 | - usr_local_bin:/usr/local/bin 10 | - ../../../:/app 11 | 12 | gitlab_runner: 13 | build: 14 | context: ../../../ 15 | dockerfile: testing/Dockerfiles/Dockerfile-gitlab 16 | environment: 17 | VAULT_ADDR: "http://vault:8200/" 18 | VAULT_EXTERNAL_ADDR: "http://localhost:8200" 19 | DOCKER_RUN_ARGS: --volume hashicorp_gitlab_automatic_git_repo:/gitrepo 20 | GIT_STRATEGY: clone 21 | YOUSHALLNOTPASS_GENERATE_JWT: "true" 22 | CI_PROJECT_PATH: youshallnotpass/demo 23 | CI_PROJECT_NAMESPACE: youshallnotpass 24 | working_dir: /gitrepo 25 | volumes: 26 | - /var/run/docker.sock:/var/run/docker.sock 27 | - ../../../custom_executors/gitlab_custom_executor:/var/gitlab_custom_executor 28 | - git_repo:/gitrepo 29 | - usr_local_bin:/usr/local/bin 30 | - certs:/certs 31 | command: | 32 | exec custom automatic_job 33 | 34 | git_repo_init: 35 | image: alpine/git 36 | volumes: 37 | - ./../../scripts/git-init.sh:/git-init.sh 38 | - git_repo:/gitrepo 39 | entrypoint: /bin/sh 40 | working_dir: /gitrepo 41 | command: /git-init.sh 42 | 43 | volumes: 44 | git_repo: 45 | usr_local_bin: 46 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_automatic/vault-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | vault: 5 | image: hashicorp/vault 6 | restart: unless-stopped 7 | ports: 8 | - 8200:8200 9 | environment: 10 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 11 | VAULT_ADDR: "http://vault:8200" 12 | VAULT_TOKEN: "1234567890" 13 | 14 | vault_init: 15 | image: hashicorp/vault 16 | command: ["/vault-init.sh" ] 17 | environment: 18 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 19 | VAULT_ADDR: "http://vault:8200" 20 | VAULT_TOKEN: "1234567890" 21 | volumes: 22 | - ./../../scripts/vault-init.sh:/vault-init.sh 23 | - certs:/certs 24 | depends_on: 25 | - vault 26 | 27 | volumes: 28 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_bash/runner-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | youshallnotpass_builder_daemon: 5 | build: 6 | context: ../../../ 7 | dockerfile: testing/Dockerfiles/Dockerfile-dev 8 | volumes: 9 | - usr_local_bin:/usr/local/bin 10 | - ../../../:/app 11 | 12 | gitlab_runner: 13 | build: 14 | context: ../../../ 15 | dockerfile: testing/Dockerfiles/Dockerfile-gitlab 16 | environment: 17 | VAULT_ADDR: "http://vault:8200" 18 | VAULT_EXTERNAL_ADDR: "http://localhost:8200" 19 | DOCKER_RUN_ARGS: --volume hashicorp_gitlab_bash_git_repo:/gitrepo 20 | GIT_STRATEGY: clone 21 | YOUSHALLNOTPASS_GENERATE_JWT: "true" 22 | CI_PROJECT_PATH: youshallnotpass/demo 23 | CI_PROJECT_NAMESPACE: youshallnotpass 24 | working_dir: /gitrepo 25 | volumes: 26 | - /var/run/docker.sock:/var/run/docker.sock 27 | - ../../../custom_executors/gitlab_custom_executor:/var/gitlab_custom_executor 28 | - git_repo:/gitrepo 29 | - usr_local_bin:/usr/local/bin 30 | - certs:/certs 31 | command: | 32 | exec custom script_job 33 | 34 | git_repo_init: 35 | image: alpine/git 36 | volumes: 37 | - ./../../scripts/git-init.sh:/git-init.sh 38 | - git_repo:/gitrepo 39 | entrypoint: /bin/sh 40 | working_dir: /gitrepo 41 | command: /git-init.sh 42 | 43 | volumes: 44 | git_repo: 45 | usr_local_bin: 46 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_bash/vault-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | vault: 5 | image: hashicorp/vault 6 | restart: unless-stopped 7 | ports: 8 | - 8200:8200 9 | environment: 10 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 11 | VAULT_ADDR: "http://vault:8200" 12 | VAULT_TOKEN: "1234567890" 13 | 14 | vault_init: 15 | image: hashicorp/vault 16 | command: ["/vault-init.sh"] 17 | environment: 18 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 19 | VAULT_ADDR: "http://vault:8200" 20 | VAULT_TOKEN: "1234567890" 21 | volumes: 22 | - ./../../scripts/vault-init.sh:/vault-init.sh 23 | - certs:/certs 24 | depends_on: 25 | - vault 26 | 27 | volumes: 28 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_fail/runner-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | youshallnotpass_builder_daemon: 5 | build: 6 | context: ../../../ 7 | dockerfile: testing/Dockerfiles/Dockerfile-dev 8 | volumes: 9 | - usr_local_bin:/usr/local/bin 10 | - ../../../:/app 11 | 12 | gitlab_runner: 13 | build: 14 | context: ../../../ 15 | dockerfile: testing/Dockerfiles/Dockerfile-gitlab 16 | environment: 17 | VAULT_ADDR: "http://vault:8200" 18 | VAULT_EXTERNAL_ADDR: "http://localhost:8200" 19 | DOCKER_RUN_ARGS: --volume hashicorp_gitlab_fail_git_repo:/gitrepo 20 | GIT_STRATEGY: clone 21 | YOUSHALLNOTPASS_GENERATE_JWT: "true" 22 | CI_PROJECT_PATH: youshallnotpass/demo 23 | CI_PROJECT_NAMESPACE: youshallnotpass 24 | working_dir: /gitrepo 25 | volumes: 26 | - /var/run/docker.sock:/var/run/docker.sock 27 | - ../../../custom_executors/gitlab_custom_executor:/var/gitlab_custom_executor 28 | - git_repo:/gitrepo 29 | - usr_local_bin:/usr/local/bin 30 | - certs:/certs 31 | command: | 32 | exec custom fail_job 33 | 34 | git_repo_init: 35 | image: alpine/git 36 | volumes: 37 | - ./../../scripts/git-init.sh:/git-init.sh 38 | - git_repo:/gitrepo 39 | entrypoint: /bin/sh 40 | working_dir: /gitrepo 41 | command: /git-init.sh 42 | 43 | volumes: 44 | git_repo: 45 | usr_local_bin: 46 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_fail/vault-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | vault: 5 | image: hashicorp/vault 6 | restart: unless-stopped 7 | ports: 8 | - 8200:8200 9 | environment: 10 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 11 | VAULT_ADDR: "http://vault:8200" 12 | VAULT_TOKEN: "1234567890" 13 | 14 | vault_init: 15 | image: hashicorp/vault 16 | command: ["/vault-init.sh"] 17 | environment: 18 | VAULT_DEV_ROOT_TOKEN: "1234567890" 19 | VAULT_ADDR: "http://vault:8200" 20 | VAULT_TOKEN: "1234567890" 21 | volumes: 22 | - ./../../scripts/vault-init.sh:/vault-init.sh 23 | - certs:/certs 24 | depends_on: 25 | - vault 26 | 27 | volumes: 28 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_mfa/runner-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | # Allows you to modify youshallnotpass go code and have it rebuilt and used in the gitlab_runner docker below 5 | youshallnotpass_builder_daemon: 6 | build: 7 | context: ../../../ 8 | dockerfile: testing/Dockerfiles/Dockerfile-dev 9 | volumes: 10 | - usr_local_bin:/usr/local/bin 11 | - ../../../:/app 12 | 13 | gitlab_runner: 14 | build: 15 | context: ../../../ 16 | dockerfile: testing/Dockerfiles/Dockerfile-gitlab 17 | environment: 18 | VAULT_ADDR: "http://vault:8200/" 19 | VAULT_EXTERNAL_ADDR: "http://localhost:8200" 20 | DOCKER_RUN_ARGS: --volume hashicorp_gitlab_mfa_git_repo:/gitrepo 21 | GIT_STRATEGY: clone 22 | YOUSHALLNOTPASS_GENERATE_JWT: "true" 23 | CI_PROJECT_PATH: youshallnotpass/demo 24 | CI_PROJECT_NAMESPACE: youshallnotpass 25 | working_dir: /gitrepo 26 | volumes: 27 | - /var/run/docker.sock:/var/run/docker.sock 28 | - ../../../custom_executors/gitlab_custom_executor:/var/gitlab_custom_executor 29 | - git_repo:/gitrepo 30 | - usr_local_bin:/usr/local/bin 31 | - certs:/certs 32 | # gitlab-runner https://docs.gitlab.com/runner/commands/#gitlab-runner-exec-deprecated 33 | command: | 34 | exec custom user_mfa_job 35 | 36 | git_repo_init: 37 | image: alpine/git 38 | volumes: 39 | - ./../../scripts/git-init.sh:/git-init.sh 40 | - git_repo:/gitrepo 41 | entrypoint: /bin/sh 42 | working_dir: /gitrepo 43 | command: /git-init.sh 44 | 45 | volumes: 46 | git_repo: 47 | usr_local_bin: 48 | certs: -------------------------------------------------------------------------------- /testing/integration/hashicorp_gitlab_mfa/vault-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | vault: 5 | image: hashicorp/vault 6 | restart: unless-stopped 7 | ports: 8 | - 8200:8200 9 | environment: 10 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 11 | VAULT_ADDR: "http://vault:8200" 12 | VAULT_TOKEN: "1234567890" 13 | 14 | vault_init: 15 | image: hashicorp/vault 16 | command: ["/vault-init.sh"] 17 | environment: 18 | VAULT_DEV_ROOT_TOKEN: "1234567890" 19 | VAULT_ADDR: "http://vault:8200" 20 | VAULT_TOKEN: "1234567890" 21 | volumes: 22 | - ./../../scripts/vault-init.sh:/vault-init.sh 23 | - certs:/certs 24 | depends_on: 25 | - vault 26 | 27 | volumes: 28 | certs: -------------------------------------------------------------------------------- /testing/integration/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1091 3 | # shellcheck disable=SC2143 4 | 5 | runTest="all" 6 | if [[ -n $1 ]]; then 7 | runTest=$(echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-zA-Z]//g') 8 | fi 9 | 10 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" 11 | 12 | source "${currentDir}/../colors.sh" 13 | 14 | # build the docker images 15 | if [[ "${runTest}" == "build" ]]; then 16 | echo -e "${GREEN}Building Integration Tests${NC}" 17 | docker compose -f "${currentDir}/vaultclient/hashicorp/docker-compose.yml" build 18 | docker compose -f "${currentDir}/hashicorp_gitlab_mfa/runner-compose.yml" build 19 | docker compose -f "${currentDir}/hashicorp_gitlab_mfa/vault-compose.yml" build 20 | docker compose -f "${currentDir}/hashicorp_gitlab_auth_timeout/runner-compose.yml" build 21 | docker compose -f "${currentDir}/hashicorp_gitlab_auth_timeout/vault-compose.yml" build 22 | docker compose -f "${currentDir}/hashicorp_gitlab_automatic/runner-compose.yml" build 23 | docker compose -f "${currentDir}/hashicorp_gitlab_automatic/vault-compose.yml" build 24 | docker compose -f "${currentDir}/hashicorp_gitlab_bash/runner-compose.yml" build 25 | docker compose -f "${currentDir}/hashicorp_gitlab_bash/vault-compose.yml" build 26 | docker compose -f "${currentDir}/hashicorp_gitlab_fail/runner-compose.yml" build 27 | docker compose -f "${currentDir}/hashicorp_gitlab_fail/vault-compose.yml" build 28 | echo -e "${BLUE}Finished Building Integration Tests${NC}" 29 | fi 30 | 31 | # run hashicorp client tests 32 | if [[ "${runTest}" == "all" || "${runTest}" == "hashicorpclient" || "${runTest}" == "hashicorp" ]]; then 33 | echo -e "${GREEN}Testing Hashicorp Vault Client${NC}" 34 | docker compose -f "${currentDir}/vaultclient/hashicorp/docker-compose.yml" up -d >> /dev/null 2>&1 35 | 36 | # wait until vault initialization is complete before we run the hasicorp tests 37 | sleep 0.2 38 | while true; do 39 | if [[ -z $(docker compose -f "${currentDir}/vaultclient/hashicorp/docker-compose.yml" ps | grep "vault_init") ]]; then 40 | break 41 | fi 42 | sleep 0.2 43 | done 44 | 45 | # run hashicorp tests 46 | export HASHICORP_TEST_PASSED 47 | export VAULT_ADDR='http://0.0.0.0:8200' 48 | if go test "${currentDir}/../../pkg/vaultclient/hashicorpclient"; then 49 | echo -e "${BLUE}Hashicorp Client Tests PASSED${NC}" 50 | HASHICORP_TEST_PASSED=true 51 | else 52 | echo -e "${RED}Hashicorp Client Tests FAILED${NC}" 53 | HASHICORP_TEST_PASSED=false 54 | fi 55 | docker compose -f "${currentDir}/vaultclient/hashicorp/docker-compose.yml" down >> /dev/null 2>&1 56 | docker kill hashicorp-vault-1 > /dev/null 2>&1 57 | echo 58 | 59 | if [[ $HASHICORP_TEST_PASSED == false ]]; then 60 | exit 1 61 | fi 62 | fi 63 | 64 | # run mattermost client tests 65 | if [[ "${runTest}" == "all" || "${runTest}" == "mattermostclient" || "${runTest}" == "mattermost" ]]; then 66 | echo -e "${GREEN}Testing Mattermost Client${NC}" 67 | docker run --name mattermost-preview -d --publish 8065:8065 mattermost/mattermost-preview > /dev/null 2>&1 68 | 69 | echo -e "${GREEN}Letting the Mattermost Docker Image Warm Up / Initialize ${NC}" 70 | 71 | sleep 5 72 | while true; do 73 | if curl -i -s http://localhost:8065 > /dev/null 2>&1; then 74 | echo "Mattermost Docker Host Has Awoken" 75 | break 76 | fi 77 | sleep 5 78 | done 79 | 80 | # run mattermost tests 81 | export MATTERMOST_TEST_PASSED 82 | if go test "${currentDir}/../../pkg/loggerclient/mattermostclient"; then 83 | echo -e "${BLUE}Mattermost Client Tests PASSED${NC}" 84 | MATTERMOST_TEST_PASSED=true 85 | else 86 | echo -e "${RED}Mattermost Client Tests FAILED${NC}" 87 | MATTERMOST_TEST_PASSED=false 88 | fi 89 | 90 | docker kill mattermost-preview > /dev/null 2>&1 91 | docker rm mattermost-preview > /dev/null 2>&1 92 | echo 93 | 94 | if [[ $MATTERMOST_TEST_PASSED == false ]]; then 95 | exit 1 96 | fi 97 | fi 98 | 99 | # run hashicorp gitlab automatic integration test 100 | if [[ "${runTest}" == "all" || \ 101 | "${runTest}" == "hashicorp" || \ 102 | "${runTest}" == "gitlab" || \ 103 | "${runTest}" == "automatic" || \ 104 | "${runTest}" == "hashicorp gitlab automatic" || \ 105 | "${runTest}" == "hashicorpgitlabautomatic" ]]; then 106 | echo -e "${GREEN}Testing Hashicorp-GitLab Automatic Workflow${NC}" 107 | 108 | # run vault client 109 | docker compose -f "${currentDir}/hashicorp_gitlab_automatic/vault-compose.yml" up -d 110 | 111 | # wait for vault to be initialized 112 | sleep 0.2 113 | while true; do 114 | if [[ -z $(docker compose -f "${currentDir}/hashicorp_gitlab_automatic/vault-compose.yml" ps | grep "vault_init") ]]; then 115 | break 116 | fi 117 | sleep 0.2 118 | done 119 | 120 | # run gitlab runner 121 | export HASHICORP_GITLAB_AUTOMATIC_PASSED 122 | if docker compose -f "${currentDir}/hashicorp_gitlab_automatic/runner-compose.yml" up --exit-code-from gitlab_runner; then 123 | HASHICORP_GITLAB_AUTOMATIC_PASSED=true 124 | else 125 | HASHICORP_GITLAB_AUTOMATIC_PASSED=false 126 | fi 127 | 128 | # shutdown the docker containers 129 | docker compose -f "${currentDir}/hashicorp_gitlab_automatic/vault-compose.yml" down 130 | docker compose -f "${currentDir}/hashicorp_gitlab_automatic/runner-compose.yml" down 131 | 132 | if [[ $HASHICORP_GITLAB_AUTOMATIC_PASSED == true ]]; then 133 | echo -e "\n\n${BLUE}Hashicorp Gitlab Automatic CI/CD Task Test SUCCEEDED${NC}" 134 | else 135 | echo -e "\n\n${RED}Hashicorp Gitlab Automatic CI/CD Task Test FAILED${NC}" 136 | exit 1 137 | fi 138 | fi 139 | 140 | # run hashicorp gitlab failure integration test 141 | if [[ "${runTest}" == "all" || \ 142 | "${runTest}" == "hashicorp" || \ 143 | "${runTest}" == "gitlab" || \ 144 | "${runTest}" == "fail" || \ 145 | "${runTest}" == "hashicorp gitlab fail" || \ 146 | "${runTest}" == "hashicorpgitlabfail" ]]; then 147 | echo -e "${GREEN}Testing Hashicorp-Gitlab Failure Workflow${NC}" 148 | 149 | # run vault client 150 | docker compose -f "${currentDir}/hashicorp_gitlab_fail/vault-compose.yml" up -d 151 | 152 | # wait for vault to be initialized 153 | sleep 0.2 154 | while true; do 155 | if [[ -z $(docker compose -f "${currentDir}/hashicorp_gitlab_fail/vault-compose.yml" ps | grep "vault_init") ]]; then 156 | break 157 | fi 158 | sleep 0.2 159 | done 160 | 161 | # run gitlab runner 162 | export HASHICORP_GITLAB_FAIL_PASSED 163 | if docker compose -f "${currentDir}/hashicorp_gitlab_fail/runner-compose.yml" up --exit-code-from gitlab_runner; then 164 | HASHICORP_GITLAB_FAIL_PASSED=false 165 | else 166 | HASHICORP_GITLAB_FAIL_PASSED=true 167 | fi 168 | 169 | # shutdown the docker containers 170 | docker compose -f "${currentDir}/hashicorp_gitlab_fail/vault-compose.yml" down 171 | docker compose -f "${currentDir}/hashicorp_gitlab_fail/runner-compose.yml" down 172 | 173 | if [[ $HASHICORP_GITLAB_FAIL_PASSED == true ]]; then 174 | echo -e "\n\n${BLUE}Hashicorp Gitlab Failure Workflow Test SUCCEEDED${NC}" 175 | else 176 | echo -e "\n\n${RED}Hashicorp Gitlab Failure Workflow Test FAILED${NC}" 177 | exit 1 178 | fi 179 | fi 180 | 181 | # run hashicorp gitlab auth integration test 182 | if [[ "${runTest}" == "all" || \ 183 | "${runTest}" == "hashicorp" || \ 184 | "${runTest}" == "gitlab" || \ 185 | "${runTest}" == "auth" || \ 186 | "${runTest}" == "hashicorp gitlab auth" || \ 187 | "${runTest}" == "hashicorpgitlabauth" ]]; then 188 | echo -e "${GREEN}Testing Hashicorp-Gitlab Auth Workflow${NC}" 189 | 190 | # run vault client 191 | docker compose -f "${currentDir}/hashicorp_gitlab_mfa/vault-compose.yml" up -d 192 | 193 | # wait for vault to be initialized 194 | sleep 0.2 195 | while true; do 196 | if [[ -z $(docker compose -f "${currentDir}/hashicorp_gitlab_mfa/vault-compose.yml" ps | grep "vault_init") ]]; then 197 | break 198 | fi 199 | sleep 0.2 200 | done 201 | 202 | # run gitlab runner 203 | export HASHICORP_GITLAB_AUTH_PASSED 204 | if docker compose -f "${currentDir}/hashicorp_gitlab_mfa/runner-compose.yml" up --exit-code-from gitlab_runner; then 205 | HASHICORP_GITLAB_AUTH_PASSED=true 206 | else 207 | HASHICORP_GITLAB_AUTH_PASSED=false 208 | fi 209 | 210 | # shutdown the docker containers 211 | docker compose -f "${currentDir}/hashicorp_gitlab_mfa/vault-compose.yml" down 212 | docker compose -f "${currentDir}/hashicorp_gitlab_mfa/runner-compose.yml" down 213 | 214 | 215 | if [[ $HASHICORP_GITLAB_AUTH_PASSED == true ]]; then 216 | echo -e "\n\n${BLUE}Hashicorp Gitlab Auth Workflow Test SUCCEEDED${NC}" 217 | else 218 | echo -e "\n\n${RED}Hashicorp Gitlab Auth Workflow Test FAILED${NC}" 219 | exit 1 220 | fi 221 | fi 222 | 223 | # run hashicorp gitlab auth timeout integration test 224 | if [[ "${runTest}" == "all" || \ 225 | "${runTest}" == "hashicorp" || \ 226 | "${runTest}" == "gitlab" || \ 227 | "${runTest}" == "timeout" || \ 228 | "${runTest}" == "hashicorp gitlab timeout" || \ 229 | "${runTest}" == "hashicorpgitlabtimeout" ]]; then 230 | echo -e "${GREEN}Testing Hashicorp-Gitlab Auth Timeout Workflow${NC}" 231 | 232 | # run vault client 233 | docker compose -f "${currentDir}/hashicorp_gitlab_auth_timeout/vault-compose.yml" up -d 234 | 235 | # wait for vault to be initialized 236 | sleep 0.2 237 | while true; do 238 | if [[ -z $(docker compose -f "${currentDir}/hashicorp_gitlab_auth_timeout/vault-compose.yml" ps | grep "vault_init") ]]; then 239 | break 240 | fi 241 | sleep 0.2 242 | done 243 | 244 | # run gitlab runner 245 | export HASHICORP_GITLAB_TIMEOUT_PASSED 246 | if docker compose -f "${currentDir}/hashicorp_gitlab_auth_timeout/runner-compose.yml" up --exit-code-from gitlab_runner; then 247 | HASHICORP_GITLAB_TIMEOUT_PASSED=false 248 | else 249 | HASHICORP_GITLAB_TIMEOUT_PASSED=true 250 | fi 251 | 252 | # shutdown the docker containers 253 | docker compose -f "${currentDir}/hashicorp_gitlab_auth_timeout/vault-compose.yml" down 254 | docker compose -f "${currentDir}/hashicorp_gitlab_auth_timeout/runner-compose.yml" down 255 | 256 | if [[ $HASHICORP_GITLAB_TIMEOUT_PASSED == true ]]; then 257 | echo -e "\n\n${BLUE}Hashicorp Gitlab Auth Timeout Workflow Test SUCCEEDED${NC}" 258 | else 259 | echo -e "\n\n${RED}Hashicorp Gitlab Auth Timeout Workflow Test FAILED${NC}" 260 | exit 1 261 | fi 262 | fi 263 | 264 | if [[ "${runTest}" == "all" || \ 265 | "${runTest}" == "hashicorp" || \ 266 | "${runTest}" == "gitlab" || \ 267 | "${runTest}" == "bash" || \ 268 | "${runTest}" == "hashicorp gitlab bash" || \ 269 | "${runTest}" == "hashicorpgitlabbash" ]]; then 270 | echo -e "${GREEN}Testing Hashicorp-GitLab Bash Script Workflow${NC}" 271 | 272 | # run vault client 273 | docker compose -f "${currentDir}/hashicorp_gitlab_bash/vault-compose.yml" up -d 274 | 275 | # wait for vault to be initialized 276 | sleep 0.2 277 | while true; do 278 | if [[ -z $(docker compose -f "${currentDir}/hashicorp_gitlab_bash/vault-compose.yml" ps | grep "vault_init") ]]; then 279 | break 280 | fi 281 | sleep 0.2 282 | done 283 | 284 | # run gitlab runner 285 | export HASHICORP_GITLAB_BASH_PASSED 286 | if docker compose -f "${currentDir}/hashicorp_gitlab_bash/runner-compose.yml" up --exit-code-from gitlab_runner; then 287 | HASHICORP_GITLAB_BASH_PASSED=true 288 | else 289 | HASHICORP_GITLAB_BASH_PASSED=false 290 | fi 291 | 292 | # shutdown the docker containers 293 | docker compose -f "${currentDir}/hashicorp_gitlab_bash/vault-compose.yml" down 294 | docker compose -f "${currentDir}/hashicorp_gitlab_bash/runner-compose.yml" down 295 | 296 | if [[ $HASHICORP_GITLAB_BASH_PASSED == true ]]; then 297 | echo -e "\n\n${BLUE}Hashicorp Gitlab Bash Script CI/CD Task Test SUCCEEDED${NC}" 298 | else 299 | echo -e "\n\n${RED}Hashicorp Gitlab Bash Script CI/CD Task Test FAILED${NC}" 300 | exit 1 301 | fi 302 | fi 303 | 304 | exit 0 -------------------------------------------------------------------------------- /testing/integration/vaultclient/hashicorp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | vault: 5 | image: hashicorp/vault 6 | restart: unless-stopped 7 | ports: 8 | - 8200:8200 9 | environment: 10 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 11 | VAULT_ADDR: "http://vault:8200" 12 | VAULT_TOKEN: "1234567890" 13 | 14 | vault_init: 15 | image: hashicorp/vault 16 | command: ["/vault-init.sh"] 17 | environment: 18 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 19 | VAULT_ADDR: "http://vault:8200" 20 | VAULT_TOKEN: "1234567890" 21 | volumes: 22 | - ./../../../scripts/vault-init.sh:/vault-init.sh 23 | - certs:/certs 24 | depends_on: 25 | - vault 26 | 27 | volumes: 28 | certs: -------------------------------------------------------------------------------- /testing/scripts/git-init.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | GIT_REPO="$(pwd)" 4 | 5 | today=$(date +%s) 6 | 7 | echo "${today}" > "$GIT_REPO/bootstrapped" 8 | 9 | cd "$GIT_REPO" || exit 10 | 11 | cat >"$GIT_REPO/test.sh" <"$GIT_REPO/.gitlab-ci.yml" <&- || git remote add origin "$GIT_REPO/.git" 73 | echo "done with git repo initialization time to sleep" 74 | sleep infinity -------------------------------------------------------------------------------- /testing/scripts/vault-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if command -v apk && ! command -v jq > /dev/null; then 4 | echo "Installing jq, openssl..." 5 | apk update && apk add jq openssl 6 | fi 7 | 8 | if [ ! -f "/certs/private-key.pem" ] && [ ! -f "/certs/public-key.pem" ]; then 9 | echo "Generate Pub/Private keys for JWT tokens" 10 | mkdir /certs/ 11 | openssl genrsa -out /certs/private-key.pem 3072 12 | openssl rsa -in /certs/private-key.pem -pubout -out /certs/public-key.pem 13 | fi 14 | 15 | ROLE="youshallnotpass-demo" 16 | POLICIES="gitlab" 17 | 18 | sleep 5s 19 | 20 | vault auth enable -path=jwt/gitlab.example.com jwt 21 | 22 | vault write auth/jwt/gitlab.example.com/config \ 23 | bond_issuer="gitlab.example.com" \ 24 | default_role="$ROLE" \ 25 | jwt_validation_pubkeys="$(cat /certs/public-key.pem)" 26 | 27 | 28 | vault write auth/jwt/gitlab.example.com/role/$ROLE -<> policy.hcl 54 | path "cicd/{{ identity.entity.aliases.$JWT_ACCESSOR.metadata.project_path }}/whitelist" { 55 | capabilities = ["read", "list"] 56 | } 57 | path "cicd/{{ identity.entity.aliases.$JWT_ACCESSOR.metadata.namespace_path }}/whitelist" { 58 | capabilities = ["read", "list"] 59 | } 60 | path "cicd/{{ identity.entity.aliases.$JWT_ACCESSOR.metadata.namespace_path }}/youshallnotpass_config" { 61 | capabilities = ["read", "list"] 62 | } 63 | path "cicd/{{ identity.entity.aliases.$JWT_ACCESSOR.metadata.project_path }}/youshallnotpass_config" { 64 | capabilities = ["read", "list"] 65 | } 66 | path "cicd/{{ identity.entity.aliases.$JWT_ACCESSOR.metadata.project_path }}/scratch/*" { 67 | capabilities = ["create", "read", "delete"] 68 | } 69 | EOF 70 | 71 | echo "Creating policy" 72 | vault policy write $POLICIES policy.hcl 73 | 74 | echo "Enabling KV" 75 | 76 | vault secrets enable -path=cicd kv 77 | 78 | printf '{ 79 | "allowed_images":[ 80 | "alpine:3.13@sha256:def822f9851ca422481ec6fee59a9966f12b351c62ccb9aca841526ffaa9f748" 81 | ] 82 | }' | vault kv put cicd/youshallnotpass/whitelist - 83 | 84 | printf '{ 85 | "allowed_images":[ 86 | "alpine:3.12.7@sha256:a9c28c813336ece5bb98b36af5b66209ed777a394f4f856c6e62267790883820" 87 | ], 88 | "allowed_scripts":[ 89 | "automatic_job@sha256:Ij3eYc5EwfiLD6rPw9qFpN82ydukCduG4bUL9ltQDy4=", 90 | "script_job@sha256:Kn9ysqTdXVzh52gp2LNiX5RMNRxdoAQytneeLcNsycQ=" 91 | ] 92 | }' | vault kv put cicd/youshallnotpass/demo/whitelist - 93 | 94 | printf '{ 95 | "logger": { 96 | "name": "console" 97 | } 98 | }' | vault kv put cicd/youshallnotpass/youshallnotpass_config - 99 | 100 | printf '{ 101 | "jobs": [ 102 | { 103 | "jobName": "user_mfa_job", 104 | "checks": [ 105 | { 106 | "name": "mfaRequired", 107 | "options": { 108 | "checkType": "script" 109 | } 110 | } 111 | ] 112 | }, 113 | { 114 | "jobName": "user_mfa_timeout_job", 115 | "checks": [ 116 | { 117 | "name": "imageHash", 118 | "options": { 119 | "abortOnFail": false, 120 | "mfaOnFail": true 121 | } 122 | }, 123 | { 124 | "name": "scriptHash", 125 | "options": { 126 | "abortOnFail": false, 127 | "mfaOnFail": true 128 | } 129 | } 130 | ] 131 | }, 132 | { 133 | "jobName": "automatic_job", 134 | "checks": [ 135 | { 136 | "name": "imageHash", 137 | "options": { 138 | "abortOnFail": true 139 | } 140 | }, 141 | { 142 | "name": "scriptHash", 143 | "options": { 144 | "abortOnFail": true 145 | } 146 | } 147 | ] 148 | }, 149 | { 150 | "jobName": "script_job", 151 | "checks": [ 152 | { 153 | "name": "scriptHash", 154 | "options": { 155 | "abortOnFail": true 156 | } 157 | } 158 | ] 159 | }, 160 | { 161 | "jobName": "fail_job", 162 | "checks": [ 163 | { 164 | "name": "imageHash", 165 | "options": { 166 | "abortOnFail": true 167 | } 168 | }, 169 | { 170 | "name": "scriptHash", 171 | "options": { 172 | "abortOnFail": true 173 | } 174 | } 175 | ] 176 | }, 177 | { 178 | "jobName": "default", 179 | "checks": [ 180 | { 181 | "name": "imageHash", 182 | "options": { 183 | "abortOnFail": true 184 | } 185 | } 186 | ] 187 | } 188 | ] 189 | } 190 | ' | vault kv put cicd/youshallnotpass/demo/youshallnotpass_config - -------------------------------------------------------------------------------- /testing/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | testType="all" 4 | if [[ -n $1 ]]; then 5 | testType=$(echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-zA-Z]//g') 6 | fi 7 | 8 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" 9 | 10 | # shellcheck disable=SC1091 11 | source "${currentDir}/colors.sh" 12 | 13 | # run unit tests 14 | if [[ "${testType}" == "all" || "${testType}" == "unit" ]]; then 15 | echo -e "${GREEN}Running Unit Tests${NC}" 16 | # TODO: Print Logo 17 | 18 | if "${currentDir}/unit/test.sh" "$2"; then 19 | echo -e "${BLUE}Unit Tests PASSED${NC}\n" 20 | else 21 | echo -e "${RED}Unit Tests FAILED${NC}\n" 22 | exit 1 23 | fi 24 | fi 25 | 26 | # run integration tests 27 | if [[ "${testType}" == "all" || "${testType}" == "integration" ]]; then 28 | echo -e "${GREEN}Running Integration Tests${NC}" 29 | # TODO: Print Logo 30 | 31 | if "${currentDir}/integration/test.sh" "$2"; then 32 | echo -e "${BLUE}Integration Tests PASSED${NC}\n" 33 | else 34 | echo -e "${RED}Integration Tests FAILED${NC}\n" 35 | exit 1 36 | fi 37 | fi 38 | 39 | exit 0 -------------------------------------------------------------------------------- /testing/unit/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1091 3 | # shellcheck disable=SC2143 4 | 5 | runTask="all" 6 | if [[ -n $1 ]]; then 7 | runTask=$(echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-zA-Z]//g') 8 | fi 9 | 10 | currentDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" 11 | 12 | # get the various color codes from colors.sh 13 | source "${currentDir}/../colors.sh" 14 | 15 | # run checkparser tests 16 | if [[ "${runTask}" == "all" || "${runTask}" == "checkparser" ]]; then 17 | echo -e "${GREEN}Testing CheckParser Client${NC}" 18 | if go test "${currentDir}/../../pkg/checkparser"; then 19 | echo -e "${BLUE}CheckParser Client Tests PASSED${NC}\n" 20 | else 21 | echo -e "${RED}CheckParser Client Tests FAILED${NC}\n" 22 | exit 1 23 | fi 24 | fi 25 | 26 | # run checks tests 27 | if [[ "${runTask}" == "all" || "${runTask}" == "checks" ]]; then 28 | echo -e "${GREEN}Testing Checks Client${NC}" 29 | if go test "${currentDir}/../../pkg/checks"; then 30 | echo -e "${BLUE}Checks Client Tests PASSED${NC}\n" 31 | else 32 | echo -e "${RED}Checks Client Tests FAILED${NC}\n" 33 | exit 1 34 | fi 35 | fi 36 | 37 | # run imageHashCheck Tests 38 | if [[ "${runTask}" == "all" || "${runTask}" == "imageHash" ]]; then 39 | echo -e "${GREEN}Testing Image Hash Check Client${NC}" 40 | if go test "${currentDir}/../../pkg/checks/imagehash"; then 41 | echo -e "${BLUE}Image Hash Check Client Tests PASSED${NC}\n" 42 | else 43 | echo -e "${RED}Image Hash Check Client Tests FAILED${NC}\n" 44 | exit 1 45 | fi 46 | fi 47 | 48 | # run scriptHashCheck Tests 49 | if [[ "${runTask}" == "all" || "${runTask}" == "scriptHash" ]]; then 50 | echo -e "${GREEN}Testing Script Hash Check Client${NC}" 51 | if go test "${currentDir}/../../pkg/checks/scripthash"; then 52 | echo -e "${BLUE}Script Hash Check Client Tests PASSED${NC}\n" 53 | else 54 | echo -e "${RED}Script Hash Check Client Tests FAILED${NC}\n" 55 | exit 1 56 | fi 57 | fi 58 | 59 | # run mfaRequiredCheck Tests 60 | if [[ "${runTask}" == "all" || "${runTask}" == "mfaRequired" ]]; then 61 | echo -e "${GREEN}Testing Mfa Required Check Client${NC}" 62 | if go test "${currentDir}/../../pkg/checks/scripthash"; then 63 | echo -e "${BLUE}Mfa Required Check Client Tests PASSED${NC}\n" 64 | else 65 | echo -e "${RED}Mfa Required Check Client Tests FAILED${NC}\n" 66 | exit 1 67 | fi 68 | fi 69 | 70 | # run dateTimeCheck Tests 71 | if [[ "${runTask}" == "all" || "${runTask}" == "datetime" ]]; then 72 | echo -e "${GREEN}Testing Date Time Check Client${NC}" 73 | if go test "${currentDir}/../../pkg/checks/datetime"; then 74 | echo -e "${BLUE}Date Time Check Client Tests PASSED${NC}\n" 75 | else 76 | echo -e "${RED}Date Time Check Client Tests FAILED${NC}\n" 77 | exit 1 78 | fi 79 | fi 80 | 81 | # run config Tests 82 | if [[ "${runTask}" == "all" || "${runTask}" == "config" ]]; then 83 | echo -e "${GREEN}Testing Config Client${NC}" 84 | if go test "${currentDir}/../../pkg/config"; then 85 | echo -e "${BLUE}Config Client Tests PASSED${NC}\n" 86 | else 87 | echo -e "${RED}Config Client Tests FAILED${NC}\n" 88 | exit 1 89 | fi 90 | fi 91 | 92 | # run scriptcleanupparser tests 93 | if [[ "${runTask}" == "all" || "${runTask}" == "scriptcleanupparser" ]]; then 94 | echo -e "${GREEN}Testing Script Cleanup Parser${NC}" 95 | if go test "${currentDir}/../../pkg/scriptcleanerclient"; then 96 | echo -e "${BLUE}Script Cleanup Parser Tests PASSED${NC}\n" 97 | else 98 | echo -e "${RED}Script Cleanup Parser Tests FAILED${NC}\n" 99 | exit 1 100 | fi 101 | fi 102 | 103 | # run gitlab cleanup tests 104 | if [[ "${runTask}" == "all" || "${runTask}" == "gitlabcleanup" ]]; then 105 | echo -e "${GREEN}Testing GitLab Cleanup Client${NC}" 106 | if go test "${currentDir}/../../pkg/scriptcleanerclient/gitlabcleanup"; then 107 | echo -e "${BLUE}GitLab Cleanup Client Tests PASSED${NC}\n" 108 | else 109 | echo -e "${RED}GitLab Cleanup Client Tests FAILED${NC}\n" 110 | exit 1 111 | fi 112 | fi 113 | 114 | # run github cleanup tests 115 | if [[ "${runTask}" == "all" || "${runTask}" == "githubcleanup" ]]; then 116 | echo -e "${GREEN}Testing GitHub Cleanup Client${NC}" 117 | if go test "${currentDir}/../../pkg/scriptcleanerclient/githubcleanup"; then 118 | echo -e "${BLUE}GitHub Cleanup Client Tests PASSED${NC}\n" 119 | else 120 | echo -e "${RED}GitHub Cleanup Client Tests FAILED${NC}\n" 121 | exit 1 122 | fi 123 | fi 124 | 125 | # run vault client tests 126 | if [[ "${runTask}" == "all" || "${runTask}" == "gitlabcleanup" ]]; then 127 | echo -e "${GREEN}Testing Vault Client${NC}" 128 | if go test "${currentDir}/../../pkg/vaultclient"; then 129 | echo -e "${BLUE}Vault Client Tests PASSED${NC}\n" 130 | else 131 | echo -e "${RED}Vault Client Tests FAILED${NC}\n" 132 | exit 1 133 | fi 134 | fi 135 | 136 | # run whitelist tests 137 | if [[ "${runTask}" == "all" || "${runTask}" == "whitelist" ]]; then 138 | echo -e "${GREEN}Testing Whitelist${NC}" 139 | if go test "${currentDir}/../../pkg/whitelist"; then 140 | echo -e "${BLUE}Whitelist Tests PASSED${NC}\n" 141 | else 142 | echo -e "${RED}Whitelist Tests FAILED${NC}\n" 143 | exit 1 144 | fi 145 | fi 146 | 147 | exit 0 -------------------------------------------------------------------------------- /testing/unit/vaultclient/hashicorp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | vault: 4 | image: hashicorp/vault 5 | ports: 6 | - 8200:8200 7 | environment: 8 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 9 | VAULT_ADDR: "http://vault:8200" 10 | VAULT_TOKEN: "1234567890" 11 | 12 | vault-init: 13 | image: hashicorp/vault 14 | command: ["/vault-init.sh"] 15 | environment: 16 | VAULT_DEV_ROOT_TOKEN_ID: "1234567890" 17 | VAULT_ADDR: "http://vault:8200" 18 | VAULT_TOKEN: "1234567890" 19 | volumes: 20 | - ../../../scripts/vault-init.sh:/vault-init.sh 21 | depends_on: 22 | - vault --------------------------------------------------------------------------------