Skip to content

Commit ff915c5

Browse files
committed
Add Docker curation support
1 parent f0eb9c7 commit ff915c5

File tree

10 files changed

+578
-6
lines changed

10 files changed

+578
-6
lines changed

cli/docs/flags.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ const (
139139
AnalyzerManagerCustomPath = "analyzer-manager-path"
140140

141141
// Unique curation flags
142-
CurationOutput = "curation-format"
142+
CurationOutput = "curation-format"
143+
DockerImageName = "image"
143144
SolutionPath = "solution-path"
144145

145146
// Unique git flags
@@ -194,7 +195,7 @@ var commandFlags = map[string][]string{
194195
StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, AddSastRules,
195196
},
196197
CurationAudit: {
197-
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, SolutionPath,
198+
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, SolutionPath,DockerImageName,
198199
},
199200
GitCountContributors: {
200201
InputFile, ScmType, ScmApiUrl, Token, Owner, RepoName, Months, DetailedSummary, InsecureTls,
@@ -314,6 +315,9 @@ var flagsMap = map[string]components.Flag{
314315

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

318+
// Docker flags
319+
DockerImageName: components.NewStringFlag(DockerImageName, "[Docker] Defines the Docker image name to audit. Format: 'repo/path/image:tag'. For example: 'curation-docker/dweomer/nginx-auth-ldap:1.13.5' or 'repo/image:tag'. If no tag is provided, 'latest' is used."),
320+
317321
// Git flags
318322
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.."),
319323
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
@@ -647,6 +647,7 @@ func getCurationCommand(c *components.Context) (*curation.CurationAuditCommand,
647647
SetInsecureTls(c.GetBoolFlagValue(flags.InsecureTls)).
648648
SetNpmScope(c.GetStringFlagValue(flags.DepType)).
649649
SetPipRequirementsFile(c.GetStringFlagValue(flags.RequirementsFile)).
650+
SetDockerImageName(c.GetStringFlagValue(flags.DockerImageName)).
650651
SetSolutionFilePath(c.GetStringFlagValue(flags.SolutionPath))
651652
return curationAuditCommand, nil
652653
}

commands/audit/auditbasicparams.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ type AuditParamsInterface interface {
4949
AllowPartialResults() bool
5050
GetXrayVersion() string
5151
GetConfigProfile() *xscservices.ConfigProfile
52+
DockerImageName() string
53+
SetDockerImageName(dockerImageName string) *AuditBasicParams
5254
SolutionFilePath() string
5355
SetSolutionFilePath(solutionFilePath string) *AuditBasicParams
5456
}
@@ -81,6 +83,7 @@ type AuditBasicParams struct {
8183
xrayVersion string
8284
xscVersion string
8385
configProfile *xscservices.ConfigProfile
86+
dockerImageName string
8487
solutionFilePath string
8588
}
8689

@@ -334,6 +337,15 @@ func (abp *AuditBasicParams) GetConfigProfile() *xscservices.ConfigProfile {
334337
return abp.configProfile
335338
}
336339

340+
func (abp *AuditBasicParams) DockerImageName() string {
341+
return abp.dockerImageName
342+
}
343+
344+
func (abp *AuditBasicParams) SetDockerImageName(dockerImageName string) *AuditBasicParams {
345+
abp.dockerImageName = dockerImageName
346+
return abp
347+
}
348+
337349
func (abp *AuditBasicParams) SolutionFilePath() string {
338350
return abp.solutionFilePath
339351
}

commands/audit/auditparams.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ func (params *AuditParams) ToBuildInfoBomGenParams() (bomParams technologies.Bui
227227
PipRequirementsFile: params.PipRequirementsFile(),
228228
// Pnpm params
229229
MaxTreeDepth: params.MaxTreeDepth(),
230+
// Docker params
231+
DockerImageName: params.DockerImageName(),
230232
}
231233
return
232234
}

commands/curation/curationaudit.go

Lines changed: 70 additions & 2 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,9 @@ 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) {
107+
return ca.checkDockerSupport()
108+
},
105109
}
106110

107111
func (ca *CurationAuditCommand) checkSupportByVersionOrEnv(tech techutils.Technology, minArtiVersion string) (bool, error) {
@@ -128,6 +132,17 @@ func (ca *CurationAuditCommand) checkSupportByVersionOrEnv(tech techutils.Techno
128132
return true, nil
129133
}
130134

135+
func (ca *CurationAuditCommand) checkDockerSupport() (bool, error) {
136+
dockerImageName := ca.DockerImageName()
137+
if dockerImageName == "" {
138+
return false, nil
139+
}
140+
if !strings.Contains(dockerImageName, "/") {
141+
return false, errorutils.CheckErrorf("invalid docker image format: '%s'. Expected format: 'repo/image:tag' or 'repo/path/image:tag'", dockerImageName)
142+
}
143+
return true, nil
144+
}
145+
131146
func (ca *CurationAuditCommand) getRtVersion(tech techutils.Technology) (string, error) {
132147
rtManager, _, err := ca.getRtManagerAndAuth(tech)
133148
if err != nil {
@@ -350,6 +365,9 @@ func getPolicyAndConditionId(policy, condition string) string {
350365

351366
func (ca *CurationAuditCommand) doCurateAudit(results map[string]*CurationReport) error {
352367
techs := techutils.DetectedTechnologiesList()
368+
if ca.DockerImageName() != "" {
369+
techs = []string{techutils.Docker.String()}
370+
}
353371
for _, tech := range techs {
354372
supportedFunc, ok := supportedTech[techutils.Technology(tech)]
355373
if !ok {
@@ -390,8 +408,20 @@ func (ca *CurationAuditCommand) getRtManagerAndAuth(tech techutils.Technology) (
390408

391409
func (ca *CurationAuditCommand) GetAuth(tech techutils.Technology) (serverDetails *config.ServerDetails, err error) {
392410
if ca.PackageManagerConfig == nil {
393-
if err = ca.SetRepo(tech); err != nil {
394-
return
411+
if tech == techutils.Docker {
412+
serverDetails, err = ca.ServerDetails()
413+
if err != nil {
414+
return
415+
}
416+
repoConfig, err := docker.GetDockerRepositoryConfig(serverDetails, ca.DockerImageName())
417+
if err != nil {
418+
return nil, err
419+
}
420+
ca.setPackageManagerConfig(repoConfig)
421+
} else {
422+
if err = ca.SetRepo(tech); err != nil {
423+
return
424+
}
395425
}
396426
}
397427
serverDetails, err = ca.PackageManagerConfig.ServerDetails()
@@ -426,6 +456,8 @@ func (ca *CurationAuditCommand) getBuildInfoParamsByTech() (technologies.BuildIn
426456
NpmOverwritePackageLock: true,
427457
// Python params
428458
PipRequirementsFile: ca.PipRequirementsFile(),
459+
// Docker params
460+
DockerImageName: ca.DockerImageName(),
429461
// NuGet params
430462
SolutionFilePath: ca.SolutionFilePath(),
431463
}, err
@@ -950,6 +982,8 @@ func getUrlNameAndVersionByTech(tech techutils.Technology, node *xrayUtils.Graph
950982
case techutils.Nuget:
951983
downloadUrls, name, version = getNugetNameScopeAndVersion(node.Id, artiUrl, repo)
952984
return
985+
case techutils.Docker:
986+
return getDockerNameScopeAndVersion(node.Id, artiUrl, repo)
953987
}
954988
return
955989
}
@@ -1121,6 +1155,40 @@ func buildNpmDownloadUrl(url, repo, name, scope, version string) []string {
11211155
return []string{packageUrl}
11221156
}
11231157

1158+
func getDockerNameScopeAndVersion(id, artiUrl, repo string) (downloadUrls []string, name, scope, version string) {
1159+
if id == "" {
1160+
return
1161+
}
1162+
1163+
id = strings.TrimPrefix(id, "docker://")
1164+
var lastColonIndex int
1165+
if strings.Contains(id, ":sha256:") {
1166+
sha256Index := strings.Index(id, ":sha256:")
1167+
if sha256Index > 0 {
1168+
lastColonIndex = sha256Index
1169+
} else {
1170+
lastColonIndex = strings.LastIndex(id, ":")
1171+
}
1172+
} else {
1173+
lastColonIndex = strings.LastIndex(id, ":")
1174+
}
1175+
1176+
if lastColonIndex > 0 {
1177+
name = id[:lastColonIndex]
1178+
version = id[lastColonIndex+1:]
1179+
} else {
1180+
name = id
1181+
version = "latest"
1182+
}
1183+
1184+
if artiUrl != "" && repo != "" {
1185+
downloadUrls = []string{fmt.Sprintf("%s/api/docker/%s/v2/%s/manifests/%s",
1186+
strings.TrimSuffix(artiUrl, "/"), repo, name, version)}
1187+
}
1188+
1189+
return
1190+
}
1191+
11241192
func GetCurationOutputFormat(formatFlagVal string) (format outFormat.OutputFormat, err error) {
11251193
// Default print format is table.
11261194
format = outFormat.Table

commands/curation/curationaudit_test.go

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ func TestDoCurationAudit(t *testing.T) {
427427
cleanUp := createCurationTestEnv(t, basePathToTests, tt, config)
428428
defer cleanUp()
429429
// Create audit command, and run it
430-
results, err := createCurationCmdAndRun(tt)
430+
results, err := createCurationCmdAndRun(tt, config)
431431
// Validate the results
432432
if tt.requestToError == nil {
433433
assert.NoError(t, err)
@@ -505,14 +505,19 @@ func runPreTestExec(t *testing.T, basePathToTests string, testCase testCase) {
505505
callbackPreTest()
506506
}
507507

508-
func createCurationCmdAndRun(tt testCase) (cmdResults map[string]*CurationReport, err error) {
508+
func createCurationCmdAndRun(tt testCase, serverDetails *config.ServerDetails) (cmdResults map[string]*CurationReport, err error) {
509509
curationCmd := NewCurationAuditCommand()
510510
curationCmd.SetIsCurationCmd(true)
511511
curationCmd.parallelRequests = 3
512512
// For tests, we use localhost http server (nuget have issues without setting insecureTls)
513513
curationCmd.SetInsecureTls(true)
514514
curationCmd.SetIgnoreConfigFile(tt.shouldIgnoreConfigFile)
515515
curationCmd.SetInsecureTls(tt.allowInsecureTls)
516+
if tt.dockerImageName != "" {
517+
curationCmd.SetDockerImageName(tt.dockerImageName)
518+
// Docker requires server details to be set explicitly
519+
curationCmd.SetServerDetails(serverDetails)
520+
}
516521
cmdResults = map[string]*CurationReport{}
517522
err = curationCmd.doCurateAudit(cmdResults)
518523
return
@@ -568,6 +573,7 @@ type testCase struct {
568573
tech techutils.Technology
569574
createServerWithoutCreds bool
570575
allowInsecureTls bool
576+
dockerImageName string
571577
}
572578

573579
func (tc testCase) getPathToTests() string {
@@ -983,6 +989,43 @@ func getTestCasesForDoCurationAudit() []testCase {
983989
},
984990
allowInsecureTls: true,
985991
},
992+
{
993+
name: "docker tree - one blocked package",
994+
tech: techutils.Docker,
995+
pathToProject: filepath.Join("projects", "package-managers", "docker", "curation-project"),
996+
dockerImageName: "repo-test-docker/dweomer/nginx-auth-ldap:1.13.5-on-alpine-3.5",
997+
requestToFail: map[string]bool{
998+
"/api/docker/repo-test-docker/v2/dweomer/nginx-auth-ldap/manifests/1.13.5-on-alpine-3.5": true,
999+
},
1000+
expectedRequest: map[string]bool{
1001+
"/api/docker/repo-test-docker/v2/dweomer/nginx-auth-ldap/manifests/1.13.5-on-alpine-3.5": false,
1002+
},
1003+
expectedResp: map[string]*CurationReport{
1004+
"root:latest": {
1005+
packagesStatus: []*PackageStatus{
1006+
{
1007+
Action: "blocked",
1008+
ParentName: "dweomer/nginx-auth-ldap",
1009+
ParentVersion: "1.13.5-on-alpine-3.5",
1010+
BlockedPackageUrl: "/api/docker/repo-test-docker/v2/dweomer/nginx-auth-ldap/manifests/1.13.5-on-alpine-3.5",
1011+
PackageName: "dweomer/nginx-auth-ldap",
1012+
PackageVersion: "1.13.5-on-alpine-3.5",
1013+
DepRelation: "direct",
1014+
PkgType: "docker",
1015+
BlockingReason: "Policy violations",
1016+
Policy: []Policy{
1017+
{
1018+
Policy: "pol1",
1019+
Condition: "cond1",
1020+
},
1021+
},
1022+
},
1023+
},
1024+
totalNumberOfPackages: 0,
1025+
},
1026+
},
1027+
allowInsecureTls: true,
1028+
},
9861029
}
9871030
return tests
9881031
}
@@ -1185,6 +1228,99 @@ func Test_getGemNameScopeAndVersion(t *testing.T) {
11851228
}
11861229
}
11871230

1231+
func Test_getDockerNameScopeAndVersion(t *testing.T) {
1232+
tests := []struct {
1233+
name string
1234+
id string
1235+
artiUrl string
1236+
repo string
1237+
wantDownloadUrls []string
1238+
wantName string
1239+
wantScope string
1240+
wantVersion string
1241+
}{
1242+
{
1243+
name: "Basic docker image with tag",
1244+
id: "docker://nginx:1.21.0",
1245+
artiUrl: "http://test.jfrog.io/artifactory",
1246+
repo: "docker-remote",
1247+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/1.21.0"},
1248+
wantName: "nginx",
1249+
wantScope: "",
1250+
wantVersion: "1.21.0",
1251+
},
1252+
{
1253+
name: "Docker image with registry prefix",
1254+
id: "docker://registry.example.com/nginx:1.21.0",
1255+
artiUrl: "http://test.jfrog.io/artifactory",
1256+
repo: "docker-remote",
1257+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/registry.example.com/nginx/manifests/1.21.0"},
1258+
wantName: "registry.example.com/nginx",
1259+
wantScope: "",
1260+
wantVersion: "1.21.0",
1261+
},
1262+
{
1263+
name: "Docker image with sha256 digest",
1264+
id: "docker://nginx:sha256:abc123def456",
1265+
artiUrl: "http://test.jfrog.io/artifactory",
1266+
repo: "docker-remote",
1267+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/sha256:abc123def456"},
1268+
wantName: "nginx",
1269+
wantScope: "",
1270+
wantVersion: "sha256:abc123def456",
1271+
},
1272+
{
1273+
name: "Docker image without version defaults to latest",
1274+
id: "docker://nginx",
1275+
artiUrl: "http://test.jfrog.io/artifactory",
1276+
repo: "docker-remote",
1277+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/latest"},
1278+
wantName: "nginx",
1279+
wantScope: "",
1280+
wantVersion: "latest",
1281+
},
1282+
{
1283+
name: "Empty id returns empty values",
1284+
id: "",
1285+
artiUrl: "http://test.jfrog.io/artifactory",
1286+
repo: "docker-remote",
1287+
wantDownloadUrls: nil,
1288+
wantName: "",
1289+
wantScope: "",
1290+
wantVersion: "",
1291+
},
1292+
{
1293+
name: "Without artiUrl and repo, no download URL",
1294+
id: "docker://nginx:1.21.0",
1295+
artiUrl: "",
1296+
repo: "",
1297+
wantDownloadUrls: nil,
1298+
wantName: "nginx",
1299+
wantScope: "",
1300+
wantVersion: "1.21.0",
1301+
},
1302+
{
1303+
name: "Artifactory URL with trailing slash",
1304+
id: "docker://nginx:1.21.0",
1305+
artiUrl: "http://test.jfrog.io/artifactory/",
1306+
repo: "docker-remote",
1307+
wantDownloadUrls: []string{"http://test.jfrog.io/artifactory/api/docker/docker-remote/v2/nginx/manifests/1.21.0"},
1308+
wantName: "nginx",
1309+
wantScope: "",
1310+
wantVersion: "1.21.0",
1311+
},
1312+
}
1313+
for _, tt := range tests {
1314+
t.Run(tt.name, func(t *testing.T) {
1315+
gotDownloadUrls, gotName, gotScope, gotVersion := getDockerNameScopeAndVersion(tt.id, tt.artiUrl, tt.repo)
1316+
assert.Equal(t, tt.wantDownloadUrls, gotDownloadUrls, "downloadUrls mismatch")
1317+
assert.Equal(t, tt.wantName, gotName, "name mismatch")
1318+
assert.Equal(t, tt.wantScope, gotScope, "scope mismatch")
1319+
assert.Equal(t, tt.wantVersion, gotVersion, "version mismatch")
1320+
})
1321+
}
1322+
}
1323+
11881324
func Test_getNugetNameScopeAndVersion(t *testing.T) {
11891325
tests := []struct {
11901326
name string

0 commit comments

Comments
 (0)