diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 531f18e4..016ff212 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -134,12 +134,12 @@ func (auditCmd *AuditCommand) Run() (err error) { } if err = utils.NewResultsWriter(auditResults). SetIsMultipleRootProject(auditResults.IsMultipleProject()). + SetHasViolationContext(auditCmd.HasViolationContext()). SetIncludeVulnerabilities(auditCmd.IncludeVulnerabilities). SetIncludeLicenses(auditCmd.IncludeLicenses). SetOutputFormat(auditCmd.OutputFormat()). SetPrintExtendedTable(auditCmd.PrintExtendedTable). SetExtraMessages(messages). - SetScanType(services.Dependency). SetSubScansPreformed(auditCmd.ScansToPerform()). PrintScanResults(); err != nil { return @@ -160,12 +160,16 @@ func (auditCmd *AuditCommand) CommandName() string { return "generic_audit" } +func (auditCmd *AuditCommand) HasViolationContext() bool { + return len(auditCmd.watches) > 0 || auditCmd.projectKey != "" || auditCmd.targetRepoPath != "" +} + // Runs an audit scan based on the provided auditParams. // Returns an audit Results object containing all the scan results. // If the current server is entitled for JAS, the advanced security results will be included in the scan results. func RunAudit(auditParams *AuditParams) (results *utils.Results, err error) { // Initialize Results struct - results = utils.NewAuditResults() + results = utils.NewAuditResults(utils.SourceCode) serverDetails, err := auditParams.ServerDetails() if err != nil { return @@ -252,7 +256,7 @@ func downloadAnalyzerManagerAndRunScanners(auditParallelRunner *utils.SecurityPa if err != nil { return fmt.Errorf("failed to create jas scanner: %s", err.Error()) } - if err = runner.AddJasScannersTasks(auditParallelRunner, scanResults, auditParams.DirectDependencies(), serverDetails, auditParams.thirdPartyApplicabilityScan, scanner, applicability.ApplicabilityScannerType, secrets.SecretsScannerType, auditParallelRunner.AddErrorToChan, auditParams.ScansToPerform()); err != nil { + if err = runner.AddJasScannersTasks(auditParallelRunner, scanResults, auditParams.DirectDependencies(), serverDetails, auditParams.thirdPartyApplicabilityScan, scanner, applicability.ApplicabilityScannerType, secrets.SecretsScannerType, auditParallelRunner.AddErrorToChan, auditParams.ScansToPerform(), auditParams.configProfile); err != nil { return fmt.Errorf("%s failed to run JAS scanners: %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) } return diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go new file mode 100644 index 00000000..1a1b0acc --- /dev/null +++ b/commands/audit/audit_test.go @@ -0,0 +1,151 @@ +package audit + +import ( + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/format" + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" + clientTests "github.com/jfrog/jfrog-client-go/utils/tests" + "github.com/jfrog/jfrog-client-go/xsc/services" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +// Note: Currently, if a config profile is provided, the scan will use the profile's settings, IGNORING jfrog-apps-config if exists. +func TestAuditWithConfigProfile(t *testing.T) { + testcases := []struct { + name string + configProfile services.ConfigProfile + expectedSastIssues int + expectedSecretsIssues int + }{ + { + name: "Enable only secrets scanner", + configProfile: services.ConfigProfile{ + ProfileName: "only-secrets", + Modules: []services.Module{{ + ModuleId: 1, + ModuleName: "only-secrets-module", + PathFromRoot: ".", + ScanConfig: services.ScanConfig{ + SastScannerConfig: services.SastScannerConfig{ + EnableSastScan: false, + }, + SecretsScannerConfig: services.SecretsScannerConfig{ + EnableSecretsScan: true, + }, + }, + }}, + IsDefault: false, + }, + expectedSastIssues: 0, + expectedSecretsIssues: 7, + }, + { + name: "Enable only sast scanner", + configProfile: services.ConfigProfile{ + ProfileName: "only-sast", + Modules: []services.Module{{ + ModuleId: 1, + ModuleName: "only-sast-module", + PathFromRoot: ".", + ScanConfig: services.ScanConfig{ + SastScannerConfig: services.SastScannerConfig{ + EnableSastScan: true, + }, + SecretsScannerConfig: services.SecretsScannerConfig{ + EnableSecretsScan: false, + }, + }, + }}, + IsDefault: false, + }, + expectedSastIssues: 1, + expectedSecretsIssues: 0, + }, + { + name: "Enable secrets and sast", + configProfile: services.ConfigProfile{ + ProfileName: "secrets&sast", + Modules: []services.Module{{ + ModuleId: 1, + ModuleName: "secrets&sast-module", + PathFromRoot: ".", + ScanConfig: services.ScanConfig{ + SastScannerConfig: services.SastScannerConfig{ + EnableSastScan: true, + }, + SecretsScannerConfig: services.SecretsScannerConfig{ + EnableSecretsScan: true, + }, + }, + }}, + IsDefault: false, + }, + expectedSastIssues: 1, + expectedSecretsIssues: 7, + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + mockServer, serverDetails := utils.XrayServer(t, utils.EntitlementsMinVersion) + defer mockServer.Close() + + auditBasicParams := (&utils.AuditBasicParams{}). + SetServerDetails(serverDetails). + SetOutputFormat(format.Table). + SetUseJas(true) + + configProfile := testcase.configProfile + auditParams := NewAuditParams(). + SetGraphBasicParams(auditBasicParams). + SetConfigProfile(&configProfile). + SetCommonGraphScanParams(&scangraph.CommonGraphScanParams{ + RepoPath: "", + ProjectKey: "", + Watches: nil, + ScanType: "dependency", + IncludeVulnerabilities: true, + XscVersion: services.ConfigProfileMinXscVersion, + MultiScanId: "random-msi", + }) + auditParams.SetIsRecursiveScan(true) + + tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + testDirPath := filepath.Join("..", "..", "tests", "testdata", "projects", "jas", "jas") + assert.NoError(t, biutils.CopyDir(testDirPath, tempDirPath, true, nil)) + + baseWd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTests.ChangeDirWithCallback(t, baseWd, tempDirPath) + defer chdirCallback() + + auditResults, err := RunAudit(auditParams) + assert.NoError(t, err) + + // Currently, the only supported scanners are Secrets and Sast, therefore if a config profile is utilized - all other scanners are disabled. + if testcase.expectedSastIssues > 0 { + assert.NotNil(t, auditResults.ExtendedScanResults.SastScanResults) + assert.Equal(t, testcase.expectedSastIssues, len(auditResults.ExtendedScanResults.SastScanResults[0].Results)) + } else { + assert.Nil(t, auditResults.ExtendedScanResults.SastScanResults) + } + + if testcase.expectedSecretsIssues > 0 { + assert.NotNil(t, auditResults.ExtendedScanResults.SecretsScanResults) + assert.Equal(t, testcase.expectedSecretsIssues, len(auditResults.ExtendedScanResults.SecretsScanResults[0].Results)) + } else { + assert.Nil(t, auditResults.ExtendedScanResults.SecretsScanResults) + } + + assert.Nil(t, auditResults.ScaResults) + assert.Nil(t, auditResults.ExtendedScanResults.ApplicabilityScanResults) + assert.Nil(t, auditResults.ExtendedScanResults.IacScanResults) + }) + } +} diff --git a/commands/audit/auditparams.go b/commands/audit/auditparams.go index edc32174..474bf9b3 100644 --- a/commands/audit/auditparams.go +++ b/commands/audit/auditparams.go @@ -5,6 +5,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" "github.com/jfrog/jfrog-client-go/xray/services" + clientservices "github.com/jfrog/jfrog-client-go/xsc/services" ) type AuditParams struct { @@ -19,6 +20,7 @@ type AuditParams struct { // Include third party dependencies source code in the applicability scan. thirdPartyApplicabilityScan bool threads int + configProfile *clientservices.ConfigProfile } func NewAuditParams() *AuditParams { @@ -92,6 +94,11 @@ func (params *AuditParams) SetCommonGraphScanParams(commonParams *scangraph.Comm return params } +func (params *AuditParams) SetConfigProfile(configProfile *clientservices.ConfigProfile) *AuditParams { + params.configProfile = configProfile + return params +} + func (params *AuditParams) createXrayGraphScanParams() *services.XrayGraphScanParams { return &services.XrayGraphScanParams{ RepoPath: params.commonGraphScanParams.RepoPath, diff --git a/commands/audit/scarunner.go b/commands/audit/scarunner.go index e8d47507..14d203d6 100644 --- a/commands/audit/scarunner.go +++ b/commands/audit/scarunner.go @@ -42,6 +42,11 @@ func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner log.Debug("Skipping SCA scan as requested by input...") return } + if auditParams.configProfile != nil { + log.Debug("Skipping SCA scan as a configuration profile is being utilized and currently only Secrets and Sast scanners are supported when utilizing a configuration profile") + return + } + // Prepare currentWorkingDir, err := os.Getwd() if errorutils.CheckError(err) != nil { diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index 65b988a6..dd63b6c8 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -4,6 +4,14 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "sync" + "golang.org/x/exp/maps" "github.com/jfrog/gofrog/datastructures" @@ -28,13 +36,6 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" xrayClient "github.com/jfrog/jfrog-client-go/xray" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" - "net/http" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - "sync" ) const ( diff --git a/commands/curation/curationaudit_test.go b/commands/curation/curationaudit_test.go index a61a5b84..05f29595 100644 --- a/commands/curation/curationaudit_test.go +++ b/commands/curation/curationaudit_test.go @@ -979,7 +979,14 @@ func Test_convertResultsToSummary(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.ElementsMatch(t, tt.expected.Scans, convertResultsToSummary(tt.input).Scans) + summary := convertResultsToSummary(tt.input) + // Sort Blocked base on count (low first) to make the test deterministic + for _, scan := range summary.Scans { + sort.Slice(scan.CuratedPackages.Blocked, func(i, j int) bool { + return len(scan.CuratedPackages.Blocked[i].Packages) < len(scan.CuratedPackages.Blocked[j].Packages) + }) + } + assert.Equal(t, tt.expected, summary) }) } } diff --git a/commands/enrich/enrich.go b/commands/enrich/enrich.go index a2137cc2..badbe2df 100644 --- a/commands/enrich/enrich.go +++ b/commands/enrich/enrich.go @@ -190,7 +190,7 @@ func (enrichCmd *EnrichCommand) Run() (err error) { scanErrors = appendErrorSlice(scanErrors, fileProducerErrors) scanErrors = appendErrorSlice(scanErrors, indexedFileProducerErrors) - scanResults := xrutils.NewAuditResults() + scanResults := xrutils.NewAuditResults(utils.SBOM) scanResults.XrayVersion = xrayVersion scanResults.ScaResults = flatResults diff --git a/commands/scan/buildscan.go b/commands/scan/buildscan.go index 40149491..a1a59148 100644 --- a/commands/scan/buildscan.go +++ b/commands/scan/buildscan.go @@ -149,17 +149,17 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS XrayDataUrl: buildScanResults.MoreDetailsUrl, }} - scanResults := utils.NewAuditResults() + scanResults := utils.NewAuditResults(utils.Build) scanResults.XrayVersion = xrayVersion scanResults.ScaResults = []*utils.ScaScanResult{{Target: fmt.Sprintf("%s (%s)", params.BuildName, params.BuildNumber), XrayResults: scanResponse}} resultsPrinter := utils.NewResultsWriter(scanResults). SetOutputFormat(bsc.outputFormat). + SetHasViolationContext(bsc.hasViolationContext()). SetIncludeVulnerabilities(bsc.includeVulnerabilities). SetIncludeLicenses(false). SetIsMultipleRootProject(true). SetPrintExtendedTable(bsc.printExtendedTable). - SetScanType(services.Binary). SetExtraMessages(nil) if bsc.outputFormat != outputFormat.Table { @@ -187,7 +187,7 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS scanResults, bsc.serverDetails, bsc.includeVulnerabilities, - bsc.buildConfiguration.GetProject() != "", + bsc.hasViolationContext(), params.BuildName, params.BuildNumber, )) return @@ -197,6 +197,10 @@ func (bsc *BuildScanCommand) CommandName() string { return "xr_build_scan" } +func (bsc *BuildScanCommand) hasViolationContext() bool { + return bsc.buildConfiguration.GetProject() != "" +} + // There are two cases. when serverDetails.Url is configured and when serverDetails.XrayUrl and serverDetails.ArtifactoryUrl are configured // The function will return the Url if configured and will trim xray if serverDetails.Url is not configured func getActualUrl(serverDetails config.ServerDetails) (string, error) { diff --git a/commands/scan/dockerscan.go b/commands/scan/dockerscan.go index 1ba036e4..dfb9c59c 100644 --- a/commands/scan/dockerscan.go +++ b/commands/scan/dockerscan.go @@ -96,17 +96,24 @@ func (dsc *DockerScanCommand) Run() (err error) { err = errorutils.CheckError(e) } }() - return dsc.ScanCommand.RunAndRecordResults(func(scanResults *utils.Results) (err error) { + return dsc.ScanCommand.RunAndRecordResults(utils.DockerImage, func(scanResults *utils.Results) (err error) { if scanResults == nil { return } + if scanResults.ScaResults != nil { + for _, result := range scanResults.ScaResults { + result.Name = dsc.imageTag + } + } dsc.analyticsMetricsService.UpdateGeneralEvent(dsc.analyticsMetricsService.CreateXscAnalyticsGeneralEventFinalizeFromAuditResults(scanResults)) - + if err = utils.RecordSarifOutput(scanResults); err != nil { + return + } return utils.RecordSecurityCommandSummary(utils.NewDockerScanSummary( scanResults, dsc.ScanCommand.serverDetails, dsc.ScanCommand.includeVulnerabilities, - hasViolationContext(dsc.ScanCommand.watches, dsc.ScanCommand.projectKey), + dsc.ScanCommand.hasViolationContext(), dsc.imageTag, )) }) diff --git a/commands/scan/scan.go b/commands/scan/scan.go index 6f0ba747..a3d913f5 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -161,6 +161,10 @@ func (scanCmd *ScanCommand) SetAnalyticsMetricsService(analyticsMetricsService * return scanCmd } +func (scanCmd *ScanCommand) hasViolationContext() bool { + return len(scanCmd.watches) > 0 || scanCmd.projectKey != "" +} + func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.BinaryGraphNode, error) { var indexerResults xrayUtils.BinaryGraphNode indexerCmd := exec.Command(scanCmd.indexerPath, indexingCommand, filePath, "--temp-dir", scanCmd.indexerTempDir) @@ -190,21 +194,20 @@ func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.BinaryGraphNo } func (scanCmd *ScanCommand) Run() (err error) { - return scanCmd.RunAndRecordResults(func(scanResults *utils.Results) error { + return scanCmd.RunAndRecordResults(utils.Binary, func(scanResults *utils.Results) (err error) { + if err = utils.RecordSarifOutput(scanResults); err != nil { + return + } return utils.RecordSecurityCommandSummary(utils.NewBinaryScanSummary( scanResults, scanCmd.serverDetails, scanCmd.includeVulnerabilities, - hasViolationContext(scanCmd.watches, scanCmd.projectKey), + scanCmd.hasViolationContext(), )) }) } -func hasViolationContext(watches []string, projectKey string) bool { - return len(watches) > 0 || projectKey != "" -} - -func (scanCmd *ScanCommand) RunAndRecordResults(recordResFunc func(scanResults *utils.Results) error) (err error) { +func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recordResFunc func(scanResults *utils.Results) error) (err error) { defer func() { if err != nil { var e *exec.ExitError @@ -220,7 +223,7 @@ func (scanCmd *ScanCommand) RunAndRecordResults(recordResFunc func(scanResults * return err } - scanResults := utils.NewAuditResults() + scanResults := utils.NewAuditResults(cmdType) scanResults.XrayVersion = xrayVersion if scanCmd.analyticsMetricsService != nil { scanResults.MultiScanId = scanCmd.analyticsMetricsService.GetMsi() @@ -318,19 +321,15 @@ func (scanCmd *ScanCommand) RunAndRecordResults(recordResFunc func(scanResults * if err = utils.NewResultsWriter(scanResults). SetOutputFormat(scanCmd.outputFormat). + SetHasViolationContext(scanCmd.hasViolationContext()). SetIncludeVulnerabilities(scanCmd.includeVulnerabilities). SetIncludeLicenses(scanCmd.includeLicenses). SetPrintExtendedTable(scanCmd.printExtendedTable). SetIsMultipleRootProject(scanResults.IsMultipleProject()). - SetScanType(services.Binary). PrintScanResults(); err != nil { return } - if err != nil { - return err - } - if err = recordResFunc(scanResults); err != nil { return err } @@ -451,7 +450,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, entitledFo log.Error(fmt.Sprintf("failed to create jas scanner: %s", err.Error())) indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) } - err = runner.AddJasScannersTasks(jasFileProducerConsumer, &scanResults, &depsList, scanCmd.serverDetails, false, scanner, applicability.ApplicabilityDockerScanScanType, secrets.SecretsScannerDockerScanType, jasErrHandlerFunc, utils.GetAllSupportedScans()) + err = runner.AddJasScannersTasks(jasFileProducerConsumer, &scanResults, &depsList, scanCmd.serverDetails, false, scanner, applicability.ApplicabilityDockerScanScanType, secrets.SecretsScannerDockerScanType, jasErrHandlerFunc, utils.GetAllSupportedScans(), nil) if err != nil { log.Error(fmt.Sprintf("scanning '%s' failed with error: %s", graph.Id, err.Error())) indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) diff --git a/formats/sarifutils/sarifutils.go b/formats/sarifutils/sarifutils.go index e061c4ff..6b183cb5 100644 --- a/formats/sarifutils/sarifutils.go +++ b/formats/sarifutils/sarifutils.go @@ -1,12 +1,10 @@ package sarifutils import ( - "encoding/json" "fmt" "path/filepath" "strings" - "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/owenrumney/go-sarif/v2/sarif" ) @@ -19,12 +17,42 @@ func NewReport() (*sarif.Report, error) { return report, nil } -func ConvertSarifReportToString(report *sarif.Report) (sarifStr string, err error) { - out, err := json.Marshal(report) - if err != nil { - return "", errorutils.CheckError(err) +func CombineReports(reports ...*sarif.Report) (combined *sarif.Report, err error) { + if combined, err = NewReport(); err != nil { + return + } + for _, report := range reports { + for _, run := range report.Runs { + combined.AddRun(run) + } + } + return +} + +func NewPhysicalLocation(physicalPath string) *sarif.PhysicalLocation { + return &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: &physicalPath, + }, + } +} + +func NewPhysicalLocationWithRegion(physicalPath string, startRow, endRow, startCol, endCol int) *sarif.PhysicalLocation { + location := NewPhysicalLocation(physicalPath) + location.Region = &sarif.Region{ + StartLine: &startRow, + EndLine: &endRow, + StartColumn: &startCol, + EndColumn: &endCol, + } + return location +} + +func NewLogicalLocation(name, kind string) *sarif.LogicalLocation { + return &sarif.LogicalLocation{ + Name: &name, + Kind: &kind, } - return utils.IndentJson(out), nil } func ReadScanRunsFromFile(fileName string) (sarifRuns []*sarif.Run, err error) { @@ -79,6 +107,19 @@ func isSameLocation(location *sarif.Location, other *sarif.Location) bool { return GetLocationId(location) == GetLocationId(other) } +func GetLogicalLocation(kind string, location *sarif.Location) *sarif.LogicalLocation { + if location == nil { + return nil + } + // Search for a logical location that has the same kind as the location + for _, logicalLocation := range location.LogicalLocations { + if logicalLocation.Kind != nil && *logicalLocation.Kind == kind { + return logicalLocation + } + } + return nil +} + func GetLocationId(location *sarif.Location) string { return fmt.Sprintf("%s:%s:%d:%d:%d:%d", GetLocationFileName(location), @@ -90,6 +131,55 @@ func GetLocationId(location *sarif.Location) string { ) } +func SetRunToolName(toolName string, run *sarif.Run) { + if run.Tool.Driver == nil { + run.Tool.Driver = &sarif.ToolComponent{} + } + run.Tool.Driver.Name = toolName +} + +func SetRunToolFullDescriptionText(txt string, run *sarif.Run) { + if run.Tool.Driver == nil { + run.Tool.Driver = &sarif.ToolComponent{} + } + if run.Tool.Driver.FullDescription == nil { + run.Tool.Driver.FullDescription = sarif.NewMultiformatMessageString(txt) + return + } + run.Tool.Driver.FullDescription.Text = &txt +} + +func SetRunToolFullDescriptionMarkdown(markdown string, run *sarif.Run) { + if run.Tool.Driver == nil { + run.Tool.Driver = &sarif.ToolComponent{} + } + if run.Tool.Driver.FullDescription == nil { + run.Tool.Driver.FullDescription = sarif.NewMarkdownMultiformatMessageString(markdown) + } + run.Tool.Driver.FullDescription.Markdown = &markdown +} + +func GetRunToolFullDescriptionText(run *sarif.Run) string { + if run.Tool.Driver != nil && run.Tool.Driver.FullDescription != nil && run.Tool.Driver.FullDescription.Text != nil { + return *run.Tool.Driver.FullDescription.Text + } + return "" +} + +func GetRunToolFullDescriptionMarkdown(run *sarif.Run) string { + if run.Tool.Driver != nil && run.Tool.Driver.FullDescription != nil && run.Tool.Driver.FullDescription.Markdown != nil { + return *run.Tool.Driver.FullDescription.Markdown + } + return "" +} + +func GetRunToolName(run *sarif.Run) string { + if run.Tool.Driver != nil { + return run.Tool.Driver.Name + } + return "" +} + func GetResultsLocationCount(runs ...*sarif.Run) (count int) { for _, run := range runs { for _, result := range run.Results { @@ -110,7 +200,10 @@ func GetRunsByWorkingDirectory(workingDirectory string, runs ...*sarif.Run) (fil } } return +} +func SetResultMsgMarkdown(markdown string, result *sarif.Result) { + result.Message.Markdown = &markdown } func GetResultMsgText(result *sarif.Result) string { @@ -127,6 +220,34 @@ func GetResultLevel(result *sarif.Result) string { return "" } +func GetResultRuleId(result *sarif.Result) string { + if result.RuleID != nil { + return *result.RuleID + } + return "" +} + +func IsFingerprintsExists(result *sarif.Result) bool { + return len(result.Fingerprints) > 0 +} + +func SetResultFingerprint(algorithm, value string, result *sarif.Result) { + if result.Fingerprints == nil { + result.Fingerprints = make(map[string]interface{}) + } + result.Fingerprints[algorithm] = value +} + +func GetResultLocationSnippets(result *sarif.Result) []string { + var snippets []string + for _, location := range result.Locations { + if snippet := GetLocationSnippet(location); snippet != "" { + snippets = append(snippets, snippet) + } + } + return snippets +} + func GetLocationSnippet(location *sarif.Location) string { region := getLocationRegion(location) if region != nil && region.Snippet != nil { @@ -148,6 +269,31 @@ func GetLocationFileName(location *sarif.Location) string { return "" } +func GetResultFileLocations(result *sarif.Result) []string { + var locations []string + for _, location := range result.Locations { + locations = append(locations, GetLocationFileName(location)) + } + return locations +} + +func ConvertRunsPathsToRelative(runs ...*sarif.Run) { + for _, run := range runs { + for _, result := range run.Results { + for _, location := range result.Locations { + SetLocationFileName(location, GetRelativeLocationFileName(location, run.Invocations)) + } + for _, flows := range result.CodeFlows { + for _, flow := range flows.ThreadFlows { + for _, location := range flow.Locations { + SetLocationFileName(location.Location, GetRelativeLocationFileName(location.Location, run.Invocations)) + } + } + } + } + } +} + func GetRelativeLocationFileName(location *sarif.Location, invocations []*sarif.Invocation) string { wd := "" if len(invocations) > 0 { @@ -227,13 +373,28 @@ func IsResultKindNotPass(result *sarif.Result) bool { return !(result.Kind != nil && *result.Kind == "pass") } -func GetRuleFullDescription(rule *sarif.ReportingDescriptor) string { +func GetRuleFullDescriptionText(rule *sarif.ReportingDescriptor) string { if rule.FullDescription != nil && rule.FullDescription.Text != nil { return *rule.FullDescription.Text } return "" } +func SetRuleShortDescriptionText(value string, rule *sarif.ReportingDescriptor) { + if rule.ShortDescription == nil { + rule.ShortDescription = sarif.NewMultiformatMessageString(value) + return + } + rule.ShortDescription.Text = &value +} + +func GetRuleShortDescriptionText(rule *sarif.ReportingDescriptor) string { + if rule.ShortDescription != nil && rule.ShortDescription.Text != nil { + return *rule.ShortDescription.Text + } + return "" +} + func GetRunRules(run *sarif.Run) []*sarif.ReportingDescriptor { if run != nil && run.Tool.Driver != nil { return run.Tool.Driver.Rules diff --git a/formats/sarifutils/sarifutils_test.go b/formats/sarifutils/sarifutils_test.go index 302cdfd9..6363b515 100644 --- a/formats/sarifutils/sarifutils_test.go +++ b/formats/sarifutils/sarifutils_test.go @@ -544,7 +544,7 @@ func TestGetRuleFullDescription(t *testing.T) { } for _, test := range tests { - assert.Equal(t, test.expectedOutput, GetRuleFullDescription(test.rule)) + assert.Equal(t, test.expectedOutput, GetRuleFullDescriptionText(test.rule)) } } diff --git a/formats/sarifutils/test_sarifutils.go b/formats/sarifutils/test_sarifutils.go index 2de6c19e..6848849a 100644 --- a/formats/sarifutils/test_sarifutils.go +++ b/formats/sarifutils/test_sarifutils.go @@ -2,8 +2,28 @@ package sarifutils import "github.com/owenrumney/go-sarif/v2/sarif" +func CreateRunWithDummyResultsInWd(wd string, results ...*sarif.Result) *sarif.Run { + return createRunWithDummyResults("", results...).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}) +} + func CreateRunWithDummyResults(results ...*sarif.Result) *sarif.Run { - run := sarif.NewRunWithInformationURI("", "") + return createRunWithDummyResults("", results...) +} + +func CreateDummyDriver(toolName, infoURI string, rules ...*sarif.ReportingDescriptor) *sarif.ToolComponent { + return &sarif.ToolComponent{ + Name: toolName, + InformationURI: &infoURI, + Rules: rules, + } +} + +func CreateRunNameWithResults(toolName string, results ...*sarif.Result) *sarif.Run { + return createRunWithDummyResults(toolName, results...) +} + +func createRunWithDummyResults(toolName string, results ...*sarif.Result) *sarif.Run { + run := sarif.NewRunWithInformationURI(toolName, "") for _, result := range results { if result.RuleID != nil { run.AddRule(*result.RuleID) @@ -24,15 +44,60 @@ func CreateRunWithDummyResultAndRuleProperties(property, value string, result *s return run } -func CreateResultWithLocations(msg, ruleId, level string, locations ...*sarif.Location) *sarif.Result { +func CreateDummyResultInPath(fileName string) *sarif.Result { + return CreateResultWithOneLocation(fileName, 0, 0, 0, 0, "snippet", "rule", "level") +} + +func CreateDummyResult(markdown, msg, ruleId, level string) *sarif.Result { return &sarif.Result{ - Message: *sarif.NewTextMessage(msg), - Locations: locations, - Level: &level, - RuleID: &ruleId, + Message: *sarif.NewTextMessage(msg).WithMarkdown(markdown), + Level: &level, + RuleID: &ruleId, } } +func CreateResultWithDummyLocationAmdProperty(fileName, property, value string) *sarif.Result { + resultWithLocation := CreateDummyResultInPath(fileName) + resultWithLocation.Properties = map[string]interface{}{property: value} + return resultWithLocation +} + +func CreateResultWithLocations(msg, ruleId, level string, locations ...*sarif.Location) *sarif.Result { + result := CreateDummyResult("", msg, ruleId, level) + result.Locations = locations + return result +} + +func CreateDummyResultWithFingerprint(markdown, msg, algorithm, value string, locations ...*sarif.Location) *sarif.Result { + result := CreateDummyResult(markdown, msg, "rule", "level") + if result.RuleIndex == nil { + result.RuleIndex = newUintPtr(0) + } + result.Locations = locations + result.Fingerprints = map[string]interface{}{algorithm: value} + return result +} + +func newUintPtr(v uint) *uint { + return &v +} + +func CreateDummyResultWithPathAndLogicalLocation(fileName, logicalName, kind, property, value string) *sarif.Result { + result := CreateDummyResult("", "", "rule", "level") + result.Locations = append(result.Locations, CreateDummyLocationWithPathAndLogicalLocation(fileName, logicalName, kind, property, value)) + return result +} + +func CreateDummyLocationWithPathAndLogicalLocation(fileName, logicalName, kind, property, value string) *sarif.Location { + location := CreateDummyLocationInPath(fileName) + location.LogicalLocations = append(location.LogicalLocations, CreateLogicalLocationWithProperty(logicalName, kind, property, value)) + return location +} + +func CreateDummyLocationInPath(fileName string) *sarif.Location { + return CreateLocation(fileName, 0, 0, 0, 0, "snippet") +} + func CreateLocation(fileName string, startLine, startCol, endLine, endCol int, snippet string) *sarif.Location { return &sarif.Location{ PhysicalLocation: &sarif.PhysicalLocation{ @@ -46,6 +111,12 @@ func CreateLocation(fileName string, startLine, startCol, endLine, endCol int, s } } +func CreateLogicalLocationWithProperty(name, kind, property, value string) *sarif.LogicalLocation { + location := sarif.NewLogicalLocation().WithName(name).WithKind(kind) + location.Properties = map[string]interface{}{property: value} + return location +} + func CreateDummyPassingResult(ruleId string) *sarif.Result { kind := "pass" return &sarif.Result{ diff --git a/go.mod b/go.mod index 42caa94e..ef59500e 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/jfrog/froggit-go v1.16.1 github.com/jfrog/gofrog v1.7.5 github.com/jfrog/jfrog-apps-config v1.0.1 - github.com/jfrog/jfrog-cli-core/v2 v2.55.6 + github.com/jfrog/jfrog-cli-core/v2 v2.55.7 github.com/jfrog/jfrog-client-go v1.46.1 github.com/magiconair/properties v1.8.7 github.com/owenrumney/go-sarif/v2 v2.3.0 diff --git a/go.sum b/go.sum index 854d47fa..8ebca8bf 100644 --- a/go.sum +++ b/go.sum @@ -898,8 +898,8 @@ github.com/jfrog/gofrog v1.7.5 h1:dFgtEDefJdlq9cqTRoe09RLxS5Bxbe1Ev5+E6SmZHcg= github.com/jfrog/gofrog v1.7.5/go.mod h1:jyGiCgiqSSR7k86hcUSu67XVvmvkkgWTmPsH25wI298= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-core/v2 v2.55.6 h1:3tQuEdYgS2q7fkrrSG66OnO0S998FXGaY9BVsxSLst4= -github.com/jfrog/jfrog-cli-core/v2 v2.55.6/go.mod h1:DPO5BfWAeOByahFMMy+PcjmbPlcyoRy7Bf2C5sGKVi0= +github.com/jfrog/jfrog-cli-core/v2 v2.55.7 h1:V4dO2FMNIH49lov3dMj3jYRg8KBTG7hyhHI8ftYByf8= +github.com/jfrog/jfrog-cli-core/v2 v2.55.7/go.mod h1:DPO5BfWAeOByahFMMy+PcjmbPlcyoRy7Bf2C5sGKVi0= github.com/jfrog/jfrog-client-go v1.46.1 h1:ExqOF8ClOG9LO3vbm6jTIwQHHhprbu8lxB2RrM6mMI0= github.com/jfrog/jfrog-client-go v1.46.1/go.mod h1:UCu2JNBfMp9rypEmCL84DCooG79xWIHVadZQR3Ab+BQ= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index 39a72f12..3e284310 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -2,7 +2,6 @@ package runner import ( "fmt" - "github.com/jfrog/gofrog/parallel" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -15,11 +14,13 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/jasutils" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/jfrog/jfrog-client-go/xsc/services" "golang.org/x/exp/slices" ) func AddJasScannersTasks(securityParallelRunner *utils.SecurityParallelRunner, scanResults *utils.Results, directDependencies *[]string, - serverDetails *config.ServerDetails, thirdPartyApplicabilityScan bool, scanner *jas.JasScanner, scanType applicability.ApplicabilityScanType, secretsScanType secrets.SecretsScanType, errHandlerFunc func(error), scansToPreform []utils.SubScanType) (err error) { + serverDetails *config.ServerDetails, thirdPartyApplicabilityScan bool, scanner *jas.JasScanner, scanType applicability.ApplicabilityScanType, + secretsScanType secrets.SecretsScanType, errHandlerFunc func(error), scansToPreform []utils.SubScanType, configProfile *services.ConfigProfile) (err error) { if serverDetails == nil || len(serverDetails.Url) == 0 { log.Warn("To include 'Advanced Security' scan as part of the audit output, please run the 'jf c add' command before running this command.") return @@ -31,27 +32,51 @@ func AddJasScannersTasks(securityParallelRunner *utils.SecurityParallelRunner, s } // Set environments variables for analytics in analyzers manager. // Don't execute other scanners when scanning third party dependencies. + // Currently, if config profile exists, the only possible scanners to run are: Secrets, Sast if !thirdPartyApplicabilityScan { for _, module := range scanner.JFrogAppsConfig.Modules { if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.SecretsScan) { log.Debug("Skipping secrets scan as requested by input...") + } else if configProfile != nil { + // This code section is related to CentralizedConfig integration in CI Next. + log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run secrets scan...", configProfile.ProfileName)) + if configProfile.Modules[0].ScanConfig.SecretsScannerConfig.EnableSecretsScan { + err = addModuleJasScanTask(jfrogappsconfig.Module{}, jasutils.Secrets, securityParallelRunner, runSecretsScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module, secretsScanType), errHandlerFunc) + } else { + log.Debug(fmt.Sprintf("Skipping secrets scan as requested by '%s' config profile...", configProfile.ProfileName)) + } } else if err = addModuleJasScanTask(module, jasutils.Secrets, securityParallelRunner, runSecretsScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module, secretsScanType), errHandlerFunc); err != nil { return } if runAllScanners { - if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.IacScan) { - log.Debug("Skipping Iac scan as requested by input...") - } else if err = addModuleJasScanTask(module, jasutils.IaC, securityParallelRunner, runIacScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module), errHandlerFunc); err != nil { - return + if configProfile == nil { + if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.IacScan) { + log.Debug("Skipping Iac scan as requested by input...") + } else if err = addModuleJasScanTask(module, jasutils.IaC, securityParallelRunner, runIacScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module), errHandlerFunc); err != nil { + return + } } if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.SastScan) { log.Debug("Skipping Sast scan as requested by input...") + } else if configProfile != nil { + log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run Sast scan...", configProfile.ProfileName)) + if configProfile.Modules[0].ScanConfig.SastScannerConfig.EnableSastScan { + err = addModuleJasScanTask(jfrogappsconfig.Module{}, jasutils.Sast, securityParallelRunner, runSastScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module), errHandlerFunc) + } else { + log.Debug(fmt.Sprintf("Skipping Sast scan as requested by '%s' config profile...", configProfile.ProfileName)) + } } else if err = addModuleJasScanTask(module, jasutils.Sast, securityParallelRunner, runSastScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module), errHandlerFunc); err != nil { return } } } } + + if configProfile != nil { + log.Debug("Config profile is in use. Skipping Contextual Analysis scan as it is not currently supported with a config profile...") + return + } + if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.ContextualAnalysisScan) { log.Debug("Skipping contextual analysis scan as requested by input...") return err diff --git a/jas/runner/jasrunner_test.go b/jas/runner/jasrunner_test.go index 886b3e3b..4e2a3a06 100644 --- a/jas/runner/jasrunner_test.go +++ b/jas/runner/jasrunner_test.go @@ -39,7 +39,7 @@ func TestGetExtendedScanResults_ServerNotValid(t *testing.T) { scanner := &jas.JasScanner{} jasScanner, err := jas.CreateJasScanner(scanner, nil, &jas.FakeServerDetails, jas.GetAnalyzerManagerXscEnvVars("", scanResults.GetScaScannedTechnologies()...)) assert.NoError(t, err) - err = AddJasScannersTasks(securityParallelRunnerForTest, scanResults, &[]string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, nil, false, jasScanner, applicability.ApplicabilityScannerType, secrets.SecretsScannerType, securityParallelRunnerForTest.AddErrorToChan, utils.GetAllSupportedScans()) + err = AddJasScannersTasks(securityParallelRunnerForTest, scanResults, &[]string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, nil, false, jasScanner, applicability.ApplicabilityScannerType, secrets.SecretsScannerType, securityParallelRunnerForTest.AddErrorToChan, utils.GetAllSupportedScans(), nil) assert.NoError(t, err) } diff --git a/tests/testdata/other/configProfile/configProfileExample.json b/tests/testdata/other/configProfile/configProfileExample.json new file mode 100644 index 00000000..e4fb139c --- /dev/null +++ b/tests/testdata/other/configProfile/configProfileExample.json @@ -0,0 +1,49 @@ +{ + "profile_name": "default-profile", + "frogbot_config": { + "email_author": "my-user@jfrog.com", + "aggregate_fixes": true, + "avoid_previous_pr_comments_deletion": true, + "branch_name_template": "frogbot-${IMPACTED_PACKAGE}-${BRANCH_NAME_HASH}", + "pr_title_template": "[🐸 Frogbot] Upgrade {IMPACTED_PACKAGE} to {FIX_VERSION}", + "pr_comment_title": "Frogbot notes:", + "commit_message_template": "Upgrade {IMPACTED_PACKAGE} to {FIX_VERSION}", + "show_secrets_as_pr_comment": false + }, + "modules": [ + { + "module_name": "default-module", + "path_from_root": ".", + "releases_repo": "nuget-remote", + "analyzer_manager_version": "1.8.1", + "additional_paths_for_module": ["lib1", "utils/lib2"], + "exclude_paths": ["**/.git/**", "**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/target/**"], + "scan_config": { + "scan_timeout": 600, + "exclude_pattern": "*.md", + "enable_sca_scan": true, + "enable_contextual_analysis_scan": true, + "sast_scanner_config": { + "enable_sast_scan": true + }, + "secrets_scanner_config": { + "enable_secrets_scan": true + }, + "iac_scanner_config": { + "enable_iac_scan": true + }, + "applications_scanner_config": { + "enable_applications_scan": true + }, + "services_scanner_config": { + "enable_services_scan": true + } + }, + "protected_branches": ["main", "master"], + "include_exclude_mode": 0, + "include_exclude_pattern": "*test*", + "report_analytics": true + } + ], + "is_default": true +} \ No newline at end of file diff --git a/tests/testdata/other/jobSummary/security_section.md b/tests/testdata/other/jobSummary/security_section.md index 517122ca..685a59ce 100644 --- a/tests/testdata/other/jobSummary/security_section.md +++ b/tests/testdata/other/jobSummary/security_section.md @@ -1,8 +1,9 @@ -

🔒 Security Summary

-#### Curation Audit +

🔒 Curation Audit

+ | Audit Summary | Project name | Audit Details | |--------|--------|---------| | failed.svg | /application1 |
Total Number of resolved packages: 6
🟢 Approved packages: 3
🔴 Blocked packages: 3
Violated Policy: cvss_score, Condition: cvss score higher than 4.0 (2)📦 npm://test:2.0.0
📦 npm://underscore:1.0.0
Violated Policy: Malicious, Condition: Malicious package (1)📦 npm://lodash:1.0.0
| | passed.svg | /application2 |
Total Number of resolved packages: 3
| -| failed.svg | /application3 |
Total Number of resolved packages: 5
🟢 Approved packages: 4
🔴 Blocked packages: 1
Violated Policy: Aged, Condition: Package is aged (1)📦 npm://test:1.0.0
|
\ No newline at end of file +| failed.svg | /application3 |
Total Number of resolved packages: 5
🟢 Approved packages: 4
🔴 Blocked packages: 1
Violated Policy: Aged, Condition: Package is aged (1)📦 npm://test:1.0.0
| +
\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/violations_not_extended_view.md b/tests/testdata/other/jobSummary/violations_not_extended_view.md index 0702399a..1aead66d 100644 --- a/tests/testdata/other/jobSummary/violations_not_extended_view.md +++ b/tests/testdata/other/jobSummary/violations_not_extended_view.md @@ -1 +1 @@ -
watch: watch1

26 Policy Violations:	20 Security	2 Operational	1 License	3 Secrets

🐸 Unlock detailed findings
\ No newline at end of file +
watch: watch1

26 Policy Violations:	20 Security	2 Operational	1 License	3 Secrets

🐸 Unlock detailed findings
\ No newline at end of file diff --git a/utils/paths.go b/utils/paths.go index 918d1b9b..cafe460c 100644 --- a/utils/paths.go +++ b/utils/paths.go @@ -1,9 +1,6 @@ package utils import ( - // #nosec G505 -- Not in use for secrets. - "crypto/sha1" - "encoding/hex" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" "os" @@ -58,13 +55,7 @@ func getProjectPathHash() (string, error) { if err != nil { return "", err } - // #nosec G401 -- Not a secret hash. - hasher := sha1.New() - _, err = hasher.Write([]byte(workingDir)) - if err != nil { - return "", err - } - return hex.EncodeToString(hasher.Sum(nil)), nil + return Sha1Hash(workingDir) } func GetCurationPipCacheFolder() (string, error) { diff --git a/utils/results.go b/utils/results.go index b1404099..bcee459b 100644 --- a/utils/results.go +++ b/utils/results.go @@ -10,6 +10,7 @@ import ( ) type Results struct { + ResultType CommandType ScaResults []*ScaScanResult XrayVersion string ScansErr error @@ -19,8 +20,8 @@ type Results struct { MultiScanId string } -func NewAuditResults() *Results { - return &Results{ExtendedScanResults: &ExtendedScanResults{}} +func NewAuditResults(resultType CommandType) *Results { + return &Results{ResultType: resultType, ExtendedScanResults: &ExtendedScanResults{}} } func (r *Results) GetScaScansXrayResults() (results []services.ScanResponse) { @@ -92,6 +93,7 @@ func (r *Results) CountScanResultsFindings(includeVulnerabilities, includeViolat type ScaScanResult struct { // Could be working directory (audit), file path (binary scan) or build name+number (build scan) Target string `json:"Target"` + Name string `json:"Name,omitempty"` Technology techutils.Technology `json:"Technology,omitempty"` XrayResults []services.ScanResponse `json:"XrayResults,omitempty"` Descriptors []string `json:"Descriptors,omitempty"` diff --git a/utils/resultstable.go b/utils/resultstable.go index 4bfd5f42..65f41164 100644 --- a/utils/resultstable.go +++ b/utils/resultstable.go @@ -38,13 +38,13 @@ const ( // In case one (or more) of the violations contains the field FailBuild set to true, CliError with exit code 3 will be returned. // Set printExtended to true to print fields with 'extended' tag. // If the scan argument is set to true, print the scan tables. -func PrintViolationsTable(violations []services.Violation, results *Results, multipleRoots, printExtended bool, scanType services.ScanType) error { +func PrintViolationsTable(violations []services.Violation, results *Results, multipleRoots, printExtended bool) error { securityViolationsRows, licenseViolationsRows, operationalRiskViolationsRows, err := prepareViolations(violations, results, multipleRoots, true, true) if err != nil { return err } // Print tables, if scan is true; print the scan tables. - if scanType == services.Binary { + if results.ResultType.IsTargetBinary() { err = coreutils.PrintTable(formats.ConvertToVulnerabilityScanTableRow(securityViolationsRows), "Security Violations", "No security violations were found", printExtended) if err != nil { return err @@ -192,13 +192,13 @@ func prepareViolations(violations []services.Violation, results *Results, multip // In case multipleRoots is true, the field Component will show the root of each impact path, otherwise it will show the root's child. // Set printExtended to true to print fields with 'extended' tag. // If the scan argument is set to true, print the scan tables. -func PrintVulnerabilitiesTable(vulnerabilities []services.Vulnerability, results *Results, multipleRoots, printExtended bool, scanType services.ScanType) error { +func PrintVulnerabilitiesTable(vulnerabilities []services.Vulnerability, results *Results, multipleRoots, printExtended bool, scanType CommandType) error { vulnerabilitiesRows, err := prepareVulnerabilities(vulnerabilities, results, multipleRoots, true, true) if err != nil { return err } - if scanType == services.Binary { + if scanType.IsTargetBinary() { return coreutils.PrintTable(formats.ConvertToVulnerabilityScanTableRow(vulnerabilitiesRows), "Vulnerable Components", "✨ No vulnerable components were found ✨", printExtended) } var emptyTableMessage string @@ -300,12 +300,12 @@ func getJfrogResearchPriority(vulnerabilityOrViolation formats.VulnerabilityOrVi // In case multipleRoots is true, the field Component will show the root of each impact path, otherwise it will show the root's child. // Set printExtended to true to print fields with 'extended' tag. // If the scan argument is set to true, print the scan tables. -func PrintLicensesTable(licenses []services.License, printExtended bool, scanType services.ScanType) error { +func PrintLicensesTable(licenses []services.License, printExtended bool, scanType CommandType) error { licensesRows, err := PrepareLicenses(licenses) if err != nil { return err } - if scanType == services.Binary { + if scanType.IsTargetBinary() { return coreutils.PrintTable(formats.ConvertToLicenseScanTableRow(licensesRows), "Licenses", "No licenses were found", printExtended) } return coreutils.PrintTable(formats.ConvertToLicenseTableRow(licensesRows), "Licenses", "No licenses were found", printExtended) @@ -398,7 +398,7 @@ func prepareIacs(iacs []*sarif.Run, isTable bool) []formats.SourceCodeRow { for _, iacResult := range iacRun.Results { scannerDescription := "" if rule, err := iacRun.GetRuleById(*iacResult.RuleID); err == nil { - scannerDescription = sarifutils.GetRuleFullDescription(rule) + scannerDescription = sarifutils.GetRuleFullDescriptionText(rule) } currSeverity, err := severityutils.ParseSeverity(sarifutils.GetResultLevel(iacResult), true) if err != nil { @@ -452,7 +452,7 @@ func prepareSast(sasts []*sarif.Run, isTable bool) []formats.SourceCodeRow { for _, sastResult := range sastRun.Results { scannerDescription := "" if rule, err := sastRun.GetRuleById(*sastResult.RuleID); err == nil { - scannerDescription = sarifutils.GetRuleFullDescription(rule) + scannerDescription = sarifutils.GetRuleFullDescriptionText(rule) } currSeverity, err := severityutils.ParseSeverity(sarifutils.GetResultLevel(sastResult), true) if err != nil { @@ -935,7 +935,7 @@ func getCveApplicabilityField(cveId string, applicabilityScanResults []*sarif.Ru var applicabilityStatuses []jasutils.ApplicabilityStatus for _, applicabilityRun := range applicabilityScanResults { if rule, _ := applicabilityRun.GetRuleById(jasutils.CveToApplicabilityRuleId(cveId)); rule != nil { - applicability.ScannerDescription = sarifutils.GetRuleFullDescription(rule) + applicability.ScannerDescription = sarifutils.GetRuleFullDescriptionText(rule) status := getApplicabilityStatusFromRule(rule) if status != "" { applicabilityStatuses = append(applicabilityStatuses, status) diff --git a/utils/resultstable_test.go b/utils/resultstable_test.go index d7398d82..b6230db4 100644 --- a/utils/resultstable_test.go +++ b/utils/resultstable_test.go @@ -27,7 +27,7 @@ func TestPrintViolationsTable(t *testing.T) { } for _, test := range tests { - err := PrintViolationsTable(test.violations, NewAuditResults(), false, true, services.Binary) + err := PrintViolationsTable(test.violations, NewAuditResults(Binary), false, true) assert.NoError(t, err) if CheckIfFailBuild([]services.ScanResponse{{Violations: test.violations}}) { err = NewFailBuildError() diff --git a/utils/resultwriter.go b/utils/resultwriter.go index 53a21d62..331d7724 100644 --- a/utils/resultwriter.go +++ b/utils/resultwriter.go @@ -4,6 +4,9 @@ import ( "bytes" "encoding/json" "fmt" + "os" + "path/filepath" + "regexp" "strconv" "strings" @@ -25,11 +28,24 @@ import ( ) const ( - BaseDocumentationURL = "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/" + BaseDocumentationURL = "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/" + CurrentWorkflowNameEnvVar = "GITHUB_WORKFLOW" + CurrentWorkflowRunNumberEnvVar = "GITHUB_RUN_NUMBER" + CurrentWorkflowWorkspaceEnvVar = "GITHUB_WORKSPACE" + + MissingCveScore = "0" + maxPossibleCve = 10.0 + + // #nosec G101 -- Not credentials. + patchedBinarySecretScannerToolName = "JFrog Binary Secrets Scanner" + jfrogFingerprintAlgorithmName = "jfrogFingerprintHash" ) -const MissingCveScore = "0" -const maxPossibleCve = 10.0 +var ( + GithubBaseWorkflowDir = filepath.Join(".github", "workflows") + dockerJasLocationPathPattern = regexp.MustCompile(`.*[\\/](?P[^\\/]+)[\\/](?P[0-9a-fA-F]+)[\\/](?P.*)`) + dockerScaComponentNamePattern = regexp.MustCompile(`(?P[^__]+)__(?P[0-9a-fA-F]+)\.tar`) +) type ResultsWriter struct { // The scan results. @@ -38,16 +54,16 @@ type ResultsWriter struct { simpleJsonError []formats.SimpleJsonError // Format The output format. format format.OutputFormat - // IncludeVulnerabilities If true, include all vulnerabilities as part of the output. Else, include violations only. + // IncludeVulnerabilities If true, include all vulnerabilities as part of the output. includeVulnerabilities bool + // If true, include violations as part of the output. + hasViolationContext bool // IncludeLicenses If true, also include license violations as part of the output. includeLicenses bool // IsMultipleRoots multipleRoots is set to true, in case the given results array contains (or may contain) results of several projects (like in binary scan). isMultipleRoots bool // PrintExtended, If true, show extended results. printExtended bool - // The scanType (binary,dependency) - scanType services.ScanType // For table format - show table only for the given subScansPreformed subScansPreformed []SubScanType // Messages - Option array of messages, to be displayed if the format is Table @@ -65,13 +81,13 @@ func GetScaScanFileName(r *Results) string { return "" } -func (rw *ResultsWriter) SetOutputFormat(f format.OutputFormat) *ResultsWriter { - rw.format = f +func (rw *ResultsWriter) SetHasViolationContext(hasViolationContext bool) *ResultsWriter { + rw.hasViolationContext = hasViolationContext return rw } -func (rw *ResultsWriter) SetScanType(scanType services.ScanType) *ResultsWriter { - rw.scanType = scanType +func (rw *ResultsWriter) SetOutputFormat(f format.OutputFormat) *ResultsWriter { + rw.format = f return rw } @@ -141,39 +157,41 @@ func (rw *ResultsWriter) printScanResultsTables() (err error) { printMessage(coreutils.PrintTitle("The full scan results are available here: ") + coreutils.PrintLink(resultsPath)) } log.Output() - if shouldPrintTable(rw.subScansPreformed, ScaScan, rw.scanType) { - if rw.includeVulnerabilities { - err = PrintVulnerabilitiesTable(vulnerabilities, rw.results, rw.isMultipleRoots, rw.printExtended, rw.scanType) - } else { - err = PrintViolationsTable(violations, rw.results, rw.isMultipleRoots, rw.printExtended, rw.scanType) + if shouldPrintTable(rw.subScansPreformed, ScaScan, rw.results.ResultType) { + if rw.hasViolationContext { + if err = PrintViolationsTable(violations, rw.results, rw.isMultipleRoots, rw.printExtended); err != nil { + return + } } - if err != nil { - return + if rw.includeVulnerabilities { + if err = PrintVulnerabilitiesTable(vulnerabilities, rw.results, rw.isMultipleRoots, rw.printExtended, rw.results.ResultType); err != nil { + return + } } if rw.includeLicenses { - if err = PrintLicensesTable(licenses, rw.printExtended, rw.scanType); err != nil { + if err = PrintLicensesTable(licenses, rw.printExtended, rw.results.ResultType); err != nil { return } } } - if shouldPrintTable(rw.subScansPreformed, SecretsScan, rw.scanType) { + if shouldPrintTable(rw.subScansPreformed, SecretsScan, rw.results.ResultType) { if err = PrintSecretsTable(rw.results.ExtendedScanResults.SecretsScanResults, rw.results.ExtendedScanResults.EntitledForJas); err != nil { return } } - if shouldPrintTable(rw.subScansPreformed, IacScan, rw.scanType) { + if shouldPrintTable(rw.subScansPreformed, IacScan, rw.results.ResultType) { if err = PrintIacTable(rw.results.ExtendedScanResults.IacScanResults, rw.results.ExtendedScanResults.EntitledForJas); err != nil { return } } - if !shouldPrintTable(rw.subScansPreformed, SastScan, rw.scanType) { + if !shouldPrintTable(rw.subScansPreformed, SastScan, rw.results.ResultType) { return nil } return PrintSastTable(rw.results.ExtendedScanResults.SastScanResults, rw.results.ExtendedScanResults.EntitledForJas) } -func shouldPrintTable(requestedScans []SubScanType, subScan SubScanType, scanType services.ScanType) bool { - if scanType == services.Binary && (subScan == IacScan || subScan == SastScan) { +func shouldPrintTable(requestedScans []SubScanType, subScan SubScanType, scanType CommandType) bool { + if scanType.IsTargetBinary() && (subScan == IacScan || subScan == SastScan) { return false } return len(requestedScans) == 0 || slices.Contains(requestedScans, subScan) @@ -192,7 +210,7 @@ func printMessage(message string) { log.Output("💬" + message) } -func GenereateSarifReportFromResults(results *Results, isMultipleRoots, includeLicenses bool, allowedLicenses []string) (report *sarif.Report, err error) { +func GenerateSarifReportFromResults(results *Results, isMultipleRoots, includeLicenses bool, allowedLicenses []string) (report *sarif.Report, err error) { report, err = sarifutils.NewReport() if err != nil { return @@ -202,11 +220,10 @@ func GenereateSarifReportFromResults(results *Results, isMultipleRoots, includeL return } - report.Runs = append(report.Runs, xrayRun) - report.Runs = append(report.Runs, results.ExtendedScanResults.ApplicabilityScanResults...) - report.Runs = append(report.Runs, results.ExtendedScanResults.IacScanResults...) - report.Runs = append(report.Runs, results.ExtendedScanResults.SecretsScanResults...) - report.Runs = append(report.Runs, results.ExtendedScanResults.SastScanResults...) + report.Runs = append(report.Runs, patchRunsToPassIngestionRules(ScaScan, results, xrayRun)...) + report.Runs = append(report.Runs, patchRunsToPassIngestionRules(IacScan, results, results.ExtendedScanResults.IacScanResults...)...) + report.Runs = append(report.Runs, patchRunsToPassIngestionRules(SecretsScan, results, results.ExtendedScanResults.SecretsScanResults...)...) + report.Runs = append(report.Runs, patchRunsToPassIngestionRules(SastScan, results, results.ExtendedScanResults.SastScanResults...)...) return } @@ -216,10 +233,10 @@ func convertXrayResponsesToSarifRun(results *Results, isMultipleRoots, includeLi if err != nil { return } - xrayRun := sarif.NewRunWithInformationURI("JFrog Xray SCA", BaseDocumentationURL+"sca") + xrayRun := sarif.NewRunWithInformationURI("JFrog Xray Scanner", BaseDocumentationURL+"sca") xrayRun.Tool.Driver.Version = &results.XrayVersion if len(xrayJson.Vulnerabilities) > 0 || len(xrayJson.SecurityViolations) > 0 || len(xrayJson.LicensesViolations) > 0 { - if err = extractXrayIssuesToSarifRun(xrayRun, xrayJson); err != nil { + if err = extractXrayIssuesToSarifRun(results, xrayRun, xrayJson); err != nil { return } } @@ -227,26 +244,26 @@ func convertXrayResponsesToSarifRun(results *Results, isMultipleRoots, includeLi return } -func extractXrayIssuesToSarifRun(run *sarif.Run, xrayJson formats.SimpleJsonResults) error { +func extractXrayIssuesToSarifRun(results *Results, run *sarif.Run, xrayJson formats.SimpleJsonResults) error { for _, vulnerability := range xrayJson.Vulnerabilities { - if err := addXrayCveIssueToSarifRun(vulnerability, run); err != nil { + if err := addXrayCveIssueToSarifRun(results, vulnerability, run); err != nil { return err } } for _, violation := range xrayJson.SecurityViolations { - if err := addXrayCveIssueToSarifRun(violation, run); err != nil { + if err := addXrayCveIssueToSarifRun(results, violation, run); err != nil { return err } } for _, license := range xrayJson.LicensesViolations { - if err := addXrayLicenseViolationToSarifRun(license, run); err != nil { + if err := addXrayLicenseViolationToSarifRun(results, license, run); err != nil { return err } } return nil } -func addXrayCveIssueToSarifRun(issue formats.VulnerabilityOrViolationRow, run *sarif.Run) (err error) { +func addXrayCveIssueToSarifRun(results *Results, issue formats.VulnerabilityOrViolationRow, run *sarif.Run) (err error) { maxCveScore, err := findMaxCVEScore(issue.Cves) if err != nil { return @@ -262,6 +279,7 @@ func addXrayCveIssueToSarifRun(issue formats.VulnerabilityOrViolationRow, run *s cveId := GetIssueIdentifier(issue.Cves, issue.IssueId) markdownDescription := getSarifTableDescription(formattedDirectDependencies, maxCveScore, issue.Applicable, issue.FixedVersions) addXrayIssueToSarifRun( + results.ResultType, cveId, issue.ImpactedDependencyName, issue.ImpactedDependencyVersion, @@ -277,12 +295,13 @@ func addXrayCveIssueToSarifRun(issue formats.VulnerabilityOrViolationRow, run *s return } -func addXrayLicenseViolationToSarifRun(license formats.LicenseRow, run *sarif.Run) (err error) { +func addXrayLicenseViolationToSarifRun(results *Results, license formats.LicenseRow, run *sarif.Run) (err error) { formattedDirectDependencies, err := getDirectDependenciesFormatted(license.Components) if err != nil { return } addXrayIssueToSarifRun( + results.ResultType, license.LicenseKey, license.ImpactedDependencyName, license.ImpactedDependencyVersion, @@ -298,21 +317,29 @@ func addXrayLicenseViolationToSarifRun(license formats.LicenseRow, run *sarif.Ru return } -func addXrayIssueToSarifRun(issueId, impactedDependencyName, impactedDependencyVersion string, severity severityutils.Severity, severityScore, summary, title, markdownDescription string, components []formats.ComponentRow, location *sarif.Location, run *sarif.Run) { +func addXrayIssueToSarifRun(resultType CommandType, issueId, impactedDependencyName, impactedDependencyVersion string, severity severityutils.Severity, severityScore, summary, title, markdownDescription string, components []formats.ComponentRow, location *sarif.Location, run *sarif.Run) { // Add rule if not exists ruleId := getXrayIssueSarifRuleId(impactedDependencyName, impactedDependencyVersion, issueId) if rule, _ := run.GetRuleById(ruleId); rule == nil { addXrayRule(ruleId, title, severityScore, summary, markdownDescription, run) } // Add result for each component - for _, directDependency := range components { msg := getXrayIssueSarifHeadline(directDependency.Name, directDependency.Version, issueId) if result := run.CreateResultForRule(ruleId).WithMessage(sarif.NewTextMessage(msg)).WithLevel(severityutils.SeverityToSarifSeverityLevel(severity).String()); location != nil { + if resultType == DockerImage { + algorithm, layer := getLayerContentFromComponentId(directDependency.Name) + if layer != "" { + logicalLocation := sarifutils.NewLogicalLocation(layer, "layer") + if algorithm != "" { + logicalLocation.Properties = map[string]interface{}{"algorithm": algorithm} + } + location.LogicalLocations = append(location.LogicalLocations, logicalLocation) + } + } result.AddLocation(location) } } - } func getDescriptorFullPath(tech techutils.Technology, run *sarif.Run) (string, error) { @@ -450,7 +477,7 @@ func getXrayIssueSarifRuleId(depName, version, key string) string { } func getXrayIssueSarifHeadline(depName, version, key string) string { - return fmt.Sprintf("[%s] %s %s", key, depName, version) + return strings.TrimSpace(fmt.Sprintf("[%s] %s %s", key, depName, version)) } func getXrayLicenseSarifHeadline(depName, version, key string) string { @@ -511,6 +538,319 @@ func findMaxCVEScore(cves []formats.CveRow) (string, error) { return strCve, nil } +func patchRules(subScanType SubScanType, cmdResults *Results, rules ...*sarif.ReportingDescriptor) (patched []*sarif.ReportingDescriptor) { + patched = []*sarif.ReportingDescriptor{} + for _, rule := range rules { + // Github code scanning ingestion rules rejects rules without help content. + // Patch by transferring the full description to the help field. + if rule.Help == nil && rule.FullDescription != nil { + rule.Help = rule.FullDescription + } + // SARIF1001 - if both 'id' and 'name' are present, they must be different. If they are identical, the tool must omit the 'name' property. + if rule.Name != nil && rule.ID == *rule.Name { + rule.Name = nil + } + if cmdResults.ResultType.IsTargetBinary() && subScanType == SecretsScan { + // Patch the rule name in case of binary scan + sarifutils.SetRuleShortDescriptionText(fmt.Sprintf("[Secret in Binary found] %s", sarifutils.GetRuleShortDescriptionText(rule)), rule) + } + patched = append(patched, rule) + } + return +} + +func patchResults(subScanType SubScanType, cmdResults *Results, run *sarif.Run, results ...*sarif.Result) (patched []*sarif.Result) { + patched = []*sarif.Result{} + for _, result := range results { + if len(result.Locations) == 0 { + // Github code scanning ingestion rules rejects results without locations. + // Patch by removing results without locations. + log.Debug(fmt.Sprintf("[%s] Removing result [ruleId=%s] without locations: %s", subScanType.String(), sarifutils.GetResultRuleId(result), sarifutils.GetResultMsgText(result))) + continue + } + if cmdResults.ResultType.IsTargetBinary() { + var markdown string + if subScanType == SecretsScan { + markdown = getSecretInBinaryMarkdownMsg(cmdResults, result) + } else { + markdown = getScaInBinaryMarkdownMsg(cmdResults, result) + } + sarifutils.SetResultMsgMarkdown(markdown, result) + // For Binary scans, override the physical location if applicable (after data already used for markdown) + convertBinaryPhysicalLocations(cmdResults, run, result) + // Calculate the fingerprints if not exists + if !sarifutils.IsFingerprintsExists(result) { + if err := calculateResultFingerprints(cmdResults.ResultType, run, result); err != nil { + log.Warn(fmt.Sprintf("Failed to calculate the fingerprint for result [ruleId=%s]: %s", sarifutils.GetResultRuleId(result), err.Error())) + } + } + } + patched = append(patched, result) + } + return patched +} + +func patchRunsToPassIngestionRules(subScanType SubScanType, cmdResults *Results, runs ...*sarif.Run) []*sarif.Run { + // Since we run in temp directories files should be relative + // Patch by converting the file paths to relative paths according to the invocations + convertPaths(cmdResults.ResultType, subScanType, runs...) + for _, run := range runs { + if cmdResults.ResultType.IsTargetBinary() && subScanType == SecretsScan { + // Patch the tool name in case of binary scan + sarifutils.SetRunToolName(patchedBinarySecretScannerToolName, run) + } + run.Tool.Driver.Rules = patchRules(subScanType, cmdResults, run.Tool.Driver.Rules...) + run.Results = patchResults(subScanType, cmdResults, run, run.Results...) + } + return runs +} + +func convertPaths(commandType CommandType, subScanType SubScanType, runs ...*sarif.Run) { + // Convert base on invocation for source code + sarifutils.ConvertRunsPathsToRelative(runs...) + if !(commandType == DockerImage && subScanType == SecretsScan) { + return + } + for _, run := range runs { + for _, result := range run.Results { + // For Docker secret scan, patch the logical location if not exists + patchDockerSecretLocations(result) + } + } +} + +// Patch the URI to be the file path from sha// +// Extract the layer from the location URI, adds it as a logical location kind "layer" +func patchDockerSecretLocations(result *sarif.Result) { + for _, location := range result.Locations { + algorithm, layerHash, relativePath := getLayerContentFromPath(sarifutils.GetLocationFileName(location)) + if layerHash != "" { + // Set Logical location kind "layer" with the layer hash + logicalLocation := sarifutils.NewLogicalLocation(layerHash, "layer") + if algorithm != "" { + logicalLocation.Properties = sarif.Properties(map[string]interface{}{"algorithm": algorithm}) + } + location.LogicalLocations = append(location.LogicalLocations, logicalLocation) + } + if relativePath != "" { + sarifutils.SetLocationFileName(location, relativePath) + } + } +} + +func convertBinaryPhysicalLocations(cmdResults *Results, run *sarif.Run, result *sarif.Result) { + if patchedLocation := getPatchedBinaryLocation(cmdResults, run); patchedLocation != "" { + for _, location := range result.Locations { + // Patch the location - Reset the uri and region + location.PhysicalLocation = sarifutils.NewPhysicalLocation(patchedLocation) + } + } +} + +func getPatchedBinaryLocation(cmdResults *Results, run *sarif.Run) (patchedLocation string) { + if cmdResults.ResultType == DockerImage { + if patchedLocation = getDockerfileLocationIfExists(run); patchedLocation != "" { + return + } + } + return getWorkflowFileLocationIfExists() +} + +func getDockerfileLocationIfExists(run *sarif.Run) string { + potentialLocations := []string{filepath.Clean("Dockerfile"), sarifutils.GetFullLocationFileName("Dockerfile", run.Invocations)} + for _, location := range potentialLocations { + if exists, err := fileutils.IsFileExists(location, false); err == nil && exists { + return location + } + } + if workspace := os.Getenv(CurrentWorkflowWorkspaceEnvVar); workspace != "" { + if exists, err := fileutils.IsFileExists(filepath.Join(workspace, "Dockerfile"), false); err == nil && exists { + return filepath.Join(workspace, "Dockerfile") + } + } + return "" +} + +func getGithubWorkflowsDirIfExists() string { + if exists, err := fileutils.IsDirExists(GithubBaseWorkflowDir, false); err == nil && exists { + return GithubBaseWorkflowDir + } + if workspace := os.Getenv(CurrentWorkflowWorkspaceEnvVar); workspace != "" { + if exists, err := fileutils.IsDirExists(filepath.Join(workspace, GithubBaseWorkflowDir), false); err == nil && exists { + return filepath.Join(workspace, GithubBaseWorkflowDir) + } + } + return "" +} + +func getWorkflowFileLocationIfExists() (location string) { + workflowName := os.Getenv(CurrentWorkflowNameEnvVar) + if workflowName == "" { + return + } + workflowsDir := getGithubWorkflowsDirIfExists() + if workflowsDir == "" { + return + } + currentWd, err := os.Getwd() + if err != nil { + log.Warn(fmt.Sprintf("Failed to get the current working directory to get workflow file location: %s", err.Error())) + return + } + // Check if exists in the .github/workflows directory as file name or in the content, return the file path or empty string + if files, err := fileutils.ListFiles(workflowsDir, false); err == nil && len(files) > 0 { + for _, file := range files { + if strings.Contains(file, workflowName) { + return strings.TrimPrefix(file, currentWd) + } + } + for _, file := range files { + if content, err := fileutils.ReadFile(file); err == nil && strings.Contains(string(content), workflowName) { + return strings.TrimPrefix(file, currentWd) + } + } + } + return +} + +func getSecretInBinaryMarkdownMsg(cmdResults *Results, result *sarif.Result) string { + if cmdResults.ResultType != Binary && cmdResults.ResultType != DockerImage { + return "" + } + content := "🔒 Found Secrets in Binary" + if cmdResults.ResultType == DockerImage { + content += " docker" + } + content += " scanning:" + return content + getBaseBinaryDescriptionMarkdown(SecretsScan, cmdResults, result) +} + +func getScaInBinaryMarkdownMsg(cmdResults *Results, result *sarif.Result) string { + return sarifutils.GetResultMsgText(result) + getBaseBinaryDescriptionMarkdown(ScaScan, cmdResults, result) +} + +func getBaseBinaryDescriptionMarkdown(subScanType SubScanType, cmdResults *Results, result *sarif.Result) (content string) { + // If in github action, add the workflow name and run number + if workflowLocation := getWorkflowFileLocationIfExists(); workflowLocation != "" { + content += fmt.Sprintf("\nGithub Actions Workflow: %s", workflowLocation) + } + if os.Getenv(CurrentWorkflowRunNumberEnvVar) != "" { + content += fmt.Sprintf("\nRun: %s", os.Getenv(CurrentWorkflowRunNumberEnvVar)) + } + // If is docker image, add the image tag + if cmdResults.ResultType == DockerImage { + if imageTag := getDockerImageTag(cmdResults); imageTag != "" { + content += fmt.Sprintf("\nImage: %s", imageTag) + } + } + var location *sarif.Location + if len(result.Locations) > 0 { + location = result.Locations[0] + } + return content + getBinaryLocationMarkdownString(cmdResults.ResultType, subScanType, location) +} + +func getDockerImageTag(cmdResults *Results) string { + if cmdResults.ResultType != DockerImage || len(cmdResults.ScaResults) == 0 { + return "" + } + for _, scaResults := range cmdResults.ScaResults { + if scaResults.Name != "" { + return scaResults.Name + } + } + return filepath.Base(cmdResults.ScaResults[0].Target) +} + +// If command is docker prepare the markdown string for the location: +// * Layer: +// * Filepath: +// * Evidence: +func getBinaryLocationMarkdownString(commandType CommandType, subScanType SubScanType, location *sarif.Location) (content string) { + if location == nil { + return "" + } + if commandType == DockerImage { + if layer, algorithm := getDockerLayer(location); layer != "" { + if algorithm != "" { + content += fmt.Sprintf("\nLayer (%s): %s", algorithm, layer) + } else { + content += fmt.Sprintf("\nLayer: %s", layer) + } + } + } + if subScanType != SecretsScan { + return + } + if locationFilePath := sarifutils.GetLocationFileName(location); locationFilePath != "" { + content += fmt.Sprintf("\nFilepath: %s", locationFilePath) + } + if snippet := sarifutils.GetLocationSnippet(location); snippet != "" { + content += fmt.Sprintf("\nEvidence: %s", snippet) + } + return +} + +func getDockerLayer(location *sarif.Location) (layer, algorithm string) { + // If location has logical location with kind "layer" return it + if logicalLocation := sarifutils.GetLogicalLocation("layer", location); logicalLocation != nil && logicalLocation.Name != nil { + layer = *logicalLocation.Name + if algorithmValue, ok := logicalLocation.Properties["algorithm"].(string); ok { + algorithm = algorithmValue + } + return + } + return +} + +// Match: +// Extract algorithm, hash and relative path +func getLayerContentFromPath(content string) (algorithm string, layerHash string, relativePath string) { + matches := dockerJasLocationPathPattern.FindStringSubmatch(content) + if len(matches) == 0 { + return + } + algorithm = matches[dockerJasLocationPathPattern.SubexpIndex("algorithm")] + layerHash = matches[dockerJasLocationPathPattern.SubexpIndex("hash")] + relativePath = matches[dockerJasLocationPathPattern.SubexpIndex("relativePath")] + return +} + +// Match: ://:/ +// Extract algorithm and hash +func getLayerContentFromComponentId(componentId string) (algorithm string, layerHash string) { + matches := dockerScaComponentNamePattern.FindStringSubmatch(componentId) + if len(matches) == 0 { + return + } + algorithm = matches[dockerScaComponentNamePattern.SubexpIndex("algorithm")] + layerHash = matches[dockerScaComponentNamePattern.SubexpIndex("hash")] + return +} + +// According to the SARIF specification: +// To determine whether a result from a subsequent run is logically the same as a result from the baseline, +// there must be a way to use information contained in the result to construct a stable identifier for the result. We refer to this identifier as a fingerprint. +// A result management system SHOULD construct a fingerprint by using information contained in the SARIF file such as: +// The name of the tool that produced the result, the rule id, the file system path to the analysis target... +func calculateResultFingerprints(resultType CommandType, run *sarif.Run, result *sarif.Result) error { + if !resultType.IsTargetBinary() { + return nil + } + ids := []string{sarifutils.GetRunToolName(run), sarifutils.GetResultRuleId(result)} + for _, location := range sarifutils.GetResultFileLocations(result) { + ids = append(ids, strings.ReplaceAll(location, string(filepath.Separator), "/")) + } + ids = append(ids, sarifutils.GetResultLocationSnippets(result)...) + // Calculate the hash value and set the fingerprint to the result + hashValue, err := Md5Hash(ids...) + if err != nil { + return err + } + sarifutils.SetResultFingerprint(jfrogFingerprintAlgorithmName, hashValue, result) + return nil +} + // Splits scan responses into aggregated lists of violations, vulnerabilities and licenses. func SplitScanResults(results []*ScaScanResult) ([]services.Violation, []services.Vulnerability, []services.License) { var violations []services.Violation @@ -537,7 +877,7 @@ func writeJsonResults(results *Results) (resultsPath string, err error) { err = e } }() - bytesRes, err := JSONMarshal(&results) + bytesRes, err := JSONMarshalNotEscaped(&results) if errorutils.CheckError(err) != nil { return } @@ -554,7 +894,20 @@ func writeJsonResults(results *Results) (resultsPath string, err error) { return } -func JSONMarshal(t interface{}) ([]byte, error) { +func WriteSarifResultsAsString(report *sarif.Report, escape bool) (sarifStr string, err error) { + var out []byte + if escape { + out, err = json.Marshal(report) + } else { + out, err = JSONMarshalNotEscaped(report) + } + if err != nil { + return "", errorutils.CheckError(err) + } + return clientUtils.IndentJson(out), nil +} + +func JSONMarshalNotEscaped(t interface{}) ([]byte, error) { buffer := &bytes.Buffer{} encoder := json.NewEncoder(buffer) encoder.SetEscapeHTML(false) @@ -563,7 +916,7 @@ func JSONMarshal(t interface{}) ([]byte, error) { } func PrintJson(output interface{}) error { - results, err := JSONMarshal(output) + results, err := JSONMarshalNotEscaped(output) if err != nil { return errorutils.CheckError(err) } @@ -572,11 +925,11 @@ func PrintJson(output interface{}) error { } func PrintSarif(results *Results, isMultipleRoots, includeLicenses bool) error { - sarifReport, err := GenereateSarifReportFromResults(results, isMultipleRoots, includeLicenses, nil) + sarifReport, err := GenerateSarifReportFromResults(results, isMultipleRoots, includeLicenses, nil) if err != nil { return err } - sarifFile, err := sarifutils.ConvertSarifReportToString(sarifReport) + sarifFile, err := WriteSarifResultsAsString(sarifReport, false) if err != nil { return err } @@ -761,7 +1114,11 @@ func getSecuritySummaryFindings(cves []services.Cve, issueId string, components } if len(cves) == 0 { // XRAY-ID, no scanners for them - uniqueFindings[jasutils.NotCovered.String()] += 1 + status := jasutils.NotScanned + if len(applicableRuns) > 0 { + status = jasutils.NotCovered + } + uniqueFindings[status.String()] += 1 } return uniqueFindings } diff --git a/utils/resultwriter_test.go b/utils/resultwriter_test.go index 0c9a5a62..d009c777 100644 --- a/utils/resultwriter_test.go +++ b/utils/resultwriter_test.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "os" "path/filepath" "sort" @@ -11,6 +12,8 @@ import ( "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + clientTests "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" @@ -242,7 +245,7 @@ func TestConvertXrayScanToSimpleJson(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 15}, ImpactedDependencyName: "component-A", }, }, @@ -250,7 +253,7 @@ func TestConvertXrayScanToSimpleJson(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 15}, ImpactedDependencyName: "component-B", }, }, @@ -258,7 +261,7 @@ func TestConvertXrayScanToSimpleJson(t *testing.T) { Summary: "summary-2", IssueId: "XRAY-2", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 9}, ImpactedDependencyName: "component-B", }, }, @@ -288,7 +291,7 @@ func TestConvertXrayScanToSimpleJson(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 15}, ImpactedDependencyName: "component-A", }, }, @@ -296,7 +299,7 @@ func TestConvertXrayScanToSimpleJson(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 15}, ImpactedDependencyName: "component-B", }, }, @@ -305,7 +308,7 @@ func TestConvertXrayScanToSimpleJson(t *testing.T) { { LicenseKey: "license-1", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 9}, ImpactedDependencyName: "component-B", }, }, @@ -391,7 +394,7 @@ func TestConvertXrayScanToSimpleJson(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - results := NewAuditResults() + results := NewAuditResults(SourceCode) scaScanResult := ScaScanResult{XrayResults: []services.ScanResponse{tc.result}} results.ScaResults = append(results.ScaResults, &scaScanResult) output, err := ConvertXrayScanToSimpleJson(results, false, tc.includeLicenses, true, tc.allowedLicenses) @@ -436,7 +439,7 @@ func TestJSONMarshall(t *testing.T) { for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { - printedString, err := JSONMarshal(tc.resultString) + printedString, err := JSONMarshalNotEscaped(tc.resultString) assert.NoError(t, err) assert.Equal(t, tc.expectedResult, string(printedString)) }) @@ -573,6 +576,273 @@ func TestGetSummary(t *testing.T) { } } +func TestGetLayerContentFromComponentId(t *testing.T) { + testCases := []struct { + name string + path string + expectedAlgorithm string + expectedLayerHash string + }{ + { + name: "Valid path", + path: "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + expectedAlgorithm: "sha256", + expectedLayerHash: "cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", + }, + { + name: "Invalid path - not hex", + path: "sha256__NOT_HEX.tar", + }, + { + name: "Invalid path - no algorithm", + path: "_cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + }, + { + name: "Invalid path - no suffix", + path: "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + algorithm, layerHash := getLayerContentFromComponentId(tc.path) + assert.Equal(t, tc.expectedAlgorithm, algorithm) + assert.Equal(t, tc.expectedLayerHash, layerHash) + }) + } +} + +func preparePatchTestEnv(t *testing.T) (string, string, func()) { + currentWd, err := os.Getwd() + assert.NoError(t, err) + wd, cleanUpTempDir := tests.CreateTempDirWithCallbackAndAssert(t) + cleanUpWd := clientTests.ChangeDirWithCallback(t, currentWd, wd) + dockerfileDir := filepath.Join(wd, "DockerfileDir") + err = fileutils.CreateDirIfNotExist(dockerfileDir) + // Prepare env content + assert.NoError(t, err) + createDummyDockerfile(t, dockerfileDir) + createDummyGithubWorkflow(t, dockerfileDir) + createDummyGithubWorkflow(t, wd) + return wd, dockerfileDir, func() { + cleanUpWd() + cleanUpTempDir() + } +} + +func createDummyGithubWorkflow(t *testing.T, baseDir string) { + assert.NoError(t, fileutils.CreateDirIfNotExist(filepath.Join(baseDir, GithubBaseWorkflowDir))) + assert.NoError(t, os.WriteFile(filepath.Join(baseDir, GithubBaseWorkflowDir, "workflowFile.yml"), []byte("workflow name"), 0644)) +} + +func createDummyDockerfile(t *testing.T, baseDir string) { + assert.NoError(t, os.WriteFile(filepath.Join(baseDir, "Dockerfile"), []byte("Dockerfile data"), 0644)) +} + +func TestPatchRunsToPassIngestionRules(t *testing.T) { + wd, dockerfileDir, cleanUp := preparePatchTestEnv(t) + defer cleanUp() + + testCases := []struct { + name string + cmdResult *Results + subScan SubScanType + withEnvVars bool + withDockerfile bool + input []*sarif.Run + expectedResults []*sarif.Run + }{ + { + name: "No runs", + cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, + subScan: SecretsScan, + input: []*sarif.Run{}, + expectedResults: []*sarif.Run{}, + }, + { + name: "Build scan - SCA", + cmdResult: &Results{ResultType: Build, ScaResults: []*ScaScanResult{{Name: "buildName (buildNumber)"}}}, + subScan: ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file")))), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file"))), + }, + }, + { + name: "Docker image scan - SCA", + cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, + subScan: ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties("applicability", "applicable", sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg"))). + WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), + }, + ), + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties("applicability", "applicable", + sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), + ), + ).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), + }), + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), + ), + ), + }, + }, + { + name: "Docker image scan - with env vars", + cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, + subScan: ScaScan, + withEnvVars: true, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), + // No location, should be removed in the output + sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "eda26ae830c578197aeda65a82d7f093", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( + sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation(filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml"))), + ), + ), + ), + }, + }, + { + name: "Docker image scan - with Dockerfile in wd", + cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, + subScan: ScaScan, + withEnvVars: true, + withDockerfile: true, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(dockerfileDir, + sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(dockerfileDir, + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "8cbd7268a4d20f2358ba2667ebd18956", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( + sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation("Dockerfile")), + ), + ), + ), + }, + }, + { + name: "Docker image scan - Secrets", + cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, + subScan: SecretsScan, + input: []*sarif.Run{ + sarifutils.CreateRunNameWithResults("some tool name", + sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "unpacked", "filesystem", "blobs", "sha1", "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "usr", "src", "app", "server", "index.js"))), + ).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), + }), + }, + expectedResults: []*sarif.Run{ + { + Tool: sarif.Tool{ + Driver: sarifutils.CreateDummyDriver(patchedBinarySecretScannerToolName, "", &sarif.ReportingDescriptor{ + ID: "rule", + ShortDescription: sarif.NewMultiformatMessageString("[Secret in Binary found] "), + }), + }, + Invocations: []*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}, + Results: []*sarif.Result{ + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("🔒 Found Secrets in Binary docker scanning:\nImage: dockerImage:imageVersion\nLayer (sha1): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: %s\nEvidence: snippet", filepath.Join("usr", "src", "app", "server", "index.js")), "", jfrogFingerprintAlgorithmName, "dee156c9fd75a4237102dc8fb29277a2", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation(filepath.Join("usr", "src", "app", "server", "index.js"), "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "layer", "algorithm", "sha1"), + ), + }, + }, + }, + }, + { + name: "Binary scan - SCA", + cmdResult: &Results{ResultType: Binary, ScaResults: []*ScaScanResult{{Target: filepath.Join(wd, "dir", "binary")}}}, + subScan: ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "binary"))), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithFingerprint("", "", jfrogFingerprintAlgorithmName, "e72a936dc73acbc4283a93230ff9b6e8", sarifutils.CreateDummyLocationInPath(filepath.Join("dir", "binary"))), + ), + }, + }, + { + name: "Audit scan - SCA", + cmdResult: &Results{ResultType: SourceCode, ScaResults: []*ScaScanResult{{Target: wd}}}, + subScan: ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(filepath.Join(wd, "Package-Descriptor")), + // No location, should be removed in the output + sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath("Package-Descriptor"), + ), + }, + }, + { + name: "Audit scan - Secrets", + cmdResult: &Results{ResultType: SourceCode, ScaResults: []*ScaScanResult{{Target: wd}}}, + subScan: SecretsScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file"))), + // No location, should be removed in the output + sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file")), + ), + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.withEnvVars { + cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "workflow name") + defer cleanFileEnv() + cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "123") + defer cleanRunNumEnv() + } else { + // Since the the env are provided by the + cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "") + defer cleanFileEnv() + cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "") + defer cleanRunNumEnv() + } + if tc.withDockerfile { + revertWd := clientTests.ChangeDirWithCallback(t, wd, dockerfileDir) + defer revertWd() + } + patchRunsToPassIngestionRules(tc.subScan, tc.cmdResult, tc.input...) + assert.ElementsMatch(t, tc.expectedResults, tc.input) + }) + } +} + func getDummyScaTestResults(vulnerability, violation bool) (responses []services.ScanResponse) { response := services.ScanResponse{} if vulnerability { diff --git a/utils/securityJobSummary.go b/utils/securityJobSummary.go index a1a4be41..210173af 100644 --- a/utils/securityJobSummary.go +++ b/utils/securityJobSummary.go @@ -3,6 +3,7 @@ package utils import ( "errors" "fmt" + "os" "path/filepath" "sort" "strings" @@ -14,18 +15,16 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/formats" + "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/resources" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" ) const ( - Build SecuritySummarySection = "Build-info Scans" - Binary SecuritySummarySection = "Artifact Scans" - Modules SecuritySummarySection = "Source Code Scans" - Docker SecuritySummarySection = "Docker Image Scans" - Curation SecuritySummarySection = "Curation Audit" - PreFormat HtmlTag = "
%s
" ImgTag HtmlTag = "\"%s\"" CenterContent HtmlTag = "
%s
" @@ -33,21 +32,17 @@ const ( Link HtmlTag = "%s" NewLine HtmlTag = "
%s" DetailsWithSummary HtmlTag = "
%s%s
" - DetailsOpenWithSummary HtmlTag = "

%s

%s
" - RedColor HtmlTag = "%s" - OrangeColor HtmlTag = "%s" - GreenColor HtmlTag = "%s" + DetailsOpenWithSummary HtmlTag = "

%s

%s\n
" TabTag HtmlTag = " %s" - ApplicableStatusCount SeverityStatus = "%d Applicable" - NotApplicableStatusCount SeverityStatus = "%d Not Applicable" + ApplicableStatusCount SeverityDisplayStatus = "%d Applicable" + NotApplicableStatusCount SeverityDisplayStatus = "%d Not Applicable" maxWatchesInLine = 4 ) -type SecuritySummarySection string type HtmlTag string -type SeverityStatus string +type SeverityDisplayStatus string func (c HtmlTag) Format(args ...any) string { return fmt.Sprintf(string(c), args...) @@ -57,7 +52,7 @@ func (c HtmlTag) FormatInt(value int) string { return fmt.Sprintf(string(c), fmt.Sprintf("%d", value)) } -func (s SeverityStatus) Format(count int) string { +func (s SeverityDisplayStatus) Format(count int) string { return fmt.Sprintf(string(s), count) } @@ -71,38 +66,38 @@ func getStatusIcon(failed bool) string { type SecurityJobSummary struct{} -func NewCurationSummary(cmdResult formats.ResultsSummary) (summary ScanCommandResultSummary) { - summary.ResultType = Curation - summary.Summary = cmdResult - return -} - -func newResultSummary(cmdResults *Results, section SecuritySummarySection, serverDetails *config.ServerDetails, vulnerabilitiesReqested, violationsReqested bool) (summary ScanCommandResultSummary) { - summary.ResultType = section +func newResultSummary(cmdResults *Results, cmdType CommandType, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary) { + summary.ResultType = cmdType summary.Args = &ResultSummaryArgs{BaseJfrogUrl: serverDetails.Url} - summary.Summary = ToSummary(cmdResults, vulnerabilitiesReqested, violationsReqested) + summary.Summary = ToSummary(cmdResults, vulnerabilitiesRequested, violationsRequested) return } -func NewBuildScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesReqested, violationsReqested bool, buildName, buildNumber string) (summary ScanCommandResultSummary) { - summary = newResultSummary(cmdResults, Build, serverDetails, vulnerabilitiesReqested, violationsReqested) +func NewBuildScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool, buildName, buildNumber string) (summary ScanCommandResultSummary) { + summary = newResultSummary(cmdResults, Build, serverDetails, vulnerabilitiesRequested, violationsRequested) summary.Args.BuildName = buildName summary.Args.BuildNumbers = []string{buildNumber} return } -func NewDockerScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesReqested, violationsReqested bool, dockerImage string) (summary ScanCommandResultSummary) { - summary = newResultSummary(cmdResults, Docker, serverDetails, vulnerabilitiesReqested, violationsReqested) +func NewDockerScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool, dockerImage string) (summary ScanCommandResultSummary) { + summary = newResultSummary(cmdResults, DockerImage, serverDetails, vulnerabilitiesRequested, violationsRequested) summary.Args.DockerImage = dockerImage return } -func NewBinaryScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesReqested, violationsReqested bool) (summary ScanCommandResultSummary) { - return newResultSummary(cmdResults, Binary, serverDetails, vulnerabilitiesReqested, violationsReqested) +func NewBinaryScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary) { + return newResultSummary(cmdResults, Binary, serverDetails, vulnerabilitiesRequested, violationsRequested) } -func NewAuditScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesReqested, violationsReqested bool) (summary ScanCommandResultSummary) { - return newResultSummary(cmdResults, Modules, serverDetails, vulnerabilitiesReqested, violationsReqested) +func NewAuditScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary) { + return newResultSummary(cmdResults, SourceCode, serverDetails, vulnerabilitiesRequested, violationsRequested) +} + +func NewCurationSummary(cmdResult formats.ResultsSummary) (summary ScanCommandResultSummary) { + summary.ResultType = Curation + summary.Summary = cmdResult + return } type ResultSummaryArgs struct { @@ -133,13 +128,18 @@ func (rsa ResultSummaryArgs) ToArgs(index commandsummary.Index) (args []string) args = append(args, rsa.BuildName) args = append(args, rsa.BuildNumbers...) } else if index == commandsummary.DockerScan { - args = append(args, rsa.DockerImage) + image := rsa.DockerImage + // if user did not provide image tag, add latest + if !strings.Contains(image, ":") { + image += ":latest" + } + args = append(args, image) } return } type ScanCommandResultSummary struct { - ResultType SecuritySummarySection `json:"resultType"` + ResultType CommandType `json:"resultType"` Args *ResultSummaryArgs `json:"args,omitempty"` Summary formats.ResultsSummary `json:"summary"` } @@ -149,12 +149,16 @@ func NewSecurityJobSummary() (js *commandsummary.CommandSummary, err error) { return commandsummary.New(&SecurityJobSummary{}, "security") } -// Record the security command outputs -func RecordSecurityCommandSummary(content ScanCommandResultSummary) (err error) { +func getRecordManager() (manager *commandsummary.CommandSummary, err error) { if !commandsummary.ShouldRecordSummary() { return } - manager, err := NewSecurityJobSummary() + return NewSecurityJobSummary() +} + +// Record the security command outputs +func RecordSecurityCommandSummary(content ScanCommandResultSummary) (err error) { + manager, err := getRecordManager() if err != nil || manager == nil { return } @@ -163,12 +167,64 @@ func RecordSecurityCommandSummary(content ScanCommandResultSummary) (err error) return } updateSummaryNamesToRelativePath(&content.Summary, wd) - if index := getDataIndexFromSection(content.ResultType); index != "" { + if index := getDataIndexFromCommandType(content.ResultType); index != "" { return recordIndexData(manager, content, index) } return manager.Record(content) } +func RecordSarifOutput(cmdResults *Results) (err error) { + manager, err := getRecordManager() + if err != nil || manager == nil { + return + } + extended := true + if !extended && !commandsummary.StaticMarkdownConfig.IsExtendedSummary() { + log.Info("Results can be uploaded to Github security tab automatically by upgrading your JFrog subscription.") + return + } + sarifReport, err := GenerateSarifReportFromResults(cmdResults, true, false, nil) + if err != nil { + return err + } + out, err := JSONMarshalNotEscaped(sarifReport) + if err != nil { + return errorutils.CheckError(err) + } + return manager.RecordWithIndex(out, commandsummary.SarifReport) +} + +func CombineSarifOutputFiles(dataFilePaths []string) (data []byte, err error) { + if len(dataFilePaths) == 0 { + return + } + // Load the content of the files + reports := []*sarif.Report{} + for _, dataFilePath := range dataFilePaths { + if report, e := loadSarifReport(dataFilePath); e != nil { + err = errors.Join(err, e) + } else { + reports = append(reports, report) + } + } + if err != nil { + return + } + combined, err := sarifutils.CombineReports(reports...) + if err != nil { + return + } + return JSONMarshalNotEscaped(combined) +} + +func loadSarifReport(dataFilePath string) (report *sarif.Report, err error) { + fileData, err := os.ReadFile(dataFilePath) + if err != nil { + return + } + return sarif.FromBytes(fileData) +} + func updateSummaryNamesToRelativePath(summary *formats.ResultsSummary, wd string) { for i, scan := range summary.Scans { if scan.Target == "" { @@ -184,15 +240,15 @@ func updateSummaryNamesToRelativePath(summary *formats.ResultsSummary, wd string } } -func getDataIndexFromSection(section SecuritySummarySection) commandsummary.Index { - switch section { +func getDataIndexFromCommandType(cmdType CommandType) commandsummary.Index { + switch cmdType { case Build: return commandsummary.BuildScan case Binary: return commandsummary.BinariesScan - case Modules: + case SourceCode: return commandsummary.BinariesScan - case Docker: + case DockerImage: return commandsummary.DockerScan } // No index for the section @@ -213,11 +269,11 @@ func recordIndexData(manager *commandsummary.CommandSummary, content ScanCommand return } -func newScanCommandResultSummary(resultType SecuritySummarySection, args *ResultSummaryArgs, scans ...formats.ScanSummary) ScanCommandResultSummary { +func newScanCommandResultSummary(resultType CommandType, args *ResultSummaryArgs, scans ...formats.ScanSummary) ScanCommandResultSummary { return ScanCommandResultSummary{ResultType: resultType, Args: args, Summary: formats.ResultsSummary{Scans: scans}} } -func loadContent(dataFiles []string, filterSections ...SecuritySummarySection) ([]formats.ResultsSummary, ResultSummaryArgs, error) { +func loadContent(dataFiles []string, filterSections ...CommandType) ([]formats.ResultsSummary, ResultSummaryArgs, error) { data := []formats.ResultsSummary{} args := ResultSummaryArgs{} for _, dataFilePath := range dataFiles { @@ -282,7 +338,7 @@ func GenerateSecuritySectionMarkdown(curationData []formats.ResultsSummary) (mar return } // Create the markdown content - markdown += fmt.Sprintf("\n\n#### %s\n| Audit Summary | Project name | Audit Details |\n|--------|--------|---------|", Curation) + markdown += "\n\n| Audit Summary | Project name | Audit Details |\n|--------|--------|---------|" for i := range curationData { for _, summary := range curationData[i].Scans { status := getStatusIcon(false) @@ -292,7 +348,7 @@ func GenerateSecuritySectionMarkdown(curationData []formats.ResultsSummary) (mar markdown += fmt.Sprintf("\n| %s | %s | %s |", status, summary.Target, PreFormat.Format(getCurationDetailsString(summary))) } } - markdown = DetailsOpenWithSummary.Format("🔒 Security Summary", markdown) + markdown = "\n" + DetailsOpenWithSummary.Format("🔒 Curation Audit", markdown) return } @@ -327,7 +383,7 @@ func getCurationDetailsString(summary formats.ScanSummary) (content string) { var blocked []blockedPackageByType // Sort the blocked packages by name for _, blockTypeValue := range summary.CuratedPackages.Blocked { - blocked = append(blocked, toBlockedPackgeByType(blockTypeValue)) + blocked = append(blocked, toBlockedPackageByType(blockTypeValue)) } sort.Slice(blocked, func(i, j int) bool { return blocked[i].BlockedType > blocked[j].BlockedType @@ -342,7 +398,7 @@ func getCurationDetailsString(summary formats.ScanSummary) (content string) { return } -func toBlockedPackgeByType(blockTypeValue formats.BlockedPackages) blockedPackageByType { +func toBlockedPackageByType(blockTypeValue formats.BlockedPackages) blockedPackageByType { return blockedPackageByType{BlockedType: formatPolicyAndCond(blockTypeValue.Policy, blockTypeValue.Condition), BlockedSummary: blockTypeValue.Packages} } @@ -563,8 +619,8 @@ func getSeverityStatusesCountString(statusCounts map[string]int) string { return generateSeverityStatusesCountString(getSeverityDisplayStatuses(statusCounts)) } -func getSeverityDisplayStatuses(statusCounts map[string]int) (displayData map[SeverityStatus]int) { - displayData = map[SeverityStatus]int{} +func getSeverityDisplayStatuses(statusCounts map[string]int) (displayData map[SeverityDisplayStatus]int) { + displayData = map[SeverityDisplayStatus]int{} for status, count := range statusCounts { switch status { case jasutils.Applicability.String(): @@ -576,7 +632,7 @@ func getSeverityDisplayStatuses(statusCounts map[string]int) (displayData map[Se return displayData } -func generateSeverityStatusesCountString(displayData map[SeverityStatus]int) string { +func generateSeverityStatusesCountString(displayData map[SeverityDisplayStatus]int) string { if len(displayData) == 0 { return "" } diff --git a/utils/securityJobSummary_test.go b/utils/securityJobSummary_test.go index e447a7fb..d5890907 100644 --- a/utils/securityJobSummary_test.go +++ b/utils/securityJobSummary_test.go @@ -39,7 +39,7 @@ var ( func TestSaveLoadData(t *testing.T) { testDockerScanSummary := ScanCommandResultSummary{ - ResultType: Docker, + ResultType: DockerImage, Args: &ResultSummaryArgs{ BaseJfrogUrl: testPlatformUrl, DockerImage: "dockerImage:version", @@ -133,7 +133,7 @@ func TestSaveLoadData(t *testing.T) { testCases := []struct { name string content []ScanCommandResultSummary - filterSections []SecuritySummarySection + filterSections []CommandType expectedArgs ResultSummaryArgs expectedContent []formats.ResultsSummary }{ @@ -156,7 +156,7 @@ func TestSaveLoadData(t *testing.T) { }, { name: "Multiple scans with filter", - filterSections: []SecuritySummarySection{Curation}, + filterSections: []CommandType{Curation}, content: []ScanCommandResultSummary{testDockerScanSummary, testBinaryScanSummary, testBuildScanSummary, testCurationSummary}, expectedContent: []formats.ResultsSummary{testCurationSummary.Summary}, }, @@ -169,7 +169,7 @@ func TestSaveLoadData(t *testing.T) { // Save the data for i := range testCase.content { updateSummaryNamesToRelativePath(&testCase.content[i].Summary, tempDir) - data, err := JSONMarshal(&testCase.content[i]) + data, err := JSONMarshalNotEscaped(&testCase.content[i]) assert.NoError(t, err) dataFilePath := filepath.Join(tempDir, fmt.Sprintf("data_%s_%d.json", testCase.name, i)) assert.NoError(t, os.WriteFile(dataFilePath, data, 0644)) diff --git a/utils/severityutils/severity.go b/utils/severityutils/severity.go index a4077d31..f33bf31c 100644 --- a/utils/severityutils/severity.go +++ b/utils/severityutils/severity.go @@ -239,6 +239,10 @@ func ParseToSeverityDetails(severity string, sarifSeverity, pretty bool, applica // -- Getters functions (With default values) -- func GetAsDetails(severity Severity, applicabilityStatus jasutils.ApplicabilityStatus, pretty bool) formats.SeverityDetails { + if applicabilityStatus == jasutils.NotScanned { + // Pass 'NotCovered' as default value to get priority, since 'NotScanned' returns 0 priority for all severities + applicabilityStatus = jasutils.NotCovered + } return GetSeverityDetails(severity, applicabilityStatus).ToDetails(severity, pretty) } diff --git a/utils/test_mocks.go b/utils/test_mocks.go index 09e61942..469b6c19 100644 --- a/utils/test_mocks.go +++ b/utils/test_mocks.go @@ -8,12 +8,17 @@ import ( "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" + "os" "testing" ) -const TestMsi = "27e175b8-e525-11ee-842b-7aa2c69b8f1f" -const TestScaScanId = "3d90ec4b-cf33-4846-6831-4bf9576f2235" -const TestMoreInfoUrl = "https://www.jfrog.com" +const ( + TestMsi = "27e175b8-e525-11ee-842b-7aa2c69b8f1f" + TestScaScanId = "3d90ec4b-cf33-4846-6831-4bf9576f2235" + TestMoreInfoUrl = "https://www.jfrog.com" + TestConfigProfileName = "default-profile" + versionApiUrl = "/%s/api/v1/system/version" +) type restsTestHandler func(w http.ResponseWriter, r *http.Request) @@ -31,11 +36,17 @@ func CreateXscRestsMockServer(t *testing.T, testHandler restsTestHandler) (*http return testServer, serverDetails, serviceManager } +func CreateXrayRestsMockServer(testHandler restsTestHandler) (*httptest.Server, *config.ServerDetails) { + testServer := CreateRestsMockServer(testHandler) + serverDetails := &config.ServerDetails{Url: testServer.URL + "/", XrayUrl: testServer.URL + "/xray/"} + return testServer, serverDetails +} + func XscServer(t *testing.T, xscVersion string) (*httptest.Server, *config.ServerDetails) { serverMock, serverDetails, _ := CreateXscRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.RequestURI == "/xsc/api/v1/system/version" { + if r.RequestURI == fmt.Sprintf(versionApiUrl, "xsc") { _, err := w.Write([]byte(fmt.Sprintf(`{"xsc_version": "%s"}`, xscVersion))) - if err != nil { + if !assert.NoError(t, err) { return } } @@ -43,7 +54,50 @@ func XscServer(t *testing.T, xscVersion string) (*httptest.Server, *config.Serve if r.Method == http.MethodPost { w.WriteHeader(http.StatusCreated) _, err := w.Write([]byte(fmt.Sprintf(`{"multi_scan_id": "%s"}`, TestMsi))) - if err != nil { + if !assert.NoError(t, err) { + return + } + } + } + if r.RequestURI == "/xsc/api/v1/profile/"+TestConfigProfileName { + if r.Method == http.MethodGet { + w.WriteHeader(http.StatusOK) + content, err := os.ReadFile("../../tests/testdata/other/configProfile/configProfileExample.json") + if !assert.NoError(t, err) { + return + } + _, err = w.Write(content) + if !assert.NoError(t, err) { + return + } + } + } + }) + return serverMock, serverDetails +} + +func XrayServer(t *testing.T, xrayVersion string) (*httptest.Server, *config.ServerDetails) { + serverMock, serverDetails := CreateXrayRestsMockServer(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == fmt.Sprintf(versionApiUrl, "xray") { + _, err := w.Write([]byte(fmt.Sprintf(`{"xray_version": "%s", "xray_revision": "xxx"}`, xrayVersion))) + if !assert.NoError(t, err) { + return + } + } + if r.RequestURI == "/xray/api/v1/entitlements/feature/contextual_analysis" { + if r.Method == http.MethodGet { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"entitled": true, "feature_id": "contextual_analysis"}`)) + if !assert.NoError(t, err) { + return + } + } + } + if r.RequestURI == "/xray/api/v1/scan/graph" { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusCreated) + _, err := w.Write([]byte(fmt.Sprintf(`{"scan_id" : "%s"}`, TestScaScanId))) + if !assert.NoError(t, err) { return } } diff --git a/utils/utils.go b/utils/utils.go index b4aa871e..1b4d2da7 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,8 @@ package utils import ( + "crypto" + "encoding/hex" "fmt" "strings" ) @@ -38,14 +40,48 @@ func (v ViolationIssueType) String() string { type SubScanType string +const ( + SourceCode CommandType = "source_code" + Binary CommandType = "binary" + DockerImage CommandType = "docker_image" + Build CommandType = "build" + Curation CommandType = "curation" + SBOM CommandType = "SBOM" +) + +type CommandType string + func (s SubScanType) String() string { return string(s) } +func (s CommandType) IsTargetBinary() bool { + return s == Binary || s == DockerImage +} + func GetAllSupportedScans() []SubScanType { return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan} } +func Md5Hash(values ...string) (string, error) { + return toHash(crypto.MD5, values...) +} + +func Sha1Hash(values ...string) (string, error) { + return toHash(crypto.SHA1, values...) +} + +func toHash(hash crypto.Hash, values ...string) (string, error) { + h := hash.New() + for _, ob := range values { + _, err := fmt.Fprint(h, ob) + if err != nil { + return "", err + } + } + return hex.EncodeToString(h.Sum(nil)), nil +} + // map[string]string to []string (key=value format) func ToCommandEnvVars(envVarsMap map[string]string) (converted []string) { converted = make([]string, 0, len(envVarsMap)) diff --git a/utils/xsc/configprofile.go b/utils/xsc/configprofile.go new file mode 100644 index 00000000..71eebef2 --- /dev/null +++ b/utils/xsc/configprofile.go @@ -0,0 +1,32 @@ +package xsc + +import ( + "fmt" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/jfrog/jfrog-client-go/xsc/services" +) + +func GetConfigProfile(serverDetails *config.ServerDetails, profileName string) (*services.ConfigProfile, error) { + xscManager, err := CreateXscServiceManager(serverDetails) + if err != nil { + return nil, err + } + + xscVersion, err := xscManager.GetVersion() + if err != nil { + return nil, fmt.Errorf("failed to get XSC service version '%s': %q", profileName, err) + } + + if err = clientutils.ValidateMinimumVersion(clientutils.Xsc, xscVersion, services.ConfigProfileMinXscVersion); err != nil { + log.Info("Minimal Xsc version required to utilize config profile is '%s'. All configurations will be induced from provided Env vars and files") + return nil, err + } + + configProfile, err := xscManager.GetConfigProfile(profileName) + if err != nil { + err = fmt.Errorf("failed to get config profile '%s': %q", profileName, err) + } + return configProfile, err +} diff --git a/utils/xsc/configprofile_test.go b/utils/xsc/configprofile_test.go new file mode 100644 index 00000000..86afca0a --- /dev/null +++ b/utils/xsc/configprofile_test.go @@ -0,0 +1,36 @@ +package xsc + +import ( + "encoding/json" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-client-go/xsc/services" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestGetConfigProfile_ValidRequest_SuccessExpected(t *testing.T) { + mockServer, serverDetails := utils.XscServer(t, services.ConfigProfileMinXscVersion) + defer mockServer.Close() + + configProfile, err := GetConfigProfile(serverDetails, utils.TestConfigProfileName) + assert.NoError(t, err) + + profileFileContent, err := os.ReadFile("../../tests/testdata/other/configProfile/configProfileExample.json") + assert.NoError(t, err) + + var configProfileForComparison services.ConfigProfile + err = json.Unmarshal(profileFileContent, &configProfileForComparison) + assert.NoError(t, err) + + assert.Equal(t, &configProfileForComparison, configProfile) +} + +func TestGetConfigProfile_TooLowXscVersion_FailureExpected(t *testing.T) { + mockServer, serverDetails := utils.XscServer(t, "1.0.0") + defer mockServer.Close() + + configProfile, err := GetConfigProfile(serverDetails, utils.TestConfigProfileName) + assert.Error(t, err) + assert.Nil(t, configProfile) +} diff --git a/utils/xsc/errorreport.go b/utils/xsc/errorreport.go index a2654893..ee14e889 100644 --- a/utils/xsc/errorreport.go +++ b/utils/xsc/errorreport.go @@ -3,7 +3,9 @@ package xsc import ( "fmt" "github.com/jfrog/jfrog-cli-core/v2/utils/config" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/jfrog/jfrog-client-go/xsc" "github.com/jfrog/jfrog-client-go/xsc/services" ) @@ -21,5 +23,30 @@ func ReportError(serverDetails *config.ServerDetails, errorToReport error, sourc Source: source, Message: errorToReport.Error(), } - return SendXscLogMessageIfEnabled(errorLog, xscManager) + return sendXscLogMessageIfEnabled(errorLog, xscManager) +} + +func sendXscLogMessageIfEnabled(errorLog *services.ExternalErrorLog, xscManager *xsc.XscServicesManager) error { + if !IsReportLogErrorEventPossible(xscManager) { + return nil + } + return xscManager.SendXscLogErrorRequest(errorLog) +} + +// Determines if reporting the error is feasible. +func IsReportLogErrorEventPossible(xscManager *xsc.XscServicesManager) bool { + xscVersion, err := xscManager.GetVersion() + if err != nil { + log.Debug(fmt.Sprintf("failed to check availability of Xsc service:%s\nReporting to JFrog analytics is skipped...", err.Error())) + return false + } + if xscVersion == "" { + log.Debug("Xsc service is not available. Reporting to JFrog analytics is skipped...") + return false + } + if err = clientutils.ValidateMinimumVersion(clientutils.Xsc, xscVersion, minXscVersionForErrorReport); err != nil { + log.Debug(err.Error()) + return false + } + return true } diff --git a/utils/xsc/xscmanager.go b/utils/xsc/xscmanager.go index 831c15c2..985d6788 100644 --- a/utils/xsc/xscmanager.go +++ b/utils/xsc/xscmanager.go @@ -2,15 +2,11 @@ package xsc import ( "fmt" - "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - clientutils "github.com/jfrog/jfrog-client-go/utils" + clientconfig "github.com/jfrog/jfrog-client-go/config" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xsc" - "github.com/jfrog/jfrog-client-go/xsc/services" - - clientconfig "github.com/jfrog/jfrog-client-go/config" ) const minXscVersionForErrorReport = "1.7.7" @@ -35,31 +31,6 @@ func CreateXscServiceManager(serviceDetails *config.ServerDetails) (*xsc.XscServ return xsc.New(serviceConfig) } -func SendXscLogMessageIfEnabled(errorLog *services.ExternalErrorLog, xscManager *xsc.XscServicesManager) error { - if !IsReportLogErrorEventPossible(xscManager) { - return nil - } - return xscManager.SendXscLogErrorRequest(errorLog) -} - -// Determines if reporting the error is feasible. -func IsReportLogErrorEventPossible(xscManager *xsc.XscServicesManager) bool { - xscVersion, err := xscManager.GetVersion() - if err != nil { - log.Debug(fmt.Sprintf("failed to check availability of Xsc service:%s\nReporting to JFrog analytics is skipped...", err.Error())) - return false - } - if xscVersion == "" { - log.Debug("Xsc service is not available. Reporting to JFrog analytics is skipped...") - return false - } - if err = clientutils.ValidateMinimumVersion(clientutils.Xsc, xscVersion, minXscVersionForErrorReport); err != nil { - log.Debug(err.Error()) - return false - } - return true -} - func GetXscMsiAndVersion(analyticsMetricsService *AnalyticsMetricsService) (multiScanId, xscVersion string) { var err error if analyticsMetricsService != nil {