diff --git a/pkg/detectors/npmtoken/npmtoken.go b/pkg/detectors/npmtoken/npmtoken.go index 8b96490ad5ba..f73d9f90aca8 100644 --- a/pkg/detectors/npmtoken/npmtoken.go +++ b/pkg/detectors/npmtoken/npmtoken.go @@ -8,36 +8,38 @@ import ( regexp "github.com/wasilibs/go-re2" - "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" ) type Scanner struct{} -// Ensure the Scanner satisfies the interfaces at compile time. var _ detectors.Detector = (*Scanner)(nil) var _ detectors.Versioner = (*Scanner)(nil) func (s Scanner) Version() int { return 1 } var ( - client = common.SaneHttpClient() + client = detectors.DetectorHttpClientWithNoLocalAddresses - // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"npm"}) + `\b([0-9Aa-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b`) + + npmrcPat = regexp.MustCompile(`//([^/]+(?:/[^:]+)*)/:_authToken\s*=\s*([^\s]+)`) ) -// Keywords are used for efficiently pre-filtering chunks. -// Use identifiers in the secret preferably, or the provider name. func (s Scanner) Keywords() []string { return []string{"npm"} } -// FromData will find and optionally verify NpmToken secrets in a given set of bytes. func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { dataStr := string(data) matches := keyPat.FindAllStringSubmatch(dataStr, -1) + + var tokenRegistryMap map[string]string + if verify { + tokenRegistryMap = extractTokenRegistryPairs(dataStr) + } + for _, match := range matches { resMatch := strings.TrimSpace(match[1]) @@ -50,20 +52,15 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://registry.npmjs.org/-/whoami", nil) - if err != nil { - continue + registry, found := tokenRegistryMap[resMatch] + if !found { + registry = "registry.npmjs.org" } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - s1.AnalysisInfo = map[string]string{ - "key": resMatch, - } - } + + isVerified, extraData := verifyToken(ctx, resMatch, registry) + s1.Verified = isVerified + if isVerified { + s1.AnalysisInfo = extraData } } @@ -73,6 +70,58 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return } +func extractTokenRegistryPairs(data string) map[string]string { + matches := npmrcPat.FindAllStringSubmatch(data, -1) + tokenMap := make(map[string]string) + + for _, match := range matches { + if len(match) > 2 { + registry := match[1] + token := match[2] + + token = strings.TrimSpace(token) + if _, exists := tokenMap[token]; !exists { + tokenMap[token] = registry + } + } + } + + return tokenMap +} + +func verifyToken(ctx context.Context, token string, registry string) (bool, map[string]string) { + registryURL := buildRegistryURL(registry) + + req, err := http.NewRequestWithContext(ctx, "GET", registryURL, nil) + if err != nil { + return false, nil + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + res, err := client.Do(req) + if err == nil { + defer res.Body.Close() + if res.StatusCode >= 200 && res.StatusCode < 300 { + return true, map[string]string{ + "key": token, + "registry": registry, + } + } + } + + return false, nil +} + +func buildRegistryURL(registry string) string { + registry = strings.TrimSpace(registry) + registry = strings.TrimSuffix(registry, "/") + + if strings.HasPrefix(registry, "http://") || strings.HasPrefix(registry, "https://") { + return registry + "/-/whoami" + } + + return "https://" + registry + "/-/whoami" +} + func (s Scanner) Type() detector_typepb.DetectorType { return detector_typepb.DetectorType_NpmToken } diff --git a/pkg/detectors/npmtoken/npmtoken_test.go b/pkg/detectors/npmtoken/npmtoken_test.go index c85bd3418acc..a850875c99a8 100644 --- a/pkg/detectors/npmtoken/npmtoken_test.go +++ b/pkg/detectors/npmtoken/npmtoken_test.go @@ -89,3 +89,100 @@ func TestNpmToken_Pattern(t *testing.T) { }) } } + +func TestExtractTokenRegistryPairs(t *testing.T) { + tests := []struct { + name string + input string + want map[string]string + }{ + { + name: "single registry from npmrc", + input: "//artifactory.example.com/:_authToken=3aAcac6c-9847-23d9-ce65-917590b81cf0", + want: map[string]string{"3aAcac6c-9847-23d9-ce65-917590b81cf0": "artifactory.example.com"}, + }, + { + name: "registry with path", + input: "//nexus.example.com/repository/npm-proxy/:_authToken=3aAcac6c-9847-23d9-ce65-917590b81cf0", + want: map[string]string{"3aAcac6c-9847-23d9-ce65-917590b81cf0": "nexus.example.com/repository/npm-proxy"}, + }, + { + name: "multiple registries with different tokens", + input: `//artifactory.example.com/:_authToken=token1 +//nexus.example.com/:_authToken=token2`, + want: map[string]string{"token1": "artifactory.example.com", "token2": "nexus.example.com"}, + }, + { + name: "no registry", + input: "npm token = 3aAcac6c-9847-23d9-ce65-917590b81cf0", + want: map[string]string{}, + }, + { + name: "duplicate token uses first registry", + input: "//registry1.example.com/:_authToken=token1\n//registry2.example.com/:_authToken=token1", + want: map[string]string{"token1": "registry1.example.com"}, + }, + { + name: "registry with port number", + input: "//localhost:4873/:_authToken=3aAcac6c-9847-23d9-ce65-917590b81cf0", + want: map[string]string{"3aAcac6c-9847-23d9-ce65-917590b81cf0": "localhost:4873"}, + }, + { + name: "registry with port and path", + input: "//nexus.example.com:8081/repository/npm/:_authToken=3aAcac6c-9847-23d9-ce65-917590b81cf0", + want: map[string]string{"3aAcac6c-9847-23d9-ce65-917590b81cf0": "nexus.example.com:8081/repository/npm"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := extractTokenRegistryPairs(test.input) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("extractTokenRegistryPairs() diff: (-want +got)\n%s", diff) + } + }) + } +} + +func TestBuildRegistryURL(t *testing.T) { + tests := []struct { + name string + registry string + want string + }{ + { + name: "simple registry", + registry: "registry.npmjs.org", + want: "https://registry.npmjs.org/-/whoami", + }, + { + name: "registry with path", + registry: "nexus.example.com/repository/npm-proxy", + want: "https://nexus.example.com/repository/npm-proxy/-/whoami", + }, + { + name: "registry with https", + registry: "https://artifactory.example.com", + want: "https://artifactory.example.com/-/whoami", + }, + { + name: "registry with http", + registry: "http://localhost:4873", + want: "http://localhost:4873/-/whoami", + }, + { + name: "registry with trailing slash", + registry: "registry.example.com/", + want: "https://registry.example.com/-/whoami", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := buildRegistryURL(test.registry) + if got != test.want { + t.Errorf("buildRegistryURL() = %v, want %v", got, test.want) + } + }) + } +} diff --git a/pkg/detectors/npmtokenv2/npmtokenv2.go b/pkg/detectors/npmtokenv2/npmtokenv2.go index f08d73a8632b..122a0f908c4d 100644 --- a/pkg/detectors/npmtokenv2/npmtokenv2.go +++ b/pkg/detectors/npmtokenv2/npmtokenv2.go @@ -4,40 +4,42 @@ import ( "context" "fmt" "net/http" + "strings" regexp "github.com/wasilibs/go-re2" - "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" ) type Scanner struct{} -// Ensure the Scanner satisfies the interfaces at compile time. var _ detectors.Detector = (*Scanner)(nil) var _ detectors.Versioner = (*Scanner)(nil) func (s Scanner) Version() int { return 2 } var ( - client = common.SaneHttpClient() + client = detectors.DetectorHttpClientWithNoLocalAddresses - // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. keyPat = regexp.MustCompile(`(npm_[0-9a-zA-Z]{36})`) + + npmrcPat = regexp.MustCompile(`//([^/]+(?:/[^:]+)*)/:_authToken\s*=\s*([^\s]+)`) ) -// Keywords are used for efficiently pre-filtering chunks. -// Use identifiers in the secret preferably, or the provider name. func (s Scanner) Keywords() []string { return []string{"npm_"} } -// FromData will find and optionally verify NpmTokenV2 secrets in a given set of bytes. func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { dataStr := string(data) matches := keyPat.FindAllStringSubmatch(dataStr, -1) + + var tokenRegistryMap map[string]string + if verify { + tokenRegistryMap = extractTokenRegistryPairs(dataStr) + } for _, match := range matches { resMatch := match[1] @@ -51,20 +53,15 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://registry.npmjs.org/-/whoami", nil) - if err != nil { - continue + registry, found := tokenRegistryMap[resMatch] + if !found { + registry = "registry.npmjs.org" } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - s1.AnalysisInfo = map[string]string{ - "key": resMatch, - } - } + + isVerified, extraData := verifyToken(ctx, resMatch, registry) + s1.Verified = isVerified + if isVerified { + s1.AnalysisInfo = extraData } } @@ -74,6 +71,58 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return } +func extractTokenRegistryPairs(data string) map[string]string { + matches := npmrcPat.FindAllStringSubmatch(data, -1) + tokenMap := make(map[string]string) + + for _, match := range matches { + if len(match) > 2 { + registry := match[1] + token := match[2] + + token = strings.TrimSpace(token) + if _, exists := tokenMap[token]; !exists { + tokenMap[token] = registry + } + } + } + + return tokenMap +} + +func verifyToken(ctx context.Context, token string, registry string) (bool, map[string]string) { + registryURL := buildRegistryURL(registry) + + req, err := http.NewRequestWithContext(ctx, "GET", registryURL, nil) + if err != nil { + return false, nil + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + res, err := client.Do(req) + if err == nil { + defer res.Body.Close() + if res.StatusCode >= 200 && res.StatusCode < 300 { + return true, map[string]string{ + "key": token, + "registry": registry, + } + } + } + + return false, nil +} + +func buildRegistryURL(registry string) string { + registry = strings.TrimSpace(registry) + registry = strings.TrimSuffix(registry, "/") + + if strings.HasPrefix(registry, "http://") || strings.HasPrefix(registry, "https://") { + return registry + "/-/whoami" + } + + return "https://" + registry + "/-/whoami" +} + func (s Scanner) Type() detector_typepb.DetectorType { return detector_typepb.DetectorType_NpmToken } diff --git a/pkg/detectors/npmtokenv2/npmtokenv2_test.go b/pkg/detectors/npmtokenv2/npmtokenv2_test.go index e74b5c15ea16..d476b9e61512 100644 --- a/pkg/detectors/npmtokenv2/npmtokenv2_test.go +++ b/pkg/detectors/npmtokenv2/npmtokenv2_test.go @@ -79,3 +79,100 @@ func TestNpmToken_New_Pattern(t *testing.T) { }) } } + +func TestExtractTokenRegistryPairs(t *testing.T) { + tests := []struct { + name string + input string + want map[string]string + }{ + { + name: "single registry from npmrc", + input: "//artifactory.example.com/:_authToken=npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY", + want: map[string]string{"npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY": "artifactory.example.com"}, + }, + { + name: "registry with path", + input: "//nexus.example.com/repository/npm-proxy/:_authToken=npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY", + want: map[string]string{"npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY": "nexus.example.com/repository/npm-proxy"}, + }, + { + name: "multiple registries with different tokens", + input: `//artifactory.example.com/:_authToken=token1 +//nexus.example.com/:_authToken=token2`, + want: map[string]string{"token1": "artifactory.example.com", "token2": "nexus.example.com"}, + }, + { + name: "no registry", + input: "npm_ token = npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY", + want: map[string]string{}, + }, + { + name: "duplicate token uses first registry", + input: "//registry1.example.com/:_authToken=token1\n//registry2.example.com/:_authToken=token1", + want: map[string]string{"token1": "registry1.example.com"}, + }, + { + name: "registry with port number", + input: "//localhost:4873/:_authToken=npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY", + want: map[string]string{"npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY": "localhost:4873"}, + }, + { + name: "registry with port and path", + input: "//nexus.example.com:8081/repository/npm/:_authToken=npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY", + want: map[string]string{"npm_hK0FJXBYCkejhEMY4Kp6bOOZn1DlfBOmtbJY": "nexus.example.com:8081/repository/npm"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := extractTokenRegistryPairs(test.input) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("extractTokenRegistryPairs() diff: (-want +got)\n%s", diff) + } + }) + } +} + +func TestBuildRegistryURL(t *testing.T) { + tests := []struct { + name string + registry string + want string + }{ + { + name: "simple registry", + registry: "registry.npmjs.org", + want: "https://registry.npmjs.org/-/whoami", + }, + { + name: "registry with path", + registry: "nexus.example.com/repository/npm-proxy", + want: "https://nexus.example.com/repository/npm-proxy/-/whoami", + }, + { + name: "registry with https", + registry: "https://artifactory.example.com", + want: "https://artifactory.example.com/-/whoami", + }, + { + name: "registry with http", + registry: "http://localhost:4873", + want: "http://localhost:4873/-/whoami", + }, + { + name: "registry with trailing slash", + registry: "registry.example.com/", + want: "https://registry.example.com/-/whoami", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := buildRegistryURL(test.registry) + if got != test.want { + t.Errorf("buildRegistryURL() = %v, want %v", got, test.want) + } + }) + } +}