Skip to content

Commit

Permalink
Support Curation to run after package manager installation failure (#135
Browse files Browse the repository at this point in the history
)
  • Loading branch information
asafambar authored Aug 21, 2024
1 parent b5acff9 commit 6a461aa
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 109 deletions.
115 changes: 104 additions & 11 deletions cli/scancommands.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package cli

import (
"errors"
"fmt"
enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich"
"github.com/jfrog/jfrog-cli-security/commands/enrich"
"os"
"strings"

"github.com/jfrog/jfrog-cli-core/v2/utils/usage"

buildInfoUtils "github.com/jfrog/build-info-go/utils"
"github.com/jfrog/gofrog/datastructures"
"github.com/jfrog/jfrog-cli-core/v2/common/cliutils"
commandsCommon "github.com/jfrog/jfrog-cli-core/v2/common/commands"
outputFormat "github.com/jfrog/jfrog-cli-core/v2/common/format"
Expand All @@ -18,8 +14,15 @@ import (
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-core/v2/utils/usage"
enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich"
"github.com/jfrog/jfrog-cli-security/commands/enrich"
"github.com/jfrog/jfrog-cli-security/utils/xray"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/log"
"github.com/urfave/cli"
"os"
"strings"

flags "github.com/jfrog/jfrog-cli-security/cli/docs"
auditSpecificDocs "github.com/jfrog/jfrog-cli-security/cli/docs/auditspecific"
Expand All @@ -39,6 +42,7 @@ import (
)

const dockerScanCmdHiddenName = "dockerscan"
const SkipCurationAfterFailureEnv = "JFROG_CLI_SKIP_CURATION_AFTER_FAILURE"

func getAuditAndScansCommands() []components.Command {
return []components.Command{
Expand Down Expand Up @@ -505,21 +509,110 @@ func AuditSpecificCmd(c *components.Context, technology techutils.Technology) er
}

func CurationCmd(c *components.Context) error {
threads, err := pluginsCommon.GetThreadsCount(c)
curationAuditCommand, err := getCurationCommand(c)
if err != nil {
return err
}
return progressbar.ExecWithProgress(curationAuditCommand)
}

var supportedCommandsForPostInstallationFailure = datastructures.MakeSetFromElements[string](
"install", "build", "i", "add", "ci", "get", "mod",
)

func IsSupportedCommandForCurationInspect(cmd string) bool {
return supportedCommandsForPostInstallationFailure.Exists(cmd)
}

func WrapCmdWithCurationPostFailureRun(c *cli.Context, cmd func(c *cli.Context) error, technology techutils.Technology, cmdName string) error {
if err := cmd(c); err != nil {
CurationInspectAfterFailure(c, cmdName, technology, err)
return err
}
return nil
}

func CurationInspectAfterFailure(c *cli.Context, cmdName string, technology techutils.Technology, errFromCmd error) {
if compContexts, errConvertCtx := components.ConvertContext(c); errConvertCtx == nil {
if errPostCuration := CurationCmdPostInstallationFailure(compContexts, technology, cmdName, errFromCmd); errPostCuration != nil {
log.Error(errPostCuration)
}
} else {
log.Error(errConvertCtx)
}
}

func CurationCmdPostInstallationFailure(c *components.Context, tech techutils.Technology, cmdName string, originError error) error {
// check the command supported
curationAuditCommand, err, runCuration := ShouldRunCurationAfterFailure(c, tech, cmdName, originError)
if err != nil {
return err
}
if !runCuration {
return nil
}
log.Info("Running curation audit after failure")
return progressbar.ExecWithProgress(curationAuditCommand)
}

func ShouldRunCurationAfterFailure(c *components.Context, tech techutils.Technology, cmdName string, originError error) (curationCmd *curation.CurationAuditCommand, err error, runCuration bool) {
if !IsSupportedCommandForCurationInspect(cmdName) {
return
}
if os.Getenv(coreutils.OutputDirPathEnv) == "" ||
os.Getenv(SkipCurationAfterFailureEnv) == "true" {
return
}
// check if the error is a forbidden error, if so, we don't want to run the curation audit automatically.
// this check have two parts:
// 1. check if the error is a forbidden error
// 2. check if the error message contains the forbidden error message, in case the output included in the error message.
forBiddenError := &buildInfoUtils.ForbiddenError{}
if !errors.Is(originError, forBiddenError) && !strings.Contains(originError.Error(), forBiddenError.Error()) &&
!buildInfoUtils.IsForbiddenOutput(buildInfoUtils.PackageManager(tech.String()), originError.Error()) {
return
}
// If the command is not running in the context of GitHub actions, we don't want to run the curation audit automatically
curationCmd, err = getCurationCommand(c)
if err != nil {
return
}
// check if user entitled for curation
serverDetails, err := curationCmd.GetAuth(tech)
if err != nil {
return
}
xrayManager, err := xray.CreateXrayServiceManager(serverDetails)
if err != nil {
return
}
entitled, err := curation.IsEntitledForCuration(xrayManager)
if err != nil {
return
}
if !entitled {
log.Info("Curation feature is not entitled, skipping curation audit")
return
}
return curationCmd, nil, true
}

func getCurationCommand(c *components.Context) (*curation.CurationAuditCommand, error) {
threads, err := pluginsCommon.GetThreadsCount(c)
if err != nil {
return nil, err
}
curationAuditCommand := curation.NewCurationAuditCommand().
SetWorkingDirs(splitByCommaAndTrim(c.GetStringFlagValue(flags.WorkingDirs))).
SetParallelRequests(threads)

serverDetails, err := pluginsCommon.CreateServerDetailsWithConfigOffer(c, true, cliutils.Rt)
if err != nil {
return err
return nil, err
}
format, err := curation.GetCurationOutputFormat(c.GetStringFlagValue(flags.OutputFormat))
if err != nil {
return err
return nil, err
}
curationAuditCommand.SetServerDetails(serverDetails).
SetIsCurationCmd(true).
Expand All @@ -529,7 +622,7 @@ func CurationCmd(c *components.Context) error {
SetInsecureTls(c.GetBoolFlagValue(flags.InsecureTls)).
SetNpmScope(c.GetStringFlagValue(flags.DepType)).
SetPipRequirementsFile(c.GetStringFlagValue(flags.RequirementsFile))
return progressbar.ExecWithProgress(curationAuditCommand)
return curationAuditCommand, nil
}

func DockerScanMockCommand() components.Command {
Expand Down
153 changes: 153 additions & 0 deletions cli/scancommands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package cli

import (
"errors"
commonCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
coretests "github.com/jfrog/jfrog-cli-core/v2/common/tests"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
clienttestutils "github.com/jfrog/jfrog-client-go/utils/tests"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"testing"

"github.com/jfrog/build-info-go/utils"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-security/utils/techutils"
"github.com/stretchr/testify/assert"
)

var TestDataDir = filepath.Join("..", "tests", "testdata")

func TestShouldRunCurationAfterFailure(t *testing.T) {
tests := []struct {
name string
cmdName string
envSkipCuration string
envOutputDirPath string
originError error
isForbiddenOutput bool
isEntitledForCuration bool
expectedRunCuration bool
expectedError error
}{
{
name: "Unsupported command",
cmdName: "unsupported",
envOutputDirPath: "path",
expectedRunCuration: false,
},
{
name: "Skip curation after failure",
cmdName: "install",
envSkipCuration: "true",
envOutputDirPath: "path",
expectedRunCuration: false,
},
{
name: "Output directory path not set",
cmdName: "install",
envOutputDirPath: "",
expectedRunCuration: false,
},
{
name: "Forbidden error",
cmdName: "install",
originError: &utils.ForbiddenError{},
envOutputDirPath: "path",
expectedRunCuration: false,
},
{
name: "Forbidden error in message",
cmdName: "install",
originError: errors.New("403 Forbidden"),
envOutputDirPath: "path",
expectedRunCuration: false,
},
{
name: "Not entitled for curation",
cmdName: "install",
originError: &utils.ForbiddenError{},
envOutputDirPath: "path",
isEntitledForCuration: false,
expectedRunCuration: false,
},
{
name: "Successful curation audit",
cmdName: "install",
originError: &utils.ForbiddenError{},
envOutputDirPath: "path",
isEntitledForCuration: true,
expectedRunCuration: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set environment variables
if tt.envSkipCuration != "" {
callBack := clienttestutils.SetEnvWithCallbackAndAssert(t, SkipCurationAfterFailureEnv, tt.envSkipCuration)
defer callBack()
}
if tt.envOutputDirPath != "" {
callBack2 := clienttestutils.SetEnvWithCallbackAndAssert(t, coreutils.OutputDirPathEnv, tt.envOutputDirPath)
defer callBack2()
}

pathToProjectDir := filepath.Join(TestDataDir, "projects", "package-managers", "npm", "npm-project")

rootDir, err := os.Getwd()
assert.NoError(t, err)
tempHomeDir := path.Join(rootDir, path.Join(pathToProjectDir, ".jfrog"))
callback := clienttestutils.SetEnvWithCallbackAndAssert(t, coreutils.HomeDir, tempHomeDir)
defer callback()

serverMock, c, _ := coretests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.String(), "system/version") {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{"xray_version":"3.99.0"}`))
assert.NoError(t, err)
return
}
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{"feature_id":"curation","entitled":` + strconv.FormatBool(tt.isEntitledForCuration) + `}`))
assert.NoError(t, err)
})
defer serverMock.Close()

configFilePath := createCliConfig(t, c.ArtifactoryUrl, pathToProjectDir)
defer func() {
assert.NoError(t, fileutils.RemoveTempDir(configFilePath))
}()

callbackPreTest := clienttestutils.ChangeDirWithCallback(t, rootDir, pathToProjectDir)
defer callbackPreTest()

_, err, runCuration := ShouldRunCurationAfterFailure(&components.Context{}, techutils.Npm, tt.cmdName, tt.originError)

// Verify the expected behavior
assert.Equal(t, tt.expectedRunCuration, runCuration)
assert.Equal(t, tt.expectedError, err)

})
}
}

func createCliConfig(t *testing.T, url string, configPath string) string {
server := &config.ServerDetails{
User: "admin",
Password: "password",
Url: url,
ArtifactoryUrl: url,
XrayUrl: url,
}
configCmd := commonCommands.NewConfigCommand(commonCommands.AddOrEdit, "test").
SetDetails(server).SetUseBasicAuthOnly(true).SetInteractive(false)
assert.NoError(t, configCmd.Run())
return filepath.Join(configPath, "jfrog-cli.conf.v"+strconv.Itoa(coreutils.GetCliConfigVersion()))
}
22 changes: 4 additions & 18 deletions commands/audit/sca/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"testing"

buildInfoUtils "github.com/jfrog/build-info-go/utils"
"github.com/jfrog/jfrog-cli-core/v2/utils/tests"
"github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/techutils"
Expand Down Expand Up @@ -167,24 +168,9 @@ func setPathsForIssues(dependency *xrayUtils.GraphNode, issuesImpactPathsMap map
}
}

func SuspectCurationBlockedError(isCurationCmd bool, tech techutils.Technology, cmdOutput string) (msgToUser string) {
if !isCurationCmd {
return
}
switch tech {
case techutils.Maven:
if strings.Contains(cmdOutput, "status code: 403") || strings.Contains(strings.ToLower(cmdOutput), "403 forbidden") ||
strings.Contains(cmdOutput, "status code: 500") {
msgToUser = fmt.Sprintf(CurationErrorMsgToUserTemplate, techutils.Maven)
}
case techutils.Pip:
if strings.Contains(strings.ToLower(cmdOutput), "http error 403") {
msgToUser = fmt.Sprintf(CurationErrorMsgToUserTemplate, techutils.Pip)
}
case techutils.Go:
if strings.Contains(strings.ToLower(cmdOutput), "403 forbidden") {
msgToUser = fmt.Sprintf(CurationErrorMsgToUserTemplate, techutils.Go)
}
func GetMsgToUserForCurationBlock(isCurationCmd bool, tech techutils.Technology, cmdOutput string) (msgToUser string) {
if isCurationCmd && buildInfoUtils.IsForbiddenOutput(buildInfoUtils.PackageManager(tech.String()), cmdOutput) {
msgToUser = fmt.Sprintf(CurationErrorMsgToUserTemplate, tech)
}
return
}
2 changes: 1 addition & 1 deletion commands/audit/sca/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ func TestSuspectCurationBlockedError(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, SuspectCurationBlockedError(tt.isCurationCmd, tt.tech, tt.output), tt.expect)
assert.Equal(t, GetMsgToUserForCurationBlock(tt.isCurationCmd, tt.tech, tt.output), tt.expect)
})
}
}
2 changes: 1 addition & 1 deletion commands/audit/sca/go/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func handleCurationGoError(err error) (bool, error) {
if err == nil {
return false, nil
}
if msgToUser := sca.SuspectCurationBlockedError(true, techutils.Go, err.Error()); msgToUser != "" {
if msgToUser := sca.GetMsgToUserForCurationBlock(true, techutils.Go, err.Error()); msgToUser != "" {
return true, errors.New(msgToUser)
}
return false, nil
Expand Down
2 changes: 1 addition & 1 deletion commands/audit/sca/java/mvn.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func (mdt *MavenDepTreeManager) RunMvnCmd(goals []string) (cmdOutput []byte, err
if len(cmdOutput) > 0 {
log.Info(stringOutput)
}
if msg := sca.SuspectCurationBlockedError(mdt.isCurationCmd, techutils.Maven, stringOutput); msg != "" {
if msg := sca.GetMsgToUserForCurationBlock(mdt.isCurationCmd, techutils.Maven, stringOutput); msg != "" {
err = fmt.Errorf("failed running command 'mvn %s\n\n%s", strings.Join(goals, " "), msg)
} else {
err = fmt.Errorf("failed running command 'mvn %s': %s", strings.Join(goals, " "), err.Error())
Expand Down
2 changes: 1 addition & 1 deletion commands/audit/sca/python/python.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func installPipDeps(auditPython *AuditPython) (restoreEnv func() error, err erro
}
}
if err != nil || reqErr != nil {
if msgToUser := sca.SuspectCurationBlockedError(auditPython.IsCurationCmd, techutils.Pip, errors.Join(err, reqErr).Error()); msgToUser != "" {
if msgToUser := sca.GetMsgToUserForCurationBlock(auditPython.IsCurationCmd, techutils.Pip, errors.Join(err, reqErr).Error()); msgToUser != "" {
err = errors.Join(err, errors.New(msgToUser))
}
}
Expand Down
Loading

0 comments on commit 6a461aa

Please sign in to comment.