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 deleted file mode 100644 index 36fa40c478..0000000000 --- a/api/src/main/openapi/components/schemas/create-team-membership-request.yaml +++ /dev/null @@ -1,29 +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. -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 deleted file mode 100644 index 8d2708fda2..0000000000 --- a/api/src/main/openapi/components/schemas/create-team-request.yaml +++ /dev/null @@ -1,31 +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. -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 deleted file mode 100644 index ce421d6bd3..0000000000 --- a/api/src/main/openapi/components/schemas/get-team-response.yaml +++ /dev/null @@ -1,31 +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. -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/list-team-memberships-response-item.yaml b/api/src/main/openapi/components/schemas/list-team-memberships-response-item.yaml deleted file mode 100644 index 36fa40c478..0000000000 --- a/api/src/main/openapi/components/schemas/list-team-memberships-response-item.yaml +++ /dev/null @@ -1,29 +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. -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 deleted file mode 100644 index f38ef451e0..0000000000 --- a/api/src/main/openapi/components/schemas/list-team-memberships-response.yaml +++ /dev/null @@ -1,27 +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. -type: object -allOf: -- $ref: "./paginated-response.yaml" -properties: - items: - type: array - items: - $ref: "./list-team-memberships-response-item.yaml" -required: -- total -- items \ 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 deleted file mode 100644 index 793c020e8d..0000000000 --- a/api/src/main/openapi/components/schemas/list-teams-response-item.yaml +++ /dev/null @@ -1,35 +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. -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-vulnerability-metrics-response-item.yaml b/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response-item.yaml deleted file mode 100644 index c435044d2d..0000000000 --- a/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response-item.yaml +++ /dev/null @@ -1,34 +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. -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 deleted file mode 100644 index 0f2d04dd0d..0000000000 --- a/api/src/main/openapi/components/schemas/list-vulnerability-metrics-response.yaml +++ /dev/null @@ -1,26 +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. -type: object -allOf: -- $ref: "./paginated-response.yaml" -properties: - items: - type: array - items: - $ref: "./list-vulnerability-metrics-response-item.yaml" -required: -- items \ 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 deleted file mode 100644 index f938ec465f..0000000000 --- a/api/src/main/openapi/components/schemas/portfolio-metrics-response.yaml +++ /dev/null @@ -1,142 +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. -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/list-teams-response.yaml b/api/src/main/openapi/components/schemas/upload-advisory-response.yaml similarity index 83% rename from api/src/main/openapi/components/schemas/list-teams-response.yaml rename to api/src/main/openapi/components/schemas/upload-advisory-response.yaml index 8394780658..1628e964b6 100644 --- a/api/src/main/openapi/components/schemas/list-teams-response.yaml +++ b/api/src/main/openapi/components/schemas/upload-advisory-response.yaml @@ -15,13 +15,10 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. type: object -allOf: -- $ref: "./paginated-response.yaml" properties: - items: - type: array - items: - $ref: "./list-teams-response-item.yaml" + id: + type: string + format: uuid + description: The unique identifier of the created advisory required: -- total -- items \ No newline at end of file +- id diff --git a/api/src/main/openapi/openapi.yaml b/api/src/main/openapi/openapi.yaml index 69482c67a0..ae20fafbfc 100644 --- a/api/src/main/openapi/openapi.yaml +++ b/api/src/main/openapi/openapi.yaml @@ -129,10 +129,6 @@ paths: $ref: "./paths/extension-points__name__extensions__name__config-schema.yaml" /extension-points/{extensionPointName}/extensions/{extensionName}/test: $ref: "./paths/extension-points__name__extensions__name__test.yaml" - /metrics/portfolio/current: - $ref: "./paths/metrics_portfolio_current.yaml" - /metrics/vulnerabilities: - $ref: "./paths/metrics_vulnerabilities.yaml" /projects/{uuid}/advisories: $ref: "./paths/projects__uuid__advisories.yaml" /projects/{uuid}/advisories/{advisoryId}/findings: @@ -159,12 +155,6 @@ paths: $ref: "./paths/vuln-policy-bundles__uuid_.yaml" /vuln-policy-bundles/{uuid}/sync: $ref: "./paths/vuln-policy-bundles__uuid__sync.yaml" - /teams: - $ref: "./paths/teams.yaml" - /teams/{name}: - $ref: "./paths/teams__name_.yaml" - /team-memberships: - $ref: "./paths/team-memberships.yaml" /workflow-instances/{id}: $ref: "./paths/workflow-instances__id_.yaml" /workflow-runs: diff --git a/api/src/main/openapi/paths/advisories.yaml b/api/src/main/openapi/paths/advisories.yaml index d07dc30b3f..e56f16bffd 100644 --- a/api/src/main/openapi/paths/advisories.yaml +++ b/api/src/main/openapi/paths/advisories.yaml @@ -83,12 +83,18 @@ post: required: - file responses: - "200": + "201": description: Advisory Uploaded - content: - text/plain: + headers: + Location: + description: URL of the created advisory schema: type: string + format: uri + content: + application/json: + schema: + $ref: "../components/schemas/upload-advisory-response.yaml" "400": $ref: "../components/responses/invalid-request-error.yaml" "401": diff --git a/api/src/main/openapi/paths/components.yaml b/api/src/main/openapi/paths/components.yaml index e69a4d4eb9..d1acc5eed6 100644 --- a/api/src/main/openapi/paths/components.yaml +++ b/api/src/main/openapi/paths/components.yaml @@ -29,6 +29,12 @@ post: responses: "201": description: Component Created + headers: + Location: + description: URL of the created component + schema: + type: string + format: uri "400": description: Bad Request content: diff --git a/api/src/main/openapi/paths/csaf/aggregators.yaml b/api/src/main/openapi/paths/csaf/aggregators.yaml index 9fba389864..f1a9c361bc 100644 --- a/api/src/main/openapi/paths/csaf/aggregators.yaml +++ b/api/src/main/openapi/paths/csaf/aggregators.yaml @@ -66,6 +66,12 @@ post: responses: "201": description: CSAF aggregator created + headers: + Location: + description: URL of the created aggregator + schema: + type: string + format: uri content: application/json: schema: diff --git a/api/src/main/openapi/paths/csaf/aggregators__id__provider-discovery.yaml b/api/src/main/openapi/paths/csaf/aggregators__id__provider-discovery.yaml index 0497c3f3f9..735d1c3f35 100644 --- a/api/src/main/openapi/paths/csaf/aggregators__id__provider-discovery.yaml +++ b/api/src/main/openapi/paths/csaf/aggregators__id__provider-discovery.yaml @@ -34,6 +34,12 @@ post: responses: "202": description: Discovery triggered + headers: + Location: + description: URL of the aggregator + schema: + type: string + format: uri "400": $ref: "../../components/responses/invalid-request-error.yaml" "401": diff --git a/api/src/main/openapi/paths/csaf/providers.yaml b/api/src/main/openapi/paths/csaf/providers.yaml index 8426688f42..9cc738282a 100644 --- a/api/src/main/openapi/paths/csaf/providers.yaml +++ b/api/src/main/openapi/paths/csaf/providers.yaml @@ -74,6 +74,12 @@ post: responses: "201": description: CSAF provider created + headers: + Location: + description: URL of the created provider + schema: + type: string + format: uri content: application/json: schema: diff --git a/api/src/main/openapi/paths/metrics_portfolio_current.yaml b/api/src/main/openapi/paths/metrics_portfolio_current.yaml deleted file mode 100644 index 088642a4a2..0000000000 --- a/api/src/main/openapi/paths/metrics_portfolio_current.yaml +++ /dev/null @@ -1,35 +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. -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 deleted file mode 100644 index a270bd02c5..0000000000 --- a/api/src/main/openapi/paths/metrics_vulnerabilities.yaml +++ /dev/null @@ -1,38 +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. -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/projects__uuid__clone.yaml b/api/src/main/openapi/paths/projects__uuid__clone.yaml index f51417b8d9..241e1c088c 100644 --- a/api/src/main/openapi/paths/projects__uuid__clone.yaml +++ b/api/src/main/openapi/paths/projects__uuid__clone.yaml @@ -36,6 +36,12 @@ post: responses: "201": description: Project cloned + headers: + Location: + description: URL of the cloned project + schema: + type: string + format: uri content: application/json: schema: diff --git a/api/src/main/openapi/paths/secrets.yaml b/api/src/main/openapi/paths/secrets.yaml index 0e14720dce..f9e0c3615d 100644 --- a/api/src/main/openapi/paths/secrets.yaml +++ b/api/src/main/openapi/paths/secrets.yaml @@ -66,8 +66,14 @@ post: schema: $ref: "../components/schemas/secrets/create-secret-request.yaml" responses: - "204": + "201": description: Secret created + headers: + Location: + description: URL of the created secret + schema: + type: string + format: uri "400": description: Bad Request content: diff --git a/api/src/main/openapi/paths/team-memberships.yaml b/api/src/main/openapi/paths/team-memberships.yaml deleted file mode 100644 index fbd517ef68..0000000000 --- a/api/src/main/openapi/paths/team-memberships.yaml +++ /dev/null @@ -1,133 +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. -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. - - Reports `EXACT` total counts for up to 500 items, and `AT_LEAST` past that. - - Requires permission `ACCESS_MANAGEMENT`. - 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: |- - Creates a team membership. - - Requires permission `ACCESS_MANAGEMENT`. - 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: - anyOf: - - $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: |- - Deletes a team membership. - - Requires permission `ACCESS_MANAGEMENT`. - 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 deleted file mode 100644 index 387574e5d5..0000000000 --- a/api/src/main/openapi/paths/teams.yaml +++ /dev/null @@ -1,80 +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. -get: - operationId: listTeams - summary: List all teams - description: |- - Returns a paginated list of teams, sorted by name in ascending order. - - Reports `EXACT` total counts for up to 500 items, and `AT_LEAST` past that. - - Requires permission `ACCESS_MANAGEMENT`. - 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: |- - Creates a team. - - Requires permission `ACCESS_MANAGEMENT`. - 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: - anyOf: - - $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 deleted file mode 100644 index 281b3d30b8..0000000000 --- a/api/src/main/openapi/paths/teams__name_.yaml +++ /dev/null @@ -1,79 +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. -get: - operationId: getTeam - summary: Get a team - description: |- - Returns detailed information about a given team. - - Requires permission `ACCESS_MANAGEMENT`. - 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: |- - Deletes a team. - - Requires permission `ACCESS_MANAGEMENT`. - 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/spectral/functions/response-conventions.js b/api/src/main/spectral/functions/response-conventions.js new file mode 100644 index 0000000000..3cd1f9cd00 --- /dev/null +++ b/api/src/main/spectral/functions/response-conventions.js @@ -0,0 +1,65 @@ +/* + * 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'; + +export default (responses, opts) => { + const method = opts.method; + const errors = []; + + switch (method) { + case 'post': { + if (!('201' in responses) && !('202' in responses) && !('204' in responses)) { + errors.push({message: 'POST must return 201 (Created), 202 (Accepted), or 204 (No Content)'}); + } + if ('200' in responses) { + errors.push({message: 'POST must not return 200; use 201, 202, or 204'}); + } + if ('201' in responses && (!responses['201'].headers || !responses['201'].headers['Location'])) { + errors.push({message: 'POST 201 response must include a Location header'}); + } + if ('202' in responses && (!responses['202'].headers || !responses['202'].headers['Location'])) { + errors.push({message: 'POST 202 response must include a Location header'}); + } + break; + } + case 'put': { + if (!('204' in responses)) { + errors.push({message: 'PUT must return 204 (No Content)'}); + } + break; + } + case 'patch': { + if (!('200' in responses) && !('204' in responses)) { + errors.push({message: 'PATCH must return 200 (OK) or 204 (No Content)'}); + } + if ('200' in responses && !responses['200'].content) { + errors.push({message: 'PATCH 200 response must include a response body'}); + } + break; + } + case 'delete': { + if (!('204' in responses)) { + errors.push({message: 'DELETE must return 204 (No Content)'}); + } + break; + } + } + + return errors.length > 0 ? errors : undefined; +}; diff --git a/api/src/main/spectral/ruleset.yaml b/api/src/main/spectral/ruleset.yaml index 7fcac570f9..e8affc84c9 100644 --- a/api/src/main/spectral/ruleset.yaml +++ b/api/src/main/spectral/ruleset.yaml @@ -21,6 +21,7 @@ formats: - "oas3" functions: - paginated-response-uses-items +- response-conventions rules: paginated-response-must-use-items-array: message: "{{error}}" @@ -40,4 +41,80 @@ rules: then: function: pattern functionOptions: - match: ^(?=((([\/a-z][a-z0-9\-\/]*)?({[^}]*})?)+))\1$ \ No newline at end of file + match: ^(?=((([\/a-z][a-z0-9\-\/]*)?({[^}]*})?)+))\1$ + + # Enforce consistent naming of operations, depending on their HTTP method. + operation-id-get-prefix: + message: GET operation IDs must start with "get" or "list" + severity: error + given: $.paths.*.get.operationId + then: + function: pattern + functionOptions: + match: ^(get|list)[A-Z] + operation-id-post-prefix: + message: POST operation IDs must not use prefixes reserved for other methods (get, list, delete, update) + severity: error + given: $.paths.*.post.operationId + then: + function: pattern + functionOptions: + notMatch: ^(get|list|delete|update)[A-Z] + operation-id-put-prefix: + message: PUT operation IDs must start with "update" + severity: error + given: $.paths.*.put.operationId + then: + function: pattern + functionOptions: + match: ^update[A-Z] + operation-id-patch-prefix: + message: PATCH operation IDs must start with "update" + severity: error + given: $.paths.*.patch.operationId + then: + function: pattern + functionOptions: + match: ^update[A-Z] + operation-id-delete-prefix: + message: DELETE operation IDs must start with "delete" + severity: error + given: $.paths.*.delete.operationId + then: + function: pattern + functionOptions: + match: ^delete[A-Z] + + # Enforce consistent response structure, depending on the operation's HTTP method. + response-conventions-post: + message: "{{error}}" + severity: error + given: "$.paths[?(!@property.match(/\\/test$/))].post.responses" + then: + function: response-conventions + functionOptions: + method: post + response-conventions-put: + message: "{{error}}" + severity: error + given: $.paths.*.put.responses + then: + function: response-conventions + functionOptions: + method: put + response-conventions-patch: + message: "{{error}}" + severity: error + given: $.paths.*.patch.responses + then: + function: response-conventions + functionOptions: + method: patch + response-conventions-delete: + message: "{{error}}" + severity: error + given: $.paths.*.delete.responses + then: + function: response-conventions + functionOptions: + method: delete \ No newline at end of file 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 38fa9742ce..d057937b1b 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java @@ -18,14 +18,9 @@ */ package org.dependencytrack.persistence.jdbi; -import org.dependencytrack.common.pagination.Page; -import org.dependencytrack.common.pagination.PageToken; -import org.dependencytrack.common.pagination.PageTokenEncoder; import org.dependencytrack.model.DependencyMetrics; import org.dependencytrack.model.PortfolioMetrics; import org.dependencytrack.model.ProjectMetrics; -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; @@ -48,53 +43,6 @@ public interface MetricsDao extends SqlObject { Pattern VALID_TABLE_IDENTIFIER_PATTERN = Pattern.compile("^\"[A-Z][A-Z0-9_]+\"$"); - record ListVulnerabilityMetricsPageToken(int year, int month) implements PageToken { - } - - record ListVulnerabilityMetricsRow(int year, int month, int count, Instant measuredAt) { - } - - default Page getVulnerabilityMetrics(final int limit, final String pageToken) { - final PageTokenEncoder pageTokenEncoder = - getHandle().getConfig(PaginationConfig.class).getPageTokenEncoder(); - final var decodedPageToken = pageTokenEncoder.decode(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, pageTokenEncoder.encode(nextPageToken)); - } - /** * Compute the portfolio metrics for the projects accessible by the calling principal. *

diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java deleted file mode 100644 index ba9fa6f204..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java +++ /dev/null @@ -1,214 +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.jdbi; - -import org.dependencytrack.common.pagination.Page; -import org.dependencytrack.common.pagination.Page.TotalCount; -import org.dependencytrack.common.pagination.PageToken; -import org.dependencytrack.common.pagination.PageTokenEncoder; -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.customizer.Bind; -import org.jdbi.v3.sqlobject.statement.SqlUpdate; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; - -@NullMarked -public interface TeamDao extends PaginationSupport { - - record ListTeamsPageToken(String lastName, TotalCount totalCount) implements PageToken { - } - - record ListTeamsRow(String name, int apiKeys, int members) { - } - - default Page listTeams(int limit, @Nullable String pageToken) { - final PageTokenEncoder pageTokenEncoder = - getHandle().getConfig(PaginationConfig.class).getPageTokenEncoder(); - final var decodedPageToken = pageTokenEncoder.decode(pageToken, ListTeamsPageToken.class); - - TotalCount totalCount; - String lastName = null; - - if (decodedPageToken != null) { - totalCount = decodedPageToken.totalCount(); - lastName = decodedPageToken.lastName(); - } else { - totalCount = getBoundedTotalCount("FROM \"TEAM\"", null, 500); - } - - 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 + 1) - """); - - final List rows = query - .bind("lastName", lastName) - .bind("limit", limit) - .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(), totalCount) - : null; - - return new Page<>(resultRows, pageTokenEncoder.encode(nextPageToken), totalCount); - } - - record ListTeamMembershipsPageToken( - String lastTeamName, - String lastUsername, - TotalCount totalCount) implements PageToken { - } - - record ListTeamMembershipsRow(String teamName, String username) { - } - - default Page listTeamMembers( - @Nullable String teamName, - @Nullable String username, - int limit, - @Nullable String pageToken) { - final var whereConditions = new ArrayList(); - final var queryParams = new HashMap(); - - whereConditions.add("TRUE"); - if (teamName != null) { - whereConditions.add("t.\"NAME\" = :teamName"); - queryParams.put("teamName", teamName); - } - if (username != null) { - whereConditions.add("u.\"USERNAME\" = :username"); - queryParams.put("username", username); - } - - final PageTokenEncoder pageTokenEncoder = - getHandle().getConfig(PaginationConfig.class).getPageTokenEncoder(); - final var decodedPageToken = pageTokenEncoder.decode(pageToken, ListTeamMembershipsPageToken.class); - - TotalCount totalCount; - String lastTeamName = null; - String lastUsername = null; - - if (decodedPageToken != null) { - totalCount = decodedPageToken.totalCount(); - lastTeamName = decodedPageToken.lastTeamName(); - lastUsername = decodedPageToken.lastUsername(); - } else { - totalCount = getBoundedTotalCount(""" - 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 %s - """.formatted(String.join(" AND ", whereConditions)), - queryParams, - 500); - } - - 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" --> - <#-- @ftlvariable name="whereConditions" type="java.util.Collection" --> - 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 ${whereConditions?join(" AND ")} - <#if lastTeamName && lastUsername> - AND (t."NAME", u."USERNAME") > (:lastTeamName, :lastUsername) - - ORDER BY t."NAME", u."USERNAME" - LIMIT (:limit + 1) - """); - - final List rows = query - .bindMap(queryParams) - .bind("lastTeamName", lastTeamName) - .bind("lastUsername", lastUsername) - .bind("limit", limit) - .define("whereConditions", whereConditions) - .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(), - totalCount) - : null; - - return new Page<>(resultRows, pageTokenEncoder.encode(nextPageToken), totalCount); - } - - @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); - - default List deleteTeamsByName(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/resources/v2/AdvisoriesResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/AdvisoriesResource.java index 2acf4893f1..40b29828b7 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/AdvisoriesResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/AdvisoriesResource.java @@ -27,6 +27,7 @@ import org.dependencytrack.api.v2.model.GetAdvisoryResponse; import org.dependencytrack.api.v2.model.ListAdvisoriesResponse; import org.dependencytrack.api.v2.model.ListAdvisoriesResponseItem; +import org.dependencytrack.api.v2.model.UploadAdvisoryResponse; import org.dependencytrack.auth.Permissions; import org.dependencytrack.common.pagination.Page; import org.dependencytrack.csaf.CsafModelConverter; @@ -172,7 +173,16 @@ private Response processCsafDocument(String content, String fileName, QueryManag persistentAdvisory.setVulnerabilities(persistentVulns); } - return Response.ok("File uploaded successfully: " + fileName).build(); + final UploadAdvisoryResponse responseBody = UploadAdvisoryResponse.builder() + .id(persistentAdvisory.getId()) + .build(); + return Response + .created(getUriInfo().getBaseUriBuilder() + .path("/advisories") + .path(persistentAdvisory.getId().toString()) + .build()) + .entity(responseBody) + .build(); }); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java index 9ff69fc7fc..2f007f4e63 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java @@ -79,7 +79,7 @@ public class ComponentsResource extends AbstractApiResource implements Component public Response createComponent(final CreateComponentRequest request) { final UUID projectUuid = request.getProjectUuid(); try (QueryManager qm = new QueryManager()) { - qm.callInTransaction(() -> { + final Component component = qm.callInTransaction(() -> { final Project project = qm.getObjectByUuid(Project.class, projectUuid); if (project == null) { throw new NotFoundException(); @@ -96,6 +96,7 @@ public Response createComponent(final CreateComponentRequest request) { return Response .created(uriInfo.getBaseUriBuilder() .path("/components") + .path(component.getUuid().toString()) .build()) .build(); } catch (RuntimeException e) { diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/CsafResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/CsafResource.java index 511018a8bc..c8a6b128c7 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/CsafResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/CsafResource.java @@ -234,7 +234,13 @@ public Response triggerCsafProviderDiscovery(UUID id) { SecurityMarkers.SECURITY_AUDIT, "Triggered provider discovery for CSAF aggregator '{}'", aggregator.getNamespace()); - return Response.accepted().build(); + return Response + .accepted() + .location(getUriInfo().getBaseUriBuilder() + .path("/csaf-aggregators") + .path(id.toString()) + .build()) + .build(); } @Override diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java deleted file mode 100644 index d233b0ae5c..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java +++ /dev/null @@ -1,105 +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.resources.v2; - -import alpine.server.auth.PermissionRequired; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.Provider; -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.common.pagination.Page; -import org.dependencytrack.model.PortfolioMetrics; -import org.dependencytrack.persistence.jdbi.MetricsDao; -import org.dependencytrack.resources.AbstractApiResource; - -import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction; -import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; - -@Provider -public class MetricsResource extends AbstractApiResource implements MetricsApi { - - @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() - .items(metricsPage.items().stream() - .map( - metricRow -> ListVulnerabilityMetricsResponseItem.builder() - .year(metricRow.year()) - .month(metricRow.month()) - .count(metricRow.count()) - .observedAt(metricRow.measuredAt().getEpochSecond()) - .build()) - .toList()) - .nextPageToken(metricsPage.nextPageToken()) - .total(convertTotalCount(metricsPage.totalCount())) - .build(); - - return Response.ok(response).build(); - } -} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/SecretsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/SecretsResource.java index a41bcb0f73..83bc991873 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/SecretsResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/SecretsResource.java @@ -66,7 +66,12 @@ public Response createSecret(final CreateSecretRequest request) { } LOGGER.info(SecurityMarkers.SECURITY_AUDIT, "Created secret: {}", request.getName()); - return Response.noContent().build(); + return Response + .created(getUriInfo().getBaseUriBuilder() + .path("/secrets") + .path(request.getName()) + .build()) + .build(); } @Override diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java deleted file mode 100644 index 45b16514f8..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java +++ /dev/null @@ -1,221 +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.resources.v2; - -import alpine.model.Permission; -import alpine.model.Team; -import alpine.model.User; -import alpine.server.auth.PermissionRequired; -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 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.common.pagination.Page; -import org.dependencytrack.exception.AlreadyExistsException; -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.resources.AbstractApiResource; -import org.owasp.security.logging.SecurityMarkers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction; -import static org.dependencytrack.util.PersistenceUtil.isUniqueConstraintViolation; - -@Provider -public class TeamsResource extends AbstractApiResource 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() - .items(teamsPage.items().stream() - .map( - teamRow -> ListTeamsResponseItem.builder() - .name(teamRow.name()) - .apiKeys(teamRow.apiKeys()) - .members(teamRow.members()) - .build()) - .toList()) - .nextPageToken(teamsPage.nextPageToken()) - .total(convertTotalCount(teamsPage.totalCount())) - .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 AlreadyExistsException("Team already exists", e); - } - - 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() - .items(membershipsPage.items().stream() - .map( - membershipRow -> ListTeamMembershipsResponseItem.builder() - .teamName(membershipRow.teamName()) - .username(membershipRow.username()) - .build()) - .toList()) - .nextPageToken(membershipsPage.nextPageToken()) - .total(convertTotalCount(membershipsPage.totalCount())) - .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 AlreadyExistsException("Team membership already exists", e); - } - - 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/test/java/org/dependencytrack/resources/v2/AdvisoriesResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/AdvisoriesResourceTest.java index ef25867079..01cb4cc105 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/AdvisoriesResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/AdvisoriesResourceTest.java @@ -300,7 +300,7 @@ public void testUploadAdvisoryCsafInvalid_returns400() { } @Test - public void testUploadAdvisoryCsafValid_returns200() throws Exception { + public void testUploadAdvisoryCsafValid_returns201() throws Exception { initializeWithPermissions(Permissions.VULNERABILITY_MANAGEMENT_CREATE); // Load a valid CSAF document from test resources @@ -322,14 +322,15 @@ public void testUploadAdvisoryCsafValid_returns200() throws Exception { .header(X_API_KEY, apiKey) .post(Entity.entity(multiPart, multiPart.getMediaType()))) { - // If not 200, print the error for debugging - if (response.getStatus() != 200) { - String errorBody = response.readEntity(String.class); - System.out.println("Error response: " + errorBody); - } - // Assert successful upload - assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getLocation()).isNotNull(); + assertThat(response.getLocation().getPath()).matches("/advisories/.+"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "id": "${json-unit.any-string}" + } + """); } } diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java index b3cd19b749..a6c40e8a49 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java @@ -71,7 +71,8 @@ public void createComponentTest() { } """.formatted(project.getUuid()))); assertThat(response.getStatus()).isEqualTo(201); - assertThat(response.getLocation()).hasPath("/components"); + assertThat(response.getLocation()).isNotNull(); + assertThat(response.getLocation().getPath()).matches("/components/.+"); assertThat(getPlainTextBody(response)).isEmpty(); qm.getPersistenceManager().evictAll(); diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/CsafResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/CsafResourceTest.java index 0f703d072b..53398a1f0f 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/CsafResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/CsafResourceTest.java @@ -404,7 +404,8 @@ void triggerCsafProviderDiscoveryShouldReturnAccepted() { .header(X_API_KEY, apiKey) .post(null); assertThat(response.getStatus()).isEqualTo(202); - assertThat(getPlainTextBody(response)).isEmpty(); + assertThat(response.getLocation()).isNotNull(); + assertThat(response.getLocation().getPath()).endsWith("/csaf-aggregators/" + aggregator.getId()); final CreateWorkflowRunRequest createRunRequest = createRunCaptor.getValue(); assertThat(createRunRequest.workflowName()).isEqualTo("discover-csaf-providers"); diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java deleted file mode 100644 index 2f665f7f69..0000000000 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java +++ /dev/null @@ -1,254 +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.resources.v2; - -import jakarta.json.JsonObject; -import jakarta.ws.rs.core.Response; -import net.javacrumbs.jsonunit.core.Option; -import org.dependencytrack.JerseyTestExtension; -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.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -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 { - - @RegisterExtension - static JerseyTestExtension jersey = new JerseyTestExtension(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 */ """ - { - "items" : - [ - { - "observed_at" : "${json-unit.any-number}", - "year" : 2025, - "month" : 1, - "count" : 1 - }, - { - "observed_at" : "${json-unit.any-number}", - "year" : 2025, - "month" : 2, - "count" : 2 - } - ], - "next_page_token": "${json-unit.any-string}" - } - """); - - final String nextPageToken = responseJson.getString("next_page_token"); - - response = jersey.target("/metrics/vulnerabilities") - .queryParam("limit", 2) - .queryParam("page_token", nextPageToken) - .request() - .header(X_API_KEY, apiKey) - .get(); - assertThat(response.getStatus()).isEqualTo(200); - assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ - { - "items" : - [ - { - "observed_at" : "${json-unit.any-number}", - "year" : 2025, - "month" : 3, - "count" : 3 - } - ] - } - """); - } -} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/SecretsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/SecretsResourceTest.java index 9c2152746d..b729bba1a8 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/SecretsResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/SecretsResourceTest.java @@ -70,7 +70,7 @@ void afterEach() { } @Test - void createSecretShouldCreateSecretAndReturnNoContent() { + void createSecretShouldCreateSecretAndReturnCreated() { initializeWithPermissions(Permissions.SECRET_MANAGEMENT_CREATE); final Response response = jersey @@ -84,8 +84,9 @@ void createSecretShouldCreateSecretAndReturnNoContent() { "value": "baz" } """)); - assertThat(response.getStatus()).isEqualTo(204); - assertThat(getPlainTextBody(response)).isEmpty(); + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getLocation()).isNotNull(); + assertThat(response.getLocation().getPath()).endsWith("/secrets/foo"); verify(SECRET_MANAGER_MOCK).createSecret(eq("foo"), eq("bar"), eq("baz")); } @@ -149,7 +150,7 @@ void createSecretShouldReturnConflictWhenAlreadyExists() { } @Test - void updateSecretShouldUpdateDescriptionAndReturnNoContent() { + void shouldUpdateDescriptionAndReturnNoContent() { initializeWithPermissions(Permissions.SECRET_MANAGEMENT_UPDATE); doReturn(true).when(SECRET_MANAGER_MOCK).updateSecret(eq("foo"), any(), any()); @@ -170,7 +171,7 @@ void updateSecretShouldUpdateDescriptionAndReturnNoContent() { } @Test - void updateSecretShouldUpdateValueAndReturnNoContent() { + void shouldUpdateValueAndReturnNoContent() { initializeWithPermissions(Permissions.SECRET_MANAGEMENT_UPDATE); doReturn(true).when(SECRET_MANAGER_MOCK).updateSecret(eq("foo"), any(), any()); diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java deleted file mode 100644 index 2866fb01a4..0000000000 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java +++ /dev/null @@ -1,578 +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.resources.v2; - -import alpine.model.Permission; -import alpine.model.Team; -import alpine.model.User; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.Response; -import org.dependencytrack.JerseyTestExtension; -import org.dependencytrack.ResourceTest; -import org.dependencytrack.auth.Permissions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -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 { - - @RegisterExtension - static JerseyTestExtension jersey = new JerseyTestExtension(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 */ """ - { - "items": [ - { - "name": "Test Users", - "api_keys": 1, - "members": 0 - }, - { - "name": "team0", - "api_keys": 0, - "members": 0 - } - ], - "next_page_token": "${json-unit.any-string}", - "total": { - "count": 3, - "type": "EXACT" - } - } - """); - - final String nextPageToken = responseJson.getString("next_page_token"); - - response = jersey.target("/teams") - .queryParam("limit", 2) - .queryParam("page_token", nextPageToken) - .request() - .header(X_API_KEY, apiKey) - .get(); - assertThat(response.getStatus()).isEqualTo(200); - assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ - { - "items": [ - { - "name": "team1", - "api_keys": 0, - "members": 0 - } - ], - "total": { - "count": 3, - "type": "EXACT" - } - } - """); - } - - @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": "Resource already exists", - "detail": "Team 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 */ """ - { - "items": [ - { - "team_name": "team-a", - "username": "bar" - }, - { - "team_name": "team-a", - "username": "foo" - } - ], - "next_page_token": "${json-unit.any-string}", - "total": { - "count": 3, - "type": "EXACT" - } - } - """); - - final String nextPageToken = responseJson.getString("next_page_token"); - - response = jersey.target("/team-memberships") - .queryParam("limit", 2) - .queryParam("page_token", nextPageToken) - .request() - .header(X_API_KEY, apiKey) - .get(); - assertThat(response.getStatus()).isEqualTo(200); - assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ - { - "items": [ - { - "team_name": "team-b", - "username": "aaa" - } - ], - "total": { - "count": 3, - "type": "EXACT" - } - } - """); - } - - @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 */ """ - { - "items": [ - { - "team_name": "team-b", - "username": "bar" - } - ], - "total": { - "count": 1, - "type": "EXACT" - } - } - """); - } - - @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 */ """ - { - "items": [ - { - "team_name": "team-b", - "username": "bar" - } - ], - "total": { - "count": 1, - "type": "EXACT" - } - } - """); - } - - @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": "Resource already exists", - "detail": "Team membership 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