diff --git a/.github/workflows/docker_security.yml b/.github/workflows/docker_security.yml index 9c3eda960..370016a5f 100644 --- a/.github/workflows/docker_security.yml +++ b/.github/workflows/docker_security.yml @@ -3,7 +3,6 @@ name: Docker Build and Security Scan permissions: contents: read security-events: write - pull-requests: write actions: read on: @@ -37,26 +36,61 @@ jobs: file: runscripts/container/Dockerfile load: true tags: vecoli:latest + build-args: INSTALL_EXTRAS=1 cache-from: type=gha cache-to: type=gha,mode=max - - name: Login to Docker Hub - uses: docker/login-action@v3 + - name: Scan image with Trivy (SARIF) + if: github.event_name != 'pull_request' + id: trivy-sarif + uses: aquasecurity/trivy-action@0.34.2 with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Analyze for critical and high CVEs - id: docker-scout-cves - uses: docker/scout-action@v1 - with: - command: cves,recommendations - image: vecoli:latest - sarif-file: sarif.output.json - summary: true + image-ref: vecoli:latest + format: sarif + output: trivy-results.sarif + vuln-type: os,library + ignore-unfixed: true - name: Upload SARIF result id: upload-sarif + if: github.event_name != 'pull_request' uses: github/codeql-action/upload-sarif@v4 with: - sarif_file: sarif.output.json + sarif_file: trivy-results.sarif + + - name: Scan image with Trivy (JUnit) + id: trivy-junit + # Can use $HOME instead of hardcoding once bug is fixed + # https://github.com/aquasecurity/trivy-action/issues/509 + uses: aquasecurity/trivy-action@0.34.2 + with: + image-ref: vecoli:latest + format: template + template: "@/home/runner/.local/bin/trivy-bin/contrib/junit.tpl" + output: trivy-junit.xml + vuln-type: os,library + ignore-unfixed: true + + - name: Publish Trivy test report (JUnit) + uses: ctrf-io/github-test-reporter@v1 + with: + report-path: trivy-junit.xml + summary-report: false + failed-report: true + pull-request: false + github-report: false + integrations-config: | + { + "junit-to-ctrf": { + "enabled": true, + "action": "convert", + "options": { + "output": "./ctrf-reports/ctrf-report.json", + "toolname": "junit-to-ctrf", + "useSuiteName": false, + "env": { + "appName": "vEcoli" + } + } + } + } diff --git a/.github/workflows/trivy_pr_comment.yml b/.github/workflows/trivy_pr_comment.yml new file mode 100644 index 000000000..5ef3faa58 --- /dev/null +++ b/.github/workflows/trivy_pr_comment.yml @@ -0,0 +1,76 @@ +name: Trivy PR Comment + +permissions: + actions: read + pull-requests: write + +on: + workflow_run: + workflows: ["Docker Build and Security Scan"] + types: [completed] + +jobs: + comment: + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Determine PR number securely + id: get_pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + HEAD_SHA="${{ github.event.workflow_run.head_sha }}" + + PR_NUM=$(gh pr list \ + --state open \ + --json number,headRefOid \ + --jq ".[] | select(.headRefOid==\"${HEAD_SHA}\") | .number") + + if [ -z "$PR_NUM" ]; then + echo "No open PR found for head SHA ${HEAD_SHA}" + exit 1 + fi + + echo "PR_NUMBER=$PR_NUM" >> $GITHUB_ENV + + - name: Comment PR with Trivy summary link + uses: actions/github-script@v8 + with: + script: | + const issue_number = Number(process.env.PR_NUMBER); + if (!issue_number) { + core.info('No PR number available.'); + return; + } + const header = '## Trivy scan results'; + const runUrl = context.payload.workflow_run.html_url; + const comment = `${header}\n\n:warning: **Please review the Trivy vulnerability report before merging.**\n\nThe full report is available in the workflow summary:\n${runUrl}`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + per_page: 100, + }); + const existing = comments.find((c) => { + if (!c.body) return false; + const isBot = c.user && c.user.type === 'Bot'; + return isBot && c.body.startsWith(header); + }); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: comment, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body: comment, + }); + } + env: + PR_NUMBER: ${{ env.PR_NUMBER }} diff --git a/runscripts/container/Dockerfile b/runscripts/container/Dockerfile index 515b3c565..489867137 100644 --- a/runscripts/container/Dockerfile +++ b/runscripts/container/Dockerfile @@ -32,11 +32,18 @@ ENV UV_CACHE_DIR=/root/.cache/uv # Silence warning about cache and sync targets being on different filesystems ENV UV_LINK_MODE=copy +# When set to a non-empty value, install all optional dependency groups. +ARG INSTALL_EXTRAS="" + # Install the dependencies only to leverage Docker layer caching RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project + if [ -n "$INSTALL_EXTRAS" ]; then \ + uv sync --frozen --no-install-project --all-extras; \ + else \ + uv sync --frozen --no-install-project; \ + fi # Activate the virtual environment ENV PATH="/vEcoli/.venv/bin:$PATH" @@ -56,7 +63,11 @@ ADD . /vEcoli # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen + if [ -n "$INSTALL_EXTRAS" ]; then \ + uv sync --frozen --all-extras; \ + else \ + uv sync --frozen; \ + fi # Record Docker Image metadata in ENV variables, viewable by `docker inspect` # and accessible to programs in the container.