cycode 3.7.2.dev1__py3-none-any.whl → 3.7.2.dev2__py3-none-any.whl

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.
cycode/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '3.7.2.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
1
+ __version__ = '3.7.2.dev2' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
cycode/cli/app.py CHANGED
@@ -110,6 +110,13 @@ def app_callback(
110
110
  rich_help_panel=_AUTH_RICH_HELP_PANEL,
111
111
  ),
112
112
  ] = None,
113
+ id_token: Annotated[
114
+ Optional[str],
115
+ typer.Option(
116
+ help='Specify a Cycode OIDC ID token for this specific scan execution.',
117
+ rich_help_panel=_AUTH_RICH_HELP_PANEL,
118
+ ),
119
+ ] = None,
113
120
  _: Annotated[
114
121
  Optional[bool],
115
122
  typer.Option(
@@ -152,6 +159,7 @@ def app_callback(
152
159
 
153
160
  ctx.obj['client_id'] = client_id
154
161
  ctx.obj['client_secret'] = client_secret
162
+ ctx.obj['id_token'] = id_token
155
163
 
156
164
  ctx.obj['progress_bar'] = get_progress_bar(hidden=no_progress_meter, sections=SCAN_PROGRESS_BAR_SECTIONS)
157
165
 
@@ -4,6 +4,7 @@ from cycode.cli.apps.auth.models import AuthInfo
4
4
  from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError
5
5
  from cycode.cli.user_settings.credentials_manager import CredentialsManager
6
6
  from cycode.cli.utils.jwt_utils import get_user_and_tenant_ids_from_access_token
7
+ from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
7
8
  from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
8
9
 
9
10
  if TYPE_CHECKING:
@@ -13,9 +14,23 @@ if TYPE_CHECKING:
13
14
  def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
14
15
  printer = ctx.obj.get('console_printer')
15
16
 
16
- client_id, client_secret = ctx.obj.get('client_id'), ctx.obj.get('client_secret')
17
+ client_id = ctx.obj.get('client_id')
18
+ client_secret = ctx.obj.get('client_secret')
19
+ id_token = ctx.obj.get('id_token')
20
+
21
+ credentials_manager = CredentialsManager()
22
+
23
+ auth_info = _try_oidc_authorization(ctx, printer, client_id, id_token)
24
+ if auth_info:
25
+ return auth_info
26
+
17
27
  if not client_id or not client_secret:
18
- client_id, client_secret = CredentialsManager().get_credentials()
28
+ stored_client_id, stored_id_token = credentials_manager.get_oidc_credentials()
29
+ auth_info = _try_oidc_authorization(ctx, printer, stored_client_id, stored_id_token)
30
+ if auth_info:
31
+ return auth_info
32
+
33
+ client_id, client_secret = credentials_manager.get_credentials()
19
34
 
20
35
  if not client_id or not client_secret:
21
36
  return None
@@ -32,3 +47,23 @@ def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
32
47
  printer.print_exception()
33
48
 
34
49
  return None
50
+
51
+
52
+ def _try_oidc_authorization(
53
+ ctx: 'Context', printer: any, client_id: Optional[str], id_token: Optional[str]
54
+ ) -> Optional[AuthInfo]:
55
+ if not client_id or not id_token:
56
+ return None
57
+
58
+ try:
59
+ access_token = CycodeOidcBasedClient(client_id, id_token).get_access_token()
60
+ if not access_token:
61
+ return None
62
+
63
+ user_id, tenant_id = get_user_and_tenant_ids_from_access_token(access_token)
64
+ return AuthInfo(user_id=user_id, tenant_id=tenant_id)
65
+ except (RequestHttpError, HttpUnauthorizedError):
66
+ if ctx:
67
+ printer.print_exception()
68
+
69
+ return None
@@ -7,6 +7,7 @@ from cycode.cli.apps.configure.prompts import (
7
7
  get_app_url_input,
8
8
  get_client_id_input,
9
9
  get_client_secret_input,
10
+ get_id_token_input,
10
11
  )
11
12
  from cycode.cli.console import console
12
13
  from cycode.cli.utils.sentry import add_breadcrumb
@@ -32,6 +33,7 @@ def configure_command() -> None:
32
33
  * APP URL: The base URL for Cycode's web application (for on-premise or EU installations)
33
34
  * Client ID: Your Cycode client ID for authentication
34
35
  * Client Secret: Your Cycode client secret for authentication
36
+ * ID Token: Your Cycode ID token for authentication
35
37
 
36
38
  Example usage:
37
39
  * `cycode configure`: Start interactive configuration
@@ -55,15 +57,22 @@ def configure_command() -> None:
55
57
  config_updated = True
56
58
 
57
59
  current_client_id, current_client_secret = CREDENTIALS_MANAGER.get_credentials_from_file()
60
+ _, current_id_token = CREDENTIALS_MANAGER.get_oidc_credentials_from_file()
58
61
  client_id = get_client_id_input(current_client_id)
59
62
  client_secret = get_client_secret_input(current_client_secret)
63
+ id_token = get_id_token_input(current_id_token)
60
64
 
61
65
  credentials_updated = False
62
66
  if _should_update_value(current_client_id, client_id) or _should_update_value(current_client_secret, client_secret):
63
67
  credentials_updated = True
64
68
  CREDENTIALS_MANAGER.update_credentials(client_id, client_secret)
65
69
 
70
+ oidc_credentials_updated = False
71
+ if _should_update_value(current_client_id, client_id) or _should_update_value(current_id_token, id_token):
72
+ oidc_credentials_updated = True
73
+ CREDENTIALS_MANAGER.update_oidc_credentials(client_id, id_token)
74
+
66
75
  if config_updated:
67
76
  console.print(get_urls_update_result_message())
68
- if credentials_updated:
77
+ if credentials_updated or oidc_credentials_updated:
69
78
  console.print(get_credentials_update_result_message())
@@ -46,3 +46,14 @@ def get_api_url_input(current_api_url: Optional[str]) -> str:
46
46
  default = current_api_url
47
47
 
48
48
  return typer.prompt(text=prompt_text, default=default, type=str)
49
+
50
+
51
+ def get_id_token_input(current_id_token: Optional[str]) -> Optional[str]:
52
+ prompt_text = 'Cycode ID Token'
53
+
54
+ prompt_suffix = ' []: '
55
+ if current_id_token:
56
+ prompt_suffix = f' [{obfuscate_text(current_id_token)}]: '
57
+
58
+ new_id_token = typer.prompt(text=prompt_text, prompt_suffix=prompt_suffix, default='', show_default=False)
59
+ return new_id_token or current_id_token
cycode/cli/config.py CHANGED
@@ -5,3 +5,4 @@ configuration_manager = ConfigurationManager()
5
5
  # env vars
6
6
  CYCODE_CLIENT_ID_ENV_VAR_NAME = 'CYCODE_CLIENT_ID'
7
7
  CYCODE_CLIENT_SECRET_ENV_VAR_NAME = 'CYCODE_CLIENT_SECRET'
8
+ CYCODE_ID_TOKEN_ENV_VAR_NAME = 'CYCODE_ID_TOKEN'
@@ -2,7 +2,11 @@ import os
2
2
  from pathlib import Path
3
3
  from typing import Optional
4
4
 
5
- from cycode.cli.config import CYCODE_CLIENT_ID_ENV_VAR_NAME, CYCODE_CLIENT_SECRET_ENV_VAR_NAME
5
+ from cycode.cli.config import (
6
+ CYCODE_CLIENT_ID_ENV_VAR_NAME,
7
+ CYCODE_CLIENT_SECRET_ENV_VAR_NAME,
8
+ CYCODE_ID_TOKEN_ENV_VAR_NAME,
9
+ )
6
10
  from cycode.cli.user_settings.base_file_manager import BaseFileManager
7
11
  from cycode.cli.user_settings.jwt_creator import JwtCreator
8
12
  from cycode.cli.utils.sentry import setup_scope_from_access_token
@@ -15,6 +19,7 @@ class CredentialsManager(BaseFileManager):
15
19
 
16
20
  CLIENT_ID_FIELD_NAME: str = 'cycode_client_id'
17
21
  CLIENT_SECRET_FIELD_NAME: str = 'cycode_client_secret'
22
+ ID_TOKEN_FIELD_NAME: str = 'cycode_id_token'
18
23
  ACCESS_TOKEN_FIELD_NAME: str = 'cycode_access_token'
19
24
  ACCESS_TOKEN_EXPIRES_IN_FIELD_NAME: str = 'cycode_access_token_expires_in'
20
25
  ACCESS_TOKEN_CREATOR_FIELD_NAME: str = 'cycode_access_token_creator'
@@ -38,6 +43,25 @@ class CredentialsManager(BaseFileManager):
38
43
  client_secret = file_content.get(self.CLIENT_SECRET_FIELD_NAME)
39
44
  return client_id, client_secret
40
45
 
46
+ def get_oidc_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]:
47
+ file_content = self.read_file()
48
+ client_id = file_content.get(self.CLIENT_ID_FIELD_NAME)
49
+ id_token = file_content.get(self.ID_TOKEN_FIELD_NAME)
50
+ return client_id, id_token
51
+
52
+ def get_oidc_credentials(self) -> tuple[Optional[str], Optional[str]]:
53
+ client_id = os.getenv(CYCODE_CLIENT_ID_ENV_VAR_NAME)
54
+ id_token = os.getenv(CYCODE_ID_TOKEN_ENV_VAR_NAME)
55
+
56
+ if client_id is not None and id_token is not None:
57
+ return client_id, id_token
58
+
59
+ return self.get_oidc_credentials_from_file()
60
+
61
+ def update_oidc_credentials(self, client_id: str, id_token: str) -> None:
62
+ file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.ID_TOKEN_FIELD_NAME: id_token}
63
+ self.write_content_to_file(file_content_to_update)
64
+
41
65
  def update_credentials(self, client_id: str, client_secret: str) -> None:
42
66
  file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.CLIENT_SECRET_FIELD_NAME: client_secret}
43
67
  self.write_content_to_file(file_content_to_update)
@@ -14,8 +14,22 @@ if TYPE_CHECKING:
14
14
 
15
15
 
16
16
  def _get_cycode_client(
17
- create_client_func: callable, client_id: Optional[str], client_secret: Optional[str], hide_response_log: bool
17
+ create_client_func: callable,
18
+ client_id: Optional[str],
19
+ client_secret: Optional[str],
20
+ hide_response_log: bool,
21
+ id_token: Optional[str] = None,
18
22
  ) -> Union['ScanClient', 'ReportClient']:
23
+ if client_id and id_token:
24
+ return create_client_func(client_id, None, hide_response_log, id_token)
25
+
26
+ if not client_id or not id_token:
27
+ oidc_client_id, oidc_id_token = _get_configured_oidc_credentials()
28
+ if oidc_client_id and oidc_id_token:
29
+ return create_client_func(oidc_client_id, None, hide_response_log, oidc_id_token)
30
+ if oidc_id_token and not oidc_client_id:
31
+ raise click.ClickException('Cycode client id needed for OIDC authentication.')
32
+
19
33
  if not client_id or not client_secret:
20
34
  client_id, client_secret = _get_configured_credentials()
21
35
  if not client_id:
@@ -23,28 +37,36 @@ def _get_cycode_client(
23
37
  if not client_secret:
24
38
  raise click.ClickException('Cycode client secret is needed.')
25
39
 
26
- return create_client_func(client_id, client_secret, hide_response_log)
40
+ return create_client_func(client_id, client_secret, hide_response_log, None)
27
41
 
28
42
 
29
43
  def get_scan_cycode_client(ctx: 'typer.Context') -> 'ScanClient':
30
44
  client_id = ctx.obj.get('client_id')
31
45
  client_secret = ctx.obj.get('client_secret')
46
+ id_token = ctx.obj.get('id_token')
32
47
  hide_response_log = not ctx.obj.get('show_secret', False)
33
- return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log)
48
+ return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log, id_token)
34
49
 
35
50
 
36
51
  def get_report_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ReportClient':
37
52
  client_id = ctx.obj.get('client_id')
38
53
  client_secret = ctx.obj.get('client_secret')
39
- return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log)
54
+ id_token = ctx.obj.get('id_token')
55
+ return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log, id_token)
40
56
 
41
57
 
42
58
  def get_import_sbom_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ImportSbomClient':
43
59
  client_id = ctx.obj.get('client_id')
44
60
  client_secret = ctx.obj.get('client_secret')
45
- return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log)
61
+ id_token = ctx.obj.get('id_token')
62
+ return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log, id_token)
46
63
 
47
64
 
48
65
  def _get_configured_credentials() -> tuple[str, str]:
49
66
  credentials_manager = CredentialsManager()
50
67
  return credentials_manager.get_credentials()
68
+
69
+
70
+ def _get_configured_oidc_credentials() -> tuple[Optional[str], Optional[str]]:
71
+ credentials_manager = CredentialsManager()
72
+ return credentials_manager.get_oidc_credentials()
@@ -0,0 +1,100 @@
1
+ from abc import ABC, abstractmethod
2
+ from threading import Lock
3
+ from typing import Any, Optional
4
+
5
+ import arrow
6
+ from requests import Response
7
+
8
+ from cycode.cli.user_settings.credentials_manager import CredentialsManager
9
+ from cycode.cli.user_settings.jwt_creator import JwtCreator
10
+ from cycode.cyclient.cycode_client import CycodeClient
11
+
12
+ _NGINX_PLAIN_ERRORS = [
13
+ b'Invalid JWT Token',
14
+ b'JWT Token Needed',
15
+ b'JWT Token validation failed',
16
+ ]
17
+
18
+
19
+ class BaseTokenAuthClient(CycodeClient, ABC):
20
+ """Base client for token-based authentication flows with cached JWTs."""
21
+
22
+ def __init__(self, client_id: str) -> None:
23
+ super().__init__()
24
+ self.client_id = client_id
25
+
26
+ self._credentials_manager = CredentialsManager()
27
+ # load cached access token
28
+ access_token, expires_in, creator = self._credentials_manager.get_access_token()
29
+
30
+ self._access_token = self._expires_in = None
31
+ expected_creator = self._create_jwt_creator()
32
+ if creator == expected_creator:
33
+ # we must be sure that cached access token is created using the same client id and client secret.
34
+ # because client id and client secret could be passed via command, via env vars or via config file.
35
+ # we must not use cached access token if client id or client secret was changed.
36
+ self._access_token = access_token
37
+ self._expires_in = arrow.get(expires_in) if expires_in else None
38
+
39
+ self._lock = Lock()
40
+
41
+ def get_access_token(self) -> str:
42
+ with self._lock:
43
+ self.refresh_access_token_if_needed()
44
+ return self._access_token
45
+
46
+ def invalidate_access_token(self, in_storage: bool = False) -> None:
47
+ self._access_token = None
48
+ self._expires_in = None
49
+
50
+ if in_storage:
51
+ self._credentials_manager.update_access_token(None, None, None)
52
+
53
+ def refresh_access_token_if_needed(self) -> None:
54
+ if self._access_token is None or self._expires_in is None or arrow.utcnow() >= self._expires_in:
55
+ self.refresh_access_token()
56
+
57
+ def refresh_access_token(self) -> None:
58
+ auth_response = self._request_new_access_token()
59
+ self._access_token = auth_response['token']
60
+
61
+ self._expires_in = arrow.utcnow().shift(seconds=auth_response['expires_in'] * 0.8)
62
+
63
+ jwt_creator = self._create_jwt_creator()
64
+ self._credentials_manager.update_access_token(self._access_token, self._expires_in.timestamp(), jwt_creator)
65
+
66
+ def get_request_headers(self, additional_headers: Optional[dict] = None, without_auth: bool = False) -> dict:
67
+ headers = super().get_request_headers(additional_headers=additional_headers)
68
+
69
+ if not without_auth:
70
+ headers = self._add_auth_header(headers)
71
+
72
+ return headers
73
+
74
+ def _add_auth_header(self, headers: dict) -> dict:
75
+ headers['Authorization'] = f'Bearer {self.get_access_token()}'
76
+ return headers
77
+
78
+ def _execute(
79
+ self,
80
+ *args,
81
+ **kwargs,
82
+ ) -> Response:
83
+ response = super()._execute(*args, **kwargs)
84
+
85
+ # backend returns 200 and plain text. no way to catch it with .raise_for_status()
86
+ nginx_error_response = any(response.content.startswith(plain_error) for plain_error in _NGINX_PLAIN_ERRORS)
87
+ if response.status_code == 200 and nginx_error_response:
88
+ # if cached token is invalid, try to refresh it and retry the request
89
+ self.refresh_access_token()
90
+ response = super()._execute(*args, **kwargs)
91
+
92
+ return response
93
+
94
+ @abstractmethod
95
+ def _create_jwt_creator(self) -> JwtCreator:
96
+ """Create a JwtCreator instance for the current credential type."""
97
+
98
+ @abstractmethod
99
+ def _request_new_access_token(self) -> dict[str, Any]:
100
+ """Return the authentication payload with token and expires_in."""
@@ -1,6 +1,9 @@
1
+ from typing import Optional
2
+
1
3
  from cycode.cyclient.config import dev_mode
2
4
  from cycode.cyclient.config_dev import DEV_CYCODE_API_URL
3
5
  from cycode.cyclient.cycode_dev_based_client import CycodeDevBasedClient
6
+ from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
4
7
  from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
5
8
  from cycode.cyclient.import_sbom_client import ImportSbomClient
6
9
  from cycode.cyclient.report_client import ReportClient
@@ -8,22 +11,41 @@ from cycode.cyclient.scan_client import ScanClient
8
11
  from cycode.cyclient.scan_config_base import DefaultScanConfig, DevScanConfig
9
12
 
10
13
 
11
- def create_scan_client(client_id: str, client_secret: str, hide_response_log: bool) -> ScanClient:
14
+ def create_scan_client(
15
+ client_id: str, client_secret: Optional[str] = None, hide_response_log: bool = False, id_token: Optional[str] = None
16
+ ) -> ScanClient:
12
17
  if dev_mode:
13
18
  client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
14
19
  scan_config = DevScanConfig()
15
20
  else:
16
- client = CycodeTokenBasedClient(client_id, client_secret)
21
+ if id_token:
22
+ client = CycodeOidcBasedClient(client_id, id_token)
23
+ else:
24
+ client = CycodeTokenBasedClient(client_id, client_secret)
17
25
  scan_config = DefaultScanConfig()
18
26
 
19
27
  return ScanClient(client, scan_config, hide_response_log)
20
28
 
21
29
 
22
- def create_report_client(client_id: str, client_secret: str, _: bool) -> ReportClient:
23
- client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
30
+ def create_report_client(
31
+ client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
32
+ ) -> ReportClient:
33
+ if dev_mode:
34
+ client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
35
+ elif id_token:
36
+ client = CycodeOidcBasedClient(client_id, id_token)
37
+ else:
38
+ client = CycodeTokenBasedClient(client_id, client_secret)
24
39
  return ReportClient(client)
25
40
 
26
41
 
27
- def create_import_sbom_client(client_id: str, client_secret: str, _: bool) -> ImportSbomClient:
28
- client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
42
+ def create_import_sbom_client(
43
+ client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
44
+ ) -> ImportSbomClient:
45
+ if dev_mode:
46
+ client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
47
+ elif id_token:
48
+ client = CycodeOidcBasedClient(client_id, id_token)
49
+ else:
50
+ client = CycodeTokenBasedClient(client_id, client_secret)
29
51
  return ImportSbomClient(client)
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+
3
+ from cycode.cli.user_settings.jwt_creator import JwtCreator
4
+ from cycode.cyclient.base_token_auth_client import BaseTokenAuthClient
5
+
6
+
7
+ class CycodeOidcBasedClient(BaseTokenAuthClient):
8
+ """Send requests with JWT obtained via OIDC ID token."""
9
+
10
+ def __init__(self, client_id: str, id_token: str) -> None:
11
+ self.id_token = id_token
12
+ super().__init__(client_id)
13
+
14
+ def _request_new_access_token(self) -> dict[str, Any]:
15
+ auth_response = self.post(
16
+ url_path='api/v1/auth/oidc/api-token',
17
+ body={'client_id': self.client_id, 'id_token': self.id_token},
18
+ without_auth=True,
19
+ hide_response_content_log=True,
20
+ )
21
+ return auth_response.json()
22
+
23
+ def _create_jwt_creator(self) -> JwtCreator:
24
+ return JwtCreator.create(self.client_id, self.id_token)
@@ -1,97 +1,24 @@
1
- from threading import Lock
2
- from typing import Optional
1
+ from typing import Any
3
2
 
4
- import arrow
5
- from requests import Response
6
-
7
- from cycode.cli.user_settings.credentials_manager import CredentialsManager
8
3
  from cycode.cli.user_settings.jwt_creator import JwtCreator
9
- from cycode.cyclient.cycode_client import CycodeClient
10
-
11
- _NGINX_PLAIN_ERRORS = [
12
- b'Invalid JWT Token',
13
- b'JWT Token Needed',
14
- b'JWT Token validation failed',
15
- ]
4
+ from cycode.cyclient.base_token_auth_client import BaseTokenAuthClient
16
5
 
17
6
 
18
- class CycodeTokenBasedClient(CycodeClient):
7
+ class CycodeTokenBasedClient(BaseTokenAuthClient):
19
8
  """Send requests with JWT."""
20
9
 
21
10
  def __init__(self, client_id: str, client_secret: str) -> None:
22
- super().__init__()
23
11
  self.client_secret = client_secret
24
- self.client_id = client_id
25
-
26
- self._credentials_manager = CredentialsManager()
27
- # load cached access token
28
- access_token, expires_in, creator = self._credentials_manager.get_access_token()
29
-
30
- self._access_token = self._expires_in = None
31
- if creator == JwtCreator.create(client_id, client_secret):
32
- # we must be sure that cached access token is created using the same client id and client secret.
33
- # because client id and client secret could be passed via command, via env vars or via config file.
34
- # we must not use cached access token if client id or client secret was changed.
35
- self._access_token = access_token
36
- self._expires_in = arrow.get(expires_in) if expires_in else None
37
-
38
- self._lock = Lock()
39
-
40
- def get_access_token(self) -> str:
41
- with self._lock:
42
- self.refresh_access_token_if_needed()
43
- return self._access_token
44
-
45
- def invalidate_access_token(self, in_storage: bool = False) -> None:
46
- self._access_token = None
47
- self._expires_in = None
48
-
49
- if in_storage:
50
- self._credentials_manager.update_access_token(None, None, None)
51
-
52
- def refresh_access_token_if_needed(self) -> None:
53
- if self._access_token is None or self._expires_in is None or arrow.utcnow() >= self._expires_in:
54
- self.refresh_access_token()
12
+ super().__init__(client_id)
55
13
 
56
- def refresh_access_token(self) -> None:
14
+ def _request_new_access_token(self) -> dict[str, Any]:
57
15
  auth_response = self.post(
58
16
  url_path='api/v1/auth/api-token',
59
17
  body={'clientId': self.client_id, 'secret': self.client_secret},
60
18
  without_auth=True,
61
19
  hide_response_content_log=True,
62
20
  )
63
- auth_response_data = auth_response.json()
64
-
65
- self._access_token = auth_response_data['token']
66
- self._expires_in = arrow.utcnow().shift(seconds=auth_response_data['expires_in'] * 0.8)
67
-
68
- jwt_creator = JwtCreator.create(self.client_id, self.client_secret)
69
- self._credentials_manager.update_access_token(self._access_token, self._expires_in.timestamp(), jwt_creator)
70
-
71
- def get_request_headers(self, additional_headers: Optional[dict] = None, without_auth: bool = False) -> dict:
72
- headers = super().get_request_headers(additional_headers=additional_headers)
73
-
74
- if not without_auth:
75
- headers = self._add_auth_header(headers)
76
-
77
- return headers
78
-
79
- def _add_auth_header(self, headers: dict) -> dict:
80
- headers['Authorization'] = f'Bearer {self.get_access_token()}'
81
- return headers
82
-
83
- def _execute(
84
- self,
85
- *args,
86
- **kwargs,
87
- ) -> Response:
88
- response = super()._execute(*args, **kwargs)
89
-
90
- # backend returns 200 and plain text. no way to catch it with .raise_for_status()
91
- nginx_error_response = any(response.content.startswith(plain_error) for plain_error in _NGINX_PLAIN_ERRORS)
92
- if response.status_code == 200 and nginx_error_response:
93
- # if cached token is invalid, try to refresh it and retry the request
94
- self.refresh_access_token()
95
- response = super()._execute(*args, **kwargs)
21
+ return auth_response.json()
96
22
 
97
- return response
23
+ def _create_jwt_creator(self) -> JwtCreator:
24
+ return JwtCreator.create(self.client_id, self.client_secret)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.7.2.dev1
3
+ Version: 3.7.2.dev2
4
4
  Summary: Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.
5
5
  License-Expression: MIT
6
6
  License-File: LICENCE
@@ -144,7 +144,7 @@ To install the Cycode CLI application on your local machine, perform the followi
144
144
  ./cycode
145
145
  ```
146
146
 
147
- 3. Finally authenticate the CLI. There are three methods to set the Cycode client ID and client secret:
147
+ 3. Finally authenticate the CLI. There are three methods to set the Cycode client ID and credentials (client secret or OIDC ID token):
148
148
 
149
149
  - [cycode auth](#using-the-auth-command) (**Recommended**)
150
150
  - [cycode configure](#using-the-configure-command)
@@ -207,11 +207,15 @@ To install the Cycode CLI application on your local machine, perform the followi
207
207
 
208
208
  `Cycode Client ID []: 7fe5346b-xxxx-xxxx-xxxx-55157625c72d`
209
209
 
210
- 5. Enter your Cycode Client Secret value.
210
+ 5. Enter your Cycode Client Secret value (skip if you plan to use an OIDC ID token).
211
211
 
212
212
  `Cycode Client Secret []: c1e24929-xxxx-xxxx-xxxx-8b08c1839a2e`
213
213
 
214
- 6. If the values were entered successfully, you'll see the following message:
214
+ 6. Enter your Cycode OIDC ID Token value (optional).
215
+
216
+ `Cycode ID Token []: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...`
217
+
218
+ 7. If the values were entered successfully, you'll see the following message:
215
219
 
216
220
  `Successfully configured CLI credentials!`
217
221
 
@@ -236,6 +240,12 @@ and
236
240
  export CYCODE_CLIENT_SECRET={your Cycode Secret Key}
237
241
  ```
238
242
 
243
+ If your organization uses OIDC authentication, you can provide the ID token instead (or in addition):
244
+
245
+ ```bash
246
+ export CYCODE_ID_TOKEN={your Cycode OIDC ID token}
247
+ ```
248
+
239
249
  #### On Windows
240
250
 
241
251
  1. From the Control Panel, navigate to the System menu:
@@ -250,7 +260,7 @@ export CYCODE_CLIENT_SECRET={your Cycode Secret Key}
250
260
 
251
261
  <img height="30" src="https://raw.githubusercontent.com/cycodehq/cycode-cli/main/images/image3.png" alt="environments variables button"/>
252
262
 
253
- 4. Create `CYCODE_CLIENT_ID` and `CYCODE_CLIENT_SECRET` variables with values matching your ID and Secret Key, respectively:
263
+ 4. Create `CYCODE_CLIENT_ID` and `CYCODE_CLIENT_SECRET` variables with values matching your ID and Secret Key, respectively. If you authenticate via OIDC, add `CYCODE_ID_TOKEN` with your OIDC ID token value as well:
254
264
 
255
265
  <img height="100" src="https://raw.githubusercontent.com/cycodehq/cycode-cli/main/images/image4.png" alt="environment variables window"/>
256
266
 
@@ -364,6 +374,7 @@ The following are the options and commands available with the Cycode CLI applica
364
374
  | `-o`, `--output [rich\|text\|json\|table]` | Specify the output type. The default is `rich`. |
365
375
  | `--client-id TEXT` | Specify a Cycode client ID for this specific scan execution. |
366
376
  | `--client-secret TEXT` | Specify a Cycode client secret for this specific scan execution. |
377
+ | `--id-token TEXT` | Specify a Cycode OIDC ID token for this specific scan execution. |
367
378
  | `--install-completion` | Install completion for the current shell.. |
368
379
  | `--show-completion [bash\|zsh\|fish\|powershell\|pwsh]` | Show completion for the specified shell, to copy it or customize the installation. |
369
380
  | `-h`, `--help` | Show options for given command. |
@@ -1,7 +1,7 @@
1
- cycode/__init__.py,sha256=ejiqC58Tc7XnagfO7grt-xdeaq8c5k77Bhsun3w0RdU,114
1
+ cycode/__init__.py,sha256=rk4uXVXiL3TGI54CrYVRFXfmyRkwZfoz-aNkqe9gNZQ,114
2
2
  cycode/__main__.py,sha256=Z3bD5yrA7yPvAChcADQrqCaZd0ChGI1gdiwALwbWJ6U,104
3
3
  cycode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- cycode/cli/app.py,sha256=qo_GU1ivrJKEjx8HQtjbHOE1hAXr4tzCexD1qw5W8bg,6146
4
+ cycode/cli/app.py,sha256=Th7skHthoJN1EuDtgjl4flflFqejfxaTORKmMKSXE00,6412
5
5
  cycode/cli/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  cycode/cli/apps/ai_remediation/__init__.py,sha256=8vYthY9RQeJqEni3AIF5sryz8n-XJQ6VNqG4aEFBAdY,553
7
7
  cycode/cli/apps/ai_remediation/ai_remediation_command.py,sha256=u1EdebaKCEmzv9fXmnIN0xDSLcCmGyjueYKvYfLOj_8,1549
@@ -9,14 +9,14 @@ cycode/cli/apps/ai_remediation/apply_fix.py,sha256=9zgqiqF9HBQXi7Oz9ZIiANIAuKAMT
9
9
  cycode/cli/apps/ai_remediation/print_remediation.py,sha256=nEVkR7gnGIryGEo0NOKzrmqsh4CjLr2QfVt9elsrzGY,590
10
10
  cycode/cli/apps/auth/__init__.py,sha256=rjf_rEBS1aS6rzY4Qh75BzOOX9SEHPdJMah-1FJM4DY,447
11
11
  cycode/cli/apps/auth/auth_command.py,sha256=qsexUDe5tkpAXNRqvTE7er40L2CRntEnkpfmPiLCHn0,1004
12
- cycode/cli/apps/auth/auth_common.py,sha256=18Kgf1N9fwXVWgYFuiwGP7DgXsdADD3eeNBMjZVxDUA,1280
12
+ cycode/cli/apps/auth/auth_common.py,sha256=bfQXqfv5bcYmc7njWOnG1VGzRU-C7spBv48gxHROCGU,2420
13
13
  cycode/cli/apps/auth/auth_manager.py,sha256=ePRI1Nl8HVwcST77LAMuzu4tm4TTIX5b-MACB59LUrQ,4286
14
14
  cycode/cli/apps/auth/models.py,sha256=XVWq_9e6tQ9farEs_ks2Hv8B_qJdbuZciO7oe8wdgoY,96
15
15
  cycode/cli/apps/configure/__init__.py,sha256=J-XJyC3zFt8vP5LrMoHCExkR8MWFfegt-PE0T28cr40,539
16
- cycode/cli/apps/configure/configure_command.py,sha256=BJSjRxYpsP64xPQgo9i28oJNyfXlVQSsqb0fwZ5Rnks,2588
16
+ cycode/cli/apps/configure/configure_command.py,sha256=ZwKEWOclMsC9b8A1HeTAEJhFNXk9_m4DBirFeNgck-w,3089
17
17
  cycode/cli/apps/configure/consts.py,sha256=wm3FV5eHRrg77zQnCRExAvBMfqnWdxb33sIdJeTgOK0,1130
18
18
  cycode/cli/apps/configure/messages.py,sha256=hZ4gFyvzPsjXKkYADmdtWl_OcIdZQLIsUSzUrThWMW8,1455
19
- cycode/cli/apps/configure/prompts.py,sha256=eyMq7xb_PKqV7oewRJ09z-BDSZ_fICZfgv6mUlw74Mk,1500
19
+ cycode/cli/apps/configure/prompts.py,sha256=z1KZiVJOlFeWKawFE_RyMOitekzuZqKu9aV6KYGbuBU,1889
20
20
  cycode/cli/apps/ignore/__init__.py,sha256=hk1jyJ5ecDeNxHu7gbbbugNiMMS5Y0wmFhi2FiokSHo,220
21
21
  cycode/cli/apps/ignore/ignore_command.py,sha256=RCMKNqSqvuddC2ntiPcPhWgz-vnsyJL5EP9ZVJl_qmU,5848
22
22
  cycode/cli/apps/mcp/__init__.py,sha256=FMXPnuSH7RV0ZckJt5HgVvTvH8QWApyVOpFzRpAH104,460
@@ -65,7 +65,7 @@ cycode/cli/apps/status/models.py,sha256=2SBpJlh_MNCPxv8aXMV5D4GfK6-G-XB0GlMFZ3Ne
65
65
  cycode/cli/apps/status/status_command.py,sha256=B8YZ4hibWkjkAkikailll2lkJmCRYh9APdvZSR4L33c,1069
66
66
  cycode/cli/apps/status/version_command.py,sha256=c6Iko_rmZo9T_kQSd3HUloBi40Qv7cjgKJNVxpMiMfE,315
67
67
  cycode/cli/cli_types.py,sha256=kSqVRkm3zGmGTugXLV3qLTMxwUdnVFsIPEPHao4xY04,2885
68
- cycode/cli/config.py,sha256=EblYUlUA4lTp_lrL3gMG-cW7FUOTE1jtGIOljcLnEzk,250
68
+ cycode/cli/config.py,sha256=Op-lX_neanJtvPvoOEx4ByBdveh5ygElIga1FdSHhOI,299
69
69
  cycode/cli/console.py,sha256=vp-DHwlkwpwdsPyfwGdjsPF-6-Bi3f8W7G-W_YXCMH8,1914
70
70
  cycode/cli/consts.py,sha256=hZF6NL3asl3GLa2nMsxjZB6LGey8nMX304s8u7ARBPs,8885
71
71
  cycode/cli/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -129,11 +129,11 @@ cycode/cli/user_settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
129
129
  cycode/cli/user_settings/base_file_manager.py,sha256=SLA5xRMTqSY-PtbeKCtVc9rP_ljxUYe3z3ZU-XE2x0w,844
130
130
  cycode/cli/user_settings/config_file_manager.py,sha256=KqMogXjtgO-ZbEGW0eN_ZUHn3tLQFJteV8zYDgEhIks,4954
131
131
  cycode/cli/user_settings/configuration_manager.py,sha256=8nTogrLRAMXKM13Zd_lvaL9nsfhYRK-IPii25v6ONck,9277
132
- cycode/cli/user_settings/credentials_manager.py,sha256=jT8pTjldk6WmDyTJPidYQxREuPiObexaP1HEmYpCcWU,3152
132
+ cycode/cli/user_settings/credentials_manager.py,sha256=jGCpNuQmnRvcWBBAknggLrBRbZro1DcpdagisMHFr48,4130
133
133
  cycode/cli/user_settings/jwt_creator.py,sha256=xEkFLFqhwbNJnXuIi02XDxoj2E-4Nw-m10uJaHl3luA,745
134
134
  cycode/cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
135
  cycode/cli/utils/enum_utils.py,sha256=h_VTCfJ-0hnhwDsEznmx56rJrCb5FQ8u6PrI6p8MP3E,187
136
- cycode/cli/utils/get_api_client.py,sha256=wj5g9blyM8drsTFs59bsXEMfKJXqlssl5H0e1q5OwKQ,2097
136
+ cycode/cli/utils/get_api_client.py,sha256=k4SVe_FAxT_tdZHJu9CvobOUPeeTffv_mdoou9dvaQk,2986
137
137
  cycode/cli/utils/git_proxy.py,sha256=FPHMBiyLFK9X9vKYpKySRKJH6Dc9Cb3nO241Q95dASE,2911
138
138
  cycode/cli/utils/ignore_utils.py,sha256=cODqhnOHA2kRo8rMY0YcmcKkmXNPOC9UTCmFu62RRqE,15567
139
139
  cycode/cli/utils/jwt_utils.py,sha256=TfTHCCCxKO6RvSKT2qspx4577Gax3n9YRj2UgigpGuQ,537
@@ -150,13 +150,15 @@ cycode/cli/utils/yaml_utils.py,sha256=R-tqzl0C-zoa42rS7nfWeHu3GJ0jpbQUyyqYYU2hle
150
150
  cycode/config.py,sha256=jHORGZQcAXkAGSf2XreC-RQoc8sdNWja69QKtPWTbWo,1044
151
151
  cycode/cyclient/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
152
152
  cycode/cyclient/auth_client.py,sha256=TwbmZ358Ancf-Q-IZolvfljZ8691_6botsqd0R0PLPk,2105
153
- cycode/cyclient/client_creator.py,sha256=g8MON1CGsBCmsh2B-Kpmpq99cixfSBzT3dQLWYomVtU,1390
153
+ cycode/cyclient/base_token_auth_client.py,sha256=3JIrSz0-ywVTIfxIs2zs5aGcE-x5GW3AgPHm9qA4ZDE,3857
154
+ cycode/cyclient/client_creator.py,sha256=HtNrQaOXnTI913kNE9LLU92X2eIVwlHlbQ3tr7oiyrQ,2006
154
155
  cycode/cyclient/config.py,sha256=Le3YYp7LyclTwS4vonuFipfRA1qhyC28_muUtxpnImc,1387
155
156
  cycode/cyclient/config_dev.py,sha256=GJ3w8Q-ow5SvXGBFA34eaeoI6GhitavAGy3UfcUh9OU,120
156
157
  cycode/cyclient/cycode_client.py,sha256=ifZMA4RlFw4QNHku5ZxmtUKglH2yd1479yYZGDtxVvw,257
157
158
  cycode/cyclient/cycode_client_base.py,sha256=qkOwul-H4cF3-ffX2iA6BKXkoQthDSO9unwB35-EdKU,6584
158
159
  cycode/cyclient/cycode_dev_based_client.py,sha256=8LxeUWizXzZ0ilpwb6Q0W4ZMLZyZdKPzgpl5xcRmT8c,664
159
- cycode/cyclient/cycode_token_based_client.py,sha256=tD_HWgkz0VDcU4AQsPxHxGTwfQ8KlpXGHNbyfRxu2jk,3779
160
+ cycode/cyclient/cycode_oidc_based_client.py,sha256=AVKBlqFOLYUQVxyPquvFwqnYpD6xgU_R6GJiQCWEdJw,857
161
+ cycode/cyclient/cycode_token_based_client.py,sha256=frbrv1jzF388SXqHNNkZ95Hbx7Vjd3UXwWnq7nVxYN8,848
160
162
  cycode/cyclient/headers.py,sha256=5aLezpRDBzueH9T1hB_6VyUydRpTs3rN17CDDPn1BxI,1448
161
163
  cycode/cyclient/import_sbom_client.py,sha256=M0RAn2dDh9woI3SUkgSHCQxhbARoLpyAM3amOausz8E,2749
162
164
  cycode/cyclient/logger.py,sha256=oTkay7QzoOIVQ71cGOy4ukkijYGA3IKJlHkL24Px5ds,70
@@ -165,8 +167,8 @@ cycode/cyclient/report_client.py,sha256=h12pz3vWCwDF73BhqFX7iDSxBgQDFwkiGh3hmul2
165
167
  cycode/cyclient/scan_client.py,sha256=uTBEjgfaCVuJREo73p_zkIVA23NQfdJ1d1-bzc7nSKk,12682
166
168
  cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
167
169
  cycode/logger.py,sha256=xAzpkWLZhixO4egRcYn4HXM9lIfx5wHdpkHxNc5jrX8,2225
168
- cycode-3.7.2.dev1.dist-info/METADATA,sha256=kpZ1L5R-ZdZDiUt2zNwyc5rYEuSSpI-mgiRuNhYqxuk,78429
169
- cycode-3.7.2.dev1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
170
- cycode-3.7.2.dev1.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
171
- cycode-3.7.2.dev1.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
172
- cycode-3.7.2.dev1.dist-info/RECORD,,
170
+ cycode-3.7.2.dev2.dist-info/METADATA,sha256=WPhT6Tuqlzpk14Ydb21rk6jH4_DLQf6gGR754EpHx7Y,79037
171
+ cycode-3.7.2.dev2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
172
+ cycode-3.7.2.dev2.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
173
+ cycode-3.7.2.dev2.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
174
+ cycode-3.7.2.dev2.dist-info/RECORD,,