Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,20 @@ public Response list(
@Parameter(
description = "Filter by product ID (metadata.product_id)"
)
@QueryParam("productId") String productId) {

var filter = uriInfo.getQueryParameters().entrySet().stream().filter(e -> !FIXED_QUERY_PARAMS.contains(e.getKey()))
.collect(Collectors.toMap(Entry::getKey, e -> e.getValue().getFirst()));
@QueryParam("productId") String productId,
@Parameter(
description = "Filter by ExploitIQ status. Valid values: TRUE, FALSE, UNKNOWN"
)
@QueryParam("exploitIqStatus") String exploitIqStatus) {

var filter = uriInfo.getQueryParameters().entrySet().stream()
.filter(e -> !FIXED_QUERY_PARAMS.contains(e.getKey()))
.collect(Collectors.toMap(
Entry::getKey,
e -> e.getValue().size() > 1
? String.join(",", e.getValue())
: e.getValue().getFirst()
));
var result = reportService.list(filter, SortField.fromSortBy(sortBy), page, pageSize);
return Response.ok(result.results)
.header("X-Total-Pages", result.totalPages)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,9 @@ public List<Report> findByName(String name) {
private static final Map<String, String> SORT_MAPPINGS = Map.of(
"completedAt", "input.scan.completed_at",
"submittedAt", "metadata.submitted_at",
"name", "input.scan.id",
"vuln_id", "output.vuln_id");
"vuln_id", "output.vuln_id",
"ref", "input.image.source_info.ref",
"gitRepo", "input.image.source_info.git_repo");

public PaginatedResult<Report> list(Map<String, String> queryFilter, List<SortField> sortFields,
Pagination pagination) {
Expand All @@ -280,11 +281,18 @@ public PaginatedResult<Report> list(Map<String, String> queryFilter, List<SortFi

List<Bson> sorts = new ArrayList<>();
sortFields.forEach(sf -> {
var fieldName = SORT_MAPPINGS.get(sf.field());
if (SortType.ASC.equals(sf.type())) {
sorts.add(Sorts.ascending(fieldName));
if ("state".equals(sf.field())) {
sorts.add(Sorts.descending("input.scan.completed_at"));
sorts.add(Sorts.ascending("error.type"));
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

why do we need this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The sorting for "state" exists because state is computed from multiple fields (not stored) so MongoDB can't sort by it directly

Copy link
Collaborator

Choose a reason for hiding this comment

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

if it's not possible we should not enable it

Copy link
Contributor Author

@rhartuv rhartuv Jan 12, 2026

Choose a reason for hiding this comment

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

There is no state field, it is calculated when reading from the fields:
error.type → "expired"/ "failed"
input.scan.completed_at → "completed"
metadata.sent_at → "sent"
metadata.submitted_at → "queued"
metadata.product_id → "pending"
Therefore, sorting by "state" requires mapping to these fields.
I added this because it appears in both the old UI and FIGMA.

sorts.add(Sorts.descending(fieldName));
var fieldName = SORT_MAPPINGS.get(sf.field());
if (fieldName != null) {
if (SortType.ASC.equals(sf.type())) {
sorts.add(Sorts.ascending(fieldName));
} else {
sorts.add(Sorts.descending(fieldName));
}
}
}
});

Expand Down Expand Up @@ -528,36 +536,127 @@ public void removeBefore(Instant threshold) {
LOGGER.debugf("Removed %s reports before %s", count, threshold);
}

private void handleMultipleValues(String valueString,
java.util.function.Function<String, Bson> filterBuilder,
List<Bson> filters) {
String[] values = valueString.split(",");
if (values.length == 1) {
filters.add(filterBuilder.apply(values[0].trim()));
} else {
List<Bson> valueFilters = new ArrayList<>();
for (String value : values) {
valueFilters.add(filterBuilder.apply(value.trim()));
}
filters.add(valueFilters.size() == 1 ? valueFilters.get(0) : Filters.or(valueFilters));
}
}

private Bson buildQueryFilter(Map<String, String> queryFilter) {
List<Bson> filters = new ArrayList<>();
String vulnId = queryFilter.get("vulnId");
String exploitIqStatus = queryFilter.get("exploitIqStatus");

queryFilter.entrySet().forEach(e -> {

switch (e.getKey()) {
case "reportId":
filters.add(Filters.eq("input.scan.id", e.getValue()));
handleMultipleValues(e.getValue(), (value) ->
Filters.eq("input.scan.id", value), filters);
break;
case "vulnId":
filters.add(Filters.elemMatch("input.scan.vulns", Filters.eq("vuln_id", e.getValue())));
handleMultipleValues(e.getValue(), (value) ->
Filters.elemMatch("input.scan.vulns", Filters.eq("vuln_id", value)), filters);
break;
case "status":
var field = e.getValue();
filters.add(STATUS_FILTERS.get(field));
var statusValues = e.getValue().split(",");
if (statusValues.length == 1) {
var statusFilter = STATUS_FILTERS.get(statusValues[0].trim());
if (statusFilter != null) {
filters.add(statusFilter);
}
} else {
List<Bson> statusFilters = new ArrayList<>();
for (String statusValue : statusValues) {
var statusFilter = STATUS_FILTERS.get(statusValue.trim());
if (statusFilter != null) {
statusFilters.add(statusFilter);
}
}
if (!statusFilters.isEmpty()) {
filters.add(Filters.or(statusFilters));
}
}
break;
case "imageName":
filters.add(Filters.eq("input.image.name", e.getValue()));
handleMultipleValues(e.getValue(), (value) ->
Filters.eq("input.image.name", value), filters);
break;
case "imageTag":
filters.add(Filters.eq("input.image.tag", e.getValue()));
handleMultipleValues(e.getValue(), (value) ->
Filters.eq("input.image.tag", value), filters);
break;
case "productId":
filters.add(Filters.eq("metadata.product_id", e.getValue()));
handleMultipleValues(e.getValue(), (value) ->
Filters.eq("metadata.product_id", value), filters);
break;
case "gitRepo":
var gitRepoValues = e.getValue().split(",");
if (gitRepoValues.length == 1) {
filters.add(Filters.elemMatch("input.image.source_info",
Filters.and(
Filters.eq("type", "code"),
Filters.regex("git_repo", gitRepoValues[0].trim(), "i")
)
));
} else {
List<Bson> gitRepoFilters = new ArrayList<>();
for (String gitRepoValue : gitRepoValues) {
gitRepoFilters.add(Filters.elemMatch("input.image.source_info",
Filters.and(
Filters.eq("type", "code"),
Filters.regex("git_repo", gitRepoValue.trim(), "i")
)
));
}
filters.add(Filters.or(gitRepoFilters));
}
break;
case "exploitIqStatus":
break;
default:
filters.add(Filters.eq(String.format("metadata.%s", e.getKey()), e.getValue()));
handleMultipleValues(e.getValue(), (value) ->
Filters.eq(String.format("metadata.%s", e.getKey()), value), filters);
break;

}
});

if (exploitIqStatus != null && !exploitIqStatus.isEmpty()) {
String[] exploitIqStatusValues = exploitIqStatus.split(",");
List<Bson> exploitIqStatusFilters = new ArrayList<>();

for (String statusValue : exploitIqStatusValues) {
String trimmedStatus = statusValue.trim();
if (vulnId != null && !vulnId.isEmpty()) {
exploitIqStatusFilters.add(Filters.elemMatch("output",
Filters.and(
Filters.eq("vuln_id", vulnId),
Filters.eq("justification.status", trimmedStatus)
)
));
} else {
exploitIqStatusFilters.add(Filters.elemMatch("output",
Filters.eq("justification.status", trimmedStatus)
));
}
}

if (!exploitIqStatusFilters.isEmpty()) {
filters.add(exploitIqStatusFilters.size() == 1
? exploitIqStatusFilters.get(0)
: Filters.or(exploitIqStatusFilters));
}
}
var filter = Filters.empty();
if (!filters.isEmpty()) {
filter = Filters.and(filters);
Expand Down
4 changes: 2 additions & 2 deletions src/main/webui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/redhat.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Morpheus Client</title>
<title>ExploitIQ</title>
</head>
<body>
<div id="root"></div>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading