Skip to content

Implemented project filtering. - #6873

Open
ElenaStroebele wants to merge 2 commits into
DependencyTrack:mainfrom
ElenaStroebele:filter-projects-man
Open

Implemented project filtering.#6873
ElenaStroebele wants to merge 2 commits into
DependencyTrack:mainfrom
ElenaStroebele:filter-projects-man

Conversation

@ElenaStroebele

@ElenaStroebele ElenaStroebele commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

Implemented project filtering for GET /api/v2/projects.

Adds a paginated list endpoint with query-based filters, sorting, optional metrics expansion, and ACL-aware results. Text filters are case-insensitive;
Frontend implementation: DependencyTrack/frontend#1721

Addressed Issue

#926
#2778
(#144)

Additional Details

Filter specification

  • Name (name_contains) — substring match on project name; empty/whitespace values are ignored
  • Version (version_contains) — substring match on version; empty/whitespace values are ignored
  • Tags (tags) — exact match on tag names; multiple values require all tags (AND)
  • Teams (teams) — exact match on team names; multiple values match any team (OR)
  • Classifier (classifier) — exact match; multiple values match any classifier (OR); unknown values → 400
  • Severity (severity) — projects with ≥1 vulnerability of the given severities (from metrics); multiple values match any (OR); unknown values → 400
  • Parent (parent_uuid) — only direct children of that parent
  • Ancestor (ancestor_uuid) — all descendants at any depth (excluding the ancestor)
  • Only root (only_root=true) — only projects without a parent
  • Has children (has_children=true) — only projects with ≥1 direct child visible to the caller
  • Active (is_active) — true = active only; false = inactive only; omit = both
  • Latest (is_latest) — true = latest only; false = non-latest only; omit = both
  • Last BOM import (last_bom_import_since / last_bom_import_before) — inclusive lower / exclusive upper bound; projects that never imported a BOM are excluded when either is set
Screenshot 2026-07-31 163750

Checklist

  • I have read and understand the [contributing guidelines]
  • This PR fixes a defect, and I have provided tests to verify that the fix is effective
  • This PR implements an enhancement, and I have provided tests to verify that it works as intended
  • This PR introduces changes to the database model, and I have updated the [migration changelog] accordingly
  • This PR introduces new or alters existing behavior, and I have updated the [documentation] accordingly
  • This PR is a substantial change (per the [ADR criteria]), and I have added an [ADR] under docs/adr/

@owasp-dt-bot

owasp-dt-bot commented Jul 31, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codacy-production

codacy-production Bot commented Jul 31, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 106 complexity

Metric Results
Complexity 106

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

<#-- @ftlvariable name="hasCursor" type="boolean" -->
<#-- @ftlvariable name="filterHasChildren" type="boolean" -->
<#-- @ftlvariable name="apiProjectAclCondition" type="String" -->
<#assign childProjectAclCondition = apiProjectAclCondition?replace('"PROJECT"."ID"', '"CHILD_PROJECT"."ID"')>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access checks on child projects are redundant. ACL is inherited, so having access to the parent implies access to the children:

Comment on lines +131 to +136
<#if filterHasChildren>
AND EXISTS (
SELECT 1
FROM "PROJECT" AS "CHILD_PROJECT"
WHERE "CHILD_PROJECT"."PARENT_PROJECT_ID" = "PROJECT"."ID"
AND (${childProjectAclCondition}))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in ProjectDao, access checks on child projects are redundant.

filterHasChildren is also a filter specific to a single query, but PaginationSupport is shared code used by multiple queries. Domain-specific filters don't belong here.

Comment on lines +1043 to +1051
, (
SELECT JSONB_STRIP_NULLS(JSONB_BUILD_OBJECT(
'supplier', "SUPPLIER"::JSONB,
'authors', "AUTHORS"::JSONB,
'tools', "TOOLS"::JSONB
))
FROM "PROJECT_METADATA"
WHERE "PROJECT_METADATA"."PROJECT_ID" = "PROJECT"."ID"
) AS "metadataJson"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selected but never used and not returned by the API.

Comment on lines +996 to +999
, "PROJECT"."CPE"
, "PROJECT"."DESCRIPTION"
, "PROJECT"."DIRECT_DEPENDENCIES"
, "PROJECT"."EXTERNAL_REFERENCES"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selected but never used and not returned by the API.

When portfolio access control is enabled and the caller does not have
access to the parent project, an empty result set is returned.

Used for tree-view navigation (lazy loading of one hierarchy level).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the API has no concept of tree-view navigation.

Comment on lines +36 to +37
is_active:
type: boolean

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use shared project state.

Comment on lines +542 to +548
, (
SELECT ARRAY_AGG("TEAM"."NAME")
FROM "TEAM"
INNER JOIN "PROJECT_ACCESS_TEAMS"
ON "PROJECT_ACCESS_TEAMS"."TEAM_ID" = "TEAM"."ID"
WHERE "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = "PROJECT"."ID"
) AS "teamNames"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query is for the API v1 endpoint, but that one doesn't return teamNames.

AND "PROJECT"."ID" > :lastId
</#if>
<#if sortByColumn?has_content>
ORDER BY "PROJECT"."${sortByColumn}" ${sortDirection!"ASC"}, "PROJECT"."ID" ASC

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks pagination when sortByColumn=LAST_RISKSCORE, because it doesn't work for collection projects. Earlier in the query there is a CASE statement that computes lastInheritedRiskScore in a way that takes collections into consideration. We thus need to sort by lastInheritedRiskScore.

Comment on lines +907 to +934
totalCount = getBoundedTotalCountWithProjectAcl(
"FROM \"PROJECT\" WHERE " + String.join(" AND ", whereConditions),
queryParams,
500,
"\"PROJECT\".\"ID\"",
filterHasChildren);
effectiveSortBy = query.sortBy() != null
? query.sortBy()
: ListAllProjectsQuery.SortBy.NAME;
effectiveSortDirection = query.sortDirection() != null
? query.sortDirection()
: SortDirection.ASC;
}

final List<ListProjectsRow> rows = listAllProjects(
whereConditions,
queryParams,
query.limit() + 1,
query.includeMetrics(),
decodedPageToken != null
? decodedPageToken.lastId()
: null,
effectiveSortBy,
effectiveSortDirection,
decodedPageToken != null,
filterHasChildren,
COLLECTION_METRICS_SUBQUERY,
LEAF_METRICS_SUBQUERY);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use withJitDisabled like the other project listing queries:

return withJitDisabled(() -> {
// Count is run separately so the LATERAL fan-out and per-row SubPlans
// in the page query stay bounded to the page rows. Without this, the
// `COUNT(*) OVER()` window function would force materialization of
// every filtered row through the LATERAL.
final TotalCount totalCount = getBoundedTotalCountWithProjectAcl(
"FROM \"PROJECT\" WHERE " + String.join(" AND ", whereConditions),
queryParams,
/* threshold */ null,
"\"PROJECT\".\"ID\"");
final List<ListProjectsRow> rows = getProjects(
whereConditions,
queryParams,
query.includeMetrics(),
COLLECTION_METRICS_SUBQUERY,
LEAF_METRICS_SUBQUERY);
return new Page<>(
rows,
/* nextPageToken */ null,
new TotalCount(totalCount.value(), TotalCount.Type.EXACT));
});

@ElenaStroebele
ElenaStroebele force-pushed the filter-projects-man branch 2 times, most recently from 17feb11 to 1db4366 Compare August 6, 2026 14:34
Signed-off-by: ElenaStroebele <elena.stroebele@rohde-schwarz.com>
@ElenaStroebele
ElenaStroebele force-pushed the filter-projects-man branch 2 times, most recently from 6b1404d to 16a1784 Compare August 6, 2026 14:58
Signed-off-by: ElenaStroebele <elena.stroebele@rohde-schwarz.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants