socketsecurity 2.2.0__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/PKG-INFO +75 -1
  2. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/README.md +74 -0
  3. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/pyproject.toml +1 -1
  4. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/__init__.py +1 -1
  5. socketsecurity-2.2.2/socketsecurity/core/scm/client.py +84 -0
  6. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/gitlab.py +112 -8
  7. socketsecurity-2.2.2/tests/unit/test_gitlab_auth.py +116 -0
  8. socketsecurity-2.2.2/tests/unit/test_gitlab_auth_fallback.py +148 -0
  9. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/workflows/gitlab-ci.yml +3 -0
  10. socketsecurity-2.2.0/socketsecurity/core/scm/client.py +0 -41
  11. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/CODEOWNERS +0 -0
  12. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  13. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  14. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  15. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/docker-stable.yml +0 -0
  17. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/pr-preview.yml +0 -0
  18. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/release.yml +0 -0
  19. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/version-check.yml +0 -0
  20. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.gitignore +0 -0
  21. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.hooks/sync_version.py +0 -0
  22. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.pre-commit-config.yaml +0 -0
  23. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.python-version +0 -0
  24. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/Dockerfile +0 -0
  25. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/LICENSE +0 -0
  26. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/Makefile +0 -0
  27. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/Pipfile.lock +0 -0
  28. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/docs/README.md +0 -0
  29. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/pytest.ini +0 -0
  30. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements-dev.lock +0 -0
  31. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements-dev.txt +0 -0
  32. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements.lock +0 -0
  33. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements.txt +0 -0
  34. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/build_container.sh +0 -0
  35. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/deploy-test-docker.sh +0 -0
  36. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/deploy-test-pypi.sh +0 -0
  37. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/run.sh +0 -0
  38. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/config.py +0 -0
  39. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/__init__.py +0 -0
  40. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/classes.py +0 -0
  41. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/cli_client.py +0 -0
  42. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/exceptions.py +0 -0
  43. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/git_interface.py +0 -0
  44. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/helper/__init__.py +0 -0
  45. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/lazy_file_loader.py +0 -0
  46. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/logging.py +0 -0
  47. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/messages.py +0 -0
  48. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/resource_utils.py +0 -0
  49. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/__init__.py +0 -0
  50. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/base.py +0 -0
  51. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/github.py +0 -0
  52. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm_comments.py +0 -0
  53. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/socket_config.py +0 -0
  54. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/utils.py +0 -0
  55. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/output.py +0 -0
  56. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/__init__.py +0 -0
  57. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/base.py +0 -0
  58. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/jira.py +0 -0
  59. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/manager.py +0 -0
  60. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/slack.py +0 -0
  61. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/teams.py +0 -0
  62. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/webhook.py +0 -0
  63. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/socketcli.py +0 -0
  64. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/__init__.py +0 -0
  65. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/conftest.py +0 -0
  66. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/create_diff_input.json +0 -0
  67. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_diff_generation.py +0 -0
  68. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_package_and_alerts.py +0 -0
  69. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_sdk_methods.py +0 -0
  70. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_supporting_methods.py +0 -0
  71. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/create_response.json +0 -0
  72. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/diff/stream_diff.json +0 -0
  73. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  74. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/metadata.json +0 -0
  75. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  76. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  77. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/new_scan/metadata.json +0 -0
  78. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  79. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/repos/repo_info_error.json +0 -0
  80. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/repos/repo_info_no_head.json +0 -0
  81. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/repos/repo_info_success.json +0 -0
  82. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/settings/security-policy.json +0 -0
  83. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/__init__.py +0 -0
  84. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_cli_config.py +0 -0
  85. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_client.py +0 -0
  86. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_config.py +0 -0
  87. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_output.py +0 -0
  88. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/workflows/bitbucket-pipelines.yml +0 -0
  89. {socketsecurity-2.2.0 → socketsecurity-2.2.2}/workflows/github-actions.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketsecurity
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Socket Security CLI for CI/CD
5
5
  Project-URL: Homepage, https://socket.dev
6
6
  Author-email: Douglas Coburn <douglas@socket.dev>
@@ -235,6 +235,74 @@ The CLI uses intelligent default branch detection with the following priority:
235
235
 
236
236
  Both `--default-branch` and `--pending-head` parameters are automatically synchronized to ensure consistent behavior.
237
237
 
238
+ ## GitLab Token Configuration
239
+
240
+ The CLI supports GitLab integration with automatic authentication pattern detection for different token types.
241
+
242
+ ### Supported Token Types
243
+
244
+ GitLab API supports two authentication methods, and the CLI automatically detects which one to use:
245
+
246
+ 1. **Bearer Token Authentication** (`Authorization: Bearer <token>`)
247
+ - GitLab CI Job Tokens (`$CI_JOB_TOKEN`)
248
+ - Personal Access Tokens with `glpat-` prefix
249
+ - OAuth 2.0 tokens (long alphanumeric tokens)
250
+
251
+ 2. **Private Token Authentication** (`PRIVATE-TOKEN: <token>`)
252
+ - Legacy personal access tokens
253
+ - Custom tokens that don't match Bearer patterns
254
+
255
+ ### Token Detection Logic
256
+
257
+ The CLI automatically determines the authentication method using this logic:
258
+
259
+ ```
260
+ if token == $CI_JOB_TOKEN:
261
+ use Bearer authentication
262
+ elif token starts with "glpat-":
263
+ use Bearer authentication
264
+ elif token is long (>40 chars) and alphanumeric:
265
+ use Bearer authentication
266
+ else:
267
+ use PRIVATE-TOKEN authentication
268
+ ```
269
+
270
+ ### Automatic Fallback
271
+
272
+ If the initial authentication method fails with a 401 error, the CLI automatically retries with the alternative method:
273
+
274
+ - **Bearer → PRIVATE-TOKEN**: If Bearer authentication fails, retry with PRIVATE-TOKEN
275
+ - **PRIVATE-TOKEN → Bearer**: If PRIVATE-TOKEN fails, retry with Bearer authentication
276
+
277
+ This ensures maximum compatibility across different GitLab configurations and token types.
278
+
279
+ ### Environment Variables
280
+
281
+ | Variable | Description | Example |
282
+ |:---------|:------------|:--------|
283
+ | `GITLAB_TOKEN` | GitLab API token (required for GitLab integration) | `glpat-xxxxxxxxxxxxxxxxxxxx` |
284
+ | `CI_JOB_TOKEN` | GitLab CI job token (automatically used in GitLab CI) | Automatically provided by GitLab CI |
285
+
286
+ ### Usage Examples
287
+
288
+ **GitLab CI with job token (recommended):**
289
+ ```yaml
290
+ variables:
291
+ GITLAB_TOKEN: $CI_JOB_TOKEN
292
+ ```
293
+
294
+ **GitLab CI with personal access token:**
295
+ ```yaml
296
+ variables:
297
+ GITLAB_TOKEN: $GITLAB_PERSONAL_ACCESS_TOKEN # Set in GitLab project/group variables
298
+ ```
299
+
300
+ **Local development:**
301
+ ```bash
302
+ export GITLAB_TOKEN="glpat-your-personal-access-token"
303
+ socketcli --integration gitlab --repo owner/repo --pr-number 123
304
+ ```
305
+
238
306
  ### Scan Behavior
239
307
 
240
308
  The CLI determines scanning behavior intelligently:
@@ -396,4 +464,10 @@ Implementation targets:
396
464
 
397
465
  ### Environment Variables
398
466
 
467
+ #### Core Configuration
468
+ - `SOCKET_SECURITY_API_KEY`: Socket Security API token (alternative to --api-token parameter)
399
469
  - `SOCKET_SDK_PATH`: Path to local socket-sdk-python repository (default: ../socket-sdk-python)
470
+
471
+ #### GitLab Integration
472
+ - `GITLAB_TOKEN`: GitLab API token for GitLab integration (supports both Bearer and PRIVATE-TOKEN authentication)
473
+ - `CI_JOB_TOKEN`: GitLab CI job token (automatically provided in GitLab CI environments)
@@ -179,6 +179,74 @@ The CLI uses intelligent default branch detection with the following priority:
179
179
 
180
180
  Both `--default-branch` and `--pending-head` parameters are automatically synchronized to ensure consistent behavior.
181
181
 
182
+ ## GitLab Token Configuration
183
+
184
+ The CLI supports GitLab integration with automatic authentication pattern detection for different token types.
185
+
186
+ ### Supported Token Types
187
+
188
+ GitLab API supports two authentication methods, and the CLI automatically detects which one to use:
189
+
190
+ 1. **Bearer Token Authentication** (`Authorization: Bearer <token>`)
191
+ - GitLab CI Job Tokens (`$CI_JOB_TOKEN`)
192
+ - Personal Access Tokens with `glpat-` prefix
193
+ - OAuth 2.0 tokens (long alphanumeric tokens)
194
+
195
+ 2. **Private Token Authentication** (`PRIVATE-TOKEN: <token>`)
196
+ - Legacy personal access tokens
197
+ - Custom tokens that don't match Bearer patterns
198
+
199
+ ### Token Detection Logic
200
+
201
+ The CLI automatically determines the authentication method using this logic:
202
+
203
+ ```
204
+ if token == $CI_JOB_TOKEN:
205
+ use Bearer authentication
206
+ elif token starts with "glpat-":
207
+ use Bearer authentication
208
+ elif token is long (>40 chars) and alphanumeric:
209
+ use Bearer authentication
210
+ else:
211
+ use PRIVATE-TOKEN authentication
212
+ ```
213
+
214
+ ### Automatic Fallback
215
+
216
+ If the initial authentication method fails with a 401 error, the CLI automatically retries with the alternative method:
217
+
218
+ - **Bearer → PRIVATE-TOKEN**: If Bearer authentication fails, retry with PRIVATE-TOKEN
219
+ - **PRIVATE-TOKEN → Bearer**: If PRIVATE-TOKEN fails, retry with Bearer authentication
220
+
221
+ This ensures maximum compatibility across different GitLab configurations and token types.
222
+
223
+ ### Environment Variables
224
+
225
+ | Variable | Description | Example |
226
+ |:---------|:------------|:--------|
227
+ | `GITLAB_TOKEN` | GitLab API token (required for GitLab integration) | `glpat-xxxxxxxxxxxxxxxxxxxx` |
228
+ | `CI_JOB_TOKEN` | GitLab CI job token (automatically used in GitLab CI) | Automatically provided by GitLab CI |
229
+
230
+ ### Usage Examples
231
+
232
+ **GitLab CI with job token (recommended):**
233
+ ```yaml
234
+ variables:
235
+ GITLAB_TOKEN: $CI_JOB_TOKEN
236
+ ```
237
+
238
+ **GitLab CI with personal access token:**
239
+ ```yaml
240
+ variables:
241
+ GITLAB_TOKEN: $GITLAB_PERSONAL_ACCESS_TOKEN # Set in GitLab project/group variables
242
+ ```
243
+
244
+ **Local development:**
245
+ ```bash
246
+ export GITLAB_TOKEN="glpat-your-personal-access-token"
247
+ socketcli --integration gitlab --repo owner/repo --pr-number 123
248
+ ```
249
+
182
250
  ### Scan Behavior
183
251
 
184
252
  The CLI determines scanning behavior intelligently:
@@ -340,4 +408,10 @@ Implementation targets:
340
408
 
341
409
  ### Environment Variables
342
410
 
411
+ #### Core Configuration
412
+ - `SOCKET_SECURITY_API_KEY`: Socket Security API token (alternative to --api-token parameter)
343
413
  - `SOCKET_SDK_PATH`: Path to local socket-sdk-python repository (default: ../socket-sdk-python)
414
+
415
+ #### GitLab Integration
416
+ - `GITLAB_TOKEN`: GitLab API token for GitLab integration (supports both Bearer and PRIVATE-TOKEN authentication)
417
+ - `CI_JOB_TOKEN`: GitLab CI job token (automatically provided in GitLab CI environments)
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.2.0"
9
+ version = "2.2.2"
10
10
  requires-python = ">= 3.10"
11
11
  license = {"file" = "LICENSE"}
12
12
  dependencies = [
@@ -1,2 +1,2 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.2.0'
2
+ __version__ = '2.2.2'
@@ -0,0 +1,84 @@
1
+ from abc import abstractmethod
2
+ from typing import Dict
3
+
4
+ from ..cli_client import CliClient
5
+
6
+
7
+ class ScmClient(CliClient):
8
+ def __init__(self, token: str, api_url: str):
9
+ self.token = token
10
+ self.api_url = api_url
11
+
12
+ @abstractmethod
13
+ def get_headers(self) -> Dict:
14
+ """Each SCM implements its own auth headers"""
15
+ pass
16
+
17
+ def request(self, path: str, **kwargs):
18
+ """Override base request to use SCM-specific headers and base_url"""
19
+ headers = kwargs.pop('headers', None) or self.get_headers()
20
+ return super().request(
21
+ path=path,
22
+ headers=headers,
23
+ base_url=self.api_url,
24
+ **kwargs
25
+ )
26
+
27
+ class GithubClient(ScmClient):
28
+ def get_headers(self) -> Dict:
29
+ return {
30
+ 'Authorization': f"Bearer {self.token}",
31
+ 'User-Agent': 'SocketPythonScript/0.0.1',
32
+ "accept": "application/json"
33
+ }
34
+
35
+ class GitlabClient(ScmClient):
36
+ def get_headers(self) -> Dict:
37
+ """
38
+ Determine the appropriate authentication headers for GitLab API.
39
+ Uses the same logic as GitlabConfig._get_auth_headers()
40
+ """
41
+ return self._get_gitlab_auth_headers(self.token)
42
+
43
+ @staticmethod
44
+ def _get_gitlab_auth_headers(token: str) -> dict:
45
+ """
46
+ Determine the appropriate authentication headers for GitLab API.
47
+
48
+ GitLab supports two authentication patterns:
49
+ 1. Bearer token (OAuth 2.0 tokens, personal access tokens with api scope)
50
+ 2. Private token (personal access tokens)
51
+ """
52
+ import os
53
+
54
+ base_headers = {
55
+ 'User-Agent': 'SocketPythonScript/0.0.1',
56
+ "accept": "application/json"
57
+ }
58
+
59
+ # Check if this is a GitLab CI job token
60
+ if token == os.getenv('CI_JOB_TOKEN'):
61
+ return {
62
+ **base_headers,
63
+ 'Authorization': f"Bearer {token}"
64
+ }
65
+
66
+ # Check for personal access token pattern
67
+ if token.startswith('glpat-'):
68
+ return {
69
+ **base_headers,
70
+ 'Authorization': f"Bearer {token}"
71
+ }
72
+
73
+ # Check for OAuth token pattern (typically longer and alphanumeric)
74
+ if len(token) > 40 and token.isalnum():
75
+ return {
76
+ **base_headers,
77
+ 'Authorization': f"Bearer {token}"
78
+ }
79
+
80
+ # Default to PRIVATE-TOKEN for other token types
81
+ return {
82
+ **base_headers,
83
+ 'PRIVATE-TOKEN': f"{token}"
84
+ }
@@ -42,6 +42,9 @@ class GitlabConfig:
42
42
  mr_source_branch = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME')
43
43
  default_branch = os.getenv('CI_DEFAULT_BRANCH', '')
44
44
 
45
+ # Determine which authentication pattern to use
46
+ headers = cls._get_auth_headers(token)
47
+
45
48
  return cls(
46
49
  commit_sha=os.getenv('CI_COMMIT_SHA', ''),
47
50
  api_url=os.getenv('CI_API_V4_URL', ''),
@@ -57,18 +60,119 @@ class GitlabConfig:
57
60
  token=token,
58
61
  repository=project_name,
59
62
  is_default_branch=(mr_source_branch == default_branch if mr_source_branch else False),
60
- headers={
61
- 'Authorization': f"Bearer {token}",
62
- 'User-Agent': 'SocketPythonScript/0.0.1',
63
- "accept": "application/json"
64
- }
63
+ headers=headers
65
64
  )
66
65
 
66
+ @staticmethod
67
+ def _get_auth_headers(token: str) -> dict:
68
+ """
69
+ Determine the appropriate authentication headers for GitLab API.
70
+
71
+ GitLab supports two authentication patterns:
72
+ 1. Bearer token (OAuth 2.0 tokens, personal access tokens with api scope)
73
+ 2. Private token (personal access tokens)
74
+
75
+ Logic for token type determination:
76
+ - CI_JOB_TOKEN: Always use Bearer (GitLab CI job token)
77
+ - Tokens starting with 'glpat-': Personal access tokens, try Bearer first
78
+ - OAuth tokens: Use Bearer
79
+ - Other tokens: Use PRIVATE-TOKEN as fallback
80
+ """
81
+ base_headers = {
82
+ 'User-Agent': 'SocketPythonScript/0.0.1',
83
+ "accept": "application/json"
84
+ }
85
+
86
+ # Check if this is a GitLab CI job token
87
+ if token == os.getenv('CI_JOB_TOKEN'):
88
+ log.debug("Using Bearer authentication for GitLab CI job token")
89
+ return {
90
+ **base_headers,
91
+ 'Authorization': f"Bearer {token}"
92
+ }
93
+
94
+ # Check for personal access token pattern
95
+ if token.startswith('glpat-'):
96
+ log.debug("Using Bearer authentication for GitLab personal access token")
97
+ return {
98
+ **base_headers,
99
+ 'Authorization': f"Bearer {token}"
100
+ }
101
+
102
+ # Check for OAuth token pattern (typically longer and alphanumeric)
103
+ if len(token) > 40 and token.isalnum():
104
+ log.debug("Using Bearer authentication for potential OAuth token")
105
+ return {
106
+ **base_headers,
107
+ 'Authorization': f"Bearer {token}"
108
+ }
109
+
110
+ # Default to PRIVATE-TOKEN for other token types
111
+ log.debug("Using PRIVATE-TOKEN authentication for GitLab token")
112
+ return {
113
+ **base_headers,
114
+ 'PRIVATE-TOKEN': f"{token}"
115
+ }
116
+
67
117
  class Gitlab:
68
118
  def __init__(self, client: CliClient, config: Optional[GitlabConfig] = None):
69
119
  self.config = config or GitlabConfig.from_env()
70
120
  self.client = client
71
121
 
122
+ def _request_with_fallback(self, **kwargs):
123
+ """
124
+ Make a request with automatic fallback between Bearer and PRIVATE-TOKEN authentication.
125
+ This provides robustness when the initial token type detection is incorrect.
126
+ """
127
+ try:
128
+ # Try the initial request with the configured headers
129
+ return self.client.request(**kwargs)
130
+ except Exception as e:
131
+ # Check if this is an authentication error (401)
132
+ if hasattr(e, 'response') and e.response and e.response.status_code == 401:
133
+ log.debug(f"Authentication failed with initial headers, trying fallback method")
134
+
135
+ # Determine the fallback headers
136
+ original_headers = kwargs.get('headers', self.config.headers)
137
+ fallback_headers = self._get_fallback_headers(original_headers)
138
+
139
+ if fallback_headers and fallback_headers != original_headers:
140
+ log.debug("Retrying request with fallback authentication method")
141
+ kwargs['headers'] = fallback_headers
142
+ return self.client.request(**kwargs)
143
+
144
+ # Re-raise the original exception if it's not an auth error or fallback failed
145
+ raise
146
+
147
+ def _get_fallback_headers(self, original_headers: dict) -> dict:
148
+ """
149
+ Generate fallback authentication headers.
150
+ If using Bearer, fallback to PRIVATE-TOKEN and vice versa.
151
+ """
152
+ base_headers = {
153
+ 'User-Agent': 'SocketPythonScript/0.0.1',
154
+ "accept": "application/json"
155
+ }
156
+
157
+ # If currently using Bearer, try PRIVATE-TOKEN
158
+ if 'Authorization' in original_headers and 'Bearer' in original_headers['Authorization']:
159
+ log.debug("Falling back from Bearer to PRIVATE-TOKEN authentication")
160
+ return {
161
+ **base_headers,
162
+ 'PRIVATE-TOKEN': f"{self.config.token}"
163
+ }
164
+
165
+ # If currently using PRIVATE-TOKEN, try Bearer
166
+ elif 'PRIVATE-TOKEN' in original_headers:
167
+ log.debug("Falling back from PRIVATE-TOKEN to Bearer authentication")
168
+ return {
169
+ **base_headers,
170
+ 'Authorization': f"Bearer {self.config.token}"
171
+ }
172
+
173
+ # No fallback available
174
+ return None
175
+
72
176
  def check_event_type(self) -> str:
73
177
  pipeline_source = self.config.pipeline_source.lower()
74
178
  if pipeline_source in ["web", 'merge_request_event', "push", "api"]:
@@ -84,7 +188,7 @@ class Gitlab:
84
188
  def post_comment(self, body: str) -> None:
85
189
  path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes"
86
190
  payload = {"body": body}
87
- self.client.request(
191
+ self._request_with_fallback(
88
192
  path=path,
89
193
  payload=payload,
90
194
  method="POST",
@@ -95,7 +199,7 @@ class Gitlab:
95
199
  def update_comment(self, body: str, comment_id: str) -> None:
96
200
  path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes/{comment_id}"
97
201
  payload = {"body": body}
98
- self.client.request(
202
+ self._request_with_fallback(
99
203
  path=path,
100
204
  payload=payload,
101
205
  method="PUT",
@@ -106,7 +210,7 @@ class Gitlab:
106
210
  def get_comments_for_pr(self) -> dict:
107
211
  log.debug(f"Getting Gitlab comments for Repo {self.config.repository} for PR {self.config.mr_iid}")
108
212
  path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes"
109
- response = self.client.request(
213
+ response = self._request_with_fallback(
110
214
  path=path,
111
215
  headers=self.config.headers,
112
216
  base_url=self.config.api_url
@@ -0,0 +1,116 @@
1
+ """Tests for GitLab authentication patterns"""
2
+ import os
3
+ import pytest
4
+ from unittest.mock import patch, MagicMock
5
+
6
+ from socketsecurity.core.scm.gitlab import GitlabConfig
7
+
8
+
9
+ class TestGitlabAuthHeaders:
10
+ """Test GitLab authentication header generation"""
11
+
12
+ def test_ci_job_token_uses_bearer(self):
13
+ """CI_JOB_TOKEN should always use Bearer authentication"""
14
+ with patch.dict(os.environ, {'CI_JOB_TOKEN': 'ci-job-token-123'}):
15
+ headers = GitlabConfig._get_auth_headers('ci-job-token-123')
16
+ assert 'Authorization' in headers
17
+ assert headers['Authorization'] == 'Bearer ci-job-token-123'
18
+ assert 'PRIVATE-TOKEN' not in headers
19
+
20
+ def test_personal_access_token_uses_bearer(self):
21
+ """Personal access tokens (glpat-*) should use Bearer authentication"""
22
+ token = 'glpat-xxxxxxxxxxxxxxxxxxxx'
23
+ headers = GitlabConfig._get_auth_headers(token)
24
+ assert 'Authorization' in headers
25
+ assert headers['Authorization'] == f'Bearer {token}'
26
+ assert 'PRIVATE-TOKEN' not in headers
27
+
28
+ def test_oauth_token_uses_bearer(self):
29
+ """Long alphanumeric tokens (OAuth) should use Bearer authentication"""
30
+ token = 'a' * 50 # 50 character alphanumeric token
31
+ headers = GitlabConfig._get_auth_headers(token)
32
+ assert 'Authorization' in headers
33
+ assert headers['Authorization'] == f'Bearer {token}'
34
+ assert 'PRIVATE-TOKEN' not in headers
35
+
36
+ def test_short_token_uses_private_token(self):
37
+ """Short tokens should use PRIVATE-TOKEN authentication"""
38
+ token = 'short-token-123'
39
+ headers = GitlabConfig._get_auth_headers(token)
40
+ assert 'PRIVATE-TOKEN' in headers
41
+ assert headers['PRIVATE-TOKEN'] == token
42
+ assert 'Authorization' not in headers
43
+
44
+ def test_mixed_alphanumeric_token_uses_private_token(self):
45
+ """Tokens with non-alphanumeric characters should use PRIVATE-TOKEN"""
46
+ token = 'token-with-dashes-and_underscores'
47
+ headers = GitlabConfig._get_auth_headers(token)
48
+ assert 'PRIVATE-TOKEN' in headers
49
+ assert headers['PRIVATE-TOKEN'] == token
50
+ assert 'Authorization' not in headers
51
+
52
+ def test_all_headers_include_base_headers(self):
53
+ """All authentication patterns should include base headers"""
54
+ test_tokens = [
55
+ 'glpat-xxxxxxxxxxxxxxxxxxxx', # Bearer
56
+ 'short-token' # PRIVATE-TOKEN
57
+ ]
58
+
59
+ for token in test_tokens:
60
+ headers = GitlabConfig._get_auth_headers(token)
61
+ assert headers['User-Agent'] == 'SocketPythonScript/0.0.1'
62
+ assert headers['accept'] == 'application/json'
63
+
64
+ @patch.dict(os.environ, {'CI_JOB_TOKEN': 'ci-token-123'})
65
+ def test_ci_job_token_detection_priority(self):
66
+ """CI_JOB_TOKEN should be detected even if token doesn't match CI_JOB_TOKEN value"""
67
+ # This tests the case where GITLAB_TOKEN != CI_JOB_TOKEN
68
+ headers = GitlabConfig._get_auth_headers('different-token')
69
+ # Should not use Bearer since token doesn't match CI_JOB_TOKEN
70
+ assert 'PRIVATE-TOKEN' in headers
71
+ assert headers['PRIVATE-TOKEN'] == 'different-token'
72
+
73
+
74
+ class TestGitlabConfigFromEnv:
75
+ """Test GitlabConfig.from_env() method"""
76
+
77
+ @patch.dict(os.environ, {
78
+ 'GITLAB_TOKEN': 'glpat-test-token',
79
+ 'CI_PROJECT_NAME': 'test-project',
80
+ 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4',
81
+ 'CI_COMMIT_SHA': 'abc123',
82
+ 'CI_PROJECT_DIR': '/builds/test',
83
+ 'CI_PIPELINE_SOURCE': 'merge_request_event'
84
+ })
85
+ def test_from_env_creates_config_with_correct_headers(self):
86
+ """from_env should create config with appropriate auth headers"""
87
+ config = GitlabConfig.from_env()
88
+
89
+ # Should use Bearer for glpat- token
90
+ assert 'Authorization' in config.headers
91
+ assert config.headers['Authorization'] == 'Bearer glpat-test-token'
92
+ assert 'PRIVATE-TOKEN' not in config.headers
93
+ assert config.token == 'glpat-test-token'
94
+
95
+ @patch.dict(os.environ, {
96
+ 'GITLAB_TOKEN': 'custom-token',
97
+ 'CI_PROJECT_NAME': 'test-project'
98
+ }, clear=True)
99
+ def test_from_env_with_private_token(self):
100
+ """from_env should use PRIVATE-TOKEN for non-standard tokens"""
101
+ config = GitlabConfig.from_env()
102
+
103
+ # Should use PRIVATE-TOKEN for custom token
104
+ assert 'PRIVATE-TOKEN' in config.headers
105
+ assert config.headers['PRIVATE-TOKEN'] == 'custom-token'
106
+ assert 'Authorization' not in config.headers
107
+
108
+ def test_from_env_missing_token_exits(self):
109
+ """from_env should exit when GITLAB_TOKEN is missing"""
110
+ with patch.dict(os.environ, {}, clear=True):
111
+ with pytest.raises(SystemExit):
112
+ GitlabConfig.from_env()
113
+
114
+
115
+ if __name__ == '__main__':
116
+ pytest.main([__file__])
@@ -0,0 +1,148 @@
1
+ """Integration test demonstrating GitLab authentication fallback"""
2
+ import os
3
+ from unittest.mock import patch, MagicMock
4
+ import pytest
5
+
6
+ from socketsecurity.core.scm.gitlab import Gitlab, GitlabConfig
7
+ from socketsecurity.socketcli import CliClient
8
+
9
+
10
+ class TestGitlabAuthFallback:
11
+ """Test GitLab authentication fallback mechanism"""
12
+
13
+ @patch.dict(os.environ, {
14
+ 'GITLAB_TOKEN': 'test-token',
15
+ 'CI_PROJECT_NAME': 'test-project',
16
+ 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4',
17
+ 'CI_MERGE_REQUEST_IID': '123',
18
+ 'CI_MERGE_REQUEST_PROJECT_ID': '456'
19
+ })
20
+ def test_fallback_from_private_token_to_bearer(self):
21
+ """Test fallback from PRIVATE-TOKEN to Bearer authentication"""
22
+ # Create a mock client that simulates auth failure then success
23
+ mock_client = MagicMock(spec=CliClient)
24
+
25
+ # First call (with PRIVATE-TOKEN) fails with 401
26
+ auth_error = Exception()
27
+ auth_error.response = MagicMock()
28
+ auth_error.response.status_code = 401
29
+
30
+ # Second call (with Bearer) succeeds
31
+ success_response = {'notes': []}
32
+
33
+ mock_client.request.side_effect = [auth_error, success_response]
34
+
35
+ # Create GitLab instance with mock client
36
+ gitlab = Gitlab(client=mock_client)
37
+
38
+ # This should trigger the fallback mechanism
39
+ result = gitlab.get_comments_for_pr()
40
+
41
+ # Verify two requests were made
42
+ assert mock_client.request.call_count == 2
43
+
44
+ # First call should use PRIVATE-TOKEN (default for 'test-token')
45
+ first_call_headers = mock_client.request.call_args_list[0][1]['headers']
46
+ assert 'PRIVATE-TOKEN' in first_call_headers
47
+ assert first_call_headers['PRIVATE-TOKEN'] == 'test-token'
48
+
49
+ # Second call should use Bearer (fallback)
50
+ second_call_headers = mock_client.request.call_args_list[1][1]['headers']
51
+ assert 'Authorization' in second_call_headers
52
+ assert second_call_headers['Authorization'] == 'Bearer test-token'
53
+
54
+ @patch.dict(os.environ, {
55
+ 'GITLAB_TOKEN': 'glpat-test-token',
56
+ 'CI_PROJECT_NAME': 'test-project',
57
+ 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4',
58
+ 'CI_MERGE_REQUEST_IID': '123',
59
+ 'CI_MERGE_REQUEST_PROJECT_ID': '456'
60
+ })
61
+ def test_fallback_from_bearer_to_private_token(self):
62
+ """Test fallback from Bearer to PRIVATE-TOKEN authentication"""
63
+ # Create a mock client that simulates auth failure then success
64
+ mock_client = MagicMock(spec=CliClient)
65
+
66
+ # First call (with Bearer) fails with 401
67
+ auth_error = Exception()
68
+ auth_error.response = MagicMock()
69
+ auth_error.response.status_code = 401
70
+
71
+ # Second call (with PRIVATE-TOKEN) succeeds
72
+ success_response = {'notes': []}
73
+
74
+ mock_client.request.side_effect = [auth_error, success_response]
75
+
76
+ # Create GitLab instance with mock client
77
+ gitlab = Gitlab(client=mock_client)
78
+
79
+ # This should trigger the fallback mechanism
80
+ result = gitlab.get_comments_for_pr()
81
+
82
+ # Verify two requests were made
83
+ assert mock_client.request.call_count == 2
84
+
85
+ # First call should use Bearer (default for 'glpat-' token)
86
+ first_call_headers = mock_client.request.call_args_list[0][1]['headers']
87
+ assert 'Authorization' in first_call_headers
88
+ assert first_call_headers['Authorization'] == 'Bearer glpat-test-token'
89
+
90
+ # Second call should use PRIVATE-TOKEN (fallback)
91
+ second_call_headers = mock_client.request.call_args_list[1][1]['headers']
92
+ assert 'PRIVATE-TOKEN' in second_call_headers
93
+ assert second_call_headers['PRIVATE-TOKEN'] == 'glpat-test-token'
94
+
95
+ @patch.dict(os.environ, {
96
+ 'GITLAB_TOKEN': 'test-token',
97
+ 'CI_PROJECT_NAME': 'test-project',
98
+ 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4',
99
+ 'CI_MERGE_REQUEST_IID': '123',
100
+ 'CI_MERGE_REQUEST_PROJECT_ID': '456'
101
+ })
102
+ def test_non_auth_error_not_retried(self):
103
+ """Test that non-authentication errors are not retried"""
104
+ # Create a mock client that simulates a non-auth error
105
+ mock_client = MagicMock(spec=CliClient)
106
+
107
+ # Simulate a 500 error (not auth-related)
108
+ server_error = Exception()
109
+ server_error.response = MagicMock()
110
+ server_error.response.status_code = 500
111
+
112
+ mock_client.request.side_effect = server_error
113
+
114
+ # Create GitLab instance with mock client
115
+ gitlab = Gitlab(client=mock_client)
116
+
117
+ # This should NOT trigger the fallback mechanism
118
+ with pytest.raises(Exception):
119
+ gitlab.get_comments_for_pr()
120
+
121
+ # Verify only one request was made (no retry)
122
+ assert mock_client.request.call_count == 1
123
+
124
+ @patch.dict(os.environ, {
125
+ 'GITLAB_TOKEN': 'test-token',
126
+ 'CI_PROJECT_NAME': 'test-project',
127
+ 'CI_API_V4_URL': 'https://gitlab.example.com/api/v4',
128
+ 'CI_MERGE_REQUEST_IID': '123',
129
+ 'CI_MERGE_REQUEST_PROJECT_ID': '456'
130
+ })
131
+ def test_successful_first_attempt_no_fallback(self):
132
+ """Test that successful requests don't trigger fallback"""
133
+ # Create a mock client that succeeds on first try
134
+ mock_client = MagicMock(spec=CliClient)
135
+ mock_client.request.return_value = {'notes': []}
136
+
137
+ # Create GitLab instance with mock client
138
+ gitlab = Gitlab(client=mock_client)
139
+
140
+ # This should succeed on first try
141
+ result = gitlab.get_comments_for_pr()
142
+
143
+ # Verify only one request was made
144
+ assert mock_client.request.call_count == 1
145
+
146
+
147
+ if __name__ == '__main__':
148
+ pytest.main([__file__])
@@ -43,6 +43,9 @@ socket-security:
43
43
  # Required for GitLab integration to work properly
44
44
  variables:
45
45
  SOCKET_SECURITY_API_KEY: $SOCKET_SECURITY_API_KEY
46
+ # GitLab token for API access - supports both authentication patterns:
47
+ # 1. CI_JOB_TOKEN: Built-in GitLab CI token (automatically uses Bearer auth)
48
+ # 2. Personal Access Token: Custom token (auto-detects Bearer vs PRIVATE-TOKEN)
46
49
  GITLAB_TOKEN: $CI_JOB_TOKEN
47
50
 
48
51
  # Optional: Run only when manifest files change (more efficient)
@@ -1,41 +0,0 @@
1
- from abc import abstractmethod
2
- from typing import Dict
3
-
4
- from ..cli_client import CliClient
5
-
6
-
7
- class ScmClient(CliClient):
8
- def __init__(self, token: str, api_url: str):
9
- self.token = token
10
- self.api_url = api_url
11
-
12
- @abstractmethod
13
- def get_headers(self) -> Dict:
14
- """Each SCM implements its own auth headers"""
15
- pass
16
-
17
- def request(self, path: str, **kwargs):
18
- """Override base request to use SCM-specific headers and base_url"""
19
- headers = kwargs.pop('headers', None) or self.get_headers()
20
- return super().request(
21
- path=path,
22
- headers=headers,
23
- base_url=self.api_url,
24
- **kwargs
25
- )
26
-
27
- class GithubClient(ScmClient):
28
- def get_headers(self) -> Dict:
29
- return {
30
- 'Authorization': f"Bearer {self.token}",
31
- 'User-Agent': 'SocketPythonScript/0.0.1',
32
- "accept": "application/json"
33
- }
34
-
35
- class GitlabClient(ScmClient):
36
- def get_headers(self) -> Dict:
37
- return {
38
- 'Authorization': f"Bearer {self.token}",
39
- 'User-Agent': 'SocketPythonScript/0.0.1',
40
- "accept": "application/json"
41
- }
File without changes
File without changes