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 @@ -456,7 +456,9 @@ public enum AttributeTypes {
Application(1),
Vulnerability(2),
Microservice(3),
Release(4);
Release(4),
Issue(5),
Scan(6);

private final int _val;

Expand All @@ -470,17 +472,20 @@ public int getValue() {

public String toString() {
switch (this._val) {
case 0:
return "All";
case 1:
return "Application";
case 2:
return "Vulnerability";
case 3:
return "Microservice";
case 4:
default:
return "Release";
case 5:
return "Issue";
case 6:
return "Scan";
default:
return "All";
}
}

Expand All @@ -493,7 +498,7 @@ public static AttributeTypes fromInt(int val) {
case 2:
return Vulnerability;
case 3:
return Microservice;
return Microservice;
case 4:
default:
return Release;
Expand Down Expand Up @@ -552,6 +557,36 @@ public enum DeveloperStatusType {
public String getValue() {
return this.value;
}

/**
* Resolve an input string which may be either the enum constant name (e.g. "InRemediation")
* or the user-facing value (e.g. "In Remediation") to the canonical user-facing value.
* Comparison for the enum name is case-insensitive. If no match is found an empty
* Optional is returned.
*/
public static java.util.Optional<String> resolveValue(String input) {
if (input == null) return java.util.Optional.empty();
var trimmed = input.trim();
if (trimmed.isEmpty()) return java.util.Optional.empty();
// First try matching enum constant name (case-insensitive)
for (DeveloperStatusType t : values()) {
if (t.name().equalsIgnoreCase(trimmed)) {
return java.util.Optional.of(t.getValue());
}
}
// Then try matching the display value exactly (case-sensitive and case-insensitive fallback)
for (DeveloperStatusType t : values()) {
if (t.getValue().equals(trimmed)) {
return java.util.Optional.of(t.getValue());
}
}
for (DeveloperStatusType t : values()) {
if (t.getValue().equalsIgnoreCase(trimmed)) {
return java.util.Optional.of(t.getValue());
}
}
return java.util.Optional.empty();
}
}

public enum AuditorStatusType {
Expand All @@ -564,7 +599,7 @@ public enum AuditorStatusType {
//Suspicious("Suspicious"),
//ProposedNotAnIssue("Proposed Not an Issue"),
RiskAccepted("Risk Accepted"),
NotAnIssue("Not an Issues");
NotAnIssue("Not an Issue");

public final String value;

Expand All @@ -575,6 +610,35 @@ public enum AuditorStatusType {
public String getValue() {
return this.value;
}

/**
* Resolve an input string which may be either the enum constant name (e.g. "PendingReview")
* or the user-facing value (e.g. "Pending Review") to the canonical user-facing value.
* Comparison for the enum name is case-insensitive. Returns an empty Optional when no match.
*/
public static java.util.Optional<String> resolveValue(String input) {
if (input == null) return java.util.Optional.empty();
var trimmed = input.trim();
if (trimmed.isEmpty()) return java.util.Optional.empty();
// Try matching enum constant name first
for (AuditorStatusType t : values()) {
if (t.name().equalsIgnoreCase(trimmed)) {
return java.util.Optional.of(t.getValue());
}
}
// Then match display value
for (AuditorStatusType t : values()) {
if (t.getValue().equals(trimmed)) {
return java.util.Optional.of(t.getValue());
}
}
for (AuditorStatusType t : values()) {
if (t.getValue().equalsIgnoreCase(trimmed)) {
return java.util.Optional.of(t.getValue());
}
}
return java.util.Optional.empty();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@
package com.fortify.cli.fod.issue.cli.cmd;

import java.util.ArrayList;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fortify.cli.common.mcp.MCPInclude;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin;
import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand;
import com.fortify.cli.fod._common.util.FoDEnums.AuditorStatusType;
import com.fortify.cli.fod._common.util.FoDEnums.DeveloperStatusType;
import com.fortify.cli.fod._common.util.FoDEnums.VulnerabilitySeverityType;
import com.fortify.cli.fod.attribute.cli.mixin.FoDAttributeUpdateOptions;
import com.fortify.cli.fod.issue.helper.FoDBulkIssueUpdateRequest;
import com.fortify.cli.fod.issue.helper.FoDBulkIssueUpdateResponse;
import com.fortify.cli.fod.issue.helper.FoDIssueHelper;
Expand All @@ -42,58 +43,120 @@
@Command(name = OutputHelperMixins.Update.CMD_NAME)
public class FoDIssueUpdateCommand extends AbstractFoDJsonNodeOutputCommand implements IActionCommandResultSupplier {
private static final Logger LOG = LoggerFactory.getLogger(FoDIssueUpdateCommand.class);
// Fields to hold last-run counts so getActionCommandResult() can report status
private int lastTotalCount = 0;
private int lastSkippedCount = 0;
private long lastErrorCount = 0;
private int lastUpdateCount = 0;
@Getter @Mixin private OutputHelperMixins.Update outputHelper;
@Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins
@Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver;
@Mixin private FoDAttributeUpdateOptions.OptionalAttrOption issueAttrsUpdate;
private final ObjectMapper objectMapper = new ObjectMapper();

@Option(names = {"--user"}, required = true)
protected String user;
@Option(names = {"--dev-status"}, required = false)
protected DeveloperStatusType developerStatus;
protected String developerStatus;
@Option(names = {"--auditor-status"}, required = false)
protected AuditorStatusType auditorStatus;
protected String auditorStatus;
@Option(names = {"--severity"}, required = false)
protected VulnerabilitySeverityType severity;
@Option(names = {"--comment"}, required = false)
protected String comment;
@Option(names = {"--vuln-ids"}, required = true, split=",")
protected ArrayList<String> vulnIds;

private long errorCount = 0;

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

FoDBulkIssueUpdateRequest issueUpdateRequest = FoDBulkIssueUpdateRequest.builder()
// 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);
totalCount = vulnFilterResult.totalCount();
issueUpdateCount = vulnFilterResult.kept().size();
skippedCount = vulnFilterResult.skipped().size();
vulnIds = new ArrayList<>(vulnFilterResult.kept());
if (!vulnFilterResult.skipped().isEmpty()) {
LOG.debug("Skipped vulnerabilities: {}", vulnFilterResult.skipped());
vulnFilterResult.skipped().forEach(vid -> LOG.warn("Vulnerability {} not found in release {}, skipping", vid, releaseDescriptor.getReleaseId()));
}
}

Map<String, String> attributeUpdates = issueAttrsUpdate.getAttributes();
JsonNode jsonAttrs = FoDIssueHelper.buildIssueAttributesNode(unirest, attributeUpdates);

// 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);
return resp.asObjectNode()
.put("totalCount", totalCount)
.put("skippedCount", skippedCount)
.put("errorCount", lastErrorCount)
.put("updateCount", issueUpdateCount);
}

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

private ResolvedStatuses resolveStatuses(UnirestInstance unirest) {
String auditorStatusValue = null;
if ( auditorStatus != null && !auditorStatus.isBlank() ) {
auditorStatusValue = FoDIssueHelper.resolveStatusValue(unirest, auditorStatus, new String[]{
"Auditor Status (Non suppressed)", "Auditor Status (Suppressed)"
}, "auditor-status");
}
String developerStatusValue = null;
if ( developerStatus != null && !developerStatus.isBlank() ) {
developerStatusValue = FoDIssueHelper.resolveStatusValue(unirest, developerStatus, new String[]{
"Developer Status (Open)", "Developer Status (Closed)"
}, "developer-status");
}
return new ResolvedStatuses(developerStatusValue, auditorStatusValue);
}

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

LOG.debug("Updating issues: {}", vulnIds.toString());
FoDBulkIssueUpdateResponse resp = FoDIssueHelper.updateIssues(unirest, releaseDescriptor.getReleaseId(), issueUpdateRequest);
errorCount = resp.getResults()
.stream()
.filter(r -> r.getErrorCode() != 0)
.count();
private FoDBulkIssueUpdateResponse performUpdate(UnirestInstance unirest, String releaseId, FoDBulkIssueUpdateRequest request, int totalCount, int skippedCount, int issueUpdateCount) {
LOG.debug("Updating issues: {}", vulnIds);
FoDBulkIssueUpdateResponse resp = FoDIssueHelper.updateIssues(unirest, releaseId, request);
long errorCount = resp.getResults().stream().filter(r -> r.getErrorCode() != 0).count();
resp.setIssueCount(resp.getResults().size());
resp.setErrorCount(errorCount);
LOG.debug("Response: {}", resp.getResults().toString());
return resp.asObjectNode().put("issueCount", resp.getResults().size()).put("errorCount", errorCount);
// Store last-run counts for action result reporting
lastTotalCount = totalCount;
lastSkippedCount = skippedCount;
lastErrorCount = errorCount;
lastUpdateCount = issueUpdateCount;
LOG.debug("Response: {}", resp.getResults());
return resp;
}

@Override
public String getActionCommandResult() {
return "ISSUES_UPDATED";
if ( lastErrorCount>0 ) { return "UPDATED_WITH_ERRORS"; }
if ( lastSkippedCount>0 ) { return "UPDATED_WITH_SKIPPED"; }
return "UPDATED";
}

@Override
public boolean isSingular() {
return true;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.formkiq.graalvm.annotations.Reflectable;
import com.fortify.cli.common.exception.FcliSimpleException;
import com.fortify.cli.fod.access_control.helper.FoDUserHelper;
Expand All @@ -43,6 +44,7 @@ public class FoDBulkIssueUpdateRequest {
private String severity;
private String comment;
private ArrayList<String> vulnerabilityIds;
private JsonNode attributes;

@JsonIgnore
public final FoDBulkIssueUpdateRequest validate(Consumer<List<String>> validationMessageConsumer) {
Expand All @@ -58,7 +60,7 @@ public final FoDBulkIssueUpdateRequest validate(Consumer<List<String>> validatio
public final FoDBulkIssueUpdateRequest validate() {
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) {
if ( obj==null || (obj instanceof String && StringUtils.isBlank((String)obj)) ) {
Expand All @@ -78,4 +80,4 @@ public FoDBulkIssueUpdateRequestBuilder user(UnirestInstance unirest, String use
return userId(userId);
}
}
}
}
Loading
Loading