Skip to content

Commit

Permalink
Add option to get SBOM information (#307)
Browse files Browse the repository at this point in the history
  • Loading branch information
attiasas authored Feb 25, 2025
1 parent 721a145 commit ccd59bf
Show file tree
Hide file tree
Showing 24 changed files with 555 additions and 26 deletions.
8 changes: 5 additions & 3 deletions cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const (
Watches = "watches"
RepoPath = "repo-path"
Licenses = "licenses"
Sbom = "sbom"
Fail = "fail"
ExtendedTable = "extended-table"
MinSeverity = "min-severity"
Expand Down Expand Up @@ -144,7 +145,7 @@ var commandFlags = map[string][]string{
OfflineUpdate: {LicenseId, From, To, Version, Target, Stream, Periodic},
XrScan: {
url, user, password, accessToken, ServerId, SpecFlag, Threads, scanRecursive, scanRegexp, scanAnt,
Project, Watches, RepoPath, Licenses, OutputFormat, Fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, ScanVuln,
Project, Watches, RepoPath, Licenses, Sbom, OutputFormat, Fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, ScanVuln,
},
Enrich: {
url, user, password, accessToken, ServerId, Threads,
Expand All @@ -153,10 +154,10 @@ var commandFlags = map[string][]string{
url, user, password, accessToken, ServerId, Project, BuildVuln, OutputFormat, Fail, ExtendedTable, Rescan,
},
DockerScan: {
ServerId, Project, Watches, RepoPath, Licenses, OutputFormat, Fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, ScanVuln, SecretValidation,
ServerId, Project, Watches, RepoPath, Licenses, Sbom, OutputFormat, Fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, ScanVuln, SecretValidation,
},
Audit: {
url, xrayUrl, user, password, accessToken, ServerId, InsecureTls, Project, Watches, RepoPath, Licenses, OutputFormat, ExcludeTestDeps,
url, xrayUrl, user, password, accessToken, ServerId, InsecureTls, Project, Watches, RepoPath, Sbom, Licenses, OutputFormat, ExcludeTestDeps,
useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm,
Pnpm, Yarn, Go, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, Threads,
Sca, Iac, Sast, Secrets, WithoutCA, ScanVuln, SecretValidation, OutputDir, SkipAutoInstall, AllowPartialResults, MaxTreeDepth,
Expand Down Expand Up @@ -226,6 +227,7 @@ var flagsMap = map[string]components.Flag{
Watches: components.NewStringFlag(Watches, "A comma-separated(,) list of Xray watches, to enable Xray to determine violations accordingly. The command accepts this option only if the --project and --repo-path options are not provided. If none of the three options are provided, the command will show all known vulnerabilities."),
RepoPath: components.NewStringFlag(RepoPath, "Artifactory repository path, to enable Xray to determine violations accordingly. The command accepts this option only if the --project and --watches options are not provided. If none of the three options are provided, the command will show all known vulnerabilities."),
Licenses: components.NewBoolFlag(Licenses, "Set if you'd also like the list of licenses to be displayed."),
Sbom: components.NewBoolFlag(Sbom, fmt.Sprintf("For displaying the SBOM for this project, set to true. Relevant only with --%s flag. Ignored if provided 'format' is not 'table'.", Sca)),
OutputFormat: components.NewStringFlag(
OutputFormat,
"Defines the output format of the command. Acceptable values are: table, json, simple-json and sarif. Note: the json format doesn't include information about scans that are included as part of the Advanced Security package.",
Expand Down
12 changes: 12 additions & 0 deletions cli/scancommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ func ScanCmd(c *components.Context) error {
if err != nil {
return err
}
if c.GetBoolFlagValue(flags.Sbom) && format != outputFormat.Table {
log.Warn("The '--sbom' flag is only supported with the 'table' output format. Ignoring the flag.")
}
pluginsCommon.FixWinPathsForFileSystemSourcedCmds(specFile, c)
minSeverity, err := getMinimumSeverity(c)
if err != nil {
Expand All @@ -245,6 +248,7 @@ func ScanCmd(c *components.Context) error {
SetBaseRepoPath(repoPath).
SetIncludeVulnerabilities(c.GetBoolFlagValue(flags.Vuln) || shouldIncludeVulnerabilities(c)).
SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)).
SetIncludeSbom(c.GetBoolFlagValue(flags.Sbom)).
SetFail(c.GetBoolFlagValue(flags.Fail)).
SetPrintExtendedTable(c.GetBoolFlagValue(flags.ExtendedTable)).
SetBypassArchiveLimits(c.GetBoolFlagValue(flags.BypassArchiveLimits)).
Expand Down Expand Up @@ -393,6 +397,9 @@ func CreateAuditCmd(c *components.Context) (string, string, *coreConfig.ServerDe
if err != nil {
return "", "", nil, nil, err
}
if c.GetBoolFlagValue(flags.Sbom) && format != outputFormat.Table {
log.Warn("The '--sbom' flag is only supported with the 'table' output format. Ignoring the flag.")
}
minSeverity, err := getMinimumSeverity(c)
if err != nil {
return "", "", nil, nil, err
Expand All @@ -406,6 +413,7 @@ func CreateAuditCmd(c *components.Context) (string, string, *coreConfig.ServerDe
SetProject(getProject(c)).
SetIncludeVulnerabilities(c.GetBoolFlagValue(flags.Vuln)).
SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)).
SetIncludeSbom(c.GetBoolFlagValue(flags.Sbom)).
SetFail(c.GetBoolFlagValue(flags.Fail)).
SetPrintExtendedTable(c.GetBoolFlagValue(flags.ExtendedTable)).
SetMinSeverityFilter(minSeverity).
Expand Down Expand Up @@ -651,6 +659,9 @@ func DockerScan(c *components.Context, image string) error {
if err != nil {
return err
}
if c.GetBoolFlagValue(flags.Sbom) && format != outputFormat.Table {
log.Warn("The '--sbom' flag is only supported with the 'table' output format. Ignoring the flag.")
}
minSeverity, err := getMinimumSeverity(c)
if err != nil {
return err
Expand All @@ -664,6 +675,7 @@ func DockerScan(c *components.Context, image string) error {
SetBaseRepoPath(addTrailingSlashToRepoPathIfNeeded(c)).
SetIncludeVulnerabilities(c.GetBoolFlagValue(flags.Vuln) || shouldIncludeVulnerabilities(c)).
SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)).
SetIncludeSbom(c.GetBoolFlagValue(flags.Sbom)).
SetFail(c.GetBoolFlagValue(flags.Fail)).
SetPrintExtendedTable(c.GetBoolFlagValue(flags.ExtendedTable)).
SetBypassArchiveLimits(c.GetBoolFlagValue(flags.BypassArchiveLimits)).
Expand Down
10 changes: 9 additions & 1 deletion commands/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type AuditCommand struct {
targetRepoPath string
IncludeVulnerabilities bool
IncludeLicenses bool
IncludeSbom bool
Fail bool
PrintExtendedTable bool
Threads int
Expand Down Expand Up @@ -80,6 +81,11 @@ func (auditCmd *AuditCommand) SetIncludeLicenses(include bool) *AuditCommand {
return auditCmd
}

func (auditCmd *AuditCommand) SetIncludeSbom(include bool) *AuditCommand {
auditCmd.IncludeSbom = include
return auditCmd
}

func (auditCmd *AuditCommand) SetFail(fail bool) *AuditCommand {
auditCmd.Fail = fail
return auditCmd
Expand All @@ -96,13 +102,14 @@ func (auditCmd *AuditCommand) SetThreads(threads int) *AuditCommand {
}

// Create a results context based on the provided parameters. resolves conflicts between the parameters based on the retrieved platform watches.
func CreateAuditResultsContext(serverDetails *config.ServerDetails, xrayVersion string, watches []string, artifactoryRepoPath, projectKey, gitRepoHttpsCloneUrl string, includeVulnerabilities, includeLicenses bool) (context results.ResultContext) {
func CreateAuditResultsContext(serverDetails *config.ServerDetails, xrayVersion string, watches []string, artifactoryRepoPath, projectKey, gitRepoHttpsCloneUrl string, includeVulnerabilities, includeLicenses, includeSbom bool) (context results.ResultContext) {
context = results.ResultContext{
RepoPath: artifactoryRepoPath,
Watches: watches,
ProjectKey: projectKey,
IncludeVulnerabilities: shouldIncludeVulnerabilities(includeVulnerabilities, watches, artifactoryRepoPath, projectKey, ""),
IncludeLicenses: includeLicenses,
IncludeSbom: includeSbom,
}
if err := clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, services.MinXrayVersionGitRepoKey); err != nil {
// Git repo key is not supported by the Xray version.
Expand Down Expand Up @@ -173,6 +180,7 @@ func (auditCmd *AuditCommand) Run() (err error) {
auditCmd.gitRepoHttpsCloneUrl,
auditCmd.IncludeVulnerabilities,
auditCmd.IncludeLicenses,
auditCmd.IncludeSbom,
)).
SetThirdPartyApplicabilityScan(auditCmd.thirdPartyApplicabilityScan).
SetThreads(auditCmd.Threads).
Expand Down
9 changes: 8 additions & 1 deletion commands/audit/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,20 +666,24 @@ func TestCreateResultsContext(t *testing.T) {
jfrogProjectKey string
includeVulnerabilities bool
includeLicenses bool
includeSbom bool

expectedArtifactoryRepoPath string
expectedHttpCloneUrl string
expectedWatches []string
expectedJfrogProjectKey string
expectedIncludeVulnerabilities bool
expectedIncludeLicenses bool
expectedIncludeSbom bool
}{
{
name: "Only Vulnerabilities",
includeLicenses: true,
includeSbom: true,
// Since no violation context is provided, the includeVulnerabilities flag should be set to true even if not provided
expectedIncludeVulnerabilities: true,
expectedIncludeLicenses: true,
expectedIncludeSbom: true,
},
{
name: "Watches",
Expand Down Expand Up @@ -711,25 +715,28 @@ func TestCreateResultsContext(t *testing.T) {
jfrogProjectKey: mockProjectKey,
includeVulnerabilities: true,
includeLicenses: true,
includeSbom: true,

expectedHttpCloneUrl: testCaseExpectedGitRepoHttpsCloneUrl,
expectedWatches: mockWatches,
expectedJfrogProjectKey: mockProjectKey,
expectedIncludeVulnerabilities: true,
expectedIncludeLicenses: true,
expectedIncludeSbom: true,
},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("%s - %s", test.name, testCase.name), func(t *testing.T) {
mockServer, serverDetails := validations.XrayServer(t, validations.MockServerParams{XrayVersion: test.xrayVersion, ReturnMockPlatformWatches: test.expectedPlatformWatches})
defer mockServer.Close()
context := CreateAuditResultsContext(serverDetails, test.xrayVersion, testCase.watches, testCase.artifactoryRepoPath, testCase.jfrogProjectKey, testCase.httpCloneUrl, testCase.includeVulnerabilities, testCase.includeLicenses)
context := CreateAuditResultsContext(serverDetails, test.xrayVersion, testCase.watches, testCase.artifactoryRepoPath, testCase.jfrogProjectKey, testCase.httpCloneUrl, testCase.includeVulnerabilities, testCase.includeLicenses, testCase.includeSbom)
assert.Equal(t, testCase.expectedArtifactoryRepoPath, context.RepoPath)
assert.Equal(t, testCase.expectedHttpCloneUrl, context.GitRepoHttpsCloneUrl)
assert.Equal(t, testCase.expectedWatches, context.Watches)
assert.Equal(t, testCase.expectedJfrogProjectKey, context.ProjectKey)
assert.Equal(t, testCase.expectedIncludeVulnerabilities, context.IncludeVulnerabilities)
assert.Equal(t, testCase.expectedIncludeLicenses, context.IncludeLicenses)
assert.Equal(t, testCase.expectedIncludeSbom, context.IncludeSbom)
})
}
}
Expand Down
7 changes: 4 additions & 3 deletions commands/audit/scarunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"

"github.com/jfrog/jfrog-cli-security/commands/audit/sca/swift"

biutils "github.com/jfrog/build-info-go/utils"
Expand Down Expand Up @@ -107,10 +108,10 @@ func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner
auditParallelRunner.ScaScansWg.Add(1)
// defer auditParallelRunner.ScaScansWg.Done()
_, taskErr := auditParallelRunner.Runner.AddTaskWithError(executeScaScanTask(auditParallelRunner, serverDetails, auditParams, targetResult, treeResult), func(err error) {
_ = targetResult.AddTargetError(fmt.Errorf("Failed to execute SCA scan: %s", err.Error()), auditParams.AllowPartialResults())
_ = targetResult.AddTargetError(fmt.Errorf("failed to execute SCA scan: %s", err.Error()), auditParams.AllowPartialResults())
})
if taskErr != nil {
_ = targetResult.AddTargetError(fmt.Errorf("Failed to create SCA scan task: %s", taskErr.Error()), auditParams.AllowPartialResults())
_ = targetResult.AddTargetError(fmt.Errorf("failed to create SCA scan task: %s", taskErr.Error()), auditParams.AllowPartialResults())
auditParallelRunner.ScaScansWg.Done()
}
}
Expand All @@ -137,7 +138,7 @@ func executeScaScanTask(auditParallelRunner *utils.SecurityParallelRunner, serve
auditParallelRunner.ResultsMu.Lock()
defer auditParallelRunner.ResultsMu.Unlock()
// We add the results before checking for errors, so we can display the results even if an error occurred.
scan.NewScaScanResults(sca.GetScaScansStatusCode(xrayErr, scanResults...), scanResults...).IsMultipleRootProject = clientutils.Pointer(len(treeResult.FullDepTrees) > 1)
scan.NewScaScanResults(sca.GetScaScansStatusCode(xrayErr, scanResults...), results.DepTreeToSbom(treeResult.FullDepTrees), scanResults...).IsMultipleRootProject = clientutils.Pointer(len(treeResult.FullDepTrees) > 1)
addThirdPartyDependenciesToParams(auditParams, scan.Technology, treeResult.FlatTree, treeResult.FullDepTrees)

if xrayErr != nil {
Expand Down
2 changes: 1 addition & 1 deletion commands/enrich/enrich.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func (enrichCmd *EnrichCommand) createIndexerHandlerFunc(indexedFileProducer par
if err != nil {
return targetResults.AddTargetError(fmt.Errorf("%s failed to import graph: %s", logPrefix, err.Error()), false)
}
targetResults.NewScaScanResults(sca.GetScaScansStatusCode(err, *scanResults), *scanResults)
targetResults.NewScaScanResults(sca.GetScaScansStatusCode(err, *scanResults), results.Sbom{}, *scanResults)
targetResults.Technology = techutils.Technology(scanResults.ScannedPackageType)
return
}
Expand Down
4 changes: 3 additions & 1 deletion commands/git/audit/gitaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ func toAuditParams(params GitAuditParams) *sourceAudit.AuditParams {
params.resultsContext.ProjectKey,
params.source.GitRepoHttpsCloneUrl,
params.resultsContext.IncludeVulnerabilities,
params.resultsContext.IncludeLicenses)
params.resultsContext.IncludeLicenses,
false,
)
auditParams.SetResultsContext(resultContext)
log.Debug(fmt.Sprintf("Results context: %+v", resultContext))
// Scan params
Expand Down
2 changes: 1 addition & 1 deletion commands/scan/buildscan.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS
SetResultsContext(results.ResultContext{ProjectKey: params.Project, IncludeVulnerabilities: bsc.includeVulnerabilities})

scanResults := cmdResults.NewScanResults(results.ScanTarget{Name: fmt.Sprintf("%s (%s)", params.BuildName, params.BuildNumber)})
scanResults.NewScaScanResults(0, services.ScanResponse{
scanResults.NewScaScanResults(0, results.Sbom{}, services.ScanResponse{
Violations: buildScanResults.Violations,
Vulnerabilities: buildScanResults.Vulnerabilities,
XrayDataUrl: buildScanResults.MoreDetailsUrl,
Expand Down
7 changes: 6 additions & 1 deletion commands/scan/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ func (scanCmd *ScanCommand) SetIncludeLicenses(include bool) *ScanCommand {
return scanCmd
}

func (scanCmd *ScanCommand) SetIncludeSbom(include bool) *ScanCommand {
scanCmd.resultsContext.IncludeSbom = include
return scanCmd
}

func (scanCmd *ScanCommand) ServerDetails() (*config.ServerDetails, error) {
return scanCmd.serverDetails, nil
}
Expand Down Expand Up @@ -459,7 +464,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, cmdResults
if err != nil {
return targetResults.AddTargetError(fmt.Errorf("%s sca scanning '%s' failed with error: %s", scanLogPrefix, graph.Id, err.Error()), false)
} else {
targetResults.NewScaScanResults(sca.GetScaScansStatusCode(err, *graphScanResults), *graphScanResults)
targetResults.NewScaScanResults(sca.GetScaScansStatusCode(err, *graphScanResults), results.CompTreeToSbom(graph), *graphScanResults)
targetResults.Technology = techutils.Technology(graphScanResults.ScannedPackageType)
}
if !cmdResults.EntitledForJas {
Expand Down
2 changes: 1 addition & 1 deletion jas/runner/jasrunner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestJasRunner(t *testing.T) {
jasScanner, err := jas.CreateJasScanner(&jas.FakeServerDetails, false, "", jas.GetAnalyzerManagerXscEnvVars("", "", "", []string{}, targetResults.GetTechnologies()...))
assert.NoError(t, err)

targetResults.NewScaScanResults(0, jas.FakeBasicXrayResults[0])
targetResults.NewScaScanResults(0, results.Sbom{}, jas.FakeBasicXrayResults[0])
testParams := JasRunnerParams{
Runner: securityParallelRunnerForTest,
Scanner: jasScanner,
Expand Down
11 changes: 11 additions & 0 deletions utils/formats/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package formats
type ResultsTables struct {
// Licenses
LicensesTable []licenseTableRow
// SBOM (Software Bill of Materials)
SbomTable []SbomTableRow
// Sca tables
SecurityVulnerabilitiesTable []scaVulnerabilityOrViolationTableRow
SecurityViolationsTable []scaVulnerabilityOrViolationTableRow
Expand Down Expand Up @@ -54,6 +56,15 @@ type vulnerabilityScanTableRow struct {
issueId string `col-name:"Issue ID" extended:"true"`
}

type SbomTableRow struct {
Component string `col-name:"Component"`
Version string `col-name:"Version"`
PackageType string `col-name:"Type"`
Relation string `col-name:"Relation"`
// For sorting
Direct bool
}

type licenseTableRow struct {
licenseKey string `col-name:"License"`
directDependencies []directDependenciesTableRow `embed-table:"true"`
Expand Down
Loading

0 comments on commit ccd59bf

Please sign in to comment.