Skip to content

Commit 98b4874

Browse files
authored
feat(gitlab): verify self-hosted tokens in scanner (#622)
* feat(gitlab): verify self-hosted tokens in scanner
1 parent 9d9fff0 commit 98b4874

7 files changed

Lines changed: 598 additions & 2 deletions

File tree

docs/introduction/secrets_verification.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ patterns:
7979
8080
You can create additional custom rules.
8181
82-
83-
8482
> **💡Tip:** Test your regexes at [regex101.com](https://regex101.com/) (select Golang flavor).
8583
8684
A simple example that detects strings that follow the Regex pattern `PIPELEEK_.*` and that are logged with a custom confidence:

internal/cmd/gitlab/scan/scan.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/CompassSecurity/pipeleek/pkg/config"
66
"github.com/CompassSecurity/pipeleek/pkg/gitlab/scan"
77
"github.com/CompassSecurity/pipeleek/pkg/logging"
8+
"github.com/CompassSecurity/pipeleek/pkg/scanner/detectors"
89
"github.com/rs/zerolog"
910
"github.com/rs/zerolog/log"
1011
"github.com/spf13/cobra"
@@ -112,6 +113,8 @@ func Scan(cmd *cobra.Command, args []string) {
112113
log.Fatal().Err(err).Msg("Invalid thread count")
113114
}
114115

116+
detectors.SetGitLabURL(gitlabUrl)
117+
115118
scanOpts, err := scan.InitializeOptions(
116119
gitlabUrl,
117120
gitlabApiToken,

internal/cmd/gitlab/scanpublic/scan_public.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/CompassSecurity/pipeleek/pkg/config"
99
gitlabscan "github.com/CompassSecurity/pipeleek/pkg/gitlab/scan"
1010
"github.com/CompassSecurity/pipeleek/pkg/logging"
11+
"github.com/CompassSecurity/pipeleek/pkg/scanner/detectors"
1112
"github.com/rs/zerolog"
1213
"github.com/rs/zerolog/log"
1314
"github.com/spf13/cobra"
@@ -112,6 +113,8 @@ func ScanPublic(cmd *cobra.Command, args []string) {
112113
log.Fatal().Err(err).Msg("Invalid thread count")
113114
}
114115

116+
detectors.SetGitLabURL(gitlabURL)
117+
115118
scanOpts, err := gitlabscan.InitializeOptions(
116119
gitlabURL,
117120
"",
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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

Comments
 (0)