cycode 3.25.1.dev6__py3-none-any.whl → 3.25.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.
Files changed (125) hide show
  1. cycode/__init__.py +1 -1
  2. cycode/cli/app.py +9 -10
  3. cycode/cli/apps/activation_manager.py +6 -6
  4. cycode/cli/apps/ai_guardrails/command_utils.py +1 -2
  5. cycode/cli/apps/ai_guardrails/hooks_manager.py +6 -7
  6. cycode/cli/apps/ai_guardrails/ides/__init__.py +8 -9
  7. cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +11 -10
  8. cycode/cli/apps/ai_guardrails/ides/_skill_utils.py +1 -2
  9. cycode/cli/apps/ai_guardrails/ides/base.py +19 -21
  10. cycode/cli/apps/ai_guardrails/ides/claude_code.py +54 -16
  11. cycode/cli/apps/ai_guardrails/ides/codex.py +54 -20
  12. cycode/cli/apps/ai_guardrails/ides/copilot.py +23 -25
  13. cycode/cli/apps/ai_guardrails/ides/cursor.py +48 -10
  14. cycode/cli/apps/ai_guardrails/install_command.py +3 -3
  15. cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +44 -22
  16. cycode/cli/apps/ai_guardrails/scan/handlers.py +79 -14
  17. cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py +103 -0
  18. cycode/cli/apps/ai_guardrails/scan/payload.py +13 -14
  19. cycode/cli/apps/ai_guardrails/scan/policy.py +3 -3
  20. cycode/cli/apps/ai_guardrails/scan/scan_command.py +4 -4
  21. cycode/cli/apps/ai_guardrails/scan/types.py +1 -0
  22. cycode/cli/apps/ai_guardrails/session_start_command.py +46 -21
  23. cycode/cli/apps/ai_guardrails/status_command.py +2 -2
  24. cycode/cli/apps/ai_guardrails/uninstall_command.py +2 -2
  25. cycode/cli/apps/api/__init__.py +3 -3
  26. cycode/cli/apps/api/api_command.py +6 -6
  27. cycode/cli/apps/api/openapi_spec.py +4 -5
  28. cycode/cli/apps/auth/auth_common.py +4 -4
  29. cycode/cli/apps/configure/configure_command.py +2 -4
  30. cycode/cli/apps/configure/messages.py +2 -4
  31. cycode/cli/apps/configure/prompts.py +5 -7
  32. cycode/cli/apps/ignore/ignore_command.py +7 -7
  33. cycode/cli/apps/mcp/mcp_command.py +11 -11
  34. cycode/cli/apps/report/sbom/common.py +5 -5
  35. cycode/cli/apps/report/sbom/sbom_command.py +2 -2
  36. cycode/cli/apps/report/sbom/sbom_report_file.py +1 -2
  37. cycode/cli/apps/report_import/sbom/sbom_command.py +3 -3
  38. cycode/cli/apps/sca_options.py +3 -3
  39. cycode/cli/apps/scan/aggregation_report.py +4 -4
  40. cycode/cli/apps/scan/code_scanner.py +8 -7
  41. cycode/cli/apps/scan/commit_range_scanner.py +10 -10
  42. cycode/cli/apps/scan/detection_excluder.py +2 -4
  43. cycode/cli/apps/scan/pre_commit/pre_commit_command.py +3 -3
  44. cycode/cli/apps/scan/pre_push/pre_push_command.py +3 -3
  45. cycode/cli/apps/scan/pre_receive/pre_receive_command.py +2 -2
  46. cycode/cli/apps/scan/remote_url_resolver.py +6 -8
  47. cycode/cli/apps/scan/repository/repository_command.py +2 -2
  48. cycode/cli/apps/scan/scan_command.py +2 -2
  49. cycode/cli/apps/scan/scan_parameters.py +1 -3
  50. cycode/cli/apps/scan/scan_result.py +5 -5
  51. cycode/cli/console.py +2 -2
  52. cycode/cli/exceptions/custom_exceptions.py +1 -1
  53. cycode/cli/exceptions/handle_errors.py +1 -3
  54. cycode/cli/exceptions/handle_scan_errors.py +1 -2
  55. cycode/cli/files_collector/commit_range_documents.py +14 -14
  56. cycode/cli/files_collector/models/in_memory_zip.py +3 -3
  57. cycode/cli/files_collector/repository_documents.py +3 -3
  58. cycode/cli/files_collector/sca/base_restore_dependencies.py +7 -8
  59. cycode/cli/files_collector/sca/go/restore_go_dependencies.py +1 -2
  60. cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +2 -3
  61. cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +3 -4
  62. cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +3 -4
  63. cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +1 -2
  64. cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +2 -3
  65. cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +2 -3
  66. cycode/cli/files_collector/sca/npm/workspace/coverage.py +5 -5
  67. cycode/cli/files_collector/sca/npm/workspace/files.py +2 -3
  68. cycode/cli/files_collector/sca/npm/workspace/globs.py +1 -2
  69. cycode/cli/files_collector/sca/npm/workspace/resolvers.py +14 -16
  70. cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +1 -2
  71. cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +2 -3
  72. cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +1 -2
  73. cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +2 -3
  74. cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +2 -3
  75. cycode/cli/files_collector/sca/sca_file_collector.py +5 -5
  76. cycode/cli/files_collector/zip_documents.py +1 -2
  77. cycode/cli/models.py +8 -8
  78. cycode/cli/printers/console_printer.py +9 -9
  79. cycode/cli/printers/json_printer.py +2 -2
  80. cycode/cli/printers/printer_base.py +3 -3
  81. cycode/cli/printers/rich_printer.py +2 -2
  82. cycode/cli/printers/tables/table.py +4 -4
  83. cycode/cli/printers/tables/table_models.py +2 -2
  84. cycode/cli/printers/tables/table_printer_base.py +2 -2
  85. cycode/cli/printers/text_printer.py +4 -4
  86. cycode/cli/printers/utils/code_snippet_syntax.py +1 -1
  87. cycode/cli/printers/utils/detection_data.py +3 -3
  88. cycode/cli/printers/utils/sca_ossf.py +4 -4
  89. cycode/cli/printers/utils/sca_policy_details.py +2 -1
  90. cycode/cli/user_settings/config_file_manager.py +10 -10
  91. cycode/cli/user_settings/configuration_manager.py +6 -6
  92. cycode/cli/user_settings/credentials_manager.py +5 -6
  93. cycode/cli/utils/get_api_client.py +5 -5
  94. cycode/cli/utils/git_proxy.py +8 -8
  95. cycode/cli/utils/host_info.py +21 -38
  96. cycode/cli/utils/ignore_utils.py +13 -15
  97. cycode/cli/utils/jwt_utils.py +2 -4
  98. cycode/cli/utils/path_utils.py +6 -6
  99. cycode/cli/utils/progress_bar.py +4 -4
  100. cycode/cli/utils/scan_batch.py +2 -1
  101. cycode/cli/utils/scan_utils.py +2 -2
  102. cycode/cli/utils/shell_executor.py +3 -4
  103. cycode/cli/utils/task_timer.py +4 -4
  104. cycode/cli/utils/trust_store.py +0 -17
  105. cycode/cli/utils/url_utils.py +1 -2
  106. cycode/cli/utils/version_checker.py +4 -5
  107. cycode/cli/utils/yaml_utils.py +1 -1
  108. cycode/config.py +1 -2
  109. cycode/cyclient/ai_security_manager_client.py +27 -21
  110. cycode/cyclient/auth_client.py +2 -4
  111. cycode/cyclient/base_token_auth_client.py +2 -2
  112. cycode/cyclient/client_creator.py +4 -6
  113. cycode/cyclient/cycode_client_base.py +10 -9
  114. cycode/cyclient/cycode_dev_based_client.py +1 -3
  115. cycode/cyclient/headers.py +1 -2
  116. cycode/cyclient/import_sbom_client.py +3 -4
  117. cycode/cyclient/models.py +88 -30
  118. cycode/cyclient/report_client.py +1 -2
  119. cycode/cyclient/scan_client.py +8 -7
  120. cycode/logger.py +4 -4
  121. {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/METADATA +12 -19
  122. {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/RECORD +125 -124
  123. {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/WHEEL +1 -1
  124. {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/entry_points.txt +0 -0
  125. {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/licenses/LICENCE +0 -0
@@ -1,11 +1,11 @@
1
1
  from _thread import interrupt_main
2
+ from collections.abc import Callable
2
3
  from threading import Event, Thread
3
4
  from types import TracebackType
4
- from typing import Callable, Optional
5
5
 
6
6
 
7
7
  class FunctionContext:
8
- def __init__(self, function: Callable, args: Optional[list] = None, kwargs: Optional[dict] = None) -> None:
8
+ def __init__(self, function: Callable, args: list | None = None, kwargs: dict | None = None) -> None:
9
9
  self.function = function
10
10
  self.args = args or []
11
11
  self.kwargs = kwargs or {}
@@ -54,7 +54,7 @@ class TimeoutAfter:
54
54
  the default option is to interrupt main thread
55
55
  """
56
56
 
57
- def __init__(self, timeout: int, quit_function: Optional[FunctionContext] = None) -> None:
57
+ def __init__(self, timeout: int, quit_function: FunctionContext | None = None) -> None:
58
58
  self.timeout = timeout
59
59
  self._quit_function = quit_function or FunctionContext(function=self.timeout_function)
60
60
  self.timer = TimerThread(timeout, quit_function=self._quit_function)
@@ -64,7 +64,7 @@ class TimeoutAfter:
64
64
  self.timer.start()
65
65
 
66
66
  def __exit__(
67
- self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
67
+ self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
68
68
  ) -> None:
69
69
  if self.timeout:
70
70
  self.timer.stop()
@@ -1,21 +1,12 @@
1
- import sys
2
-
3
1
  from cycode import config
4
2
  from cycode.cli import consts
5
3
  from cycode.logger import get_logger
6
4
 
7
5
  logger = get_logger('Trust Store')
8
6
 
9
- # truststore requires Python 3.10+, so on 3.9 the OS trust store is unavailable
10
- _MIN_PYTHON_VERSION = (3, 10)
11
-
12
7
  _installed = False
13
8
 
14
9
 
15
- def is_supported() -> bool:
16
- return sys.version_info >= _MIN_PYTHON_VERSION
17
-
18
-
19
10
  def is_enabled() -> bool:
20
11
  return config.get_val_as_bool(consts.ENABLE_TRUSTSTORE_ENV_VAR_NAME)
21
12
 
@@ -45,14 +36,6 @@ def install() -> bool:
45
36
  )
46
37
  return False
47
38
 
48
- if not is_supported():
49
- logger.warning(
50
- 'OS trust store requires Python %s+, using the bundled CA store (certifi). Current version: %s',
51
- '.'.join(map(str, _MIN_PYTHON_VERSION)),
52
- '.'.join(map(str, sys.version_info[:3])),
53
- )
54
- return False
55
-
56
39
  try:
57
40
  import truststore
58
41
 
@@ -1,4 +1,3 @@
1
- from typing import Optional
2
1
  from urllib.parse import urlparse, urlunparse
3
2
 
4
3
  from cycode.logger import get_logger
@@ -6,7 +5,7 @@ from cycode.logger import get_logger
6
5
  logger = get_logger('URL Utils')
7
6
 
8
7
 
9
- def sanitize_repository_url(url: Optional[str]) -> Optional[str]:
8
+ def sanitize_repository_url(url: str | None) -> str | None:
10
9
  """Remove credentials (username, password, tokens) from repository URL.
11
10
 
12
11
  This function sanitizes repository URLs to prevent sending PAT tokens or other
@@ -2,7 +2,6 @@ import os
2
2
  import re
3
3
  import time
4
4
  from pathlib import Path
5
- from typing import Optional
6
5
 
7
6
  from cycode.cli.console import console
8
7
  from cycode.cli.user_settings.configuration_manager import ConfigurationManager
@@ -19,7 +18,7 @@ def _compare_versions(
19
18
  current_is_pre: bool,
20
19
  latest_is_pre: bool,
21
20
  latest_version: str,
22
- ) -> Optional[str]:
21
+ ) -> str | None:
23
22
  """Compare version numbers and determine if an update is needed.
24
23
 
25
24
  Implements version comparison logic with special handling for pre-release versions:
@@ -43,7 +42,7 @@ def _compare_versions(
43
42
  return None
44
43
 
45
44
  # Compare version numbers
46
- for current, latest in zip(current_parts, latest_parts):
45
+ for current, latest in zip(current_parts, latest_parts, strict=False):
47
46
  if latest > current:
48
47
  return latest_version
49
48
  if current > latest:
@@ -78,7 +77,7 @@ class VersionChecker(CycodeClientBase):
78
77
  config_dir = configuration_manager.global_config_file_manager.get_config_directory_path()
79
78
  self.cache_file = Path(config_dir) / '.version_check'
80
79
 
81
- def get_latest_version(self) -> Optional[str]:
80
+ def get_latest_version(self) -> str | None:
82
81
  """Fetch the latest version of the package from PyPI.
83
82
 
84
83
  Makes an HTTP request to PyPI's JSON API to get the latest version information.
@@ -160,7 +159,7 @@ class VersionChecker(CycodeClientBase):
160
159
  except Exception as e:
161
160
  logger.debug('Failed to update version check cache file: %s', {'file': self.cache_file}, exc_info=e)
162
161
 
163
- def check_for_update(self, current_version: str, use_cache: bool = True) -> Optional[str]:
162
+ def check_for_update(self, current_version: str, use_cache: bool = True) -> str | None:
164
163
  """Check if an update is available for the current version.
165
164
 
166
165
  Respects the update check frequency (daily/weekly) based on the version type
@@ -15,7 +15,7 @@ def _deep_update(source: dict[Hashable, Any], overrides: dict[Hashable, Any]) ->
15
15
  if isinstance(value, dict) and value:
16
16
  source[key] = _deep_update(source.get(key, {}), value)
17
17
  else:
18
- source[key] = overrides[key]
18
+ source[key] = value
19
19
 
20
20
  return source
21
21
 
cycode/config.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  import os
3
- from typing import Optional
4
3
  from urllib.parse import urlparse
5
4
 
6
5
  from cycode.cli import consts
@@ -26,7 +25,7 @@ def get_val_as_bool(key: str, default: bool = False) -> bool:
26
25
  return configuration[key].lower() in {'true', '1', 'yes', 'y', 'on', 'enabled'}
27
26
 
28
27
 
29
- def get_val_as_int(key: str) -> Optional[int]:
28
+ def get_val_as_int(key: str) -> int | None:
30
29
  val = configuration.get(key)
31
30
  if not val:
32
31
  return None
@@ -1,10 +1,11 @@
1
1
  """Client for AI Security Manager service."""
2
2
 
3
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError
6
6
  from cycode.cyclient.cycode_client_base import CycodeClientBase
7
7
  from cycode.cyclient.logger import logger
8
+ from cycode.cyclient.models import SessionContextResponse, SessionContextResponseSchema
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
@@ -31,7 +32,7 @@ class AISecurityManagerClient:
31
32
  return f'{service_name}/{path}'
32
33
  return path
33
34
 
34
- def create_conversation(self, payload: 'AIHookPayload') -> Optional[str]:
35
+ def create_conversation(self, payload: 'AIHookPayload') -> str | None:
35
36
  """Creates an AI conversation from hook payload."""
36
37
  conversation_id = payload.conversation_id
37
38
  if not conversation_id:
@@ -62,10 +63,10 @@ class AISecurityManagerClient:
62
63
  payload: 'AIHookPayload',
63
64
  event_type: 'AiHookEventType',
64
65
  outcome: 'AIHookOutcome',
65
- scan_id: Optional[str] = None,
66
- block_reason: Optional['BlockReason'] = None,
67
- error_message: Optional[str] = None,
68
- file_path: Optional[str] = None,
66
+ scan_id: str | None = None,
67
+ block_reason: 'BlockReason | None' = None,
68
+ error_message: str | None = None,
69
+ file_path: str | None = None,
69
70
  ) -> None:
70
71
  """Create an AI hook event from hook payload."""
71
72
  conversation_id = payload.conversation_id
@@ -94,7 +95,7 @@ class AISecurityManagerClient:
94
95
  logger.debug('Failed to create AI hook event', exc_info=e)
95
96
  # Don't fail the hook if tracking fails
96
97
 
97
- def get_resolved_guardrails(self) -> Optional[dict]:
98
+ def get_resolved_guardrails(self) -> dict | None:
98
99
  """Fetch the tenant's resolved guardrail config (per-agent modes + sensitive-path globs)."""
99
100
  try:
100
101
  response = self.client.get(self._build_endpoint_path(self._RESOLVED_GUARDRAILS_PATH))
@@ -105,17 +106,17 @@ class AISecurityManagerClient:
105
106
 
106
107
  def report_session_context(
107
108
  self,
108
- hostname: Optional[str] = None,
109
- platform_name: Optional[str] = None,
110
- os_version: Optional[str] = None,
111
- serial_number: Optional[str] = None,
112
- last_login_user: Optional[str] = None,
113
- config_files: Optional[list[dict]] = None,
114
- enabled_plugins: Optional[dict] = None,
115
- skill_files: Optional[list[dict]] = None,
116
- user_email: Optional[str] = None,
117
- ) -> bool:
118
- """Report session context to the backend. Returns whether the report was accepted."""
109
+ hostname: str | None = None,
110
+ platform_name: str | None = None,
111
+ os_version: str | None = None,
112
+ serial_number: str | None = None,
113
+ last_login_user: str | None = None,
114
+ config_files: list[dict] | None = None,
115
+ enabled_plugins: dict | None = None,
116
+ skill_files: list[dict] | None = None,
117
+ user_email: str | None = None,
118
+ ) -> SessionContextResponse | None:
119
+ """Report session context to the backend. Returns None when the report was not accepted."""
119
120
  body: dict = {
120
121
  'hostname': hostname,
121
122
  'platform_name': platform_name,
@@ -129,9 +130,14 @@ class AISecurityManagerClient:
129
130
  }
130
131
 
131
132
  try:
132
- self.client.post(self._build_endpoint_path(self._SESSION_CONTEXT_PATH), body=body)
133
- return True
133
+ response = self.client.post(self._build_endpoint_path(self._SESSION_CONTEXT_PATH), body=body)
134
134
  except Exception as e:
135
135
  logger.debug('Failed to report session context', exc_info=e)
136
136
  # Don't fail the session if reporting fails
137
- return False
137
+ return None
138
+
139
+ try:
140
+ return SessionContextResponseSchema().load(response.json())
141
+ except Exception as e:
142
+ logger.debug('Failed to parse the session context response', exc_info=e)
143
+ return SessionContextResponse()
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from requests import Request, Response
4
2
 
5
3
  from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError
@@ -24,7 +22,7 @@ class AuthClient:
24
22
  response = self.cycode_client.post(url_path=path, body=body)
25
23
  return self.parse_start_session_response(response)
26
24
 
27
- def get_api_token(self, session_id: str, code_verifier: str) -> Optional[models.ApiTokenGenerationPollingResponse]:
25
+ def get_api_token(self, session_id: str, code_verifier: str) -> models.ApiTokenGenerationPollingResponse | None:
28
26
  path = f'{self.AUTH_CONTROLLER_PATH}/token'
29
27
  body = {'session_id': session_id, 'code_verifier': code_verifier}
30
28
  try:
@@ -40,7 +38,7 @@ class AuthClient:
40
38
  return models.AuthenticationSessionSchema().load(response.json())
41
39
 
42
40
  @staticmethod
43
- def parse_api_token_polling_response(response: Response) -> Optional[models.ApiTokenGenerationPollingResponse]:
41
+ def parse_api_token_polling_response(response: Response) -> models.ApiTokenGenerationPollingResponse | None:
44
42
  try:
45
43
  return models.ApiTokenGenerationPollingResponseSchema().load(response.json())
46
44
  except Exception:
@@ -2,7 +2,7 @@ import secrets
2
2
  import time
3
3
  from abc import ABC, abstractmethod
4
4
  from threading import Lock
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  import arrow
8
8
  from requests import Response
@@ -97,7 +97,7 @@ class BaseTokenAuthClient(CycodeClient, ABC):
97
97
  jwt_creator = self._create_jwt_creator()
98
98
  self._credentials_manager.update_access_token(self._access_token, self._expires_in.timestamp(), jwt_creator)
99
99
 
100
- def get_request_headers(self, additional_headers: Optional[dict] = None, without_auth: bool = False) -> dict:
100
+ def get_request_headers(self, additional_headers: dict | None = None, without_auth: bool = False) -> dict:
101
101
  headers = super().get_request_headers(additional_headers=additional_headers)
102
102
 
103
103
  if not without_auth:
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from cycode.cyclient.ai_security_manager_client import AISecurityManagerClient
4
2
  from cycode.cyclient.ai_security_manager_service_config import (
5
3
  DefaultAISecurityManagerServiceConfig,
@@ -17,7 +15,7 @@ from cycode.cyclient.scan_config_base import DefaultScanConfig, DevScanConfig
17
15
 
18
16
 
19
17
  def create_scan_client(
20
- client_id: str, client_secret: Optional[str] = None, hide_response_log: bool = False, id_token: Optional[str] = None
18
+ client_id: str, client_secret: str | None = None, hide_response_log: bool = False, id_token: str | None = None
21
19
  ) -> ScanClient:
22
20
  if dev_mode:
23
21
  client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
@@ -33,7 +31,7 @@ def create_scan_client(
33
31
 
34
32
 
35
33
  def create_report_client(
36
- client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
34
+ client_id: str, client_secret: str | None = None, _: bool = False, id_token: str | None = None
37
35
  ) -> ReportClient:
38
36
  if dev_mode:
39
37
  client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
@@ -45,7 +43,7 @@ def create_report_client(
45
43
 
46
44
 
47
45
  def create_import_sbom_client(
48
- client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
46
+ client_id: str, client_secret: str | None = None, _: bool = False, id_token: str | None = None
49
47
  ) -> ImportSbomClient:
50
48
  if dev_mode:
51
49
  client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
@@ -57,7 +55,7 @@ def create_import_sbom_client(
57
55
 
58
56
 
59
57
  def create_ai_security_manager_client(
60
- client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None
58
+ client_id: str, client_secret: str | None = None, _: bool = False, id_token: str | None = None
61
59
  ) -> AISecurityManagerClient:
62
60
  if dev_mode:
63
61
  client = CycodeDevBasedClient(DEV_CYCODE_API_URL)
@@ -2,8 +2,9 @@ import functools
2
2
  import os
3
3
  import platform
4
4
  import ssl
5
+ from collections.abc import Callable
5
6
  from io import BytesIO
6
- from typing import TYPE_CHECKING, Callable, ClassVar, Optional
7
+ from typing import TYPE_CHECKING, ClassVar
7
8
 
8
9
  import requests
9
10
  from requests import Response, exceptions
@@ -114,7 +115,7 @@ def _should_retry_exception(exception: BaseException) -> bool:
114
115
  class UploadProgressTracker:
115
116
  """File-like wrapper that tracks bytes read during upload and fires a progress callback."""
116
117
 
117
- def __init__(self, data: bytes, callback: Optional[Callable[[int, int], None]]) -> None:
118
+ def __init__(self, data: bytes, callback: Callable[[int, int], None] | None) -> None:
118
119
  self._io = BytesIO(data)
119
120
  self._callback = callback
120
121
  self.bytes_read = 0
@@ -146,13 +147,13 @@ class CycodeClientBase:
146
147
  def enrich_user_agent(user_agent_suffix: str) -> None:
147
148
  CycodeClientBase.MANDATORY_HEADERS['User-Agent'] += f' {user_agent_suffix}'
148
149
 
149
- def post(self, url_path: str, body: Optional[dict] = None, headers: Optional[dict] = None, **kwargs) -> Response:
150
+ def post(self, url_path: str, body: dict | None = None, headers: dict | None = None, **kwargs) -> Response:
150
151
  return self._execute(method='post', endpoint=url_path, json=body, headers=headers, **kwargs)
151
152
 
152
- def put(self, url_path: str, body: Optional[dict] = None, headers: Optional[dict] = None, **kwargs) -> Response:
153
+ def put(self, url_path: str, body: dict | None = None, headers: dict | None = None, **kwargs) -> Response:
153
154
  return self._execute(method='put', endpoint=url_path, json=body, headers=headers, **kwargs)
154
155
 
155
- def get(self, url_path: str, headers: Optional[dict] = None, **kwargs) -> Response:
156
+ def get(self, url_path: str, headers: dict | None = None, **kwargs) -> Response:
156
157
  return self._execute(method='get', endpoint=url_path, headers=headers, **kwargs)
157
158
 
158
159
  def post_multipart(
@@ -160,7 +161,7 @@ class CycodeClientBase:
160
161
  url_path: str,
161
162
  form_fields: dict,
162
163
  files: dict,
163
- on_upload_progress: Optional[Callable[[int, int], None]] = None,
164
+ on_upload_progress: Callable[[int, int], None] | None = None,
164
165
  hide_response_content_log: bool = False,
165
166
  ) -> Response:
166
167
  """POST a multipart form body with optional upload progress tracking and retry."""
@@ -191,7 +192,7 @@ class CycodeClientBase:
191
192
  url: str,
192
193
  body: bytes,
193
194
  content_type: str,
194
- on_upload_progress: Optional[Callable[[int, int], None]],
195
+ on_upload_progress: Callable[[int, int], None] | None,
195
196
  hide_response_content_log: bool,
196
197
  ) -> Response:
197
198
  # Wrap the body in a fresh tracker each attempt so bytes_read starts from zero.
@@ -227,7 +228,7 @@ class CycodeClientBase:
227
228
  self,
228
229
  method: str,
229
230
  endpoint: str,
230
- headers: Optional[dict] = None,
231
+ headers: dict | None = None,
231
232
  without_auth: bool = False,
232
233
  hide_response_content_log: bool = False,
233
234
  **kwargs,
@@ -253,7 +254,7 @@ class CycodeClientBase:
253
254
  except Exception as e:
254
255
  self._handle_exception(e)
255
256
 
256
- def get_request_headers(self, additional_headers: Optional[dict] = None, **kwargs) -> dict[str, str]:
257
+ def get_request_headers(self, additional_headers: dict | None = None, **kwargs) -> dict[str, str]:
257
258
  if additional_headers is None:
258
259
  return self.MANDATORY_HEADERS.copy()
259
260
  return {**self.MANDATORY_HEADERS, **additional_headers}
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from cycode.cyclient.config import dev_tenant_id
4
2
  from cycode.cyclient.cycode_client_base import CycodeClientBase
5
3
 
@@ -12,7 +10,7 @@ class CycodeDevBasedClient(CycodeClientBase):
12
10
  def __init__(self, api_url: str) -> None:
13
11
  super().__init__(api_url)
14
12
 
15
- def get_request_headers(self, additional_headers: Optional[dict] = None, **_) -> dict[str, str]:
13
+ def get_request_headers(self, additional_headers: dict | None = None, **_) -> dict[str, str]:
16
14
  headers = super().get_request_headers(additional_headers=additional_headers)
17
15
  headers['X-Tenant-Id'] = dev_tenant_id
18
16
 
@@ -1,5 +1,4 @@
1
1
  import platform
2
- from typing import Optional
3
2
  from uuid import uuid4
4
3
 
5
4
  from cycode import __version__
@@ -25,7 +24,7 @@ def get_cli_user_agent() -> str:
25
24
 
26
25
 
27
26
  class _CorrelationId:
28
- _id: Optional[str] = None
27
+ _id: str | None = None
29
28
 
30
29
  def get_correlation_id(self) -> str:
31
30
  """Get correlation ID.
@@ -1,6 +1,5 @@
1
1
  import dataclasses
2
2
  from pathlib import Path
3
- from typing import Optional
4
3
 
5
4
  from requests import Response
6
5
 
@@ -15,8 +14,8 @@ class ImportSbomParameters:
15
14
  Name: str
16
15
  Vendor: str
17
16
  BusinessImpact: BusinessImpactOption
18
- Labels: Optional[list[str]]
19
- Owners: Optional[list[str]]
17
+ Labels: list[str] | None
18
+ Owners: list[str] | None
20
19
 
21
20
  def _owners_to_ids(self) -> list[str]:
22
21
  return []
@@ -71,7 +70,7 @@ class ImportSbomClient:
71
70
  member_details = self.parse_requested_member_details_response(response)
72
71
 
73
72
  if not member_details.items:
74
- raise Exception(
73
+ raise Exception( # noqa: TRY002
75
74
  f"Failed to find user with email '{email}'. Verify this email is registered to Cycode platform"
76
75
  )
77
76
  return member_details.items.pop(0).external_id
cycode/cyclient/models.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from dataclasses import dataclass
2
- from typing import Any, Optional
2
+ from enum import Enum
3
+ from typing import Any
3
4
 
4
- from marshmallow import EXCLUDE, Schema, fields, post_load
5
+ from marshmallow import EXCLUDE, Schema, fields, missing, post_load
5
6
 
6
7
 
7
8
  class Detection(Schema):
@@ -12,8 +13,8 @@ class Detection(Schema):
12
13
  message: str,
13
14
  detection_details: dict,
14
15
  detection_rule_id: str,
15
- severity: Optional[str] = None,
16
- id: Optional[str] = None,
16
+ severity: str | None = None,
17
+ id: str | None = None,
17
18
  ) -> None:
18
19
  super().__init__()
19
20
  self.id = id
@@ -61,7 +62,7 @@ class DetectionSchema(Schema):
61
62
 
62
63
 
63
64
  class DetectionsPerFile(Schema):
64
- def __init__(self, file_name: str, detections: list[Detection], commit_id: Optional[str] = None) -> None:
65
+ def __init__(self, file_name: str, detections: list[Detection], commit_id: str | None = None) -> None:
65
66
  super().__init__()
66
67
  self.file_name = file_name
67
68
  self.detections = detections
@@ -73,10 +74,10 @@ class ZippedFileScanResult(Schema):
73
74
  self,
74
75
  did_detect: bool,
75
76
  detections_per_file: list[DetectionsPerFile],
76
- report_url: Optional[str] = None,
77
- scan_id: Optional[str] = None,
78
- err: Optional[str] = None,
79
- verdict: Optional[str] = None,
77
+ report_url: str | None = None,
78
+ scan_id: str | None = None,
79
+ err: str | None = None,
80
+ verdict: str | None = None,
80
81
  ) -> None:
81
82
  super().__init__()
82
83
  self.did_detect = did_detect
@@ -91,9 +92,9 @@ class ScanResult(Schema):
91
92
  def __init__(
92
93
  self,
93
94
  did_detect: bool,
94
- scan_id: Optional[str] = None,
95
- detections: Optional[list[Detection]] = None,
96
- err: Optional[str] = None,
95
+ scan_id: str | None = None,
96
+ detections: list[Detection] | None = None,
97
+ err: str | None = None,
97
98
  ) -> None:
98
99
  super().__init__()
99
100
  self.did_detect = did_detect
@@ -137,7 +138,7 @@ class UploadLinkResponseSchema(Schema):
137
138
 
138
139
 
139
140
  class ScanInitializationResponse(Schema):
140
- def __init__(self, scan_id: Optional[str] = None, err: Optional[str] = None) -> None:
141
+ def __init__(self, scan_id: str | None = None, err: str | None = None) -> None:
141
142
  super().__init__()
142
143
  self.scan_id = scan_id
143
144
  self.err = err
@@ -158,13 +159,13 @@ class ScanInitializationResponseSchema(Schema):
158
159
  class ScanDetailsResponse(Schema):
159
160
  def __init__(
160
161
  self,
161
- id: Optional[str] = None,
162
- scan_status: Optional[str] = None,
163
- results_count: Optional[int] = None,
164
- metadata: Optional[str] = None,
165
- message: Optional[str] = None,
166
- scan_update_at: Optional[str] = None,
167
- err: Optional[str] = None,
162
+ id: str | None = None,
163
+ scan_status: str | None = None,
164
+ results_count: int | None = None,
165
+ metadata: str | None = None,
166
+ message: str | None = None,
167
+ scan_update_at: str | None = None,
168
+ err: str | None = None,
168
169
  ) -> None:
169
170
  super().__init__()
170
171
  self.id = id
@@ -388,9 +389,9 @@ class SbomReportStorageDetailsSchema(Schema):
388
389
  class ReportExecution:
389
390
  id: int
390
391
  status: str
391
- error_message: Optional[str] = None
392
- status_message: Optional[str] = None
393
- storage_details: Optional[SbomReportStorageDetails] = None
392
+ error_message: str | None = None
393
+ status_message: str | None = None
394
+ storage_details: SbomReportStorageDetails | None = None
394
395
 
395
396
 
396
397
  class ReportExecutionSchema(Schema):
@@ -444,7 +445,7 @@ class MemberSchema(Schema):
444
445
  class MemberDetails:
445
446
  items: list[Member]
446
447
  page_size: int
447
- next_page_token: Optional[str]
448
+ next_page_token: str | None
448
449
 
449
450
 
450
451
  class RequestedMemberDetailsResultSchema(Schema):
@@ -480,11 +481,11 @@ class ClassificationDataSchema(Schema):
480
481
  class DetectionRule:
481
482
  classification_data: list[ClassificationData]
482
483
  detection_rule_id: str
483
- custom_remediation_guidelines: Optional[str] = None
484
- remediation_guidelines: Optional[str] = None
485
- description: Optional[str] = None
486
- policy_name: Optional[str] = None
487
- display_name: Optional[str] = None
484
+ custom_remediation_guidelines: str | None = None
485
+ remediation_guidelines: str | None = None
486
+ description: str | None = None
487
+ policy_name: str | None = None
488
+ display_name: str | None = None
488
489
 
489
490
 
490
491
  class DetectionRuleSchema(Schema):
@@ -508,7 +509,7 @@ class DetectionRuleSchema(Schema):
508
509
  class ScanResultsSyncFlow:
509
510
  id: str
510
511
  detection_messages: list[dict]
511
- verdict: Optional[str] = None
512
+ verdict: str | None = None
512
513
 
513
514
 
514
515
  class ScanResultsSyncFlowSchema(Schema):
@@ -578,3 +579,60 @@ class ScanConfigurationSchema(Schema):
578
579
  @post_load
579
580
  def build_dto(self, data: dict[str, Any], **_) -> 'ScanConfiguration':
580
581
  return ScanConfiguration(**data)
582
+
583
+
584
+ class McpServerAuthorizationStatus(str, Enum):
585
+ AUTHORIZED = 'Authorized'
586
+ UNREVIEWED = 'Unreviewed'
587
+ UNAUTHORIZED = 'Unauthorized'
588
+
589
+ @classmethod
590
+ def parse(cls, raw_status: object) -> 'McpServerAuthorizationStatus':
591
+ """An unknown status reads as Unreviewed: no decision was made on the server."""
592
+ lowered = str(raw_status or '').lower()
593
+ return next((status for status in cls if status.value.lower() == lowered), cls.UNREVIEWED)
594
+
595
+
596
+ class McpServerAuthorizationStatusField(fields.Field):
597
+ def _serialize(self, value: McpServerAuthorizationStatus | None, *_, **__) -> str | None:
598
+ return value.value if value is not None else None
599
+
600
+ def deserialize(self, value: object, *_, **__) -> McpServerAuthorizationStatus:
601
+ return McpServerAuthorizationStatus.parse(None if value is missing else value)
602
+
603
+
604
+ @dataclass
605
+ class McpServerStatus:
606
+ alias: str | None = None
607
+ normalized_id: str | None = None
608
+ status: McpServerAuthorizationStatus = McpServerAuthorizationStatus.UNREVIEWED
609
+
610
+
611
+ class McpServerStatusSchema(Schema):
612
+ class Meta:
613
+ unknown = EXCLUDE
614
+
615
+ alias = fields.String(allow_none=True, load_default=None)
616
+ normalized_id = fields.String(allow_none=True, load_default=None)
617
+ status = McpServerAuthorizationStatusField()
618
+
619
+ @post_load
620
+ def build_dto(self, data: dict[str, Any], **_) -> McpServerStatus:
621
+ return McpServerStatus(**data)
622
+
623
+
624
+ @dataclass
625
+ class SessionContextResponse:
626
+ # None when the response carries no (or malformed) MCP server statuses.
627
+ mcp_servers: list[McpServerStatus] | None = None
628
+
629
+
630
+ class SessionContextResponseSchema(Schema):
631
+ class Meta:
632
+ unknown = EXCLUDE
633
+
634
+ mcp_servers = fields.List(fields.Nested(McpServerStatusSchema), allow_none=True, load_default=None)
635
+
636
+ @post_load
637
+ def build_dto(self, data: dict[str, Any], **_) -> SessionContextResponse:
638
+ return SessionContextResponse(**data)
@@ -1,6 +1,5 @@
1
1
  import dataclasses
2
2
  import json
3
- from typing import Optional
4
3
 
5
4
  from requests import Response
6
5
 
@@ -45,7 +44,7 @@ class ReportClient:
45
44
  self.client = client
46
45
 
47
46
  def request_sbom_report_execution(
48
- self, params: ReportParameters, zip_file: InMemoryZip = None, repository_url: Optional[str] = None
47
+ self, params: ReportParameters, zip_file: InMemoryZip = None, repository_url: str | None = None
49
48
  ) -> models.ReportExecution:
50
49
  report_type = 'zipped-file' if zip_file else 'repository-url'
51
50
  url_path = f'{self.SERVICE_NAME}/{self.CREATE_SBOM_REPORT_REQUEST_PATH}'.format(report_type=report_type)