Skip to content
Draft
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ Global flags work with every subcommand. Combine them with configuration files a
|------|------|---------|-------------|
| `--config` | string | | Path to a YAML or JSON configuration file. |
| `--log-level` | string | `info` | Logging level: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `none`. |
| `--stdout-format` | string | `yaml` | `yaml`, `json`, or `sarif` output on stdout. |
| `--stdout-format` | string | `yaml` | `yaml`, `json`, `sarif`, or `human` output on stdout. |
| `--report-path` | string slice | | Write findings to one or more files; format is inferred from the extension. |
| `--ignore-on-exit` | enum | `none` | Control exit codes: `all`, `results`, `errors`, or `none`. |
| `--max-target-megabytes` | int | `0` | Skip files larger than the threshold (0 disables the check). |
Expand Down Expand Up @@ -291,6 +291,8 @@ You can still override values via CLI flags; the CLI always wins over config val
--stdout-format json \
--report-path build/2ms.sarif \
--report-path build/2ms.yaml

Set `--stdout-format human` for a terse, human-friendly summary on the console (great for local runs), while still writing machine-readable reports via `--report-path`.
```

SARIF reports plug directly into GitHub Advanced Security or other code-scanning dashboards. All outputs include rule metadata, severity scores, file locations, and (when enabled) validation status.
Expand Down
24 changes: 19 additions & 5 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var (
errInvalidReportExtension = fmt.Errorf("invalid report extension")
)

const humanStdoutFormat = "human"

func processFlags(rootCmd *cobra.Command) error {
configFilePath, err := rootCmd.PersistentFlags().GetString(configFileFlag)
if err != nil {
Expand All @@ -32,6 +34,11 @@ func processFlags(rootCmd *cobra.Command) error {
}

// Apply all flag mappings immediately
if logLevelFlag := rootCmd.PersistentFlags().Lookup(logLevelFlagName); logLevelFlag != nil {
logLevelUserDefined = logLevelFlag.Changed
}
applyDefaultLogLevelForHumanFormat()

engineConfigVar.ScanConfig.WithValidation = validateVar
if len(customRegexRuleVar) > 0 {
engineConfigVar.CustomRegexPatterns = customRegexRuleVar
Expand Down Expand Up @@ -64,16 +71,23 @@ func setupLogging() {
log.Logger = log.Logger.Level(logLevel)
}

func applyDefaultLogLevelForHumanFormat() {
if strings.EqualFold(stdoutFormatVar, humanStdoutFormat) && !logLevelUserDefined {
logLevelVar = "warn"
}
}

func validateFormat(stdout string, reportPath []string) error {
r := regexp.MustCompile(outputFormatRegexpPattern)
if !(r.MatchString(stdout)) {
return fmt.Errorf(`%w: %s, available formats are: json, yaml and sarif`, errInvalidOutputFormat, stdout)
stdoutRegex := regexp.MustCompile(stdoutFormatRegexpPattern)
if !stdoutRegex.MatchString(stdout) {
return fmt.Errorf(`%w: %s, available formats are: json, yaml, sarif, human`, errInvalidOutputFormat, stdout)
}

reportRegex := regexp.MustCompile(reportFormatRegexpPattern)
for _, path := range reportPath {
fileExtension := filepath.Ext(path)
format := strings.TrimPrefix(fileExtension, ".")
if !(r.MatchString(format)) {
if !reportRegex.MatchString(format) {
return fmt.Errorf(`%w: %s, available extensions are: json, yaml and sarif`, errInvalidReportExtension, format)
}
}
Expand All @@ -92,7 +106,7 @@ func setupFlags(rootCmd *cobra.Command) {
"path to generate report files. The output format will be determined by the file extension (.json, .yaml, .sarif)")

rootCmd.PersistentFlags().
StringVar(&stdoutFormatVar, stdoutFormatFlagName, "yaml", "stdout output format, available formats are: json, yaml, sarif")
StringVar(&stdoutFormatVar, stdoutFormatFlagName, "yaml", "stdout output format, available formats are: json, yaml, sarif, human")

rootCmd.PersistentFlags().
StringArrayVar(&customRegexRuleVar, customRegexRuleFlagName, []string{}, "custom regexes to apply to the scan, must be valid Go regex")
Expand Down
8 changes: 7 additions & 1 deletion cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ func TestValidateFormat(t *testing.T) {
reportPath: []string{"report.sarif"},
expectedErr: nil,
},
{
name: "valid output format human",
stdoutFormatVar: "human",
reportPath: []string{"report.yaml"},
expectedErr: nil,
},
{
name: "invalid output format",
stdoutFormatVar: "invalid",
Expand All @@ -46,7 +52,7 @@ func TestValidateFormat(t *testing.T) {
{
name: "invalid report extension",
stdoutFormatVar: "json",
reportPath: []string{"report.invalid"},
reportPath: []string{"report.human"},
expectedErr: errInvalidReportExtension,
},
}
Expand Down
18 changes: 10 additions & 8 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import (
var Version = "0.0.0"

const (
outputFormatRegexpPattern = `^(ya?ml|json|sarif)$`
stdoutFormatRegexpPattern = `^(ya?ml|json|sarif|human)$`
reportFormatRegexpPattern = `^(ya?ml|json|sarif)$`
configFileFlag = "config"

logLevelFlagName = "log-level"
Expand All @@ -34,13 +35,14 @@ const (
)

var (
logLevelVar string
reportPathVar []string
stdoutFormatVar string
customRegexRuleVar []string
ignoreOnExitVar = ignoreOnExitNone
engineConfigVar engine.EngineConfig
validateVar bool
logLevelVar string
reportPathVar []string
stdoutFormatVar string
customRegexRuleVar []string
ignoreOnExitVar = ignoreOnExitNone
engineConfigVar engine.EngineConfig
validateVar bool
logLevelUserDefined bool
)

const envPrefix = "2MS"
Expand Down
7 changes: 7 additions & 0 deletions engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"sync"
"text/tabwriter"
"time"

"github.com/checkmarx/2ms/v4/engine/chunk"
"github.com/checkmarx/2ms/v4/engine/extra"
Expand Down Expand Up @@ -78,6 +79,8 @@ type Engine struct {

ScanConfig resources.ScanConfig

startTime time.Time

wg conc.WaitGroup
}

Expand Down Expand Up @@ -734,6 +737,7 @@ func (e *Engine) GetCvssScoreWithoutValidationCh() chan *secrets.Secret {
}

func (e *Engine) Scan(pluginName string) {
e.startTime = time.Now()
e.wg.Go(func() {
e.processItems(pluginName)
})
Expand All @@ -750,6 +754,9 @@ func (e *Engine) Scan(pluginName string) {

func (e *Engine) Wait() {
e.wg.Wait()
if !e.startTime.IsZero() {
e.Report.SetScanDuration(time.Since(e.startTime))
}
}

// isSecretFromConfluenceResourceIdentifier reports whether a regex match found in a line
Expand Down
Loading
Loading