diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sourceanalyzer-translate-scan.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sourceanalyzer-translate-scan.yaml new file mode 100644 index 0000000000..daef1af825 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sourceanalyzer-translate-scan.yaml @@ -0,0 +1,119 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev-2.x.json + +author: Fortify +usage: + header: Local Source Analyzer translate/scan with optional SSC upload + description: | + This action automates a local Fortify Source Analyzer workflow: + 1) Optionally update local Source Analyzer rulepacks using the registered Source Analyzer installation + 2) Translate source code using the specified build ID + 3) Scan the translated project and generate an FPR + 4) Optionally upload the generated FPR to SSC for the given application version + 5) Optionally wait until the uploaded SSC artifact processing is complete (unless --skip-wait is used) + +config: + output: immediate + rest.target.default: ssc + run.fcli.status.log.default: true # By default, we log all exit statuses + run.fcli.status.check.default: true + +cli.options: + buildId: + names: --build-id, -b + description: | + Fortify Source Analyzer build ID to use for this translation and scan. + required: true + sourceDir: + names: --source-dir, -d + description: | + Source directory to translate/scan. + required: true + default: . + fpr: + names: --fpr, -o + description: | + Output FPR file path. + required: true + default: audit.fpr + sourceAnalyzerVersion: + names: --source-analyzer-version, -v + description: | + Optional Source Analyzer version to run. If omitted, the default + registered Source Analyzer installation is used. + required: false + appVersion: + names: --app-version, --av + description: | + Optional SSC application version (app-name:version-name) to upload + the generated FPR to. When specified, the action will attempt to + ensure the application version exists in SSC and upload the FPR + using the current SSC session. + required: false + updateRulePacks: + names: --update-rule-packs + description: | + When set, run 'fcli tool sourceanalyzer update-rule-packs' before + translation and scan to update local Source Analyzer rulepacks. + required: false + default: false + type: boolean + skipWait: + names: --skip-wait + description: >- + By default, the action will wait for the uploaded SSC artifact processing + to complete. Use this option to skip waiting for processing to complete. + required: false + type: boolean + translateExtraOpts: + names: --translate-extra-opts + description: | + Extra options to pass only to the translation (sourceanalyzer) step. + required: false + default: ${#extraOpts('SOURCEANALYZER_TRANSLATE_EXTRA_OPTS')} + scanExtraOpts: + names: --scan-extra-opts + description: | + Extra options to pass only to the scan (sourceanalyzer) step. + required: false + default: ${#extraOpts('SOURCEANALYZER_SCAN_EXTRA_OPTS')} + +steps: + - var.set: + scan.buildId: ${cli.buildId} + scan.fpr: ${#resolveAgainstCurrentWorkDir(cli.fpr)} + scan.sourceDir: ${#resolveAgainstCurrentWorkDir(cli.sourceDir)} + global.sourceanalyzerPublish.fcliVarName: sourceanalyzer_scan_${#action.runID().replace('-','_')} + global.sourceanalyzerPublish.waitForCmd: 'fcli ssc artifact wait-for ::${global.sourceanalyzerPublish.fcliVarName}::' + + # Optionally update rulepacks before translate and scan + - if: ${cli.updateRulePacks==true} + run.fcli: + UPDATE_RULEPACKS: + cmd: > + fcli tool sourceanalyzer update-rule-packs ${#opt("--version", cli.sourceAnalyzerVersion)} + + # 1) Translate + - run.fcli: + TRANSLATE: + cmd: > + fcli tool sourceanalyzer run ${#opt("--version", cli.sourceAnalyzerVersion)} -- -b "${scan.buildId}" ${cli.extraOpts} ${cli.translateExtraOpts} "${scan.sourceDir}" + + # 2) Scan + - run.fcli: + SCAN: + cmd: > + fcli tool sourceanalyzer run ${#opt("--version", cli.sourceAnalyzerVersion)} -- -b "${scan.buildId}" -scan -f "${scan.fpr}" ${cli.extraOpts} ${cli.scanExtraOpts} + + # 3) In case app version is mentioned, ensure it exists and upload the FPR to SSC + - if: ${!#isBlank(cli.appVersion)} + do: + - run.fcli: + SSC_SETUP_APPVERSION: + cmd: ${#actionCmd('SETUP', 'ssc', 'setup-appversion')} "--av=${cli.appVersion}" + - run.fcli: + SSC_UPLOAD_FPR: + cmd: > + fcli ssc artifact upload --av="${cli.appVersion}" -f "${scan.fpr}" --store ${global.sourceanalyzerPublish.fcliVarName} + - if: ${!cli.skipWait} + run.fcli: + WAIT: ${global.sourceanalyzerPublish.waitForCmd} diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolGetCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolGetCommand.java index 5962e9fd9a..20e0ebf450 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolGetCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolGetCommand.java @@ -19,13 +19,14 @@ import com.fortify.cli.tool._common.helper.Tool; import com.fortify.cli.tool._common.helper.ToolInstallationDescriptor; import com.fortify.cli.tool._common.helper.ToolInstallationOutputDescriptor; +import com.fortify.cli.tool.definitions.helper.ToolDefinitionVersionDescriptor; import com.fortify.cli.tool.definitions.helper.ToolDefinitionsHelper; import picocli.CommandLine.Parameters; /** - * Abstract base class for tool 'get' commands that retrieve information about - * a specific tool version. Similar to AbstractToolListCommand but returns a + * Abstract base class for tool 'get' commands that retrieve information about + * a specific tool version. Similar to AbstractToolListCommand but returns a * single record instead of a list. * * Subclasses must implement: @@ -34,50 +35,78 @@ * @author Ruud Senden */ public abstract class AbstractToolGetCommand extends AbstractOutputCommand implements IJsonNodeSupplier { - + @Parameters(index = "0", descriptionKey = "fcli.tool.get.version") private String requestedVersion; - + @Override public final JsonNode getJsonNode() { - var toolName = getTool().getToolName(); - var toolDefinition = ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName); - + var tool = getTool(); + var toolName = tool.getToolName(); + var optDefinition = ToolDefinitionsHelper.tryGetToolDefinitionRootDescriptor(toolName); + + //TODO - Check the need of definitions for other tools as well, and if this is the best way to handle this (e.g. should we check for the presence of definitions in the list command instead?) + if (tool == Tool.SOURCE_ANALYZER && optDefinition.isEmpty()) { + return getJsonNodeWithoutDefinitions(toolName); + } + + var toolDefinition = optDefinition.orElseGet( + () -> ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName)); + // Resolve version (handles aliases like 'latest') var versionDescriptor = toolDefinition.getVersion(requestedVersion); - + // Load installation descriptor if tool is installed var installationDescriptor = ToolInstallationDescriptor.load(toolName, versionDescriptor); - + // Check if this is the default (last installed) version var lastInstalledDescriptor = ToolInstallationDescriptor.loadLastModified(toolName); boolean isDefault = isDefaultVersion(installationDescriptor, lastInstalledDescriptor); - + // Create output descriptor var outputDescriptor = new ToolInstallationOutputDescriptor( - toolName, - versionDescriptor, - installationDescriptor, - "", - isDefault - ); - + toolName, + versionDescriptor, + installationDescriptor, + "", + isDefault); + + return JsonHelper.getObjectMapper().valueToTree(outputDescriptor); + } + + private JsonNode getJsonNodeWithoutDefinitions(String toolName) { + ToolDefinitionVersionDescriptor versionDescriptor = new ToolDefinitionVersionDescriptor(); + versionDescriptor.setVersion(requestedVersion); + versionDescriptor.setStable(true); + versionDescriptor.setAliases(new String[0]); + + var installationDescriptor = ToolInstallationDescriptor.load(toolName, versionDescriptor); + var lastInstalledDescriptor = ToolInstallationDescriptor.loadLastModified(toolName); + boolean isDefault = isDefaultVersion(installationDescriptor, lastInstalledDescriptor); + + var outputDescriptor = new ToolInstallationOutputDescriptor( + toolName, + versionDescriptor, + installationDescriptor, + "", + isDefault); return JsonHelper.getObjectMapper().valueToTree(outputDescriptor); } - + @Override public final boolean isSingular() { return true; } - - private boolean isDefaultVersion(ToolInstallationDescriptor installationDescriptor, ToolInstallationDescriptor lastInstalledDescriptor) { + + private boolean isDefaultVersion(ToolInstallationDescriptor installationDescriptor, + ToolInstallationDescriptor lastInstalledDescriptor) { if (installationDescriptor == null || lastInstalledDescriptor == null) { return false; } - return installationDescriptor.getInstallDir() != null + return installationDescriptor.getInstallDir() != null && installationDescriptor.getInstallDir().equals(lastInstalledDescriptor.getInstallDir()); } - + /** * @return Tool enum entry for this tool */ diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolRunCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolRunCommand.java index 3734af84cf..8a4b65fff3 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolRunCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolRunCommand.java @@ -31,6 +31,8 @@ import com.fortify.cli.tool._common.helper.Tool; import com.fortify.cli.tool._common.helper.ToolInstallationDescriptor; import com.fortify.cli.tool._common.helper.ToolInstallationsResolver; +import com.fortify.cli.tool.definitions.helper.ToolDefinitionVersionDescriptor; +import com.fortify.cli.tool.definitions.helper.ToolDefinitionsHelper; import lombok.Getter; import lombok.SneakyThrows; @@ -59,7 +61,7 @@ public abstract class AbstractToolRunCommand extends AbstractRunnableCommand { private String workDir = System.getProperty("user.dir"); @Parameters(descriptionKey="fcli.tool.run.tool-args") @Getter private List toolArgs; - + @Override public final Integer call() throws Exception { validateWorkingDirectory(); @@ -71,13 +73,13 @@ public final Integer call() throws Exception { return call(baseCommands.get(0)); } catch ( Exception e ) { if ( baseCommands.size()==1) { throw e; } // No more base commands - LOG.debug("Command execution failed ({}): {}; trying fallback command", + LOG.debug("Command execution failed ({}): {}; trying fallback command", e.getClass().getSimpleName(), e.getMessage()); baseCommands.remove(0); } } } - + private void validateWorkingDirectory() { File workDirFile = new File(workDir); if (!workDirFile.exists()) { @@ -91,7 +93,7 @@ private void validateWorkingDirectory() { )); } } - + private final Integer call(List baseCmd) throws Exception { if ( baseCmd==null ) { throw new FcliBugException("Base command to execute may not be null"); } var fullCmd = Stream.of(baseCmd, getToolArgs()) @@ -102,7 +104,7 @@ private final Integer call(List baseCmd) throws Exception { var pb = new ProcessBuilder() .command(fullCmd) .directory(new File(workDir)) - // .inheritIO(); + // .inheritIO(); // Can't use inheritIO as this as it may inherit original stdout/stderr, rather than // those created by OutputHelper.OutputType (for example through FcliCommandExecutor). // Instead, we use pipes and manually copy the output to current System.out/System.err. @@ -125,7 +127,7 @@ private final Integer call(List baseCmd) throws Exception { } return process.exitValue(); } - + private static void inheritIO(final InputStream src, final PrintStream dest) { new Thread(new Runnable() { @SneakyThrows @@ -136,7 +138,8 @@ public void run() { } private final ToolInstallationDescriptor getToolInstallationDescriptor() { - var installations = ToolInstallationsResolver.resolve(getTool()); + var tool = getTool(); + var installations = ToolInstallationsResolver.resolve(tool); var toolName = installations.tool().getToolName(); if (StringUtils.isBlank(versionToRun)) { return checkNotNull( @@ -145,6 +148,22 @@ private final ToolInstallationDescriptor getToolInstallationDescriptor() { .orElse(null), "No tool installations detected"); } + + // SCA: allow run without sca.yaml + if ( tool == Tool.SOURCE_ANALYZER + && ToolDefinitionsHelper.tryGetToolDefinitionRootDescriptor(toolName).isEmpty() ) { + var descriptor = installations.findByVersion(versionToRun) + .map(ToolInstallationsResolver.ToolInstallationRecord::installationDescriptor) + .orElseGet(() -> { + ToolDefinitionVersionDescriptor vd = new ToolDefinitionVersionDescriptor(); + vd.setVersion(versionToRun); + vd.setStable(true); + vd.setAliases(new String[0]); + return ToolInstallationDescriptor.load(toolName, vd); + }); + return checkNotNull(descriptor, "No tool installation detected for version " + versionToRun); + } + var versionDescriptor = installations.definition().getVersion(versionToRun); var descriptor = installations.findByVersion(versionDescriptor.getVersion()) .map(ToolInstallationsResolver.ToolInstallationRecord::installationDescriptor) @@ -158,7 +177,7 @@ private ToolInstallationDescriptor checkNotNull(ToolInstallationDescriptor descr } return descriptor; } - + protected abstract Tool getTool(); protected List> getBaseCommands(ToolInstallationDescriptor descriptor) { return List.of(getBaseCommand(descriptor)); @@ -167,5 +186,5 @@ protected List getBaseCommand(ToolInstallationDescriptor descriptor) { return null; } protected void updateProcessBuilder(ProcessBuilder pb) {}; - + } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/Tool.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/Tool.java index f45ddf75ed..c20469c4ca 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/Tool.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/Tool.java @@ -29,7 +29,8 @@ public enum Tool { FOD_UPLOADER(new ToolHelperFoDUploader(), "fod-uploader"), BUGTRACKER_UTILITY(new ToolHelperBugTrackerUtility(), "bugtracker-utility", "fbtu"), VULN_EXPORTER(new ToolHelperVulnExporter(), "vuln-exporter", "fve"), - DEBRICKED_CLI(new ToolHelperDebrickedCli(), "debricked-cli", "dcli"); + DEBRICKED_CLI(new ToolHelperDebrickedCli(), "debricked-cli", "dcli"), + SOURCE_ANALYZER(new ToolHelperSourceAnalyzer(), "sourceanalyzer"); private static final Map TOOL_NAME_MAP = new HashMap<>(); private static final Map TOOL_ALIAS_MAP = new HashMap<>(); @@ -231,4 +232,26 @@ public String getDefaultEnvPrefix() { return "DEBRICKED"; } } + + /** + * Helper implementation for sourceanalyzer tool. + */ + private static final class ToolHelperSourceAnalyzer implements IToolHelper { + private static final String TOOL_NAME = "sourceanalyzer"; + + @Override + public String getToolName() { + return TOOL_NAME; + } + + @Override + public String getDefaultBinaryName() { + return PlatformHelper.isWindows() ? "sourceanalyzer.exe" : "sourceanalyzer"; + } + + @Override + public String getDefaultEnvPrefix() { + return "SOURCEANALYZER"; + } + } } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationsResolver.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationsResolver.java index 39cf949bd1..2143c3d1be 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationsResolver.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationsResolver.java @@ -32,17 +32,44 @@ * the new env command hierarchy. */ public final class ToolInstallationsResolver { - private ToolInstallationsResolver() {} + private ToolInstallationsResolver() { + } public static ToolInstallations resolve(Tool tool) { var toolName = tool.getToolName(); - var definition = ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName); - var lastInstalled = ToolInstallationDescriptor.loadLastModified(toolName); - var definedRecords = definition.getVersionsStream() - .map(vd -> createRecord(toolName, vd, lastInstalled, true)); - var unknownRecords = getUnknownRecords(toolName, definition, lastInstalled); - var records = Stream.concat(definedRecords, unknownRecords) - .toList(); + + // Non-SCA tools keep strict behavior + if (tool != Tool.SOURCE_ANALYZER) { + var definition = ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName); + var lastInstalled = ToolInstallationDescriptor.loadLastModified(toolName); + var definedRecords = definition.getVersionsStream() + .map(vd -> createRecord(toolName, vd, lastInstalled, true)); + var unknownRecords = getUnknownRecords(toolName, definition, lastInstalled); + var records = Stream.concat(definedRecords, unknownRecords).toList(); + return new ToolInstallations(tool, definition, lastInstalled, records); + } + + // SCA: definitions optional + var optDef = ToolDefinitionsHelper.tryGetToolDefinitionRootDescriptor(toolName); + ToolDefinitionRootDescriptor definition; + ToolInstallationDescriptor lastInstalled = ToolInstallationDescriptor.loadLastModified(toolName); + Stream definedRecords; + Stream unknownRecords; + + if (optDef.isPresent()) { + definition = optDef.get(); + definedRecords = definition.getVersionsStream() + .map(vd -> createRecord(toolName, vd, lastInstalled, true)); + unknownRecords = getUnknownRecords(toolName, definition, lastInstalled); + } else { + // No sca.yaml: only installed versions, all treated as "known" + definition = buildSyntheticDefinitionFromInstallations(toolName); + definedRecords = definition.getVersionsStream() + .map(vd -> createRecord(toolName, vd, lastInstalled, true)); + unknownRecords = Stream.empty(); + } + + var records = Stream.concat(definedRecords, unknownRecords).toList(); return new ToolInstallations(tool, definition, lastInstalled, records); } @@ -71,7 +98,8 @@ private static boolean isUnknownVersion(String versionFileName, String toolName, if (versionFileName.equals(toolName)) { return false; } - // Special handling for "unknown" version - don't try to look it up in definitions + // Special handling for "unknown" version - don't try to look it up in + // definitions if ("unknown".equals(versionFileName)) { return true; } @@ -123,12 +151,15 @@ public static record ToolInstallations( public Stream stream() { return records.stream(); } + public Stream installedStream() { return records.stream().filter(ToolInstallationRecord::isInstalled); } + public Optional defaultInstallation() { return installedStream().filter(ToolInstallationRecord::isDefault).findFirst(); } + public Optional findByVersion(String version) { return records.stream() .filter(record -> record.versionDescriptor().getVersion().equals(version)) @@ -145,4 +176,28 @@ public boolean isInstalled() { return installationDescriptor != null && StringUtils.isNotBlank(installationDescriptor.getInstallDir()); } } + + private static ToolDefinitionRootDescriptor buildSyntheticDefinitionFromInstallations(String toolName) { + Path stateDir = ToolInstallationHelper.getToolsStatePath().resolve(toolName); + ToolDefinitionRootDescriptor def = new ToolDefinitionRootDescriptor(); + File[] versionFiles = Files.isDirectory(stateDir) + ? stateDir.toFile().listFiles(File::isFile) + : null; + if (versionFiles == null || versionFiles.length == 0) { + def.setVersions(new ToolDefinitionVersionDescriptor[0]); + return def; + } + ToolDefinitionVersionDescriptor[] versions = Stream.of(versionFiles) + .map(File::getName) + .map(vName -> { + ToolDefinitionVersionDescriptor vd = new ToolDefinitionVersionDescriptor(); + vd.setVersion(vName); + vd.setStable(true); + vd.setAliases(new String[0]); + return vd; + }) + .toArray(ToolDefinitionVersionDescriptor[]::new); + def.setVersions(versions); + return def; + } } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolRegistrationHelper.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolRegistrationHelper.java index d6f1956f6c..8c1cdc0ea2 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolRegistrationHelper.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolRegistrationHelper.java @@ -20,6 +20,7 @@ import com.fortify.cli.common.exception.FcliSimpleException; import com.fortify.cli.common.util.FcliDataHelper; +import com.fortify.cli.tool.definitions.helper.ToolDefinitionRootDescriptor; import com.fortify.cli.tool.definitions.helper.ToolDefinitionVersionDescriptor; import com.fortify.cli.tool.definitions.helper.ToolDefinitionsHelper; @@ -33,7 +34,7 @@ * @author Ruud Senden */ public class ToolRegistrationHelper { - + /** * Find all potential tool binary candidates from fcli installed versions and provided paths. * Used when version filtering is needed - returns all candidates for version matching. @@ -201,11 +202,14 @@ public static class RegistrationContext { private final String defaultBinaryName; private final VersionDetector versionDetector; private RegistrationAction action; + private final boolean definitionsOptional; public RegistrationContext(String toolName, String defaultBinaryName, VersionDetector versionDetector) { this.toolName = toolName; this.defaultBinaryName = defaultBinaryName; this.versionDetector = versionDetector; + Tool tool = Tool.getByToolName(toolName); + this.definitionsOptional = (tool == Tool.SOURCE_ANALYZER); } /** @@ -276,7 +280,15 @@ private File findToolBinaryInSinglePath(String path) { } private File findExistingInstallation(String requestedVersion) { - var toolDefinition = ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName); + ToolDefinitionRootDescriptor toolDefinition; + if (!definitionsOptional) { + toolDefinition = getRequiredDefinition(); + } else { + toolDefinition = getOptionalDefinitionOrNull(); + if (toolDefinition == null) { + return null; + } + } // If specific version requested, look for matching installation if (!"any".equals(requestedVersion)) { @@ -319,25 +331,28 @@ private File findExistingInstallation(String requestedVersion) { } private File findToolBinaryInMultiplePaths(String[] paths, String requestedVersion) { - if (!"any".equals(requestedVersion)) { + boolean hasRequestedVersion = !"any".equals(requestedVersion); + boolean hasDefinitions = !definitionsOptional || getOptionalDefinitionOrNull() != null; + + if (hasRequestedVersion && hasDefinitions) { var candidates = findAllToolBinariesInPaths(toolName, defaultBinaryName, paths); - + File toolBinary = findMatchingCandidate(candidates, requestedVersion); if (toolBinary == null) { throw new FcliSimpleException( - String.format("%s version matching %s not found in specified paths", - toolName, requestedVersion)); + String.format("%s version matching %s not found in specified paths", + toolName, requestedVersion)); } action = RegistrationAction.REGISTERED; return toolBinary; } else { File toolBinary = findToolBinaryInPaths(toolName, defaultBinaryName, paths); - + if (toolBinary == null) { throw new FcliSimpleException( - toolName + " not found in specified paths"); + toolName + " not found in specified paths"); } - + validateBinaryExecutable(toolBinary); action = RegistrationAction.REGISTERED; return toolBinary; @@ -371,18 +386,45 @@ private String detectVersionFromBinary(File toolBinary, File installDir) { } private void validateVersionMatch(ToolDefinitionVersionDescriptor versionDescriptor, String requestedVersion) { - var toolDefinition = ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName); + if (!definitionsOptional) { + var toolDefinition = getRequiredDefinition(); + var optionalRequestedVersion = toolDefinition.getOptionalVersionOrDefault(requestedVersion); + if (optionalRequestedVersion.isEmpty()) { + throw new FcliSimpleException( + String.format("Requested version %s not found in tool definitions. Detected version is %s", + requestedVersion, versionDescriptor.getVersion())); + } + var requestedVersionDescriptor = optionalRequestedVersion.get(); + if (!versionDescriptor.getVersion().equals(requestedVersionDescriptor.getVersion())) { + throw new FcliSimpleException( + String.format("Detected %s version %s does not match requested version %s (resolves to %s)", + toolName, versionDescriptor.getVersion(), requestedVersion, + requestedVersionDescriptor.getVersion())); + } + return; + } + + // SCA with optional definitions: try strict check if definitions exist, else + // skip + var toolDefinition = getOptionalDefinitionOrNull(); + if (toolDefinition == null) { + // No definitions → accept any detected version for requestedVersion + return; + } var optionalRequestedVersion = toolDefinition.getOptionalVersionOrDefault(requestedVersion); if (optionalRequestedVersion.isEmpty()) { + // Without definitions, we already returned; if we got here, behave like strict + // mode throw new FcliSimpleException( - String.format("Requested version %s not found in tool definitions. Detected version is %s", - requestedVersion, versionDescriptor.getVersion())); + String.format("Requested version %s not found in tool definitions. Detected version is %s", + requestedVersion, versionDescriptor.getVersion())); } var requestedVersionDescriptor = optionalRequestedVersion.get(); if (!versionDescriptor.getVersion().equals(requestedVersionDescriptor.getVersion())) { throw new FcliSimpleException( - String.format("Detected %s version %s does not match requested version %s (resolves to %s)", - toolName, versionDescriptor.getVersion(), requestedVersion, requestedVersionDescriptor.getVersion())); + String.format("Detected %s version %s does not match requested version %s (resolves to %s)", + toolName, versionDescriptor.getVersion(), requestedVersion, + requestedVersionDescriptor.getVersion())); } } @@ -435,9 +477,28 @@ private File findMatchingCandidate(List candidates, String requestedVersio } private ToolDefinitionVersionDescriptor resolveVersionDescriptor(String detectedVersion) { - var toolDefinition = ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName); - - // If version is unknown, create synthetic descriptor immediately without normalization + // Decide how to obtain the definition + ToolDefinitionRootDescriptor toolDefinition; + if (!definitionsOptional) { + toolDefinition = getRequiredDefinition(); // strict: throws if missing + } else { + toolDefinition = getOptionalDefinitionOrNull(); // may be null for SCA + } + + // If we are in definitions-optional mode and no definitions exist: + if (definitionsOptional && toolDefinition == null) { + ToolDefinitionVersionDescriptor syntheticDescriptor = new ToolDefinitionVersionDescriptor(); + syntheticDescriptor.setVersion(detectedVersion); + syntheticDescriptor.setStable(true); + syntheticDescriptor.setAliases(new String[0]); + return syntheticDescriptor; + } + + // From here on, toolDefinition is non-null (either strict mode or + // optional+present) + + // If version is unknown, create synthetic descriptor immediately without + // normalization if ("unknown".equals(detectedVersion)) { ToolDefinitionVersionDescriptor syntheticDescriptor = new ToolDefinitionVersionDescriptor(); syntheticDescriptor.setVersion("unknown"); @@ -445,20 +506,30 @@ private ToolDefinitionVersionDescriptor resolveVersionDescriptor(String detected syntheticDescriptor.setAliases(new String[0]); return syntheticDescriptor; } - - // Normalize version format to match tool definitions (e.g., 24.2.0.0050 -> 24.2.0) + + // Normalize version format to match tool definitions (e.g., 24.2.0.0050 -> + // 24.2.0) String normalizedVersion = toolDefinition.normalizeVersionFormat(detectedVersion); - + // Try to find matching version in tool definitions using normalized version return toolDefinition.getOptionalVersion(normalizedVersion) - .orElseGet(() -> { - // Version not found in definitions, create synthetic descriptor with normalized version - ToolDefinitionVersionDescriptor syntheticDescriptor = new ToolDefinitionVersionDescriptor(); - syntheticDescriptor.setVersion(normalizedVersion); - syntheticDescriptor.setStable(true); - syntheticDescriptor.setAliases(new String[0]); - return syntheticDescriptor; - }); + .orElseGet(() -> { + // Version not found in definitions, create synthetic descriptor with normalized + // version + ToolDefinitionVersionDescriptor syntheticDescriptor = new ToolDefinitionVersionDescriptor(); + syntheticDescriptor.setVersion(normalizedVersion); + syntheticDescriptor.setStable(true); + syntheticDescriptor.setAliases(new String[0]); + return syntheticDescriptor; + }); + } + + private ToolDefinitionRootDescriptor getRequiredDefinition() { + return ToolDefinitionsHelper.getToolDefinitionRootDescriptor(toolName); + } + + private ToolDefinitionRootDescriptor getOptionalDefinitionOrNull() { + return ToolDefinitionsHelper.tryGetToolDefinitionRootDescriptor(toolName).orElse(null); } } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_main/cli/cmd/ToolCommands.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_main/cli/cmd/ToolCommands.java index ed99ffd473..adfd4ee948 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_main/cli/cmd/ToolCommands.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_main/cli/cmd/ToolCommands.java @@ -20,6 +20,7 @@ import com.fortify.cli.tool.fcli.cli.cmd.ToolFcliCommands; import com.fortify.cli.tool.fod_uploader.cli.cmd.ToolFoDUploaderCommands; import com.fortify.cli.tool.sc_client.cli.cmd.ToolSCClientCommands; +import com.fortify.cli.tool.sourceanalyzer.cli.cmd.ToolSourceAnalyzerCommands; import com.fortify.cli.tool.vuln_exporter.cli.cmd.ToolVulnExporterCommands; import picocli.CommandLine.Command; @@ -34,6 +35,7 @@ ToolFcliCommands.class, ToolFoDUploaderCommands.class, ToolSCClientCommands.class, + ToolSourceAnalyzerCommands.class, ToolVulnExporterCommands.class, ToolDefinitionsCommands.class } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsHelper.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsHelper.java index 51b030a401..8c99ab07c2 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsHelper.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsHelper.java @@ -25,6 +25,7 @@ import java.util.Enumeration; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -60,6 +61,7 @@ public final class ToolDefinitionsHelper { /** * List current tool definitions. + * * @return List of tool definitions output descriptors */ public static final List listToolDefinitions() { @@ -68,16 +70,21 @@ public static final List listToolDefinitions() addYamlOutputDescriptors(result); return result; } - + /** - * Update tool definitions from the specified source if needed based on forceUpdate and maxAge. - * @param source Tool definitions source zip URL or file path; if null or blank, default URL is used + * Update tool definitions from the specified source if needed based on + * forceUpdate and maxAge. + * + * @param source Tool definitions source zip URL or file path; if null or + * blank, default URL is used * @param forceUpdate If true, always update regardless of age - * @param maxAge Optional max age string (e.g., "4h", "1d"); if null, default max age of 6 hours is used + * @param maxAge Optional max age string (e.g., "4h", "1d"); if null, + * default max age of 6 hours is used * @return */ @SneakyThrows - public static final List updateToolDefinitions(String source, boolean forceUpdate, String maxAge) { + public static final List updateToolDefinitions(String source, boolean forceUpdate, + String maxAge) { String normalizedSource = normalizeSource(source); boolean shouldUpdate = shouldUpdateToolDefinitions(forceUpdate, maxAge); if (shouldUpdate) { @@ -91,6 +98,7 @@ public static final List updateToolDefinitions( /** * Reset tool definitions to internal defaults by deleting state files. + * * @return List of tool definitions output descriptors after reset */ @SneakyThrows @@ -117,7 +125,8 @@ private static final void createDefinitionsStateDir(Path dir) throws IOException } private static final FileTime getModifiedTime(Path path) throws IOException { - if (!Files.exists(path)) return null; + if (!Files.exists(path)) + return null; return Files.getLastModifiedTime(path); } @@ -145,13 +154,16 @@ private static final ToolDefinitionsStateDescriptor update(String source, Path d } /** - * Validates that a local file is a valid ZIP file containing at least one expected tool definition YAML. + * Validates that a local file is a valid ZIP file containing at least one + * expected tool definition YAML. *

- * The merge logic will handle missing required files by falling back to state directory + * The merge logic will handle missing required files by falling back to state + * directory * or internal resources, and will ignore any unknown files in the ZIP. * * @param source the file path to validate - * @return true if the file exists, is a valid ZIP, and contains at least one required YAML file + * @return true if the file exists, is a valid ZIP, and contains at least one + * required YAML file * @throws FcliSimpleException if an I/O error occurs while reading the ZIP file */ private static boolean isValidZip(String source) { @@ -177,45 +189,52 @@ private static boolean isValidZip(String source) { return false; // No required files found } - /** - * Merges tool definition YAML files from multiple sources into a single destination ZIP file. + * Merges tool definition YAML files from multiple sources into a single + * destination ZIP file. *

- * This method searches for required tool definition YAML files in the following priority order: + * This method searches for required tool definition YAML files in the following + * priority order: *

    *
  1. User-specified ZIP file (source parameter)
  2. *
  3. Existing state directory ZIP file
  4. *
  5. Internal resource ZIP file embedded in the fcli JAR
  6. *
- * For each required YAML file, the first location where it's found is used. This allows users - * to override specific tool definitions while falling back to previously downloaded or built-in + * For each required YAML file, the first location where it's found is used. + * This allows users + * to override specific tool definitions while falling back to previously + * downloaded or built-in * definitions for tools they haven't customized. * - * @param dest the destination ZIP file path where merged definitions will be written - * @param source the user-specified source ZIP file path, or null to use only state/internal sources - * @throws FcliSimpleException if the user-provided ZIP doesn't contain any required YAML files, - * or if I/O errors occur during processing + * @param dest the destination ZIP file path where merged definitions will be + * written + * @param source the user-specified source ZIP file path, or null to use only + * state/internal sources + * @throws FcliSimpleException if the user-provided ZIP doesn't contain any + * required YAML files, + * or if I/O errors occur during processing */ @SneakyThrows private static void mergeDefinitionsZip(Path dest, String source) { if (StringUtils.isNotBlank(source)) { validateUserZipContainsRequiredFiles(source); } - + createDefinitionsStateDir(DEFINITIONS_STATE_DIR); - - // If dest already exists and we're about to overwrite it, move it to temp location + + // If dest already exists and we're about to overwrite it, move it to temp + // location // so we can use it as a fallback source Path existingStateZip = null; if (Files.exists(dest) && dest.equals(DEFINITIONS_STATE_ZIP)) { existingStateZip = DEFINITIONS_STATE_DIR.resolve(".tool-definitions.yaml.zip.old"); Files.move(dest, existingStateZip, java.nio.file.StandardCopyOption.REPLACE_EXISTING); } - + try { createMergedZipFile(dest, source, existingStateZip); Files.setLastModifiedTime(dest, FileTime.fromMillis(System.currentTimeMillis())); - + // Clean up temp file if it exists if (existingStateZip != null && Files.exists(existingStateZip)) { Files.delete(existingStateZip); @@ -228,16 +247,16 @@ private static void mergeDefinitionsZip(Path dest, String source) { throw e; } } - + private static void validateUserZipContainsRequiredFiles(String source) throws IOException { Path sourcePath = Path.of(source); if (!Files.exists(sourcePath)) { throw new FcliSimpleException("ZIP file not found: " + sourcePath); } - + Set requiredYamlFiles = getRequiredYamlFileNames(); boolean foundAtLeastOne = false; - + try (ZipFile zipFile = new ZipFile(sourcePath.toFile())) { Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { @@ -253,13 +272,14 @@ private static void validateUserZipContainsRequiredFiles(String source) throws I } catch (IOException e) { throw new FcliSimpleException("Invalid or corrupted ZIP file: " + sourcePath, e); } - + if (!foundAtLeastOne) { - throw new FcliSimpleException("ZIP file does not contain any expected tool definition files. Expected files: " - + String.join(", ", requiredYamlFiles)); + throw new FcliSimpleException( + "ZIP file does not contain any expected tool definition files. Expected files: " + + String.join(", ", requiredYamlFiles)); } } - + private static void createMergedZipFile(Path dest, String source, Path existingStateZip) throws IOException { try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(dest))) { for (String yamlFileName : getRequiredYamlFileNames()) { @@ -267,31 +287,33 @@ private static void createMergedZipFile(Path dest, String source, Path existingS } } } - - private static void copyYamlFileFromFirstAvailableSource(String yamlFileName, String userSource, + + private static void copyYamlFileFromFirstAvailableSource(String yamlFileName, String userSource, Path existingStateZip, java.util.zip.ZipOutputStream zos) throws IOException { // Try user-provided source first if (StringUtils.isNotBlank(userSource) && copyYamlFromZipToZip(Path.of(userSource), yamlFileName, zos)) { return; } // Fall back to existing state ZIP (if provided) - if (existingStateZip != null && Files.exists(existingStateZip) + if (existingStateZip != null && Files.exists(existingStateZip) && copyYamlFromZipToZip(existingStateZip, yamlFileName, zos)) { return; } // Fall back to internal resource copyYamlFromResourceZipToZip(DEFINITIONS_INTERNAL_ZIP, yamlFileName, zos); } + /** * Copies a specific YAML file from a ZIP file to an output ZIP stream. * - * @param zipPath the source ZIP file path + * @param zipPath the source ZIP file path * @param yamlFileName the name of the YAML file to copy - * @param zos the destination ZIP output stream + * @param zos the destination ZIP output stream * @return true if the file was found and copied, false if not found * @throws IOException if an I/O error occurs during reading or writing */ - private static boolean copyYamlFromZipToZip(Path zipPath, String yamlFileName, java.util.zip.ZipOutputStream zos) throws IOException { + private static boolean copyYamlFromZipToZip(Path zipPath, String yamlFileName, java.util.zip.ZipOutputStream zos) + throws IOException { if (!Files.exists(zipPath)) { return false; } @@ -317,16 +339,19 @@ private static boolean copyYamlFromZipToZip(Path zipPath, String yamlFileName, j } /** - * Copies a specific YAML file from an internal resource ZIP to an output ZIP stream. + * Copies a specific YAML file from an internal resource ZIP to an output ZIP + * stream. * - * @param resourceZip the resource path of the internal ZIP file + * @param resourceZip the resource path of the internal ZIP file * @param yamlFileName the name of the YAML file to copy - * @param zos the destination ZIP output stream + * @param zos the destination ZIP output stream * @return true if the file was found and copied, false if not found * @throws IOException if an I/O error occurs during reading or writing */ - private static boolean copyYamlFromResourceZipToZip(String resourceZip, String yamlFileName, java.util.zip.ZipOutputStream zos) throws IOException { - try (InputStream is = FileUtils.getResourceInputStream(resourceZip); ZipInputStream zis = new ZipInputStream(is)) { + private static boolean copyYamlFromResourceZipToZip(String resourceZip, String yamlFileName, + java.util.zip.ZipOutputStream zos) throws IOException { + try (InputStream is = FileUtils.getResourceInputStream(resourceZip); + ZipInputStream zis = new ZipInputStream(is)) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (!entry.isDirectory() && Path.of(entry.getName()).getFileName().toString().equals(yamlFileName)) { @@ -339,8 +364,8 @@ private static boolean copyYamlFromResourceZipToZip(String resourceZip, String y zos.closeEntry(); return true; } - } } + } return false; } @@ -359,6 +384,24 @@ public static final ToolDefinitionRootDescriptor getToolDefinitionRootDescriptor } } + public static final Optional tryGetToolDefinitionRootDescriptor(String toolName) { + String yamlFileName = toolName + ".yaml"; + try (InputStream is = getToolDefinitionsInputStream(); ZipInputStream zis = new ZipInputStream(is)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (yamlFileName.equals(entry.getName())) { + ToolDefinitionRootDescriptor descriptor = yamlObjectMapper.readValue(zis, + ToolDefinitionRootDescriptor.class); + return Optional.of(descriptor); + } + } + // No matching YAML entry → no definitions for this tool + return Optional.empty(); + } catch (IOException e) { + throw new FcliSimpleException("Error loading tool definitions", e); + } + } + private static final InputStream getToolDefinitionsInputStream() throws IOException { return Files.exists(DEFINITIONS_STATE_ZIP) ? Files.newInputStream(DEFINITIONS_STATE_ZIP) : FileUtils.getResourceInputStream(DEFINITIONS_INTERNAL_ZIP); @@ -368,18 +411,19 @@ private static final void addZipOutputDescriptor(List result, boolean shouldUpdate) { + private static final void addZipOutputDescriptor(List result, + boolean shouldUpdate) { var stateDescriptor = FcliDataHelper.readFile(DESCRIPTOR_PATH, ToolDefinitionsStateDescriptor.class, false); String actionResult = determineActionResult(stateDescriptor, shouldUpdate); - + if (stateDescriptor != null) { result.add(new ToolDefinitionsOutputDescriptor(ZIP_FILE_NAME, stateDescriptor, actionResult)); } else { - result.add(new ToolDefinitionsOutputDescriptor(ZIP_FILE_NAME, "INTERNAL", + result.add(new ToolDefinitionsOutputDescriptor(ZIP_FILE_NAME, "INTERNAL", FcliBuildProperties.INSTANCE.getFcliBuildDate(), actionResult)); } } - + private static String determineActionResult(ToolDefinitionsStateDescriptor stateDescriptor, boolean shouldUpdate) { if (stateDescriptor == null) { return "RESET"; @@ -389,9 +433,8 @@ private static String determineActionResult(ToolDefinitionsStateDescriptor state private static Set getRequiredYamlFileNames() { var toolNames = Stream.concat( - Arrays.stream(Tool.values()).map(Tool::getToolName), - Arrays.stream(ToolDependency.values()).map(ToolDependency::getToolName) - ); + Arrays.stream(Tool.values()).map(Tool::getToolName), + Arrays.stream(ToolDependency.values()).map(ToolDependency::getToolName)); return toolNames.map(s -> s + ".yaml").collect(Collectors.toSet()); } @@ -400,7 +443,8 @@ private static final void addYamlOutputDescriptors(List requiredYamlNames = getRequiredYamlFileNames(); if (!shouldUpdate) { addYamlDescriptor(result, requiredYamlNames, "SKIPPED_BY_AGE"); - } - else if (source != null && source.contains("https://")) { + } else if (source != null && source.contains("https://")) { addYamlDescriptor(result, requiredYamlNames, "UPDATED"); - } - else { + } else { Set foundYamlNames = new HashSet<>(); String zipPathOnly = source != null - ? Path.of(source).getFileName().toString() - : null; + ? Path.of(source).getFileName().toString() + : null; if (source != null) { updateActionResultForUserFile(result, requiredYamlNames, foundYamlNames, zipPathOnly, source); } @@ -448,12 +490,12 @@ private static void updateActionResultForUserFile(List result, Set requiredYamlNames, Set foundYamlNames, String zipPathOnly) { String name = Path.of(entry.getName()).getFileName().toString(); Date lastModified = getEntryLastModified(entry); - + if (requiredYamlNames.contains(name)) { result.add(new ToolDefinitionsOutputDescriptor(name, zipPathOnly, lastModified, "UPDATED")); foundYamlNames.add(name); @@ -461,11 +503,11 @@ private static void processUserZipEntry(ZipEntry entry, List result, String fileName) { Date lastModified = getFileOrResourceLastModified(fileName); result.add(new ToolDefinitionsOutputDescriptor(fileName, ZIP_FILE_NAME, lastModified, "NOT_PRESENT")); } - + private static Date getFileOrResourceLastModified(String fileName) { Path filePath = DEFINITIONS_STATE_DIR.resolve(fileName); try { @@ -501,7 +543,8 @@ private static void addYamlDescriptor(List resu while ((entry = zis.getNextEntry()) != null) { String name = Path.of(entry.getName()).getFileName().toString(); if (requiredYamlNames.contains(name)) { - result.add(new ToolDefinitionsOutputDescriptor(name, ZIP_FILE_NAME, getEntryLastModified(entry), action)); + result.add(new ToolDefinitionsOutputDescriptor(name, ZIP_FILE_NAME, getEntryLastModified(entry), + action)); } } } catch (IOException e) { @@ -509,14 +552,14 @@ private static void addYamlDescriptor(List resu } } - private static Date getInternalResourceZipEntryLastModified(String fileName) { try (InputStream is = FileUtils.getResourceInputStream(DEFINITIONS_INTERNAL_ZIP); ZipInputStream zis = new ZipInputStream(is)) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.getName().equals(fileName)) { - return entry.getLastModifiedTime() != null ? new Date(entry.getLastModifiedTime().toMillis()) : null; + return entry.getLastModifiedTime() != null ? new Date(entry.getLastModifiedTime().toMillis()) + : null; } } } catch (IOException e) { @@ -526,13 +569,16 @@ private static Date getInternalResourceZipEntryLastModified(String fileName) { } /** - * Determines whether tool definitions should be updated based on force flag or age. + * Determines whether tool definitions should be updated based on force flag or + * age. *

- * If force is true, always returns true. If maxAge is specified, checks if current + * If force is true, always returns true. If maxAge is specified, checks if + * current * definitions are older than that age. Otherwise, uses default age of 6 hours. * * @param forceUpdate if true, always update regardless of age - * @param maxAge optional max age string (e.g., "4h", "1d"), or null to use default + * @param maxAge optional max age string (e.g., "4h", "1d"), or null to use + * default * @return true if definitions should be updated, false otherwise * @throws IOException if unable to determine file modification time */ @@ -543,16 +589,16 @@ private static boolean shouldUpdateToolDefinitions(boolean forceUpdate, String m if (!Files.exists(DEFINITIONS_STATE_ZIP)) { return true; } - + FileTime modTime = getModifiedTime(DEFINITIONS_STATE_ZIP); if (modTime == null) { throw new FcliSimpleException("Could not determine last modified time for: " + DEFINITIONS_STATE_ZIP); } - + long ageThresholdMillis = StringUtils.isNotBlank(maxAge) - ? parseDurationToMillis(maxAge) - : DEFAULT_UPDATE_AGE_HOURS * 60 * 60 * 1000; - + ? parseDurationToMillis(maxAge) + : DEFAULT_UPDATE_AGE_HOURS * 60 * 60 * 1000; + long now = System.currentTimeMillis(); long age = now - modTime.toMillis(); return age > ageThresholdMillis; @@ -561,12 +607,14 @@ private static boolean shouldUpdateToolDefinitions(boolean forceUpdate, String m /** * Parses a duration string to milliseconds using only days, hours, and minutes. *

- * Supported format examples: "1d" (1 day), "4h" (4 hours), "30m" (30 minutes), "1d4h" (1 day 4 hours). + * Supported format examples: "1d" (1 day), "4h" (4 hours), "30m" (30 minutes), + * "1d4h" (1 day 4 hours). * Seconds are explicitly not supported to avoid confusion with "6h" default. * * @param duration the duration string to parse * @return the duration in milliseconds - * @throws FcliSimpleException if the format is invalid or contains unsupported units + * @throws FcliSimpleException if the format is invalid or contains unsupported + * units */ private static long parseDurationToMillis(String duration) { try { @@ -574,7 +622,8 @@ private static long parseDurationToMillis(String duration) { var helper = DateTimePeriodHelper.byRange(Period.MINUTES, Period.DAYS); return helper.parsePeriodToMillis(duration); } catch (IllegalArgumentException e) { - throw new FcliSimpleException("Invalid duration format: " + duration + ". Use only d (days), h (hours), m (minutes)", e); + throw new FcliSimpleException( + "Invalid duration format: " + duration + ". Use only d (days), h (hours), m (minutes)", e); } } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerCommands.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerCommands.java new file mode 100644 index 0000000000..7464ecd082 --- /dev/null +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerCommands.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.tool.sourceanalyzer.cli.cmd; + +import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; + +import picocli.CommandLine.Command; + +/** + * Container command for all 'fcli tool sourceanalyzer' subcommands. + * + * @author Sangamesh Vijaykumar + */ +@Command( + name = ToolSourceAnalyzerCommands.TOOL_NAME, + subcommands = { + ToolSourceAnalyzerListCommand.class, + ToolSourceAnalyzerGetCommand.class, + ToolSourceAnalyzerRegisterCommand.class, + ToolSourceAnalyzerRunCommand.class, + ToolSourceAnalyzerUpdateRulePacksCommand.class + } + +) +public class ToolSourceAnalyzerCommands extends AbstractContainerCommand { + static final String TOOL_NAME = "sourceanalyzer"; + static final String[] TOOL_ENV_VAR_PREFIXES = {"SOURCEANALYZER"}; + +} \ No newline at end of file diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerGetCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerGetCommand.java new file mode 100644 index 0000000000..d03aa09cf1 --- /dev/null +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerGetCommand.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.tool.sourceanalyzer.cli.cmd; + +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.tool._common.cli.cmd.AbstractToolGetCommand; +import com.fortify.cli.tool._common.helper.Tool; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +/** + * Command to get information about a specific Fortify Source Analyzer version. + * + * @author Sangamesh Vijaykumar + */ +@Command(name = OutputHelperMixins.Get.CMD_NAME) +public class ToolSourceAnalyzerGetCommand extends AbstractToolGetCommand { + @Getter @Mixin private OutputHelperMixins.Get outputHelper; + + @Override + protected final Tool getTool() { + return Tool.SOURCE_ANALYZER; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerListCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerListCommand.java new file mode 100644 index 0000000000..92b23abbc5 --- /dev/null +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerListCommand.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.tool.sourceanalyzer.cli.cmd; + +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.tool._common.cli.cmd.AbstractToolListCommand; +import com.fortify.cli.tool._common.helper.Tool; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +/** + * Command to list available and installed Fortify Source Analyzer versions. + * + * @author Sangamesh Vijaykumar + */ +@Command(name = OutputHelperMixins.List.CMD_NAME) +public class ToolSourceAnalyzerListCommand extends AbstractToolListCommand { + @Getter @Mixin private OutputHelperMixins.List outputHelper; + + @Override + protected final Tool getTool() { + return Tool.SOURCE_ANALYZER; + } +} diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerRegisterCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerRegisterCommand.java new file mode 100644 index 0000000000..dcd5280b4a --- /dev/null +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerRegisterCommand.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.tool.sourceanalyzer.cli.cmd; + +import java.io.File; + +import com.fortify.cli.tool._common.cli.cmd.AbstractToolRegisterCommand; +import com.fortify.cli.tool._common.helper.Tool; +import com.fortify.cli.tool._common.helper.ToolVersionDetector; + +import picocli.CommandLine.Command; + +/** + * Command to register a specific Fortify Source Analyzer version. + * + * @author Sangamesh Vijaykumar + */ +@Command(name = "register") +public class ToolSourceAnalyzerRegisterCommand extends AbstractToolRegisterCommand { + + @Override + protected final Tool getTool() { + return Tool.SOURCE_ANALYZER; + } + + @Override + protected String detectVersion(File toolBinary, File installDir) { + // Execute sourceanalyzer --version + String output = ToolVersionDetector.tryExecute(toolBinary, "--version"); + if (output != null) { + String version = ToolVersionDetector.extractVersionFromOutput(output); + if (version != null) { + return version; + } + } + + return "unknown"; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerRunCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerRunCommand.java new file mode 100644 index 0000000000..9ba64baa51 --- /dev/null +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerRunCommand.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.tool.sourceanalyzer.cli.cmd; + +import java.util.List; + +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.util.PlatformHelper; +import com.fortify.cli.tool._common.cli.cmd.AbstractToolRunCommand; +import com.fortify.cli.tool._common.helper.Tool; +import com.fortify.cli.tool._common.helper.ToolInstallationDescriptor; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +/** + * Command for running Fortify Source Analyzer. This command allows for running Fortify Source Analyzer as already installed in the user's machine, \ + * and is not limited to running versions of Fortify Source Analyzer that were installed through the 'fcli tool sourceanalyzer install' command. It is recommended to use double dashes to separate fcli options from Fortify Source Analyzer options, \ + * i.e., 'fcli tool sourceanalyzer run -- ' to explicitly differentiate between fcli options and Fortify Source Analyzer options. + * + * @author Sangamesh Vijaykumar + */ +@Command(name = "run") +public class ToolSourceAnalyzerRunCommand extends AbstractToolRunCommand { + @Getter @Mixin private OutputHelperMixins.Get outputHelper; + + @Override + protected final Tool getTool() { + return Tool.SOURCE_ANALYZER; + } + + @Override + protected List getBaseCommand(ToolInstallationDescriptor descriptor) { + var baseCmd = PlatformHelper.isWindows() ? "sourceanalyzer.exe" : "sourceanalyzer"; + return List.of(descriptor.getBinPath().resolve(baseCmd).toString()); + } +} diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerUpdateRulePacksCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerUpdateRulePacksCommand.java new file mode 100644 index 0000000000..12b81de7ae --- /dev/null +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sourceanalyzer/cli/cmd/ToolSourceAnalyzerUpdateRulePacksCommand.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.tool.sourceanalyzer.cli.cmd; + +import java.util.List; + +import com.fortify.cli.common.util.PlatformHelper; +import com.fortify.cli.tool._common.cli.cmd.AbstractToolRunCommand; +import com.fortify.cli.tool._common.helper.Tool; +import com.fortify.cli.tool._common.helper.ToolInstallationDescriptor; + +import picocli.CommandLine.Command; + +/** + * Command to update Fortify Source Analyzer rulepacks by running the + * fortifyupdate binary from the registered installation. + * + * This command uses the same installation resolution logic as other + * sourceanalyzer tool commands and simply executes the platform-specific + * fortifyupdate executable from the installation's bin directory. + * + * @author Sangamesh Vijaykumar + */ +@Command(name = "update-rule-packs") +public class ToolSourceAnalyzerUpdateRulePacksCommand extends AbstractToolRunCommand { + @Override + protected final Tool getTool() { + return Tool.SOURCE_ANALYZER; + } + + @Override + protected List getBaseCommand(ToolInstallationDescriptor descriptor) { + var baseCmd = PlatformHelper.isWindows() ? "fortifyupdate.cmd" : "fortifyupdate"; + return List.of(descriptor.getBinPath().resolve(baseCmd).toString()); + } +} diff --git a/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties b/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties index 05a005117b..0145a30314 100644 --- a/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties +++ b/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties @@ -183,6 +183,8 @@ fcli.tool.env.init.usage.description = Detects, registers, and installs Fortify \n ${fcli.tool.bugtracker-utility.envPrefix}_VERSION, ${fcli.tool.bugtracker-utility.envPrefix}_HOME\n\ - vuln-exporter: \ \n ${fcli.tool.vuln-exporter.envPrefix}_VERSION, ${fcli.tool.vuln-exporter.envPrefix}_HOME\n\ + - sourceanalyzer: \ + \n ${fcli.tool.sourceanalyzer.envPrefix}_VERSION, ${fcli.tool.sourceanalyzer.envPrefix}_HOME\n\ \n\ For ScanCentral Client, the init command will look for a compatible JRE based on the following environment variables \ (in this order):\n\ @@ -455,6 +457,29 @@ fcli.tool.vuln-exporter.uninstall.usage.header = Uninstall Fortify Vulnerability fcli.tool.vuln-exporter.uninstall.usage.description = This command removes one or more Fortify Vulnerability Exporter installations that were previously installed using the 'fcli tool vuln-exporter install' command. ${fcli.tool.uninstall.generic-global-bin-description} fcli.tool.vuln-exporter.uninstall.confirm = Confirm removal of Fortify Vulnerability Exporter. +# fcli tool sourceanalyzer (sca) +fcli.tool.sourceanalyzer.usage.header = Manage Fortify Source Analyzer registrations. +fcli.tool.sourceanalyzer.usage.description = This command analyzes source code, bytecode, or an intermediate representation, which helps identify vulnerabilities early in the Software Development Lifecycle (SDLC) when they are less expensive to fix. +fcli.tool.sourceanalyzer.list.usage.header = List available and installed Fortify Source Analyzer versions. +fcli.tool.sourceanalyzer.list.usage.description = List available and installed Fortify Source Analyzer versions. +fcli.tool.sourceanalyzer.get.usage.header = Get information about a specific Fortify Source Analyzer version. +fcli.tool.sourceanalyzer.get.usage.description = This command retrieves detailed information about a specific Fortify Source Analyzer version, \ + including available platforms and installation status. +fcli.tool.sourceanalyzer.register.usage.header = Register an external Fortify Source Analyzer installation. +fcli.tool.sourceanalyzer.register.usage.description.0 = ${fcli.tool.register.generic-description} +fcli.tool.sourceanalyzer.register.usage.description.1 = Examples:\n\ + \ --path /opt/fortify/tools/sourceanalyzer\n\ + \ --path $SOURCE_ANALYZER_HOME\n\ + \ --path $PATH\n\ + \ --path $SOURCE_ANALYZER_HOME:$PATH\n + +fcli.tool.sourceanalyzer.run.usage.header = Run Fortify Source Analyzer. +fcli.tool.sourceanalyzer.run.usage.description = This command allows for running Fortify Source Analyzer as already installed in the user's machine. It is recommended to use double dashes to separate \ + fcli options from Fortify Source Analyzer options, i.e., 'fcli tool sourceanalyzer run -- ' \ + to explicitly differentiate between fcli options and Fortify Source Analyzer options. +fcli.tool.sourceanalyzer.update-rule-packs.usage.header = Update Fortify Source Analyzer rulepacks. +fcli.tool.sourceanalyzer.update-rule-packs.usage.description = This command runs the fortifyupdate utility from the registered Fortify Source Analyzer installation to update local rulepacks. \ + Ensure that the installation has network access to your Fortify update server or that it is otherwise configured for offline updates. ################################################################################################################# # The following are technical properties that shouldn't be internationalized #################################### ################################################################################################################# @@ -462,4 +487,4 @@ fcli.tool.output.table.header.isDefaultMarker = fcli.tool.output.table.args = name,version,aliasesString,stable,installDir,isDefaultMarker fcli.tool.list-platforms.output.table.args = platform fcli.tool.definitions.output.table.args = name,source,lastUpdate -fcli.tool.env.init.output.table.args = name,version,installDir +fcli.tool.env.init.output.table.args = name,version,installDir \ No newline at end of file