Skip to content

Commit 7907f40

Browse files
authored
Docker curation supprot (#608)
1 parent 059d7e7 commit 7907f40

File tree

9 files changed

+562
-3
lines changed

9 files changed

+562
-3
lines changed

cli/docs/flags.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,9 @@ const (
151151
AnalyzerManagerCustomPath = "analyzer-manager-path"
152152

153153
// Unique curation flags
154-
CurationOutput = "curation-format"
155-
SolutionPath = "solution-path"
154+
CurationOutput = "curation-format"
155+
DockerImageName = "image"
156+
SolutionPath = "solution-path"
156157

157158
// Unique git flags
158159
InputFile = "input-file"
@@ -211,7 +212,7 @@ var commandFlags = map[string][]string{
211212
StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, AddSastRules,
212213
},
213214
CurationAudit: {
214-
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, SolutionPath,
215+
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, SolutionPath, DockerImageName,
215216
},
216217
GitCountContributors: {
217218
InputFile, ScmType, ScmApiUrl, Token, Owner, RepoName, Months, DetailedSummary, InsecureTls,
@@ -336,6 +337,9 @@ var flagsMap = map[string]components.Flag{
336337

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

340+
// Docker flags
341+
DockerImageName: components.NewStringFlag(DockerImageName, "Specifies the Docker image name to audit. Uses the same format as the Docker CLI, including Artifactory-hosted images."),
342+
339343
// Git flags
340344
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.."),
341345
ScmType: components.NewStringFlag(ScmType, fmt.Sprintf("SCM type. Possible values are: %s.", contributors.NewScmType().GetValidScmTypeString()), components.SetMandatory()),

cli/scancommands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,7 @@ func getCurationCommand(c *components.Context) (*curation.CurationAuditCommand,
697697
SetNpmScope(c.GetStringFlagValue(flags.DepType)).
698698
SetPipRequirementsFile(c.GetStringFlagValue(flags.RequirementsFile)).
699699
SetSolutionFilePath(c.GetStringFlagValue(flags.SolutionPath))
700+
curationAuditCommand.SetDockerImageName(c.GetStringFlagValue(flags.DockerImageName))
700701
return curationAuditCommand, nil
701702
}
702703

commands/curation/curationaudit.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/jfrog/jfrog-cli-security/commands/audit"
3838
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo"
3939
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies"
40+
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/docker"
4041
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/python"
4142
"github.com/jfrog/jfrog-cli-security/utils"
4243
"github.com/jfrog/jfrog-cli-security/utils/formats"
@@ -102,6 +103,7 @@ var supportedTech = map[techutils.Technology]func(ca *CurationAuditCommand) (boo
102103
techutils.Gem: func(ca *CurationAuditCommand) (bool, error) {
103104
return ca.checkSupportByVersionOrEnv(techutils.Gem, MinArtiGradleGemSupport)
104105
},
106+
techutils.Docker: func(ca *CurationAuditCommand) (bool, error) { return true, nil },
105107
}
106108

107109
func (ca *CurationAuditCommand) checkSupportByVersionOrEnv(tech techutils.Technology, minArtiVersion string) (bool, error) {
@@ -217,6 +219,7 @@ type CurationAuditCommand struct {
217219
workingDirs []string
218220
OriginPath string
219221
parallelRequests int
222+
dockerImageName string
220223
audit.AuditParamsInterface
221224
}
222225

@@ -253,6 +256,15 @@ func (ca *CurationAuditCommand) SetParallelRequests(threads int) *CurationAuditC
253256
return ca
254257
}
255258

259+
func (ca *CurationAuditCommand) DockerImageName() string {
260+
return ca.dockerImageName
261+
}
262+
263+
func (ca *CurationAuditCommand) SetDockerImageName(dockerImageName string) *CurationAuditCommand {
264+
ca.dockerImageName = dockerImageName
265+
return ca
266+
}
267+
256268
func (ca *CurationAuditCommand) Run() (err error) {
257269
rootDir, err := os.Getwd()
258270
if err != nil {
@@ -350,6 +362,10 @@ func getPolicyAndConditionId(policy, condition string) string {
350362

351363
func (ca *CurationAuditCommand) doCurateAudit(results map[string]*CurationReport) error {
352364
techs := techutils.DetectedTechnologiesList()
365+
if ca.DockerImageName() != "" {
366+
log.Debug(fmt.Sprintf("Docker image name '%s' was provided, running Docker curation audit.", ca.DockerImageName()))
367+
techs = []string{techutils.Docker.String()}
368+
}
353369
for _, tech := range techs {
354370
supportedFunc, ok := supportedTech[techutils.Technology(tech)]
355371
if !ok {
@@ -426,6 +442,8 @@ func (ca *CurationAuditCommand) getBuildInfoParamsByTech() (technologies.BuildIn
426442
NpmOverwritePackageLock: true,
427443
// Python params
428444
PipRequirementsFile: ca.PipRequirementsFile(),
445+
// Docker params
446+
DockerImageName: ca.DockerImageName(),
429447
// NuGet params
430448
SolutionFilePath: ca.SolutionFilePath(),
431449
}, err
@@ -718,6 +736,16 @@ func (ca *CurationAuditCommand) CommandName() string {
718736
}
719737

720738
func (ca *CurationAuditCommand) SetRepo(tech techutils.Technology) error {
739+
// If the technology is Docker, we need to get the repository config from the Docker image name
740+
if tech == techutils.Docker {
741+
repoConfig, err := docker.GetDockerRepositoryConfig(ca.DockerImageName())
742+
if err != nil {
743+
return err
744+
}
745+
ca.setPackageManagerConfig(repoConfig)
746+
return nil
747+
}
748+
721749
resolverParams, err := ca.getRepoParams(techutils.TechToProjectType[tech])
722750
if err != nil {
723751
return err
@@ -950,6 +978,9 @@ func getUrlNameAndVersionByTech(tech techutils.Technology, node *xrayUtils.Graph
950978
case techutils.Nuget:
951979
downloadUrls, name, version = getNugetNameScopeAndVersion(node.Id, artiUrl, repo)
952980
return
981+
case techutils.Docker:
982+
downloadUrls, name, version = getDockerNameAndVersion(node.Id, artiUrl, repo)
983+
return
953984
}
954985
return
955986
}
@@ -1134,6 +1165,39 @@ func buildNpmDownloadUrl(url, repo, name, scope, version string) []string {
11341165
return []string{packageUrl}
11351166
}
11361167

1168+
func getDockerNameAndVersion(id, artiUrl, repo string) (downloadUrls []string, name, version string) {
1169+
if id == "" {
1170+
return
1171+
}
1172+
1173+
id = strings.TrimPrefix(id, "docker://")
1174+
1175+
sha256Idx := strings.Index(id, ":sha256:")
1176+
tagIdx := strings.LastIndex(id, ":")
1177+
1178+
switch {
1179+
// Example: docker://nginx:sha256:abc123def456
1180+
case sha256Idx > 0:
1181+
name = id[:sha256Idx]
1182+
version = id[sha256Idx+1:]
1183+
// Example: docker://nginx:1.21
1184+
case tagIdx > 0:
1185+
name = id[:tagIdx]
1186+
version = id[tagIdx+1:]
1187+
// Example: docker://nginx (no tag specified, defaults to "latest")
1188+
default:
1189+
name = id
1190+
version = "latest"
1191+
}
1192+
1193+
if artiUrl != "" && repo != "" {
1194+
downloadUrls = []string{fmt.Sprintf("%s/api/docker/%s/v2/%s/manifests/%s",
1195+
strings.TrimSuffix(artiUrl, "/"), repo, name, version)}
1196+
}
1197+
1198+
return
1199+
}
1200+
11371201
func GetCurationOutputFormat(formatFlagVal string) (format outFormat.OutputFormat, err error) {
11381202
// Default print format is table.
11391203
format = outFormat.Table

commands/curation/curationaudit_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,62 @@ func Test_getGemNameScopeAndVersion(t *testing.T) {
11871187
}
11881188
}
11891189

1190+
func Test_getDockerNameAndVersion(t *testing.T) {
1191+
tests := []struct {
1192+
name string
1193+
id string
1194+
artiUrl string
1195+
repo string
1196+
wantDownloadUrls []string
1197+
wantName string
1198+
wantVersion string
1199+
}{
1200+
{
1201+
name: "Basic docker image with tag",
1202+
id: "docker://nginx:1.21.0",
1203+
artiUrl: "http://test.jfrog.io/artifactory",
1204+
repo: "docker-remote",
1205+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/1.21.0"},
1206+
wantName: "nginx",
1207+
wantVersion: "1.21.0",
1208+
},
1209+
{
1210+
name: "Docker image with registry prefix",
1211+
id: "docker://registry.example.com/nginx:1.21.0",
1212+
artiUrl: "http://test.jfrog.io/artifactory",
1213+
repo: "docker-remote",
1214+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/registry.example.com/nginx/manifests/1.21.0"},
1215+
wantName: "registry.example.com/nginx",
1216+
wantVersion: "1.21.0",
1217+
},
1218+
{
1219+
name: "Docker image with sha256 digest",
1220+
id: "docker://nginx:sha256:abc123def456",
1221+
artiUrl: "http://test.jfrog.io/artifactory",
1222+
repo: "docker-remote",
1223+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/sha256:abc123def456"},
1224+
wantName: "nginx",
1225+
wantVersion: "sha256:abc123def456",
1226+
},
1227+
{
1228+
name: "Docker image without version defaults to latest",
1229+
id: "docker://nginx",
1230+
artiUrl: "http://test.jfrog.io/artifactory",
1231+
repo: "docker-remote",
1232+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/latest"},
1233+
wantName: "nginx",
1234+
wantVersion: "latest",
1235+
},
1236+
}
1237+
for _, tt := range tests {
1238+
t.Run(tt.name, func(t *testing.T) {
1239+
gotDownloadUrls, gotName, gotVersion := getDockerNameAndVersion(tt.id, tt.artiUrl, tt.repo)
1240+
assert.Equal(t, tt.wantDownloadUrls, gotDownloadUrls, "downloadUrls mismatch")
1241+
assert.Equal(t, tt.wantName, gotName, "name mismatch")
1242+
assert.Equal(t, tt.wantVersion, gotVersion, "version mismatch")
1243+
})
1244+
}
1245+
}
11901246
func Test_getNugetNameScopeAndVersion(t *testing.T) {
11911247
tests := []struct {
11921248
name string

curation_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"path/filepath"
9+
"runtime"
910
"strings"
1011
"sync"
1112
"testing"
@@ -105,6 +106,34 @@ func getCurationExpectedResponse(config *config.ServerDetails) []curation.Packag
105106
return expectedResp
106107
}
107108

109+
func TestDockerCurationAudit(t *testing.T) {
110+
integration.InitCurationTest(t)
111+
if securityTests.ContainerRegistry == nil || *securityTests.ContainerRegistry == "" || runtime.GOOS == "darwin" {
112+
t.Skip("Skipping Docker curation test - container registry not configured")
113+
}
114+
cleanUp := integration.UseTestHomeWithDefaultXrayConfig(t)
115+
defer cleanUp()
116+
117+
testImage := fmt.Sprintf("%s/%s/%s", *securityTests.ContainerRegistry, "docker-curation", "ganodndentcom/drupal")
118+
119+
output := securityTests.PlatformCli.WithoutCredentials().RunCliCmdWithOutput(t, "curation-audit",
120+
"--image="+testImage,
121+
"--format="+string(format.Json))
122+
bracketIndex := strings.Index(output, "[")
123+
require.GreaterOrEqual(t, bracketIndex, 0, "Expected JSON array in output, got: %s", output)
124+
125+
var results []curation.PackageStatus
126+
err := json.Unmarshal([]byte(output[bracketIndex:]), &results)
127+
require.NoError(t, err)
128+
129+
require.NotEmpty(t, results, "Expected at least one blocked package")
130+
assert.Equal(t, "blocked", results[0].Action)
131+
assert.Equal(t, "ganodndentcom/drupal", results[0].PackageName)
132+
assert.Equal(t, curation.BlockingReasonPolicy, results[0].BlockingReason)
133+
require.NotEmpty(t, results[0].Policy, "Expected at least one policy violation")
134+
assert.Equal(t, "Malicious package", results[0].Policy[0].Condition)
135+
}
136+
108137
func curationServer(t *testing.T, expectedRequest map[string]bool, requestToFail map[string]bool) (*httptest.Server, *config.ServerDetails) {
109138
mapLockReadWrite := sync.Mutex{}
110139
serverMock, config, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) {

sca/bom/buildinfo/buildinfobom.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies"
3030
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/cocoapods"
3131
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/conan"
32+
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/docker"
3233
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/gem"
3334
_go "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/go"
3435
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/java"
@@ -225,6 +226,8 @@ func GetTechDependencyTree(params technologies.BuildInfoBomGeneratorParams, arti
225226
return depTreeResult, fmt.Errorf("your xray version %s does not allow swift scanning", params.XrayVersion)
226227
}
227228
depTreeResult.FullDepTrees, uniqueDepsIds, err = swift.BuildDependencyTree(params)
229+
case techutils.Docker:
230+
depTreeResult.FullDepTrees, uniqueDepsIds, err = docker.BuildDependencyTree(params)
228231
default:
229232
err = errorutils.CheckErrorf("%s is currently not supported", string(tech))
230233
}

sca/bom/buildinfo/technologies/common.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ type BuildInfoBomGeneratorParams struct {
5858
NpmOverwritePackageLock bool
5959
// Pnpm params
6060
MaxTreeDepth string
61+
// Docker params
62+
DockerImageName string
6163
// NuGet params
6264
SolutionFilePath string
6365
}

0 commit comments

Comments
 (0)