From f7780cecb36a998796660993430b8deaf220e7c7 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Mon, 2 May 2022 12:09:45 +1200 Subject: [PATCH] feat(ci/security): add code dependency security scan and docker image vulnerability scan [EE-2537] (#6853) This PR supports to scan code security of js and golang dependencies and image vulnerability of locally built docker image --- .github/workflows/nightly-security-scan.yml | 230 +++++++++++++++++++ .github/workflows/pr-security.yml | 233 ++++++++++++++++++++ 2 files changed, 463 insertions(+) create mode 100644 .github/workflows/nightly-security-scan.yml create mode 100644 .github/workflows/pr-security.yml diff --git a/.github/workflows/nightly-security-scan.yml b/.github/workflows/nightly-security-scan.yml new file mode 100644 index 000000000..c793fce80 --- /dev/null +++ b/.github/workflows/nightly-security-scan.yml @@ -0,0 +1,230 @@ +name: Nightly Code Security Scan + +on: + schedule: + - cron: '0 8 * * *' + workflow_dispatch: + +jobs: + client-dependencies: + name: Client dependency check + runs-on: ubuntu-latest + if: >- # only run for develop branch + github.ref == 'refs/heads/develop' + outputs: + js: ${{ steps.set-matrix.outputs.js_result }} + steps: + - uses: actions/checkout@master + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@master + continue-on-error: true # To make sure that artifact upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + json: true + + - name: Upload js security scan result as artifact + uses: actions/upload-artifact@v3 + with: + name: js-security-scan-develop-result + path: snyk.json + + - name: Export scan result to html file + run: | + $(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result") + + - name: Upload js result html file + uses: actions/upload-artifact@v3 + with: + name: html-js-result-${{github.run_id}} + path: js-result.html + + - name: Analyse the js result + id: set-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix) + echo "::set-output name=js_result::${result}" + + server-dependencies: + name: Server dependency check + runs-on: ubuntu-latest + if: >- # only run for develop branch + github.ref == 'refs/heads/develop' + outputs: + go: ${{ steps.set-matrix.outputs.go_result }} + steps: + - uses: actions/checkout@master + + - name: Download go modules + run: cd ./api && go get -t -v -d ./... + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/golang@master + continue-on-error: true # To make sure that artifact upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --file=./api/go.mod + json: true + + - name: Upload go security scan result as artifact + uses: actions/upload-artifact@v3 + with: + name: go-security-scan-develop-result + path: snyk.json + + - name: Export scan result to html file + run: | + $(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result") + + - name: Upload go result html file + uses: actions/upload-artifact@v3 + with: + name: html-go-result-${{github.run_id}} + path: go-result.html + + - name: Analyse the go result + id: set-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix) + echo "::set-output name=go_result::${result}" + + image-vulnerability: + name: Build docker image and Image vulnerability check + runs-on: ubuntu-latest + if: >- + github.ref == 'refs/heads/develop' + outputs: + image: ${{ steps.set-matrix.outputs.image_result }} + steps: + - name: Checkout code + uses: actions/checkout@master + + - name: Use golang 1.18 + uses: actions/setup-go@v3 + with: + go-version: '1.18' + + - name: Use Node.js 12.x + uses: actions/setup-node@v1 + with: + node-version: 12.x + + - name: Install packages and build + run: yarn install && yarn build + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: build/linux/Dockerfile + tags: trivy-portainer:${{ github.sha }} + outputs: type=docker,dest=/tmp/trivy-portainer-image.tar + + - name: Load docker image + run: | + docker load --input /tmp/trivy-portainer-image.tar + + - name: Run Trivy vulnerability scanner + 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 }} + + - name: Upload image security scan result as artifact + uses: actions/upload-artifact@v3 + with: + name: image-security-scan-develop-result + path: image-trivy.json + + - name: Export scan result to html file + run: | + $(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result") + + - name: Upload go result html file + uses: actions/upload-artifact@v3 + with: + name: html-image-result-${{github.run_id}} + path: image-result.html + + - name: Analyse the trivy result + id: set-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix) + echo "::set-output name=image_result::${result}" + + result-analysis: + name: Analyse scan result + needs: [client-dependencies, server-dependencies, image-vulnerability] + runs-on: ubuntu-latest + if: >- + github.ref == 'refs/heads/develop' + strategy: + matrix: + js: ${{fromJson(needs.client-dependencies.outputs.js)}} + go: ${{fromJson(needs.server-dependencies.outputs.go)}} + image: ${{fromJson(needs.image-vulnerability.outputs.image)}} + steps: + - name: Display the results of js, go and image + run: | + echo ${{ matrix.js.status }} + echo ${{ matrix.go.status }} + echo ${{ matrix.image.status }} + echo ${{ matrix.js.summary }} + echo ${{ matrix.go.summary }} + echo ${{ matrix.image.summary }} + + - name: Send Slack message + if: >- + matrix.js.status == 'failure' || + matrix.go.status == 'failure' || + matrix.image.status == 'failure' + uses: slackapi/slack-github-action@v1.18.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*" + } + } + ], + "attachments": [ + { + "color": "#FF0000", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*JS dependency check*: *${{ matrix.js.status }}*\n${{ matrix.js.summary }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Go dependency check*: *${{ matrix.go.status }}*\n${{ matrix.go.summary }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Image vulnerability check*: *${{ matrix.image.status }}*\n${{ matrix.image.summary }}\n" + } + } + ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml new file mode 100644 index 000000000..e88720ef8 --- /dev/null +++ b/.github/workflows/pr-security.yml @@ -0,0 +1,233 @@ +name: PR Code Security Scan + +on: + pull_request_review: + types: + - submitted + - edited + paths: + - 'package.json' + - 'api/go.mod' + - 'gruntfile.js' + - 'build/linux/Dockerfile' + - 'build/linux/alpine.Dockerfile' + - 'build/windows/Dockerfile' + +jobs: + client-dependencies: + name: Client dependency check + runs-on: ubuntu-latest + if: >- + github.event.pull_request && + github.event.review.body == '/scan' + outputs: + jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }} + steps: + - uses: actions/checkout@master + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@master + continue-on-error: true # To make sure that artifact upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + json: true + + - name: Upload js security scan result as artifact + uses: actions/upload-artifact@v3 + with: + name: js-security-scan-feat-result + path: snyk.json + + - name: Download artifacts from develop branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mv ./snyk.json ./js-snyk-feature.json + (gh run download -n js-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || : + if [[ -e ./snyk.json ]]; then + mv ./snyk.json ./js-snyk-develop.json + else + echo "null" > ./js-snyk-develop.json + fi + + - name: Export scan result to html file + run: | + $(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result") + + - name: Upload js result html file + uses: actions/upload-artifact@v3 + with: + name: html-js-result-compare-to-develop-${{github.run_id}} + path: js-result.html + + - name: Analyse the js diff result + id: set-diff-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix) + echo "::set-output name=js_diff_result::${result}" + + server-dependencies: + name: Server dependency check + runs-on: ubuntu-latest + if: >- + github.event.pull_request && + github.event.review.body == '/scan' + outputs: + godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }} + steps: + - uses: actions/checkout@master + + - name: Download go modules + run: cd ./api && go get -t -v -d ./... + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/golang@master + continue-on-error: true # To make sure that artifact upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --file=./api/go.mod + json: true + + - name: Upload go security scan result as artifact + uses: actions/upload-artifact@v3 + with: + name: go-security-scan-feature-result + path: snyk.json + + - name: Download artifacts from develop branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mv ./snyk.json ./go-snyk-feature.json + (gh run download -n go-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || : + if [[ -e ./snyk.json ]]; then + mv ./snyk.json ./go-snyk-develop.json + else + echo "null" > ./go-snyk-develop.json + fi + + - name: Export scan result to html file + run: | + $(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result") + + - name: Upload go result html file + uses: actions/upload-artifact@v3 + with: + name: html-go-result-compare-to-develop-${{github.run_id}} + path: go-result.html + + - name: Analyse the go diff result + id: set-diff-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix) + echo "::set-output name=go_diff_result::${result}" + + image-vulnerability: + name: Build docker image and Image vulnerability check + runs-on: ubuntu-latest + if: >- + github.event.pull_request && + github.event.review.body == '/scan' + outputs: + imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }} + steps: + - name: Checkout code + uses: actions/checkout@master + + - name: Use golang 1.18 + uses: actions/setup-go@v3 + with: + go-version: '1.18' + + - name: Use Node.js 12.x + uses: actions/setup-node@v1 + with: + node-version: 12.x + + - name: Install packages and build + run: yarn install && yarn build + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + file: build/linux/Dockerfile + tags: trivy-portainer:${{ github.sha }} + outputs: type=docker,dest=/tmp/trivy-portainer-image.tar + + - name: Load docker image + run: | + docker load --input /tmp/trivy-portainer-image.tar + + - name: Run Trivy vulnerability scanner + 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 }} + + - name: Upload 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 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mv ./image-trivy.json ./image-trivy-feature.json + (gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || : + if [[ -e ./image-trivy.json ]]; then + mv ./image-trivy.json ./image-trivy-develop.json + else + echo "null" > ./image-trivy-develop.json + fi + + - name: Export scan result to html file + run: | + $(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 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") + + - name: Upload image result html file + uses: actions/upload-artifact@v3 + with: + name: html-image-result-compare-to-develop-${{github.run_id}} + path: image-result.html + + - name: Analyse the image diff result + id: set-diff-matrix + run: | + result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix) + echo "::set-output name=image_diff_result::${result}" + + result-analysis: + name: Analyse scan result compared to develop + needs: [client-dependencies, server-dependencies, image-vulnerability] + runs-on: ubuntu-latest + if: >- + github.event.pull_request && + github.event.review.body == '/scan' + strategy: + matrix: + jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}} + godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}} + imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}} + steps: + + - name: Check job status of diff result + if: >- + matrix.jsdiff.status == 'failure' || + matrix.godiff.status == 'failure' || + matrix.imagediff.status == 'failure' + run: | + echo ${{ matrix.jsdiff.status }} + echo ${{ matrix.godiff.status }} + echo ${{ matrix.imagediff.status }} + echo ${{ matrix.jsdiff.summary }} + echo ${{ matrix.godiff.summary }} + echo ${{ matrix.imagediff.summary }} + exit 1