Repository: gitleaks/gitleaks (go)

Issue:
False positives for curl-auth rule if variables are quoted
**Describe the bug**
The new rule `curl-auth-user` is supposed to find leaks of basic auth credentials used with curl by determining if the curl command was called with the `-u` or `--user` flag, where the value contains a leaked secret (i.e. the credentials of an account).

However, the rule also matches if the flag is used in a script with variables. This is not a leak, as the environment variables are retrieved through other means and never committed to source code or any artifacts.

**To Reproduce**

Create a file `test.sh` (example AQL call to Artifactory):

```sh
curl -sSf -u ${USER}:${PASSWORD} \
    -H "content-type: text/plain" \
    -X POST 'https://xxx.com/artifactory/api/search/aql' \
    -d @query.aql \
    > output.json
```

Run gitleaks and inspect the finding.

> ...
Finding:     curl -sSf -u REDACTED \
Secret:      REDACTED
RuleID:      curl-auth-user
Entropy:     4.169890
...

If the quotes around the value passed using `-u` are removed, the false positive disappears. This means the rule must be fixed to not simply match if quotes are given.

**Expected behavior**
Variables do not match the rule, even if they are quoted, as they are a valid way to pass the credentials without leaks.
One wants to quote variables to prevent word-splitting and globbing.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Basic Info (please complete the following information):**
 - OS: MacOs
 - Gitleaks Version: v8.21.1

**Additional context**
You can also verify the behavior with regex101:
https://regex101.com/r/LcccLw/1

cc @zricethezav

<!--

Check out the Secret Scanning Discord Server for live discussions:

https://discord.gg/sydS6AHTUP

--->


Pull request:
Further improvements to curl rules
### Description:
This adds the following improvements to the CURL rule. It closes #1589.

1. `curl-auth-header` matches provider-specific headers like `X-AIO-Key` or `X-Octopus-ApiKey`.
2. `curl-auth-user` no longer matches placeholders where individual segments are quoted (e.g., `"${USR}":"${PWD}"`).

### Checklist:

* [x] Does your PR pass tests?
* [x] Have you written new tests for your changes?
* [x] Have you lint your code locally prior to submission?


Patch:
diff --git a/cmd/generate/config/base/config.go b/cmd/generate/config/base/config.go
index b0aa7c8..e9c9fab 100644
--- a/cmd/generate/config/base/config.go
+++ b/cmd/generate/config/base/config.go
@@ -50,6 +50,8 @@ func CreateGlobalConfig() config.Config {
 				regexp.MustCompile(`^\{\d{0,2}}$`),                       // Python (https://docs.python.org/3/tutorial/inputoutput.html)
 				// Urban Code Deploy (https://www.ibm.com/support/pages/replace-token-step-replaces-replacement-values-windows-variables)
 				regexp.MustCompile(`^@([A-Z_]+|[a-z_]+)@$`),
+
+				// ----------- Miscellaneous -----------
 			},
 			Paths: []*regexp.Regexp{
 				regexp.MustCompile(`gitleaks\.toml`),
@@ -80,7 +82,7 @@ func CreateGlobalConfig() config.Config {
 				regexp.MustCompile(`(^|/)npm-shrinkwrap\.json$`),
 				regexp.MustCompile(`(^|/)bower_components/.*?$`),
 				// TODO: Add more common static assets, such as swagger-ui.
-				regexp.MustCompile(`(^|/)jquery(-ui)?-[a-zA-Z0-9.-]+\.js$`),
+				regexp.MustCompile(`(^|/)(jquery(-?ui)?|swagger-?ui)-[a-zA-Z0-9.-]+\.js(\.map)?$`),
 
 				// ----------- Python files -----------
 				// Dependencies and lock files.
@@ -100,6 +102,11 @@ func CreateGlobalConfig() config.Config {
 				regexp.MustCompile(`Database.refactorlog`),
 				//regexp.MustCompile(`vendor`),
 			},
+			StopWords: []string{
+				// ----------- Secrets -----------
+				// Checkmarx client secret. (https://github.com/checkmarx-ts/checkmarx-python-sdk/blob/86560f6e2a3e46d16322101294da10d5d190312d/README.md?plain=1#L56)
+				"014df517-39d1-4453-b7b3-9930c563627c",
+			},
 		},
 	}
 }
diff --git a/cmd/generate/config/main.go b/cmd/generate/config/main.go
index 3ca8cdd..cbbdfbd 100644
--- a/cmd/generate/config/main.go
+++ b/cmd/generate/config/main.go
@@ -55,8 +55,8 @@ func main() {
 		rules.ConfluentAccessToken(),
 		rules.ConfluentSecretKey(),
 		rules.Contentful(),
-		rules.CurlBasicAuth(),
 		rules.CurlHeaderAuth(),
+		rules.CurlBasicAuth(),
 		rules.Databricks(),
 		rules.DatadogtokenAccessToken(),
 		rules.DefinedNetworkingAPIToken(),
diff --git a/cmd/generate/config/rules/config.tmpl b/cmd/generate/config/rules/config.tmpl
index a0729d9..7af1b53 100644
--- a/cmd/generate/config/rules/config.tmpl
+++ b/cmd/generate/config/rules/config.tmpl
@@ -22,6 +22,9 @@ regexes = [{{ range $i, $regex := . }}
 paths = [{{ range $i, $path := . }}
     '''{{ $path }}''',{{ end }}
 ]{{ end }}
+{{- with .Allowlist.StopWords }}{{println}}stopwords = [{{ range $j, $stopword := . }}
+    "{{ $stopword }}",{{ end }}
+]{{ end }}
 
 {{ range $i, $rule := .Rules }}[[rules]]
 id = "{{$rule.RuleID}}"
diff --git a/cmd/generate/config/rules/curl.go b/cmd/generate/config/rules/curl.go
index 01e6568..2135c16 100644
--- a/cmd/generate/config/rules/curl.go
+++ b/cmd/generate/config/rules/curl.go
@@ -20,11 +20,11 @@ func CurlBasicAuth() *config.Rule {
 			{
 				Regexes: []*regexp.Regexp{
 					regexp.MustCompile(`[^:]+:(change(it|me)|pass(word)?|pwd|test|token|\*+|x+)`), // common placeholder passwords
-					regexp.MustCompile(`<[^>]+>:<[^>]+>|<[^:]+:[^>]+>`),                           // <placeholder>
+					regexp.MustCompile(`['"]?<[^>]+>['"]?:['"]?<[^>]+>|<[^:]+:[^>]+>['"]?`),       // <placeholder>
 					regexp.MustCompile(`[^:]+:\[[^]]+]`),                                          // [placeholder]
-					regexp.MustCompile(`[^:]+:\$(\d|\w+|\{(\d|\w+)})`),                            // $1 or $VARIABLE
+					regexp.MustCompile(`['"]?[^:]+['"]?:['"]?\$(\d|\w+|\{(\d|\w+)})['"]?`),        // $1 or $VARIABLE
 					regexp.MustCompile(`\$\([^)]+\):\$\([^)]+\)`),                                 // $(cat login.txt)
-					regexp.MustCompile(`\$?{{[^}]+}}:\$?{{[^}]+}}`),                               // ${{ secrets.FOO }} or {{ .Values.foo }}
+					regexp.MustCompile(`['"]?\$?{{[^}]+}}['"]?:['"]?\$?{{[^}]+}}['"]?`),           // ${{ secrets.FOO }} or {{ .Values.foo }}
 				},
 			},
 		},
@@ -63,6 +63,7 @@ func CurlBasicAuth() *config.Rule {
 		`curl -u "myusername" http://localhost:15130/api/check_user/`, // no password
 		`curl -u username:token`,
 		`curl -u "${_username}:${_password}"`,
+		`curl -u "${username}":"${password}"`,
 		`curl -k -X POST -I -u "SRVC_JENKINS:${APPID}"`,
 
 		// long
@@ -96,7 +97,7 @@ nc -u -l 41234`,
 // https://curl.se/docs/manpage.html#-H
 func CurlHeaderAuth() *config.Rule {
 	// language=regexp
-	authPat := `(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-)?Api-?(?:Key|Token)|Token):[ \t]{0,5}([\w=~@.+/-]{8,}))`
+	authPat := `(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))`
 	r := config.Rule{
 		RuleID: "curl-auth-header",
 		// TODO: Description: "",
@@ -160,6 +161,8 @@ func CurlHeaderAuth() *config.Rule {
      -H "Accept: application/json" \`, // apikey
 		`curl -X POST --header "Api-Token: Sk94HG7f6KB"`, // api-token
 		`curl -XPOST http://localhost:8080/api/tasks -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "Token: 3fea6af1349166ea" -d "content=hello-curl"`, // token
+		`curl -X GET https://octopus.corp.net/
+     -H "X-Octopus-ApiKey: 3a16750d-d363-41a4-8ebd-035408f7730f" \`, // X-$thing-ApiKey
 	}
 	fps := []string{
 		// Placeholders
diff --git a/config/gitleaks.toml b/config/gitleaks.toml
index 5f0bba9..dd1fcd0 100644
--- a/config/gitleaks.toml
+++ b/config/gitleaks.toml
@@ -43,7 +43,7 @@ paths = [
     '''(^|/)pnpm-lock\.yaml$''',
     '''(^|/)npm-shrinkwrap\.json$''',
     '''(^|/)bower_components/.*?$''',
-    '''(^|/)jquery(-ui)?-[a-zA-Z0-9.-]+\.js$''',
+    '''(^|/)(jquery(-?ui)?|swagger-?ui)-[a-zA-Z0-9.-]+\.js(\.map)?$''',
     '''(^|/)(Pipfile|poetry)\.lock$''',
     '''(?i)/?(v?env|virtualenv)/lib(64)?/.+$''',
     '''(?i)(^|/)(lib(64)?/python[23](\.\d{1,2})+/|python/[23](\.\d{1,2})+/lib(64)?/).+$''',
@@ -53,6 +53,9 @@ paths = [
     '''verification-metadata.xml''',
     '''Database.refactorlog''',
 ]
+stopwords = [
+    "014df517-39d1-4453-b7b3-9930c563627c",
+]
 
 [[rules]]
 id = "1password-service-account-token"
@@ -276,7 +279,7 @@ keywords = ["contentful"]
 [[rules]]
 id = "curl-auth-header"
 description = ""
-regex = '''\bcurl\b(?:.*?|.*?(?:[\r\n]{1,2}.*?){1,5})[ \t\n\r](?:-H|--header)(?:=|[ \t]{0,5})(?:"(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-)?Api-?(?:Key|Token)|Token):[ \t]{0,5}([\w=~@.+/-]{8,}))"|'(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-)?Api-?(?:Key|Token)|Token):[ \t]{0,5}([\w=~@.+/-]{8,}))')(?:\B|\s|\z)'''
+regex = '''\bcurl\b(?:.*?|.*?(?:[\r\n]{1,2}.*?){1,5})[ \t\n\r](?:-H|--header)(?:=|[ \t]{0,5})(?:"(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))"|'(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))')(?:\B|\s|\z)'''
 entropy = 2.75
 keywords = ["curl"]
 
@@ -290,11 +293,11 @@ keywords = ["curl"]
 [rules.allowlist]
 regexes = [
     '''[^:]+:(change(it|me)|pass(word)?|pwd|test|token|\*+|x+)''',
-    '''<[^>]+>:<[^>]+>|<[^:]+:[^>]+>''',
+    '''['"]?<[^>]+>['"]?:['"]?<[^>]+>|<[^:]+:[^>]+>['"]?''',
     '''[^:]+:\[[^]]+]''',
-    '''[^:]+:\$(\d|\w+|\{(\d|\w+)})''',
+    '''['"]?[^:]+['"]?:['"]?\$(\d|\w+|\{(\d|\w+)})['"]?''',
     '''\$\([^)]+\):\$\([^)]+\)''',
-    '''\$?{{[^}]+}}:\$?{{[^}]+}}''',
+    '''['"]?\$?{{[^}]+}}['"]?:['"]?\$?{{[^}]+}}['"]?''',
 ]
 
 [[rules]]


Test patch:
diff --git a/cmd/generate/config/base/config_test.go b/cmd/generate/config/base/config_test.go
index e35612e..01e58c7 100644
--- a/cmd/generate/config/base/config_test.go
+++ b/cmd/generate/config/base/config_test.go
@@ -89,10 +89,12 @@ func TestConfigAllowlistPaths(t *testing.T) {
 		invalid []string
 		valid   []string
 	}{
-		"javascript - jquery.js": {
+		"javascript - common static assets": {
 			invalid: []string{
 				`src/main/resources/static/jquery-ui-1.12.1/jquery-ui-min.js`,
 				`src/main/resources/static/js/jquery-ui-1.10.4.min.js`,
+				`swagger/swaggerui/swagger-ui-bundle.js.map`,
+				`swagger/swaggerui/swagger-ui-es-bundle.js.map`,
 			},
 		},
 		"python": {