From 9191d31e9209f01933559b72d557a319b0ce7e4e Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Wed, 17 Apr 2024 16:09:05 +1200 Subject: [PATCH] fix(auth): prevent user enumeration attack [EE-6832] (#11591) --- .github/workflows/ci.yaml | 176 ++++++++++++++++++ .github/workflows/lint.yml | 17 +- .github/workflows/nightly-security-scan.yml | 93 ++++++--- .github/workflows/pr-security.yml | 116 +++++++++--- .github/workflows/stale.yml | 5 +- .github/workflows/test.yaml | 39 +++- .github/workflows/validate-openapi-spec.yaml | 14 +- .golangci.yaml | 32 ++++ Makefile | 4 +- .../test_data/output_24_to_latest.json | 2 +- api/http/handler/auth/authenticate.go | 13 +- api/http/handler/handler.go | 2 +- api/ldap/ldap.go | 9 +- api/portainer.go | 2 +- api/go.mod => go.mod | 3 +- api/go.sum => go.sum | 4 - go.work | 9 - golangci-lint.sh | 11 -- lint-staged.config.js | 2 +- package.json | 4 +- 20 files changed, 456 insertions(+), 101 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .golangci.yaml rename api/go.mod => go.mod (99%) rename api/go.sum => go.sum (99%) delete mode 100644 go.work delete mode 100755 golangci-lint.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..b0c42507a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,176 @@ +name: ci + +on: + workflow_dispatch: + push: + branches: + - 'develop' + - 'release/*' + pull_request: + branches: + - 'develop' + - 'release/*' + - 'feat/*' + - 'fix/*' + - 'refactor/*' + types: + - opened + - reopened + - synchronize + - ready_for_review + +env: + DOCKER_HUB_REPO: portainerci/portainer-ce + EXTENSION_HUB_REPO: portainerci/portainer-docker-extension + GO_VERSION: 1.21.6 + NODE_VERSION: 18.x + +jobs: + build_images: + strategy: + matrix: + config: + - { platform: linux, arch: amd64, version: "" } + - { platform: linux, arch: arm64, version: "" } + - { platform: linux, arch: arm, version: "" } + - { platform: linux, arch: ppc64le, version: "" } + - { platform: linux, arch: s390x, version: "" } + - { platform: windows, arch: amd64, version: 1809 } + - { platform: windows, arch: amd64, version: ltsc2022 } + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + steps: + - name: '[preparation] checkout the current branch' + uses: actions/checkout@v4.1.1 + with: + ref: ${{ github.event.inputs.branch }} + - name: '[preparation] set up golang' + uses: actions/setup-go@v5.0.0 + with: + go-version: ${{ env.GO_VERSION }} + - name: '[preparation] set up node.js' + uses: actions/setup-node@v4.0.1 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'yarn' + - name: '[preparation] set up qemu' + uses: docker/setup-qemu-action@v3.0.0 + - name: '[preparation] set up docker context for buildx' + run: docker context create builders + - name: '[preparation] set up docker buildx' + uses: docker/setup-buildx-action@v3.0.0 + with: + endpoint: builders + - name: '[preparation] docker login' + uses: docker/login-action@v3.0.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + - name: '[preparation] set the container image tag' + run: | + if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then + # use the release branch name as the tag for release branches + # for instance, release/2.19 becomes 2.19 + CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2) + elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then + # use pr${{ github.event.number }} as the tag for pull requests + # for instance, pr123 + CONTAINER_IMAGE_TAG="pr${{ github.event.number }}" + else + # replace / with - in the branch name + # for instance, feature/1.0.0 -> feature-1.0.0 + CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g') + fi + + echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}" >> $GITHUB_ENV + - name: '[execution] build linux & windows portainer binaries' + run: | + export YARN_VERSION=$(yarn --version) + export WEBPACK_VERSION=$(yarn list webpack --depth=0 | grep webpack | awk -F@ '{print $2}') + export BUILDNUMBER=${GITHUB_RUN_NUMBER} + GIT_COMMIT_HASH_LONG=${{ github.sha }} + export GIT_COMMIT_HASH_SHORT={GIT_COMMIT_HASH_LONG:0:7} + + NODE_ENV="testing" + if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then + NODE_ENV="production" + fi + + make build-all PLATFORM=${{ matrix.config.platform }} ARCH=${{ matrix.config.arch }} ENV=${NODE_ENV} + env: + CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }} + - name: '[execution] build and push docker images' + run: | + if [ "${{ matrix.config.platform }}" == "windows" ]; then + mv dist/portainer dist/portainer.exe + docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile . + else + docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile . + docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile . + + if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then + docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile . + docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile . + fi + fi + env: + CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }} + build_manifests: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + needs: [build_images] + steps: + - name: '[preparation] docker login' + uses: docker/login-action@v3.0.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + - name: '[preparation] set up docker context for buildx' + run: docker version && docker context create builders + - name: '[preparation] set up docker buildx' + uses: docker/setup-buildx-action@v3.0.0 + with: + endpoint: builders + - name: '[execution] build and push manifests' + run: | + if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then + # use the release branch name as the tag for release branches + # for instance, release/2.19 becomes 2.19 + CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2) + elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then + # use pr${{ github.event.number }} as the tag for pull requests + # for instance, pr123 + CONTAINER_IMAGE_TAG="pr${{ github.event.number }}" + else + # replace / with - in the branch name + # for instance, feature/1.0.0 -> feature-1.0.0 + CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g') + fi + + docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64" + + docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \ + "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine" + + if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then + docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x" + + docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \ + "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine" + fi diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a2f9ac1ef..09ebfaa7b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,21 +11,31 @@ on: - master - develop - release/* + types: + - opened + - reopened + - synchronize + - ready_for_review + +env: + GO_VERSION: 1.21.6 + NODE_VERSION: 18.x jobs: run-linters: name: Run linters runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: '18' + node-version: ${{ env.NODE_VERSION }} cache: 'yarn' - uses: actions/setup-go@v4 with: - go-version: 1.19.5 + go-version: ${{ env.GO_VERSION }} - run: yarn --frozen-lockfile - name: Run linters uses: wearerequired/lint-action@v1 @@ -41,6 +51,5 @@ jobs: - name: GolangCI-Lint uses: golangci/golangci-lint-action@v3 with: - version: v1.54.1 - working-directory: api + version: v1.55.2 args: --timeout=10m -c .golangci.yaml diff --git a/.github/workflows/nightly-security-scan.yml b/.github/workflows/nightly-security-scan.yml index db27d87d7..31d45d181 100644 --- a/.github/workflows/nightly-security-scan.yml +++ b/.github/workflows/nightly-security-scan.yml @@ -5,6 +5,9 @@ on: - cron: '0 20 * * *' workflow_dispatch: +env: + GO_VERSION: 1.21.6 + jobs: client-dependencies: name: Client Dependency Check @@ -25,7 +28,7 @@ jobs: with: json: true - - name: upload scan result as develop artifact + - name: upload scan result as develop artifact uses: actions/upload-artifact@v3 with: name: js-security-scan-develop-result @@ -41,7 +44,7 @@ jobs: name: html-js-result-${{github.run_id}} path: js-result.html - - name: analyse vulnerabilities + - name: analyse vulnerabilities id: set-matrix run: | result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix) @@ -58,10 +61,10 @@ jobs: - name: checkout repository uses: actions/checkout@master - - name: install Go + - name: install Go uses: actions/setup-go@v3 with: - go-version: '1.19.5' + go-version: ${{ env.GO_VERSION }} - name: download Go modules run: cd ./api && go get -t -v -d ./... @@ -72,9 +75,9 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} run: | yarn global add snyk - snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || : + snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || : - - name: upload scan result as develop artifact + - name: upload scan result as develop artifact uses: actions/upload-artifact@v3 with: name: go-security-scan-develop-result @@ -102,35 +105,68 @@ jobs: if: >- github.ref == 'refs/heads/develop' outputs: - image: ${{ steps.set-matrix.outputs.image_result }} + image-trivy: ${{ steps.set-trivy-matrix.outputs.image_trivy_result }} + image-docker-scout: ${{ steps.set-docker-scout-matrix.outputs.image_docker_scout_result }} steps: - - name: scan vulnerabilities by Trivy + - name: scan vulnerabilities by Trivy uses: docker://docker.io/aquasec/trivy:latest continue-on-error: true with: args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop - - name: upload image security scan result as artifact + - name: upload Trivy image security scan result as artifact uses: actions/upload-artifact@v3 with: name: image-security-scan-develop-result path: image-trivy.json - - name: develop scan report export to html + - name: develop Trivy scan report export to html run: | - $(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result") + $(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-trivy-result") - - name: upload html file as artifact + - name: upload html file as Trivy artifact uses: actions/upload-artifact@v3 with: name: html-image-result-${{github.run_id}} - path: image-result.html + path: image-trivy-result.html - - name: analyse vulnerabilities - id: set-matrix + - name: analyse vulnerabilities from Trivy + id: set-trivy-matrix run: | result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix) - echo "image_result=${result}" >> $GITHUB_OUTPUT + echo "image_trivy_result=${result}" >> $GITHUB_OUTPUT + + - name: scan vulnerabilities by Docker Scout + uses: docker/scout-action@v1 + continue-on-error: true + with: + command: cves + image: portainerci/portainer:develop + sarif-file: image-docker-scout.json + dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }} + dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: upload Docker Scout image security scan result as artifact + uses: actions/upload-artifact@v3 + with: + name: image-security-scan-develop-result + path: image-docker-scout.json + + - name: develop Docker Scout scan report export to html + run: | + $(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=table --export --export-filename="/data/image-docker-scout-result") + + - name: upload html file as Docker Scout artifact + uses: actions/upload-artifact@v3 + with: + name: html-image-result-${{github.run_id}} + path: image-docker-scout-result.html + + - name: analyse vulnerabilities from Docker Scout + id: set-docker-scout-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=matrix) + echo "image_docker_scout_result=${result}" >> $GITHUB_OUTPUT result-analysis: name: Analyse Scan Results @@ -142,22 +178,26 @@ jobs: matrix: js: ${{fromJson(needs.client-dependencies.outputs.js)}} go: ${{fromJson(needs.server-dependencies.outputs.go)}} - image: ${{fromJson(needs.image-vulnerability.outputs.image)}} + image-trivy: ${{fromJson(needs.image-vulnerability.outputs.image-trivy)}} + image-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.image-docker-scout)}} steps: - name: display the results of js, Go, and image scan run: | echo "${{ matrix.js.status }}" echo "${{ matrix.go.status }}" - echo "${{ matrix.image.status }}" + echo "${{ matrix.image-trivy.status }}" + echo "${{ matrix.image-docker-scout.status }}" echo "${{ matrix.js.summary }}" echo "${{ matrix.go.summary }}" - echo "${{ matrix.image.summary }}" + echo "${{ matrix.image-trivy.summary }}" + echo "${{ matrix.image-docker-scout.summary }}" - - name: send message to Slack - if: >- + - name: send message to Slack + if: >- matrix.js.status == 'failure' || matrix.go.status == 'failure' || - matrix.image.status == 'failure' + matrix.image-trivy.status == 'failure' || + matrix.image-docker-scout.status == 'failure' uses: slackapi/slack-github-action@v1.23.0 with: payload: | @@ -193,7 +233,14 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*Image vulnerability check*: *${{ matrix.image.status }}*\n${{ matrix.image.summary }}\n" + "text": "*Image Trivy vulnerability check*: *${{ matrix.image-trivy.status }}*\n${{ matrix.image-trivy.summary }}\n" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Image Docker Scout vulnerability check*: *${{ matrix.image-docker-scout.status }}*\n${{ matrix.image-docker-scout.summary }}\n" } } ] diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml index efb803381..fb1b1ceed 100644 --- a/.github/workflows/pr-security.yml +++ b/.github/workflows/pr-security.yml @@ -7,20 +7,24 @@ on: - edited paths: - 'package.json' - - 'api/go.mod' - - 'gruntfile.js' + - 'go.mod' - 'build/linux/Dockerfile' - 'build/linux/alpine.Dockerfile' - 'build/windows/Dockerfile' - '.github/workflows/pr-security.yml' +env: + GO_VERSION: 1.21.6 + NODE_VERSION: 18.x + jobs: client-dependencies: name: Client Dependency Check runs-on: ubuntu-latest if: >- github.event.pull_request && - github.event.review.body == '/scan' + github.event.review.body == '/scan' && + github.event.pull_request.draft == false outputs: jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }} steps: @@ -74,7 +78,8 @@ jobs: runs-on: ubuntu-latest if: >- github.event.pull_request && - github.event.review.body == '/scan' + github.event.review.body == '/scan' && + github.event.pull_request.draft == false outputs: godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }} steps: @@ -84,7 +89,7 @@ jobs: - name: install Go uses: actions/setup-go@v3 with: - go-version: '1.19.5' + go-version: ${{ env.GO_VERSION }} - name: download Go modules run: cd ./api && go get -t -v -d ./... @@ -95,7 +100,7 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} run: | yarn global add snyk - snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || : + snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || : - name: upload scan result as pull-request artifact uses: actions/upload-artifact@v3 @@ -136,22 +141,24 @@ jobs: runs-on: ubuntu-latest if: >- github.event.pull_request && - github.event.review.body == '/scan' + github.event.review.body == '/scan' && + github.event.pull_request.draft == false outputs: - imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }} + imagediff-trivy: ${{ steps.set-diff-trivy-matrix.outputs.image_diff_trivy_result }} + imagediff-docker-scout: ${{ steps.set-diff-docker-scout-matrix.outputs.image_diff_docker_scout_result }} steps: - name: checkout code uses: actions/checkout@master - - name: install Go 1.19.5 + - name: install Go uses: actions/setup-go@v3 with: - go-version: '1.19.5' + go-version: ${{ env.GO_VERSION }} - - name: install Node.js 18.x + - name: install Node.js uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: ${{ env.NODE_VERSION }} - name: Install packages run: yarn --frozen-lockfile @@ -167,26 +174,26 @@ jobs: with: context: . file: build/linux/Dockerfile - tags: trivy-portainer:${{ github.sha }} - outputs: type=docker,dest=/tmp/trivy-portainer-image.tar + tags: local-portainer:${{ github.sha }} + outputs: type=docker,dest=/tmp/local-portainer-image.tar - name: load docker image run: | - docker load --input /tmp/trivy-portainer-image.tar + docker load --input /tmp/local-portainer-image.tar - name: scan vulnerabilities by Trivy uses: docker://docker.io/aquasec/trivy:latest continue-on-error: true with: - args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }} + args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress local-portainer:${{ github.sha }} - - name: upload image security scan result as artifact + - name: upload Trivy image security scan result as artifact uses: actions/upload-artifact@v3 with: name: image-security-scan-feature-result path: image-trivy.json - - name: download artifacts from develop branch built by nightly scan + - name: download Trivy artifacts from develop branch built by nightly scan env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -198,21 +205,65 @@ jobs: echo "null" > ./image-trivy-develop.json fi - - name: pr vs develop scan report comparison export to html + - name: pr vs develop Trivy scan report comparison export to html run: | - $(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-result") + $(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-trivy-result") - - name: upload html file as artifact + - name: upload html file as Trivy artifact uses: actions/upload-artifact@v3 with: name: html-image-result-compare-to-develop-${{github.run_id}} - path: image-result.html + path: image-trivy-result.html - - name: analyse different vulnerabilities against develop branch - id: set-diff-matrix + - name: analyse different vulnerabilities against develop branch by Trivy + id: set-diff-trivy-matrix run: | result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix) - echo "image_diff_result=${result}" >> $GITHUB_OUTPUT + echo "image_diff_trivy_result=${result}" >> $GITHUB_OUTPUT + + - name: scan vulnerabilities by Docker Scout + uses: docker/scout-action@v1 + continue-on-error: true + with: + command: cves + image: local-portainer:${{ github.sha }} + sarif-file: image-docker-scout.json + dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }} + dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: upload Docker Scout image security scan result as artifact + uses: actions/upload-artifact@v3 + with: + name: image-security-scan-feature-result + path: image-docker-scout.json + + - name: download Docker Scout artifacts from develop branch built by nightly scan + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mv ./image-docker-scout.json ./image-docker-scout-feature.json + (gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || : + if [[ -e ./image-docker-scout.json ]]; then + mv ./image-docker-scout.json ./image-docker-scout-develop.json + else + echo "null" > ./image-docker-scout-develop.json + fi + + - name: pr vs develop Docker Scout scan report comparison export to html + run: | + $(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=table --export --export-filename="/data/image-docker-scout-result") + + - name: upload html file as Docker Scout artifact + uses: actions/upload-artifact@v3 + with: + name: html-image-result-compare-to-develop-${{github.run_id}} + path: image-docker-scout-result.html + + - name: analyse different vulnerabilities against develop branch by Docker Scout + id: set-diff-docker-scout-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=matrix) + echo "image_diff_docker_scout_result=${result}" >> $GITHUB_OUTPUT result-analysis: name: Analyse Scan Result Against develop Branch @@ -220,23 +271,28 @@ jobs: runs-on: ubuntu-latest if: >- github.event.pull_request && - github.event.review.body == '/scan' + github.event.review.body == '/scan' && + github.event.pull_request.draft == false strategy: matrix: jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}} godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}} - imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}} + imagediff-trivy: ${{fromJson(needs.image-vulnerability.outputs.imagediff-trivy)}} + imagediff-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.imagediff-docker-scout)}} steps: - name: check job status of diff result if: >- matrix.jsdiff.status == 'failure' || matrix.godiff.status == 'failure' || - matrix.imagediff.status == 'failure' + matrix.imagediff-trivy.status == 'failure' || + matrix.imagediff-docker-scout.status == 'failure' run: | echo "${{ matrix.jsdiff.status }}" echo "${{ matrix.godiff.status }}" - echo "${{ matrix.imagediff.status }}" + echo "${{ matrix.imagediff-trivy.status }}" + echo "${{ matrix.imagediff-docker-scout.status }}" echo "${{ matrix.jsdiff.summary }}" echo "${{ matrix.godiff.summary }}" - echo "${{ matrix.imagediff.summary }}" + echo "${{ matrix.imagediff-trivy.summary }}" + echo "${{ matrix.imagediff-docker-scout.summary }}" exit 1 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index cf978446c..878948206 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,7 +1,8 @@ name: Close Stale Issues on: schedule: - - cron: '0 12 * * *' + - cron: '0 12 * * *' + workflow_dispatch: jobs: stale: runs-on: ubuntu-latest @@ -9,7 +10,7 @@ jobs: issues: write steps: - - uses: actions/stale@v4.0.0 + - uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 43a9b13e2..22cfb2803 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,25 +1,56 @@ name: Test -on: push + +env: + GO_VERSION: 1.21.6 + NODE_VERSION: 18.x + +on: + pull_request: + branches: + - master + - develop + - release/* + types: + - opened + - reopened + - synchronize + - ready_for_review + push: + branches: + - master + - develop + - release/* + jobs: test-client: runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: '18' + node-version: ${{ env.NODE_VERSION }} cache: 'yarn' - run: yarn --frozen-lockfile - name: Run tests - run: yarn jest --maxWorkers=2 + run: make test-client ARGS="--maxWorkers=2 --minWorkers=1" test-server: + strategy: + matrix: + config: + - { platform: linux, arch: amd64 } + - { platform: linux, arch: arm64 } + - { platform: windows, arch: amd64, version: 1809 } + - { platform: windows, arch: amd64, version: ltsc2022 } runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: 1.19.5 + go-version: ${{ env.GO_VERSION }} - name: Run tests run: make test-server diff --git a/.github/workflows/validate-openapi-spec.yaml b/.github/workflows/validate-openapi-spec.yaml index aeb32b651..1f4cfdea8 100644 --- a/.github/workflows/validate-openapi-spec.yaml +++ b/.github/workflows/validate-openapi-spec.yaml @@ -6,22 +6,32 @@ on: - master - develop - 'release/*' + types: + - opened + - reopened + - synchronize + - ready_for_review + +env: + GO_VERSION: 1.21.6 + NODE_VERSION: 18.x jobs: openapi-spec: runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: ${{ env.GO_VERSION }} - name: Download golang modules run: cd ./api && go get -t -v -d ./... - uses: actions/setup-node@v3 with: - node-version: '18' + node-version: ${{ env.NODE_VERSION }} cache: 'yarn' - run: yarn --frozen-lockfile diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 000000000..59bd5cb2c --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,32 @@ +linters: + # Disable all linters, the defaults don't pass on our code yet + disable-all: true + + # Enable these for now + enable: + - depguard + - govet + - errorlint + - exportloopref + +linters-settings: + depguard: + rules: + main: + deny: + - pkg: 'github.com/sirupsen/logrus' + desc: 'logging is allowed only by github.com/rs/zerolog' + - pkg: 'golang.org/x/exp' + desc: 'exp is not allowed' + files: + - '!**/*_test.go' + - '!**/base.go' + - '!**/base_tx.go' + +# errorlint is causing a typecheck error for some reason. The go compiler will report these +# anyway, so ignore them from the linter +issues: + exclude-rules: + - path: ./ + linters: + - typecheck diff --git a/Makefile b/Makefile index 7c430f161..326cc0556 100644 --- a/Makefile +++ b/Makefile @@ -102,8 +102,7 @@ lint-client: ## Lint client code yarn lint lint-server: ## Lint server code - cd api && go vet ./... - + golangci-lint run --timeout=10m -c .golangci.yaml ##@ Extension .PHONY: dev-extension @@ -124,3 +123,4 @@ docs-validate: docs-build ## Validate docs .PHONY: help help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index f3e6f4385..708f108df 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -944,6 +944,6 @@ } ], "version": { - "VERSION": "{\"SchemaVersion\":\"2.19.4\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" + "VERSION": "{\"SchemaVersion\":\"2.19.5\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" } } \ No newline at end of file diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index 3a5cacb04..2b480255e 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -74,7 +74,12 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h if settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth || (settings.AuthenticationMethod == portainer.AuthenticationLDAP && !settings.LDAPSettings.AutoCreateUsers) { - return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Invalid credentials", Err: httperrors.ErrUnauthorized} + // avoid username enumeration timing attack by creating a fake user + // https://en.wikipedia.org/wiki/Timing_attack + user = &portainer.User{ + Username: "portainer-fake-username", + Password: "$2a$10$abcdefghijklmnopqrstuvwx..ABCDEFGHIJKLMNOPQRSTUVWXYZ12", // fake but valid format bcrypt hash + } } } @@ -111,7 +116,11 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError { err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings) if err != nil { - return httperror.Forbidden("Only initial admin is allowed to login without oauth", err) + if errors.Is(err, httperrors.ErrUnauthorized) { + return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized) + } + + return httperror.InternalServerError("Unable to authenticate user against LDAP", err) } if user == nil { diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index b5275a793..8e0528c86 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -84,7 +84,7 @@ type Handler struct { } // @title PortainerCE API -// @version 2.19.4 +// @version 2.19.5 // @description.markdown api-description.md // @termsOfService diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index 21358816c..6202c716a 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -75,7 +75,14 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer. userDN, err := searchUser(username, connection, settings.SearchSettings) if err != nil { - return err + if errors.Is(err, errUserNotFound) { + // prevent user enumeration timing attack by attempting the bind with a fake user + // and whatever password was provided should definately fail + // https://en.wikipedia.org/wiki/Timing_attack + userDN = "portainer-fake-ldap-username" + } else { + return err + } } err = connection.Bind(userDN, password) diff --git a/api/portainer.go b/api/portainer.go index 2f502b178..ad8f2cf46 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1561,7 +1561,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.19.4" + APIVersion = "2.19.5" // Edition is what this edition of Portainer is called Edition = PortainerCE // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax diff --git a/api/go.mod b/go.mod similarity index 99% rename from api/go.mod rename to go.mod index 38ee77308..0ac6997f5 100644 --- a/api/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/portainer/portainer/api +module github.com/portainer/portainer go 1.20 @@ -164,3 +164,4 @@ require ( // Remove below line when the "determinstic key" patch for Chisel merged replace github.com/jpillora/chisel => github.com/portainer/chisel v0.0.0-20230704222304-426f515c6c25 + diff --git a/api/go.sum b/go.sum similarity index 99% rename from api/go.sum rename to go.sum index fd9bb6d7e..cde066b7d 100644 --- a/api/go.sum +++ b/go.sum @@ -318,10 +318,6 @@ github.com/portainer/libhttp v0.0.0-20230615144939-a999f666d9a9 h1:Jq8g/pDcFL1Z/ github.com/portainer/libhttp v0.0.0-20230615144939-a999f666d9a9/go.mod h1:H49JLiywwLt2rrJVroafEWy8fIs0i7mThAThK40sbb8= github.com/portainer/portainer/pkg/featureflags v0.0.0-20230711022654-64b227b2e146 h1:8JVXjyFhGgSogKGOddnOROZxBEeWNvvo3EPT9uIJ2Xo= github.com/portainer/portainer/pkg/featureflags v0.0.0-20230711022654-64b227b2e146/go.mod h1:x4Lpq/BjFhZmuNB8e8FO0ObRPQ/Z/V9rTe54bMedf1A= -github.com/portainer/portainer/pkg/libhelm v0.0.0-20230711022654-64b227b2e146 h1:1qW7quKyFG4tOnMcnnqyYsDVfL09etO1h/Cu/3ak7KU= -github.com/portainer/portainer/pkg/libhelm v0.0.0-20230711022654-64b227b2e146/go.mod h1:cFRD6PvOwpd2pf/O1r/IMKl+ZB12pWfo/Evleh3aCfM= -github.com/portainer/portainer/pkg/libhelm v0.0.0-20230919060741-8f42ba025479 h1:DbmhSQZpDo5f0cr+CKLJqoqhQiuxp8QFXdZsjPS1lI4= -github.com/portainer/portainer/pkg/libhelm v0.0.0-20230919060741-8f42ba025479/go.mod h1:cFRD6PvOwpd2pf/O1r/IMKl+ZB12pWfo/Evleh3aCfM= github.com/portainer/portainer/pkg/libhelm v0.0.0-20230928223730-157393c965ce h1:DQTMXYH1zn2DzuAe+4rT40JqdHLhpHHJ2pzRFhvZ/+c= github.com/portainer/portainer/pkg/libhelm v0.0.0-20230928223730-157393c965ce/go.mod h1:cFRD6PvOwpd2pf/O1r/IMKl+ZB12pWfo/Evleh3aCfM= github.com/portainer/portainer/pkg/libstack v0.0.0-20230711022654-64b227b2e146 h1:ZGj+j5HoajaO+mXgCm6NzOU+zUdIlJK2amagB+QIDvc= diff --git a/go.work b/go.work deleted file mode 100644 index df63092d4..000000000 --- a/go.work +++ /dev/null @@ -1,9 +0,0 @@ -go 1.19 - -use ( - ./api - ./pkg/featureflags - ./pkg/libhelm - ./pkg/libstack - ./third_party/digest -) diff --git a/golangci-lint.sh b/golangci-lint.sh deleted file mode 100755 index 412146787..000000000 --- a/golangci-lint.sh +++ /dev/null @@ -1,11 +0,0 @@ - -#!/bin/bash - -cd api -if golangci-lint run --timeout=10m -c .golangci.yaml -then - echo "golangci-lint run successfully" -else - echo "golangci-lint run failed" - exit 1 -fi diff --git a/lint-staged.config.js b/lint-staged.config.js index e0267a2a2..2dca01137 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -2,5 +2,5 @@ module.exports = { '*.(js|ts){,x}': 'eslint --cache --fix', '*.(ts){,x}': () => 'tsc --noEmit', '*.{js,ts,tsx,css,md,html,json}': 'prettier --write', - '*.go': 'bash golangci-lint.sh', + '*.go': () => 'make lint-server', }; diff --git a/package.json b/package.json index 5b0eab95b..729a13b34 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "2.19.4", + "version": "2.19.5", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git" @@ -231,4 +231,4 @@ "**/moment": "^2.21.0" }, "browserslist": "last 2 versions" -} \ No newline at end of file +}