|
21 | 21 | ) |
22 | 22 | from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload |
23 | 23 | from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType, AIHookOutcome, BlockReason |
24 | | -from cycode.cli.models import Document, LocalScanResult |
| 24 | +from cycode.cli.apps.ai_guardrails.scan.utils import MAX_VIOLATION_DETAIL_LINES, build_violation_summary |
| 25 | +from cycode.cli.models import Document, DocumentDetections, LocalScanResult |
| 26 | +from cycode.cyclient.models import Detection |
25 | 27 |
|
26 | 28 |
|
27 | 29 | @pytest.fixture |
@@ -420,6 +422,60 @@ def test_perform_scan_no_violation_when_all_detections_excluded(mock_ctx: MagicM |
420 | 422 | assert scan_id == 'scan-id-123' |
421 | 423 |
|
422 | 424 |
|
| 425 | +def _local_scan_result_with_detections(*shas: str) -> LocalScanResult: |
| 426 | + document = Document(path='prompt-content.txt', content='some content', is_git_diff_format=False) |
| 427 | + detections = [ |
| 428 | + Detection( |
| 429 | + detection_type_id='type-id', |
| 430 | + type='GitHub Token', |
| 431 | + message='Hardcoded secret', |
| 432 | + detection_details={'sha512': sha}, |
| 433 | + detection_rule_id='rule-id', |
| 434 | + severity='High', |
| 435 | + ) |
| 436 | + for sha in shas |
| 437 | + ] |
| 438 | + return LocalScanResult( |
| 439 | + scan_id='scan-id-123', |
| 440 | + report_url=None, |
| 441 | + document_detections=[DocumentDetections(document=document, detections=detections)], |
| 442 | + issue_detected=True, |
| 443 | + detections_count=len(detections), |
| 444 | + relevant_detections_count=len(detections), |
| 445 | + ) |
| 446 | + |
| 447 | + |
| 448 | +def test_violation_summary_lists_one_line_per_distinct_sha() -> None: |
| 449 | + """A blocked developer needs the value hash to act on the finding (e.g. `cycode ignore --by-sha`).""" |
| 450 | + summary = build_violation_summary([_local_scan_result_with_detections('sha-aaa', 'sha-bbb', 'sha-aaa')]) |
| 451 | + |
| 452 | + assert 'Cycode found 3 violations' in summary |
| 453 | + assert 'GitHub Token: sha-aaa' in summary |
| 454 | + assert 'GitHub Token: sha-bbb' in summary |
| 455 | + # Repeated values collapse to one line; the hash identifies the value, not the occurrence |
| 456 | + assert summary.count('sha-aaa') == 1 |
| 457 | + |
| 458 | + |
| 459 | +def test_violation_summary_caps_the_detection_lines() -> None: |
| 460 | + """A file full of detections must not turn the hook message into a wall of text.""" |
| 461 | + shas = [f'sha-{index}' for index in range(MAX_VIOLATION_DETAIL_LINES + 3)] |
| 462 | + |
| 463 | + summary = build_violation_summary([_local_scan_result_with_detections(*shas)]) |
| 464 | + |
| 465 | + assert summary.count('GitHub Token: ') == MAX_VIOLATION_DETAIL_LINES |
| 466 | + assert '...and 3 more' in summary |
| 467 | + |
| 468 | + |
| 469 | +def test_violation_summary_omits_lines_without_a_sha() -> None: |
| 470 | + """Non-secret scan types carry no value hash, so there is nothing to list.""" |
| 471 | + local_scan_result = _local_scan_result_with_detections('sha-aaa') |
| 472 | + local_scan_result.document_detections[0].detections[0].detection_details = {} |
| 473 | + |
| 474 | + summary = build_violation_summary([local_scan_result]) |
| 475 | + |
| 476 | + assert 'GitHub Token' not in summary |
| 477 | + |
| 478 | + |
423 | 479 | # Tests for handle_before_mcp_execution |
424 | 480 |
|
425 | 481 |
|
|
0 commit comments