cycode 3.24.1.dev1__py3-none-any.whl → 3.24.1.dev3__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/apps/ai_guardrails/scan/handlers.py +3 -4
- cycode/cli/apps/ai_guardrails/scan/utils.py +59 -1
- cycode/cli/apps/scan/code_scanner.py +76 -21
- cycode/cli/apps/scan/commit_range_scanner.py +3 -0
- cycode/cli/consts.py +6 -0
- cycode/cli/exceptions/handle_scan_errors.py +8 -0
- cycode/cli/files_collector/models/in_memory_zip.py +74 -9
- cycode/cli/utils/host_info.py +5 -0
- cycode/cli/utils/scan_utils.py +0 -23
- {cycode-3.24.1.dev1.dist-info → cycode-3.24.1.dev3.dist-info}/METADATA +1 -1
- {cycode-3.24.1.dev1.dist-info → cycode-3.24.1.dev3.dist-info}/RECORD +15 -15
- {cycode-3.24.1.dev1.dist-info → cycode-3.24.1.dev3.dist-info}/WHEEL +0 -0
- {cycode-3.24.1.dev1.dist-info → cycode-3.24.1.dev3.dist-info}/entry_points.txt +0 -0
- {cycode-3.24.1.dev1.dist-info → cycode-3.24.1.dev3.dist-info}/licenses/LICENCE +0 -0
cycode/__init__.py
CHANGED
|
@@ -5,4 +5,4 @@ import time as _time
|
|
|
5
5
|
# end-to-end scan duration from the moment the user actually triggered it.
|
|
6
6
|
_BOOT_WALL: float = _time.time()
|
|
7
7
|
|
|
8
|
-
__version__ = '3.24.1.
|
|
8
|
+
__version__ = '3.24.1.dev3' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
|
|
@@ -30,7 +30,7 @@ from cycode.cli.apps.ai_guardrails.scan.types import (
|
|
|
30
30
|
AIHookOutcome,
|
|
31
31
|
BlockReason,
|
|
32
32
|
)
|
|
33
|
-
from cycode.cli.apps.ai_guardrails.scan.utils import is_denied_path, truncate_utf8
|
|
33
|
+
from cycode.cli.apps.ai_guardrails.scan.utils import build_violation_summary, is_denied_path, truncate_utf8
|
|
34
34
|
from cycode.cli.apps.scan.code_scanner import _get_scan_documents_thread_func
|
|
35
35
|
from cycode.cli.apps.scan.scan_parameters import get_scan_parameters
|
|
36
36
|
from cycode.cli.cli_types import ScanTypeOption, SeverityOption
|
|
@@ -38,7 +38,6 @@ from cycode.cli.files_collector.file_excluder import is_path_configured_in_exclu
|
|
|
38
38
|
from cycode.cli.models import Document
|
|
39
39
|
from cycode.cli.utils.host_info import get_hostname, get_serial_number
|
|
40
40
|
from cycode.cli.utils.progress_bar import DummyProgressBar, ScanProgressBarSection
|
|
41
|
-
from cycode.cli.utils.scan_utils import build_violation_summary
|
|
42
41
|
from cycode.logger import get_logger
|
|
43
42
|
|
|
44
43
|
logger = get_logger('AI Guardrails')
|
|
@@ -76,7 +75,7 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
|
|
|
76
75
|
block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[AiHookEventType.PROMPT]
|
|
77
76
|
if effective_mode == GuardrailsMode.BLOCK:
|
|
78
77
|
outcome = AIHookOutcome.BLOCKED
|
|
79
|
-
user_message = f'
|
|
78
|
+
user_message = f'Remove secrets before sending. {violation_summary}'
|
|
80
79
|
return HookDecision.deny(AiHookEventType.PROMPT, user_message)
|
|
81
80
|
outcome = AIHookOutcome.WARNED
|
|
82
81
|
return HookDecision.allow(AiHookEventType.PROMPT)
|
|
@@ -283,7 +282,7 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
|
|
|
283
282
|
event_type=AiHookEventType.MCP_EXECUTION,
|
|
284
283
|
deny_message=lambda v: f'Cycode blocked MCP tool call "{tool}". {v}',
|
|
285
284
|
deny_agent_message='Do not pass secrets to tools. Use secret references (name/id) instead.',
|
|
286
|
-
ask_message=lambda v: f'
|
|
285
|
+
ask_message=lambda v: f'Allow MCP tool call "{tool}"? {v}',
|
|
287
286
|
ask_agent_message='Possible secrets detected in tool arguments; proceed with caution.',
|
|
288
287
|
),
|
|
289
288
|
scan_text=args_text,
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Utility functions for AI guardrails.
|
|
3
3
|
|
|
4
|
-
Includes JSON parsing, path matching,
|
|
4
|
+
Includes JSON parsing, path matching, text handling and hook-message utilities.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
import sys
|
|
10
|
+
from collections import defaultdict
|
|
10
11
|
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
11
13
|
|
|
12
14
|
from cycode.cli.apps.ai_guardrails.scan.policy import get_policy_value
|
|
15
|
+
from cycode.cli.cli_types import SeverityOption
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from cycode.cli.models import LocalScanResult
|
|
19
|
+
|
|
20
|
+
# Keeps the hook message readable when a single file trips dozens of detections
|
|
21
|
+
MAX_VIOLATION_DETAIL_LINES = 5
|
|
13
22
|
|
|
14
23
|
|
|
15
24
|
def read_stdin_text() -> str:
|
|
@@ -87,3 +96,52 @@ def is_denied_path(file_path: str, policy: dict) -> bool:
|
|
|
87
96
|
def output_json(obj: dict) -> None:
|
|
88
97
|
"""Write JSON response to stdout (for IDE to read)."""
|
|
89
98
|
print(json.dumps(obj), end='') # noqa: T201
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_detection_lines(
|
|
102
|
+
local_scan_results: list['LocalScanResult'], max_lines: int = MAX_VIOLATION_DETAIL_LINES
|
|
103
|
+
) -> str:
|
|
104
|
+
"""One line per distinct finding: what it is, and the value hash identifying it.
|
|
105
|
+
|
|
106
|
+
The value hash is safe to display; the value itself is not. Detections excluded by an existing
|
|
107
|
+
ignore rule are already gone from `document_detections`, so only what actually blocked is listed.
|
|
108
|
+
"""
|
|
109
|
+
type_by_sha = {}
|
|
110
|
+
for local_scan_result in local_scan_results:
|
|
111
|
+
for document_detections in local_scan_result.document_detections:
|
|
112
|
+
for detection in document_detections.detections:
|
|
113
|
+
sha = detection.detection_details.get('sha512')
|
|
114
|
+
if sha and sha not in type_by_sha:
|
|
115
|
+
type_by_sha[sha] = detection.type or detection.message
|
|
116
|
+
|
|
117
|
+
if not type_by_sha:
|
|
118
|
+
return ''
|
|
119
|
+
|
|
120
|
+
lines = [f' - {detection_type}: {sha}' for sha, detection_type in list(type_by_sha.items())[:max_lines]]
|
|
121
|
+
remaining = len(type_by_sha) - len(lines)
|
|
122
|
+
if remaining:
|
|
123
|
+
lines.append(f' - ...and {remaining} more')
|
|
124
|
+
|
|
125
|
+
return '\n' + '\n'.join(lines)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_violation_summary(local_scan_results: list['LocalScanResult']) -> str:
|
|
129
|
+
"""Build violation summary string with severity breakdown and emojis."""
|
|
130
|
+
detections_count = 0
|
|
131
|
+
severity_counts = defaultdict(int)
|
|
132
|
+
|
|
133
|
+
for local_scan_result in local_scan_results:
|
|
134
|
+
for document_detections in local_scan_result.document_detections:
|
|
135
|
+
for detection in document_detections.detections:
|
|
136
|
+
if detection.severity:
|
|
137
|
+
detections_count += 1
|
|
138
|
+
severity_counts[SeverityOption(detection.severity)] += 1
|
|
139
|
+
|
|
140
|
+
severity_parts = []
|
|
141
|
+
for severity in reversed(SeverityOption):
|
|
142
|
+
emoji = SeverityOption.get_member_unicode_emoji(severity)
|
|
143
|
+
count = severity_counts[severity]
|
|
144
|
+
severity_parts.append(f'{emoji} {severity.upper()} - {count}')
|
|
145
|
+
|
|
146
|
+
summary = f'Cycode found {detections_count} violations: {" | ".join(severity_parts)}'
|
|
147
|
+
return summary + _build_detection_lines(local_scan_results)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
|
+
import zipfile
|
|
3
4
|
from platform import platform
|
|
4
5
|
from typing import TYPE_CHECKING, Callable, Optional
|
|
5
6
|
|
|
@@ -23,6 +24,7 @@ from cycode.cli.files_collector.path_documents import get_relevant_documents
|
|
|
23
24
|
from cycode.cli.files_collector.sca.sca_file_collector import add_sca_dependencies_tree_documents_if_needed
|
|
24
25
|
from cycode.cli.files_collector.zip_documents import zip_documents
|
|
25
26
|
from cycode.cli.models import CliError, Document, LocalScanResult
|
|
27
|
+
from cycode.cli.utils.host_info import is_64bit
|
|
26
28
|
from cycode.cli.utils.path_utils import get_absolute_path, get_path_by_os
|
|
27
29
|
from cycode.cli.utils.progress_bar import ScanProgressBarSection
|
|
28
30
|
from cycode.cli.utils.scan_batch import run_parallel_batched_scan
|
|
@@ -145,6 +147,7 @@ def _get_scan_documents_thread_func(
|
|
|
145
147
|
is_git_diff: bool,
|
|
146
148
|
is_commit_range: bool,
|
|
147
149
|
scan_parameters: dict,
|
|
150
|
+
prezipped: Optional['InMemoryZip'] = None,
|
|
148
151
|
) -> Callable[[list[Document]], tuple[str, CliError, LocalScanResult]]:
|
|
149
152
|
cycode_client = ctx.obj['client']
|
|
150
153
|
scan_type = ctx.obj['scan_type']
|
|
@@ -164,9 +167,14 @@ def _get_scan_documents_thread_func(
|
|
|
164
167
|
|
|
165
168
|
should_use_sync_flow = _should_use_sync_flow(command_scan_type, scan_type, sync_option)
|
|
166
169
|
|
|
170
|
+
# the single ZIP flow already built the archive to check that it fits; don't build it twice
|
|
171
|
+
zipped_documents = prezipped
|
|
172
|
+
|
|
167
173
|
try:
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
if zipped_documents is None:
|
|
175
|
+
logger.debug('Preparing local files, %s', {'batch_files_count': len(batch)})
|
|
176
|
+
zipped_documents = zip_documents(scan_type, batch)
|
|
177
|
+
|
|
170
178
|
zip_file_size = zipped_documents.size
|
|
171
179
|
scan_result = _perform_scan(
|
|
172
180
|
cycode_client,
|
|
@@ -189,6 +197,9 @@ def _get_scan_documents_thread_func(
|
|
|
189
197
|
except Exception as e:
|
|
190
198
|
error = handle_scan_exception(ctx, e, return_exception=True)
|
|
191
199
|
error_message = str(e)
|
|
200
|
+
finally:
|
|
201
|
+
if zipped_documents is not None:
|
|
202
|
+
zipped_documents.cleanup()
|
|
192
203
|
|
|
193
204
|
if local_scan_result:
|
|
194
205
|
detections_count = local_scan_result.detections_count
|
|
@@ -225,34 +236,77 @@ def _get_scan_documents_thread_func(
|
|
|
225
236
|
return _scan_batch_thread_func
|
|
226
237
|
|
|
227
238
|
|
|
239
|
+
def _log_selected_upload_mode(mode: str, reason: str, documents_count: int) -> None:
|
|
240
|
+
logger.debug(
|
|
241
|
+
'Selected upload mode, %s',
|
|
242
|
+
{
|
|
243
|
+
'mode': mode,
|
|
244
|
+
'reason': reason,
|
|
245
|
+
'documents_count': documents_count,
|
|
246
|
+
'max_files_count': consts.ZIP_MAX_FILES_COUNT,
|
|
247
|
+
'zip64_enabled': is_64bit(),
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _exceeds_non_zip64_files_count(documents_to_scan: list[Document]) -> bool:
|
|
253
|
+
"""Whether a single ZIP can't hold all the documents because ZIP64 is unavailable.
|
|
254
|
+
|
|
255
|
+
Without ZIP64 (32-bit interpreter) the archive is capped at 65,535 entries.
|
|
256
|
+
"""
|
|
257
|
+
return not is_64bit() and len(documents_to_scan) > consts.ZIP_MAX_FILES_COUNT
|
|
258
|
+
|
|
259
|
+
|
|
228
260
|
def _run_presigned_upload_scan(
|
|
229
|
-
|
|
230
|
-
|
|
261
|
+
ctx: typer.Context,
|
|
262
|
+
is_git_diff: bool,
|
|
263
|
+
is_commit_range: bool,
|
|
264
|
+
scan_parameters: dict,
|
|
231
265
|
documents_to_scan: list[Document],
|
|
232
266
|
progress_bar: 'BaseProgressBar',
|
|
233
267
|
printer: 'ConsolePrinter',
|
|
234
268
|
) -> tuple:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
269
|
+
scan_type = ctx.obj['scan_type']
|
|
270
|
+
documents_count = len(documents_to_scan)
|
|
271
|
+
|
|
272
|
+
def run_batched() -> tuple:
|
|
239
273
|
return run_parallel_batched_scan(
|
|
240
|
-
|
|
274
|
+
_get_scan_documents_thread_func(ctx, is_git_diff, is_commit_range, scan_parameters),
|
|
241
275
|
scan_type,
|
|
242
276
|
documents_to_scan,
|
|
243
277
|
progress_bar=progress_bar,
|
|
244
|
-
skip_batching=True,
|
|
245
278
|
)
|
|
246
|
-
|
|
279
|
+
|
|
280
|
+
if _exceeds_non_zip64_files_count(documents_to_scan):
|
|
281
|
+
# Don't waste time zipping documents we already know won't fit into a single ZIP
|
|
282
|
+
_log_selected_upload_mode('batched', 'files_count_exceeds_non_zip64_limit', documents_count)
|
|
283
|
+
return run_batched()
|
|
284
|
+
|
|
285
|
+
zipped_documents = None
|
|
286
|
+
try:
|
|
287
|
+
# Try to zip all documents as a single batch; ZipTooLargeError raised if it exceeds the scan type's limit
|
|
288
|
+
zipped_documents = zip_documents(scan_type, documents_to_scan)
|
|
289
|
+
except (custom_exceptions.ZipTooLargeError, zipfile.LargeZipFile):
|
|
290
|
+
# LargeZipFile is a safety net: the files count pre-check above should have caught it already
|
|
291
|
+
_log_selected_upload_mode('batched', 'zip_too_large', documents_count)
|
|
292
|
+
if zipped_documents is not None:
|
|
293
|
+
zipped_documents.cleanup()
|
|
294
|
+
|
|
247
295
|
printer.print_warning(
|
|
248
296
|
'The scan is too large to upload as a single file. This may result in corrupted scan results.'
|
|
249
297
|
)
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
298
|
+
return run_batched()
|
|
299
|
+
|
|
300
|
+
# It fits: skip batching and upload everything as one ZIP. The archive we just built is the one
|
|
301
|
+
# that gets uploaded, so the scan doesn't pay for compressing every document twice
|
|
302
|
+
_log_selected_upload_mode('single_zip', 'fits_single_zip', documents_count)
|
|
303
|
+
return run_parallel_batched_scan(
|
|
304
|
+
_get_scan_documents_thread_func(ctx, is_git_diff, is_commit_range, scan_parameters, zipped_documents),
|
|
305
|
+
scan_type,
|
|
306
|
+
documents_to_scan,
|
|
307
|
+
progress_bar=progress_bar,
|
|
308
|
+
skip_batching=True,
|
|
309
|
+
)
|
|
256
310
|
|
|
257
311
|
|
|
258
312
|
def scan_documents(
|
|
@@ -277,18 +331,19 @@ def scan_documents(
|
|
|
277
331
|
)
|
|
278
332
|
return
|
|
279
333
|
|
|
280
|
-
scan_batch_thread_func = _get_scan_documents_thread_func(ctx, is_git_diff, is_commit_range, scan_parameters)
|
|
281
|
-
|
|
282
334
|
# Presigned single-file upload is async-only; a --sync scan must stay on the batched inline path
|
|
283
335
|
# so it never builds one oversized zip to POST synchronously.
|
|
284
336
|
should_use_sync_flow = _should_use_sync_flow(ctx.info_name, scan_type, ctx.obj['sync'])
|
|
285
337
|
if should_use_presigned_upload(scan_type) and not should_use_sync_flow:
|
|
286
338
|
errors, local_scan_results = _run_presigned_upload_scan(
|
|
287
|
-
|
|
339
|
+
ctx, is_git_diff, is_commit_range, scan_parameters, documents_to_scan, progress_bar, printer
|
|
288
340
|
)
|
|
289
341
|
else:
|
|
290
342
|
errors, local_scan_results = run_parallel_batched_scan(
|
|
291
|
-
|
|
343
|
+
_get_scan_documents_thread_func(ctx, is_git_diff, is_commit_range, scan_parameters),
|
|
344
|
+
scan_type,
|
|
345
|
+
documents_to_scan,
|
|
346
|
+
progress_bar=progress_bar,
|
|
292
347
|
)
|
|
293
348
|
|
|
294
349
|
try_set_aggregation_report_url_if_needed(ctx, scan_parameters, ctx.obj['client'], scan_type)
|
|
@@ -214,6 +214,9 @@ def _scan_commit_range_documents(
|
|
|
214
214
|
|
|
215
215
|
zip_file_size = from_commit_zipped_documents.size + to_commit_zipped_documents.size
|
|
216
216
|
|
|
217
|
+
from_commit_zipped_documents.cleanup()
|
|
218
|
+
to_commit_zipped_documents.cleanup()
|
|
219
|
+
|
|
217
220
|
detections_count = relevant_detections_count = 0
|
|
218
221
|
if local_scan_result:
|
|
219
222
|
detections_count = local_scan_result.detections_count
|
cycode/cli/consts.py
CHANGED
|
@@ -227,6 +227,12 @@ FILE_MAX_SIZE_LIMIT_IN_BYTES = 5000000
|
|
|
227
227
|
PRESIGNED_LINK_UPLOADED_ZIP_MAX_SIZE_LIMIT_IN_BYTES = 5 * 1024 * 1024 * 1024 # 5 GB (S3 presigned POST limit)
|
|
228
228
|
PRESIGNED_UPLOAD_SCAN_TYPES = {SAST_SCAN_TYPE, SECRET_SCAN_TYPE}
|
|
229
229
|
|
|
230
|
+
# the non-ZIP64 central directory stores the entry count in 16 bits; ZIP64 (64-bit interpreters) lifts it
|
|
231
|
+
ZIP_MAX_FILES_COUNT = 65_535
|
|
232
|
+
|
|
233
|
+
# the ZIP is built in memory up to this size, and spilled to a temp file beyond it
|
|
234
|
+
ZIP_SPOOL_MAX_SIZE_IN_BYTES = 64 * 1024 * 1024
|
|
235
|
+
|
|
230
236
|
DEFAULT_ZIP_MAX_SIZE_LIMIT_IN_BYTES = 20 * 1024 * 1024
|
|
231
237
|
ZIP_MAX_SIZE_LIMIT_IN_BYTES = {
|
|
232
238
|
SCA_SCAN_TYPE: 200 * 1024 * 1024,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import zipfile
|
|
1
2
|
from typing import Optional
|
|
2
3
|
|
|
3
4
|
import typer
|
|
@@ -26,6 +27,13 @@ def handle_scan_exception(ctx: typer.Context, err: Exception, *, return_exceptio
|
|
|
26
27
|
'Please try ignoring irrelevant paths using the `cycode ignore --by-path` command '
|
|
27
28
|
'and execute the scan again',
|
|
28
29
|
),
|
|
30
|
+
zipfile.LargeZipFile: CliError(
|
|
31
|
+
soft_fail=True,
|
|
32
|
+
code='zip_too_large_error',
|
|
33
|
+
message='The path you attempted to scan contains too many files to pack into a single archive. '
|
|
34
|
+
'Scanning such paths requires a 64-bit Python interpreter. '
|
|
35
|
+
'Please try ignoring irrelevant paths using a .cycodeignore file and execute the scan again',
|
|
36
|
+
),
|
|
29
37
|
custom_exceptions.FileCollectionError: CliError(
|
|
30
38
|
soft_fail=False,
|
|
31
39
|
code='file_collection_error',
|
|
@@ -1,20 +1,48 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import tempfile
|
|
1
3
|
from collections import defaultdict
|
|
2
|
-
from
|
|
4
|
+
from os import SEEK_END
|
|
3
5
|
from pathlib import Path
|
|
4
|
-
from
|
|
5
|
-
from typing import Optional
|
|
6
|
+
from typing import IO, Optional
|
|
6
7
|
from zipfile import ZIP_DEFLATED, ZipFile
|
|
7
8
|
|
|
9
|
+
from cycode.cli import consts
|
|
8
10
|
from cycode.cli.user_settings.configuration_manager import ConfigurationManager
|
|
11
|
+
from cycode.cli.utils.host_info import is_64bit
|
|
9
12
|
from cycode.cli.utils.path_utils import concat_unique_id
|
|
13
|
+
from cycode.logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger('ZIP')
|
|
16
|
+
|
|
17
|
+
_SPOOL_DIRECTORY_NAME = 'tmp'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_spool_directory(configuration_manager: ConfigurationManager) -> Optional[str]:
|
|
21
|
+
"""Directory to spill big ZIPs into. None falls back to the system temp directory."""
|
|
22
|
+
try:
|
|
23
|
+
directory = Path(configuration_manager.global_config_file_manager.get_config_directory_path())
|
|
24
|
+
spool_directory = directory / _SPOOL_DIRECTORY_NAME
|
|
25
|
+
spool_directory.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
return str(spool_directory)
|
|
27
|
+
except OSError as e:
|
|
28
|
+
logger.debug('Failed to create the spool directory; falling back to the system one', exc_info=e)
|
|
29
|
+
return None
|
|
10
30
|
|
|
11
31
|
|
|
12
32
|
class InMemoryZip:
|
|
13
33
|
def __init__(self) -> None:
|
|
14
34
|
self.configuration_manager = ConfigurationManager()
|
|
15
35
|
|
|
16
|
-
self.
|
|
17
|
-
self.
|
|
36
|
+
self._spool_max_size = consts.ZIP_SPOOL_MAX_SIZE_IN_BYTES
|
|
37
|
+
self._buffer = tempfile.SpooledTemporaryFile( # noqa: SIM115 # closed by cleanup(), lives past close()
|
|
38
|
+
max_size=self._spool_max_size,
|
|
39
|
+
dir=_get_spool_directory(self.configuration_manager),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# ZIP64 lifts the 65,535 entries and 4 GiB caps of the original ZIP format.
|
|
43
|
+
# It requires 64-bit offsets, so we only enable it on a 64-bit interpreter.
|
|
44
|
+
self._allow_zip64 = is_64bit()
|
|
45
|
+
self.zip = ZipFile(self._buffer, mode='a', compression=ZIP_DEFLATED, allowZip64=self._allow_zip64)
|
|
18
46
|
|
|
19
47
|
self._files_count = 0
|
|
20
48
|
self._extension_statistics = defaultdict(int)
|
|
@@ -35,17 +63,54 @@ class InMemoryZip:
|
|
|
35
63
|
def close(self) -> None:
|
|
36
64
|
self.zip.close()
|
|
37
65
|
|
|
66
|
+
def cleanup(self) -> None:
|
|
67
|
+
"""Release the buffer, deleting the spilled temp file if there is one."""
|
|
68
|
+
self._buffer.close()
|
|
69
|
+
|
|
70
|
+
def __enter__(self) -> 'InMemoryZip': # noqa: PYI034 # typing.Self needs Python 3.11
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(self, *_: object) -> None:
|
|
74
|
+
self.cleanup()
|
|
75
|
+
|
|
76
|
+
def stream(self) -> IO[bytes]:
|
|
77
|
+
"""The whole archive as a file object, rewound. Doesn't copy it into memory.
|
|
78
|
+
|
|
79
|
+
Note: before Python 3.11 SpooledTemporaryFile isn't a real IOBase, so the returned object
|
|
80
|
+
has no seekable()/readable()/writable(). read/seek/tell work on every supported version.
|
|
81
|
+
"""
|
|
82
|
+
self._buffer.seek(0)
|
|
83
|
+
return self._buffer
|
|
84
|
+
|
|
38
85
|
def read(self) -> bytes:
|
|
39
|
-
self.
|
|
40
|
-
return self.
|
|
86
|
+
self._buffer.seek(0)
|
|
87
|
+
return self._buffer.read()
|
|
41
88
|
|
|
42
89
|
def write_on_disk(self, path: 'Path') -> None:
|
|
43
90
|
with open(path, 'wb') as f:
|
|
44
|
-
|
|
91
|
+
shutil.copyfileobj(self.stream(), f)
|
|
45
92
|
|
|
46
93
|
@property
|
|
47
94
|
def size(self) -> int:
|
|
48
|
-
|
|
95
|
+
position = self._buffer.tell()
|
|
96
|
+
try:
|
|
97
|
+
self._buffer.seek(0, SEEK_END)
|
|
98
|
+
return self._buffer.tell()
|
|
99
|
+
finally:
|
|
100
|
+
self._buffer.seek(position)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def is_rolled_over(self) -> bool:
|
|
104
|
+
"""Whether the archive outgrew the threshold and moved from memory to the disk.
|
|
105
|
+
|
|
106
|
+
SpooledTemporaryFile spills on the write that crosses max_size, and the archive only grows,
|
|
107
|
+
so the size says it without reaching into the private _rolled flag.
|
|
108
|
+
"""
|
|
109
|
+
return self.size > self._spool_max_size
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def allow_zip64(self) -> bool:
|
|
113
|
+
return self._allow_zip64
|
|
49
114
|
|
|
50
115
|
@property
|
|
51
116
|
def files_count(self) -> int:
|
cycode/cli/utils/host_info.py
CHANGED
|
@@ -49,6 +49,11 @@ def _read_text_file(path: str) -> Optional[str]:
|
|
|
49
49
|
return None
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def is_64bit() -> bool:
|
|
53
|
+
"""Whether the running Python interpreter is 64-bit (not the OS)."""
|
|
54
|
+
return sys.maxsize > 2**32
|
|
55
|
+
|
|
56
|
+
|
|
52
57
|
def get_hostname() -> Optional[str]:
|
|
53
58
|
try:
|
|
54
59
|
return socket.gethostname() or None
|
cycode/cli/utils/scan_utils.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from collections import defaultdict
|
|
3
2
|
from typing import TYPE_CHECKING, Optional
|
|
4
3
|
from uuid import UUID, uuid4
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
8
7
|
from cycode.cli import consts
|
|
9
|
-
from cycode.cli.cli_types import SeverityOption
|
|
10
8
|
|
|
11
9
|
if TYPE_CHECKING:
|
|
12
10
|
from cycode.cli.models import LocalScanResult
|
|
@@ -41,24 +39,3 @@ def generate_unique_scan_id() -> UUID:
|
|
|
41
39
|
return UUID(os.environ['PYTEST_TEST_UNIQUE_ID'])
|
|
42
40
|
|
|
43
41
|
return uuid4()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def build_violation_summary(local_scan_results: list['LocalScanResult']) -> str:
|
|
47
|
-
"""Build violation summary string with severity breakdown and emojis."""
|
|
48
|
-
detections_count = 0
|
|
49
|
-
severity_counts = defaultdict(int)
|
|
50
|
-
|
|
51
|
-
for local_scan_result in local_scan_results:
|
|
52
|
-
for document_detections in local_scan_result.document_detections:
|
|
53
|
-
for detection in document_detections.detections:
|
|
54
|
-
if detection.severity:
|
|
55
|
-
detections_count += 1
|
|
56
|
-
severity_counts[SeverityOption(detection.severity)] += 1
|
|
57
|
-
|
|
58
|
-
severity_parts = []
|
|
59
|
-
for severity in reversed(SeverityOption):
|
|
60
|
-
emoji = SeverityOption.get_member_unicode_emoji(severity)
|
|
61
|
-
count = severity_counts[severity]
|
|
62
|
-
severity_parts.append(f'{emoji} {severity.upper()} - {count}')
|
|
63
|
-
|
|
64
|
-
return f'Cycode found {detections_count} violations: {" | ".join(severity_parts)}'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
cycode/__init__.py,sha256=
|
|
1
|
+
cycode/__init__.py,sha256=xN7LgjtKq-0qdFl0zVHMknIPgyr202mSalE6jyAUwGk,396
|
|
2
2
|
cycode/__main__.py,sha256=Z3bD5yrA7yPvAChcADQrqCaZd0ChGI1gdiwALwbWJ6U,104
|
|
3
3
|
cycode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
cycode/cli/app.py,sha256=AlR2durAEbsa47PDfIj7JtMvJDWA_Dq6wPtVuMJYSCs,10250
|
|
@@ -21,12 +21,12 @@ cycode/cli/apps/ai_guardrails/scan/__init__.py,sha256=qJc82XiQGiAuc1sYY8Ij_A-qXp
|
|
|
21
21
|
cycode/cli/apps/ai_guardrails/scan/consts.py,sha256=8H8JXlgm65sYkgTXqsBInHhHe80UPjqY4vumZXB4GGM,1060
|
|
22
22
|
cycode/cli/apps/ai_guardrails/scan/detach.py,sha256=8BRgBq9Qi8nXEx6NKlE8scSRPkO30XCfsiI5PLvq6Gk,2700
|
|
23
23
|
cycode/cli/apps/ai_guardrails/scan/guardrail_config.py,sha256=QSBwmkTdlC9zXIqI3gIiOlefviJWrpeJHzFu1Cs-zgE,7051
|
|
24
|
-
cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=
|
|
24
|
+
cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=i8fJ_K3m2v45DRSfkK0Wt4yEZfYIxOaLtZXiGna--5A,17641
|
|
25
25
|
cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=sWsWq5yXP54MVCajsb180kLuprI_M2kspMOzQGh7r_o,1534
|
|
26
26
|
cycode/cli/apps/ai_guardrails/scan/policy.py,sha256=3HuDoL_NYE3lyHRAlIMleq6pi-stIDQwotHsKhUFhTQ,5000
|
|
27
27
|
cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=fgV32phgkBPwqSau7oFDISZ58CVpq214JlYwZQM9bnM,8318
|
|
28
28
|
cycode/cli/apps/ai_guardrails/scan/types.py,sha256=ybQm242QN0l_4SSNX4xMHXxzqEK-MW-hfIOixI7zvGU,1497
|
|
29
|
-
cycode/cli/apps/ai_guardrails/scan/utils.py,sha256=
|
|
29
|
+
cycode/cli/apps/ai_guardrails/scan/utils.py,sha256=4CIZAILS9m5-JsFbUderwBVzFAjqAVVVRhWgE9oLPhM,5292
|
|
30
30
|
cycode/cli/apps/ai_guardrails/session_start_command.py,sha256=oEW-OsHXkf8P7SX1vKa9Ddlv_hASRiQ42JZR3MnJfRg,7171
|
|
31
31
|
cycode/cli/apps/ai_guardrails/status_command.py,sha256=Uqss68TEPCYPXpLix6Bh-4J3g-khxWsAqlIGYH5x4bQ,3203
|
|
32
32
|
cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=dOmePfZmlHAPy2zEJM1yMtSuDqvzDwtqgmLKYK-T9PI,2698
|
|
@@ -68,10 +68,10 @@ cycode/cli/apps/report_import/sbom/sbom_command.py,sha256=uWvBhVdROHcHsjoR3l44h3
|
|
|
68
68
|
cycode/cli/apps/sca_options.py,sha256=-3iXoJV5qOkfjr-WGIWuAgaeNYeItJIbm2n6O2Kg5D8,1666
|
|
69
69
|
cycode/cli/apps/scan/__init__.py,sha256=-q1AIBnrQ4GP0CVKFLr_2CdWf9TBQC90ejSL4I7rxuA,2444
|
|
70
70
|
cycode/cli/apps/scan/aggregation_report.py,sha256=8f9kPfO7biNf5OsDZG6UhMPqG6ymoFrX5GBtlEIfFAg,1540
|
|
71
|
-
cycode/cli/apps/scan/code_scanner.py,sha256=
|
|
71
|
+
cycode/cli/apps/scan/code_scanner.py,sha256=kCCt08MHye3UIJo1cgSz2uP3E-6EZfAwsR7mNU6S_8g,19741
|
|
72
72
|
cycode/cli/apps/scan/commit_history/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
73
|
cycode/cli/apps/scan/commit_history/commit_history_command.py,sha256=zTVmN8yeLXGAiCbyDL-EyEzSPNLzcRpP2q6Qq7p4uZA,1011
|
|
74
|
-
cycode/cli/apps/scan/commit_range_scanner.py,sha256=
|
|
74
|
+
cycode/cli/apps/scan/commit_range_scanner.py,sha256=kUth9ksOHmoarseE8EhAzVCycs0Tqz2g1E6WGKhMCtE,17368
|
|
75
75
|
cycode/cli/apps/scan/detection_excluder.py,sha256=0zaNa1PxVshATHv8axp4e-xWvmuNQdg_r5DYsdQ9EVo,6432
|
|
76
76
|
cycode/cli/apps/scan/path/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
77
77
|
cycode/cli/apps/scan/path/path_command.py,sha256=x4HXqq1Wy6onziKMc6ELQxqeI5k-m3t_T3RG9kQxrq0,591
|
|
@@ -98,14 +98,14 @@ cycode/cli/apps/status/version_command.py,sha256=c6Iko_rmZo9T_kQSd3HUloBi40Qv7cj
|
|
|
98
98
|
cycode/cli/cli_types.py,sha256=Xqf2eSx3yUvFoQG29ObUZ3QLVBhtZ4mkqxf_cJXpntU,3406
|
|
99
99
|
cycode/cli/config.py,sha256=Op-lX_neanJtvPvoOEx4ByBdveh5ygElIga1FdSHhOI,299
|
|
100
100
|
cycode/cli/console.py,sha256=vp-DHwlkwpwdsPyfwGdjsPF-6-Bi3f8W7G-W_YXCMH8,1914
|
|
101
|
-
cycode/cli/consts.py,sha256=
|
|
101
|
+
cycode/cli/consts.py,sha256=7RXx2ezNO7Ss_tEBcNsjklYt0esldiqM0iKhgIFrDt8,10268
|
|
102
102
|
cycode/cli/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
103
103
|
cycode/cli/exceptions/custom_exceptions.py,sha256=SImny5zXhnKocTw6GDaTm9x4I3bWRZW4wZEXg2Q-NTI,4742
|
|
104
104
|
cycode/cli/exceptions/handle_ai_remediation_errors.py,sha256=mA70upSYXK3rL_fmanzKYeUzLENhpXdkW8k3aIHrKzU,785
|
|
105
105
|
cycode/cli/exceptions/handle_auth_errors.py,sha256=m3q9keRUKAg6OnFlOlzpNUFzdQHGEeyq8N2Ywqs-QQ4,597
|
|
106
106
|
cycode/cli/exceptions/handle_errors.py,sha256=za3vQcM_eFTvbT-53tTc6ky-J0wav6lupD1hXWw0e54,881
|
|
107
107
|
cycode/cli/exceptions/handle_report_sbom_errors.py,sha256=bi0EizHtQLL-ovhHRH98CZ7qXdDPLTYnI59Jn1Y5c0E,926
|
|
108
|
-
cycode/cli/exceptions/handle_scan_errors.py,sha256=
|
|
108
|
+
cycode/cli/exceptions/handle_scan_errors.py,sha256=po91RQgRi0beQChSrjuiV2DXVARb-NOqzpVDhbvRKmw,2617
|
|
109
109
|
cycode/cli/files_collector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
110
110
|
cycode/cli/files_collector/commit_range_documents.py,sha256=Bvv9MJBoiraXI396A46412x9zgusjPS3g3nWdSFprPE,21095
|
|
111
111
|
cycode/cli/files_collector/documents_walk_ignore.py,sha256=G4e-3vfP4WZ7wa9-VbZ66xCKCioTXnPBfbrs4_hh8xY,4705
|
|
@@ -113,7 +113,7 @@ cycode/cli/files_collector/file_excluder.py,sha256=atua_L2qDbmhFXj4nB6rDqSn5--Mu
|
|
|
113
113
|
cycode/cli/files_collector/iac/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
114
114
|
cycode/cli/files_collector/iac/tf_content_generator.py,sha256=a65zA0Ejv_LSA5jac2omHck4IKoNS5MX6v6ltF2wo4E,2873
|
|
115
115
|
cycode/cli/files_collector/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
116
|
-
cycode/cli/files_collector/models/in_memory_zip.py,sha256=
|
|
116
|
+
cycode/cli/files_collector/models/in_memory_zip.py,sha256=jYLb8P2tcHWyvnDjDdREIpDXUTZxvgFdEZoB77RnXss,4411
|
|
117
117
|
cycode/cli/files_collector/path_documents.py,sha256=7oNLcFZFHC_3sO34iD9FaKdS6_0lCogAl-nh11ejT_w,5040
|
|
118
118
|
cycode/cli/files_collector/repository_documents.py,sha256=53QQsfCzXXSV6F3EcInaN8G6aCwEVzceWiYwasG8CgM,860
|
|
119
119
|
cycode/cli/files_collector/sca/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -180,13 +180,13 @@ cycode/cli/utils/binary_utils.py,sha256=PxP-rVJ1lEiOam-DQ1XU68oWtfwup4sCFHXv54cf
|
|
|
180
180
|
cycode/cli/utils/enum_utils.py,sha256=h_VTCfJ-0hnhwDsEznmx56rJrCb5FQ8u6PrI6p8MP3E,187
|
|
181
181
|
cycode/cli/utils/get_api_client.py,sha256=wwHabfVCDbFjcIwOn5Raho8MEPiOAgkHlGUEfXKpl8U,3542
|
|
182
182
|
cycode/cli/utils/git_proxy.py,sha256=FPHMBiyLFK9X9vKYpKySRKJH6Dc9Cb3nO241Q95dASE,2911
|
|
183
|
-
cycode/cli/utils/host_info.py,sha256=
|
|
183
|
+
cycode/cli/utils/host_info.py,sha256=Clln3qCJR00rw6HndD0bAIv329n_bOEx42TKW6FqXFQ,6379
|
|
184
184
|
cycode/cli/utils/ignore_utils.py,sha256=cODqhnOHA2kRo8rMY0YcmcKkmXNPOC9UTCmFu62RRqE,15567
|
|
185
185
|
cycode/cli/utils/jwt_utils.py,sha256=EGI-0CKhCGY8hIcZ9b9diq9hqtOUf8Ha8ukeVJIf974,818
|
|
186
186
|
cycode/cli/utils/path_utils.py,sha256=zc48CSU7hxqjSgfH6h5M1B0kIcKW44BOJrUa_6z-mAo,4317
|
|
187
187
|
cycode/cli/utils/progress_bar.py,sha256=bKBWHHdZsVkdDdWMJLfgLGR0cBYeB44P_DpRM8pvWqU,9528
|
|
188
188
|
cycode/cli/utils/scan_batch.py,sha256=5xKGVDVqoRxdKhuZkK11x4QrNqKmU20Q83E_fy8Nndk,5188
|
|
189
|
-
cycode/cli/utils/scan_utils.py,sha256=
|
|
189
|
+
cycode/cli/utils/scan_utils.py,sha256=_VkZ7maLVST6J8dsqDsKYWQj2EVfqI3iPPsA1QJOGEU,1259
|
|
190
190
|
cycode/cli/utils/shell_executor.py,sha256=VkzzQPZCmTkFvDjhgJrkv-Icej3U1wLW9LLN6k6OahA,1848
|
|
191
191
|
cycode/cli/utils/string_utils.py,sha256=KyPSAHDRPEGNCCcKTF0v99vad5z9djpVGc8nxpBqdYo,2445
|
|
192
192
|
cycode/cli/utils/task_timer.py,sha256=wxfM2TtJGjc1F17CIja_Qmt6zd4a1qdMwuz0ltgTDAg,2722
|
|
@@ -217,8 +217,8 @@ cycode/cyclient/report_client.py,sha256=Scq30NeJPzgXv0hPLO1U05AdE9i_2iu6cIrSKpEJ
|
|
|
217
217
|
cycode/cyclient/scan_client.py,sha256=DqAZ7u6Z_cvw9A9RlLkAQUgLRwPCCAsUq5U9umt4F7Y,16955
|
|
218
218
|
cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
|
|
219
219
|
cycode/logger.py,sha256=EfZGRK6VC5rE_LAjIcRrHFiQCueylCDXoG6bvGkrIME,2111
|
|
220
|
-
cycode-3.24.1.
|
|
221
|
-
cycode-3.24.1.
|
|
222
|
-
cycode-3.24.1.
|
|
223
|
-
cycode-3.24.1.
|
|
224
|
-
cycode-3.24.1.
|
|
220
|
+
cycode-3.24.1.dev3.dist-info/METADATA,sha256=bM6WYXKcnPc1t1mUojDIV4fzk3XU4rHwaJA2UJPS2Wc,93687
|
|
221
|
+
cycode-3.24.1.dev3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
222
|
+
cycode-3.24.1.dev3.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
|
|
223
|
+
cycode-3.24.1.dev3.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
|
|
224
|
+
cycode-3.24.1.dev3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|