Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev-2.x.json

author: Fortify
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's skip adding this action for now, I need to think more about how to best fit this into fcli for all possible scenarios (FoD/SSC, local translation, local scan, ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. I'll keep changes in a private branch for future reference.

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}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't want hardcoded tool names in generic base classes. Instead, either:

  • Introduce an overridable method like requireToolDefinitions that returns true by default, and override this method in ToolSourceAnalyzer* commands to return false
  • Introduce this method in IToolHelper, such that base classes can call for example getTool().requiresToolDefinitions()

I think I like the latter better, as then we have centralized configuration as to whether tool definitions are required, and any abstract base class can check for this info. For example, if we ever update any other base classes with optional tool definitions behavior, we only need to update the base class, not the individual command classes.

Not completely sure whether require(s)ToolDefinitions is the best method name to describe the intended behavior; better names are welcome.

Same comment applies to other generic classes like AbstractToolRunCommand, ToolInstallationsResolver, ToolRegistrationHelper, ...

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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> toolArgs;

@Override
public final Integer call() throws Exception {
validateWorkingDirectory();
Expand All @@ -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()) {
Expand All @@ -91,7 +93,7 @@ private void validateWorkingDirectory() {
));
}
}

private final Integer call(List<String> baseCmd) throws Exception {
if ( baseCmd==null ) { throw new FcliBugException("Base command to execute may not be null"); }
var fullCmd = Stream.of(baseCmd, getToolArgs())
Expand All @@ -102,7 +104,7 @@ private final Integer call(List<String> 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.
Expand All @@ -125,7 +127,7 @@ private final Integer call(List<String> baseCmd) throws Exception {
}
return process.exitValue();
}

private static void inheritIO(final InputStream src, final PrintStream dest) {
new Thread(new Runnable() {
@SneakyThrows
Expand All @@ -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(
Expand All @@ -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)
Expand All @@ -158,7 +177,7 @@ private ToolInstallationDescriptor checkNotNull(ToolInstallationDescriptor descr
}
return descriptor;
}

protected abstract Tool getTool();
protected List<List<String>> getBaseCommands(ToolInstallationDescriptor descriptor) {
return List.of(getBaseCommand(descriptor));
Expand All @@ -167,5 +186,5 @@ protected List<String> getBaseCommand(ToolInstallationDescriptor descriptor) {
return null;
}
protected void updateProcessBuilder(ProcessBuilder pb) {};

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Tool> TOOL_NAME_MAP = new HashMap<>();
private static final Map<String, Tool> TOOL_ALIAS_MAP = new HashMap<>();
Expand Down Expand Up @@ -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";
}
}
}
Loading
Loading