diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java index dad1b4f23b..d7eb675594 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java @@ -241,6 +241,8 @@ public static final class ActionStepDescriptor implements IActionIfSupplier { @JsonProperty("throw") private TemplateExpression _throw; /** Optional set operations */ private List set; + /** Optional unset operations */ + private List unset; /** Optional write operations */ private List write; /** Optional forEach operation */ @@ -295,6 +297,22 @@ public static enum ActionStepSetOperation { } } + /** + * This class describes an operation to explicitly unset a data property. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepUnsetDescriptor implements IActionIfSupplier { + /** Optional if-expression, executing this step only if condition evaluates to true */ + @JsonProperty("if") private SimpleExpression _if; + /** Required name for this step element */ + private String name; + + public void postLoad(ActionDescriptor action) { + checkNotBlank("set name", name, this); + } + } + /** * This class describes a 'write' step. */ @@ -327,6 +345,8 @@ public static final class ActionStepForEachDescriptor implements IActionIfSuppli /** Processor that runs the forEach steps. This expression must evaluate to an * IActionStepForEachProcessor instance. */ private SimpleExpression processor; + /** Values to iterate over */ + private SimpleExpression values; /** Optional if-expression, executing steps only if condition evaluates to true */ @JsonProperty("if") private SimpleExpression _if; /** Optional break-expression, terminating forEach if condition evaluates to true */ diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java index e31809b918..364b4d480d 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java @@ -55,6 +55,7 @@ import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepRequestDescriptor.ActionStepRequestType; import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepSetDescriptor; import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepSetDescriptor.ActionStepSetOperation; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepUnsetDescriptor; import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepWriteDescriptor; import com.fortify.cli.common.action.helper.ActionDescriptor.ActionValidationException; import com.fortify.cli.common.action.helper.ActionDescriptor.ActionValueTemplateDescriptor; @@ -311,6 +312,7 @@ private final void processStep(ActionStepDescriptor step) { processSupplier(step::getRequests, this::processRequestsStep); processSupplier(step::getForEach, this::processForEachStep); processAll(step::getSet, this::processSetStep); + processAll(step::getUnset, this::processUnsetStep); processAll(step::getWrite, this::processWriteStep); } } @@ -344,11 +346,20 @@ private void processSetStep(ActionStepSetDescriptor set) { var value = getValueForSetStep(set); setDataValue(name, value); } + + private void processUnsetStep(ActionStepUnsetDescriptor unset) { + unsetDataValue(unset.getName()); + } private void setDataValue(String name, JsonNode value) { localData.set(name, value); if ( parent!=null ) { parent.setDataValue(name, value); } } + + private void unsetDataValue(String name) { + localData.remove(name); + if ( parent!=null ) { parent.unsetDataValue(name); } + } private JsonNode getValueForSetStep(ActionStepSetDescriptor set) { var valueExpression = set.getValue(); @@ -451,8 +462,15 @@ private void processThrowStep(TemplateExpression message) { } private void processForEachStep(ActionStepForEachDescriptor forEach) { - spelEvaluator.evaluate(forEach.getProcessor(), localData, IActionStepForEachProcessor.class) - .process(node->processForEachStepNode(forEach, node)); + var processorExpression = forEach.getProcessor(); + var valuesExpression = forEach.getValues(); + if ( processorExpression!=null ) { + var processor = spelEvaluator.evaluate(processorExpression, localData, IActionStepForEachProcessor.class); + if ( processor!=null ) { processor.process(node->processForEachStepNode(forEach, node)); } + } else if ( valuesExpression!=null ) { + var values = spelEvaluator.evaluate(valuesExpression, localData, ArrayNode.class); + if ( values!=null ) { values.forEach(value->processForEachStepNode(forEach, value)); } + } } private boolean processForEachStepNode(ActionStepForEachDescriptor forEach, JsonNode node) { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java index eae9122f72..ed2b5093e8 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java @@ -18,6 +18,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -84,6 +85,15 @@ public static final String abbreviate(String text, int maxWidth) { return StringUtils.abbreviate(text, maxWidth); } + public static final String repeat(String text, int count) { + if ( count<0 ) { return ""; } + StringBuilder sb = new StringBuilder(); + for ( int i=0; i Iterable asIterable(Iterator iterator) { + return () -> iterator; + } + public static final String copyright() { return String.format("Copyright (c) %s Open Text", Year.now().getValue()); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java index 6842b36431..d6004ab4ad 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java @@ -55,7 +55,7 @@ public JSONDateTimeConverter(DateTimeFormatter fmtDateTime, ZoneId defaultZoneId this.defaultZoneId = defaultZoneId!=null ? defaultZoneId : ZoneId.systemDefault(); } - private static final DateTimeFormatter createDefaultDateTimeFormatter() { + public static final DateTimeFormatter createDefaultDateTimeFormatter() { return DateTimeFormatter.ofPattern("yyyy-MM-dd[['T'][' ']HH:mm:ss[.SSS][.SS]][ZZZZ][Z][XXX][XX][X]"); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java index 7a91fb215a..91899e7350 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java @@ -70,4 +70,9 @@ public static final String indent(String str, String indentStr) { if ( str==null ) { return null; } return Stream.of(str.split("\n")).collect(Collectors.joining("\n"+indentStr, indentStr, "")); } + + // For use in SpEL expressions + public static final String fmt(String fmt, Object... input) { + return String.format(fmt, input); + } } diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/release-summary.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/release-summary.yaml new file mode 100644 index 0000000000..4696747eeb --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/release-summary.yaml @@ -0,0 +1,58 @@ +usage: + header: Generate release summary. + description: | + This action generates a short summary listing issue counts and other statistics + for a given release. + +defaults: + requestTarget: fod + +parameters: + - name: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: stdout" + required: false + defaultValue: stdout + - name: release + cliAliases: rel + description: "Required release id or :[:]" + type: release_single + +steps: + - set: + # Add short alias for release object, as we reference it a lot + - name: r + value: ${parameters.release} + # Define output date format + - name: dateFmt + value: YYYY-MM-dd HH:mm + - write: + - to: ${parameters.file} + valueTemplate: summary-md + - if: parameters.file!='stdout' + to: stdout + value: | + Output written to ${parameters.file} + +valueTemplates: + - name: summary-md + contents: | + # Fortify on Demand Release Summary + + ## [${r.applicationName}${#isNotBlank(r.microserviceNae)?'- '+r.microserviceName:''} - ${r.releaseName}](${#fod.releaseBrowserUrl(r)}) + + Summary generated on: ${#formatDateTime(dateFmt)} + + ### Security Policy + **Rating:** ${#repeat("★", r.rating)}${#repeat("☆", 5-r.rating)} + **Status:** ${r.isPassed?'Pass':'Fail'} + + ### Issue Counts + | Type | Last Scan Date | Critical | High | Medium | Low | + | ----------- | ---------------- | -------- | -------- | -------- | -------- | + ${ + #isNotBlank(r.staticScanDate)? '| **Static** | '+#formatDateTime(dateFmt, r.staticScanDate) +' | '+#fmt('%8s', r.staticCritical) +' | '+#fmt('%8s', r.staticHigh) +' | '+#fmt('%8s', r.staticMedium) +' | '+#fmt('%8s', r.staticLow) +' |\n':'' + +#isNotBlank(r.dynamicScanDate)?'| **Dynamic** | '+#formatDateTime(dateFmt, r.dynamicScanDate)+' | '+#fmt('%8s', r.dynamicCritical)+' | '+#fmt('%8s', r.dynamicHigh)+' | '+#fmt('%8s', r.dynamicMedium)+' | '+#fmt('%8s', r.dynamicLow)+' |\n':'' + +#isNotBlank(r.mobileScanDate)? '| **Mobile** | '+#formatDateTime(dateFmt, r.mobileScanDate) +' | '+#fmt('%8s', r.mobileCritical) +' | '+#fmt('%8s', r.mobileHigh) +' | '+#fmt('%8s', r.mobileMedium) +' | '+#fmt('%8s', r.mobileLow) +' |\n':'' + } + \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/appversion-summary.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/appversion-summary.yaml new file mode 100644 index 0000000000..38f120312f --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/appversion-summary.yaml @@ -0,0 +1,182 @@ +usage: + header: Generate application version summary. + description: | + This action generates a short summary listing issue counts and other statistics + for a given application version. + +defaults: + requestTarget: ssc + +parameters: + - name: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: stdout" + required: false + defaultValue: stdout + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filtersets + cliAliases: fs + description: "Comma-separated list of filter set names, guid's or 'default' to display in the summary. If not specified, all filter sets will be included." + required: false + +steps: + # Set output date format and convert filtersets parameter to array + - set: + - name: dateFmt + value: YYYY-MM-dd HH:mm + - name: filtersetsArray + value: ${parameters.filtersets?.split(',')} + # Load SSC issue selector filter by sets + - progress: Loading issue selector sets + - requests: + - name: issueSelectorSet + uri: /api/v1/projectVersions/${parameters.appversion.id}/issueSelectorSet?fields=filterBySet + + # Collect analysis types from SSC issue selector filter by sets, + # and collect latest scan for each analysis type + - forEach: + values: issueSelectorSet.filterBySet.^[displayName=='Analysis Type'].selectorOptions + name: analysisType + do: + # Collect analysis types + - set: + - name: analysisTypes + value: ${analysisType} + operation: append + # Load latest scan for current analysis type + - progress: Loading ${analysisType.displayName} scan data + - requests: + - name: artifactsResponse + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: lastScans!=null && lastScans[analysisType.guid]!=null + do: + - set: + - name: lastScans + value: "${{analysisType.displayName: artifact._embed.scans?.^[type==#root.analysisType.guid]?.getRealNode()}}" + operation: merge + + # Collect SSC filter set data, together with issue counts by analysis type & folder + - requests: + - name: filterSetsResponse + uri: /api/v1/projectVersions/${parameters.appversion.id}/filterSets + type: paged + forEach: + # Process each filter set if included by filtersets parameter value + name: filterset + if: filtersetsArray==null || filtersetsArray.contains(filterset.title) || filtersetsArray.contains(filterset.guid) || (filtersetsArray.contains('default') && filterset.defaultFilterSet) + do: + - progress: Loading ${filterset.title} filter set data + # Collect filter sets + - set: + - name: filtersets + value: ${filterset} + operation: append + # Collect issue counts for current filter set and each analysis type + - forEach: + name: analysisType + values: analysisTypes + do: + # Load SSC issue counts by folder for current filter set and analysis type + - requests: + - name: issueGroups + uri: /api/v1/projectVersions/${parameters.appversion.id}/issueGroups + query: + qm: issues + groupingtype: FOLDER + filter: ISSUE[11111111-1111-1111-1111-111111111151]:${analysisType.guid} + filterset: ${filterset.guid} + type: paged + forEach: + name: issueGroupFolder + do: + # Collect issue count by filter set, analysis type & folder + - set: + - name: issueCounts + value: ${{filterset.title+':'+analysisType.displayName+':'+issueGroupFolder.id:issueGroupFolder.visibleCount}} + operation: merge + + - progress: Generating output data + + # For each filter set, generate the issue counts table + - forEach: + name: filterset + values: filtersets + do: + # Clear variables for each filter set being processed + - unset: + - name: folderNames + - name: issueCountRows + # Collect folder names from current filter set + - forEach: + name: folderName + values: filterset.folders.![name] + do: + - set: + - name: folderNames + value: ${folderName} + operation: append + # For current filter set, generate an issue count table row for each analysis type + - forEach: + name: analysisTypeName + if: analysisTypes!=null + values: analysisTypes.![displayName] + do: + # Clear counts for each analysis type being processed + - unset: + - name: issueCountRowValues + # For each folder, collect issue counts for current filter set & analysis type + - forEach: + name: folderName + values: filterset.folders.![name] + do: + - set: + - name: issueCountRowValues + operation: append + value: ${#fmt('%10s', issueCounts[filterset.title+':'+analysisTypeName+':'+folderName]?:0)} + # Generate issue count row for current filter set and analysis type, listing + # issue counts as collected above + - set: + - name: issueCountRows + operation: append + value: | + | ${#fmt('%-22s', '**'+analysisTypeName+'**')} | ${#formatDateTime(dateFmt, lastScans[analysisTypeName].uploadDate)} | ${#join(' | ', issueCountRowValues.![#fmt('%10s', #this)])} | + # Combine the output of the steps above to generate full issue counts table for current filter set + - set: + - name: issueCountsOutput + operation: append + value: | + #### ${filterset.title} ${filterset.defaultFilterSet?'(default)':''} + | Analysis Type | Last Scan Date | ${#join(' | ', folderNames.![#fmt('%10s', #this)])} | + | ---------------------- | ---------------- | ${#join(' | ', folderNames.!['----------'])} | + ${#join('', issueCountRows)} + + # Write output based on data collected above, and value template defined below + - write: + - to: ${parameters.file} + valueTemplate: summary-md + - if: parameters.file!='stdout' + to: stdout + value: | + Output written to ${parameters.file} + +valueTemplates: + - name: summary-md + contents: | + # SSC Application Version Summary + + ## [${parameters.appversion.project.name} - ${parameters.appversion.name}](${#ssc.appversionBrowserUrl(parameters.appversion)}) + + Summary generated on: ${#formatDateTime(dateFmt)} + + ### Issue Counts + + ${#join('\n', issueCountsOutput)} +