Skip to content

Add file path for secrets (AST-103853) #1232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 22, 2025
Merged
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/Checkmarx/secret-detection v0.0.3-0.20250327150305-31c2c3be9edf
github.com/MakeNowJust/heredoc v1.0.0
github.com/bouk/monkey v1.0.0
github.com/checkmarx/2ms/v3 v3.20.0
github.com/checkmarx/2ms/v3 v3.21.0
github.com/gofrs/flock v0.12.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/gomarkdown/markdown v0.0.0-20241102151059-6bc1ffdc6e8c
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/checkmarx/2ms v1.4.1-0.20250327145719-b78804cb08c7 h1:COsC3skOJeJaSoCPuhLZ0byRGKm+ZHlyw5qm9ydlab0=
github.com/checkmarx/2ms v1.4.1-0.20250327145719-b78804cb08c7/go.mod h1:Bnd2YSh8LQSc4fHAFN0BKz8LYThB6qHg3Wn/+H+WZ4I=
github.com/checkmarx/2ms/v3 v3.20.0 h1:dr3vSVUoYXwS40DUGR5ueXaPROKTkMs+9xAij2/vzUA=
github.com/checkmarx/2ms/v3 v3.20.0/go.mod h1:f5ZwVFEDBr8WRa/9aXluPnufi2wuq6tGWCROcW3CdbA=
github.com/checkmarx/2ms/v3 v3.21.0 h1:EcabeDypNMsSidISQbziZ062HjMZQ+Hm/uOJ5AOxK8o=
github.com/checkmarx/2ms/v3 v3.21.0/go.mod h1:e8f4F94MZ+iCetR/G3aw7nXdPe6TgPI92Zzk/NG1l0o=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
Expand Down
29 changes: 13 additions & 16 deletions internal/commands/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,33 +529,30 @@ func scanContainersRealtimeSubCommand(realtimeScannerWrapper wrappers.RealtimeSc
return scanContainersRealtimeCmd
}

func scanSecretsRealtimeSubCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command {
func scanSecretsRealtimeSubCommand(
jwtWrapper wrappers.JWTWrapper,
featureFlagsWrapper wrappers.FeatureFlagsWrapper,
) *cobra.Command {
scanSecretsRealtimeCmd := &cobra.Command{
Hidden: true,
Use: "secrets-realtime",
Short: "Run a Secrets-Realtime scan",
Long: "Running a Secrets-Realtime scan is a fast and efficient way to identify exposed secrets in a file.",
Example: heredoc.Doc(
`
$ cx scan secrets-realtime -s <path to file separated>
`,
),
Example: heredoc.Doc(`
$ cx scan secrets-realtime -s <path to file>
$ cx scan secrets-realtime -s <path to file> --ignored-file-path <path to ignored secrets file>
`),
Annotations: map[string]string{
"command:doc": heredoc.Doc(
`
"command:doc": heredoc.Doc(`
https://docs.checkmarx.com/en/34965-68625-checkmarx-one-cli-commands.html
`,
),
`),
},
RunE: RunScanSecretsRealtimeCommand(jwtWrapper, featureFlagsWrapper),
}

scanSecretsRealtimeCmd.PersistentFlags().StringP(
commonParams.SourcesFlag,
commonParams.SourcesFlagSh,
"",
"The file source should be the path to a single file or multiple files separated by commas",
)
scanSecretsRealtimeCmd.Flags().StringP(commonParams.SourcesFlag, "s", "", "Path to the file to scan")
scanSecretsRealtimeCmd.Flags().String(commonParams.IgnoredFilePathFlag, "", "Path to ignored secrets file")

return scanSecretsRealtimeCmd
}

Expand Down
11 changes: 8 additions & 3 deletions internal/commands/secrets-realtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,26 @@ import (

func RunScanSecretsRealtimeCommand(
jwtWrapper wrappers.JWTWrapper,
featureFlagWrapper wrappers.FeatureFlagsWrapper) func(cmd *cobra.Command, args []string) error {
featureFlagWrapper wrappers.FeatureFlagsWrapper,
) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, _ []string) error {
fileSourceFlag, _ := cmd.Flags().GetString(commonParams.SourcesFlag)
if fileSourceFlag == "" {
return errorconstants.NewRealtimeEngineError(errorconstants.RealtimeEngineFilePathRequired).Error()
}

ignoredFilePathFlag, _ := cmd.Flags().GetString(commonParams.IgnoredFilePathFlag)

secretsRealtimeService := secretsrealtime.NewSecretsRealtimeService(jwtWrapper, featureFlagWrapper)

results, err := secretsRealtimeService.RunSecretsRealtimeScan(fileSourceFlag)
results, err := secretsRealtimeService.RunSecretsRealtimeScan(fileSourceFlag, ignoredFilePathFlag)
if err != nil {
return err
}

err = printer.Print(cmd.OutOrStdout(), results, printer.FormatJSON)
if err != nil {
return errorconstants.NewRealtimeEngineError("failed to return packages").Error()
return errorconstants.NewRealtimeEngineError("failed to return secrets").Error()
}

return nil
Expand Down
6 changes: 3 additions & 3 deletions internal/commands/secrets-realtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestRunScanSecretsRealtimeCommand_TxtFile_ScanSuccess(t *testing.T) {
mock.Flag = wrappers.FeatureFlagResponseModel{Name: wrappers.OssRealtimeEnabled, Status: true}
execCmdNilAssertion(
t,
"scan", "secrets-realtime", "-s", "data/secret-exposed.txt",
"scan", "secrets-realtime", "-s", "data/secret-exposed.txt", "--ignored-file-path", "",
)
}

Expand All @@ -22,7 +22,7 @@ func TestRunScanSecretsRealtimeCommand_EmptyFilePath_ScanFailed(t *testing.T) {
mock.Flag = wrappers.FeatureFlagResponseModel{Name: wrappers.OssRealtimeEnabled, Status: true}
err := execCmdNotNilAssertion(
t,
"scan", "secrets-realtime", "-s", "",
"scan", "secrets-realtime", "-s", "", "--ignored-file-path", "",
)
assert.NotNil(t, err)
}
Expand All @@ -32,7 +32,7 @@ func TestRunScanSecretsRealtimeCommand_FFDisable_ScanFailed(t *testing.T) {
mock.Flag = wrappers.FeatureFlagResponseModel{Name: wrappers.OssRealtimeEnabled, Status: false}
err := execCmdNotNilAssertion(
t,
"scan", "secrets-realtime", "-s", "data/secret-exposed.txt",
"scan", "secrets-realtime", "-s", "data/secret-exposed.txt", "--ignored-file-path", "",
)
assert.NotNil(t, err)
}
6 changes: 6 additions & 0 deletions internal/services/realtimeengine/secretsrealtime/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ type SecretsRealtimeResult struct {
Severity string `json:"Severity"`
Locations []realtimeengine.Location `json:"Locations"`
}

type IgnoredSecret struct {
Title string `json:"Title"`
FilePath string `json:"FilePath"`
Line int `json:"Line"`
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package secretsrealtime

import (
"encoding/json"
"fmt"
errorconstants "github.com/checkmarx/ast-cli/internal/constants/errors"
"github.com/checkmarx/ast-cli/internal/logger"
"os"

"github.com/checkmarx/2ms/v3/lib/reporting"
"github.com/checkmarx/2ms/v3/lib/secrets"
scanner "github.com/checkmarx/2ms/v3/pkg"

errorconstants "github.com/checkmarx/ast-cli/internal/constants/errors"
"github.com/checkmarx/ast-cli/internal/logger"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine"
"github.com/checkmarx/ast-cli/internal/wrappers"
)
Expand Down Expand Up @@ -39,7 +40,44 @@ func NewSecretsRealtimeService(
}
}

func (s *SecretsRealtimeService) RunSecretsRealtimeScan(filePath string) ([]SecretsRealtimeResult, error) {
func filterIgnoredSecrets(results []SecretsRealtimeResult, ignoreMap map[string]bool) []SecretsRealtimeResult {
filtered := make([]SecretsRealtimeResult, 0, len(results))
for _, r := range results {
if len(r.Locations) == 0 {
filtered = append(filtered, r)
continue
}
key := fmt.Sprintf("%s_%s_%d", r.Title, r.FilePath, r.Locations[0].Line)
if !ignoreMap[key] {
filtered = append(filtered, r)
}
}
return filtered
}

func buildIgnoreMap(ignored []IgnoredSecret) map[string]bool {
m := make(map[string]bool)
for _, s := range ignored {
key := fmt.Sprintf("%s_%s_%d", s.Title, s.FilePath, s.Line)
m[key] = true
}
return m
}

func loadIgnoredSecrets(path string) ([]IgnoredSecret, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var ignored []IgnoredSecret
err = json.Unmarshal(data, &ignored)
if err != nil {
return nil, err
}
return ignored, nil
}

func (s *SecretsRealtimeService) RunSecretsRealtimeScan(filePath, ignoredFilePath string) ([]SecretsRealtimeResult, error) {
if filePath == "" {
return nil, errorconstants.NewRealtimeEngineError(errorconstants.RealtimeEngineFilePathRequired).Error()
}
Expand All @@ -66,7 +104,18 @@ func (s *SecretsRealtimeService) RunSecretsRealtimeScan(filePath string) ([]Secr
return nil, errorconstants.NewRealtimeEngineError("failed to run secrets scan").Error()
}

return convertToSecretsRealtimeResult(report), nil
results := convertToSecretsRealtimeResult(report)

if ignoredFilePath == "" {
return results, nil
}
ignoredSecrets, err := loadIgnoredSecrets(ignoredFilePath)
if err != nil {
return nil, errorconstants.NewRealtimeEngineError("failed to load ignored secrets").Error()
}
ignoreMap := buildIgnoreMap(ignoredSecrets)
results = filterIgnoredSecrets(results, ignoreMap)
return results, nil
}

func readFile(filePath string) (string, error) {
Expand All @@ -78,6 +127,7 @@ func readFile(filePath string) (string, error) {
}

func runScan(source, content string) (*reporting.Report, error) {

item := scanner.ScanItem{
Content: &content,
Source: source,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package secretsrealtime

import (
"encoding/json"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -29,7 +30,7 @@ func TestRunSecretsRealtimeScan_EmptyFilePath_ReturnsError(t *testing.T) {
FeatureFlagWrapper: &mock.FeatureFlagsMockWrapper{},
}

results, err := service.RunSecretsRealtimeScan("")
results, err := service.RunSecretsRealtimeScan("", "")

assert.Nil(t, results)
assert.NotNil(t, err)
Expand All @@ -44,7 +45,7 @@ func TestRunSecretsRealtimeScan_FeatureFlagDisabled_ReturnsError(t *testing.T) {
FeatureFlagWrapper: &mock.FeatureFlagsMockWrapper{},
}

results, err := service.RunSecretsRealtimeScan("test.txt")
results, err := service.RunSecretsRealtimeScan("test.txt", "")

assert.Nil(t, results)
assert.NotNil(t, err)
Expand All @@ -59,13 +60,43 @@ func TestRunSecretsRealtimeScan_FileNotFound_ReturnsError(t *testing.T) {
FeatureFlagWrapper: &mock.FeatureFlagsMockWrapper{},
}

results, err := service.RunSecretsRealtimeScan("nonexistent-file.txt")
results, err := service.RunSecretsRealtimeScan("nonexistent-file.txt", "")

assert.Nil(t, results)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "failed to read file")
}

func TestRunSecretsRealtimeScan_WithIgnoreFile_FiltersResult(t *testing.T) {
mock.Flag = wrappers.FeatureFlagResponseModel{Name: wrappers.OssRealtimeEnabled, Status: true}

tempDir := t.TempDir()

testFile := filepath.Join(tempDir, "test.txt")
testContent := "aws_access_key_id = AKIAIOSFODNN7EXAMPLE\ngithub_token = ghp_XXXXXXXXXXXXXXXXXXXX"
assert.NoError(t, os.WriteFile(testFile, []byte(testContent), 0644))

ignoreFile := filepath.Join(tempDir, "ignored.json")
ignored := []IgnoredSecret{
{Title: "github-token", FilePath: "test.txt", Line: 2},
}
data, _ := json.Marshal(ignored)
assert.NoError(t, os.WriteFile(ignoreFile, data, 0644))

service := &SecretsRealtimeService{
JwtWrapper: &mock.JWTMockWrapper{},
FeatureFlagWrapper: &mock.FeatureFlagsMockWrapper{},
}

results, err := service.RunSecretsRealtimeScan(testFile, ignoreFile)
assert.NoError(t, err)
assert.NotNil(t, results)

for _, r := range results {
assert.NotEqual(t, "github-token", r.Title)
}
}

func TestRunSecretsRealtimeScan_ValidFile_Success(t *testing.T) {
mock.Flag = wrappers.FeatureFlagResponseModel{Name: wrappers.OssRealtimeEnabled, Status: true}

Expand All @@ -81,7 +112,7 @@ func TestRunSecretsRealtimeScan_ValidFile_Success(t *testing.T) {
FeatureFlagWrapper: &mock.FeatureFlagsMockWrapper{},
}

results, err := service.RunSecretsRealtimeScan(tempFile)
results, err := service.RunSecretsRealtimeScan(tempFile, "")

assert.NoError(t, err)
assert.NotNil(t, results)
Expand Down
Loading