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.
- cycode/__init__.py +1 -1
- cycode/cli/app.py +9 -10
- cycode/cli/apps/activation_manager.py +6 -6
- cycode/cli/apps/ai_guardrails/command_utils.py +1 -2
- cycode/cli/apps/ai_guardrails/hooks_manager.py +6 -7
- cycode/cli/apps/ai_guardrails/ides/__init__.py +8 -9
- cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +11 -10
- cycode/cli/apps/ai_guardrails/ides/_skill_utils.py +1 -2
- cycode/cli/apps/ai_guardrails/ides/base.py +19 -21
- cycode/cli/apps/ai_guardrails/ides/claude_code.py +54 -16
- cycode/cli/apps/ai_guardrails/ides/codex.py +54 -20
- cycode/cli/apps/ai_guardrails/ides/copilot.py +23 -25
- cycode/cli/apps/ai_guardrails/ides/cursor.py +48 -10
- cycode/cli/apps/ai_guardrails/install_command.py +3 -3
- cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +44 -22
- cycode/cli/apps/ai_guardrails/scan/handlers.py +79 -14
- cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py +103 -0
- cycode/cli/apps/ai_guardrails/scan/payload.py +13 -14
- cycode/cli/apps/ai_guardrails/scan/policy.py +3 -3
- cycode/cli/apps/ai_guardrails/scan/scan_command.py +4 -4
- cycode/cli/apps/ai_guardrails/scan/types.py +1 -0
- cycode/cli/apps/ai_guardrails/session_start_command.py +46 -21
- cycode/cli/apps/ai_guardrails/status_command.py +2 -2
- cycode/cli/apps/ai_guardrails/uninstall_command.py +2 -2
- cycode/cli/apps/api/__init__.py +3 -3
- cycode/cli/apps/api/api_command.py +6 -6
- cycode/cli/apps/api/openapi_spec.py +4 -5
- cycode/cli/apps/auth/auth_common.py +4 -4
- cycode/cli/apps/configure/configure_command.py +2 -4
- cycode/cli/apps/configure/messages.py +2 -4
- cycode/cli/apps/configure/prompts.py +5 -7
- cycode/cli/apps/ignore/ignore_command.py +7 -7
- cycode/cli/apps/mcp/mcp_command.py +11 -11
- cycode/cli/apps/report/sbom/common.py +5 -5
- cycode/cli/apps/report/sbom/sbom_command.py +2 -2
- cycode/cli/apps/report/sbom/sbom_report_file.py +1 -2
- cycode/cli/apps/report_import/sbom/sbom_command.py +3 -3
- cycode/cli/apps/sca_options.py +3 -3
- cycode/cli/apps/scan/aggregation_report.py +4 -4
- cycode/cli/apps/scan/code_scanner.py +8 -7
- cycode/cli/apps/scan/commit_range_scanner.py +10 -10
- cycode/cli/apps/scan/detection_excluder.py +2 -4
- cycode/cli/apps/scan/pre_commit/pre_commit_command.py +3 -3
- cycode/cli/apps/scan/pre_push/pre_push_command.py +3 -3
- cycode/cli/apps/scan/pre_receive/pre_receive_command.py +2 -2
- cycode/cli/apps/scan/remote_url_resolver.py +6 -8
- cycode/cli/apps/scan/repository/repository_command.py +2 -2
- cycode/cli/apps/scan/scan_command.py +2 -2
- cycode/cli/apps/scan/scan_parameters.py +1 -3
- cycode/cli/apps/scan/scan_result.py +5 -5
- cycode/cli/console.py +2 -2
- cycode/cli/exceptions/custom_exceptions.py +1 -1
- cycode/cli/exceptions/handle_errors.py +1 -3
- cycode/cli/exceptions/handle_scan_errors.py +1 -2
- cycode/cli/files_collector/commit_range_documents.py +14 -14
- cycode/cli/files_collector/models/in_memory_zip.py +3 -3
- cycode/cli/files_collector/repository_documents.py +3 -3
- cycode/cli/files_collector/sca/base_restore_dependencies.py +7 -8
- cycode/cli/files_collector/sca/go/restore_go_dependencies.py +1 -2
- cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +2 -3
- cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +3 -4
- cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +3 -4
- cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +1 -2
- cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +2 -3
- cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +2 -3
- cycode/cli/files_collector/sca/npm/workspace/coverage.py +5 -5
- cycode/cli/files_collector/sca/npm/workspace/files.py +2 -3
- cycode/cli/files_collector/sca/npm/workspace/globs.py +1 -2
- cycode/cli/files_collector/sca/npm/workspace/resolvers.py +14 -16
- cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +1 -2
- cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +2 -3
- cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +1 -2
- cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +2 -3
- cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +2 -3
- cycode/cli/files_collector/sca/sca_file_collector.py +5 -5
- cycode/cli/files_collector/zip_documents.py +1 -2
- cycode/cli/models.py +8 -8
- cycode/cli/printers/console_printer.py +9 -9
- cycode/cli/printers/json_printer.py +2 -2
- cycode/cli/printers/printer_base.py +3 -3
- cycode/cli/printers/rich_printer.py +2 -2
- cycode/cli/printers/tables/table.py +4 -4
- cycode/cli/printers/tables/table_models.py +2 -2
- cycode/cli/printers/tables/table_printer_base.py +2 -2
- cycode/cli/printers/text_printer.py +4 -4
- cycode/cli/printers/utils/code_snippet_syntax.py +1 -1
- cycode/cli/printers/utils/detection_data.py +3 -3
- cycode/cli/printers/utils/sca_ossf.py +4 -4
- cycode/cli/printers/utils/sca_policy_details.py +2 -1
- cycode/cli/user_settings/config_file_manager.py +10 -10
- cycode/cli/user_settings/configuration_manager.py +6 -6
- cycode/cli/user_settings/credentials_manager.py +5 -6
- cycode/cli/utils/get_api_client.py +5 -5
- cycode/cli/utils/git_proxy.py +8 -8
- cycode/cli/utils/host_info.py +21 -38
- cycode/cli/utils/ignore_utils.py +13 -15
- cycode/cli/utils/jwt_utils.py +2 -4
- cycode/cli/utils/path_utils.py +6 -6
- cycode/cli/utils/progress_bar.py +4 -4
- cycode/cli/utils/scan_batch.py +2 -1
- cycode/cli/utils/scan_utils.py +2 -2
- cycode/cli/utils/shell_executor.py +3 -4
- cycode/cli/utils/task_timer.py +4 -4
- cycode/cli/utils/trust_store.py +0 -17
- cycode/cli/utils/url_utils.py +1 -2
- cycode/cli/utils/version_checker.py +4 -5
- cycode/cli/utils/yaml_utils.py +1 -1
- cycode/config.py +1 -2
- cycode/cyclient/ai_security_manager_client.py +27 -21
- cycode/cyclient/auth_client.py +2 -4
- cycode/cyclient/base_token_auth_client.py +2 -2
- cycode/cyclient/client_creator.py +4 -6
- cycode/cyclient/cycode_client_base.py +10 -9
- cycode/cyclient/cycode_dev_based_client.py +1 -3
- cycode/cyclient/headers.py +1 -2
- cycode/cyclient/import_sbom_client.py +3 -4
- cycode/cyclient/models.py +88 -30
- cycode/cyclient/report_client.py +1 -2
- cycode/cyclient/scan_client.py +8 -7
- cycode/logger.py +4 -4
- {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/METADATA +12 -19
- {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/RECORD +125 -124
- {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/WHEEL +1 -1
- {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/entry_points.txt +0 -0
- {cycode-3.25.1.dev6.dist-info → cycode-3.25.2.dev2.dist-info}/licenses/LICENCE +0 -0
cycode/cli/utils/task_timer.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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()
|
cycode/cli/utils/trust_store.py
CHANGED
|
@@ -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
|
|
cycode/cli/utils/url_utils.py
CHANGED
|
@@ -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:
|
|
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
|
-
) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
cycode/cli/utils/yaml_utils.py
CHANGED
|
@@ -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] =
|
|
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) ->
|
|
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
|
|
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') ->
|
|
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:
|
|
66
|
-
block_reason:
|
|
67
|
-
error_message:
|
|
68
|
-
file_path:
|
|
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) ->
|
|
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:
|
|
109
|
-
platform_name:
|
|
110
|
-
os_version:
|
|
111
|
-
serial_number:
|
|
112
|
-
last_login_user:
|
|
113
|
-
config_files:
|
|
114
|
-
enabled_plugins:
|
|
115
|
-
skill_files:
|
|
116
|
-
user_email:
|
|
117
|
-
) ->
|
|
118
|
-
"""Report session context to the backend. Returns
|
|
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
|
|
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()
|
cycode/cyclient/auth_client.py
CHANGED
|
@@ -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) ->
|
|
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) ->
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
cycode/cyclient/headers.py
CHANGED
|
@@ -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:
|
|
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:
|
|
19
|
-
Owners:
|
|
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
|
|
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:
|
|
16
|
-
id:
|
|
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:
|
|
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:
|
|
77
|
-
scan_id:
|
|
78
|
-
err:
|
|
79
|
-
verdict:
|
|
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:
|
|
95
|
-
detections:
|
|
96
|
-
err:
|
|
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:
|
|
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:
|
|
162
|
-
scan_status:
|
|
163
|
-
results_count:
|
|
164
|
-
metadata:
|
|
165
|
-
message:
|
|
166
|
-
scan_update_at:
|
|
167
|
-
err:
|
|
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:
|
|
392
|
-
status_message:
|
|
393
|
-
storage_details:
|
|
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:
|
|
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:
|
|
484
|
-
remediation_guidelines:
|
|
485
|
-
description:
|
|
486
|
-
policy_name:
|
|
487
|
-
display_name:
|
|
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:
|
|
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)
|
cycode/cyclient/report_client.py
CHANGED
|
@@ -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:
|
|
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)
|