Skip to content

Commit 5a05f52

Browse files
Ilanlidoclaude
andcommitted
CM-72834: Move the violation summary into the guardrails package
build_violation_summary sat in the shared utils/scan_utils.py but has only ever had one caller, and it is now shaped for a hook message rather than a terminal - multi-line, with a value hash per finding. Other scan commands render through the printers, which already have their own Secret SHA column. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent da20eba commit 5a05f52

4 files changed

Lines changed: 61 additions & 60 deletions

File tree

‎cycode/cli/apps/ai_guardrails/scan/handlers.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,14 @@
3030
AIHookOutcome,
3131
BlockReason,
3232
)
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
3434
from cycode.cli.apps.scan.code_scanner import _get_scan_documents_thread_func
3535
from cycode.cli.apps.scan.scan_parameters import get_scan_parameters
3636
from cycode.cli.cli_types import ScanTypeOption, SeverityOption
3737
from cycode.cli.files_collector.file_excluder import is_path_configured_in_exclusions
3838
from cycode.cli.models import Document
3939
from cycode.cli.utils.host_info import get_hostname, get_serial_number
4040
from cycode.cli.utils.progress_bar import DummyProgressBar, ScanProgressBarSection
41-
from cycode.cli.utils.scan_utils import build_violation_summary
4241
from cycode.logger import get_logger
4342

4443
logger = get_logger('AI Guardrails')
@@ -76,7 +75,6 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
7675
block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[AiHookEventType.PROMPT]
7776
if effective_mode == GuardrailsMode.BLOCK:
7877
outcome = AIHookOutcome.BLOCKED
79-
# Summary last: it is multi-line, so it must not be interpolated mid-sentence
8078
user_message = f'Remove secrets before sending. {violation_summary}'
8179
return HookDecision.deny(AiHookEventType.PROMPT, user_message)
8280
outcome = AIHookOutcome.WARNED
@@ -284,7 +282,6 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
284282
event_type=AiHookEventType.MCP_EXECUTION,
285283
deny_message=lambda v: f'Cycode blocked MCP tool call "{tool}". {v}',
286284
deny_agent_message='Do not pass secrets to tools. Use secret references (name/id) instead.',
287-
# Summary last: it is multi-line, so it must not be interpolated mid-sentence
288285
ask_message=lambda v: f'Allow MCP tool call "{tool}"? {v}',
289286
ask_agent_message='Possible secrets detected in tool arguments; proceed with caution.',
290287
),

‎cycode/cli/apps/ai_guardrails/scan/utils.py‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
"""
22
Utility functions for AI guardrails.
33
4-
Includes JSON parsing, path matching, and text handling utilities.
4+
Includes JSON parsing, path matching, text handling and hook-message utilities.
55
"""
66

77
import json
88
import os
99
import sys
10+
from collections import defaultdict
1011
from pathlib import Path
12+
from typing import TYPE_CHECKING
1113

1214
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
1322

1423

1524
def read_stdin_text() -> str:
@@ -87,3 +96,52 @@ def is_denied_path(file_path: str, policy: dict) -> bool:
8796
def output_json(obj: dict) -> None:
8897
"""Write JSON response to stdout (for IDE to read)."""
8998
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)

‎cycode/cli/utils/scan_utils.py‎

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import os
2-
from collections import defaultdict
32
from typing import TYPE_CHECKING, Optional
43
from uuid import UUID, uuid4
54

65
import typer
76

87
from cycode.cli import consts
9-
from cycode.cli.cli_types import SeverityOption
108

119
if TYPE_CHECKING:
1210
from cycode.cli.models import LocalScanResult
1311
from cycode.cyclient.models import ScanConfiguration
1412

15-
# Keeps the hook message readable when a single file trips dozens of detections
16-
MAX_VIOLATION_DETAIL_LINES = 5
17-
1813

1914
def set_issue_detected(ctx: typer.Context, issue_detected: bool) -> None:
2015
ctx.obj['issue_detected'] = issue_detected
@@ -44,52 +39,3 @@ def generate_unique_scan_id() -> UUID:
4439
return UUID(os.environ['PYTEST_TEST_UNIQUE_ID'])
4540

4641
return uuid4()
47-
48-
49-
def _build_detection_lines(
50-
local_scan_results: list['LocalScanResult'], max_lines: int = MAX_VIOLATION_DETAIL_LINES
51-
) -> str:
52-
"""One line per distinct finding: what it is, and the value hash identifying it.
53-
54-
The value hash is safe to display; the value itself is not. Detections excluded by an existing
55-
ignore rule are already gone from `document_detections`, so only what actually blocked is listed.
56-
"""
57-
type_by_sha = {}
58-
for local_scan_result in local_scan_results:
59-
for document_detections in local_scan_result.document_detections:
60-
for detection in document_detections.detections:
61-
sha = detection.detection_details.get('sha512')
62-
if sha and sha not in type_by_sha:
63-
type_by_sha[sha] = detection.type or detection.message
64-
65-
if not type_by_sha:
66-
return ''
67-
68-
lines = [f' - {detection_type}: {sha}' for sha, detection_type in list(type_by_sha.items())[:max_lines]]
69-
remaining = len(type_by_sha) - len(lines)
70-
if remaining:
71-
lines.append(f' - ...and {remaining} more')
72-
73-
return '\n' + '\n'.join(lines)
74-
75-
76-
def build_violation_summary(local_scan_results: list['LocalScanResult']) -> str:
77-
"""Build violation summary string with severity breakdown and emojis."""
78-
detections_count = 0
79-
severity_counts = defaultdict(int)
80-
81-
for local_scan_result in local_scan_results:
82-
for document_detections in local_scan_result.document_detections:
83-
for detection in document_detections.detections:
84-
if detection.severity:
85-
detections_count += 1
86-
severity_counts[SeverityOption(detection.severity)] += 1
87-
88-
severity_parts = []
89-
for severity in reversed(SeverityOption):
90-
emoji = SeverityOption.get_member_unicode_emoji(severity)
91-
count = severity_counts[severity]
92-
severity_parts.append(f'{emoji} {severity.upper()} - {count}')
93-
94-
summary = f'Cycode found {detections_count} violations: {" | ".join(severity_parts)}'
95-
return summary + _build_detection_lines(local_scan_results)

‎tests/cli/commands/ai_guardrails/scan/test_handlers.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
)
2222
from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
2323
from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType, AIHookOutcome, BlockReason
24+
from cycode.cli.apps.ai_guardrails.scan.utils import MAX_VIOLATION_DETAIL_LINES, build_violation_summary
2425
from cycode.cli.models import Document, DocumentDetections, LocalScanResult
25-
from cycode.cli.utils.scan_utils import MAX_VIOLATION_DETAIL_LINES, build_violation_summary
2626
from cycode.cyclient.models import Detection
2727

2828

0 commit comments

Comments
 (0)