Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SBOM ingestion #81

Merged
merged 38 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
df99a18
sbom ingestion
barv-jfrog Jun 2, 2024
56cb83f
sbom ingestion
barv-jfrog Jun 6, 2024
97ffa7d
merge dev
barv-jfrog Jun 9, 2024
0eac481
merge dev
barv-jfrog Jun 10, 2024
43b9137
merge dev
barv-jfrog Jun 10, 2024
2e580fe
merge dev
barv-jfrog Jun 10, 2024
c025dd1
merge dev
barv-jfrog Jun 10, 2024
311877e
merge dev
barv-jfrog Jun 10, 2024
7665c86
merge dev
barv-jfrog Jun 10, 2024
5e4a141
merge dev
barv-jfrog Jun 10, 2024
e399bb6
merge dev
barv-jfrog Jun 10, 2024
2234fc3
merge dev
barv-jfrog Jun 10, 2024
5d0bda8
tests & fixes
barv-jfrog Jun 13, 2024
c5c03bc
tests & fixes
barv-jfrog Jun 13, 2024
82d85d6
tests & fixes
barv-jfrog Jun 13, 2024
8c4fd8f
tests & fixes
barv-jfrog Jun 13, 2024
0592577
tests & fixes
barv-jfrog Jun 17, 2024
97cc333
tests & fixes
barv-jfrog Jun 17, 2024
a2f1679
tests & fixes
barv-jfrog Jun 26, 2024
b90273d
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-security int…
barv-jfrog Jun 26, 2024
0a91aa9
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-security int…
barv-jfrog Jun 27, 2024
0705cee
tests & fixes
barv-jfrog Jun 27, 2024
b7e6fcb
tests & fixes
barv-jfrog Jul 4, 2024
8d89133
tests & fixes
barv-jfrog Jul 4, 2024
fce8692
tests & fixes
barv-jfrog Jul 7, 2024
d06b5e8
tests & fixes
barv-jfrog Jul 7, 2024
548dc2a
tests & fixes
barv-jfrog Jul 7, 2024
70e6f6b
tests & fixes
barv-jfrog Jul 7, 2024
5784b16
Merge branch 'dev' into sbom-ingestion
barv-jfrog Jul 10, 2024
8c2f08e
tests & fixes
barv-jfrog Jul 10, 2024
da8170a
Merge remote-tracking branch 'origin/sbom-ingestion' into sbom-ingestion
barv-jfrog Jul 10, 2024
e61129c
tests & fixes
barv-jfrog Jul 11, 2024
164aec5
tests & fixes
barv-jfrog Jul 15, 2024
84e0fd8
tests & fixes
barv-jfrog Jul 15, 2024
cabfafd
tests & fixes
barv-jfrog Jul 15, 2024
9aa29df
Merge branch 'dev' into sbom-ingestion
barv-jfrog Jul 15, 2024
1d9fb23
tests & fixes
barv-jfrog Jul 15, 2024
e0a3c34
Merge remote-tracking branch 'origin/sbom-ingestion' into sbom-ingestion
barv-jfrog Jul 15, 2024
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
13 changes: 13 additions & 0 deletions cli/docs/enrich/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package enrich

import (
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
)

func GetDescription() string {
return "Enrich sbom format JSON located on the local file-system with Xray."
}

func GetArguments() []components.Argument {
return []components.Argument{{Name: "Specific file path", Description: `Specifies the local file system path of the JSON to be scanned.`}}
barv-jfrog marked this conversation as resolved.
Show resolved Hide resolved
}
4 changes: 4 additions & 0 deletions cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
Audit = "audit"
CurationAudit = "curation-audit"
GitCountContributors = "count-contributors"
Enrich = "sbom-enrich"

// TODO: Deprecated commands (remove at next CLI major version)
AuditMvn = "audit-maven"
Expand Down Expand Up @@ -134,6 +135,9 @@ var commandFlags = map[string][]string{
url, user, password, accessToken, ServerId, SpecFlag, Threads, scanRecursive, scanRegexp, scanAnt,
Project, Watches, RepoPath, Licenses, OutputFormat, Fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly,
},
Enrich: {
url, user, password, accessToken, ServerId, SpecFlag, Threads,
barv-jfrog marked this conversation as resolved.
Show resolved Hide resolved
},
barv-jfrog marked this conversation as resolved.
Show resolved Hide resolved
BuildScan: {
url, user, password, accessToken, ServerId, Project, Vuln, OutputFormat, Fail, ExtendedTable, Rescan,
},
Expand Down
36 changes: 36 additions & 0 deletions cli/scancommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cli

import (
"fmt"
enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich"
"github.com/jfrog/jfrog-cli-security/commands/enrich"
"os"
"strings"

Expand Down Expand Up @@ -51,6 +53,14 @@ func getAuditAndScansCommands() []components.Command {
Category: auditScanCategory,
Action: ScanCmd,
},
{
Name: "sbom-enrich",
Aliases: []string{"se"},
Flags: flags.GetCommandFlags(flags.Enrich),
Description: enrichDocs.GetDescription(),
Arguments: enrichDocs.GetArguments(),
Action: EnrichCmd,
},
{
Name: "build-scan",
Aliases: []string{"bs"},
Expand Down Expand Up @@ -154,6 +164,32 @@ func getAuditAndScansCommands() []components.Command {
}
}

func EnrichCmd(c *components.Context) error {
if len(c.Arguments) == 0 {
return pluginsCommon.PrintHelpAndReturnError("providing a file path argument is mandatory", c)
}
serverDetails, err := createServerDetailsWithConfigOffer(c)
if err != nil {
return err
}
if err = validateXrayContext(c, serverDetails); err != nil {
return err
}
specFile := createDefaultScanSpec(c, addTrailingSlashToRepoPathIfNeeded(c))
if err = spec.ValidateSpec(specFile.Files, false, false); err != nil {
return err
}
threads, err := pluginsCommon.GetThreadsCount(c)
if err != nil {
return err
}
EnrichCmd := enrich.NewEnrichCommand().
SetServerDetails(serverDetails).
SetThreads(threads).
SetSpec(specFile)
return commandsCommon.Exec(EnrichCmd)
}

func ScanCmd(c *components.Context) error {
if len(c.Arguments) == 0 && !c.IsFlagSet(flags.SpecFlag) {
return pluginsCommon.PrintHelpAndReturnError("providing either a <source pattern> argument or the 'spec' option is mandatory", c)
Expand Down
275 changes: 275 additions & 0 deletions commands/enrich/enrich.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package enrich

import (
"encoding/xml"
"errors"
"github.com/jfrog/gofrog/parallel"
"github.com/jfrog/jfrog-cli-core/v2/common/spec"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-security/commands/enrich/enrichgraph"
"github.com/jfrog/jfrog-cli-security/formats"
"github.com/jfrog/jfrog-cli-security/utils"
xrutils "github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/xray"
"github.com/jfrog/jfrog-client-go/artifactory/services/fspatterns"
clientutils "github.com/jfrog/jfrog-client-go/utils"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
ioUtils "github.com/jfrog/jfrog-client-go/utils/io"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
"github.com/jfrog/jfrog-client-go/utils/log"
"github.com/jfrog/jfrog-client-go/xray/services"
"os"
"os/exec"
)

type FileContext func(string) parallel.TaskFunc
type indexFileHandlerFunc func(file string)

type ScanInfo struct {
Target string
Result *services.ScanResponse
}

type EnrichCommand struct {
serverDetails *config.ServerDetails
spec *spec.SpecFiles
threads int
progress ioUtils.ProgressMgr
}

func (enrichCmd *EnrichCommand) SetProgress(progress ioUtils.ProgressMgr) {
enrichCmd.progress = progress
}

func (enrichCmd *EnrichCommand) SetThreads(threads int) *EnrichCommand {
enrichCmd.threads = threads
return enrichCmd
}

func (enrichCmd *EnrichCommand) SetServerDetails(server *config.ServerDetails) *EnrichCommand {
enrichCmd.serverDetails = server
return enrichCmd
}

func (enrichCmd *EnrichCommand) SetSpec(spec *spec.SpecFiles) *EnrichCommand {
enrichCmd.spec = spec
return enrichCmd
}

func (enrichCmd *EnrichCommand) ServerDetails() (*config.ServerDetails, error) {
return enrichCmd.serverDetails, nil
}

func isXML(scaResults []*utils.ScaScanResult) (bool, error) {
if len(scaResults) == 0 {
return false, errors.New("unable to retrieve file")
}
fileName := scaResults[0].Target
var x interface{}
content, err := os.ReadFile(fileName)
if err != nil {
return false, err
}
return xml.Unmarshal(content, &x) == nil, nil
}

func (enrichCmd *EnrichCommand) Run() (err error) {
defer func() {
if err != nil {
var e *exec.ExitError
if errors.As(err, &e) {
if e.ExitCode() != coreutils.ExitCodeVulnerableBuild.Code {
err = errors.New("Enrich command failed. " + err.Error())
}
}
}
}()
_, xrayVersion, err := xray.CreateXrayServiceManagerAndGetVersion(enrichCmd.serverDetails)
if err != nil {
return err
}

// Validate Xray minimum version for graph scan command
err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, enrichgraph.EnrichMinimumVersionXray)
if err != nil {
return err
}

log.Info("JFrog Xray version is:", xrayVersion)

threads := 1
if enrichCmd.threads > 1 {
threads = enrichCmd.threads
}

// resultsArr is a two-dimensional array. Each array in it contains a list of ScanResponses that were requested and collected by a specific thread.
resultsArr := make([][]*ScanInfo, threads)
fileProducerConsumer := parallel.NewRunner(enrichCmd.threads, 20000, false)
fileProducerErrors := make([][]formats.SimpleJsonError, threads)
indexedFileProducerConsumer := parallel.NewRunner(enrichCmd.threads, 20000, false)
indexedFileProducerErrors := make([][]formats.SimpleJsonError, threads)
fileCollectingErrorsQueue := clientutils.NewErrorsQueue(1)
// Start walking on the filesystem to "produce" files that match the given pattern
// while the consumer uses the indexer to index those files.
enrichCmd.prepareScanTasks(fileProducerConsumer, indexedFileProducerConsumer, resultsArr, indexedFileProducerErrors, fileCollectingErrorsQueue, xrayVersion)
enrichCmd.performScanTasks(fileProducerConsumer, indexedFileProducerConsumer)

// Handle results
var flatResults []*xrutils.ScaScanResult
for _, arr := range resultsArr {
for _, res := range arr {
flatResults = append(flatResults, &xrutils.ScaScanResult{Target: res.Target, XrayResults: []services.ScanResponse{*res.Result}})
}
}
if enrichCmd.progress != nil {
if err = enrichCmd.progress.Quit(); err != nil {
return err
}

}

fileCollectingErr := fileCollectingErrorsQueue.GetError()
var scanErrors []formats.SimpleJsonError
if fileCollectingErr != nil {
scanErrors = append(scanErrors, formats.SimpleJsonError{ErrorMessage: fileCollectingErr.Error()})
}
scanErrors = appendErrorSlice(scanErrors, fileProducerErrors)
scanErrors = appendErrorSlice(scanErrors, indexedFileProducerErrors)

scanResults := xrutils.NewAuditResults()
scanResults.XrayVersion = xrayVersion
scanResults.ScaResults = flatResults

isxml, err := isXML(scanResults.ScaResults)
if err != nil {
return
}
if isxml {
if err = utils.AppendVulnsToXML(scanResults); err != nil {
return
}
} else {
if err = utils.AppendVulnsToJson(scanResults); err != nil {
return
}
}

if err != nil {
return err
}

if len(scanErrors) > 0 {
return errorutils.CheckErrorf(scanErrors[0].ErrorMessage)
}
log.Info("Enrich process completed successfully.")
return nil
}

func NewEnrichCommand() *EnrichCommand {
return &EnrichCommand{}
}

func (enrichCmd *EnrichCommand) CommandName() string {
return "xr_enrich"
}

func (enrichCmd *EnrichCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, resultsArr [][]*ScanInfo, indexedFileErrors [][]formats.SimpleJsonError, fileCollectingErrorsQueue *clientutils.ErrorsQueue, xrayVersion string) {
go func() {
defer fileProducer.Done()
// Iterate over file-spec groups and produce indexing tasks.
// When encountering an error, log and move to next group.
specFiles := enrichCmd.spec.Files
artifactHandlerFunc := enrichCmd.createIndexerHandlerFunc(indexedFileProducer, resultsArr, indexedFileErrors, xrayVersion)
taskHandler := getAddTaskToProducerFunc(fileProducer, artifactHandlerFunc)

err := FileForEnriching(specFiles[0], taskHandler)
if err != nil {
log.Error(err)
fileCollectingErrorsQueue.AddError(err)
}
}()
}

func (enrichCmd *EnrichCommand) createIndexerHandlerFunc(indexedFileProducer parallel.Runner, resultsArr [][]*ScanInfo, indexedFileErrors [][]formats.SimpleJsonError, xrayVersion string) FileContext {
return func(filePath string) parallel.TaskFunc {
return func(threadId int) (err error) {
// Add a new task to the second producer/consumer
// which will send the indexed binary to Xray and then will store the received result.
taskFunc := func(threadId int) (err error) {
fileContent, err := os.ReadFile(filePath)
if err != nil {
return err
}
params := &services.XrayGraphImportParams{
SBOMInput: fileContent,
ScanType: services.Binary,
}
importGraphParams := enrichgraph.NewEnrichGraphParams().
SetServerDetails(enrichCmd.serverDetails).
SetXrayGraphScanParams(params).
SetXrayVersion(xrayVersion)
xrayManager, err := xray.CreateXrayServiceManager(importGraphParams.ServerDetails())
if err != nil {
return err
}
scanResults, err := enrichgraph.RunImportGraphAndGetResults(importGraphParams, xrayManager)
if err != nil {
indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()})
return
}
resultsArr[threadId] = append(resultsArr[threadId], &ScanInfo{Target: filePath, Result: scanResults})
return
}

_, _ = indexedFileProducer.AddTask(taskFunc)
return
}
}
}

func getAddTaskToProducerFunc(producer parallel.Runner, fileHandlerFunc FileContext) indexFileHandlerFunc {
return func(filePath string) {
taskFunc := fileHandlerFunc(filePath)
_, _ = producer.AddTask(taskFunc)
}
}

func (enrichCmd *EnrichCommand) performScanTasks(fileConsumer parallel.Runner, indexedFileConsumer parallel.Runner) {
go func() {
// Blocking until consuming is finished.
fileConsumer.Run()
// After all files have been indexed, The second producer notifies that no more tasks will be produced.
indexedFileConsumer.Done()
}()
// Blocking until consuming is finished.
indexedFileConsumer.Run()
}

func FileForEnriching(fileData spec.File, dataHandlerFunc indexFileHandlerFunc) error {
fileData.Pattern = clientutils.ReplaceTildeWithUserHome(fileData.Pattern)
patternType := fileData.GetPatternType()
rootPath, err := fspatterns.GetRootPath(fileData.Pattern, fileData.Target, "", patternType, false)
if err != nil {
return err
}

isDir, err := fileutils.IsDirExists(rootPath, false)
if err != nil {
return err
}

// path should be a single file
if !isDir {
dataHandlerFunc(rootPath)
return nil
}
return errors.New("directory instead of a single file")
}

func appendErrorSlice(scanErrors []formats.SimpleJsonError, errorsToAdd [][]formats.SimpleJsonError) []formats.SimpleJsonError {
for _, errorSlice := range errorsToAdd {
scanErrors = append(scanErrors, errorSlice...)
}
return scanErrors
}
23 changes: 23 additions & 0 deletions commands/enrich/enrichgraph/enrichgraph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package enrichgraph

import (
"github.com/jfrog/jfrog-client-go/xray"
"github.com/jfrog/jfrog-client-go/xray/services"
)

const (
EnrichMinimumVersionXray = "3.90"
barv-jfrog marked this conversation as resolved.
Show resolved Hide resolved
)

func RunImportGraphAndGetResults(params *EnrichGraphParams, xrayManager *xray.XrayServicesManager) (*services.ScanResponse, error) {
scanId, err := xrayManager.ImportGraph(*params.xrayGraphImportParams)
if err != nil {
return nil, err
}

scanResult, err := xrayManager.GetImportGraphResults(scanId)
if err != nil {
return nil, err
}
return scanResult, nil
}
Loading
Loading