Skip to content
Open
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 @@ -37,6 +37,7 @@

import kong.unirest.UnirestInstance;
import lombok.Getter;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;
Expand Down Expand Up @@ -66,27 +67,45 @@ public class FoDIssueUpdateCommand extends AbstractFoDJsonNodeOutputCommand impl
protected VulnerabilitySeverityType severity;
@Option(names = {"--comment"}, required = false)
protected String comment;
@Option(names = {"--vuln-ids"}, required = true, split=",")
protected ArrayList<String> vulnIds;
@ArgGroup(exclusive = true, multiplicity = "1")
private VulnSelectionArgs vulnSelection;

static class VulnSelectionArgs {
@Option(names = {"--vuln-ids", "--issue-ids"}, required = true, split=",")
ArrayList<String> vulnIds;
@Option(names = {"--include-all", "--all"}, required = true)
boolean includeAllVulnerabilities;
}

@Override
public JsonNode getJsonNode(UnirestInstance unirest) {
FoDReleaseDescriptor releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest);

// If vulnIds are provided, filter them against the release vulnerabilities using a helper.
int issueUpdateCount = 0;
int totalCount = 0;
int skippedCount = 0;
if ( vulnIds != null && !vulnIds.isEmpty() ) {
var vulnFilterResult = FoDIssueHelper.filterRequestedVulnIds(unirest, releaseDescriptor.getReleaseId(), vulnIds);
ArrayList<String> effectiveVulnIds;

if (vulnSelection.includeAllVulnerabilities) {
effectiveVulnIds = new ArrayList<>(FoDIssueHelper.getAllVulnNumericIdsForRelease(unirest, releaseDescriptor.getReleaseId()));
totalCount = effectiveVulnIds.size();
issueUpdateCount = totalCount;
Comment thread
kadraman marked this conversation as resolved.
if (effectiveVulnIds.isEmpty()) {
return createNoOpResponse(totalCount, skippedCount, issueUpdateCount);
}
} else {
var vulnFilterResult = FoDIssueHelper.filterRequestedVulnIds(unirest, releaseDescriptor.getReleaseId(), vulnSelection.vulnIds);
totalCount = vulnFilterResult.totalCount();
issueUpdateCount = vulnFilterResult.kept().size();
skippedCount = vulnFilterResult.skipped().size();
vulnIds = new ArrayList<>(vulnFilterResult.kept());
effectiveVulnIds = new ArrayList<>(vulnFilterResult.kept());
if (!vulnFilterResult.skipped().isEmpty()) {
Comment thread
kadraman marked this conversation as resolved.
LOG.debug("Skipped vulnerabilities: {}", vulnFilterResult.skipped());
vulnFilterResult.skipped().forEach(vid -> LOG.warn("Vulnerability {} not found in release {}, skipping", vid, releaseDescriptor.getReleaseId()));
}
if (effectiveVulnIds.isEmpty()) {
return createNoOpResponse(totalCount, skippedCount, issueUpdateCount);
}
}

Map<String, String> attributeUpdates = issueAttrsUpdate.getAttributes();
Expand All @@ -95,15 +114,27 @@ public JsonNode getJsonNode(UnirestInstance unirest) {
// Validate auditor and developer status values against attribute picklists
ResolvedStatuses resolvedStatuses = resolveStatuses(unirest);

FoDBulkIssueUpdateRequest issueUpdateRequest = buildIssueUpdateRequest(unirest, resolvedStatuses.developerStatusValue(), resolvedStatuses.auditorStatusValue(), jsonAttrs);
FoDBulkIssueUpdateResponse resp = performUpdate(unirest, releaseDescriptor.getReleaseId(), issueUpdateRequest, totalCount, skippedCount, issueUpdateCount);
FoDBulkIssueUpdateRequest issueUpdateRequest = buildIssueUpdateRequest(unirest, resolvedStatuses.developerStatusValue(), resolvedStatuses.auditorStatusValue(), jsonAttrs, effectiveVulnIds);
FoDBulkIssueUpdateResponse resp = performUpdate(unirest, releaseDescriptor.getReleaseId(), issueUpdateRequest, totalCount, skippedCount, issueUpdateCount, effectiveVulnIds);
return resp.asObjectNode()
.put("totalCount", totalCount)
.put("skippedCount", skippedCount)
.put("errorCount", lastErrorCount)
.put("updateCount", issueUpdateCount);
}

private JsonNode createNoOpResponse(int totalCount, int skippedCount, int issueUpdateCount) {
lastTotalCount = totalCount;
lastSkippedCount = skippedCount;
lastErrorCount = 0;
lastUpdateCount = issueUpdateCount;
return objectMapper.createObjectNode()
.put("totalCount", totalCount)
.put("skippedCount", skippedCount)
.put("errorCount", 0)
.put("updateCount", issueUpdateCount);
}

private record ResolvedStatuses(String developerStatusValue, String auditorStatusValue) {}

private ResolvedStatuses resolveStatuses(UnirestInstance unirest) {
Expand All @@ -122,20 +153,20 @@ private ResolvedStatuses resolveStatuses(UnirestInstance unirest) {
return new ResolvedStatuses(developerStatusValue, auditorStatusValue);
}

private FoDBulkIssueUpdateRequest buildIssueUpdateRequest(UnirestInstance unirest, String developerStatusValue, String auditorStatusValue, JsonNode jsonAttrs) {
private FoDBulkIssueUpdateRequest buildIssueUpdateRequest(UnirestInstance unirest, String developerStatusValue, String auditorStatusValue, JsonNode jsonAttrs, ArrayList<String> effectiveVulnIds) {
return FoDBulkIssueUpdateRequest.builder()
.user(unirest, user)
.developerStatus(developerStatusValue)
.auditorStatus(auditorStatusValue)
.severity(severity != null ? severity.toString() : null)
.comment(comment)
.vulnerabilityIds(vulnIds)
.vulnerabilityIds(effectiveVulnIds)
.attributes(jsonAttrs)
.build().validate();
}

private FoDBulkIssueUpdateResponse performUpdate(UnirestInstance unirest, String releaseId, FoDBulkIssueUpdateRequest request, int totalCount, int skippedCount, int issueUpdateCount) {
LOG.debug("Updating issues: {}", vulnIds);
private FoDBulkIssueUpdateResponse performUpdate(UnirestInstance unirest, String releaseId, FoDBulkIssueUpdateRequest request, int totalCount, int skippedCount, int issueUpdateCount, ArrayList<String> effectiveVulnIds) {
LOG.debug("Updating issues: {}", effectiveVulnIds);
FoDBulkIssueUpdateResponse resp = FoDIssueHelper.updateIssues(unirest, releaseId, request);
long errorCount = resp.getResults().stream().filter(r -> r.getErrorCode() != 0).count();
resp.setIssueCount(resp.getResults().size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ public final FoDBulkIssueUpdateRequest validate(Consumer<List<String>> validatio

@JsonIgnore
public final FoDBulkIssueUpdateRequest validate() {
return validate(messages->{throw new FcliSimpleException("Unable to update issues:\n\t"+String.join("\n\t", messages)); });
return validate(messages -> { throw new FcliSimpleException("Unable to update issues:\n\t"+String.join("\n\t", messages)); });
}

@JsonIgnore
private final void validateRequired(List<String> messages, Object obj, String message) {
private void validateRequired(List<String> messages, Object obj, String message) {
if ( obj==null || (obj instanceof String && StringUtils.isBlank((String)obj)) ) {
messages.add(message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,38 @@ public static java.util.Set<String> getVulnIdsForRelease(UnirestInstance unirest
return result;
}

/**
* Returns the numeric {@code id} for every vulnerability in the release, with no duplicates.
* Unlike {@link #getVulnIdsForRelease}, which collects both {@code id} and {@code vulnId} per
* item (needed for user-supplied identifier matching), this method collects only the canonical
* numeric id so that the result count matches the actual vulnerability count and the bulk-edit
* API is not sent duplicate identifiers.
*/
public static List<String> getAllVulnNumericIdsForRelease(UnirestInstance unirest, String releaseId) {
var result = new ArrayList<String>();
try {
var request = unirest.get(FoDUrls.VULNERABILITIES)
.routeParam("relId", releaseId)
.queryString("fields", "id")
.queryString("includeFixed", "true")
.queryString("includeSuppressed", "true");
var stream = FoDPagingHelper.pagedRequest(request).stream()
.map(HttpResponse::getBody)
.map(FoDInputTransformer::getItems)
.filter(items -> items != null && items.isArray())
.map(ArrayNode.class::cast)
.flatMap(JsonHelper::stream);
for ( JsonNode item : (Iterable<JsonNode>)stream::iterator ) {
if ( item.has("id") && !item.get("id").isNull() ) {
result.add(item.get("id").asText().trim());
}
}
} catch (Exception e) {
throw new FcliTechnicalException("Error retrieving vulnerabilities for release", e);
}
return result;
}

/**
* Resolve a status value (developer/auditor) against one or more FoD attribute picklists.
* Returns the canonical picklist name when found, or throws a FcliSimpleException listing allowed values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,8 +889,8 @@ fcli.fod.issue.output.table.header.totalCount = Total Issues
fcli.fod.issue.output.table.header.updateCount = Issues Updated
fcli.fod.issue.output.table.header.skippedCount = Issues Skipped
fcli.fod.issue.output.table.header.errorCount = Errors
fcli.fod.issue.list.usage.header = List vulnerabilities.
fcli.fod.issue.list.usage.description = This command allows for listing FoD vulnerability data \
fcli.fod.issue.list.usage.header = List issues (vulnerabilities).
fcli.fod.issue.list.usage.description = This command allows for listing FoD issue data \
for a given application or release. By default, only visible issues will be returned; the --include option can \
be used to (also) include suppressed or fixed issues. If any such issues are included, the \
default table output will show (S) and/or (F) for respectively suppressed and fixed issues. \
Expand Down Expand Up @@ -922,17 +922,20 @@ fcli.fod.issue.list.includeIssue = By default, only visible issues will be retur
for example `--include visible,fixed` (to return both visible and fixed issues) or `--include \
fixed` (to return only fixed issues). Allowed values: ${COMPLETION-CANDIDATES}.
fcli.fod.issue.list.aggregate = Include aggregation data.
fcli.fod.issue.update.usage.header = Bulk update vulnerabilities.
fcli.fod.issue.update.usage.header = Bulk update issues (vulnerabilities).
fcli.fod.issue.update.usage.description = This command allows for updating the audit information \
for multiple vulnerabilities. Note: for "vuln-ids" you can use either the numeric Id as shown in the FOD UI, \
or the "vulnId" UUID field that is retrieved using the `fcli fod issue ls` command.
fcli.fod.issue.update.user = The username or user id of the user the update will be recorded as.
fcli.fod.issue.update.dev-status = The Developer Status to set for the vulnerabilities, see the FoD UI for valid values.
fcli.fod.issue.update.auditor-status = The Auditor Status to set for the vulnerabilities, see the FoD UI for valid values.
fcli.fod.issue.update.severity = The Severity to set for the vulnerabilities. Allowed values: ${COMPLETION-CANDIDATES}.
fcli.fod.issue.update.comment = A comment to apply to all of the vulnerabilities that are updated.
fcli.fod.issue.update.vuln-ids = Comma separated list of the vulnerability ids to be updated.
fcli.fod.issue.update.attributes = A comma separated list of "attributeName=attributeValue" pairs to set on the vulnerabilities. \
for multiple issues (vulnerabilities). Note: for --vuln-ids/--issue-ids, you can use either the numeric Id as shown in the FoD UI, \
or the "vulnId" UUID field that is retrieved using the `fcli fod issue ls` command. Alternatively, \
use --include-all/--all to update all issues from the specified release without providing issue ids.
fcli.fod.issue.update.user = The username or user id of the user to assign the issue to.
fcli.fod.issue.update.dev-status = The Developer Status to set for the issues, see the FoD UI for valid values.
fcli.fod.issue.update.auditor-status = The Auditor Status to set for the issues, see the FoD UI for valid values.
fcli.fod.issue.update.severity = The Severity to set for the issues. Allowed values: ${COMPLETION-CANDIDATES}.
fcli.fod.issue.update.comment = A comment to apply to all of the issues that are updated.
fcli.fod.issue.update.vuln-ids = Comma separated list of the issue ids to be updated.
fcli.fod.issue.update.include-all = Include all the issues from the specified release in the update. \
%nWARNING: This will update all issues in the specified release regardless of their current status, so use with caution.
fcli.fod.issue.update.attributes = A comma separated list of "attributeName=attributeValue" pairs to set on the issues. \
It is recommended to provide attribute names as they appear in the FoD UI. For example: "Business Criticality=High,Attribute 2=Value". \
You can also use multiple --attributes options to provide more "attributeName=attributeValue" pairs.

Expand Down
Loading