Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
GitCountContributors = "count-contributors"
Enrich = "sbom-enrich"
UploadCdx = "upload-cdx"
MaliciousScan = "malicious-scan"

// TODO: Deprecated commands (remove at next CLI major version)
AuditMvn = "audit-maven"
Expand Down Expand Up @@ -175,6 +176,9 @@ var commandFlags = map[string][]string{
Enrich: {
Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls,
},
MaliciousScan: {
Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, OutputFormat, MinSeverity, AnalyzerManagerCustomPath, WorkingDirs,
},
BuildScan: {
Url, XrayUrl, user, password, accessToken, ServerId, scanProjectKey, BuildVuln, OutputFormat, Fail, ExtendedTable, Rescan, InsecureTls, TriggerScanRetries,
},
Expand Down
13 changes: 13 additions & 0 deletions cli/docs/maliciousscan/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package maliciousscan

import (
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
)

func GetDescription() string {
return "[Beta] Scan malicious models (pickle files, etc.) located in the working directory."
}

func GetArguments() []components.Argument {
return []components.Argument{}
}
47 changes: 47 additions & 0 deletions cli/scancommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
flags "github.com/jfrog/jfrog-cli-security/cli/docs"
auditSpecificDocs "github.com/jfrog/jfrog-cli-security/cli/docs/auditspecific"
enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich"
maliciousScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/maliciousscan"
mcpDocs "github.com/jfrog/jfrog-cli-security/cli/docs/mcp"
auditDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/audit"
buildScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/buildscan"
Expand All @@ -40,6 +41,7 @@ import (

"github.com/jfrog/jfrog-cli-security/commands/audit"
"github.com/jfrog/jfrog-cli-security/commands/curation"
"github.com/jfrog/jfrog-cli-security/commands/maliciousscan"
"github.com/jfrog/jfrog-cli-security/commands/scan"
"github.com/jfrog/jfrog-cli-security/commands/upload"

Expand Down Expand Up @@ -72,6 +74,15 @@ func getAuditAndScansCommands() []components.Command {
Category: securityCategory,
Action: EnrichCmd,
},
{
Name: "malicious-scan",
Aliases: []string{"ms"},
Flags: flags.GetCommandFlags(flags.MaliciousScan),
Description: maliciousScanDocs.GetDescription(),
Arguments: maliciousScanDocs.GetArguments(),
Category: securityCategory,
Action: MaliciousScanCmd,
},
{
Name: "build-scan",
Aliases: []string{"bs"},
Expand Down Expand Up @@ -230,6 +241,42 @@ func EnrichCmd(c *components.Context) error {
return commandsCommon.Exec(EnrichCmd)
}

func MaliciousScanCmd(c *components.Context) error {
serverDetails, err := CreateServerDetailsFromFlags(c)
if err != nil {
return err
}
if err = validateConnectionInputs(serverDetails); err != nil {
return err
}
format, err := outputFormat.GetOutputFormat(c.GetStringFlagValue(flags.OutputFormat))
if err != nil {
return err
}
threads, err := pluginsCommon.GetThreadsCount(c)
if err != nil {
return err
}
minSeverity, err := getMinimumSeverity(c)
if err != nil {
return err
}
workingDirs := []string{}
if c.GetStringFlagValue(flags.WorkingDirs) != "" {
workingDirs = splitByCommaAndTrim(c.GetStringFlagValue(flags.WorkingDirs))
}
maliciousScanCmd := maliciousscan.NewMaliciousScanCommand().
SetServerDetails(serverDetails).
SetWorkingDirs(workingDirs).
SetThreads(threads).
SetOutputFormat(format).
SetMinSeverityFilter(minSeverity)
if c.IsFlagSet(flags.AnalyzerManagerCustomPath) {
maliciousScanCmd.SetCustomAnalyzerManagerPath(c.GetStringFlagValue(flags.AnalyzerManagerCustomPath))
}
return commandsCommon.Exec(maliciousScanCmd)
}

func ScanCmd(c *components.Context) error {
if len(c.Arguments) == 0 && !c.IsFlagSet(flags.SpecFlag) {
return pluginsCommon.PrintHelpAndReturnError("providing either a <source pattern> argument or the 'spec' option is mandatory", c)
Expand Down
275 changes: 275 additions & 0 deletions commands/maliciousscan/maliciousscan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package maliciousscan

import (
"errors"
"fmt"
"path/filepath"
"strings"

"github.com/jfrog/jfrog-cli-core/v2/common/format"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-security/jas"
"github.com/jfrog/jfrog-cli-security/jas/maliciouscode"
"github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/jasutils"
"github.com/jfrog/jfrog-cli-security/utils/results"
"github.com/jfrog/jfrog-cli-security/utils/results/output"
"github.com/jfrog/jfrog-cli-security/utils/severityutils"
"github.com/jfrog/jfrog-cli-security/utils/xray"
ioUtils "github.com/jfrog/jfrog-client-go/utils/io"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
"github.com/jfrog/jfrog-client-go/utils/log"
)

type MaliciousScanCommand struct {
serverDetails *config.ServerDetails
workingDirs []string
threads int
outputFormat format.OutputFormat
minSeverityFilter severityutils.Severity
progress ioUtils.ProgressMgr
customAnalyzerManagerPath string
}

func (cmd *MaliciousScanCommand) SetProgress(progress ioUtils.ProgressMgr) {
cmd.progress = progress
}

func (cmd *MaliciousScanCommand) SetThreads(threads int) *MaliciousScanCommand {
cmd.threads = threads
return cmd
}

func (cmd *MaliciousScanCommand) SetServerDetails(server *config.ServerDetails) *MaliciousScanCommand {
cmd.serverDetails = server
return cmd
}

func (cmd *MaliciousScanCommand) SetWorkingDirs(workingDirs []string) *MaliciousScanCommand {
cmd.workingDirs = workingDirs
return cmd
}

func (cmd *MaliciousScanCommand) SetOutputFormat(format format.OutputFormat) *MaliciousScanCommand {
cmd.outputFormat = format
return cmd
}

func (cmd *MaliciousScanCommand) SetMinSeverityFilter(minSeverity severityutils.Severity) *MaliciousScanCommand {
cmd.minSeverityFilter = minSeverity
return cmd
}

func (cmd *MaliciousScanCommand) SetCustomAnalyzerManagerPath(path string) *MaliciousScanCommand {
cmd.customAnalyzerManagerPath = path
return cmd
}

func (cmd *MaliciousScanCommand) ServerDetails() (*config.ServerDetails, error) {
return cmd.serverDetails, nil
}

func (cmd *MaliciousScanCommand) CommandName() string {
return "malicious_scan"
}

func NewMaliciousScanCommand() *MaliciousScanCommand {
return &MaliciousScanCommand{}
}

func (cmd *MaliciousScanCommand) Run() (err error) {
xrayVersion, entitledForJas, workingDirs, err := cmd.validateAndPrepare()
if err != nil {
return err
}

cmdResults := cmd.initializeCommandResults(xrayVersion, entitledForJas)
populateScanTargets(cmdResults, workingDirs)

scanner, err := cmd.createJasScanner()
if err != nil {
return err
}

if err = cmd.runMaliciousScans(cmdResults, scanner); err != nil {
return err
}

return cmd.outputResults(cmdResults)
}

func (cmd *MaliciousScanCommand) validateAndPrepare() (xrayVersion string, entitledForJas bool, workingDirs []string, err error) {
xrayManager, xrayVersion, err := xray.CreateXrayServiceManagerAndGetVersion(cmd.serverDetails)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to make it work with project scoped token? if so, we need to add --project flag and pass it to the service manager, similar to other places

if err != nil {
return "", false, nil, err
}

entitledForJas, err = jas.IsEntitledForJas(xrayManager, xrayVersion)
if err != nil {
return "", false, nil, err
}
if !entitledForJas {
return "", false, nil, errors.New("JAS (Advanced Security) feature is not entitled")
}

log.Info("JFrog Xray version is:", xrayVersion)

workingDirs, err = coreutils.GetFullPathsWorkingDirs(cmd.workingDirs)
if err != nil {
return "", false, nil, err
}
logScanPaths(workingDirs)

return xrayVersion, entitledForJas, workingDirs, nil
}

func (cmd *MaliciousScanCommand) initializeCommandResults(xrayVersion string, entitledForJas bool) *results.SecurityCommandResults {
cmdResults := results.NewCommandResults(utils.SourceCode)
cmdResults.SetXrayVersion(xrayVersion)
cmdResults.SetEntitledForJas(entitledForJas)
cmdResults.SetResultsContext(results.ResultContext{
IncludeVulnerabilities: true,
})
return cmdResults
}

func (cmd *MaliciousScanCommand) createJasScanner() (*jas.JasScanner, error) {
scannerOptions := []jas.JasScannerOption{
jas.WithEnvVars(
false,
jas.NotDiffScanEnvValue,
jas.GetAnalyzerManagerXscEnvVars(
"",
"",
"",
nil,
),
),
jas.WithMinSeverity(cmd.minSeverityFilter),
}

scanner, err := jas.NewJasScanner(cmd.serverDetails, scannerOptions...)
if err != nil {
return nil, fmt.Errorf("failed to create JAS scanner: %w", err)
}
if scanner == nil {
return nil, errors.New("JAS scanner was not created")
}

if err = cmd.setAnalyzerManagerPath(scanner); err != nil {
return nil, err
}

log.Debug(fmt.Sprintf("Using analyzer manager executable at: %s", scanner.AnalyzerManager.AnalyzerManagerFullPath))
return scanner, nil
}

func (cmd *MaliciousScanCommand) setAnalyzerManagerPath(scanner *jas.JasScanner) error {
if cmd.customAnalyzerManagerPath == "" {
if err := jas.DownloadAnalyzerManagerIfNeeded(0); err != nil {
return fmt.Errorf("failed to download analyzer manager: %s", err.Error())
}
var err error
if scanner.AnalyzerManager.AnalyzerManagerFullPath, err = jas.GetAnalyzerManagerExecutable(); err != nil {
return fmt.Errorf("failed to set analyzer manager executable path: %s", err.Error())
}
} else {
scanner.AnalyzerManager.AnalyzerManagerFullPath = cmd.customAnalyzerManagerPath
log.Debug("using custom analyzer manager binary path")
}
return nil
}

func (cmd *MaliciousScanCommand) runMaliciousScans(cmdResults *results.SecurityCommandResults, scanner *jas.JasScanner) error {
jasScanProducerConsumer := utils.NewSecurityParallelRunner(cmd.threads)
jasScanProducerConsumer.JasWg.Add(1)
createMaliciousScansTask := func(threadId int) (generalError error) {
defer func() {
jasScanProducerConsumer.JasWg.Done()
}()
for _, targetResult := range cmdResults.Targets {
vulnerabilitiesResults, err := maliciouscode.RunMaliciousScan(
scanner,
maliciouscode.MaliciousScannerType,
targetResult.Target,
len(cmdResults.Targets),
threadId,
)
jasScanProducerConsumer.ResultsMu.Lock()
// Malicious code scans only return vulnerabilities, not violations
targetResult.AddJasScanResults(jasutils.MaliciousCode, vulnerabilitiesResults, nil, jas.GetAnalyzerManagerExitCode(err))
jasScanProducerConsumer.ResultsMu.Unlock()
if err = jas.ParseAnalyzerManagerError(jasutils.MaliciousCode, err); err != nil {
_ = targetResult.AddTargetError(fmt.Errorf("failed to run malicious scan: %w", err), false)
}
}
return
}

if _, addTaskErr := jasScanProducerConsumer.Runner.AddTaskWithError(createMaliciousScansTask, func(taskErr error) {
cmdResults.AddGeneralError(fmt.Errorf("failed while adding malicious scan tasks: %s", taskErr.Error()), false)
}); addTaskErr != nil {
return fmt.Errorf("failed to create malicious scan task: %w", addTaskErr)
}

jasScanProducerConsumer.Start()
return nil
}

func (cmd *MaliciousScanCommand) outputResults(cmdResults *results.SecurityCommandResults) error {
if err := output.NewResultsWriter(cmdResults).
SetOutputFormat(cmd.outputFormat).
SetPlatformUrl(cmd.serverDetails.Url).
SetPrintExtendedTable(false).
SetIsMultipleRootProject(cmdResults.HasMultipleTargets()).
SetSubScansPerformed([]utils.SubScanType{utils.MaliciousCodeScan}).
PrintScanResults(); err != nil {
return errors.Join(err, cmdResults.GetErrors())
}

if err := cmdResults.GetErrors(); err != nil {
return err
}

log.Info("Malicious scan completed successfully.")
return nil
}

func logScanPaths(workingDirs []string) {
if len(workingDirs) == 0 {
return
}
if len(workingDirs) == 1 {
log.Debug("Scanning path:", workingDirs[0])
return
}
log.Debug("Scanning paths:", strings.Join(workingDirs, ", "))
}

func populateScanTargets(cmdResults *results.SecurityCommandResults, workingDirs []string) {
for _, requestedDirectory := range workingDirs {
if !fileutils.IsPathExists(requestedDirectory, false) {
log.Warn("The working directory", requestedDirectory, "doesn't exist. Skipping scan...")
continue
}
cmdResults.NewScanResults(results.ScanTarget{Target: requestedDirectory, Name: filepath.Base(requestedDirectory)})
}

if len(cmdResults.Targets) == 0 {
log.Warn("No scan targets were detected.")
return
}

logScanTargetsInfo(cmdResults)
}

func logScanTargetsInfo(cmdResults *results.SecurityCommandResults) {
if len(cmdResults.Targets) == 0 {
return
}
log.Info("Scanning", len(cmdResults.Targets), "target(s)...")
for _, targetResult := range cmdResults.Targets {
log.Info("Scanning target:", targetResult.Target)
}
}
1 change: 1 addition & 0 deletions jas/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type SpecificScannersExcludePatterns struct {
SastExcludePatterns []string
SecretsExcludePatterns []string
IacExcludePatterns []string
MaliciousCodeExcludePatterns []string
}

type JasScannerOption func(f *JasScanner) error
Expand Down
Loading
Loading