├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ ├── SUPPORT_QUESTION.md │ └── WRONG_DETECTION.md ├── pull_request_template.md └── workflows │ ├── build.yml │ └── release.yaml ├── .gitignore ├── .goreleaser.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.goreleaser ├── LICENSE ├── README.md ├── cmd └── chain-bench │ └── main.go ├── docs ├── CIS-Software-Supply-Chain-Security-Guide-v1.0.pdf ├── build │ ├── Dockerfile │ └── requirements.txt ├── getting-started │ ├── installation.md │ └── quickstart.md ├── imgs │ ├── banner_dm.png │ ├── banner_lm.png │ ├── demo.gif │ └── logo.png └── index.md ├── go.mod ├── go.sum ├── internal ├── checker │ └── runner.go ├── checks │ ├── artifacts │ │ ├── access-to-artifacts │ │ │ ├── access_to_artifacts.go │ │ │ ├── access_to_artifacts_test.go │ │ │ ├── rules.metadata.json │ │ │ └── rules.rego │ │ ├── artifacts.go │ │ ├── package-registries │ │ │ ├── package_registries.go │ │ │ ├── package_registries_test.go │ │ │ ├── rules.metadata.json │ │ │ └── rules.rego │ │ └── sections.metadata.json │ ├── build-pipelines │ │ ├── build-pipelines.go │ │ ├── pipeline-instructions │ │ │ ├── pipeline_instructions.go │ │ │ ├── pipeline_instructions_test.go │ │ │ ├── rules.metadata.json │ │ │ └── rules.rego │ │ ├── pipeline-integrity │ │ │ ├── pipeline_integrity.go │ │ │ ├── pipeline_integrity_test.go │ │ │ ├── rules.metadata.json │ │ │ └── rules.rego │ │ └── sections.metadata.json │ ├── checks.go │ ├── checks_test.go │ ├── common │ │ ├── assets │ │ │ ├── consts.rego │ │ │ ├── permissions.rego │ │ │ └── utils.rego │ │ ├── checker_test.go │ │ ├── common.go │ │ └── opa │ │ │ ├── rego.go │ │ │ └── rego_test.go │ ├── consts │ │ ├── details.go │ │ └── errors.go │ ├── dependencies │ │ ├── dependencies.go │ │ ├── sections.metadata.json │ │ ├── third-party-packages │ │ │ ├── rules.metadata.json │ │ │ ├── rules.rego │ │ │ ├── third_party_packages.go │ │ │ └── third_party_packages_test.go │ │ └── validate_packages │ │ │ ├── rules.metadata.json │ │ │ ├── rules.rego │ │ │ ├── validate_packages.go │ │ │ └── validate_packages_test.go │ └── source-code │ │ ├── code-changes │ │ ├── code_changes.go │ │ ├── code_changes_test.go │ │ ├── rules.metadata.json │ │ └── rules.rego │ │ ├── contribution-access │ │ ├── contribution_access.go │ │ ├── contribution_access_test.go │ │ ├── rules.metadata.json │ │ └── rules.rego │ │ ├── repository-management │ │ ├── repository_management.go │ │ ├── repository_management_test.go │ │ ├── rules.metadata.json │ │ └── rules.rego │ │ ├── sections.metadata.json │ │ └── source-code.go ├── commands │ ├── config.go │ ├── flags.go │ ├── root.go │ └── scan.go ├── config │ ├── config.go │ ├── default.go │ └── models.go ├── consts │ └── error.go ├── logger │ ├── global.go │ ├── logger.go │ └── zerolog.go ├── models │ ├── app.go │ ├── branch.go │ ├── checkmodels │ │ └── check.go │ ├── hook.go │ ├── organization.go │ ├── package_registry.go │ ├── repository.go │ ├── team.go │ └── user.go ├── printer │ ├── colors.go │ ├── helpers.go │ ├── printer.go │ ├── statistics.go │ └── table_builder.go ├── scm-clients │ ├── adapter │ │ └── adapter.go │ ├── clients │ │ ├── clients.go │ │ └── clients_test.go │ ├── github │ │ ├── adapter.go │ │ ├── client_mocks.go │ │ ├── client_test.go │ │ ├── github.go │ │ ├── github_mocks.go │ │ ├── mapper.go │ │ └── mapper_test.go │ └── gitlab │ │ ├── adapter.go │ │ ├── gitlab.go │ │ ├── mapper.go │ │ └── mapper_test.go ├── testutils │ ├── builders │ │ ├── assets_data_builder.go │ │ ├── branch_builder.go │ │ ├── branch_protection_builder.go │ │ ├── job_builder.go │ │ ├── organization_builder.go │ │ ├── package_registry_builder.go │ │ ├── pipeline_builder.go │ │ └── repository_builder.go │ └── testutils.go └── utils │ ├── array.go │ ├── http-client.go │ ├── timestamp.go │ └── values.go ├── makefile ├── mkdocs.yml └── templates └── gitlab_security_scanner.tpl /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .github 3 | .goreleaser.yml 4 | .gitignore 5 | *.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | labels: kind/bug 4 | about: If something isn't working as expected. 5 | --- 6 | 7 | ## Description 8 | 9 | 12 | 13 | ## What did you expect to happen? 14 | 15 | 16 | ## What happened instead? 17 | 18 | 19 | ``` 20 | (paste your output here) 21 | ``` 22 | 23 | ## Additional details (base image name, container registry info...): -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | labels: kind/feature 4 | about: I have a suggestion (and might want to implement myself)! 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Question 3 | labels: triage/support 4 | about: If you have a question about chain-bench. 5 | --- 6 | 7 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/WRONG_DETECTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Wrong Detection 3 | labels: ["kind/bug"] 4 | about: If Chain-Bench doesn't detect something, or shows false positive detection 5 | --- 6 | 7 | ## Description 8 | 9 | 12 | 13 | ## Additional details in case it does not expose sensitive data (scanned pipeline files, PR, etc): -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | ## Related issues 4 | - Close #XXX 5 | 6 | ## Related PRs 7 | - [ ] #XXX 8 | - [ ] #YYY 9 | 10 | Remove this section if you don't have related PRs. 11 | 12 | ## Checklist 13 | - [ ] I've read the [guidelines for contributing](https://github.com/aquasecurity/chain-bench/blob/main/CONTRIBUTING.md) to this repository. 14 | - [ ] I've followed the [conventions](https://github.com/aquasecurity/chain-bench/blob/main/CONTRIBUTING.md#pull-requests) in the PR title. 15 | - [ ] I've added tests that prove my fix is effective or that my feature works. 16 | - [ ] I've updated the [readme](https://github.com/aquasecurity/chain-bench/blob/main/README.md) with the relevant information (if needed). 17 | - [ ] I've added usage information (if the PR introduces new options) 18 | - [ ] I've included a "before" and "after" example to the description (if the PR is a user interface change). 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Install Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: 1.18 18 | 19 | - name: Configure Git URL 20 | run: | 21 | git config --global url.https://$GH_ACCESS_TOKEN@github.com/.insteadOf https://github.com/ 22 | env: 23 | GH_ACCESS_TOKEN: ${{ secrets.GH_TOKEN }} 24 | 25 | - name: Build 26 | run: | 27 | make 28 | 29 | - name: Execute Tests with Coverage 30 | run: | 31 | make test-coverage 32 | 33 | - name: Upload Coverage 34 | uses: codecov/codecov-action@v2 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | env: 8 | GO_VERSION: "1.18" 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | env: 14 | DOCKER_CLI_EXPERIMENTAL: "enabled" 15 | DOCKER_BUILDKIT: "1" 16 | steps: 17 | - name: Setup Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ${{ env.GO_VERSION }} 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | 25 | - name: Set up Docker Buildx 26 | id: buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Show available Docker Buildx platforms 30 | run: echo ${{ steps.buildx.outputs.platforms }} 31 | 32 | - name: Checkout code 33 | uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Login to Dockerhub 38 | uses: docker/login-action@v2 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USER }} 41 | password: ${{ secrets.DOCKERHUB_TOKEN }} 42 | - name: Login to Github Container Registry 43 | uses: docker/login-action@v2 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.repository_owner }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Run GoReleaser 50 | uses: goreleaser/goreleaser-action@v3 51 | with: 52 | distribution: goreleaser 53 | version: latest 54 | args: release --rm-dist 55 | workdir: ${{ env.git_repo_name }} 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} 58 | GHCR_PATH: ghcr.io/aquasecurity 59 | DOCKERHUB_PATH: docker.io/aquasec 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | /result.json 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | /vendor 17 | /chain-bench 18 | /.vscode 19 | /__debug_bin 20 | 21 | .DS_Store 22 | /chain-bench 23 | /results.json -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for taking an interest in contributing to chain-bench ! 2 | 3 | ## Contributing, bug reporting, openning issues and starting discussions 4 | 5 | ### Issues 6 | 7 | - Feel free to open an issue for any reason as long as you make it clear if the issue is about a bug/feature/question/comment. 8 | - Please spend some time giving due diligence to the issue tracker. Your issue might be a duplicate. If it is, please add your comment to the existing issue. 9 | - Remember, users might be searching for your issue in the future. So please give it a meaningful title to help others. 10 | - The issue should clearly explain the reason for opening the proposal if you have any, along with any relevant technical information. 11 | - For questions and bug reports, please include the following information: 12 | - version of chain-bench you are running (from chain-bench version) along with the command line options you are using. 13 | 14 | ### Bugs 15 | 16 | If you think you have found a bug please follow the instructions below. 17 | 18 | - Open a [new bug](https://github.com/aquasecurity/chain-bench/issues/new?assignees=&labels=&template=bug_report.md) if a duplicate doesn't already exist. 19 | - Make sure to give as much information as possible in the following questions 20 | - Overview 21 | - How did you run chain-bench? 22 | - What happened? 23 | - What did you expect to happen 24 | - Environment 25 | - Running processes 26 | - Configuration files 27 | - Anything else you would like to add 28 | 29 | 30 | ### Features 31 | 32 | We also use the GitHub discussions to track feature requests. If you have an idea to make chain-bench even more awesome follow the steps below. 33 | 34 | - Open a [new discussion](https://github.com/aquasecurity/chain-bench/discussions/new?category_id=19113743) if a duplicate doesn't already exist. 35 | - Remember users might be searching for your discussion in the future, so please give it a meaningful title to helps others. 36 | - Clearly define the use case, using concrete examples. For example, I type `this` and chain-bench does `that`. 37 | - If you would like to include a technical design for your feature please feel free to do so. 38 | 39 | ### Questions 40 | 41 | We also use the GitHub discussions to Q&A. 42 | 43 | - Open a [new discussion](https://github.com/aquasecurity/chain-bench/discussions/new) if a duplicate doesn't already exist. 44 | - Remember users might be searching for your discussion in the future, so please give it a meaningful title to helps others. 45 | 46 | 47 | ### Pull Requests 48 | 49 | We welcome pull requests! 50 | - Every Pull Request should have an associated Issue, unless you are fixing a trivial documentation issue. 51 | - We will not accept changes to LICENSE, NOTICE or CONTRIBUTING from outside the Argon Security team. Please raise an Issue if you believe there is a problem with any of these files. 52 | - Your PR is more likely to be accepted if it focuses on just one change. 53 | - Describe what the PR does. There's no convention enforced, but please try to be concise and descriptive. Treat the PR description as a commit message. Titles that start with "fix"/"add"/"improve"/"remove" are good examples. 54 | - Please add the associated Issue in the PR description. 55 | - Please include a comment with the results before and after your change. 56 | - There's no need to add or tag reviewers. 57 | - If a reviewer commented on your code or asked for changes, please remember to mark the discussion as resolved after you address it. PRs with unresolved issues should not be merged (even if the comment is unclear or requires no action from your side). 58 | - Please include a comment with the results before and after your change. 59 | - Your PR is more likely to be accepted if it includes tests (We have not historically been very strict about tests, but we would like to improve this!). 60 | - You're welcome to submit a draft PR if you would like early feedback on an idea or an approach. 61 | - Happy coding! 62 | 63 | ### Documentation 64 | 65 | You can build the documents as below and view it at . 66 | 67 | ```bash 68 | make mkdocs-serve 69 | ``` 70 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 as build 2 | 3 | WORKDIR /home/chain-bench 4 | 5 | RUN apk update && \ 6 | apk --no-cache add make go 7 | 8 | COPY . . 9 | 10 | RUN make build 11 | 12 | FROM alpine:3.16 as product 13 | 14 | WORKDIR /home/chain-bench 15 | 16 | COPY --from=build /home/chain-bench/chain-bench /usr/local/bin/chain-bench 17 | COPY --from=build /home/chain-bench/templates/*.tpl templates/ 18 | 19 | 20 | ENTRYPOINT [ "chain-bench" ] -------------------------------------------------------------------------------- /Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | # This dockerfile is used by the goreleaser, only the relevant binary (OS/arch) is copied in. 2 | FROM alpine:3.16 3 | RUN apk --no-cache add ca-certificates 4 | COPY chain-bench /usr/local/bin/chain-bench 5 | COPY templates/*.tpl templates/ 6 | 7 | ENTRYPOINT [ "chain-bench" ] 8 | -------------------------------------------------------------------------------- /cmd/chain-bench/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/commands" 7 | ) 8 | 9 | var version = "dev" 10 | 11 | func main() { 12 | if err := commands.Execute(version); err != nil { 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/CIS-Software-Supply-Chain-Security-Guide-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/chain-bench/936b65a3576ed5f1bc99bef5d9e7ec513a352de3/docs/CIS-Software-Supply-Chain-Security-Guide-v1.0.pdf -------------------------------------------------------------------------------- /docs/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM squidfunk/mkdocs-material:8.2.10 2 | 3 | ## If you want to see exactly the same version as is published to GitHub pages 4 | ## use a private image for insiders, which requires authentication. 5 | 6 | # docker login -u ${GITHUB_USERNAME} -p ${GITHUB_TOKEN} ghcr.io 7 | # FROM ghcr.io/squidfunk/mkdocs-material-insiders 8 | 9 | COPY requirements.txt . 10 | RUN pip install -r requirements.txt 11 | -------------------------------------------------------------------------------- /docs/build/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.2 2 | csscompressor==0.9.5 3 | ghp-import==2.0.2 4 | htmlmin==0.1.12 5 | importlib-metadata==4.11.3 6 | Jinja2==3.1.1 7 | jsmin==3.0.1 8 | Markdown==3.3.6 9 | MarkupSafe==2.1.1 10 | mergedeep==1.3.4 11 | mike==1.1.2 12 | mkdocs==1.3.0 13 | mkdocs-macros-plugin==0.7.0 14 | mkdocs-material==8.2.10 15 | mkdocs-material-extensions==1.0.3 16 | mkdocs-minify-plugin==0.5.0 17 | mkdocs-redirects==1.0.4 18 | packaging==21.3 19 | Pygments==2.11.2 20 | pymdown-extensions==9.3 21 | pyparsing==3.0.8 22 | python-dateutil==2.8.2 23 | PyYAML==6.0 24 | pyyaml-env-tag==0.1 25 | six==1.16.0 26 | termcolor==1.1.0 27 | verspec==0.1.0 28 | watchdog==2.1.7 29 | zipp==3.8.0 30 | 31 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Nix/NixOS 4 | 5 | Direct issues installing `chain-bench` via `nix` through the channels mentioned [here](https://nixos.wiki/wiki/Support) 6 | 7 | You can use `nix` on Linux or macOS and on other platforms unofficially. 8 | 9 | `nix-env --install -A nixpkgs.chain-bench` 10 | 11 | Or through your configuration as usual 12 | 13 | NixOS: 14 | 15 | ```nix 16 | # your other config ... 17 | environment.systemPackages = with pkgs; [ 18 | # your other packages ... 19 | chain-bench 20 | ]; 21 | ``` 22 | 23 | home-manager: 24 | 25 | ```nix 26 | # your other config ... 27 | home.packages = with pkgs; [ 28 | # your other packages ... 29 | chain-bench 30 | ]; 31 | ``` 32 | 33 | ## Binary 34 | 35 | Download the archive file for your operating system/architecture from [here](https://github.com/aquasecurity/chain-bench/releases/latest). 36 | 37 | Unpack the archive, and put the binary somewhere in your `$PATH` (on UNIX-y systems, `/usr/local/bin` or the like). 38 | Make sure it has execution bits turned on. 39 | 40 | ## From source 41 | 42 | ```bash 43 | mkdir -p $GOPATH/src/github.com/aquasecurity 44 | cd $GOPATH/src/github.com/aquasecurity 45 | git clone --depth 1 https://github.com/aquasecurity/chain-bench 46 | cd chain-bench/cmd/chain-bench/ 47 | export GO111MODULE=on 48 | go install 49 | ``` 50 | 51 | 52 | ## From source with `go install` 53 | 54 | With a sufficient version of `go` you can install and build with `go install github.com/aquasecurity/chain-bench/cmd/chain-bench@latest` 55 | 56 | 57 | ## Docker 58 | 59 | ### Docker Hub 60 | 61 | ```bash 62 | docker pull aquasec/chain-bench:latest 63 | ``` 64 | 65 | 66 | Example: 67 | 68 | ```bash 69 | docker run --rm aquasec/chain-bench:latest scan --repository-url --access-token 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/imgs/banner_dm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/chain-bench/936b65a3576ed5f1bc99bef5d9e7ec513a352de3/docs/imgs/banner_dm.png -------------------------------------------------------------------------------- /docs/imgs/banner_lm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/chain-bench/936b65a3576ed5f1bc99bef5d9e7ec513a352de3/docs/imgs/banner_lm.png -------------------------------------------------------------------------------- /docs/imgs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/chain-bench/936b65a3576ed5f1bc99bef5d9e7ec513a352de3/docs/imgs/demo.gif -------------------------------------------------------------------------------- /docs/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/chain-bench/936b65a3576ed5f1bc99bef5d9e7ec513a352de3/docs/imgs/logo.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | - toc 5 | --- 6 | 7 | 8 | 9 | Chain-bench is an open-source tool for auditing your software supply chain stack for security compliance based on a new CIS Software Supply Chain benchmark. 10 | The auditing focuses on the entire SDLC process, where it can reveal risks from code time into deploy time. To win the race against hackers and protect your sensitive data and customer trust, you need to ensure your code is compliant with your organization’s policies. 11 | 12 |
13 |

Demo

14 |
15 | 16 |
17 | 18 |
Demo: Vulnerability Detection
19 |
20 | 21 | --- 22 | 23 | Chain-bench is an [Aqua Security][aquasec] open source project. 24 | Learn about our open source work and portfolio [here][oss]. 25 | Contact us about any matter by opening a GitHub Discussion [here][discussions] 26 | 27 | [aquasec]: https://aquasec.com 28 | [oss]: https://www.aquasec.com/products/open-source-projects/ 29 | [discussions]: https://github.com/aquasecurity/chain-bench/discussions 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aquasecurity/chain-bench 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alexeyco/simpletable v1.0.0 7 | github.com/argonsecurity/pipeline-parser v0.1.12 8 | github.com/google/go-github/v41 v41.0.0 9 | github.com/google/uuid v1.2.0 10 | github.com/imdario/mergo v0.3.12 11 | github.com/migueleliasweb/go-github-mock v0.0.8 12 | github.com/rs/zerolog v1.26.1 13 | github.com/spf13/cobra v1.5.0 14 | github.com/spf13/viper v1.11.0 15 | github.com/stretchr/testify v1.8.0 16 | github.com/thoas/go-funk v0.9.2 17 | golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c 18 | ) 19 | 20 | require ( 21 | github.com/agnivade/levenshtein v1.0.1 // indirect 22 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 23 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 24 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect 25 | github.com/vektah/gqlparser/v2 v2.4.6 // indirect 26 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect 27 | ) 28 | 29 | require ( 30 | github.com/OneOfOne/xxhash v1.2.8 // indirect 31 | github.com/davecgh/go-spew v1.1.1 // indirect 32 | github.com/ghodss/yaml v1.0.0 // indirect 33 | github.com/gobwas/glob v0.2.3 // indirect 34 | github.com/mattn/go-runewidth v0.0.13 // indirect 35 | github.com/pkg/errors v0.9.1 // indirect 36 | github.com/pmezard/go-difflib v1.0.0 // indirect 37 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 38 | github.com/rivo/uniseg v0.2.0 // indirect; indirect= 39 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 40 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 41 | github.com/yashtewari/glob-intersection v0.1.0 // indirect 42 | gopkg.in/yaml.v3 v3.0.1 // indirect 43 | ) 44 | 45 | require ( 46 | github.com/enescakir/emoji v1.0.0 47 | github.com/fsnotify/fsnotify v1.5.4 // indirect 48 | github.com/golang/protobuf v1.5.2 // indirect 49 | github.com/google/go-querystring v1.1.0 // indirect 50 | github.com/gorilla/mux v1.8.0 // indirect 51 | github.com/hashicorp/go-version v1.4.0 52 | github.com/hashicorp/hcl v1.0.0 // indirect 53 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 54 | github.com/magiconair/properties v1.8.6 // indirect 55 | github.com/mitchellh/mapstructure v1.5.0 56 | github.com/open-policy-agent/opa v0.43.1 57 | github.com/pelletier/go-toml v1.9.5 // indirect 58 | github.com/spf13/afero v1.8.2 // indirect 59 | github.com/spf13/cast v1.4.1 // indirect 60 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 61 | github.com/spf13/pflag v1.0.5 // indirect 62 | github.com/subosito/gotenv v1.2.0 // indirect 63 | github.com/xanzy/go-gitlab v0.73.1 64 | golang.org/x/crypto v0.21.0 // indirect 65 | golang.org/x/net v0.23.0 // indirect 66 | golang.org/x/sys v0.20.0 // indirect 67 | golang.org/x/text v0.14.0 // indirect 68 | google.golang.org/appengine v1.6.7 // indirect 69 | google.golang.org/protobuf v1.33.0 // indirect 70 | gopkg.in/ini.v1 v1.66.4 // indirect 71 | gopkg.in/yaml.v2 v2.4.0 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /internal/checker/runner.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/config" 7 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 8 | ) 9 | 10 | func RunChecks(ad *checkmodels.AssetsData, c *config.Configuration, checks []*checkmodels.Check) ([]checkmodels.CheckRunResult, []error) { 11 | checksCount := getChecksCount(checks) 12 | resultsChan := make(chan checkmodels.CheckRunResult, checksCount) 13 | errsChan := make(chan error, checksCount) 14 | 15 | var wg sync.WaitGroup 16 | 17 | checkData := checkmodels.CheckData{ 18 | AssetsMetadata: ad, 19 | Configuration: c, 20 | } 21 | 22 | for _, check := range checks { 23 | wg.Add(1) 24 | 25 | go func(wg *sync.WaitGroup, action checkmodels.CheckAction, checkData *checkmodels.CheckData) { 26 | defer wg.Done() 27 | results, err := action(checkData) 28 | if err != nil { 29 | errsChan <- err 30 | return 31 | } 32 | 33 | for _, r := range results { 34 | resultsChan <- checkmodels.CheckRunResult{ 35 | ID: r.ID, 36 | Metadata: r.Metadata, 37 | Result: r.Result, 38 | } 39 | } 40 | 41 | }(&wg, check.Action, &checkData) 42 | } 43 | 44 | wg.Wait() 45 | close(resultsChan) 46 | close(errsChan) 47 | 48 | return readChan(resultsChan), readChan(errsChan) 49 | } 50 | 51 | func readChan[T any](dataChan <-chan T) []T { 52 | data := make([]T, 0) 53 | for d := range dataChan { 54 | data = append(data, d) 55 | } 56 | return data 57 | } 58 | 59 | func getChecksCount(checks []*checkmodels.Check) int { 60 | ids := map[string]bool{} 61 | for _, action := range checks { 62 | for key := range action.CheckMetadataMap.Checks { 63 | ids[key] = true 64 | } 65 | } 66 | return len(ids) 67 | } 68 | -------------------------------------------------------------------------------- /internal/checks/artifacts/access-to-artifacts/access_to_artifacts.go: -------------------------------------------------------------------------------- 1 | package accesstoartifacts 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/artifacts/access-to-artifacts/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4.2", 3 | "name": "Access to Artifacts", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-artifacts/4.2", 5 | "checks": { 6 | "4.2.3": { 7 | "title": "Ensure user's access to the package registry utilizes MFA", 8 | "severity": "Critical", 9 | "type": "ARTIFACT", 10 | "entity": "PackageRegistry", 11 | "description": "Enforce Multi Factor Authentication for user access to the package registry.", 12 | "remediation": "For each package registry in use, enforce Multi-Factor Authentication as the only way to authenticate.", 13 | "scannerType": "Rego", 14 | "slsa_level": [4] 15 | }, 16 | "4.2.5": { 17 | "title": "Ensure anonymous access to artifacts is revoked", 18 | "severity": "Critical", 19 | "type": "ARTIFACT", 20 | "entity": "PackageRegistry", 21 | "description": "Disable anonymous access to artifacts.", 22 | "remediation": "Disable the anonymous access option on every artifact or package manager in use.", 23 | "scannerType": "Rego", 24 | "slsa_level": [4] 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /internal/checks/artifacts/access-to-artifacts/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.common.permissions as permissionslib 5 | import data.generic.utils as utilsLib 6 | import future.keywords.in 7 | 8 | is_two_factor_authentication_disabled_in_registry { 9 | input.Registry.TwoFactorRequirementEnabled == false 10 | } 11 | 12 | is_registry_packages_allows_anonymous_access[unauth_packages] { 13 | unauth_packages := count([p | 14 | p := input.Registry.Packages[_] 15 | p.Repository.ID == input.Repository.ID 16 | p.Visibility == "public" 17 | p.Repository.IsPrivate == true 18 | ]) 19 | 20 | unauth_packages > 0 21 | } 22 | 23 | CbPolicy[msg] { 24 | utilsLib.is_registry_data_missing 25 | msg := {"ids": ["4.2.3", "4.2.5"], "status": constsLib.status.Unknown, "details": constsLib.details.registry_data_is_missing} 26 | } 27 | 28 | CbPolicy[msg] { 29 | not utilsLib.is_registry_data_missing 30 | permissionslib.is_missing_org_settings_permission 31 | msg := {"ids": ["4.2.3"], "status": constsLib.status.Unknown, "details": constsLib.details.organization_missing_minimal_permissions} 32 | } 33 | 34 | CbPolicy[msg] { 35 | not utilsLib.is_registry_data_missing 36 | permissionslib.is_missing_org_packages_permission 37 | msg := {"ids": ["4.2.5"], "status": constsLib.status.Unknown, "details": constsLib.details.organization_packages_missing_minimal_permissions} 38 | } 39 | 40 | CbPolicy[msg] { 41 | not utilsLib.is_registry_data_missing 42 | not permissionslib.is_missing_org_settings_permission 43 | is_two_factor_authentication_disabled_in_registry 44 | msg := {"ids": ["4.2.3"], "status": constsLib.status.Failed} 45 | } 46 | 47 | CbPolicy[msg] { 48 | not utilsLib.is_registry_data_missing 49 | not permissionslib.is_missing_org_packages_permission 50 | unauth_packages := is_registry_packages_allows_anonymous_access[i] 51 | details := sprintf("%v %v", [format_int(unauth_packages, 10), "anonymous accessed packages"]) 52 | msg := {"ids": ["4.2.5"], "status": constsLib.status.Failed, "details": details} 53 | } 54 | -------------------------------------------------------------------------------- /internal/checks/artifacts/artifacts.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | accesstoartifacts "github.com/aquasecurity/chain-bench/internal/checks/artifacts/access-to-artifacts" 5 | packageregistries "github.com/aquasecurity/chain-bench/internal/checks/artifacts/package-registries" 6 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 7 | ) 8 | 9 | func GetChecks() []*checkmodels.Check { 10 | checks := []*checkmodels.Check{} 11 | checks = append(checks, accesstoartifacts.GetChecks()...) 12 | checks = append(checks, packageregistries.GetChecks()...) 13 | return checks 14 | } 15 | -------------------------------------------------------------------------------- /internal/checks/artifacts/package-registries/package_registries.go: -------------------------------------------------------------------------------- 1 | package packageregistries 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/artifacts/package-registries/package_registries_test.go: -------------------------------------------------------------------------------- 1 | package packageregistries 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/checks/common" 7 | "github.com/aquasecurity/chain-bench/internal/checks/consts" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | "github.com/aquasecurity/chain-bench/internal/testutils" 10 | "github.com/aquasecurity/chain-bench/internal/testutils/builders" 11 | "github.com/aquasecurity/chain-bench/internal/utils" 12 | ) 13 | 14 | func TestPackageRegistryChecker(t *testing.T) { 15 | tests := []testutils.CheckTest{ 16 | 17 | { 18 | Name: "should return unknown when there are no hooks permissions", 19 | Data: &checkmodels.CheckData{ 20 | AssetsMetadata: builders.NewAssetsDataBuilder(). 21 | WithOrganization(builders.NewOrganizationBuilder().WithNoPackageWebHooks().Build()). 22 | Build(), 23 | }, 24 | Expected: []*checkmodels.CheckRunResult{ 25 | checkmodels.ToCheckRunResult("4.3.4", checksMetadata.Checks["4.3.4"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_hooks_missingMinimalPermissions}), 26 | }, 27 | }, 28 | { 29 | Name: "should fail and return the number of unsecured hooks when the user has 1 org webhook with no ssl", 30 | Data: &checkmodels.CheckData{ 31 | AssetsMetadata: builders.NewAssetsDataBuilder(). 32 | WithOrganization(builders.NewOrganizationBuilder().WithPackageWebHooks("https://endpoint.com", "1", utils.GetPtr("**")).Build()).Build(), 33 | }, 34 | Expected: []*checkmodels.CheckRunResult{ 35 | checkmodels.ToCheckRunResult("4.3.4", checksMetadata.Checks["4.3.4"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: "1 unsecured webhooks"}), 36 | }, 37 | }, 38 | { 39 | Name: "should fail and return the number of unsecured hooks when the user has 1 repo webhook with no secret", 40 | Data: &checkmodels.CheckData{ 41 | AssetsMetadata: builders.NewAssetsDataBuilder(). 42 | WithRepository(builders.NewRepositoryBuilder().WithPackageWebHooks("https://endpoint.com", "0", nil).Build()). 43 | Build(), 44 | }, 45 | Expected: []*checkmodels.CheckRunResult{ 46 | checkmodels.ToCheckRunResult("4.3.4", checksMetadata.Checks["4.3.4"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: "1 unsecured webhooks"}), 47 | }, 48 | }, 49 | { 50 | Name: "should fail and return the number of unsecured hooks when the user has 1 org webhook with no https", 51 | Data: &checkmodels.CheckData{ 52 | AssetsMetadata: builders.NewAssetsDataBuilder(). 53 | WithOrganization(builders.NewOrganizationBuilder().WithPackageWebHooks("http://endpoint.com", "0", utils.GetPtr("**")). 54 | Build()). 55 | Build(), 56 | }, 57 | Expected: []*checkmodels.CheckRunResult{ 58 | checkmodels.ToCheckRunResult("4.3.4", checksMetadata.Checks["4.3.4"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: "1 unsecured webhooks"}), 59 | }, 60 | }, 61 | { 62 | Name: "valid input - all rules should pass", 63 | Data: &checkmodels.CheckData{ 64 | AssetsMetadata: builders.NewAssetsDataBuilder().Build(), 65 | }, 66 | Expected: []*checkmodels.CheckRunResult{}, 67 | }, 68 | } 69 | testutils.RunCheckTests(t, common.GetRegoRunAction(regoQuery, checksMetadata), tests, checksMetadata) 70 | } 71 | -------------------------------------------------------------------------------- /internal/checks/artifacts/package-registries/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4.3", 3 | "name": "Package Registries", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-artifacts/4.3", 5 | "checks": { 6 | "4.3.4": { 7 | "title": "Ensure webhooks of the package registry are secured", 8 | "severity": "Critical", 9 | "type": "ARTIFACT", 10 | "entity": "PackageRegistry", 11 | "description": "Use secured webhooks of the package registry.", 12 | "remediation": "For each webhook in use, change it to secured (over HTTPS).", 13 | "scannerType": "Rego", 14 | "slsa_level": [4] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /internal/checks/artifacts/package-registries/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.common.permissions as permissionslib 5 | import data.generic.utils as utilsLib 6 | import future.keywords.in 7 | 8 | is_org_have_unsucured_hooks[unsecuredHooks] { 9 | hooks := array.concat(input.Organization.Hooks, input.Repository.Hooks) 10 | packageHooks := [p | p := hooks[_]; p.Events[t] in ["package", "registry_package"]] 11 | securedHooks := count({i | 12 | hook := packageHooks[i] 13 | hook.Config.Insecure_SSL == "0" 14 | hook.Config.Secret != null 15 | not regex.match("^http://", hook.Config.URL) 16 | }) 17 | 18 | unsecuredHooks := count(packageHooks) - securedHooks 19 | unsecuredHooks > 0 20 | } 21 | 22 | CbPolicy[msg] { 23 | utilsLib.is_repository_data_missing 24 | utilsLib.is_organization_data_missing 25 | msg := {"ids": ["4.3.4"], "status": constsLib.status.Unknown} 26 | } 27 | 28 | CbPolicy[msg] { 29 | permissionslib.is_missing_hooks_permission 30 | msg := {"ids": ["4.3.4"], "status": constsLib.status.Unknown, "details": constsLib.details.hooks_missing_minimal_permissions} 31 | } 32 | 33 | #Looking for organization 2mfa enforcements that is disabled 34 | CbPolicy[msg] { 35 | not utilsLib.is_repository_data_missing 36 | not utilsLib.is_organization_data_missing 37 | not permissionslib.is_missing_hooks_permission 38 | unsecuredHooks := is_org_have_unsucured_hooks[i] 39 | details := sprintf("%v %v", [format_int(unsecuredHooks, 10), "unsecured webhooks"]) 40 | msg := {"ids": ["4.3.4"], "status": constsLib.status.Failed, "details": details} 41 | } 42 | -------------------------------------------------------------------------------- /internal/checks/artifacts/sections.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 4, 3 | "text": "Artifacts", 4 | "type": "artifacts" 5 | } -------------------------------------------------------------------------------- /internal/checks/build-pipelines/build-pipelines.go: -------------------------------------------------------------------------------- 1 | package buildpipelines 2 | 3 | import ( 4 | pipelineinstructions "github.com/aquasecurity/chain-bench/internal/checks/build-pipelines/pipeline-instructions" 5 | pipelineintegrity "github.com/aquasecurity/chain-bench/internal/checks/build-pipelines/pipeline-integrity" 6 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 7 | ) 8 | 9 | func GetChecks() []*checkmodels.Check { 10 | checks := []*checkmodels.Check{} 11 | checks = append(checks, pipelineinstructions.GetChecks()...) 12 | checks = append(checks, pipelineintegrity.GetChecks()...) 13 | return checks 14 | } 15 | -------------------------------------------------------------------------------- /internal/checks/build-pipelines/pipeline-instructions/pipeline_instructions.go: -------------------------------------------------------------------------------- 1 | package pipelineinstructions 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/build-pipelines/pipeline-instructions/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2.3", 3 | "name": "Build Instructions", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-buildpipelines/2.3", 5 | "checks": { 6 | "2.3.1": { 7 | "title": "Ensure all build steps are defined as code", 8 | "severity": "High", 9 | "type": "BUILD", 10 | "entity": "Pipeline", 11 | "description": "Use Pipeline as Code for build pipelines and their defined steps.", 12 | "remediation": "Convert pipeline instructions into code-based syntax, and upload them to the organization's version control platform.", 13 | "scannerType": "Rego", 14 | "slsa_level": [1,2,3,4] 15 | }, 16 | "2.3.5": { 17 | "title": "Ensure access to the build process's triggering is minimized", 18 | "severity": "Medium", 19 | "type": "BUILD", 20 | "entity": "Pipeline", 21 | "description": "Restrict access to the pipelines' triggers.", 22 | "remediation": "For every pipeline in use, grant only the necessary members permission to trigger it.", 23 | "scannerType": "Rego", 24 | "slsa_level": [] 25 | }, 26 | "2.3.7": { 27 | "title": "Ensure pipelines are automatically scanned for vulnerabilities", 28 | "severity": "Critical", 29 | "type": "BUILD", 30 | "entity": "Pipeline", 31 | "description": "Scan pipelines for vulnerabilities. It is recommended to do that automatically.", 32 | "remediation": "For each pipeline, set automated vulnerabilities scanning.", 33 | "scannerType": "Rego", 34 | "slsa_level": [4] 35 | }, 36 | "2.3.8": { 37 | "title": "Ensure scanners are in place to identify and prevent sensitive data in pipeline files", 38 | "severity": "Critical", 39 | "type": "BUILD", 40 | "entity": "Pipeline", 41 | "description": "Detect and prevent sensitive data, such as confidential ID numbers, passwords, etc. in pipelines.", 42 | "remediation": "For every pipeline that is in use, set scanners that will identify and prevent sensitive data in it.", 43 | "scannerType": "Rego", 44 | "slsa_level": [4] 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /internal/checks/build-pipelines/pipeline-instructions/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.common.permissions as permissionsLib 5 | import data.generic.utils as utilsLib 6 | import future.keywords.in 7 | 8 | pipelineRuleIds = [ 9 | "2.3.1", 10 | "2.3.7", 11 | "2.3.8", 12 | ] 13 | 14 | secret_scan_commands = [ 15 | `spectral.* scan`, 16 | `git secrets --scan`, 17 | `whispers`, 18 | `docker run.* abhartiya/tools_gitallsecrets`, 19 | `detect-secrets.* scan`, 20 | ] 21 | 22 | does_job_contain_one_of_tasks(job, regexes) { 23 | job.steps[i].type == "task" 24 | regex.match(regexes[_], job.steps[i].task.name) 25 | } 26 | 27 | does_job_contain_one_of_shell_commands(job, regexes) { 28 | job.steps[i].type == "shell" 29 | r := regexes[_] 30 | regex.match(r, job.steps[i].shell.script) 31 | } 32 | 33 | is_pipeline_scaning_tasks_missing { 34 | count({job | job := input.Pipelines[_].jobs[_]; does_job_contain_one_of_tasks(job, constsLib.pipeline_vulnerability_scan_tasks)}) == 0 35 | } 36 | 37 | is_repository_scanning_tasks_missing { 38 | count({job | job := input.Pipelines[_].jobs[_]; does_job_contain_one_of_tasks(job, constsLib.secret_scan_tasks)}) == 0 39 | count({job | job := input.Pipelines[_].jobs[_]; does_job_contain_one_of_shell_commands(job, secret_scan_commands)}) == 0 40 | } 41 | 42 | is_build_job_missing { 43 | count({job | job := input.Pipelines[_].jobs[_]; job.metadata.build == true}) == 0 44 | } 45 | 46 | # In case pipelines weren't fetched 47 | CbPolicy[msg] { 48 | utilsLib.is_pipelines_data_missing 49 | msg = {"ids": pipelineRuleIds, "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_data_is_missing} 50 | } 51 | 52 | # In case there are no pipelines 53 | CbPolicy[msg] { 54 | not utilsLib.is_pipelines_data_missing 55 | utilsLib.is_pipelines_list_empty 56 | msg = {"ids": pipelineRuleIds, "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_no_pipelines_found} 57 | } 58 | 59 | # There is no build job 60 | CbPolicy[msg] { 61 | not utilsLib.is_pipelines_data_missing 62 | not utilsLib.is_pipelines_list_empty 63 | is_build_job_missing 64 | msg := {"ids": ["2.3.1"], "status": constsLib.status.Failed, "details": constsLib.details.pipeline_no_build_job} 65 | } 66 | 67 | # In case organization is not fetched 68 | CbPolicy[msg] { 69 | utilsLib.is_organization_data_missing 70 | msg = {"ids": ["2.3.5"], "status": constsLib.status.Unknown, "details": constsLib.details.organization_not_fetched} 71 | } 72 | 73 | # In case oraganization default permissions weren't fetched 74 | CbPolicy[msg] { 75 | not utilsLib.is_organization_data_missing 76 | permissionsLib.is_missing_org_settings_permission 77 | msg = {"ids": ["2.3.5"], "status": constsLib.status.Unknown, "details": constsLib.details.organization_missing_minimal_permissions} 78 | } 79 | 80 | # In case organzation default permissions are too permissive 81 | CbPolicy[msg] { 82 | not utilsLib.is_organization_data_missing 83 | not permissionsLib.is_org_default_permission_strict 84 | msg = {"ids": ["2.3.5"], "status": constsLib.status.Failed, "details": constsLib.details.organization_premissive_default_repository_permissions} 85 | } 86 | 87 | # Looking for a pipeline that scans for vulnerabilities 88 | CbPolicy[msg] { 89 | not utilsLib.is_pipelines_data_missing 90 | not utilsLib.is_pipelines_list_empty 91 | is_pipeline_scaning_tasks_missing 92 | msg = {"ids": ["2.3.7"], "status": constsLib.status.Failed, "details": constsLib.details.pipeline_pipelines_not_scanned_for_vulnerabilities} 93 | } 94 | 95 | # Looking for a pipelinethat scans for secrets 96 | CbPolicy[msg] { 97 | not utilsLib.is_pipelines_data_missing 98 | not utilsLib.is_pipelines_list_empty 99 | is_repository_scanning_tasks_missing 100 | msg = {"ids": ["2.3.8"], "status": constsLib.status.Failed, "details": constsLib.details.pipeline_repository_not_scanned_for_secrets} 101 | } 102 | -------------------------------------------------------------------------------- /internal/checks/build-pipelines/pipeline-integrity/pipeline_integrity.go: -------------------------------------------------------------------------------- 1 | package pipelineintegrity 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/build-pipelines/pipeline-integrity/pipeline_integrity_test.go: -------------------------------------------------------------------------------- 1 | package pipelineintegrity 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/checks/common" 7 | "github.com/aquasecurity/chain-bench/internal/checks/consts" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | "github.com/aquasecurity/chain-bench/internal/testutils" 10 | "github.com/aquasecurity/chain-bench/internal/testutils/builders" 11 | ) 12 | 13 | func TestBuildChecker(t *testing.T) { 14 | tests := []testutils.CheckTest{ 15 | { 16 | Name: "Should fail and return number of piplines contain build job without SBOM task", 17 | Data: &checkmodels.CheckData{ 18 | AssetsMetadata: builders.NewAssetsDataBuilder(). 19 | WithPipeline(builders. 20 | NewPipelineBuilder().WithNoJobs(). 21 | WithJob(builders. 22 | NewJobBuilder().WithNoTasks(). 23 | WithTask("NORMAL_TASK_NAME", "commit"). 24 | Build(), 25 | ). 26 | Build(), 27 | ).Build(), 28 | }, 29 | Expected: []*checkmodels.CheckRunResult{ 30 | checkmodels.ToCheckRunResult("2.4.6", checksMetadata.Checks["2.4.6"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: "1 pipeline(s) contain a build job without SBOM generation"}), 31 | }, 32 | }, 33 | { 34 | Name: "Multiple pipelines one with unpinned task should fail snd return number of tasks", 35 | Data: &checkmodels.CheckData{ 36 | AssetsMetadata: builders.NewAssetsDataBuilder(). 37 | WithPipeline(builders. 38 | NewPipelineBuilder().WithNoJobs().WithJob(builders.NewJobBuilder().WithTask("NORMAL_TASK_NAME", "tag").Build()). 39 | Build(), 40 | ).Build(), 41 | }, 42 | Expected: []*checkmodels.CheckRunResult{ 43 | checkmodels.ToCheckRunResult("2.4.2", checksMetadata.Checks["2.4.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: "1 task(s) are not pinned"}), 44 | }, 45 | }, 46 | { 47 | Name: "valid input - all rules should pass", 48 | Data: &checkmodels.CheckData{ 49 | AssetsMetadata: builders.NewAssetsDataBuilder().Build(), 50 | }, 51 | Expected: []*checkmodels.CheckRunResult{}, 52 | }, 53 | { 54 | Name: "Should return unkown when failed to fetch pipelines", 55 | Data: &checkmodels.CheckData{ 56 | AssetsMetadata: builders.NewAssetsDataBuilder().WithNoPipelinesData().Build(), 57 | }, 58 | Expected: []*checkmodels.CheckRunResult{ 59 | checkmodels.ToCheckRunResult("2.4.2", checksMetadata.Checks["2.4.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_are_missing}), 60 | checkmodels.ToCheckRunResult("2.4.6", checksMetadata.Checks["2.4.6"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_are_missing}), 61 | }, 62 | }, 63 | { 64 | Name: "Should return unkown when there are no pipelines", 65 | Data: &checkmodels.CheckData{ 66 | AssetsMetadata: builders.NewAssetsDataBuilder(). 67 | WithZeroPipelines(). 68 | Build(), 69 | }, 70 | Expected: []*checkmodels.CheckRunResult{ 71 | checkmodels.ToCheckRunResult("2.4.2", checksMetadata.Checks["2.4.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_noPipelinesFound}), 72 | checkmodels.ToCheckRunResult("2.4.6", checksMetadata.Checks["2.4.6"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_noPipelinesFound}), 73 | }, 74 | }, 75 | } 76 | testutils.RunCheckTests(t, common.GetRegoRunAction(regoQuery, checksMetadata), tests, checksMetadata) 77 | } 78 | -------------------------------------------------------------------------------- /internal/checks/build-pipelines/pipeline-integrity/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2.4", 3 | "name": "Pipeline Integrity", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-buildpipelines/2.4", 5 | "checks": { 6 | "2.4.2": { 7 | "title": "Ensure all external dependencies used in the build process are locked", 8 | "severity": "Critical", 9 | "type": "BUILD", 10 | "entity": "Pipeline", 11 | "description": "External dependencies might be public packages needed in the pipeline or even the public image used for the build worker. Lock these external dependencies in every build pipeline.", 12 | "remediation": "For every external dependency in use in pipelines, lock it.", 13 | "scannerType": "Rego", 14 | "slsa_level": [4] 15 | }, 16 | "2.4.6": { 17 | "title": "Ensure pipeline steps produce an SBOM", 18 | "severity": "High", 19 | "type": "BUILD", 20 | "entity": "Pipeline", 21 | "description": "SBOM (Software Bill Of Materials) is a file that specifies each component of software or a build process. Generate an SBOM after each run of a pipeline.", 22 | "remediation": "For each pipeline, configure it to produce an SBOM on every run.", 23 | "scannerType": "Rego", 24 | "slsa_level": [1,2,3,4] 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /internal/checks/build-pipelines/pipeline-integrity/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.generic.utils as utilsLib 5 | import future.keywords.in 6 | 7 | ruleIds = [ 8 | "2.4.2", 9 | "2.4.6", 10 | ] 11 | 12 | sbom_tasks = [ 13 | `argonsecurity/actions/generate-manifest`, 14 | `anchore/sbom-action`, 15 | `CycloneDX/gh-\w+-generate-sbom`, 16 | ] 17 | 18 | sbom_generation_commands = [ 19 | `billy generate`, 20 | `trivy sbom`, 21 | `trivy .* --format cyclonedx`, 22 | `syft .*`, 23 | `spdx-sbom-generator`, 24 | `cyclonedx-\w+`, 25 | `jake sbom`, 26 | ] 27 | 28 | does_job_contain_one_of_tasks(job, regexes) { 29 | job.steps[i].type == "task" 30 | regex.match(regexes[_], job.steps[i].task.name) 31 | } 32 | 33 | does_job_contain_one_of_shell_commands(job, regexes) { 34 | job.steps[i].type == "shell" 35 | r := regexes[_] 36 | regex.match(r, job.steps[i].shell.script) 37 | } 38 | 39 | is_task_version_pinned(step) { 40 | step.type == "task" 41 | step.task.version_type != "commit" 42 | } 43 | 44 | is_all_tasks_pinned[unpinnedtaskCount] { 45 | unpinnedtaskCount = count({step | 46 | step = input.Pipelines[_].jobs[_].steps[_] 47 | is_task_version_pinned(step) 48 | }) 49 | 50 | unpinnedtaskCount > 0 51 | } 52 | 53 | are_there_pipelines_without_sbom[pipelinesWithoutSBOM] { 54 | pipelinesWithoutSBOM := count({i | 55 | input.Pipelines[i].jobs[j].metadata.build == true 56 | job := input.Pipelines[i].jobs[j] 57 | not does_job_contain_one_of_tasks(job, sbom_tasks) 58 | not does_job_contain_one_of_shell_commands(job, sbom_generation_commands) 59 | }) 60 | 61 | pipelinesWithoutSBOM > 0 62 | } 63 | 64 | # In case pipelines weren't fetched 65 | CbPolicy[msg] { 66 | utilsLib.is_pipelines_data_missing 67 | msg = {"ids": ruleIds, "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_data_is_missing} 68 | } 69 | 70 | # In case there are no pipelines 71 | CbPolicy[msg] { 72 | not utilsLib.is_pipelines_data_missing 73 | utilsLib.is_pipelines_list_empty 74 | msg = {"ids": ruleIds, "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_no_pipelines_found} 75 | } 76 | 77 | # Looking for tasks that are not pinned 78 | CbPolicy[msg] { 79 | not utilsLib.is_pipelines_data_missing 80 | unpinnedtaskCount := is_all_tasks_pinned[i] 81 | details := sprintf("%v task(s) are not pinned", [unpinnedtaskCount]) 82 | msg := {"ids": ["2.4.2"], "status": constsLib.status.Failed, "details": details} 83 | } 84 | 85 | # Looking for build jobs with an SBOM 86 | CbPolicy[msg] { 87 | not utilsLib.is_pipelines_data_missing 88 | pipelinesWithoutSBOM := are_there_pipelines_without_sbom[i] 89 | details := sprintf("%v pipeline(s) contain a build job without SBOM generation", [pipelinesWithoutSBOM]) 90 | msg = {"ids": ["2.4.6"], "status": constsLib.status.Failed, "details": details} 91 | } 92 | -------------------------------------------------------------------------------- /internal/checks/build-pipelines/sections.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "text": "Build Pipelines", 4 | "type": "buildPipelines" 5 | } -------------------------------------------------------------------------------- /internal/checks/checks.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/checks/artifacts" 5 | buildpipelines "github.com/aquasecurity/chain-bench/internal/checks/build-pipelines" 6 | "github.com/aquasecurity/chain-bench/internal/checks/dependencies" 7 | sourcecode "github.com/aquasecurity/chain-bench/internal/checks/source-code" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | func GetChecks(ad *checkmodels.AssetsData) []*checkmodels.Check { 12 | checks := make([]*checkmodels.Check, 0) 13 | checks = append(checks, sourcecode.GetChecks()...) 14 | checks = append(checks, buildpipelines.GetChecks()...) 15 | checks = append(checks, dependencies.GetChecks()...) 16 | checks = append(checks, artifacts.GetChecks()...) 17 | return checks 18 | } 19 | -------------------------------------------------------------------------------- /internal/checks/checks_test.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/models" 7 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 8 | "github.com/aquasecurity/chain-bench/internal/utils" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var ( 13 | codeRules = []string{ 14 | "1.1.3", 15 | "1.1.4", 16 | "1.1.5", 17 | "1.1.6", 18 | "1.1.8", 19 | "1.1.9", 20 | "1.1.10", 21 | "1.1.11", 22 | "1.1.12", 23 | "1.1.13", 24 | "1.1.14", 25 | "1.1.15", 26 | "1.1.16", 27 | "1.1.17", 28 | "1.2.1", 29 | "1.2.2", 30 | "1.2.3", 31 | "1.2.4", 32 | "1.3.1", 33 | "1.3.3", 34 | "1.3.5", 35 | "1.3.7", 36 | "1.3.8", 37 | "1.3.9", 38 | } 39 | 40 | buildRules = []string{ 41 | "2.3.1", 42 | "2.3.5", 43 | "2.3.7", 44 | "2.3.8", 45 | "2.4.2", 46 | "2.4.6", 47 | } 48 | 49 | dependenciesRules = []string{ 50 | "3.1.7", 51 | "3.2.2", 52 | "3.2.3", 53 | } 54 | 55 | artifactRules = []string{ 56 | "4.2.3", 57 | "4.2.5", 58 | "4.3.4", 59 | } 60 | ) 61 | 62 | func TestGetChecks(t *testing.T) { 63 | tests := []struct { 64 | Name string 65 | ExpectedIds []string 66 | AssetsData checkmodels.AssetsData 67 | }{ 68 | { 69 | Name: "All checks loaded", 70 | ExpectedIds: append(append(append( 71 | codeRules, 72 | buildRules...), 73 | dependenciesRules...), 74 | artifactRules..., 75 | ), 76 | AssetsData: checkmodels.AssetsData{Organization: &models.Organization{}, Repository: &models.Repository{}}, 77 | }, 78 | } 79 | 80 | for _, test := range tests { 81 | test := test 82 | t.Run(test.Name, func(t *testing.T) { 83 | t.Parallel() 84 | checks := GetChecks(&test.AssetsData) 85 | actualIds := getIds(checks) 86 | assert.ElementsMatch(t, test.ExpectedIds, actualIds) 87 | }) 88 | } 89 | } 90 | 91 | func getIds(checks []*checkmodels.Check) []string { 92 | ids := []string{} 93 | for _, cm := range checks { 94 | for m := range cm.CheckMetadataMap.Checks { 95 | if !utils.Contains(ids, m) { 96 | ids = append(ids, m) 97 | } 98 | } 99 | } 100 | return ids 101 | } 102 | -------------------------------------------------------------------------------- /internal/checks/common/assets/consts.rego: -------------------------------------------------------------------------------- 1 | package common.consts 2 | 3 | details := details { 4 | details := { 5 | "organization_missing_minimal_permissions": "Organization is missing minimal permissions", 6 | "organization_not_fetched": "Organization is not fetched", 7 | "organization_packages_missing_minimal_permissions": "Organization Packages is missing minimal permissions", 8 | "organization_premissive_default_repository_permissions": "Organization default permissions are too permissive", 9 | "repository_missing_minimal_permissions": "Repository is missing minimal permissions", 10 | "repository_missing_minimal_permissions_for_branch_protection": "Repository is missing admin permissions for branch protection settings", 11 | "repository_data_is_missing": "Repository is not fetched", 12 | "hooks_missing_minimal_permissions": "Organization & Repository Hooks is missing minimal permissions", 13 | "linear_history_merge_commit_enabled": "MergeCommit is enabled for repository", 14 | "linear_history_require_rebase_or_squash_commit_enabled": "Repository is not configured to allow rebase or squash merge", 15 | "pipeline_no_pipelines_found": "No pipelines were found", 16 | "pipeline_no_build_job": "No build job was found in pipelines", 17 | "pipeline_data_is_missing": "Pipelines are not fetched", 18 | "pipeline_pipelines_not_scanned_for_vulnerabilities": "Pipelines are not scanned for vulnerabilities", 19 | "pipeline_repository_not_scanned_for_secrets": "Repository is not scanned for secrets", 20 | "dependencies_pipelines_not_scanned_for_vulnerabilities": "Pipeline dependencies are not scanned for vulnerabilities", 21 | "dependencies_pipelines_not_scanned_for_licenses": "Pipeline dependencies are not scanned for licenses", 22 | "registry_data_is_missing": "Registry is not fetched", 23 | } 24 | } 25 | 26 | actions := actions { 27 | actions := { 28 | "argon_scanner_action": "argonsecurity/scanner-action", 29 | "trivy_scanner_action": "aquasecurity/trivy-action", 30 | } 31 | } 32 | 33 | pipeline_vulnerability_scan_tasks = [actions.argon_scanner_action, actions.trivy_scanner_action] 34 | 35 | secret_scan_tasks = [ 36 | actions.argon_scanner_action, 37 | "zricethezav/gitleaks-action", 38 | "ShiftLeftSecurity/scan-action", 39 | ] 40 | 41 | status := stat { 42 | stat := { 43 | "Unknown": "Unknown", 44 | "Failed": "Failed", 45 | "Success": "Success", 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/checks/common/assets/permissions.rego: -------------------------------------------------------------------------------- 1 | package common.permissions 2 | 3 | import future.keywords.in 4 | 5 | #for org settings you need to have full private repo permissions(the upper checks under repo) 6 | is_missing_org_settings_permission { 7 | input.Organization.DefaultRepoPermission == null 8 | } 9 | 10 | #for org settings you need to have full private repo permissions(the upper checks under repo) 11 | is_missing_repo_settings_permission { 12 | input.Repository.AllowRebaseMerge == null 13 | } 14 | 15 | is_repo_admin { 16 | some i in input.Repository.Collaborators 17 | i.id == input.AuthorizedUser.id 18 | i.permissions.admin == true 19 | } 20 | 21 | is_missing_hooks_permission { 22 | missingOrgPerm := to_number(input.Organization.Hooks == null) 23 | missingRepoPerm := to_number(input.Repository.Hooks == null) 24 | missingOrgPerm + missingRepoPerm > 0 25 | } 26 | 27 | is_missing_org_packages_permission { 28 | input.Registry.Packages == null 29 | } 30 | 31 | is_org_default_permission_strict { 32 | input.Organization.DefaultRepoPermission in ["read", "none"] 33 | } 34 | -------------------------------------------------------------------------------- /internal/checks/common/assets/utils.rego: -------------------------------------------------------------------------------- 1 | package generic.utils 2 | 3 | is_pipelines_data_missing { 4 | input.Pipelines == null 5 | } 6 | 7 | is_pipelines_list_empty { 8 | count(input.Pipelines) == 0 9 | } 10 | 11 | is_organization_data_missing { 12 | input.Organization == null 13 | } 14 | 15 | is_registry_data_missing { 16 | input.Registry == null 17 | } 18 | 19 | is_repository_data_missing { 20 | input.Repository == null 21 | } 22 | -------------------------------------------------------------------------------- /internal/checks/common/checker_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/consts" 7 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func createDummyAction() checkmodels.CheckAction { 12 | return func(data *checkmodels.CheckData) ([]*checkmodels.CheckRunResult, error) { 13 | return []*checkmodels.CheckRunResult{}, nil 14 | } 15 | } 16 | 17 | func TestValidateCheck(t *testing.T) { 18 | tests := []struct { 19 | Name string 20 | Expected error 21 | CheckMetadata checkmodels.CheckMetadata 22 | CheckId string 23 | Action checkmodels.CheckAction 24 | }{ 25 | { 26 | Name: "A check with all required fields", 27 | Expected: nil, 28 | CheckMetadata: checkmodels.CheckMetadata{ 29 | Title: "SOME NAME", 30 | Type: checkmodels.SCM, 31 | Entity: checkmodels.Organization, 32 | Description: "SOME DESCRIPTION", 33 | Remediation: "SOME REMEDIATION", 34 | Url: "SOME URL", 35 | }, 36 | CheckId: "1.1.9", 37 | Action: createDummyAction(), 38 | }, 39 | { 40 | Name: "A check with no ID", 41 | Expected: consts.ErrorNoCheckID, 42 | CheckMetadata: checkmodels.CheckMetadata{ 43 | Title: "SOME NAME", 44 | Type: checkmodels.SCM, 45 | Entity: checkmodels.Organization, 46 | Description: "SOME DESCRIPTION", 47 | Remediation: "SOME REMEDIATION", 48 | Url: "SOME URL", 49 | }, 50 | CheckId: "", 51 | Action: createDummyAction(), 52 | }, 53 | { 54 | Name: "A check with no name", 55 | Expected: consts.ErrorNoName, 56 | CheckMetadata: checkmodels.CheckMetadata{ 57 | Type: checkmodels.SCM, 58 | Entity: checkmodels.Organization, 59 | Description: "SOME DESCRIPTION", 60 | Remediation: "SOME REMEDIATION", 61 | Url: "SOME URL", 62 | }, 63 | CheckId: "1.1.9", 64 | Action: createDummyAction(), 65 | }, 66 | { 67 | Name: "A check with no description", 68 | Expected: consts.ErrorNoDescription, 69 | CheckMetadata: checkmodels.CheckMetadata{ 70 | Type: checkmodels.SCM, 71 | Entity: checkmodels.Organization, 72 | Title: "SOME NAME", 73 | Url: "SOME URL", 74 | }, 75 | CheckId: "1.1.9", 76 | Action: createDummyAction(), 77 | }, 78 | { 79 | Name: "A check with no remediation", 80 | Expected: consts.ErrorNoRemediation, 81 | CheckMetadata: checkmodels.CheckMetadata{ 82 | Type: checkmodels.SCM, 83 | Entity: checkmodels.Organization, 84 | Title: "SOME NAME", 85 | Description: "SOME DESCRIPTION", 86 | Url: "SOME URL", 87 | }, 88 | CheckId: "1.1.9", 89 | Action: createDummyAction(), 90 | }, 91 | { 92 | Name: "A check with no url", 93 | Expected: consts.ErrorNoUrl, 94 | CheckMetadata: checkmodels.CheckMetadata{ 95 | Type: checkmodels.SCM, 96 | Entity: checkmodels.Organization, 97 | Title: "SOME NAME", 98 | Description: "SOME DESCRIPTION", 99 | Remediation: "SOME REMEDIATION", 100 | }, 101 | CheckId: "1.1.9", 102 | Action: createDummyAction(), 103 | }, 104 | { 105 | Name: "A check with no type", 106 | Expected: consts.ErrorNoType, 107 | CheckMetadata: checkmodels.CheckMetadata{ 108 | Entity: checkmodels.Organization, 109 | Title: "SOME NAME", 110 | Description: "SOME DESCRIPTION", 111 | Remediation: "SOME REMEDIATION", 112 | Url: "SOME URL", 113 | }, 114 | CheckId: "1.1.9", 115 | Action: createDummyAction(), 116 | }, 117 | { 118 | Name: "A check with no entity", 119 | Expected: consts.ErrorNoEntity, 120 | CheckMetadata: checkmodels.CheckMetadata{ 121 | Type: checkmodels.SCM, 122 | Title: "SOME NAME", 123 | Description: "SOME DESCRIPTION", 124 | Remediation: "SOME REMEDIATION", 125 | Url: "SOME URL", 126 | }, 127 | CheckId: "1.1.9", 128 | Action: createDummyAction(), 129 | }, 130 | { 131 | Name: "A check with no action", 132 | Expected: consts.ErrorNoCheckAction, 133 | CheckMetadata: checkmodels.CheckMetadata{ 134 | Type: checkmodels.SCM, 135 | Entity: checkmodels.Organization, 136 | Title: "SOME NAME", 137 | Description: "SOME DESCRIPTION", 138 | Remediation: "SOME REMEDIATION", 139 | Url: "SOME URL", 140 | }, 141 | CheckId: "1.1.9", 142 | Action: nil, 143 | }, 144 | } 145 | 146 | for _, test := range tests { 147 | test := test 148 | t.Run(test.Name, func(t *testing.T) { 149 | t.Parallel() 150 | 151 | actualError := ValidateCheck(test.CheckId, test.CheckMetadata, test.Action, test.CheckMetadata.Url) 152 | 153 | if test.Expected == nil { 154 | assert.NoError(t, actualError) 155 | } else { 156 | assert.EqualError(t, actualError, test.Expected.Error()) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /internal/checks/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/checks/common/opa" 7 | "github.com/aquasecurity/chain-bench/internal/consts" 8 | "github.com/aquasecurity/chain-bench/internal/logger" 9 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 10 | ) 11 | 12 | //go:embed assets/utils.rego 13 | var utilsModule string 14 | 15 | //go:embed assets/permissions.rego 16 | var permissionsModule string 17 | 18 | //go:embed assets/consts.rego 19 | var constsModule string 20 | 21 | func getRegoLibs(regoQuery string) []checkmodels.RegoCustomModule { 22 | return []checkmodels.RegoCustomModule{ 23 | { 24 | Name: "security.rego", 25 | Content: regoQuery, 26 | }, 27 | { 28 | Name: "Generic", 29 | Content: utilsModule, 30 | }, 31 | { 32 | Name: "Consts", 33 | Content: constsModule, 34 | }, 35 | { 36 | Name: "Permissions", 37 | Content: permissionsModule, 38 | }, 39 | } 40 | } 41 | 42 | func GetRegoRunAction(regoQuery string, metadata checkmodels.CheckMetadataMap) checkmodels.CheckAction { 43 | regoLibs := getRegoLibs(regoQuery) 44 | 45 | return func(data *checkmodels.CheckData) ([]*checkmodels.CheckRunResult, error) { 46 | results, err := opa.RunRego(data.AssetsMetadata, regoLibs, &metadata) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return results, nil 51 | } 52 | } 53 | 54 | func ValidateCheck(id string, cm checkmodels.CheckMetadata, action checkmodels.CheckAction, url string) error { 55 | 56 | if id == "" { 57 | return consts.ErrorNoCheckID 58 | } 59 | if cm.Title == "" { 60 | return consts.ErrorNoName 61 | } 62 | 63 | if cm.Description == "" { 64 | return consts.ErrorNoDescription 65 | } 66 | 67 | if cm.Remediation == "" { 68 | return consts.ErrorNoRemediation 69 | } 70 | 71 | if url == "" { 72 | return consts.ErrorNoUrl 73 | } 74 | 75 | if cm.Type == "" { 76 | return consts.ErrorNoType 77 | } 78 | 79 | if cm.Entity == "" { 80 | return consts.ErrorNoEntity 81 | } 82 | 83 | if action == nil { 84 | return consts.ErrorNoCheckAction 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func ValidateChecks(check checkmodels.Check) error { 91 | for id, metadata := range check.CheckMetadataMap.Checks { 92 | if err := ValidateCheck(id, metadata, check.Action, check.Url); err != nil { 93 | logger.Errorf(err, "error in register check Id: %s", id) 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func AppendCheck(checks *[]*checkmodels.Check, action checkmodels.Check) error { 101 | err := ValidateChecks(action) 102 | if err == nil { 103 | *checks = append(*checks, &action) 104 | } 105 | 106 | return err 107 | } 108 | -------------------------------------------------------------------------------- /internal/checks/common/opa/rego.go: -------------------------------------------------------------------------------- 1 | package opa 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | _ "embed" 8 | 9 | "github.com/aquasecurity/chain-bench/internal/consts" 10 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 11 | "github.com/mitchellh/mapstructure" 12 | "github.com/open-policy-agent/opa/rego" 13 | ) 14 | 15 | var ( 16 | errorNoResultStatus = errors.New("Missing check result") 17 | ) 18 | 19 | type RegoResult struct { 20 | Status checkmodels.ResultStatus `json:"status,omitempty"` 21 | IDs []string `json:"ids,omitempty"` 22 | Details string `json:"details,omitempty"` 23 | } 24 | 25 | func RunRego(input interface{}, regoModules []checkmodels.RegoCustomModule, checksMetadata *checkmodels.CheckMetadataMap) ([]*checkmodels.CheckRunResult, error) { 26 | regoInitOptions := []func(r *rego.Rego){ 27 | rego.Input(input), 28 | rego.Query("data.main.CbPolicy"), 29 | } 30 | 31 | for _, m := range regoModules { 32 | regoInitOptions = append(regoInitOptions, rego.Module(m.Name, m.Content)) 33 | } 34 | 35 | rego := rego.New( 36 | regoInitOptions..., 37 | ) 38 | result, err := rego.Eval(context.TODO()) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | if len(result) == 0 { 44 | return nil, nil 45 | } 46 | 47 | results := make([]*RegoResult, 0) 48 | for _, rule := range result[0].Expressions[0].Value.([]interface{}) { 49 | parsedRule, err := parseRegoRule(rule) 50 | if err != nil { 51 | return nil, err 52 | } 53 | results = append(results, parsedRule) 54 | } 55 | 56 | return parseRegoResult(results, checksMetadata), nil 57 | } 58 | 59 | func parseRegoRule(rule interface{}) (*RegoResult, error) { 60 | result := RegoResult{} 61 | if err := mapstructure.Decode(rule, &result); err != nil { 62 | return nil, err 63 | } 64 | 65 | if result.Status == "" { 66 | return nil, errorNoResultStatus 67 | } 68 | if result.IDs == nil { 69 | return nil, consts.ErrorNoCheckID 70 | } 71 | 72 | return &result, nil 73 | } 74 | 75 | func parseRegoResult(findings []*RegoResult, checksMetadata *checkmodels.CheckMetadataMap) []*checkmodels.CheckRunResult { 76 | mappedResults := parseRegoResultToMap(findings) 77 | 78 | results := make([]*checkmodels.CheckRunResult, 0) 79 | for id, cm := range checksMetadata.Checks { 80 | if cm.ScannerType == checkmodels.Rego { 81 | results = append(results, checkmodels.ToCheckRunResult(id, cm, checksMetadata.Url, getRunResult(id, mappedResults))) 82 | } 83 | } 84 | 85 | return results 86 | } 87 | 88 | func parseRegoResultToMap(findings []*RegoResult) checkmodels.CheckIdToCheckResultMap { 89 | resultMap := make(checkmodels.CheckIdToCheckResultMap) 90 | 91 | for _, f := range findings { 92 | for _, id := range f.IDs { 93 | resultMap[id] = checkmodels.CheckResult{ 94 | Status: f.Status, 95 | Details: f.Details, 96 | } 97 | } 98 | } 99 | return resultMap 100 | } 101 | 102 | func getRunResult(id string, resultsMap checkmodels.CheckIdToCheckResultMap) *checkmodels.CheckResult { 103 | if val, ok := resultsMap[id]; ok { 104 | return &checkmodels.CheckResult{Status: val.Status, Details: val.Details} 105 | } else { 106 | return &checkmodels.CheckResult{Status: checkmodels.Passed} 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/checks/common/opa/rego_test.go: -------------------------------------------------------------------------------- 1 | package opa 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/consts" 7 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type RegoResultTest []struct { 12 | Name string 13 | Findings []*RegoResult 14 | Metadata checkmodels.CheckMetadataMap 15 | Expected []*checkmodels.CheckRunResult 16 | } 17 | 18 | type RegoRuleResultTest []struct { 19 | Name string 20 | Findings *RegoResult 21 | Expected *RegoResult 22 | ExpectedE error 23 | } 24 | 25 | func TestParseRegoResult(t *testing.T) { 26 | metadata := checkmodels.CheckMetadata{ 27 | Title: "Ensure any change to code receives approval of two strongly authenticated users", 28 | Type: "SCM", 29 | Entity: "Organization", 30 | Description: "SOME DESCRIPTION", 31 | ScannerType: checkmodels.Rego, 32 | Url: "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-sourcecode/1.1/#1113-ensure-any-change-to-code-receives-approval-of-two-strongly-authenticated-users", 33 | } 34 | metadataMap := checkmodels.CheckMetadataMap{ 35 | Checks: map[string]checkmodels.CheckMetadata{"1.1.13": metadata}, 36 | Url: "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-sourcecode/1.1", 37 | } 38 | res := []*RegoResult{{IDs: []string{"1.1.13"}, Status: checkmodels.Passed, Details: "Details"}} 39 | 40 | tests := RegoResultTest{ 41 | { 42 | Name: "Some Name", 43 | Findings: res, 44 | Metadata: metadataMap, 45 | Expected: []*checkmodels.CheckRunResult{ 46 | { 47 | ID: "1.1.13", 48 | Metadata: metadata, 49 | Result: &checkmodels.CheckResult{ 50 | Status: checkmodels.Passed, 51 | Details: "Details"}, 52 | }, 53 | }, 54 | }, 55 | } 56 | 57 | for _, test := range tests { 58 | test := test 59 | t.Run(test.Name, func(t *testing.T) { 60 | t.Parallel() 61 | 62 | actual := parseRegoResult(test.Findings, &test.Metadata) 63 | assert.Equal(t, test.Expected, actual) 64 | }) 65 | } 66 | } 67 | 68 | func TestParseRegoRule(t *testing.T) { 69 | tests := RegoRuleResultTest{ 70 | { 71 | Name: "rego result with all fields", 72 | Findings: &RegoResult{IDs: []string{"1.1.9"}, Status: checkmodels.Passed, Details: "Details"}, 73 | Expected: &RegoResult{IDs: []string{"1.1.9"}, Status: checkmodels.Passed, Details: "Details"}, 74 | ExpectedE: nil, 75 | }, 76 | { 77 | Name: "rego result missing status", 78 | Findings: &RegoResult{IDs: []string{"1.1.9"}, Details: "Details"}, 79 | ExpectedE: errorNoResultStatus, 80 | }, 81 | { 82 | Name: "rego result missing ids", 83 | Findings: &RegoResult{Status: checkmodels.Passed, Details: "Details"}, 84 | ExpectedE: consts.ErrorNoCheckID, 85 | }, 86 | } 87 | 88 | for _, test := range tests { 89 | test := test 90 | t.Run(test.Name, func(t *testing.T) { 91 | t.Parallel() 92 | actual, actualE := parseRegoRule(test.Findings) 93 | 94 | if test.ExpectedE == nil { 95 | assert.NoError(t, actualE) 96 | } else { 97 | assert.EqualError(t, actualE, test.ExpectedE.Error()) 98 | } 99 | 100 | if test.Expected == nil { 101 | assert.Nil(t, actual) 102 | } else { 103 | assert.Equal(t, test.Expected, actual) 104 | } 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /internal/checks/consts/details.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | var ( 4 | Details_linearHistory_mergeCommitEnabled = "MergeCommit is enabled for repository" 5 | Details_linearHistory_requireRebaseOrSquashCommitEnabled = "Repository is not configured to allow rebase or squash merge" 6 | 7 | Details_organization_notFetched = "Organization is not fetched" 8 | Details_organization_premissiveDefaultRepositoryPermissions = "Organization default permissions are too permissive" 9 | Details_organization_missingMinimalPermissions = "Organization is missing minimal permissions" 10 | Details_hooks_missingMinimalPermissions = "Organization & Repository Hooks is missing minimal permissions" 11 | 12 | Details_organization_hooks_missingMinimalPermissions = "Organization Packages is missing minimal permissions" 13 | 14 | Details_repository_missing_minimal_permissions = "Repository is missing minimal permissions" 15 | Details_repository_missing_minimal_permissions_for_protections = "Repository is missing admin permissions for branch protection settings" 16 | 17 | Details_pipeline_pipelinesNotScannedForVulnerabilities = "Pipelines are not scanned for vulnerabilities" 18 | Details_dependencies_pipelinesNotScannedForVulnerabilities = "Pipeline dependencies are not scanned for vulnerabilities" 19 | Details_dependencies_pipelinesNotScannedForLicenses = "Pipeline dependencies are not scanned for licenses" 20 | Details_pipeline_repositoryNotScannedForSecrets = "Repository is not scanned for secrets" 21 | Details_pipeline_noPipelinesFound = "No pipelines were found" 22 | Details_pipeline_noBuildJob = "No build job was found in pipelines" 23 | Details_registry_data_is_missing = "Registry is not fetched" 24 | Details_pipeline_are_missing = "Pipelines are not fetched" 25 | Details_repository_is_missing = "Repository is not fetched" 26 | ) 27 | -------------------------------------------------------------------------------- /internal/checks/consts/errors.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // checks execution errors 9 | ErrorNoRepository = errors.New("Repository cannot be nil") 10 | ErrorNoOrganization = errors.New("Organization cannot be nil") 11 | ErrorNoBranchProtection = errors.New("Branch Protection cannot be nil") 12 | ) 13 | -------------------------------------------------------------------------------- /internal/checks/dependencies/dependencies.go: -------------------------------------------------------------------------------- 1 | package dependencies 2 | 3 | import ( 4 | thirdpartypackages "github.com/aquasecurity/chain-bench/internal/checks/dependencies/third-party-packages" 5 | validatepackages "github.com/aquasecurity/chain-bench/internal/checks/dependencies/validate_packages" 6 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 7 | ) 8 | 9 | func GetChecks() []*checkmodels.Check { 10 | checks := []*checkmodels.Check{} 11 | checks = append(checks, thirdpartypackages.GetChecks()...) 12 | checks = append(checks, validatepackages.GetChecks()...) 13 | return checks 14 | } 15 | -------------------------------------------------------------------------------- /internal/checks/dependencies/sections.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "text": "Dependencies", 4 | "type": "dependencies" 5 | } -------------------------------------------------------------------------------- /internal/checks/dependencies/third-party-packages/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3.1", 3 | "name": "Third-Party Packages", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-dependencies/3.1", 5 | "checks": { 6 | "3.1.7": { 7 | "title": "Ensure dependencies are pinned to a specific, verified version", 8 | "severity": "Critical", 9 | "type": "DEPENDENCIES", 10 | "entity": "Dependencies", 11 | "description": "Pin dependencies to a specific version. Avoid using the \"latest\" tag or broad version.", 12 | "remediation": "For every dependency in use, pin to a specific version.", 13 | "scannerType": "Rego", 14 | "slsa_level": [4] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /internal/checks/dependencies/third-party-packages/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.generic.utils as utilsLib 5 | import future.keywords.in 6 | 7 | is_task_version_pinned(step) { 8 | step.type == "task" 9 | step.task.version_type != "commit" 10 | } 11 | 12 | are_there_unpinned_deps[unpinnedDepsCount] { 13 | unpinnedDepsCount := count({step | 14 | step = input.Pipelines[_].jobs[_].steps[_] 15 | is_task_version_pinned(step) 16 | }) 17 | 18 | unpinnedDepsCount > 0 19 | } 20 | 21 | CbPolicy[msg] { 22 | utilsLib.is_pipelines_data_missing 23 | msg = {"ids": ["3.1.7"], "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_data_is_missing} 24 | } 25 | 26 | # In case there are no pipelines 27 | CbPolicy[msg] { 28 | not utilsLib.is_pipelines_data_missing 29 | utilsLib.is_pipelines_list_empty 30 | msg = {"ids": ["3.1.7"], "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_no_pipelines_found} 31 | } 32 | 33 | # Looking for tasks that are not pinned 34 | CbPolicy[msg] { 35 | not utilsLib.is_pipelines_data_missing 36 | unpinnedDepsCount := are_there_unpinned_deps[i] 37 | details := sprintf("%v dependencies are not pinned", [unpinnedDepsCount]) 38 | msg := {"ids": ["3.1.7"], "status": constsLib.status.Failed, "details": details} 39 | } 40 | -------------------------------------------------------------------------------- /internal/checks/dependencies/third-party-packages/third_party_packages.go: -------------------------------------------------------------------------------- 1 | package thirdpartypackages 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/dependencies/third-party-packages/third_party_packages_test.go: -------------------------------------------------------------------------------- 1 | package thirdpartypackages 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/checks/common" 7 | "github.com/aquasecurity/chain-bench/internal/checks/consts" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | "github.com/aquasecurity/chain-bench/internal/testutils" 10 | "github.com/aquasecurity/chain-bench/internal/testutils/builders" 11 | ) 12 | 13 | func TestBuildChecker(t *testing.T) { 14 | tests := []testutils.CheckTest{ 15 | { 16 | Name: "Should return unknown when failed to fetch pipelines", 17 | Data: &checkmodels.CheckData{ 18 | AssetsMetadata: builders.NewAssetsDataBuilder().WithNoPipelinesData().Build(), 19 | }, 20 | Expected: []*checkmodels.CheckRunResult{ 21 | checkmodels.ToCheckRunResult("3.1.7", checksMetadata.Checks["3.1.7"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_are_missing}), 22 | }, 23 | }, 24 | { 25 | Name: "Should return unknown with explanation when there are no pipelines", 26 | Data: &checkmodels.CheckData{ 27 | AssetsMetadata: builders.NewAssetsDataBuilder(). 28 | WithZeroPipelines(). 29 | Build(), 30 | }, 31 | Expected: []*checkmodels.CheckRunResult{ 32 | checkmodels.ToCheckRunResult("3.1.7", checksMetadata.Checks["3.1.7"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_noPipelinesFound}), 33 | }, 34 | }, 35 | { 36 | Name: "valid input - all rules should pass", 37 | Data: &checkmodels.CheckData{ 38 | AssetsMetadata: builders.NewAssetsDataBuilder().Build(), 39 | }, 40 | Expected: []*checkmodels.CheckRunResult{ 41 | checkmodels.ToCheckRunResult("3.1.7", checksMetadata.Checks["3.1.7"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Passed}), 42 | }, 43 | }, 44 | { 45 | Name: "Should fail and return number of dependencies when there is pipeline with unpinned job", 46 | Data: &checkmodels.CheckData{ 47 | AssetsMetadata: builders.NewAssetsDataBuilder().WithZeroPipelines().WithPipeline( 48 | builders.NewPipelineBuilder().WithNoJobs().WithJob(builders. 49 | NewJobBuilder(). 50 | WithTask("NORMAL_TASK_NAME", "tag"). 51 | Build()). 52 | Build(), 53 | ).Build(), 54 | }, 55 | Expected: []*checkmodels.CheckRunResult{ 56 | checkmodels.ToCheckRunResult("3.1.7", checksMetadata.Checks["3.1.7"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: "1 dependencies are not pinned"}), 57 | }, 58 | }, 59 | } 60 | testutils.RunCheckTests(t, common.GetRegoRunAction(regoQuery, checksMetadata), tests, checksMetadata) 61 | } 62 | -------------------------------------------------------------------------------- /internal/checks/dependencies/validate_packages/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3.2", 3 | "name": "Validate Packages", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-dependencies/3.2", 5 | "checks": { 6 | "3.2.2": { 7 | "title": "Ensure packages are automatically scanned for known vulnerabilities", 8 | "severity": "Critical", 9 | "type": "DEPENDENCIES", 10 | "entity": "Dependencies", 11 | "description": "Automatically scan every package for vulnerabilities.", 12 | "remediation": "Set automatic scanning of packages for vulnerabilities.", 13 | "scannerType": "Rego", 14 | "slsa_level": [4] 15 | }, 16 | "3.2.3": { 17 | "title": "Ensure packages are automatically scanned for license implications", 18 | "severity": "High", 19 | "type": "DEPENDENCIES", 20 | "entity": "Dependencies", 21 | "description": "A software license is a document that provides legal conditions and guidelines for the use and distribution of software, usually defined by the author. It is recommended to scan for any legal implications automatically.", 22 | "remediation": "Set automatic package scanning for license implications.", 23 | "scannerType": "Rego", 24 | "slsa_level": [] 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /internal/checks/dependencies/validate_packages/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.generic.utils as utilsLib 5 | import future.keywords.in 6 | 7 | does_job_contain_one_of_tasks(job, regexes) { 8 | job.steps[i].type == "task" 9 | regex.match(regexes[_], job.steps[i].task.name) 10 | } 11 | 12 | are_pipelines_dependencies_scanned_for_vulnerabilities { 13 | count({job | job := input.Pipelines[_].jobs[_]; does_job_contain_one_of_tasks(job, constsLib.pipeline_vulnerability_scan_tasks)}) == 0 14 | } 15 | 16 | are_pipelines_dependencies_scanned_for_licenses { 17 | count({job | job := input.Pipelines[_].jobs[_]; does_job_contain_one_of_tasks(job, constsLib.pipeline_vulnerability_scan_tasks)}) == 0 18 | } 19 | 20 | CbPolicy[msg] { 21 | utilsLib.is_pipelines_data_missing 22 | msg = {"ids": ["3.2.2", "3.2.3"], "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_data_is_missing} 23 | } 24 | 25 | # In case there are no pipelines 26 | CbPolicy[msg] { 27 | not utilsLib.is_pipelines_data_missing 28 | utilsLib.is_pipelines_list_empty 29 | msg = {"ids": ["3.2.2", "3.2.3"], "status": constsLib.status.Unknown, "details": constsLib.details.pipeline_no_pipelines_found} 30 | } 31 | 32 | # Looking for a pipeline that scans for vulnerabilities 33 | CbPolicy[msg] { 34 | not utilsLib.is_pipelines_data_missing 35 | not utilsLib.is_pipelines_list_empty 36 | are_pipelines_dependencies_scanned_for_vulnerabilities 37 | msg = {"ids": ["3.2.2"], "status": constsLib.status.Failed, "details": constsLib.details.dependencies_pipelines_not_scanned_for_vulnerabilities} 38 | } 39 | 40 | # Looking for a pipeline that scans for licenses 41 | CbPolicy[msg] { 42 | not utilsLib.is_pipelines_data_missing 43 | not utilsLib.is_pipelines_list_empty 44 | are_pipelines_dependencies_scanned_for_licenses 45 | msg = {"ids": ["3.2.3"], "status": constsLib.status.Failed, "details": constsLib.details.dependencies_pipelines_not_scanned_for_licenses} 46 | } 47 | -------------------------------------------------------------------------------- /internal/checks/dependencies/validate_packages/validate_packages.go: -------------------------------------------------------------------------------- 1 | package validatepackages 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/dependencies/validate_packages/validate_packages_test.go: -------------------------------------------------------------------------------- 1 | package validatepackages 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/checks/common" 7 | "github.com/aquasecurity/chain-bench/internal/checks/consts" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | "github.com/aquasecurity/chain-bench/internal/testutils" 10 | "github.com/aquasecurity/chain-bench/internal/testutils/builders" 11 | ) 12 | 13 | func TestBuildChecker(t *testing.T) { 14 | tests := []testutils.CheckTest{ 15 | { 16 | Name: "Should return unknown when failed to fetch pipelines", 17 | Data: &checkmodels.CheckData{ 18 | AssetsMetadata: builders.NewAssetsDataBuilder().WithNoPipelinesData().Build(), 19 | }, 20 | Expected: []*checkmodels.CheckRunResult{ 21 | checkmodels.ToCheckRunResult("3.2.2", checksMetadata.Checks["3.2.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_are_missing}), 22 | checkmodels.ToCheckRunResult("3.2.3", checksMetadata.Checks["3.2.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_are_missing}), 23 | }, 24 | }, 25 | { 26 | Name: "Should return unknown with explanation when there are no pipelines", 27 | Data: &checkmodels.CheckData{ 28 | AssetsMetadata: builders.NewAssetsDataBuilder(). 29 | WithZeroPipelines(). 30 | Build(), 31 | }, 32 | Expected: []*checkmodels.CheckRunResult{ 33 | checkmodels.ToCheckRunResult("3.2.2", checksMetadata.Checks["3.2.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_noPipelinesFound}), 34 | checkmodels.ToCheckRunResult("3.2.3", checksMetadata.Checks["3.2.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_pipeline_noPipelinesFound}), 35 | }, 36 | }, 37 | { 38 | Name: "Should fail with explanation when there is pipeline with one job without vulnerability scanner task", 39 | Data: &checkmodels.CheckData{ 40 | AssetsMetadata: builders.NewAssetsDataBuilder().WithZeroPipelines().WithPipeline( 41 | builders.NewPipelineBuilder().WithNoJobs(). 42 | WithJob(builders. 43 | NewJobBuilder().WithNoTasks(). 44 | WithTask("NORMAL_TASK_NAME", "commit").WithNoVulnerabilityScannerTask(). 45 | Build()). 46 | Build(), 47 | ).Build(), 48 | }, 49 | Expected: []*checkmodels.CheckRunResult{ 50 | checkmodels.ToCheckRunResult("3.2.2", checksMetadata.Checks["3.2.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: consts.Details_dependencies_pipelinesNotScannedForVulnerabilities}), 51 | checkmodels.ToCheckRunResult("3.2.3", checksMetadata.Checks["3.2.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: consts.Details_dependencies_pipelinesNotScannedForLicenses}), 52 | }, 53 | }, 54 | { 55 | Name: "valid input - Job with a pipeline with a vulnerability scanner task - all rules should pass", 56 | Data: &checkmodels.CheckData{ 57 | AssetsMetadata: builders.NewAssetsDataBuilder().Build(), 58 | }, 59 | Expected: []*checkmodels.CheckRunResult{ 60 | checkmodels.ToCheckRunResult("3.2.2", checksMetadata.Checks["3.2.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Passed}), 61 | checkmodels.ToCheckRunResult("3.2.3", checksMetadata.Checks["3.2.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Passed}), 62 | }, 63 | }, 64 | } 65 | testutils.RunCheckTests(t, common.GetRegoRunAction(regoQuery, checksMetadata), tests, checksMetadata) 66 | } 67 | -------------------------------------------------------------------------------- /internal/checks/source-code/code-changes/code_changes.go: -------------------------------------------------------------------------------- 1 | package codechanges 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/source-code/contribution-access/contribution_access.go: -------------------------------------------------------------------------------- 1 | package contributionaccess 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/source-code/contribution-access/contribution_access_test.go: -------------------------------------------------------------------------------- 1 | package contributionaccess 2 | 3 | import ( 4 | _ "embed" 5 | "testing" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | "github.com/aquasecurity/chain-bench/internal/testutils" 10 | "github.com/aquasecurity/chain-bench/internal/testutils/builders" 11 | ) 12 | 13 | func TestOrganizationChecker(t *testing.T) { 14 | tests := []testutils.CheckTest{ 15 | { 16 | Name: "Should return unknown for organization without org settings permissions, without admin permissions, and with no commits", 17 | Data: &checkmodels.CheckData{ 18 | AssetsMetadata: builders.NewAssetsDataBuilder().WithOrganization(builders.NewOrganizationBuilder().WithNoMembers().WithReposDefaultPermissions("").Build()).WithRepository(builders.NewRepositoryBuilder().WithNoCommits().Build()).Build(), 19 | }, 20 | Expected: []*checkmodels.CheckRunResult{ 21 | checkmodels.ToCheckRunResult("1.3.1", checksMetadata.Checks["1.3.1"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown}), 22 | checkmodels.ToCheckRunResult("1.3.3", checksMetadata.Checks["1.3.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown}), 23 | checkmodels.ToCheckRunResult("1.3.5", checksMetadata.Checks["1.3.5"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown}), 24 | checkmodels.ToCheckRunResult("1.3.7", checksMetadata.Checks["1.3.7"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown}), 25 | checkmodels.ToCheckRunResult("1.3.8", checksMetadata.Checks["1.3.8"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown}), 26 | }, 27 | }, 28 | { 29 | Name: "Should fail for organization with unverified status", 30 | Data: &checkmodels.CheckData{ 31 | AssetsMetadata: builders.NewAssetsDataBuilder().WithOrganization(builders.NewOrganizationBuilder().WithVerifiedBadge(false).Build()).Build(), 32 | }, 33 | Expected: []*checkmodels.CheckRunResult{ 34 | checkmodels.ToCheckRunResult("1.3.9", checksMetadata.Checks["1.3.9"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 35 | }, 36 | }, 37 | { 38 | Name: "Should fail for organization without strict default permission", 39 | Data: &checkmodels.CheckData{ 40 | AssetsMetadata: builders.NewAssetsDataBuilder().WithOrganization(builders.NewOrganizationBuilder(). 41 | WithReposDefaultPermissions("write").Build()).Build(), 42 | }, 43 | Expected: []*checkmodels.CheckRunResult{ 44 | checkmodels.ToCheckRunResult("1.3.8", checksMetadata.Checks["1.3.8"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 45 | }, 46 | }, 47 | { 48 | Name: "Should fail for organization with 2mfa disabled", 49 | Data: &checkmodels.CheckData{ 50 | AssetsMetadata: builders.NewAssetsDataBuilder().WithOrganization(builders.NewOrganizationBuilder(). 51 | WithMFAEnabled(false).Build()).Build(), 52 | }, 53 | Expected: []*checkmodels.CheckRunResult{ 54 | checkmodels.ToCheckRunResult("1.3.5", checksMetadata.Checks["1.3.5"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 55 | }, 56 | }, 57 | { 58 | Name: "Should fail for organization with less then 2 admins", 59 | Data: &checkmodels.CheckData{ 60 | AssetsMetadata: builders.NewAssetsDataBuilder(). 61 | WithOrganization(builders.NewOrganizationBuilder().WithMembers("admin", 1).Build()).Build(), 62 | }, 63 | Expected: []*checkmodels.CheckRunResult{ 64 | checkmodels.ToCheckRunResult("1.3.3", checksMetadata.Checks["1.3.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 65 | }, 66 | }, 67 | { 68 | Name: "Should fail for repository with no 2 admins", 69 | Data: &checkmodels.CheckData{ 70 | AssetsMetadata: builders.NewAssetsDataBuilder(). 71 | WithRepository(builders.NewRepositoryBuilder().WithAdminCollborator(true, 1).Build()).Build(), 72 | }, 73 | Expected: []*checkmodels.CheckRunResult{ 74 | checkmodels.ToCheckRunResult("1.3.7", checksMetadata.Checks["1.3.7"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 75 | }, 76 | }, 77 | { 78 | Name: "Should fail for repository with inactive user", 79 | Data: &checkmodels.CheckData{ 80 | AssetsMetadata: builders.NewAssetsDataBuilder(). 81 | WithOrganization(builders.NewOrganizationBuilder().WithMembers("admin", 5).Build()).Build(), 82 | }, 83 | Expected: []*checkmodels.CheckRunResult{ 84 | checkmodels.ToCheckRunResult("1.3.1", checksMetadata.Checks["1.3.1"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed, Details: "1 inactive users"}), 85 | }, 86 | }, 87 | { 88 | Name: "valid input - all rules should pass", 89 | Data: &checkmodels.CheckData{ 90 | AssetsMetadata: builders.NewAssetsDataBuilder().Build(), 91 | }, 92 | Expected: []*checkmodels.CheckRunResult{}, 93 | }, 94 | } 95 | testutils.RunCheckTests(t, common.GetRegoRunAction(regoQuery, checksMetadata), tests, checksMetadata) 96 | } 97 | -------------------------------------------------------------------------------- /internal/checks/source-code/contribution-access/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1.3", 3 | "name": "Contribution Access", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-sourcecode/1.3", 5 | "checks": { 6 | "1.3.1": { 7 | "title": "Ensure inactive users are reviewed and removed periodically", 8 | "severity": "High", 9 | "type": "SCM", 10 | "entity": "Repository", 11 | "description": "Track inactive user accounts and periodically remove them.", 12 | "remediation": "For each repository in use, review inactive user accounts (members that left the organization, etc.) and remove them.", 13 | "scannerType": "Rego", 14 | "slsa_level": [ 15 | 4 16 | ] 17 | }, 18 | "1.3.3": { 19 | "title": "Ensure minimum admins are set for the organization", 20 | "severity": "High", 21 | "type": "SCM", 22 | "entity": "Organization", 23 | "description": "Ensure the organization has a minimum number of admins.", 24 | "remediation": "Set the minimum number of administrators in your organization.", 25 | "scannerType": "Rego", 26 | "slsa_level": [ 27 | 4 28 | ] 29 | }, 30 | "1.3.5": { 31 | "title": "Ensure the organization is requiring members to use MFA", 32 | "severity": "Critical", 33 | "type": "SCM", 34 | "entity": "Organization", 35 | "description": "Require members of the organization to use Multi-Factor Authentication, in addition to using a standard user name and password, when authenticating to the source code management platform.", 36 | "remediation": "Use the built-in setting to set the enforcement of Multi-Factor Authentication for each member of the organization.", 37 | "scannerType": "Rego", 38 | "slsa_level": [ 39 | 3, 40 | 4 41 | ] 42 | }, 43 | "1.3.7": { 44 | "title": "Ensure 2 admins are set for each repository", 45 | "severity": "High", 46 | "type": "SCM", 47 | "entity": "Repository", 48 | "description": "Ensure every repository has 2 users with admin permissions to it.", 49 | "remediation": "For every repository in use, set two administrators.", 50 | "scannerType": "Rego", 51 | "slsa_level": [ 52 | 4 53 | ] 54 | }, 55 | "1.3.8": { 56 | "title": "Ensure strict base permissions are set for repositories", 57 | "severity": "High", 58 | "type": "SCM", 59 | "entity": "Organization", 60 | "description": "Base permissions define the permission level granted to all the organization members automatically. Define strict base access permissions for all of the repositories in the organization, which should apply to new ones as well.", 61 | "remediation": "Set strict base permissions for the organization repositories — either \"None\" or \"Read.\"", 62 | "scannerType": "Rego", 63 | "slsa_level": [ 64 | 4 65 | ] 66 | }, 67 | "1.3.9": { 68 | "title": "Ensure an organization's identity is confirmed with a Verified badge", 69 | "severity": "High", 70 | "type": "SCM", 71 | "entity": "Organization", 72 | "description": "Verify the domains that the organization owns", 73 | "remediation": "Verify the organization's domains and secure a \"Verified\" badge next to its name.", 74 | "scannerType": "Rego", 75 | "slsa_level": [] 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /internal/checks/source-code/contribution-access/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.common.permissions as permissionslib 5 | import data.generic.utils as utilsLib 6 | import future.keywords.in 7 | 8 | is_2mfa_enforcement_disabled { 9 | input.Organization.TwoFactorRequirementEnabled == false 10 | } 11 | 12 | is_not_verified { 13 | input.Organization.IsVerified == false 14 | } 15 | 16 | is_less_then_two_admins { 17 | input.Organization.Members != null 18 | adminCount := count({i | 19 | input.Organization.Members[i].Role == "admin" 20 | }) 21 | 22 | adminCount < 2 23 | } 24 | 25 | is_repository_dont_have_2_admins { 26 | filtered := [c | c := input.Repository.Collaborators[_]; c.permissions.admin == true] 27 | count(filtered) != 2 28 | } 29 | 30 | is_repo_has_no_commits { 31 | not input.Repository.Commits 32 | } 33 | 34 | is_repo_has_no_commits { 35 | input.Repository.Commits == null 36 | } 37 | 38 | is_organization_admin { 39 | input.Organization.Members != null 40 | count(input.Organization.Members) != 0 41 | } 42 | 43 | is_repository_has_inactive_users[inactiveCount] { 44 | filtered := [m | 45 | m := input.Organization.Members[_] 46 | count({i | input.Repository.Commits[i].Author.username == m.login}) == 0 47 | ] 48 | 49 | inactiveCount := count(filtered) 50 | inactiveCount > 0 51 | } 52 | 53 | CbPolicy[msg] { 54 | utilsLib.is_repository_data_missing 55 | msg := {"ids": ["1.3.1", "1.3.7"], "status": constsLib.status.Unknown, "details": constsLib.details.repository_data_is_missing} 56 | } 57 | 58 | #Looking for organization missing data 59 | CbPolicy[msg] { 60 | utilsLib.is_organization_data_missing 61 | msg := {"ids": ["1.3.3", "1.3.5", "1.3.7", "1.3.8", "1.3.9"], "status": constsLib.status.Unknown, "details": constsLib.details.organization_not_fetched} 62 | } 63 | 64 | #Looking for organization missing permissions 65 | CbPolicy[msg] { 66 | not utilsLib.is_organization_data_missing 67 | permissionslib.is_missing_org_settings_permission 68 | msg := {"ids": ["1.3.3", "1.3.5", "1.3.7", "1.3.8"], "status": constsLib.status.Unknown} 69 | } 70 | 71 | CbPolicy[msg] { 72 | not utilsLib.is_organization_data_missing 73 | not permissionslib.is_missing_org_settings_permission 74 | not is_organization_admin 75 | msg := {"ids": ["1.3.3"], "status": constsLib.status.Unknown} 76 | } 77 | 78 | CbPolicy[msg] { 79 | not utilsLib.is_repository_data_missing 80 | is_repo_has_no_commits 81 | msg := {"ids": ["1.3.1"], "status": constsLib.status.Unknown} 82 | } 83 | 84 | # Check if organization inactive users 85 | CbPolicy[msg] { 86 | not is_repo_has_no_commits 87 | inactiveCount := is_repository_has_inactive_users[i] 88 | details := sprintf("%v %v", [format_int(inactiveCount, 10), "inactive users"]) 89 | msg := {"ids": ["1.3.1"], "status": constsLib.status.Failed, "details": details} 90 | } 91 | 92 | # Check if organization has min 2 admins 93 | CbPolicy[msg] { 94 | is_less_then_two_admins 95 | msg := {"ids": ["1.3.3"], "status": constsLib.status.Failed} 96 | } 97 | 98 | #Looking for organization 2mfa enforcements that is disabled 99 | CbPolicy[msg] { 100 | not utilsLib.is_organization_data_missing 101 | not permissionslib.is_missing_org_settings_permission 102 | is_2mfa_enforcement_disabled 103 | msg := {"ids": ["1.3.5"], "status": constsLib.status.Failed} 104 | } 105 | 106 | #Looking for repository with no 2 admins 107 | CbPolicy[msg] { 108 | not utilsLib.is_repository_data_missing 109 | not permissionslib.is_missing_org_settings_permission 110 | is_repository_dont_have_2_admins 111 | msg := {"ids": ["1.3.7"], "status": constsLib.status.Failed} 112 | } 113 | 114 | #Looking for organization with non strict base permission 115 | CbPolicy[msg] { 116 | not utilsLib.is_organization_data_missing 117 | not permissionslib.is_missing_org_settings_permission 118 | not permissionslib.is_org_default_permission_strict 119 | msg := {"ids": ["1.3.8"], "status": constsLib.status.Failed} 120 | } 121 | 122 | #Looking for organization that is not verified 123 | CbPolicy[msg] { 124 | is_not_verified 125 | msg := {"ids": ["1.3.9"], "status": constsLib.status.Failed} 126 | } 127 | -------------------------------------------------------------------------------- /internal/checks/source-code/repository-management/repository_management.go: -------------------------------------------------------------------------------- 1 | package repositorymanagement 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/checks/common" 8 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 9 | ) 10 | 11 | var ( 12 | //go:embed rules.metadata.json 13 | metadataString []byte 14 | 15 | //go:embed rules.rego 16 | regoQuery string 17 | 18 | checksMetadata checkmodels.CheckMetadataMap 19 | 20 | checks = []*checkmodels.Check{} 21 | ) 22 | 23 | func init() { 24 | if err := json.Unmarshal(metadataString, &checksMetadata); err != nil { 25 | panic(err) 26 | } 27 | 28 | if err := common.AppendCheck(&checks, 29 | checkmodels.Check{ 30 | Action: common.GetRegoRunAction(regoQuery, checksMetadata), 31 | CheckMetadataMap: checksMetadata, 32 | }, 33 | ); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func GetChecks() []*checkmodels.Check { 39 | return checks 40 | } 41 | -------------------------------------------------------------------------------- /internal/checks/source-code/repository-management/repository_management_test.go: -------------------------------------------------------------------------------- 1 | package repositorymanagement 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/aquasecurity/chain-bench/internal/checks/common" 9 | "github.com/aquasecurity/chain-bench/internal/checks/consts" 10 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 11 | "github.com/aquasecurity/chain-bench/internal/testutils" 12 | "github.com/aquasecurity/chain-bench/internal/testutils/builders" 13 | ) 14 | 15 | func TestRepositoryChecker(t *testing.T) { 16 | 17 | var checksMetadata checkmodels.CheckMetadataMap 18 | json.Unmarshal(metadataString, &checksMetadata) 19 | 20 | tests := []testutils.CheckTest{ 21 | { 22 | Name: "Should fail for public repository without security.md file", 23 | Data: &checkmodels.CheckData{ 24 | 25 | AssetsMetadata: builders.NewAssetsDataBuilder(). 26 | WithRepository(builders.NewRepositoryBuilder().WithPrivate(false).WithSecurityMdFile(false).Build()).Build(), 27 | }, 28 | Expected: []*checkmodels.CheckRunResult{ 29 | checkmodels.ToCheckRunResult("1.2.1", checksMetadata.Checks["1.2.1"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 30 | }, 31 | }, 32 | { 33 | Name: "Should return unknown if repository not fetched", 34 | Data: &checkmodels.CheckData{ 35 | 36 | AssetsMetadata: builders.NewAssetsDataBuilder().WithNoRepositoryData().Build(), 37 | }, 38 | Expected: []*checkmodels.CheckRunResult{ 39 | checkmodels.ToCheckRunResult("1.2.1", checksMetadata.Checks["1.2.1"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_repository_is_missing}), 40 | }, 41 | }, 42 | { 43 | Name: "Should return unknown if organization not fetched", 44 | Data: &checkmodels.CheckData{ 45 | 46 | AssetsMetadata: builders.NewAssetsDataBuilder().WithNoOrganization().Build(), 47 | }, 48 | Expected: []*checkmodels.CheckRunResult{ 49 | checkmodels.ToCheckRunResult("1.2.2", checksMetadata.Checks["1.2.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_organization_notFetched}), 50 | checkmodels.ToCheckRunResult("1.2.3", checksMetadata.Checks["1.2.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_organization_notFetched}), 51 | checkmodels.ToCheckRunResult("1.2.4", checksMetadata.Checks["1.2.4"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Unknown, Details: consts.Details_organization_notFetched}), 52 | }, 53 | }, 54 | { 55 | Name: "Should fail for public repository without security.md file", 56 | Data: &checkmodels.CheckData{ 57 | 58 | AssetsMetadata: builders.NewAssetsDataBuilder(). 59 | WithRepository(builders.NewRepositoryBuilder().WithPrivate(false).WithSecurityMdFile(false).Build()).Build(), 60 | }, 61 | Expected: []*checkmodels.CheckRunResult{ 62 | checkmodels.ToCheckRunResult("1.2.1", checksMetadata.Checks["1.2.1"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 63 | }, 64 | }, 65 | { 66 | Name: "Should fail for repository without creation limited to trusted users", 67 | Data: &checkmodels.CheckData{ 68 | 69 | AssetsMetadata: builders.NewAssetsDataBuilder(). 70 | WithOrganization(builders.NewOrganizationBuilder().WithMembersCanCreateRepos(true).Build()).Build(), 71 | }, 72 | Expected: []*checkmodels.CheckRunResult{ 73 | checkmodels.ToCheckRunResult("1.2.2", checksMetadata.Checks["1.2.2"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 74 | }, 75 | }, 76 | { 77 | Name: "Should fail for repository without issue deletion limited to trusted users", 78 | Data: &checkmodels.CheckData{ 79 | 80 | AssetsMetadata: builders.NewAssetsDataBuilder(). 81 | WithOrganization(builders.NewOrganizationBuilder().WithIssuesDeletionLimitation(false).Build()).Build(), 82 | }, 83 | Expected: []*checkmodels.CheckRunResult{ 84 | checkmodels.ToCheckRunResult("1.2.4", checksMetadata.Checks["1.2.4"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 85 | }, 86 | }, 87 | { 88 | Name: "Should fail for organization with no limitations for repository deletion", 89 | Data: &checkmodels.CheckData{ 90 | 91 | AssetsMetadata: builders.NewAssetsDataBuilder(). 92 | WithOrganization(builders.NewOrganizationBuilder().WithReposDeletionLimitation(false).Build()).Build(), 93 | }, 94 | Expected: []*checkmodels.CheckRunResult{ 95 | checkmodels.ToCheckRunResult("1.2.3", checksMetadata.Checks["1.2.3"], checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Failed}), 96 | }, 97 | }, 98 | { 99 | Name: "Valid input -all rules should pass", 100 | Data: &checkmodels.CheckData{ 101 | AssetsMetadata: builders.NewAssetsDataBuilder().Build(), 102 | }, 103 | Expected: []*checkmodels.CheckRunResult{}, 104 | }, 105 | } 106 | testutils.RunCheckTests(t, common.GetRegoRunAction(regoQuery, checksMetadata), tests, checksMetadata) 107 | } 108 | -------------------------------------------------------------------------------- /internal/checks/source-code/repository-management/rules.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1.2", 3 | "name": "Repository Management", 4 | "url": "https://avd.aquasec.com/compliance/softwaresupplychain/cis-1.0/cis-1.0-sourcecode/1.2", 5 | "checks": { 6 | "1.2.1": { 7 | "title": "Ensure all public repositories contain a SECURITY.md file", 8 | "severity": "Low", 9 | "type": "SCM", 10 | "entity": "Repository", 11 | "description": "SECURITY.md file is a security policy file, which gives people instructions when they are reporting security vulnerabilities in a project. When someone creates an issue in that project, a link to the SECURITY.md file will be shown.", 12 | "remediation": "For each repository in use, create a SECURITY.md file and save it in the documents or root directory of the repository.", 13 | "scannerType": "Rego", 14 | "slsa_level": [] 15 | }, 16 | "1.2.2": { 17 | "title": "Ensure repository creation is limited to specific members", 18 | "severity": "Medium", 19 | "type": "SCM", 20 | "entity": "Organization", 21 | "description": "Limit the ability to create repositories to trusted users and teams.", 22 | "remediation": "Restrict repository creation to trusted users and teams only.", 23 | "scannerType": "Rego", 24 | "slsa_level": [] 25 | }, 26 | "1.2.3": { 27 | "title": "Ensure repository deletion is limited to specific members", 28 | "severity": "Medium", 29 | "type": "SCM", 30 | "entity": "Organization", 31 | "description": "Ensure only a limited number of trusted members can delete repositories.", 32 | "remediation": "Enforce repository deletion by a few trusted and responsible users only.", 33 | "scannerType": "Rego", 34 | "slsa_level": [3, 4] 35 | }, 36 | "1.2.4": { 37 | "title": "Ensure issue deletion is limited to specific members", 38 | "severity": "High", 39 | "type": "SCM", 40 | "entity": "Organization", 41 | "description": "Ensure only trusted an responsible members can delete issues.", 42 | "remediation": "Restrict issue deletion to a few trusted and responsible users only.", 43 | "scannerType": "Rego", 44 | "slsa_level": [3, 4] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/checks/source-code/repository-management/rules.rego: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import data.common.consts as constsLib 4 | import data.generic.utils as utilsLib 5 | import future.keywords.in 6 | 7 | #checks if the repository is public one 8 | is_public_repository { 9 | input.Repository.IsPrivate == false 10 | } 11 | 12 | # each one of public repositories should conatin security.md file 13 | is_missing_security_md_file { 14 | input.Repository.IsContainsSecurityMd == false 15 | } 16 | 17 | is_repository_deletion_not_limited_to_trusted_memebers { 18 | input.Organization.IsRepositoryDeletionLimited == false 19 | } 20 | 21 | is_repository_creation_not_limited_to_trusted_memebers { 22 | input.Organization.MembersCanCreateRepos == true 23 | } 24 | 25 | is_issue_deletion_not_limited_to_trusted_memebers { 26 | input.Organization.IsIssueDeletionLimited == false 27 | } 28 | 29 | CbPolicy[msg] { 30 | utilsLib.is_repository_data_missing 31 | msg := {"ids": ["1.2.1"], "status": constsLib.status.Unknown, "details": constsLib.details.repository_data_is_missing} 32 | } 33 | 34 | CbPolicy[msg] { 35 | utilsLib.is_organization_data_missing 36 | msg := {"ids": ["1.2.2", "1.2.3", "1.2.4"], "status": constsLib.status.Unknown, "details": constsLib.details.organization_not_fetched} 37 | } 38 | 39 | #Looking for security md file in repository 40 | CbPolicy[msg] { 41 | not utilsLib.is_repository_data_missing 42 | is_public_repository 43 | is_missing_security_md_file 44 | msg := {"ids": ["1.2.1"], "status": constsLib.status.Failed} 45 | } 46 | 47 | CbPolicy[msg] { 48 | not utilsLib.is_organization_data_missing 49 | is_repository_creation_not_limited_to_trusted_memebers 50 | msg := {"ids": ["1.2.2"], "status": constsLib.status.Failed} 51 | } 52 | 53 | CbPolicy[msg] { 54 | not utilsLib.is_organization_data_missing 55 | is_repository_deletion_not_limited_to_trusted_memebers 56 | msg := {"ids": ["1.2.3"], "status": constsLib.status.Failed} 57 | } 58 | 59 | CbPolicy[msg] { 60 | not utilsLib.is_organization_data_missing 61 | is_issue_deletion_not_limited_to_trusted_memebers 62 | msg := {"ids": ["1.2.4"], "status": constsLib.status.Failed} 63 | } 64 | -------------------------------------------------------------------------------- /internal/checks/source-code/sections.metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "text": "Source Code", 4 | "type": "sourceCode" 5 | } -------------------------------------------------------------------------------- /internal/checks/source-code/source-code.go: -------------------------------------------------------------------------------- 1 | package sourcecode 2 | 3 | import ( 4 | codechanges "github.com/aquasecurity/chain-bench/internal/checks/source-code/code-changes" 5 | contributionaccess "github.com/aquasecurity/chain-bench/internal/checks/source-code/contribution-access" 6 | repositorymanagement "github.com/aquasecurity/chain-bench/internal/checks/source-code/repository-management" 7 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 8 | ) 9 | 10 | func GetChecks() []*checkmodels.Check { 11 | checks := []*checkmodels.Check{} 12 | checks = append(checks, codechanges.GetChecks()...) 13 | checks = append(checks, contributionaccess.GetChecks()...) 14 | checks = append(checks, repositorymanagement.GetChecks()...) 15 | return checks 16 | } 17 | -------------------------------------------------------------------------------- /internal/commands/config.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/config" 5 | "github.com/aquasecurity/chain-bench/internal/logger" 6 | ) 7 | 8 | func generateCliConfig() *config.Configuration { 9 | return &config.Configuration{ 10 | LogConfiguration: &config.LogConfiguration{ 11 | LogFilePath: logFilePath, 12 | LogLevel: determineLogLevel(), 13 | LogFormat: logFormat, 14 | NoColor: noColor, 15 | }, 16 | OutputFilePath: outputFilePath, 17 | RepositoryUrl: repositoryUrl, 18 | OutputTemplateFilePath: outputTemplateFilePath, 19 | AccessToken: accessToken, 20 | } 21 | } 22 | 23 | func determineLogLevel() logger.LogLevel { 24 | if isQuiet { 25 | return logger.ErrorLevel 26 | } 27 | 28 | if verbosity == 0 { // if no cli flag, prefer default/config file 29 | return "" 30 | } 31 | 32 | if verbosity == 1 { 33 | return logger.DebugLevel 34 | } 35 | 36 | return logger.TraceLevel 37 | } 38 | -------------------------------------------------------------------------------- /internal/commands/flags.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | // General flags 4 | var ( 5 | // isQuiet if true, disable all console prints 6 | isQuiet bool 7 | isQuietFlagName = "quiet" 8 | isQuietShortFlag = "q" 9 | 10 | // outputFilePath if provided, output will be appended to a file 11 | outputFilePath string 12 | outputFilePathFlagName = "output-file" 13 | outputFilePathShortFlag = "o" 14 | 15 | // outputFilePath if provided, output will be appended to a file 16 | outputTemplateFilePath string 17 | outputTemplateFilePathFlagName = "template" 18 | 19 | // logFilePath if provided, logs will be appended to a file 20 | logFilePath string 21 | logFilePathFlagName = "log-file" 22 | logFilePathShortFlag = "l" 23 | 24 | // logFormat the format of the logs 25 | logFormat string 26 | logFormatFlagName = "log-format" 27 | 28 | // number if flags determinds log level verbosiry (0 - info, 1 - debug, 2 - trace) 29 | verbosity int 30 | verbosityFlagName = "verbose" 31 | verbosityShortFlag = "v" 32 | 33 | // noColor disables output color 34 | noColor bool 35 | noColorFlagName = "no-color" 36 | 37 | // configFilePath path to local configuration file 38 | configFilePath string 39 | configFilePathFlagName = "config-file" 40 | configFilePathShortFlag = "c" 41 | ) 42 | 43 | // Scan flags 44 | var ( 45 | // repositoryUrl the path to the repository to scan 46 | repositoryUrl string 47 | repositoryUrlFlagName = "repository-url" 48 | repositoryUrlShortFlag = "r" 49 | 50 | // accessToken the access token to use for the repository 51 | accessToken string 52 | accessTokenFlagName = "access-token" 53 | accessTokenShortFlag = "t" 54 | 55 | // SCM platform for self-hosted/dedicated environments 56 | scmPlatform string 57 | scmPlatformFlagName = "scm-platform" 58 | scmPlatformShortFlag = "s" 59 | 60 | // branch the branch name to scan 61 | branch string 62 | branchFlagName = "branch" 63 | branchShortFlag = "b" 64 | ) 65 | -------------------------------------------------------------------------------- /internal/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/config" 7 | "github.com/aquasecurity/chain-bench/internal/logger" 8 | "github.com/imdario/mergo" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | chainbenchConfig *config.Configuration 14 | ) 15 | 16 | func Execute(version string) error { 17 | rootCmd := NewChainBenchCommand(version) 18 | 19 | initialize(rootCmd) 20 | 21 | if err := rootCmd.Execute(); err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func NewChainBenchCommand(version string) *cobra.Command { 29 | return &cobra.Command{ 30 | Use: "chain-bench", 31 | Short: "Run CIS Benchmarks checks against your software supply chain", 32 | Version: version, 33 | SilenceUsage: true, 34 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 35 | var err error 36 | 37 | if chainbenchConfig, err = config.LoadConfiguration(configFilePath); err != nil { 38 | return err 39 | } 40 | 41 | if err = mergo.Merge(chainbenchConfig, generateCliConfig(), mergo.WithOverride); err != nil { 42 | return err 43 | } 44 | 45 | return initLogger() 46 | }, 47 | } 48 | } 49 | 50 | func initLogger() error { 51 | logConfig := chainbenchConfig.LogConfiguration 52 | if err := logger.InitLogger(logConfig.LogLevel, logConfig.LogFormat, logConfig.LogFilePath, logConfig.NoColor); err != nil { 53 | return fmt.Errorf("failed to init logger - %s", err.Error()) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func initialize(rootCmd *cobra.Command) { 60 | rootCmd.AddCommand(NewScanCommand()) 61 | 62 | rootCmd.PersistentFlags().BoolVarP(&isQuiet, 63 | isQuietFlagName, isQuietShortFlag, false, 64 | "silence logs, prints only error messages") 65 | rootCmd.PersistentFlags().StringVarP(&outputFilePath, 66 | outputFilePathFlagName, outputFilePathShortFlag, "", 67 | "the path to a file that will contain the results of the scanning") 68 | rootCmd.PersistentFlags().StringVar(&outputTemplateFilePath, 69 | outputTemplateFilePathFlagName, "", 70 | "the path to an output template format file") 71 | rootCmd.PersistentFlags().StringVarP(&configFilePath, 72 | configFilePathFlagName, configFilePathShortFlag, "", 73 | "the path to a local configuration file") 74 | rootCmd.PersistentFlags().StringVarP(&logFilePath, 75 | logFilePathFlagName, logFilePathShortFlag, "", 76 | "set to print logs into a file") 77 | rootCmd.PersistentFlags().StringVar(&logFormat, logFormatFlagName, "", 78 | fmt.Sprintf("sets the format of the logs (%s, %s)", logger.NormalFormat, logger.JsonFormat)) 79 | rootCmd.PersistentFlags().CountVarP(&verbosity, verbosityFlagName, verbosityShortFlag, 80 | "set the verbosity level (-v: debug, -vv: trace), default: info") 81 | rootCmd.PersistentFlags().BoolVar(&noColor, noColorFlagName, false, 82 | "disables output color") 83 | } 84 | -------------------------------------------------------------------------------- /internal/commands/scan.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/checker" 7 | "github.com/aquasecurity/chain-bench/internal/checks" 8 | "github.com/aquasecurity/chain-bench/internal/logger" 9 | "github.com/aquasecurity/chain-bench/internal/printer" 10 | "github.com/aquasecurity/chain-bench/internal/scm-clients/clients" 11 | "github.com/enescakir/emoji" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func NewScanCommand() *cobra.Command { 16 | scanCommand := &cobra.Command{ 17 | Use: "scan", 18 | Short: "Run CIS Benchmarks checks against a software supply chain", 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | start := time.Now() 21 | logger.Infof("%v Fetch Starting", emoji.TriangularFlag) 22 | assetsData, supportedChecks, err := clients.FetchClientData(accessToken, repositoryUrl, scmPlatform, branch) 23 | if assetsData.AuthorizedUser == nil || err != nil { 24 | logger.Error(err, "Failed to fetch client data") 25 | return err 26 | } else { 27 | logger.Infof("%v Fetch succeeded", emoji.ChequeredFlag) 28 | } 29 | 30 | checks := checks.GetChecks(assetsData) 31 | results, errors := checker.RunChecks(assetsData, chainbenchConfig, checks) 32 | 33 | printer.PrintFindings(results, supportedChecks, outputFilePath, isQuiet, repositoryUrl, outputTemplateFilePath) 34 | printer.PrintErrors(errors) 35 | elapsed := time.Since(start) 36 | logger.Infof("Scan completed: %s", elapsed.Round(time.Millisecond)) 37 | return nil 38 | }, 39 | } 40 | 41 | scanCommand.PersistentFlags().StringVarP(&repositoryUrl, 42 | repositoryUrlFlagName, repositoryUrlShortFlag, "", 43 | "the url to the repository") 44 | 45 | scanCommand.PersistentFlags().StringVarP(&accessToken, 46 | accessTokenFlagName, accessTokenShortFlag, "", 47 | "the access token to use for the repository") 48 | 49 | scanCommand.PersistentFlags().StringVarP(&scmPlatform, 50 | scmPlatformFlagName, scmPlatformShortFlag, "", 51 | "the SCM platform for self-hosted/dedicated environments (options: 'github', 'gitlab')") 52 | 53 | scanCommand.PersistentFlags().StringVarP(&branch, 54 | branchFlagName, branchShortFlag, "", 55 | "the branch to scan for branch protection") 56 | 57 | return scanCommand 58 | } 59 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/imdario/mergo" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | func loadConfigFile(configFilePath string) (*Configuration, error) { 12 | if configFilePath != "" { 13 | viper.SetConfigFile(configFilePath) 14 | } else if wd, err := os.Getwd(); err != nil { 15 | return nil, err 16 | } else { 17 | viper.AddConfigPath(wd) 18 | viper.SetConfigName("config") 19 | } 20 | 21 | if err := viper.ReadInConfig(); err != nil { 22 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { 23 | return nil, err 24 | } 25 | } 26 | 27 | var loadedConfig *Configuration 28 | if err := viper.Unmarshal(&loadedConfig); err != nil { 29 | return nil, fmt.Errorf("failed to load config to object - %s", err.Error()) 30 | } 31 | 32 | return loadedConfig, nil 33 | } 34 | 35 | func LoadConfiguration(configFilePath string) (*Configuration, error) { 36 | loadedConfig, err := loadConfigFile(configFilePath) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | defaultConfig := loadDefaultConfiguration() 42 | 43 | if loadedConfig == nil { 44 | return defaultConfig, nil 45 | } 46 | 47 | if err := mergo.Merge(loadedConfig, defaultConfig); err != nil { 48 | return nil, err 49 | } 50 | 51 | return loadedConfig, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/logger" 5 | ) 6 | 7 | func loadDefaultConfiguration() *Configuration { 8 | return &Configuration{ 9 | LogConfiguration: &LogConfiguration{ 10 | LogLevel: "info", 11 | LogFormat: logger.NormalFormat, 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/config/models.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/aquasecurity/chain-bench/internal/logger" 4 | 5 | type Configuration struct { 6 | LogConfiguration *LogConfiguration `mapstructure:"logs"` 7 | OutputFilePath string `mapstructure:"output_path"` 8 | OutputTemplateFilePath string `mapstructure:"output_template_path"` 9 | RepositoryUrl string `mapstructure:"repository_url"` 10 | AccessToken string `mapstructure:"access_token"` 11 | } 12 | 13 | type LogConfiguration struct { 14 | LogFilePath string `mapstructure:"log_path"` 15 | LogLevel logger.LogLevel `mapstructure:"log_level"` 16 | LogFormat string `mapstructure:"log_format"` 17 | NoColor bool `mapstructure:"no_color"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/consts/error.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | import "errors" 4 | 5 | // checks registration errors 6 | var ( 7 | ErrorNoCheckID = errors.New("Missing Check ID") 8 | ErrorNoName = errors.New("Missing Name") 9 | ErrorNoEntity = errors.New("Missing Entity") 10 | ErrorNoType = errors.New("Missing Type") 11 | ErrorNoDescription = errors.New("Missing Description") 12 | ErrorNoRemediation = errors.New("Missing Remediation") 13 | ErrorNoUrl = errors.New("Missing Url") 14 | ErrorNoCheckAction = errors.New("Check action cannot be nil") 15 | ) 16 | -------------------------------------------------------------------------------- /internal/logger/global.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "os" 8 | "time" 9 | 10 | "github.com/enescakir/emoji" 11 | "github.com/rs/zerolog" 12 | ) 13 | 14 | var logger zerolog.Logger 15 | 16 | const ( 17 | NormalFormat = "normal" 18 | JsonFormat = "json" 19 | ) 20 | 21 | // InitLogger initiates the global logger 22 | func InitLogger(logLevel LogLevel, logFormat string, filePath string, noColor bool) error { 23 | logFile := io.Discard 24 | consoleOutput := os.Stdout 25 | 26 | var consoleWriter io.Writer 27 | var fileWriter io.Writer 28 | 29 | rand.Seed(time.Now().UnixNano()) 30 | 31 | if filePath != "" { 32 | f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 33 | if err != nil { 34 | return err 35 | } 36 | logFile = f 37 | } 38 | 39 | if err := setLogLevel(logLevel); err != nil { 40 | return err 41 | } 42 | 43 | if logFormat == NormalFormat { 44 | normalFileWriter.Out = logFile 45 | normalConsoleWriter.Out = consoleOutput 46 | 47 | if noColor { 48 | normalConsoleWriter.NoColor = true 49 | } 50 | 51 | consoleWriter = normalConsoleWriter 52 | fileWriter = normalFileWriter 53 | 54 | } else if logFormat == JsonFormat { 55 | fileWriter = logFile 56 | consoleWriter = consoleOutput 57 | } else { 58 | return fmt.Errorf("log format '%s' is not supported (json, normal)", logFormat) 59 | } 60 | logger = zerolog.New(zerolog.MultiLevelWriter(consoleWriter, fileWriter)).With().Timestamp().Logger() 61 | return nil 62 | } 63 | 64 | func Debug(msg string) { 65 | logger.Debug().Msg(msg) 66 | } 67 | func Debugf(msg string, v ...interface{}) { 68 | logger.Debug().Msgf(msg, v...) 69 | } 70 | 71 | func Info(msg string) { 72 | logger.Info().Msg(msg) 73 | } 74 | func Infof(msg string, v ...interface{}) { 75 | logger.Info().Msgf(msg, v...) 76 | } 77 | 78 | func FetchingFinished(msg string, icon emoji.Emoji) { 79 | Infof("%v Fetching %s Finished", icon, msg) 80 | } 81 | 82 | func Error(err error, msg string) error { 83 | logger.Error().Msgf(msg) 84 | if err != nil { 85 | logger.Debug().Str("error", err.Error()).Msg(msg) 86 | } 87 | return err 88 | } 89 | 90 | func Errorf(err error, msg string, v ...interface{}) error { 91 | logger.Error().Msgf(msg, v...) 92 | if err != nil { 93 | logger.Debug().Str("error", err.Error()).Msgf(msg, v...) 94 | } 95 | return err 96 | } 97 | 98 | func Warn(msg string) { 99 | logger.Warn().Msg(msg) 100 | } 101 | func Warnf(msg string, v ...interface{}) { 102 | logger.Warn().Msgf(msg, v...) 103 | } 104 | 105 | func WarnE(err error, msg string) error { 106 | logger.Warn().Msg(msg) 107 | if err != nil { 108 | logger.Debug().Str("error", err.Error()).Msgf(msg) 109 | } 110 | return err 111 | } 112 | 113 | func Panic(msg string) { 114 | logger.Panic().Msg(msg) 115 | } 116 | func Panicf(msg string, v ...interface{}) { 117 | logger.Panic().Msgf(msg, v...) 118 | } 119 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/rs/zerolog" 5 | ) 6 | 7 | type Logger interface { 8 | Debug(msg string) 9 | Debugf(msg string, v ...interface{}) 10 | Info(msg string) 11 | Infof(msg string, v ...interface{}) 12 | Warn(msg string) 13 | Warnf(msg string, v ...interface{}) 14 | Error(err error, msg string) error 15 | Errorf(err error, msg string, v ...interface{}) error 16 | Panic(msg string) 17 | Panicf(msg string, v ...interface{}) 18 | } 19 | 20 | type LogLevel string 21 | 22 | const ( 23 | TraceLevel LogLevel = "trace" 24 | DebugLevel = "debug" 25 | InfoLevel = "info" 26 | WarnLevel = "warn" 27 | ErrorLevel = "error" 28 | PanicLevel = "panic" 29 | ) 30 | 31 | func (l LogLevel) String() string { 32 | return string(l) 33 | } 34 | 35 | type ArgonLogger struct { 36 | context string 37 | } 38 | 39 | func NewLogger(context string) Logger { 40 | return &ArgonLogger{context: context} 41 | } 42 | 43 | func (l *ArgonLogger) appendFieldsToLogger(event *zerolog.Event) *zerolog.Event { 44 | return event.Str("context", l.context) 45 | } 46 | 47 | func (l *ArgonLogger) Debug(msg string) { 48 | l.appendFieldsToLogger(logger.Debug()).Msg(msg) 49 | } 50 | 51 | func (l *ArgonLogger) Debugf(msg string, v ...interface{}) { 52 | l.appendFieldsToLogger(logger.Debug()).Msgf(msg, v...) 53 | } 54 | 55 | func (l *ArgonLogger) Info(msg string) { 56 | l.appendFieldsToLogger(logger.Info()).Msg(msg) 57 | } 58 | 59 | func (l *ArgonLogger) Infof(msg string, v ...interface{}) { 60 | l.appendFieldsToLogger(logger.Info()).Msgf(msg, v...) 61 | } 62 | 63 | func (l *ArgonLogger) Warn(msg string) { 64 | l.appendFieldsToLogger(logger.Warn()).Msg(msg) 65 | } 66 | 67 | func (l *ArgonLogger) Warnf(msg string, v ...interface{}) { 68 | l.appendFieldsToLogger(logger.Warn()).Msgf(msg, v...) 69 | } 70 | 71 | func (l *ArgonLogger) Error(err error, msg string) error { 72 | if err != nil { 73 | l.appendFieldsToLogger(logger.Error()).Str("error", err.Error()).Msg(msg) 74 | } else { 75 | l.appendFieldsToLogger(logger.Error()).Msg(msg) 76 | } 77 | return err 78 | } 79 | 80 | func (l *ArgonLogger) Errorf(err error, msg string, v ...interface{}) error { 81 | if err != nil { 82 | l.appendFieldsToLogger(logger.Error()).Str("error", err.Error()).Msgf(msg, v...) 83 | } else { 84 | l.appendFieldsToLogger(logger.Error()).Msgf(msg, v...) 85 | } 86 | return err 87 | } 88 | 89 | func (l *ArgonLogger) Panic(msg string) { 90 | l.appendFieldsToLogger(logger.Panic()).Msg(msg) 91 | } 92 | 93 | func (l *ArgonLogger) Panicf(msg string, v ...interface{}) { 94 | l.appendFieldsToLogger(logger.Panic()).Msgf(msg, v...) 95 | } 96 | -------------------------------------------------------------------------------- /internal/logger/zerolog.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | var ( 12 | normalConsoleWriter zerolog.ConsoleWriter = zerolog.ConsoleWriter{Out: io.Discard, TimeFormat: "2006-01-02 15:04:05", NoColor: false} 13 | normalFileWriter zerolog.ConsoleWriter = zerolog.ConsoleWriter{Out: io.Discard, TimeFormat: "2006-01-02T15:04:05Z07:00", NoColor: true} 14 | ) 15 | 16 | func setLogLevel(levelName LogLevel) error { 17 | var level zerolog.Level 18 | 19 | switch strings.ToUpper(levelName.String()) { 20 | case "TRACE": 21 | level = zerolog.TraceLevel 22 | case "DEBUG": 23 | level = zerolog.DebugLevel 24 | case "INFO": 25 | level = zerolog.InfoLevel 26 | case "WARNING": 27 | level = zerolog.WarnLevel 28 | case "ERROR": 29 | level = zerolog.ErrorLevel 30 | case "PANIC": 31 | level = zerolog.PanicLevel 32 | default: 33 | return fmt.Errorf("log level '%s' does not exist", levelName) 34 | } 35 | zerolog.SetGlobalLevel(level) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/models/app.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/aquasecurity/chain-bench/internal/utils" 4 | 5 | // App represents a GitHub App. 6 | type App struct { 7 | ID *int64 8 | Slug *string 9 | NodeID *string 10 | Owner *User 11 | Name *string 12 | Description *string 13 | ExternalURL *string 14 | HTMLURL *string 15 | CreatedAt *utils.Timestamp 16 | UpdatedAt *utils.Timestamp 17 | Permissions *InstallationPermissions 18 | Events []string 19 | } 20 | 21 | // InstallationPermissions lists the repository and organization permissions for an installation. 22 | type InstallationPermissions struct { 23 | Actions *string 24 | Administration *string 25 | Blocking *string 26 | Checks *string 27 | Contents *string 28 | ContentReferences *string 29 | Deployments *string 30 | Emails *string 31 | Environments *string 32 | Followers *string 33 | Issues *string 34 | Metadata *string 35 | Members *string 36 | OrganizationAdministration *string 37 | OrganizationHooks *string 38 | OrganizationPlan *string 39 | OrganizationPreReceiveHooks *string 40 | OrganizationProjects *string 41 | OrganizationSecrets *string 42 | OrganizationSelfHostedRunners *string 43 | OrganizationUserBlocking *string 44 | Packages *string 45 | Pages *string 46 | PullRequests *string 47 | RepositoryHooks *string 48 | RepositoryProjects *string 49 | RepositoryPreReceiveHooks *string 50 | Secrets *string 51 | SecretScanningAlerts *string 52 | SecurityEvents *string 53 | SingleFile *string 54 | Statuses *string 55 | TeamDiscussions *string 56 | VulnerabilityAlerts *string 57 | Workflows *string 58 | } 59 | -------------------------------------------------------------------------------- /internal/models/branch.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Protection struct { 8 | RequiredStatusChecks *RequiredStatusChecks 9 | RequiredPullRequestReviews *PullRequestReviewsEnforcement 10 | EnforceAdmins *AdminEnforcement 11 | Restrictions *BranchRestrictions 12 | RequireLinearHistory bool 13 | AllowForcePushes bool 14 | AllowDeletions bool 15 | RequiredConversationResolution bool 16 | RequiredSignedCommit bool 17 | //PreventSecrets bool 18 | } 19 | 20 | // PullRequestReviewsEnforcement represents the pull request reviews enforcement of a protected branch. 21 | type PullRequestReviewsEnforcement struct { 22 | // Specifies which users and teams can dismiss pull request reviews. 23 | DismissalRestrictions *DismissalRestrictions 24 | // Specifies if approved reviews are dismissed automatically, when a new commit is pushed. 25 | DismissStaleReviews bool 26 | // RequireCodeOwnerReviews specifies if an approved review is required in pull requests including files with a designated code owner. 27 | RequireCodeOwnerReviews bool 28 | // RequiredApprovingReviewCount specifies the number of approvals required before the pull request can be merged. 29 | // Valid values are 1-6. 30 | RequiredApprovingReviewCount int 31 | } 32 | 33 | type RequiredStatusChecks struct { 34 | Strict bool 35 | } 36 | 37 | // AdminEnforcement represents the configuration to enforce required status checks for repository administrators. 38 | type AdminEnforcement struct { 39 | URL *string 40 | Enabled bool 41 | } 42 | 43 | // BranchRestrictions represents the restriction that only certain users or 44 | // teams may push to a branch. 45 | type BranchRestrictions struct { 46 | // The list of user logins with push access. 47 | Users []*User 48 | // The list of team slugs with push access. 49 | Teams []*Team 50 | // The list of app slugs with push access. 51 | Apps []*App 52 | } 53 | 54 | // DismissalRestrictions specifies which users and teams can dismiss pull request reviews. 55 | type DismissalRestrictions struct { 56 | // The list of users who can dimiss pull request reviews. 57 | Users []*User `json:"users"` 58 | } 59 | 60 | // Branch represents a repository branch 61 | type Branch struct { 62 | Name *string `json:"name,omitempty"` 63 | Commit *RepositoryCommit 64 | Protected *bool `json:"protected,omitempty"` 65 | } 66 | 67 | type RepositoryCommit struct { 68 | NodeID *string `json:"node_id,omitempty"` 69 | SHA *string `json:"sha,omitempty"` 70 | Author *CommitAuthor 71 | Committer *CommitAuthor 72 | URL *string `json:"url,omitempty"` 73 | Verification *SignatureVerification `json:"verification,omitempty"` 74 | } 75 | 76 | type CommitAuthor struct { 77 | Date *time.Time 78 | Name *string `json:"name,omitempty"` 79 | Email *string `json:"email,omitempty"` 80 | 81 | // The following fields are only populated by Webhook events. 82 | Login *string `json:"username,omitempty"` // Renamed for go-github consistency. 83 | } 84 | 85 | // SignatureVerification represents GPG signature verification. 86 | type SignatureVerification struct { 87 | Verified *bool `json:"verified,omitempty"` 88 | Reason *string `json:"reason,omitempty"` 89 | Signature *string `json:"signature,omitempty"` 90 | Payload *string `json:"payload,omitempty"` 91 | } 92 | -------------------------------------------------------------------------------- /internal/models/checkmodels/check.go: -------------------------------------------------------------------------------- 1 | package checkmodels 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/config" 8 | "github.com/aquasecurity/chain-bench/internal/models" 9 | pipelineModels "github.com/argonsecurity/pipeline-parser/pkg/models" 10 | ) 11 | 12 | type ScannerType string 13 | 14 | const ( 15 | Rego ScannerType = "Rego" 16 | Custom ScannerType = "Custom" 17 | ) 18 | 19 | type CheckType string 20 | 21 | const ( 22 | SCM CheckType = "SCM" 23 | ) 24 | 25 | type EntityType string 26 | 27 | const ( 28 | Organization EntityType = "Organization" 29 | Repository EntityType = "Repository" 30 | ) 31 | 32 | type CheckData struct { 33 | Configuration *config.Configuration 34 | AssetsMetadata *AssetsData 35 | } 36 | 37 | type AssetsData struct { 38 | AuthorizedUser *models.User 39 | Organization *models.Organization 40 | Repository *models.Repository 41 | BranchProtections *models.Protection 42 | Users []*models.User 43 | Pipelines []*pipelineModels.Pipeline 44 | Registry *models.PackageRegistry 45 | } 46 | 47 | type ResultStatus string 48 | 49 | const ( 50 | Passed ResultStatus = "Passed" 51 | Failed ResultStatus = "Failed" 52 | Unknown ResultStatus = "Unknown" 53 | ) 54 | 55 | type CheckMetadataMap struct { 56 | ID string 57 | Name string 58 | Url string 59 | Checks map[string]CheckMetadata 60 | } 61 | 62 | type CheckMetadata struct { 63 | Title string 64 | Type CheckType 65 | Entity EntityType 66 | Description string 67 | Remediation string 68 | Url string 69 | Severity string 70 | ScannerType 71 | } 72 | 73 | type CheckResult struct { 74 | Status ResultStatus `json:"status,omitempty"` 75 | Details string `json:"details,omitempty"` 76 | } 77 | 78 | type CheckIdToCheckResultMap map[string]CheckResult 79 | 80 | type CheckAction func(*CheckData) ([]*CheckRunResult, error) 81 | 82 | type Check struct { 83 | CheckMetadataMap 84 | Action CheckAction 85 | ScannerType 86 | } 87 | 88 | type CheckRunResult struct { 89 | ID string 90 | Metadata CheckMetadata 91 | Result *CheckResult 92 | } 93 | 94 | type RegoCustomModule struct { 95 | Name string 96 | Content string 97 | } 98 | 99 | func ToCheckRunResult(id string, metadata CheckMetadata, sectionUrl string, result *CheckResult) *CheckRunResult { 100 | return &CheckRunResult{ 101 | ID: id, 102 | Metadata: CheckMetadata{ 103 | Title: metadata.Title, 104 | Type: metadata.Type, 105 | Entity: metadata.Entity, 106 | Description: metadata.Description, 107 | Remediation: metadata.Remediation, 108 | Severity: metadata.Severity, 109 | Url: getPermalink(sectionUrl, id, metadata.Title), 110 | ScannerType: metadata.ScannerType, 111 | }, 112 | Result: result} 113 | } 114 | 115 | func getPermalink(sectionUrl, id, name string) string { 116 | parsedId := fmt.Sprintf("#%s", strings.ToLower(strings.ReplaceAll(id, ".", ""))) 117 | parsedName := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(name, " ", "-"), "'", "")) 118 | return fmt.Sprintf("%s/%s-%s", sectionUrl, parsedId, parsedName) 119 | } 120 | -------------------------------------------------------------------------------- /internal/models/hook.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Hook struct { 8 | CreatedAt *time.Time 9 | UpdatedAt *time.Time 10 | URL *string 11 | ID *int64 12 | Type *string 13 | Name *string 14 | TestURL *string 15 | PingURL *string 16 | LastResponse map[string]interface{} 17 | 18 | // Only the following fields are used when creating a hook. 19 | // Config is required. 20 | Config *HookConfig 21 | Events []string 22 | Active *bool 23 | } 24 | 25 | type HookConfig struct { 26 | Insecure_SSL *string 27 | URL *string 28 | Secret *string 29 | } 30 | -------------------------------------------------------------------------------- /internal/models/organization.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Organization struct { 6 | Login *string 7 | ID *int64 8 | NodeID *string 9 | Name *string 10 | Company *string 11 | Location *string 12 | Email *string 13 | Description *string 14 | PublicRepos *int 15 | CreatedAt *time.Time 16 | UpdatedAt *time.Time 17 | TotalPrivateRepos *int 18 | OwnedPrivateRepos *int 19 | Collaborators *int 20 | Type *string 21 | Plan *Plan 22 | DefaultRepoPermission *string 23 | DefaultRepoSettings *string 24 | MembersCanCreateRepos *bool 25 | MembersCanCreatePublicRepos *bool 26 | MembersCanCreatePrivateRepos *bool 27 | MembersCanCreateInternalRepos *bool 28 | TwoFactorRequirementEnabled *bool 29 | IsVerified *bool 30 | Members []*User 31 | IsRepositoryDeletionLimited *bool 32 | IsIssueDeletionLimited *bool 33 | Hooks []*Hook 34 | } 35 | 36 | type Plan struct { 37 | Name *string 38 | Space *int 39 | Collaborators *int 40 | PrivateRepos *int 41 | } 42 | -------------------------------------------------------------------------------- /internal/models/package_registry.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/utils" 5 | ) 6 | 7 | type PackageRegistry struct { 8 | TwoFactorRequirementEnabled *bool 9 | Packages []*Package 10 | } 11 | 12 | type Package struct { 13 | ID *int64 14 | Name *string 15 | PackageType *string 16 | HTMLURL *string 17 | CreatedAt *utils.Timestamp 18 | UpdatedAt *utils.Timestamp 19 | Owner *User 20 | Version *string 21 | URL *string 22 | VersionCount *int64 23 | Visibility *string 24 | Repository *Repository 25 | } 26 | -------------------------------------------------------------------------------- /internal/models/repository.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/aquasecurity/chain-bench/internal/utils" 4 | 5 | type Repository struct { 6 | ID *int64 7 | NodeID *string `json:"node_id,omitempty"` 8 | Owner *User `json:"owner,omitempty"` 9 | Name *string `json:"name,omitempty"` 10 | Description *string `json:"description,omitempty"` 11 | DefaultBranch *string `json:"default_branch,omitempty"` 12 | MasterBranch *string `json:"master_branch,omitempty"` 13 | CreatedAt *utils.Timestamp `json:"created_at,omitempty"` 14 | PushedAt *utils.Timestamp `json:"pushed_at,omitempty"` 15 | UpdatedAt *utils.Timestamp `json:"updated_at,omitempty"` 16 | Language *string `json:"language,omitempty"` 17 | Fork *bool `json:"fork,omitempty"` 18 | ForksCount *int `json:"forks_count,omitempty"` 19 | NetworkCount *int `json:"network_count,omitempty"` 20 | OpenIssuesCount *int `json:"open_issues_count,omitempty"` 21 | StargazersCount *int `json:"stargazers_count,omitempty"` 22 | SubscribersCount *int `json:"subscribers_count,omitempty"` 23 | WatchersCount *int `json:"watchers_count,omitempty"` 24 | Size *int `json:"size,omitempty"` 25 | AutoInit *bool `json:"auto_init,omitempty"` 26 | Parent *Repository `json:"parent,omitempty"` 27 | Source *Repository `json:"source,omitempty"` 28 | Organization *Organization `json:"organization,omitempty"` 29 | AllowRebaseMerge *bool 30 | AllowSquashMerge *bool 31 | AllowMergeCommit *bool 32 | Topics []string `json:"topics,omitempty"` 33 | 34 | // Only provided when using RepositoriesService.Get while in preview 35 | License *License `json:"license,omitempty"` 36 | 37 | // Additional mutable fields when creating and editing a repository 38 | IsPrivate *bool 39 | HasIssues *bool `json:"has_issues,omitempty"` 40 | LicenseTemplate *string `json:"license_template,omitempty"` 41 | GitignoreTemplate *string `json:"gitignore_template,omitempty"` 42 | Archived *bool `json:"archived,omitempty"` 43 | 44 | // Creating an organization repository. Required for non-owners. 45 | TeamID *int64 `json:"team_id,omitempty"` 46 | 47 | // API URLs 48 | URL *string `json:"url,omitempty"` 49 | 50 | Branches []*Branch 51 | Collaborators []*User 52 | IsContainsSecurityMd bool 53 | Commits []*RepositoryCommit 54 | Hooks []*Hook 55 | } 56 | 57 | type License struct { 58 | Key *string `json:"key,omitempty"` 59 | Name *string `json:"name,omitempty"` 60 | URL *string `json:"url,omitempty"` 61 | SPDXID *string `json:"spdx_id,omitempty"` 62 | HTMLURL *string `json:"html_url,omitempty"` 63 | Featured *bool `json:"featured,omitempty"` 64 | Description *string `json:"description,omitempty"` 65 | Implementation *string `json:"implementation,omitempty"` 66 | Conditions *[]string `json:"conditions,omitempty"` 67 | Permissions *[]string `json:"permissions,omitempty"` 68 | Limitations *[]string `json:"limitations,omitempty"` 69 | Body *string `json:"body,omitempty"` 70 | } 71 | -------------------------------------------------------------------------------- /internal/models/team.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Team represents a team within a GitHub organization. Teams are used to 4 | // manage access to an organization's repositories. 5 | type Team struct { 6 | ID *int64 7 | Name *string 8 | Description *string 9 | URL *string 10 | Slug *string 11 | 12 | // Permission specifies the default permission for repositories owned by the team. 13 | Permission *string 14 | 15 | // Permissions identifies the permissions that a team has on a given 16 | // repository. This is only populated when calling Repositories.ListTeams. 17 | Permissions map[string]bool 18 | 19 | // Privacy identifies the level of privacy this team should have. 20 | // Possible values are: 21 | // secret - only visible to organization owners and members of this team 22 | // closed - visible to all members of this organization 23 | // Default is "secret". 24 | Privacy *string 25 | 26 | MembersCount *int 27 | ReposCount *int 28 | } 29 | -------------------------------------------------------------------------------- /internal/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/aquasecurity/chain-bench/internal/utils" 4 | 5 | type User struct { 6 | Login *string `json:"login,omitempty"` 7 | ID *int64 `json:"id,omitempty"` 8 | NodeID *string `json:"node_id,omitempty"` 9 | AvatarURL *string `json:"avatar_url,omitempty"` 10 | HTMLURL *string `json:"html_url,omitempty"` 11 | GravatarID *string `json:"gravatar_id,omitempty"` 12 | Name *string `json:"name,omitempty"` 13 | Company *string `json:"company,omitempty"` 14 | Blog *string `json:"blog,omitempty"` 15 | Location *string `json:"location,omitempty"` 16 | Email *string `json:"email,omitempty"` 17 | Hireable *bool `json:"hireable,omitempty"` 18 | Bio *string `json:"bio,omitempty"` 19 | PublicRepos *int `json:"public_repos,omitempty"` 20 | PublicGists *int `json:"public_gists,omitempty"` 21 | Followers *int `json:"followers,omitempty"` 22 | Following *int `json:"following,omitempty"` 23 | CreatedAt *utils.Timestamp `json:"created_at,omitempty"` 24 | UpdatedAt *utils.Timestamp `json:"updated_at,omitempty"` 25 | SuspendedAt *utils.Timestamp `json:"suspended_at,omitempty"` 26 | Type *string `json:"type,omitempty"` 27 | SiteAdmin *bool `json:"site_admin,omitempty"` 28 | TotalPrivateRepos *int `json:"total_private_repos,omitempty"` 29 | OwnedPrivateRepos *int `json:"owned_private_repos,omitempty"` 30 | PrivateGists *int `json:"private_gists,omitempty"` 31 | DiskUsage *int `json:"disk_usage,omitempty"` 32 | Collaborators *int `json:"collaborators,omitempty"` 33 | Plan *Plan `json:"plan,omitempty"` 34 | Role string 35 | 36 | // API URLs 37 | URL *string `json:"url,omitempty"` 38 | EventsURL *string `json:"events_url,omitempty"` 39 | FollowingURL *string `json:"following_url,omitempty"` 40 | FollowersURL *string `json:"followers_url,omitempty"` 41 | GistsURL *string `json:"gists_url,omitempty"` 42 | OrganizationsURL *string `json:"organizations_url,omitempty"` 43 | ReceivedEventsURL *string `json:"received_events_url,omitempty"` 44 | ReposURL *string `json:"repos_url,omitempty"` 45 | StarredURL *string `json:"starred_url,omitempty"` 46 | SubscriptionsURL *string `json:"subscriptions_url,omitempty"` 47 | 48 | // Permissions identifies the permissions that a user has on a given 49 | // repository. This is only populated when calling Repositories.ListCollaborators. 50 | Permissions *map[string]bool `json:"permissions,omitempty"` 51 | } 52 | -------------------------------------------------------------------------------- /internal/printer/colors.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | var ( 4 | ColorRed = "\x1b[91m" 5 | ColorGreen = "\x1b[32m" 6 | ColorBlue = "\x1b[94m" 7 | ColorGray = "\x1b[90m" 8 | ColorYellow = "\x1b[33m" 9 | ColorWhite = "\x1b[37m" 10 | ) 11 | -------------------------------------------------------------------------------- /internal/printer/helpers.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "strings" 11 | "text/template" 12 | "time" 13 | 14 | "github.com/aquasecurity/chain-bench/internal/logger" 15 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 16 | "github.com/google/uuid" 17 | ) 18 | 19 | var ( 20 | output io.Writer = os.Stdout 21 | ) 22 | 23 | type reportResult struct { 24 | ID string `json:"id,omitempty"` 25 | Name string `json:"name,omitempty"` 26 | Description string `json:"description,omitempty"` 27 | Remediation string `json:"remediation,omitempty"` 28 | Severity string `json:"severity,omitempty"` 29 | Result string `json:"result,omitempty"` 30 | Reason string `json:"reason,omitempty"` 31 | Url string `json:"url,omitempty"` 32 | } 33 | 34 | type reportMetadata struct { 35 | Date string `json:"date"` 36 | ScanID uuid.UUID `json:"scan_id"` 37 | Statistics Statistics `json:"statistics"` 38 | Url string `json:"url,omitempty"` 39 | } 40 | 41 | type reportResults struct { 42 | Metadata reportMetadata `json:"metadata"` 43 | Results []reportResult `json:"results"` 44 | } 45 | 46 | // println prints a string to the current configured output 47 | func println(msg string) { 48 | fmt.Fprintln(output, msg) 49 | } 50 | 51 | func PrintOutputToFile(data []checkmodels.CheckRunResult, outputFilePath string, repositoryUrl string, outputTemplate string) { 52 | reportRes, statistics := getPrintData(data) 53 | 54 | // Populate the report metadata. 55 | reportMetadata := reportMetadata{ 56 | Date: time.Now().Format(time.RFC3339), 57 | ScanID: uuid.New(), 58 | Url: repositoryUrl, 59 | Statistics: statistics, 60 | } 61 | 62 | // Populate the report. 63 | report := reportResults{ 64 | reportMetadata, 65 | reportRes, 66 | } 67 | 68 | if strings.HasPrefix(outputTemplate, "@") { 69 | buf, err := os.ReadFile(strings.TrimPrefix(outputTemplate, "@")) 70 | if err != nil { 71 | logger.Errorf(err, "error retrieving template from path: %s", outputTemplate) 72 | } 73 | outputTemplate = string(buf) 74 | t, _ := template.New("output template").Parse(outputTemplate) 75 | outputFile, err := os.OpenFile(outputFilePath, os.O_RDWR|os.O_CREATE, 0644) 76 | if err != nil { 77 | PrintError("Failed to create an output file, make sure your path is valid") 78 | } 79 | err = t.Execute(outputFile, reportRes) 80 | if err != nil { 81 | PrintError("Failed to create the template, check the template is valid") 82 | } 83 | } else { 84 | file, _ := json.MarshalIndent(report, "", " ") 85 | err := ioutil.WriteFile(outputFilePath, file, 0644) 86 | if err != nil { 87 | PrintError("Failed to write to output file, make sure your path is valid") 88 | } 89 | } 90 | } 91 | 92 | func getPrintData(results []checkmodels.CheckRunResult) ([]reportResult, Statistics) { 93 | resultsToDisplay := []reportResult{} 94 | statistics := NewStatistics() 95 | 96 | for _, r := range results { 97 | resultsToDisplay = append(resultsToDisplay, reportResult{ 98 | Name: r.Metadata.Title, 99 | ID: r.ID, 100 | Description: r.Metadata.Description, 101 | Remediation: r.Metadata.Remediation, 102 | Severity: r.Metadata.Severity, 103 | Result: string(r.Result.Status), 104 | Reason: r.Result.Details, 105 | Url: r.Metadata.Url}) 106 | 107 | statistics.Add(r.Result.Status) 108 | } 109 | 110 | return resultsToDisplay, statistics 111 | } 112 | 113 | // PrintErrorf prints a message with error color 114 | func PrintError(msg string) { 115 | println(fmt.Sprint(ColorRed, msg)) 116 | } 117 | -------------------------------------------------------------------------------- /internal/printer/printer.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/alexeyco/simpletable" 9 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 10 | "github.com/aquasecurity/chain-bench/internal/utils" 11 | "github.com/hashicorp/go-version" 12 | ) 13 | 14 | type CellData struct { 15 | text string 16 | color string 17 | align int 18 | span int 19 | } 20 | 21 | var table *simpletable.Table 22 | 23 | func init() { 24 | table = simpletable.New() 25 | table.SetStyle(simpletable.StyleCompactLite) 26 | } 27 | 28 | func PrintFindings(results []checkmodels.CheckRunResult, supportedChecks []string, outputFilePath string, isQuiet bool, repositoryUrl string, outputTemplateFilePath string) { 29 | filteredResults := getSupportedChecks(results, supportedChecks) 30 | sortResults(filteredResults) 31 | if outputFilePath != "" { 32 | PrintOutputToFile(filteredResults, outputFilePath, repositoryUrl, outputTemplateFilePath) 33 | } 34 | if !isQuiet { 35 | s := NewStatistics() 36 | table.Header = CreateHeader([]string{"ID", "Name", "Result", "Reason"}) 37 | for _, row := range filteredResults { 38 | rowData := []CellData{ 39 | {text: row.ID}, 40 | {text: row.Metadata.Title}, 41 | getResultData(row.Result.Status), 42 | {text: row.Result.Details}, 43 | } 44 | table.Body.Cells = append(table.Body.Cells, CreateBodyRow(rowData)) 45 | s.Add(row.Result.Status) 46 | } 47 | table.Footer = CreateFooter(s, len(table.Header.Cells)) 48 | fmt.Println(table.String()) 49 | } 50 | } 51 | 52 | func sortResults(results []checkmodels.CheckRunResult) { 53 | sort.SliceStable(results, func(i, j int) bool { 54 | id1, _ := version.NewVersion(results[i].ID) 55 | id2, _ := version.NewVersion(results[j].ID) 56 | return id1.LessThan(id2) 57 | }) 58 | } 59 | 60 | func getResultData(status checkmodels.ResultStatus) CellData { 61 | if status == checkmodels.Passed { 62 | return CellData{text: string(status), color: ColorGreen} 63 | } else if status == checkmodels.Unknown { 64 | return CellData{text: string(status), color: ColorYellow} 65 | } else { 66 | return CellData{text: string(status), color: ColorRed} 67 | } 68 | } 69 | 70 | func PrintErrors(errors []error) { 71 | if len(errors) > 0 { 72 | PrintError(errorsToString(errors)) 73 | } 74 | } 75 | 76 | func errorsToString(errs []error) string { 77 | errorMessages := "" 78 | for _, err := range errs { 79 | errorMessages += fmt.Sprintln(err.Error()) 80 | } 81 | return errorMessages 82 | } 83 | 84 | func getSupportedChecks(results []checkmodels.CheckRunResult, supportedIds []string) []checkmodels.CheckRunResult { 85 | if supportedIds == nil { // all checks supported 86 | return results 87 | } 88 | 89 | res := []checkmodels.CheckRunResult{} 90 | for _, r := range results { 91 | if utils.Contains(supportedIds, r.ID) { 92 | res = append(res, r) 93 | } 94 | } 95 | 96 | return res 97 | } 98 | -------------------------------------------------------------------------------- /internal/printer/statistics.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 7 | ) 8 | 9 | type Statistics struct { 10 | Passed int `json:"passed,omitempty"` 11 | Failed int `json:"failed,omitempty"` 12 | Unknown int `json:"unknown,omitempty"` 13 | Total int `json:"total,omitempty"` 14 | } 15 | 16 | // NewStatistics initializes a new Statistics struct. 17 | func NewStatistics() Statistics { 18 | return Statistics{Passed: 0, Failed: 0, Unknown: 0, Total: 0} 19 | } 20 | 21 | // Add increments the value of a specific field as well as the total value. 22 | func (s *Statistics) Add(value checkmodels.ResultStatus) error { 23 | switch value { 24 | case checkmodels.Passed: 25 | s.Passed++ 26 | case checkmodels.Failed: 27 | s.Failed++ 28 | case checkmodels.Unknown: 29 | s.Unknown++ 30 | default: 31 | return fmt.Errorf("unknown statistical value: %s", value) 32 | } 33 | s.Total++ 34 | 35 | return nil 36 | } 37 | 38 | // Sub decrements the value of a specific field as well as the total value. 39 | func (s *Statistics) Sub(value checkmodels.ResultStatus) error { 40 | switch value { 41 | case checkmodels.Passed: 42 | s.Passed-- 43 | case checkmodels.Failed: 44 | s.Failed-- 45 | case checkmodels.Unknown: 46 | s.Unknown-- 47 | default: 48 | return fmt.Errorf("unknown statistical value: %s", value) 49 | } 50 | 51 | s.Total-- 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/printer/table_builder.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/alexeyco/simpletable" 7 | ) 8 | 9 | func CreateHeader(titles []string) *simpletable.Header { 10 | header := new(simpletable.Header) 11 | for _, t := range titles { 12 | header.Cells = append(header.Cells, headerCell(CellData{text: t})) 13 | } 14 | return header 15 | } 16 | 17 | func CreateFooter(s Statistics, columnsCount int) *simpletable.Footer { 18 | footer := new(simpletable.Footer) 19 | footer.Cells = append(footer.Cells, cell(CellData{span: columnsCount, align: simpletable.AlignLeft, text: fmt.Sprintf("Total Passed Rules: %d out of %d", s.Passed, s.Failed+s.Passed), color: ColorWhite})) 20 | return footer 21 | } 22 | 23 | func CreateBodyRow(cellsData []CellData) []*simpletable.Cell { 24 | row := []*simpletable.Cell{} 25 | for _, cd := range cellsData { 26 | row = append(row, cell(cd)) 27 | } 28 | return row 29 | } 30 | 31 | func cell(data CellData) *simpletable.Cell { 32 | color := ColorBlue 33 | if data.color != "" { 34 | color = data.color 35 | } 36 | return &simpletable.Cell{Text: fmt.Sprint(color, data.text), Span: data.span, Align: data.align} 37 | } 38 | 39 | func headerCell(data CellData) *simpletable.Cell { 40 | data.color = ColorGray 41 | data.align = simpletable.AlignCenter 42 | return cell(data) 43 | } 44 | -------------------------------------------------------------------------------- /internal/scm-clients/adapter/adapter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/models" 7 | pipelineModels "github.com/argonsecurity/pipeline-parser/pkg/models" 8 | ) 9 | 10 | type ClientAdapter interface { 11 | Init(client *http.Client, token string, host string) error 12 | ListSupportedChecksIDs() ([]string, error) 13 | GetRepository(owner, repo, branchName string) (*models.Repository, error) 14 | ListRepositoryBranches(owner, repo string) ([]*models.Branch, error) 15 | GetBranchProtection(owner string, repo *models.Repository, branch string) (*models.Protection, error) 16 | GetOrganization(owner string) (*models.Organization, error) 17 | GetRegistry(organization *models.Organization) (*models.PackageRegistry, error) 18 | ListOrganizationMembers(organization string) ([]*models.User, error) 19 | GetPipelines(owner, repo, branch string) ([]*pipelineModels.Pipeline, error) 20 | GetAuthorizedUser() (*models.User, error) 21 | } 22 | -------------------------------------------------------------------------------- /internal/scm-clients/clients/clients.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "net/url" 9 | 10 | "github.com/aquasecurity/chain-bench/internal/logger" 11 | "github.com/aquasecurity/chain-bench/internal/models" 12 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 13 | "github.com/aquasecurity/chain-bench/internal/scm-clients/adapter" 14 | "github.com/aquasecurity/chain-bench/internal/scm-clients/github" 15 | "github.com/aquasecurity/chain-bench/internal/scm-clients/gitlab" 16 | "github.com/aquasecurity/chain-bench/internal/utils" 17 | pipelineModels "github.com/argonsecurity/pipeline-parser/pkg/models" 18 | "github.com/enescakir/emoji" 19 | ) 20 | 21 | const ( 22 | GithubEndpoint = "github.com" 23 | GitlabEndpoint = "gitlab.com" 24 | 25 | GithubPlatform = "github" 26 | GitlabPlatform = "gitlab" 27 | ) 28 | 29 | func FetchClientData(accessToken string, repoUrl string, scmPlatform string, branch string) (*checkmodels.AssetsData, []string, error) { 30 | host, orgName, repoName, err := getRepoInfo(repoUrl) 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | 35 | switch host { 36 | case GithubEndpoint: 37 | scmPlatform = GithubPlatform 38 | case GitlabEndpoint: 39 | scmPlatform = GitlabPlatform 40 | } 41 | 42 | adapter, err := getClientAdapter(scmPlatform, accessToken, host) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | authorizedUser, _ := adapter.GetAuthorizedUser() 47 | 48 | repo, _ := adapter.GetRepository(orgName, repoName, branch) 49 | logger.FetchingFinished("Repository Settings", emoji.OilDrum) 50 | 51 | var protection *models.Protection 52 | var pipelines []*pipelineModels.Pipeline 53 | var org *models.Organization 54 | var registry *models.PackageRegistry 55 | 56 | if repo != nil { 57 | branchName := utils.GetBranchName(utils.GetValue(repo.DefaultBranch), branch) 58 | 59 | logger.FetchingFinished("Branch Protection Settings", emoji.Seedling) 60 | protection, _ = adapter.GetBranchProtection(orgName, repo, branchName) 61 | 62 | pipelines, _ = adapter.GetPipelines(orgName, repoName, branchName) 63 | logger.FetchingFinished("Pipelines", emoji.Wrench) 64 | 65 | if *repo.Owner.Type == "Organization" { 66 | org, _ = adapter.GetOrganization(orgName) 67 | logger.FetchingFinished("Organization Settings", emoji.OfficeBuilding) 68 | 69 | registry, _ = adapter.GetRegistry(org) 70 | 71 | orgMembers, err := adapter.ListOrganizationMembers(orgName) 72 | if err == nil { 73 | org.Members = orgMembers 74 | logger.FetchingFinished("Members", emoji.Emoji(emoji.WomanAndManHoldingHands.Tone())) 75 | } 76 | } 77 | } 78 | 79 | checksIds, err := adapter.ListSupportedChecksIDs() 80 | 81 | return &checkmodels.AssetsData{ 82 | AuthorizedUser: authorizedUser, 83 | Organization: org, 84 | Repository: repo, 85 | BranchProtections: protection, 86 | Pipelines: pipelines, 87 | Registry: registry, 88 | }, checksIds, err 89 | } 90 | 91 | func getRepoInfo(repoFullUrl string) (string, string, string, error) { 92 | u, err := url.Parse(repoFullUrl) 93 | if err != nil || u.Scheme == "" { 94 | logger.Errorf(err, "error in parsing repoUrl %s", repoFullUrl) 95 | if err == nil { 96 | err = errors.New("error in parsing the host") 97 | } 98 | return "", "", "", err 99 | } 100 | 101 | path := strings.Split(u.EscapedPath(), "/") 102 | if len(path) < 3 { 103 | return "", "", "", fmt.Errorf("missing org/repo in the repository url: %s", repoFullUrl) 104 | } 105 | repo := path[len(path)-1] 106 | namespace := strings.Join(path[:len(path)-1], "/") 107 | trimedNamespace := strings.TrimLeft(namespace, "/") 108 | 109 | return u.Host, trimedNamespace, repo, nil 110 | } 111 | 112 | func getClientAdapter(scmPlatform string, accessToken string, host string) (adapter.ClientAdapter, error) { 113 | var err error 114 | var adapter adapter.ClientAdapter 115 | httpClient := utils.GetHttpClient(accessToken) 116 | 117 | switch scmPlatform { 118 | case GithubPlatform: 119 | err = github.Adapter.Init(httpClient, accessToken, host) 120 | adapter = &github.Adapter 121 | case GitlabPlatform: 122 | err = gitlab.Adapter.Init(httpClient, accessToken, host) 123 | adapter = &gitlab.Adapter 124 | default: 125 | adapter = nil 126 | } 127 | 128 | if err != nil { 129 | logger.Error(err, "error with SCM init client") 130 | return &github.ClientAdapterImpl{}, nil 131 | } 132 | return adapter, err 133 | } 134 | -------------------------------------------------------------------------------- /internal/scm-clients/clients/clients_test.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type RepoInfo struct { 11 | BaseUrl string 12 | Namespace string 13 | Project string 14 | } 15 | 16 | func TestGetRepoInfo(t *testing.T) { 17 | 18 | tests := []struct { 19 | Name string 20 | RepoUrl string 21 | ExpectedErr error 22 | Expected RepoInfo 23 | }{{ 24 | Name: "missing url schema", 25 | RepoUrl: "gitlab.com/rootgroup/subgroup/secondsubgroup/test", 26 | ExpectedErr: fmt.Errorf("error in parsing the host"), 27 | Expected: RepoInfo{BaseUrl: "", Namespace: "", Project: ""}, 28 | }, { 29 | Name: "invalid url", 30 | RepoUrl: "https://gitlab?com/rootgroup/subgroup/secondsubgroup/test", 31 | ExpectedErr: fmt.Errorf("missing org/repo in the repository url: %s", "https://gitlab?com/rootgroup/subgroup/secondsubgroup/test"), 32 | Expected: RepoInfo{BaseUrl: "", Namespace: "", Project: ""}, 33 | }, { 34 | Name: "github repo", 35 | RepoUrl: "https://github.com/aquasecurity/chain-bench", 36 | ExpectedErr: nil, 37 | Expected: RepoInfo{BaseUrl: "github.com", Namespace: "aquasecurity", Project: "chain-bench"}, 38 | }, { 39 | Name: "gitlab project under root group", 40 | RepoUrl: "https://gitlab.com/rootgroup/test", 41 | ExpectedErr: nil, 42 | Expected: RepoInfo{BaseUrl: "gitlab.com", Namespace: "rootgroup", Project: "test"}, 43 | }, { 44 | Name: "gitlab project under sub group", 45 | RepoUrl: "https://gitlab.com/rootgroup/subgroup/secondsubgroup/test", 46 | ExpectedErr: nil, 47 | Expected: RepoInfo{BaseUrl: "gitlab.com", Namespace: "rootgroup/subgroup/secondsubgroup", Project: "test"}, 48 | }, { 49 | Name: "gitlab project under sub group with same name as repo", 50 | RepoUrl: "https://gitlab.com/rootgroup/subgroup/secondsubgroup/secondsubgroup", 51 | ExpectedErr: nil, 52 | Expected: RepoInfo{BaseUrl: "gitlab.com", Namespace: "rootgroup/subgroup/secondsubgroup", Project: "secondsubgroup"}, 53 | }, { 54 | Name: "gitlab project under sub org with same name as repo", 55 | RepoUrl: "https://gitlab.com/codekuu/suborg/secondsuborg/secondsuborg", 56 | ExpectedErr: nil, 57 | Expected: RepoInfo{BaseUrl: "gitlab.com", Namespace: "codekuu/suborg/secondsuborg", Project: "secondsuborg"}, 58 | }} 59 | 60 | for _, test := range tests { 61 | test := test 62 | t.Run(test.Name, func(t *testing.T) { 63 | t.Parallel() 64 | 65 | baseUrl, namespace, project, err := getRepoInfo(test.RepoUrl) 66 | 67 | if test.ExpectedErr == nil { 68 | assert.NoError(t, err) 69 | } else { 70 | assert.EqualError(t, err, test.ExpectedErr.Error()) 71 | } 72 | assert.Equal(t, test.Expected.BaseUrl, baseUrl) 73 | assert.Equal(t, test.Expected.Namespace, namespace) 74 | assert.Equal(t, test.Expected.Project, project) 75 | }) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /internal/scm-clients/github/client_mocks.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "github.com/google/go-github/v41/github" 5 | "github.com/migueleliasweb/go-github-mock/src/mock" 6 | ) 7 | 8 | func MockGetRepo(repo *github.Repository) *GithubClient { 9 | mockedHTTPClient := mock.NewMockedHTTPClient( 10 | mock.WithRequestMatch( 11 | mock.GetReposByOwnerByRepo, 12 | *repo, 13 | ), 14 | ) 15 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 16 | return &gc 17 | } 18 | 19 | func MockGetBranchProtections(protection *github.Protection) *GithubClient { 20 | mockedHTTPClient := mock.NewMockedHTTPClient( 21 | mock.WithRequestMatch( 22 | mock.GetReposBranchesProtectionByOwnerByRepoByBranch, 23 | *protection, 24 | ), 25 | ) 26 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 27 | return &gc 28 | } 29 | 30 | func MockGetSignaturesOfProtectedBranch(signedCommits *github.SignaturesProtectedBranch) *GithubClient { 31 | mockedHTTPClient := mock.NewMockedHTTPClient( 32 | mock.WithRequestMatch( 33 | mock.GetReposBranchesProtectionRequiredSignaturesByOwnerByRepoByBranch, 34 | *signedCommits, 35 | ), 36 | ) 37 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 38 | return &gc 39 | } 40 | 41 | func MockGetOrganization(org *github.Organization) *GithubClient { 42 | mockedHTTPClient := mock.NewMockedHTTPClient( 43 | mock.WithRequestMatch( 44 | mock.GetOrgsByOrg, 45 | *org, 46 | ), 47 | ) 48 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 49 | return &gc 50 | } 51 | 52 | func MockGetOrganizationMembers(users []*github.User) *GithubClient { 53 | mockedHTTPClient := mock.NewMockedHTTPClient( 54 | mock.WithRequestMatch( 55 | mock.GetOrgsMembersByOrg, 56 | users, 57 | ), 58 | ) 59 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 60 | return &gc 61 | } 62 | 63 | func MockGetWorkflows(workflows *github.Workflows) *GithubClient { 64 | mockedHTTPClient := mock.NewMockedHTTPClient( 65 | mock.WithRequestMatch( 66 | mock.GetReposActionsWorkflowsByOwnerByRepo, 67 | workflows, 68 | ), 69 | ) 70 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 71 | return &gc 72 | } 73 | 74 | func MockGetContent(content *github.RepositoryContent) *GithubClient { 75 | mockedHTTPClient := mock.NewMockedHTTPClient( 76 | mock.WithRequestMatch( 77 | mock.GetReposContentsByOwnerByRepoByPath, 78 | content, 79 | ), 80 | ) 81 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 82 | return &gc 83 | } 84 | 85 | func MockGetOrganizationWebhooks(hooks []*github.Hook) *GithubClient { 86 | mockedHTTPClient := mock.NewMockedHTTPClient( 87 | mock.WithRequestMatch( 88 | mock.GetOrgsHooksByOrg, 89 | hooks, 90 | ), 91 | ) 92 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 93 | return &gc 94 | } 95 | 96 | func MockGetRepositoryWebhooks(hooks []*github.Hook) *GithubClient { 97 | mockedHTTPClient := mock.NewMockedHTTPClient( 98 | mock.WithRequestMatch( 99 | mock.GetReposHooksByOwnerByRepo, 100 | hooks, 101 | ), 102 | ) 103 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 104 | return &gc 105 | } 106 | 107 | func MockListCommits(commits []*github.RepositoryCommit) *GithubClient { 108 | mockedHTTPClient := mock.NewMockedHTTPClient( 109 | mock.WithRequestMatch( 110 | mock.GetReposCommitsByOwnerByRepo, 111 | commits, 112 | ), 113 | ) 114 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 115 | return &gc 116 | } 117 | 118 | func MockListOrganizationPackages(packages []*github.Package) *GithubClient { 119 | mockedHTTPClient := mock.NewMockedHTTPClient( 120 | mock.WithRequestMatch( 121 | mock.GetOrgsPackagesByOrg, 122 | packages, 123 | ), 124 | ) 125 | gc, _ := InitClient(mockedHTTPClient, "", "github.com") 126 | return &gc 127 | } 128 | -------------------------------------------------------------------------------- /internal/scm-clients/gitlab/adapter.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/logger" 8 | "github.com/aquasecurity/chain-bench/internal/models" 9 | "github.com/aquasecurity/chain-bench/internal/scm-clients/adapter" 10 | pipelineModels "github.com/argonsecurity/pipeline-parser/pkg/models" 11 | "github.com/xanzy/go-gitlab" 12 | ) 13 | 14 | var ( 15 | Adapter ClientAdapterImpl 16 | ) 17 | 18 | type ClientAdapterImpl struct { 19 | client GitlabClient 20 | } 21 | 22 | func (*ClientAdapterImpl) Init(client *http.Client, token string, host string) error { 23 | glClient, err := InitClient(client, token, host) 24 | Adapter = ClientAdapterImpl{client: glClient} 25 | return err 26 | } 27 | 28 | func (ca *ClientAdapterImpl) GetAuthorizedUser() (*models.User, error) { 29 | res, _, err := ca.client.GetAuthorizedUser() 30 | if err != nil { 31 | logger.Error(err, "error in authenticated user data") 32 | return nil, err 33 | } 34 | 35 | return toUser(res), nil 36 | } 37 | 38 | // GetRepository implements clients.ClientAdapter 39 | func (ca *ClientAdapterImpl) GetRepository(owner string, repo string, branch string) (*models.Repository, error) { 40 | rep, _, err := ca.client.GetRepository(owner, repo) 41 | if err != nil { 42 | logger.Error(err, "error in fetching repository data") 43 | return nil, err 44 | } 45 | 46 | // TODO: ListCommits 47 | 48 | branches, err := ca.ListRepositoryBranches(owner, strconv.Itoa(rep.ID)) 49 | if err != nil { 50 | logger.WarnE(err, "failed to fetch branches data") 51 | } 52 | 53 | // TODO: isRepoContainsSecurityMD 54 | 55 | // TODO: ListRepositoryCollaborators 56 | 57 | // TODO: ListRepositoryHooks 58 | 59 | return toRepository(rep, branches, nil, nil, nil, false), nil 60 | } 61 | 62 | // listRepositoryBranches implements clients.ClientAdapter 63 | func (ca *ClientAdapterImpl) ListRepositoryBranches(owner string, repo string) ([]*models.Branch, error) { 64 | branches, _, err := ca.client.ListRepositoryBranches(owner, repo) 65 | if err != nil { 66 | logger.Error(err, "error in fetching branches") 67 | return nil, err 68 | } 69 | enhancedBranches := []*gitlab.Branch{} 70 | 71 | for _, b := range branches { 72 | //TODO: GetCommit 73 | if err != nil { 74 | logger.WarnE(err, "failed to fetch branches commit") 75 | } else { 76 | branch := &gitlab.Branch{ 77 | Name: b.Name, 78 | Commit: b.Commit, 79 | Protected: b.Protected, 80 | } 81 | enhancedBranches = append(enhancedBranches, branch) 82 | } 83 | } 84 | return toBranches(enhancedBranches), nil 85 | } 86 | 87 | func (ca *ClientAdapterImpl) GetOrganization(owner string) (*models.Organization, error) { 88 | org, _, err := ca.client.GetOrganization(owner) 89 | if err != nil { 90 | logger.Error(err, "error in fetching organization") 91 | return nil, err 92 | } 93 | 94 | return toOrganization(org, nil), nil 95 | } 96 | 97 | // GetBranchProtection implements clients.ClientAdapter 98 | func (ca *ClientAdapterImpl) GetBranchProtection(owner string, repo *models.Repository, branch string) (*models.Protection, error) { 99 | projectId := strconv.Itoa(int(*repo.ID)) 100 | prot, _, err := ca.client.GetBranchProtection(owner, projectId, branch) 101 | if err != nil { 102 | logger.Error(err, "error in fetching branch protection") 103 | return nil, err 104 | } 105 | 106 | appConf, _, err := ca.client.GetApprovalConfiguration(projectId) 107 | if err != nil { 108 | logger.WarnE(err, "failed to fetch approval configuration") 109 | } 110 | 111 | pushRules, _, err := ca.client.GetProjectPushRules(projectId) 112 | if err != nil { 113 | logger.WarnE(err, "failed to fetch approval configuration") 114 | } 115 | 116 | appRules, _, err := ca.client.GetProjectApprovalRules(projectId) 117 | if err != nil { 118 | logger.WarnE(err, "failed to fetch approval rules") 119 | } 120 | 121 | proj, _, err := ca.client.GetRepository(owner, *repo.Name) 122 | if err != nil { 123 | logger.WarnE(err, "failed to fetch project") 124 | } 125 | return toBranchProtection(proj, prot, appConf, appRules, pushRules), nil 126 | } 127 | 128 | func (ca *ClientAdapterImpl) ListOrganizationMembers(organization string) ([]*models.User, error) { 129 | // TODO 130 | return nil, nil 131 | } 132 | 133 | func (ca *ClientAdapterImpl) GetRegistry(organization *models.Organization) (*models.PackageRegistry, error) { 134 | //TODO 135 | return nil, nil 136 | } 137 | 138 | func (ca *ClientAdapterImpl) GetPipelines(owner string, repo string, branch string) ([]*pipelineModels.Pipeline, error) { 139 | //TODO 140 | return nil, nil 141 | } 142 | 143 | func (ca *ClientAdapterImpl) ListSupportedChecksIDs() ([]string, error) { 144 | return []string{"1.1.3", "1.1.4", "1.1.6", "1.1.11", "1.1.12", "1.1.13", "1.1.16", "1.3.5", "1.3.8"}, nil 145 | } 146 | 147 | var _ adapter.ClientAdapter = (*ClientAdapterImpl)(nil) // Verify that *ClientAdapterImpl implements ClientAdapter. 148 | -------------------------------------------------------------------------------- /internal/scm-clients/gitlab/gitlab.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/xanzy/go-gitlab" 9 | ) 10 | 11 | var ( 12 | Client GitlabClient 13 | ) 14 | 15 | type GitlabClient interface { 16 | GetAuthorizedUser() (*gitlab.User, *gitlab.Response, error) 17 | ListRepositoryBranches(org string, repoId string) ([]*gitlab.Branch, *gitlab.Response, error) 18 | GetBranchProtection(owner string, repo string, branch string) (*gitlab.ProtectedBranch, *gitlab.Response, error) 19 | GetApprovalConfiguration(project string) (*gitlab.ProjectApprovals, *gitlab.Response, error) 20 | GetProjectApprovalRules(project string) ([]*gitlab.ProjectApprovalRule, *gitlab.Response, error) 21 | GetProjectPushRules(project string) (*gitlab.ProjectPushRules, *gitlab.Response, error) 22 | GetRepository(owner, repo string) (*gitlab.Project, *gitlab.Response, error) 23 | GetOrganization(owner string) (*gitlab.Group, *gitlab.Response, error) 24 | } 25 | 26 | type GitlabClientImpl struct { 27 | ctx context.Context 28 | client *gitlab.Client 29 | } 30 | 31 | var _ GitlabClient = (*GitlabClientImpl)(nil) // Verify that *GitlabClientImpl implements gitlabClient. 32 | 33 | func InitClient(client *http.Client, token string, host string) (GitlabClient, error) { 34 | var gc *gitlab.Client 35 | if host == "gitlab.com" { 36 | gc, _ = gitlab.NewClient(token, gitlab.WithHTTPClient(client)) 37 | } else { 38 | gc, _ = gitlab.NewClient(token, gitlab.WithHTTPClient(client), gitlab.WithBaseURL(fmt.Sprintf("https://%s/api/v4", host))) 39 | } 40 | Client = &GitlabClientImpl{ctx: context.TODO(), client: gc} 41 | return Client, nil 42 | } 43 | func (gca *GitlabClientImpl) GetAuthorizedUser() (*gitlab.User, *gitlab.Response, error) { 44 | return gca.client.Users.GetUser(1, gitlab.GetUsersOptions{}) 45 | } 46 | 47 | func (gca *GitlabClientImpl) ListRepositoryBranches(org string, repoId string) ([]*gitlab.Branch, *gitlab.Response, error) { 48 | return gca.client.Branches.ListBranches(repoId, &gitlab.ListBranchesOptions{}) 49 | } 50 | 51 | func (gca *GitlabClientImpl) GetBranchProtection(organization, repo, branch string) (*gitlab.ProtectedBranch, *gitlab.Response, error) { 52 | return gca.client.ProtectedBranches.GetProtectedBranch(repo, branch) 53 | } 54 | 55 | func (gca *GitlabClientImpl) GetApprovalConfiguration(project string) (*gitlab.ProjectApprovals, *gitlab.Response, error) { 56 | return gca.client.Projects.GetApprovalConfiguration(project) 57 | } 58 | 59 | func (gca *GitlabClientImpl) GetProjectApprovalRules(project string) ([]*gitlab.ProjectApprovalRule, *gitlab.Response, error) { 60 | return gca.client.Projects.GetProjectApprovalRules(project) 61 | } 62 | 63 | func (gca *GitlabClientImpl) GetProjectPushRules(project string) (*gitlab.ProjectPushRules, *gitlab.Response, error) { 64 | return gca.client.Projects.GetProjectPushRules(project) 65 | } 66 | 67 | func (gca *GitlabClientImpl) GetRepository(owner, repo string) (*gitlab.Project, *gitlab.Response, error) { 68 | return gca.client.Projects.GetProject(fmt.Sprintf("%s/%s", owner, repo), &gitlab.GetProjectOptions{}) 69 | } 70 | 71 | func (gca *GitlabClientImpl) GetOrganization(owner string) (*gitlab.Group, *gitlab.Response, error) { 72 | return gca.client.Groups.GetGroup(owner, &gitlab.GetGroupOptions{}) 73 | } 74 | -------------------------------------------------------------------------------- /internal/scm-clients/gitlab/mapper_test.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/aquasecurity/chain-bench/internal/models" 8 | "github.com/aquasecurity/chain-bench/internal/utils" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/xanzy/go-gitlab" 11 | ) 12 | 13 | func TestToRepository(t *testing.T) { 14 | repoName := "myrepo" 15 | time := time.Now() 16 | excepted := models.Repository{ 17 | Name: &repoName, 18 | ID: utils.GetPtr(int64(433)), 19 | Description: utils.GetPtr(""), 20 | DefaultBranch: utils.GetPtr(""), 21 | Owner: &models.User{ 22 | Type: utils.GetPtr("Organization")}, 23 | OpenIssuesCount: utils.GetPtr(0), 24 | StargazersCount: utils.GetPtr(0), 25 | AllowRebaseMerge: utils.GetPtr(false), 26 | AllowSquashMerge: utils.GetPtr(false), 27 | AllowMergeCommit: utils.GetPtr(false), 28 | IsPrivate: utils.GetPtr(true), 29 | HasIssues: utils.GetPtr(false), 30 | Archived: utils.GetPtr(false), 31 | URL: utils.GetPtr(""), 32 | CreatedAt: &utils.Timestamp{}, 33 | PushedAt: &utils.Timestamp{}, 34 | UpdatedAt: &utils.Timestamp{}, 35 | Branches: []*models.Branch{ 36 | { 37 | Commit: &models.RepositoryCommit{ 38 | Committer: &models.CommitAuthor{ 39 | Date: utils.GetPtr(time), 40 | }, 41 | }, 42 | }, 43 | }, 44 | Commits: []*models.RepositoryCommit{ 45 | { 46 | Committer: &models.CommitAuthor{ 47 | Date: utils.GetPtr(time), 48 | }, 49 | }, 50 | }, 51 | Hooks: []*models.Hook{{URL: utils.GetPtr("https://example.com")}}, 52 | Collaborators: []*models.User{}, 53 | IsContainsSecurityMd: true, 54 | } 55 | 56 | actual := toRepository( 57 | &gitlab.Project{ 58 | Path: repoName, 59 | ID: 433, 60 | Owner: &gitlab.User{}, 61 | }, 62 | []*models.Branch{ 63 | { 64 | Commit: &models.RepositoryCommit{ 65 | Committer: &models.CommitAuthor{ 66 | Date: utils.GetPtr(time), 67 | }, 68 | }, 69 | }, 70 | }, 71 | []*models.User{}, 72 | []*models.Hook{{URL: utils.GetPtr("https://example.com")}}, 73 | []*models.RepositoryCommit{ 74 | { 75 | Committer: &models.CommitAuthor{ 76 | Date: utils.GetPtr(time), 77 | }, 78 | }, 79 | }, 80 | true, 81 | ) 82 | 83 | assert.Equal(t, excepted, *actual) 84 | } 85 | 86 | func TestToOrganization(t *testing.T) { 87 | orgName := "org1" 88 | 89 | excepted := models.Organization{Name: &orgName, 90 | IsRepositoryDeletionLimited: utils.GetPtr(false), 91 | IsIssueDeletionLimited: utils.GetPtr(false), 92 | ID: utils.GetPtr(int64(33432)), 93 | MembersCanCreateRepos: utils.GetPtr(false), 94 | Type: utils.GetPtr("Organization"), 95 | DefaultRepoPermission: utils.GetPtr("inherit"), 96 | Description: utils.GetPtr(""), 97 | TwoFactorRequirementEnabled: utils.GetPtr(false), 98 | Hooks: []*models.Hook{{URL: utils.GetPtr("https://example.com")}}} 99 | 100 | actual := toOrganization(&gitlab.Group{ID: 33432, Name: orgName, ProjectCreationLevel: gitlab.NoOneProjectCreation}, []*models.Hook{{URL: utils.GetPtr("https://example.com")}}) 101 | 102 | assert.Equal(t, excepted, *actual) 103 | } 104 | func TestToUsers(t *testing.T) { 105 | 106 | login := "liorvais" 107 | var excepted = models.User{Login: &login, 108 | ID: utils.GetPtr(int64(213)), 109 | Type: utils.GetPtr("User"), 110 | CreatedAt: &utils.Timestamp{}, 111 | HTMLURL: utils.GetPtr(""), 112 | AvatarURL: utils.GetPtr(""), 113 | Name: utils.GetPtr(""), 114 | Location: utils.GetPtr(""), 115 | Email: utils.GetPtr(""), 116 | Bio: utils.GetPtr(""), 117 | SiteAdmin: utils.GetPtr(false), 118 | URL: utils.GetPtr("")} 119 | 120 | actual := toUsers([]*gitlab.User{{Username: login, ID: 213}}) 121 | 122 | assert.Equal(t, excepted, *actual[0]) 123 | } 124 | 125 | func TestToBranchProtection(t *testing.T) { 126 | excepted := models.Protection{ 127 | AllowForcePushes: false, 128 | RequiredPullRequestReviews: &models.PullRequestReviewsEnforcement{ 129 | DismissStaleReviews: true, RequiredApprovingReviewCount: 3}, 130 | RequiredSignedCommit: true, EnforceAdmins: &models.AdminEnforcement{}} 131 | actual := toBranchProtection(&gitlab.Project{DefaultBranch: "main"}, &gitlab.ProtectedBranch{AllowForcePush: false}, &gitlab.ProjectApprovals{ResetApprovalsOnPush: true}, []*gitlab.ProjectApprovalRule{{ApprovalsRequired: 3}}, &gitlab.ProjectPushRules{RejectUnsignedCommits: true}) 132 | 133 | assert.Equal(t, excepted, *actual) 134 | } 135 | -------------------------------------------------------------------------------- /internal/testutils/builders/assets_data_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/models" 5 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 6 | "github.com/aquasecurity/chain-bench/internal/testutils" 7 | "github.com/aquasecurity/chain-bench/internal/utils" 8 | pipelineParserModels "github.com/argonsecurity/pipeline-parser/pkg/models" 9 | ) 10 | 11 | type AssetsDataBuilder struct { 12 | assetsData *checkmodels.AssetsData 13 | } 14 | 15 | func NewAssetsDataBuilder() *AssetsDataBuilder { 16 | return &AssetsDataBuilder{assetsData: &checkmodels.AssetsData{ 17 | Organization: NewOrganizationBuilder().Build(), 18 | Repository: NewRepositoryBuilder().Build(), 19 | BranchProtections: NewBranchProtectionBuilder().Build(), 20 | AuthorizedUser: &models.User{ID: utils.GetPtr(testutils.AuthorizedUserMockId)}, 21 | Pipelines: []*pipelineParserModels.Pipeline{NewPipelineBuilder().Build()}, 22 | Registry: &models.PackageRegistry{Packages: []*models.Package{}}, 23 | }} 24 | } 25 | 26 | func (b *AssetsDataBuilder) WithRepository(repo *models.Repository) *AssetsDataBuilder { 27 | b.assetsData.Repository = repo 28 | return b 29 | } 30 | 31 | func (b *AssetsDataBuilder) WithAuthorizedUser() *AssetsDataBuilder { 32 | b.assetsData.AuthorizedUser = &models.User{ID: utils.GetPtr(testutils.AuthorizedUserMockId)} 33 | return b 34 | } 35 | 36 | func (b *AssetsDataBuilder) WithOrganization(org *models.Organization) *AssetsDataBuilder { 37 | b.assetsData.Organization = org 38 | return b 39 | } 40 | 41 | func (b *AssetsDataBuilder) WithUsers(users []*models.User) *AssetsDataBuilder { 42 | b.assetsData.Users = users 43 | return b 44 | } 45 | 46 | func (b *AssetsDataBuilder) WithBranchProtections(bp *models.Protection) *AssetsDataBuilder { 47 | b.assetsData.BranchProtections = bp 48 | return b 49 | } 50 | 51 | func (b *AssetsDataBuilder) WithPackageRegistry(bp *models.PackageRegistry) *AssetsDataBuilder { 52 | b.assetsData.Registry = bp 53 | return b 54 | } 55 | 56 | func (b *AssetsDataBuilder) WithPipeline(pipeline *pipelineParserModels.Pipeline) *AssetsDataBuilder { 57 | if b.assetsData.Pipelines == nil { 58 | b.assetsData.Pipelines = []*pipelineParserModels.Pipeline{} 59 | } 60 | b.assetsData.Pipelines = append(b.assetsData.Pipelines, pipeline) 61 | return b 62 | } 63 | 64 | func (b *AssetsDataBuilder) WithZeroPipelines() *AssetsDataBuilder { 65 | b.assetsData.Pipelines = []*pipelineParserModels.Pipeline{} 66 | return b 67 | } 68 | 69 | func (b *AssetsDataBuilder) WithNoPipelinesData() *AssetsDataBuilder { 70 | b.assetsData.Pipelines = nil 71 | return b 72 | } 73 | 74 | func (b *AssetsDataBuilder) WithNoRepositoryData() *AssetsDataBuilder { 75 | b.assetsData.Repository = nil 76 | return b 77 | } 78 | 79 | func (b *AssetsDataBuilder) WithNoRegistryData() *AssetsDataBuilder { 80 | b.assetsData.Registry = nil 81 | return b 82 | } 83 | 84 | func (b *AssetsDataBuilder) WithNoOrganization() *AssetsDataBuilder { 85 | b.assetsData.Organization = nil 86 | return b 87 | } 88 | 89 | func (b *AssetsDataBuilder) Build() *checkmodels.AssetsData { 90 | return b.assetsData 91 | } 92 | 93 | func (b *AssetsDataBuilder) WithBranch(branch *models.Branch) *AssetsDataBuilder { 94 | b.assetsData.Repository.Branches = append(b.assetsData.Repository.Branches, branch) 95 | return b 96 | } 97 | -------------------------------------------------------------------------------- /internal/testutils/builders/branch_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/models" 7 | "github.com/aquasecurity/chain-bench/internal/utils" 8 | ) 9 | 10 | type BranchBuilder struct { 11 | branch *models.Branch 12 | } 13 | 14 | func NewBranchBuilder() *BranchBuilder { 15 | return &BranchBuilder{branch: &models.Branch{ 16 | Commit: &models.RepositoryCommit{ 17 | SHA: utils.GetPtr("GD2"), 18 | Author: &models.CommitAuthor{}, 19 | Committer: &models.CommitAuthor{Date: utils.GetPtr(time.Now())}, 20 | Verification: &models.SignatureVerification{}, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | func (b *BranchBuilder) WithOldCommit() *BranchBuilder { 27 | b.branch.Commit.Committer.Date = utils.GetPtr(time.Now().AddDate(0, -3, 0)) 28 | return b 29 | } 30 | 31 | func (b *BranchBuilder) Build() *models.Branch { 32 | return b.branch 33 | } 34 | -------------------------------------------------------------------------------- /internal/testutils/builders/branch_protection_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/models" 5 | "github.com/aquasecurity/chain-bench/internal/utils" 6 | ) 7 | 8 | type BranchProtectionBuilder struct { 9 | br_protection *models.Protection 10 | } 11 | 12 | func NewBranchProtectionBuilder() *BranchProtectionBuilder { 13 | return &BranchProtectionBuilder{br_protection: &models.Protection{ 14 | EnforceAdmins: &models.AdminEnforcement{ 15 | Enabled: true, 16 | }, 17 | RequiredStatusChecks: &models.RequiredStatusChecks{Strict: true}, 18 | RequiredPullRequestReviews: &models.PullRequestReviewsEnforcement{ 19 | RequiredApprovingReviewCount: 2, 20 | RequireCodeOwnerReviews: true, 21 | DismissStaleReviews: true, 22 | DismissalRestrictions: &models.DismissalRestrictions{Users: []*models.User{}}, 23 | }, 24 | Restrictions: &models.BranchRestrictions{Users: []*models.User{{Name: utils.GetPtr("default")}}, Teams: []*models.Team{}, Apps: []*models.App{}}, 25 | RequiredSignedCommit: true, 26 | RequiredConversationResolution: true, 27 | AllowForcePushes: true, 28 | AllowDeletions: true, 29 | }} 30 | } 31 | 32 | func (b *BranchProtectionBuilder) WithStatusCheckEnabled(enabled bool) *BranchProtectionBuilder { 33 | if !enabled { 34 | b.br_protection.RequiredStatusChecks = nil 35 | } 36 | return b 37 | } 38 | 39 | func (b *BranchProtectionBuilder) WithStrictMode(enabled bool) *BranchProtectionBuilder { 40 | if b.br_protection.RequiredStatusChecks != nil { 41 | b.br_protection.RequiredStatusChecks.Strict = enabled 42 | } 43 | return b 44 | } 45 | 46 | func (b *BranchProtectionBuilder) WithMinimumReviwersBeforeMerge(num int) *BranchProtectionBuilder { 47 | b.br_protection.RequiredPullRequestReviews.RequiredApprovingReviewCount = num 48 | return b 49 | } 50 | 51 | func (b *BranchProtectionBuilder) WithCodeOwnersReview(requireCodeOwnerReviews bool) *BranchProtectionBuilder { 52 | b.br_protection.RequiredPullRequestReviews.RequireCodeOwnerReviews = requireCodeOwnerReviews 53 | return b 54 | } 55 | 56 | func (b *BranchProtectionBuilder) WithDismissStaleReviews(dismissStaleReviews bool) *BranchProtectionBuilder { 57 | b.br_protection.RequiredPullRequestReviews.DismissStaleReviews = dismissStaleReviews 58 | return b 59 | } 60 | 61 | func (b *BranchProtectionBuilder) WithDismissalRestrictions(dismissalRestrictions bool) *BranchProtectionBuilder { 62 | if !dismissalRestrictions { 63 | b.br_protection.RequiredPullRequestReviews.DismissalRestrictions = nil 64 | } 65 | return b 66 | } 67 | 68 | func (b *BranchProtectionBuilder) WithRequiredSignedCommits(enable bool) *BranchProtectionBuilder { 69 | b.br_protection.RequiredSignedCommit = enable 70 | return b 71 | } 72 | 73 | func (b *BranchProtectionBuilder) WithResolveConversations(resolveConversion bool) *BranchProtectionBuilder { 74 | b.br_protection.RequiredConversationResolution = resolveConversion 75 | return b 76 | } 77 | 78 | func (b *BranchProtectionBuilder) WithEnforceAdmin(enforceAdmin bool) *BranchProtectionBuilder { 79 | b.br_protection.EnforceAdmins = &models.AdminEnforcement{Enabled: enforceAdmin} 80 | return b 81 | } 82 | 83 | func (b *BranchProtectionBuilder) WithPushRestrictions(pushRestriction bool) *BranchProtectionBuilder { 84 | if !pushRestriction { 85 | b.br_protection.Restrictions = nil 86 | } 87 | return b 88 | } 89 | 90 | func (b *BranchProtectionBuilder) WithDeleteBranch(deleteBranch bool) *BranchProtectionBuilder { 91 | b.br_protection.AllowDeletions = deleteBranch 92 | return b 93 | } 94 | 95 | func (b *BranchProtectionBuilder) WithForcePush(forcePush bool) *BranchProtectionBuilder { 96 | b.br_protection.AllowForcePushes = forcePush 97 | return b 98 | } 99 | 100 | func (b *BranchProtectionBuilder) Build() *models.Protection { 101 | return b.br_protection 102 | } 103 | -------------------------------------------------------------------------------- /internal/testutils/builders/job_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/testutils" 5 | "github.com/aquasecurity/chain-bench/internal/utils" 6 | "github.com/argonsecurity/pipeline-parser/pkg/models" 7 | ) 8 | 9 | type JobBuilder struct { 10 | job models.Job 11 | } 12 | 13 | func NewJobBuilder() *JobBuilder { 14 | return &JobBuilder{job: models.Job{ 15 | Steps: []*models.Step{{ 16 | Name: utils.GetPtr(testutils.ArgonScannerAction), 17 | Type: "task", 18 | Task: &models.Task{ 19 | Name: utils.GetPtr(testutils.ArgonScannerAction), 20 | VersionType: models.VersionType("commit"), 21 | }, 22 | }, { 23 | Name: utils.GetPtr(testutils.SbomTask), 24 | Type: "task", 25 | Task: &models.Task{ 26 | Name: utils.GetPtr(testutils.SbomTask), 27 | VersionType: models.VersionType("commit"), 28 | }, 29 | }}, 30 | Metadata: models.Metadata{Build: true}, 31 | }, 32 | } 33 | } 34 | 35 | func (j *JobBuilder) WithTask(name, versionType string) *JobBuilder { 36 | j.appendStep(models.Step{ 37 | Name: utils.GetPtr(name), 38 | Type: "task", 39 | Task: &models.Task{ 40 | Name: utils.GetPtr(name), 41 | VersionType: models.VersionType(versionType), 42 | }, 43 | }) 44 | return j 45 | } 46 | 47 | func (j *JobBuilder) WithNoVulnerabilityScannerTask() *JobBuilder { 48 | var newStepsList = []*models.Step{} 49 | 50 | for _, s := range j.job.Steps { 51 | if utils.GetValue(s.Name) != testutils.ArgonScannerAction && 52 | utils.GetValue(s.Name) != testutils.TrivyScannerAction { 53 | newStepsList = append(newStepsList, s) 54 | } 55 | } 56 | 57 | j.job.Steps = newStepsList 58 | return j 59 | } 60 | 61 | func (j *JobBuilder) WithShellCommand(name string, command string) *JobBuilder { 62 | j.appendStep(models.Step{ 63 | Name: utils.GetPtr(name), 64 | Type: "shell", 65 | Shell: &models.Shell{ 66 | Script: utils.GetPtr(command), 67 | }, 68 | }) 69 | return j 70 | } 71 | 72 | func (j *JobBuilder) SetAsBuildJob(buildJob bool) *JobBuilder { 73 | j.job.Metadata.Build = buildJob 74 | return j 75 | } 76 | 77 | func (j *JobBuilder) WithNoTasks() *JobBuilder { 78 | j.job.Steps = []*models.Step{} 79 | return j 80 | } 81 | 82 | func (j *JobBuilder) appendStep(step models.Step) *JobBuilder { 83 | steps := j.job.Steps 84 | if steps == nil { 85 | steps = make([]*models.Step, 0) 86 | } 87 | j.job.Steps = append(steps, &step) 88 | return j 89 | } 90 | 91 | func (p *JobBuilder) Build() models.Job { 92 | return p.job 93 | } 94 | -------------------------------------------------------------------------------- /internal/testutils/builders/organization_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/models" 7 | "github.com/aquasecurity/chain-bench/internal/utils" 8 | ) 9 | 10 | type OrganizationBuilder struct { 11 | org *models.Organization 12 | } 13 | 14 | func NewOrganizationBuilder() *OrganizationBuilder { 15 | return &OrganizationBuilder{org: &models.Organization{ 16 | TwoFactorRequirementEnabled: utils.GetPtr(true), 17 | IsVerified: utils.GetPtr(true), 18 | DefaultRepoPermission: utils.GetPtr("read"), 19 | MembersCanCreateRepos: utils.GetPtr(false), 20 | IsRepositoryDeletionLimited: utils.GetPtr(true), 21 | IsIssueDeletionLimited: utils.GetPtr(true), 22 | Hooks: []*models.Hook{{ 23 | URL: utils.GetPtr("https://endpoint.com"), 24 | Config: &models.HookConfig{ 25 | URL: utils.GetPtr("https://endpoint.com"), 26 | Insecure_SSL: utils.GetPtr("0"), 27 | Secret: utils.GetPtr("**"), 28 | }, 29 | Events: []string{"package"}, 30 | }}, 31 | Members: []*models.User{{Role: "admin", Login: utils.GetPtr("user0")}, 32 | {Role: "admin", Login: utils.GetPtr("user1")}, 33 | {Role: "member", Login: utils.GetPtr("user2")}, 34 | {Role: "member", Login: utils.GetPtr("user3")}}}, 35 | } 36 | } 37 | 38 | func (b *OrganizationBuilder) WithMFAEnabled(enable bool) *OrganizationBuilder { 39 | b.org.TwoFactorRequirementEnabled = utils.GetPtr(enable) 40 | return b 41 | } 42 | 43 | func (b *OrganizationBuilder) WithVerifiedBadge(enable bool) *OrganizationBuilder { 44 | b.org.IsVerified = utils.GetPtr(enable) 45 | return b 46 | } 47 | 48 | func (b *OrganizationBuilder) WithReposDefaultPermissions(defaultPermissions string) *OrganizationBuilder { 49 | if defaultPermissions == "" { 50 | b.org.DefaultRepoPermission = nil 51 | } else { 52 | b.org.DefaultRepoPermission = utils.GetPtr(defaultPermissions) 53 | } 54 | return b 55 | } 56 | 57 | func (b *OrganizationBuilder) WithMembersCanCreateRepos(membersCanCreateRepos bool) *OrganizationBuilder { 58 | b.org.MembersCanCreateRepos = utils.GetPtr(membersCanCreateRepos) 59 | return b 60 | } 61 | 62 | func (b *OrganizationBuilder) WithReposDeletionLimitation(repoDeletionLimitation bool) *OrganizationBuilder { 63 | b.org.IsRepositoryDeletionLimited = utils.GetPtr(repoDeletionLimitation) 64 | return b 65 | } 66 | 67 | func (b *OrganizationBuilder) WithIssuesDeletionLimitation(issueDeletionLimitation bool) *OrganizationBuilder { 68 | b.org.IsIssueDeletionLimited = utils.GetPtr(issueDeletionLimitation) 69 | return b 70 | } 71 | 72 | func (b *OrganizationBuilder) WithMembers(role string, num int) *OrganizationBuilder { 73 | var newMembers []*models.User 74 | for i := 0; i < num; i++ { 75 | login := fmt.Sprintf("user%d", i) 76 | newMembers = append(newMembers, &models.User{Role: role, Login: &login}) 77 | } 78 | b.org.Members = newMembers 79 | return b 80 | } 81 | 82 | func (b *OrganizationBuilder) WithPackageWebHooks(url string, is_ssl string, secret *string) *OrganizationBuilder { 83 | b.org.Hooks = []*models.Hook{{ 84 | URL: &url, 85 | Config: &models.HookConfig{ 86 | URL: utils.GetPtr(url), 87 | Insecure_SSL: utils.GetPtr(is_ssl), 88 | Secret: secret, 89 | }, 90 | Events: []string{"package"}, 91 | }} 92 | return b 93 | } 94 | 95 | func (b *OrganizationBuilder) WithNoPackageWebHooks() *OrganizationBuilder { 96 | b.org.Hooks = nil 97 | return b 98 | } 99 | 100 | func (b *OrganizationBuilder) WithNoMembers() *OrganizationBuilder { 101 | b.org.Members = nil 102 | return b 103 | } 104 | 105 | func (b *OrganizationBuilder) Build() *models.Organization { 106 | return b.org 107 | } 108 | -------------------------------------------------------------------------------- /internal/testutils/builders/package_registry_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/models" 5 | "github.com/aquasecurity/chain-bench/internal/utils" 6 | ) 7 | 8 | type PackageRegistryBuilder struct { 9 | registry *models.PackageRegistry 10 | } 11 | 12 | func NewRegistryBuilder() *PackageRegistryBuilder { 13 | return &PackageRegistryBuilder{registry: &models.PackageRegistry{ 14 | TwoFactorRequirementEnabled: utils.GetPtr(true), 15 | Packages: []*models.Package{{ 16 | PackageType: utils.GetPtr("npm"), 17 | Visibility: utils.GetPtr("private"), 18 | Repository: &models.Repository{IsPrivate: utils.GetPtr(true)}, 19 | }}, 20 | }} 21 | } 22 | 23 | func (p *PackageRegistryBuilder) WithTwoFactorAuthenticationEnabled(enabled bool) *PackageRegistryBuilder { 24 | p.registry.TwoFactorRequirementEnabled = utils.GetPtr(enabled) 25 | return p 26 | } 27 | 28 | func (p *PackageRegistryBuilder) WithPackages(packagetype string, visability string, isRepoPrivate bool, repoID int64) *PackageRegistryBuilder { 29 | pkg := &models.Package{ 30 | PackageType: utils.GetPtr(packagetype), 31 | Visibility: utils.GetPtr(visability), 32 | Repository: &models.Repository{ID: utils.GetPtr(repoID), IsPrivate: utils.GetPtr(isRepoPrivate)}} 33 | 34 | if p.registry.Packages == nil { 35 | p.registry.Packages = []*models.Package{pkg} 36 | } else { 37 | p.registry.Packages = append(p.registry.Packages, pkg) 38 | } 39 | return p 40 | } 41 | 42 | func (p *PackageRegistryBuilder) WithNoPackages() *PackageRegistryBuilder { 43 | p.registry.Packages = nil 44 | return p 45 | } 46 | 47 | func (p *PackageRegistryBuilder) Build() *models.PackageRegistry { 48 | return p.registry 49 | } 50 | -------------------------------------------------------------------------------- /internal/testutils/builders/pipeline_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/utils" 5 | "github.com/argonsecurity/pipeline-parser/pkg/models" 6 | ) 7 | 8 | type PipelineBuilder struct { 9 | pipeline *models.Pipeline 10 | } 11 | 12 | func NewPipelineBuilder() *PipelineBuilder { 13 | return &PipelineBuilder{pipeline: &models.Pipeline{ 14 | Jobs: []*models.Job{utils.GetPtr(NewJobBuilder().Build())}, 15 | }} 16 | } 17 | 18 | func (p *PipelineBuilder) WithNoJobs() *PipelineBuilder { 19 | p.pipeline.Jobs = make([]*models.Job, 0) 20 | return p 21 | } 22 | 23 | func (p *PipelineBuilder) WithJob(job models.Job) *PipelineBuilder { 24 | if p.pipeline.Jobs == nil { 25 | p.pipeline.Jobs = make([]*models.Job, 0) 26 | } 27 | p.pipeline.Jobs = append(p.pipeline.Jobs, &job) 28 | return p 29 | } 30 | 31 | func (p *PipelineBuilder) Build() *models.Pipeline { 32 | return p.pipeline 33 | } 34 | -------------------------------------------------------------------------------- /internal/testutils/builders/repository_builder.go: -------------------------------------------------------------------------------- 1 | package builders 2 | 3 | import ( 4 | "github.com/aquasecurity/chain-bench/internal/models" 5 | "github.com/aquasecurity/chain-bench/internal/testutils" 6 | "github.com/aquasecurity/chain-bench/internal/utils" 7 | ) 8 | 9 | type RepositoryBuilder struct { 10 | repository *models.Repository 11 | } 12 | 13 | func NewRepositoryBuilder() *RepositoryBuilder { 14 | return &RepositoryBuilder{repository: &models.Repository{ 15 | AllowRebaseMerge: utils.GetPtr(true), 16 | AllowSquashMerge: utils.GetPtr(true), 17 | AllowMergeCommit: utils.GetPtr(false), 18 | Collaborators: []*models.User{{ 19 | ID: utils.GetPtr(testutils.AuthorizedUserMockId), 20 | Permissions: utils.GetPtr(map[string]bool{ 21 | "admin": true, 22 | }), 23 | }, { 24 | ID: utils.GetPtr(int64(1)), 25 | Permissions: utils.GetPtr(map[string]bool{ 26 | "admin": true, 27 | }), 28 | }}, 29 | IsPrivate: utils.GetPtr(true), 30 | IsContainsSecurityMd: true, 31 | Hooks: []*models.Hook{{ 32 | URL: utils.GetPtr("https://endpoint.com"), 33 | Config: &models.HookConfig{ 34 | URL: utils.GetPtr("https://endpoint.com"), 35 | Insecure_SSL: utils.GetPtr("0"), 36 | Secret: utils.GetPtr("**"), 37 | }, 38 | Events: []string{"package"}, 39 | }}, 40 | Commits: []*models.RepositoryCommit{{ 41 | Author: &models.CommitAuthor{ 42 | Login: utils.GetPtr("user0"), 43 | }, 44 | }, { 45 | Author: &models.CommitAuthor{ 46 | Login: utils.GetPtr("user1"), 47 | }, 48 | }, { 49 | Author: &models.CommitAuthor{ 50 | Login: utils.GetPtr("user2"), 51 | }, 52 | }, { 53 | Author: &models.CommitAuthor{ 54 | Login: utils.GetPtr("user3"), 55 | }, 56 | }}, 57 | Branches: []*models.Branch{NewBranchBuilder().Build()}, 58 | }} 59 | } 60 | 61 | func (b *RepositoryBuilder) WithID(id int64) *RepositoryBuilder { 62 | b.repository.ID = utils.GetPtr(id) 63 | return b 64 | } 65 | 66 | func (b *RepositoryBuilder) WithAllowRebaseMerge(enable bool) *RepositoryBuilder { 67 | b.repository.AllowRebaseMerge = utils.GetPtr(enable) 68 | return b 69 | } 70 | 71 | func (b *RepositoryBuilder) WithNoRepoPemissions() *RepositoryBuilder { 72 | b.repository.AllowRebaseMerge = nil 73 | return b 74 | } 75 | 76 | func (b *RepositoryBuilder) WithAdminCollborator(admin bool, count int) *RepositoryBuilder { 77 | b.repository.Collaborators = []*models.User{} 78 | authorizedUserId := testutils.AuthorizedUserMockId 79 | 80 | for i := 0; i < count; i++ { 81 | b.repository.Collaborators = append(b.repository.Collaborators, &models.User{ID: utils.GetPtr(authorizedUserId + int64(i)), Permissions: utils.GetPtr(map[string]bool{ 82 | "admin": admin, 83 | })}) 84 | } 85 | 86 | return b 87 | } 88 | 89 | func (b *RepositoryBuilder) WithNoCollborator() *RepositoryBuilder { 90 | b.repository.Collaborators = nil 91 | return b 92 | } 93 | 94 | func (b *RepositoryBuilder) WithAllowSquashMerge(enable bool) *RepositoryBuilder { 95 | b.repository.AllowSquashMerge = utils.GetPtr(enable) 96 | return b 97 | } 98 | func (b *RepositoryBuilder) WithAllowMergeCommit(enable bool) *RepositoryBuilder { 99 | b.repository.AllowMergeCommit = utils.GetPtr(enable) 100 | return b 101 | } 102 | func (b *RepositoryBuilder) WithPrivate(isPrivate bool) *RepositoryBuilder { 103 | b.repository.IsPrivate = utils.GetPtr(isPrivate) 104 | return b 105 | } 106 | func (b *RepositoryBuilder) WithSecurityMdFile(containsSecurityMd bool) *RepositoryBuilder { 107 | b.repository.IsContainsSecurityMd = containsSecurityMd 108 | return b 109 | } 110 | func (b *RepositoryBuilder) WithCommit(login string) *RepositoryBuilder { 111 | b.repository.Commits = append(b.repository.Commits, &models.RepositoryCommit{ 112 | Author: &models.CommitAuthor{ 113 | Login: &login, 114 | }, 115 | }) 116 | return b 117 | } 118 | 119 | func (b *RepositoryBuilder) WithNoCommits() *RepositoryBuilder { 120 | b.repository.Commits = nil 121 | return b 122 | } 123 | 124 | func (b *RepositoryBuilder) WithPackageWebHooks(url string, is_ssl string, secret *string) *RepositoryBuilder { 125 | b.repository.Hooks = []*models.Hook{{ 126 | URL: &url, 127 | Config: &models.HookConfig{ 128 | URL: utils.GetPtr(url), 129 | Insecure_SSL: utils.GetPtr(is_ssl), 130 | Secret: secret, 131 | }, 132 | Events: []string{"package"}, 133 | }} 134 | return b 135 | } 136 | 137 | func (b *RepositoryBuilder) WithBranch(branch *models.Branch) *RepositoryBuilder { 138 | b.repository.Branches = append(b.repository.Branches, branch) 139 | return b 140 | } 141 | 142 | func (b *RepositoryBuilder) Build() *models.Repository { 143 | return b.repository 144 | } 145 | -------------------------------------------------------------------------------- /internal/testutils/testutils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/chain-bench/internal/models/checkmodels" 7 | "github.com/stretchr/testify/assert" 8 | funk "github.com/thoas/go-funk" 9 | ) 10 | 11 | type CheckTest struct { 12 | Name string 13 | Data *checkmodels.CheckData 14 | Expected []*checkmodels.CheckRunResult 15 | ExpectedResultsForRules []*checkmodels.CheckRunResult 16 | ExpectedE error 17 | } 18 | 19 | var ( 20 | AuthorizedUserMockId = int64(1234) 21 | SbomTask = "CycloneDX/gh-dotnet-generate-sbom" 22 | ArgonScannerAction = "argonsecurity/scanner-action" 23 | TrivyScannerAction = "aquasecurity/trivy-action" 24 | ) 25 | 26 | func RunCheckTests(t *testing.T, testedAction checkmodels.CheckAction, tests []CheckTest, checksMetadata checkmodels.CheckMetadataMap) { 27 | for _, test := range tests { 28 | test := test 29 | test.ExpectedResultsForRules = generateChecksByMetadata(checksMetadata, test.Expected) 30 | t.Run(test.Name, func(t *testing.T) { 31 | t.Parallel() 32 | 33 | actual, actualE := testedAction(test.Data) 34 | 35 | if test.ExpectedE == nil { 36 | assert.NoError(t, actualE) 37 | } else { 38 | assert.EqualError(t, actualE, test.ExpectedE.Error()) 39 | } 40 | 41 | if test.ExpectedResultsForRules == nil { 42 | assert.Nil(t, actual) 43 | } else { 44 | assert.ElementsMatch(t, test.ExpectedResultsForRules, actual) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func generateChecksByMetadata(checksMetadata checkmodels.CheckMetadataMap, expectedResults []*checkmodels.CheckRunResult) []*checkmodels.CheckRunResult { 51 | checksArr := []*checkmodels.CheckRunResult{} 52 | for id, check := range checksMetadata.Checks { 53 | if !isCheckAlreadyExist(expectedResults, id) { 54 | checksArr = append(checksArr, checkmodels.ToCheckRunResult(id, check, checksMetadata.Url, &checkmodels.CheckResult{Status: checkmodels.Passed})) 55 | } 56 | } 57 | 58 | if expectedResults != nil { 59 | checksArr = append(checksArr, expectedResults...) 60 | } 61 | 62 | return checksArr 63 | } 64 | 65 | func isCheckAlreadyExist(expectedResults []*checkmodels.CheckRunResult, checkId string) bool { 66 | return funk.Contains(expectedResults, func(c *checkmodels.CheckRunResult) bool { 67 | return c.ID == checkId 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /internal/utils/array.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func Contains[T comparable](slice []T, searchTerm T) bool { 4 | for _, t := range slice { 5 | if t == searchTerm { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /internal/utils/http-client.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "golang.org/x/oauth2" 8 | ) 9 | 10 | func GetHttpClient(token string) *http.Client { 11 | ts := oauth2.StaticTokenSource( 12 | &oauth2.Token{AccessToken: token}, 13 | ) 14 | hc := oauth2.NewClient(context.TODO(), ts) 15 | return hc 16 | } 17 | -------------------------------------------------------------------------------- /internal/utils/timestamp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The go-github AUTHORS. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package utils 7 | 8 | import ( 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // Timestamp represents a time that can be unmarshalled from a JSON string 14 | // formatted as either an RFC3339 or Unix timestamp. This is necessary for some 15 | // fields since the GitHub API is inconsistent in how it represents times. All 16 | // exported methods of time.Time can be called on Timestamp. 17 | type Timestamp struct { 18 | time.Time 19 | } 20 | 21 | func (t Timestamp) String() string { 22 | return t.Time.String() 23 | } 24 | 25 | // UnmarshalJSON implements the json.Unmarshaler interface. 26 | // Time is expected in RFC3339 or Unix format. 27 | func (t *Timestamp) UnmarshalJSON(data []byte) (err error) { 28 | str := string(data) 29 | i, err := strconv.ParseInt(str, 10, 64) 30 | if err == nil { 31 | t.Time = time.Unix(i, 0) 32 | } else { 33 | t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str) 34 | } 35 | return 36 | } 37 | 38 | // Equal reports whether t and u are equal based on time.Equal 39 | func (t Timestamp) Equal(u Timestamp) bool { 40 | return t.Time.Equal(u.Time) 41 | } 42 | -------------------------------------------------------------------------------- /internal/utils/values.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func GetValue[T any](p *T) T { 4 | if p == nil { 5 | var v T 6 | return v 7 | } 8 | 9 | return *p 10 | } 11 | 12 | func GetPtr[T any](value T) *T { 13 | return &value 14 | } 15 | 16 | func GetBranchName(defaultBranch, branchName string) string { 17 | if branchName != "" { 18 | return branchName 19 | } 20 | 21 | return defaultBranch 22 | } 23 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell git describe --tags --always) 2 | LDFLAGS=-ldflags "-s -w -X=main.version=$(VERSION)" 3 | 4 | MKDOCS_IMAGE := aquasec/mkdocs-material:dev 5 | MKDOCS_PORT := 8000 6 | 7 | # If the first argument is "run"... 8 | ifeq (run,$(firstword $(MAKECMDGOALS))) 9 | # use the rest as arguments for "run" 10 | RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 11 | # ...and turn them into do-nothing targets 12 | $(eval $(RUN_ARGS):;@:) 13 | endif 14 | 15 | .PHONY: build 16 | build: 17 | go build $(LDFLAGS) ./cmd/chain-bench 18 | 19 | .PHONY: run 20 | run: 21 | go run $(LDFLAGS) ./cmd/chain-bench $(RUN_ARGS) 22 | 23 | .PHONY: test 24 | test: 25 | go test -v ./... 26 | 27 | .PHONY: test-coverage 28 | test-coverage: 29 | go test -coverprofile=coverage.out -covermode=atomic -v ./... 30 | 31 | # Run MkDocs development server to preview the documentation page 32 | .PHONY: mkdocs-serve 33 | mkdocs-serve: 34 | docker build -t $(MKDOCS_IMAGE) -f docs/build/Dockerfile docs/build 35 | docker run --name mkdocs-serve --rm -v $(PWD):/docs -p $(MKDOCS_PORT):8000 $(MKDOCS_IMAGE) 36 | 37 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Chain-bench 2 | site_url: https://aquasecurity.github.io/chain-bench/ 3 | site_description: An open-source tool for auditing your software supply chain stack for security compliance based on a new CIS Software Supply Chain benchmarks 4 | docs_dir: docs/ 5 | repo_name: GitHub 6 | repo_url: https://github.com/aquasecurity/chain-bench 7 | edit_uri: "" 8 | 9 | nav: 10 | - HOME: index.md 11 | - Getting started: 12 | - Quick Start: getting-started/quickstart.md 13 | - Installation: getting-started/installation.md 14 | theme: 15 | name: material 16 | language: "en" 17 | # TODO: replace with svg 18 | logo: imgs/logo.png 19 | features: 20 | - navigation.tabs 21 | - navigation.tabs.sticky 22 | - navigation.sections 23 | 24 | markdown_extensions: 25 | - pymdownx.highlight 26 | - pymdownx.superfences 27 | - admonition 28 | - footnotes 29 | - attr_list 30 | - pymdownx.tabbed 31 | - def_list 32 | - pymdownx.details 33 | - pymdownx.emoji: 34 | emoji_index: !!python/name:materialx.emoji.twemoji 35 | emoji_generator: !!python/name:materialx.emoji.to_svg 36 | 37 | extra: 38 | generator: false 39 | version: 40 | method: mike 41 | provider: mike 42 | var: 43 | prev_git_tag: "v0.0.0" 44 | operator_version: "v0.0.7" 45 | 46 | plugins: 47 | - search 48 | - macros 49 | -------------------------------------------------------------------------------- /templates/gitlab_security_scanner.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "14.1.4", 3 | "scan":{ 4 | "scanner":{ 5 | "id":"chain-bench", 6 | "name":"Supply chain Scanner", 7 | "vendor": { 8 | "name":"chain-bench" 9 | }, 10 | "version":"1.0" 11 | }, 12 | "start_time":"2022-04-07T12:26:58", 13 | "end_time":"2022-04-26T12:26:00", 14 | "status":"success", 15 | "messages": [ 16 | ], 17 | "type":"container_scanning" 18 | }, 19 | "vulnerabilities": [ 20 | {{- $t_first := true }} 21 | {{- range . }} 22 | {{- if eq .Result "Failed"}} 23 | {{- if $t_first -}} 24 | {{- $t_first = false -}} 25 | {{ else -}} 26 | , 27 | {{- end }} 28 | { 29 | "id": "{{ .ID }}", 30 | "category": "container_scanning", 31 | "message": {{ .Name | printf "%q" }}, 32 | "description": {{ .Description | printf "%q" }}, 33 | "cve": "{{ .ID }}", 34 | "severity": {{ .Severity | printf "%q" }}, 35 | "confidence": "Unknown", 36 | "solution": {{ .Remediation | printf "%q" }}, 37 | "scanner": { 38 | "id": "chain-bench", 39 | "name": "chain-bench", 40 | "vendor": { 41 | "name":"chain-bench" 42 | } 43 | }, 44 | "location": { 45 | "dependency": { 46 | "package": { 47 | "name": "{{ .ID }}" 48 | }, 49 | "version": "0.1.3" 50 | }, 51 | "operating_system": "Unknown", 52 | "image": "{{ "Supply Chain Scanner" }}" 53 | }, 54 | "identifiers": [ 55 | { 56 | "type": "cve", 57 | "name": "{{ .ID }}", 58 | "value": "{{ .ID }}", 59 | "url": "{{ .Url }}" 60 | } 61 | ] 62 | } 63 | {{- end }} 64 | {{- end }} 65 | ], 66 | "remediations": [] 67 | } --------------------------------------------------------------------------------