diff --git a/.github/ISSUE_TEMPLATE/defect-report.yml b/.github/ISSUE_TEMPLATE/defect-report.yml index 974edda727..e4b3601e84 100644 --- a/.github/ISSUE_TEMPLATE/defect-report.yml +++ b/.github/ISSUE_TEMPLATE/defect-report.yml @@ -73,7 +73,9 @@ body: - 4.13.4 - 4.13.5 - 4.13.6 - - 4.14.0-SNAPSHOT + - 4.14.0 + - 4.14.1 + - 4.15.0-SNAPSHOT validations: required: true - type: dropdown diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8c031d6975..c7a80d48b7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,33 +5,45 @@ updates: directory: / schedule: interval: daily + cooldown: + default-days: 7 - package-ecosystem: docker directory: /src/main/docker schedule: interval: daily + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: / schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: bundler directory: /docs schedule: interval: weekly + cooldown: + default-days: 7 # Receive minor and patch updates on latest release branch. - package-ecosystem: maven - target-branch: 4.13.x + target-branch: 4.14.x directory: / schedule: interval: daily + cooldown: + default-days: 7 ignore: - dependency-name: "*" update-types: - version-update:semver-major - package-ecosystem: docker - target-branch: 4.13.x + target-branch: 4.14.x directory: /src/main/docker schedule: interval: daily + cooldown: + default-days: 7 ignore: - dependency-name: "*" update-types: diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 25174d79bb..ac35536708 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Set up JDK uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # tag=v5.2.0 @@ -78,15 +80,17 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Download Artifacts - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # tag=v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # tag=v8.0.1 with: name: assembled-wars path: target - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # tag=v3.7.0 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # tag=v4.0.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # tag=v4.0.0 @@ -104,19 +108,22 @@ jobs: - name: Set Container Tags id: tags + env: + REF_NAME: ${{ inputs.ref-name }} + APP_VERSION: ${{ inputs.app-version }} + DISTRIBUTION: ${{ matrix.distribution }} run: |- - IMAGE_NAME="docker.io/dependencytrack/${{ matrix.distribution }}" - REF_NAME="${{ inputs.ref-name }}" + IMAGE_NAME="docker.io/dependencytrack/${DISTRIBUTION}" TAGS="" TAGS_ALPINE="" - + if [[ $REF_NAME == feature-* ]]; then TAGS="${IMAGE_NAME}:${REF_NAME,,}" TAGS_ALPINE="${IMAGE_NAME}:${REF_NAME,,}-alpine" else - TAGS="${IMAGE_NAME}:${{ inputs.app-version }}" - TAGS_ALPINE="${IMAGE_NAME}:${{ inputs.app-version }}-alpine" - if [[ "${{ inputs.app-version }}" != "snapshot" ]]; then + TAGS="${IMAGE_NAME}:${APP_VERSION}" + TAGS_ALPINE="${IMAGE_NAME}:${APP_VERSION}-alpine" + if [[ "${APP_VERSION}" != "snapshot" ]]; then TAGS="${TAGS},${IMAGE_NAME}:latest" TAGS_ALPINE="${TAGS_ALPINE},${IMAGE_NAME}:latest-alpine" fi @@ -125,7 +132,7 @@ jobs: echo "tags-alpine=${TAGS_ALPINE}" >> $GITHUB_OUTPUT - name: Build multi-arch Container Image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # tag=v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # tag=v7.0.0 with: tags: ${{ steps.tags.outputs.tags }} build-args: |- @@ -138,7 +145,7 @@ jobs: file: src/main/docker/Dockerfile - name: Build Alpine multi-arch Container Image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # tag=v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # tag=v7.0.0 with: tags: ${{ steps.tags.outputs.tags-alpine }} build-args: |- @@ -149,23 +156,3 @@ jobs: push: ${{ inputs.publish-container }} context: . file: src/main/docker/Dockerfile.alpine - - - name: Run Trivy Vulnerability Scanner - if: ${{ inputs.publish-container }} - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # tag=0.35.0 - env: - # https://github.com/aquasecurity/trivy-action/issues/389 - TRIVY_DB_REPOSITORY: "public.ecr.aws/aquasecurity/trivy-db:2" - TRIVY_JAVA_DB_REPOSITORY: "public.ecr.aws/aquasecurity/trivy-java-db:1" - with: - image-ref: docker.io/dependencytrack/${{ matrix.distribution }}:${{ inputs.app-version }} - format: 'sarif' - output: 'trivy-results.sarif' - ignore-unfixed: true - vuln-type: 'os' - - - name: Upload Trivy Scan Results to GitHub Security Tab - if: ${{ inputs.publish-container }} - uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # tag=v3.29.5 - with: - sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index bba40342f5..b57ba1ad3e 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -3,9 +3,9 @@ name: Publish CI on: - release: - types: - - released + push: + tags: + - '[0-9]*' workflow_dispatch: permissions: { } @@ -24,6 +24,8 @@ jobs: fi - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Parse Version from POM id: parse @@ -47,15 +49,19 @@ jobs: update-github-release: runs-on: ubuntu-latest + permissions: + contents: write # Required to update GitHub release assets and notes needs: - read-version - call-build steps: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Download Artifacts - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # tag=v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # tag=v8.0.1 with: name: assembled-wars path: target @@ -73,23 +79,32 @@ jobs: - name: Update Release env: - GITHUB_TOKEN: ${{ secrets.BOT_RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ needs.read-version.outputs.version }} run: |- cat << EOF >> .github/default-release-notes.md \`\`\`text $(cat target/checksums.txt) \`\`\` EOF - - gh release view ${{ needs.read-version.outputs.version }} \ + + gh release view "${VERSION}" \ --json body --jq .body >> .github/default-release-notes.md - gh release edit ${{ needs.read-version.outputs.version }} \ + gh release edit "${VERSION}" \ --notes-file ".github/default-release-notes.md" - gh release upload ${{ needs.read-version.outputs.version }} \ + gh release upload "${VERSION}" \ --clobber \ target/dependency-track-apiserver.jar \ target/dependency-track-bundled.jar \ target/checksums.txt \ target/bom.json + + - name: Publish Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ needs.read-version.outputs.version }} + run: |- + gh release edit "${VERSION}" \ + --draft=false \ No newline at end of file diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index 7ec6d90c64..128e5ded8a 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -21,11 +21,15 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Setup Environment id: variables + env: + VERSION_OVERWRITE: ${{ github.event.inputs.version-overwrite }} run: |- - VERSION="${{ github.event.inputs.version-overwrite }}" + VERSION="${VERSION_OVERWRITE}" if [[ -z ${VERSION} ]]; then CURRENT_SNAPSHOT=`yq -p=xml '.project.version' pom.xml` VERSION=${CURRENT_SNAPSHOT%-SNAPSHOT} @@ -38,10 +42,6 @@ jobs: create-release: runs-on: ubuntu-latest - permissions: - # Required for pushing changes via git command (rather than via GitHub API). - # TODO: Use bot credentials for git, or rewrite the "Commit Version" step to use API instead. - contents: write needs: - prepare-release @@ -52,6 +52,8 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + token: ${{ secrets.BOT_RELEASE_GITHUB_TOKEN }} - name: Set up JDK uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # tag=v5.2.0 @@ -60,54 +62,43 @@ jobs: java-version: '21' cache: 'maven' - - name: Set Version - run: mvn -B --no-transfer-progress versions:set -DnewVersion=${VERSION} - - - name: Commit Version - env: - GITHUB_TOKEN: ${{ secrets.BOT_RELEASE_TOKEN }} + - name: Set Version and Commit run: |- - MESSAGE="prepare-release: set version to ${VERSION}" - CONTENT=$(base64 -i pom.xml) - - if [[ -z `git ls-remote --quiet --heads origin "${BRANCH_NAME}"` ]]; then - SHA=$(git rev-parse ${GITHUB_REF#refs/heads/}:pom.xml) - - # https://gist.github.com/swinton/03e84635b45c78353b1f71e41007fc7c - gh api --method PUT /repos/{owner}/{repo}/contents/pom.xml \ - --field message="${MESSAGE}" \ - --field content="${CONTENT}" \ - --field encoding="base64" \ - --field branch="${GITHUB_REF_NAME}" \ - --field sha="${SHA}" - - git fetch - git reset --hard "origin/${GITHUB_REF_NAME}" + git config user.name "dependencytrack-bot" + git config user.email "106437498+dependencytrack-bot@users.noreply.github.com" + + if [[ -z $(git ls-remote --quiet --heads origin "${BRANCH_NAME}") ]]; then git checkout -b "${BRANCH_NAME}" - git push origin "${BRANCH_NAME}" else + git fetch origin "${BRANCH_NAME}" git checkout "${BRANCH_NAME}" - SHA=$(git rev-parse ${BRANCH_NAME}:pom.xml) - - gh api --method PUT /repos/{owner}/{repo}/contents/pom.xml \ - --field message="${MESSAGE}" \ - --field content="${CONTENT}" \ - --field encoding="base64" \ - --field branch="${BRANCH_NAME}" \ - --field sha="${SHA}" fi + mvn -B --no-transfer-progress versions:set -DnewVersion=${VERSION} + + git add pom.xml + git commit -m "prepare-release: set version to ${VERSION}" + git push origin "${BRANCH_NAME}" + + git tag "${VERSION}" + git push origin "${VERSION}" + - name: Create GitHub Release env: - GITHUB_TOKEN: ${{ secrets.BOT_RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.BOT_RELEASE_GITHUB_TOKEN }} + RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} + RELEASE_BRANCH: ${{ needs.prepare-release.outputs.release-branch }} run: |- - gh release create "${{ needs.prepare-release.outputs.version }}" \ - --target "${{ needs.prepare-release.outputs.release-branch }}" \ - --title "${{ needs.prepare-release.outputs.version }}" \ - --generate-notes + gh release create "${RELEASE_VERSION}" \ + --target "${RELEASE_BRANCH}" \ + --title "${RELEASE_VERSION}" \ + --generate-notes \ + --draft post-release: runs-on: ubuntu-latest + permissions: + contents: write # Required to push pom.xml update needs: - prepare-release - create-release @@ -122,20 +113,20 @@ jobs: with: ref: ${{ needs.prepare-release.outputs.release-branch }} - - name: Set SNAPSHOT Version after Release - run: mvn -B --no-transfer-progress versions:set -DnewVersion=${NEXT_VERSION} + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # tag=v5.2.0 + with: + distribution: 'temurin' + java-version: '21' + cache: 'maven' - - name: Commit SNAPSHOT Version - env: - GITHUB_TOKEN: ${{ secrets.BOT_RELEASE_TOKEN }} + - name: Set SNAPSHOT Version after Release run: |- - MESSAGE="prepare-iteration: set version to ${NEXT_VERSION}" - CONTENT=$(base64 -i pom.xml) - SHA=$(git rev-parse ${BRANCH_NAME}:pom.xml) - - gh api --method PUT /repos/{owner}/{repo}/contents/pom.xml \ - --field message="${MESSAGE}" \ - --field content="${CONTENT}" \ - --field encoding="base64" \ - --field branch="${BRANCH_NAME}" \ - --field sha="${SHA}" + git config user.name "dependencytrack-bot" + git config user.email "106437498+dependencytrack-bot@users.noreply.github.com" + + mvn -B --no-transfer-progress versions:set -DnewVersion=${NEXT_VERSION} + + git add pom.xml + git commit -m "prepare-iteration: set version to ${NEXT_VERSION}" + git push origin "${BRANCH_NAME}" \ No newline at end of file diff --git a/.github/workflows/ci-test-pr-coverage.yml b/.github/workflows/ci-test-pr-coverage.yml index 615d89d3d6..1541063a69 100644 --- a/.github/workflows/ci-test-pr-coverage.yml +++ b/.github/workflows/ci-test-pr-coverage.yml @@ -18,15 +18,18 @@ jobs: && github.event.workflow_run.conclusion == 'success' steps: - name: Download PR test coverage report - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # tag=v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # tag=v8.0.1 with: name: pr-test-coverage-report github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - name: Report Coverage to Codacy + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} run: |- bash <(curl -Ls https://coverage.codacy.com/get.sh) report \ - --project-token ${{ secrets.CODACY_PROJECT_TOKEN }} \ - --commit-uuid ${{ github.event.workflow_run.head_sha }} \ + --project-token "${CODACY_PROJECT_TOKEN}" \ + --commit-uuid "${HEAD_SHA}" \ --coverage-reports ./target/jacoco-ut/jacoco.xml \ --language Java diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index ec650f3af4..32874e51e3 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -34,6 +34,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Set up JDK uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # tag=v5.2.0 @@ -52,7 +54,7 @@ jobs: # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ - name: Publish test coverage if: ${{ github.event_name != 'pull_request' && github.repository_owner == 'DependencyTrack' }} - uses: codacy/codacy-coverage-reporter-action@v1.3.0 + uses: codacy/codacy-coverage-reporter-action@89d6c85cfafaec52c72b6c5e8b2878d33104c699 # tag=v1.3.0 with: project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} language: Java diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index 30e6579169..1d37bd0ede 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -10,6 +10,8 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false - name: Dependency Review uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # tag=v4.9.0 diff --git a/docs/_config.yml b/docs/_config.yml index 8731aafeb2..61367df92a 100755 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -6,7 +6,7 @@ url: "https://docs.dependencytrack.org" baseurl: show_full_navigation: true -version: v4.13 +version: v4.14 # Values for the jekyll-seo-tag gem (https://github.com/jekyll/jekyll-seo-tag) logo: /siteicon.png diff --git a/docs/_docs/usage/policy-compliance.md b/docs/_docs/usage/policy-compliance.md index 9ade41ed97..06f1d4b677 100644 --- a/docs/_docs/usage/policy-compliance.md +++ b/docs/_docs/usage/policy-compliance.md @@ -48,11 +48,13 @@ will not result in a policy violation. ## Operational Violation Policy conditions can specify zero or more: +* Age (publish date of the package version) * Coordinates (group, name, version) * Package URL * CPE * SWID Tag ID * Hash (MD5, SHA, SHA3, Blake2b, Blake3) +* Version distance (used version vs latest available version) This allows organizations to create lists of allowable and/or prohibited components. Future versions of Dependency-Track will incorporate additional operational parameters into the policy framework. diff --git a/docs/_posts/2026-03-09-v4.14.0.md b/docs/_posts/2026-03-09-v4.14.0.md index 2633c93433..2696c90719 100644 --- a/docs/_posts/2026-03-09-v4.14.0.md +++ b/docs/_posts/2026-03-09-v4.14.0.md @@ -89,15 +89,15 @@ Special thanks to everyone who contributed code to implement enhancements and fi | Algorithm | Checksum | |:----------|:---------| -| SHA-1 | | -| SHA-256 | | +| SHA-1 | a06d7f57876befc80b6653fcc44b321958388f12 | +| SHA-256 | 2e3d5bcfb7b5d4ad4daf789bc5ca3802ef05d012c516090e8bc5323f46585f53 | ###### dependency-track-bundled.jar | Algorithm | Checksum | |:----------|:---------| -| SHA-1 | | -| SHA-256 | | +| SHA-1 | 6573a4522dd84520859ab951d86d8a9e4dd43fb2 | +| SHA-256 | a8edd7c94ba811bae73d9213d769687c493e1bd95435dbe39dfeee28ff1f8008 | ###### frontend-dist.zip @@ -172,4 +172,4 @@ Special thanks to everyone who contributed code to implement enhancements and fi [@WoozyMasta]: https://github.com/WoozyMasta [@wengct]: https://github.com/wengct -[dpkg sorting algorithm]: https://manpages.debian.org/stretch/dpkg-dev/deb-version.5.en.html#Sorting_algorithm \ No newline at end of file +[dpkg sorting algorithm]: https://manpages.debian.org/stretch/dpkg-dev/deb-version.5.en.html#Sorting_algorithm diff --git a/docs/_posts/2026-04-03-v4.14.1.md b/docs/_posts/2026-04-03-v4.14.1.md new file mode 100644 index 0000000000..8dac1b259a --- /dev/null +++ b/docs/_posts/2026-04-03-v4.14.1.md @@ -0,0 +1,81 @@ +--- +title: v4.14.1 +type: patch +--- + +**Features:** + +* Add support for NuGet versioning scheme - [apiserver/#5958] +* Add support for Composer versioning scheme - [apiserver/#5963] +* Document age and version distance operational policy criteria - [apiserver/#5964] +* Use ecosystem-aware version comparison for latest version detection - [apiserver/#5995] +* Support Sonatype Guide tokens for OSS Index analyzer - [apiserver/#5996] +* Improve Chinese translations - [frontend/#1490] + +**Fixes:** + +* Fix PURL-specific version matching being bypassed for components with CPE - [apiserver/#5959] +* Fix wasteful existence queries - [apiserver/#5960] +* Fix potentially wrong version being used for CPE comparison - [apiserver/#5962] +* Fix scheduled notification query failing when ID columns are not of type BIGINT - [apiserver/#5979] +* Avoid NPE when computing Trivy pkgType - [apiserver/#5987] +* Remove leading whitespace from vulnerability badge SVG template - [apiserver/#6000] +* Fix Japanese Trivy analyzer strings - [frontend/#1489] + +For a complete list of changes, refer to the respective GitHub milestones: + +* [API server milestone 4.14.1](https://github.com/DependencyTrack/dependency-track/milestone/50?closed=1) +* [Frontend milestone 4.14.1](https://github.com/DependencyTrack/frontend/milestone/35?closed=1) + +We thank all organizations and individuals who contributed to this release, from logging issues to taking part in discussions on GitHub & Slack to testing of fixes. + +Special thanks to everyone who contributed code to implement enhancements and fix defects: + +[@Zureno], [@jonbally], [@retanoj], [@shayFoo], [@stohrendorf] + +###### dependency-track-apiserver.jar + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | 750b0c768208d7c6b7e32e8f1a7500eb94788069 | +| SHA-256 | 142bdfa36defffc2304d03f9ef7ecd162f1185dcbc00933a73529cac7f12980c | + +###### dependency-track-bundled.jar + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | 61eac5828458dfea46507c26f3384bb452ebeefe | +| SHA-256 | 6cedc727a3f8eb2343397e50a1b5515a99c2a361b7c55aa60dbeff85c1f4af2d | + +###### frontend-dist.zip + +| Algorithm | Checksum | +|:----------|:-----------------------------------------------------------------| +| SHA-1 | a08b4280aad4e9946908ca6fd05e1fbc0ad0f1af | +| SHA-256 | e13d9b729d2082fcfb440bc1deb6f373290d1ead414447d8834368b4dbceec27 | + +###### Software Bill of Materials (SBOM) + +* API Server: [bom.json](https://github.com/DependencyTrack/dependency-track/releases/download/4.14.1/bom.json) +* Frontend: [bom.json](https://github.com/DependencyTrack/frontend/releases/download/4.14.1/bom.json) + +[apiserver/#5958]: https://github.com/DependencyTrack/dependency-track/pull/5958 +[apiserver/#5959]: https://github.com/DependencyTrack/dependency-track/pull/5959 +[apiserver/#5960]: https://github.com/DependencyTrack/dependency-track/pull/5960 +[apiserver/#5962]: https://github.com/DependencyTrack/dependency-track/pull/5962 +[apiserver/#5963]: https://github.com/DependencyTrack/dependency-track/pull/5963 +[apiserver/#5964]: https://github.com/DependencyTrack/dependency-track/pull/5964 +[apiserver/#5979]: https://github.com/DependencyTrack/dependency-track/pull/5979 +[apiserver/#5987]: https://github.com/DependencyTrack/dependency-track/pull/5987 +[apiserver/#5995]: https://github.com/DependencyTrack/dependency-track/pull/5995 +[apiserver/#5996]: https://github.com/DependencyTrack/dependency-track/pull/5996 +[apiserver/#6000]: https://github.com/DependencyTrack/dependency-track/pull/6000 + +[frontend/#1489]: https://github.com/DependencyTrack/frontend/pull/1489 +[frontend/#1490]: https://github.com/DependencyTrack/frontend/pull/1490 + +[@Zureno]: https://github.com/Zureno +[@jonbally]: https://github.com/jonbally +[@retanoj]: https://github.com/retanoj +[@shayFoo]: https://github.com/shayFoo +[@stohrendorf]: https://github.com/stohrendorf diff --git a/pom.xml b/pom.xml index a86de8bd45..10e38cddd1 100644 --- a/pom.xml +++ b/pom.xml @@ -85,34 +85,34 @@ 21 - 4.14.0 + 4.14.1 ${project.parent.version} 4.3.0 0.1.2 13.3.0 - 1.28.1 - 1.28.1 - 1.28.1 + 1.28.2 + 1.28.2 + 1.28.2 3.0.1 1.28.0 1.15.0 - 0.153.1 + 0.153.2 1.0.1 12.1.0 2.1.8 20251224 8.11.4 - 3.9.13 + 3.9.14 5.15.0 9.0.3 1.5.0 4.1.1 - 4.34.0 - 2.3.0 - 2.1.38 + 4.34.1 + 2.4.0 + 2.1.39 2.3.0 - 2.0.3 - 0.16.1 + 2.0.4 + 0.18.0 2.35.2 7.1.1 2.1.1 @@ -122,7 +122,7 @@ 1.330 1.4.0 - 13.2.1.jre11 + 13.4.0.jre11 8.2.0 42.7.10 @@ -558,7 +558,7 @@ io.github.ascopes protobuf-maven-plugin - 5.0.2 + 5.1.0 ${lib.protobuf-java.version} diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index b53241d7a8..6544fe87d5 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -25,9 +25,9 @@ ARG UID=1000 ARG GID=1000 ARG WAR_FILENAME=dependency-track-apiserver.jar -FROM eclipse-temurin:25.0.2_10-jre-jammy@sha256:2866f12d7d9dc8d33f018d87fa4e0e9befe066d94affe8e22d72357877f907de AS jre-build +FROM eclipse-temurin:25.0.2_10-jre-jammy@sha256:305fb0c3e5f47ea70ed6670845deb8bddbc362ad4c42354e85273ec9a31bd064 AS jre-build -FROM debian:stable-slim@sha256:85dfcffff3c1e193877f143d05eaba8ae7f3f95cb0a32e0bc04a448077e1ac69 +FROM debian:stable-slim@sha256:99fc6d2a0882fcbcdc452948d2d54eab91faafc7db037df82425edcdcf950e1f ARG COMMIT_SHA ARG APP_VERSION diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index 9d5b542a4e..39ef61858f 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -710,12 +710,9 @@ public long deleteComponentPropertyByUuid(final Component component, final UUID public boolean hasComponents(final Project project) { final Query query = pm.newQuery(Component.class, "project == :project"); query.setParameters(project); - query.setResult("count(this)"); - try { - return query.executeResultUnique(Long.class) > 0; - } finally { - query.closeAll(); - } + query.setRange(0, 1); + query.setResult("id"); + return !executeAndCloseResultList(query, Long.class).isEmpty(); } public void synchronizeComponentProperties(final Component component, final List properties) { diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 3f2fbd8d52..aabbaf291a 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -1803,12 +1803,9 @@ public boolean doesProjectExist(final String name, final String version) { "name", name )); } - query.setResult("count(this)"); - try { - return query.executeResultUnique(Long.class) > 0; - } finally { - query.closeAll(); - } + query.setRange(0, 1); + query.setResult("id"); + return !executeAndCloseResultList(query, Long.class).isEmpty(); } private static boolean isChildOf(Project project, UUID uuid) { diff --git a/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java index 68a3464ca1..cdca62409d 100644 --- a/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java @@ -213,12 +213,9 @@ private void deleteServiceComponents(Project project) { public boolean hasServiceComponents(final Project project) { final Query query = pm.newQuery(ServiceComponent.class, "project == :project"); query.setParameters(project); - query.setResult("count(this)"); - try { - return query.executeResultUnique(Long.class) > 0; - } finally { - query.closeAll(); - } + query.setRange(0, 1); + query.setResult("id"); + return !executeAndCloseResultList(query, Long.class).isEmpty(); } /** diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index ebaf1d41f2..083db0a96b 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -1069,12 +1069,9 @@ public boolean hasAffectedVersionAttribution(final Vulnerability vulnerability, "vuln", vulnerability, "vs", vulnerableSoftware )); - query.setResult("count(this)"); - try { - return query.executeResultUnique(Long.class) > 0; - } finally { - query.closeAll(); - } + query.setRange(0, 1); + query.setResult("id"); + return !executeAndCloseResultList(query, Long.class).isEmpty(); } } diff --git a/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java index 300e1ab036..91cf93c97f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ViolationAnalysisResource.java @@ -35,6 +35,15 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; @@ -46,15 +55,7 @@ import org.dependencytrack.resources.v1.vo.ViolationAnalysisRequest; import org.dependencytrack.util.NotificationUtil; -import jakarta.validation.Validator; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import java.util.Objects; /** * JAX-RS resources for processing violation analysis decisions. @@ -168,6 +169,10 @@ public Response updateAnalysis(ViolationAnalysisRequest request) { if (ViolationAnalysisState.NOT_SET != request.getAnalysisState()) { qm.makeViolationAnalysisComment(analysis, String.format("%s → %s", ViolationAnalysisState.NOT_SET, request.getAnalysisState()), commenter); } + if (Objects.equals(request.isSuppressed(), true)) { + suppressionChange = true; + qm.makeViolationAnalysisComment(analysis, "Suppressed", commenter); + } } final String comment = StringUtils.trimToNull(request.getComment()); diff --git a/src/main/java/org/dependencytrack/tasks/ScheduledNotificationDispatchTask.java b/src/main/java/org/dependencytrack/tasks/ScheduledNotificationDispatchTask.java index 7c2b93a1dd..1d62a8c691 100644 --- a/src/main/java/org/dependencytrack/tasks/ScheduledNotificationDispatchTask.java +++ b/src/main/java/org/dependencytrack/tasks/ScheduledNotificationDispatchTask.java @@ -23,6 +23,7 @@ import alpine.event.framework.Subscriber; import alpine.notification.Notification; import alpine.notification.NotificationLevel; +import alpine.server.util.DbUtil; import org.dependencytrack.event.ScheduledNotificationDispatchEvent; import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Component; @@ -398,10 +399,13 @@ private List getNewFindingsSince( queryParams.put("projectId" + index, projectId); } + // MySQL and MariaDB do not support casting to BIGINT. Use SIGNED instead. + final String bigintCastTarget = DbUtil.isMysql() ? "SIGNED" : "BIGINT"; + final Query query = qm.getPersistenceManager().newQuery(Query.SQL, /* language=SQL */ """ - SELECT "COMPONENT"."PROJECT_ID" AS "projectId" - , "COMPONENT"."ID" AS "componentId" - , "COMPONENTS_VULNERABILITIES"."VULNERABILITY_ID" AS "vulnabilityId" + SELECT CAST("COMPONENT"."PROJECT_ID" AS %1$s) AS "projectId" + , CAST("COMPONENT"."ID" AS %1$s) AS "componentId" + , CAST("COMPONENTS_VULNERABILITIES"."VULNERABILITY_ID" AS %1$s) AS "vulnerabilityId" , "FINDINGATTRIBUTION"."ANALYZERIDENTITY" AS "analyzerIdentity" , "FINDINGATTRIBUTION"."ATTRIBUTED_ON" AS "attributedOn" , "FINDINGATTRIBUTION"."REFERENCE_URL" AS "referenceUrl" @@ -417,8 +421,8 @@ private List getNewFindingsSince( ON "ANALYSIS"."COMPONENT_ID" = "COMPONENT"."ID" AND "ANALYSIS"."VULNERABILITY_ID" = "COMPONENTS_VULNERABILITIES"."VULNERABILITY_ID" WHERE "FINDINGATTRIBUTION"."ATTRIBUTED_ON" >= :sinceAttributedOn - AND %s - """.formatted(projectIdCondition)); + AND %2$s + """.formatted(bigintCastTarget, projectIdCondition)); query.setNamedParameters(queryParams); try { return List.copyOf(query.executeResultList(NewFinding.class)); diff --git a/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java index 08ed5d350e..1fde488b6d 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java @@ -22,11 +22,14 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.packageurl.PackageURL; +import io.github.nscuro.versatile.VersionFactory; +import io.github.nscuro.versatile.spi.InvalidVersionException; +import io.github.nscuro.versatile.spi.Version; +import io.github.nscuro.versatile.version.KnownVersioningSchemes; import jakarta.ws.rs.core.UriBuilder; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.util.EntityUtils; -import org.apache.maven.artifact.versioning.ComparableVersion; import org.dependencytrack.exception.MetaAnalyzerException; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; @@ -52,9 +55,9 @@ public class ComposerMetaAnalyzer extends AbstractMetaAnalyzer { /** * @see Packagist's API - * doc for - * "Getting package data - Using the Composer v1 metadata (DEPRECATED)" - * Example: https://repo.packagist.org/p/monolog/monolog.json + * doc for + * "Getting package data - Using the Composer v1 metadata (DEPRECATED)" + * Example: https://repo.packagist.org/p/monolog/monolog.json */ private static final String PACKAGE_META_DATA_PATH_PATTERN_V1 = "/p/%package%.json"; @@ -66,10 +69,10 @@ public class ComposerMetaAnalyzer extends AbstractMetaAnalyzer { * Some of the properties of the root package.json are documented at * https://github.com/composer/composer/blob/main/doc/05-repositories.md * Properties to investigate / implement: - * + *

* - security-advisories: very relevant, but only in a VulnerabilityAnalyzer (or * mirrored VulnerabilitySource) context - * + *

* - providers-lazy-url: old v1 construct for which I haven't seen any example, * in v2 the metadata-url is used for this. seems like it's not relevant for DT * - list: returns only package names, seems like repo.packagist.org (and .com?) @@ -273,7 +276,7 @@ private void loadIncludedPackages(final JSONObject repoRoot, final JSONObject da } private MetaModel analyzeFromMetadataUrl(final MetaModel meta, final Component component, - final String packageMetaDataPathPattern) { + final String packageMetaDataPathPattern) { final String composerPackageMetadataFilename = packageMetaDataPathPattern.replaceAll("%package%", getComposerPackageName(component)); final String url; @@ -307,7 +310,7 @@ private MetaModel analyzeFromMetadataUrl(final MetaModel meta, final Component c if (!responsePackages.has(expectedResponsePackage)) { // the package no longer exists - for v2 there's no example (yet), v1 example // https://repo.packagist.org/p/magento/adobe-ims.json - LOGGER.debug("%s: Package no longer exists in repository %s.". formatted(component.getPurl(), this.repositoryId)); + LOGGER.debug("%s: Package no longer exists in repository %s.".formatted(component.getPurl(), this.repositoryId)); return meta; } @@ -353,38 +356,40 @@ private JSONObject expandPackages(JSONObject packages) { } private MetaModel analyzePackageVersions(final MetaModel meta, Component component, JSONObject packageVersions) { - final ComparableVersion latestVersion = new ComparableVersion(stripLeadingV(component.getPurl().getVersion())); + Version latestVersion = null; final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); LOGGER.debug("%s: analyzing package versions in %s: ".formatted(component.getPurl(), this.repositoryId)); - packageVersions.keySet().forEach(item -> { - JSONObject packageVersion = packageVersions.getJSONObject((String) item); + for (final String item : packageVersions.keySet()) { + final JSONObject packageVersion = packageVersions.getJSONObject(item); // Sometimes the JSON key differs from the the version inside the JSON value. The latter is leading. - String version = packageVersion.getString("version"); - if (version.startsWith("dev-") || version.endsWith("-dev")) { - // dev versions are excluded, since they are not pinned but a VCS-branch. - // this case doesn't seem to happen anymore with V2, as dev (untagged) releases - // are not part of the response anymore - return; - } + final String version = packageVersion.getString("version"); // Some (old?) repositories like composer.amasty.com/enterprise do not include a - // 'version_normalized' field - // TODO Should we attempt to normalize ourselves? The PHP code uses something - // that results in 4 parts instead of 3, i.e. 2.3.8.0 instead of 2.3.8. Not sure - // if that works with Semver4j - String version_normalized = packageVersion.getString("version"); + // 'version_normalized' field. versatile handles both 3- and 4-part version + // strings (e.g. 2.3.8 and 2.3.8.0) natively, so falling back to the raw version + // is safe. + String version_normalized = version; if (packageVersion.has("version_normalized")) { version_normalized = packageVersion.getString("version_normalized"); } - ComparableVersion currentComparableVersion = new ComparableVersion(version_normalized); - if (currentComparableVersion.compareTo(latestVersion) < 0) { - // smaller version can be skipped - return; + final Version currentVersion; + try { + currentVersion = VersionFactory.forScheme(KnownVersioningSchemes.SCHEME_COMPOSER, version_normalized); + } catch (InvalidVersionException e) { + LOGGER.debug("%s: Skipping unparseable Composer version %s in repository %s".formatted(component.getPurl(), version_normalized, this.repositoryId), e); + continue; + } + if (!currentVersion.isStable()) { + continue; + } + + if (latestVersion != null && currentVersion.compareTo(latestVersion) < 0) { + continue; } - latestVersion.parseVersion(stripLeadingV(version_normalized)); + latestVersion = currentVersion; meta.setLatestVersion(version); if (packageVersion.has("time")) { @@ -401,16 +406,10 @@ private MetaModel analyzePackageVersions(final MetaModel meta, Component compone // do not include the name field for a version, so print purl LOGGER.warn("%s: Field 'time' not present in metadata in repository %s".formatted(component.getPurl(), this.repositoryId)); } - }); + } return meta; } - private static String stripLeadingV(String s) { - return s.startsWith("v") || s.startsWith("V") - ? s.substring(1) - : s; - } - private static boolean isMinified(JSONObject data) { if (data.has("minified") && "composer/2.0".equals(data.getString("minified"))) { return true; diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java index 20d9d73f0c..cec9676fb5 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java @@ -22,10 +22,13 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.packageurl.PackageURL; +import io.github.nscuro.versatile.VersionFactory; +import io.github.nscuro.versatile.spi.InvalidVersionException; +import io.github.nscuro.versatile.spi.Version; +import io.github.nscuro.versatile.version.KnownVersioningSchemes; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.util.EntityUtils; -import org.apache.maven.artifact.versioning.ComparableVersion; import org.dependencytrack.exception.MetaAnalyzerException; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; @@ -103,6 +106,7 @@ public class NugetMetaAnalyzer extends AbstractMetaAnalyzer { * specified a repo URL ending with index.json, it should be considered "fully qualified" and used as is to maximise * compatability with non-nuget.org repos such as Artifactory. If not, preserve the previous Dependency Track * behaviour of appending the nuget.org index to the supplied URL. + * * @param baseUrl the base URL to the repository */ @Override @@ -126,7 +130,7 @@ public void setRepositoryBaseUrl(String baseUrl) { * @param registrationsBaseUrlStem Registrations URL to be set */ public void setRegistrationsBaseUrl(String registrationsBaseUrlStem) { - if(registrationsBaseUrlStem == null || registrationsBaseUrlStem.isBlank()) { + if (registrationsBaseUrlStem == null || registrationsBaseUrlStem.isBlank()) { return; } this.registrationsBaseUrl = stripTrailingSlash(registrationsBaseUrlStem) + "/%s/%s.json"; @@ -158,7 +162,7 @@ public RepositoryType supportedRepositoryType() { */ public MetaModel analyze(final Component component) { - if(component == null) { + if (component == null) { throw new IllegalArgumentException("Component cannot be null"); } @@ -174,7 +178,8 @@ public MetaModel analyze(final Component component) { /** * Attempts to find the latest version of the supplied component and return its published date, if one exists. * Ignores pre-release and unlisted versions. - * @param meta {@link MetaModel} to be updated with detected version information + * + * @param meta {@link MetaModel} to be updated with detected version information * @param component {@link Component} to be looked up in the NuGet repo */ private void performVersionCheck(final MetaModel meta, final Component component) { @@ -189,7 +194,7 @@ private void performVersionCheck(final MetaModel meta, final Component component try { final var packageRegistrationRoot = fetchPackageRegistrationIndex(registrationsBaseUrl, component); - if(packageRegistrationRoot == null) { + if (packageRegistrationRoot == null) { return; } @@ -216,8 +221,9 @@ private void performVersionCheck(final MetaModel meta, final Component component /** * Retrieves the package registration index for the specified component * (e.g. https://api.nuget.org/v3/registration5-gz-semver2/microsoft.data.sqlclient/index.json) and converts to JSON + * * @param registrationsBaseUrl Registration base URL to look up package info - * @param component Component for which retrieve package registration data should be retrieved + * @param component Component for which retrieve package registration data should be retrieved * @return JSONObject containing package data if found or null if data not found * @throws IOException if HTTP request errors */ @@ -234,10 +240,14 @@ private JSONObject fetchPackageRegistrationIndex(final String registrationsBaseU } } + private record PageWithUpperBound(JSONObject page, String upperRaw, Version upperVersion) { + } + /** * Parses the NuGet Registrations to find latest version information. Handles both inline items and paged items. * Sorts pages in descending order by the upper version number - if a listed, final version can be found in that * page, it will be returned or pages will be searched in descending order until a match is found. + * * @param registrationData Registrations to be searched * @return Version metadata if a suitable version found, or null if not * @throws IOException if network error occurs @@ -250,23 +260,41 @@ private AbridgedNugetCatalogEntry findLatestViaRegistrations(final JSONObject re return null; } - // Build a list of pages sorted by descending "upper" property. - final List pageUpperBounds = new ArrayList<>(); + // Pre-parse each page's upper version once, logging any failures exactly once per page. + final List pageUpperBounds = new ArrayList<>(pages.length()); for (int i = 0; i < pages.length(); i++) { final JSONObject page = pages.optJSONObject(i); - if (page != null && page.has(NUGET_KEY_UPPER)) { - pageUpperBounds.add(page); + if (page == null || !page.has(NUGET_KEY_UPPER)) { + continue; } + + final String upperRaw = page.optString(NUGET_KEY_UPPER, ""); + Version upperVersion = null; + try { + upperVersion = VersionFactory.forScheme(KnownVersioningSchemes.SCHEME_NUGET, upperRaw); + } catch (InvalidVersionException e) { + LOGGER.debug("Failed to parse NuGet page upper bound: " + upperRaw, e); + } + + pageUpperBounds.add(new PageWithUpperBound(page, upperRaw, upperVersion)); } - // Sort upper page bounds in descending order to get newest page first e.g, [ "6.1.0", "5.1.0" ] - pageUpperBounds.sort((pageOne, pageTwo) -> { - final ComparableVersion pageOneUpper = new ComparableVersion(pageOne.optString(NUGET_KEY_UPPER, "0")); - final ComparableVersion pageTwoUpper = new ComparableVersion(pageTwo.optString(NUGET_KEY_UPPER, "0")); - return pageTwoUpper.compareTo(pageOneUpper); // descending + // Sort by upper bound in descending order. + // Unparseable bounds sort last with lexicographic fallback. + pageUpperBounds.sort((a, b) -> { + if (a.upperVersion() != null && b.upperVersion() != null) { + return b.upperVersion().compareTo(a.upperVersion()); + } else if (a.upperVersion() != null) { + return -1; + } else if (b.upperVersion() != null) { + return 1; + } + + return b.upperRaw().compareToIgnoreCase(a.upperRaw()); }); - for (final JSONObject page : pageUpperBounds) { + for (final PageWithUpperBound pwub : pageUpperBounds) { + final JSONObject page = pwub.page(); try { final JSONArray leaves = resolveLeaves(page); final AbridgedNugetCatalogEntry bestOnPage = findHighestVersionFromLeaves(leaves, includePreRelease); @@ -286,9 +314,10 @@ private AbridgedNugetCatalogEntry findLatestViaRegistrations(final JSONObject re /** * Parse the page JSON to find item leaves, retrieving data from the repo if needed. Returns null if neither * inline items nor a fetchable @id exist/succeed. + * * @param page Page to be parsed * @return JSONArray containing leaf data if available, null if not - * @throws IOException if network error occurs + * @throws IOException if network error occurs * @throws MetaAnalyzerException if the repo returns an unexpected result */ private JSONArray resolveLeaves(final JSONObject page) throws IOException, MetaAnalyzerException { @@ -322,7 +351,8 @@ private JSONArray resolveLeaves(final JSONObject page) throws IOException, MetaA /** * Scan the supplied leaves to extract the latest listed version. NuGet does not guarantee release order * so scan the entire array although, anecdotally, the collection does generally appear to be in ascending order - * @param leaves Items to be scanned + * + * @param leaves Items to be scanned * @param includePreRelease include pre-release versions in latest version lookup * @return {@link AbridgedNugetCatalogEntry containing the latest version found in the leaves collection */ @@ -333,7 +363,7 @@ private AbridgedNugetCatalogEntry findHighestVersionFromLeaves(final JSONArray l } AbridgedNugetCatalogEntry bestEntry = null; - ComparableVersion newestVersionFound = null; + Version newestVersionFound = null; for (int i = 0; i < leaves.length(); i++) { final JSONObject leaf = leaves.optJSONObject(i); @@ -343,11 +373,21 @@ private AbridgedNugetCatalogEntry findHighestVersionFromLeaves(final JSONArray l entry = parseCatalogEntry(leaf.optJSONObject("catalogEntry")); } - if (entry == null || entry.getVersion() == null || (isPreRelease(entry.getVersion()) && !includePreRelease)) { + if (entry == null || entry.getVersion() == null) { + continue; + } + + final Version entryVersion; + try { + entryVersion = VersionFactory.forScheme(KnownVersioningSchemes.SCHEME_NUGET, entry.getVersion()); + } catch (InvalidVersionException e) { + LOGGER.debug("Skipping NuGet catalog entry with unparseable version %s".formatted(entry.getVersion()), e); + continue; + } + if (!entryVersion.isStable() && !includePreRelease) { continue; } - final ComparableVersion entryVersion = new ComparableVersion(entry.getVersion()); if (newestVersionFound == null || entryVersion.compareTo(newestVersionFound) > 0) { newestVersionFound = entryVersion; bestEntry = entry; @@ -360,6 +400,7 @@ private AbridgedNugetCatalogEntry findHighestVersionFromLeaves(final JSONArray l /** * Parse a single catalog entry to extract the version and published information. Could be extended to include other * fields (such as listed) if required. Returns null immediately if the entry is unlisted. + * * @param catalogEntry Catalog entry to be parsed * @return {@link AbridgedNugetCatalogEntry} if version is valid, null if not */ @@ -368,7 +409,7 @@ private AbridgedNugetCatalogEntry parseCatalogEntry(final JSONObject catalogEntr // Listed is optional so assume package is listed unless explicitly hidden boolean listed = catalogEntry.optBoolean("listed", true); - if(!listed) { + if (!listed) { return null; } @@ -388,19 +429,9 @@ private AbridgedNugetCatalogEntry parseCatalogEntry(final JSONObject catalogEntr return entry; } - /** - * NuGet considers a version string with any suffix after a hyphen to be pre-release according to - * the documentation. This method could be expanded if we need to cover other rules. - * @param version Version string to be tested - * @return True if version matches pre-release conventions, false otherwise - */ - private boolean isPreRelease(final String version) { - return version.contains("-"); - } - /** * Connects to the NuGet repo, retrieves the service index and attempts to find the best RegistrationsBaseUrl + * * @return RegistrationsBaseUrl if found, null otherwise */ private String findRegistrationsBaseUrl() { @@ -436,6 +467,7 @@ private String findRegistrationsBaseUrl() { * compression, SemVer 2 without compression then non-compressed, non-SemVer2. * See vsList, - final Cpe targetCpe, final PackageURL targetPURL, final String targetVersion, final Component component, - final VulnerabilityAnalysisLevel vulnerabilityAnalysisLevel) { + protected void analyzeVersionRange( + QueryManager qm, + List vsList, + Cpe targetCpe, + PackageURL targetPURL, + Component component, + VulnerabilityAnalysisLevel vulnerabilityAnalysisLevel) { boolean ran = false; if (targetCpe != null) { - analyzeCpeVersionRange(qm, vsList, targetCpe, targetVersion, component, vulnerabilityAnalysisLevel); + analyzeCpeVersionRange(qm, vsList, targetCpe, component, vulnerabilityAnalysisLevel); ran = true; } if (targetPURL != null) { - analyzePurlVersionRange(qm, vsList, targetPURL, targetVersion, component, vulnerabilityAnalysisLevel); + analyzePurlVersionRange(qm, vsList, targetPURL, component, vulnerabilityAnalysisLevel); ran = true; } if (!ran) { @@ -83,11 +87,10 @@ protected void analyzePurlVersionRange( QueryManager qm, List vsList, PackageURL targetPurl, - String targetVersion, Component component, VulnerabilityAnalysisLevel vulnerabilityAnalysisLevel) { for (final VulnerableSoftware vs : vsList) { - if (comparePurlVersions(targetPurl, vs, targetVersion)) { + if (matchesPurl(vs, targetPurl) && comparePurlVersions(targetPurl, vs)) { if (vs.getVulnerabilities() != null) { for (final Vulnerability vulnerability : vs.getVulnerabilities()) { NotificationUtil.analyzeNotificationCriteria(qm, vulnerability, component, @@ -103,12 +106,10 @@ private void analyzeCpeVersionRange( QueryManager qm, List vsList, Cpe targetCpe, - String targetVersion, Component component, VulnerabilityAnalysisLevel vulnerabilityAnalysisLevel) { for (final VulnerableSoftware vs : vsList) { - final Boolean isCpeMatch = maybeMatchCpe(vs, targetCpe, targetVersion); - if ((isCpeMatch == null || isCpeMatch) && compareCpeVersions(vs, targetVersion, component)) { + if (matchesCpe(vs, targetCpe) && compareCpeVersions(vs, targetCpe, component)) { if (vs.getVulnerabilities() != null) { for (final Vulnerability vulnerability : vs.getVulnerabilities()) { NotificationUtil.analyzeNotificationCriteria(qm, vulnerability, component, vulnerabilityAnalysisLevel); @@ -123,16 +124,16 @@ private static String toLowerCaseNullable(final String string) { return string == null ? null : string.toLowerCase(); } - private Boolean maybeMatchCpe(final VulnerableSoftware vs, final Cpe targetCpe, final String targetVersion) { + private boolean matchesCpe(final VulnerableSoftware vs, final Cpe targetCpe) { if (targetCpe == null || vs.getCpe23() == null) { - return null; + return false; } final List relations = List.of( Cpe.compareAttribute(vs.getPart(), toLowerCaseNullable(targetCpe.getPart().getAbbreviation())), Cpe.compareAttribute(vs.getVendor(), toLowerCaseNullable(targetCpe.getVendor())), Cpe.compareAttribute(vs.getProduct(), toLowerCaseNullable(targetCpe.getProduct())), - Cpe.compareAttribute(vs.getVersion(), targetVersion), + Cpe.compareAttribute(vs.getVersion(), targetCpe.getVersion()), Cpe.compareAttribute(vs.getUpdate(), targetCpe.getUpdate()), Cpe.compareAttribute(vs.getEdition(), targetCpe.getEdition()), Cpe.compareAttribute(vs.getLanguage(), targetCpe.getLanguage()), @@ -161,7 +162,21 @@ private Boolean maybeMatchCpe(final VulnerableSoftware vs, final Cpe targetCpe, return isMatch; } - private boolean comparePurlVersions(PackageURL componentPurl, VulnerableSoftware vs, String targetVersion) { + private boolean matchesPurl(VulnerableSoftware vs, PackageURL purl) { + if (purl == null) { + return false; + } + + return Objects.equals(vs.getPurlType(), purl.getType()) + && Objects.equals(vs.getPurlNamespace(), purl.getNamespace()) + && Objects.equals(vs.getPurlName(), purl.getName()); + } + + private boolean comparePurlVersions(PackageURL componentPurl, VulnerableSoftware vs) { + if (componentPurl.getVersion() == null) { + return false; + } + final String componentDistroQualifier = PurlUtil.getDistroQualifier(componentPurl); final String vsDistroQualifier = PurlUtil.getDistroQualifier(vs.getPurl()); @@ -199,7 +214,7 @@ private boolean comparePurlVersions(PackageURL componentPurl, VulnerableSoftware .flatMap(KnownVersioningSchemes::fromPurl) .orElse(KnownVersioningSchemes.SCHEME_GENERIC); - return compareWithVers(vs, targetVersion, versioningScheme); + return compareWithVers(vs, componentPurl.getVersion(), versioningScheme); } /** @@ -208,19 +223,19 @@ private boolean comparePurlVersions(PackageURL componentPurl, VulnerableSoftware * versionStartIncluding. * * @param vs a reference to the vulnerable software to compare - * @param targetVersion the version to compare + * @param targetCpe the CPE to compare against * @return true if the target version is matched; otherwise * false *

* Ported from Dependency-Check v5.2.1 */ - private boolean compareCpeVersions(VulnerableSoftware vs, String targetVersion, Component component) { + private boolean compareCpeVersions(VulnerableSoftware vs, Cpe targetCpe, Component component) { // Modified from original by @nscuro. // Special cases for CPE matching of ANY (*) and NA (*) versions. // These don't make sense to use for version range comparison and // can be dealt with upfront based on the matching documentation: // https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7696.pdf - if ("*".equals(targetVersion)) { + if ("*".equals(targetCpe.getVersion())) { // | No. | Source A-V | Target A-V | Relation | // | :-- | :------------- | :--------- | :------- | // | 1 | ANY | ANY | EQUAL | @@ -228,7 +243,7 @@ private boolean compareCpeVersions(VulnerableSoftware vs, String targetVersion, // | 13 | i | ANY | SUBSET | // | 15 | m + wild cards | ANY | SUBSET | return true; - } else if ("-".equals(targetVersion)) { + } else if ("-".equals(targetCpe.getVersion())) { // | No. | Source A-V | Target A-V | Relation | // | :-- | :------------- | :--------- | :------- | // | 2 | ANY | NA | SUPERSET | @@ -242,7 +257,7 @@ private boolean compareCpeVersions(VulnerableSoftware vs, String targetVersion, // Modified from original by Steve Springett // Added null check: vs.getVersion() != null as purl sources that use version ranges may not have version populated. - if (!result && vs.getVersion() != null && Cpe.compareAttribute(vs.getVersion(), targetVersion) != Relation.DISJOINT) { + if (!result && vs.getVersion() != null && Cpe.compareAttribute(vs.getVersion(), targetCpe.getVersion()) != Relation.DISJOINT) { return true; } @@ -252,7 +267,7 @@ private boolean compareCpeVersions(VulnerableSoftware vs, String targetVersion, .flatMap(KnownVersioningSchemes::fromPurl) .orElse(KnownVersioningSchemes.SCHEME_GENERIC); - return compareWithVers(vs, targetVersion, versioningScheme); + return compareWithVers(vs, targetCpe.getVersion(), versioningScheme); } private boolean compareWithVers(VulnerableSoftware vs, String targetVersion, String versioningScheme) { diff --git a/src/main/java/org/dependencytrack/tasks/scanners/InternalAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/InternalAnalysisTask.java index f1894c7a8c..8aeeb2abc9 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/InternalAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/InternalAnalysisTask.java @@ -21,6 +21,7 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.event.framework.Subscriber; +import com.github.packageurl.PackageURL; import org.dependencytrack.event.InternalAnalysisEvent; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; @@ -28,10 +29,10 @@ import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.search.FuzzyVulnerableSoftwareSearchManager; +import us.springett.parsers.cpe.Cpe; import us.springett.parsers.cpe.CpeParser; import us.springett.parsers.cpe.exceptions.CpeParsingException; -import java.util.Collections; import java.util.List; /** @@ -98,8 +99,8 @@ private void versionRangeAnalysis(final QueryManager qm, final Component compone (!component.isInternal() || !super.isEnabled(ConfigPropertyConstants.SCANNER_INTERNAL_FUZZY_EXCLUDE_INTERNAL)); final boolean excludeComponentsWithPurl = super.isEnabled(ConfigPropertyConstants.SCANNER_INTERNAL_FUZZY_EXCLUDE_PURL); - com.github.packageurl.PackageURL parsedPurl = null; - us.springett.parsers.cpe.Cpe parsedCpe = null; + PackageURL parsedPurl = null; + Cpe parsedCpe = null; if (component.getCpe() != null) { try { @@ -107,41 +108,12 @@ private void versionRangeAnalysis(final QueryManager qm, final Component compone } catch (CpeParsingException e) { LOGGER.warn("An error occurred while parsing: " + component.getCpe() + " - The CPE is invalid and will be discarded. " + e.getMessage()); } - } else if (component.getPurl() != null) { - parsedPurl = component.getPurl(); - } - - List vsList = Collections.emptyList(); - - String componentVersion = component.getVersion(); - if (componentVersion == null || componentVersion.isBlank()) { - if (parsedCpe != null && parsedCpe.getVersion() != null && !parsedCpe.getVersion().isBlank()) { - componentVersion = parsedCpe.getVersion(); - } else if (parsedPurl != null && parsedPurl.getVersion() != null && !parsedPurl.getVersion().isBlank()) { - componentVersion = parsedPurl.getVersion(); - } else { - LOGGER.debug("Neither CPE, PURL, nor component version provide a version - skipping analysis"); - return; - } - } - // In some cases, componentVersion may be null, such as when a Package URL does not have a version specified - if (componentVersion == null) { - return; } - // https://github.com/DependencyTrack/dependency-track/issues/1574 - // Some ecosystems use the "v" version prefix (e.g. v1.2.3) for their components. - // However, both the NVD and GHSA store versions without that prefix. - // For this reason, the prefix is stripped before running analyzeVersionRange. - // - // REVISIT THIS WHEN ADDING NEW VULNERABILITY SOURCES! - if (componentVersion.length() > 1 && componentVersion.startsWith("v")) { - if (componentVersion.matches("v0.0.0-\\d{14}-[a-f0-9]{12}")) { - componentVersion = componentVersion.substring(7,11) + "-" + componentVersion.substring(11,13) + "-" + componentVersion.substring(13,15); - } else { - componentVersion = componentVersion.substring(1); - } + if (component.getPurl() != null) { + parsedPurl = component.getPurl(); } - + + List vsList; if (parsedCpe != null) { vsList = qm.getAllVulnerableSoftware(parsedCpe.getPart().getAbbreviation(), parsedCpe.getVendor(), parsedCpe.getProduct(), component.getPurl()); @@ -153,7 +125,8 @@ private void versionRangeAnalysis(final QueryManager qm, final Component compone FuzzyVulnerableSoftwareSearchManager fm = new FuzzyVulnerableSoftwareSearchManager(excludeComponentsWithPurl); vsList = fm.fuzzyAnalysis(qm, component, parsedCpe); } - super.analyzeVersionRange(qm, vsList, parsedCpe, parsedPurl, componentVersion, component, vulnerabilityAnalysisLevel); + + super.analyzeVersionRange(qm, vsList, parsedCpe, parsedPurl, component, vulnerabilityAnalysisLevel); } } diff --git a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java index cab86432ed..aa1a331728 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java @@ -202,21 +202,22 @@ public void inform(final Event e) { SCANNER_OSSINDEX_API_TOKEN.getGroupName(), SCANNER_OSSINDEX_API_TOKEN.getPropertyName() ); - if (apiUsernameProperty == null - || apiUsernameProperty.getPropertyValue() == null - || apiTokenProperty == null - || apiTokenProperty.getPropertyValue() == null) { - LOGGER.warn("An API username or token has not been specified for use with OSS Index; Skipping"); + if (apiTokenProperty == null || apiTokenProperty.getPropertyValue() == null) { + LOGGER.warn("An API token has not been specified for use with OSS Index; Skipping"); return; - } else { - try { - apiUsername = apiUsernameProperty.getPropertyValue(); - apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); - } catch (Exception ex) { - // OSS Index will stop supporting unauthenticated requests - LOGGER.error("An error occurred decrypting the OSS Index API Token; Skipping", ex); + } + try { + apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); + } catch (Exception ex) { + LOGGER.error("An error occurred decrypting the OSS Index API Token; Skipping", ex); + return; + } + if (!isBearerToken(apiToken)) { + if (apiUsernameProperty == null || apiUsernameProperty.getPropertyValue() == null) { + LOGGER.warn("An API username has not been specified for use with OSS Index; Skipping"); return; } + apiUsername = apiUsernameProperty.getPropertyValue(); } aliasSyncEnabled = super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ALIAS_SYNC_ENABLED); } @@ -322,6 +323,10 @@ private static String minimizePurl(final PackageURL purl) { return purl.getCoordinates().replaceFirst("@v", "@"); } + private static boolean isBearerToken(final String token) { + return token != null && token.startsWith("sonatype_pat_"); + } + /** * Submits the payload to the Sonatype OSS Index service */ @@ -331,7 +336,9 @@ private List submit(final JSONObject payload) throws Throwable request.addHeader(HttpHeaders.CONTENT_TYPE, "application/json"); request.addHeader(HttpHeaders.USER_AGENT, ManagedHttpClientFactory.getUserAgent()); request.setEntity(new StringEntity(payload.toString())); - if (apiUsername != null && apiToken != null) { + if (isBearerToken(apiToken)) { + request.addHeader("Authorization", "Bearer " + apiToken); + } else if (apiUsername != null && apiToken != null) { request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(apiUsername, apiToken)); } try (final CloseableHttpResponse response = RETRY.executeCheckedSupplier(() -> HttpClientPool.getClient().execute(request))) { diff --git a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java index f1752e8a76..f6b606ed5b 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java @@ -245,15 +245,16 @@ public void analyze(final List components) { String arch = null; Integer epoch = null; - if (component.getPurl().getQualifiers() != null) { - arch = component.getPurl().getQualifiers().get("arch"); + final var qualifiers = component.getPurl().getQualifiers(); + if (qualifiers != null) { + arch = qualifiers.get("arch"); - String tmpEpoch = component.getPurl().getQualifiers().get("epoch"); + String tmpEpoch = qualifiers.get("epoch"); if (tmpEpoch != null) { epoch = Integer.parseInt(tmpEpoch); } - String distro = component.getPurl().getQualifiers().get("distro"); + String distro = qualifiers.get("distro"); if (distro != null) { pkgType = URLDecoder.decode(distro, StandardCharsets.UTF_8); @@ -272,7 +273,7 @@ public void analyze(final List components) { } else if (!pkgType.contains("-") && property.getPropertyName().equals("trivy:PkgType")) { pkgType = property.getPropertyValue(); - String distro = component.getPurl().getQualifiers().get("distro"); + String distro = qualifiers == null ? null : qualifiers.get("distro"); if (distro != null) { pkgType += "-" + URLDecoder.decode(distro, StandardCharsets.UTF_8); diff --git a/src/main/resources/templates/badge/project-vulns.peb b/src/main/resources/templates/badge/project-vulns.peb index 0c9c83e0b2..43ee44024d 100644 --- a/src/main/resources/templates/badge/project-vulns.peb +++ b/src/main/resources/templates/badge/project-vulns.peb @@ -1,7 +1,7 @@ -{% set width = 234 %} -{% if unassigned equals "0" %} - {% set width = 204 %} -{% endif %} +{%- set width = 234 -%} +{%- if unassigned equals "0" -%} + {%- set width = 204 -%} +{%- endif -%} diff --git a/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java index 933f55cc85..c7d231e4b5 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java @@ -257,6 +257,66 @@ void updateAnalysisCreateNewTest() throws Exception { assertThat(notification.getContent()).isEqualTo("An violation analysis decision was made to a policy violation affecting a project"); } + @Test + void updateAnalysisCreateNewLogsSuppressedTest() throws Exception { + initializeWithPermissions(Permissions.POLICY_VIOLATION_ANALYSIS); + + final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + + var component = new Component(); + component.setProject(project); + component.setName("Acme Component"); + component.setVersion("1.0"); + component = qm.createComponent(component, false); + + final Policy policy = qm.createPolicy("Blacklisted Version", Policy.Operator.ALL, Policy.ViolationState.FAIL); + final PolicyCondition condition = qm.createPolicyCondition(policy, Subject.VERSION, Operator.NUMERIC_EQUAL, "1.0"); + + var violation = new PolicyViolation(); + violation.setType(Type.OPERATIONAL); + violation.setComponent(component); + violation.setPolicyCondition(condition); + violation.setTimestamp(new Date()); + violation = qm.persist(violation); + + final var request = new ViolationAnalysisRequest(component.getUuid().toString(), + violation.getUuid().toString(), ViolationAnalysisState.APPROVED, "Some comment", true); + + final Response response = jersey.target(V1_VIOLATION_ANALYSIS) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + + final JsonObject jsonObject = parseJsonObject(response); + assertThat(jsonObject).isNotNull(); + assertThat(jsonObject.getString("analysisState")).isEqualTo("APPROVED"); + assertThat(jsonObject.getBoolean("isSuppressed")).isTrue(); + + assertThat(jsonObject.getJsonArray("analysisComments")).hasSize(3); + assertThat(jsonObject.getJsonArray("analysisComments").getJsonObject(0)) + .hasFieldOrPropertyWithValue("comment", Json.createValue("NOT_SET → APPROVED")) + .doesNotContainKey("commenter"); // Not set when authenticating via API key + assertThat(jsonObject.getJsonArray("analysisComments").getJsonObject(1)) + .hasFieldOrPropertyWithValue("comment", Json.createValue("Suppressed")) + .doesNotContainKey("commenter"); // Not set when authenticating via API key + assertThat(jsonObject.getJsonArray("analysisComments").getJsonObject(2)) + .hasFieldOrPropertyWithValue("comment", Json.createValue("Some comment")) + .doesNotContainKey("commenter"); // Not set when authenticating via API key; + + assertConditionWithTimeout(() -> NOTIFICATIONS.size() == 2, Duration.ofSeconds(5)); + final Notification projectNotification = NOTIFICATIONS.poll(); + assertThat(projectNotification).isNotNull(); + final Notification notification = NOTIFICATIONS.poll(); + assertThat(notification).isNotNull(); + assertThat(notification.getScope()).isEqualTo(NotificationScope.PORTFOLIO.name()); + assertThat(notification.getGroup()).isEqualTo(NotificationGroup.PROJECT_AUDIT_CHANGE.name()); + assertThat(notification.getLevel()).isEqualTo(NotificationLevel.INFORMATIONAL); + assertThat(notification.getTitle()).isEqualTo(NotificationUtil.generateNotificationTitle(NotificationConstants.Title.VIOLATIONANALYSIS_DECISION_APPROVED, project)); + assertThat(notification.getContent()).isEqualTo("An violation analysis decision was made to a policy violation affecting a project"); + } + @Test void updateAnalysisCreateNewWithEmptyRequestTest() throws Exception { initializeWithPermissions(Permissions.POLICY_VIOLATION_ANALYSIS); diff --git a/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskPurlMatchingTest.java b/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskPurlMatchingTest.java index 88e7bfbe77..988687ca87 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskPurlMatchingTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskPurlMatchingTest.java @@ -20,28 +20,25 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_INTERNAL_ENABLED; - +import static org.dependencytrack.tasks.scanners.InternalAnalysisTaskPurlMatchingTest.Range.withRange; public class InternalAnalysisTaskPurlMatchingTest extends PersistenceCapableTest { public static Collection parameters() { return Arrays.asList( - - Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?packaging=sources", WITHOUT_RANGE, MATCHES, "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?packaging=sources"), - Arguments.of("pkg:npm/foobar@12.3.1", WITHOUT_RANGE, MATCHES, "pkg:npm/foobar@12.3.1"), - - - Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim", WITHOUT_RANGE, MATCHES, "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?packaging=sources"), - Arguments.of("pkg:npm/foobar", WITHOUT_RANGE, MATCHES, "pkg:npm/foobar@12.3.1"), - - - Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim", Range.withRange().havingStartIncluding("1.5.0").havingEndExcluding("2.0.0"), MATCHES, "pkg:maven/org.apache.xmlgraphics/batik-anim@1.8.0"), - Arguments.of("pkg:npm/foobar", Range.withRange().havingStartExcluding("10.0.0").havingEndIncluding("15.0.0"), MATCHES, "pkg:npm/foobar@12.3.1"), - - Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1", WITHOUT_RANGE, DOES_NOT_MATCH, "pkg:npm/org.apache.xmlgraphics/batik-anim@1.9.1"), - - Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1", WITHOUT_RANGE, DOES_NOT_MATCH, "pkg:maven/com.example/batik-anim@1.9.1") - + Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?packaging=sources", WITHOUT_RANGE, MATCHES, "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?packaging=sources"), + Arguments.of("pkg:npm/foobar@12.3.1", WITHOUT_RANGE, MATCHES, "pkg:npm/foobar@12.3.1"), + Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim", WITHOUT_RANGE, MATCHES, "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?packaging=sources"), + Arguments.of("pkg:npm/foobar", WITHOUT_RANGE, MATCHES, "pkg:npm/foobar@12.3.1"), + Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim", withRange().havingStartIncluding("1.5.0").havingEndExcluding("2.0.0"), MATCHES, "pkg:maven/org.apache.xmlgraphics/batik-anim@1.8.0"), + Arguments.of("pkg:npm/foobar", withRange().havingStartExcluding("10.0.0").havingEndIncluding("15.0.0"), MATCHES, "pkg:npm/foobar@12.3.1"), + Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1", WITHOUT_RANGE, DOES_NOT_MATCH, "pkg:npm/org.apache.xmlgraphics/batik-anim@1.9.1"), + Arguments.of("pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1", WITHOUT_RANGE, DOES_NOT_MATCH, "pkg:maven/com.example/batik-anim@1.9.1"), + Arguments.of("pkg:nuget/System.IO.Packaging", withRange().havingStartIncluding("8.0.0-preview.1.23110.8").havingEndIncluding("8.0.0"), MATCHES, "pkg:nuget/System.IO.Packaging@8.0.0"), + Arguments.of("pkg:nuget/System.IO.Packaging", withRange().havingStartIncluding("8.0.0-preview.1.23110.8").havingEndIncluding("8.0.0"), DOES_NOT_MATCH, "pkg:nuget/System.IO.Packaging@8.0.1"), + Arguments.of("pkg:composer/typo3/cms-backend", withRange().havingStartIncluding("4.1.0").havingEndExcluding("4.1.13"), DOES_NOT_MATCH, "pkg:composer/typo3/cms-backend@v12.4.44"), + Arguments.of("pkg:composer/typo3/cms-backend", withRange().havingStartIncluding("4.3alpha1").havingEndExcluding("4.3beta2"), MATCHES, "pkg:composer/typo3/cms-backend@4.3beta1"), + Arguments.of("pkg:composer/typo3/cms-backend", withRange().havingStartIncluding("4.3alpha1").havingEndExcluding("4.3beta2"), DOES_NOT_MATCH, "pkg:composer/typo3/cms-backend@4.3.0") ); } @@ -115,7 +112,7 @@ void test(final String sourcePurlString, final var component = new Component(); component.setProject(project); component.setName("acme-lib"); - component.setPurl(targetPurlString);; + component.setPurl(targetPurlString); qm.persist(component); new InternalAnalysisTask().inform(new InternalAnalysisEvent( diff --git a/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskTest.java index ea5973fe50..8bb658f00e 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisTaskTest.java @@ -51,6 +51,246 @@ void testIssue1574() { assertThat(vulnerabilities.getList(Vulnerability.class).get(0).getVulnId()).isEqualTo("GHSA-wjm3-fq3r-5x46"); } + @Test + void testPurlAnalysisNotSkippedWhenCpeIsInvalid() { + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + + var component = new Component(); + component.setProject(project); + component.setName("jackson-databind"); + component.setVersion("2.13.0"); + component.setCpe("cpe:invalid"); + component.setPurl("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.0"); + component = qm.createComponent(component, false); + + var purlVs = new VulnerableSoftware(); + purlVs.setPurlType("maven"); + purlVs.setPurlNamespace("com.fasterxml.jackson.core"); + purlVs.setPurlName("jackson-databind"); + purlVs.setVersionEndExcluding("2.13.1"); + purlVs.setVulnerable(true); + purlVs = qm.persist(purlVs); + + var ghsaVuln = new Vulnerability(); + ghsaVuln.setVulnId("GHSA-0000-0000-0001"); + ghsaVuln.setSource(Vulnerability.Source.GITHUB); + ghsaVuln = qm.createVulnerability(ghsaVuln, false); + ghsaVuln.setVulnerableSoftware(List.of(purlVs)); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulnerabilities = qm.getVulnerabilities(component); + assertThat(vulnerabilities.getTotal()).isEqualTo(1); + assertThat(vulnerabilities.getList(Vulnerability.class).getFirst().getVulnId()).isEqualTo("GHSA-0000-0000-0001"); + } + + @Test + void testComponentWithBothValidCpeAndPurl() throws CpeParsingException, CpeEncodingException { + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + + var component = new Component(); + component.setProject(project); + component.setName("jackson-databind"); + component.setVersion("2.13.0"); + component.setCpe("cpe:2.3:a:fasterxml:jackson-databind:2.13.0:*:*:*:*:*:*:*"); + component.setPurl("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.0"); + component = qm.createComponent(component, false); + + var cpeVs = ModelConverter.convertCpe23UriToVulnerableSoftware( + "cpe:2.3:a:fasterxml:jackson-databind:2.13.0:*:*:*:*:*:*:*"); + cpeVs = qm.persist(cpeVs); + + var cveVuln = new Vulnerability(); + cveVuln.setVulnId("CVE-2022-00001"); + cveVuln.setSource(Vulnerability.Source.NVD); + cveVuln = qm.createVulnerability(cveVuln, false); + cveVuln.setVulnerableSoftware(List.of(cpeVs)); + + var purlVs = new VulnerableSoftware(); + purlVs.setPurlType("maven"); + purlVs.setPurlNamespace("com.fasterxml.jackson.core"); + purlVs.setPurlName("jackson-databind"); + purlVs.setVersionEndExcluding("2.13.1"); + purlVs.setVulnerable(true); + purlVs = qm.persist(purlVs); + + var ghsaVuln = new Vulnerability(); + ghsaVuln.setVulnId("GHSA-0000-0000-0001"); + ghsaVuln.setSource(Vulnerability.Source.GITHUB); + ghsaVuln = qm.createVulnerability(ghsaVuln, false); + ghsaVuln.setVulnerableSoftware(List.of(purlVs)); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulnerabilities = qm.getVulnerabilities(component); + assertThat(vulnerabilities.getTotal()).isEqualTo(2); + assertThat(vulnerabilities.getList(Vulnerability.class)) + .extracting(Vulnerability::getVulnId) + .containsExactlyInAnyOrder("CVE-2022-00001", "GHSA-0000-0000-0001"); + } + + @Test + void testCpeVersionDiffersFromComponentVersion() throws Exception { + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + + var component = new Component(); + component.setProject(project); + component.setName("stm32l4_firmware"); + component.setVersion("1.2.3"); + component.setCpe("cpe:2.3:o:st:stm32l4_firmware:-:*:*:*:*:*:*:*"); + component = qm.createComponent(component, false); + + var vs = ModelConverter.convertCpe23UriToVulnerableSoftware( + "cpe:2.3:o:st:stm32l4_firmware:-:*:*:*:*:*:*:*"); + vs = qm.persist(vs); + + var vuln = new Vulnerability(); + vuln.setVulnId("CVE-2023-00001"); + vuln.setSource(Vulnerability.Source.NVD); + vuln = qm.createVulnerability(vuln, false); + vuln.setVulnerableSoftware(List.of(vs)); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulns = qm.getVulnerabilities(component); + assertThat(vulns.getTotal()).isEqualTo(1); + assertThat(vulns.getList(Vulnerability.class).getFirst().getVulnId()).isEqualTo("CVE-2023-00001"); + } + + @Test + void testCpeVersionUsedInsteadOfComponentVersion() throws Exception { + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + + var component = new Component(); + component.setProject(project); + component.setName("product"); + component.setVersion("1.5.0"); + component.setCpe("cpe:2.3:a:vendor:product:5.0:*:*:*:*:*:*:*"); + component = qm.createComponent(component, false); + + var vs = ModelConverter.convertCpe23UriToVulnerableSoftware( + "cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*"); + vs.setVersionStartIncluding("1.0.0"); + vs.setVersionEndExcluding("2.0.0"); + vs = qm.persist(vs); + + var vuln = new Vulnerability(); + vuln.setVulnId("CVE-2023-00002"); + vuln.setSource(Vulnerability.Source.NVD); + vuln = qm.createVulnerability(vuln, false); + vuln.setVulnerableSoftware(List.of(vs)); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulns = qm.getVulnerabilities(component); + assertThat(vulns.getTotal()).isEqualTo(0); + } + + @Test + void testPurlVersionDiffersFromComponentVersion() { + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + + var component = new Component(); + component.setProject(project); + component.setName("lib"); + component.setVersion("1.0-SNAPSHOT"); + component.setPurl("pkg:maven/com.example/lib@1.0.0"); + component = qm.createComponent(component, false); + + var vs = new VulnerableSoftware(); + vs.setPurlType("maven"); + vs.setPurlNamespace("com.example"); + vs.setPurlName("lib"); + vs.setVersionEndExcluding("1.0.1"); + vs.setVulnerable(true); + vs = qm.persist(vs); + + var vuln = new Vulnerability(); + vuln.setVulnId("CVE-2023-00003"); + vuln.setSource(Vulnerability.Source.NVD); + vuln = qm.createVulnerability(vuln, false); + vuln.setVulnerableSoftware(List.of(vs)); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulns = qm.getVulnerabilities(component); + assertThat(vulns.getTotal()).isEqualTo(1); + assertThat(vulns.getList(Vulnerability.class).getFirst().getVulnId()).isEqualTo("CVE-2023-00003"); + } + + @Test + void testCpeWithAnyVersionMatchesEverything() throws Exception { + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + + var component = new Component(); + component.setProject(project); + component.setName("product"); + component.setVersion("2.5.0"); + component.setCpe("cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*"); + component = qm.createComponent(component, false); + + var vs = ModelConverter.convertCpe23UriToVulnerableSoftware( + "cpe:2.3:a:vendor:product:2.5.0:*:*:*:*:*:*:*"); + vs = qm.persist(vs); + + var vuln = new Vulnerability(); + vuln.setVulnId("CVE-2023-00004"); + vuln.setSource(Vulnerability.Source.NVD); + vuln = qm.createVulnerability(vuln, false); + vuln.setVulnerableSoftware(List.of(vs)); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulns = qm.getVulnerabilities(component); + assertThat(vulns.getTotal()).isEqualTo(1); + assertThat(vulns.getList(Vulnerability.class).getFirst().getVulnId()).isEqualTo("CVE-2023-00004"); + } + + @Test + void testPurlWithNullVersionNoMatch() { + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + + var component = new Component(); + component.setProject(project); + component.setName("lib"); + component.setVersion("1.0.0"); + component.setPurl("pkg:maven/com.example/lib"); + component = qm.createComponent(component, false); + + var vs = new VulnerableSoftware(); + vs.setPurlType("maven"); + vs.setPurlNamespace("com.example"); + vs.setPurlName("lib"); + vs.setVersionEndExcluding("2.0.0"); + vs.setVulnerable(true); + vs = qm.persist(vs); + + var vuln = new Vulnerability(); + vuln.setVulnId("CVE-2023-00005"); + vuln.setSource(Vulnerability.Source.NVD); + vuln = qm.createVulnerability(vuln, false); + vuln.setVulnerableSoftware(List.of(vs)); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulns = qm.getVulnerabilities(component); + assertThat(vulns.getTotal()).isEqualTo(0); + } + @Test void testExactMatchWithNAUpdate() throws CpeParsingException, CpeEncodingException { var project = new Project(); diff --git a/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java index 4b124690c1..be3fb91460 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java @@ -232,8 +232,8 @@ void testAnalyzeWithoutApiToken() { } @Test - void testAnalyzeWithoutUser() { - configApiToken(null, API_TOKEN); + void testAnalyzeWithoutUser() throws Exception { + configApiToken(null, DataEncryption.encryptAsString(API_TOKEN)); stubPOSTRequest(); var project = configProject(); @@ -241,10 +241,53 @@ void testAnalyzeWithoutUser() { var component = getComponent(project); qm.persist(component); - assertThatNoException().isThrownBy(() -> analysisTask.inform(new OssIndexAnalysisEvent( - List.of(component), VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS))); + assertThatNoException().isThrownBy( + () -> analysisTask.inform( + new OssIndexAnalysisEvent( + List.of(component), + VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS))); - verify(0, getRequestedPost()); + verify(0, postRequestedFor(urlPathEqualTo("/api/v3/component-report"))); + } + + @Test + void testAnalyzeWithBearerToken() throws Exception { + final String bearerToken = "sonatype_pat_testtoken123"; + configApiToken(null, DataEncryption.encryptAsString(bearerToken)); + stubPOSTRequest(); + + var project = configProject(); + var component = getComponent(project); + qm.persist(component); + + assertThatNoException().isThrownBy( + () -> analysisTask.inform( + new OssIndexAnalysisEvent( + List.of(component), + VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS))); + + verify(postRequestedFor(urlPathEqualTo("/api/v3/component-report")) + .withHeader("Authorization", equalTo("Bearer " + bearerToken))); + } + + @Test + void testAnalyzeWithBearerTokenAndUsername() throws Exception { + final String bearerToken = "sonatype_pat_testtoken123"; + configApiToken(API_USER, DataEncryption.encryptAsString(bearerToken)); + stubPOSTRequest(); + + var project = configProject(); + var component = getComponent(project); + qm.persist(component); + + assertThatNoException().isThrownBy( + () -> analysisTask.inform( + new OssIndexAnalysisEvent( + List.of(component), + VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS))); + + verify(postRequestedFor(urlPathEqualTo("/api/v3/component-report")) + .withHeader("Authorization", equalTo("Bearer " + bearerToken))); } private @NotNull Project configProject() { diff --git a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java index 2802c07e3d..d05927c5bc 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTaskIntegrationTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -54,13 +55,14 @@ import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_TRIVY_SCAN_OS; import static org.testcontainers.containers.wait.strategy.Wait.forLogMessage; +@Disabled("Pulling Trivy images is unreliable until https://github.com/aquasecurity/trivy/discussions/10425 is fully resolved.") class TrivyAnalysisTaskIntegrationTest extends PersistenceCapableTest { public static Collection testParameters() { return Arrays.asList( Arguments.of("0.51.1"), // Pre breaking change of Application#libraries -> Application#packages Arguments.of("0.51.2"), // Post breaking change of Application#libraries -> Application#packages - Arguments.of("latest") + Arguments.of("0.69.3") ); }