Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e7ab6f3
Add Docker curation support
basel1322 Dec 10, 2025
a7b6843
Improved Docker curation to use docker pull and add Accept headers fo…
basel1322 Dec 10, 2025
a814b8d
Simplify Docker curation code
basel1322 Dec 10, 2025
57396a7
Merge branch 'dev' into docker-curation-supprot
basel1322 Dec 14, 2025
ac16c65
Fixed unit test for Docker curation
basel1322 Dec 14, 2025
3171ad9
improved the code
basel1322 Dec 16, 2025
ecd4a99
fixed unit test commit
basel1322 Dec 16, 2025
4c0adc9
added get version for docker
basel1322 Dec 16, 2025
958ee03
Add Docker curation audit integration test
basel1322 Dec 18, 2025
b22d8cf
Add Docker login in test and fix panic on parse error
basel1322 Dec 18, 2025
d1a5492
Use same test setup pattern as Docker scan tests
basel1322 Dec 18, 2025
cb6f4e5
Use anonymous Docker access for curation test
basel1322 Dec 18, 2025
2e5ad3d
Skip Docker curation test on macOS CI
basel1322 Dec 18, 2025
59a3a7e
Update commands/curation/curationaudit.go
basel1322 Dec 18, 2025
490440d
Update curationaudit.go
basel1322 Dec 18, 2025
06c852a
Update commands/curation/curationaudit.go
basel1322 Dec 21, 2025
5337c9c
Refactor Docker image parsing and remove redundant checkDockerSupport
basel1322 Dec 21, 2025
5f568e8
Fix unparam lint issue
basel1322 Dec 21, 2025
97100eb
feat: get arch-specific digest for successful docker pulls using buildx
basel1322 Dec 22, 2025
207e9b1
fix: fixed typo in buildx inspect debug logs
basel1322 Dec 22, 2025
fe4c435
fix: show actual error message for docker buildx imagetools inspect f…
basel1322 Dec 22, 2025
ae9c166
fix: properly return error from docker buildx imagetools inspect
basel1322 Dec 22, 2025
77c2f19
Merge branch 'dev' into docker-curation-supprot
basel1322 Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ const (
AnalyzerManagerCustomPath = "analyzer-manager-path"

// Unique curation flags
CurationOutput = "curation-format"
SolutionPath = "solution-path"
CurationOutput = "curation-format"
DockerImageName = "image"
SolutionPath = "solution-path"

// Unique git flags
InputFile = "input-file"
Expand Down Expand Up @@ -206,7 +207,7 @@ var commandFlags = map[string][]string{
StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, AddSastRules,
},
CurationAudit: {
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, SolutionPath,
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, SolutionPath, DockerImageName,
},
GitCountContributors: {
InputFile, ScmType, ScmApiUrl, Token, Owner, RepoName, Months, DetailedSummary, InsecureTls,
Expand Down Expand Up @@ -330,6 +331,9 @@ var flagsMap = map[string]components.Flag{

AddSastRules: components.NewStringFlag(AddSastRules, "Incorporate any additional SAST rules (in JSON format, with absolute path) into this local scan."),

// Docker flags
DockerImageName: components.NewStringFlag(DockerImageName, "[Docker] Defines the Docker image name to audit. Uses the same format as Docker client with Artifactory. Examples: 'acme.jfrog.io/docker-local/nginx:1.21' (repository path) or 'acme-docker-local.jfrog.io/nginx:1.21' (subdomain)."),

// Git flags
InputFile: components.NewStringFlag(InputFile, "Path to an input file in YAML format contains multiple git providers. With this option, all other scm flags will be ignored and only git servers mentioned in the file will be examined.."),
ScmType: components.NewStringFlag(ScmType, fmt.Sprintf("SCM type. Possible values are: %s.", contributors.NewScmType().GetValidScmTypeString()), components.SetMandatory()),
Expand Down
1 change: 1 addition & 0 deletions cli/scancommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ func getCurationCommand(c *components.Context) (*curation.CurationAuditCommand,
SetInsecureTls(c.GetBoolFlagValue(flags.InsecureTls)).
SetNpmScope(c.GetStringFlagValue(flags.DepType)).
SetPipRequirementsFile(c.GetStringFlagValue(flags.RequirementsFile)).
SetDockerImageName(c.GetStringFlagValue(flags.DockerImageName)).
SetSolutionFilePath(c.GetStringFlagValue(flags.SolutionPath))
return curationAuditCommand, nil
}
Expand Down
12 changes: 12 additions & 0 deletions commands/audit/auditbasicparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type AuditParamsInterface interface {
AllowPartialResults() bool
GetXrayVersion() string
GetConfigProfile() *xscservices.ConfigProfile
DockerImageName() string
SetDockerImageName(dockerImageName string) *AuditBasicParams
SolutionFilePath() string
SetSolutionFilePath(solutionFilePath string) *AuditBasicParams
}
Expand Down Expand Up @@ -81,6 +83,7 @@ type AuditBasicParams struct {
xrayVersion string
xscVersion string
configProfile *xscservices.ConfigProfile
dockerImageName string
solutionFilePath string
}

Expand Down Expand Up @@ -334,6 +337,15 @@ func (abp *AuditBasicParams) GetConfigProfile() *xscservices.ConfigProfile {
return abp.configProfile
}

func (abp *AuditBasicParams) DockerImageName() string {
return abp.dockerImageName
}

func (abp *AuditBasicParams) SetDockerImageName(dockerImageName string) *AuditBasicParams {
abp.dockerImageName = dockerImageName
return abp
}

func (abp *AuditBasicParams) SolutionFilePath() string {
return abp.solutionFilePath
}
Expand Down
2 changes: 2 additions & 0 deletions commands/audit/auditparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ func (params *AuditParams) ToBuildInfoBomGenParams() (bomParams technologies.Bui
PipRequirementsFile: params.PipRequirementsFile(),
// Pnpm params
MaxTreeDepth: params.MaxTreeDepth(),
// Docker params
DockerImageName: params.DockerImageName(),
}
return
}
Expand Down
45 changes: 45 additions & 0 deletions commands/curation/curationaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/jfrog/jfrog-cli-security/commands/audit"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/docker"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/python"
"github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/formats"
Expand Down Expand Up @@ -102,6 +103,7 @@ var supportedTech = map[techutils.Technology]func(ca *CurationAuditCommand) (boo
techutils.Gem: func(ca *CurationAuditCommand) (bool, error) {
return ca.checkSupportByVersionOrEnv(techutils.Gem, MinArtiGradleGemSupport)
},
techutils.Docker: func(ca *CurationAuditCommand) (bool, error) { return true, nil },
}

func (ca *CurationAuditCommand) checkSupportByVersionOrEnv(tech techutils.Technology, minArtiVersion string) (bool, error) {
Expand Down Expand Up @@ -350,6 +352,9 @@ func getPolicyAndConditionId(policy, condition string) string {

func (ca *CurationAuditCommand) doCurateAudit(results map[string]*CurationReport) error {
techs := techutils.DetectedTechnologiesList()
if ca.DockerImageName() != "" {
techs = []string{techutils.Docker.String()}
}
for _, tech := range techs {
supportedFunc, ok := supportedTech[techutils.Technology(tech)]
if !ok {
Expand Down Expand Up @@ -426,6 +431,8 @@ func (ca *CurationAuditCommand) getBuildInfoParamsByTech() (technologies.BuildIn
NpmOverwritePackageLock: true,
// Python params
PipRequirementsFile: ca.PipRequirementsFile(),
// Docker params
DockerImageName: ca.DockerImageName(),
// NuGet params
SolutionFilePath: ca.SolutionFilePath(),
}, err
Expand Down Expand Up @@ -718,6 +725,16 @@ func (ca *CurationAuditCommand) CommandName() string {
}

func (ca *CurationAuditCommand) SetRepo(tech techutils.Technology) error {
// If the technology is Docker, we need to get the repository config from the Docker image name
if tech == techutils.Docker {
repoConfig, err := docker.GetDockerRepositoryConfig(ca.DockerImageName())
if err != nil {
return err
}
ca.setPackageManagerConfig(repoConfig)
return nil
}

resolverParams, err := ca.getRepoParams(techutils.TechToProjectType[tech])
if err != nil {
return err
Expand Down Expand Up @@ -950,6 +967,8 @@ func getUrlNameAndVersionByTech(tech techutils.Technology, node *xrayUtils.Graph
case techutils.Nuget:
downloadUrls, name, version = getNugetNameScopeAndVersion(node.Id, artiUrl, repo)
return
case techutils.Docker:
return getDockerNameScopeAndVersion(node.Id, artiUrl, repo)
}
return
}
Expand Down Expand Up @@ -1134,6 +1153,32 @@ func buildNpmDownloadUrl(url, repo, name, scope, version string) []string {
return []string{packageUrl}
}

func getDockerNameScopeAndVersion(id, artiUrl, repo string) (downloadUrls []string, name, scope, version string) {
if id == "" {
return
}

id = strings.TrimPrefix(id, "docker://")

if idx := strings.Index(id, ":sha256:"); idx > 0 {
name = id[:idx]
version = id[idx+1:]
} else if idx := strings.LastIndex(id, ":"); idx > 0 {
name = id[:idx]
version = id[idx+1:]
} else {
name = id
version = "latest"
}

if artiUrl != "" && repo != "" {
downloadUrls = []string{fmt.Sprintf("%s/api/docker/%s/v2/%s/manifests/%s",
strings.TrimSuffix(artiUrl, "/"), repo, name, version)}
}

return downloadUrls, name, scope, version
}

func GetCurationOutputFormat(formatFlagVal string) (format outFormat.OutputFormat, err error) {
// Default print format is table.
format = outFormat.Table
Expand Down
62 changes: 62 additions & 0 deletions commands/curation/curationaudit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,68 @@ func Test_getGemNameScopeAndVersion(t *testing.T) {
}
}

func Test_getDockerNameScopeAndVersion(t *testing.T) {
tests := []struct {
name string
id string
artiUrl string
repo string
wantDownloadUrls []string
wantName string
wantScope string
wantVersion string
}{
{
name: "Basic docker image with tag",
id: "docker://nginx:1.21.0",
artiUrl: "http://test.jfrog.io/artifactory",
repo: "docker-remote",
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/1.21.0"},
wantName: "nginx",
wantScope: "",
wantVersion: "1.21.0",
},
{
name: "Docker image with registry prefix",
id: "docker://registry.example.com/nginx:1.21.0",
artiUrl: "http://test.jfrog.io/artifactory",
repo: "docker-remote",
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/registry.example.com/nginx/manifests/1.21.0"},
wantName: "registry.example.com/nginx",
wantScope: "",
wantVersion: "1.21.0",
},
{
name: "Docker image with sha256 digest",
id: "docker://nginx:sha256:abc123def456",
artiUrl: "http://test.jfrog.io/artifactory",
repo: "docker-remote",
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/sha256:abc123def456"},
wantName: "nginx",
wantScope: "",
wantVersion: "sha256:abc123def456",
},
{
name: "Docker image without version defaults to latest",
id: "docker://nginx",
artiUrl: "http://test.jfrog.io/artifactory",
repo: "docker-remote",
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/latest"},
wantName: "nginx",
wantScope: "",
wantVersion: "latest",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotDownloadUrls, gotName, gotScope, gotVersion := getDockerNameScopeAndVersion(tt.id, tt.artiUrl, tt.repo)
assert.Equal(t, tt.wantDownloadUrls, gotDownloadUrls, "downloadUrls mismatch")
assert.Equal(t, tt.wantName, gotName, "name mismatch")
assert.Equal(t, tt.wantScope, gotScope, "scope mismatch")
assert.Equal(t, tt.wantVersion, gotVersion, "version mismatch")
})
}
}
func Test_getNugetNameScopeAndVersion(t *testing.T) {
tests := []struct {
name string
Expand Down
34 changes: 34 additions & 0 deletions curation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"net/http"
"net/http/httptest"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/jfrog/jfrog-cli-security/cli"
"github.com/jfrog/jfrog-cli-security/commands/curation"
securityTests "github.com/jfrog/jfrog-cli-security/tests"
securityTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils"
Expand Down Expand Up @@ -105,6 +107,38 @@ func getCurationExpectedResponse(config *config.ServerDetails) []curation.Packag
return expectedResp
}

func TestDockerCurationAudit(t *testing.T) {
integration.InitCurationTest(t)
if securityTests.ContainerRegistry == nil || *securityTests.ContainerRegistry == "" || runtime.GOOS == "darwin" {
t.Skip("Skipping Docker curation test - container registry not configured")
}
cleanUp := integration.UseTestHomeWithDefaultXrayConfig(t)
defer cleanUp()
integration.CreateJfrogHomeConfig(t, "", securityTests.XrDetails, true)

testCli := integration.GetXrayTestCli(cli.GetJfrogCliSecurityApp(), false)

testImage := fmt.Sprintf("%s/%s/%s", *securityTests.ContainerRegistry, "docker-curation", "ganodndentcom/drupal")

output := testCli.WithoutCredentials().RunCliCmdWithOutput(t, "curation-audit",
"--image="+testImage,
"--format="+string(format.Json))

bracketIndex := strings.Index(output, "[")
require.GreaterOrEqual(t, bracketIndex, 0, "Expected JSON array in output, got: %s", output)

var results []curation.PackageStatus
err := json.Unmarshal([]byte(output[bracketIndex:]), &results)
require.NoError(t, err)

require.NotEmpty(t, results, "Expected at least one blocked package")
assert.Equal(t, "blocked", results[0].Action)
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)
}

func curationServer(t *testing.T, expectedRequest map[string]bool, requestToFail map[string]bool) (*httptest.Server, *config.ServerDetails) {
mapLockReadWrite := sync.Mutex{}
serverMock, config, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) {
Expand Down
3 changes: 3 additions & 0 deletions sca/bom/buildinfo/buildinfobom.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/cocoapods"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/conan"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/docker"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/gem"
_go "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/go"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/java"
Expand Down Expand Up @@ -225,6 +226,8 @@ func GetTechDependencyTree(params technologies.BuildInfoBomGeneratorParams, arti
return depTreeResult, fmt.Errorf("your xray version %s does not allow swift scanning", params.XrayVersion)
}
depTreeResult.FullDepTrees, uniqueDepsIds, err = swift.BuildDependencyTree(params)
case techutils.Docker:
depTreeResult.FullDepTrees, uniqueDepsIds, err = docker.BuildDependencyTree(params)
default:
err = errorutils.CheckErrorf("%s is currently not supported", string(tech))
}
Expand Down
2 changes: 2 additions & 0 deletions sca/bom/buildinfo/technologies/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ type BuildInfoBomGeneratorParams struct {
NpmOverwritePackageLock bool
// Pnpm params
MaxTreeDepth string
// Docker params
DockerImageName string
// NuGet params
SolutionFilePath string
}
Expand Down
Loading
Loading