|
| 1 | +package detectors |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "regexp" |
| 6 | + "sync" |
| 7 | + |
| 8 | + "github.com/CompassSecurity/pipeleek/pkg/gitlab/util" |
| 9 | + "github.com/rs/zerolog/log" |
| 10 | + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" |
| 11 | + gitlab "gitlab.com/gitlab-org/api/client-go" |
| 12 | +) |
| 13 | + |
| 14 | +var ( |
| 15 | + gitlabURLMutex sync.RWMutex |
| 16 | + gitlabURL string |
| 17 | + detectorOnce sync.Once |
| 18 | + detector *GitLabURLDetector |
| 19 | +) |
| 20 | + |
| 21 | +type gitlabPattern struct { |
| 22 | + name string |
| 23 | + regex *regexp.Regexp |
| 24 | + strategy verificationStrategy |
| 25 | +} |
| 26 | + |
| 27 | +type verificationStrategy uint8 |
| 28 | + |
| 29 | +const ( |
| 30 | + verifyNone verificationStrategy = iota |
| 31 | + verifyUserAPI |
| 32 | + verifyRunnerAPI |
| 33 | +) |
| 34 | + |
| 35 | +func SetGitLabURL(url string) { |
| 36 | + gitlabURLMutex.Lock() |
| 37 | + defer gitlabURLMutex.Unlock() |
| 38 | + gitlabURL = url |
| 39 | +} |
| 40 | + |
| 41 | +func GetGitLabURL() string { |
| 42 | + gitlabURLMutex.RLock() |
| 43 | + defer gitlabURLMutex.RUnlock() |
| 44 | + return gitlabURL |
| 45 | +} |
| 46 | + |
| 47 | +func ClearGitLabURL() { |
| 48 | + gitlabURLMutex.Lock() |
| 49 | + defer gitlabURLMutex.Unlock() |
| 50 | + gitlabURL = "" |
| 51 | +} |
| 52 | + |
| 53 | +type GitLabURLDetector struct { |
| 54 | + patterns []gitlabPattern |
| 55 | + verificationCache sync.Map |
| 56 | +} |
| 57 | + |
| 58 | +func NewGitLabURLDetector() *GitLabURLDetector { |
| 59 | + patterns := []gitlabPattern{ |
| 60 | + {name: "Gitlab - Personal Access Token v2", regex: regexp.MustCompile(`glpat-[a-zA-Z0-9\-=_]{20,22}`), strategy: verifyUserAPI}, |
| 61 | + {name: "Gitlab - Personal Access Token v3", regex: regexp.MustCompile(`\b(glpat-[a-zA-Z0-9\-=_]{27,300}.[0-9a-z]{2}.[a-z0-9]{9})\b`), strategy: verifyUserAPI}, |
| 62 | + {name: "Gitlab - Pipeline Trigger Token", regex: regexp.MustCompile(`glptt-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 63 | + {name: "Gitlab - Runner Authentication Token", regex: regexp.MustCompile(`glrt-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyRunnerAPI}, |
| 64 | + {name: "Gitlab - Runner Registration Token", regex: regexp.MustCompile(`glrtr-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 65 | + {name: "Gitlab - Deploy Token", regex: regexp.MustCompile(`gldt-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 66 | + {name: "Gitlab - CI Build Token", regex: regexp.MustCompile(`glcbt-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 67 | + {name: "Gitlab - OAuth Application Secret", regex: regexp.MustCompile(`gloas-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 68 | + {name: "Gitlab - SCIM/OAuth Access Token", regex: regexp.MustCompile(`glsoat-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyUserAPI}, |
| 69 | + {name: "Gitlab - Feed Token", regex: regexp.MustCompile(`glft-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 70 | + {name: "Gitlab - Incoming Mail Token", regex: regexp.MustCompile(`glimt-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 71 | + {name: "Gitlab - Feature Flags Client Token", regex: regexp.MustCompile(`glffct-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 72 | + {name: "Gitlab - Agent for Kubernetes Token", regex: regexp.MustCompile(`glagent-[a-zA-Z0-9\-=_]{20,}`), strategy: verifyNone}, |
| 73 | + {name: "Gitlab - Runner Token (Legacy)", regex: regexp.MustCompile(`GR1348941[a-zA-Z0-9\-=_]{20,}`), strategy: verifyRunnerAPI}, |
| 74 | + } |
| 75 | + |
| 76 | + return &GitLabURLDetector{patterns: patterns} |
| 77 | +} |
| 78 | + |
| 79 | +func GetGitLabURLDetector() *GitLabURLDetector { |
| 80 | + detectorOnce.Do(func() { |
| 81 | + detector = NewGitLabURLDetector() |
| 82 | + }) |
| 83 | + return detector |
| 84 | +} |
| 85 | + |
| 86 | +func (d *GitLabURLDetector) FromData(ctx context.Context, verify bool, data []byte) ([]detectors.Result, error) { |
| 87 | + var results []detectors.Result |
| 88 | + url := GetGitLabURL() |
| 89 | + |
| 90 | + for _, pattern := range d.patterns { |
| 91 | + if err := ctx.Err(); err != nil { |
| 92 | + return results, err |
| 93 | + } |
| 94 | + |
| 95 | + matches := pattern.regex.FindAll(data, -1) |
| 96 | + seenMatches := make(map[string]struct{}, len(matches)) |
| 97 | + for _, matchBytes := range matches { |
| 98 | + if err := ctx.Err(); err != nil { |
| 99 | + return results, err |
| 100 | + } |
| 101 | + |
| 102 | + match := string(matchBytes) |
| 103 | + if _, seen := seenMatches[match]; seen { |
| 104 | + continue |
| 105 | + } |
| 106 | + seenMatches[match] = struct{}{} |
| 107 | + |
| 108 | + result := detectors.Result{ |
| 109 | + DetectorName: pattern.name, |
| 110 | + Raw: append([]byte(nil), matchBytes...), |
| 111 | + Verified: false, |
| 112 | + } |
| 113 | + |
| 114 | + if verify && url != "" && pattern.strategy != verifyNone { |
| 115 | + if d.verifyTokenAgainstURL(ctx, match, url, pattern.name, pattern.strategy) { |
| 116 | + result.Verified = true |
| 117 | + } else { |
| 118 | + continue |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + results = append(results, result) |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + return results, nil |
| 127 | +} |
| 128 | + |
| 129 | +func (d *GitLabURLDetector) verifyTokenAgainstURL(ctx context.Context, token string, gitlabURL string, tokenName string, strategy verificationStrategy) bool { |
| 130 | + if err := ctx.Err(); err != nil { |
| 131 | + return false |
| 132 | + } |
| 133 | + |
| 134 | + cacheKey := string(rune(strategy)) + "|" + gitlabURL + "|" + token |
| 135 | + if cached, ok := d.verificationCache.Load(cacheKey); ok { |
| 136 | + return cached.(bool) |
| 137 | + } |
| 138 | + |
| 139 | + client, err := util.GetGitlabClient(token, gitlabURL) |
| 140 | + if err != nil { |
| 141 | + log.Debug().Err(err).Str("url", gitlabURL).Str("token_type", tokenName).Msg("Failed to create GitLab client for token verification") |
| 142 | + d.verificationCache.Store(cacheKey, false) |
| 143 | + return false |
| 144 | + } |
| 145 | + |
| 146 | + switch strategy { |
| 147 | + case verifyUserAPI: |
| 148 | + _, _, err = client.Users.CurrentUser(gitlab.WithContext(ctx)) |
| 149 | + case verifyRunnerAPI: |
| 150 | + _, err = client.Runners.VerifyRegisteredRunner(&gitlab.VerifyRegisteredRunnerOptions{Token: gitlab.Ptr(token)}, gitlab.WithContext(ctx)) |
| 151 | + default: |
| 152 | + d.verificationCache.Store(cacheKey, false) |
| 153 | + return false |
| 154 | + } |
| 155 | + if err != nil { |
| 156 | + log.Debug().Err(err).Str("url", gitlabURL).Str("token_type", tokenName).Msg("Token verification failed against GitLab instance") |
| 157 | + d.verificationCache.Store(cacheKey, false) |
| 158 | + return false |
| 159 | + } |
| 160 | + |
| 161 | + log.Debug().Str("url", gitlabURL).Str("token_type", tokenName).Msg("Token verified successfully against GitLab instance") |
| 162 | + d.verificationCache.Store(cacheKey, true) |
| 163 | + return true |
| 164 | +} |
| 165 | + |
| 166 | +func (d *GitLabURLDetector) Keywords() []string { |
| 167 | + return []string{ |
| 168 | + "glpat-", |
| 169 | + "glptt-", |
| 170 | + "gldt-", |
| 171 | + "glrt-", |
| 172 | + "glrtr-", |
| 173 | + "glcbt-", |
| 174 | + "gloas-", |
| 175 | + "glsoat-", |
| 176 | + "glft-", |
| 177 | + "glimt-", |
| 178 | + "glffct-", |
| 179 | + "glagent-", |
| 180 | + "GR1348941", |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +func (d *GitLabURLDetector) Type() string { |
| 185 | + return "GitLab" |
| 186 | +} |
| 187 | + |
| 188 | +func (d *GitLabURLDetector) Description() string { |
| 189 | + return "GitLab Token Detector with Self-Hosted Instance Verification" |
| 190 | +} |
0 commit comments