Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ 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")),
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()),
SkipAutoInstall: components.NewBoolFlag(SkipAutoInstall, "Set to true to skip auto-install of dependencies in un-built modules. Currently supported for Yarn, NPM, Pip, and Poetry.", 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(
Exclusions,
Expand Down
8 changes: 6 additions & 2 deletions commands/curation/curationaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ var supportedTech = map[techutils.Technology]func(ca *CurationAuditCommand) (boo
return ca.checkSupportByVersionOrEnv(techutils.Gem, MinArtiGradleGemSupport)
},
techutils.Docker: func(ca *CurationAuditCommand) (bool, error) { return true, nil },
techutils.Poetry: func(ca *CurationAuditCommand) (bool, error) {
return ca.checkSupportByVersionOrEnv(techutils.Poetry, MinArtiPassThroughSupport)
},
}

func (ca *CurationAuditCommand) checkSupportByVersionOrEnv(tech techutils.Technology, minArtiVersion string) (bool, error) {
Expand Down Expand Up @@ -447,6 +450,7 @@ func (ca *CurationAuditCommand) getBuildInfoParamsByTech() (technologies.BuildIn
IgnoreConfigFile: ca.IgnoreConfigFile(),
InsecureTls: ca.InsecureTls(),
// Install params
SkipAutoInstall: ca.SkipAutoInstall(),
InstallCommandName: ca.InstallCommandName(),
Args: ca.Args(),
InstallCommandArgs: ca.InstallCommandArgs(),
Expand Down Expand Up @@ -1074,7 +1078,7 @@ func getUrlNameAndVersionByTech(tech techutils.Technology, node *xrayUtils.Graph
return getGradleNameScopeAndVersion(node.Id, artiUrl, repo, node)
case techutils.Gem:
return getGemNameScopeAndVersion(node.Id, artiUrl, repo)
case techutils.Pip:
case techutils.Pip, techutils.Poetry:
downloadUrls, name, version = getPythonNameVersion(node.Id, downloadUrlsMap)
return
case techutils.Go:
Expand Down Expand Up @@ -1114,7 +1118,7 @@ func getPythonNameVersion(id string, downloadUrlsMap map[string]string) (downloa
if dl, ok := downloadUrlsMap[normalizedId]; ok {
downloadUrls = []string{dl}
} else {
log.Warn(fmt.Sprintf("couldn't find download url for node id %s in report.json", id))
log.Warn(fmt.Sprintf("Couldn't find download URL for node ID %s", id))
}
return
}
Expand Down
125 changes: 125 additions & 0 deletions commands/curation/curationaudit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1693,3 +1693,128 @@ func TestFetchNodesStatusConcurrentMapWrite(t *testing.T) {
})
assert.Equal(t, numNodes, count, "expected all %d packages to be recorded as blocked", numNodes)
}
// =============================================================================
// Tests for Poetry support added to curationaudit.go.
// Covers the new dispatcher case (Pip, Poetry -> getPythonNameVersion) and the
// supportedTech registration.
// =============================================================================

func Test_getPythonNameVersion(t *testing.T) {
const exampleUrl = "http://test.jfrog.io/artifactory/api/pypi/pypi-remote/packages/aa/bb/flask-2.0.0-py3-none-any.whl"

tests := []struct {
name string
id string
downloadUrlsMap map[string]string
wantDownloadUrls []string
wantName string
wantVersion string
}{
{
name: "pip id with matching download url",
id: "pypi://flask:2.0.0",
downloadUrlsMap: map[string]string{"pypi://flask:2.0.0": exampleUrl},
wantDownloadUrls: []string{exampleUrl},
wantName: "flask",
wantVersion: "2.0.0",
},
{
name: "poetry id with matching download url (same pypi:// prefix)",
id: "pypi://click:8.0.1",
downloadUrlsMap: map[string]string{"pypi://click:8.0.1": exampleUrl},
wantDownloadUrls: []string{exampleUrl},
wantName: "click",
wantVersion: "8.0.1",
},
{
name: "id present in map but no entry returns name+version only",
id: "pypi://requests:2.31.0",
downloadUrlsMap: map[string]string{"pypi://other:1.0.0": exampleUrl},
wantDownloadUrls: nil,
wantName: "requests",
wantVersion: "2.31.0",
},
{
name: "nil downloadUrlsMap returns name+version only",
id: "pypi://requests:2.31.0",
downloadUrlsMap: nil,
wantDownloadUrls: nil,
wantName: "requests",
wantVersion: "2.31.0",
},
{
name: "malformed id (no version separator) returns empty",
id: "pypi://malformed",
downloadUrlsMap: nil,
wantDownloadUrls: nil,
wantName: "",
wantVersion: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotDownloadUrls, gotName, gotVersion := getPythonNameVersion(tt.id, tt.downloadUrlsMap)
assert.Equal(t, tt.wantDownloadUrls, gotDownloadUrls, "downloadUrls mismatch")
assert.Equal(t, tt.wantName, gotName, "name mismatch")
assert.Equal(t, tt.wantVersion, gotVersion, "version mismatch")
})
}
}

// TestGetBlockedPackageDetails_403UnparsableBodyReturnsError verifies that
// getBlockedPackageDetails returns an error (and no PackageStatus) when a 403
// response body cannot be resolved to a known curation block reason:
// (1) the body is not valid JSON (e.g. an HTML error page), or
// (2) the body is valid JSON but the Errors array is empty.
func TestGetBlockedPackageDetails_403UnparsableBodyReturnsError(t *testing.T) {
tests := []struct {
name string
respBody string
expectedErrMsg string
}{
{
name: "non-JSON body (HTML error page)",
respBody: "<html><body><h1>403 Forbidden</h1></body></html>",
expectedErrMsg: "invalid character",
},
{
name: "JSON body with empty errors list",
respBody: `{"errors":[]}`,
expectedErrMsg: "received 403 for unknown reason",
},
}

const (
pkgName = "telnyx"
pkgVersion = "4.87.1"
)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverMock, _, rtManager := coreCommonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(tt.respBody))
})
defer serverMock.Close()

rtAuth := rtManager.GetConfig().GetServiceDetails()
httpClientDetails := rtAuth.CreateHttpClientDetails()
analyzer := treeAnalyzer{
rtManager: rtManager,
rtAuth: rtAuth,
httpClientDetails: httpClientDetails,
extractPoliciesRegex: regexp.MustCompile(extractPoliciesRegexTemplate),
url: rtAuth.GetUrl(),
repo: "pypi-remote",
tech: techutils.Poetry,
}
packageUrl := fmt.Sprintf("%sapi/pypi/pypi-remote/packages/%s-%s.tar.gz", rtAuth.GetUrl(), pkgName, pkgVersion)

got, err := analyzer.getBlockedPackageDetails(packageUrl, pkgName, pkgVersion)

require.Error(t, err, "unparseable 403 body must surface as an error")
assert.Nil(t, got, "no PackageStatus should be returned when the block reason cannot be determined")
assert.Contains(t, err.Error(), tt.expectedErrMsg)
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,4 @@ replace github.com/CycloneDX/cyclonedx-go => github.com/CycloneDX/cyclonedx-go v

// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go dev

// replace github.com/jfrog/froggit-go => github.com/jfrog/froggit-go master
// replace github.com/jfrog/froggit-go => github.com/jfrog/froggit-go master
14 changes: 14 additions & 0 deletions sca/bom/buildinfo/technologies/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func TestSuspectCurationBlockedError(t *testing.T) {
mvnOutput2 := "status code: 500, reason phrase: Server Error (500)"
pipOutput := "because of HTTP error 403 Client Error: Forbidden for url"
goOutput := "Failed running Go command: 403 Forbidden"
poetryOutput := "because of HTTP error 403 Client Error: Forbidden for url"

tests := []struct {
name string
Expand Down Expand Up @@ -190,6 +191,19 @@ func TestSuspectCurationBlockedError(t *testing.T) {
output: goOutput,
expect: fmt.Sprintf(CurationErrorMsgToUserTemplate, techutils.Go),
},
{
name: "poetry 403 error (pass-through disabled)",
isCurationCmd: true,
tech: techutils.Poetry,
output: poetryOutput,
expect: fmt.Sprintf(CurationErrorMsgToUserTemplate, techutils.Poetry),
},
{
name: "poetry not pass through error",
isCurationCmd: true,
tech: techutils.Poetry,
output: "http error 401",
},
{
name: "not a supported tech",
isCurationCmd: true,
Expand Down
Loading
Loading