diff --git a/api/src/main/openapi/paths/components.yaml b/api/src/main/openapi/paths/components.yaml index d1acc5eed6..b5b77f2a98 100644 --- a/api/src/main/openapi/paths/components.yaml +++ b/api/src/main/openapi/paths/components.yaml @@ -56,90 +56,94 @@ get: operationId: listComponents summary: List all components description: |- - Retrieves a list of all components that have the specified component identity. This resource accepts coordinates (group, name, version) , `purl`, `cpe`, `swidTagId`, `project_uuid`, or `hash`. - + Retrieves a list of all components matching the provided filter criteria. + + Text filters are case-insensitive. + ### Sortable fields - + Sorting is supported for the following fields: - + * `name` * `version` * `group` * `purl` * `cpe` - * `swid_tag_id` * `last_inherited_risk_score` - + Requires permission VIEW_PORTFOLIO tags: - - Components + - Components parameters: - - name: project_uuid - in: query - description: The UUID of the project to retrieve components for - schema: - type: string - format: uuid - - name: group - in: query - description: The group of the component - schema: - type: string - - name: name - in: query - description: The name of the component - schema: - type: string - - name: version - in: query - description: The version of the component - schema: - type: string - - name: purl - in: query - description: The PURL of the component - schema: - type: string - - name: cpe - in: query - description: The CPE of the component - schema: - type: string - - name: swid_tag_id - in: query - description: The SWID Tag ID of the component - schema: - type: string - - name: hash_type - in: query - description: The MD5, SHA1, SHA_256, SHA_384, SHA_512, SHA3_256, SHA3_384, SHA3_512, BLAKE2B_256, BLAKE2B_384, BLAKE2B_512, or BLAKE3 hash type of the component - schema: - type: string - enum: - - MD5 - - SHA1 - - SHA_256 - - SHA_384 - - SHA_512 - - SHA3_256 - - SHA3_384 - - SHA3_512 - - BLAKE2B_256 - - BLAKE2B_384 - - BLAKE2B_512 - - BLAKE3 - - name: hash - in: query - description: The hash value of the component - schema: - type: string - - $ref: "../components/parameters/pagination-limit.yaml" - - $ref: "../components/parameters/page-token.yaml" - - $ref: "../components/parameters/sort-direction.yaml" - - $ref: "../components/parameters/sort-by.yaml" + - name: group_contains + in: query + description: Filter by group (substring match) + schema: + type: string + - name: name_contains + in: query + description: Filter by name (substring match) + schema: + type: string + - name: version_contains + in: query + description: Filter by version (substring match) + schema: + type: string + - name: purl_prefix + in: query + description: |- + Filter by PURL (prefix match). + + Must be a valid PURL, with at least `pkg:/` populated. + schema: + type: string + - name: cpe + in: query + description: |- + Filter by CPE (exact match). + + Must be a valid CPE. + schema: + type: string + - name: swid_tag_id_contains + in: query + description: Filter by SWID Tag ID (substring match) + schema: + type: string + - name: hash_type + in: query + description: The hash type to filter by + schema: + type: string + enum: + - MD5 + - SHA1 + - SHA_256 + - SHA_384 + - SHA_512 + - SHA3_256 + - SHA3_384 + - SHA3_512 + - BLAKE2B_256 + - BLAKE2B_384 + - BLAKE2B_512 + - BLAKE3 + - name: hash + in: query + description: |- + Filter by hash value (exact match). + + Requires `hash_type` to be set. + schema: + type: string + - $ref: "../components/parameters/pagination-limit.yaml" + - $ref: "../components/parameters/page-token.yaml" + - $ref: "../components/parameters/sort-direction.yaml" + - $ref: "../components/parameters/sort-by.yaml" responses: "200": - description: A list of all components for a given identity + description: A list of components matching the provided filters content: application/json: schema: @@ -150,13 +154,11 @@ get: application/problem+json: schema: anyOf: - - $ref: "../components/schemas/json-schema-validation-problem-details.yaml" - - $ref: "../components/schemas/problem-details.yaml" + - $ref: "../components/schemas/json-schema-validation-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" default: $ref: "../components/responses/generic-error.yaml" \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java index cb3ee49363..2850c3e524 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ComponentDao.java @@ -249,7 +249,7 @@ default Page listComponents( queryParams.put("componentPurl", componentPurl); } if (componentCpe != null) { - whereConditions.add("LOWER(\"C\".\"CPE\") LIKE ('%' || LOWER(:componentCpe) || '%')"); + whereConditions.add("LOWER(\"C\".\"CPE\") = LOWER(:componentCpe)"); queryParams.put("componentCpe", componentCpe); } if (componentSwidTagId != null) { @@ -298,7 +298,6 @@ default Page listComponents( case "group" -> SortBy.GROUP; case "purl" -> SortBy.PURL; case "cpe" -> SortBy.CPE; - case "swid_tag_id" -> SortBy.SWIDTAGID; case "last_inherited_risk_score" -> SortBy.LAST_RISKSCORE; case null, default -> null; }; @@ -321,7 +320,6 @@ default Page listComponents( case SortBy.GROUP -> lastRow.getGroup(); case SortBy.PURL -> lastRow.getPurl(); case SortBy.CPE -> lastRow.getCpe(); - case SortBy.SWIDTAGID -> lastRow.getSwidTagId(); case SortBy.LAST_RISKSCORE -> lastRow.getLastInheritedRiskScore(); case null -> lastRow.getName(); }; @@ -450,7 +448,6 @@ enum SortBy { GROUP, PURL, CPE, - SWIDTAGID, LAST_RISKSCORE } 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 2f007f4e63..f7ab56a2f8 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/ComponentsResource.java @@ -44,7 +44,6 @@ import org.dependencytrack.model.Project; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.persistence.jdbi.ComponentDao; -import org.dependencytrack.persistence.jdbi.ProjectDao; import org.dependencytrack.resources.AbstractApiResource; import org.dependencytrack.util.InternalComponentIdentifier; import org.dependencytrack.util.PurlUtil; @@ -109,23 +108,15 @@ public Response createComponent(final CreateComponentRequest request) { @Override @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response listComponents(UUID projectUuid, String group, String name, String version, String purl, String cpe, - String swidTagId, String hashType, String hash, Integer limit, String pageToken, SortDirection sortDirection, String sortBy) { + public Response listComponents(String groupContains, String nameContains, String versionContains, String purlPrefix, String cpe, + String swidTagIdContains, String hashType, String hash, Integer limit, String pageToken, SortDirection sortDirection, String sortBy) { return inJdbiTransaction(getAlpineRequest(), handle -> { - Long projectId = null; - if (projectUuid != null) { - projectId = handle.attach(ProjectDao.class).getProjectId(projectUuid); - if (projectId == null) { - throw new NotFoundException(); - } - requireProjectAccess(handle, projectUuid); - } PackageURL packageURL = null; - if (purl != null) { + if (purlPrefix != null) { try { - packageURL = new PackageURL(StringUtils.trimToNull(purl)); + packageURL = new PackageURL(StringUtils.trimToNull(purlPrefix)); } catch (MalformedPackageURLException e) { - throw new BadRequestException("Invalid package URL: %s".formatted(purl)); + throw new BadRequestException("Invalid package URL: %s".formatted(purlPrefix)); } } if (cpe != null) { @@ -144,9 +135,9 @@ public Response listComponents(UUID projectUuid, String group, String name, Stri } } final Page componentsPage = handle.attach(ComponentDao.class) - .listComponents(projectId, true, packageURL != null ? packageURL.canonicalize().toLowerCase() : null, StringUtils.trimToNull(cpe), - StringUtils.trimToNull(swidTagId), StringUtils.trimToNull(group), StringUtils.trimToNull(name), - StringUtils.trimToNull(version), hashTypeEnum, StringUtils.trimToNull(hash), limit, pageToken, sortBy, mapSortDirection(sortDirection)); + .listComponents(null, true, packageURL != null ? packageURL.canonicalize().toLowerCase() : null, StringUtils.trimToNull(cpe), + StringUtils.trimToNull(swidTagIdContains), StringUtils.trimToNull(groupContains), StringUtils.trimToNull(nameContains), + StringUtils.trimToNull(versionContains), hashTypeEnum, StringUtils.trimToNull(hash), limit, pageToken, sortBy, mapSortDirection(sortDirection)); final var response = ListComponentsResponse.builder() .items(componentsPage.items().stream() 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 a6c40e8a49..cd3692618c 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/ComponentsResourceTest.java @@ -31,8 +31,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import java.util.UUID; - import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -302,9 +300,9 @@ public void listComponentsSortingTest() { public void listComponentsWithCoordinatesTest() { prepareComponents(); Response response = jersey.target("/components") - .queryParam("group", "B") - .queryParam("name", "B") - .queryParam("version", "versionB") + .queryParam("group_contains", "B") + .queryParam("name_contains", "B") + .queryParam("version_contains", "versionB") .queryParam("limit", 2) .request() .header(X_API_KEY, apiKey) @@ -341,7 +339,7 @@ public void listComponentsWithCoordinatesTest() { public void listComponentsWithPurlTest() { prepareComponents(); Response response = jersey.target("/components") - .queryParam("purl", "pkg:maven/groupB/nameB@versionB") + .queryParam("purl_prefix", "pkg:maven/groupB/nameB@versionB") .queryParam("limit", 2) .request() .header(X_API_KEY, apiKey) @@ -423,63 +421,13 @@ public void listComponentsWithCpeTest() { """); } - @Test - public void listComponentsWithProjectTest() { - prepareComponents(); - Response response = jersey.target("/components") - .queryParam("project_uuid", qm.getProject("projectA", "1.0").getUuid()) - .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": "nameA", - "version": "versionA", - "group": "groupA", - "cpe": "cpe:2.3:a:groupA:nameA:versionA:*:*:*:*:*:*:*", - "purl":"pkg:maven/groupA/nameA@versionA?foo=bar", - "internal": false, - "uuid": "${json-unit.any-string}", - "project": { - "name": "projectA", - "version": "1.0", - "uuid": "${json-unit.any-string}" - } - } - ], - "total": { - "count": 1, - "type": "EXACT" - } - } - """); - } - - @Test - public void listComponentsWithProjectWhenProjectDoesNotExistTest() { - prepareComponents(); - Response response = jersey.target("/components") - .queryParam("project_uuid", UUID.randomUUID()) - .queryParam("limit", 2) - .request() - .header(X_API_KEY, apiKey) - .get(); - assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_NOT_FOUND); - assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); - assertThat(getPlainTextBody(response)).contains("Not Found"); - } - @Test public void listComponentsAclTest() { enablePortfolioAccessControl(); initializeWithPermissions(Permissions.PORTFOLIO_MANAGEMENT); prepareComponents(); Response response = jersey.target("/components") - .queryParam("name", "name") + .queryParam("name_contains", "name") .queryParam("limit", 2) .request() .header(X_API_KEY, apiKey)