diff --git a/README.md b/README.md index 62ea26fc..6bceb546 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ Flags: --report-path strings path to generate report files. The output format will be determined by the file extension (.json, .yaml, .sarif) --rule strings select rules by name or tag to apply to this scan --stdout-format string stdout output format, available formats are: json, yaml, sarif (default "yaml") + --validate trigger additional validation to check if discovered secrets are active or revoked -v, --version version for 2ms Use "2ms [command] --help" for more information about a command. @@ -158,6 +159,20 @@ Use "2ms [command] --help" for more information about a command. +## Validity Check + +From the help message: `--validate trigger additional validation to check if discovered secrets are active or revoked`. + +The `--validate` flag will check the validity of the secrets found. For example, if it is a Github token, it will check if the token is valid by making a request to the Github API. We will use the less intrusive method to check the validity of the secret. + +The result of the validation can be: + +- `valid` - The secret is valid +- `revoked` - The secret is revoked +- `unknown` - We failed to check, or we are not checking the validity of the secret at all + +If the `--validate` flag is not provided, the validation field will be omitted from the output, or its value will be an empty string. + ## Special Rules Special rules are rules that are not part of the default ruleset, usually because they are too noisy or too specific. You can use the `--add-special-rule` flag to add special rules by rule ID. diff --git a/cmd/main.go b/cmd/main.go index 2056bfcc..567fe85c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,6 +30,7 @@ const ( specialRulesFlagName = "add-special-rule" ignoreOnExitFlagName = "ignore-on-exit" maxTargetMegabytesFlagName = "max-target-megabytes" + validate = "validate" ) var ( @@ -40,6 +41,7 @@ var ( ignoreVar []string ignoreOnExitVar = ignoreOnExitNone secretsConfigVar secrets.SecretsConfig + validateVar bool ) var rootCmd = &cobra.Command{ @@ -71,6 +73,7 @@ var channels = plugins.Channels{ var report = reporting.Init() var secretsChan = make(chan *secrets.Secret) +var validationChan = make(chan *secrets.Secret) func Execute() (int, error) { vConfig.SetEnvPrefix(envPrefix) @@ -89,6 +92,7 @@ func Execute() (int, error) { rootCmd.PersistentFlags().StringSliceVar(&secretsConfigVar.SpecialList, specialRulesFlagName, []string{}, "special (non-default) rules to apply.\nThis list is not affected by the --rule and --ignore-rule flags.") rootCmd.PersistentFlags().Var(&ignoreOnExitVar, ignoreOnExitFlagName, "defines which kind of non-zero exits code should be ignored\naccepts: all, results, errors, none\nexample: if 'results' is set, only engine errors will make 2ms exit code different from 0") rootCmd.PersistentFlags().IntVar(&secretsConfigVar.MaxTargetMegabytes, maxTargetMegabytesFlagName, 0, "files larger than this will be skipped.\nOmit or set to 0 to disable this check.") + rootCmd.PersistentFlags().BoolVar(&validateVar, validate, false, "trigger additional validation to check if discovered secrets are active or revoked") rootCmd.AddCommand(secrets.GetRulesCommand(&secretsConfigVar)) @@ -135,6 +139,11 @@ func preRun(cmd *cobra.Command, args []string) error { channels.WaitGroup.Add(1) go processSecrets() + if validateVar { + channels.WaitGroup.Add(1) + go processValidation() + } + return nil } diff --git a/cmd/workers.go b/cmd/workers.go index 2246949b..2ebc5330 100644 --- a/cmd/workers.go +++ b/cmd/workers.go @@ -24,6 +24,21 @@ func processSecrets() { for secret := range secretsChan { report.TotalSecretsFound++ + if validateVar { + validationChan <- secret + } report.Results[secret.ID] = append(report.Results[secret.ID], secret) } + close(validationChan) +} + +func processValidation() { + defer channels.WaitGroup.Done() + + wgValidation := &sync.WaitGroup{} + for secret := range validationChan { + wgValidation.Add(1) + go secret.Validate(wgValidation) + } + wgValidation.Wait() } diff --git a/secrets/secret.go b/secrets/secret.go index ea03a15f..b012b992 100644 --- a/secrets/secret.go +++ b/secrets/secret.go @@ -1,12 +1,68 @@ package secrets +import ( + "fmt" + "net/http" + "sync" + + "github.com/rs/zerolog/log" +) + +type ValidationResult string + +const ( + Valid ValidationResult = "Valid" + Revoked ValidationResult = "Revoked" + Unknown ValidationResult = "Unknown" +) + type Secret struct { - ID string `json:"id"` - Source string `json:"source"` - RuleID string `json:"ruleId"` - StartLine int `json:"startLine"` - EndLine int `json:"endLine"` - StartColumn int `json:"startColumn"` - EndColumn int `json:"endColumn"` - Value string `json:"value"` + ID string `json:"id"` + Source string `json:"source"` + RuleID string `json:"ruleId"` + StartLine int `json:"startLine"` + EndLine int `json:"endLine"` + StartColumn int `json:"startColumn"` + EndColumn int `json:"endColumn"` + Value string `json:"value"` + ValidationStatus ValidationResult `json:"validationStatus,omitempty"` +} + +type validationFunc = func(*Secret) ValidationResult + +var ruleIDToFunction = map[string]validationFunc{ + "github-fine-grained-pat": validateGithub, + "github-pat": validateGithub, +} + +func (s *Secret) Validate(wg *sync.WaitGroup) { + defer wg.Done() + if f, ok := ruleIDToFunction[s.RuleID]; ok { + s.ValidationStatus = f(s) + } else { + s.ValidationStatus = Unknown + } +} + +func validateGithub(s *Secret) ValidationResult { + const githubURL = "https://api.github.com/" + + req, err := http.NewRequest("GET", githubURL, nil) + if err != nil { + log.Warn().Err(err).Msg("Failed to validate secret") + return Unknown + } + req.Header.Set("Authorization", fmt.Sprintf("token %s", s.Value)) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Warn().Err(err).Msg("Failed to validate secret") + return Unknown + } + + if resp.StatusCode == http.StatusOK { + return Valid + } + return Revoked }