Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-security int…
Browse files Browse the repository at this point in the history
…o centralized-config-implementation

# Conflicts:
#	go.mod
#	go.sum
  • Loading branch information
eranturgeman committed Aug 22, 2024
2 parents dc4c36c + ba47070 commit 83c9155
Show file tree
Hide file tree
Showing 18 changed files with 356 additions and 112 deletions.
2 changes: 1 addition & 1 deletion cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func GetJfrogCliSecurityApp() components.App {
app.Subcommands = append(app.Subcommands, components.Namespace{
Name: "git",
Description: "Git commands.",
Hidden: true,
Hidden: true,
Commands: getGitNameSpaceCommands(),
Category: "Command Namespaces",
})
Expand Down
129 changes: 113 additions & 16 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 @@ -232,7 +236,7 @@ func ScanCmd(c *components.Context) error {
SetThreads(threads).
SetSpec(specFile).
SetOutputFormat(format).
SetProject(c.GetStringFlagValue(flags.Project)).
SetProject(getProject(c)).
SetIncludeVulnerabilities(c.GetBoolFlagValue(flags.Vuln) || shouldIncludeVulnerabilities(c)).
SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)).
SetFail(c.GetBoolFlagValue(flags.Fail)).
Expand Down Expand Up @@ -283,10 +287,14 @@ func getMinimumSeverity(c *components.Context) (severity severityutils.Severity,
}

func isProjectProvided(c *components.Context) bool {
return getProject(c) != ""
}

func getProject(c *components.Context) string {
if c.IsFlagSet(flags.Project) {
return c.GetStringFlagValue(flags.Project) != ""
return c.GetStringFlagValue(flags.Project)
}
return os.Getenv(coreutils.Project) != ""
return os.Getenv(coreutils.Project)
}

func addTrailingSlashToRepoPathIfNeeded(c *components.Context) string {
Expand Down Expand Up @@ -450,7 +458,7 @@ func CreateAuditCmd(c *components.Context) (*audit.AuditCommand, error) {
auditCmd.SetAnalyticsMetricsService(xsc.NewAnalyticsMetricsService(serverDetails))

auditCmd.SetTargetRepoPath(addTrailingSlashToRepoPathIfNeeded(c)).
SetProject(c.GetStringFlagValue(flags.Project)).
SetProject(getProject(c)).
SetIncludeVulnerabilities(c.GetBoolFlagValue(flags.Vuln) || shouldIncludeVulnerabilities(c)).
SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)).
SetFail(c.GetBoolFlagValue(flags.Fail)).
Expand Down Expand Up @@ -505,21 +513,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 +626,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 Expand Up @@ -596,7 +693,7 @@ func DockerScan(c *components.Context, image string) error {
SetTargetRepoPath(addTrailingSlashToRepoPathIfNeeded(c)).
SetServerDetails(serverDetails).
SetOutputFormat(format).
SetProject(c.GetStringFlagValue(flags.Project)).
SetProject(getProject(c)).
SetIncludeVulnerabilities(c.GetBoolFlagValue(flags.Vuln) || shouldIncludeVulnerabilities(c)).
SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)).
SetFail(c.GetBoolFlagValue(flags.Fail)).
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()))
}
7 changes: 1 addition & 6 deletions commands/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package audit
import (
"errors"
"fmt"
"os"

jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
Expand Down Expand Up @@ -92,11 +91,7 @@ func (auditCmd *AuditCommand) CreateCommonGraphScanParams() *scangraph.CommonGra
Watches: auditCmd.watches,
ScanType: services.Dependency,
}
if auditCmd.projectKey == "" {
commonParams.ProjectKey = os.Getenv(coreutils.Project)
} else {
commonParams.ProjectKey = auditCmd.projectKey
}
commonParams.ProjectKey = auditCmd.projectKey
commonParams.IncludeVulnerabilities = auditCmd.IncludeVulnerabilities
commonParams.IncludeLicenses = auditCmd.IncludeLicenses
commonParams.MultiScanId, commonParams.XscVersion = xsc.GetXscMsiAndVersion(auditCmd.analyticsMetricsService)
Expand Down
Loading

0 comments on commit 83c9155

Please sign in to comment.