|
| 1 | +package repositorykey |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "errors" |
| 6 | + "fmt" |
| 7 | + "net/url" |
| 8 | + "os/exec" |
| 9 | + "strconv" |
| 10 | + "strings" |
| 11 | + |
| 12 | + regexp "github.com/wasilibs/go-re2" |
| 13 | + |
| 14 | + "github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple" |
| 15 | + logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context" |
| 16 | + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" |
| 17 | + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" |
| 18 | +) |
| 19 | + |
| 20 | +type Scanner struct { |
| 21 | + detectors.DefaultMultiPartCredentialProvider |
| 22 | +} |
| 23 | + |
| 24 | +// Ensure the Scanner satisfies the interface at compile time. |
| 25 | +var _ detectors.Detector = (*Scanner)(nil) |
| 26 | + |
| 27 | +var ( |
| 28 | + // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. |
| 29 | + urlPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", "url"}) + `([a-z0-9][a-z0-9-]{0,48}[a-z0-9]\.scm\.azure-api\.net)`) |
| 30 | + passwordPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", "password"}) + `\b(git&[0-9]{12}&[a-zA-Z0-9\/+]{85}[a-zA-Z0-9]==)`) |
| 31 | + |
| 32 | + invalidHosts = simple.NewCache[struct{}]() |
| 33 | + noSuchHostErr = errors.New("Could not resolve host") |
| 34 | +) |
| 35 | + |
| 36 | +const ( |
| 37 | + azureGitUsername = "apim" |
| 38 | +) |
| 39 | + |
| 40 | +// Keywords are used for efficiently pre-filtering chunks. |
| 41 | +// Use identifiers in the secret preferably, or the provider name. |
| 42 | +func (s Scanner) Keywords() []string { |
| 43 | + return []string{"azure", ".scm.azure-api.net"} |
| 44 | +} |
| 45 | + |
| 46 | +// FromData will find and optionally verify AzureDevopsPersonalAccessToken secrets in a given set of bytes. |
| 47 | +func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { |
| 48 | + logger := logContext.AddLogger(ctx).Logger().WithName("azurecr") |
| 49 | + dataStr := string(data) |
| 50 | + |
| 51 | + // Deduplicate matches. |
| 52 | + uniqueUrlsMatches := make(map[string]struct{}) |
| 53 | + uniquePasswordMatches := make(map[string]struct{}) |
| 54 | + |
| 55 | + for _, matches := range urlPat.FindAllStringSubmatch(dataStr, -1) { |
| 56 | + uniqueUrlsMatches[strings.TrimSpace(matches[1])] = struct{}{} |
| 57 | + } |
| 58 | + |
| 59 | + for _, matches := range passwordPat.FindAllStringSubmatch(dataStr, -1) { |
| 60 | + uniquePasswordMatches[strings.TrimSpace(matches[1])] = struct{}{} |
| 61 | + } |
| 62 | + |
| 63 | +EndpointLoop: |
| 64 | + for urlMatch := range uniqueUrlsMatches { |
| 65 | + for passwordMatch := range uniquePasswordMatches { |
| 66 | + s1 := detectors.Result{ |
| 67 | + DetectorType: detectorspb.DetectorType_AzureApiManagementRepositoryKey, |
| 68 | + Raw: []byte(passwordMatch), |
| 69 | + RawV2: []byte(urlMatch + passwordMatch), |
| 70 | + } |
| 71 | + |
| 72 | + if verify { |
| 73 | + if invalidHosts.Exists(urlMatch) { |
| 74 | + logger.V(3).Info("Skipping invalid registry", "url", urlMatch) |
| 75 | + continue EndpointLoop |
| 76 | + } |
| 77 | + |
| 78 | + isVerified, err := verifyUrlPassword(ctx, urlMatch, azureGitUsername, passwordMatch) |
| 79 | + s1.Verified = isVerified |
| 80 | + if err != nil { |
| 81 | + if errors.Is(err, noSuchHostErr) { |
| 82 | + invalidHosts.Set(urlMatch, struct{}{}) |
| 83 | + continue EndpointLoop |
| 84 | + } |
| 85 | + s1.SetVerificationError(err, urlMatch) |
| 86 | + } |
| 87 | + } |
| 88 | + results = append(results, s1) |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + return results, nil |
| 93 | +} |
| 94 | + |
| 95 | +func (s Scanner) Type() detectorspb.DetectorType { |
| 96 | + return detectorspb.DetectorType_AzureApiManagementRepositoryKey |
| 97 | +} |
| 98 | + |
| 99 | +func (s Scanner) Description() string { |
| 100 | + return "Azure API Management Repository Keys provide access to the API Management (APIM) configuration repository, allowing users to directly interact with and modify API definitions, policies, and settings. These keys enable programmatic access to APIM's Git-based repository, where configurations can be cloned, edited, and pushed back to apply changes. They are primarily used for managing API configurations as code, automating deployments, and synchronizing APIM settings across environments." |
| 101 | +} |
| 102 | + |
| 103 | +func gitCmdCheck() error { |
| 104 | + if errors.Is(exec.Command("git").Run(), exec.ErrNotFound) { |
| 105 | + return fmt.Errorf("'git' command not found in $PATH. Make sure git is installed and included in $PATH") |
| 106 | + } |
| 107 | + |
| 108 | + // Check the version is greater than or equal to 2.20.0 |
| 109 | + out, err := exec.Command("git", "--version").Output() |
| 110 | + if err != nil { |
| 111 | + return fmt.Errorf("failed to check git version: %w", err) |
| 112 | + } |
| 113 | + |
| 114 | + // Extract the version string using a regex to find the version numbers |
| 115 | + var regex = regexp.MustCompile(`\d+\.\d+\.\d+`) |
| 116 | + |
| 117 | + versionStr := regex.FindString(string(out)) |
| 118 | + versionParts := strings.Split(versionStr, ".") |
| 119 | + |
| 120 | + // Parse version numbers |
| 121 | + major, _ := strconv.Atoi(versionParts[0]) |
| 122 | + minor, _ := strconv.Atoi(versionParts[1]) |
| 123 | + |
| 124 | + // Compare with version 2.20.0<=x<3.0.0 |
| 125 | + if major == 2 && minor >= 20 { |
| 126 | + return nil |
| 127 | + } |
| 128 | + return fmt.Errorf("git version is %s, but must be greater than or equal to 2.20.0, and less than 3.0.0", versionStr) |
| 129 | +} |
| 130 | + |
| 131 | +func verifyUrlPassword(_ context.Context, repoUrl, user, password string) (bool, error) { |
| 132 | + if err := gitCmdCheck(); err != nil { |
| 133 | + return false, err |
| 134 | + } |
| 135 | + |
| 136 | + parsedURL, err := url.Parse(repoUrl) |
| 137 | + if err != nil { |
| 138 | + return false, err |
| 139 | + } |
| 140 | + |
| 141 | + if parsedURL.User == nil { |
| 142 | + parsedURL.User = url.UserPassword(user, password) |
| 143 | + } |
| 144 | + parsedURL.Scheme = "https" // Force HTTPS |
| 145 | + |
| 146 | + fakeRef := "TRUFFLEHOG_CHECK_GIT_REMOTE_URL_REACHABILITY" |
| 147 | + gitArgs := []string{"ls-remote", parsedURL.String(), "--quiet", fakeRef} |
| 148 | + cmd := exec.Command("git", gitArgs...) |
| 149 | + output, err := cmd.CombinedOutput() |
| 150 | + if err != nil { |
| 151 | + outputString := string(output) |
| 152 | + if strings.Contains(outputString, "Authentication failed") { |
| 153 | + return false, nil |
| 154 | + } else if strings.Contains(outputString, "Could not resolve host") { |
| 155 | + return false, noSuchHostErr |
| 156 | + } |
| 157 | + return false, err |
| 158 | + } |
| 159 | + |
| 160 | + return true, nil |
| 161 | +} |
0 commit comments