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.
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/PKG-INFO +75 -1
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/README.md +74 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/pyproject.toml +1 -1
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/__init__.py +1 -1
- socketsecurity-2.2.2/socketsecurity/core/scm/client.py +84 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/gitlab.py +112 -8
- socketsecurity-2.2.2/tests/unit/test_gitlab_auth.py +116 -0
- socketsecurity-2.2.2/tests/unit/test_gitlab_auth_fallback.py +148 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/workflows/gitlab-ci.yml +3 -0
- socketsecurity-2.2.0/socketsecurity/core/scm/client.py +0 -41
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/CODEOWNERS +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/docker-stable.yml +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/pr-preview.yml +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/release.yml +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.github/workflows/version-check.yml +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.gitignore +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.hooks/sync_version.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.pre-commit-config.yaml +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/.python-version +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/Dockerfile +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/LICENSE +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/Makefile +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/Pipfile.lock +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/docs/README.md +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/pytest.ini +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements-dev.lock +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements-dev.txt +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements.lock +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/requirements.txt +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/build_container.sh +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/deploy-test-docker.sh +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/deploy-test-pypi.sh +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/scripts/run.sh +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/config.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/__init__.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/classes.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/cli_client.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/exceptions.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/git_interface.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/helper/__init__.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/lazy_file_loader.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/logging.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/messages.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/resource_utils.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/__init__.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/base.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm/github.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/scm_comments.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/socket_config.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/core/utils.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/output.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/__init__.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/base.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/jira.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/manager.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/slack.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/teams.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/plugins/webhook.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/socketsecurity/socketcli.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/__init__.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/conftest.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/create_diff_input.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_diff_generation.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_package_and_alerts.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_sdk_methods.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/core/test_supporting_methods.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/create_response.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/diff/stream_diff.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/metadata.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/new_scan/metadata.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/repos/repo_info_error.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/repos/repo_info_no_head.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/repos/repo_info_success.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/settings/security-policy.json +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/__init__.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_cli_config.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_client.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_config.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/unit/test_output.py +0 -0
- {socketsecurity-2.2.0 → socketsecurity-2.2.2}/workflows/bitbucket-pipelines.yml +0 -0
- {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.
|
|
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)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__author__ = 'socket.dev'
|
|
2
|
-
__version__ = '2.2.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/diff/stream_diff_full.json
RENAMED
|
File without changes
|
|
File without changes
|
{socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/stream_scan.json
RENAMED
|
File without changes
|
{socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/head_scan/stream_scan_full.json
RENAMED
|
File without changes
|
|
File without changes
|
{socketsecurity-2.2.0 → socketsecurity-2.2.2}/tests/data/fullscans/new_scan/stream_scan.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|