From 91f5013a25f7ddf380cf4f131b8984c7e42e3be3 Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 13 Jan 2026 12:04:05 +0200 Subject: [PATCH 1/2] Pass is new flow to AnalyzerManager --- commands/audit/audit.go | 8 ++------ commands/maliciousscan/maliciousscan.go | 7 +------ commands/scan/scan.go | 1 + jas/analyzermanager.go | 1 + jas/common.go | 4 ++-- jas/common_test.go | 2 +- jas/runner/jasrunner_test.go | 2 +- 7 files changed, 9 insertions(+), 16 deletions(-) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 88c0a082f..47d6c6fc6 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -590,18 +590,14 @@ func addJasScansToRunner(auditParallelRunner *utils.SecurityParallelRunner, audi return } auditParallelRunner.ResultsMu.Lock() - repoKey := utils.GetGitRepoUrlKey(auditParams.resultsContext.GitRepoHttpsCloneUrl) - if isNewFlow { - // Violations by git repo key In JAS by AM not needed in the new flow. - repoKey = "" - } scannerOptions := []jas.JasScannerOption{ jas.WithEnvVars( scanResults.SecretValidation, jas.GetDiffScanTypeValue(auditParams.diffMode, auditParams.resultsToCompare), jas.GetAnalyzerManagerXscEnvVars( + isNewFlow, auditParams.GetMultiScanId(), - repoKey, + utils.GetGitRepoUrlKey(auditParams.resultsContext.GitRepoHttpsCloneUrl), auditParams.resultsContext.ProjectKey, auditParams.resultsContext.Watches, scanResults.GetTechnologies()..., diff --git a/commands/maliciousscan/maliciousscan.go b/commands/maliciousscan/maliciousscan.go index 4b00d60ea..d0ac92a9f 100644 --- a/commands/maliciousscan/maliciousscan.go +++ b/commands/maliciousscan/maliciousscan.go @@ -145,12 +145,7 @@ func (cmd *MaliciousScanCommand) createJasScanner() (*jas.JasScanner, error) { jas.WithEnvVars( false, jas.NotDiffScanEnvValue, - jas.GetAnalyzerManagerXscEnvVars( - "", - "", - cmd.project, - nil, - ), + jas.GetAnalyzerManagerXscEnvVars(false, "", "", cmd.project, nil), ), jas.WithMinSeverity(cmd.minSeverityFilter), } diff --git a/commands/scan/scan.go b/commands/scan/scan.go index 3f37200d7..36b783662 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -550,6 +550,7 @@ func (scanCmd *ScanCommand) RunBinaryJasScans(cmdType utils.CommandType, msi str secretValidation, jas.NotDiffScanEnvValue, jas.GetAnalyzerManagerXscEnvVars( + false, msi, // Passing but empty since not supported for binary scans scanCmd.resultsContext.GitRepoHttpsCloneUrl, diff --git a/jas/analyzermanager.go b/jas/analyzermanager.go index d93c5ec18..4b838d061 100644 --- a/jas/analyzermanager.go +++ b/jas/analyzermanager.go @@ -37,6 +37,7 @@ const ( watchesEnvVariable = "AM_WATCHES" projectEnvVariable = "AM_PROJECT_KEY" gitRepoEnvVariable = "AM_GIT_REPO_VIOLATIONS" + newFlowEnvVariable = "AM_DUMMY_MODE" notEntitledExitCode = 31 unsupportedCommandExitCode = 13 unsupportedOsExitCode = 55 diff --git a/jas/common.go b/jas/common.go index 974a4d044..07693d056 100644 --- a/jas/common.go +++ b/jas/common.go @@ -474,8 +474,8 @@ func CheckForSecretValidation(xrayManager *xray.XrayServicesManager, xrayVersion return err == nil && isEnabled } -func GetAnalyzerManagerXscEnvVars(msi string, gitRepoUrl, projectKey string, watches []string, technologies ...techutils.Technology) map[string]string { - envVars := map[string]string{utils.JfMsiEnvVariable: msi} +func GetAnalyzerManagerXscEnvVars(newFlow bool, msi string, gitRepoUrl, projectKey string, watches []string, technologies ...techutils.Technology) map[string]string { + envVars := map[string]string{utils.JfMsiEnvVariable: msi, newFlowEnvVariable: strconv.FormatBool(newFlow)} if gitRepoUrl != "" { envVars[gitRepoEnvVariable] = gitRepoUrl } diff --git a/jas/common_test.go b/jas/common_test.go index 48faa1b39..1ddee3d5f 100644 --- a/jas/common_test.go +++ b/jas/common_test.go @@ -487,7 +487,7 @@ func TestGetAnalyzerManagerXscEnvVars(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expectedOutput, GetAnalyzerManagerXscEnvVars(test.msi, test.gitRepoUrl, test.projectKey, test.watches, test.technologies...)) + assert.Equal(t, test.expectedOutput, GetAnalyzerManagerXscEnvVars(false, test.msi, test.gitRepoUrl, test.projectKey, test.watches, test.technologies...)) }) } } diff --git a/jas/runner/jasrunner_test.go b/jas/runner/jasrunner_test.go index 0f383b071..3d77abf27 100644 --- a/jas/runner/jasrunner_test.go +++ b/jas/runner/jasrunner_test.go @@ -44,7 +44,7 @@ func TestJasRunner(t *testing.T) { securityParallelRunnerForTest := utils.CreateSecurityParallelRunner(cliutils.Threads) targetResults := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true).NewScanResults(results.ScanTarget{Target: "target", Technology: techutils.Pip}) - jasScanner, err := jas.NewJasScanner(&jas.FakeServerDetails, jas.WithEnvVars(false, jas.NotDiffScanEnvValue, jas.GetAnalyzerManagerXscEnvVars("", "", "", []string{}, targetResults.GetTechnologies()...))) + jasScanner, err := jas.NewJasScanner(&jas.FakeServerDetails, jas.WithEnvVars(false, jas.NotDiffScanEnvValue, jas.GetAnalyzerManagerXscEnvVars(false, "", "", "", []string{}, targetResults.GetTechnologies()...))) assert.NoError(t, err) jasScanner.AnalyzerManager.AnalyzerManagerFullPath, err = jas.GetAnalyzerManagerExecutable() assert.NoError(t, err) From dea1c6c7f990e924fa22091a430ca37a7af6fa53 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 2 Feb 2026 14:43:38 +0200 Subject: [PATCH 2/2] Merge dev --- artifactory_test.go | 2 +- cli/docs/flags.go | 14 +- cli/scancommands.go | 21 +- commands/curation/curationaudit.go | 98 +++++---- commands/curation/curationaudit_test.go | 32 +++ curation_test.go | 2 +- go.mod | 12 +- go.sum | 15 ++ maliciousscan_test.go | 193 ++++++++-------- policy/enforcer/policyenforcer.go | 2 +- policy/local/localconvertor_test.go | 6 +- .../buildinfo/technologies/docker/docker.go | 3 + .../technologies/nuget/nuget_test.go | 2 +- .../integration/test_integrationutils.go | 2 +- utils/formats/cdxutils/cyclonedxutils.go | 36 ++- utils/formats/cdxutils/cyclonedxutils_test.go | 91 ++++++++ utils/formats/simplejsonapi.go | 1 + utils/results/common.go | 43 +++- utils/results/common_test.go | 207 +++++++++++++++++- utils/results/conversion/convertor.go | 14 +- .../cyclonedxparser/cyclonedxparser.go | 2 +- .../conversion/sarifparser/sarifparser.go | 24 +- .../simplejsonparser/simplejsonparser.go | 20 +- .../simplejsonparser/simplejsonparser_test.go | 21 +- .../conversion/summaryparser/summaryparser.go | 2 +- .../conversion/tableparser/tableparser.go | 4 +- 26 files changed, 645 insertions(+), 224 deletions(-) diff --git a/artifactory_test.go b/artifactory_test.go index 676e83e88..0a56ec1f3 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -119,7 +119,7 @@ func TestDependencyResolutionFromArtifactory(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.projectType.String(), func(t *testing.T) { if testCase.skipMsg != "" { - securityTestUtils.SkipTestIfDurationNotPassed(t, "22-12-2025", 30, testCase.skipMsg) + securityTestUtils.SkipTestIfDurationNotPassed(t, "22-01-2026", 30, testCase.skipMsg) } testSingleTechDependencyResolution(t, testCase.testProjectPath, testCase.resolveRepoName, testCase.cacheRepoName, testCase.projectType) }) diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 0b97794a8..eb4cb0520 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -153,9 +153,10 @@ const ( AnalyzerManagerCustomPath = "analyzer-manager-path" // Unique curation flags - CurationOutput = "curation-format" - DockerImageName = "image" - SolutionPath = "solution-path" + CurationOutput = "curation-format" + DockerImageName = "image" + SolutionPath = "solution-path" + IncludeCachedPackages = "include-cached-packages" // Unique git flags InputFile = "input-file" @@ -195,7 +196,7 @@ var commandFlags = map[string][]string{ useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm, Pnpm, Yarn, Go, Swift, Cocoapods, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, Threads, auditSca, auditIac, auditSast, auditSecrets, auditWithoutCA, SecretValidation, ScanVuln, OutputDir, SkipAutoInstall, AllowPartialResults, MaxTreeDepth, - StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, UploadRtRepoPath, UseIncludedBuilds, + StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, UploadRtRepoPath, UseIncludedBuilds, AddSastRules, }, UploadCdx: { UploadRepoPath, uploadProjectKey, @@ -214,7 +215,7 @@ var commandFlags = map[string][]string{ StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, AddSastRules, }, CurationAudit: { - CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, UseIncludedBuilds, SolutionPath, DockerImageName, + CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, UseIncludedBuilds, SolutionPath, DockerImageName, IncludeCachedPackages, }, GitCountContributors: { InputFile, ScmType, ScmApiUrl, Token, Owner, RepoName, Months, DetailedSummary, InsecureTls, @@ -298,7 +299,6 @@ var flagsMap = map[string]components.Flag{ WorkingDirs: components.NewStringFlag(WorkingDirs, "A comma-separated(,) list of relative working directories, to determine the audit targets locations. If flag isn't provided, a recursive scan is triggered from the root directory of the project."), OutputDir: components.NewStringFlag(OutputDir, "Target directory to save partial results to.", components.SetHiddenStrFlag()), UploadRepoPath: components.NewStringFlag(UploadRepoPath, "Artifactory repository name or path to upload the cyclonedx file to. If no name or path are provided, a local generic repository will be created which will automatically be indexed by Xray.", components.WithStrDefaultValue("import-cdx-scan-results")), - UploadRtRepoPath: components.NewStringFlag(UploadRtRepoPath, fmt.Sprintf("Artifactory repository name or path to upload the scan results to. If no name or path are provided, a local generic repository will be created which will automatically be indexed by Xray. only relevant when using --%s", StaticSca), components.WithStrDefaultValue("cli-scan-results")), SkipAutoInstall: components.NewBoolFlag(SkipAutoInstall, "Set to true to skip auto-install of dependencies in un-built modules. Currently supported for Yarn and NPM only.", components.SetHiddenBoolFlag()), AllowPartialResults: components.NewBoolFlag(AllowPartialResults, "Set to true to allow partial results and continuance of the scan in case of certain errors.", components.SetHiddenBoolFlag()), ExclusionsAudit: components.NewStringFlag( @@ -329,8 +329,10 @@ var flagsMap = map[string]components.Flag{ AnalyzerManagerCustomPath: components.NewStringFlag(AnalyzerManagerCustomPath, "Defines the custom path to the analyzer-manager binary.", components.SetHiddenStrFlag()), XrayLibPluginBinaryCustomPath: components.NewStringFlag(XrayLibPluginBinaryCustomPath, "Defines the custom path to the xray-lib-plugin binary.", components.SetHiddenStrFlag()), StaticSca: components.NewBoolFlag(StaticSca, "Set to true to use the new SCA engine which is based on lock files.", components.SetHiddenBoolFlag()), + UploadRtRepoPath: components.NewStringFlag(UploadRtRepoPath, fmt.Sprintf("Artifactory repository name or path to upload the scan results to. If no name or path are provided, a local generic repository will be created which will automatically be indexed by Xray. only relevant when using --%s", StaticSca), components.WithStrDefaultValue("cli-scan-results"), components.SetHiddenStrFlag()), CurationOutput: components.NewStringFlag(OutputFormat, "Defines the output format of the command. Acceptable values are: table, json.", components.WithStrDefaultValue("table")), SolutionPath: components.NewStringFlag(SolutionPath, "Path to the .NET solution file (.sln) to use when multiple solution files are present in the directory."), + IncludeCachedPackages: components.NewBoolFlag(IncludeCachedPackages, "When set to true, the system will audit cached packages. This configuration is mandatory for Curation on-demand workflows, which rely on package caching."), binarySca: components.NewBoolFlag(Sca, fmt.Sprintf("Selective scanners mode: Execute SCA (Software Composition Analysis) sub-scan. Use --%s to run both SCA and Contextual Analysis. Use --%s --%s to to run SCA. Can be combined with --%s.", Sca, Sca, WithoutCA, Secrets)), binarySecrets: components.NewBoolFlag(Secrets, fmt.Sprintf("Selective scanners mode: Execute Secrets sub-scan. Can be combined with --%s.", Sca)), binaryWithoutCA: components.NewBoolFlag(WithoutCA, fmt.Sprintf("Selective scanners mode: Disable Contextual Analysis scanner after SCA. Relevant only with --%s flag.", Sca)), diff --git a/cli/scancommands.go b/cli/scancommands.go index a6ee14cd5..3d13b13a4 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -22,7 +22,7 @@ import ( flags "github.com/jfrog/jfrog-cli-security/cli/docs" auditSpecificDocs "github.com/jfrog/jfrog-cli-security/cli/docs/auditspecific" enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich" - maliciousScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/maliciousscan" + // maliciousScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/maliciousscan" mcpDocs "github.com/jfrog/jfrog-cli-security/cli/docs/mcp" auditDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/audit" buildScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/buildscan" @@ -76,15 +76,15 @@ func getAuditAndScansCommands() []components.Command { Category: securityCategory, Action: EnrichCmd, }, - { - Name: "malicious-scan", - Aliases: []string{"ms"}, - Flags: flags.GetCommandFlags(flags.MaliciousScan), - Description: maliciousScanDocs.GetDescription(), - Arguments: maliciousScanDocs.GetArguments(), - Category: securityCategory, - Action: MaliciousScanCmd, - }, + // { + // Name: "malicious-scan", + // Aliases: []string{"ms"}, + // Flags: flags.GetCommandFlags(flags.MaliciousScan), + // Description: maliciousScanDocs.GetDescription(), + // Arguments: maliciousScanDocs.GetArguments(), + // Category: securityCategory, + // Action: MaliciousScanCmd, + // }, { Name: "build-scan", Aliases: []string{"bs"}, @@ -701,6 +701,7 @@ func getCurationCommand(c *components.Context) (*curation.CurationAuditCommand, SetPipRequirementsFile(c.GetStringFlagValue(flags.RequirementsFile)). SetSolutionFilePath(c.GetStringFlagValue(flags.SolutionPath)) curationAuditCommand.SetDockerImageName(c.GetStringFlagValue(flags.DockerImageName)) + curationAuditCommand.SetIncludeCachedPackages(c.GetBoolFlagValue(flags.IncludeCachedPackages)) return curationAuditCommand, nil } diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index 4a00738c7..cac76c591 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -53,12 +53,14 @@ const ( blocked = "blocked" BlockingReasonPolicy = "Policy violations" BlockingReasonNotFound = "Package pending update" + BlockingReasonOnDemand = "Package pending — Curation on-demand scan in progress" directRelation = "direct" indirectRelation = "indirect" BlockMessageKey = "jfrog packages curation" NotBeingFoundKey = "not being found" + IsOnDemand = "on-demand" extractPoliciesRegexTemplate = "({.*?})" @@ -202,24 +204,26 @@ type PackageStatusTable struct { } type treeAnalyzer struct { - rtManager artifactory.ArtifactoryServicesManager - extractPoliciesRegex *regexp.Regexp - rtAuth auth.ServiceDetails - httpClientDetails httputils.HttpClientDetails - url string - repo string - tech techutils.Technology - parallelRequests int - downloadUrls map[string]string + rtManager artifactory.ArtifactoryServicesManager + extractPoliciesRegex *regexp.Regexp + rtAuth auth.ServiceDetails + httpClientDetails httputils.HttpClientDetails + url string + repo string + tech techutils.Technology + parallelRequests int + downloadUrls map[string]string + includeCachedPackages bool } type CurationAuditCommand struct { - PackageManagerConfig *project.RepositoryConfig - extractPoliciesRegex *regexp.Regexp - workingDirs []string - OriginPath string - parallelRequests int - dockerImageName string + PackageManagerConfig *project.RepositoryConfig + extractPoliciesRegex *regexp.Regexp + workingDirs []string + OriginPath string + parallelRequests int + dockerImageName string + includeCachedPackages bool audit.AuditParamsInterface } @@ -265,6 +269,11 @@ func (ca *CurationAuditCommand) SetDockerImageName(dockerImageName string) *Cura return ca } +func (ca *CurationAuditCommand) SetIncludeCachedPackages(includeCachedPackages bool) *CurationAuditCommand { + ca.includeCachedPackages = includeCachedPackages + return ca +} + func (ca *CurationAuditCommand) Run() (err error) { rootDir, err := os.Getwd() if err != nil { @@ -500,15 +509,16 @@ func (ca *CurationAuditCommand) auditTree(tech techutils.Technology, results map } var packagesStatus []*PackageStatus analyzer := treeAnalyzer{ - rtManager: rtManager, - extractPoliciesRegex: ca.extractPoliciesRegex, - rtAuth: rtAuth, - httpClientDetails: rtAuth.CreateHttpClientDetails(), - url: rtAuth.GetUrl(), - repo: ca.PackageManagerConfig.TargetRepo(), - tech: tech, - parallelRequests: ca.parallelRequests, - downloadUrls: depTreeResult.DownloadUrls, + rtManager: rtManager, + extractPoliciesRegex: ca.extractPoliciesRegex, + rtAuth: rtAuth, + httpClientDetails: rtAuth.CreateHttpClientDetails(), + url: rtAuth.GetUrl(), + repo: ca.PackageManagerConfig.TargetRepo(), + tech: tech, + parallelRequests: ca.parallelRequests, + downloadUrls: depTreeResult.DownloadUrls, + includeCachedPackages: ca.includeCachedPackages, } rootNodes := map[string]struct{}{} @@ -866,7 +876,7 @@ func (nc *treeAnalyzer) fetchNodeStatus(node xrayUtils.GraphNode, p *sync.Map) e if resp != nil && resp.StatusCode >= 400 && resp.StatusCode != http.StatusForbidden { return errorutils.CheckErrorf(errorTemplateHeadRequest, packageUrl, name, version, resp.StatusCode, err) } - if resp.StatusCode == http.StatusForbidden { + if resp.StatusCode == http.StatusForbidden || (nc.includeCachedPackages && resp.StatusCode == http.StatusOK) { pkStatus, err := nc.getBlockedPackageDetails(packageUrl, name, version) if err != nil { return err @@ -911,6 +921,8 @@ func (nc *treeAnalyzer) getBlockedPackageDetails(packageUrl string, name string, blockingReason := BlockingReasonPolicy if strings.Contains(strings.ToLower(respError.Errors[0].Message), NotBeingFoundKey) { blockingReason = BlockingReasonNotFound + } else if strings.Contains(strings.ToLower(respError.Errors[0].Message), IsOnDemand) { + blockingReason = BlockingReasonOnDemand } policies := nc.extractPoliciesFromMsg(respError) return &PackageStatus{ @@ -933,21 +945,29 @@ func (nc *treeAnalyzer) getBlockedPackageDetails(packageUrl string, name string, func (nc *treeAnalyzer) extractPoliciesFromMsg(respError *ErrorsResp) []Policy { var policies []Policy msg := respError.Errors[0].Message - allMatches := nc.extractPoliciesRegex.FindAllString(msg, -1) - for _, match := range allMatches { - match = strings.TrimSuffix(strings.TrimPrefix(match, "{"), "}") - polCond := strings.Split(match, ",") - if len(polCond) >= 2 { - pol := polCond[0] - cond := polCond[1] - - if len(polCond) == 4 { - exp, rec := makeLegiblePolicyDetails(polCond[2], polCond[3]) - policies = append(policies, Policy{Policy: strings.TrimSpace(pol), - Condition: strings.TrimSpace(cond), Explanation: strings.TrimSpace(exp), Recommendation: strings.TrimSpace(rec)}) - continue + lowerMsg := strings.ToLower(msg) + switch { + case strings.Contains(lowerMsg, IsOnDemand): + policies = []Policy{{Explanation: BlockingReasonOnDemand}} + case strings.Contains(lowerMsg, NotBeingFoundKey): + policies = []Policy{{Explanation: BlockingReasonNotFound}} + default: + allMatches := nc.extractPoliciesRegex.FindAllString(msg, -1) + for _, match := range allMatches { + match = strings.TrimSuffix(strings.TrimPrefix(match, "{"), "}") + polCond := strings.Split(match, ",") + if len(polCond) >= 2 { + pol := polCond[0] + cond := polCond[1] + + if len(polCond) == 4 { + exp, rec := makeLegiblePolicyDetails(polCond[2], polCond[3]) + policies = append(policies, Policy{Policy: strings.TrimSpace(pol), + Condition: strings.TrimSpace(cond), Explanation: strings.TrimSpace(exp), Recommendation: strings.TrimSpace(rec)}) + continue + } + policies = append(policies, Policy{Policy: strings.TrimSpace(pol), Condition: strings.TrimSpace(cond)}) } - policies = append(policies, Policy{Policy: strings.TrimSpace(pol), Condition: strings.TrimSpace(cond)}) } } return policies diff --git a/commands/curation/curationaudit_test.go b/commands/curation/curationaudit_test.go index c4617e358..2053f9496 100644 --- a/commands/curation/curationaudit_test.go +++ b/commands/curation/curationaudit_test.go @@ -131,6 +131,38 @@ func getTestCasesForExtractPoliciesFromMsg() []struct { }, expect: nil, }, + { + name: "on-demand in progress", + errResp: &ErrorsResp{ + Errors: []ErrorResp{ + { + Status: 403, + Message: "Package test:1.0.0 download was blocked by JFrog Packages Curation service due to the package not being found in catalog, curation on-demand scan in progress.", + }, + }, + }, + expect: []Policy{ + { + Explanation: BlockingReasonOnDemand, + }, + }, + }, + { + name: "package not found in catalog", + errResp: &ErrorsResp{ + Errors: []ErrorResp{ + { + Status: 403, + Message: "package test:1.0.0 download was blocked by jfrog packages curation service due to the package not being found in catalog", + }, + }, + }, + expect: []Policy{ + { + Explanation: BlockingReasonNotFound, + }, + }, + }, } return tests } diff --git a/curation_test.go b/curation_test.go index 79f9e84e4..af2254595 100644 --- a/curation_test.go +++ b/curation_test.go @@ -131,7 +131,7 @@ func TestDockerCurationAudit(t *testing.T) { assert.Equal(t, "ganodndentcom/drupal", results[0].PackageName) assert.Equal(t, curation.BlockingReasonPolicy, results[0].BlockingReason) require.NotEmpty(t, results[0].Policy, "Expected at least one policy violation") - assert.Equal(t, "Malicious package", results[0].Policy[0].Condition) + assert.Equal(t, "Image is not Docker Hub official", results[0].Policy[0].Condition) } func curationServer(t *testing.T, expectedRequest map[string]bool, requestToFail map[string]bool) (*httptest.Server, *config.ServerDetails) { diff --git a/go.mod b/go.mod index 105b9bd66..27ee18eb6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jfrog/jfrog-cli-security -go 1.25.4 +go 1.25.5 require ( github.com/CycloneDX/cyclonedx-go v0.9.3 @@ -11,13 +11,13 @@ require ( github.com/gookit/color v1.6.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.3 - github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9 + github.com/jfrog/build-info-go v1.13.1-0.20260120103048-d7f367bfa36e github.com/jfrog/froggit-go v1.20.6 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-apps-config v1.0.1 - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93 - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5 - github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465 + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260120063955-c654c159290e + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260112010739-87fc7275623c + github.com/jfrog/jfrog-client-go v1.55.1-0.20260120055025-12f25e12798a github.com/magiconair/properties v1.8.10 github.com/owenrumney/go-sarif/v3 v3.2.3 github.com/package-url/packageurl-go v0.1.3 @@ -111,6 +111,7 @@ require ( github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/ulikunitz/xz v0.5.15 // indirect + github.com/urfave/cli/v2 v2.27.7 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/vbauerster/mpb/v8 v8.10.2 // indirect github.com/xanzy/go-gitlab v0.110.0 // indirect @@ -120,6 +121,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/mod v0.30.0 // indirect diff --git a/go.sum b/go.sum index 1ec25e861..09f5cc4fb 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,11 @@ github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5 github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9 h1:CL7lp7Y7srwQ1vy1btX66t4wbztzEGQbqi/9tdEz7xk= github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9/go.mod h1:9W4U440fdTHwW1HiB/R0VQvz/5q8ZHsms9MWcq+JrdY= +github.com/jfrog/build-info-go v1.13.0/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= +github.com/jfrog/build-info-go v1.13.1-0.20260106203543-03b99793ca5a/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= +github.com/jfrog/build-info-go v1.13.1-0.20260119231731-3cc4a0771bbd/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= +github.com/jfrog/build-info-go v1.13.1-0.20260120103048-d7f367bfa36e h1:STiWjuLtlEFR1H3kSKw6vDGhGdtUmV6O+ljPfrQ14sI= +github.com/jfrog/build-info-go v1.13.1-0.20260120103048-d7f367bfa36e/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= github.com/jfrog/froggit-go v1.20.6 h1:Xp7+LlEh0m1KGrQstb+u0aGfjRUtv1eh9xQBV3571jQ= github.com/jfrog/froggit-go v1.20.6/go.mod h1:obSG1SlsWjktkuqmKtpq7MNTTL63e0ot+ucTnlOMV88= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= @@ -156,10 +161,16 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93 h1:rpkJZN0TigpAGY/bfgmLO4nwhyhkr0gkBTLz/0B5zS8= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93/go.mod h1:7cCaRhXorlbyXZgiW5bplCExFxlnROaG21K12d8inpQ= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260120063955-c654c159290e h1:F/VQ7UJ4jaEr9tLJ8jLfy4BF4Obhhd0pWu007SBSHt8= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260120063955-c654c159290e/go.mod h1:LbhCULfa/eIPSXNgQ5Xw8BIZRmJ0qfF2I4sPa7AHXkY= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5 h1:GYE67ubwl+ZRw3CcXFUi49EwwQp6k+qS8sX0QuHDHO8= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5/go.mod h1:BMoGi2rG0udCCeaghqlNgiW3fTmT+TNnfTnBoWFYgcg= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260112010739-87fc7275623c h1:K9anqOZ7ASxlsijsl9u4jh92wqqIvJA4kTYfXrcOmJA= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260112010739-87fc7275623c/go.mod h1:+Hnaikp/xCSPD/q7txxRy4Zc0wzjW/usrCSf+6uONSQ= github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465 h1:Ff3BlNPndrAfa1xFI/ORFzfWTxQxF0buWG61PEJwd3U= github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465/go.mod h1:WQ5Y+oKYyHFAlCbHN925bWhnShTd2ruxZ6YTpb76fpU= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260120055025-12f25e12798a h1:tbHqd+9SJB6pMJn9aXkD4aMYfwsKwah5kuhZV6Q+e88= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260120055025-12f25e12798a/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= @@ -299,6 +310,8 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vbauerster/mpb/v8 v8.10.2 h1:2uBykSHAYHekE11YvJhKxYmLATKHAGorZwFlyNw4hHM= @@ -320,6 +333,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/maliciousscan_test.go b/maliciousscan_test.go index 2633b36ee..78d211b59 100644 --- a/maliciousscan_test.go +++ b/maliciousscan_test.go @@ -1,111 +1,112 @@ package main import ( - "path/filepath" - "strconv" - "strings" - "testing" +// "strconv" +// "strings" +// "testing" - "github.com/stretchr/testify/assert" +// "path/filepath" - "github.com/jfrog/jfrog-cli-core/v2/common/format" - securityTests "github.com/jfrog/jfrog-cli-security/tests" - securityTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils" - securityIntegrationTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils/integration" - "github.com/jfrog/jfrog-cli-security/tests/validations" +// "github.com/stretchr/testify/assert" +// securityTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils" +// "github.com/jfrog/jfrog-cli-security/tests/validations" + +// "github.com/jfrog/jfrog-cli-core/v2/common/format" +// securityTests "github.com/jfrog/jfrog-cli-security/tests" +// securityIntegrationTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils/integration" ) -type maliciousScanCommandTestParams struct { - WorkingDirsToScan []string - Format format.OutputFormat - Threads int - MinSeverity string -} +// type maliciousScanCommandTestParams struct { +// WorkingDirsToScan []string +// Format format.OutputFormat +// Threads int +// MinSeverity string +// } -func getMaliciousScanCmdArgs(params maliciousScanCommandTestParams) (args []string) { - args = []string{"malicious-scan"} - if len(params.WorkingDirsToScan) > 0 { - args = append(args, "--working-dirs="+strings.Join(params.WorkingDirsToScan, ",")) - } - if params.Format != "" { - args = append(args, "--format="+string(params.Format)) - } - if params.Threads > 0 { - args = append(args, "--threads="+strconv.Itoa(params.Threads)) - } - if params.MinSeverity != "" { - args = append(args, "--min-severity="+params.MinSeverity) - } - return args -} +// func getMaliciousScanCmdArgs(params maliciousScanCommandTestParams) (args []string) { +// args = []string{"malicious-scan"} +// if len(params.WorkingDirsToScan) > 0 { +// args = append(args, "--working-dirs="+strings.Join(params.WorkingDirsToScan, ",")) +// } +// if params.Format != "" { +// args = append(args, "--format="+string(params.Format)) +// } +// if params.Threads > 0 { +// args = append(args, "--threads="+strconv.Itoa(params.Threads)) +// } +// if params.MinSeverity != "" { +// args = append(args, "--min-severity="+params.MinSeverity) +// } +// return args +// } -func runMaliciousScan(t *testing.T, params maliciousScanCommandTestParams) (string, error) { - cleanUp := securityIntegrationTestUtils.UseTestHomeWithDefaultXrayConfig(t) - defer cleanUp() - return securityTests.PlatformCli.RunCliCmdWithOutputs(t, getMaliciousScanCmdArgs(params)...) -} +// func runMaliciousScan(t *testing.T, params maliciousScanCommandTestParams) (string, error) { +// cleanUp := securityIntegrationTestUtils.UseTestHomeWithDefaultXrayConfig(t) +// defer cleanUp() +// return securityTests.PlatformCli.RunCliCmdWithOutputs(t, getMaliciousScanCmdArgs(params)...) +// } -func TestMaliciousScan(t *testing.T) { - testCases := []struct { - name string - format format.OutputFormat - projectPath string - expectedIssues int - }{ - { - name: "Malicious scan with findings (Simple JSON)", - format: format.SimpleJson, - projectPath: filepath.Join("projects", "jas", "jas", "malicious"), - expectedIssues: 1, - }, - { - name: "Malicious scan without findings (Simple JSON)", - format: format.SimpleJson, - projectPath: filepath.Join("projects", "empty_project", "python_project_with_no_deps"), - expectedIssues: 0, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - fullProjectPath := filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), tc.projectPath) - _, cleanUp := securityTestUtils.CreateTestProjectEnvAndChdir(t, fullProjectPath) - defer cleanUp() +// func TestMaliciousScan(t *testing.T) { +// testCases := []struct { +// name string +// format format.OutputFormat +// projectPath string +// expectedIssues int +// }{ +// { +// name: "Malicious scan with findings (Simple JSON)", +// format: format.SimpleJson, +// projectPath: filepath.Join("projects", "jas", "jas", "malicious"), +// expectedIssues: 1, +// }, +// { +// name: "Malicious scan without findings (Simple JSON)", +// format: format.SimpleJson, +// projectPath: filepath.Join("projects", "empty_project", "python_project_with_no_deps"), +// expectedIssues: 0, +// }, +// } +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// fullProjectPath := filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), tc.projectPath) +// _, cleanUp := securityTestUtils.CreateTestProjectEnvAndChdir(t, fullProjectPath) +// defer cleanUp() - params := maliciousScanCommandTestParams{ - Format: tc.format, - } - output, err := runMaliciousScan(t, params) - assert.NoError(t, err) +// params := maliciousScanCommandTestParams{ +// Format: tc.format, +// } +// output, err := runMaliciousScan(t, params) +// assert.NoError(t, err) - validationsParams := validations.ValidationParams{ - Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{MaliciousCode: tc.expectedIssues}, - }, - } - if tc.expectedIssues == 0 { - validationsParams.ExactResultsMatch = true - } - validations.ValidateCommandOutput(t, output, tc.format, validationsParams) - }) - } -} +// validationsParams := validations.ValidationParams{ +// Vulnerabilities: &validations.VulnerabilityCount{ +// ValidateScan: &validations.ScanCount{MaliciousCode: tc.expectedIssues}, +// }, +// } +// if tc.expectedIssues == 0 { +// validationsParams.ExactResultsMatch = true +// } +// validations.ValidateCommandOutput(t, output, tc.format, validationsParams) +// }) +// } +// } -func TestMaliciousScanWithWorkingDirs(t *testing.T) { - maliciousProjectPath := filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), "projects", "jas", "jas", "malicious") - _, cleanUp := securityTestUtils.CreateTestProjectEnvAndChdir(t, maliciousProjectPath) - defer cleanUp() +// func TestMaliciousScanWithWorkingDirs(t *testing.T) { +// maliciousProjectPath := filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), "projects", "jas", "jas", "malicious") +// _, cleanUp := securityTestUtils.CreateTestProjectEnvAndChdir(t, maliciousProjectPath) +// defer cleanUp() - params := maliciousScanCommandTestParams{ - WorkingDirsToScan: []string{"."}, - Format: format.SimpleJson, - } - output, err := runMaliciousScan(t, params) - assert.NoError(t, err) +// params := maliciousScanCommandTestParams{ +// WorkingDirsToScan: []string{"."}, +// Format: format.SimpleJson, +// } +// output, err := runMaliciousScan(t, params) +// assert.NoError(t, err) - validationsParams := validations.ValidationParams{ - Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{MaliciousCode: 1}, - }, - } - validations.ValidateCommandOutput(t, output, format.SimpleJson, validationsParams) -} +// validationsParams := validations.ValidationParams{ +// Vulnerabilities: &validations.VulnerabilityCount{ +// ValidateScan: &validations.ScanCount{MaliciousCode: 1}, +// }, +// } +// validations.ValidateCommandOutput(t, output, format.SimpleJson, validationsParams) +// } diff --git a/policy/enforcer/policyenforcer.go b/policy/enforcer/policyenforcer.go index e9eabd00c..b63ad3a53 100644 --- a/policy/enforcer/policyenforcer.go +++ b/policy/enforcer/policyenforcer.go @@ -241,8 +241,8 @@ func locateBomComponentInfo(cmdResults *results.SecurityCommandResults, impacted if target.ScaResults.Sbom.Dependencies != nil { dependencies = *target.ScaResults.Sbom.Dependencies } - directComponents = results.GetDirectDependenciesAsComponentRows(component, *target.ScaResults.Sbom.Components, dependencies) impactPaths = results.BuildImpactPath(component, *target.ScaResults.Sbom.Components, dependencies...) + directComponents = results.ExtractComponentDirectComponentsInBOM(target.ScaResults.Sbom, component, impactPaths) break } } diff --git a/policy/local/localconvertor_test.go b/policy/local/localconvertor_test.go index a4abe30e1..fdb2890c9 100644 --- a/policy/local/localconvertor_test.go +++ b/policy/local/localconvertor_test.go @@ -214,10 +214,10 @@ func createScaTestViolation(id, component string, vioType violationutils.Violati Type: cyclonedx.ComponentTypeLibrary, Name: component, }, - DirectComponents: []formats.ComponentRow{{Name: component}}, + DirectComponents: []formats.ComponentRow{{Id: component, Name: component}}, ImpactPaths: [][]formats.ComponentRow{{ - {Name: "root"}, - {Name: component}, + {Id: "root", Name: "root"}, + {Id: component, Name: component}, }}, } } diff --git a/sca/bom/buildinfo/technologies/docker/docker.go b/sca/bom/buildinfo/technologies/docker/docker.go index 181c95126..b156d7fa5 100644 --- a/sca/bom/buildinfo/technologies/docker/docker.go +++ b/sca/bom/buildinfo/technologies/docker/docker.go @@ -223,6 +223,9 @@ func findDigestForPlatform(imageName, targetOS, targetArch string) (string, erro buildxCmd := exec.Command("docker", "buildx", "imagetools", "inspect", imageName, "--raw") buildxOutput, buildxErr := buildxCmd.CombinedOutput() if buildxErr != nil { + if strings.Contains(string(buildxOutput), "curation service") { + return extractDigestFromBlockedMessage(string(buildxOutput)), nil + } return "", fmt.Errorf("docker buildx imagetools inspect failed: %s", strings.TrimSpace(string(buildxOutput))) } diff --git a/sca/bom/buildinfo/technologies/nuget/nuget_test.go b/sca/bom/buildinfo/technologies/nuget/nuget_test.go index 4226ce88f..94934452d 100644 --- a/sca/bom/buildinfo/technologies/nuget/nuget_test.go +++ b/sca/bom/buildinfo/technologies/nuget/nuget_test.go @@ -203,7 +203,7 @@ func TestSkipBuildDepTreeWhenInstallForbidden(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { if test.skipMsg != "" { - securityTestUtils.SkipTestIfDurationNotPassed(t, "22-12-2025", 30, test.skipMsg) + securityTestUtils.SkipTestIfDurationNotPassed(t, "22-01-2026", 30, test.skipMsg) } // Create and change directory to test workspace _, cleanUp := technologies.CreateTestWorkspace(t, test.testDir) diff --git a/tests/utils/integration/test_integrationutils.go b/tests/utils/integration/test_integrationutils.go index 05724cfcf..7aa5d329e 100644 --- a/tests/utils/integration/test_integrationutils.go +++ b/tests/utils/integration/test_integrationutils.go @@ -115,7 +115,7 @@ func InitAuditNewScaTests(t *testing.T, minVersion string) { if !*configTests.TestAuditNewSca { t.Skip(getSkipTestMsg("Audit command new SCA integration", "--test.audit.NewSca")) } - testUtils.SkipTestIfDurationNotPassed(t, "22-12-2025", 30, "Catalog API not available yet in test platform.") + testUtils.SkipTestIfDurationNotPassed(t, "22-01-2026", 30, "Catalog API not available yet in test platform.") testUtils.GetAndValidateXrayVersion(t, minVersion) } diff --git a/utils/formats/cdxutils/cyclonedxutils.go b/utils/formats/cdxutils/cyclonedxutils.go index 3700410fd..5ddd1cc61 100644 --- a/utils/formats/cdxutils/cyclonedxutils.go +++ b/utils/formats/cdxutils/cyclonedxutils.go @@ -32,11 +32,14 @@ const ( // Indicates that the component is a root component in the BOM RootRelation ComponentRelation = "root" // Indicates that the component is a direct dependency of another component - DirectRelation ComponentRelation = "direct_dependency" + DirectRelation ComponentRelation = "direct" // Indicates that the component is a transitive dependency of another component - TransitiveRelation ComponentRelation = "transitive_dependency" + TransitiveRelation ComponentRelation = "transitive" // Undefined relation UnknownRelation ComponentRelation = "" + + // JFrog specific properties + JfrogRelationProperty = "jfrog:dependency:type" ) type ComponentRelation string @@ -96,6 +99,17 @@ func SearchDependencyEntry(dependencies *[]cyclonedx.Dependency, ref string) *cy return nil } +func GetJfrogRelationProperty(component *cyclonedx.Component) ComponentRelation { + if component == nil || component.Properties == nil || len(*component.Properties) == 0 { + return UnknownRelation + } + property := GetProperty(component.Properties, JfrogRelationProperty) + if property == nil || property.Value == "" { + return UnknownRelation + } + return ComponentRelation(property.Value) +} + func GetComponentRelation(bom *cyclonedx.BOM, componentRef string, skipDefaultRoot bool) ComponentRelation { if bom == nil || bom.Components == nil { return UnknownRelation @@ -105,6 +119,10 @@ func GetComponentRelation(bom *cyclonedx.BOM, componentRef string, skipDefaultRo // The component is not found in the BOM components or not library, return UnknownRelation return UnknownRelation } + // Check if the component has a JFrog specific relation property + if relation := GetJfrogRelationProperty(component); relation != UnknownRelation { + return relation + } dependencies := []cyclonedx.Dependency{} if bom.Dependencies != nil { dependencies = *bom.Dependencies @@ -178,6 +196,20 @@ func GetRootDependenciesEntries(bom *cyclonedx.BOM, skipDefaultRoot bool) (roots if bom == nil || bom.Components == nil || len(*bom.Components) == 0 { return } + // First, let collect all Jfrog defined root components if exists + for _, comp := range *bom.Components { + if GetJfrogRelationProperty(&comp) == RootRelation { + if compDepEntry := SearchDependencyEntry(bom.Dependencies, comp.BOMRef); compDepEntry != nil { + roots = append(roots, *compDepEntry) + } else { + roots = append(roots, cyclonedx.Dependency{Ref: comp.BOMRef}) + } + } + } + if len(roots) > 0 { + // Jfrog defined roots found, return them + return + } // Create a Set to track all references that are listed in `dependsOn` refs := datastructures.MakeSet[string]() dependedRefs := datastructures.MakeSet[string]() diff --git a/utils/formats/cdxutils/cyclonedxutils_test.go b/utils/formats/cdxutils/cyclonedxutils_test.go index 44e8628f2..7caa58c0c 100644 --- a/utils/formats/cdxutils/cyclonedxutils_test.go +++ b/utils/formats/cdxutils/cyclonedxutils_test.go @@ -65,6 +65,76 @@ func TestSearchDependencyEntry(t *testing.T) { } } +func TestGetJfrogRelationProperty(t *testing.T) { + tests := []struct { + name string + component *cyclonedx.Component + expected ComponentRelation + }{ + { + name: "Component with nil properties", + component: &cyclonedx.Component{BOMRef: "comp1", Properties: nil}, + expected: UnknownRelation, + }, + { + name: "Component with empty properties", + component: &cyclonedx.Component{BOMRef: "comp1", Properties: &[]cyclonedx.Property{}}, + expected: UnknownRelation, + }, + { + name: "Component without jfrog:dependency:type property", + component: &cyclonedx.Component{ + BOMRef: "comp1", + Properties: &[]cyclonedx.Property{{Name: "other:property", Value: "value"}}, + }, + expected: UnknownRelation, + }, + { + name: "Component with empty jfrog:dependency:type value", + component: &cyclonedx.Component{ + BOMRef: "comp1", + Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: ""}}, + }, + expected: UnknownRelation, + }, + { + name: "Component with root relation", + component: &cyclonedx.Component{ + BOMRef: "root", + Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(RootRelation)}}, + }, + expected: RootRelation, + }, + { + name: "Component with direct relation", + component: &cyclonedx.Component{ + BOMRef: "comp1", + Properties: &[]cyclonedx.Property{ + {Name: "some:other:property", Value: "value1"}, + {Name: JfrogRelationProperty, Value: string(DirectRelation)}, + {Name: "another:property", Value: "value2"}, + }, + }, + expected: DirectRelation, + }, + { + name: "Component with transitive relation", + component: &cyclonedx.Component{ + BOMRef: "trans1", + Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(TransitiveRelation)}}, + }, + expected: TransitiveRelation, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetJfrogRelationProperty(tt.component) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestGetComponentRelation(t *testing.T) { tests := []struct { name string @@ -809,6 +879,27 @@ func TestGetRootDependenciesEntries(t *testing.T) { // file1 is not included because it's not a library type }, }, + { + name: "Multiple roots with jfrog:dependency:type property", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root1", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 1", Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(RootRelation)}}}, + {BOMRef: "root2", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 2", Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(RootRelation)}}}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(DirectRelation)}}}, + {BOMRef: "trans1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root1", Dependencies: &[]string{"root2"}}, + {Ref: "root2", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"trans1"}}, + }, + }, + skipRoot: true, + expected: []cyclonedx.Dependency{ + {Ref: "root1", Dependencies: &[]string{"root2"}}, + {Ref: "root2", Dependencies: &[]string{"direct1"}}, + }, + }, } for _, tt := range tests { diff --git a/utils/formats/simplejsonapi.go b/utils/formats/simplejsonapi.go index 18481f404..b9fb7a2bf 100644 --- a/utils/formats/simplejsonapi.go +++ b/utils/formats/simplejsonapi.go @@ -138,6 +138,7 @@ func (l Location) ToString() string { } type ComponentRow struct { + Id string `json:"id,omitempty"` Name string `json:"name"` Version string `json:"version"` Location *Location `json:"location,omitempty"` diff --git a/utils/results/common.go b/utils/results/common.go index 5cd34e72d..3a0dac740 100644 --- a/utils/results/common.go +++ b/utils/results/common.go @@ -257,7 +257,12 @@ func getDirectComponentsAndImpactPaths(target string, impactPaths [][]services.I componentId := impactPath[impactPathIndex].ComponentId if _, exist := componentsMap[componentId]; !exist { compName, compVersion, _ := techutils.SplitComponentIdRaw(componentId) - componentsMap[componentId] = formats.ComponentRow{Name: compName, Version: compVersion, Location: getComponentLocation(impactPath[impactPathIndex].FullPath, target)} + componentsMap[componentId] = formats.ComponentRow{ + Id: componentId, + Name: compName, + Version: compVersion, + Location: getComponentLocation(impactPath[impactPathIndex].FullPath, target), + } } // Convert the impact path @@ -265,6 +270,7 @@ func getDirectComponentsAndImpactPaths(target string, impactPaths [][]services.I for _, pathNode := range impactPath { nodeCompName, nodeCompVersion, _ := techutils.SplitComponentIdRaw(pathNode.ComponentId) compImpactPathRows = append(compImpactPathRows, formats.ComponentRow{ + Id: pathNode.ComponentId, Name: nodeCompName, Version: nodeCompVersion, Location: getComponentLocation(pathNode.FullPath), @@ -286,8 +292,10 @@ func BuildImpactPath(affectedComponent cyclonedx.Component, components []cyclone impactedPath := buildImpactPathForComponent(parent, componentAppearances, components, dependencies...) // Add the affected component at the end of the impact path impactedPath = append(impactedPath, formats.ComponentRow{ - Name: affectedComponent.Name, - Version: affectedComponent.Version, + Id: affectedComponent.BOMRef, + Name: affectedComponent.Name, + Version: affectedComponent.Version, + Location: CdxEvidenceToLocation(affectedComponent), }) // Add the impact path to the list of impact paths impactPathsRows = append(impactPathsRows, impactedPath) @@ -300,8 +308,10 @@ func buildImpactPathForComponent(component cyclonedx.Component, componentAppeara // Build the impact path for the component impactPath = []formats.ComponentRow{ { - Name: component.Name, - Version: component.Version, + Id: component.BOMRef, + Name: component.Name, + Version: component.Version, + Location: CdxEvidenceToLocation(component), }, } // Add the parent components to the impact path @@ -1369,13 +1379,26 @@ func CdxToFixedVersions(affectedVersions *[]cyclonedx.AffectedVersions) (fixedVe return } -func GetDirectDependenciesAsComponentRows(component cyclonedx.Component, components []cyclonedx.Component, dependencies []cyclonedx.Dependency) (directComponents []formats.ComponentRow) { - for _, parent := range cdxutils.SearchParents(component.BOMRef, components, dependencies...) { +func ExtractComponentDirectComponentsInBOM(bom *cyclonedx.BOM, component cyclonedx.Component, impactPaths [][]formats.ComponentRow) (directComponents []formats.ComponentRow) { + if relation := cdxutils.GetComponentRelation(bom, component.BOMRef, true); relation == cdxutils.RootRelation || relation == cdxutils.DirectRelation { + // The component is a root or direct dependency, no parents to extract, return the component itself directComponents = append(directComponents, formats.ComponentRow{ - Name: parent.Name, - Version: parent.Version, - Location: CdxEvidenceToLocation(parent), + Id: component.BOMRef, + Name: component.Name, + Version: component.Version, + Location: CdxEvidenceToLocation(component), }) + return + } + // The component is a transitive dependency, go over path from start until we find the first direct dependency relation + for _, path := range impactPaths { + for _, pathComponent := range path { + if relation := cdxutils.GetComponentRelation(bom, pathComponent.Id, true); relation == cdxutils.DirectRelation { + // Found the first direct dependency in the path, add it to the direct components and stop processing this path + directComponents = append(directComponents, pathComponent) + break + } + } } return } diff --git a/utils/results/common_test.go b/utils/results/common_test.go index 983b393ca..f15c745a4 100644 --- a/utils/results/common_test.go +++ b/utils/results/common_test.go @@ -606,25 +606,25 @@ func TestGetDirectComponents(t *testing.T) { { name: "one direct component", impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack:1.2.3"}}}, - expectedDirectComponentRows: []formats.ComponentRow{{Name: "jfrog:pack", Version: "1.2.3"}}, - expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack", Version: "1.2.3"}}}, + expectedDirectComponentRows: []formats.ComponentRow{{Id: "gav://jfrog:pack:1.2.3", Name: "jfrog:pack", Version: "1.2.3"}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Id: "gav://jfrog:pack:1.2.3", Name: "jfrog:pack", Version: "1.2.3"}}}, }, { name: "one direct component with target", target: filepath.Join("root", "dir", "file"), impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack2:1.2.3"}}}, - expectedDirectComponentRows: []formats.ComponentRow{{Name: "jfrog:pack2", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}}, - expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack2", Version: "1.2.3"}}}, + expectedDirectComponentRows: []formats.ComponentRow{{Id: "gav://jfrog:pack2:1.2.3", Name: "jfrog:pack2", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Id: "gav://jfrog:pack1:1.2.3", Name: "jfrog:pack1", Version: "1.2.3"}, {Id: "gav://jfrog:pack2:1.2.3", Name: "jfrog:pack2", Version: "1.2.3"}}}, }, { name: "multiple direct components", target: filepath.Join("root", "dir", "file"), impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack21:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}, {services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack22:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}}, expectedDirectComponentRows: []formats.ComponentRow{ - {Name: "jfrog:pack21", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, - {Name: "jfrog:pack22", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, + {Id: "gav://jfrog:pack21:1.2.3", Name: "jfrog:pack21", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, + {Id: "gav://jfrog:pack22:1.2.3", Name: "jfrog:pack22", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, }, - expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack21", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}, {{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack22", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Id: "gav://jfrog:pack1:1.2.3", Name: "jfrog:pack1", Version: "1.2.3"}, {Id: "gav://jfrog:pack21:1.2.3", Name: "jfrog:pack21", Version: "1.2.3"}, {Id: "gav://jfrog:pack3:1.2.3", Name: "jfrog:pack3", Version: "1.2.3"}}, {{Id: "gav://jfrog:pack1:1.2.3", Name: "jfrog:pack1", Version: "1.2.3"}, {Id: "gav://jfrog:pack22:1.2.3", Name: "jfrog:pack22", Version: "1.2.3"}, {Id: "gav://jfrog:pack3:1.2.3", Name: "jfrog:pack3", Version: "1.2.3"}}}, }, } @@ -637,6 +637,199 @@ func TestGetDirectComponents(t *testing.T) { } } +func TestExtractComponentDirectComponentsInBOM(t *testing.T) { + tests := []struct { + name string + bom *cyclonedx.BOM + component cyclonedx.Component + impactPaths [][]formats.ComponentRow + expectedDirects []formats.ComponentRow + }{ + { + name: "Component is root dependency - returns component itself", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "comp1", Type: cyclonedx.ComponentTypeLibrary, Name: "Component 1", Version: "1.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"comp1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "root", Name: "Root Component", Version: "1.0.0"}, + impactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "Root Component", Version: "1.0.0"}}}, + expectedDirects: []formats.ComponentRow{ + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + }, + }, + { + name: "Component is direct dependency - returns component itself", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "direct1", Name: "Direct 1", Version: "2.0.0"}, + impactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "Root Component", Version: "1.0.0"}, {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}}}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + }, + }, + { + name: "Component is transitive - returns first direct from impact path", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"transitive1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + impactPaths: [][]formats.ComponentRow{{ + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + }}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + }, + }, + { + name: "Deep transitive - returns first direct in path", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root1", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 1", Version: "1.0.0", Properties: &[]cyclonedx.Property{{Name: "jfrog:dependency:type", Value: "root"}}}, + {BOMRef: "root2", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 2", Version: "1.0.0", Properties: &[]cyclonedx.Property{{Name: "jfrog:dependency:type", Value: "root"}}}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + {BOMRef: "trans1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + {BOMRef: "trans2", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 2", Version: "4.0.0"}, + {BOMRef: "deepTrans", Type: cyclonedx.ComponentTypeLibrary, Name: "Deep Transitive", Version: "5.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root1", Dependencies: &[]string{"root2"}}, + {Ref: "root2", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"trans1"}}, + {Ref: "trans1", Dependencies: &[]string{"trans2"}}, + {Ref: "trans2", Dependencies: &[]string{"deepTrans"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "deepTrans", Name: "Deep Transitive", Version: "5.0.0"}, + impactPaths: [][]formats.ComponentRow{{ + {Id: "root1", Name: "Root 1", Version: "1.0.0"}, + {Id: "root2", Name: "Root 2", Version: "1.0.0"}, + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + {Id: "trans1", Name: "Transitive 1", Version: "3.0.0"}, + {Id: "trans2", Name: "Transitive 2", Version: "4.0.0"}, + {Id: "deepTrans", Name: "Deep Transitive", Version: "5.0.0"}, + }}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + }, + }, + { + name: "Component is transitive with multiple impact paths - returns first direct from each path", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "directA", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct A", Version: "2.0.0"}, + {BOMRef: "directB", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct B", Version: "2.1.0"}, + {BOMRef: "directC", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct C", Version: "2.2.0"}, + {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + {BOMRef: "transitive2", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 2", Version: "3.1.0"}, + {BOMRef: "transitive3", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 3", Version: "3.2.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"directA", "directB", "directC"}}, + {Ref: "directA", Dependencies: &[]string{"transitive1", "transitive3"}}, + {Ref: "directB", Dependencies: &[]string{"transitive1"}}, + {Ref: "directC", Dependencies: &[]string{"transitive2"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + impactPaths: [][]formats.ComponentRow{ + { + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + {Id: "directA", Name: "Direct A", Version: "2.0.0"}, + {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + }, + { + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + {Id: "directB", Name: "Direct B", Version: "2.1.0"}, + {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + }, + }, + expectedDirects: []formats.ComponentRow{ + {Id: "directA", Name: "Direct A", Version: "2.0.0"}, + {Id: "directB", Name: "Direct B", Version: "2.1.0"}, + }, + }, + { + name: "Component with evidence location - location preserved in result", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + { + BOMRef: "direct1", + Type: cyclonedx.ComponentTypeLibrary, + Name: "Direct 1", + Version: "2.0.0", + Evidence: &cyclonedx.Evidence{ + Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}}, + }, + }, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + }, + }, + component: cyclonedx.Component{ + BOMRef: "direct1", + Name: "Direct 1", + Version: "2.0.0", + Evidence: &cyclonedx.Evidence{ + Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}}, + }, + }, + impactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "Root Component", Version: "1.0.0"}, {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}}}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0", Location: &formats.Location{File: "package.json"}}, + }, + }, + { + name: "Component not in impact paths - return empty", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"transitive1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + impactPaths: [][]formats.ComponentRow{}, + expectedDirects: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actualDirects := ExtractComponentDirectComponentsInBOM(test.bom, test.component, test.impactPaths) + assert.ElementsMatch(t, test.expectedDirects, actualDirects) + }) + } +} + func TestGetFinalApplicabilityStatus(t *testing.T) { testCases := []struct { name string diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go index d6e7c52ce..493e9498a 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -66,7 +66,7 @@ type ResultsStreamFormatParser[T interface{}] interface { DeprecatedParseLicenses(scaResponse services.ScanResponse) error // Parse SCA content to the current scan target ParseSbom(sbom *cyclonedx.BOM) error - ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) error + ParseSbomLicenses(sbom *cyclonedx.BOM) error ParseCVEs(enrichedSbom *cyclonedx.BOM, applicableScan ...[]*sarif.Run) error // Parse JAS content to the current scan target ParseSecrets(secrets ...[]*sarif.Run) error @@ -159,17 +159,17 @@ func parseScaResults[T interface{}](params ResultConvertParams, parser ResultsSt } // Parse the SCA results from the enriched SBOM if params.IncludeVulnerabilities && targetScansResults.ScaResults.Sbom.Vulnerabilities != nil { - if err = parser.ParseCVEs(targetScansResults.ScaResults.Sbom, targetScansResults.JasResults.ApplicabilityScanResults); err != nil { + var applicableRuns []*sarif.Run + if targetScansResults.JasResults != nil { + applicableRuns = targetScansResults.JasResults.ApplicabilityScanResults + } + if err = parser.ParseCVEs(targetScansResults.ScaResults.Sbom, applicableRuns); err != nil { return } } // Must be called last for cyclonedxparser to be able to attach the licenses to all the components if params.IncludeLicenses && targetScansResults.ScaResults.Sbom.Components != nil { - dependencies := []cyclonedx.Dependency{} - if targetScansResults.ScaResults.Sbom.Dependencies != nil { - dependencies = append(dependencies, *targetScansResults.ScaResults.Sbom.Dependencies...) - } - if err = parser.ParseSbomLicenses(*targetScansResults.ScaResults.Sbom.Components, dependencies...); err != nil { + if err = parser.ParseSbomLicenses(targetScansResults.ScaResults.Sbom); err != nil { return } } diff --git a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go index 6e2d2d069..b74a2ffb4 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -124,7 +124,7 @@ func (cdc *CmdResultsCycloneDxConverter) ParseSbom(sbom *cyclonedx.BOM) (err err return } -func (cdc *CmdResultsCycloneDxConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (cdc *CmdResultsCycloneDxConverter) ParseSbomLicenses(_ *cyclonedx.BOM) (err error) { // In CycloneDX, licenses are part of the components and dependencies, so we don't need to parse them separately. return nil } diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index e577f4123..fb7811d63 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -324,7 +324,7 @@ func (sc *CmdResultsSarifConverter) ParseSbom(_ *cyclonedx.BOM) (err error) { return } -func (sc *CmdResultsSarifConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (sc *CmdResultsSarifConverter) ParseSbomLicenses(_ *cyclonedx.BOM) (err error) { // Not supported in Sarif format return } @@ -349,15 +349,16 @@ func (sc *CmdResultsSarifConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, appli func addCdxScaVulnerability(cmdType utils.CommandType, enrichedSbom *cyclonedx.BOM, sarifResults *[]*sarif.Result, rules *map[string]*sarif.ReportingDescriptor) results.ParseBomScaVulnerabilityFunc { return func(vulnerability cyclonedx.Vulnerability, component cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { // Prepare the required fields - directDependencies := getDirectDependenciesForSarif(component, enrichedSbom) - applicabilityStatus, maxCveScore, cves, fixedVersions, markdownDescription, e := prepareCdxInfoForSarif(vulnerability, severity, applicability, directDependencies, fixedVersion) - if e != nil { - return - } dependencies := []cyclonedx.Dependency{} if enrichedSbom.Dependencies != nil { dependencies = append(dependencies, *enrichedSbom.Dependencies...) } + impactPaths := results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...) + directDependencies := results.ExtractComponentDirectComponentsInBOM(enrichedSbom, component, impactPaths) + applicabilityStatus, maxCveScore, cves, fixedVersions, markdownDescription, e := prepareCdxInfoForSarif(vulnerability, severity, applicability, directDependencies, fixedVersion) + if e != nil { + return + } compName, compVersion, _ := techutils.SplitPackageURL(component.PackageURL) createAndAddScaIssue(scaParseParams{ CmdType: cmdType, @@ -374,21 +375,12 @@ func addCdxScaVulnerability(cmdType utils.CommandType, enrichedSbom *cyclonedx.B AddFixedVersionProperty: true, FixedVersions: fixedVersions, DirectComponents: directDependencies, - ImpactPaths: results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...), + ImpactPaths: impactPaths, }, sarifResults, rules) return } } -func getDirectDependenciesForSarif(component cyclonedx.Component, enrichedSbom *cyclonedx.BOM) (directDependencies []formats.ComponentRow) { - // Extract the direct dependencies - dependencies := []cyclonedx.Dependency{} - if enrichedSbom.Dependencies != nil { - dependencies = append(dependencies, *enrichedSbom.Dependencies...) - } - return results.GetDirectDependenciesAsComponentRows(component, *enrichedSbom.Components, dependencies) -} - func prepareCdxInfoForSarif(vulnerability cyclonedx.Vulnerability, severity severityutils.Severity, applicability *formats.Applicability, directDependencies []formats.ComponentRow, fixedVersion *[]cyclonedx.AffectedVersions) (applicabilityStatus jasutils.ApplicabilityStatus, maxCveScore string, cves []formats.CveRow, fixedVersions []string, markdownDescription string, err error) { // Extract the applicability status applicabilityStatus = jasutils.NotScanned diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index 1b5e673e4..36dcde021 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -86,15 +86,19 @@ func (sjc *CmdResultsSimpleJsonConverter) DeprecatedParseScaVulnerabilities(desc return } -func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(sbom *cyclonedx.BOM) (err error) { if sjc.current == nil { return results.ErrResetConvertor } - if len(components) == 0 { + if sbom == nil || sbom.Components == nil || len(*sbom.Components) == 0 { return } + dependencies := []cyclonedx.Dependency{} + if sbom.Dependencies != nil { + dependencies = append(dependencies, *sbom.Dependencies...) + } // Iterate through the components and collect licenses - for _, component := range components { + for _, component := range *sbom.Components { if component.Licenses == nil || len(*component.Licenses) == 0 { // No licenses found for this component, continue to the next one continue @@ -109,6 +113,7 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(components []cyclone if name == "" { name = license.License.ID } + impactPaths := results.BuildImpactPath(component, *sbom.Components, dependencies...) sjc.current.Licenses = append(sjc.current.Licenses, formats.LicenseRow{ LicenseKey: license.License.ID, LicenseName: name, @@ -116,9 +121,9 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(components []cyclone ImpactedDependencyName: strings.ReplaceAll(compName, "/", ":"), ImpactedDependencyVersion: compVersion, ImpactedDependencyType: techutils.ConvertXrayPackageType(techutils.CdxPackageTypeToXrayPackageType(compType)), - Components: results.GetDirectDependenciesAsComponentRows(component, components, dependencies), + Components: results.ExtractComponentDirectComponentsInBOM(sbom, component, impactPaths), }, - ImpactPaths: results.BuildImpactPath(component, components, dependencies...), + ImpactPaths: impactPaths, }) } } @@ -135,6 +140,7 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, if enrichedSbom.Dependencies != nil { dependencies = append(dependencies, *enrichedSbom.Dependencies...) } + impactPaths := results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...) // Convert the CycloneDX vulnerability to a simple JSON vulnerability row sjc.current.Vulnerabilities = append(sjc.current.Vulnerabilities, sjc.createVulnerabilityOrViolationRowFromCdx( vulnerability.ID, @@ -143,8 +149,8 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, applicability, vulnerability, component, - results.GetDirectDependenciesAsComponentRows(component, *enrichedSbom.Components, dependencies), - results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...), + results.ExtractComponentDirectComponentsInBOM(enrichedSbom, component, impactPaths), + impactPaths, fixedVersions, // TODO: implement JfrogResearchInformation conversion nil, diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go index 6da577b98..d792a701e 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go @@ -239,11 +239,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-A", // Direct Components: []formats.ComponentRow{{ + Id: "component-A", Name: "component-A", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-A", Name: "component-A"}}}, }, { Summary: "summary-1", @@ -254,11 +255,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, { Summary: "summary-2", @@ -269,11 +271,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, }, }, @@ -309,11 +312,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-A", // Direct Components: []formats.ComponentRow{{ + Id: "component-A", Name: "component-A", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-A", Name: "component-A"}}}, }, { Summary: "summary-1", @@ -331,11 +335,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, { Summary: "summary-2", @@ -360,11 +365,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, }, }, @@ -414,11 +420,12 @@ func TestPrepareSimpleJsonLicenses(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "target"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, }, }, diff --git a/utils/results/conversion/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go index 0c7a547be..0b2fcbe9c 100644 --- a/utils/results/conversion/summaryparser/summaryparser.go +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -176,7 +176,7 @@ func (sc *CmdResultsSummaryConverter) ParseSbom(_ *cyclonedx.BOM) (err error) { return } -func (sc *CmdResultsSummaryConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (sc *CmdResultsSummaryConverter) ParseSbomLicenses(_ *cyclonedx.BOM) (err error) { // Not supported in the summary return } diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go index 0785c2d10..1ff074132 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -68,8 +68,8 @@ func (tc *CmdResultsTableConverter) DeprecatedParseLicenses(scaResponse services return tc.simpleJsonConvertor.DeprecatedParseLicenses(scaResponse) } -func (tc *CmdResultsTableConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { - return tc.simpleJsonConvertor.ParseSbomLicenses(components, dependencies...) +func (tc *CmdResultsTableConverter) ParseSbomLicenses(sbom *cyclonedx.BOM) (err error) { + return tc.simpleJsonConvertor.ParseSbomLicenses(sbom) } func (tc *CmdResultsTableConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, applicableScan ...[]*sarif.Run) (err error) {