Skip to content

feat(aviator): Improve apply-remediations UX with --latest, --all-ope…#948

Merged
rsenden merged 5 commits intofortify:feat/v3.x/aviator/26.2from
mneeta:feature/improved-auto-remediation-ux
Mar 26, 2026
Merged

feat(aviator): Improve apply-remediations UX with --latest, --all-ope…#948
rsenden merged 5 commits intofortify:feat/v3.x/aviator/26.2from
mneeta:feature/improved-auto-remediation-ux

Conversation

@mneeta
Copy link
Copy Markdown
Member

@mneeta mneeta commented Mar 17, 2026

This enhancement provides a more flexible and user-friendly experience for applying Aviator auto-remediations for SSC by introducing multiple artifact selection modes:

  • --latest: Automatically select the most recent Aviator-processed artifact
  • --all-open-issues: Process all artifacts with open issues in bulk
  • --since: Filter artifacts by upload date (relative: 7d, 2w, 1M; absolute: 2025-01-01) Key Changes:
  • Replaced required --artifact-id with flexible selection modes
  • Added SinceOptionHelper for robust date/period parsing
  • Enhanced SSCArtifactHelper with getLatestAviatorArtifact() and getAllAviatorArtifacts()
  • Improved command validation with mutual exclusivity checks
  • Added comprehensive unit tests for all new options
  • Updated i18n messages with detailed usage descriptions Technical Details:
  • SinceOptionHelper supports relative periods (d, w, M, y) and absolute ISO-8601 dates
  • DateTimePeriodHelper integration for consistent period parsing across fcli
  • Proper UTC timezone handling for date comparisons
  • Backward compatible - existing --artifact-id usage unchanged

Enabled apply-remediations command for FoD as well

Neeta Meshram added 2 commits March 17, 2026 15:28
…n-issues, and --since options

This enhancement provides a more flexible and user-friendly experience for applying
Aviator auto-remediations by introducing multiple artifact selection modes:
- --latest: Automatically select the most recent Aviator-processed artifact
- --all-open-issues: Process all artifacts with open issues in bulk
- --since: Filter artifacts by upload date (relative: 7d, 2w, 1M; absolute: 2025-01-01)
Key Changes:
- Replaced required --artifact-id with flexible selection modes
- Added SinceOptionHelper for robust date/period parsing
- Enhanced SSCArtifactHelper with getLatestAviatorArtifact() and getAllAviatorArtifacts()
- Improved command validation with mutual exclusivity checks
- Added comprehensive unit tests for all new options
- Updated i18n messages with detailed usage descriptions
Technical Details:
- SinceOptionHelper supports relative periods (d, w, M, y) and absolute ISO-8601 dates
- DateTimePeriodHelper integration for consistent period parsing across fcli
- Proper UTC timezone handling for date comparisons
- Backward compatible - existing --artifact-id usage unchanged
Closes: #XXX
AviatorEntitlementCommands.class,
AviatorSSCCommands.class,
// AviatorFoDCommands.class,
AviatorFoDCommands.class,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's some discussion as to whether this should live under fcli aviator or fcli fod. Given that Aviator-related functionality is handled by FoD in the background, none of the other fcli aviator commands is relevant for FoD (like user/admin session management, app & entitlement managent, ...). So, unless we plan on supporting any of this functionality also for FoD, moving this to fcli fod might make more sense.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd lean towards keeping this under fcli aviator fod and disagree with moving it to fcli fod, for the following reasons:

  1. Feature ownership is Aviator, not FoD. The command's primary purpose is applying AI-generated code remediations to source files using Aviator's engine (ApplyAutoRemediationOnSource). FoD's only role is providing the FPR file download — it's the data source, not the product context. The same logic would apply to fcli aviator ssc apply-remediations (it also just downloads an FPR from SSC), and we wouldn't move that to fcli ssc.

  2. Symmetry and discoverability. Having fcli aviator ssc apply-remediations and fcli aviator fod apply-remediations as parallel commands gives users a consistent mental model — regardless of which product stores their scans, the Aviator remediation experience lives in the same place. A developer thinking "I want Aviator to fix my code" will look under fcli aviator, not fcli fod.

  3. It's true that most fcli aviator commands (user/admin session, app, entitlement management) are SSC-specific and not applicable to FoD. But apply-remediations is precisely the one Aviator feature that IS relevant to FoD users — it's the core end-user-facing capability. That distinction is a reason to grow fcli aviator fod further, not to move this command out.

Happy to discuss further if there's a strong reason for the alternative, but the current placement feels architecturally correct.

}


private void validateOptions() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use Picocli ArgGroups for handling exclusivity and related options, either as inner classes in this class, or as a separate single class (with sub-arggroups as inner classes) in the cli.mixin package.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done! Created a separate AviatorSSCApplyRemediationsArtifactSelectorMixin class in the cli.mixin package with ArtifactSelectionArgGroup as an inner class annotated with @Arggroup(exclusive = true, multiplicity = "1"), making --artifact-id, --latest, and --all-open-issues mutually exclusive via Picocli.
One constraint worth noting: Picocli does not support @mixin inside @Arggroup classes (InitializationException: @Mixins are not supported on @ArgGroups), and placing the same option name (e.g. --appversion) in multiple exclusive sub-groups raises a DuplicateOptionAnnotationsException. To work around this, --av/--appversion and --delim are declared at the top level of the mixin (outside the ArgGroup) with a validate() method enforcing that --av is required with --latest/--all-open-issues and forbidden with --artifact-id. All other exclusivity constraints are fully handled declaratively by Picocli.

AviatorEntitlementCommands.class,
AviatorSSCCommands.class,
// AviatorFoDCommands.class,
AviatorFoDCommands.class,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As discussed with Frans, since none of the other fcli aviator commands make sense for FoD (Aviator user/admin session, app & entitlement management), we'll want to move this to the FoD module, i.e., have fcli fod aviator apply-remediations. This likely requires adding a dependency from fcli-fod module on fcli-aviator-common module, and possibly some refactoring (moving code from fcli-aviator to fcli-aviator-common if FoD Aviator commands depend on any code in fcli-aviator module.

}


private void validateOptions() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use Picocli ArgGroups to model exclusivity and related options, to have Picocli take care of validation and provide consistent error messaging across fcli. The ArgGroups could be either inner classes in this command class, or potentially a single top-level class (with inner classes for sub-arggroups) in the cli.mixin package.

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()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's already a progress writer instantiated in getJsonNode method; nested progress writers are not supported and could cause issues. Also, to avoid having to pass around unirest, logger, and progressWriter all the time, probably better to have an inner class that takes these through Lombok RequiredArgsConstructor and performs all of the processing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done! Both issues have been addressed:
Nested progress writer removed — downloadArtifactFpr() no longer calls progressWriterFactoryMixin.create(); it now uses the single progressWriter instance created in getJsonNode().
Inner class added — Introduced a private ArtifactProcessor inner class annotated with Lombok's @requiredargsconstructor, which holds unirest, logger, and progressWriter as final fields. All processing methods (processAllAviatorArtifacts, processFprRemediations, downloadArtifactFpr) are moved into this class, and the outer class methods access the inner class fields directly — no more parameter passing.


JsonNode data = response.get("data");
if (data == null || !data.isArray() || data.size() == 0) {
throw new FcliSimpleException(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this throw an exception, or simply return null?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Throwing is the right choice here. The method returns a non-nullable SSCArtifactDescriptor, and the caller (processFprRemediations) uses it directly — returning null would just push the failure further down as an NPE with no context.
This check also catches a distinct failure case: no artifacts exist at all for the app version (e.g. invalid app version ID or nothing ever uploaded), which is different from the check further down that throws when artifacts exist but none are Aviator-processed. Throwing here gives the user an immediate, actionable error message for each scenario.

for (JsonNode artifact : data) {
String originalFileName = artifact.path("originalFileName").asText("");
if (!originalFileName.startsWith("aviator_")) { continue; }
if (sinceDate != null && !isUploadDateOnOrAfter(artifact, sinceDate)) { continue; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this logic correct? If upload date for current non-Aviator artifact is older than sinceDate, then all subsequent artifacts will also be older than sinceDate, so once we reach this point, we'll never find an Aviator artifact that's newer than sinceDate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Great catch, you're absolutely right! Since artifacts are fetched in DESC order (newest first), once we encounter an artifact with uploadDate < sinceDate, all subsequent artifacts will also be older — so continue was wrong and was causing unnecessary iterations. Fixed by:
Checking the date first (before the filename check) — if it's too old, no point checking the filename
Changing continue → break to stop the loop immediately
Also took the opportunity to switch getAllAviatorArtifacts() from ASC to DESC ordering for the same reason — with ASC, a recent --since filter would wastefully fetch all older pages before reaching matching artifacts. With DESC, we hit the cutoff early, break, then reverse the result list before returning to maintain the ascending order contract.

return getDescriptor(artifact);
}

throw new FcliSimpleException(buildNoArtifactsMessage(appVersionId, sinceDate));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as above; null or exception?


while (true) {
JsonNode response = unirest.get(SSCUrls.PROJECT_VERSION_ARTIFACTS(appVersionId))
.queryString("orderby", "uploadDate ASC")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same comment about orderBy as above, but given that you're trying to order by ascending date, this might have more impact. Also, if a customer has years of uploads, searching from the oldest date might not be very efficient.

* @return SSCArtifactDescriptor of the most recent qualifying Aviator artifact
* @throws FcliSimpleException if no matching Aviator artifacts found
*/
public static final SSCArtifactDescriptor getLatestAviatorArtifact(UnirestInstance unirest, String appVersionId, OffsetDateTime sinceDate) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any way to improve this code/have less code duplication between the various Aviator-related methods?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done! Extracted the shared logic into three private helpers:
fetchAviatorArtifacts(unirest, appVersionId, sinceDate, maxResults) — single unified method handling DESC fetch, Aviator filename filtering, date-based early termination, and pagination; maxResults = 1 gives getLatestAviatorArtifact() behaviour, Integer.MAX_VALUE gives getAllAviatorArtifacts() behaviour
isAviatorArtifact(artifact) — extracts the aviator_ filename check
shouldStopProcessing(artifact, sinceDate) — extracts the date cutoff early-termination check
Both public methods are now thin wrappers — getLatestAviatorArtifact() calls fetchAviatorArtifacts(..., 1) and returns the first result, while getAllAviatorArtifacts() calls fetchAviatorArtifacts(..., MAX_VALUE) and reverses before returning to maintain the ascending order contract.

Neeta Meshram added 3 commits March 24, 2026 10:18
- Use Picocli ArgGroups for mutually exclusive --artifact-id, --latest,
  --all-open-issues options in new AviatorSSCApplyRemediationsArtifactSelectorMixin;
  note @mixin is not supported inside @ArgGroups, so --av/--delim options
  are inlined with manual validation in validate()
- Refactor AviatorSSCApplyRemediationsCommand to use inner ArtifactProcessor
  class with @requiredargsconstructor, eliminating repeated unirest/logger/
  progressWriter parameters and removing nested progress writer in
  downloadArtifactFpr()
- Fix isSingular() to always return true; all three modes (--artifact-id,
  --latest, --all-open-issues) return exactly one aggregated ObjectNode,
  so output structure must be consistent regardless of option chosen
- Refactor SSCArtifactHelper to reduce code duplication between
  getLatestAviatorArtifact() and getAllAviatorArtifacts() by extracting
  shared logic into fetchAviatorArtifacts(), isAviatorArtifact(), and
  shouldStopProcessing() helpers
- Fix loop early-termination bug in getLatestAviatorArtifact(): change
  continue to break when uploadDate < sinceDate, since artifacts are
  ordered DESC and all subsequent artifacts will also be too old
- Switch getAllAviatorArtifacts() from ASC to DESC ordering to enable
  early termination when --since is used; reverse result list before
  returning to maintain ascending order contract
- Add SLF4J logger to SSCArtifactHelper and log a warning with artifact
  id when uploadDate cannot be parsed, instead of silently returning false
- Add explanatory comment on the no-data check in getLatestAviatorArtifact()
  clarifying why an exception is thrown rather than returning null
- Remove AviatorSSCApplyRemediationsCommandTest as validation is now
  handled declaratively by Picocli ArgGroups
…nd add usage examples

- Move apply-remediations command from fcli-aviator to fcli-fod module
  (FoDAviatorApplyRemediationsCommand, FoDAviatorCommands, AviatorFoDApplyRemediationsHelper)
- Remove FoD aviator subcommand from AviatorCommands; add aviator subcommand to FoDCommands
- Add fcli-aviator dependency to fcli-fod build.gradle.kts
- Update FoDMessages.properties with i18n keys for the moved command
- Add concrete usage examples to apply-remediations command description
  covering --artifact-id, --latest, --all-open-issues, --since, and --source-dir combinations
- Remove CHANGELOG.md (not applicable for this branch)
@rsenden rsenden merged commit e29132f into fortify:feat/v3.x/aviator/26.2 Mar 26, 2026
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.

2 participants