@@ -263,6 +263,109 @@ def test_handle_before_read_file_scan_disabled(
263263 mock_scan .assert_not_called ()
264264
265265
266+ @patch ('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path' )
267+ @patch ('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets' )
268+ def test_handle_before_read_file_sensitive_path_warn_mode_scans_content (
269+ mock_scan : MagicMock , mock_is_denied : MagicMock , mock_ctx : MagicMock , default_policy : dict [str , Any ]
270+ ) -> None :
271+ """Test that sensitive path in warn mode still scans file content and emits two events."""
272+ mock_is_denied .return_value = True
273+ mock_scan .return_value = (None , 'scan-id-123' )
274+ default_policy ['mode' ] = 'warn'
275+ payload = AIHookPayload (
276+ event_name = 'file_read' ,
277+ ide_provider = 'cursor' ,
278+ file_path = '/path/to/.env' ,
279+ )
280+
281+ result = handle_before_read_file (mock_ctx , payload , default_policy )
282+
283+ # Content was scanned even though path is sensitive
284+ mock_scan .assert_called_once ()
285+ # Still warns about sensitive path since no secrets found
286+ assert result ['permission' ] == 'ask'
287+ assert '.env' in result ['user_message' ]
288+
289+ # Two events: sensitive path warn + content scan result (no secrets, but still warned due to path)
290+ assert mock_ctx .obj ['ai_security_client' ].create_event .call_count == 2
291+ first_event = mock_ctx .obj ['ai_security_client' ].create_event .call_args_list [0 ]
292+ assert first_event .args [2 ] == AIHookOutcome .WARNED
293+ assert first_event .kwargs ['block_reason' ] == BlockReason .SENSITIVE_PATH
294+ second_event = mock_ctx .obj ['ai_security_client' ].create_event .call_args_list [1 ]
295+ assert second_event .args [2 ] == AIHookOutcome .WARNED
296+ assert second_event .kwargs ['block_reason' ] is None
297+
298+
299+ @patch ('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path' )
300+ @patch ('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets' )
301+ def test_handle_before_read_file_sensitive_path_warn_mode_with_secrets (
302+ mock_scan : MagicMock , mock_is_denied : MagicMock , mock_ctx : MagicMock , default_policy : dict [str , Any ]
303+ ) -> None :
304+ """Test that sensitive path in warn mode reports secrets and emits two events."""
305+ mock_is_denied .return_value = True
306+ mock_scan .return_value = ('Found 1 secret: API key' , 'scan-id-456' )
307+ default_policy ['mode' ] = 'warn'
308+ payload = AIHookPayload (
309+ event_name = 'file_read' ,
310+ ide_provider = 'cursor' ,
311+ file_path = '/path/to/.env' ,
312+ )
313+
314+ result = handle_before_read_file (mock_ctx , payload , default_policy )
315+
316+ mock_scan .assert_called_once ()
317+ assert result ['permission' ] == 'ask'
318+ assert 'Found 1 secret: API key' in result ['user_message' ]
319+
320+ # Two events: sensitive path warn + secrets warn
321+ assert mock_ctx .obj ['ai_security_client' ].create_event .call_count == 2
322+ first_event = mock_ctx .obj ['ai_security_client' ].create_event .call_args_list [0 ]
323+ assert first_event .args [2 ] == AIHookOutcome .WARNED
324+ assert first_event .kwargs ['block_reason' ] == BlockReason .SENSITIVE_PATH
325+ second_event = mock_ctx .obj ['ai_security_client' ].create_event .call_args_list [1 ]
326+ assert second_event .args [2 ] == AIHookOutcome .WARNED
327+ assert second_event .kwargs ['block_reason' ] == BlockReason .SECRETS_IN_FILE
328+
329+
330+ @patch ('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path' )
331+ @patch ('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets' )
332+ def test_handle_before_read_file_sensitive_path_scan_disabled_warns (
333+ mock_scan : MagicMock , mock_is_denied : MagicMock , mock_ctx : MagicMock , default_policy : dict [str , Any ]
334+ ) -> None :
335+ """Test that sensitive path in warn mode with scan disabled emits a single event."""
336+ mock_is_denied .return_value = True
337+ default_policy ['mode' ] = 'warn'
338+ default_policy ['file_read' ]['scan_content' ] = False
339+ payload = AIHookPayload (
340+ event_name = 'file_read' ,
341+ ide_provider = 'cursor' ,
342+ file_path = '/path/to/.env' ,
343+ )
344+
345+ result = handle_before_read_file (mock_ctx , payload , default_policy )
346+
347+ mock_scan .assert_not_called ()
348+ assert result ['permission' ] == 'ask'
349+ assert '.env' in result ['user_message' ]
350+
351+ # Single event: sensitive path warn (no separate scan event when scan is disabled)
352+ mock_ctx .obj ['ai_security_client' ].create_event .assert_called_once ()
353+ call_args = mock_ctx .obj ['ai_security_client' ].create_event .call_args
354+ assert call_args .args [2 ] == AIHookOutcome .WARNED
355+ assert call_args .kwargs ['block_reason' ] == BlockReason .SENSITIVE_PATH
356+
357+
358+ def test_scan_path_for_secrets_directory (mock_ctx : MagicMock , default_policy : dict [str , Any ], fs : Any ) -> None :
359+ """Test that _scan_path_for_secrets returns (None, None) for directories."""
360+ from cycode .cli .apps .ai_guardrails .scan .handlers import _scan_path_for_secrets
361+
362+ fs .create_dir ('/path/to/some_directory' )
363+
364+ result = _scan_path_for_secrets (mock_ctx , '/path/to/some_directory' , default_policy )
365+
366+ assert result == (None , None )
367+
368+
266369# Tests for handle_before_mcp_execution
267370
268371
0 commit comments