diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 4819ff67e5..40c11ecda2 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -67,13 +67,20 @@ jobs: mvn -B -Pquick -Dservices.bom.merge.skip=false package - name: Upload Artifacts - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag=v4.6.2 with: name: assembled-wars path: |- apiserver/target/*.jar apiserver/target/bom.json + - name: Upload OpenAPI Spec + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag=v4.6.2 + with: + name: openapi-spec + path: |- + api/target/classes/**/openapi.yaml + build-container: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.github/workflows/ci-openapi.yaml b/.github/workflows/ci-openapi.yaml new file mode 100644 index 0000000000..96f85eb18a --- /dev/null +++ b/.github/workflows/ci-openapi.yaml @@ -0,0 +1,59 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +name: OpenAPI + +on: + pull_request: + paths: + - api/src/main/openapi/** + - api/src/main/spectral/** + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: { } + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + permissions: + checks: write + timeout-minutes: 5 + steps: + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 + - name: Lint OpenAPI Spec + uses: stoplightio/spectral-action@6416fd018ae38e60136775066eb3e98172143141 # tag=v0.8.13 + with: + spectral_ruleset: "api/src/main/spectral/ruleset.yaml" + file_glob: "api/src/main/openapi/openapi.yaml" + + breaking-changes: + name: Breaking Changes + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 + - name: Detect Breaking Changes + uses: oasdiff/oasdiff-action/breaking@1c611ffb1253a72924624aa4fb662e302b3565d3 # tag=v0.0.21 + with: + base: https://raw.githubusercontent.com/${{ github.repository }}/refs/heads/main/api/src/main/openapi/openapi.yaml + revision: api/src/main/openapi/openapi.yaml + fail-on: ERR \ No newline at end of file diff --git a/.mvn/maven-build-cache-config.xml b/.mvn/maven-build-cache-config.xml index 3eecd9f34c..761cac1a50 100644 --- a/.mvn/maven-build-cache-config.xml +++ b/.mvn/maven-build-cache-config.xml @@ -31,7 +31,7 @@ - {*.java,*.properties,*.proto,*.sql,*.xml} + {*.java,*.properties,*.proto,*.sql,*.xml,*.yaml} src/ diff --git a/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java b/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java index b5c693dce1..32979e87d8 100644 --- a/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java +++ b/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java @@ -43,6 +43,7 @@ /** * The primary class that starts an embedded Jetty server + * * @author Steve Springett * @since 1.0.0 */ @@ -73,7 +74,7 @@ public static void main(final String[] args) throws Exception { final Server server = new Server(); final HttpConfiguration httpConfig = new HttpConfiguration(); - httpConfig.addCustomizer( new org.eclipse.jetty.server.ForwardedRequestCustomizer() ); // Add support for X-Forwarded headers + httpConfig.addCustomizer(new org.eclipse.jetty.server.ForwardedRequestCustomizer()); // Add support for X-Forwarded headers // Enable legacy (mimicking Jetty 9) URI compliance. // This is required to allow URL encoding in path segments, e.g. "/foo/bar%2Fbaz". @@ -89,7 +90,7 @@ public static void main(final String[] args) throws Exception { // here, the only viable long-term solution is to adapt REST APIs to follow Servlet API 6 spec. httpConfig.setUriCompliance(UriCompliance.LEGACY); - final HttpConnectionFactory connectionFactory = new HttpConnectionFactory( httpConfig ); + final HttpConnectionFactory connectionFactory = new HttpConnectionFactory(httpConfig); final ServerConnector connector = new ServerConnector(server, connectionFactory); connector.setHost(host); connector.setPort(port); @@ -102,6 +103,7 @@ public static void main(final String[] args) throws Exception { context.setErrorHandler(new ErrorHandler()); context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/[^/]*taglibs.*\\.jar$"); + context.setThrowUnavailableOnStartupException(true); // Prevent loading of logging classes context.getProtectedClassMatcher().add("org.apache.log4j."); diff --git a/alpine/alpine-infra/pom.xml b/alpine/alpine-infra/pom.xml index a64ad6d743..30abbe75f7 100644 --- a/alpine/alpine-infra/pom.xml +++ b/alpine/alpine-infra/pom.xml @@ -31,10 +31,12 @@ org.dependencytrack alpine-common + ${project.version} org.dependencytrack alpine-model + ${project.version} org.apache.commons diff --git a/alpine/alpine-model/pom.xml b/alpine/alpine-model/pom.xml index c77bf87b46..210776fb1b 100644 --- a/alpine/alpine-model/pom.xml +++ b/alpine/alpine-model/pom.xml @@ -31,6 +31,7 @@ org.dependencytrack alpine-common + ${project.version} org.apache.commons diff --git a/alpine/alpine-server/pom.xml b/alpine/alpine-server/pom.xml index 45f745f5a2..ef3725d73f 100644 --- a/alpine/alpine-server/pom.xml +++ b/alpine/alpine-server/pom.xml @@ -34,14 +34,17 @@ org.dependencytrack alpine-common + ${project.version} org.dependencytrack alpine-infra + ${project.version} org.dependencytrack alpine-model + ${project.version} diff --git a/alpine/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java b/alpine/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java index 26d332b313..bb4de20380 100644 --- a/alpine/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java +++ b/alpine/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java @@ -20,8 +20,8 @@ import alpine.common.logging.Logger; import alpine.model.ApiKey; -import alpine.server.auth.ApiKeyAuthenticationService; import alpine.server.auth.AllowApiKeyInQueryParameter; +import alpine.server.auth.ApiKeyAuthenticationService; import alpine.server.auth.JwtAuthenticationService; import org.glassfish.jersey.server.ContainerRequest; import org.owasp.security.logging.SecurityMarkers; @@ -29,14 +29,15 @@ import jakarta.annotation.Priority; import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; -import jakarta.ws.rs.core.Response; import jakarta.ws.rs.container.ResourceInfo; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; import javax.naming.AuthenticationException; import java.io.IOException; import java.security.Principal; @@ -79,8 +80,7 @@ public void filter(ContainerRequestContext requestContext) { } } catch (AuthenticationException e) { LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Invalid API key asserted"); - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); - return; + throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); } } @@ -90,13 +90,12 @@ public void filter(ContainerRequestContext requestContext) { principal = jwtAuthService.authenticate(); } catch (AuthenticationException e) { LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Invalid JWT asserted"); - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); - return; + throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); } } if (principal == null) { - requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); + throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build()); } else { requestContext.setProperty("Principal", principal); MDC.put("principal", principal.getName()); diff --git a/alpine/alpine-server/src/main/java/alpine/server/filters/AuthorizationFilter.java b/alpine/alpine-server/src/main/java/alpine/server/filters/AuthorizationFilter.java index 1c7d6e2d78..d838bcff40 100644 --- a/alpine/alpine-server/src/main/java/alpine/server/filters/AuthorizationFilter.java +++ b/alpine/alpine-server/src/main/java/alpine/server/filters/AuthorizationFilter.java @@ -27,6 +27,7 @@ import org.owasp.security.logging.SecurityMarkers; import jakarta.annotation.Priority; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; @@ -62,8 +63,7 @@ public void filter(ContainerRequestContext requestContext) { final Principal principal = (Principal) requestContext.getProperty("Principal"); if (principal == null) { LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "A request was made without the assertion of a valid user principal"); - requestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); - return; + throw new ForbiddenException(Response.status(Response.Status.FORBIDDEN).build()); } final Set effectivePermissions; @@ -97,7 +97,7 @@ public void filter(ContainerRequestContext requestContext) { LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Unauthorized access attempt made by %s to %s" .formatted(requestPrincipal, requestUri)); - requestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); + throw new ForbiddenException(Response.status(Response.Status.FORBIDDEN).build()); } else { requestContext.setProperty(EFFECTIVE_PERMISSIONS_PROPERTY, effectivePermissions); } diff --git a/alpine/alpine-server/src/main/java/alpine/server/persistence/PersistenceManagerFactory.java b/alpine/alpine-server/src/main/java/alpine/server/persistence/PersistenceManagerFactory.java index 8833f7043e..2c49421491 100644 --- a/alpine/alpine-server/src/main/java/alpine/server/persistence/PersistenceManagerFactory.java +++ b/alpine/alpine-server/src/main/java/alpine/server/persistence/PersistenceManagerFactory.java @@ -25,6 +25,7 @@ import alpine.persistence.JdoProperties; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.metrics.micrometer.MicrometerMetricsTrackerFactory; import io.micrometer.core.instrument.FunctionCounter; import io.micrometer.core.instrument.Gauge; import org.datanucleus.PropertyNames; @@ -347,7 +348,7 @@ private HikariConfig createBaseHikariConfig(final String poolName) { hikariConfig.setPassword(Config.getInstance().getPropertyOrFile(Config.AlpineKey.DATABASE_PASSWORD)); if (Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.METRICS_ENABLED)) { - hikariConfig.setMetricRegistry(Metrics.getRegistry()); + hikariConfig.setMetricsTrackerFactory(new MicrometerMetricsTrackerFactory(Metrics.getRegistry())); } return hikariConfig; diff --git a/alpine/pom.xml b/alpine/pom.xml index a4554a12c5..e03aa6b4f9 100644 --- a/alpine/pom.xml +++ b/alpine/pom.xml @@ -76,12 +76,12 @@ 2.0.3 0.4 - 3.2.1 + 3.2.2 4.5.0 - 2.19.0 - 3.17.0 + 2.20.0 + 3.18.0 2.3.232 - 6.3.0 + 6.3.2 3.30.2-GA 4.0.5 3.2.1 @@ -90,9 +90,9 @@ 3.0.2 1.5.18 8.1 - 1.15.1 + 1.15.2 4.0.1 - 11.26 + 11.26.1 1.3.1 1.1.7 1.1.7 diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000000..7bce0e890d --- /dev/null +++ b/api/README.md @@ -0,0 +1,15 @@ +# api + +Definition of Dependency-Track's REST API, in [OpenAPI v3.0] format. + +The API draws inspiration from [Zalando's RESTful API Guidelines]. + +Conformance to API guidelines is enforced with [spectral] in CI. +Validation may be performed locally using [`openapi-lint.sh`](../dev/scripts/openapi-lint.sh). + +Interfaces and model classes are generated as part of the build using [openapi-generator]. + +[OpenAPI v3.0]: https://spec.openapis.org/oas/v3.0.3.html +[Zalando's RESTful API Guidelines]: https://opensource.zalando.com/restful-api-guidelines/ +[openapi-generator]: https://github.com/OpenAPITools/openapi-generator +[spectral]: https://github.com/stoplightio/spectral \ No newline at end of file diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000000..2a92cc9d2c --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,113 @@ + + + + 4.0.0 + + org.dependencytrack + dependency-track-parent + 5.6.0-SNAPSHOT + + + api + jar + + + ${project.basedir}/.. + true + + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.validation + jakarta.validation-api + provided + + + jakarta.ws.rs + jakarta.ws.rs-api + provided + + + + + + + org.openapitools + openapi-generator-maven-plugin + 7.14.0 + + + generate-api-v2 + generate-sources + + generate + + + true + ${basedir}/src/main/openapi/openapi.yaml + ${project.build.directory}/classes/org/dependencytrack/api/v2/openapi + jaxrs-spec + false + false + false + true + REF_AS_PARENT_IN_ALLOF=true + + org.dependencytrack.api.v2 + org.dependencytrack.api.v2.model + true + true + true + true + java8 + false + true + false + . + + @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) + + + + + + + + + + \ No newline at end of file diff --git a/api/src/main/openapi/components/parameters/page-token.yaml b/api/src/main/openapi/components/parameters/page-token.yaml new file mode 100644 index 0000000000..4082e81a02 --- /dev/null +++ b/api/src/main/openapi/components/parameters/page-token.yaml @@ -0,0 +1,21 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +name: page_token +description: Opaque token pointing to a specific position in a collection +in: query +schema: + type: string \ No newline at end of file diff --git a/api/src/main/openapi/components/parameters/pagination-limit.yaml b/api/src/main/openapi/components/parameters/pagination-limit.yaml new file mode 100644 index 0000000000..f0ee13e1f2 --- /dev/null +++ b/api/src/main/openapi/components/parameters/pagination-limit.yaml @@ -0,0 +1,25 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +name: limit +description: Maximum number of items to retrieve from the collection +in: query +schema: + type: integer + format: int32 + minimum: 1 + maximum: 1000 + default: 100 \ No newline at end of file diff --git a/api/src/main/openapi/components/responses/generic-conflict-error.yaml b/api/src/main/openapi/components/responses/generic-conflict-error.yaml new file mode 100644 index 0000000000..311467633e --- /dev/null +++ b/api/src/main/openapi/components/responses/generic-conflict-error.yaml @@ -0,0 +1,26 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +description: Forbidden +content: + application/problem+json: + schema: + $ref: "../schemas/problem-details.yaml" + example: + type: about:blank + status: 409 + title: Conflict + detail: The resource already exists. \ No newline at end of file diff --git a/api/src/main/openapi/components/responses/generic-error.yaml b/api/src/main/openapi/components/responses/generic-error.yaml new file mode 100644 index 0000000000..6795cdb471 --- /dev/null +++ b/api/src/main/openapi/components/responses/generic-error.yaml @@ -0,0 +1,21 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +description: Unexpected error +content: + application/problem+json: + schema: + $ref: "../schemas/problem-details.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/components/responses/generic-forbidden-error.yaml b/api/src/main/openapi/components/responses/generic-forbidden-error.yaml new file mode 100644 index 0000000000..575f662105 --- /dev/null +++ b/api/src/main/openapi/components/responses/generic-forbidden-error.yaml @@ -0,0 +1,26 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +description: Forbidden +content: + application/problem+json: + schema: + $ref: "../schemas/problem-details.yaml" + example: + type: about:blank + status: 403 + title: Forbidden + detail: Not permitted to access the requested resource. \ No newline at end of file diff --git a/api/src/main/openapi/components/responses/generic-not-found-error.yaml b/api/src/main/openapi/components/responses/generic-not-found-error.yaml new file mode 100644 index 0000000000..867797036e --- /dev/null +++ b/api/src/main/openapi/components/responses/generic-not-found-error.yaml @@ -0,0 +1,26 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +description: Not found +content: + application/problem+json: + schema: + $ref: "../schemas/problem-details.yaml" + example: + type: about:blank + status: 404 + title: Not Found + detail: The requested resource could not be found. \ No newline at end of file diff --git a/api/src/main/openapi/components/responses/generic-unauthorized-error.yaml b/api/src/main/openapi/components/responses/generic-unauthorized-error.yaml new file mode 100644 index 0000000000..d3d041abe6 --- /dev/null +++ b/api/src/main/openapi/components/responses/generic-unauthorized-error.yaml @@ -0,0 +1,26 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +description: Unauthorized +content: + application/problem+json: + schema: + $ref: "../schemas/problem-details.yaml" + example: + type: about:blank + status: 401 + title: Unauthorized + detail: Not authorized to access the requested resource. \ No newline at end of file diff --git a/api/src/main/openapi/components/responses/invalid-request-error.yaml b/api/src/main/openapi/components/responses/invalid-request-error.yaml new file mode 100644 index 0000000000..252b4f40e4 --- /dev/null +++ b/api/src/main/openapi/components/responses/invalid-request-error.yaml @@ -0,0 +1,30 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +description: Bad request +content: + application/problem+json: + schema: + $ref: "../schemas/invalid-request-problem-details.yaml" + example: + type: about:blank + status: 400 + title: Bad Request + detail: The request could not be processed because it failed validation. + errors: + - path: foo.bar + value: baz + message: Must be a number \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/constraint-violation-error.yaml b/api/src/main/openapi/components/schemas/constraint-violation-error.yaml new file mode 100644 index 0000000000..b6e4932240 --- /dev/null +++ b/api/src/main/openapi/components/schemas/constraint-violation-error.yaml @@ -0,0 +1,29 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + path: + type: string + description: Path to the invalid field in the request + value: + type: string + description: The invalid value + message: + type: string + description: Message explaining the error +required: +- message \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/create-team-membership-request.yaml b/api/src/main/openapi/components/schemas/create-team-membership-request.yaml new file mode 100644 index 0000000000..36fa40c478 --- /dev/null +++ b/api/src/main/openapi/components/schemas/create-team-membership-request.yaml @@ -0,0 +1,29 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + team_name: + type: string + description: Name of the team + maxLength: 255 + username: + type: string + description: Name of the user + maxLength: 255 +required: +- team_name +- username \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/create-team-request.yaml b/api/src/main/openapi/components/schemas/create-team-request.yaml new file mode 100644 index 0000000000..8d2708fda2 --- /dev/null +++ b/api/src/main/openapi/components/schemas/create-team-request.yaml @@ -0,0 +1,31 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + name: + type: string + description: Name of the team to create + maxLength: 255 + permissions: + type: array + items: + type: string + maxLength: 255 + uniqueItems: true + description: Permissions to assign to the team +required: +- name \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/get-team-response.yaml b/api/src/main/openapi/components/schemas/get-team-response.yaml new file mode 100644 index 0000000000..ce421d6bd3 --- /dev/null +++ b/api/src/main/openapi/components/schemas/get-team-response.yaml @@ -0,0 +1,31 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + name: + type: string + description: Name of the team + maxLength: 255 + permissions: + type: array + items: + type: string + maxLength: 255 + description: Permissions assigned to the team +required: +- name +- permissions \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/invalid-request-problem-details.yaml b/api/src/main/openapi/components/schemas/invalid-request-problem-details.yaml new file mode 100644 index 0000000000..0d7fa15e46 --- /dev/null +++ b/api/src/main/openapi/components/schemas/invalid-request-problem-details.yaml @@ -0,0 +1,26 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +allOf: +- $ref: "./problem-details.yaml" +properties: + errors: + type: array + items: + $ref: "./constraint-violation-error.yaml" +required: +- errors \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-team-memberships-response-item.yaml b/api/src/main/openapi/components/schemas/list-team-memberships-response-item.yaml new file mode 100644 index 0000000000..36fa40c478 --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-team-memberships-response-item.yaml @@ -0,0 +1,29 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + team_name: + type: string + description: Name of the team + maxLength: 255 + username: + type: string + description: Name of the user + maxLength: 255 +required: +- team_name +- username \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-team-memberships-response.yaml b/api/src/main/openapi/components/schemas/list-team-memberships-response.yaml new file mode 100644 index 0000000000..389ea33eff --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-team-memberships-response.yaml @@ -0,0 +1,27 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +allOf: +- $ref: "./paginated-response.yaml" +properties: + memberships: + type: array + items: + $ref: "./list-team-memberships-response-item.yaml" +required: +- _pagination +- memberships \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-teams-response-item.yaml b/api/src/main/openapi/components/schemas/list-teams-response-item.yaml new file mode 100644 index 0000000000..793c020e8d --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-teams-response-item.yaml @@ -0,0 +1,35 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + name: + type: string + description: Name of the team + maxLength: 255 + api_keys: + type: integer + format: int32 + minimum: 0 + description: Number of API keys assigned to this team + members: + type: integer + format: int32 + minimum: 0 + description: Number of users that are member of this team +required: +- name +- members \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-teams-response.yaml b/api/src/main/openapi/components/schemas/list-teams-response.yaml new file mode 100644 index 0000000000..ac96641443 --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-teams-response.yaml @@ -0,0 +1,27 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +allOf: +- $ref: "./paginated-response.yaml" +properties: + teams: + type: array + items: + $ref: "./list-teams-response-item.yaml" +required: +- _pagination +- teams \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response-item.yaml b/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response-item.yaml new file mode 100644 index 0000000000..c435044d2d --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response-item.yaml @@ -0,0 +1,34 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + year: + type: integer + format: int32 + month: + type: integer + format: int32 + count: + type: integer + format: int32 + observed_at: + $ref: "./timestamp.yaml" +required: +- count +- month +- observed_at +- year \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response.yaml b/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response.yaml new file mode 100644 index 0000000000..f33b0be994 --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response.yaml @@ -0,0 +1,27 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +allOf: + - $ref: "./paginated-response.yaml" +properties: + metrics: + type: array + items: + $ref: "./list-vulnerability-metrics-response-item.yaml" +required: + - _pagination + - metrics \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-workflow-states-response-item.yaml b/api/src/main/openapi/components/schemas/list-workflow-states-response-item.yaml new file mode 100644 index 0000000000..cdbc9e3ba9 --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-workflow-states-response-item.yaml @@ -0,0 +1,47 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + step: + type: string + enum: + - BOM_CONSUMPTION + - BOM_PROCESSING + - VULN_ANALYSIS + - REPO_META_ANALYSIS + - POLICY_EVALUATION + - METRICS_UPDATE + - POLICY_BUNDLE_SYNC + - PROJECT_CLONE + status: + type: string + enum: + - PENDING + - TIMED_OUT + - COMPLETED + - FAILED + - CANCELLED + - NOT_APPLICABLE + failure_reason: + type: string + token: + type: string + format: uuid + started_at: + $ref: "./timestamp.yaml" + updated_at: + $ref: "./timestamp.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/list-workflow-states-response.yaml b/api/src/main/openapi/components/schemas/list-workflow-states-response.yaml new file mode 100644 index 0000000000..ae57ddd26b --- /dev/null +++ b/api/src/main/openapi/components/schemas/list-workflow-states-response.yaml @@ -0,0 +1,22 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + states: + type: array + items: + $ref: "./list-workflow-states-response-item.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/paginated-response.yaml b/api/src/main/openapi/components/schemas/paginated-response.yaml new file mode 100644 index 0000000000..6675b43fa1 --- /dev/null +++ b/api/src/main/openapi/components/schemas/paginated-response.yaml @@ -0,0 +1,45 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + _pagination: + title: Pagination Metadata + description: Metadata of paginated responses + type: object + properties: + links: + title: Pagination Links + description: Links to navigate through the collection + type: object + properties: + self: + type: string + format: uri + description: Link to the current page of the collection + next: + type: string + format: uri + description: >- + Link to the next page of the collection. + If not present, no more items exist. + required: + - self + required: + - links +# Enable inheritance for schemas that extend this object via allOf. +# https://github.com/OpenAPITools/openapi-generator/pull/14172 +x-parent: true \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/portfolio-metrics-response.yaml b/api/src/main/openapi/components/schemas/portfolio-metrics-response.yaml new file mode 100644 index 0000000000..f938ec465f --- /dev/null +++ b/api/src/main/openapi/components/schemas/portfolio-metrics-response.yaml @@ -0,0 +1,142 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +properties: + critical: + type: integer + format: int32 + high: + type: integer + format: int32 + medium: + type: integer + format: int32 + low: + type: integer + format: int32 + unassigned: + type: integer + format: int32 + vulnerabilities: + type: integer + format: int32 + projects: + type: integer + format: int32 + vulnerable_projects: + type: integer + format: int32 + components: + type: integer + format: int32 + vulnerable_components: + type: integer + format: int32 + suppressed: + type: integer + format: int32 + findings_total: + type: integer + format: int32 + findings_audited: + type: integer + format: int32 + findings_unaudited: + type: integer + format: int32 + inherited_risk_score: + type: number + format: double + policy_violations_fail: + type: integer + format: int32 + policy_violations_warn: + type: integer + format: int32 + policy_violations_info: + type: integer + format: int32 + policy_violations_total: + type: integer + format: int32 + policy_violations_audited: + type: integer + format: int32 + policy_violations_unaudited: + type: integer + format: int32 + policy_violations_security_total: + type: integer + format: int32 + policy_violations_security_audited: + type: integer + format: int32 + policy_violations_security_unaudited: + type: integer + format: int32 + policy_violations_license_total: + type: integer + format: int32 + policy_violations_license_audited: + type: integer + format: int32 + policy_violations_license_unaudited: + type: integer + format: int32 + policy_violations_operational_total: + type: integer + format: int32 + policy_violations_operational_audited: + type: integer + format: int32 + policy_violations_operational_unaudited: + type: integer + format: int32 + observed_at: + $ref: "./timestamp.yaml" +required: +- components +- critical +- findings_audited +- findings_total +- findings_unaudited +- high +- inherited_risk_score +- low +- medium +- observed_at +- policy_violations_audited +- policy_violations_fail +- policy_violations_info +- policy_violations_license_audited +- policy_violations_license_total +- policy_violations_license_unaudited +- policy_violations_operational_audited +- policy_violations_operational_total +- policy_violations_operational_unaudited +- policy_violations_security_audited +- policy_violations_security_total +- policy_violations_security_unaudited +- policy_violations_total +- policy_violations_unaudited +- policy_violations_warn +- projects +- suppressed +- unassigned +- vulnerabilities +- vulnerable_components +- vulnerable_projects \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/problem-details.yaml b/api/src/main/openapi/components/schemas/problem-details.yaml new file mode 100644 index 0000000000..15c408e315 --- /dev/null +++ b/api/src/main/openapi/components/schemas/problem-details.yaml @@ -0,0 +1,51 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: object +description: An RFC 9457 problem object. +externalDocs: + url: https://www.rfc-editor.org/rfc/rfc9457.html +properties: + type: + type: string + format: uri + default: about:blank + description: A URI reference that identifies the problem type + status: + type: integer + format: int32 + minimum: 400 + maximum: 599 + example: 500 + description: HTTP status code generated by the origin server for this occurrence of the problem + title: + type: string + description: Short, human-readable summary of the problem type + maxLength: 255 + detail: + type: string + description: Human-readable explanation specific to this occurrence of the problem + maxLength: 1024 + instance: + type: string + format: uri + description: Reference URI that identifies the specific occurrence of the problem +required: +- title +- detail +# Enable inheritance for schemas that extend this object via allOf. +# https://github.com/OpenAPITools/openapi-generator/pull/14172 +x-parent: true \ No newline at end of file diff --git a/api/src/main/openapi/components/schemas/timestamp.yaml b/api/src/main/openapi/components/schemas/timestamp.yaml new file mode 100644 index 0000000000..6e0cec4047 --- /dev/null +++ b/api/src/main/openapi/components/schemas/timestamp.yaml @@ -0,0 +1,20 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +type: integer +format: int64 +description: Epoch timestamp in milliseconds since January 1, 1970 UTC. +example: 1752209050377 \ No newline at end of file diff --git a/api/src/main/openapi/openapi.yaml b/api/src/main/openapi/openapi.yaml new file mode 100644 index 0000000000..76a05264c3 --- /dev/null +++ b/api/src/main/openapi/openapi.yaml @@ -0,0 +1,66 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +openapi: 3.0.3 +info: + title: OWASP Dependency-Track + description: REST API of OWASP Dependency-Track + version: 2.0.0 + contact: + name: The Dependency-Track Authors + email: dependencytrack@owasp.org + url: https://github.com/DependencyTrack/dependency-track + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +security: +- apiKeyAuth: [ ] +- bearerAuth: [ ] +servers: +- url: /api/v2 +tags: +- name: Metrics + description: Endpoints related to metrics +- name: Teams + description: Endpoints related to teams +- name: Workflows + description: Endpoints related to workflows + +paths: + /metrics/portfolio/current: + $ref: "./paths/metrics_portfolio_current.yaml" + /metrics/vulnerabilities: + $ref: "./paths/metrics_vulnerabilities.yaml" + /teams: + $ref: "./paths/teams.yaml" + /teams/{name}: + $ref: "./paths/teams__name_.yaml" + /team-memberships: + $ref: "./paths/team-memberships.yaml" + /workflows/{token}: + $ref: "./paths/workflows__token__.yaml" + +components: + securitySchemes: + apiKeyAuth: + name: X-Api-Key + description: Authentication via API key + type: apiKey + in: header + bearerAuth: + description: Authentication via Bearer token + type: http + scheme: Bearer diff --git a/api/src/main/openapi/paths/metrics_portfolio_current.yaml b/api/src/main/openapi/paths/metrics_portfolio_current.yaml new file mode 100644 index 0000000000..088642a4a2 --- /dev/null +++ b/api/src/main/openapi/paths/metrics_portfolio_current.yaml @@ -0,0 +1,35 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +get: + operationId: getPortfolioCurrentMetrics + summary: Returns current metrics for the entire portfolio. + description: Requires permission VIEW_PORTFOLIO + tags: + - Metrics + responses: + "200": + description: Current metrics for the entire portfolio + content: + application/json: + schema: + $ref: "../components/schemas/portfolio-metrics-response.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/paths/metrics_vulnerabilities.yaml b/api/src/main/openapi/paths/metrics_vulnerabilities.yaml new file mode 100644 index 0000000000..a270bd02c5 --- /dev/null +++ b/api/src/main/openapi/paths/metrics_vulnerabilities.yaml @@ -0,0 +1,38 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +get: + operationId: getVulnerabilityMetrics + summary: Returns the sum of all vulnerabilities in the database by year and month. + description: Requires permission VIEW_PORTFOLIO + tags: + - Metrics + parameters: + - $ref: "../components/parameters/pagination-limit.yaml" + - $ref: "../components/parameters/page-token.yaml" + responses: + "200": + description: The sum of all vulnerabilities in the database by year and month + content: + application/json: + schema: + $ref: "../components/schemas/list-vulnerability-metrics-response.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/paths/team-memberships.yaml b/api/src/main/openapi/paths/team-memberships.yaml new file mode 100644 index 0000000000..25b2fbf24e --- /dev/null +++ b/api/src/main/openapi/paths/team-memberships.yaml @@ -0,0 +1,123 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +get: + operationId: listTeamMemberships + summary: List all team memberships + description: >- + Returns a paginated list of team memberships, + sorted by team name and username in ascending order + tags: + - Teams + parameters: + - name: team + in: query + description: Name of the team to filter by. Must be an exact match. + schema: + type: string + maxLength: 255 + - name: user + in: query + description: Name of the user to filter by. Must be an exact match. + schema: + type: string + maxLength: 255 + - $ref: "../components/parameters/pagination-limit.yaml" + - $ref: "../components/parameters/page-token.yaml" + responses: + "200": + description: Paginated list of team memberships + content: + application/json: + schema: + $ref: "../components/schemas/list-team-memberships-response.yaml" + "400": + $ref: "../components/responses/invalid-request-error.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" + +post: + operationId: createTeamMembership + summary: Create team membership + description: Create a team membership + tags: + - Teams + requestBody: + required: true + content: + application/json: + schema: + $ref: "../components/schemas/create-team-membership-request.yaml" + responses: + "201": + description: Team membership created + "400": + description: Bad Request + content: + application/problem+json: + schema: + oneOf: + - $ref: "../components/schemas/invalid-request-problem-details.yaml" + - $ref: "../components/schemas/problem-details.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + "404": + $ref: "../components/responses/generic-not-found-error.yaml" + "409": + $ref: "../components/responses/generic-conflict-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" + +delete: + operationId: deleteTeamMembership + summary: Delete team membership + description: Delete a team membership + tags: + - Teams + parameters: + - name: team + in: query + required: true + description: Name of the team + schema: + type: string + maxLength: 255 + - name: user + in: query + required: true + description: Name of the user + schema: + type: string + maxLength: 255 + responses: + "204": + description: Team membership deleted + "400": + $ref: "../components/responses/invalid-request-error.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + "404": + $ref: "../components/responses/generic-not-found-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/paths/teams.yaml b/api/src/main/openapi/paths/teams.yaml new file mode 100644 index 0000000000..b81ce72663 --- /dev/null +++ b/api/src/main/openapi/paths/teams.yaml @@ -0,0 +1,72 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +get: + operationId: listTeams + summary: List all teams + description: Returns a paginated list of teams, sorted by name in ascending order + tags: + - Teams + parameters: + - $ref: "../components/parameters/pagination-limit.yaml" + - $ref: "../components/parameters/page-token.yaml" + responses: + "200": + description: Paginated list of teams + content: + application/json: + schema: + $ref: "../components/schemas/list-teams-response.yaml" + "400": + $ref: "../components/responses/invalid-request-error.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" + +post: + operationId: createTeam + summary: Create team + description: Create a team + tags: + - Teams + requestBody: + required: true + content: + application/json: + schema: + $ref: "../components/schemas/create-team-request.yaml" + responses: + "201": + description: Team Created + "400": + description: Bad Request + content: + application/problem+json: + schema: + oneOf: + - $ref: "../components/schemas/invalid-request-problem-details.yaml" + - $ref: "../components/schemas/problem-details.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + "409": + $ref: "../components/responses/generic-conflict-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/paths/teams__name_.yaml b/api/src/main/openapi/paths/teams__name_.yaml new file mode 100644 index 0000000000..5a3386cbcf --- /dev/null +++ b/api/src/main/openapi/paths/teams__name_.yaml @@ -0,0 +1,73 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +get: + operationId: getTeam + summary: Get a team + description: Returns detailed information about a given team + tags: + - Teams + parameters: + - name: name + description: Name of the team + in: path + required: true + schema: + type: string + maxLength: 255 + responses: + "200": + description: Team details + content: + application/json: + schema: + $ref: "../components/schemas/get-team-response.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + "404": + $ref: "../components/responses/generic-not-found-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" + +delete: + operationId: deleteTeam + summary: Delete team + description: Delete a team + tags: + - Teams + parameters: + - name: name + description: Name of the team + in: path + required: true + schema: + type: string + maxLength: 255 + responses: + "204": + description: Team Deleted + "400": + $ref: "../components/responses/invalid-request-error.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + "404": + $ref: "../components/responses/generic-not-found-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" \ No newline at end of file diff --git a/api/src/main/openapi/paths/workflows__token__.yaml b/api/src/main/openapi/paths/workflows__token__.yaml new file mode 100644 index 0000000000..a852f8c9a9 --- /dev/null +++ b/api/src/main/openapi/paths/workflows__token__.yaml @@ -0,0 +1,45 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +get: + operationId: getWorkflowStates + summary: Retrieves workflow states associated with the token received from bom upload. + description: Requires permission BOM_UPLOAD + tags: + - Workflows + parameters: + - name: token + in: path + description: The token to query + required: true + schema: + type: string + format: uuid + responses: + "200": + description: A list of workflow states + content: + application/json: + schema: + $ref: "../components/schemas/list-workflow-states-response.yaml" + "401": + $ref: "../components/responses/generic-unauthorized-error.yaml" + "403": + $ref: "../components/responses/generic-forbidden-error.yaml" + "404": + $ref: "../components/responses/generic-not-found-error.yaml" + default: + $ref: "../components/responses/generic-error.yaml" \ No newline at end of file diff --git a/api/src/main/spectral/functions/is-object-schema.js b/api/src/main/spectral/functions/is-object-schema.js new file mode 100644 index 0000000000..5d39a67d4c --- /dev/null +++ b/api/src/main/spectral/functions/is-object-schema.js @@ -0,0 +1,49 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +'use strict'; + +const assertObjectSchema = (schema) => { + if (schema.type !== 'object') { + throw 'Schema type is not `object`'; + } + if (schema.additionalProperties) { + throw 'Schema is a map'; + } +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.forEach(check); + } else { + assertObjectSchema(schema); + } +}; + +export default (targetValue) => { + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; \ No newline at end of file diff --git a/api/src/main/spectral/functions/is-problem-json-schema.js b/api/src/main/spectral/functions/is-problem-json-schema.js new file mode 100644 index 0000000000..9e39d4e928 --- /dev/null +++ b/api/src/main/spectral/functions/is-problem-json-schema.js @@ -0,0 +1,110 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +'use strict'; + +/* +Minimal required problem json schema: + +type: object +properties: + type: + type: string + format: uri + title: + type: string + status: + type: integer + format: int32 + detail: + type: string + instance: + type: string +*/ + +const assertProblemSchema = (schema) => { + if (schema.type !== 'object') { + throw "Problem json must have type 'object'"; + } + const type = (schema.properties || {}).type || {}; + if (type.type !== 'string' || type.format !== 'uri') { + throw "Problem json must have property 'type' with type 'string' and format 'uri'"; + } + const title = (schema.properties || {}).title || {}; + if (title.type !== 'string') { + throw "Problem json must have property 'title' with type 'string'"; + } + const status = (schema.properties || {}).status || {}; + if (status.type !== 'integer' || status.format !== 'int32') { + throw "Problem json must have property 'status' with type 'integer' and format 'int32'"; + } + const detail = (schema.properties || {}).detail || {}; + if (detail.type !== 'string') { + throw "Problem json must have property 'detail' with type 'string'"; + } + const instance = (schema.properties || {}).instance || {}; + if (instance.type !== 'string') { + throw "Problem json must have property 'instance' with type 'string'"; + } +}; + +/* + * Merge list of schema definitions of type = 'object'. + * Return object will have a super set of attributes 'properties' and 'required'. + */ +const mergeObjectDefinitions = (allOfTypes) => { + if (allOfTypes.filter((item) => item.type !== 'object').length !== 0) { + throw "All schema definitions must be of type 'object'"; + } + + return allOfTypes.reduce((acc, item) => { + return { + type: 'object', + properties: { ...(acc.properties || {}), ...(item.properties || {}) }, + required: [...(acc.required || []), ...(item.required || [])], + }; + }, {}); +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || [])]; + if (schema.allOf) { + const mergedAllOf = mergeObjectDefinitions(schema.allOf); + if (mergedAllOf) { + combinedSchemas.push(mergedAllOf); + } + } + + if (combinedSchemas.length > 0) { + combinedSchemas.forEach(check); + } else { + assertProblemSchema(schema); + } +}; + +export default (targetValue) => { + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; \ No newline at end of file diff --git a/api/src/main/spectral/ruleset.yaml b/api/src/main/spectral/ruleset.yaml new file mode 100644 index 0000000000..a440830ee8 --- /dev/null +++ b/api/src/main/spectral/ruleset.yaml @@ -0,0 +1,21 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +extends: +- ["spectral:oas", "all"] +- ["./zalando.yaml", "all"] +formats: +- "oas3" \ No newline at end of file diff --git a/api/src/main/spectral/zalando.yaml b/api/src/main/spectral/zalando.yaml new file mode 100644 index 0000000000..876159b3aa --- /dev/null +++ b/api/src/main/spectral/zalando.yaml @@ -0,0 +1,215 @@ +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +# Rules to assert conformance to a curated subset of Zalando's +# RESTful API guidelines: https://opensource.zalando.com/restful-api-guidelines/# +# +# Credit to the folks at baloise for providing these spectral rules: +# https://github.com/baloise-incubator/spectral-ruleset/blob/main/zalando.yml +functions: +- is-object-schema +- is-problem-json-schema + +rules: + must-always-return-json-objects-as-top-level-data-structures: + message: 'Top-level data structure must be an object' + description: MUST always return JSON objects as top-level data structures [110] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#110 + severity: error + given: "$.paths.*.*[responses,requestBody]..content[?(@property.match(/^application\\/([a-zA-Z0-9._-]+\\+)?json(;.*)?$/))]..schema" + then: + function: is-object-schema + + must-use-semantic-versioning: + message: '{{error}}' + description: MUST use semantic versioning [116] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#116 + severity: error + given: $.info.version + then: + function: schema + functionOptions: + schema: + type: string + pattern: '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$' + + must-use-snake-case-for-property-names: + message: Property name has to be ASCII snake_case + description: MUST property names must be ASCII snake_case [118] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#118 + severity: error + given: $.paths.*.*[responses,requestBody]..content..schema..properties.*~ + then: + function: pattern + functionOptions: + match: ^[a-z_][a-z_0-9]*$ + + must-use-lowercase-with-hypens-for-path-segements: + message: Path segments have to be lowercase separate words with hyphens + description: MUST use lowercase separate words with hyphens for path segments [129] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#129 + severity: error + given: $.paths.*~ + then: + function: pattern + functionOptions: + match: ^(?=((([\/a-z][a-z0-9\-\/]*)?({[^}]*})?)+))\1$ + + must-use-snake-case-for-query-parameters: + message: Query parameters must be snake_case + description: MUST use snake_case (never camelCase) for query parameters [130] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#130 + severity: error + given: $.paths.*.*.parameters[?(@ && @.in=='query')].name + then: + function: pattern + functionOptions: + match: ^[a-z][_a-z0-9]*$ + + must-use-normalized-paths-without-empty-path-segments: + message: Empty path segments are not allowed + description: MUST use normalized paths without empty path segments and trailing slashes [136] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#136 + severity: error + given: $.paths.*~ + then: + function: pattern + functionOptions: + notMatch: // + + must-use-normalized-paths-without-trailing-slash: + message: Path with trailing slash is not allowed + description: MUST use normalized paths without empty path segments and trailing slashes [136] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#136 + severity: error + given: $.paths.*~ + then: + function: pattern + functionOptions: + notMatch: /$ + + must-specify-default-response: + message: Operation does not contain a default response + description: MUST specify success and error responses [151] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#151 + severity: error + given: $.paths.*.*.responses + then: + field: default + function: truthy + + must-use-standard-formats-for-date-and-time-properties-example: + message: "You should provide an example for {{property}}" + description: MUST use standard formats for date and time properties [169] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#169 + severity: warn # Not an error as you only should provide an example to help your consumers + given: $.paths..[?(@.type === 'string' && (@.format === 'date-time' || @.format === 'date' || @.format === 'time' || @.format === 'duration' || @.format === 'period'))] + then: + field: example + function: truthy + + must-use-standard-formats-for-date-and-time-properties-utc: + message: "You should UTC for {{property}}" + description: MUST use standard formats for date and time properties [169] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#169 + severity: warn # Not an error as you only should provide an example to help your consumers + given: $.paths..[?(@.type === 'string' && @.format === 'date-time')] + then: + field: example + function: pattern + functionOptions: + match: "Z$" + + must-use-problem-json-as-default-response: + message: Operation must use problem json as default response + description: MUST specify success and error responses [151] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#151 + severity: error + given: $.paths.*.*.responses.default + then: + field: content.application/problem+json + function: truthy + + must-define-a-format-for-number-types: + message: Numeric properties must have valid format specified + description: MUST define a format for number and integer types [171] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#171 + severity: error + given: $.paths.*.*..schema..properties..[?(@ && @.type=='number')] + then: + - field: format + function: defined + - field: format + function: pattern + functionOptions: + match: ^(float|double|decimal)$ + + must-define-a-format-for-integer-types: + message: Numeric properties must have valid format specified + description: MUST define a format for number and integer types [171] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#171 + severity: error + given: $.paths.*.*..schema..properties..[?(@ && @.type=='integer')] + then: + - field: format + function: defined + - field: format + function: pattern + functionOptions: + match: ^(int32|int64|bigint)$ + + should-prefer-standard-media-type-names: + message: Custom media types should only be used for versioning + description: SHOULD prefer standard media type name application/json [172] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#172 + severity: warn + given: $.paths.*.*.responses.*.content.*~ + then: + function: pattern + functionOptions: + match: ^application\/(problem\+)?json$|^[a-zA-Z0-9_]+\/[-+.a-zA-Z0-9_]+;(v|version)=[0-9]+$ + + must-use-problem-json-for-errors: + message: Error response must be application/problem+json + description: MUST support problem JSON [176] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#176 + severity: error + given: $.paths.*.*.responses[?(@ && @property.match(/^(4|5)/))] + then: + field: content.application/problem+json + function: truthy + + must-use-valid-problem-json-schema: + message: '{{error}}' + description: MUST support problem JSON [176] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#176 + severity: error + given: $.paths.*.*.responses.*.content.application/problem+json + then: + field: schema + function: is-problem-json-schema + + should-declare-enum-values-using-upper-snake-case-format: + message: 'Enum values should be in UPPER_SNAKE_CASE format' + description: SHOULD declare enum values using UPPER_SNAKE_CASE format [240] + documentationUrl: https://opensource.zalando.com/restful-api-guidelines/#240 + severity: warn + given: $.paths..[?(@ && @.type=='string')].[enum,x-extensible-enum].* + then: + function: pattern + functionOptions: + match: ^[A-Z][A-Z_0-9]*$ \ No newline at end of file diff --git a/apiserver/pom.xml b/apiserver/pom.xml index e3bdef43cc..41cbdb9478 100644 --- a/apiserver/pom.xml +++ b/apiserver/pom.xml @@ -19,7 +19,7 @@ 4.3.0 - 1.25.1 + 1.25.2 1.27.1 3.0.0 1.4.3 @@ -28,7 +28,7 @@ 4.0.5 4.1.0 4.13.2 - 3.9.0 + 3.9.1 0.2.2 8.5.17 2.2.0 @@ -38,7 +38,7 @@ 3.2.4 2.3.0 2.2.34 - 2.1.30 + 2.1.31 1.19.0 0.7.0 7.1.1 @@ -73,43 +73,57 @@ + + org.dependencytrack + api + ${project.version} + org.dependencytrack datanucleus-plugin + ${project.version} org.dependencytrack liquibase-support + ${project.version} org.dependencytrack persistence-jooq + ${project.version} org.dependencytrack persistence-migration + ${project.version} org.dependencytrack proto + ${project.version} org.dependencytrack alpine-common + ${project.version} org.dependencytrack alpine-model + ${project.version} org.dependencytrack alpine-infra + ${project.version} org.dependencytrack alpine-server + ${project.version} @@ -126,6 +140,7 @@ org.dependencytrack alpine-executable-war + ${project.version} provided @@ -309,7 +324,7 @@ org.apache.maven maven-artifact - 3.9.10 + 3.9.11 diff --git a/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java b/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java index 58440d66a6..a11bbdbdac 100644 --- a/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java +++ b/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java @@ -52,17 +52,15 @@ public enum ConfigKey implements Config.Key { VULNERABILITY_POLICY_S3_BUCKET_NAME("vulnerability.policy.s3.bucket.name", null), VULNERABILITY_POLICY_S3_BUNDLE_NAME("vulnerability.policy.s3.bundle.name", null), VULNERABILITY_POLICY_S3_REGION("vulnerability.policy.s3.region", null), - DATABASE_MIGRATION_URL("database.migration.url", null), - DATABASE_MIGRATION_USERNAME("database.migration.username", null), - DATABASE_MIGRATION_PASSWORD("database.migration.password", null), - DATABASE_RUN_MIGRATIONS("database.run.migrations", true), - DATABASE_RUN_MIGRATIONS_ONLY("database.run.migrations.only", false), INIT_TASKS_ENABLED("init.tasks.enabled", true), + INIT_TASKS_DATABASE_URL("init.tasks.database.url", null), + INIT_TASKS_DATABASE_USERNAME("init.tasks.database.username", null), + INIT_TASKS_DATABASE_PASSWORD("init.tasks.database.password", null), INIT_AND_EXIT("init.and.exit", false), DEV_SERVICES_ENABLED("dev.services.enabled", false), DEV_SERVICES_IMAGE_FRONTEND("dev.services.image.frontend", "ghcr.io/dependencytrack/hyades-frontend:snapshot"), - DEV_SERVICES_IMAGE_KAFKA("dev.services.image.kafka", "apache/kafka-native:3.9.0"), + DEV_SERVICES_IMAGE_KAFKA("dev.services.image.kafka", "apache/kafka-native:3.9.1"), DEV_SERVICES_IMAGE_POSTGRES("dev.services.image.postgres", "postgres:13-alpine"), DEV_SERVICES_PORT_FRONTEND("dev.services.port.frontend", 8081), DEV_SERVICES_PORT_KAFKA("dev.services.port.kafka", 9092), diff --git a/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java index b580191d0a..cc8fa39014 100644 --- a/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java +++ b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java @@ -23,12 +23,9 @@ import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider; import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener; -import jakarta.ws.rs.ext.Provider; - /** * @since 5.5.0 */ -@Provider public class JerseyMetricsApplicationEventListener extends MetricsApplicationEventListener { public JerseyMetricsApplicationEventListener() { diff --git a/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsFeature.java b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsFeature.java new file mode 100644 index 0000000000..4843a94c03 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsFeature.java @@ -0,0 +1,45 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.filters; + +import alpine.Config; + +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ext.Provider; + +import static alpine.Config.AlpineKey.METRICS_ENABLED; + +/** + * @since 5.6.0 + */ +@Provider +public class JerseyMetricsFeature implements Feature { + + @Override + public boolean configure(final FeatureContext context) { + if (Config.getInstance().getPropertyAsBoolean(METRICS_ENABLED)) { + context.register(JerseyMetricsApplicationEventListener.class); + return true; + } + + return false; + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java b/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java index 331b604bee..acb9fb7724 100644 --- a/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java +++ b/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java @@ -18,11 +18,9 @@ */ package org.dependencytrack.health; -import alpine.Config; import alpine.common.logging.Logger; import alpine.server.health.HealthCheckRegistry; import alpine.server.health.checks.DatabaseHealthCheck; -import org.dependencytrack.common.ConfigKey; import org.dependencytrack.event.kafka.processor.ProcessorsHealthCheck; import jakarta.servlet.ServletContextEvent; @@ -34,12 +32,6 @@ public class HealthCheckInitializer implements ServletContextListener { @Override public void contextInitialized(final ServletContextEvent event) { - if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) { - LOGGER.debug("Not registering health checks because %s is enabled" - .formatted(ConfigKey.INIT_AND_EXIT.getPropertyName())); - return; - } - LOGGER.info("Registering health checks"); HealthCheckRegistry.getInstance().register("database", new DatabaseHealthCheck()); HealthCheckRegistry.getInstance().register("kafka-processors", new ProcessorsHealthCheck()); diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTask.java b/apiserver/src/main/java/org/dependencytrack/init/InitTask.java new file mode 100644 index 0000000000..cea6327f2c --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/init/InitTask.java @@ -0,0 +1,51 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.init; + +/** + * A task to be run on application startup. + * + * @since 5.6.0 + */ +public interface InitTask { + + int PRIORITY_HIGHEST = 100; + int PRIORITY_LOWEST = 0; + + /** + * @return Priority of the task. + * @see #PRIORITY_HIGHEST + * @see #PRIORITY_LOWEST + */ + int priority(); + + /** + * @return Name of the task. Must be globally unique. + */ + String name(); + + /** + * Execute the task. + * + * @param ctx Context in which the task is executed. + * @throws Exception When the task execution failed. + */ + void execute(InitTaskContext ctx) throws Exception; + +} diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java new file mode 100644 index 0000000000..be01f297fe --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java @@ -0,0 +1,36 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.init; + +import alpine.Config; + +import javax.sql.DataSource; + +/** + * Context available to {@link InitTask}s. + *

+ * TODO: Introduce a tiny abstraction over {@link Config} such that + * Alpine specifics don't bleed through to {@link InitTask}s. + * + * @param config A {@link Config} instance to read application configuration. + * @param dataSource A {@link DataSource} which may be used for database interactions. + * @since 5.6.0 + */ +public record InitTaskContext(Config config, DataSource dataSource) { +} diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java new file mode 100644 index 0000000000..2ee6c5837a --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java @@ -0,0 +1,180 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.init; + +import alpine.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.ServletContextListener; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static java.util.Comparator.comparing; +import static java.util.Comparator.reverseOrder; +import static java.util.Objects.requireNonNull; +import static org.dependencytrack.util.ConfigUtil.getPassThroughProperties; + +/** + * @since 5.6.0 + */ +final class InitTaskExecutor implements ServletContextListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(InitTaskExecutor.class); + private static final long ADVISORY_LOCK_KEY = "dependency-track-init-tasks".hashCode(); + + private final Config config; + private final DataSource dataSource; + private final List tasks; + + InitTaskExecutor(final Config config, final DataSource dataSource) { + this(config, dataSource, loadInitTasks()); + } + + InitTaskExecutor(final Config config, final DataSource dataSource, final List tasks) { + this.config = requireNonNull(config, "config must not be null"); + this.dataSource = requireNonNull(dataSource, "dataSource must not be null"); + this.tasks = requireNonNull(tasks, "tasks must not be null"); + } + + public void execute() { + final List orderedTasks = this.tasks.stream() + .peek(requireUniqueName()) + .peek(requireValidPriority()) + .filter(isTaskEnabled()) + .sorted(comparing(InitTask::priority, reverseOrder()) + .thenComparing(InitTask::name)) + .toList(); + + final long startTimeNanos = System.nanoTime(); + + // We're using session-level advisory locks here, + // which won't work when using PgBouncer in "transaction" mode. + // We can't use transaction-level locking because that would + // block some DDL statements executed by database migrations, + // such as "CREATE INDEX CONCURRENTLY". + // + // This GitLab issue describes the problem well: + // https://gitlab.com/gitlab-com/support/support-training/-/issues/3823#locks-block-a-gitlab-database-migration + // + // The intended workaround is to use a separate set of connection + // details specifically for init tasks, which bypasses PgBouncer. + try (final Connection connection = dataSource.getConnection(); + final PreparedStatement lockStatement = connection.prepareStatement(""" + SELECT PG_ADVISORY_LOCK(?) + """); + final PreparedStatement unlockStatement = connection.prepareStatement(""" + SELECT PG_ADVISORY_UNLOCK(?) + """)) { + LOGGER.debug("Trying to acquire lock {}", ADVISORY_LOCK_KEY); + lockStatement.setLong(1, ADVISORY_LOCK_KEY); + lockStatement.execute(); + LOGGER.debug( + "Lock {} acquired after {}ms", + ADVISORY_LOCK_KEY, + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos)); + + final var taskContext = new InitTaskContext(config, dataSource); + + try { + long taskStartTimeNanos; + for (final InitTask task : orderedTasks) { + taskStartTimeNanos = System.nanoTime(); + LOGGER.info("Executing init task {}", task.name()); + try { + task.execute(taskContext); + LOGGER.info( + "Completed init task {} in {}ms", + task.name(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - taskStartTimeNanos)); + } catch (Exception e) { + throw new IllegalStateException("Failed to execute init task " + task.name(), e); + } + } + } finally { + LOGGER.debug("Releasing lock {}", ADVISORY_LOCK_KEY); + unlockStatement.setLong(1, ADVISORY_LOCK_KEY); + final ResultSet rs = unlockStatement.executeQuery(); + if (!rs.next() || !rs.getBoolean(1)) { + LOGGER.warn(""" + Lock {} could not be released, likely because a connection pooler \ + in "transaction" mode is being used. Ensure that a direct database connection \ + is provided when executing init tasks.""", ADVISORY_LOCK_KEY); + } + } + } catch (SQLException e) { + throw new IllegalStateException("Failed to acquire or release lock " + ADVISORY_LOCK_KEY, e); + } + + LOGGER.info( + "All init tasks completed in {}ms", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos)); + } + + private static List loadInitTasks() { + return ServiceLoader.load(InitTask.class).stream() + .map(ServiceLoader.Provider::get) + .toList(); + } + + private Consumer requireUniqueName() { + final var seenTaskClassesByTaskName = + new HashMap>(this.tasks.size()); + + return task -> { + final Class previousClass = + seenTaskClassesByTaskName.put(task.name(), task.getClass()); + if (previousClass != null) { + throw new IllegalStateException( + "Duplicate task name %s: Registered by %s and %s".formatted( + task.name(), previousClass.getName(), task.getClass().getName())); + } + }; + } + + private Consumer requireValidPriority() { + return task -> { + if (task.priority() < InitTask.PRIORITY_LOWEST + || task.priority() > InitTask.PRIORITY_HIGHEST) { + throw new IllegalStateException( + "Invalid priority of task %s: Must be within [%d..%d] but is %d".formatted( + task.name(), InitTask.PRIORITY_LOWEST, InitTask.PRIORITY_HIGHEST, task.priority())); + } + }; + } + + private Predicate isTaskEnabled() { + return task -> { + final String propertyPrefix = "init.task." + task.name(); + final Map properties = getPassThroughProperties(config, propertyPrefix); + return !"false".equals(properties.get(propertyPrefix + ".enabled")); + }; + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java new file mode 100644 index 0000000000..5686412dfc --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java @@ -0,0 +1,99 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.init; + +import alpine.Config; +import org.dependencytrack.common.ConfigKey; +import org.postgresql.ds.PGSimpleDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import javax.sql.DataSource; + +import static alpine.Config.AlpineKey.DATABASE_PASSWORD; +import static alpine.Config.AlpineKey.DATABASE_URL; +import static alpine.Config.AlpineKey.DATABASE_USERNAME; +import static java.util.Objects.requireNonNullElseGet; +import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_PASSWORD; +import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_URL; +import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_USERNAME; + +/** + * @since 5.6.0 + */ +public final class InitTaskServletContextListener implements ServletContextListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(InitTaskServletContextListener.class); + + private final Config config; + + @SuppressWarnings("unused") + public InitTaskServletContextListener() { + this(Config.getInstance()); + } + + InitTaskServletContextListener(final Config config) { + this.config = config; + } + + @Override + public void contextInitialized(final ServletContextEvent event) { + if (!config.getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) { + LOGGER.debug( + "Not executing init tasks because {} is disabled", + ConfigKey.INIT_TASKS_ENABLED.getPropertyName()); + return; + } + + final DataSource dataSource; + try { + dataSource = createDataSource(config); + } catch (RuntimeException e) { + throw new IllegalStateException("Failed to create data source", e); + } + + final var taskExecutor = new InitTaskExecutor(config, dataSource); + taskExecutor.execute(); + + if (config.getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) { + LOGGER.info( + "Exiting because {} is enabled", + ConfigKey.INIT_AND_EXIT.getPropertyName()); + System.exit(0); + } + } + + private DataSource createDataSource(final Config config) { + final var dataSource = new PGSimpleDataSource(); + dataSource.setUrl(requireNonNullElseGet( + config.getProperty(INIT_TASKS_DATABASE_URL), + () -> config.getProperty(DATABASE_URL))); + dataSource.setUser(requireNonNullElseGet( + config.getProperty(INIT_TASKS_DATABASE_USERNAME), + () -> config.getProperty(DATABASE_USERNAME))); + dataSource.setPassword(requireNonNullElseGet( + config.getProperty(INIT_TASKS_DATABASE_PASSWORD), + () -> config.getPropertyOrFile(DATABASE_PASSWORD))); + + return dataSource; + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java b/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java new file mode 100644 index 0000000000..6a7df63fbb --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java @@ -0,0 +1,72 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +/** + * @since 5.6.0 + */ +public enum DefaultRepository { + + CPAN_PUBLIC_REGISTRY(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", 1), + GEM_RUBYGEMS(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", 1), + HEX_HEX_PM(RepositoryType.HEX, "hex.pm", "https://hex.pm/", 1), + MAVEN_CENTRAL(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", 1), + MAVEN_ATLASSIAN_PUBLIC(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", 2), + MAVEN_JBOSS_RELEASES(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", 3), + MAVEN_CLOJARS(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", 4), + MAVEN_GOOGLE_ANDROID(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", 5), + NPM_PUBLIC_REGISTRY(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", 1), + PYPI_PYPI_ORG(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", 1), + NUGET_GALLERY(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", 1), + COMPOSER_PACKAGIST(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", 1), + CARGO_CRATES_IO(RepositoryType.CARGO, "crates.io", "https://crates.io", 1), + GO_PROXY_GOLANG_ORG(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", 1), + GITHUB(RepositoryType.GITHUB, "github", "https://github.com", 1), + HACKAGE(RepositoryType.HACKAGE, "hackage.haskell", "https://hackage.haskell.org/", 1), + NIXPKGS_NIXOS_ORG(RepositoryType.NIXPKGS, "nixos.org", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", 1); + + private final RepositoryType type; + private final String identifier; + private final String url; + private final int resolutionOrder; + + DefaultRepository(final RepositoryType type, final String identifier, final String url, final int resolutionOrder) { + this.type = type; + this.identifier = identifier; + this.url = url; + this.resolutionOrder = resolutionOrder; + } + + public RepositoryType getType() { + return type; + } + + public String getIdentifier() { + return identifier; + } + + public String getUrl() { + return url; + } + + public int getResolutionOrder() { + return resolutionOrder; + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java b/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java index a775425a51..694842adea 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java +++ b/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; -import org.jdbi.v3.core.mapper.reflect.ColumnName; import jakarta.validation.constraints.NotNull; import java.io.Serializable; @@ -39,80 +38,37 @@ public class DependencyMetrics implements Serializable { private static final long serialVersionUID = 5231823328085979791L; @JsonIgnore - @NotNull private long projectId; @JsonIgnore - @NotNull private long componentId; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int critical; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int high; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int medium; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int low; - - private Integer unassigned; - + private int unassigned; private int vulnerabilities; - private int suppressed; - - private Integer findingsTotal; - - private Integer findingsAudited; - - private Integer findingsUnaudited; - + private int findingsTotal; + private int findingsAudited; + private int findingsUnaudited; private double inheritedRiskScore; - - private Integer policyViolationsFail; - - private Integer policyViolationsWarn; - - private Integer policyViolationsInfo; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsTotal; - - // New column, must allow nulls on existing databases) - private Integer policyViolationsAudited; - - // New column, must allow nulls on existing databases) - private Integer policyViolationsUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalUnaudited; + private int policyViolationsFail; + private int policyViolationsWarn; + private int policyViolationsInfo; + private int policyViolationsTotal; + private int policyViolationsAudited; + private int policyViolationsUnaudited; + private int policyViolationsSecurityTotal; + private int policyViolationsSecurityAudited; + private int policyViolationsSecurityUnaudited; + private int policyViolationsLicenseTotal; + private int policyViolationsLicenseAudited; + private int policyViolationsLicenseUnaudited; + private int policyViolationsOperationalTotal; + private int policyViolationsOperationalAudited; + private int policyViolationsOperationalUnaudited; @NotNull @Schema(type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED, description = "UNIX epoch timestamp in milliseconds") @@ -171,7 +127,7 @@ public void setLow(int low) { } public int getUnassigned() { - return unassigned != null ? unassigned : 0; + return unassigned; } public void setUnassigned(int unassigned) { @@ -195,7 +151,7 @@ public void setSuppressed(int suppressed) { } public int getFindingsTotal() { - return findingsTotal != null ? findingsTotal : 0; + return findingsTotal; } public void setFindingsTotal(int findingsTotal) { @@ -203,7 +159,7 @@ public void setFindingsTotal(int findingsTotal) { } public int getFindingsAudited() { - return findingsAudited != null ? findingsAudited : 0; + return findingsAudited; } public void setFindingsAudited(int findingsAudited) { @@ -211,14 +167,13 @@ public void setFindingsAudited(int findingsAudited) { } public int getFindingsUnaudited() { - return findingsUnaudited != null ? findingsUnaudited : 0; + return findingsUnaudited; } public void setFindingsUnaudited(int findingsUnaudited) { this.findingsUnaudited = findingsUnaudited; } - @ColumnName("RISKSCORE") public double getInheritedRiskScore() { return inheritedRiskScore; } @@ -228,7 +183,7 @@ public void setInheritedRiskScore(double inheritedRiskScore) { } public int getPolicyViolationsFail() { - return policyViolationsFail != null ? policyViolationsFail : 0; + return policyViolationsFail; } public void setPolicyViolationsFail(int policyViolationsFail) { @@ -236,7 +191,7 @@ public void setPolicyViolationsFail(int policyViolationsFail) { } public int getPolicyViolationsWarn() { - return policyViolationsWarn != null ? policyViolationsWarn : 0; + return policyViolationsWarn; } public void setPolicyViolationsWarn(int policyViolationsWarn) { @@ -244,7 +199,7 @@ public void setPolicyViolationsWarn(int policyViolationsWarn) { } public int getPolicyViolationsInfo() { - return policyViolationsInfo != null ? policyViolationsInfo : 0; + return policyViolationsInfo; } public void setPolicyViolationsInfo(int policyViolationsInfo) { @@ -252,7 +207,7 @@ public void setPolicyViolationsInfo(int policyViolationsInfo) { } public int getPolicyViolationsTotal() { - return policyViolationsTotal != null ? policyViolationsTotal : 0; + return policyViolationsTotal; } public void setPolicyViolationsTotal(int policyViolationsTotal) { @@ -260,7 +215,7 @@ public void setPolicyViolationsTotal(int policyViolationsTotal) { } public int getPolicyViolationsAudited() { - return policyViolationsAudited != null ? policyViolationsAudited : 0; + return policyViolationsAudited; } public void setPolicyViolationsAudited(int policyViolationsAudited) { @@ -268,7 +223,7 @@ public void setPolicyViolationsAudited(int policyViolationsAudited) { } public int getPolicyViolationsUnaudited() { - return policyViolationsUnaudited != null ? policyViolationsUnaudited : 0; + return policyViolationsUnaudited; } public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) { @@ -276,7 +231,7 @@ public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) { } public int getPolicyViolationsSecurityTotal() { - return policyViolationsSecurityTotal != null ? policyViolationsSecurityTotal : 0; + return policyViolationsSecurityTotal; } public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) { @@ -284,7 +239,7 @@ public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) } public int getPolicyViolationsSecurityAudited() { - return policyViolationsSecurityAudited != null ? policyViolationsSecurityAudited : 0; + return policyViolationsSecurityAudited; } public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudited) { @@ -292,7 +247,7 @@ public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudit } public int getPolicyViolationsSecurityUnaudited() { - return policyViolationsSecurityUnaudited != null ? policyViolationsSecurityUnaudited : 0; + return policyViolationsSecurityUnaudited; } public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUnaudited) { @@ -300,7 +255,7 @@ public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUna } public int getPolicyViolationsLicenseTotal() { - return policyViolationsLicenseTotal != null ? policyViolationsLicenseTotal : 0; + return policyViolationsLicenseTotal; } public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) { @@ -308,7 +263,7 @@ public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) { } public int getPolicyViolationsLicenseAudited() { - return policyViolationsLicenseAudited != null ? policyViolationsLicenseAudited : 0; + return policyViolationsLicenseAudited; } public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited) { @@ -316,7 +271,7 @@ public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited } public int getPolicyViolationsLicenseUnaudited() { - return policyViolationsLicenseUnaudited != null ? policyViolationsLicenseUnaudited : 0; + return policyViolationsLicenseUnaudited; } public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaudited) { @@ -324,7 +279,7 @@ public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaud } public int getPolicyViolationsOperationalTotal() { - return policyViolationsOperationalTotal != null ? policyViolationsOperationalTotal : 0; + return policyViolationsOperationalTotal; } public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalTotal) { @@ -332,7 +287,7 @@ public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalT } public int getPolicyViolationsOperationalAudited() { - return policyViolationsOperationalAudited != null ? policyViolationsOperationalAudited : 0; + return policyViolationsOperationalAudited; } public void setPolicyViolationsOperationalAudited(int policyViolationsOperationalAudited) { @@ -340,7 +295,7 @@ public void setPolicyViolationsOperationalAudited(int policyViolationsOperationa } public int getPolicyViolationsOperationalUnaudited() { - return policyViolationsOperationalUnaudited != null ? policyViolationsOperationalUnaudited : 0; + return policyViolationsOperationalUnaudited; } public void setPolicyViolationsOperationalUnaudited(int policyViolationsOperationalUnaudited) { diff --git a/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java b/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java index da6ebb857d..bf254e5bbc 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java +++ b/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java @@ -18,12 +18,10 @@ */ package org.dependencytrack.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import org.jdbi.v3.core.mapper.reflect.ColumnName; +import jakarta.validation.constraints.NotNull; import java.io.Serializable; import java.util.Date; @@ -39,81 +37,36 @@ public class PortfolioMetrics implements Serializable { private static final long serialVersionUID = -7690624184866776922L; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int critical; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int high; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int medium; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int low; - - private Integer unassigned; - + private int unassigned; private int vulnerabilities; - private int projects; - private int vulnerableProjects; - private int components; - private int vulnerableComponents; - private int suppressed; - - private Integer findingsTotal; - - private Integer findingsAudited; - - private Integer findingsUnaudited; - + private int findingsTotal; + private int findingsAudited; + private int findingsUnaudited; private double inheritedRiskScore; - - private Integer policyViolationsFail; - - private Integer policyViolationsWarn; - - private Integer policyViolationsInfo; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsTotal; - - // New column, must allow nulls on existing databases) - private Integer policyViolationsAudited; - - // New column, must allow nulls on existing databases) - private Integer policyViolationsUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalUnaudited; + private int policyViolationsFail; + private int policyViolationsWarn; + private int policyViolationsInfo; + private int policyViolationsTotal; + private int policyViolationsAudited; + private int policyViolationsUnaudited; + private int policyViolationsSecurityTotal; + private int policyViolationsSecurityAudited; + private int policyViolationsSecurityUnaudited; + private int policyViolationsLicenseTotal; + private int policyViolationsLicenseAudited; + private int policyViolationsLicenseUnaudited; + private int policyViolationsOperationalTotal; + private int policyViolationsOperationalAudited; + private int policyViolationsOperationalUnaudited; @NotNull @Schema(type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED, description = "UNIX epoch timestamp in milliseconds") @@ -156,7 +109,7 @@ public void setLow(int low) { } public int getUnassigned() { - return unassigned != null ? unassigned : 0; + return unassigned; } public void setUnassigned(int unassigned) { @@ -212,7 +165,7 @@ public void setSuppressed(int suppressed) { } public int getFindingsTotal() { - return findingsTotal != null ? findingsTotal : 0; + return findingsTotal; } public void setFindingsTotal(int findingsTotal) { @@ -220,7 +173,7 @@ public void setFindingsTotal(int findingsTotal) { } public int getFindingsAudited() { - return findingsAudited != null ? findingsAudited : 0; + return findingsAudited; } public void setFindingsAudited(int findingsAudited) { @@ -228,14 +181,13 @@ public void setFindingsAudited(int findingsAudited) { } public int getFindingsUnaudited() { - return findingsUnaudited != null ? findingsUnaudited : 0; + return findingsUnaudited; } public void setFindingsUnaudited(int findingsUnaudited) { this.findingsUnaudited = findingsUnaudited; } - @ColumnName("RISKSCORE") public double getInheritedRiskScore() { return inheritedRiskScore; } @@ -245,7 +197,7 @@ public void setInheritedRiskScore(double inheritedRiskScore) { } public int getPolicyViolationsFail() { - return policyViolationsFail != null ? policyViolationsFail : 0; + return policyViolationsFail; } public void setPolicyViolationsFail(int policyViolationsFail) { @@ -253,7 +205,7 @@ public void setPolicyViolationsFail(int policyViolationsFail) { } public int getPolicyViolationsWarn() { - return policyViolationsWarn != null ? policyViolationsWarn : 0; + return policyViolationsWarn; } public void setPolicyViolationsWarn(int policyViolationsWarn) { @@ -261,7 +213,7 @@ public void setPolicyViolationsWarn(int policyViolationsWarn) { } public int getPolicyViolationsInfo() { - return policyViolationsInfo != null ? policyViolationsInfo : 0; + return policyViolationsInfo; } public void setPolicyViolationsInfo(int policyViolationsInfo) { @@ -269,7 +221,7 @@ public void setPolicyViolationsInfo(int policyViolationsInfo) { } public int getPolicyViolationsTotal() { - return policyViolationsTotal != null ? policyViolationsTotal : 0; + return policyViolationsTotal; } public void setPolicyViolationsTotal(int policyViolationsTotal) { @@ -277,7 +229,7 @@ public void setPolicyViolationsTotal(int policyViolationsTotal) { } public int getPolicyViolationsAudited() { - return policyViolationsAudited != null ? policyViolationsAudited : 0; + return policyViolationsAudited; } public void setPolicyViolationsAudited(int policyViolationsAudited) { @@ -285,7 +237,7 @@ public void setPolicyViolationsAudited(int policyViolationsAudited) { } public int getPolicyViolationsUnaudited() { - return policyViolationsUnaudited != null ? policyViolationsUnaudited : 0; + return policyViolationsUnaudited; } public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) { @@ -293,7 +245,7 @@ public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) { } public int getPolicyViolationsSecurityTotal() { - return policyViolationsSecurityTotal != null ? policyViolationsSecurityTotal : 0; + return policyViolationsSecurityTotal; } public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) { @@ -301,7 +253,7 @@ public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) } public int getPolicyViolationsSecurityAudited() { - return policyViolationsSecurityAudited != null ? policyViolationsSecurityAudited : 0; + return policyViolationsSecurityAudited; } public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudited) { @@ -309,7 +261,7 @@ public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudit } public int getPolicyViolationsSecurityUnaudited() { - return policyViolationsSecurityUnaudited != null ? policyViolationsSecurityUnaudited : 0; + return policyViolationsSecurityUnaudited; } public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUnaudited) { @@ -317,7 +269,7 @@ public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUna } public int getPolicyViolationsLicenseTotal() { - return policyViolationsLicenseTotal != null ? policyViolationsLicenseTotal : 0; + return policyViolationsLicenseTotal; } public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) { @@ -325,7 +277,7 @@ public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) { } public int getPolicyViolationsLicenseAudited() { - return policyViolationsLicenseAudited != null ? policyViolationsLicenseAudited : 0; + return policyViolationsLicenseAudited; } public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited) { @@ -333,7 +285,7 @@ public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited } public int getPolicyViolationsLicenseUnaudited() { - return policyViolationsLicenseUnaudited != null ? policyViolationsLicenseUnaudited : 0; + return policyViolationsLicenseUnaudited; } public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaudited) { @@ -341,7 +293,7 @@ public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaud } public int getPolicyViolationsOperationalTotal() { - return policyViolationsOperationalTotal != null ? policyViolationsOperationalTotal : 0; + return policyViolationsOperationalTotal; } public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalTotal) { @@ -349,7 +301,7 @@ public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalT } public int getPolicyViolationsOperationalAudited() { - return policyViolationsOperationalAudited != null ? policyViolationsOperationalAudited : 0; + return policyViolationsOperationalAudited; } public void setPolicyViolationsOperationalAudited(int policyViolationsOperationalAudited) { @@ -357,7 +309,7 @@ public void setPolicyViolationsOperationalAudited(int policyViolationsOperationa } public int getPolicyViolationsOperationalUnaudited() { - return policyViolationsOperationalUnaudited != null ? policyViolationsOperationalUnaudited : 0; + return policyViolationsOperationalUnaudited; } public void setPolicyViolationsOperationalUnaudited(int policyViolationsOperationalUnaudited) { @@ -380,39 +332,4 @@ public void setLastOccurrence(Date lastOccurrence) { this.lastOccurrence = lastOccurrence; } - @JsonIgnore - public boolean hasChanged(final PortfolioMetrics comparedTo) { - return comparedTo == null - || comparedTo.getCritical() != this.critical - || comparedTo.getHigh() != this.high - || comparedTo.getMedium() != this.medium - || comparedTo.getLow() != this.low - || comparedTo.getUnassigned() != this.unassigned - || comparedTo.getVulnerabilities() != this.vulnerabilities - || comparedTo.getInheritedRiskScore() != this.inheritedRiskScore - || comparedTo.getPolicyViolationsFail() != this.policyViolationsFail - || comparedTo.getPolicyViolationsWarn() != this.policyViolationsWarn - || comparedTo.getPolicyViolationsInfo() != this.policyViolationsInfo - || comparedTo.getPolicyViolationsTotal() != this.policyViolationsTotal - || comparedTo.getPolicyViolationsAudited() != this.policyViolationsAudited - || comparedTo.getPolicyViolationsUnaudited() != this.policyViolationsUnaudited - || comparedTo.getPolicyViolationsSecurityTotal() != this.policyViolationsSecurityTotal - || comparedTo.getPolicyViolationsSecurityAudited() != this.policyViolationsSecurityAudited - || comparedTo.getPolicyViolationsSecurityUnaudited() != this.policyViolationsSecurityUnaudited - || comparedTo.getPolicyViolationsLicenseTotal() != this.policyViolationsLicenseTotal - || comparedTo.getPolicyViolationsLicenseAudited() != this.policyViolationsLicenseAudited - || comparedTo.getPolicyViolationsLicenseUnaudited() != this.policyViolationsLicenseUnaudited - || comparedTo.getPolicyViolationsOperationalTotal() != this.policyViolationsOperationalTotal - || comparedTo.getPolicyViolationsOperationalAudited() != this.policyViolationsOperationalAudited - || comparedTo.getPolicyViolationsOperationalUnaudited() != this.policyViolationsOperationalUnaudited - || comparedTo.getComponents() != this.components - || comparedTo.getVulnerableComponents() != this.vulnerableComponents - || comparedTo.getSuppressed() != this.suppressed - || comparedTo.getFindingsTotal() != this.findingsTotal - || comparedTo.getFindingsAudited() != this.findingsAudited - || comparedTo.getFindingsUnaudited() != this.findingsUnaudited - || comparedTo.getProjects() != this.projects - || comparedTo.getVulnerableProjects() != this.vulnerableProjects; - } - } diff --git a/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java b/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java index fbd9f321b8..0324911fcd 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; -import org.jdbi.v3.core.mapper.reflect.ColumnName; import jakarta.validation.constraints.NotNull; import java.io.Serializable; @@ -39,80 +38,36 @@ public class ProjectMetrics implements Serializable { private static final long serialVersionUID = 8741534340846353210L; @JsonIgnore - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private long projectId; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int critical; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int high; - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private int medium; - - @NotNull private int low; - - private Integer unassigned; - + private int unassigned; private int vulnerabilities; - private int vulnerableComponents; - private int components; - private int suppressed; - - private Integer findingsTotal; - - private Integer findingsAudited; - - private Integer findingsUnaudited; - + private int findingsTotal; + private int findingsAudited; + private int findingsUnaudited; private double inheritedRiskScore; - - private Integer policyViolationsFail; - - private Integer policyViolationsWarn; - - private Integer policyViolationsInfo; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsTotal; - - // New column, must allow nulls on existing databases) - private Integer policyViolationsAudited; - - // New column, must allow nulls on existing databases) - private Integer policyViolationsUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsSecurityUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsLicenseUnaudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalTotal; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalAudited; - - // New column, must allow nulls on existing data bases) - private Integer policyViolationsOperationalUnaudited; + private int policyViolationsFail; + private int policyViolationsWarn; + private int policyViolationsInfo; + private int policyViolationsTotal; + private int policyViolationsAudited; + private int policyViolationsUnaudited; + private int policyViolationsSecurityTotal; + private int policyViolationsSecurityAudited; + private int policyViolationsSecurityUnaudited; + private int policyViolationsLicenseTotal; + private int policyViolationsLicenseAudited; + private int policyViolationsLicenseUnaudited; + private int policyViolationsOperationalTotal; + private int policyViolationsOperationalAudited; + private int policyViolationsOperationalUnaudited; @NotNull @Schema(type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED, description = "UNIX epoch timestamp in milliseconds") @@ -163,7 +118,7 @@ public void setLow(int low) { } public int getUnassigned() { - return unassigned != null ? unassigned : 0; + return unassigned; } public void setUnassigned(int unassigned) { @@ -203,7 +158,7 @@ public void setSuppressed(int suppressed) { } public int getFindingsTotal() { - return findingsTotal != null ? findingsTotal : 0; + return findingsTotal; } public void setFindingsTotal(int findingsTotal) { @@ -211,7 +166,7 @@ public void setFindingsTotal(int findingsTotal) { } public int getFindingsAudited() { - return findingsAudited != null ? findingsAudited : 0; + return findingsAudited; } public void setFindingsAudited(int findingsAudited) { @@ -219,14 +174,13 @@ public void setFindingsAudited(int findingsAudited) { } public int getFindingsUnaudited() { - return findingsUnaudited != null ? findingsUnaudited : 0; + return findingsUnaudited; } public void setFindingsUnaudited(int findingsUnaudited) { this.findingsUnaudited = findingsUnaudited; } - @ColumnName("RISKSCORE") public double getInheritedRiskScore() { return inheritedRiskScore; } @@ -236,7 +190,7 @@ public void setInheritedRiskScore(double inheritedRiskScore) { } public int getPolicyViolationsFail() { - return policyViolationsFail != null ? policyViolationsFail : 0; + return policyViolationsFail; } public void setPolicyViolationsFail(int policyViolationsFail) { @@ -244,7 +198,7 @@ public void setPolicyViolationsFail(int policyViolationsFail) { } public int getPolicyViolationsWarn() { - return policyViolationsWarn != null ? policyViolationsWarn : 0; + return policyViolationsWarn; } public void setPolicyViolationsWarn(int policyViolationsWarn) { @@ -252,7 +206,7 @@ public void setPolicyViolationsWarn(int policyViolationsWarn) { } public int getPolicyViolationsInfo() { - return policyViolationsInfo != null ? policyViolationsInfo : 0; + return policyViolationsInfo; } public void setPolicyViolationsInfo(int policyViolationsInfo) { @@ -260,7 +214,7 @@ public void setPolicyViolationsInfo(int policyViolationsInfo) { } public int getPolicyViolationsTotal() { - return policyViolationsTotal != null ? policyViolationsTotal : 0; + return policyViolationsTotal; } public void setPolicyViolationsTotal(int policyViolationsTotal) { @@ -268,7 +222,7 @@ public void setPolicyViolationsTotal(int policyViolationsTotal) { } public int getPolicyViolationsAudited() { - return policyViolationsAudited != null ? policyViolationsAudited : 0; + return policyViolationsAudited; } public void setPolicyViolationsAudited(int policyViolationsAudited) { @@ -276,7 +230,7 @@ public void setPolicyViolationsAudited(int policyViolationsAudited) { } public int getPolicyViolationsUnaudited() { - return policyViolationsUnaudited != null ? policyViolationsUnaudited : 0; + return policyViolationsUnaudited; } public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) { @@ -284,7 +238,7 @@ public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) { } public int getPolicyViolationsSecurityTotal() { - return policyViolationsSecurityTotal != null ? policyViolationsSecurityTotal : 0; + return policyViolationsSecurityTotal; } public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) { @@ -292,7 +246,7 @@ public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) } public int getPolicyViolationsSecurityAudited() { - return policyViolationsSecurityAudited != null ? policyViolationsSecurityAudited : 0; + return policyViolationsSecurityAudited; } public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudited) { @@ -300,7 +254,7 @@ public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudit } public int getPolicyViolationsSecurityUnaudited() { - return policyViolationsSecurityUnaudited != null ? policyViolationsSecurityUnaudited : 0; + return policyViolationsSecurityUnaudited; } public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUnaudited) { @@ -308,7 +262,7 @@ public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUna } public int getPolicyViolationsLicenseTotal() { - return policyViolationsLicenseTotal != null ? policyViolationsLicenseTotal : 0; + return policyViolationsLicenseTotal; } public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) { @@ -316,7 +270,7 @@ public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) { } public int getPolicyViolationsLicenseAudited() { - return policyViolationsLicenseAudited != null ? policyViolationsLicenseAudited : 0; + return policyViolationsLicenseAudited; } public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited) { @@ -324,7 +278,7 @@ public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited } public int getPolicyViolationsLicenseUnaudited() { - return policyViolationsLicenseUnaudited != null ? policyViolationsLicenseUnaudited : 0; + return policyViolationsLicenseUnaudited; } public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaudited) { @@ -332,7 +286,7 @@ public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaud } public int getPolicyViolationsOperationalTotal() { - return policyViolationsOperationalTotal != null ? policyViolationsOperationalTotal : 0; + return policyViolationsOperationalTotal; } public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalTotal) { @@ -340,7 +294,7 @@ public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalT } public int getPolicyViolationsOperationalAudited() { - return policyViolationsOperationalAudited != null ? policyViolationsOperationalAudited : 0; + return policyViolationsOperationalAudited; } public void setPolicyViolationsOperationalAudited(int policyViolationsOperationalAudited) { @@ -348,7 +302,7 @@ public void setPolicyViolationsOperationalAudited(int policyViolationsOperationa } public int getPolicyViolationsOperationalUnaudited() { - return policyViolationsOperationalUnaudited != null ? policyViolationsOperationalUnaudited : 0; + return policyViolationsOperationalUnaudited; } public void setPolicyViolationsOperationalUnaudited(int policyViolationsOperationalUnaudited) { @@ -371,37 +325,4 @@ public void setLastOccurrence(Date lastOccurrence) { this.lastOccurrence = lastOccurrence; } - @JsonIgnore - public boolean hasChanged(final ProjectMetrics comparedTo) { - return comparedTo == null - || comparedTo.getCritical() != this.critical - || comparedTo.getHigh() != this.high - || comparedTo.getMedium() != this.medium - || comparedTo.getLow() != this.low - || comparedTo.getUnassigned() != this.unassigned - || comparedTo.getVulnerabilities() != this.vulnerabilities - || comparedTo.getSuppressed() != this.suppressed - || comparedTo.getFindingsTotal() != this.findingsTotal - || comparedTo.getFindingsAudited() != this.findingsAudited - || comparedTo.getFindingsUnaudited() != this.findingsUnaudited - || comparedTo.getInheritedRiskScore() != this.inheritedRiskScore - || comparedTo.getPolicyViolationsFail() != this.policyViolationsFail - || comparedTo.getPolicyViolationsWarn() != this.policyViolationsWarn - || comparedTo.getPolicyViolationsInfo() != this.policyViolationsInfo - || comparedTo.getPolicyViolationsTotal() != this.policyViolationsTotal - || comparedTo.getPolicyViolationsAudited() != this.policyViolationsAudited - || comparedTo.getPolicyViolationsUnaudited() != this.policyViolationsUnaudited - || comparedTo.getPolicyViolationsSecurityTotal() != this.policyViolationsSecurityTotal - || comparedTo.getPolicyViolationsSecurityAudited() != this.policyViolationsSecurityAudited - || comparedTo.getPolicyViolationsSecurityUnaudited() != this.policyViolationsSecurityUnaudited - || comparedTo.getPolicyViolationsLicenseTotal() != this.policyViolationsLicenseTotal - || comparedTo.getPolicyViolationsLicenseAudited() != this.policyViolationsLicenseAudited - || comparedTo.getPolicyViolationsLicenseUnaudited() != this.policyViolationsLicenseUnaudited - || comparedTo.getPolicyViolationsOperationalTotal() != this.policyViolationsOperationalTotal - || comparedTo.getPolicyViolationsOperationalAudited() != this.policyViolationsOperationalAudited - || comparedTo.getPolicyViolationsOperationalUnaudited() != this.policyViolationsOperationalUnaudited - || comparedTo.getComponents() != this.components - || comparedTo.getVulnerableComponents() != this.vulnerableComponents; - } - } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java new file mode 100644 index 0000000000..83c10e87c8 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java @@ -0,0 +1,58 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence; + +import alpine.server.util.DbUtil; +import org.dependencytrack.init.InitTask; +import org.dependencytrack.init.InitTaskContext; +import org.dependencytrack.support.liquibase.MigrationExecutor; + +import java.sql.Connection; + +/** + * @since 5.6.0 + */ +public final class DatabaseMigrationInitTask implements InitTask { + + @Override + public int priority() { + return PRIORITY_HIGHEST; + } + + @Override + public String name() { + return "database.migration"; + } + + @Override + public void execute(InitTaskContext ctx) throws Exception { + try (final Connection connection = ctx.dataSource().getConnection()) { + // Ensure that DbUtil#isPostgreSQL will work as expected. + // Some legacy code ported over from v4 still uses this. + // + // NB: This was previously done in alpine.server.upgrade.UpgradeExecutor. + // + // TODO: Remove once DbUtil#isPostgreSQL is no longer used. + DbUtil.initPlatformName(connection); + } + + new MigrationExecutor(ctx.dataSource(), "migration/changelog-main.xml").executeMigration(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java new file mode 100644 index 0000000000..f391fafdfb --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java @@ -0,0 +1,54 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence; + +import org.dependencytrack.init.InitTask; +import org.dependencytrack.init.InitTaskContext; +import org.dependencytrack.persistence.jdbi.JdbiFactory; +import org.dependencytrack.persistence.jdbi.MetricsDao; + +import java.time.LocalDate; + +/** + * @since 5.6.0 + */ +public final class DatabasePartitionMaintenanceInitTask implements InitTask { + + @Override + public int priority() { + return PRIORITY_HIGHEST - 10; + } + + @Override + public String name() { + return "database.partition.maintenance"; + } + + @Override + public void execute(final InitTaskContext ctx) throws Exception { + final var jdbi = JdbiFactory.createLocalJdbi(ctx.dataSource()); + + jdbi.useTransaction(handle -> { + var metricsDao = handle.attach(MetricsDao.class); + metricsDao.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString()); + metricsDao.createMetricsPartitionsForDate(LocalDate.now().plusDays(1).toString(), LocalDate.now().plusDays(2).toString()); + }); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java new file mode 100644 index 0000000000..a9dbe19fc4 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java @@ -0,0 +1,502 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence; + +import alpine.server.auth.PasswordService; +import org.apache.commons.lang3.SerializationUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.init.InitTask; +import org.dependencytrack.init.InitTaskContext; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.DefaultRepository; +import org.dependencytrack.model.License; +import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; +import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; +import org.dependencytrack.persistence.jdbi.ConfigPropertyDao; +import org.dependencytrack.persistence.jdbi.JdbiFactory; +import org.jdbi.v3.core.Handle; +import org.jdbi.v3.core.statement.PreparedBatch; +import org.jdbi.v3.core.statement.Update; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.dependencytrack.model.ConfigPropertyConstants.INTERNAL_DEFAULT_OBJECTS_VERSION; +import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR; +import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED; + +/** + * @since 5.6.0 + */ +public final class DatabaseSeedingInitTask implements InitTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseSeedingInitTask.class); + + private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of( + "Administrators", Stream.of(Permissions.values()) + .map(Permissions::name) + .toList(), + "Portfolio Managers", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.PORTFOLIO_MANAGEMENT, + Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE), + "Automation", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.BOM_UPLOAD), + "Badge Viewers", List.of( + Permissions.Constants.VIEW_BADGES)); + + private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of( + "Project Admin", List.of( + Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE, + Permissions.Constants.VULNERABILITY_ANALYSIS, + Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ, + Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT, + Permissions.Constants.POLICY_MANAGEMENT_CREATE, + Permissions.Constants.POLICY_MANAGEMENT_READ, + Permissions.Constants.POLICY_MANAGEMENT_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT_DELETE), + "Project Auditor", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VIEW_POLICY_VIOLATION, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ), + "Project Editor", List.of( + Permissions.Constants.BOM_UPLOAD, + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ, + Permissions.Constants.PROJECT_CREATION_UPLOAD), + "Project Viewer", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VIEW_BADGES)); + + @Override + public int priority() { + return PRIORITY_HIGHEST - 10; + } + + @Override + public String name() { + return "database.seeding"; + } + + @Override + public void execute(final InitTaskContext ctx) throws Exception { + final var jdbi = JdbiFactory.createLocalJdbi(ctx.dataSource()); + + jdbi.useTransaction(handle -> { + final var configPropertyDao = handle.attach(ConfigPropertyDao.class); + + final String defaultObjectsVersion = configPropertyDao + .getOptionalValue(INTERNAL_DEFAULT_OBJECTS_VERSION) + .orElse(null); + if (ctx.config().getApplicationBuildUuid().equals(defaultObjectsVersion)) { + LOGGER.info( + "Default objects already populated for build {} (timestamp: {}); Skipping", + ctx.config().getApplicationBuildUuid(), + ctx.config().getApplicationBuildTimestamp()); + return; + } + + seedDefaultConfigProperties(handle); + seedDefaultPermissions(handle); + seedDefaultLicenses(handle); + seedDefaultNotificationPublishers(handle); + seedDefaultRepositories(handle); + + final boolean isFirstExecution = defaultObjectsVersion == null; + if (isFirstExecution) { + seedDefaultTeams(handle); + seedDefaultRoles(handle); + seedDefaultUsers(handle); + seedDefaultLicenseGroups(handle); + } + + configPropertyDao.setValue( + INTERNAL_DEFAULT_OBJECTS_VERSION, + ctx.config().getApplicationBuildUuid()); + }); + } + + public static void seedDefaultConfigProperties(final Handle jdbiHandle) { + final PreparedBatch preparedBatch = jdbiHandle.prepareBatch(""" + INSERT INTO "CONFIGPROPERTY" ("GROUPNAME", "PROPERTYNAME", "PROPERTYTYPE", "PROPERTYVALUE", "DESCRIPTION") + VALUES (:groupName, :propertyName, :propertyType, :defaultPropertyValue, :description) + ON CONFLICT ("GROUPNAME", "PROPERTYNAME") DO NOTHING + """); + + for (final ConfigPropertyConstants configProperty : ConfigPropertyConstants.values()) { + preparedBatch.bindBean(configProperty); + preparedBatch.add(); + } + + final int configPropertiesCreated = Arrays.stream(preparedBatch.execute()).sum(); + LOGGER.debug("Created {} config properties", configPropertiesCreated); + } + + public static void seedDefaultPermissions(final Handle jdbiHandle) { + final PreparedBatch preparedBatch = jdbiHandle.prepareBatch(""" + INSERT INTO "PERMISSION" ("NAME", "DESCRIPTION") + VALUES (:name, :description) + ON CONFLICT ("NAME") DO NOTHING + """); + + for (final Permissions permission : Permissions.values()) { + preparedBatch.bind("name", permission.name()); + preparedBatch.bind("description", permission.getDescription()); + preparedBatch.add(); + } + + final int permissionsCreated = Arrays.stream(preparedBatch.execute()).sum(); + LOGGER.debug("Created {} permissions", permissionsCreated); + } + + public static void seedDefaultTeams(final Handle jdbiHandle) { + final Update update = jdbiHandle.createUpdate(""" + WITH cte_team_permission AS ( + SELECT * + FROM UNNEST(:teamNames, :permissionNames) AS t(team_name, permission_name) + ), + cte_created_team AS ( + INSERT INTO "TEAM" ("NAME", "UUID") + SELECT DISTINCT ON (team_name) + team_name + , GEN_RANDOM_UUID() + FROM cte_team_permission + RETURNING "ID" AS id + , "NAME" AS name + ) + INSERT INTO "TEAMS_PERMISSIONS" ("TEAM_ID", "PERMISSION_ID") + SELECT cte_created_team.id + , (SELECT "ID" FROM "PERMISSION" WHERE "NAME" = cte_team_permission.permission_name) + FROM cte_team_permission + INNER JOIN cte_created_team + ON cte_created_team.name = cte_team_permission.team_name + """); + + final var teamNames = new ArrayList(); + final var permissionNames = new ArrayList(); + + for (final Map.Entry> entry : DEFAULT_TEAM_PERMISSIONS.entrySet()) { + for (final String permissionName : entry.getValue()) { + teamNames.add(entry.getKey()); + permissionNames.add(permissionName); + } + } + + update + .bindArray("teamNames", String.class, teamNames) + .bindArray("permissionNames", String.class, permissionNames) + .execute(); + } + + public static void seedDefaultRoles(final Handle jdbiHandle) { + final Update update = jdbiHandle.createUpdate(""" + WITH cte_role_permission AS ( + SELECT * + FROM UNNEST(:roleNames, :permissionNames) AS t(role_name, permission_name) + ), + cte_created_role AS ( + INSERT INTO "ROLE" ("NAME", "UUID") + SELECT DISTINCT ON (role_name) + role_name + , GEN_RANDOM_UUID() + FROM cte_role_permission + RETURNING "ID" AS id + , "NAME" AS name + ) + INSERT INTO "ROLES_PERMISSIONS" ("ROLE_ID", "PERMISSION_ID") + SELECT cte_created_role.id + , (SELECT "ID" FROM "PERMISSION" WHERE "NAME" = cte_role_permission.permission_name) + FROM cte_role_permission + INNER JOIN cte_created_role + ON cte_created_role.name = cte_role_permission.role_name + """); + + final var roleNames = new ArrayList(); + final var permissionNames = new ArrayList(); + + for (final Map.Entry> entry : DEFAULT_ROLE_PERMISSIONS.entrySet()) { + for (final String permissionName : entry.getValue()) { + roleNames.add(entry.getKey()); + permissionNames.add(permissionName); + } + } + + update + .bindArray("roleNames", String.class, roleNames) + .bindArray("permissionNames", String.class, permissionNames) + .execute(); + } + + public static void seedDefaultUsers(final Handle jdbiHandle) { + final long adminUserId = jdbiHandle.createUpdate(""" + INSERT INTO "USER" ( + "TYPE", "USERNAME", "EMAIL", "PASSWORD", "LAST_PASSWORD_CHANGE" + , "FORCE_PASSWORD_CHANGE", "NON_EXPIRY_PASSWORD", "SUSPENDED") + VALUES ('MANAGED', 'admin', 'admin@localhost', :password, NOW(), TRUE, TRUE, FALSE) + RETURNING "ID" + """) + .bind("password", new String(PasswordService.createHash("admin".toCharArray()))) + .executeAndReturnGeneratedKeys() + .mapTo(Long.class) + .one(); + + jdbiHandle.createUpdate(""" + INSERT INTO "USERS_TEAMS" ("USER_ID", "TEAM_ID") + SELECT :adminUserId, (SELECT "ID" FROM "TEAM" WHERE "NAME" = 'Administrators') + """) + .bind("adminUserId", adminUserId) + .execute(); + + jdbiHandle.createUpdate(""" + INSERT INTO "USERS_PERMISSIONS" ("USER_ID", "PERMISSION_ID") + SELECT :adminUserId, "PERMISSION"."ID" FROM "PERMISSION" + """) + .bind("adminUserId", adminUserId) + .execute(); + } + + public static void seedDefaultLicenses(final Handle jdbiHandle) { + final List licenses; + try { + licenses = new SpdxLicenseDetailParser().getLicenseDefinitions(); + } catch (IOException e) { + throw new IllegalStateException("Failed to load license details", e); + } + + // We have hundreds of licenses, the majority of which is *very* unlikely to change between executions + // of this init task. In the future, we should store the version of the SPDX license list, + // and then only sync licenses when that version has changed. + final PreparedBatch preparedBatch = jdbiHandle.prepareBatch(""" + INSERT INTO "LICENSE" ( + "LICENSEID", "NAME", "HEADER", "TEXT", "TEMPLATE", "ISDEPRECATED" + , "FSFLIBRE", "ISOSIAPPROVED", "COMMENT", "SEEALSO", "UUID" + ) + VALUES ( + :licenseId, :name, :header, :text, :template, :deprecatedLicenseId + , :fsfLibre, :osiApproved, :comment, :seeAlsoSerialized, GEN_RANDOM_UUID() + ) + ON CONFLICT ("LICENSEID") DO UPDATE + SET "NAME" = EXCLUDED."NAME" + , "HEADER" = EXCLUDED."HEADER" + , "TEXT" = EXCLUDED."TEXT" + , "TEMPLATE" = EXCLUDED."TEMPLATE" + , "ISDEPRECATED" = EXCLUDED."ISDEPRECATED" + , "FSFLIBRE" = EXCLUDED."FSFLIBRE" + , "ISOSIAPPROVED" = EXCLUDED."ISOSIAPPROVED" + , "COMMENT" = EXCLUDED."COMMENT" + , "SEEALSO" = EXCLUDED."SEEALSO" + -- Only update when at least one relevant field has changed. + WHERE "LICENSE"."NAME" IS DISTINCT FROM EXCLUDED."NAME" + OR "LICENSE"."HEADER" IS DISTINCT FROM EXCLUDED."HEADER" + OR "LICENSE"."TEXT" IS DISTINCT FROM EXCLUDED."TEXT" + OR "LICENSE"."TEMPLATE" IS DISTINCT FROM EXCLUDED."TEMPLATE" + OR "LICENSE"."ISDEPRECATED" IS DISTINCT FROM EXCLUDED."ISDEPRECATED" + OR "LICENSE"."FSFLIBRE" IS DISTINCT FROM EXCLUDED."FSFLIBRE" + OR "LICENSE"."ISOSIAPPROVED" IS DISTINCT FROM EXCLUDED."ISOSIAPPROVED" + OR "LICENSE"."COMMENT" IS DISTINCT FROM EXCLUDED."COMMENT" + OR "LICENSE"."SEEALSO" IS DISTINCT FROM EXCLUDED."SEEALSO" + """); + + for (final License license : licenses) { + preparedBatch.bindBean(license); + preparedBatch.bind( + "seeAlsoSerialized", + license.getSeeAlso() != null + ? SerializationUtils.serialize(license.getSeeAlso()) + : null); + preparedBatch.add(); + } + + int licensesCreatedOrUpdated = Arrays.stream(preparedBatch.execute()).sum(); + LOGGER.debug("Created or updated {} licenses", licensesCreatedOrUpdated); + } + + public static void seedDefaultLicenseGroups(final Handle jdbiHandle) { + final Update update = jdbiHandle.createUpdate(""" + WITH cte_group_license AS ( + SELECT * + FROM UNNEST(:groupNames, :groupRiskWeights, :licenseIds) AS t(group_name, group_risk_weight, license_id) + ), + cte_created_group AS ( + INSERT INTO "LICENSEGROUP" ("NAME", "RISKWEIGHT", "UUID") + SELECT DISTINCT ON (group_name) + group_name + , group_risk_weight + , GEN_RANDOM_UUID() + FROM cte_group_license + RETURNING "ID" AS id, "NAME" AS name + ) + INSERT INTO "LICENSEGROUP_LICENSE" ("LICENSEGROUP_ID", "LICENSE_ID") + SELECT cte_created_group.id + , (SELECT "ID" FROM "LICENSE" WHERE "LICENSEID" = cte_group_license.license_id) + FROM cte_group_license + INNER JOIN cte_created_group + ON cte_created_group.name = cte_group_license.group_name + """); + + final JsonArray groupDefsJson; + try (final InputStream inputStream = DatabaseSeedingInitTask.class.getResourceAsStream("/default-objects/licenseGroups.json"); + final JsonReader jsonReader = Json.createReader(inputStream)) { + groupDefsJson = jsonReader.readArray(); + } catch (IOException e) { + throw new IllegalStateException("Failed to parse license group definition", e); + } + + final var groupNames = new ArrayList(); + final var groupRiskWeights = new ArrayList(); + final var licenseIds = new ArrayList(); + + for (int i = 0; i < groupDefsJson.size(); i++) { + final JsonObject groupDefJson = groupDefsJson.getJsonObject(i); + final String groupName = groupDefJson.getString("name"); + final int riskWeight = groupDefJson.getInt("riskWeight"); + + final JsonArray licenseIdsJson = groupDefJson.getJsonArray("licenses"); + for (int j = 0; j < licenseIdsJson.size(); j++) { + groupNames.add(groupName); + groupRiskWeights.add(riskWeight); + licenseIds.add(licenseIdsJson.getString(j)); + } + } + + update + .bindArray("groupNames", String.class, groupNames) + .bindArray("groupRiskWeights", Integer.class, groupRiskWeights) + .bindArray("licenseIds", String.class, licenseIds) + .execute(); + } + + public static void seedDefaultNotificationPublishers(final Handle jdbiHandle) { + final PreparedBatch preparedBatch = jdbiHandle.prepareBatch(""" + INSERT INTO "NOTIFICATIONPUBLISHER" ( + "NAME", "PUBLISHER_CLASS", "DEFAULT_PUBLISHER", "DESCRIPTION" + , "TEMPLATE", "TEMPLATE_MIME_TYPE", "UUID") + VALUES ( + :publisherName, :publisherClass, TRUE, :publisherDescription + , :templateContent, :templateMimeType, GEN_RANDOM_UUID()) + ON CONFLICT ("NAME") DO UPDATE + SET "PUBLISHER_CLASS" = EXCLUDED."PUBLISHER_CLASS" + , "DESCRIPTION" = EXCLUDED."DESCRIPTION" + , "TEMPLATE" = EXCLUDED."TEMPLATE" + , "TEMPLATE_MIME_TYPE" = EXCLUDED."TEMPLATE_MIME_TYPE" + -- Only update when at least one relevant field has changed. + WHERE "NOTIFICATIONPUBLISHER"."PUBLISHER_CLASS" IS DISTINCT FROM EXCLUDED."PUBLISHER_CLASS" + OR "NOTIFICATIONPUBLISHER"."DESCRIPTION" IS DISTINCT FROM EXCLUDED."DESCRIPTION" + OR "NOTIFICATIONPUBLISHER"."TEMPLATE" IS DISTINCT FROM EXCLUDED."TEMPLATE" + OR "NOTIFICATIONPUBLISHER"."TEMPLATE_MIME_TYPE" IS DISTINCT FROM EXCLUDED."TEMPLATE_MIME_TYPE" + """); + + final var configPropertyDao = jdbiHandle.attach(ConfigPropertyDao.class); + final var templateOverrideEnabled = configPropertyDao.getOptionalValue( + NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED, Boolean.class).orElse(false); + final var templateOverrideBaseDir = configPropertyDao.getOptionalValue( + NOTIFICATION_TEMPLATE_BASE_DIR).orElse(null); + if (templateOverrideEnabled && templateOverrideBaseDir == null) { + throw new IllegalStateException("%s is enabled but %s is not configured".formatted( + NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName(), + NOTIFICATION_TEMPLATE_BASE_DIR.getPropertyName())); + } + + for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) { + final URL templateFileUrl = DatabaseSeedingInitTask.class.getResource(publisher.getPublisherTemplateFile()); + if (templateFileUrl == null) { + throw new IllegalStateException("Template file %s of default publisher %s does not exist".formatted( + publisher.getPublisherTemplateFile(), publisher.getPublisherName())); + } + + Path templateFilePath; + try { + templateFilePath = Paths.get(templateFileUrl.toURI()); + } catch (URISyntaxException e) { + throw new IllegalStateException("Failed to construct path for template file: " + templateFileUrl, e); + } + + if (templateOverrideEnabled) { + final Path customTemplateFilePath = Paths.get(templateOverrideBaseDir, publisher.getPublisherTemplateFile()); + if (Files.exists(customTemplateFilePath)) { + templateFilePath = customTemplateFilePath; + } + } + + final String templateContent; + try { + templateContent = Files.readString(templateFilePath); + } catch (IOException e) { + throw new IllegalStateException("Failed to read template file: " + templateFilePath, e); + } + + preparedBatch.bindBean(publisher); + preparedBatch.bind("templateContent", templateContent); + preparedBatch.add(); + } + + final int publishersCreatedOrUpdated = Arrays.stream(preparedBatch.execute()).sum(); + LOGGER.debug("Created or updated {} publishers", publishersCreatedOrUpdated); + } + + public static void seedDefaultRepositories(final Handle jdbiHandle) { + final PreparedBatch preparedBatch = jdbiHandle.prepareBatch(""" + INSERT INTO "REPOSITORY"( + "TYPE", "IDENTIFIER", "URL", "INTERNAL", "RESOLUTION_ORDER" + , "ENABLED", "AUTHENTICATIONREQUIRED", "UUID") + VALUES ( + :type, :identifier, :url, FALSE, :resolutionOrder + , TRUE, FALSE, GEN_RANDOM_UUID()) + ON CONFLICT ("TYPE", "IDENTIFIER") DO NOTHING + """); + + for (final DefaultRepository repository : DefaultRepository.values()) { + preparedBatch.bindBean(repository); + preparedBatch.add(); + } + + final int reposCreated = Arrays.stream(preparedBatch.execute()).sum(); + LOGGER.debug("Created {} repositories", reposCreated); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java deleted file mode 100644 index fc4ddcc31c..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ /dev/null @@ -1,436 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.persistence; - -import alpine.Config; -import alpine.common.logging.Logger; -import alpine.model.ConfigProperty; -import alpine.model.ManagedUser; -import alpine.model.Permission; -import alpine.server.auth.PasswordService; - -import jakarta.servlet.ServletContextEvent; -import jakarta.servlet.ServletContextListener; - -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.common.ConfigKey; -import org.dependencytrack.model.ConfigPropertyConstants; -import org.dependencytrack.model.License; -import org.dependencytrack.model.RepositoryType; -import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; -import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; -import org.dependencytrack.persistence.jdbi.MetricsDao; -import org.dependencytrack.util.NotificationUtil; -import org.dependencytrack.util.WaitingLockConfiguration; - -import java.io.IOException; - -import java.time.Duration; -import java.time.Instant; -import java.util.Collections; -import java.time.LocalDate; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; - -import static net.javacrumbs.shedlock.core.LockAssert.assertLocked; -import static org.dependencytrack.model.ConfigPropertyConstants.INTERNAL_DEFAULT_OBJECTS_VERSION; -import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; -import static org.dependencytrack.util.LockProvider.executeWithLockWaiting; - -/** - * Creates default objects on an empty database. - * - * @author Steve Springett - * @since 3.0.0 - */ -public class DefaultObjectGenerator implements ServletContextListener { - - private static final Logger LOGGER = Logger.getLogger(DefaultObjectGenerator.class); - - private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of( - "Administrators", Stream.of(Permissions.values()) - .map(Permissions::name) - .toList(), - "Portfolio Managers", List.of( - Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.PORTFOLIO_MANAGEMENT, - Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, - Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE), - "Automation", List.of( - Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.BOM_UPLOAD), - "Badge Viewers", List.of( - Permissions.Constants.VIEW_BADGES)); - - private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of( - "Project Admin", List.of( - Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, - Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE, - Permissions.Constants.VULNERABILITY_ANALYSIS, - Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE, - Permissions.Constants.VULNERABILITY_ANALYSIS_READ, - Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE, - Permissions.Constants.POLICY_MANAGEMENT, - Permissions.Constants.POLICY_MANAGEMENT_CREATE, - Permissions.Constants.POLICY_MANAGEMENT_READ, - Permissions.Constants.POLICY_MANAGEMENT_UPDATE, - Permissions.Constants.POLICY_MANAGEMENT_DELETE), - "Project Auditor", List.of( - Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.VIEW_VULNERABILITY, - Permissions.Constants.VIEW_POLICY_VIOLATION, - Permissions.Constants.VULNERABILITY_ANALYSIS_READ), - "Project Editor", List.of( - Permissions.Constants.BOM_UPLOAD, - Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, - Permissions.Constants.VIEW_VULNERABILITY, - Permissions.Constants.VULNERABILITY_ANALYSIS_READ, - Permissions.Constants.PROJECT_CREATION_UPLOAD), - "Project Viewer", List.of( - Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.VIEW_VULNERABILITY, - Permissions.Constants.VIEW_BADGES)); - - private final Map persistentPermissionByName = new HashMap<>(); - - /** - * {@inheritDoc} - */ - @Override - public void contextInitialized(final ServletContextEvent event) { - if (!Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) { - LOGGER.info("Not populating database with default objects because %s is disabled" - .formatted(ConfigKey.INIT_TASKS_ENABLED.getPropertyName())); - return; - } - - // Ensure that this task is only executed by a single instance at once. - // Wait for lock acquisition rather than simply skipping execution, - // since application logic may depend on default objects being present. - final var lockConfig = new WaitingLockConfiguration( - /* createdAt */ Instant.now(), - /* name */ getClass().getName(), - /* lockAtMostFor */ Duration.ofMinutes(5), - /* lockAtLeastFor */ Duration.ZERO, - /* pollInterval */ Duration.ofSeconds(1), - /* waitTimeout */ Duration.ofMinutes(5)); - - try { - executeWithLockWaiting(lockConfig, this::executeLocked); - } catch (Throwable t) { - if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) { - // Make absolutely sure that we exit with non-zero code so - // the container orchestrator knows to restart the container. - LOGGER.error("Failed to populate database with default objects", t); - System.exit(1); - } - - throw new RuntimeException("Failed to populate database with default objects", t); - } - - if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) { - LOGGER.info("Exiting because %s is enabled".formatted(ConfigKey.INIT_AND_EXIT.getPropertyName())); - System.exit(0); - } - } - - private void executeLocked() { - assertLocked(); - - if (!shouldExecute()) { - LOGGER.info("Default objects already populated for build %s (timestamp: %s); Skipping".formatted( - Config.getInstance().getApplicationBuildUuid(), - Config.getInstance().getApplicationBuildTimestamp())); - return; - } - - // TODO: Make population transactional with recordDefaultObjectsVersion(). - - LOGGER.info("Initializing default object generator"); - try (final var qm = new QueryManager()) { - loadDefaultPermissions(qm); - loadDefaultPersonas(qm); - loadDefaultLicenses(qm); - loadDefaultLicenseGroups(qm); - loadDefaultRepositories(qm); - loadDefaultRoles(qm); - loadDefaultConfigProperties(qm); - loadDefaultNotificationPublishers(qm); - recordDefaultObjectsVersion(qm); - } - - LOGGER.info("Ensuring the metrics partitions for today and tomorrow exist."); - ensureMetricsPartitions(); - } - - /** - * {@inheritDoc} - */ - @Override - public void contextDestroyed(final ServletContextEvent event) { - /* Intentionally blank to satisfy interface */ - } - - private boolean shouldExecute() { - try (final var qm = new QueryManager()) { - final ConfigProperty configProperty = qm.getConfigProperty( - INTERNAL_DEFAULT_OBJECTS_VERSION.getGroupName(), - INTERNAL_DEFAULT_OBJECTS_VERSION.getPropertyName()); - - return configProperty == null - || configProperty.getPropertyValue() == null - || !Config.getInstance().getApplicationBuildUuid().equals(configProperty.getPropertyValue()); - } - } - - private void recordDefaultObjectsVersion(final QueryManager qm) { - qm.runInTransaction(() -> { - final ConfigProperty configProperty = qm.getConfigProperty( - INTERNAL_DEFAULT_OBJECTS_VERSION.getGroupName(), - INTERNAL_DEFAULT_OBJECTS_VERSION.getPropertyName()); - - configProperty.setPropertyValue(Config.getInstance().getApplicationBuildUuid()); - }); - } - - public static void loadDefaultLicenses() { - try (final var qm = new QueryManager()) { - loadDefaultLicenses(qm); - } - } - - /** - * Loads the default licenses into the database if no license data exists. - */ - private static void loadDefaultLicenses(final QueryManager qm) { - LOGGER.info("Synchronizing SPDX license definitions to datastore"); - - final SpdxLicenseDetailParser parser = new SpdxLicenseDetailParser(); - try { - final List licenses = parser.getLicenseDefinitions(); - for (final License license : licenses) { - LOGGER.debug("Synchronizing: " + license.getName()); - qm.synchronizeLicense(license, false); - } - } catch (IOException e) { - LOGGER.error("An error occurred during the parsing SPDX license definitions"); - LOGGER.error(e.getMessage()); - } - } - - /** - * Loads the default license groups into the database if no license groups exists. - */ - private void loadDefaultLicenseGroups(final QueryManager qm) { - final DefaultLicenseGroupImporter importer = new DefaultLicenseGroupImporter(qm); - if (!importer.shouldImport()) { - return; - } - LOGGER.info("Adding default license group definitions to datastore"); - try { - importer.loadDefaults(); - } catch (IOException e) { - LOGGER.error("An error occurred loading default license group definitions"); - LOGGER.error(e.getMessage()); - } - } - - public void loadDefaultPermissions() { - try (final var qm = new QueryManager()) { - loadDefaultPermissions(qm); - } - } - - /** - * Loads the default permissions - */ - private void loadDefaultPermissions(final QueryManager qm) { - LOGGER.info("Synchronizing permissions to datastore"); - - List existing = Objects.requireNonNullElse(qm.getPermissions(), Collections.emptyList()) - .stream() - .map(Permission::getName) - .toList(); - - for (final Permissions value : Permissions.values()) - if (!existing.contains(value.name())) { - LOGGER.debug("Creating permission: " + value.name()); - persistentPermissionByName.put(value.name(), qm.createPermission(value.name(), value.getDescription())); - } - } - - @SuppressWarnings("unused") - void loadDefaultPersonas() { - try (final var qm = new QueryManager()) { - loadDefaultPersonas(qm); - } - } - - /** - * Loads the default users and teams - */ - private void loadDefaultPersonas(final QueryManager qm) { - if (!qm.getManagedUsers().isEmpty() && qm.getTeams().getTotal() != 0) - return; - - LOGGER.info("Adding default users and teams to datastore"); - - LOGGER.debug("Creating user: admin"); - ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", - new String(PasswordService.createHash("admin".toCharArray())), true, true, false); - - for (var name : DEFAULT_TEAM_PERMISSIONS.keySet()) { - LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name); - - LOGGER.debug("Assigning default permissions for team: " + name); - team.setPermissions(getPermissionsByName(DEFAULT_TEAM_PERMISSIONS.get(name))); - - qm.persist(team); - } - - LOGGER.debug("Adding admin user to System Administrators"); - qm.addUserToTeam(admin, qm.getTeam("Administrators")); - - admin = qm.getObjectById(ManagedUser.class, admin.getId()); - admin.setPermissions(qm.getPermissions()); - qm.persist(admin); - } - - /** - * Perform a lookup of {@link Permission}s for specified name(s). - * - * @param names permission names - * @return list of {@link Permission}s - */ - private List getPermissionsByName(List names) { - return names.stream().map(persistentPermissionByName::get).filter(Objects::nonNull).toList(); - } - - public void loadDefaultRoles() { - try (final var qm = new QueryManager()) { - loadDefaultRoles(qm); - } - } - - /** - * Loads the default Roles - */ - private void loadDefaultRoles(final QueryManager qm) { - if (!qm.getRoles().isEmpty()) - return; - - LOGGER.info("Adding default roles to datastore"); - - for (var name : DEFAULT_ROLE_PERMISSIONS.keySet()) { - LOGGER.debug("Creating role: " + name); - qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name))); - } - } - - public void loadDefaultRepositories() { - try (final var qm = new QueryManager()) { - loadDefaultRepositories(qm); - } - } - - /** - * Loads the default repositories - */ - private void loadDefaultRepositories(final QueryManager qm) { - LOGGER.info("Synchronizing default repositories to datastore"); - // @formatter:off - qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null); - qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false,null, null); - qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null); - qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false, false, null, null); - qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false, false, null, null); - qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false, false, null, null); - qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell", "https://hackage.haskell.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.NIXPKGS, "nixos.org", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null); - // @formatter:on - } - - @SuppressWarnings("unused") - void loadDefaultConfigProperties() { - try (final var qm = new QueryManager()) { - loadDefaultConfigProperties(qm); - } - } - - /** - * Loads the default ConfigProperty objects - */ - private void loadDefaultConfigProperties(final QueryManager qm) { - LOGGER.info("Synchronizing config properties to datastore"); - for (final ConfigPropertyConstants cpc : ConfigPropertyConstants.values()) { - LOGGER.debug("Creating config property: " + cpc.getGroupName() + " / " + cpc.getPropertyName()); - if (qm.getConfigProperty(cpc.getGroupName(), cpc.getPropertyName()) == null) { - qm.createConfigProperty(cpc.getGroupName(), cpc.getPropertyName(), cpc.getDefaultPropertyValue(), - cpc.getPropertyType(), cpc.getDescription()); - } - } - } - - public void loadDefaultNotificationPublishers() { - try (final var qm = new QueryManager()) { - loadDefaultNotificationPublishers(qm); - } - } - - /** - * Loads the default notification publishers - */ - private void loadDefaultNotificationPublishers(final QueryManager qm) { - LOGGER.info("Synchronizing notification publishers to datastore"); - try { - NotificationUtil.loadDefaultNotificationPublishers(qm); - } catch (IOException e) { - LOGGER.error("An error occurred while synchronizing a default notification publisher", e); - } - } - - /** - * Create metrics partitions for today and tomorrow if they don't exist. - */ - void ensureMetricsPartitions() { - useJdbiHandle(handle -> { - var metricsHandle = handle.attach(MetricsDao.class); - metricsHandle.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString()); - metricsHandle.createMetricsPartitionsForDate(LocalDate.now().plusDays(1).toString(), LocalDate.now().plusDays(2).toString()); - }); - } -} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java b/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java deleted file mode 100644 index 05347dcb2c..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.persistence; - -import alpine.Config; -import alpine.common.logging.Logger; -import alpine.server.util.DbUtil; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.dependencytrack.common.ConfigKey; -import org.dependencytrack.support.liquibase.MigrationExecutor; - -import jakarta.servlet.ServletContextEvent; -import jakarta.servlet.ServletContextListener; -import java.sql.Connection; -import java.util.Optional; - -public class MigrationInitializer implements ServletContextListener { - - private static final Logger LOGGER = Logger.getLogger(MigrationInitializer.class); - - private final Config config; - - @SuppressWarnings("unused") - public MigrationInitializer() { - this(Config.getInstance()); - } - - MigrationInitializer(final Config config) { - this.config = config; - } - - @Override - public void contextInitialized(final ServletContextEvent event) { - if (!config.getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) { - LOGGER.debug("Not running migrations because %s is disabled" - .formatted(ConfigKey.INIT_TASKS_ENABLED.getPropertyName())); - return; - } - if (!config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS)) { - LOGGER.debug("Not running migrations because %s is disabled" - .formatted(ConfigKey.DATABASE_RUN_MIGRATIONS.getPropertyName())); - return; - } - - LOGGER.info("Running migrations"); - try (final HikariDataSource dataSource = createDataSource()) { - try (final Connection connection = dataSource.getConnection()) { - // Ensure that DbUtil#isPostgreSQL will work as expected. - // Some legacy code ported over from v4 still uses this. - // - // NB: This was previously done in alpine.server.upgrade.UpgradeExecutor. - // - // TODO: Remove once DbUtil#isPostgreSQL is no longer used. - DbUtil.initPlatformName(connection); - } - - new MigrationExecutor(dataSource, "migration/changelog-main.xml").executeMigration(); - } catch (Exception e) { - if (config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS_ONLY) - || config.getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) { - // Make absolutely sure that we exit with non-zero code so - // the container orchestrator knows to restart the container. - LOGGER.error("Failed to execute migrations", e); - System.exit(1); - } - - throw new RuntimeException("Failed to execute migrations", e); - } - - if (config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS_ONLY)) { - LOGGER.info("Exiting because %s is enabled".formatted(ConfigKey.DATABASE_RUN_MIGRATIONS.getPropertyName())); - System.exit(0); - } - } - - private HikariDataSource createDataSource() { - final String jdbcUrl = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_URL)) - .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_URL)); - final String username = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_USERNAME)) - .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_USERNAME)); - final String password = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_PASSWORD)) - .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_PASSWORD)); - - final var hikariCfg = new HikariConfig(); - hikariCfg.setJdbcUrl(jdbcUrl); - hikariCfg.setDriverClassName(config.getProperty(Config.AlpineKey.DATABASE_DRIVER)); - hikariCfg.setUsername(username); - hikariCfg.setPassword(password); - hikariCfg.setMaximumPoolSize(1); - hikariCfg.setMinimumIdle(1); - - return new HikariDataSource(hikariCfg); - } -} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java b/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java deleted file mode 100644 index a52727c63c..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.persistence.defaults; - -import alpine.common.logging.Logger; -import org.dependencytrack.model.License; -import org.dependencytrack.model.LicenseGroup; -import org.dependencytrack.persistence.QueryManager; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.List; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Imports default LicenseGroup objects into the datastore. - * - * @author Steve Springett - * @since 4.0.0 - */ -public class DefaultLicenseGroupImporter implements IDefaultObjectImporter { - - private static final Logger LOGGER = Logger.getLogger(DefaultLicenseGroupImporter.class); - - private QueryManager qm; - - public DefaultLicenseGroupImporter(final QueryManager qm) { - this.qm = qm; - } - - public boolean shouldImport() { - if (qm.getLicenseGroups().getTotal() > 0) { - return false; - } - return true; - } - - public void loadDefaults() throws IOException { - final File defaultsFile = new File(URLDecoder.decode(getClass().getProtectionDomain().getCodeSource().getLocation().getPath(), UTF_8.name()) + "default-objects/licenseGroups.json"); - final JSONArray licenseGroups = readFile(defaultsFile); - for (int i = 0; i < licenseGroups.length(); i++) { - final JSONObject json = licenseGroups.getJSONObject(i); - final LicenseGroup licenseGroup = new LicenseGroup(); - licenseGroup.setName(json.getString("name")); - licenseGroup.setRiskWeight(json.getInt("riskWeight")); - LOGGER.debug("Adding " + licenseGroup.getName()); - final List licenses = new ArrayList<>(); - for (int k=0; k T getValue(final ConfigPropertyConstants property, final Class cl return getOptionalValue(property, clazz).orElseThrow(NoSuchElementException::new); } + @SqlUpdate(""" + UPDATE "CONFIGPROPERTY" + SET "PROPERTYVALUE" = :value + WHERE "GROUPNAME" = :groupName + AND "PROPERTYNAME" = :propertyName + """) + void setValue(@BindBean ConfigPropertyConstants property, @Bind String value); + } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java index 25b387ed53..916eff5952 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java @@ -154,6 +154,10 @@ public static Jdbi createLocalJdbi(final QueryManager qm) { return createLocalJdbi(qm.getPersistenceManager()); } + public static Jdbi createLocalJdbi(final DataSource dataSource) { + return customizeJdbi(Jdbi.create(dataSource)); + } + private static Jdbi createLocalJdbi(final PersistenceManager pm) { if (!pm.currentTransaction().isActive()) { throw new IllegalStateException(""" diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java index 21ac7a32d2..a58ad468dc 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java @@ -21,6 +21,9 @@ import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.ProjectMetrics; +import org.dependencytrack.persistence.pagination.Page; +import org.jdbi.v3.core.mapper.reflect.ConstructorMapper; +import org.jdbi.v3.core.statement.Query; import org.jdbi.v3.sqlobject.SqlObject; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.customizer.Bind; @@ -35,21 +38,170 @@ import java.util.Collection; import java.util.List; +import static org.dependencytrack.persistence.pagination.PageUtil.decodePageToken; +import static org.dependencytrack.persistence.pagination.PageUtil.encodePageToken; + /** * @since 5.6.0 */ public interface MetricsDao extends SqlObject { + record ListVulnerabilityMetricsPageToken(int year, int month) { + } + + record ListVulnerabilityMetricsRow(int year, int month, int count, Instant measuredAt) { + } + + default Page getVulnerabilityMetrics(final int limit, final String pageToken) { + final var decodedPageToken = decodePageToken(getHandle(), pageToken, ListVulnerabilityMetricsPageToken.class); + + final Query query = getHandle().createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="year" type="Boolean" --> + <#-- @ftlvariable name="month" type="Boolean" --> + SELECT * + FROM "VULNERABILITYMETRICS" + WHERE TRUE + <#if year && month> + AND ("YEAR", "MONTH") > (:year, :month) + + ORDER BY "YEAR" ASC, "MONTH" ASC + LIMIT :limit + """); + + final List rows = query + .bind("year", decodedPageToken != null + ? decodedPageToken.year() + : null) + .bind("month", decodedPageToken != null + ? decodedPageToken.month() + : null) + .bind("limit", limit + 1) + .defineNamedBindings() + .map(ConstructorMapper.of(ListVulnerabilityMetricsRow.class)) + .list(); + + final List resultRows = rows.size() > 1 + ? rows.subList(0, Math.min(rows.size(), limit)) + : rows; + + final ListVulnerabilityMetricsPageToken nextPageToken = rows.size() > limit + ? new ListVulnerabilityMetricsPageToken(resultRows.getLast().year, resultRows.getLast().month) + : null; + + return new Page<>(resultRows, encodePageToken(getHandle(), nextPageToken)); + } + + /** + * Note that generate_series is invoked with integers rather + * than dates, because the query planner tends to overestimate + * rows with the latter approach. + * + * @see generate_series quirk + */ @SqlQuery(""" - SELECT * FROM "PORTFOLIOMETRICS" - WHERE "LAST_OCCURRENCE" >= :since - ORDER BY "LAST_OCCURRENCE" ASC + WITH + date_range AS( + SELECT DATE_TRUNC('day', CURRENT_DATE - (INTERVAL '1 day' * day)) AS metrics_date + FROM GENERATE_SERIES(0, GREATEST(:days - 1, 0)) day + ), + projects_in_scope AS( + SELECT "ID" + FROM "PROJECT" + WHERE "INACTIVE_SINCE" IS NULL + AND ${apiProjectAclCondition} + ), + latest_daily_project_metrics AS( + SELECT date_range.metrics_date + , latest_metrics.* + FROM date_range + LEFT JOIN LATERAL ( + SELECT DISTINCT ON (pm."PROJECT_ID") + pm.* + FROM projects_in_scope + INNER JOIN "PROJECTMETRICS" pm + ON pm."PROJECT_ID" = projects_in_scope."ID" + WHERE pm."LAST_OCCURRENCE" < date_range.metrics_date + INTERVAL '1 day' + AND pm."LAST_OCCURRENCE" >= DATE_TRUNC('day', CURRENT_DATE - (INTERVAL '1 day' * GREATEST(:days - 1, 0))) + ORDER BY pm."PROJECT_ID", pm."LAST_OCCURRENCE" DESC + ) AS latest_metrics ON TRUE + ), + daily_metrics AS( + SELECT COUNT(DISTINCT "PROJECT_ID") AS projects + , SUM("COMPONENTS") AS components + , SUM("CRITICAL") AS critical + , metrics_date + , SUM("FINDINGS_AUDITED") AS findings_audited + , SUM("FINDINGS_TOTAL") AS findings_total + , SUM("FINDINGS_UNAUDITED") AS findings_unaudited + , SUM("HIGH") AS high + , SUM("RISKSCORE") as inherited_risk_score + , SUM("LOW") AS low + , SUM("MEDIUM") AS medium + , SUM("POLICYVIOLATIONS_AUDITED") AS policy_violations_audited + , SUM("POLICYVIOLATIONS_FAIL") AS policy_violations_fail + , SUM("POLICYVIOLATIONS_INFO") AS policy_violations_info + , SUM("POLICYVIOLATIONS_LICENSE_AUDITED") AS policy_violations_license_audited + , SUM("POLICYVIOLATIONS_LICENSE_TOTAL") AS policy_violations_license_total + , SUM("POLICYVIOLATIONS_LICENSE_UNAUDITED") AS policy_violations_license_unaudited + , SUM("POLICYVIOLATIONS_OPERATIONAL_AUDITED") AS policy_violations_operational_audited + , SUM("POLICYVIOLATIONS_OPERATIONAL_TOTAL") AS policy_violations_operational_total + , SUM("POLICYVIOLATIONS_OPERATIONAL_UNAUDITED") AS policy_violations_operational_unaudited + , SUM("POLICYVIOLATIONS_SECURITY_AUDITED") AS policy_violations_security_audited + , SUM("POLICYVIOLATIONS_SECURITY_TOTAL") AS policy_violations_security_total + , SUM("POLICYVIOLATIONS_SECURITY_UNAUDITED") AS policy_violations_security_unaudited + , SUM("POLICYVIOLATIONS_TOTAL") AS policy_violations_total + , SUM("POLICYVIOLATIONS_UNAUDITED") AS policy_violations_unaudited + , SUM("POLICYVIOLATIONS_WARN") AS policy_violations_warn + , SUM("SUPPRESSED") AS suppressed + , SUM("UNASSIGNED_SEVERITY") AS unassigned + , SUM("VULNERABILITIES") AS vulnerabilities + , SUM("VULNERABLECOMPONENTS") AS vulnerable_components + , SUM(CASE WHEN "VULNERABLECOMPONENTS" > 0 THEN 1 ELSE 0 END) AS vulnerable_projects + FROM latest_daily_project_metrics + GROUP BY metrics_date + ) + SELECT COALESCE(dm.components, 0) AS components + , COALESCE(dm.critical, 0) AS critical + , COALESCE(dm.findings_audited, 0) AS findings_audited + , COALESCE(dm.findings_total, 0) AS findings_total + , COALESCE(dm.findings_unaudited, 0) AS findings_unaudited + , date_range.metrics_date AS first_occurrence + , COALESCE(dm.high, 0) AS high + , COALESCE(dm.inherited_risk_score, 0) AS inherited_risk_score + , date_range.metrics_date AS last_occurrence + , COALESCE(dm.low, 0) AS low + , COALESCE(dm.medium, 0) AS medium + , COALESCE(dm.policy_violations_audited, 0) AS policy_violations_audited + , COALESCE(dm.policy_violations_fail, 0) AS policy_violations_fail + , COALESCE(dm.policy_violations_info, 0) AS policy_violations_info + , COALESCE(dm.policy_violations_license_audited, 0) AS policy_violations_license_audited + , COALESCE(dm.policy_violations_license_total, 0) AS policy_violations_license_total + , COALESCE(dm.policy_violations_license_unaudited, 0) AS policy_violations_license_unaudited + , COALESCE(dm.policy_violations_operational_audited, 0) AS policy_violations_operational_audited + , COALESCE(dm.policy_violations_operational_total, 0) AS policy_violations_operational_total + , COALESCE(dm.policy_violations_operational_unaudited, 0) AS policy_violations_operational_unaudited + , COALESCE(dm.policy_violations_security_audited, 0) AS policy_violations_security_audited + , COALESCE(dm.policy_violations_security_total, 0) AS policy_violations_security_total + , COALESCE(dm.policy_violations_security_unaudited, 0) AS policy_violations_security_unaudited + , COALESCE(dm.policy_violations_total, 0) AS policy_violations_total + , COALESCE(dm.policy_violations_unaudited, 0) AS policy_violations_unaudited + , COALESCE(dm.policy_violations_warn, 0) AS policy_violations_warn + , COALESCE(dm.projects, 0) AS projects + , COALESCE(dm.suppressed, 0) AS suppressed + , COALESCE(dm.unassigned, 0) AS unassigned + , COALESCE(dm.vulnerabilities, 0) AS vulnerabilities + , COALESCE(dm.vulnerable_components, 0) AS vulnerable_components + , COALESCE(dm.vulnerable_projects, 0) AS vulnerable_projects + FROM date_range + LEFT JOIN daily_metrics AS dm + ON date_range.metrics_date = dm.metrics_date + ORDER BY date_range.metrics_date; """) @RegisterBeanMapper(PortfolioMetrics.class) - List getPortfolioMetricsSince(@Bind Instant since); + List getPortfolioMetricsForDays(@Bind int days); @SqlQuery(""" - SELECT * FROM "PROJECTMETRICS" + SELECT *, "RISKSCORE" AS inherited_risk_score FROM "PROJECTMETRICS" WHERE "PROJECT_ID" = :projectId AND "LAST_OCCURRENCE" >= :since ORDER BY "LAST_OCCURRENCE" ASC @@ -58,7 +210,7 @@ public interface MetricsDao extends SqlObject { List getProjectMetricsSince(@Bind long projectId, @Bind Instant since); @SqlQuery(""" - SELECT * FROM "DEPENDENCYMETRICS" + SELECT *, "RISKSCORE" AS inherited_risk_score FROM "DEPENDENCYMETRICS" WHERE "COMPONENT_ID" = :componentId AND "LAST_OCCURRENCE" >= :since ORDER BY "LAST_OCCURRENCE" ASC @@ -66,17 +218,14 @@ public interface MetricsDao extends SqlObject { @RegisterBeanMapper(DependencyMetrics.class) List getDependencyMetricsSince(@Bind long componentId, @Bind Instant since); - @SqlQuery(""" - SELECT * - FROM "PORTFOLIOMETRICS" - ORDER BY "LAST_OCCURRENCE" DESC - LIMIT 1 - """) - @RegisterBeanMapper(PortfolioMetrics.class) - PortfolioMetrics getMostRecentPortfolioMetrics(); + default PortfolioMetrics getMostRecentPortfolioMetrics() { + // Request metrics since yesterday, such that we cater for projects that do + // not have fresh metrics from today yet. + return getPortfolioMetricsForDays(2).getLast(); + } @SqlQuery(""" - SELECT * + SELECT *, "RISKSCORE" AS inherited_risk_score FROM "PROJECTMETRICS" WHERE "PROJECT_ID" = :projectId ORDER BY "LAST_OCCURRENCE" DESC @@ -86,7 +235,7 @@ public interface MetricsDao extends SqlObject { ProjectMetrics getMostRecentProjectMetrics(@Bind final long projectId); @SqlQuery(""" - SELECT metrics.* + SELECT metrics.*, metrics."RISKSCORE" AS inherited_risk_score FROM UNNEST(:projectIds) AS project(id) INNER JOIN LATERAL ( SELECT * @@ -100,7 +249,7 @@ INNER JOIN LATERAL ( List getMostRecentProjectMetrics(@Bind Collection projectIds); @SqlQuery(""" - SELECT * + SELECT *, "RISKSCORE" AS inherited_risk_score FROM "DEPENDENCYMETRICS" WHERE "COMPONENT_ID" = :componentId ORDER BY "LAST_OCCURRENCE" DESC @@ -110,7 +259,7 @@ INNER JOIN LATERAL ( DependencyMetrics getMostRecentDependencyMetrics(@Bind long componentId); @SqlQuery(""" - SELECT metrics.* + SELECT metrics.*, metrics."RISKSCORE" AS inherited_risk_score FROM UNNEST(:componentIds) AS component(id) INNER JOIN LATERAL ( SELECT * @@ -126,21 +275,16 @@ INNER JOIN LATERAL ( @SqlQuery(""" SELECT inhrelid::regclass AS partition_name FROM pg_inherits - WHERE inhparent = '"PORTFOLIOMETRICS"'::regclass; - """) - List getPortfolioMetricsPartitions(); - - @SqlQuery(""" - SELECT inhrelid::regclass AS partition_name - FROM pg_inherits - WHERE inhparent = '"PROJECTMETRICS"'::regclass; + WHERE inhparent = '"PROJECTMETRICS"'::regclass + ORDER BY partition_name; """) List getProjectMetricsPartitions(); @SqlQuery(""" SELECT inhrelid::regclass AS partition_name FROM pg_inherits - WHERE inhparent = '"DEPENDENCYMETRICS"'::regclass; + WHERE inhparent = '"DEPENDENCYMETRICS"'::regclass + ORDER BY partition_name; """) List getDependencyMetricsPartitions(); @@ -153,7 +297,7 @@ INNER JOIN LATERAL ( partition_name TEXT; partition_exists BOOLEAN; table_name TEXT; - metric_tables TEXT[] := ARRAY['PORTFOLIOMETRICS', 'PROJECTMETRICS', 'DEPENDENCYMETRICS']; + metric_tables TEXT[] := ARRAY['PROJECTMETRICS', 'DEPENDENCYMETRICS']; BEGIN FOREACH table_name IN ARRAY metric_tables LOOP @@ -182,11 +326,6 @@ EXECUTE format( """) void createMetricsPartitionsForDate(@Define("targetDate") String targetDate, @Define("nextDate") String nextDate); - default int deletePortfolioMetricsForRetentionDuration(Duration retentionDuration) { - List metricsPartitions = getPortfolioMetricsPartitions(); - return dropOldPartitions(metricsPartitions, retentionDuration); - } - default int deleteProjectMetricsForRetentionDuration(Duration retentionDuration) { List metricsPartitions = getProjectMetricsPartitions(); return dropOldPartitions(metricsPartitions, retentionDuration); diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java index de8994cc23..60b9dd0726 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java @@ -22,7 +22,6 @@ import alpine.persistence.PaginatedResult; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.Nullable; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.Tag; @@ -44,6 +43,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import jakarta.annotation.Nullable; import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; @@ -66,6 +66,7 @@ public interface ProjectDao { @SqlQuery(/* language=InjectedFreeMarker */ """ <#-- @ftlvariable name="nameFilter" type="Boolean" --> + <#-- @ftlvariable name="versionFilter" type="Boolean" --> <#-- @ftlvariable name="classifierFilter" type="Boolean" --> <#-- @ftlvariable name="tagFilter" type="Boolean" --> <#-- @ftlvariable name="teamFilter" type="Boolean" --> @@ -103,37 +104,39 @@ public interface ProjectDao { FROM "PROJECT" AS "CHILD_PROJECT" WHERE "CHILD_PROJECT"."PARENT_PROJECT_ID" = "PROJECT"."ID")) AS "hasChildren" <#if includeMetrics> - , TO_JSONB("metrics") AS "metrics" + , (SELECT TO_JSONB(m) + FROM ( + SELECT "COMPONENTS" + , "CRITICAL" + , "HIGH" + , "LOW" + , "MEDIUM" + , "POLICYVIOLATIONS_FAIL" + , "POLICYVIOLATIONS_INFO" + , "POLICYVIOLATIONS_LICENSE_TOTAL" + , "POLICYVIOLATIONS_OPERATIONAL_TOTAL" + , "POLICYVIOLATIONS_SECURITY_TOTAL" + , "POLICYVIOLATIONS_TOTAL" + , "POLICYVIOLATIONS_WARN" + , "RISKSCORE" + , "UNASSIGNED_SEVERITY" + , "VULNERABILITIES" + FROM "PROJECTMETRICS" + WHERE "PROJECTMETRICS"."PROJECT_ID" = "PROJECT"."ID" + ORDER BY "PROJECTMETRICS"."LAST_OCCURRENCE" DESC + LIMIT 1 + ) AS m + ) AS "metrics" , COUNT(*) OVER() AS "totalCount" FROM "PROJECT" - <#if includeMetrics> - LEFT JOIN LATERAL ( - SELECT "COMPONENTS" - , "CRITICAL" - , "HIGH" - , "LOW" - , "MEDIUM" - , "POLICYVIOLATIONS_FAIL" - , "POLICYVIOLATIONS_INFO" - , "POLICYVIOLATIONS_LICENSE_TOTAL" - , "POLICYVIOLATIONS_OPERATIONAL_TOTAL" - , "POLICYVIOLATIONS_SECURITY_TOTAL" - , "POLICYVIOLATIONS_TOTAL" - , "POLICYVIOLATIONS_WARN" - , "RISKSCORE" - , "UNASSIGNED_SEVERITY" - , "VULNERABILITIES" - FROM "PROJECTMETRICS" - WHERE "PROJECTMETRICS"."PROJECT_ID" = "PROJECT"."ID" - ORDER BY "PROJECTMETRICS"."LAST_OCCURRENCE" DESC - LIMIT 1 - ) AS "metrics" ON TRUE - WHERE ${apiProjectAclCondition} <#if nameFilter> AND "PROJECT"."NAME" = :nameFilter + <#if versionFilter> + AND "PROJECT"."VERSION" = :versionFilter + <#if classifierFilter> AND "PROJECT"."CLASSIFIER" = :classifierFilter @@ -194,13 +197,10 @@ OR EXISTS (SELECT 1 FROM "TAG" WHERE "TAG"."NAME" = ${apiFilterParameter})) @AllowApiOrdering.Column(name = "isLatest"), @AllowApiOrdering.Column(name = "lastBomImport"), @AllowApiOrdering.Column(name = "lastBomImportFormat"), - @AllowApiOrdering.Column(name = "metrics.components", queryName = "\"metrics\".\"COMPONENTS\""), - @AllowApiOrdering.Column(name = "metrics.inheritedRiskScore", queryName = "\"metrics\".\"RISKSCORE\""), - @AllowApiOrdering.Column(name = "metrics.policyViolationsTotal", queryName = "\"metrics\".\"POLICYVIOLATIONS_TOTAL\""), - @AllowApiOrdering.Column(name = "metrics.vulnerabilities", queryName = "\"metrics\".\"VULNERABILITIES\"") }) List getPageConcise( @Bind String nameFilter, + @Bind String versionFilter, @Bind String classifierFilter, @Bind String tagFilter, @Bind String teamFilter, diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java index dced37e427..9a054557d7 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java @@ -18,10 +18,138 @@ */ package org.dependencytrack.persistence.jdbi; +import org.dependencytrack.persistence.pagination.Page; +import org.jdbi.v3.core.mapper.reflect.ConstructorMapper; +import org.jdbi.v3.core.statement.Query; +import org.jdbi.v3.core.statement.Update; +import org.jdbi.v3.sqlobject.SqlObject; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.statement.SqlUpdate; -public interface TeamDao { +import java.util.Collection; +import java.util.List; + +import static org.dependencytrack.persistence.pagination.PageUtil.decodePageToken; +import static org.dependencytrack.persistence.pagination.PageUtil.encodePageToken; + +public interface TeamDao extends SqlObject { + + record ListTeamsPageToken(String lastName) { + } + + record ListTeamsRow(String name, int apiKeys, int members) { + } + + default Page listTeams(final int limit, final String pageToken) { + final var decodedPageToken = decodePageToken(getHandle(), pageToken, ListTeamsPageToken.class); + + final Query query = getHandle().createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="lastName" type="Boolean" --> + SELECT "NAME" AS name + , (SELECT COUNT(*) FROM "APIKEYS_TEAMS" WHERE "TEAM_ID" = "TEAM"."ID") AS api_keys + , (SELECT COUNT(*) FROM "USERS_TEAMS" WHERE "TEAM_ID" = "TEAM"."ID") AS members + FROM "TEAM" + WHERE TRUE + <#if lastName> + AND "NAME" > :lastName + + ORDER BY "NAME" + LIMIT :limit + """); + + final List rows = query + .bind("lastName", decodedPageToken != null + ? decodedPageToken.lastName() + : null) + .bind("limit", limit + 1) + .defineNamedBindings() + .map(ConstructorMapper.of(ListTeamsRow.class)) + .list(); + + final List resultRows = rows.size() > 1 + ? rows.subList(0, Math.min(rows.size(), limit)) + : rows; + + final ListTeamsPageToken nextPageToken = rows.size() > limit + ? new ListTeamsPageToken(resultRows.getLast().name()) + : null; + + return new Page<>(resultRows, encodePageToken(getHandle(), nextPageToken)); + } + + record ListTeamMembershipsPageToken(String lastTeamName, String lastUsername) { + } + + record ListTeamMembershipsRow(String teamName, String username) { + } + + default Page listTeamMembers( + final String teamName, + final String username, + final int limit, + final String pageToken) { + final var decodedPageToken = decodePageToken(getHandle(), pageToken, ListTeamMembershipsPageToken.class); + + final Query query = getHandle().createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="teamName" type="Boolean" --> + <#-- @ftlvariable name="username" type="Boolean" --> + <#-- @ftlvariable name="lastTeamName" type="Boolean" --> + <#-- @ftlvariable name="lastUsername" type="Boolean" --> + SELECT t."NAME" AS team_name + , u."USERNAME" AS username + FROM "USERS_TEAMS" AS ut + INNER JOIN "TEAM" AS t + ON t."ID" = ut."TEAM_ID" + INNER JOIN "USER" AS u + ON u."ID" = ut."USER_ID" + WHERE TRUE + <#if teamName> + AND t."NAME" = :teamName + + <#if username> + AND u."USERNAME" = :username + + <#if lastTeamName && lastUsername> + AND (t."NAME", u."USERNAME") > (:lastTeamName, :lastUsername) + + ORDER BY t."NAME", u."USERNAME" + LIMIT :limit + """); + + final List rows = query + .bind("teamName", teamName) + .bind("username", username) + .bind("lastTeamName", decodedPageToken != null + ? decodedPageToken.lastTeamName() + : null) + .bind("lastUsername", decodedPageToken != null + ? decodedPageToken.lastUsername() + : null) + .bind("limit", limit + 1) + .defineNamedBindings() + .map(ConstructorMapper.of(ListTeamMembershipsRow.class)) + .list(); + + final List resultRows = rows.size() > 1 + ? rows.subList(0, Math.min(rows.size(), limit)) + : rows; + + final ListTeamMembershipsPageToken nextPageToken = rows.size() > limit + ? new ListTeamMembershipsPageToken( + resultRows.getLast().teamName(), + resultRows.getLast().username()) + : null; + + return new Page<>(resultRows, encodePageToken(getHandle(), nextPageToken)); + } + + @SqlUpdate(""" + DELETE + FROM "USERS_TEAMS" + WHERE "TEAM_ID" = (SELECT "ID" FROM "TEAM" WHERE "NAME" = :teamName) + AND "USER_ID" = (SELECT "ID" FROM "USER" WHERE "USERNAME" = :username) + """) + boolean deleteTeamMembership(@Bind String teamName, @Bind String username); @SqlUpdate(""" DELETE @@ -29,4 +157,19 @@ public interface TeamDao { WHERE "ID" = :teamId """) int deleteTeam(@Bind final long teamId); + + default List deleteTeamsByName(final Collection names) { + final Update update = getHandle().createUpdate(""" + DELETE + FROM "TEAM" + WHERE "NAME" = ANY(:names) + RETURNING "NAME" + """); + + return update + .bindArray("names", String.class, names) + .executeAndReturnGeneratedKeys() + .mapTo(String.class) + .list(); + } } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/pagination/InvalidPageTokenException.java b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/InvalidPageTokenException.java new file mode 100644 index 0000000000..359722fd4a --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/InvalidPageTokenException.java @@ -0,0 +1,27 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence.pagination; + +public class InvalidPageTokenException extends IllegalArgumentException { + + public InvalidPageTokenException(final Throwable cause) { + super(cause); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/Page.java similarity index 78% rename from apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java rename to apiserver/src/main/java/org/dependencytrack/persistence/pagination/Page.java index f2971a7ff9..dd62e1c52e 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/Page.java @@ -16,14 +16,9 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) OWASP Foundation. All Rights Reserved. */ -package org.dependencytrack.persistence.defaults; +package org.dependencytrack.persistence.pagination; -import java.io.IOException; - -public interface IDefaultObjectImporter { - - boolean shouldImport(); - - void loadDefaults() throws IOException; +import java.util.List; +public record Page(List items, String nextPageToken) { } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/pagination/PageUtil.java b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/PageUtil.java new file mode 100644 index 0000000000..02377594df --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/PageUtil.java @@ -0,0 +1,88 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence.pagination; + +import alpine.security.crypto.DataEncryption; +import org.dependencytrack.api.v2.model.PaginationLinks; +import org.dependencytrack.api.v2.model.PaginationMetadata; +import org.jdbi.v3.core.Handle; +import org.jdbi.v3.json.JsonConfig; +import org.jdbi.v3.json.JsonMapper; + +import jakarta.ws.rs.core.UriInfo; +import java.util.Base64; + +/** + * @since 5.6.0 + */ +public final class PageUtil { + + private PageUtil() {} + + public static T decodePageToken(final Handle handle, final String encodedToken, final Class tokenClass) { + if (encodedToken == null) { + return null; + } + + final JsonMapper.TypedJsonMapper jsonMapper = handle + .getConfig(JsonConfig.class) + .getJsonMapper() + .forType(tokenClass, handle.getConfig()); + + try { + final byte[] encryptedTokenBytes = Base64.getUrlDecoder().decode(encodedToken); + final byte[] decryptedToken = DataEncryption.decryptAsBytes(encryptedTokenBytes); + return (T) jsonMapper.fromJson(new String(decryptedToken), handle.getConfig()); + } catch (Exception e) { + throw new InvalidPageTokenException(e); + } + } + + public static String encodePageToken(final Handle handle, final T pageToken) { + if (pageToken == null) { + return null; + } + + final JsonMapper.TypedJsonMapper jsonMapper = handle + .getConfig(JsonConfig.class) + .getJsonMapper() + .forType(Object.class, handle.getConfig()); + + try { + final String tokenJson = jsonMapper.toJson(pageToken, handle.getConfig()); + final byte[] encryptedTokenBytes = DataEncryption.encryptAsBytes(tokenJson); + return Base64.getUrlEncoder().encodeToString(encryptedTokenBytes); + } catch (Exception e) { + throw new InvalidPageTokenException(e); + } + } + + public static PaginationMetadata createPaginationMetadata(final UriInfo uriInfo, final Page page) { + return PaginationMetadata.builder() + .links(PaginationLinks.builder() + .self(uriInfo.getRequestUri()) + .next(page.nextPageToken() != null ? + uriInfo.getRequestUriBuilder() + .replaceQueryParam("page_token", page.nextPageToken()) + .build() + : null) + .build()) + .build(); + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java b/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java index 5449f8f313..5751f325ff 100644 --- a/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java +++ b/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java @@ -18,9 +18,7 @@ */ package org.dependencytrack.plugin; -import alpine.Config; import alpine.common.logging.Logger; -import org.dependencytrack.common.ConfigKey; import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletContextListener; @@ -36,12 +34,6 @@ public class PluginInitializer implements ServletContextListener { @Override public void contextInitialized(final ServletContextEvent event) { - if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) { - LOGGER.debug("Not loading plugins because %s is enabled" - .formatted(ConfigKey.INIT_AND_EXIT.getPropertyName())); - return; - } - LOGGER.info("Loading plugins"); pluginManager.loadPlugins(); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 38602a65f4..e0865ab75a 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -24,13 +24,14 @@ import alpine.notification.Notification; import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; +import alpine.server.filters.ResourceAccessRequired; +import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.SignatureException; -import alpine.server.filters.ResourceAccessRequired; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -40,23 +41,6 @@ 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.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonReader; -import jakarta.json.JsonString; -import jakarta.validation.Validator; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.StringUtils; @@ -93,8 +77,23 @@ import org.glassfish.jersey.media.multipart.FormDataParam; import org.owasp.security.logging.SecurityMarkers; -import com.fasterxml.jackson.databind.ObjectMapper; - +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonReader; +import jakarta.json.JsonString; +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; @@ -107,6 +106,8 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; + +import static java.util.Objects.requireNonNull; import static java.util.function.Predicate.not; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE; @@ -314,13 +315,14 @@ public Response exportComponentAsCycloneDx( @ResourceAccessRequired public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) { final Validator validator = getValidator(); + final ProcessingResult processingResult; if (request.getProject() != null) { // behavior in v3.0.0 failOnValidationError( validator.validateProperty(request, "project"), validator.validateProperty(request, "bom") ); try (QueryManager qm = new QueryManager()) { - return qm.callInTransaction(() -> { + processingResult = qm.callInTransaction(() -> { final Project project = qm.getObjectByUuid(Project.class, request.getProject()); return process(qm, project, request.getBom()); }); @@ -332,7 +334,7 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) validator.validateProperty(request, "bom") ); try (final var qm = new QueryManager()) { - return qm.callInTransaction(() -> { + processingResult = qm.callInTransaction(() -> { Project project = qm.getProject(request.getProjectName(), request.getProjectVersion()); if (project == null && request.isAutoCreate()) { if (hasPermission(Permissions.Constants.PORTFOLIO_MANAGEMENT) || hasPermission(Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE) || hasPermission(Permissions.Constants.PROJECT_CREATION_UPLOAD)) { @@ -352,20 +354,27 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) } if (parent == null) { // if parent project is specified but not found - return Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build(); + final var response = Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build(); + return new ProcessingResult(response, null); } requireAccess(qm, parent, "Access to the specified parent project is forbidden"); } createNewProject(request.getProjectName(), request.getProjectVersion(), request.getProjectTags(), parent, request.isLatestProjectVersion(), null); } else { - return Response.status(Response.Status.UNAUTHORIZED) - .entity("The principal does not have permission to create project.").build(); + final var response = Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build(); + return new ProcessingResult(response, null); } } return process(qm, project, request.getBom()); }); } } + + if (processingResult.event() != null) { + Event.dispatch(processingResult.event()); + } + + return processingResult.response(); } @POST @@ -540,16 +549,17 @@ public Response uploadBom( @DefaultValue("false") @FormDataParam("isLatest") boolean isLatest, @Parameter(schema = @Schema(type = "string")) @FormDataParam("bom") final List artifactParts ) { + final ProcessingResult processingResult; if (projectUuid != null) { // behavior in v3.0.0 try (QueryManager qm = new QueryManager()) { - return qm.callInTransaction(() -> { + processingResult = qm.callInTransaction(() -> { final Project project = qm.getObjectByUuid(Project.class, projectUuid); return process(qm, project, artifactParts); }); } } else { // additional behavior added in v3.1.0 try (QueryManager qm = new QueryManager()) { - return qm.callInTransaction(() -> { + processingResult = qm.callInTransaction(() -> { final String trimmedProjectName = StringUtils.trimToNull(projectName); final String trimmedProjectVersion = StringUtils.trimToNull(projectVersion); Project project = qm.getProject(trimmedProjectName, trimmedProjectVersion); @@ -567,7 +577,8 @@ public Response uploadBom( } if (parent == null) { // if parent project is specified but not found - return Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build(); + final var response = Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build(); + return new ProcessingResult(response, null); } requireAccess(qm, parent, "Access to the specified parent project is forbidden"); } @@ -576,20 +587,34 @@ public Response uploadBom( : null; createNewProject(projectName, projectVersion, tags, parent, isLatest, null); } else { - return Response.status(Response.Status.UNAUTHORIZED) - .entity("The principal does not have permission to create project.").build(); + final var response = Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build(); + return new ProcessingResult(response, null); } } return process(qm, project, artifactParts); }); } } + + if (processingResult.event() != null) { + Event.dispatch(processingResult.event()); + } + + return processingResult.response(); + } + + private record ProcessingResult(Response response, BomUploadEvent event) { + + private ProcessingResult { + requireNonNull(response, "response must not be null"); + } + } /** * Common logic that processes a BOM given a project and encoded payload. */ - private Response process(QueryManager qm, Project project, String encodedBomData) { + private ProcessingResult process(QueryManager qm, Project project, String encodedBomData) { if (project != null) { requireAccess(qm, project); @@ -600,25 +625,28 @@ private Response process(QueryManager qm, Project project, String encodedBomData bomFileMetadata = validateAndStoreBom(IOUtils.toByteArray(byteOrderMarkInputStream), project); } catch (IOException e) { LOGGER.error("An unexpected error occurred while validating or storing a BOM uploaded to project: " + project.getUuid(), e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + final var response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + return new ProcessingResult(response, null); } final BomUploadEvent bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomFileMetadata); qm.createWorkflowSteps(bomUploadEvent.getChainIdentifier()); - Event.dispatch(bomUploadEvent); BomUploadResponse bomUploadResponse = new BomUploadResponse(); bomUploadResponse.setToken(bomUploadEvent.getChainIdentifier()); - return Response.ok(bomUploadResponse).build(); + final var response = Response.ok(bomUploadResponse).build(); + + return new ProcessingResult(response, bomUploadEvent); } else { - return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + final var response = Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + return new ProcessingResult(response, null); } } /** * Common logic that processes a BOM given a project and list of multi-party form objects containing decoded payloads. */ - private Response process(QueryManager qm, Project project, List artifactParts) { + private ProcessingResult process(QueryManager qm, Project project, List artifactParts) { for (final FormDataBodyPart artifactPart : artifactParts) { final BodyPartEntity bodyPartEntity = (BodyPartEntity) artifactPart.getEntity(); if (project != null) { @@ -630,7 +658,8 @@ private Response process(QueryManager qm, Project project, List - handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); + PortfolioMetrics metrics = withJdbiHandle( + getAlpineRequest(), + handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); return Response.ok(metrics).build(); } @@ -145,13 +152,25 @@ public Response getPortfolioCurrentMetrics() { public Response getPortfolioMetricsSince( @Parameter(description = "The start date to retrieve metrics for", required = true) @PathParam("date") String date) { - - final Date since = DateUtil.parseShortDate(date); - if (since == null) { + final LocalDate since; + try { + since = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd")); + } catch (DateTimeParseException e) { return Response.status(Response.Status.BAD_REQUEST).entity("The specified date format is incorrect.").build(); } - List metrics = withJdbiHandle(handle -> - handle.attach(MetricsDao.class).getPortfolioMetricsSince(since.toInstant())); + List metrics = withJdbiHandle(getAlpineRequest(), handle -> { + final int retentionDays = handle.attach(ConfigPropertyDao.class) + .getOptionalValue(MAINTENANCE_METRICS_RETENTION_DAYS, Integer.class) + .orElseGet(() -> Integer.parseInt(MAINTENANCE_METRICS_RETENTION_DAYS.getDefaultPropertyValue())); + + // NB: Calculate days between the given date and *tomorrow*, + // because LocalDate#until's end date is exclusive, + // and we want to include data for *today*. + final var sincePeriod = since.until(LocalDate.now().plusDays(1)); + final int sinceDays = sincePeriod.getDays(); + + return handle.attach(MetricsDao.class).getPortfolioMetricsForDays(Math.min(retentionDays, sinceDays)); + }); return Response.ok(metrics).build(); } @@ -174,10 +193,14 @@ public Response getPortfolioMetricsSince( @ResourceAccessRequired public Response getPortfolioMetricsXDays( @Parameter(description = "The number of days back to retrieve metrics for", required = true) - @PathParam("days") int days) { - final Date since = DateUtils.addDays(new Date(), -days); - List metrics = withJdbiHandle(handle -> - handle.attach(MetricsDao.class).getPortfolioMetricsSince(since.toInstant())); + @PathParam("days") @Positive int days) { + List metrics = withJdbiHandle(getAlpineRequest(), handle -> { + final int retentionDays = handle.attach(ConfigPropertyDao.class) + .getOptionalValue(MAINTENANCE_METRICS_RETENTION_DAYS, Integer.class) + .orElseGet(() -> Integer.parseInt(MAINTENANCE_METRICS_RETENTION_DAYS.getDefaultPropertyValue())); + + return handle.attach(MetricsDao.class).getPortfolioMetricsForDays(Math.min(days, retentionDays)); + }); return Response.ok(metrics).build(); } @@ -294,7 +317,7 @@ public Response getProjectMetricsXDays( @PathParam("uuid") @ValidUuid String uuid, @Parameter(description = "The number of days back to retrieve metrics for", required = true) @PathParam("days") int days) { - final Date since = DateUtils.addDays(new Date(), -days); + final Date since = addDays(new Date(), -days); return getProjectMetrics(uuid, since); } @@ -429,7 +452,7 @@ public Response getComponentMetricsXDays( @PathParam("uuid") @ValidUuid String uuid, @Parameter(description = "The number of days back to retrieve metrics for", required = true) @PathParam("days") int days) { - final Date since = DateUtils.addDays(new Date(), -days); + final Date since = addDays(new Date(), -days); return getComponentMetrics(uuid, since); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index 888a48dd7c..7a2df295a3 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -19,7 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; -import alpine.model.ConfigProperty; import alpine.notification.Notification; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -33,32 +32,36 @@ 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.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; -import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.publisher.PublisherClass; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.ConfigPropertyDao; import org.dependencytrack.util.NotificationUtil; +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.Arrays; import java.util.List; +import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; + /** * JAX-RS resources for processing notification publishers. * @@ -263,21 +266,14 @@ public Response deleteNotificationPublisher(@Parameter(description = "The UUID o }) @PermissionRequired({Permissions.Constants.SYSTEM_CONFIGURATION, Permissions.Constants.SYSTEM_CONFIGURATION_CREATE}) public Response restoreDefaultTemplates() { - try (QueryManager qm = new QueryManager()) { - return qm.callInTransaction(() -> { - final ConfigProperty property = qm.getConfigProperty( - ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getGroupName(), - ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName() - ); - property.setPropertyValue("false"); - qm.persist(property); - NotificationUtil.loadDefaultNotificationPublishers(qm); - return Response.ok().build(); - }); - } catch (Exception exception) { - LOGGER.error(exception.getMessage(), exception); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Exception occured while restoring default notification publisher templates.").build(); - } + useJdbiTransaction(handle -> { + handle.attach(ConfigPropertyDao.class).setValue( + NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED, "false"); + + DatabaseSeedingInitTask.seedDefaultNotificationPublishers(handle); + }); + + return Response.ok().build(); } @POST diff --git a/apiserver/src/main/java/org/dependencytrack/resources/OpenApiResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/OpenApiResource.java similarity index 97% rename from apiserver/src/main/java/org/dependencytrack/resources/OpenApiResource.java rename to apiserver/src/main/java/org/dependencytrack/resources/v1/OpenApiResource.java index f67af48a7b..9c49cd3db6 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/OpenApiResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/OpenApiResource.java @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) OWASP Foundation. All Rights Reserved. */ -package org.dependencytrack.resources; +package org.dependencytrack.resources.v1; import alpine.server.auth.AuthenticationNotRequired; import io.swagger.v3.jaxrs2.integration.resources.BaseOpenApiResource; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 87af1017d4..5b023d434b 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -37,21 +37,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import jakarta.validation.Validator; -import jakarta.ws.rs.ClientErrorException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PATCH; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.ServerErrorException; -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.event.CloneProjectEvent; @@ -72,6 +57,21 @@ import org.dependencytrack.resources.v1.vo.ConciseProject; import org.jdbi.v3.core.Handle; +import jakarta.validation.Validator; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import javax.jdo.FetchGroup; import java.security.Principal; import java.util.Collection; @@ -171,6 +171,8 @@ public Response getProjects(@Parameter(description = "The optional name of the p public Response getProjectsConcise( @Parameter(description = "Name to filter on. Must be exact match.") @QueryParam("name") final String nameFilter, + @Parameter(description = "Version to filter on. Must be exact match.") + @QueryParam("version") final String versionFilter, @Parameter(description = "Classifier to filter on. Must be exact match.") @QueryParam("classifier") final String classifierFilter, @Parameter(description = "Tag to filter on. Must be exact match.") @@ -185,7 +187,7 @@ public Response getProjectsConcise( @QueryParam("includeMetrics") final boolean includeMetrics ) { final List projectRows = withJdbiHandle(getAlpineRequest(), handle -> handle.attach(ProjectDao.class) - .getPageConcise(nameFilter, classifierFilter, tagFilter, teamFilter, activeFilter, onlyRootFilter, /* parentUuidFilter */ null, includeMetrics)); + .getPageConcise(nameFilter, versionFilter, classifierFilter, tagFilter, teamFilter, activeFilter, onlyRootFilter, /* parentUuidFilter */ null, includeMetrics)); final long totalCount = projectRows.isEmpty() ? 0 : projectRows.getFirst().totalCount(); final List projects = projectRows.stream().map(ConciseProject::new).toList(); @@ -216,6 +218,8 @@ public Response getProjectChildrenConcise( @PathParam("uuid") final String parentUuid, @Parameter(description = "Name to filter on. Must be exact match.") @QueryParam("name") final String nameFilter, + @Parameter(description = "Version to filter on. Must be exact match.") + @QueryParam("version") final String versionFilter, @Parameter(description = "Classifier to filter on. Must be exact match.") @QueryParam("classifier") final String classifierFilter, @Parameter(description = "Tag to filter on. Must be exact match.") @@ -228,7 +232,7 @@ public Response getProjectChildrenConcise( @QueryParam("includeMetrics") final boolean includeMetrics ) { final List projectRows = withJdbiHandle(getAlpineRequest(), handle -> handle.attach(ProjectDao.class) - .getPageConcise(nameFilter, classifierFilter, tagFilter, teamFilter, activeFilter, /* onlyRootFilter */ null, UUID.fromString(parentUuid), includeMetrics)); + .getPageConcise(nameFilter, versionFilter, classifierFilter, tagFilter, teamFilter, activeFilter, /* onlyRootFilter */ null, UUID.fromString(parentUuid), includeMetrics)); final long totalCount = projectRows.isEmpty() ? 0 : projectRows.getFirst().totalCount(); final List projects = projectRows.stream().map(ConciseProject::new).toList(); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java new file mode 100644 index 0000000000..3596f851b9 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java @@ -0,0 +1,111 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import alpine.server.auth.PermissionRequired; +import alpine.server.resources.AlpineResource; +import org.dependencytrack.api.v2.MetricsApi; +import org.dependencytrack.api.v2.model.ListVulnerabilityMetricsResponse; +import org.dependencytrack.api.v2.model.ListVulnerabilityMetricsResponseItem; +import org.dependencytrack.api.v2.model.PortfolioMetricsResponse; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.PortfolioMetrics; +import org.dependencytrack.persistence.jdbi.MetricsDao; +import org.dependencytrack.persistence.pagination.Page; + +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.ext.Provider; + +import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; +import static org.dependencytrack.persistence.pagination.PageUtil.createPaginationMetadata; + +@Provider +public class MetricsResource extends AlpineResource implements MetricsApi { + + @Context + private UriInfo uriInfo; + + @Override + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getPortfolioCurrentMetrics() { + PortfolioMetrics metrics = withJdbiHandle( + getAlpineRequest(), + handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); + final var response = PortfolioMetricsResponse.builder() + .components(metrics.getComponents()) + .critical(metrics.getCritical()) + .findingsAudited(metrics.getFindingsAudited()) + .findingsTotal(metrics.getFindingsTotal()) + .findingsUnaudited(metrics.getFindingsUnaudited()) + .high(metrics.getHigh()) + .inheritedRiskScore(metrics.getInheritedRiskScore()) + .observedAt(metrics.getLastOccurrence().getTime()) + .low(metrics.getLow()) + .medium(metrics.getMedium()) + .policyViolationsAudited(metrics.getPolicyViolationsAudited()) + .policyViolationsFail(metrics.getPolicyViolationsFail()) + .policyViolationsInfo(metrics.getPolicyViolationsInfo()) + .policyViolationsLicenseAudited(metrics.getPolicyViolationsLicenseAudited()) + .policyViolationsLicenseTotal(metrics.getPolicyViolationsLicenseTotal()) + .policyViolationsLicenseUnaudited(metrics.getPolicyViolationsLicenseUnaudited()) + .policyViolationsOperationalAudited(metrics.getPolicyViolationsOperationalAudited()) + .policyViolationsOperationalTotal(metrics.getPolicyViolationsOperationalTotal()) + .policyViolationsOperationalUnaudited(metrics.getPolicyViolationsOperationalUnaudited()) + .policyViolationsSecurityAudited(metrics.getPolicyViolationsSecurityAudited()) + .policyViolationsSecurityTotal(metrics.getPolicyViolationsSecurityTotal()) + .policyViolationsSecurityUnaudited(metrics.getPolicyViolationsSecurityUnaudited()) + .policyViolationsTotal(metrics.getPolicyViolationsTotal()) + .policyViolationsUnaudited(metrics.getPolicyViolationsUnaudited()) + .policyViolationsWarn(metrics.getPolicyViolationsWarn()) + .projects(metrics.getProjects()) + .suppressed(metrics.getSuppressed()) + .unassigned(metrics.getUnassigned()) + .vulnerabilities(metrics.getVulnerabilities()) + .vulnerableComponents(metrics.getVulnerableComponents()) + .vulnerableProjects(metrics.getVulnerableProjects()) + .build(); + return Response.ok(response).build(); + } + + @Override + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getVulnerabilityMetrics(Integer limit, String pageToken) { + final Page metricsPage = inJdbiTransaction( + getAlpineRequest(), + handle -> handle.attach(MetricsDao.class).getVulnerabilityMetrics(limit, pageToken)); + + final var response = ListVulnerabilityMetricsResponse.builder() + .metrics(metricsPage.items().stream() + .map( + metricRow -> ListVulnerabilityMetricsResponseItem.builder() + .year(metricRow.year()) + .month(metricRow.month()) + .count(metricRow.count()) + .observedAt(metricRow.measuredAt().getEpochSecond()) + .build()) + .toList()) + .pagination(createPaginationMetadata(uriInfo, metricsPage)) + .build(); + + return Response.ok(response).build(); + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/OpenApiResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/OpenApiResource.java new file mode 100644 index 0000000000..6c8d84677b --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/OpenApiResource.java @@ -0,0 +1,77 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import alpine.server.auth.AuthenticationNotRequired; +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static java.util.Objects.requireNonNull; + +@Path("/openapi.yaml") +public class OpenApiResource { + + private static final ReadWriteLock LOCK = new ReentrantReadWriteLock(); + private static String OPENAPI_YAML; + + @GET + @Produces("application/yaml") + @Operation(hidden = true) + @AuthenticationNotRequired + public String getOpenApi() throws IOException { + LOCK.readLock().lock(); + try { + if (OPENAPI_YAML == null) { + LOCK.readLock().unlock(); + + LOCK.writeLock().lock(); + try { + if (OPENAPI_YAML == null) { + OPENAPI_YAML = loadOpenapiYaml(); + } + + LOCK.readLock().lock(); + } finally { + LOCK.writeLock().unlock(); + } + } + + return OPENAPI_YAML; + } finally { + LOCK.readLock().unlock(); + } + } + + private static String loadOpenapiYaml() throws IOException { + try (final InputStream inputStream = + OpenApiResource.class.getResourceAsStream( + "/org/dependencytrack/api/v2/openapi.yaml")) { + requireNonNull(inputStream, "inputStream must not be null"); + return new String(inputStream.readAllBytes()); + } + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/ResourceConfig.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/ResourceConfig.java new file mode 100644 index 0000000000..6f5741f580 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/ResourceConfig.java @@ -0,0 +1,59 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFeature; +import alpine.server.filters.AuthorizationFeature; +import alpine.server.filters.GZipInterceptor; +import alpine.server.filters.HeaderFilter; +import alpine.server.filters.RequestIdFilter; +import alpine.server.filters.RequestMdcEnrichmentFilter; +import org.dependencytrack.filters.JerseyMetricsFeature; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +import static org.glassfish.jersey.server.ServerProperties.PROVIDER_PACKAGES; +import static org.glassfish.jersey.server.ServerProperties.PROVIDER_SCANNING_RECURSIVE; + +/** + * @since 5.6.0 + */ +public final class ResourceConfig extends org.glassfish.jersey.server.ResourceConfig { + + public ResourceConfig() { + // Only scan the v2 package for providers, register everything else manually. + // This gives us more flexibility to pick-and-choose, and potentially configure + // specific features that do not necessarily overlap with v1. + property(PROVIDER_PACKAGES, getClass().getPackageName()); + property(PROVIDER_SCANNING_RECURSIVE, true); + + register(ApiFilter.class); + register(AuthenticationFeature.class); + register(AuthorizationFeature.class); + register(GZipInterceptor.class); + register(HeaderFilter.class); + register(JacksonFeature.withoutExceptionMappers()); + register(JerseyMetricsFeature.class); + register(MultiPartFeature.class); + register(RequestIdFilter.class); + register(RequestMdcEnrichmentFilter.class); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java new file mode 100644 index 0000000000..c4746e3aa9 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java @@ -0,0 +1,219 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import alpine.model.Permission; +import alpine.model.Team; +import alpine.model.User; +import alpine.server.auth.PermissionRequired; +import org.dependencytrack.api.v2.TeamsApi; +import org.dependencytrack.api.v2.model.CreateTeamMembershipRequest; +import org.dependencytrack.api.v2.model.CreateTeamRequest; +import org.dependencytrack.api.v2.model.GetTeamResponse; +import org.dependencytrack.api.v2.model.ListTeamMembershipsResponse; +import org.dependencytrack.api.v2.model.ListTeamMembershipsResponseItem; +import org.dependencytrack.api.v2.model.ListTeamsResponse; +import org.dependencytrack.api.v2.model.ListTeamsResponseItem; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.TeamDao; +import org.dependencytrack.persistence.jdbi.TeamDao.ListTeamMembershipsRow; +import org.dependencytrack.persistence.jdbi.TeamDao.ListTeamsRow; +import org.dependencytrack.persistence.pagination.Page; +import org.owasp.security.logging.SecurityMarkers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.ext.Provider; +import java.util.List; + +import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction; +import static org.dependencytrack.persistence.pagination.PageUtil.createPaginationMetadata; +import static org.dependencytrack.util.PersistenceUtil.isUniqueConstraintViolation; + +@Provider +public class TeamsResource implements TeamsApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(TeamsResource.class); + + @Context + private UriInfo uriInfo; + + @Override + @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) + public Response listTeams(final Integer limit, final String pageToken) { + final Page teamsPage = inJdbiTransaction( + handle -> handle.attach(TeamDao.class).listTeams(limit, pageToken)); + + final var response = ListTeamsResponse.builder() + .teams(teamsPage.items().stream() + .map( + teamRow -> ListTeamsResponseItem.builder() + .name(teamRow.name()) + .apiKeys(teamRow.apiKeys()) + .members(teamRow.members()) + .build()) + .toList()) + .pagination(createPaginationMetadata(uriInfo, teamsPage)) + .build(); + + return Response.ok(response).build(); + } + + @Override + @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) + public Response getTeam(final String name) { + try (final var qm = new QueryManager()) { + final Team team = qm.getTeam(name); + if (team == null) { + throw new NotFoundException(); + } + + final var response = GetTeamResponse.builder() + .name(name) + .permissions( + team.getPermissions().stream() + .map(Permission::getName) + .sorted() + .toList()) + .build(); + + return Response.ok(response).build(); + } + } + + @Override + @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) + public Response createTeam(final CreateTeamRequest request) { + try (final var qm = new QueryManager()) { + qm.runInTransaction(() -> { + final List permissions = + qm.getPermissionsByName(request.getPermissions()); + + final var team = new Team(); + team.setName(request.getName()); + team.setPermissions(permissions); + qm.persist(team); + }); + } catch (RuntimeException e) { + if (isUniqueConstraintViolation(e)) { + throw new ClientErrorException(Response.Status.CONFLICT); + } + + throw e; + } + + LOGGER.info( + SecurityMarkers.SECURITY_AUDIT, + "Team created: {}", request.getName()); + return Response + .created(uriInfo.getBaseUriBuilder() + .path("/teams") + .path(request.getName()) + .build()) + .build(); + } + + @Override + @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) + public Response deleteTeam(final String name) { + final List deletedTeamNames = inJdbiTransaction( + handle -> handle.attach(TeamDao.class).deleteTeamsByName(List.of(name))); + if (deletedTeamNames.isEmpty()) { + throw new NotFoundException(); + } + + LOGGER.info(SecurityMarkers.SECURITY_AUDIT, "Team deleted: {}", name); + return Response.noContent().build(); + } + + @Override + @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) + public Response listTeamMemberships(final String team, final String user, final Integer limit, final String pageToken) { + final Page membershipsPage = inJdbiTransaction( + handle -> handle.attach(TeamDao.class).listTeamMembers(team, user, limit, pageToken)); + + final var response = ListTeamMembershipsResponse.builder() + .memberships(membershipsPage.items().stream() + .map( + membershipRow -> ListTeamMembershipsResponseItem.builder() + .teamName(membershipRow.teamName()) + .username(membershipRow.username()) + .build()) + .toList()) + .pagination(createPaginationMetadata(uriInfo, membershipsPage)) + .build(); + + return Response.ok(response).build(); + } + + @Override + @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) + public Response createTeamMembership(final CreateTeamMembershipRequest request) { + try (final var qm = new QueryManager()) { + qm.runInTransaction(() -> { + final Team team = qm.getTeam(request.getTeamName()); + if (team == null) { + throw new NotFoundException(); + } + + final User user = qm.getUser(request.getUsername()); + if (user == null) { + throw new NotFoundException(); + } + + team.getUsers().add(user); + user.getTeams().add(team); + }); + } catch (RuntimeException e) { + if (isUniqueConstraintViolation(e)) { + throw new ClientErrorException(Response.Status.CONFLICT); + } + + throw e; + } + + LOGGER.info( + SecurityMarkers.SECURITY_AUDIT, + "Team membership created: team={}, user={}", + request.getTeamName(), + request.getUsername()); + return Response.created(null).build(); + } + + @Override + @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) + public Response deleteTeamMembership(final String team, final String user) { + final boolean deleted = inJdbiTransaction( + handle -> handle.attach(TeamDao.class).deleteTeamMembership(team, user)); + if (!deleted) { + throw new NotFoundException(); + } + + LOGGER.info( + SecurityMarkers.SECURITY_AUDIT, + "Team membership deleted: team={}, user={}", team, user); + return Response.noContent().build(); + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/WorkflowsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/WorkflowsResource.java new file mode 100644 index 0000000000..4c730f027e --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/WorkflowsResource.java @@ -0,0 +1,70 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import alpine.server.auth.PermissionRequired; +import org.dependencytrack.api.v2.WorkflowsApi; +import org.dependencytrack.api.v2.model.ListWorkflowStatesResponse; +import org.dependencytrack.api.v2.model.ListWorkflowStatesResponseItem; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.WorkflowState; +import org.dependencytrack.persistence.QueryManager; + +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Provider +public class WorkflowsResource implements WorkflowsApi { + + @Override + @PermissionRequired(Permissions.Constants.BOM_UPLOAD) + public Response getWorkflowStates(final UUID token) { + List workflowStates; + try (final var qm = new QueryManager()) { + workflowStates = qm.getAllWorkflowStatesForAToken(token); + if (workflowStates.isEmpty()) { + throw new NotFoundException(); + } + } + List states = workflowStates.stream() + .map(this::mapWorkflowStateResponse) + .collect(Collectors.toList()); + return Response.ok(ListWorkflowStatesResponse.builder().states(states).build()).build(); + } + + private ListWorkflowStatesResponseItem mapWorkflowStateResponse(WorkflowState workflowState) { + var mappedState = ListWorkflowStatesResponseItem.builder() + .token(workflowState.getToken()) + .status(ListWorkflowStatesResponseItem.StatusEnum.fromString(workflowState.getStatus().name())) + .step(ListWorkflowStatesResponseItem.StepEnum.fromString(workflowState.getStep().name())) + .failureReason(workflowState.getFailureReason()) + .build(); + if (workflowState.getStartedAt() != null) { + mappedState.setStartedAt(workflowState.getStartedAt().getTime()); + } + if (workflowState.getUpdatedAt() != null) { + mappedState.setUpdatedAt(workflowState.getUpdatedAt().getTime()); + } + return mappedState; + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ClientErrorExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ClientErrorExceptionMapper.java new file mode 100644 index 0000000000..b01a155a64 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ClientErrorExceptionMapper.java @@ -0,0 +1,50 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import org.dependencytrack.api.v2.model.ProblemDetails; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.ext.Provider; +import java.util.Map; + +/** + * @since 5.6.0 + */ +@Provider +public final class ClientErrorExceptionMapper extends ProblemDetailsExceptionMapper { + + private static final Map DETAIL_BY_STATUS = Map.ofEntries( + Map.entry(401, "Not authorized to access the requested resource."), + Map.entry(403, "Not permitted to access the requested resource."), + Map.entry(404, "The requested resource could not be found."), + Map.entry(409, "The resource already exists.")); + + @Override + public ProblemDetails map(final ClientErrorException exception) { + return ProblemDetails.builder() + .status(exception.getResponse().getStatus()) + .title(exception.getResponse().getStatusInfo().getReasonPhrase()) + .detail(DETAIL_BY_STATUS.getOrDefault( + exception.getResponse().getStatus(), + exception.getMessage())) + .build(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapper.java new file mode 100644 index 0000000000..a9313b19b3 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapper.java @@ -0,0 +1,62 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import org.dependencytrack.api.v2.model.ConstraintViolationError; +import org.dependencytrack.api.v2.model.InvalidRequestProblemDetails; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; +import java.util.ArrayList; +import java.util.Set; + +/** + * @since 5.6.0 + */ +@Provider +public final class ConstraintViolationExceptionMapper extends ProblemDetailsExceptionMapper { + + @Override + public InvalidRequestProblemDetails map(final ConstraintViolationException exception) { + final Set> violations = exception.getConstraintViolations(); + + final var errors = new ArrayList(violations.size()); + + for (final ConstraintViolation violation : violations) { + errors.add( + ConstraintViolationError.builder() + .path(violation.getPropertyPath().toString()) + .value(violation.getInvalidValue() != null + ? violation.getInvalidValue().toString() + : null) + .message(violation.getMessage()) + .build()); + } + + return InvalidRequestProblemDetails.builder() + .status(Response.Status.BAD_REQUEST.getStatusCode()) + .title("Bad Request") + .detail("The request could not be processed because it failed validation.") + .errors(errors) + .build(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapper.java new file mode 100644 index 0000000000..01f372f4c1 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapper.java @@ -0,0 +1,30 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import org.dependencytrack.api.v2.model.ProblemDetails; + +import jakarta.ws.rs.ext.Provider; + +/** + * @since 5.6.0 + */ +@Provider +public final class DefaultExceptionMapper extends LoggingProblemDetailsExceptionMapper { +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapper.java new file mode 100644 index 0000000000..95698148f0 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapper.java @@ -0,0 +1,48 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import org.dependencytrack.api.v2.model.ProblemDetails; + +import jakarta.ws.rs.ext.Provider; + +/** + * @since 5.6.0 + */ +@Provider +public class JsonProcessingExceptionMapper extends LoggingProblemDetailsExceptionMapper { + + @Override + ProblemDetails map(final JsonProcessingException exception) { + if (exception instanceof InvalidDefinitionException + || exception instanceof JsonGenerationException) { + return super.map(exception); + } + + return ProblemDetails.builder() + .status(400) + .title("JSON Processing Failed") + .detail("The provided JSON could not be processed.") + .build(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/LoggingProblemDetailsExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/LoggingProblemDetailsExceptionMapper.java new file mode 100644 index 0000000000..4b1dfc08f9 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/LoggingProblemDetailsExceptionMapper.java @@ -0,0 +1,50 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import org.dependencytrack.api.v2.model.ProblemDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.ServerErrorException; + +/** + * @since 5.6.0 + */ +abstract class LoggingProblemDetailsExceptionMapper extends ProblemDetailsExceptionMapper { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + @SuppressWarnings("unchecked") + P map(final E exception) { + logger.error("Uncaught exception occurred during request processing", exception); + + final int status = exception instanceof final ServerErrorException see + ? see.getResponse().getStatus() + : 500; + + return (P) ProblemDetails.builder() + .status(status) + .title("Unexpected error") + .detail("An error occurred that was not anticipated.") + .build(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ProblemDetailsExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ProblemDetailsExceptionMapper.java new file mode 100644 index 0000000000..4912108c77 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ProblemDetailsExceptionMapper.java @@ -0,0 +1,44 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import org.dependencytrack.api.v2.model.ProblemDetails; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; + +/** + * @since 5.6.0 + */ +abstract class ProblemDetailsExceptionMapper implements ExceptionMapper { + + abstract P map(E exception); + + @Override + public Response toResponse(final E exception) { + final P problemDetails = map(exception); + + return Response + .status(problemDetails.getStatus()) + .header("Content-Type", "application/problem+json") + .entity(problemDetails) + .build(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java b/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java new file mode 100644 index 0000000000..ebd3dc067f --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java @@ -0,0 +1,48 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.security; + +import alpine.security.crypto.KeyManager; +import org.dependencytrack.init.InitTask; +import org.dependencytrack.init.InitTaskContext; + +/** + * @since 5.6.0 + */ +public class KeyGenerationInitTask implements InitTask { + + @Override + public int priority() { + return PRIORITY_HIGHEST - 5; + } + + @Override + public String name() { + return "key.generation"; + } + + @Override + public void execute(final InitTaskContext ctx) throws Exception { + // Force initialization of KeyManager, which will cause + // the secret, as well as the public-private key pair + // to be generated if necessary. + final var ignored = KeyManager.getInstance(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java index cc55e95573..1b5ab052ea 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java @@ -71,8 +71,7 @@ public void inform(final Event event) { private record Statistics( Duration retentionDuration, int deletedComponentMetrics, - int deletedProjectMetrics, - int deletedPortfolioMetrics) { + int deletedProjectMetrics) { } private Statistics informLocked(final Handle jdbiHandle) { @@ -97,8 +96,7 @@ private Statistics informLocked(final Handle jdbiHandle) { final int numDeletedComponent = metricsDao.deleteComponentMetricsForRetentionDuration(retentionDuration); final int numDeletedProject = metricsDao.deleteProjectMetricsForRetentionDuration(retentionDuration); - final int numDeletedPortfolio = metricsDao.deletePortfolioMetricsForRetentionDuration(retentionDuration); - return new Statistics(retentionDuration, numDeletedComponent, numDeletedProject, numDeletedPortfolio); + return new Statistics(retentionDuration, numDeletedComponent, numDeletedProject); } } diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java index e0f09c5696..4515ea3cd9 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java @@ -28,12 +28,8 @@ import org.dependencytrack.event.CallbackEvent; import org.dependencytrack.event.PortfolioMetricsUpdateEvent; import org.dependencytrack.event.ProjectMetricsUpdateEvent; -import org.dependencytrack.metrics.Metrics; -import org.dependencytrack.model.Project; -import org.dependencytrack.persistence.QueryManager; +import org.jdbi.v3.core.mapper.reflect.ConstructorMapper; -import javax.jdo.PersistenceManager; -import javax.jdo.Query; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -42,6 +38,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; import static org.dependencytrack.util.LockProvider.executeWithLock; import static org.dependencytrack.util.LockProvider.isTaskLockToBeExtended; import static org.dependencytrack.util.TaskUtil.getLockConfigForTask; @@ -63,7 +60,7 @@ public void inform(final Event e) { try { executeWithLock( getLockConfigForTask(PortfolioMetricsUpdateTask.class), - (LockingTaskExecutor.Task)() -> updateMetrics(event.isForceRefresh())); + (LockingTaskExecutor.Task) () -> updateMetrics(event.isForceRefresh())); } catch (Throwable ex) { LOGGER.error("Error in acquiring lock and executing portfolio metrics task", ex); } @@ -79,88 +76,97 @@ private void updateMetrics(final boolean forceRefresh) throws Exception { LOGGER.info("Refreshing project metrics"); refreshProjectMetrics(); } - - Metrics.updatePortfolioMetrics(); } finally { LOGGER.info("Completed portfolio metrics update in " + Duration.ofNanos(System.nanoTime() - startTimeNs)); } } private static void refreshProjectMetrics() throws Exception { - try (final var qm = new QueryManager()) { - final PersistenceManager pm = qm.getPersistenceManager(); - - LOGGER.debug("Fetching first " + BATCH_SIZE + " projects"); - LockConfiguration portfolioMetricsTaskConfig = getLockConfigForTask(PortfolioMetricsUpdateTask.class); - List activeProjects = fetchNextActiveProjectsPage(pm, null); - long processStartTime = System.currentTimeMillis(); - while (!activeProjects.isEmpty()) { - long startTimeOfBatch = System.currentTimeMillis(); - final long firstId = activeProjects.get(0).id(); - final long lastId = activeProjects.get(activeProjects.size() - 1).id(); - - // Distribute the batch across at most MAX_CONCURRENCY events, and process them asynchronously. - final List> partitions = partition(activeProjects, MAX_CONCURRENCY); - final var countDownLatch = new CountDownLatch(partitions.size()); - - for (final List partition : partitions) { - final var partitionEvent = new CallbackEvent(() -> { - for (final ProjectProjection project : partition) { - new ProjectMetricsUpdateTask().inform(new ProjectMetricsUpdateEvent(project.uuid())); - } - }); - - final var countDownEvent = new CallbackEvent(countDownLatch::countDown); - Event.dispatch(partitionEvent - .onSuccess(countDownEvent) - .onFailure(countDownEvent)); - } - - LOGGER.debug("Waiting for metrics updates for projects " + firstId + "-" + lastId + " to complete"); - if (!countDownLatch.await(15, TimeUnit.MINUTES)) { - // Depending on the system load, it may take a while for the queued events - // to be processed. And depending on how large the projects are, it may take a - // while for the processing of the respective event to complete. - // It is unlikely though that either of these situations causes a block for - // over 15 minutes. If that happens, the system is under-resourced. - LOGGER.warn("Updating metrics for projects " + firstId + "-" + lastId + + LOGGER.debug("Fetching first " + BATCH_SIZE + " projects"); + LockConfiguration portfolioMetricsTaskConfig = getLockConfigForTask(PortfolioMetricsUpdateTask.class); + List activeProjects = fetchNextActiveProjectsPage(/* lastId */ null); + long processStartTime = System.currentTimeMillis(); + while (!activeProjects.isEmpty()) { + long startTimeOfBatch = System.currentTimeMillis(); + final long firstId = activeProjects.getFirst().id(); + final long lastId = activeProjects.getLast().id(); + + // Distribute the batch across at most MAX_CONCURRENCY events, and process them asynchronously. + final List> partitions = partition(activeProjects, MAX_CONCURRENCY); + final var countDownLatch = new CountDownLatch(partitions.size()); + + for (final List partition : partitions) { + final var partitionEvent = new CallbackEvent(() -> { + for (final ProjectProjection project : partition) { + new ProjectMetricsUpdateTask().inform(new ProjectMetricsUpdateEvent(project.uuid())); + } + }); + + final var countDownEvent = new CallbackEvent(countDownLatch::countDown); + Event.dispatch(partitionEvent + .onSuccess(countDownEvent) + .onFailure(countDownEvent)); + } + + LOGGER.debug("Waiting for metrics updates for projects " + firstId + "-" + lastId + " to complete"); + if (!countDownLatch.await(15, TimeUnit.MINUTES)) { + // Depending on the system load, it may take a while for the queued events + // to be processed. And depending on how large the projects are, it may take a + // while for the processing of the respective event to complete. + // It is unlikely though that either of these situations causes a block for + // over 15 minutes. If that happens, the system is under-resourced. + LOGGER.warn("Updating metrics for projects " + firstId + "-" + lastId + " took longer than expected (15m); Proceeding with potentially stale data"); - } - LOGGER.debug("Completed metrics updates for projects " + firstId + "-" + lastId); - LOGGER.debug("Fetching next " + BATCH_SIZE + " projects"); - long now = System.currentTimeMillis(); - long processDurationInMillis = now - startTimeOfBatch; - long cumulativeDurationInMillis = now - processStartTime; - //extend the lock for the duration of process - //initial duration of portfolio metrics can be set to 20min. - //No thread calculating metrics would be executing for more than 15min. - //lock can only be extended if lock until is held for time after current db time - if(isTaskLockToBeExtended(cumulativeDurationInMillis, PortfolioMetricsUpdateTask.class)) { - Duration extendLockByDuration = Duration.ofMillis(processDurationInMillis).plus(portfolioMetricsTaskConfig.getLockAtLeastFor()); - LOGGER.debug("Extending lock duration by ms: " + extendLockByDuration); - LockExtender.extendActiveLock(extendLockByDuration, portfolioMetricsTaskConfig.getLockAtLeastFor()); - } - activeProjects = fetchNextActiveProjectsPage(pm, lastId); } + LOGGER.debug("Completed metrics updates for projects " + firstId + "-" + lastId); + LOGGER.debug("Fetching next " + BATCH_SIZE + " projects"); + long now = System.currentTimeMillis(); + long processDurationInMillis = now - startTimeOfBatch; + long cumulativeDurationInMillis = now - processStartTime; + //extend the lock for the duration of process + //initial duration of portfolio metrics can be set to 20min. + //No thread calculating metrics would be executing for more than 15min. + //lock can only be extended if lock until is held for time after current db time + if (isTaskLockToBeExtended(cumulativeDurationInMillis, PortfolioMetricsUpdateTask.class)) { + Duration extendLockByDuration = Duration.ofMillis(processDurationInMillis).plus(portfolioMetricsTaskConfig.getLockAtLeastFor()); + LOGGER.debug("Extending lock duration by ms: " + extendLockByDuration); + LockExtender.extendActiveLock(extendLockByDuration, portfolioMetricsTaskConfig.getLockAtLeastFor()); + } + activeProjects = fetchNextActiveProjectsPage(lastId); } } public record ProjectProjection(long id, UUID uuid) { } - private static List fetchNextActiveProjectsPage(final PersistenceManager pm, final Long lastId) throws Exception { - try (final Query query = pm.newQuery(Project.class)) { - if (lastId == null) { - query.setFilter("inactiveSince == null"); - } else { - query.setFilter("inactiveSince == null && id < :lastId"); - query.setParameters(lastId); - } - query.setOrdering("id DESC"); - query.range(0, BATCH_SIZE); - query.setResult("id, uuid"); - return List.copyOf(query.executeResultList(ProjectProjection.class)); - } + private static List fetchNextActiveProjectsPage(final Long lastId) { + return withJdbiHandle(handle -> { + final org.jdbi.v3.core.statement.Query query = handle.createQuery(""" + SELECT "ID" + , "UUID" + FROM "PROJECT" + WHERE "INACTIVE_SINCE" IS NULL + AND NOT EXISTS( + SELECT 1 + FROM "PROJECTMETRICS" + WHERE "PROJECT_ID" = "PROJECT"."ID" + AND "LAST_OCCURRENCE" >= CURRENT_DATE + AND "LAST_OCCURRENCE" < CURRENT_DATE + INTERVAL '1 day' + ) + <#if lastId> + AND "ID" > :lastId + + ORDER BY "ID" + LIMIT :batchSize + """); + + return query + .bind("lastId", lastId) + .bind("batchSize", BATCH_SIZE) + .defineNamedBindings() + .map(ConstructorMapper.of(ProjectProjection.class)) + .list(); + }); } static List> partition(final List list, int numPartitions) { diff --git a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java b/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java index 6d5bd22c2d..ee5c4d7731 100644 --- a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -18,18 +18,14 @@ */ package org.dependencytrack.util; -import alpine.model.ConfigProperty; import alpine.notification.Notification; import alpine.notification.NotificationLevel; -import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Bom; import org.dependencytrack.model.Component; -import org.dependencytrack.model.ConfigPropertyConstants; -import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.Policy; import org.dependencytrack.model.PolicyCondition; import org.dependencytrack.model.PolicyViolation; @@ -44,7 +40,6 @@ import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; -import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.notification.vo.AnalysisDecisionChange; import org.dependencytrack.notification.vo.BomConsumedOrProcessed; import org.dependencytrack.notification.vo.BomProcessingFailed; @@ -58,10 +53,6 @@ import javax.jdo.FetchPlan; import javax.jdo.Query; -import java.io.File; -import java.io.IOException; -import java.net.URLDecoder; -import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -72,8 +63,6 @@ import java.util.UUID; import java.util.stream.Collectors; -import static java.nio.charset.StandardCharsets.UTF_8; - public final class NotificationUtil { /** @@ -319,39 +308,6 @@ public static void analyzeNotificationCriteria(final QueryManager qm, final Long .subject(new PolicyViolationIdentified(violation, component, project))); } - public static void loadDefaultNotificationPublishers(QueryManager qm) throws IOException { - for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) { - File templateFile = new File(URLDecoder.decode(NotificationUtil.class.getResource(publisher.getPublisherTemplateFile()).getFile(), UTF_8.name())); - if (qm.isEnabled(ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED)) { - ConfigProperty templateBaseDir = qm.getConfigProperty( - ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR.getGroupName(), - ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR.getPropertyName() - ); - File userProvidedTemplateFile = new File(Path.of(templateBaseDir.getPropertyValue(), publisher.getPublisherTemplateFile()).toUri()); - if (userProvidedTemplateFile.exists()) { - templateFile = userProvidedTemplateFile; - } - } - final String templateContent = FileUtils.readFileToString(templateFile, UTF_8); - final NotificationPublisher existingPublisher = qm.getDefaultNotificationPublisherByName(publisher.getPublisherName()); - if (existingPublisher == null) { - qm.createNotificationPublisher( - publisher.getPublisherName(), publisher.getPublisherDescription(), - publisher.getPublisherClass().name(), templateContent, publisher.getTemplateMimeType(), - publisher.isDefaultPublisher() - ); - } else { - existingPublisher.setName(publisher.getPublisherName()); - existingPublisher.setDescription(publisher.getPublisherDescription()); - existingPublisher.setPublisherClass(publisher.getPublisherClass().name()); - existingPublisher.setTemplate(templateContent); - existingPublisher.setTemplateMimeType(publisher.getTemplateMimeType()); - existingPublisher.setDefaultPublisher(publisher.isDefaultPublisher()); - qm.updateNotificationPublisher(existingPublisher); - } - } - } - public static String generateNotificationContent(final org.dependencytrack.proto.notification.v1.Vulnerability vulnerability) { final String content; if (vulnerability.hasDescription()) { diff --git a/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask b/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask new file mode 100644 index 0000000000..c7826402b6 --- /dev/null +++ b/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask @@ -0,0 +1,4 @@ +org.dependencytrack.persistence.DatabaseMigrationInitTask +org.dependencytrack.persistence.DatabasePartitionMaintenanceInitTask +org.dependencytrack.persistence.DatabaseSeedingInitTask +org.dependencytrack.security.KeyGenerationInitTask \ No newline at end of file diff --git a/apiserver/src/main/resources/application.properties b/apiserver/src/main/resources/application.properties index d243a21c1f..79cca2a77a 100644 --- a/apiserver/src/main/resources/application.properties +++ b/apiserver/src/main/resources/application.properties @@ -156,27 +156,7 @@ alpine.datanucleus.executioncontext.maxidle=0 # @hidden alpine.datanucleus.deletionpolicy=DataNucleus -# Defines whether database migrations should be executed on startup. -#

-# From v5.6.0 onwards, migrations are considered part of the initialization tasks. -# Setting init.tasks.enabled to `false` will disable migrations, -# even if database.run.migrations is enabled. -# -# @category: Database -# @type: boolean -database.run.migrations=true - -# Defines whether the application should exit upon successful execution of database migrations. -# Enabling this option makes the application suitable for running as k8s init container. -# Has no effect unless database.run.migrations is `true`. -#

-# From v5.6.0 onwards, usage of init.and.exit should be preferred. -# -# @category: Database -# @type: boolean -# database.run.migrations.only=false - -# Defines the database JDBC URL to use when executing migrations. +# Defines the database JDBC URL to use when executing init tasks. # If not set, the value of alpine.database.url will be used. # Should generally not be set, unless TLS authentication is used, # and custom connection variables are required. @@ -184,23 +164,23 @@ database.run.migrations=true # @category: Database # @default: ${alpine.database.url} # @type: string -# database.migration.url= +# init.tasks.database.url= -# Defines the database user for executing migrations. +# Defines the database user for executing init tasks. # If not set, the value of alpine.database.username will be used. # # @category: Database # @default: ${alpine.database.username} # @type: string -# database.migration.username= +# init.tasks.database.username= -# Defines the database password for executing migrations. +# Defines the database password for executing init tasks. # If not set, the value of alpine.database.password will be used. # # @category: Database # @default: ${alpine.database.password} # @type: string -# database.migration.password= +# init.tasks.database.password= # Specifies the number of bcrypt rounds to use when hashing a user's password. # The higher the number the more secure the password, at the expense of @@ -1228,16 +1208,46 @@ vulnerability.policy.s3.bundle.name= vulnerability.policy.s3.region= # Whether to execute initialization tasks on startup. -# Initialization tasks include: -#

    -#
  • Execution of database migrations
  • -#
  • Populating the database with default objects (permissions, users, licenses, etc.)
  • -#
# # @category: General # @type: boolean init.tasks.enabled=true +# Whether to enable the database migration init task. +# +# Has no effect unless init.tasks.enabled is `true`. +# +# @category: General +# @type: boolean +init.task.database.migration.enabled=true + +# Whether to enable the database partition maintenance init task. +# +# Has no effect unless init.tasks.enabled is `true`. +# +# @category: General +# @type: boolean +init.task.database.partition.maintenance.enabled=true + +# Whether to enable the database seeding init task. +# +# Seeding involves populating the database with default objects, +# such as permissions, users, licenses, etc. +# +# Has no effect unless init.tasks.enabled is `true`. +# +# @category: General +# @type: boolean +init.task.database.seeding.enabled=true + +# Whether to enable the key generation init task. +# +# Has no effect unless init.tasks.enabled is `true`. +# +# @category: General +# @type: boolean +init.task.key.generation.enabled=true + # Whether to only execute initialization tasks and exit. # # @category: General @@ -1277,7 +1287,7 @@ dev.services.image.frontend=ghcr.io/dependencytrack/hyades-frontend:snapshot # # @category: Development # @type: string -dev.services.image.kafka=apache/kafka-native:3.9.0 +dev.services.image.kafka=apache/kafka-native:3.9.1 # The image to use for the PostgreSQL dev services container. # @@ -1338,7 +1348,6 @@ task.component.metadata.maintenance.lock.min.duration=PT1M #
    #
  • DEPENDENCYMETRICS
  • #
  • PROJECTMETRICS
  • -#
  • PORTFOLIOMETRICS
  • #
# # @category: Task Scheduling diff --git a/apiserver/src/main/webapp/WEB-INF/web.xml b/apiserver/src/main/webapp/WEB-INF/web.xml index 645caec27c..ed671bd089 100644 --- a/apiserver/src/main/webapp/WEB-INF/web.xml +++ b/apiserver/src/main/webapp/WEB-INF/web.xml @@ -30,10 +30,7 @@ alpine.server.metrics.MetricsInitializer - org.dependencytrack.common.KeyManagerInitializer - - - org.dependencytrack.persistence.MigrationInitializer + org.dependencytrack.init.InitTaskServletContextListener alpine.server.persistence.PersistenceManagerFactory @@ -44,9 +41,6 @@ org.dependencytrack.health.HealthCheckInitializer - - org.dependencytrack.persistence.DefaultObjectGenerator - org.dependencytrack.event.kafka.KafkaProducerInitializer @@ -116,7 +110,12 @@ alpine.server.AlpineServlet jersey.config.server.provider.packages - alpine.server.filters,alpine.server.resources,org.dependencytrack.resources,org.dependencytrack.filters + + alpine.server.filters, + alpine.server.resources, + org.dependencytrack.filters, + org.dependencytrack.resources.v1 + jersey.config.server.provider.classnames @@ -132,6 +131,19 @@ DependencyTrack /api/* + + + REST-API-v2 + org.glassfish.jersey.servlet.ServletContainer + + jakarta.ws.rs.Application + org.dependencytrack.resources.v2.ResourceConfig + + + + REST-API-v2 + /api/v2/* + Health diff --git a/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java b/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java index 8bffa30f48..0dd53ea86b 100644 --- a/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java +++ b/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java @@ -18,7 +18,9 @@ */ package org.dependencytrack; -import org.dependencytrack.resources.v1.exception.ClientErrorExceptionMapper; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider; import org.glassfish.jersey.server.ResourceConfig; @@ -32,6 +34,9 @@ import org.junit.rules.ExternalResource; import jakarta.ws.rs.client.WebTarget; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.List; /** * @since 4.11.0 @@ -41,6 +46,7 @@ public class JerseyTestRule extends ExternalResource { private final JerseyTest jerseyTest; public JerseyTestRule(final ResourceConfig resourceConfig) { + final boolean isV2 = isV2(resourceConfig); this.jerseyTest = new JerseyTest() { @Override @@ -54,14 +60,25 @@ protected void configureClient(final ClientConfig config) { // using the default HttpUrlConnection connector provider. // See https://github.com/eclipse-ee4j/jersey/issues/4825 config.connectorProvider(new GrizzlyConnectorProvider()); + + if (isV2) { + config.register(OpenApiValidationClientResponseFilter.class); + } } @Override protected DeploymentContext configureDeployment() { forceSet(TestProperties.CONTAINER_PORT, "0"); - return ServletDeploymentContext.forServlet(new ServletContainer( - // Ensure exception mappers are registered. - resourceConfig.packages(ClientErrorExceptionMapper.class.getPackageName()))).build(); + + // Ensure exception mappers are registered. + if (isV2) { + resourceConfig.packages("org.dependencytrack.resources.v2.exception"); + } else { + resourceConfig.packages("org.dependencytrack.resources.v1.exception"); + } + + return ServletDeploymentContext.forServlet( + new ServletContainer(resourceConfig)).build(); } }; @@ -89,4 +106,28 @@ public final WebTarget target(final String path) { return jerseyTest.target(path); } + public final WebTarget target(final URI uri) { + WebTarget target = jerseyTest.target(uri.getPath()); + + if (uri.getQuery() != null) { + final List uriQueryParams = + URLEncodedUtils.parse(uri, StandardCharsets.UTF_8); + for (final NameValuePair queryParam : uriQueryParams) { + target = target.queryParam(queryParam.getName(), queryParam.getValue()); + } + } + + return target; + } + + private boolean isV2(final ResourceConfig resourceConfig) { + for (final Class clazz : resourceConfig.getClasses()) { + if (clazz.getPackageName().startsWith("org.dependencytrack.resources.v2")) { + return true; + } + } + + return false; + } + } \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java b/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java index 421a53cd20..3a87619323 100644 --- a/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java +++ b/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java @@ -126,6 +126,25 @@ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = CURRENT_SCHEMA()) L END LOOP; END $$; """); + + statement.execute(""" + DO $$ + DECLARE + partition_name TEXT; + today_partition_pattern TEXT := format('^(PROJECT|DEPENDENCY)METRICS_%s', TO_CHAR(CURRENT_DATE, 'YYYYMMDD')); + tomorrow_partition_pattern TEXT := format('^(PROJECT|DEPENDENCY)METRICS_%s', TO_CHAR(CURRENT_DATE + 1, 'YYYYMMDD')); + BEGIN + FOR partition_name IN + SELECT tablename + FROM pg_tables + WHERE tablename ~ '^(PROJECT|DEPENDENCY)METRICS_[0-9]{8}$' + AND tablename !~ today_partition_pattern + AND tablename !~ tomorrow_partition_pattern + LOOP + EXECUTE format('DROP TABLE "%s"', partition_name); + END LOOP; + END $$; + """); } } diff --git a/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java b/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java index 9341f3dde9..862b2465e6 100644 --- a/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java +++ b/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java @@ -58,11 +58,7 @@ public class ProcessorManagerTest { public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); @Rule - public KafkaContainer kafkaContainer = new KafkaContainer("apache/kafka-native:3.9.0") - // TODO: Remove this when Kafka >= 3.9.1 is available. - // * https://github.com/testcontainers/testcontainers-java/issues/9506#issuecomment-2463504967 - // * https://issues.apache.org/jira/browse/KAFKA-18281 - .withEnv("KAFKA_LISTENERS", "PLAINTEXT://:9092,BROKER://:9093,CONTROLLER://:9094"); + public KafkaContainer kafkaContainer = new KafkaContainer("apache/kafka-native:3.9.1"); private AdminClient adminClient; private Producer producer; diff --git a/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java b/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java new file mode 100644 index 0000000000..0ac943fae0 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java @@ -0,0 +1,137 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.init; + +import alpine.Config; +import org.dependencytrack.PersistenceCapableTest; +import org.junit.Before; +import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +public class InitTaskExecutorTest extends PersistenceCapableTest { + + private Config configMock; + private PGSimpleDataSource dataSource; + + @Before + public void before() throws Exception { + super.before(); + + configMock = mock(Config.class); + + dataSource = new PGSimpleDataSource(); + dataSource.setUrl(postgresContainer.getJdbcUrl()); + dataSource.setUser(postgresContainer.getUsername()); + dataSource.setPassword(postgresContainer.getPassword()); + } + + @Test + public void shouldExecuteTasksInPriorityOrder() { + final var executedTaskNames = new ArrayList(3); + + final var executor = new InitTaskExecutor(configMock, dataSource, List.of( + new TestInitTask(1, "a", () -> executedTaskNames.add("a")), + new TestInitTask(5, "b", () -> executedTaskNames.add("b")), + new TestInitTask(3, "c", () -> executedTaskNames.add("c")))); + executor.execute(); + + assertThat(executedTaskNames).containsExactly("b", "c", "a"); + } + + @Test + public void shouldThrowWhenTaskExecutionFails() { + final var executor = new InitTaskExecutor(configMock, dataSource, List.of( + new TestInitTask(1, "test", () -> { + throw new IllegalStateException("boom"); + }))); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(executor::execute) + .withMessage("Failed to execute init task test") + .withCauseInstanceOf(IllegalStateException.class); + } + + @Test + public void shouldThrowOnDuplicateTaskName() { + final var executor = new InitTaskExecutor(configMock, dataSource, List.of( + new TestInitTask(1, "test"), + new TestInitTask(2, "test"))); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(executor::execute) + // NB: In a real-world scenario, the class names would be different. + .withMessage(""" + Duplicate task name test: Registered by \ + org.dependencytrack.init.InitTaskExecutorTest$TestInitTask and \ + org.dependencytrack.init.InitTaskExecutorTest$TestInitTask"""); + } + + @Test + public void shouldThrowOnInvalidTaskPriority() { + final var executor = new InitTaskExecutor(configMock, dataSource, List.of( + new TestInitTask(-1, "test"))); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(executor::execute) + .withMessage("Invalid priority of task test: Must be within [0..100] but is -1"); + } + + private static final class TestInitTask implements InitTask { + + private final int priority; + private final String name; + private final Runnable runnable; + + private TestInitTask(final int priority, final String name) { + this(priority, name, null); + } + + private TestInitTask(final int priority, final String name, final Runnable runnable) { + this.priority = priority; + this.name = name; + this.runnable = runnable; + } + + @Override + public int priority() { + return priority; + } + + @Override + public String name() { + return name; + } + + @Override + public void execute(final InitTaskContext ctx) { + if (runnable != null) { + runnable.run(); + } + } + + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java b/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java new file mode 100644 index 0000000000..aab5ea215a --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java @@ -0,0 +1,52 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class DefaultRepositoryTest { + + @Test + public void shouldNotHaveDuplicateResolutionOrdersPerType() { + final Map> defaultRepoByType = + Arrays.stream(DefaultRepository.values()).collect( + Collectors.groupingBy( + DefaultRepository::getType, + Collectors.mapping( + DefaultRepository::getResolutionOrder, + Collectors.toList()))); + + final var softAsserts = new SoftAssertions(); + for (final RepositoryType type : RepositoryType.values()) { + final List defaultRepos = defaultRepoByType.get(type); + if (defaultRepos != null) { + softAsserts.assertThat(defaultRepos).doesNotHaveDuplicates(); + } + } + + softAsserts.assertAll(); + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java similarity index 56% rename from apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java rename to apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java index d0636b9d84..526859115a 100644 --- a/apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java +++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java @@ -20,10 +20,12 @@ import alpine.Config; import org.dependencytrack.common.ConfigKey; +import org.dependencytrack.init.InitTaskContext; import org.jdbi.v3.core.Jdbi; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.utility.DockerImageName; @@ -34,9 +36,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class MigrationInitializerTest { +public class DatabaseMigrationInitTaskTest { private PostgreSQLContainer postgresContainer; + private PGSimpleDataSource dataSource; private Jdbi jdbi; @Before @@ -44,11 +47,12 @@ public void setUp() { postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:13-alpine")); postgresContainer.start(); - jdbi = Jdbi.create( - postgresContainer.getJdbcUrl(), - postgresContainer.getUsername(), - postgresContainer.getPassword() - ); + dataSource = new PGSimpleDataSource(); + dataSource.setUrl(postgresContainer.getJdbcUrl()); + dataSource.setUser(postgresContainer.getUsername()); + dataSource.setPassword(postgresContainer.getPassword()); + + jdbi = Jdbi.create(dataSource); } @After @@ -59,67 +63,35 @@ public void tearDown() { } @Test - public void test() { + public void test() throws Exception { final var configMock = mock(Config.class); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl()); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName()); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername()); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword()); when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true); - when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true); - new MigrationInitializer(configMock).contextInitialized(null); + new DatabaseMigrationInitTask().execute(new InitTaskContext(configMock, dataSource)); assertMigrationExecuted(/* expectExecuted */ true); } @Test - public void testWithMigrationCredentials() { + public void testWithMigrationCredentials() throws Exception { final var configMock = mock(Config.class); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl()); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName()); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn("username"); when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn("password"); when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true); - when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true); - when(configMock.getProperty(eq(ConfigKey.DATABASE_MIGRATION_USERNAME))).thenReturn(postgresContainer.getUsername()); - when(configMock.getProperty(eq(ConfigKey.DATABASE_MIGRATION_PASSWORD))).thenReturn(postgresContainer.getPassword()); + when(configMock.getProperty(eq(ConfigKey.INIT_TASKS_DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername()); + when(configMock.getProperty(eq(ConfigKey.INIT_TASKS_DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword()); - new MigrationInitializer(configMock).contextInitialized(null); + new DatabaseMigrationInitTask().execute(new InitTaskContext(configMock, dataSource)); assertMigrationExecuted(/* expectExecuted */ true); } - @Test - public void testWithRunMigrationsDisabled() { - final var configMock = mock(Config.class); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl()); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName()); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername()); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword()); - when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true); - when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(false); - - new MigrationInitializer(configMock).contextInitialized(null); - - assertMigrationExecuted(/* expectExecuted */ false); - } - - @Test - public void testWithInitTasksDisabled() { - final var configMock = mock(Config.class); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl()); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName()); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername()); - when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword()); - when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(false); - when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true); - - new MigrationInitializer(configMock).contextInitialized(null); - - assertMigrationExecuted(/* expectExecuted */ false); - } - private void assertMigrationExecuted(final boolean expectExecuted) { final List tableNames = jdbi.withHandle(handle -> handle.createQuery(""" SELECT "table_name" @@ -136,4 +108,4 @@ private void assertMigrationExecuted(final boolean expectExecuted) { } } -} +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java new file mode 100644 index 0000000000..47fd057387 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java @@ -0,0 +1,59 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence; + +import alpine.Config; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.init.InitTaskContext; +import org.dependencytrack.persistence.jdbi.MetricsDao; +import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; +import static org.mockito.Mockito.mock; + +public class DatabasePartitionMaintenanceInitTaskTest extends PersistenceCapableTest { + + @Test + public void testMetricsPartitionsForTodayAndTomorrow() throws Exception { + final var dataSource = new PGSimpleDataSource(); + dataSource.setUrl(postgresContainer.getJdbcUrl()); + dataSource.setUser(postgresContainer.getUsername()); + dataSource.setPassword(postgresContainer.getPassword()); + + new DatabasePartitionMaintenanceInitTask().execute(new InitTaskContext(mock(Config.class), dataSource)); + + var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE); + + useJdbiHandle(handle -> { + var metricsDao = handle.attach(MetricsDao.class); + assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1); + assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1); + assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1); + assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1); + }); + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java new file mode 100644 index 0000000000..8a7b23edf2 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java @@ -0,0 +1,202 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.persistence; + +import alpine.Config; +import alpine.model.ConfigProperty; +import alpine.model.ManagedUser; +import alpine.model.Permission; +import alpine.model.Team; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.init.InitTaskContext; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.DefaultRepository; +import org.dependencytrack.model.License; +import org.dependencytrack.model.LicenseGroup; +import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.Repository; +import org.dependencytrack.model.Role; +import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; +import org.junit.Before; +import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class DatabaseSeedingInitTaskTest extends PersistenceCapableTest { + + private Config configMock; + private PGSimpleDataSource dataSource; + + @Before + public void before() throws Exception { + super.before(); + + configMock = mock(Config.class); + doReturn("25b88cc9-ff96-4ec5-9921-6212e954a46f").when(configMock).getApplicationBuildUuid(); + doReturn("1970-01-01 00:00:00").when(configMock).getApplicationBuildTimestamp(); + + dataSource = new PGSimpleDataSource(); + dataSource.setUrl(postgresContainer.getJdbcUrl()); + dataSource.setUser(postgresContainer.getUsername()); + dataSource.setPassword(postgresContainer.getPassword()); + } + + @Test + public void test() throws Exception { + new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource)); + + final List configProperties = qm.getConfigProperties(); + assertThat(configProperties).hasSize(ConfigPropertyConstants.values().length); + assertThat(configProperties).allSatisfy(property -> { + assertThat(property.getGroupName()).isNotBlank(); + assertThat(property.getPropertyName()).isNotBlank(); + assertThat(property.getPropertyType()).isNotNull(); + assertThat(property.getDescription()).isNotNull(); + }); + assertThat(configProperties).anySatisfy(property -> assertThat(property.getPropertyValue()).isNotBlank()); + + final List permissions = qm.getPermissions(); + assertThat(permissions).hasSize(Permissions.values().length); + assertThat(permissions).allSatisfy(permission -> { + assertThat(permission.getName()).isNotBlank(); + assertThat(permission.getDescription()).isNotBlank(); + }); + + final List teams = qm.getTeams().getList(Team.class); + assertThat(teams).isNotEmpty(); + assertThat(teams).allSatisfy(team -> { + assertThat(team.getName()).isNotBlank(); + assertThat(team.getUuid()).isNotNull(); + assertThat(team.getPermissions()).isNotEmpty(); + }); + + final List roles = qm.getRoles(); + assertThat(roles).isNotEmpty(); + assertThat(roles).allSatisfy(role -> { + assertThat(role.getName()).isNotBlank(); + assertThat(role.getUuid()).isNotNull(); + assertThat(role.getPermissions()).isNotEmpty(); + }); + + final List users = qm.getManagedUsers(); + assertThat(users).satisfiesExactly(user -> { + assertThat(user.getUsername()).isEqualTo("admin"); + assertThat(user.getEmail()).isEqualTo("admin@localhost"); + assertThat(user.getPassword()).isNotBlank(); + assertThat(user.getLastPasswordChange()).isNotNull(); + assertThat(user.isForcePasswordChange()).isTrue(); + assertThat(user.isNonExpiryPassword()).isTrue(); + assertThat(user.isSuspended()).isFalse(); + assertThat(user.getPermissions()).hasSize(Permissions.values().length); + assertThat(user.getTeams()).extracting(Team::getName).containsOnly("Administrators"); + }); + + final List licenses = qm.getLicenses().getList(License.class); + assertThat(licenses).isNotEmpty(); + assertThat(licenses).allSatisfy(license -> { + assertThat(license.getLicenseId()).isNotBlank(); + assertThat(license.getName()).isNotBlank(); + assertThat(license.getUuid()).isNotNull(); + }); + assertThat(licenses).anySatisfy(license -> assertThat(license.getHeader()).isNotBlank()); + assertThat(licenses).anySatisfy(license -> assertThat(license.getHeader()).isNotBlank()); + assertThat(licenses).anySatisfy(license -> assertThat(license.getText()).isNotBlank()); + assertThat(licenses).anySatisfy(license -> assertThat(license.getTemplate()).isNotBlank()); + assertThat(licenses).anySatisfy(license -> assertThat(license.getComment()).isNotBlank()); + assertThat(licenses).anySatisfy(license -> assertThat(license.getSeeAlso()).isNotEmpty()); + + final List licenseGroups = qm.getLicenseGroups().getList(LicenseGroup.class); + assertThat(licenseGroups).isNotEmpty(); + assertThat(licenseGroups).allSatisfy(licenseGroup -> { + assertThat(licenseGroup.getName()).isNotBlank(); + assertThat(licenseGroup.getUuid()).isNotNull(); + assertThat(licenseGroup.getLicenses()).isNotEmpty(); + }); + + final List notificationPublishers = qm.getAllNotificationPublishers(); + assertThat(notificationPublishers).hasSize(DefaultNotificationPublishers.values().length); + assertThat(notificationPublishers).allSatisfy(notificationPublisher -> { + assertThat(notificationPublisher.getName()).isNotBlank(); + assertThat(notificationPublisher.getPublisherClass()).isNotBlank(); + assertThat(notificationPublisher.getDescription()).isNotBlank(); + assertThat(notificationPublisher.getTemplate()).isNotBlank(); + assertThat(notificationPublisher.getTemplateMimeType()).isNotBlank(); + assertThat(notificationPublisher.isDefaultPublisher()).isTrue(); + }); + + final List repositories = qm.getRepositories().getList(Repository.class); + assertThat(repositories).hasSize(DefaultRepository.values().length); + assertThat(repositories).allSatisfy(repository -> { + assertThat(repository.getType()).isNotNull(); + assertThat(repository.getIdentifier()).isNotBlank(); + assertThat(repository.getUrl()).isNotBlank(); + assertThat(repository.getResolutionOrder()).isNotZero(); + assertThat(repository.isEnabled()).isTrue(); + assertThat(repository.getUuid()).isNotNull(); + }); + } + + @Test + public void testWithDefaultObjectsAlreadyPopulated() throws Exception { + new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource)); + + List licenses = qm.getLicenses().getList(License.class); + assertThat(licenses).isNotEmpty(); + + qm.delete(licenses); + + new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource)); + + // Default objects must not have been populated again, since their + // version is already current for this application build. + licenses = qm.getLicenses().getList(License.class); + assertThat(licenses).isEmpty(); + } + + @Test + public void testLoadDefaultLicensesUpdatesExistingLicenses() throws Exception { + final var license = new License(); + license.setLicenseId("LGPL-2.1+"); + license.setName("name"); + license.setComment("comment"); + license.setHeader("header"); + license.setSeeAlso("seeAlso"); + license.setTemplate("template"); + license.setText("text"); + qm.persist(license); + + new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource)); + + qm.getPersistenceManager().refresh(license); + assertThat(license.getLicenseId()).isEqualTo("LGPL-2.1+"); + assertThat(license.getName()).isEqualTo("GNU Lesser General Public License v2.1 or later"); + assertThat(license.getComment()).isNotEqualTo("comment"); + assertThat(license.getHeader()).isNotEqualTo("header"); + assertThat(license.getSeeAlso()).isNotEqualTo(new String[]{"seeAlso"}); + assertThat(license.getTemplate()).isNotEqualTo("template"); + assertThat(license.getText()).isNotEqualTo("text"); + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java deleted file mode 100644 index 4bf32e82c4..0000000000 --- a/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.persistence; - -import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.common.ConfigKey; -import org.dependencytrack.model.ConfigPropertyConstants; -import org.dependencytrack.model.License; -import org.dependencytrack.model.Repository; -import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; -import org.dependencytrack.persistence.jdbi.MetricsDao; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.contrib.java.lang.system.EnvironmentVariables; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Collections; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; - -public class DefaultObjectGeneratorTest extends PersistenceCapableTest { - - @Rule - public EnvironmentVariables environmentVariables = new EnvironmentVariables(); - - @Test - public void testContextInitialized() throws Exception { - testLoadDefaultPermissions(); - testLoadDefaultPersonas(); - testLoadDefaultLicenses(); - testLoadDefaultRepositories(); - testLoadDefaultConfigProperties(); - testLoadDefaultNotificationPublishers(); - } - - @Test - public void testWithInitTasksDisabled() { - environmentVariables.set(ConfigKey.INIT_TASKS_ENABLED.name(), "false"); - - new DefaultObjectGenerator().contextInitialized(null); - - assertThat(qm.getPermissions()).isEmpty(); - assertThat(qm.getManagedUsers()).isEmpty(); - assertThat(qm.getLicenses().getList(License.class)).isEmpty(); - assertThat(qm.getRepositories().getList(Repository.class)).isEmpty(); - assertThat(qm.getConfigProperties()).isEmpty(); - assertThat(qm.getAllNotificationPublishers()).isEmpty(); - } - - @Test - public void testWithDefaultObjectsAlreadyPopulated() { - new DefaultObjectGenerator().contextInitialized(null); - - List licenses = qm.getLicenses().getList(License.class); - assertThat(licenses).isNotEmpty(); - - qm.delete(licenses); - - new DefaultObjectGenerator().contextInitialized(null); - - // Default objects must not have been populated again, since their - // version is already current for this application build. - licenses = qm.getLicenses().getList(License.class); - assertThat(licenses).isEmpty(); - } - - @Test - public void testLoadDefaultLicenses() throws Exception { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.loadDefaultLicenses(); - Assert.assertEquals(738, qm.getAllLicensesConcise().size()); - } - - @Test - public void testLoadDefaultLicensesUpdatesExistingLicenses() throws Exception { - final var license = new License(); - license.setLicenseId("LGPL-2.1+"); - license.setName("name"); - license.setComment("comment"); - license.setHeader("header"); - license.setSeeAlso("seeAlso"); - license.setTemplate("template"); - license.setText("text"); - qm.persist(license); - - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultLicenses(); - - qm.getPersistenceManager().refresh(license); - assertThat(license.getLicenseId()).isEqualTo("LGPL-2.1+"); - assertThat(license.getName()).isEqualTo("GNU Lesser General Public License v2.1 or later"); - assertThat(license.getComment()).isNotEqualTo("comment"); - assertThat(license.getHeader()).isNotEqualTo("header"); - assertThat(license.getSeeAlso()).isNotEqualTo(new String[]{"seeAlso"}); - assertThat(license.getTemplate()).isNotEqualTo("template"); - assertThat(license.getText()).isNotEqualTo("text"); - } - - @Test - public void testLoadDefaultPermissions() throws Exception { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.loadDefaultPermissions(); - Assert.assertEquals(Permissions.values().length, qm.getPermissions().size()); - } - - @Test - public void testLoadDefaultPersonas() throws Exception { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.loadDefaultPersonas(); - Assert.assertEquals(4, qm.getTeams().getTotal()); - } - - @Test - public void testLoadDefaultRepositories() throws Exception { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.loadDefaultRepositories(); - Assert.assertEquals(17, qm.getAllRepositories().size()); - } - - @Test - public void testLoadDefaultConfigProperties() throws Exception { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.loadDefaultConfigProperties(); - Assert.assertEquals(ConfigPropertyConstants.values().length, qm.getConfigProperties().size()); - } - - @Test - public void testLoadDefaultNotificationPublishers() throws Exception { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.loadDefaultNotificationPublishers(); - Assert.assertEquals(DefaultNotificationPublishers.values().length, qm.getAllNotificationPublishers().size()); - } - - @Test - public void testMetricsPartitionsForTodayAndTomorrow() { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.ensureMetricsPartitions(); - var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); - var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE); - withJdbiHandle(handle -> { - var metricsHandle = handle.attach(MetricsDao.class); - assertThat(Collections.frequency(metricsHandle.getPortfolioMetricsPartitions(), "\"PORTFOLIOMETRICS_%s\"".formatted(today))).isEqualTo(1); - assertThat(Collections.frequency(metricsHandle.getPortfolioMetricsPartitions(), "\"PORTFOLIOMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1); - assertThat(Collections.frequency(metricsHandle.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1); - assertThat(Collections.frequency(metricsHandle.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1); - assertThat(Collections.frequency(metricsHandle.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1); - assertThat(Collections.frequency(metricsHandle.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1); - return null; - }); - } -} diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java index c0dbfe64f7..7e92a5a370 100644 --- a/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java +++ b/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java @@ -23,20 +23,22 @@ import org.junit.Assert; import org.junit.Test; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; + public class NotificationQueryManagerTest extends PersistenceCapableTest { @Test public void testGetNotificationPublisher() { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.contextInitialized(null); + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers); + var publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName()); Assert.assertEquals("SlackPublisher", publisher.getPublisherClass()); } @Test public void testGetDefaultNotificationPublisher() { - DefaultObjectGenerator generator = new DefaultObjectGenerator(); - generator.contextInitialized(null); + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers); + var publisher = qm.getDefaultNotificationPublisherByName(DefaultNotificationPublishers.SLACK.getPublisherName()); Assert.assertEquals("Slack", publisher.getName()); Assert.assertEquals("SlackPublisher", publisher.getPublisherClass()); diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java index 825f4e5638..6352e0e413 100644 --- a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java +++ b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java @@ -21,7 +21,6 @@ import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.model.Component; import org.dependencytrack.model.DependencyMetrics; -import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.ProjectMetrics; import org.jdbi.v3.core.Handle; import org.junit.After; @@ -61,35 +60,6 @@ public void after() { super.after(); } - @Test - public void testGetPortfolioMetricsForXDays() { - metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 40); - var metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(4); - metrics.setFirstOccurrence(Date.from(Instant.now())); - metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(40)))); - metricsTestDao.createPortfolioMetrics(metrics); - - metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 30); - metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(3); - metrics.setFirstOccurrence(Date.from(Instant.now())); - metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(30)))); - metricsTestDao.createPortfolioMetrics(metrics); - - metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 20); - metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(2); - metrics.setFirstOccurrence(Date.from(Instant.now())); - metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(20)))); - metricsTestDao.createPortfolioMetrics(metrics); - - var portfolioMetrics = metricsDao.getPortfolioMetricsSince(Instant.now().minus(Duration.ofDays(35))); - assertThat(portfolioMetrics.size()).isEqualTo(2); - assertThat(portfolioMetrics.get(0).getVulnerabilities()).isEqualTo(3); - assertThat(portfolioMetrics.get(1).getVulnerabilities()).isEqualTo(2); - } - @Test public void testGetProjectMetricsForXDays() { final var project = qm.createProject("acme-app", null, "1.0.0", null, null, null, null, false); @@ -172,10 +142,8 @@ public void testGetDependencyMetricsForXDays() { public void testCreateMetricsPartitionsForToday() { metricsDao.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString()); var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); - var metricsPartition = metricsDao.getPortfolioMetricsPartitions(); - assertThat(metricsPartition.contains("\"PORTFOLIOMETRICS_%s\"".formatted(today))).isTrue(); - metricsPartition = metricsDao.getProjectMetricsPartitions(); + var metricsPartition = metricsDao.getProjectMetricsPartitions(); assertThat(metricsPartition.contains("\"PROJECTMETRICS_%s\"".formatted(today))).isTrue(); metricsPartition = metricsDao.getDependencyMetricsPartitions(); @@ -184,7 +152,6 @@ public void testCreateMetricsPartitionsForToday() { // If called again on the same day with partitions already created, // It won't create more. metricsDao.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString()); - assertThat(Collections.frequency(metricsDao.getPortfolioMetricsPartitions(), "\"PORTFOLIOMETRICS_%s\"".formatted(today))).isEqualTo(1); assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1); assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1); } diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java index 0512860fcc..b04143f990 100644 --- a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java +++ b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java @@ -19,7 +19,6 @@ package org.dependencytrack.persistence.jdbi; import org.dependencytrack.model.DependencyMetrics; -import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.ProjectMetrics; import org.jdbi.v3.sqlobject.SqlObject; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; @@ -31,25 +30,72 @@ public interface MetricsTestDao extends SqlObject { - - @SqlQuery(""" - INSERT INTO "PORTFOLIOMETRICS"( - "PROJECTS", "COMPONENTS", "FIRST_OCCURRENCE", "LAST_OCCURRENCE", "CRITICAL", "HIGH", "MEDIUM", "LOW", "RISKSCORE", - "SUPPRESSED", "VULNERABILITIES", "VULNERABLEPROJECTS", "VULNERABLECOMPONENTS") - VALUES (:projects, :components, :firstOccurrence, :lastOccurrence, :critical, :high, :medium, :low, :inheritedRiskScore, - :suppressed, :vulnerabilities, :vulnerableProjects, :vulnerableComponents) - RETURNING * - """) - @RegisterBeanMapper(PortfolioMetrics.class) - PortfolioMetrics createPortfolioMetrics(@BindBean PortfolioMetrics portfolioMetrics); - - @SqlQuery(""" INSERT INTO "PROJECTMETRICS"( - "PROJECT_ID", "COMPONENTS", "FIRST_OCCURRENCE", "LAST_OCCURRENCE", "CRITICAL", "HIGH", "MEDIUM", "LOW", "RISKSCORE", - "SUPPRESSED", "VULNERABILITIES", "VULNERABLECOMPONENTS") - VALUES (:projectId, :components, :firstOccurrence, :lastOccurrence, :critical, :high, :medium, :low, :inheritedRiskScore, - :suppressed, :vulnerabilities, :vulnerableComponents) + "COMPONENTS" + , "CRITICAL" + , "FINDINGS_AUDITED" + , "FINDINGS_TOTAL" + , "FINDINGS_UNAUDITED" + , "FIRST_OCCURRENCE" + , "HIGH" + , "LAST_OCCURRENCE" + , "LOW" + , "MEDIUM" + , "POLICYVIOLATIONS_AUDITED" + , "POLICYVIOLATIONS_FAIL" + , "POLICYVIOLATIONS_INFO" + , "POLICYVIOLATIONS_LICENSE_AUDITED" + , "POLICYVIOLATIONS_LICENSE_TOTAL" + , "POLICYVIOLATIONS_LICENSE_UNAUDITED" + , "POLICYVIOLATIONS_OPERATIONAL_AUDITED" + , "POLICYVIOLATIONS_OPERATIONAL_TOTAL" + , "POLICYVIOLATIONS_OPERATIONAL_UNAUDITED" + , "POLICYVIOLATIONS_SECURITY_AUDITED" + , "POLICYVIOLATIONS_SECURITY_TOTAL" + , "POLICYVIOLATIONS_SECURITY_UNAUDITED" + , "POLICYVIOLATIONS_TOTAL" + , "POLICYVIOLATIONS_UNAUDITED" + , "POLICYVIOLATIONS_WARN" + , "PROJECT_ID" + , "RISKSCORE" + , "SUPPRESSED" + , "UNASSIGNED_SEVERITY" + , "VULNERABILITIES" + , "VULNERABLECOMPONENTS" + ) VALUES ( + :components + , :critical + , :findingsAudited + , :findingsTotal + , :findingsUnaudited + , :firstOccurrence + , :high + , :lastOccurrence + , :low + , :medium + , :policyViolationsAudited + , :policyViolationsFail + , :policyViolationsInfo + , :policyViolationsLicenseAudited + , :policyViolationsLicenseTotal + , :policyViolationsLicenseUnaudited + , :policyViolationsOperationalAudited + , :policyViolationsOperationalTotal + , :policyViolationsOperationalUnaudited + , :policyViolationsSecurityAudited + , :policyViolationsSecurityTotal + , :policyViolationsSecurityUnaudited + , :policyViolationsTotal + , :policyViolationsUnaudited + , :policyViolationsWarn + , :projectId + , :inheritedRiskScore + , :suppressed + , :unassigned + , :vulnerabilities + , :vulnerableComponents + ) RETURNING * """) @RegisterBeanMapper(ProjectMetrics.class) diff --git a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java index 65f589d6b1..45777105fe 100644 --- a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java +++ b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java @@ -48,7 +48,7 @@ import org.dependencytrack.model.ViolationAnalysisState; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAlias; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.dependencytrack.plugin.PluginManager; import org.dependencytrack.proto.filestorage.v1.FileMetadata; import org.dependencytrack.tasks.BomUploadProcessingTask; @@ -73,6 +73,7 @@ import static org.apache.commons.io.IOUtils.resourceToURL; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; public class CelPolicyEngineTest extends PersistenceCapableTest { @@ -1949,8 +1950,10 @@ public void testEvaluateProjectWithNoLongerApplicableViolationWithAnalysis() { @Test @Ignore // Un-ignore for manual profiling purposes. public void testWithBloatedBom() throws Exception { - // Import all default objects (includes licenses and license groups). - new DefaultObjectGenerator().contextInitialized(null); + useJdbiTransaction(handle -> { + DatabaseSeedingInitTask.seedDefaultLicenses(handle); + DatabaseSeedingInitTask.seedDefaultLicenseGroups(handle); + }); final var project = new Project(); project.setName("acme-app"); diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java index dc90233938..8397c070a1 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java @@ -24,7 +24,7 @@ import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.License; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.ClassRule; @@ -36,6 +36,8 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; + public class LicenseResourceTest extends ResourceTest { @ClassRule @@ -47,8 +49,8 @@ public class LicenseResourceTest extends ResourceTest { @Override public void before() throws Exception { super.before(); - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultLicenses(); + + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses); } @Test diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java index 91234e9fb6..f4a8b8ab58 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java @@ -21,29 +21,30 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFeature; import alpine.server.filters.AuthorizationFeature; -import jakarta.json.JsonArray; -import jakarta.ws.rs.core.Response; +import net.javacrumbs.jsonunit.core.Option; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; -import org.dependencytrack.persistence.jdbi.MetricsTestDao; import org.dependencytrack.model.Component; -import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.Project; -import org.dependencytrack.util.DateUtil; +import org.dependencytrack.model.ProjectMetrics; +import org.dependencytrack.persistence.jdbi.MetricsTestDao; import org.glassfish.jersey.server.ResourceConfig; import org.junit.ClassRule; import org.junit.Test; -import java.time.Duration; -import java.time.Instant; +import jakarta.ws.rs.core.Response; +import java.math.BigDecimal; import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; +import static org.hamcrest.Matchers.closeTo; public class MetricsResourceTest extends ResourceTest { @@ -323,38 +324,392 @@ public void refreshComponentMetricsAclTest() { } @Test - public void getPortfolioMetricsXDaysAclTest() { + public void getCurrentPortfolioMetricsEmptyTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final Response response = jersey + .target(V1_METRICS + "/portfolio/current") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)) + .isEqualTo(/* language=JSON */ """ + { + "components": 0, + "critical": 0, + "findingsAudited": 0, + "findingsTotal": 0, + "findingsUnaudited": 0, + "firstOccurrence": "${json-unit.any-number}", + "high": 0, + "inheritedRiskScore": 0.0, + "lastOccurrence": "${json-unit.any-number}", + "low": 0, + "medium": 0, + "policyViolationsAudited": 0, + "policyViolationsFail": 0, + "policyViolationsInfo": 0, + "policyViolationsLicenseAudited": 0, + "policyViolationsLicenseTotal": 0, + "policyViolationsLicenseUnaudited": 0, + "policyViolationsOperationalAudited": 0, + "policyViolationsOperationalTotal": 0, + "policyViolationsOperationalUnaudited": 0, + "policyViolationsSecurityAudited": 0, + "policyViolationsSecurityTotal": 0, + "policyViolationsSecurityUnaudited": 0, + "policyViolationsTotal": 0, + "policyViolationsUnaudited": 0, + "policyViolationsWarn": 0, + "projects": 0, + "suppressed": 0, + "unassigned": 0, + "vulnerabilities": 0, + "vulnerableComponents": 0, + "vulnerableProjects": 0 + } + """); + } + + @Test + public void getCurrentPortfolioMetricsAclTest() { initializeWithPermissions(Permissions.VIEW_PORTFOLIO); enablePortfolioAccessControl(); + final var accessibleProjectA = new Project(); + accessibleProjectA.setName("acme-app-a"); + accessibleProjectA.addAccessTeam(super.team); + qm.persist(accessibleProjectA); + + final var accessibleProjectB = new Project(); + accessibleProjectB.setName("acme-app-b"); + accessibleProjectB.addAccessTeam(super.team); + qm.persist(accessibleProjectB); + + final var inactiveAccessibleProject = new Project(); + inactiveAccessibleProject.setName("acme-app-inactive"); + inactiveAccessibleProject.setInactiveSince(new Date()); + inactiveAccessibleProject.addAccessTeam(super.team); + qm.persist(inactiveAccessibleProject); + + final var inaccessibleProject = new Project(); + inaccessibleProject.setName("acme-app-inaccessible"); + qm.persist(inaccessibleProject); + + final var today = LocalDate.now(); + useJdbiHandle(handle -> { var dao = handle.attach(MetricsTestDao.class); - dao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 30); - var metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(3); - metrics.setFirstOccurrence(Date.from(Instant.now())); - metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(30)))); - dao.createPortfolioMetrics(metrics); - - dao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 20); - metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(2); - metrics.setFirstOccurrence(Date.from(Instant.now())); - metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(20)))); - dao.createPortfolioMetrics(metrics); + + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1)); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2)); + + { + // Create metrics for "yesterday". + + var accessibleProjectAMetrics = new ProjectMetrics(); + accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId()); + accessibleProjectAMetrics.setComponents(2); + accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectAMetrics); + } + + { + // Create metrics for "today". + + // Do not create metrics for accessibleProjectA. + // Its metrics from "yesterday" are supposed to carry over to "today". + + var accessibleProjectBMetrics = new ProjectMetrics(); + accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId()); + accessibleProjectBMetrics.setComponents(1); + accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectBMetrics); + + // Metrics of inactive projects must not be considered. + var inactiveAccessibleProjectMetrics = new ProjectMetrics(); + inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId()); + inactiveAccessibleProjectMetrics.setComponents(111); + inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant())); + inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inactiveAccessibleProjectMetrics); + + // Metrics of inaccessible projects must not be considered. + var inaccessibleProjectMetrics = new ProjectMetrics(); + inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId()); + inaccessibleProjectMetrics.setComponents(666); + inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant())); + inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inaccessibleProjectMetrics); + } }); - final Supplier responseSupplier = () -> jersey - .target(V1_METRICS + "/portfolio/25/days") + final Response response = jersey + .target(V1_METRICS + "/portfolio/current") .request() .header(X_API_KEY, apiKey) .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)) + .withOptions(Option.IGNORING_EXTRA_FIELDS) + .isEqualTo(/* language=JSON */ """ + { + "projects": 2, + "components": 3 + } + """); + } - Response response = responseSupplier.get(); + @Test + public void getPortfolioMetricsXDaysAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var accessibleProjectA = new Project(); + accessibleProjectA.setName("acme-app-a"); + accessibleProjectA.addAccessTeam(super.team); + qm.persist(accessibleProjectA); + + final var accessibleProjectB = new Project(); + accessibleProjectB.setName("acme-app-b"); + accessibleProjectB.addAccessTeam(super.team); + qm.persist(accessibleProjectB); + + final var inactiveAccessibleProject = new Project(); + inactiveAccessibleProject.setName("acme-app-inactive"); + inactiveAccessibleProject.setInactiveSince(new Date()); + inactiveAccessibleProject.addAccessTeam(super.team); + qm.persist(inactiveAccessibleProject); + + final var inaccessibleProject = new Project(); + inaccessibleProject.setName("acme-app-inaccessible"); + qm.persist(inaccessibleProject); + + final var today = LocalDate.now(); + + useJdbiHandle(handle -> { + var dao = handle.attach(MetricsTestDao.class); + + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1)); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2)); + + { + // Create metrics for "yesterday". + + var accessibleProjectAMetrics = new ProjectMetrics(); + accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId()); + accessibleProjectAMetrics.setComponents(1); + accessibleProjectAMetrics.setCritical(1); + accessibleProjectAMetrics.setFindingsAudited(1); + accessibleProjectAMetrics.setFindingsTotal(1); + accessibleProjectAMetrics.setFindingsUnaudited(1); + accessibleProjectAMetrics.setHigh(1); + accessibleProjectAMetrics.setInheritedRiskScore(1.1); + accessibleProjectAMetrics.setLow(1); + accessibleProjectAMetrics.setMedium(1); + accessibleProjectAMetrics.setPolicyViolationsAudited(1); + accessibleProjectAMetrics.setPolicyViolationsFail(1); + accessibleProjectAMetrics.setPolicyViolationsInfo(1); + accessibleProjectAMetrics.setPolicyViolationsLicenseAudited(1); + accessibleProjectAMetrics.setPolicyViolationsLicenseTotal(1); + accessibleProjectAMetrics.setPolicyViolationsLicenseUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsOperationalAudited(1); + accessibleProjectAMetrics.setPolicyViolationsOperationalTotal(1); + accessibleProjectAMetrics.setPolicyViolationsOperationalUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsSecurityAudited(1); + accessibleProjectAMetrics.setPolicyViolationsSecurityTotal(1); + accessibleProjectAMetrics.setPolicyViolationsSecurityUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsTotal(1); + accessibleProjectAMetrics.setPolicyViolationsUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsWarn(1); + accessibleProjectAMetrics.setSuppressed(1); + accessibleProjectAMetrics.setUnassigned(1); + accessibleProjectAMetrics.setVulnerabilities(1); + accessibleProjectAMetrics.setVulnerableComponents(1); + accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectAMetrics); + } + + { + // Create metrics for "today". + + // Do not create metrics for accessibleProjectA. + // Its metrics from "yesterday" are supposed to carry over to "today". + + var accessibleProjectBMetrics = new ProjectMetrics(); + accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId()); + accessibleProjectBMetrics.setComponents(2); + accessibleProjectBMetrics.setCritical(2); + accessibleProjectBMetrics.setFindingsAudited(2); + accessibleProjectBMetrics.setFindingsTotal(2); + accessibleProjectBMetrics.setFindingsUnaudited(2); + accessibleProjectBMetrics.setHigh(2); + accessibleProjectBMetrics.setInheritedRiskScore(2.2); + accessibleProjectBMetrics.setLow(2); + accessibleProjectBMetrics.setMedium(2); + accessibleProjectBMetrics.setPolicyViolationsAudited(2); + accessibleProjectBMetrics.setPolicyViolationsFail(2); + accessibleProjectBMetrics.setPolicyViolationsInfo(2); + accessibleProjectBMetrics.setPolicyViolationsLicenseAudited(2); + accessibleProjectBMetrics.setPolicyViolationsLicenseTotal(2); + accessibleProjectBMetrics.setPolicyViolationsLicenseUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsOperationalAudited(2); + accessibleProjectBMetrics.setPolicyViolationsOperationalTotal(2); + accessibleProjectBMetrics.setPolicyViolationsOperationalUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsSecurityAudited(2); + accessibleProjectBMetrics.setPolicyViolationsSecurityTotal(2); + accessibleProjectBMetrics.setPolicyViolationsSecurityUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsTotal(2); + accessibleProjectBMetrics.setPolicyViolationsUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsWarn(2); + accessibleProjectBMetrics.setSuppressed(2); + accessibleProjectBMetrics.setUnassigned(2); + accessibleProjectBMetrics.setVulnerabilities(2); + accessibleProjectBMetrics.setVulnerableComponents(2); + accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectBMetrics); + + // Metrics of inactive projects must not be considered. + var inactiveAccessibleProjectMetrics = new ProjectMetrics(); + inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId()); + inactiveAccessibleProjectMetrics.setComponents(111); + inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant())); + inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inactiveAccessibleProjectMetrics); + + // Metrics of inaccessible projects must not be considered. + var inaccessibleProjectMetrics = new ProjectMetrics(); + inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId()); + inaccessibleProjectMetrics.setVulnerabilities(666); + inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant())); + inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inaccessibleProjectMetrics); + } + }); + + final Response response = jersey + .target(V1_METRICS + "/portfolio/3/days") + .request() + .header(X_API_KEY, apiKey) + .get(); assertThat(response.getStatus()).isEqualTo(200); - JsonArray json = parseJsonArray(response); - assertThat(json.size()).isEqualTo(1); - assertThat(json.getJsonObject(0).getInt("vulnerabilities")).isEqualTo(2); + assertThatJson(getPlainTextBody(response)) + .withMatcher("inheritedRiskScoreDay2", closeTo(BigDecimal.valueOf(1.1), BigDecimal.valueOf(0.01))) + .withMatcher("inheritedRiskScoreDay3", closeTo(BigDecimal.valueOf(3.3), BigDecimal.valueOf(0.01))) + .isEqualTo(/* language=JSON */ """ + [ + { + "components": 0, + "critical": 0, + "findingsAudited": 0, + "findingsTotal": 0, + "findingsUnaudited": 0, + "firstOccurrence": "${json-unit.any-number}", + "high": 0, + "inheritedRiskScore": 0.0, + "lastOccurrence": "${json-unit.any-number}", + "low": 0, + "medium": 0, + "policyViolationsAudited": 0, + "policyViolationsFail": 0, + "policyViolationsInfo": 0, + "policyViolationsLicenseAudited": 0, + "policyViolationsLicenseTotal": 0, + "policyViolationsLicenseUnaudited": 0, + "policyViolationsOperationalAudited": 0, + "policyViolationsOperationalTotal": 0, + "policyViolationsOperationalUnaudited": 0, + "policyViolationsSecurityAudited": 0, + "policyViolationsSecurityTotal": 0, + "policyViolationsSecurityUnaudited": 0, + "policyViolationsTotal": 0, + "policyViolationsUnaudited": 0, + "policyViolationsWarn": 0, + "projects": 0, + "suppressed": 0, + "unassigned": 0, + "vulnerabilities": 0, + "vulnerableComponents": 0, + "vulnerableProjects": 0 + }, + { + "components": 1, + "critical": 1, + "findingsAudited": 1, + "findingsTotal": 1, + "findingsUnaudited": 1, + "firstOccurrence": "${json-unit.any-number}", + "high": 1, + "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay2}", + "lastOccurrence": "${json-unit.any-number}", + "low": 1, + "medium": 1, + "policyViolationsAudited": 1, + "policyViolationsFail": 1, + "policyViolationsInfo": 1, + "policyViolationsLicenseAudited": 1, + "policyViolationsLicenseTotal": 1, + "policyViolationsLicenseUnaudited": 1, + "policyViolationsOperationalAudited": 1, + "policyViolationsOperationalTotal": 1, + "policyViolationsOperationalUnaudited": 1, + "policyViolationsSecurityAudited": 1, + "policyViolationsSecurityTotal": 1, + "policyViolationsSecurityUnaudited": 1, + "policyViolationsTotal": 1, + "policyViolationsUnaudited": 1, + "policyViolationsWarn": 1, + "projects": 1, + "suppressed": 1, + "unassigned": 1, + "vulnerabilities": 1, + "vulnerableComponents": 1, + "vulnerableProjects": 1 + }, + { + "components": 3, + "critical": 3, + "findingsAudited": 3, + "findingsTotal": 3, + "findingsUnaudited": 3, + "firstOccurrence": "${json-unit.any-number}", + "high": 3, + "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay3}", + "lastOccurrence": "${json-unit.any-number}", + "low": 3, + "medium": 3, + "policyViolationsAudited": 3, + "policyViolationsFail": 3, + "policyViolationsInfo": 3, + "policyViolationsLicenseAudited": 3, + "policyViolationsLicenseTotal": 3, + "policyViolationsLicenseUnaudited": 3, + "policyViolationsOperationalAudited": 3, + "policyViolationsOperationalTotal": 3, + "policyViolationsOperationalUnaudited": 3, + "policyViolationsSecurityAudited": 3, + "policyViolationsSecurityTotal": 3, + "policyViolationsSecurityUnaudited": 3, + "policyViolationsTotal": 3, + "policyViolationsUnaudited": 3, + "policyViolationsWarn": 3, + "projects": 2, + "suppressed": 3, + "unassigned": 3, + "vulnerabilities": 3, + "vulnerableComponents": 3, + "vulnerableProjects": 2 + } + ] + """); } @Test @@ -362,33 +717,245 @@ public void getPortfolioMetricsSinceAclTest() { initializeWithPermissions(Permissions.VIEW_PORTFOLIO); enablePortfolioAccessControl(); + final var accessibleProjectA = new Project(); + accessibleProjectA.setName("acme-app-a"); + accessibleProjectA.addAccessTeam(super.team); + qm.persist(accessibleProjectA); + + final var accessibleProjectB = new Project(); + accessibleProjectB.setName("acme-app-b"); + accessibleProjectB.addAccessTeam(super.team); + qm.persist(accessibleProjectB); + + final var inactiveAccessibleProject = new Project(); + inactiveAccessibleProject.setName("acme-app-inactive"); + inactiveAccessibleProject.setInactiveSince(new Date()); + inactiveAccessibleProject.addAccessTeam(super.team); + qm.persist(inactiveAccessibleProject); + + final var inaccessibleProject = new Project(); + inaccessibleProject.setName("acme-app-inaccessible"); + qm.persist(inaccessibleProject); + + final var today = LocalDate.now(); + useJdbiHandle(handle -> { var dao = handle.attach(MetricsTestDao.class); - dao.createMetricsPartitionsForDate("PORTFOLIOMETRICS", LocalDate.of(2025, 1, 1)); - var metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(3); - metrics.setFirstOccurrence(Date.from(Instant.now())); - metrics.setLastOccurrence(DateUtil.parseShortDate("20250101")); - dao.createPortfolioMetrics(metrics); - - dao.createMetricsPartitionsForDate("PORTFOLIOMETRICS", LocalDate.of(2025, 2, 1)); - metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(2); - metrics.setFirstOccurrence(Date.from(Instant.now())); - metrics.setLastOccurrence(DateUtil.parseShortDate("20250201")); - dao.createPortfolioMetrics(metrics); + + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1)); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2)); + + { + // Create metrics for "yesterday". + + var accessibleProjectAMetrics = new ProjectMetrics(); + accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId()); + accessibleProjectAMetrics.setComponents(1); + accessibleProjectAMetrics.setCritical(1); + accessibleProjectAMetrics.setFindingsAudited(1); + accessibleProjectAMetrics.setFindingsTotal(1); + accessibleProjectAMetrics.setFindingsUnaudited(1); + accessibleProjectAMetrics.setHigh(1); + accessibleProjectAMetrics.setInheritedRiskScore(1.1); + accessibleProjectAMetrics.setLow(1); + accessibleProjectAMetrics.setMedium(1); + accessibleProjectAMetrics.setPolicyViolationsAudited(1); + accessibleProjectAMetrics.setPolicyViolationsFail(1); + accessibleProjectAMetrics.setPolicyViolationsInfo(1); + accessibleProjectAMetrics.setPolicyViolationsLicenseAudited(1); + accessibleProjectAMetrics.setPolicyViolationsLicenseTotal(1); + accessibleProjectAMetrics.setPolicyViolationsLicenseUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsOperationalAudited(1); + accessibleProjectAMetrics.setPolicyViolationsOperationalTotal(1); + accessibleProjectAMetrics.setPolicyViolationsOperationalUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsSecurityAudited(1); + accessibleProjectAMetrics.setPolicyViolationsSecurityTotal(1); + accessibleProjectAMetrics.setPolicyViolationsSecurityUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsTotal(1); + accessibleProjectAMetrics.setPolicyViolationsUnaudited(1); + accessibleProjectAMetrics.setPolicyViolationsWarn(1); + accessibleProjectAMetrics.setSuppressed(1); + accessibleProjectAMetrics.setUnassigned(1); + accessibleProjectAMetrics.setVulnerabilities(1); + accessibleProjectAMetrics.setVulnerableComponents(1); + accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectAMetrics); + } + + { + // Create metrics for "today". + + // Do not create metrics for accessibleProjectA. + // Its metrics from "yesterday" are supposed to carry over to "today". + + var accessibleProjectBMetrics = new ProjectMetrics(); + accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId()); + accessibleProjectBMetrics.setComponents(2); + accessibleProjectBMetrics.setCritical(2); + accessibleProjectBMetrics.setFindingsAudited(2); + accessibleProjectBMetrics.setFindingsTotal(2); + accessibleProjectBMetrics.setFindingsUnaudited(2); + accessibleProjectBMetrics.setHigh(2); + accessibleProjectBMetrics.setInheritedRiskScore(2.2); + accessibleProjectBMetrics.setLow(2); + accessibleProjectBMetrics.setMedium(2); + accessibleProjectBMetrics.setPolicyViolationsAudited(2); + accessibleProjectBMetrics.setPolicyViolationsFail(2); + accessibleProjectBMetrics.setPolicyViolationsInfo(2); + accessibleProjectBMetrics.setPolicyViolationsLicenseAudited(2); + accessibleProjectBMetrics.setPolicyViolationsLicenseTotal(2); + accessibleProjectBMetrics.setPolicyViolationsLicenseUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsOperationalAudited(2); + accessibleProjectBMetrics.setPolicyViolationsOperationalTotal(2); + accessibleProjectBMetrics.setPolicyViolationsOperationalUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsSecurityAudited(2); + accessibleProjectBMetrics.setPolicyViolationsSecurityTotal(2); + accessibleProjectBMetrics.setPolicyViolationsSecurityUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsTotal(2); + accessibleProjectBMetrics.setPolicyViolationsUnaudited(2); + accessibleProjectBMetrics.setPolicyViolationsWarn(2); + accessibleProjectBMetrics.setSuppressed(2); + accessibleProjectBMetrics.setUnassigned(2); + accessibleProjectBMetrics.setVulnerabilities(2); + accessibleProjectBMetrics.setVulnerableComponents(2); + accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectBMetrics); + + // Metrics of inactive projects must not be considered. + var inactiveAccessibleProjectMetrics = new ProjectMetrics(); + inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId()); + inactiveAccessibleProjectMetrics.setComponents(111); + inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant())); + inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inactiveAccessibleProjectMetrics); + + // Metrics of inaccessible projects must not be considered. + var inaccessibleProjectMetrics = new ProjectMetrics(); + inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId()); + inaccessibleProjectMetrics.setVulnerabilities(666); + inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant())); + inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inaccessibleProjectMetrics); + } }); - final Supplier responseSupplier = () -> jersey - .target(V1_METRICS + "/portfolio/since/20250201") + final Response response = jersey + .target(V1_METRICS + "/portfolio/since/" + today.minusDays(2).format(DateTimeFormatter.ofPattern("yyyyMMdd"))) .request() .header(X_API_KEY, apiKey) .get(); - - Response response = responseSupplier.get(); assertThat(response.getStatus()).isEqualTo(200); - JsonArray json = parseJsonArray(response); - assertThat(json.size()).isEqualTo(1); - assertThat(json.getJsonObject(0).getInt("vulnerabilities")).isEqualTo(2); + assertThatJson(getPlainTextBody(response)) + .withMatcher("inheritedRiskScoreDay2", closeTo(BigDecimal.valueOf(1.1), BigDecimal.valueOf(0.01))) + .withMatcher("inheritedRiskScoreDay3", closeTo(BigDecimal.valueOf(3.3), BigDecimal.valueOf(0.01))) + .isEqualTo(/* language=JSON */ """ + [ + { + "components": 0, + "critical": 0, + "findingsAudited": 0, + "findingsTotal": 0, + "findingsUnaudited": 0, + "firstOccurrence": "${json-unit.any-number}", + "high": 0, + "inheritedRiskScore": 0.0, + "lastOccurrence": "${json-unit.any-number}", + "low": 0, + "medium": 0, + "policyViolationsAudited": 0, + "policyViolationsFail": 0, + "policyViolationsInfo": 0, + "policyViolationsLicenseAudited": 0, + "policyViolationsLicenseTotal": 0, + "policyViolationsLicenseUnaudited": 0, + "policyViolationsOperationalAudited": 0, + "policyViolationsOperationalTotal": 0, + "policyViolationsOperationalUnaudited": 0, + "policyViolationsSecurityAudited": 0, + "policyViolationsSecurityTotal": 0, + "policyViolationsSecurityUnaudited": 0, + "policyViolationsTotal": 0, + "policyViolationsUnaudited": 0, + "policyViolationsWarn": 0, + "projects": 0, + "suppressed": 0, + "unassigned": 0, + "vulnerabilities": 0, + "vulnerableComponents": 0, + "vulnerableProjects": 0 + }, + { + "components": 1, + "critical": 1, + "findingsAudited": 1, + "findingsTotal": 1, + "findingsUnaudited": 1, + "firstOccurrence": "${json-unit.any-number}", + "high": 1, + "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay2}", + "lastOccurrence": "${json-unit.any-number}", + "low": 1, + "medium": 1, + "policyViolationsAudited": 1, + "policyViolationsFail": 1, + "policyViolationsInfo": 1, + "policyViolationsLicenseAudited": 1, + "policyViolationsLicenseTotal": 1, + "policyViolationsLicenseUnaudited": 1, + "policyViolationsOperationalAudited": 1, + "policyViolationsOperationalTotal": 1, + "policyViolationsOperationalUnaudited": 1, + "policyViolationsSecurityAudited": 1, + "policyViolationsSecurityTotal": 1, + "policyViolationsSecurityUnaudited": 1, + "policyViolationsTotal": 1, + "policyViolationsUnaudited": 1, + "policyViolationsWarn": 1, + "projects": 1, + "suppressed": 1, + "unassigned": 1, + "vulnerabilities": 1, + "vulnerableComponents": 1, + "vulnerableProjects": 1 + }, + { + "components": 3, + "critical": 3, + "findingsAudited": 3, + "findingsTotal": 3, + "findingsUnaudited": 3, + "firstOccurrence": "${json-unit.any-number}", + "high": 3, + "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay3}", + "lastOccurrence": "${json-unit.any-number}", + "low": 3, + "medium": 3, + "policyViolationsAudited": 3, + "policyViolationsFail": 3, + "policyViolationsInfo": 3, + "policyViolationsLicenseAudited": 3, + "policyViolationsLicenseTotal": 3, + "policyViolationsLicenseUnaudited": 3, + "policyViolationsOperationalAudited": 3, + "policyViolationsOperationalTotal": 3, + "policyViolationsOperationalUnaudited": 3, + "policyViolationsSecurityAudited": 3, + "policyViolationsSecurityTotal": 3, + "policyViolationsSecurityUnaudited": 3, + "policyViolationsTotal": 3, + "policyViolationsUnaudited": 3, + "policyViolationsWarn": 3, + "projects": 2, + "suppressed": 3, + "unassigned": 3, + "vulnerabilities": 3, + "vulnerableComponents": 3, + "vulnerableProjects": 2 + } + ] + """); } } diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java index 479fa5e6a4..7506111f2f 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java @@ -22,11 +22,6 @@ import alpine.notification.NotificationLevel; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFeature; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.ConfigPropertyConstants; @@ -35,19 +30,25 @@ import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.HashSet; import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; public class NotificationPublisherResourceTest extends ResourceTest { @@ -60,8 +61,8 @@ public class NotificationPublisherResourceTest extends ResourceTest { @Before public void before() throws Exception { super.before(); - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultNotificationPublishers(); + + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers); } @Test diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java index b7596752f5..aa74b5a2ad 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java @@ -23,11 +23,6 @@ import alpine.notification.NotificationLevel; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFeature; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import net.javacrumbs.jsonunit.core.Option; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; @@ -37,7 +32,7 @@ import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; @@ -45,6 +40,11 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -56,6 +56,7 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; import static org.hamcrest.Matchers.equalTo; public class NotificationRuleResourceTest extends ResourceTest { @@ -69,8 +70,8 @@ public class NotificationRuleResourceTest extends ResourceTest { @Before public void before() throws Exception { super.before(); - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultNotificationPublishers(); + + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers); } @Test diff --git a/apiserver/src/test/java/org/dependencytrack/resources/OpenApiResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/OpenApiResourceTest.java similarity index 98% rename from apiserver/src/test/java/org/dependencytrack/resources/OpenApiResourceTest.java rename to apiserver/src/test/java/org/dependencytrack/resources/v1/OpenApiResourceTest.java index c7fda53cbd..46f24cecac 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/OpenApiResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/OpenApiResourceTest.java @@ -16,12 +16,11 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) OWASP Foundation. All Rights Reserved. */ -package org.dependencytrack.resources; +package org.dependencytrack.resources.v1; import alpine.server.filters.ApiFilter; import io.swagger.parser.OpenAPIParser; import io.swagger.v3.parser.core.models.SwaggerParseResult; -import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.glassfish.jersey.client.ClientProperties; @@ -29,6 +28,7 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.ws.rs.core.Response; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java index 90c3ea8698..0f4778baf5 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java @@ -27,7 +27,7 @@ import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Role; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.Before; @@ -40,12 +40,13 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; + public class PermissionResourceTest extends ResourceTest { @ClassRule @@ -57,8 +58,8 @@ public class PermissionResourceTest extends ResourceTest { @Before public void before() throws Exception { super.before(); - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultPermissions(); + + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions); } @Test diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index b7dde48e7b..63717c3d97 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -27,14 +27,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFeature; import com.github.packageurl.PackageURL; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.HttpMethod; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; @@ -79,6 +71,14 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -835,6 +835,50 @@ public void getProjectsConciseFilterByNameTest() { """); } + @Test + public void getProjectsConciseFilterByVersionTest() { + final var projectA = new Project(); + projectA.setName("acme-app-a"); + projectA.setVersion("1.0"); + qm.persist(projectA); + + final var projectB = new Project(); + projectB.setName("acme-app-b"); + projectB.setVersion("2.0"); + qm.persist(projectB); + + // Should not return results for partial matches. + Response response = jersey.target(V1_PROJECT + "/concise") + .queryParam("version", "0") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("0"); + assertThat(getPlainTextBody(response)).isEqualTo("[]"); + + // Should return results for exact matches. + response = jersey.target(V1_PROJECT + "/concise") + .queryParam("version", "2.0") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "uuid": "${json-unit.any-string}", + "name": "acme-app-b", + "version": "2.0", + "active": true, + "isLatest": false, + "hasChildren": false + } + ] + """); + } + @Test public void getProjectsConciseFilterByTagTest() { final var projectA = new Project(); @@ -1155,15 +1199,15 @@ public void getProjectsConciseWithLatestMetricsTest() { "high": 3, "low": 4, "medium": 5, - "policyViolationsFail": 0, - "policyViolationsInfo": 0, - "policyViolationsLicenseTotal": 0, - "policyViolationsOperationalTotal": 0, - "policyViolationsSecurityTotal": 0, - "policyViolationsTotal": 0, - "policyViolationsWarn": 0, + "policyViolationsFail": 6, + "policyViolationsInfo": 7, + "policyViolationsLicenseTotal": 8, + "policyViolationsOperationalTotal": 9, + "policyViolationsSecurityTotal": 10, + "policyViolationsTotal": 11, + "policyViolationsWarn": 12, "inheritedRiskScore": 13.13, - "unassigned": 0, + "unassigned": 14, "vulnerabilities": 15 } } @@ -1445,6 +1489,56 @@ public void getProjectChildrenConciseFilterByNameTest() { """); } + @Test + public void getProjectChildrenConciseFilterByVersionTest() { + final var parentProject = new Project(); + parentProject.setName("acme-app"); + qm.persist(parentProject); + + final var childProjectA = new Project(); + childProjectA.setParent(parentProject); + childProjectA.setName("acme-child-app-a"); + childProjectA.setVersion("1.0"); + qm.persist(childProjectA); + + final var childProjectB = new Project(); + childProjectB.setParent(parentProject); + childProjectB.setName("acme-child-app-b"); + childProjectB.setVersion("2.0"); + qm.persist(childProjectB); + + // Should not return results for partial matches. + Response response = jersey.target(V1_PROJECT + "/concise/" + parentProject.getUuid() + "/children") + .queryParam("version", "0") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("0"); + assertThat(getPlainTextBody(response)).isEqualTo("[]"); + + // Should return results for exact matches. + response = jersey.target(V1_PROJECT + "/concise/" + parentProject.getUuid() + "/children") + .queryParam("version", "1.0") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "uuid": "${json-unit.any-string}", + "name": "acme-child-app-a", + "version": "1.0", + "active": true, + "isLatest": false, + "hasChildren": false + } + ] + """); + } + @Test public void getProjectChildrenConciseFilterByTagTest() { final var parentProject = new Project(); @@ -1641,15 +1735,15 @@ public void getProjectChildrenConciseWithLatestMetricsTest() { "high": 3, "low": 4, "medium": 5, - "policyViolationsFail": 0, - "policyViolationsInfo": 0, - "policyViolationsLicenseTotal": 0, - "policyViolationsOperationalTotal": 0, - "policyViolationsSecurityTotal": 0, - "policyViolationsTotal": 0, - "policyViolationsWarn": 0, + "policyViolationsFail": 6, + "policyViolationsInfo": 7, + "policyViolationsLicenseTotal": 8, + "policyViolationsOperationalTotal": 9, + "policyViolationsSecurityTotal": 10, + "policyViolationsTotal": 11, + "policyViolationsWarn": 12, "inheritedRiskScore": 13.13, - "unassigned": 0, + "unassigned": 14, "vulnerabilities": 15 } } diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java index 0b14692f5a..dbea1ec7df 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java @@ -25,7 +25,7 @@ import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.dependencytrack.persistence.QueryManager; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; @@ -41,6 +41,8 @@ import java.util.Date; import java.util.List; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; + public class RepositoryResourceTest extends ResourceTest { @ClassRule @@ -53,8 +55,8 @@ public class RepositoryResourceTest extends ResourceTest { @Override public void before() throws Exception { super.before(); - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultRepositories(); + + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultRepositories); } @Test diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java index eb208e6d61..7f73489f24 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java @@ -18,33 +18,34 @@ */ package org.dependencytrack.resources.v1; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - +import alpine.common.util.UuidUtil; +import alpine.model.ManagedUser; +import alpine.model.Permission; +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFeature; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; -import alpine.common.util.UuidUtil; -import alpine.model.ManagedUser; -import alpine.model.Permission; -import alpine.server.filters.ApiFilter; -import alpine.server.filters.AuthenticationFeature; import jakarta.json.JsonArray; import jakarta.json.JsonObject; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; public class RoleResourceTest extends ResourceTest { @@ -58,9 +59,11 @@ public class RoleResourceTest extends ResourceTest { @Override public void before() throws Exception { super.before(); - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultPermissions(); - generator.loadDefaultRoles(); + + useJdbiTransaction(handle -> { + DatabaseSeedingInitTask.seedDefaultPermissions(handle); + DatabaseSeedingInitTask.seedDefaultRoles(handle); + }); } @Test diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index 11214a9298..02b0dff16c 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -27,30 +27,31 @@ import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFeature; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.assertj.core.api.Assertions; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.UUID; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; import static org.hamcrest.CoreMatchers.equalTo; public class TeamResourceTest extends ResourceTest { @@ -70,8 +71,7 @@ public void setUpUser(boolean isAdmin) { qm.addUserToTeam(testUser, team); userNotPartof = qm.createTeam("UserNotPartof"); if (isAdmin) { - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultPermissions(); + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions); List permissionsList = new ArrayList<>(); final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT"); permissionsList.add(adminPermission); @@ -521,8 +521,7 @@ public void getVisibleNonApiKeyTeams() { @Test public void getVisibleAdminApiKeyTeams() { userNotPartof = qm.createTeam("UserNotPartof"); - final var generator = new DefaultObjectGenerator(); - generator.loadDefaultPermissions(); + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions); List permissionsList = new ArrayList<>(); final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT"); permissionsList.add(adminPermission); diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java new file mode 100644 index 0000000000..91fda35170 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java @@ -0,0 +1,267 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import net.javacrumbs.jsonunit.core.Option; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectMetrics; +import org.dependencytrack.model.VulnerabilityMetrics; +import org.dependencytrack.persistence.jdbi.MetricsTestDao; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; + +public class MetricsResourceTest extends ResourceTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig()); + + @Test + public void getCurrentPortfolioMetricsEmptyTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final Response response = jersey + .target("/metrics/portfolio/current") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)) + .isEqualTo(/* language=JSON */ """ + { + "components": 0, + "critical": 0, + "findings_audited": 0, + "findings_total": 0, + "findings_unaudited": 0, + "high": 0, + "inherited_risk_score": 0.0, + "low": 0, + "medium": 0, + "observed_at": "${json-unit.any-number}", + "policy_violations_audited": 0, + "policy_violations_fail": 0, + "policy_violations_info": 0, + "policy_violations_license_audited": 0, + "policy_violations_license_total": 0, + "policy_violations_license_unaudited": 0, + "policy_violations_operational_audited": 0, + "policy_violations_operational_total": 0, + "policy_violations_operational_unaudited": 0, + "policy_violations_security_audited": 0, + "policy_violations_security_total": 0, + "policy_violations_security_unaudited": 0, + "policy_violations_total": 0, + "policy_violations_unaudited": 0, + "policy_violations_warn": 0, + "projects": 0, + "suppressed": 0, + "unassigned": 0, + "vulnerabilities": 0, + "vulnerable_components": 0, + "vulnerable_projects": 0 + } + """); + } + + @Test + public void getCurrentPortfolioMetricsAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var accessibleProjectA = new Project(); + accessibleProjectA.setName("acme-app-a"); + accessibleProjectA.addAccessTeam(super.team); + qm.persist(accessibleProjectA); + + final var accessibleProjectB = new Project(); + accessibleProjectB.setName("acme-app-b"); + accessibleProjectB.addAccessTeam(super.team); + qm.persist(accessibleProjectB); + + final var inactiveAccessibleProject = new Project(); + inactiveAccessibleProject.setName("acme-app-inactive"); + inactiveAccessibleProject.setInactiveSince(new Date()); + inactiveAccessibleProject.addAccessTeam(super.team); + qm.persist(inactiveAccessibleProject); + + final var inaccessibleProject = new Project(); + inaccessibleProject.setName("acme-app-inaccessible"); + qm.persist(inaccessibleProject); + + final var today = LocalDate.now(); + + useJdbiHandle(handle -> { + var dao = handle.attach(MetricsTestDao.class); + + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1)); + dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2)); + + { + // Create metrics for "yesterday". + + var accessibleProjectAMetrics = new ProjectMetrics(); + accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId()); + accessibleProjectAMetrics.setComponents(2); + accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectAMetrics); + } + + { + // Create metrics for "today". + + // Do not create metrics for accessibleProjectA. + // Its metrics from "yesterday" are supposed to carry over to "today". + + var accessibleProjectBMetrics = new ProjectMetrics(); + accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId()); + accessibleProjectBMetrics.setComponents(1); + accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant())); + accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence()); + dao.createProjectMetrics(accessibleProjectBMetrics); + + // Metrics of inactive projects must not be considered. + var inactiveAccessibleProjectMetrics = new ProjectMetrics(); + inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId()); + inactiveAccessibleProjectMetrics.setComponents(111); + inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant())); + inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inactiveAccessibleProjectMetrics); + + // Metrics of inaccessible projects must not be considered. + var inaccessibleProjectMetrics = new ProjectMetrics(); + inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId()); + inaccessibleProjectMetrics.setComponents(666); + inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant())); + inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence()); + dao.createProjectMetrics(inaccessibleProjectMetrics); + } + }); + + final Response response = jersey + .target("/metrics/portfolio/current") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)) + .withOptions(Option.IGNORING_EXTRA_FIELDS) + .isEqualTo(/* language=JSON */ """ + { + "projects": 2, + "components": 3 + } + """); + } + + @Test + public void getVulnerabilityMetricsPaginated() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + for (int i = 1; i < 4; i++) { + var metrics = new VulnerabilityMetrics(); + metrics.setYear(2025); + metrics.setMonth(i); + metrics.setCount(i); + metrics.setMeasuredAt(Date.from(Instant.now())); + qm.persist(metrics); + } + + Response response = jersey.target("/metrics/vulnerabilities") + .queryParam("limit", 2) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + final JsonObject responseJson = parseJsonObject(response); + assertThatJson(responseJson.toString()).isEqualTo(/* language=JSON */ """ + { + "metrics" : + [ + { + "observed_at" : "${json-unit.any-number}", + "year" : 2025, + "month" : 1, + "count" : 1 + }, + { + "observed_at" : "${json-unit.any-number}", + "year" : 2025, + "month" : 2, + "count" : 2 + } + ], + "_pagination" : { + "links" : { + "self" : "${json-unit.any-string}", + "next": "${json-unit.any-string}" + } + } + } + """); + + final var nextPageUri = URI.create( + responseJson + .getJsonObject("_pagination") + .getJsonObject("links") + .getString("next")); + + response = jersey.target(nextPageUri) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "metrics" : + [ + { + "observed_at" : "${json-unit.any-number}", + "year" : 2025, + "month" : 3, + "count" : 3 + } + ], + "_pagination": { + "links": { + "self": "${json-unit.any-string}" + } + } + } + """); + } +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiResourceTest.java new file mode 100644 index 0000000000..d6bbc772ac --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiResourceTest.java @@ -0,0 +1,54 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import org.dependencytrack.JerseyTestRule; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.ws.rs.core.Response; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION; + +public class OpenApiResourceTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig()); + + @Test + public void shouldReturnSpecYaml() { + final Response response = jersey.target("/openapi.yaml") + .request() + .property(DISABLE_OPENAPI_VALIDATION, "true") + .get(Response.class); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/yaml"); + + final String openApiYaml = response.readEntity(String.class); + final SwaggerParseResult parseResult = new OpenAPIParser().readContents(openApiYaml, null, null); + + final List validationMessages = parseResult.getMessages(); + assertThat(validationMessages).isEmpty(); + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiValidationClientResponseFilter.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiValidationClientResponseFilter.java new file mode 100644 index 0000000000..5a228716fd --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiValidationClientResponseFilter.java @@ -0,0 +1,186 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.InputFormat; +import com.networknt.schema.JsonMetaSchema; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.NonValidationKeyword; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import com.networknt.schema.oas.OpenApi30; +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.parser.ObjectMapperFactory; +import io.swagger.v3.parser.core.models.ParseOptions; +import org.junit.Assert; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @since 5.6.0 + */ +public class OpenApiValidationClientResponseFilter implements ClientResponseFilter { + + public static final String DISABLE_OPENAPI_VALIDATION = "disable-openapi-validation"; + + private static final JsonSchemaFactory SCHEMA_FACTORY = + JsonSchemaFactory.getInstance( + SpecVersion.VersionFlag.V4, + builder -> builder + .metaSchema(JsonMetaSchema.builder(OpenApi30.getInstance()) + .keyword(new NonValidationKeyword("exampleSetFlag")) + .keyword(new NonValidationKeyword("extensions")) + .keyword(new NonValidationKeyword("types")) + .build()) + .defaultMetaSchemaIri(OpenApi30.getInstance().getIri())); + + private final OpenAPI openApiSpec; + private final ObjectMapper objectMapper; + + public OpenApiValidationClientResponseFilter() { + this.openApiSpec = loadOpenApiSpec(); + this.objectMapper = ObjectMapperFactory.createJson(); + } + + @Override + public void filter( + final ClientRequestContext requestContext, + final ClientResponseContext responseContext) throws IOException { + if (requestContext.hasProperty(DISABLE_OPENAPI_VALIDATION)) { + return; + } + + final Operation operationDef = findOpenApiOperation(requestContext); + if (operationDef == null) { + // Undocumented request? + Assert.fail("No OpenAPI operation found for %s %s".formatted( + requestContext.getMethod(), requestContext.getUri())); + } + + // Read the response content and assign it back to the response context. + // Without this, clients won't be able to read the response anymore. + final byte[] responseBytes = responseContext.getEntityStream().readAllBytes(); + responseContext.setEntityStream(new ByteArrayInputStream(responseBytes)); + + // Identity the correct response object in the spec based on the status. + final String responseStatus = String.valueOf(responseContext.getStatus()); + assertThat(operationDef.getResponses().keySet()).contains(responseStatus); + final ApiResponse responseDef = operationDef.getResponses().get(responseStatus); + + // If the spec does not define a response, ensure that the actual + // response is also empty. + if (responseDef.getContent() == null) { + assertThat(responseBytes).asString().isEmpty(); + return; + } + + // Identity the correct media type in the spec response. + final String responseContentType = responseContext.getHeaderString("Content-Type"); + assertThat(responseDef.getContent().keySet()).contains(responseContentType); + final MediaType mediaType = responseDef.getContent().get(responseContentType); + + // Serialize the response schema to JSON so it can be used for validation. + // NB: The schema already has all $refs resolved so can be handled "standalone". + final String schemaJson = objectMapper.writeValueAsString(mediaType.getSchema()); + final JsonSchema schema = SCHEMA_FACTORY.getSchema(schemaJson); + + final Set messages = schema.validate( + new String(responseBytes), InputFormat.JSON); + assertThat(messages).isEmpty(); + } + + private Operation findOpenApiOperation(final ClientRequestContext requestContext) { + final String requestPath = requestContext.getUri().getPath(); + + for (final String specPath : openApiSpec.getPaths().keySet()) { + if (!pathsMatch(requestPath, specPath)) { + continue; + } + + final PathItem pathItem = openApiSpec.getPaths().get(specPath); + return switch (requestContext.getMethod()) { + case "DELETE" -> pathItem.getDelete(); + case "GET" -> pathItem.getGet(); + case "HEAD" -> pathItem.getHead(); + case "OPTIONS" -> pathItem.getOptions(); + case "PATCH" -> pathItem.getPatch(); + case "POST" -> pathItem.getPost(); + case "PUT" -> pathItem.getPut(); + case "TRACE" -> pathItem.getTrace(); + default -> null; + }; + } + + return null; + } + + private boolean pathsMatch(final String requestPath, final String specPath) { + final String[] requestPathSegments = requestPath.split("/"); + final String[] specPathSegments = specPath.split("/"); + + if (requestPathSegments.length != specPathSegments.length) { + return false; + } + + for (int i = 0; i < requestPathSegments.length; i++) { + final String requestPathSegment = requestPathSegments[i]; + final String specPathSegment = specPathSegments[i]; + + if (!requestPathSegment.equals(specPathSegment) && !specPathSegment.startsWith("{")) { + return false; + } + } + + return true; + } + + private static OpenAPI loadOpenApiSpec() { + try (final InputStream specInputStream = + OpenApiValidationClientResponseFilter.class.getResourceAsStream( + "/org/dependencytrack/api/v2/openapi.yaml")) { + requireNonNull(specInputStream); + final String specString = new String(specInputStream.readAllBytes()); + + final var parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + parseOptions.setResolveFully(true); + + return new OpenAPIParser().readContents(specString, null, parseOptions).getOpenAPI(); + } catch (IOException e) { + throw new IllegalStateException("Failed to load OpenAPI spec", e); + } + } + +} diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java new file mode 100644 index 0000000000..ad9f50b2e3 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java @@ -0,0 +1,589 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import alpine.model.Permission; +import alpine.model.Team; +import alpine.model.User; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.auth.Permissions; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.List; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class TeamsResourceTest extends ResourceTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig()); + + @Test + public void listTeamsShouldReturnPaginatedTeams() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createTeam("team0"); + qm.createTeam("team1"); + + Response response = jersey.target("/teams") + .queryParam("limit", 2) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + final JsonObject responseJson = parseJsonObject(response); + assertThatJson(responseJson.toString()).isEqualTo(/* language=JSON */ """ + { + "teams": [ + { + "name": "Test Users", + "api_keys": 1, + "members": 0 + }, + { + "name": "team0", + "api_keys": 0, + "members": 0 + } + ], + "_pagination": { + "links": { + "self": "${json-unit.any-string}", + "next": "${json-unit.any-string}" + } + } + } + """); + + final var nextPageUri = URI.create( + responseJson + .getJsonObject("_pagination") + .getJsonObject("links") + .getString("next")); + + response = jersey.target(nextPageUri) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "teams": [ + { + "name": "team1", + "api_keys": 0, + "members": 0 + } + ], + "_pagination": { + "links": { + "self": "${json-unit.any-string}" + } + } + } + """); + } + + @Test + public void createTeamShouldCreateTeam() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createPermission( + Permissions.VIEW_PORTFOLIO.name(), + Permissions.VIEW_PORTFOLIO.getDescription()); + + final Response response = jersey.target("/teams") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "name": "foo", + "permissions": [ + "VIEW_PORTFOLIO" + ] + } + """)); + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getLocation()).hasPath("/teams/foo"); + assertThat(getPlainTextBody(response)).isEmpty(); + + qm.getPersistenceManager().evictAll(); + + final Team team = qm.getTeam("foo"); + assertThat(team).isNotNull(); + assertThat(team.getPermissions()).extracting(Permission::getName).containsOnly("VIEW_PORTFOLIO"); + } + + @Test + public void createTeamShouldReturnConflictWhenAlreadyExists() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createTeam("foo"); + + final Response response = jersey.target("/teams") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "name": "foo", + "permissions": [ + "VIEW_PORTFOLIO" + ] + } + """)); + assertThat(response.getStatus()).isEqualTo(409); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status":409, + "title": "Conflict", + "detail": "The resource already exists." + } + """); + + qm.getPersistenceManager().evictAll(); + + final Team team = qm.getTeam("foo"); + assertThat(team).isNotNull(); + assertThat(team.getPermissions()).isEmpty(); + } + + @Test + public void getTeamShouldReturnTeam() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Team team = qm.createTeam("foo"); + team.setPermissions(List.of( + qm.createPermission( + Permissions.VIEW_PORTFOLIO.name(), + Permissions.VIEW_PORTFOLIO.getDescription()), + qm.createPermission( + Permissions.VIEW_VULNERABILITY.name(), + Permissions.VIEW_VULNERABILITY.getDescription()))); + + final Response response = jersey.target("/teams/foo") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "name": "foo", + "permissions": [ + "VIEW_PORTFOLIO", + "VIEW_VULNERABILITY" + ] + } + """); + } + + @Test + public void getTeamShouldReturnNotFoundWhenTeamDoesNotExist() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Response response = jersey.target("/teams/foo") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } + + @Test + public void deleteTeamShouldDeleteTeam() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createTeam("foo"); + + final Response response = jersey.target("/teams/foo") + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(204); + assertThat(getPlainTextBody(response)).isEmpty(); + + qm.getPersistenceManager().evictAll(); + + assertThat(qm.getTeam("foo")).isNull(); + } + + @Test + public void deleteTeamsShouldReturnNotFoundWhenNotExists() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Response response = jersey.target("/teams/does-not-exist") + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type": "about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } + + @Test + public void listTeamMembershipsShouldReturnPaginatedTeamMemberships() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Team teamA = qm.createTeam("team-a"); + qm.addUserToTeam(qm.createManagedUser("foo", "password"), teamA); + qm.addUserToTeam(qm.createManagedUser("bar", "password"), teamA); + + final Team teamB = qm.createTeam("team-b"); + qm.addUserToTeam(qm.createManagedUser("aaa", "password"), teamB); + + Response response = jersey.target("/team-memberships") + .queryParam("limit", 2) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + final JsonObject responseJson = parseJsonObject(response); + assertThatJson(responseJson.toString()).isEqualTo(/* language=JSON */ """ + { + "memberships": [ + { + "team_name": "team-a", + "username": "bar" + }, + { + "team_name": "team-a", + "username": "foo" + } + ], + "_pagination": { + "links": { + "self": "${json-unit.any-string}", + "next": "${json-unit.any-string}" + } + } + } + """); + + final var nextPageUri = URI.create( + responseJson + .getJsonObject("_pagination") + .getJsonObject("links") + .getString("next")); + + response = jersey.target(nextPageUri) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "memberships": [ + { + "team_name": "team-b", + "username": "aaa" + } + ], + "_pagination": { + "links": { + "self": "${json-unit.any-string}" + } + } + } + """); + } + + @Test + public void listTeamMembershipsShouldFilterByTeamName() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Team teamA = qm.createTeam("team-a"); + qm.addUserToTeam(qm.createManagedUser("foo", "password"), teamA); + + final Team teamB = qm.createTeam("team-b"); + qm.addUserToTeam(qm.createManagedUser("bar", "password"), teamB); + + Response response = jersey.target("/team-memberships") + .queryParam("team", "team-b") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "memberships": [ + { + "team_name": "team-b", + "username": "bar" + } + ], + "_pagination": { + "links": { + "self": "${json-unit.any-string}" + } + } + } + """); + } + + @Test + public void listTeamMembershipsShouldFilterByUsername() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Team teamA = qm.createTeam("team-a"); + qm.addUserToTeam(qm.createManagedUser("foo", "password"), teamA); + + final Team teamB = qm.createTeam("team-b"); + qm.addUserToTeam(qm.createManagedUser("bar", "password"), teamB); + + final Response response = jersey.target("/team-memberships") + .queryParam("user", "bar") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "memberships": [ + { + "team_name": "team-b", + "username": "bar" + } + ], + "_pagination": { + "links": { + "self": "${json-unit.any-string}" + } + } + } + """); + } + + @Test + public void createTeamMembershipShouldCreateTeamMembership() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createTeam("team-foo"); + qm.createManagedUser("user-bar", "password"); + + final Response response = jersey.target("/team-memberships") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "team_name": "team-foo", + "username": "user-bar" + } + """)); + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getLocation()).isNull(); + assertThat(getPlainTextBody(response)).isEmpty(); + + qm.getPersistenceManager().evictAll(); + + assertThat(qm.getTeam("team-foo").getUsers()) + .extracting(User::getUsername) + .contains("user-bar"); + } + + @Test + public void createTeamMembershipShouldReturnConflictWhenAlreadyExists() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Team team = qm.createTeam("team-foo"); + final User user = qm.createManagedUser("user-bar", "password"); + qm.addUserToTeam(user, team); + + final Response response = jersey.target("/team-memberships") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "team_name": "team-foo", + "username": "user-bar" + } + """)); + assertThat(response.getStatus()).isEqualTo(409); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status":409, + "title": "Conflict", + "detail": "The resource already exists." + } + """); + } + + @Test + public void createTeamMembershipShouldReturnNotFoundWhenTeamNotExists() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createManagedUser("user-bar", "password"); + + final Response response = jersey.target("/team-memberships") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "team_name": "team-foo", + "username": "user-bar" + } + """)); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } + + @Test + public void createTeamMembershipShouldReturnNotFoundWhenUserNotExists() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createTeam("team-foo"); + + final Response response = jersey.target("/team-memberships") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "team_name": "team-foo", + "username": "user-bar" + } + """)); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } + + @Test + public void deleteTeamMembershipShouldDeleteTeamMembership() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + final Team team = qm.createTeam("foo"); + qm.addUserToTeam(qm.createManagedUser("bar", "password"), team); + + final Response response = jersey.target("/team-memberships") + .queryParam("team", "foo") + .queryParam("user", "bar") + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(204); + + qm.getPersistenceManager().evictAll(); + + assertThat(team.getUsers()).isEmpty(); + } + + @Test + public void deleteTeamMembershipShouldReturnNotFoundWhenTeamDoesNotExist() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createManagedUser("bar", "password"); + + final Response response = jersey.target("/team-memberships") + .queryParam("team", "foo") + .queryParam("user", "bar") + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } + + @Test + public void deleteTeamMembershipShouldReturnNotFoundWhenUserDoesNotExist() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createTeam("foo"); + + final Response response = jersey.target("/team-memberships") + .queryParam("team", "foo") + .queryParam("user", "bar") + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } + + @Test + public void deleteTeamMembershipShouldReturnNotFoundWhenMembershipDoesNotExist() { + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT); + + qm.createTeam("foo"); + qm.createManagedUser("bar", "password"); + + final Response response = jersey.target("/team-memberships") + .queryParam("team", "foo") + .queryParam("user", "bar") + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/WorkflowsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/WorkflowsResourceTest.java new file mode 100644 index 0000000000..5e9edbf004 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/WorkflowsResourceTest.java @@ -0,0 +1,133 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2; + +import net.javacrumbs.jsonunit.core.Option; +import org.apache.http.HttpStatus; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.WorkflowState; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.ws.rs.core.Response; +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.model.WorkflowStatus.COMPLETED; +import static org.dependencytrack.model.WorkflowStatus.PENDING; +import static org.dependencytrack.model.WorkflowStep.BOM_CONSUMPTION; +import static org.dependencytrack.model.WorkflowStep.BOM_PROCESSING; +import static org.hamcrest.CoreMatchers.equalTo; + +public class WorkflowsResourceTest extends ResourceTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig()); + + @Test + public void getWorkflowStatusOk() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + UUID uuid = UUID.randomUUID(); + WorkflowState workflowState1 = new WorkflowState(); + workflowState1.setParent(null); + workflowState1.setFailureReason(null); + workflowState1.setStep(BOM_CONSUMPTION); + workflowState1.setStatus(COMPLETED); + workflowState1.setToken(uuid); + workflowState1.setUpdatedAt(new Date()); + var workflowState1Persisted = qm.persist(workflowState1); + + WorkflowState workflowState2 = new WorkflowState(); + workflowState2.setParent(workflowState1Persisted); + workflowState2.setFailureReason(null); + workflowState2.setStep(BOM_PROCESSING); + workflowState2.setStatus(PENDING); + workflowState2.setToken(uuid); + workflowState2.setStartedAt(Date.from(Instant.now())); + workflowState2.setUpdatedAt(Date.from(Instant.now())); + qm.persist(workflowState2); + + Response response = jersey.target("/workflows/" + uuid).request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + final String jsonResponse = getPlainTextBody(response); + assertThatJson(jsonResponse) + .withOptions(Option.IGNORING_ARRAY_ORDER) + .withMatcher("token", equalTo(uuid.toString())) + .withMatcher("step1", equalTo("BOM_CONSUMPTION")) + .withMatcher("status1", equalTo("COMPLETED")) + .withMatcher("step2", equalTo("BOM_PROCESSING")) + .withMatcher("status2", equalTo("PENDING")) + .isEqualTo(/* language=JSON */ """ + { + "states": [ + { + "token": "${json-unit.matches:token}", + "step": "${json-unit.matches:step1}", + "status": "${json-unit.matches:status1}", + "updated_at": "${json-unit.any-number}" + }, + { + "token": "${json-unit.matches:token}", + "started_at": "${json-unit.any-number}", + "updated_at": "${json-unit.any-number}", + "step": "${json-unit.matches:step2}", + "status": "${json-unit.matches:status2}" + } + ] + } + """); + } + + @Test + public void getWorkflowStatusNotFound() { + initializeWithPermissions(Permissions.BOM_UPLOAD); + + WorkflowState workflowState1 = new WorkflowState(); + workflowState1.setParent(null); + workflowState1.setFailureReason(null); + workflowState1.setStep(BOM_CONSUMPTION); + workflowState1.setStatus(COMPLETED); + workflowState1.setToken(UUID.randomUUID()); + workflowState1.setUpdatedAt(new Date()); + qm.persist(workflowState1); + + UUID randomUuid = UUID.randomUUID(); + Response response = jersey.target("/workflows/" + randomUuid).request() + .header(X_API_KEY, apiKey) + .get(); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_NOT_FOUND); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "type":"about:blank", + "status": 404, + "title": "Not Found", + "detail": "The requested resource could not be found." + } + """); + } +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapperTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapperTest.java new file mode 100644 index 0000000000..dce59268f2 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapperTest.java @@ -0,0 +1,96 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import alpine.server.auth.AuthenticationNotRequired; +import net.javacrumbs.jsonunit.core.Option; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.resources.v2.ResourceConfig; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.validation.constraints.Pattern; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION; + +public class ConstraintViolationExceptionMapperTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig() + .register(JsonProcessingExceptionMapperTest.TestResource.class)); + + @Test + public void test() { + final Response response = jersey.target("/test/not-a-uuid") + .queryParam("foo", "666") + .request() + .property(DISABLE_OPENAPI_VALIDATION, "true") + .get(); + assertThat(response.getStatus()).isEqualTo(400); + assertThatJson(response.readEntity(String.class)) + .withOptions(Option.IGNORING_ARRAY_ORDER) + .isEqualTo(/* language=JSON */ """ + { + "type": "about:blank", + "status": 400, + "title": "Bad Request", + "detail": "The request could not be processed because it failed validation.", + "errors": [ + { + "message": "must match \\"^[a-z]+$\\"", + "path": "get.foo", + "value": "666" + }, + { + "message": "Invalid UUID", + "path": "get.uuid", + "value": "not-a-uuid" + } + ] + } + """); + } + + @Path("/test") + public static class TestResource { + + @GET + @Path("/{uuid}") + @Produces(MediaType.APPLICATION_JSON) + @AuthenticationNotRequired + public Response get(@PathParam("uuid") @ValidUuid final String uuid, + @QueryParam("optionalUuid") @ValidUuid final String optionalUuid, + @QueryParam("foo") @Pattern(regexp = "^[a-z]+$") final String foo) { + return Response.noContent().build(); + } + + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapperTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapperTest.java new file mode 100644 index 0000000000..c2172663ea --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapperTest.java @@ -0,0 +1,96 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import alpine.server.auth.AuthenticationNotRequired; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.resources.v2.ResourceConfig; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.core.Response; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION; + +public class DefaultExceptionMapperTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig() + .register(JsonProcessingExceptionMapperTest.TestResource.class)); + + @Test + public void shouldReturnInternalServerError() { + final Response response = jersey.target("/test/error") + .request() + .property(DISABLE_OPENAPI_VALIDATION, "true") + .get(); + assertThat(response.getStatus()).isEqualTo(500); + assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """ + { + "type": "about:blank", + "status": 500, + "title": "Unexpected error", + "detail": "An error occurred that was not anticipated." + } + """); + } + + @Test + public void shouldReturnGivenStatusForServerErrors() { + final Response response = jersey.target("/test/server-error") + .request() + .property(DISABLE_OPENAPI_VALIDATION, "true") + .get(); + assertThat(response.getStatus()).isEqualTo(503); + assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """ + { + "type": "about:blank", + "status": 503, + "title": "Unexpected error", + "detail": "An error occurred that was not anticipated." + } + """); + } + + @Path("/test") + public static class TestResource { + + @GET + @Path("/error") + @AuthenticationNotRequired + public Response fail() throws Exception { + throw new ClassNotFoundException("test"); + } + + @GET + @Path("/server-error") + @AuthenticationNotRequired + public Response serverError() { + throw new ServerErrorException(Response.status(Response.Status.SERVICE_UNAVAILABLE).build()); + } + + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapperTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapperTest.java new file mode 100644 index 0000000000..a437321487 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapperTest.java @@ -0,0 +1,105 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v2.exception; + +import alpine.server.auth.AuthenticationNotRequired; +import com.fasterxml.jackson.core.JsonGenerationException; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.resources.v2.ResourceConfig; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION; + +public class JsonProcessingExceptionMapperTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig() + .register(TestResource.class)); + + @Test + public void shouldReturnInternalServerErrorForServerSideJsonException() { + final Response response = jersey.target("/test/json-generation") + .request() + .property(DISABLE_OPENAPI_VALIDATION, "true") + .get(); + assertThat(response.getStatus()).isEqualTo(500); + assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """ + { + "type": "about:blank", + "status": 500, + "title": "Unexpected error", + "detail": "An error occurred that was not anticipated." + } + """); + } + + @Test + public void shouldReturnBadRequestForClientSideJsonException() { + final Response response = jersey.target("/test") + .request() + .property(DISABLE_OPENAPI_VALIDATION, "true") + .post(Entity.json("[]")); + assertThat(response.getStatus()).isEqualTo(400); + assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """ + { + "type": "about:blank", + "status": 400, + "title": "JSON Processing Failed", + "detail": "The provided JSON could not be processed." + } + """); + } + + @Path("/test") + public static class TestResource { + + public record TestRequest(String name) { + } + + @POST + @Path("/") + @Consumes(MediaType.APPLICATION_JSON) + @AuthenticationNotRequired + public Response test(final TestRequest request) { + return Response.ok(request.name()).build(); + } + + @GET + @Path("/json-generation") + @AuthenticationNotRequired + @SuppressWarnings("deprecation") + public Response jsonGeneration() throws Exception { + throw new JsonGenerationException("boom"); + } + + } + +} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index c362035c5f..17010c4d2f 100644 --- a/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -47,7 +47,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.VulnerabilityScan; import org.dependencytrack.model.WorkflowStep; -import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.DatabaseSeedingInitTask; import org.dependencytrack.plugin.PluginManager; import org.dependencytrack.proto.filestorage.v1.FileMetadata; import org.dependencytrack.proto.notification.v1.BomProcessingFailedSubject; @@ -99,6 +99,7 @@ import static org.dependencytrack.model.WorkflowStep.METRICS_UPDATE; import static org.dependencytrack.model.WorkflowStep.POLICY_EVALUATION; import static org.dependencytrack.model.WorkflowStep.VULN_ANALYSIS; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSED; import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSING_FAILED; import static org.dependencytrack.proto.notification.v1.Level.LEVEL_ERROR; @@ -127,7 +128,7 @@ public void before() throws Exception { @Test public void informTest() throws Exception { // Required for license resolution. - DefaultObjectGenerator.loadDefaultLicenses(); + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses); Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); @@ -283,7 +284,7 @@ public void informTest() throws Exception { @Test public void informTestWithComponentAlreadyExistsForIntegrityCheck() throws Exception { // Required for license resolution. - DefaultObjectGenerator.loadDefaultLicenses(); + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses); Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); @@ -1747,7 +1748,7 @@ public void informWithEmptyComponentAndServiceNameTest() throws Exception { @Test public void informBomWithProtobufFormat() throws Exception { - DefaultObjectGenerator.loadDefaultLicenses(); + useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses); Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java index c29120d2be..0e45f80bd6 100644 --- a/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java +++ b/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java @@ -22,7 +22,6 @@ import org.dependencytrack.event.maintenance.MetricsMaintenanceEvent; import org.dependencytrack.model.Component; import org.dependencytrack.model.DependencyMetrics; -import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.persistence.jdbi.MetricsDao; @@ -104,14 +103,6 @@ public void test() throws Exception { metricsTestDao.createProjectMetrics(metrics); }; - final BiConsumer createPortfolioMetricsForLastOccurrence = (lastOccurrence, vulns) -> { - var metrics = new PortfolioMetrics(); - metrics.setVulnerabilities(vulns); - metrics.setFirstOccurrence(Date.from(lastOccurrence)); - metrics.setLastOccurrence(Date.from(lastOccurrence)); - metricsTestDao.createPortfolioMetrics(metrics); - }; - final Instant now = Instant.now(); // Create component metrics partitions for dates required @@ -132,15 +123,6 @@ public void test() throws Exception { createProjectMetricsForLastOccurrence.accept(now.minus(90, ChronoUnit.DAYS), 90); createProjectMetricsForLastOccurrence.accept(now.minus(89, ChronoUnit.DAYS), 89); - // Create portfolio metrics partitions for dates required - metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 91); - metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 90); - metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 89); - - createPortfolioMetricsForLastOccurrence.accept(now.minus(91, ChronoUnit.DAYS), 91); - createPortfolioMetricsForLastOccurrence.accept(now.minus(90, ChronoUnit.DAYS), 90); - createPortfolioMetricsForLastOccurrence.accept(now.minus(89, ChronoUnit.DAYS), 89); - final var task = new MetricsMaintenanceTask(); assertThatNoException().isThrownBy(() -> task.inform(new MetricsMaintenanceEvent())); @@ -149,21 +131,22 @@ public void test() throws Exception { assertThat(metricsDao.getProjectMetricsSince(project.getId(), now.minus(91, ChronoUnit.DAYS))).satisfiesExactly( metrics -> assertThat(metrics.getVulnerabilities()).isEqualTo(89)); - - assertThat(metricsDao.getPortfolioMetricsSince(now.minus(91, ChronoUnit.DAYS))).satisfiesExactly( - metrics -> assertThat(metrics.getVulnerabilities()).isEqualTo(89)); } @Test public void testCreateMetricsPartitions() { + qm.createConfigProperty( + MAINTENANCE_METRICS_RETENTION_DAYS.getGroupName(), + MAINTENANCE_METRICS_RETENTION_DAYS.getPropertyName(), + MAINTENANCE_METRICS_RETENTION_DAYS.getDefaultPropertyValue(), + MAINTENANCE_METRICS_RETENTION_DAYS.getPropertyType(), + MAINTENANCE_METRICS_RETENTION_DAYS.getDescription()); + new MetricsMaintenanceTask().inform(new MetricsMaintenanceEvent()); var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE); - var metricsPartitions = metricsDao.getPortfolioMetricsPartitions(); - assertThat(metricsPartitions.getFirst()).isEqualTo("\"PORTFOLIOMETRICS_%s\"".formatted(today)); - assertThat(metricsPartitions.getLast()).isEqualTo("\"PORTFOLIOMETRICS_%s\"".formatted(tomorrow)); - metricsPartitions = metricsDao.getProjectMetricsPartitions(); + var metricsPartitions = metricsDao.getProjectMetricsPartitions(); assertThat(metricsPartitions.getFirst()).isEqualTo("\"PROJECTMETRICS_%s\"".formatted(today)); assertThat(metricsPartitions.getLast()).isEqualTo("\"PROJECTMETRICS_%s\"".formatted(tomorrow)); diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java index 54fe961d2d..a87b291395 100644 --- a/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java +++ b/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java @@ -42,14 +42,12 @@ import org.junit.BeforeClass; import org.junit.Test; -import java.time.Duration; -import java.time.Instant; import java.util.Collections; import java.util.Date; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; import static org.dependencytrack.tasks.metrics.PortfolioMetricsUpdateTask.partition; @@ -107,49 +105,6 @@ public void testUpdateMetricsEmpty() { assertThat(metrics.getPolicyViolationsOperationalUnaudited()).isZero(); } - @Test - public void testUpdateMetricsUnchanged() throws Exception { - // Create risk score configproperties - createTestConfigProperties(); - - // Record initial portfolio metrics - new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent()); - final PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); - assertThat(metrics.getLastOccurrence()).isEqualTo(metrics.getFirstOccurrence()); - - //sleep for the least duration lock held for, so lock could be released - Thread.sleep(2000); - - // Run the task a second time, without any metric being changed - final var beforeSecondRun = new Date(); - new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent()); - - // Two records should be created in today's partition since it's append-only - var totalMetricsForToday = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getPortfolioMetricsSince(Instant.now().minus(Duration.ofDays(1)))); - assertThat(totalMetricsForToday.size()).isEqualTo(2); - var recentMetrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); - assertThat(recentMetrics.getLastOccurrence()).isNotEqualTo(metrics.getFirstOccurrence()); - assertThat(recentMetrics.getLastOccurrence()).isAfterOrEqualTo(beforeSecondRun); - } - - @Test - public void testUpdateMetricsDidNotExecuteWhenLockWasHeld() throws Exception { - // Create risk score configproperties - createTestConfigProperties(); - - // Record initial portfolio metrics - new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent()); - PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); - assertThat(metrics.getLastOccurrence()).isEqualTo(metrics.getFirstOccurrence()); - - // Run the task a second time, without any metric being changed - new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent()); - - // Ensure no new record of metrics is appended - var totalMetricsForToday = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getPortfolioMetricsSince(Instant.now().minus(Duration.ofDays(1)))); - assertThat(totalMetricsForToday.size()).isEqualTo(1); - } - @Test public void testUpdateMetricsVulnerabilities() { var vuln = new Vulnerability(); @@ -173,7 +128,7 @@ public void testUpdateMetricsVulnerabilities() { var projectAudited = new Project(); projectAudited.setName("acme-app-b"); qm.createProject(projectAudited, List.of(), false); - + // Create risk score configproperties createTestConfigProperties(); @@ -189,7 +144,7 @@ public void testUpdateMetricsVulnerabilities() { var projectSuppressed = new Project(); projectSuppressed.setName("acme-app-c"); qm.createProject(projectSuppressed, List.of(), false); - + var componentSuppressed = new Component(); componentSuppressed.setProject(projectSuppressed); componentSuppressed.setName("acme-lib-c"); @@ -198,34 +153,6 @@ public void testUpdateMetricsVulnerabilities() { withJdbiHandle(handle -> handle.attach(AnalysisDao.class) .makeAnalysis(projectSuppressed.getId(), componentSuppressed.getId(), vuln.getId(), AnalysisState.FALSE_POSITIVE, null, null, null, true)); - // Create "old" metrics data points for all three projects. - // When the calculating portfolio metrics, only the latest data point for each project - // must be considered. Because the update task calculates new project metrics data points, - // the ones created below must be ignored. - useJdbiHandle(handle -> { - var dao = handle.attach(MetricsTestDao.class); - final var projectUnauditedOldMetrics = new ProjectMetrics(); - projectUnauditedOldMetrics.setProjectId(projectUnaudited.getId()); - projectUnauditedOldMetrics.setCritical(666); - projectUnauditedOldMetrics.setFirstOccurrence(Date.from(Instant.now())); - projectUnauditedOldMetrics.setLastOccurrence(Date.from(Instant.now())); - dao.createProjectMetrics(projectUnauditedOldMetrics); - - final var projectAuditedOldMetrics = new ProjectMetrics(); - projectAuditedOldMetrics.setProjectId(projectAudited.getId()); - projectAuditedOldMetrics.setHigh(666); - projectAuditedOldMetrics.setFirstOccurrence(Date.from(Instant.now())); - projectAuditedOldMetrics.setLastOccurrence(Date.from(Instant.now())); - dao.createProjectMetrics(projectAuditedOldMetrics); - - final var projectSuppressedOldMetrics = new ProjectMetrics(); - projectSuppressedOldMetrics.setProjectId(projectSuppressed.getId()); - projectSuppressedOldMetrics.setMedium(666); - projectSuppressedOldMetrics.setFirstOccurrence(Date.from(Instant.now())); - projectSuppressedOldMetrics.setLastOccurrence(Date.from(Instant.now())); - dao.createProjectMetrics(projectSuppressedOldMetrics); - }); - new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent()); final PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); @@ -276,10 +203,10 @@ public void testUpdateMetricsPolicyViolations() { var projectUnaudited = new Project(); projectUnaudited.setName("acme-app-a"); qm.createProject(projectUnaudited, List.of(), false); - + // Create risk score configproperties createTestConfigProperties(); - + var componentUnaudited = new Component(); componentUnaudited.setProject(projectUnaudited); componentUnaudited.setName("acme-lib-a"); @@ -290,7 +217,7 @@ public void testUpdateMetricsPolicyViolations() { var projectAudited = new Project(); projectAudited.setName("acme-app-b"); qm.createProject(projectAudited, List.of(), false); - + var componentAudited = new Component(); componentAudited.setProject(projectAudited); componentAudited.setName("acme-lib-b"); @@ -302,7 +229,7 @@ public void testUpdateMetricsPolicyViolations() { var projectSuppressed = new Project(); projectSuppressed.setName("acme-app-c"); qm.createProject(projectSuppressed, List.of(), false); - + var componentSuppressed = new Component(); componentSuppressed.setProject(projectSuppressed); componentSuppressed.setName("acme-lib-c"); @@ -310,34 +237,6 @@ public void testUpdateMetricsPolicyViolations() { final var violationSuppressed = createPolicyViolation(componentSuppressed, Policy.ViolationState.INFO, PolicyViolation.Type.SECURITY); qm.makeViolationAnalysis(componentSuppressed, violationSuppressed, ViolationAnalysisState.REJECTED, true); - // Create "old" metrics data points for all three projects. - // When the calculating portfolio metrics, only the latest data point for each project - // must be considered. Because the update task calculates new project metrics data points, - // the ones created below must be ignored. - useJdbiHandle(handle -> { - var dao = handle.attach(MetricsTestDao.class); - final var projectUnauditedOldMetrics = new ProjectMetrics(); - projectUnauditedOldMetrics.setProjectId(projectUnaudited.getId()); - projectUnauditedOldMetrics.setPolicyViolationsFail(666); - projectUnauditedOldMetrics.setFirstOccurrence(Date.from(Instant.now())); - projectUnauditedOldMetrics.setLastOccurrence(Date.from(Instant.now())); - dao.createProjectMetrics(projectUnauditedOldMetrics); - - final var projectAuditedOldMetrics = new ProjectMetrics(); - projectAuditedOldMetrics.setProjectId(projectAudited.getId()); - projectAuditedOldMetrics.setPolicyViolationsWarn(666); - projectAuditedOldMetrics.setFirstOccurrence(Date.from(Instant.now())); - projectAuditedOldMetrics.setLastOccurrence(Date.from(Instant.now())); - dao.createProjectMetrics(projectAuditedOldMetrics); - - final var projectSuppressedOldMetrics = new ProjectMetrics(); - projectSuppressedOldMetrics.setProjectId(projectSuppressed.getId()); - projectSuppressedOldMetrics.setPolicyViolationsInfo(666); - projectSuppressedOldMetrics.setFirstOccurrence(Date.from(Instant.now())); - projectSuppressedOldMetrics.setLastOccurrence(Date.from(Instant.now())); - dao.createProjectMetrics(projectSuppressedOldMetrics); - }); - new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent()); final PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics()); @@ -382,6 +281,64 @@ public void testUpdateMetricsPolicyViolations() { assertThat(componentSuppressed.getLastInheritedRiskScore()).isZero(); } + @Test + public void shouldNotUpdateMetricsForProjectsWithRecentMetrics() { + createTestConfigProperties(); + + final var projectA = new Project(); + projectA.setName("acme-app-a"); + qm.persist(projectA); + final var componentA = new Component(); + componentA.setProject(projectA); + componentA.setName("acme-lib-a"); + qm.persist(componentA); + + final var projectB = new Project(); + projectB.setName("acme-app-b"); + qm.persist(projectB); + final var componentB = new Component(); + componentB.setProject(projectB); + componentB.setName("acme-lib-b"); + qm.persist(componentB); + + final var inactiveProject = new Project(); + inactiveProject.setName("inactive-project"); + inactiveProject.setInactiveSince(new Date()); + qm.persist(inactiveProject); + + // Create a metrics data point for projectA, where it has no components. + // Despite this difference, we expect no metrics refresh to be performed + // for it, because a data point for the current day is already present. + useJdbiTransaction(handle -> { + var dao = handle.attach(MetricsTestDao.class); + final var projectAMetrics = new ProjectMetrics(); + projectAMetrics.setProjectId(projectA.getId()); + projectAMetrics.setComponents(0); + projectAMetrics.setFirstOccurrence(new Date()); + projectAMetrics.setLastOccurrence(new Date()); + dao.createProjectMetrics(projectAMetrics); + }); + + new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent()); + + final List recentProjectMetrics = withJdbiHandle( + handle -> handle.attach(MetricsDao.class) + .getMostRecentProjectMetrics( + List.of(projectA.getId(), projectB.getId(), inactiveProject.getId()))); + + assertThat(recentProjectMetrics).satisfiesExactlyInAnyOrder( + metrics -> { + assertThat(metrics.getProjectId()).isEqualTo(projectA.getId()); + assertThat(metrics.getComponents()).isEqualTo(0); // Old value. + }, + metrics -> { + assertThat(metrics.getProjectId()).isEqualTo(projectB.getId()); + assertThat(metrics.getComponents()).isEqualTo(1); + } + // No metrics for inactiveProject. + ); + } + @Test public void testPartitionWithNull() { final List list = null; diff --git a/coverage-report/pom.xml b/coverage-report/pom.xml index a1df1a2da0..8d8a2df8a1 100644 --- a/coverage-report/pom.xml +++ b/coverage-report/pom.xml @@ -25,33 +25,39 @@ org.dependencytrack alpine-common + ${project.version} provided org.dependencytrack alpine-model + ${project.version} provided org.dependencytrack alpine-infra + ${project.version} provided org.dependencytrack alpine-server + ${project.version} provided org.dependencytrack apiserver + ${project.version} classes provided org.dependencytrack datanucleus-plugin + ${project.version} provided @@ -62,11 +68,13 @@ org.dependencytrack liquibase-support + ${project.version} provided org.dependencytrack persistence-migration + ${project.version} provided
diff --git a/dev/scripts/openapi-lint.sh b/dev/scripts/openapi-lint.sh new file mode 100755 index 0000000000..ab1edd1955 --- /dev/null +++ b/dev/scripts/openapi-lint.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# This file is part of Dependency-Track. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +set -euo pipefail + +SCRIPT_DIR="$(cd -P -- "$(dirname "$0")" && pwd -P)" +API_MODULE_DIR="$(cd -P -- "${SCRIPT_DIR}/../../api" && pwd -P)" + +# NB: Currently there's no arm64 image variant. +docker run --rm -it -w /work \ + --platform linux/amd64 \ + -v "${API_MODULE_DIR}:/work" \ + stoplight/spectral lint \ + --ruleset src/main/spectral/ruleset.yaml \ + src/main/openapi/openapi.yaml \ No newline at end of file diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java index 93b9096ee6..bf2d135275 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java @@ -4,9 +4,6 @@ package org.dependencytrack.persistence.jooq.generated; -import java.util.Arrays; -import java.util.List; - import org.dependencytrack.persistence.jooq.generated.tables.AffectedVersionAttribution; import org.dependencytrack.persistence.jooq.generated.tables.Analysis; import org.dependencytrack.persistence.jooq.generated.tables.AnalysisComment; @@ -40,7 +37,6 @@ import org.dependencytrack.persistence.jooq.generated.tables.PolicyProjects; import org.dependencytrack.persistence.jooq.generated.tables.PolicyTags; import org.dependencytrack.persistence.jooq.generated.tables.PolicyViolation; -import org.dependencytrack.persistence.jooq.generated.tables.PortfolioMetrics; import org.dependencytrack.persistence.jooq.generated.tables.Project; import org.dependencytrack.persistence.jooq.generated.tables.ProjectAccessTeams; import org.dependencytrack.persistence.jooq.generated.tables.ProjectHierarchy; @@ -80,6 +76,9 @@ import org.jooq.impl.DSL; import org.jooq.impl.SchemaImpl; +import java.util.Arrays; +import java.util.List; + /** * standard public schema @@ -87,7 +86,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class DefaultSchema extends SchemaImpl { - private static final long serialVersionUID = 359281095; + private static final long serialVersionUID = -569524980; /** * The reference instance of DEFAULT_SCHEMA @@ -259,11 +258,6 @@ public class DefaultSchema extends SchemaImpl { */ public final PolicyViolation POLICYVIOLATION = PolicyViolation.POLICYVIOLATION; - /** - * The table PORTFOLIOMETRICS. - */ - public final PortfolioMetrics PORTFOLIOMETRICS = PortfolioMetrics.PORTFOLIOMETRICS; - /** * The table PROJECT. */ @@ -483,7 +477,6 @@ public final List> getTables() { PolicyTags.POLICY_TAGS, PolicyCondition.POLICYCONDITION, PolicyViolation.POLICYVIOLATION, - PortfolioMetrics.PORTFOLIOMETRICS, Project.PROJECT, ProjectAccessTeams.PROJECT_ACCESS_TEAMS, ProjectHierarchy.PROJECT_HIERARCHY, diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java index 588e607a7e..af55c5edd5 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java @@ -37,7 +37,6 @@ import org.dependencytrack.persistence.jooq.generated.tables.PolicyProjects; import org.dependencytrack.persistence.jooq.generated.tables.PolicyTags; import org.dependencytrack.persistence.jooq.generated.tables.PolicyViolation; -import org.dependencytrack.persistence.jooq.generated.tables.PortfolioMetrics; import org.dependencytrack.persistence.jooq.generated.tables.Project; import org.dependencytrack.persistence.jooq.generated.tables.ProjectAccessTeams; import org.dependencytrack.persistence.jooq.generated.tables.ProjectHierarchy; @@ -105,7 +104,6 @@ import org.dependencytrack.persistence.jooq.generated.tables.records.PolicyRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.PolicyTagsRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.PolicyViolationRecord; -import org.dependencytrack.persistence.jooq.generated.tables.records.PortfolioMetricsRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.ProjectAccessTeamsRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.ProjectHierarchyRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.ProjectMetadataRecord; @@ -208,7 +206,6 @@ public class Keys { public static final UniqueKey POLICYCONDITION_UUID_IDX = Internal.createUniqueKey(PolicyCondition.POLICYCONDITION, DSL.name("POLICYCONDITION_UUID_IDX"), new TableField[] { PolicyCondition.POLICYCONDITION.uuid }, true); public static final UniqueKey POLICYVIOLATION_PK = Internal.createUniqueKey(PolicyViolation.POLICYVIOLATION, DSL.name("POLICYVIOLATION_PK"), new TableField[] { PolicyViolation.POLICYVIOLATION.id }, true); public static final UniqueKey POLICYVIOLATION_UUID_IDX = Internal.createUniqueKey(PolicyViolation.POLICYVIOLATION, DSL.name("POLICYVIOLATION_UUID_IDX"), new TableField[] { PolicyViolation.POLICYVIOLATION.uuid }, true); - public static final UniqueKey PORTFOLIOMETRICS_PK = Internal.createUniqueKey(PortfolioMetrics.PORTFOLIOMETRICS, DSL.name("PORTFOLIOMETRICS_PK"), new TableField[] { PortfolioMetrics.PORTFOLIOMETRICS.lastOccurrence }, true); public static final UniqueKey PROJECT_PK = Internal.createUniqueKey(Project.PROJECT, DSL.name("PROJECT_PK"), new TableField[] { Project.PROJECT.id }, true); public static final UniqueKey PROJECT_UUID_IDX = Internal.createUniqueKey(Project.PROJECT, DSL.name("PROJECT_UUID_IDX"), new TableField[] { Project.PROJECT.uuid }, true); public static final UniqueKey PROJECT_ACCESS_TEAMS_PK = Internal.createUniqueKey(ProjectAccessTeams.PROJECT_ACCESS_TEAMS, DSL.name("PROJECT_ACCESS_TEAMS_PK"), new TableField[] { ProjectAccessTeams.PROJECT_ACCESS_TEAMS.projectId, ProjectAccessTeams.PROJECT_ACCESS_TEAMS.teamId }, true); diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java index d11d28607c..85ef720f61 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java @@ -4,21 +4,20 @@ package org.dependencytrack.persistence.jooq.generated; -import java.math.BigDecimal; -import java.util.UUID; - import org.dependencytrack.persistence.jooq.generated.routines.CalcRiskScore; import org.dependencytrack.persistence.jooq.generated.routines.HasProjectAccess; import org.dependencytrack.persistence.jooq.generated.routines.HasUserProjectAccess; import org.dependencytrack.persistence.jooq.generated.routines.JsonbVulnAliases; import org.dependencytrack.persistence.jooq.generated.routines.RecalcUserProjectEffectivePermissions; import org.dependencytrack.persistence.jooq.generated.routines.UpdateComponentMetrics; -import org.dependencytrack.persistence.jooq.generated.routines.UpdatePortfolioMetrics; import org.dependencytrack.persistence.jooq.generated.routines.UpdateProjectMetrics; import org.jooq.Configuration; import org.jooq.Field; import org.jooq.JSONB; +import java.math.BigDecimal; +import java.util.UUID; + /** * Convenience access to all stored procedures and functions in the default @@ -247,17 +246,6 @@ public static void updateComponentMetrics( p.execute(configuration); } - /** - * Call UPDATE_PORTFOLIO_METRICS - */ - public static void updatePortfolioMetrics( - Configuration configuration - ) { - UpdatePortfolioMetrics p = new UpdatePortfolioMetrics(); - - p.execute(configuration); - } - /** * Call UPDATE_PROJECT_METRICS */ diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java index 66f642e114..c6fe6589c7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java @@ -37,7 +37,6 @@ import org.dependencytrack.persistence.jooq.generated.tables.PolicyProjects; import org.dependencytrack.persistence.jooq.generated.tables.PolicyTags; import org.dependencytrack.persistence.jooq.generated.tables.PolicyViolation; -import org.dependencytrack.persistence.jooq.generated.tables.PortfolioMetrics; import org.dependencytrack.persistence.jooq.generated.tables.Project; import org.dependencytrack.persistence.jooq.generated.tables.ProjectAccessTeams; import org.dependencytrack.persistence.jooq.generated.tables.ProjectHierarchy; @@ -245,11 +244,6 @@ public class Tables { */ public static final PolicyViolation POLICYVIOLATION = PolicyViolation.POLICYVIOLATION; - /** - * The table PORTFOLIOMETRICS. - */ - public static final PortfolioMetrics PORTFOLIOMETRICS = PortfolioMetrics.PORTFOLIOMETRICS; - /** * The table PROJECT. */ diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java deleted file mode 100644 index 6e2c51d06f..0000000000 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is generated by jOOQ. - */ -package org.dependencytrack.persistence.jooq.generated.routines; - - -import org.dependencytrack.persistence.jooq.generated.DefaultSchema; -import org.jooq.impl.AbstractRoutine; -import org.jooq.impl.DSL; - - -/** - * This class is generated by jOOQ. - */ -@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) -public class UpdatePortfolioMetrics extends AbstractRoutine { - - private static final long serialVersionUID = 1145470713; - - /** - * Create a new routine call instance - */ - public UpdatePortfolioMetrics() { - super("UPDATE_PORTFOLIO_METRICS", DefaultSchema.DEFAULT_SCHEMA, DSL.comment("")); - setSQLUsable(false); - } -} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PortfolioMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PortfolioMetrics.java deleted file mode 100644 index 51398ac499..0000000000 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PortfolioMetrics.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * This file is generated by jOOQ. - */ -package org.dependencytrack.persistence.jooq.generated.tables; - - -import java.time.OffsetDateTime; -import java.util.Collection; - -import org.dependencytrack.persistence.jooq.generated.DefaultSchema; -import org.dependencytrack.persistence.jooq.generated.Keys; -import org.dependencytrack.persistence.jooq.generated.tables.records.PortfolioMetricsRecord; -import org.jooq.Condition; -import org.jooq.Field; -import org.jooq.Name; -import org.jooq.PlainSQL; -import org.jooq.QueryPart; -import org.jooq.SQL; -import org.jooq.Schema; -import org.jooq.Select; -import org.jooq.Stringly; -import org.jooq.Table; -import org.jooq.TableField; -import org.jooq.TableOptions; -import org.jooq.UniqueKey; -import org.jooq.impl.DSL; -import org.jooq.impl.SQLDataType; -import org.jooq.impl.TableImpl; - - -/** - * This class is generated by jOOQ. - */ -@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) -public class PortfolioMetrics extends TableImpl { - - private static final long serialVersionUID = 176854177; - - /** - * The reference instance of PORTFOLIOMETRICS - */ - public static final PortfolioMetrics PORTFOLIOMETRICS = new PortfolioMetrics(); - - /** - * The class holding records for this type - */ - @Override - public Class getRecordType() { - return PortfolioMetricsRecord.class; - } - - /** - * The column PORTFOLIOMETRICS.COMPONENTS. - */ - public final TableField components = createField(DSL.name("COMPONENTS"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.CRITICAL. - */ - public final TableField critical = createField(DSL.name("CRITICAL"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.FINDINGS_AUDITED. - */ - public final TableField findingsAudited = createField(DSL.name("FINDINGS_AUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.FINDINGS_TOTAL. - */ - public final TableField findingsTotal = createField(DSL.name("FINDINGS_TOTAL"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.FINDINGS_UNAUDITED. - */ - public final TableField findingsUnaudited = createField(DSL.name("FINDINGS_UNAUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.FIRST_OCCURRENCE. - */ - public final TableField firstOccurrence = createField(DSL.name("FIRST_OCCURRENCE"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.HIGH. - */ - public final TableField high = createField(DSL.name("HIGH"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.RISKSCORE. - */ - public final TableField riskScore = createField(DSL.name("RISKSCORE"), SQLDataType.DOUBLE.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.LAST_OCCURRENCE. - */ - public final TableField lastOccurrence = createField(DSL.name("LAST_OCCURRENCE"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.LOW. - */ - public final TableField low = createField(DSL.name("LOW"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.MEDIUM. - */ - public final TableField medium = createField(DSL.name("MEDIUM"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_AUDITED. - */ - public final TableField policyViolationsAudited = createField(DSL.name("POLICYVIOLATIONS_AUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_FAIL. - */ - public final TableField policyViolationsFail = createField(DSL.name("POLICYVIOLATIONS_FAIL"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_INFO. - */ - public final TableField policyViolationsInfo = createField(DSL.name("POLICYVIOLATIONS_INFO"), SQLDataType.INTEGER, this, ""); - - /** - * The column - * PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_AUDITED. - */ - public final TableField policyViolationsLicenseAudited = createField(DSL.name("POLICYVIOLATIONS_LICENSE_AUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_TOTAL. - */ - public final TableField policyViolationsLicenseTotal = createField(DSL.name("POLICYVIOLATIONS_LICENSE_TOTAL"), SQLDataType.INTEGER, this, ""); - - /** - * The column - * PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_UNAUDITED. - */ - public final TableField policyViolationsLicenseUnaudited = createField(DSL.name("POLICYVIOLATIONS_LICENSE_UNAUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_AUDITED. - */ - public final TableField policyViolationsOperationalAudited = createField(DSL.name("POLICYVIOLATIONS_OPERATIONAL_AUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_TOTAL. - */ - public final TableField policyViolationsOperationalTotal = createField(DSL.name("POLICYVIOLATIONS_OPERATIONAL_TOTAL"), SQLDataType.INTEGER, this, ""); - - /** - * The column - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_UNAUDITED. - */ - public final TableField policyViolationsOperationalUnaudited = createField(DSL.name("POLICYVIOLATIONS_OPERATIONAL_UNAUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column - * PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_AUDITED. - */ - public final TableField policyViolationsSecurityAudited = createField(DSL.name("POLICYVIOLATIONS_SECURITY_AUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_TOTAL. - */ - public final TableField policyViolationsSecurityTotal = createField(DSL.name("POLICYVIOLATIONS_SECURITY_TOTAL"), SQLDataType.INTEGER, this, ""); - - /** - * The column - * PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_UNAUDITED. - */ - public final TableField policyViolationsSecurityUnaudited = createField(DSL.name("POLICYVIOLATIONS_SECURITY_UNAUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_TOTAL. - */ - public final TableField policyViolationsTotal = createField(DSL.name("POLICYVIOLATIONS_TOTAL"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_UNAUDITED. - */ - public final TableField policyViolationsUnaudited = createField(DSL.name("POLICYVIOLATIONS_UNAUDITED"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.POLICYVIOLATIONS_WARN. - */ - public final TableField policyViolationsWarn = createField(DSL.name("POLICYVIOLATIONS_WARN"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.PROJECTS. - */ - public final TableField projects = createField(DSL.name("PROJECTS"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.SUPPRESSED. - */ - public final TableField suppressed = createField(DSL.name("SUPPRESSED"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.UNASSIGNED_SEVERITY. - */ - public final TableField unassignedSeverity = createField(DSL.name("UNASSIGNED_SEVERITY"), SQLDataType.INTEGER, this, ""); - - /** - * The column PORTFOLIOMETRICS.VULNERABILITIES. - */ - public final TableField vulnerabilities = createField(DSL.name("VULNERABILITIES"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.VULNERABLECOMPONENTS. - */ - public final TableField vulnerableComponents = createField(DSL.name("VULNERABLECOMPONENTS"), SQLDataType.INTEGER.nullable(false), this, ""); - - /** - * The column PORTFOLIOMETRICS.VULNERABLEPROJECTS. - */ - public final TableField vulnerableProjects = createField(DSL.name("VULNERABLEPROJECTS"), SQLDataType.INTEGER.nullable(false), this, ""); - - private PortfolioMetrics(Name alias, Table aliased) { - this(alias, aliased, (Field[]) null, null); - } - - private PortfolioMetrics(Name alias, Table aliased, Field[] parameters, Condition where) { - super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table(), where); - } - - /** - * Create an aliased PORTFOLIOMETRICS table reference - */ - public PortfolioMetrics(String alias) { - this(DSL.name(alias), PORTFOLIOMETRICS); - } - - /** - * Create an aliased PORTFOLIOMETRICS table reference - */ - public PortfolioMetrics(Name alias) { - this(alias, PORTFOLIOMETRICS); - } - - /** - * Create a PORTFOLIOMETRICS table reference - */ - public PortfolioMetrics() { - this(DSL.name("PORTFOLIOMETRICS"), null); - } - - @Override - public Schema getSchema() { - return aliased() ? null : DefaultSchema.DEFAULT_SCHEMA; - } - - @Override - public UniqueKey getPrimaryKey() { - return Keys.PORTFOLIOMETRICS_PK; - } - - @Override - public PortfolioMetrics as(String alias) { - return new PortfolioMetrics(DSL.name(alias), this); - } - - @Override - public PortfolioMetrics as(Name alias) { - return new PortfolioMetrics(alias, this); - } - - @Override - public PortfolioMetrics as(Table alias) { - return new PortfolioMetrics(alias.getQualifiedName(), this); - } - - /** - * Rename this table - */ - @Override - public PortfolioMetrics rename(String name) { - return new PortfolioMetrics(DSL.name(name), null); - } - - /** - * Rename this table - */ - @Override - public PortfolioMetrics rename(Name name) { - return new PortfolioMetrics(name, null); - } - - /** - * Rename this table - */ - @Override - public PortfolioMetrics rename(Table name) { - return new PortfolioMetrics(name.getQualifiedName(), null); - } - - /** - * Create an inline derived table from this table - */ - @Override - public PortfolioMetrics where(Condition condition) { - return new PortfolioMetrics(getQualifiedName(), aliased() ? this : null, null, condition); - } - - /** - * Create an inline derived table from this table - */ - @Override - public PortfolioMetrics where(Collection conditions) { - return where(DSL.and(conditions)); - } - - /** - * Create an inline derived table from this table - */ - @Override - public PortfolioMetrics where(Condition... conditions) { - return where(DSL.and(conditions)); - } - - /** - * Create an inline derived table from this table - */ - @Override - public PortfolioMetrics where(Field condition) { - return where(DSL.condition(condition)); - } - - /** - * Create an inline derived table from this table - */ - @Override - @PlainSQL - public PortfolioMetrics where(SQL condition) { - return where(DSL.condition(condition)); - } - - /** - * Create an inline derived table from this table - */ - @Override - @PlainSQL - public PortfolioMetrics where(@Stringly.SQL String condition) { - return where(DSL.condition(condition)); - } - - /** - * Create an inline derived table from this table - */ - @Override - @PlainSQL - public PortfolioMetrics where(@Stringly.SQL String condition, Object... binds) { - return where(DSL.condition(condition, binds)); - } - - /** - * Create an inline derived table from this table - */ - @Override - @PlainSQL - public PortfolioMetrics where(@Stringly.SQL String condition, QueryPart... parts) { - return where(DSL.condition(condition, parts)); - } - - /** - * Create an inline derived table from this table - */ - @Override - public PortfolioMetrics whereExists(Select select) { - return where(DSL.exists(select)); - } - - /** - * Create an inline derived table from this table - */ - @Override - public PortfolioMetrics whereNotExists(Select select) { - return where(DSL.notExists(select)); - } -} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java deleted file mode 100644 index d854ee4bcf..0000000000 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * This file is generated by jOOQ. - */ -package org.dependencytrack.persistence.jooq.generated.tables.records; - - -import java.time.OffsetDateTime; - -import org.dependencytrack.persistence.jooq.generated.tables.PortfolioMetrics; -import org.jooq.Record1; -import org.jooq.impl.UpdatableRecordImpl; - - -/** - * This class is generated by jOOQ. - */ -@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) -public class PortfolioMetricsRecord extends UpdatableRecordImpl { - - private static final long serialVersionUID = -1714348849; - - /** - * Setter for PORTFOLIOMETRICS.COMPONENTS. - */ - public PortfolioMetricsRecord setComponents(Integer value) { - set(0, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.COMPONENTS. - */ - public Integer getComponents() { - return (Integer) get(0); - } - - /** - * Setter for PORTFOLIOMETRICS.CRITICAL. - */ - public PortfolioMetricsRecord setCritical(Integer value) { - set(1, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.CRITICAL. - */ - public Integer getCritical() { - return (Integer) get(1); - } - - /** - * Setter for PORTFOLIOMETRICS.FINDINGS_AUDITED. - */ - public PortfolioMetricsRecord setFindingsAudited(Integer value) { - set(2, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.FINDINGS_AUDITED. - */ - public Integer getFindingsAudited() { - return (Integer) get(2); - } - - /** - * Setter for PORTFOLIOMETRICS.FINDINGS_TOTAL. - */ - public PortfolioMetricsRecord setFindingsTotal(Integer value) { - set(3, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.FINDINGS_TOTAL. - */ - public Integer getFindingsTotal() { - return (Integer) get(3); - } - - /** - * Setter for PORTFOLIOMETRICS.FINDINGS_UNAUDITED. - */ - public PortfolioMetricsRecord setFindingsUnaudited(Integer value) { - set(4, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.FINDINGS_UNAUDITED. - */ - public Integer getFindingsUnaudited() { - return (Integer) get(4); - } - - /** - * Setter for PORTFOLIOMETRICS.FIRST_OCCURRENCE. - */ - public PortfolioMetricsRecord setFirstOccurrence(OffsetDateTime value) { - set(5, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.FIRST_OCCURRENCE. - */ - public OffsetDateTime getFirstOccurrence() { - return (OffsetDateTime) get(5); - } - - /** - * Setter for PORTFOLIOMETRICS.HIGH. - */ - public PortfolioMetricsRecord setHigh(Integer value) { - set(6, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.HIGH. - */ - public Integer getHigh() { - return (Integer) get(6); - } - - /** - * Setter for PORTFOLIOMETRICS.RISKSCORE. - */ - public PortfolioMetricsRecord setRiskscore(Double value) { - set(7, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.RISKSCORE. - */ - public Double getRiskscore() { - return (Double) get(7); - } - - /** - * Setter for PORTFOLIOMETRICS.LAST_OCCURRENCE. - */ - public PortfolioMetricsRecord setLastOccurrence(OffsetDateTime value) { - set(8, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.LAST_OCCURRENCE. - */ - public OffsetDateTime getLastOccurrence() { - return (OffsetDateTime) get(8); - } - - /** - * Setter for PORTFOLIOMETRICS.LOW. - */ - public PortfolioMetricsRecord setLow(Integer value) { - set(9, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.LOW. - */ - public Integer getLow() { - return (Integer) get(9); - } - - /** - * Setter for PORTFOLIOMETRICS.MEDIUM. - */ - public PortfolioMetricsRecord setMedium(Integer value) { - set(10, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.MEDIUM. - */ - public Integer getMedium() { - return (Integer) get(10); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_AUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsAudited(Integer value) { - set(11, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_AUDITED. - */ - public Integer getPolicyviolationsAudited() { - return (Integer) get(11); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_FAIL. - */ - public PortfolioMetricsRecord setPolicyviolationsFail(Integer value) { - set(12, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_FAIL. - */ - public Integer getPolicyviolationsFail() { - return (Integer) get(12); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_INFO. - */ - public PortfolioMetricsRecord setPolicyviolationsInfo(Integer value) { - set(13, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_INFO. - */ - public Integer getPolicyviolationsInfo() { - return (Integer) get(13); - } - - /** - * Setter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_AUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsLicenseAudited(Integer value) { - set(14, value); - return this; - } - - /** - * Getter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_AUDITED. - */ - public Integer getPolicyviolationsLicenseAudited() { - return (Integer) get(14); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_TOTAL. - */ - public PortfolioMetricsRecord setPolicyviolationsLicenseTotal(Integer value) { - set(15, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_TOTAL. - */ - public Integer getPolicyviolationsLicenseTotal() { - return (Integer) get(15); - } - - /** - * Setter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_UNAUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsLicenseUnaudited(Integer value) { - set(16, value); - return this; - } - - /** - * Getter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_LICENSE_UNAUDITED. - */ - public Integer getPolicyviolationsLicenseUnaudited() { - return (Integer) get(16); - } - - /** - * Setter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_AUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsOperationalAudited(Integer value) { - set(17, value); - return this; - } - - /** - * Getter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_AUDITED. - */ - public Integer getPolicyviolationsOperationalAudited() { - return (Integer) get(17); - } - - /** - * Setter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_TOTAL. - */ - public PortfolioMetricsRecord setPolicyviolationsOperationalTotal(Integer value) { - set(18, value); - return this; - } - - /** - * Getter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_TOTAL. - */ - public Integer getPolicyviolationsOperationalTotal() { - return (Integer) get(18); - } - - /** - * Setter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_UNAUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsOperationalUnaudited(Integer value) { - set(19, value); - return this; - } - - /** - * Getter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_OPERATIONAL_UNAUDITED. - */ - public Integer getPolicyviolationsOperationalUnaudited() { - return (Integer) get(19); - } - - /** - * Setter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_AUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsSecurityAudited(Integer value) { - set(20, value); - return this; - } - - /** - * Getter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_AUDITED. - */ - public Integer getPolicyviolationsSecurityAudited() { - return (Integer) get(20); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_TOTAL. - */ - public PortfolioMetricsRecord setPolicyviolationsSecurityTotal(Integer value) { - set(21, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_TOTAL. - */ - public Integer getPolicyviolationsSecurityTotal() { - return (Integer) get(21); - } - - /** - * Setter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_UNAUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsSecurityUnaudited(Integer value) { - set(22, value); - return this; - } - - /** - * Getter for - * PORTFOLIOMETRICS.POLICYVIOLATIONS_SECURITY_UNAUDITED. - */ - public Integer getPolicyviolationsSecurityUnaudited() { - return (Integer) get(22); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_TOTAL. - */ - public PortfolioMetricsRecord setPolicyviolationsTotal(Integer value) { - set(23, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_TOTAL. - */ - public Integer getPolicyviolationsTotal() { - return (Integer) get(23); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_UNAUDITED. - */ - public PortfolioMetricsRecord setPolicyviolationsUnaudited(Integer value) { - set(24, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_UNAUDITED. - */ - public Integer getPolicyviolationsUnaudited() { - return (Integer) get(24); - } - - /** - * Setter for PORTFOLIOMETRICS.POLICYVIOLATIONS_WARN. - */ - public PortfolioMetricsRecord setPolicyviolationsWarn(Integer value) { - set(25, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.POLICYVIOLATIONS_WARN. - */ - public Integer getPolicyviolationsWarn() { - return (Integer) get(25); - } - - /** - * Setter for PORTFOLIOMETRICS.PROJECTS. - */ - public PortfolioMetricsRecord setProjects(Integer value) { - set(26, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.PROJECTS. - */ - public Integer getProjects() { - return (Integer) get(26); - } - - /** - * Setter for PORTFOLIOMETRICS.SUPPRESSED. - */ - public PortfolioMetricsRecord setSuppressed(Integer value) { - set(27, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.SUPPRESSED. - */ - public Integer getSuppressed() { - return (Integer) get(27); - } - - /** - * Setter for PORTFOLIOMETRICS.UNASSIGNED_SEVERITY. - */ - public PortfolioMetricsRecord setUnassignedSeverity(Integer value) { - set(28, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.UNASSIGNED_SEVERITY. - */ - public Integer getUnassignedSeverity() { - return (Integer) get(28); - } - - /** - * Setter for PORTFOLIOMETRICS.VULNERABILITIES. - */ - public PortfolioMetricsRecord setVulnerabilities(Integer value) { - set(29, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.VULNERABILITIES. - */ - public Integer getVulnerabilities() { - return (Integer) get(29); - } - - /** - * Setter for PORTFOLIOMETRICS.VULNERABLECOMPONENTS. - */ - public PortfolioMetricsRecord setVulnerablecomponents(Integer value) { - set(30, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.VULNERABLECOMPONENTS. - */ - public Integer getVulnerablecomponents() { - return (Integer) get(30); - } - - /** - * Setter for PORTFOLIOMETRICS.VULNERABLEPROJECTS. - */ - public PortfolioMetricsRecord setVulnerableprojects(Integer value) { - set(31, value); - return this; - } - - /** - * Getter for PORTFOLIOMETRICS.VULNERABLEPROJECTS. - */ - public Integer getVulnerableprojects() { - return (Integer) get(31); - } - - // ------------------------------------------------------------------------- - // Primary key information - // ------------------------------------------------------------------------- - - @Override - public Record1 key() { - return (Record1) super.key(); - } - - // ------------------------------------------------------------------------- - // Constructors - // ------------------------------------------------------------------------- - - /** - * Create a detached PortfolioMetricsRecord - */ - public PortfolioMetricsRecord() { - super(PortfolioMetrics.PORTFOLIOMETRICS); - } - - /** - * Create a detached, initialised PortfolioMetricsRecord - */ - public PortfolioMetricsRecord(Integer components, Integer critical, Integer findingsAudited, Integer findingsTotal, Integer findingsUnaudited, OffsetDateTime firstOccurrence, Integer high, Double riskscore, OffsetDateTime lastOccurrence, Integer low, Integer medium, Integer policyviolationsAudited, Integer policyviolationsFail, Integer policyviolationsInfo, Integer policyviolationsLicenseAudited, Integer policyviolationsLicenseTotal, Integer policyviolationsLicenseUnaudited, Integer policyviolationsOperationalAudited, Integer policyviolationsOperationalTotal, Integer policyviolationsOperationalUnaudited, Integer policyviolationsSecurityAudited, Integer policyviolationsSecurityTotal, Integer policyviolationsSecurityUnaudited, Integer policyviolationsTotal, Integer policyviolationsUnaudited, Integer policyviolationsWarn, Integer projects, Integer suppressed, Integer unassignedSeverity, Integer vulnerabilities, Integer vulnerablecomponents, Integer vulnerableprojects) { - super(PortfolioMetrics.PORTFOLIOMETRICS); - - setComponents(components); - setCritical(critical); - setFindingsAudited(findingsAudited); - setFindingsTotal(findingsTotal); - setFindingsUnaudited(findingsUnaudited); - setFirstOccurrence(firstOccurrence); - setHigh(high); - setRiskscore(riskscore); - setLastOccurrence(lastOccurrence); - setLow(low); - setMedium(medium); - setPolicyviolationsAudited(policyviolationsAudited); - setPolicyviolationsFail(policyviolationsFail); - setPolicyviolationsInfo(policyviolationsInfo); - setPolicyviolationsLicenseAudited(policyviolationsLicenseAudited); - setPolicyviolationsLicenseTotal(policyviolationsLicenseTotal); - setPolicyviolationsLicenseUnaudited(policyviolationsLicenseUnaudited); - setPolicyviolationsOperationalAudited(policyviolationsOperationalAudited); - setPolicyviolationsOperationalTotal(policyviolationsOperationalTotal); - setPolicyviolationsOperationalUnaudited(policyviolationsOperationalUnaudited); - setPolicyviolationsSecurityAudited(policyviolationsSecurityAudited); - setPolicyviolationsSecurityTotal(policyviolationsSecurityTotal); - setPolicyviolationsSecurityUnaudited(policyviolationsSecurityUnaudited); - setPolicyviolationsTotal(policyviolationsTotal); - setPolicyviolationsUnaudited(policyviolationsUnaudited); - setPolicyviolationsWarn(policyviolationsWarn); - setProjects(projects); - setSuppressed(suppressed); - setUnassignedSeverity(unassignedSeverity); - setVulnerabilities(vulnerabilities); - setVulnerablecomponents(vulnerablecomponents); - setVulnerableprojects(vulnerableprojects); - resetTouchedOnNotNull(); - } -} diff --git a/persistence-migration/pom.xml b/persistence-migration/pom.xml index 43c00a4292..da92c245b5 100644 --- a/persistence-migration/pom.xml +++ b/persistence-migration/pom.xml @@ -20,6 +20,7 @@ org.dependencytrack liquibase-support + ${project.version} test diff --git a/persistence-migration/src/main/resources/migration/changelog-procedures.xml b/persistence-migration/src/main/resources/migration/changelog-procedures.xml index 0bc3f56706..ab16a233e3 100644 --- a/persistence-migration/src/main/resources/migration/changelog-procedures.xml +++ b/persistence-migration/src/main/resources/migration/changelog-procedures.xml @@ -2,11 +2,8 @@ @@ -20,9 +17,6 @@ - - - diff --git a/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml b/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml index 683ff81b97..ac1c63620b 100644 --- a/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml +++ b/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml @@ -2648,4 +2648,17 @@ ON CONFLICT ("USER_ID", "PERMISSION_ID") DO NOTHING; + + + + DROP TABLE "PORTFOLIOMETRICS"; + DROP PROCEDURE IF EXISTS "UPDATE_PORTFOLIO_METRICS"; + + + + + + + + diff --git a/persistence-migration/src/main/resources/migration/procedures/procedure_update-portfolio-metrics.sql b/persistence-migration/src/main/resources/migration/procedures/procedure_update-portfolio-metrics.sql deleted file mode 100644 index 2226a6c685..0000000000 --- a/persistence-migration/src/main/resources/migration/procedures/procedure_update-portfolio-metrics.sql +++ /dev/null @@ -1,179 +0,0 @@ -CREATE OR REPLACE PROCEDURE "UPDATE_PORTFOLIO_METRICS"() - LANGUAGE "plpgsql" -AS -$$ -DECLARE - "v_projects" INT; -- Total number of projects in the portfolio - "v_vulnerable_projects" INT; -- Number of vulnerable projects in the portfolio - "v_components" INT; -- Total number of components in the portfolio - "v_vulnerable_components" INT; -- Number of vulnerable components in the portfolio - "v_vulnerabilities" INT; -- Total number of vulnerabilities - "v_critical" INT; -- Number of vulnerabilities with critical severity - "v_high" INT; -- Number of vulnerabilities with high severity - "v_medium" INT; -- Number of vulnerabilities with medium severity - "v_low" INT; -- Number of vulnerabilities with low severity - "v_unassigned" INT; -- Number of vulnerabilities with unassigned severity - "v_risk_score" NUMERIC; -- Inherited risk score - "v_findings_total" INT; -- Total number of findings - "v_findings_audited" INT; -- Number of audited findings - "v_findings_unaudited" INT; -- Number of unaudited findings - "v_findings_suppressed" INT; -- Number of suppressed findings - "v_policy_violations_total" INT; -- Total number of policy violations - "v_policy_violations_fail" INT; -- Number of policy violations with level fail - "v_policy_violations_warn" INT; -- Number of policy violations with level warn - "v_policy_violations_info" INT; -- Number of policy violations with level info - "v_policy_violations_audited" INT; -- Number of audited policy violations - "v_policy_violations_unaudited" INT; -- Number of unaudited policy violations - "v_policy_violations_license_total" INT; -- Total number of policy violations of type license - "v_policy_violations_license_audited" INT; -- Number of audited policy violations of type license - "v_policy_violations_license_unaudited" INT; -- Number of unaudited policy violations of type license - "v_policy_violations_operational_total" INT; -- Total number of policy violations of type operational - "v_policy_violations_operational_audited" INT; -- Number of audited policy violations of type operational - "v_policy_violations_operational_unaudited" INT; -- Number of unaudited policy violations of type operational - "v_policy_violations_security_total" INT; -- Total number of policy violations of type security - "v_policy_violations_security_audited" INT; -- Number of audited policy violations of type security - "v_policy_violations_security_unaudited" INT; -- Number of unaudited policy violations of type security -BEGIN - -- Aggregate over all most recent DEPENDENCYMETRICS. - -- NOTE: SUM returns NULL when no rows match the query, but COUNT returns 0. - -- For nullable result columns, use COALESCE(..., 0) to have a default value. - SELECT COUNT(*)::INT, - COALESCE(SUM(CASE WHEN "VULNERABILITIES" > 0 THEN 1 ELSE 0 END)::INT, 0), - COALESCE(SUM("COMPONENTS")::INT, 0), - COALESCE(SUM("VULNERABLECOMPONENTS")::INT, 0), - COALESCE(SUM("VULNERABILITIES")::INT, 0), - COALESCE(SUM("CRITICAL")::INT, 0), - COALESCE(SUM("HIGH")::INT, 0), - COALESCE(SUM("MEDIUM")::INT, 0), - COALESCE(SUM("LOW")::INT, 0), - COALESCE(SUM("UNASSIGNED_SEVERITY")::INT, 0), - COALESCE(SUM("FINDINGS_TOTAL")::INT, 0), - COALESCE(SUM("FINDINGS_AUDITED")::INT, 0), - COALESCE(SUM("FINDINGS_UNAUDITED")::INT, 0), - COALESCE(SUM("SUPPRESSED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_TOTAL")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_FAIL")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_WARN")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_INFO")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_AUDITED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_UNAUDITED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_LICENSE_TOTAL")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_LICENSE_AUDITED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_LICENSE_UNAUDITED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_OPERATIONAL_TOTAL")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_OPERATIONAL_AUDITED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_OPERATIONAL_UNAUDITED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_SECURITY_TOTAL")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_SECURITY_AUDITED")::INT, 0), - COALESCE(SUM("POLICYVIOLATIONS_SECURITY_UNAUDITED")::INT, 0) - FROM ( - SELECT metrics.* - FROM "PROJECT" - INNER JOIN LATERAL ( - SELECT * - FROM "PROJECTMETRICS" - WHERE "PROJECT_ID" = "PROJECT"."ID" - ORDER BY "LAST_OCCURRENCE" DESC - LIMIT 1 - ) AS metrics ON TRUE - WHERE "INACTIVE_SINCE" IS NULL - ) AS "LATEST_PROJECT_METRICS" - INTO - "v_projects", - "v_vulnerable_projects", - "v_components", - "v_vulnerable_components", - "v_vulnerabilities", - "v_critical", - "v_high", - "v_medium", - "v_low", - "v_unassigned", - "v_findings_total", - "v_findings_audited", - "v_findings_unaudited", - "v_findings_suppressed", - "v_policy_violations_total", - "v_policy_violations_fail", - "v_policy_violations_warn", - "v_policy_violations_info", - "v_policy_violations_audited", - "v_policy_violations_unaudited", - "v_policy_violations_license_total", - "v_policy_violations_license_audited", - "v_policy_violations_license_unaudited", - "v_policy_violations_operational_total", - "v_policy_violations_operational_audited", - "v_policy_violations_operational_unaudited", - "v_policy_violations_security_total", - "v_policy_violations_security_audited", - "v_policy_violations_security_unaudited"; - - "v_risk_score" = "CALC_RISK_SCORE"("v_critical", "v_high", "v_medium", "v_low", "v_unassigned"); - - INSERT INTO "PORTFOLIOMETRICS" ("PROJECTS", - "VULNERABLEPROJECTS", - "COMPONENTS", - "VULNERABLECOMPONENTS", - "VULNERABILITIES", - "CRITICAL", - "HIGH", - "MEDIUM", - "LOW", - "UNASSIGNED_SEVERITY", - "RISKSCORE", - "FINDINGS_TOTAL", - "FINDINGS_AUDITED", - "FINDINGS_UNAUDITED", - "SUPPRESSED", - "POLICYVIOLATIONS_TOTAL", - "POLICYVIOLATIONS_FAIL", - "POLICYVIOLATIONS_WARN", - "POLICYVIOLATIONS_INFO", - "POLICYVIOLATIONS_AUDITED", - "POLICYVIOLATIONS_UNAUDITED", - "POLICYVIOLATIONS_LICENSE_TOTAL", - "POLICYVIOLATIONS_LICENSE_AUDITED", - "POLICYVIOLATIONS_LICENSE_UNAUDITED", - "POLICYVIOLATIONS_OPERATIONAL_TOTAL", - "POLICYVIOLATIONS_OPERATIONAL_AUDITED", - "POLICYVIOLATIONS_OPERATIONAL_UNAUDITED", - "POLICYVIOLATIONS_SECURITY_TOTAL", - "POLICYVIOLATIONS_SECURITY_AUDITED", - "POLICYVIOLATIONS_SECURITY_UNAUDITED", - "FIRST_OCCURRENCE", - "LAST_OCCURRENCE") - VALUES ("v_projects", - "v_vulnerable_projects", - "v_components", - "v_vulnerable_components", - "v_vulnerabilities", - "v_critical", - "v_high", - "v_medium", - "v_low", - "v_unassigned", - "v_risk_score", - "v_findings_total", - "v_findings_audited", - "v_findings_unaudited", - "v_findings_suppressed", - "v_policy_violations_total", - "v_policy_violations_fail", - "v_policy_violations_warn", - "v_policy_violations_info", - "v_policy_violations_audited", - "v_policy_violations_unaudited", - "v_policy_violations_license_total", - "v_policy_violations_license_audited", - "v_policy_violations_license_unaudited", - "v_policy_violations_operational_total", - "v_policy_violations_operational_audited", - "v_policy_violations_operational_unaudited", - "v_policy_violations_security_total", - "v_policy_violations_security_audited", - "v_policy_violations_security_unaudited", - NOW(), - NOW()); -END; -$$; \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5d0ede191b..f05c4069d3 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ support/datanucleus-plugin support/liquibase + api alpine proto persistence-migration @@ -100,13 +101,13 @@ 6.0.10 10.0.0 5.1.0 - 2.19.1 + 2.19.2 3.49.5 3.1.10 12.0.23 3.20.5 - 5.13.3 - 4.32.0 + 5.13.4 + 4.33.0 42.7.7 4.31.1 1.21.3 @@ -121,64 +122,6 @@ - - org.dependencytrack - alpine-common - ${project.version} - - - org.dependencytrack - alpine-model - ${project.version} - - - org.dependencytrack - alpine-infra - ${project.version} - - - org.dependencytrack - alpine-server - ${project.version} - - - org.dependencytrack - alpine-executable-war - ${project.version} - - - - org.dependencytrack - apiserver - classes - ${project.version} - - - org.dependencytrack - datanucleus-plugin - ${project.version} - - - org.dependencytrack - liquibase-support - ${project.version} - - - org.dependencytrack - persistence-migration - ${project.version} - - - org.dependencytrack - persistence-jooq - ${project.version} - - - org.dependencytrack - proto - ${project.version} - - org.assertj assertj-core diff --git a/proto/pom.xml b/proto/pom.xml index dd4b51aa1d..7151a87652 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -28,7 +28,7 @@ io.github.ascopes protobuf-maven-plugin - 3.6.0 + 3.6.1 ${lib.protobuf-java.version} diff --git a/support/datanucleus-plugin/test/pom.xml b/support/datanucleus-plugin/test/pom.xml index 46c9e38889..e31031224c 100644 --- a/support/datanucleus-plugin/test/pom.xml +++ b/support/datanucleus-plugin/test/pom.xml @@ -38,6 +38,7 @@ org.dependencytrack datanucleus-plugin + ${project.version}