1717
1818import typer
1919
20- from cycode .cli .apps .ai_guardrails .consts import PolicyMode
20+ from cycode .cli .apps .ai_guardrails .consts import GuardrailsMode , PolicyMode
2121from cycode .cli .apps .ai_guardrails .ides .base import HookDecision
2222from cycode .cli .apps .ai_guardrails .scan .payload import AIHookPayload
2323from cycode .cli .apps .ai_guardrails .scan .policy import get_policy_value
24- from cycode .cli .apps .ai_guardrails .scan .types import AiHookEventType , AIHookOutcome , BlockReason
24+ from cycode .cli .apps .ai_guardrails .scan .types import (
25+ SECRETS_BLOCK_REASON_BY_EVENT_TYPE ,
26+ AiHookEventType ,
27+ AIHookOutcome ,
28+ BlockReason ,
29+ )
2530from cycode .cli .apps .ai_guardrails .scan .utils import is_denied_path , truncate_utf8
2631from cycode .cli .apps .scan .code_scanner import _get_scan_documents_thread_func
2732from cycode .cli .apps .scan .scan_parameters import get_scan_parameters
2833from cycode .cli .cli_types import ScanTypeOption , SeverityOption
2934from cycode .cli .files_collector .file_excluder import is_path_configured_in_exclusions
3035from cycode .cli .models import Document
36+ from cycode .cli .utils .host_info import get_hostname , get_serial_number
3137from cycode .cli .utils .progress_bar import DummyProgressBar , ScanProgressBarSection
3238from cycode .cli .utils .scan_utils import build_violation_summary
3339from cycode .logger import get_logger
3440
3541logger = get_logger ('AI Guardrails' )
3642
37-
3843HandlerFn = Callable [[typer .Context , AIHookPayload , dict ], HookDecision ]
3944
4045
@@ -47,7 +52,7 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
4752 ai_client .create_event (payload , AiHookEventType .PROMPT , AIHookOutcome .ALLOWED )
4853 return HookDecision .allow (AiHookEventType .PROMPT )
4954
50- mode = get_policy_value (policy , 'mode' , default = PolicyMode . BLOCK )
55+ effective_mode = get_effective_mode (policy , prompt_config )
5156 prompt = payload .prompt or ''
5257 max_bytes = get_policy_value (policy , 'secrets' , 'max_bytes' , default = 200000 )
5358 timeout_ms = get_policy_value (policy , 'secrets' , 'timeout_ms' , default = 30000 )
@@ -59,12 +64,18 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
5964 error_message = None
6065
6166 try :
62- violation_summary , scan_id = _scan_text_for_secrets (ctx , clipped , timeout_ms )
67+ violation_summary , scan_id = _scan_text_for_secrets (
68+ ctx ,
69+ clipped ,
70+ timeout_ms ,
71+ payload = payload ,
72+ event_type = AiHookEventType .PROMPT ,
73+ effective_mode = effective_mode ,
74+ )
6375
6476 if violation_summary :
65- block_reason = BlockReason .SECRETS_IN_PROMPT
66- action = get_policy_value (prompt_config , 'action' , default = PolicyMode .BLOCK )
67- if action == PolicyMode .BLOCK and mode == PolicyMode .BLOCK :
77+ block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE [AiHookEventType .PROMPT ]
78+ if effective_mode == GuardrailsMode .BLOCK :
6879 outcome = AIHookOutcome .BLOCKED
6980 user_message = f'{ violation_summary } . Remove secrets before sending.'
7081 return HookDecision .deny (AiHookEventType .PROMPT , user_message )
@@ -97,9 +108,8 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
97108 ai_client .create_event (payload , AiHookEventType .FILE_READ , AIHookOutcome .ALLOWED )
98109 return HookDecision .allow (AiHookEventType .FILE_READ )
99110
100- mode = get_policy_value (policy , 'mode' , default = PolicyMode .BLOCK )
101111 file_path = payload .file_path or ''
102- action = get_policy_value ( file_read_config , 'action' , default = PolicyMode . BLOCK )
112+ effective_mode = get_effective_mode ( policy , file_read_config )
103113
104114 scan_id = None
105115 block_reason = None
@@ -110,7 +120,7 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
110120 is_sensitive_path = is_denied_path (file_path , policy )
111121 if is_sensitive_path :
112122 block_reason = BlockReason .SENSITIVE_PATH
113- if mode == PolicyMode . BLOCK and action == PolicyMode .BLOCK :
123+ if effective_mode == GuardrailsMode .BLOCK :
114124 outcome = AIHookOutcome .BLOCKED
115125 user_message = f'Cycode blocked sending { file_path } to the AI (sensitive path policy).'
116126 return HookDecision .deny (
@@ -133,10 +143,12 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
133143 outcome = AIHookOutcome .ALLOWED
134144
135145 if get_policy_value (file_read_config , 'scan_content' , default = True ):
136- violation_summary , scan_id = _scan_path_for_secrets (ctx , file_path , policy )
146+ violation_summary , scan_id = _scan_path_for_secrets (
147+ ctx , file_path , policy , payload = payload , effective_mode = effective_mode
148+ )
137149 if violation_summary :
138- block_reason = BlockReason . SECRETS_IN_FILE
139- if mode == PolicyMode . BLOCK and action == PolicyMode .BLOCK :
150+ block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE [ AiHookEventType . FILE_READ ]
151+ if effective_mode == GuardrailsMode .BLOCK :
140152 outcome = AIHookOutcome .BLOCKED
141153 user_message = f'Cycode blocked reading { file_path } . { violation_summary } '
142154 return HookDecision .deny (
@@ -191,7 +203,6 @@ class _ArgScanFeature:
191203 policy_key : str # 'mcp' or 'command_exec'
192204 scan_key : str # 'scan_arguments' or 'scan_command'
193205 event_type : AiHookEventType
194- block_reason : BlockReason
195206 deny_message : Callable [[str ], str ]
196207 deny_agent_message : str
197208 ask_message : Callable [[str ], str ]
@@ -213,11 +224,10 @@ def _handle_arg_scan(
213224 ai_client .create_event (payload , feature .event_type , AIHookOutcome .ALLOWED )
214225 return HookDecision .allow (feature .event_type )
215226
216- mode = get_policy_value (policy , 'mode' , default = PolicyMode .BLOCK )
217227 max_bytes = get_policy_value (policy , 'secrets' , 'max_bytes' , default = 200000 )
218228 timeout_ms = get_policy_value (policy , 'secrets' , 'timeout_ms' , default = 30000 )
219229 clipped = truncate_utf8 (scan_text , max_bytes )
220- action = get_policy_value ( feature_config , 'action' , default = PolicyMode . BLOCK )
230+ effective_mode = get_effective_mode ( policy , feature_config )
221231
222232 scan_id = None
223233 block_reason = None
@@ -226,10 +236,17 @@ def _handle_arg_scan(
226236
227237 try :
228238 if get_policy_value (feature_config , feature .scan_key , default = True ):
229- violation_summary , scan_id = _scan_text_for_secrets (ctx , clipped , timeout_ms )
239+ violation_summary , scan_id = _scan_text_for_secrets (
240+ ctx ,
241+ clipped ,
242+ timeout_ms ,
243+ payload = payload ,
244+ event_type = feature .event_type ,
245+ effective_mode = effective_mode ,
246+ )
230247 if violation_summary :
231- block_reason = feature .block_reason
232- if mode == PolicyMode . BLOCK and action == PolicyMode .BLOCK :
248+ block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE [ feature .event_type ]
249+ if effective_mode == GuardrailsMode .BLOCK :
233250 outcome = AIHookOutcome .BLOCKED
234251 return HookDecision .deny (
235252 feature .event_type ,
@@ -275,7 +292,6 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
275292 policy_key = 'mcp' ,
276293 scan_key = 'scan_arguments' ,
277294 event_type = AiHookEventType .MCP_EXECUTION ,
278- block_reason = BlockReason .SECRETS_IN_MCP_ARGS ,
279295 deny_message = lambda v : f'Cycode blocked MCP tool call "{ tool } ". { v } ' ,
280296 deny_agent_message = 'Do not pass secrets to tools. Use secret references (name/id) instead.' ,
281297 ask_message = lambda v : f'{ v } in MCP tool call "{ tool } ". Allow execution?' ,
@@ -295,6 +311,36 @@ def get_handler_for_event(event_type: str) -> Optional[HandlerFn]:
295311 return handlers .get (event_type )
296312
297313
314+ def get_effective_mode (policy : dict , feature_config : dict ) -> GuardrailsMode :
315+ """The event only blocks when both the global mode and the per-guardrail action are block."""
316+ mode = get_policy_value (policy , 'mode' , default = PolicyMode .BLOCK )
317+ action = get_policy_value (feature_config , 'action' , default = PolicyMode .BLOCK )
318+ return GuardrailsMode .BLOCK if (mode == PolicyMode .BLOCK and action == PolicyMode .BLOCK ) else GuardrailsMode .REPORT
319+
320+
321+ def build_ai_guardrails_scan_parameters (
322+ ctx : typer .Context ,
323+ paths : Optional [tuple [str , ...]],
324+ payload : AIHookPayload ,
325+ event_type : AiHookEventType ,
326+ effective_mode : GuardrailsMode ,
327+ ) -> dict :
328+ scan_parameters = get_scan_parameters (ctx , paths )
329+ scan_parameters .setdefault ('metadata' , {})['ai_guardrails' ] = {
330+ 'mode' : effective_mode .value ,
331+ 'ide_provider' : payload .ide_provider ,
332+ 'detection_source' : SECRETS_BLOCK_REASON_BY_EVENT_TYPE [event_type ].value ,
333+ 'device_id' : get_serial_number (),
334+ 'device_hostname' : get_hostname (),
335+ 'conversation_id' : payload .conversation_id ,
336+ 'generation_id' : payload .generation_id ,
337+ 'ide_user_email' : payload .ide_user_email ,
338+ 'mcp_server_name' : payload .mcp_server_name ,
339+ 'mcp_tool_name' : payload .mcp_tool_name ,
340+ }
341+ return scan_parameters
342+
343+
298344def _setup_scan_context (ctx : typer .Context ) -> typer .Context :
299345 """Set up minimal context for scan_documents without progress bars or printing."""
300346 ctx .obj ['progress_bar' ] = DummyProgressBar ([ScanProgressBarSection ])
@@ -345,18 +391,32 @@ def _perform_scan(
345391 return None , scan_id
346392
347393
348- def _scan_text_for_secrets (ctx : typer .Context , text : str , timeout_ms : int ) -> tuple [Optional [str ], Optional [str ]]:
394+ def _scan_text_for_secrets (
395+ ctx : typer .Context ,
396+ text : str ,
397+ timeout_ms : int ,
398+ payload : AIHookPayload ,
399+ event_type : AiHookEventType ,
400+ effective_mode : GuardrailsMode ,
401+ ) -> tuple [Optional [str ], Optional [str ]]:
349402 """Scan text content for secrets using Cycode CLI."""
350403 if not text :
351404 return None , None
352405
353406 document = Document (path = 'prompt-content.txt' , content = text , is_git_diff_format = False )
354407 scan_ctx = _setup_scan_context (ctx )
355408 timeout_seconds = timeout_ms / 1000.0
356- return _perform_scan (scan_ctx , [document ], get_scan_parameters (scan_ctx , None ), timeout_seconds )
409+ scan_parameters = build_ai_guardrails_scan_parameters (scan_ctx , None , payload , event_type , effective_mode )
410+ return _perform_scan (scan_ctx , [document ], scan_parameters , timeout_seconds )
357411
358412
359- def _scan_path_for_secrets (ctx : typer .Context , file_path : str , policy : dict ) -> tuple [Optional [str ], Optional [str ]]:
413+ def _scan_path_for_secrets (
414+ ctx : typer .Context ,
415+ file_path : str ,
416+ policy : dict ,
417+ payload : AIHookPayload ,
418+ effective_mode : GuardrailsMode ,
419+ ) -> tuple [Optional [str ], Optional [str ]]:
360420 """Scan a file path for secrets."""
361421 if not file_path or not os .path .isfile (file_path ):
362422 return None , None
@@ -375,4 +435,7 @@ def _scan_path_for_secrets(ctx: typer.Context, file_path: str, policy: dict) ->
375435
376436 document = Document (path = os .path .basename (file_path ), content = content , is_git_diff_format = False )
377437 scan_ctx = _setup_scan_context (ctx )
378- return _perform_scan (scan_ctx , [document ], get_scan_parameters (scan_ctx , (file_path ,)), timeout_seconds )
438+ scan_parameters = build_ai_guardrails_scan_parameters (
439+ scan_ctx , (file_path ,), payload , AiHookEventType .FILE_READ , effective_mode
440+ )
441+ return _perform_scan (scan_ctx , [document ], scan_parameters , timeout_seconds )
0 commit comments