Skip to content

Commit 2f097bf

Browse files
authored
SCA SBOM report format support (#542)
* Adding support to SBOM report format * Adding support to SBOM report format - The sca /export/requests api is not working properly * Adding SCA SBOM report features * SCA SBOM - improvements - unity + integration tests * linter * pr review suggestions && fixes * improving SBOM error messages
1 parent dd8742f commit 2f097bf

21 files changed

+494
-131
lines changed

cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ func main() {
4242
descriptionsPath := viper.GetString(params.DescriptionsPathKey)
4343
tenantConfigurationPath := viper.GetString(params.TenantConfigurationPathKey)
4444
resultsPdfPath := viper.GetString(params.ResultsPdfReportPathKey)
45+
resultsSbomPath := viper.GetString(params.ResultsSbomReportPathKey)
4546

4647
scansWrapper := wrappers.NewHTTPScansWrapper(scans)
4748
resultsPdfReportsWrapper := wrappers.NewResultsPdfReportsHTTPWrapper(resultsPdfPath)
49+
resultsSbomReportsWrapper := wrappers.NewResultsSbomReportsHTTPWrapper(resultsSbomPath)
4850
groupsWrapper := wrappers.NewHTTPGroupsWrapper(groups)
4951
logsWrapper := wrappers.NewLogsWrapper(logs)
5052
uploadsWrapper := wrappers.NewUploadsHTTPWrapper(uploads)
@@ -68,6 +70,7 @@ func main() {
6870

6971
astCli := commands.NewAstCLI(
7072
scansWrapper,
73+
resultsSbomReportsWrapper,
7174
resultsPdfReportsWrapper,
7275
resultsPredicatesWrapper,
7376
codeBashingWrapper,

internal/commands/result.go

Lines changed: 124 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,48 +24,51 @@ import (
2424
)
2525

2626
const (
27-
failedCreatingSummary = "Failed creating summary"
28-
failedGettingScan = "Failed getting scan"
29-
failedListingResults = "Failed listing results"
30-
failedListingCodeBashing = "Failed codebashing link"
31-
mediumLabel = "medium"
32-
highLabel = "high"
33-
lowLabel = "low"
34-
infoLabel = "info"
35-
sonarTypeLabel = "_sonar"
36-
directoryPermission = 0700
37-
infoSonar = "INFO"
38-
lowSonar = "MINOR"
39-
mediumSonar = "MAJOR"
40-
highSonar = "CRITICAL"
41-
infoLowSarif = "note"
42-
mediumSarif = "warning"
43-
highSarif = "error"
44-
vulnerabilitySonar = "VULNERABILITY"
45-
infoCx = "INFO"
46-
lowCx = "LOW"
47-
mediumCx = "MEDIUM"
48-
highCx = "HIGH"
49-
codeBashingKey = "cb-url"
50-
failedGettingBfl = "Failed getting BFL"
51-
notAvailableString = "N/A"
52-
notAvailableNumber = -1
53-
defaultPaddingSize = -14
54-
scanPendingMessage = "Scan triggered in asynchronous mode or still running. Click more details to get the full status."
55-
scaType = "sca"
56-
directDependencyType = "Direct Dependency"
57-
indirectDependencyType = "Transitive Dependency"
58-
startedStatus = "started"
59-
27+
failedCreatingSummary = "Failed creating summary"
28+
failedGettingScan = "Failed getting scan"
29+
failedListingResults = "Failed listing results"
30+
failedListingCodeBashing = "Failed codebashing link"
31+
mediumLabel = "medium"
32+
highLabel = "high"
33+
lowLabel = "low"
34+
infoLabel = "info"
35+
sonarTypeLabel = "_sonar"
36+
directoryPermission = 0700
37+
infoSonar = "INFO"
38+
lowSonar = "MINOR"
39+
mediumSonar = "MAJOR"
40+
highSonar = "CRITICAL"
41+
infoLowSarif = "note"
42+
mediumSarif = "warning"
43+
highSarif = "error"
44+
vulnerabilitySonar = "VULNERABILITY"
45+
infoCx = "INFO"
46+
lowCx = "LOW"
47+
mediumCx = "MEDIUM"
48+
highCx = "HIGH"
49+
codeBashingKey = "cb-url"
50+
failedGettingBfl = "Failed getting BFL"
51+
notAvailableString = "N/A"
52+
notAvailableNumber = -1
53+
defaultPaddingSize = -14
54+
scanPendingMessage = "Scan triggered in asynchronous mode or still running. Click more details to get the full status."
55+
scaType = "sca"
56+
directDependencyType = "Direct Dependency"
57+
indirectDependencyType = "Transitive Dependency"
58+
startedStatus = "started"
6059
completedStatus = "completed"
60+
exportingStatus = "Exporting"
61+
pendingStatus = "Pending"
6162
pdfToEmailFlagDescription = "Send the PDF report to the specified email address." +
6263
" Use \",\" as the delimiter for multiple emails"
6364
pdfOptionsFlagDescription = "Sections to generate PDF report. Available options: Iac-Security,Sast,Sca," +
6465
defaultPdfOptionsDataSections
65-
delayValueForPdfReport = 150
66+
sbomReportFlagDescription = "Sections to generate SBOM report. Available options: CycloneDxJson,CycloneDxXml,SpdxJson"
67+
delayValueForReport = 150
6668
reportNameScanReport = "scan-report"
6769
reportTypeEmail = "email"
6870
defaultPdfOptionsDataSections = "ScanSummary,ExecutiveSummary,ScanResults"
71+
defaultSbomOption = "CycloneDxJson"
6972
exploitablePathFlagDescription = "Enable or disable exploitable path in scan. Available options: true,false"
7073
scaLastScanTimeFlagDescription = "SCA last scan time. Available options: integer above 1"
7174
projectPrivatePackageFlagDescription = "Enable or disable project private package. Available options: true,false"
@@ -109,6 +112,7 @@ var sonarSeverities = map[string]string{
109112
func NewResultsCommand(
110113
resultsWrapper wrappers.ResultsWrapper,
111114
scanWrapper wrappers.ScansWrapper,
115+
resultsSbomWrapper wrappers.ResultsSbomWrapper,
112116
resultsPdfReportsWrapper wrappers.ResultsPdfWrapper,
113117
codeBashingWrapper wrappers.CodeBashingWrapper,
114118
bflWrapper wrappers.BflWrapper,
@@ -125,7 +129,7 @@ func NewResultsCommand(
125129
),
126130
},
127131
}
128-
showResultCmd := resultShowSubCommand(resultsWrapper, scanWrapper, resultsPdfReportsWrapper, risksOverviewWrapper)
132+
showResultCmd := resultShowSubCommand(resultsWrapper, scanWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, risksOverviewWrapper)
129133
codeBashingCmd := resultCodeBashing(codeBashingWrapper)
130134
bflResultCmd := resultBflSubCommand(bflWrapper)
131135
resultCmd.AddCommand(
@@ -137,6 +141,7 @@ func NewResultsCommand(
137141
func resultShowSubCommand(
138142
resultsWrapper wrappers.ResultsWrapper,
139143
scanWrapper wrappers.ScansWrapper,
144+
resultsSbomWrapper wrappers.ResultsSbomWrapper,
140145
resultsPdfReportsWrapper wrappers.ResultsPdfWrapper,
141146
risksOverviewWrapper wrappers.RisksOverviewWrapper,
142147
) *cobra.Command {
@@ -149,7 +154,7 @@ func resultShowSubCommand(
149154
$ cx results show --scan-id <scan Id>
150155
`,
151156
),
152-
RunE: runGetResultCommand(resultsWrapper, scanWrapper, resultsPdfReportsWrapper, risksOverviewWrapper),
157+
RunE: runGetResultCommand(resultsWrapper, scanWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, risksOverviewWrapper),
153158
}
154159
addScanIDFlag(resultShowCmd, "ID to report on.")
155160
addResultFormatFlag(
@@ -159,10 +164,12 @@ func resultShowSubCommand(
159164
printer.FormatSummaryConsole,
160165
printer.FormatSarif,
161166
printer.FormatSummaryJSON,
167+
printer.FormatSbom,
162168
printer.FormatPDF,
163169
printer.FormatSummaryMarkdown,
164170
)
165171
resultShowCmd.PersistentFlags().String(commonParams.ReportFormatPdfToEmailFlag, "", pdfToEmailFlagDescription)
172+
resultShowCmd.PersistentFlags().String(commonParams.ReportSbomFormatFlag, defaultSbomOption, sbomReportFlagDescription)
166173
resultShowCmd.PersistentFlags().String(commonParams.ReportFormatPdfOptionsFlag, defaultPdfOptionsDataSections, pdfOptionsFlagDescription)
167174
resultShowCmd.PersistentFlags().String(commonParams.TargetFlag, "cx_result", "Output file")
168175
resultShowCmd.PersistentFlags().String(commonParams.TargetPathFlag, ".", "Output Path")
@@ -511,6 +518,7 @@ func generateScanSummaryURL(summary *wrappers.ResultSummary) string {
511518
func runGetResultCommand(
512519
resultsWrapper wrappers.ResultsWrapper,
513520
scanWrapper wrappers.ScansWrapper,
521+
resultsSbomWrapper wrappers.ResultsSbomWrapper,
514522
resultsPdfReportsWrapper wrappers.ResultsPdfWrapper,
515523
risksOverviewWrapper wrappers.RisksOverviewWrapper,
516524
) func(cmd *cobra.Command, args []string) error {
@@ -520,6 +528,7 @@ func runGetResultCommand(
520528
format, _ := cmd.Flags().GetString(commonParams.TargetFormatFlag)
521529
formatPdfToEmail, _ := cmd.Flags().GetString(commonParams.ReportFormatPdfToEmailFlag)
522530
formatPdfOptions, _ := cmd.Flags().GetString(commonParams.ReportFormatPdfOptionsFlag)
531+
formatSbomOptions, _ := cmd.Flags().GetString(commonParams.ReportSbomFormatFlag)
523532

524533
scanID, _ := cmd.Flags().GetString(commonParams.ScanIDFlag)
525534
params, err := getFilters(cmd)
@@ -530,11 +539,13 @@ func runGetResultCommand(
530539
resultsWrapper,
531540
risksOverviewWrapper,
532541
scanWrapper,
542+
resultsSbomWrapper,
533543
resultsPdfReportsWrapper,
534544
scanID,
535545
format,
536546
formatPdfToEmail,
537547
formatPdfOptions,
548+
formatSbomOptions,
538549
targetFile,
539550
targetPath,
540551
params)
@@ -585,11 +596,13 @@ func CreateScanReport(
585596
resultsWrapper wrappers.ResultsWrapper,
586597
risksOverviewWrapper wrappers.RisksOverviewWrapper,
587598
scanWrapper wrappers.ScansWrapper,
599+
resultsSbomWrapper wrappers.ResultsSbomWrapper,
588600
resultsPdfReportsWrapper wrappers.ResultsPdfWrapper,
589601
scanID,
590602
reportTypes,
591603
formatPdfToEmail,
592604
formatPdfOptions,
605+
formatSbomOptions,
593606
targetFile,
594607
targetPath string,
595608
params map[string]string,
@@ -621,7 +634,7 @@ func CreateScanReport(
621634

622635
reportList := strings.Split(reportTypes, ",")
623636
for _, reportType := range reportList {
624-
err = createReport(reportType, formatPdfToEmail, formatPdfOptions, targetFile, targetPath, results, summary, resultsPdfReportsWrapper)
637+
err = createReport(reportType, formatPdfToEmail, formatPdfOptions, formatSbomOptions, targetFile, targetPath, results, summary, resultsSbomWrapper, resultsPdfReportsWrapper)
625638
if err != nil {
626639
return err
627640
}
@@ -674,10 +687,12 @@ func createReport(
674687
format,
675688
formatPdfToEmail,
676689
formatPdfOptions,
690+
formatSbomOptions,
677691
targetFile,
678692
targetPath string,
679693
results *wrappers.ScanResultsCollection,
680694
summary *wrappers.ResultSummary,
695+
resultsSbomWrapper wrappers.ResultsSbomWrapper,
681696
resultsPdfReportsWrapper wrappers.ResultsPdfWrapper,
682697

683698
) error {
@@ -719,8 +734,20 @@ func createReport(
719734
convertNotAvailableNumberToZero(summary)
720735
return writeMarkdownSummary(summaryRpt, summary)
721736
}
722-
err := fmt.Errorf("bad report format %s", format)
723-
return err
737+
if printer.IsFormat(format, printer.FormatSbom) {
738+
targetType := printer.FormatJSON
739+
if strings.Contains(strings.ToLower(formatSbomOptions), printer.FormatXML) {
740+
targetType = printer.FormatXML
741+
}
742+
summaryRpt := createTargetName(fmt.Sprintf("%s_%s", targetFile, printer.FormatSbom), targetPath, targetType)
743+
convertNotAvailableNumberToZero(summary)
744+
745+
if !contains(summary.EnginesEnabled, scaType) {
746+
return fmt.Errorf("to generate %s report, SCA engine must be enabled on scan summary", printer.FormatSbom)
747+
}
748+
return exportSbomResults(resultsSbomWrapper, summaryRpt, summary, formatSbomOptions)
749+
}
750+
return fmt.Errorf("bad report format %s", format)
724751
}
725752

726753
func createTargetName(targetFile, targetPath, targetType string) string {
@@ -869,9 +896,47 @@ func exportJSONSummaryResults(targetFile string, results *wrappers.ResultSummary
869896
return nil
870897
}
871898

899+
func exportSbomResults(sbomWrapper wrappers.ResultsSbomWrapper, targetFile string, results *wrappers.ResultSummary, formatSbomOptions string) error {
900+
payload := &wrappers.SbomReportsPayload{
901+
ScanID: results.ScanID,
902+
FileFormat: defaultSbomOption,
903+
}
904+
if formatSbomOptions != "" && formatSbomOptions != defaultSbomOption {
905+
format, err := validateSbomOptions(formatSbomOptions)
906+
if err != nil {
907+
return err
908+
}
909+
payload.FileFormat = format
910+
}
911+
912+
pollingResp := &wrappers.SbomPollingResponse{}
913+
914+
sbomresp, err := sbomWrapper.GenerateSbomReport(payload)
915+
if err != nil {
916+
return err
917+
}
918+
919+
log.Println("Generating SBOM report with " + payload.FileFormat + " file format")
920+
pollingResp.ExportStatus = exportingStatus
921+
for pollingResp.ExportStatus == exportingStatus || pollingResp.ExportStatus == pendingStatus {
922+
pollingResp, err = sbomWrapper.GetSbomReportStatus(sbomresp.ExportID)
923+
if err != nil {
924+
return errors.Wrapf(err, "%s", "failed getting SBOM report status")
925+
}
926+
time.Sleep(delayValueForReport * time.Millisecond)
927+
}
928+
if !strings.EqualFold(pollingResp.ExportStatus, completedStatus) {
929+
return errors.Errorf("SBOM generating failed - Current status: %s", pollingResp.ExportStatus)
930+
}
931+
err = sbomWrapper.DownloadSbomReport(pollingResp.ExportID, targetFile)
932+
if err != nil {
933+
return errors.Wrapf(err, "%s", "Failed downloading SBOM report")
934+
}
935+
return nil
936+
}
872937
func exportPdfResults(pdfWrapper wrappers.ResultsPdfWrapper, summary *wrappers.ResultSummary, summaryRpt, formatPdfToEmail, pdfOptions string) error {
873938
pdfReportsPayload := &wrappers.PdfReportsPayload{}
874-
poolingResp := &wrappers.PdfPoolingResponse{}
939+
pollingResp := &wrappers.PdfPollingResponse{}
875940

876941
pdfOptionsSections, pdfOptionsEngines, err := validatePdfOptions(pdfOptions)
877942
if err != nil {
@@ -910,16 +975,16 @@ func exportPdfResults(pdfWrapper wrappers.ResultsPdfWrapper, summary *wrappers.R
910975
}
911976

912977
log.Println("Generating PDF report")
913-
poolingResp.Status = startedStatus
914-
for poolingResp.Status == startedStatus {
915-
poolingResp, webErr, err = pdfWrapper.CheckPdfReportStatus(pdfReportID.ReportID)
978+
pollingResp.Status = startedStatus
979+
for pollingResp.Status == startedStatus {
980+
pollingResp, webErr, err = pdfWrapper.CheckPdfReportStatus(pdfReportID.ReportID)
916981
if err != nil || webErr != nil {
917982
return errors.Wrapf(err, "%v", webErr)
918983
}
919-
time.Sleep(delayValueForPdfReport * time.Millisecond)
984+
time.Sleep(delayValueForReport * time.Millisecond)
920985
}
921-
if poolingResp.Status != completedStatus {
922-
return errors.Errorf("PDF generating failed - Current status: %s", poolingResp.Status)
986+
if pollingResp.Status != completedStatus {
987+
return errors.Errorf("PDF generating failed - Current status: %s", pollingResp.Status)
923988
}
924989
err = pdfWrapper.DownloadPdfReport(pdfReportID.ReportID, summaryRpt)
925990
if err != nil {
@@ -928,6 +993,19 @@ func exportPdfResults(pdfWrapper wrappers.ResultsPdfWrapper, summary *wrappers.R
928993
return nil
929994
}
930995

996+
func validateSbomOptions(sbomOption string) (string, error) {
997+
var sbomOptionsStringMap = map[string]string{
998+
"cyclonedxjson": "CycloneDxJson",
999+
"cyclonedxxml": "CycloneDxXml",
1000+
"spdxjson": "SpdxJson",
1001+
}
1002+
sbomOption = strings.ToLower(strings.ReplaceAll(sbomOption, " ", ""))
1003+
if sbomOptionsStringMap[sbomOption] != "" {
1004+
return sbomOptionsStringMap[sbomOption], nil
1005+
}
1006+
return "", errors.Errorf("invalid SBOM option: %s", sbomOption)
1007+
}
1008+
9311009
func validatePdfOptions(pdfOptions string) (pdfOptionsSections, pdfOptionsEngines []string, err error) {
9321010
var pdfOptionsSectionsMap = map[string]string{
9331011
"scansummary": "ScanSummary",

internal/commands/result_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,28 @@ func TestRunGetResultsGeneratingPdfReporWithOptions(t *testing.T) {
277277
_, err = os.Stat(fmt.Sprintf("%s.%s", fileName, printer.FormatPDF))
278278
assert.NilError(t, err, "report file should exist: "+fileName+printer.FormatPDF)
279279
}
280+
281+
func TestSBOMReportInvalidSBOMOption(t *testing.T) {
282+
err := execCmdNotNilAssertion(t,
283+
"results", "show",
284+
"--report-format", "sbom",
285+
"--scan-id", "MOCK",
286+
"--report-sbom-format", "invalid")
287+
assert.Equal(t, err.Error(), "invalid SBOM option: invalid", "Wrong expected error message")
288+
}
289+
290+
func TestSBOMReportJson(t *testing.T) {
291+
execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sbom")
292+
_, err := os.Stat(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatJSON))
293+
assert.NilError(t, err, "Report file should exist for extension "+printer.FormatJSON)
294+
// Remove generated json file
295+
os.Remove(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatJSON))
296+
}
297+
298+
func TestSBOMReportXML(t *testing.T) {
299+
execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sbom", "--report-sbom-format", "CycloneDxXml")
300+
_, err := os.Stat(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatXML))
301+
assert.NilError(t, err, "Report file should exist for extension "+printer.FormatXML)
302+
// Remove generated json file
303+
os.Remove(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatXML))
304+
}

internal/commands/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const ErrorCodeFormat = "%s: CODE: %d, %s\n"
2424
// NewAstCLI Return a Checkmarx One CLI root command to execute
2525
func NewAstCLI(
2626
scansWrapper wrappers.ScansWrapper,
27+
resultsSbomWrapper wrappers.ResultsSbomWrapper,
2728
resultsPdfReportsWrapper wrappers.ResultsPdfWrapper,
2829
resultsPredicatesWrapper wrappers.ResultsPredicatesWrapper,
2930
codeBashingWrapper wrappers.CodeBashingWrapper,
@@ -128,6 +129,7 @@ func NewAstCLI(
128129
// Create the CLI command structure
129130
scanCmd := NewScanCommand(
130131
scansWrapper,
132+
resultsSbomWrapper,
131133
resultsPdfReportsWrapper,
132134
uploadsWrapper,
133135
resultsWrapper,
@@ -141,6 +143,7 @@ func NewAstCLI(
141143
resultsCmd := NewResultsCommand(
142144
resultsWrapper,
143145
scansWrapper,
146+
resultsSbomWrapper,
144147
resultsPdfReportsWrapper,
145148
codeBashingWrapper,
146149
bflWrapper,

0 commit comments

Comments
 (0)