Skip to content

Commit bcf96da

Browse files
authored
Merge pull request #782 from pafmaf/plugin/gitlab_tokens
introducing GitLab Plugin analogous to GitHubTokenDetector
2 parents cd77447 + 1a0fd30 commit bcf96da

File tree

3 files changed

+198
-0
lines changed

3 files changed

+198
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ BasicAuthDetector
9898
CloudantDetector
9999
DiscordBotTokenDetector
100100
GitHubTokenDetector
101+
GitLabTokenDetector
101102
Base64HighEntropyString
102103
HexHighEntropyString
103104
IbmCloudIamDetector
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
This plugin searches for GitLab tokens
3+
"""
4+
import re
5+
6+
from detect_secrets.plugins.base import RegexBasedDetector
7+
8+
9+
class GitLabTokenDetector(RegexBasedDetector):
10+
"""Scans for GitLab tokens."""
11+
12+
secret_type = 'GitLab Token'
13+
14+
denylist = [
15+
# ref:
16+
# - https://docs.gitlab.com/ee/security/token_overview.html#gitlab-tokens
17+
# - https://gitlab.com/groups/gitlab-org/-/epics/8923
18+
# - https://github.com/gitlabhq/gitlabhq/blob/master/gems
19+
# /gitlab-secret_detection/lib/gitleaks.toml#L6-L76
20+
21+
# `gl..-` prefix and a token of length >20
22+
# characters are typically alphanumeric, underscore, dash
23+
# Most tokens are generated either with:
24+
# - `Devise.friendly_token`, a string with a default length of 20, or
25+
# - `SecureRandom.hex`, default data size of 16 bytes, encoded in different ways.
26+
# String length may vary depending on the type of token, and probably
27+
# even GL-settings in the future, so we expect between 20 and 50 chars.
28+
29+
# Personal Access Token - glpat
30+
# Deploy Token - gldt
31+
# Feed Token - glft
32+
# OAuth Access Token - glsoat
33+
# Runner Token - glrt
34+
re.compile(
35+
r'(glpat|gldt|glft|glsoat|glrt)-'
36+
r'[A-Za-z0-9_\-]{20,50}(?!\w)',
37+
),
38+
39+
# Runner Registration Token
40+
re.compile(r'GR1348941[A-Za-z0-9_\-]{20,50}(?!\w)'),
41+
42+
# CI/CD Token - `glcbt` or `glcbt-XY_` where XY is a 2-char hex 'partition_id'
43+
re.compile(r'glcbt-([0-9a-fA-F]{2}_)?[A-Za-z0-9_\-]{20,50}(?!\w)'),
44+
45+
# Incoming Mail Token - generated by SecureRandom.hex, default length 16 bytes
46+
# resulting token length is 26 when Base-36 encoded
47+
re.compile(r'glimt-[A-Za-z0-9_\-]{25}(?!\w)'),
48+
49+
# Trigger Token - generated by `SecureRandom.hex(20)`
50+
re.compile(r'glptt-[A-Za-z0-9_\-]{40}(?!\w)'),
51+
52+
# Agent Token - generated by `Devise.friendly_token(50)`
53+
# tokens have a minimum length of 50 chars, up to 1024 chars
54+
re.compile(r'glagent-[A-Za-z0-9_\-]{50,1024}(?!\w)'),
55+
56+
# GitLab OAuth Application Secret - generated by `SecureRandom.hex(32)`
57+
# -> becomes 64 base64-encoded characters
58+
re.compile(r'gloas-[A-Za-z0-9_\-]{64}(?!\w)'),
59+
]

tests/plugins/gitlab_token_test.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import pytest
2+
3+
from detect_secrets.plugins.gitlab_token import GitLabTokenDetector
4+
5+
6+
class TestGitLabTokenDetector:
7+
@pytest.mark.parametrize(
8+
'payload, should_flag',
9+
[
10+
(
11+
# valid PAT prefix and token length
12+
'glpat-hellOworld380_testin',
13+
True,
14+
),
15+
(
16+
# spaces are not part of the token
17+
'glpat-hellOWorld380 testin',
18+
False,
19+
),
20+
(
21+
# invalid separator (underscore VS dash)
22+
'glpat_hellOworld380_testin',
23+
False,
24+
),
25+
(
26+
# valid different prefix and token length
27+
'gldt-HwllOuhfw-wu0rlD_yep',
28+
True,
29+
),
30+
(
31+
# token < 20 chars should be too short
32+
'gldt-seems_too000Sshorty',
33+
False,
34+
),
35+
(
36+
# invalid prefix, but valid token length
37+
'foo-hello-world80_testin',
38+
False,
39+
),
40+
(
41+
# token length may vary depending on the impl., but <= 50 chars should be fine
42+
'glsoat-PREfix_helloworld380_testin_pretty_long_token_long',
43+
True,
44+
),
45+
(
46+
# token > 50 chars is too long
47+
'glsoat-PREfix_helloworld380_testin_pretty_long_token_long_',
48+
False,
49+
),
50+
(
51+
# GitLab is not GitHub
52+
'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',
53+
False,
54+
),
55+
],
56+
)
57+
def test_base_token_format(self, payload, should_flag):
58+
logic = GitLabTokenDetector()
59+
output = logic.analyze_line(filename='mock_filename', line=payload)
60+
assert len(output) == int(should_flag)
61+
62+
@pytest.mark.parametrize(
63+
'payload, should_flag',
64+
[
65+
('GR1348941PREfix_helloworld380', True),
66+
('GR1348941PREfix_helloworld380_testin_pretty_long_token_long', True),
67+
('GR1348941PREfix_helloworld380_testin_pretty_long_token_long_', False), # too long
68+
('GR1348941helloWord0', False), # too short
69+
],
70+
)
71+
def test_runner_registration_token(self, payload, should_flag):
72+
logic = GitLabTokenDetector()
73+
output = logic.analyze_line(filename='mock_filename', line=payload)
74+
assert len(output) == int(should_flag)
75+
76+
@pytest.mark.parametrize(
77+
'payload, should_flag',
78+
[
79+
('glcbt-helloworld380_testin', True),
80+
],
81+
)
82+
def test_cicd_token(self, payload, should_flag):
83+
logic = GitLabTokenDetector()
84+
output = logic.analyze_line(filename='mock_filename', line=payload)
85+
assert len(output) == int(should_flag)
86+
87+
@pytest.mark.parametrize(
88+
'payload, should_flag',
89+
[
90+
('glimt-my-tokens_are-correctAB38', True),
91+
('glimt-my-tokens_are-correctAB', False), # too short
92+
('glimt-my-tokens_are-correctAB38_280', False), # too long
93+
],
94+
)
95+
def test_incoming_mail_token(self, payload, should_flag):
96+
logic = GitLabTokenDetector()
97+
output = logic.analyze_line(filename='mock_filename', line=payload)
98+
assert len(output) == int(should_flag)
99+
100+
@pytest.mark.parametrize(
101+
'payload, should_flag',
102+
[
103+
('glptt-Need5_T00-be-exactly-40-chars--ELse_fail', True),
104+
('glptt-Need5_T00-be-exactly-40-chars--ELse_failing', False), # too long
105+
('glptt-hellOworld380_testin', False), # too short
106+
],
107+
)
108+
def test_trigger_token(self, payload, should_flag):
109+
logic = GitLabTokenDetector()
110+
output = logic.analyze_line(filename='mock_filename', line=payload)
111+
assert len(output) == int(should_flag)
112+
113+
@pytest.mark.parametrize(
114+
'payload, should_flag',
115+
[
116+
('glagent-Need5_T00-bee-longer-than-50_chars-or-else-failING', True),
117+
('glagent-Need5_T00-bee-longer-than-50_chars-or-else-failING-still_OK', True),
118+
(('glagent-' + 'X' * 1025), False), # 2 long
119+
('glagent-hellOworld380_testin', False), # len 20 is too short
120+
],
121+
)
122+
def test_agent_token(self, payload, should_flag):
123+
logic = GitLabTokenDetector()
124+
output = logic.analyze_line(filename='mock_filename', line=payload)
125+
assert len(output) == int(should_flag)
126+
127+
@pytest.mark.parametrize(
128+
'payload, should_flag',
129+
[
130+
('gloas-checking_Length-Is-_exactly_64--checking_Length-Is-_exactly_64--', True),
131+
('gloas-checking_Length-Is-checking_Length-Is-', False), # too short
132+
('gloas-checking_Length-Is-_exactly_64--Xchecking_Length-Is-_longer_longer', False),
133+
],
134+
)
135+
def test_oauth_application_secret(self, payload, should_flag):
136+
logic = GitLabTokenDetector()
137+
output = logic.analyze_line(filename='mock_filename', line=payload)
138+
assert len(output) == int(should_flag)

0 commit comments

Comments
 (0)