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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -990,3 +990,8 @@
### Miscellaneous Chores

* release 1.0.0 ([d983f62](https://www.github.com/fortify-ps/fcli/commit/d983f62c01d38ca5cef8963f9ce98c7a2d19c0ab))





Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static RemediationMetric applyRemediations(FprHandle fprHandle, String so
LOG.info("Starting apply auto-remediation process for file: {}", fprHandle.getFprPath());

if (!fprHandle.hasRemediations()) {
LOG.error("FPR file does not contain remediations.xml file: {}", fprHandle.getFprPath());
//LOG.error("FPR file does not contain remediations.xml file: {}", fprHandle.getFprPath());
throw new AviatorSimpleException("FPR file does not contain remediations.xml file.");
}
LOG.info("FPR validation successful");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.fortify.cli.aviator._common.session.user.cli.cmd.AviatorUserSessionCommands;
import com.fortify.cli.aviator.app.cli.cmd.AviatorAppCommands;
import com.fortify.cli.aviator.entitlement.cli.cmd.AviatorEntitlementCommands;
import com.fortify.cli.aviator.fod.cli.cmd.AviatorFoDCommands;
import com.fortify.cli.aviator.ssc.cli.cmd.AviatorSSCCommands;
import com.fortify.cli.aviator.token.cli.cmd.AviatorTokenCommands;
import com.fortify.cli.common.cli.cmd.AbstractContainerCommand;
Expand All @@ -28,18 +29,18 @@
subcommands = {
// This list of product subcommands should be in alphabetical
// order, except for:
// - session command (should be the first command, as it is a
// - session command (should be the first command, as it is a
// prerequisite for all other commands)
// - rest command (should be the last command, as it's a low-level
// command and looks better in the usage command list, as usually
// 'rest' has a different header ('Interact with' compared to most
// command and looks better in the usage command list, as usually
// 'rest' has a different header ('Interact with' compared to most
// other commands ('Manage').
AviatorAdminConfigCommands.class,
AviatorUserSessionCommands.class,
AviatorAppCommands.class,
AviatorEntitlementCommands.class,
AviatorSSCCommands.class,
// AviatorFoDCommands.class,
AviatorFoDCommands.class,
AviatorTokenCommands.class,
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import com.fortify.cli.aviator.fod.helper.AviatorFoDApplyRemediationsHelper;
import com.fortify.cli.aviator.util.FprHandle;
import com.fortify.cli.common.exception.FcliSimpleException;
import com.fortify.cli.common.output.cli.mixin.IOutputHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
import com.fortify.cli.common.output.transform.IRecordTransformer;
import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin;
Expand All @@ -40,13 +40,15 @@

import kong.unirest.GetRequest;
import kong.unirest.UnirestInstance;
import lombok.Getter;
import lombok.SneakyThrows;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;

@Command(name = "apply-remediations")
public class AviatorFoDApplyRemediationsCommand extends AbstractFoDJsonNodeOutputCommand implements IRecordTransformer, IActionCommandResultSupplier {
@Getter @Mixin private OutputHelperMixins.TableNoQuery outputHelper;
@Mixin private ProgressWriterFactoryMixin progressWriterFactoryMixin;
@Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins
@Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver;
Expand All @@ -62,7 +64,7 @@ public JsonNode getJsonNode(UnirestInstance unirest) {
return processFprRemediations(unirest, rd, logger);
}
}

private void validateSourceCodeDirectory() {
if (sourceCodeDirectory == null || sourceCodeDirectory.isBlank()) {
throw new FcliSimpleException("--source-dir must specify a valid directory path");
Expand All @@ -79,6 +81,8 @@ private JsonNode processFprRemediations(UnirestInstance unirest, FoDReleaseDescr
logger.progress("Status: Processing FPR with Aviator for Applying Auto Remediations");
try (FprHandle fprHandle = new FprHandle(downloadedFprPath)) {
var remediationMetric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, logger);
LOG.info("Applied remediation {}", remediationMetric.appliedRemediations());
LOG.info("Total remediation {}", remediationMetric.totalRemediations());
String status = remediationMetric.appliedRemediations() > 0 ? "Remediation-Applied" : "No-Remediation-Applied";
return AviatorFoDApplyRemediationsHelper.buildResultNode(rd, remediationMetric.totalRemediations(), remediationMetric.appliedRemediations(), remediationMetric.skippedRemediations(), status);
}
Expand Down Expand Up @@ -129,10 +133,6 @@ public boolean isSingular() {
return false;
}

@Override
public IOutputHelper getOutputHelper() {
return null;
}

@Override
public String getActionCommandResult() {
Expand All @@ -141,6 +141,6 @@ public String getActionCommandResult() {

@Override
public JsonNode transformRecord(JsonNode record) {
return null;
return record;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

@CommandLine.Command(
name = "fod",
hidden = true,
subcommands = {
AviatorFoDApplyRemediationsCommand.class
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.util.List;

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

import com.fasterxml.jackson.databind.JsonNode;
import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
import com.fortify.cli.aviator.applyRemediation.ApplyAutoRemediationOnSource;
import com.fortify.cli.aviator.config.AviatorLoggerImpl;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCApplyRemediationsHelper;
import com.fortify.cli.aviator.ssc.helper.SinceOptionHelper;
import com.fortify.cli.aviator.util.FprHandle;
import com.fortify.cli.common.exception.FcliSimpleException;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
Expand All @@ -33,8 +37,9 @@
import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand;
import com.fortify.cli.ssc._common.rest.ssc.SSCUrls;
import com.fortify.cli.ssc._common.rest.ssc.transfer.SSCFileTransferHelper;
import com.fortify.cli.ssc.artifact.cli.mixin.SSCArtifactResolverMixin;
import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin;
import com.fortify.cli.ssc.artifact.helper.SSCArtifactDescriptor;
import com.fortify.cli.ssc.artifact.helper.SSCArtifactHelper;

import kong.unirest.UnirestInstance;
import lombok.Getter;
Expand All @@ -47,49 +52,147 @@
public class AviatorSSCApplyRemediationsCommand extends AbstractSSCJsonNodeOutputCommand implements IRecordTransformer, IActionCommandResultSupplier {
@Getter @Mixin private OutputHelperMixins.TableNoQuery outputHelper;
@Mixin private ProgressWriterFactoryMixin progressWriterFactoryMixin;
//Downloading of the FPR will be based on artifact and not app version
@Mixin private SSCArtifactResolverMixin.RequiredOption artifactResolver;
@Mixin private SSCAppVersionResolverMixin.OptionalOption appVersionResolver;

@Option(names = {"--artifact-id"}, descriptionKey = "fcli.aviator.ssc.apply-remediations.artifact-id")
private String artifactId;

@Option(names = {"--latest"}, descriptionKey = "fcli.aviator.ssc.apply-remediations.latest")
private boolean latest;

@Option(names = {"--all-open-issues"}, descriptionKey = "fcli.aviator.ssc.apply-remediations.all-open-issues")
private boolean allOpenIssues;

@Option(names = {"--since"}, descriptionKey = "fcli.aviator.ssc.apply-remediations.since")
private String since;

private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCApplyRemediationsCommand.class);
@Option(names = {"--source-dir"}) private String sourceCodeDirectory = System.getProperty("user.dir");
@Option(names = {"--source-dir"}, descriptionKey = "fcli.aviator.ssc.apply-remediations.source-dir")
private String sourceCodeDirectory = System.getProperty("user.dir");

@Override
@SneakyThrows
public JsonNode getJsonNode(UnirestInstance unirest) {
validateOptions();
validateSourceCodeDirectory();
OffsetDateTime sinceDate = SinceOptionHelper.parse(since);
try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
AviatorLoggerImpl logger = new AviatorLoggerImpl(progressWriter);
SSCArtifactDescriptor ad = artifactResolver.getArtifactDescriptor(unirest);
if (allOpenIssues) {
return processAllAviatorArtifacts(unirest, logger, sinceDate);
}
SSCArtifactDescriptor ad = resolveArtifactDescriptor(unirest, sinceDate);
return processFprRemediations(unirest, ad, logger);
}
}


private void validateOptions() {
boolean hasArtifactId = artifactId != null && !artifactId.isBlank();
boolean hasSince = since != null && !since.isBlank();
int optionCount = (hasArtifactId ? 1 : 0) + (latest ? 1 : 0) + (allOpenIssues ? 1 : 0);

if (optionCount > 1) {
throw new FcliSimpleException("--artifact-id, --latest, and --all-open-issues are mutually exclusive");
}
if (optionCount == 0) {
throw new FcliSimpleException("One of --artifact-id, --latest, or --all-open-issues must be specified");
}
if ((latest || allOpenIssues) && appVersionResolver.getAppVersionNameOrId() == null) {
throw new FcliSimpleException("--av/--appversion is required when using --latest or --all-open-issues");
}
if (hasSince && hasArtifactId) {
throw new FcliSimpleException("--since cannot be used with --artifact-id; use --latest or --all-open-issues");
}
if (hasSince && !latest && !allOpenIssues) {
throw new FcliSimpleException("--since can only be used with --latest or --all-open-issues");
}
}

private SSCArtifactDescriptor resolveArtifactDescriptor(UnirestInstance unirest, OffsetDateTime sinceDate) {
if (latest) {
return getLatestAviatorArtifact(unirest, sinceDate);
} else {
return SSCArtifactHelper.getArtifactDescriptor(unirest, artifactId);
}
}

private SSCArtifactDescriptor getLatestAviatorArtifact(UnirestInstance unirest, OffsetDateTime sinceDate) {
String appVersionId = appVersionResolver.getAppVersionId(unirest);
return SSCArtifactHelper.getLatestAviatorArtifact(unirest, appVersionId, sinceDate);
}

private void validateSourceCodeDirectory() {
if (sourceCodeDirectory == null || sourceCodeDirectory.isBlank()) {
throw new FcliSimpleException("--source-dir must specify a valid directory path");
}
}

@SneakyThrows
private JsonNode processFprRemediations(UnirestInstance unirest, SSCArtifactDescriptor ad, AviatorLoggerImpl logger) {
private JsonNode processAllAviatorArtifacts(UnirestInstance unirest, AviatorLoggerImpl logger, OffsetDateTime sinceDate) {
String appVersionId = appVersionResolver.getAppVersionId(unirest);
List<SSCArtifactDescriptor> artifacts = SSCArtifactHelper.getAllAviatorArtifacts(unirest, appVersionId, sinceDate);

int totalRemediations = 0, appliedRemediations = 0, skippedRemediations = 0;
int artifactsProcessed = 0, artifactsSkipped = 0;

for (SSCArtifactDescriptor ad : artifacts) {
int artifactIndex = artifactsProcessed + artifactsSkipped + 1;
logger.progress("Processing artifact " + artifactIndex + "/" + artifacts.size() + " (id=" + ad.getId() + ")");
Path fprPath = null;
try {
fprPath = downloadArtifactFpr(unirest, ad, logger);
try (FprHandle fprHandle = new FprHandle(fprPath)) {
var metric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, logger);
totalRemediations += metric.totalRemediations();
appliedRemediations += metric.appliedRemediations();
skippedRemediations += metric.skippedRemediations();
artifactsProcessed++;
}
} catch (AviatorSimpleException e) {
LOG.warn("Skipping artifact {} as {}", ad.getId(), e.getMessage());
artifactsSkipped++;
} finally {
if (fprPath != null) {
try {
Files.deleteIfExists(fprPath);
} catch (IOException e) {
LOG.warn("Failed to delete temporary FPR file: {}", fprPath, e);
}
}
}
}

String action = appliedRemediations > 0 ? "Remediation-Applied" : "No-Remediation-Applied";
return AviatorSSCApplyRemediationsHelper.buildAggregatedResultNode(
appVersionId, artifactsProcessed, artifactsSkipped,
totalRemediations, appliedRemediations, skippedRemediations, action);
}

@SneakyThrows
private Path downloadArtifactFpr(UnirestInstance unirest, SSCArtifactDescriptor ad, AviatorLoggerImpl logger) {
Path fprPath = Files.createTempFile("aviator_" + ad.getId() + "_", ".fpr");
try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()){
logger.progress("Status: Downloading Audited FPR from SSC");
try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
logger.progress("Status: Downloading Audited FPR from SSC (artifact id=" + ad.getId() + ")");
SSCFileTransferHelper.download(
unirest,
SSCUrls.DOWNLOAD_ARTIFACT(ad.getId(), true),
fprPath.toFile(),
SSCFileTransferHelper.ISSCAddDownloadTokenFunction.ROUTEPARAM_DOWNLOADTOKEN,
progressWriter);
}
return fprPath;
}

@SneakyThrows
private JsonNode processFprRemediations(UnirestInstance unirest, SSCArtifactDescriptor ad, AviatorLoggerImpl logger) {
Path fprPath = downloadArtifactFpr(unirest, ad, logger);
try {
logger.progress("Status: Processing FPR with Aviator for Applying Auto Remediations");

try (FprHandle fprHandle = new FprHandle(fprPath)) {
var remediationMetric = ApplyAutoRemediationOnSource.applyRemediations(fprHandle, sourceCodeDirectory, logger);
String status = remediationMetric.appliedRemediations() > 0 ? "Remediation-Applied" : "No-Remediation-Applied";

return AviatorSSCApplyRemediationsHelper.buildResultNode(ad, remediationMetric.totalRemediations(), remediationMetric.appliedRemediations(), remediationMetric.skippedRemediations(), status);
}

} finally {
try {
Files.deleteIfExists(fprPath);
Expand All @@ -100,7 +203,7 @@ private JsonNode processFprRemediations(UnirestInstance unirest, SSCArtifactDesc
}

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

@Override
public String getActionCommandResult() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package com.fortify.cli.aviator.ssc.helper;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.common.json.JsonHelper;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
import com.fortify.cli.ssc.artifact.helper.SSCArtifactDescriptor;

Expand All @@ -24,18 +25,47 @@ public final class AviatorSSCApplyRemediationsHelper {
private AviatorSSCApplyRemediationsHelper() {}

/**
* Builds the final JSON result node for the command output.
* @param ad The SSCAppVersionDescriptor.
* @param totalRemediation Total no. of Remediations
* @param appliedRemediation Remediations that has been applied successfully
* @param skippedRemediation Remediations that has been skipped
* Builds the unified JSON result node for a single-artifact remediation (--artifact-id or --latest).
* Uses the same output shape as buildAggregatedResultNode for consistent table columns.
* @param ad The SSCArtifactDescriptor; its projectVersionId is used as appVersionId.
* @param totalRemediation Total no. of remediations in the artifact.
* @param appliedRemediation Remediations applied successfully.
* @param skippedRemediation Remediations skipped.
* @param action Final action.
* @return An ObjectNode representing the result.
*/

public static ObjectNode buildResultNode(SSCArtifactDescriptor ad,int totalRemediation, int appliedRemediation, int skippedRemediation, String action) {
ObjectNode result = ad.asObjectNode();
public static ObjectNode buildResultNode(SSCArtifactDescriptor ad, int totalRemediation, int appliedRemediation, int skippedRemediation, String action) {
ObjectNode result = JsonHelper.getObjectMapper().createObjectNode();
result.put("appVersionId", ad.asObjectNode().path("projectVersionId").asText("N/A"));
result.put("artifactId", ad.getId());
result.put("artifactsProcessed", 1);
result.put("artifactsSkipped", 0);
result.put("totalRemediation", totalRemediation);
result.put("appliedRemediation", appliedRemediation);
result.put("skippedRemediation", skippedRemediation);
result.put(IActionCommandResultSupplier.actionFieldName, action);
return result;
}

/**
* Builds the unified JSON result node for --all-open-issues, aggregating across all artifacts.
* Uses the same output shape as buildResultNode for consistent table columns.
* @param appVersionId The application version ID processed.
* @param artifactsProcessed Number of artifacts successfully processed.
* @param artifactsSkipped Number of artifacts skipped (e.g. no remediations.xml).
* @param totalRemediation Total remediations across all artifacts.
* @param appliedRemediation Total applied remediations across all artifacts.
* @param skippedRemediation Total skipped remediations across all artifacts.
* @param action Final action result.
* @return An ObjectNode representing the aggregated result.
*/
public static ObjectNode buildAggregatedResultNode(String appVersionId, int artifactsProcessed, int artifactsSkipped,
int totalRemediation, int appliedRemediation, int skippedRemediation, String action) {
ObjectNode result = JsonHelper.getObjectMapper().createObjectNode();
result.put("appVersionId", appVersionId);
result.put("artifactId", "N/A");
result.put("artifactsProcessed", artifactsProcessed);
result.put("artifactsSkipped", artifactsSkipped);
result.put("totalRemediation", totalRemediation);
result.put("appliedRemediation", appliedRemediation);
result.put("skippedRemediation", skippedRemediation);
Expand Down
Loading