controlzero 1.13.9__tar.gz → 1.13.11__tar.gz
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.
- {controlzero-1.13.9 → controlzero-1.13.11}/CHANGELOG.md +129 -2
- {controlzero-1.13.9 → controlzero-1.13.11}/PKG-INFO +3 -3
- {controlzero-1.13.9 → controlzero-1.13.11}/README.md +2 -2
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/__init__.py +1 -1
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/action_validator.py +8 -0
- controlzero-1.13.11/controlzero/_internal/dlp_output_observer.py +95 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/dlp_scanner.py +110 -30
- controlzero-1.13.11/controlzero/_internal/enforcement_outcome.py +543 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/enforcer.py +293 -40
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/audit_remote.py +197 -2
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/debug_bundle.py +6 -1
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/antigravity.py +142 -36
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/base.py +103 -1
- controlzero-1.13.11/controlzero/cli/hosts/claims.py +566 -0
- controlzero-1.13.11/controlzero/cli/hosts/claude_code.py +569 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/codex_cli.py +14 -1
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/cursor_cli.py +6 -0
- controlzero-1.13.11/controlzero/cli/hosts/gemini_cli.py +221 -0
- controlzero-1.13.11/controlzero/cli/hosts/kiro.py +729 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/main.py +1050 -103
- controlzero-1.13.11/controlzero/cli/maturity.py +72 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/setup_wizard.py +294 -30
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/spool_cmd.py +1 -1
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/client.py +318 -33
- controlzero-1.13.11/controlzero/hook_integrity.py +1315 -0
- controlzero-1.13.11/controlzero/integrations/_export_dlp.py +171 -0
- controlzero-1.13.11/controlzero/integrations/_mask_apply.py +146 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/anthropic.py +5 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/autogen.py +15 -4
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/braintrust.py +32 -11
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/agent.py +5 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/crew.py +15 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/task.py +30 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/tool.py +43 -4
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google.py +5 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google_adk/agent.py +5 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google_adk/tool.py +34 -2
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/agent.py +20 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/chain.py +25 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/graph.py +30 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/tool.py +41 -2
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langfuse.py +68 -22
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/litellm.py +5 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/openai.py +10 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/pydantic_ai.py +17 -2
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/vercel_ai.py +5 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/pyproject.toml +18 -1
- controlzero-1.13.9/Dockerfile.test +0 -24
- controlzero-1.13.9/controlzero/cli/hosts/claude_code.py +0 -279
- controlzero-1.13.9/controlzero/cli/hosts/gemini_cli.py +0 -114
- controlzero-1.13.9/controlzero/cli/hosts/kiro.py +0 -364
- controlzero-1.13.9/controlzero/hook_integrity.py +0 -680
- controlzero-1.13.9/examples/hello_world.py +0 -20
- controlzero-1.13.9/ruff.toml +0 -40
- controlzero-1.13.9/tests/_fixtures/antigravity-envelope-expected.json +0 -146
- controlzero-1.13.9/tests/_fixtures/jcs_args_hash_vectors.json +0 -111
- controlzero-1.13.9/tests/conftest.py +0 -68
- controlzero-1.13.9/tests/fixtures/adapter-contracts/antigravity.json +0 -21
- controlzero-1.13.9/tests/fixtures/adapter-contracts/claude_code.json +0 -31
- controlzero-1.13.9/tests/fixtures/adapter-contracts/codex_cli.json +0 -24
- controlzero-1.13.9/tests/fixtures/adapter-contracts/cursor.json +0 -31
- controlzero-1.13.9/tests/fixtures/adapter-contracts/gemini_cli.json +0 -25
- controlzero-1.13.9/tests/fixtures/adapter-contracts/kiro.json +0 -39
- controlzero-1.13.9/tests/integrations/__init__.py +0 -0
- controlzero-1.13.9/tests/integrations/test_google.py +0 -96
- controlzero-1.13.9/tests/parity/action_aliases.json +0 -55
- controlzero-1.13.9/tests/spool/__init__.py +0 -0
- controlzero-1.13.9/tests/spool/conftest.py +0 -208
- controlzero-1.13.9/tests/spool/test_spool_cli.py +0 -168
- controlzero-1.13.9/tests/spool/test_spool_concurrency.py +0 -110
- controlzero-1.13.9/tests/spool/test_spool_conformance.py +0 -316
- controlzero-1.13.9/tests/spool/test_spool_core.py +0 -415
- controlzero-1.13.9/tests/spool/test_spool_crash.py +0 -106
- controlzero-1.13.9/tests/spool/test_spool_diskfull.py +0 -77
- controlzero-1.13.9/tests/spool/test_spool_durable_default_tamper.py +0 -353
- controlzero-1.13.9/tests/spool/test_spool_keychain_dek.py +0 -444
- controlzero-1.13.9/tests/spool/test_spool_sink_wiring.py +0 -671
- controlzero-1.13.9/tests/spool/test_spool_transcript_localack.py +0 -102
- controlzero-1.13.9/tests/spool/test_spool_uploader.py +0 -569
- controlzero-1.13.9/tests/test_action_aliases.py +0 -224
- controlzero-1.13.9/tests/test_action_canonicalization.py +0 -36
- controlzero-1.13.9/tests/test_action_validator_t86.py +0 -129
- controlzero-1.13.9/tests/test_adapter_contracts.py +0 -215
- controlzero-1.13.9/tests/test_adapter_interface.py +0 -402
- controlzero-1.13.9/tests/test_agent_name_env.py +0 -83
- controlzero-1.13.9/tests/test_agy_docs_no_exit2_1373.py +0 -131
- controlzero-1.13.9/tests/test_antigravity_adapter.py +0 -531
- controlzero-1.13.9/tests/test_antigravity_envelope_parity.py +0 -108
- controlzero-1.13.9/tests/test_antigravity_ga_blockers_1248.py +0 -565
- controlzero-1.13.9/tests/test_antigravity_hook_check.py +0 -164
- controlzero-1.13.9/tests/test_antigravity_install.py +0 -431
- controlzero-1.13.9/tests/test_antigravity_posttooluse_observe_58.py +0 -295
- controlzero-1.13.9/tests/test_antigravity_tool_vocab_1303.py +0 -322
- controlzero-1.13.9/tests/test_api_key_mask.py +0 -59
- controlzero-1.13.9/tests/test_api_url_config.py +0 -72
- controlzero-1.13.9/tests/test_audit_project_id_attribution_pr2.py +0 -304
- controlzero-1.13.9/tests/test_audit_remote.py +0 -715
- controlzero-1.13.9/tests/test_audit_remote_io_capture.py +0 -141
- controlzero-1.13.9/tests/test_audit_remote_sdk_version.py +0 -144
- controlzero-1.13.9/tests/test_audit_sink_isolation.py +0 -67
- controlzero-1.13.9/tests/test_bundle_cache_freshness_1303.py +0 -302
- controlzero-1.13.9/tests/test_bundle_parser.py +0 -238
- controlzero-1.13.9/tests/test_bundle_translate.py +0 -263
- controlzero-1.13.9/tests/test_canonical_phase1a.py +0 -230
- controlzero-1.13.9/tests/test_capability_matrix_doc.py +0 -175
- controlzero-1.13.9/tests/test_cli_carve_out.py +0 -513
- controlzero-1.13.9/tests/test_cli_coverage.py +0 -62
- controlzero-1.13.9/tests/test_cli_debug_bundle.py +0 -390
- controlzero-1.13.9/tests/test_cli_extractor_integration.py +0 -527
- controlzero-1.13.9/tests/test_cli_hook.py +0 -437
- controlzero-1.13.9/tests/test_cli_hook_entry.py +0 -189
- controlzero-1.13.9/tests/test_cli_hosted_refresh.py +0 -103
- controlzero-1.13.9/tests/test_cli_init.py +0 -43
- controlzero-1.13.9/tests/test_cli_init_templates.py +0 -24
- controlzero-1.13.9/tests/test_cli_tail.py +0 -33
- controlzero-1.13.9/tests/test_cli_test.py +0 -59
- controlzero-1.13.9/tests/test_cli_validate.py +0 -37
- controlzero-1.13.9/tests/test_coding_agent_hooks.py +0 -894
- controlzero-1.13.9/tests/test_condition_operators_1495.py +0 -191
- controlzero-1.13.9/tests/test_conditions.py +0 -200
- controlzero-1.13.9/tests/test_config_format_parity_1303.py +0 -145
- controlzero-1.13.9/tests/test_conformance.py +0 -335
- controlzero-1.13.9/tests/test_console.py +0 -87
- controlzero-1.13.9/tests/test_credential_hook.py +0 -737
- controlzero-1.13.9/tests/test_cross_adapter_shell_enforcement.py +0 -334
- controlzero-1.13.9/tests/test_cursor_adapter.py +0 -373
- controlzero-1.13.9/tests/test_dashboard_shell_template_1904.py +0 -746
- controlzero-1.13.9/tests/test_default_action.py +0 -505
- controlzero-1.13.9/tests/test_deny_rule_reconciliation_1011.py +0 -247
- controlzero-1.13.9/tests/test_deny_wins_default_catchall_1620.py +0 -130
- controlzero-1.13.9/tests/test_device.py +0 -357
- controlzero-1.13.9/tests/test_dlp_agent_remediation_1938.py +0 -366
- controlzero-1.13.9/tests/test_dlp_mask.py +0 -519
- controlzero-1.13.9/tests/test_dlp_mask_hosts_1938.py +0 -164
- controlzero-1.13.9/tests/test_dlp_origin_contract.py +0 -620
- controlzero-1.13.9/tests/test_dlp_scan_coverage_1927_1931.py +0 -634
- controlzero-1.13.9/tests/test_dlp_scanner.py +0 -1215
- controlzero-1.13.9/tests/test_doctor.py +0 -150
- controlzero-1.13.9/tests/test_engine_version_consistency.py +0 -82
- controlzero-1.13.9/tests/test_enrollment.py +0 -820
- controlzero-1.13.9/tests/test_env_dump_438.py +0 -148
- controlzero-1.13.9/tests/test_epic_1247_bryan_acceptance.py +0 -531
- controlzero-1.13.9/tests/test_error_codes.py +0 -122
- controlzero-1.13.9/tests/test_errors_e_codes.py +0 -301
- controlzero-1.13.9/tests/test_fail_closed_eval.py +0 -19
- controlzero-1.13.9/tests/test_failopen_1303.py +0 -96
- controlzero-1.13.9/tests/test_git_target_1734.py +0 -242
- controlzero-1.13.9/tests/test_glob_matching.py +0 -287
- controlzero-1.13.9/tests/test_hitl_5d_email_install.py +0 -137
- controlzero-1.13.9/tests/test_hitl_6a_cli_flag.py +0 -306
- controlzero-1.13.9/tests/test_hitl_6a_exceptions.py +0 -166
- controlzero-1.13.9/tests/test_hitl_6a_get_secret_hitl.py +0 -672
- controlzero-1.13.9/tests/test_hitl_6a_mock_backend.py +0 -342
- controlzero-1.13.9/tests/test_hitl_6a_pending_approval.py +0 -362
- controlzero-1.13.9/tests/test_hitl_6a_request_approval.py +0 -729
- controlzero-1.13.9/tests/test_hitl_6a_secret_leak_guard.py +0 -334
- controlzero-1.13.9/tests/test_hitl_6a_wait.py +0 -673
- controlzero-1.13.9/tests/test_hitl_conformance.py +0 -286
- controlzero-1.13.9/tests/test_hitl_phase2b_protocol.py +0 -643
- controlzero-1.13.9/tests/test_hitl_reason_codes.py +0 -78
- controlzero-1.13.9/tests/test_hitl_validator_keys.py +0 -271
- controlzero-1.13.9/tests/test_hook_command_pinning_1705.py +0 -325
- controlzero-1.13.9/tests/test_hook_extractors.py +0 -566
- controlzero-1.13.9/tests/test_hook_integrity.py +0 -521
- controlzero-1.13.9/tests/test_hook_mask_failclosed.py +0 -95
- controlzero-1.13.9/tests/test_hook_warn_surfaces_reason_1903.py +0 -479
- controlzero-1.13.9/tests/test_hosted_local_audit_1247.py +0 -219
- controlzero-1.13.9/tests/test_hosted_local_shadow_warn_1265.py +0 -265
- controlzero-1.13.9/tests/test_hosted_policy_e2e.py +0 -383
- controlzero-1.13.9/tests/test_hosts_adapter.py +0 -686
- controlzero-1.13.9/tests/test_hybrid_mode_strict.py +0 -90
- controlzero-1.13.9/tests/test_hybrid_mode_warn.py +0 -28
- controlzero-1.13.9/tests/test_install_api_url.py +0 -69
- controlzero-1.13.9/tests/test_install_hook_command.py +0 -145
- controlzero-1.13.9/tests/test_install_hooks.py +0 -1244
- controlzero-1.13.9/tests/test_io_capture_producer.py +0 -172
- controlzero-1.13.9/tests/test_kiro_adapter.py +0 -1422
- controlzero-1.13.9/tests/test_kiro_agent_harness_routing.py +0 -196
- controlzero-1.13.9/tests/test_kiro_cli_e2e.py +0 -353
- controlzero-1.13.9/tests/test_kiro_cli_hook_pin_1265.py +0 -133
- controlzero-1.13.9/tests/test_kiro_cli_v210_hooks.py +0 -120
- controlzero-1.13.9/tests/test_kiro_hook_templates.py +0 -324
- controlzero-1.13.9/tests/test_kiro_ide_v2_hooks_1944.py +0 -480
- controlzero-1.13.9/tests/test_kiro_install.py +0 -463
- controlzero-1.13.9/tests/test_kiro_install_standardisation.py +0 -218
- controlzero-1.13.9/tests/test_kiro_version_schemes.py +0 -123
- controlzero-1.13.9/tests/test_layout_migration_t101.py +0 -158
- controlzero-1.13.9/tests/test_layout_parity_t102.py +0 -229
- controlzero-1.13.9/tests/test_local_mode_dict.py +0 -98
- controlzero-1.13.9/tests/test_local_mode_file_json.py +0 -33
- controlzero-1.13.9/tests/test_local_mode_file_yaml.py +0 -62
- controlzero-1.13.9/tests/test_localized_reason.py +0 -303
- controlzero-1.13.9/tests/test_log_fallback_stderr.py +0 -33
- controlzero-1.13.9/tests/test_log_options_ignored_hosted.py +0 -50
- controlzero-1.13.9/tests/test_log_rotation.py +0 -66
- controlzero-1.13.9/tests/test_migrate.py +0 -128
- controlzero-1.13.9/tests/test_min_sdk_version_gate.py +0 -206
- controlzero-1.13.9/tests/test_multi_client_per_project_175.py +0 -810
- controlzero-1.13.9/tests/test_no_policy_no_key.py +0 -28
- controlzero-1.13.9/tests/test_observe_mode_1247.py +0 -362
- controlzero-1.13.9/tests/test_package_rename_shim.py +0 -41
- controlzero-1.13.9/tests/test_part3_active_policy_count_1303.py +0 -142
- controlzero-1.13.9/tests/test_per_client_isolation_1011.py +0 -250
- controlzero-1.13.9/tests/test_policy_engine_version_phase1b.py +0 -73
- controlzero-1.13.9/tests/test_policy_freshness.py +0 -274
- controlzero-1.13.9/tests/test_policy_load_failclosed_f3.py +0 -278
- controlzero-1.13.9/tests/test_policy_settings.py +0 -84
- controlzero-1.13.9/tests/test_policy_source_audit.py +0 -149
- controlzero-1.13.9/tests/test_pr4_install_messaging.py +0 -376
- controlzero-1.13.9/tests/test_pr4_output.py +0 -144
- controlzero-1.13.9/tests/test_pr4_status.py +0 -160
- controlzero-1.13.9/tests/test_probe_audit_isolation.py +0 -177
- controlzero-1.13.9/tests/test_quarantine.py +0 -191
- controlzero-1.13.9/tests/test_reason_code.py +0 -392
- controlzero-1.13.9/tests/test_reason_code_hitl_grant_1596.py +0 -34
- controlzero-1.13.9/tests/test_refresh.py +0 -567
- controlzero-1.13.9/tests/test_secrets.py +0 -225
- controlzero-1.13.9/tests/test_secrets_reference_resolver.py +0 -191
- controlzero-1.13.9/tests/test_setup_wizard.py +0 -196
- controlzero-1.13.9/tests/test_shell_semantic_class.py +0 -243
- controlzero-1.13.9/tests/test_source_labels.py +0 -95
- controlzero-1.13.9/tests/test_sql_semantic_class.py +0 -212
- controlzero-1.13.9/tests/test_synthetic_policy_id_t79.py +0 -324
- controlzero-1.13.9/tests/test_t103_precedence.py +0 -164
- controlzero-1.13.9/tests/test_t104_cache_gc.py +0 -174
- controlzero-1.13.9/tests/test_t108_local_override_audit.py +0 -161
- controlzero-1.13.9/tests/test_t96_single_audit_log.py +0 -125
- controlzero-1.13.9/tests/test_t99_install_prefetch_bundle.py +0 -182
- controlzero-1.13.9/tests/test_tamper.py +0 -203
- controlzero-1.13.9/tests/test_tamper_behavior.py +0 -163
- controlzero-1.13.9/tests/test_tamper_hook.py +0 -307
- controlzero-1.13.9/tests/test_telemetry_consent.py +0 -90
- controlzero-1.13.9/tests/test_tls_ca_bundle.py +0 -112
- controlzero-1.13.9/tests/test_tracecontext.py +0 -89
- controlzero-1.13.9/tests/test_unsafe_int_boundary.py +0 -66
- controlzero-1.13.9/tests/test_update_cmd.py +0 -77
- controlzero-1.13.9/tests/test_upgrade_nudge.py +0 -54
- controlzero-1.13.9/tests/test_version_aware_fail_closed.py +0 -212
- controlzero-1.13.9/tools/cz-kiro-adapter +0 -31
- {controlzero-1.13.9 → controlzero-1.13.11}/.gitignore +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/LICENSE +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/__main__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/_messages.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/condition_ops.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/dlp_remediation.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/io_capture_producer.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/source_labels.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/types.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/audit_local.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/canonical.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/agent_config.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/console.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/enforcement.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/exec_cmd.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hook_entry.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/cursor.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/output.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/device.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/enrollment.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/error_codes.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/errors.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/status.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/layout_migration.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/policy_loader.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/reference.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/resolver.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/secretstr.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_state.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/tamper.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/tracecontext.py +0 -0
|
@@ -1,5 +1,132 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.11 (2026-08-10): kiro_cli prompts were never actually scanned
|
|
4
|
+
|
|
5
|
+
1.13.10 said "Prompt submission is now a scanned surface on the hosts that
|
|
6
|
+
expose one". For `kiro_cli` that was not true, and this release makes it true.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **`kiro_cli` returned an affirmative ALLOW on a prompt it never read (#2108).**
|
|
11
|
+
A `userPromptSubmit` payload carries no `tool_name`, so it took the
|
|
12
|
+
missing-tool_name branch: under `--strict` (what the installer writes) it was
|
|
13
|
+
REFUSED without being scanned, and without `--strict` it was ALLOWED without
|
|
14
|
+
being scanned. Driven against the real kiro-cli 2.16.2 payload, all three of
|
|
15
|
+
a secret-bearing prompt, a clean prompt and an empty prompt produced
|
|
16
|
+
BYTE-IDENTICAL output -- the prompt text was not an input to the decision at
|
|
17
|
+
all -- and NO audit row was written, so the call was unattributable.
|
|
18
|
+
The prompt is now lifted into the `prompt` pseudo-tool and rides the existing
|
|
19
|
+
DLP path. The three cases now differ, and every one is audited.
|
|
20
|
+
|
|
21
|
+
- **An ambient `CLAUDECODE` environment variable stole the routing.** A
|
|
22
|
+
camelCase kiro payload in a shell where `CLAUDECODE=1` was claimed by the
|
|
23
|
+
Claude Code adapter, and the secret never reached the scanner. Anyone running
|
|
24
|
+
kiro-cli inside a Claude Code session had no prompt DLP at all.
|
|
25
|
+
|
|
26
|
+
- **Padding a prompt past the 1 MiB scan cap bypassed DLP entirely.** The
|
|
27
|
+
bounded UTF-8 prefix is now scanned, and a clean prefix is still a deny with
|
|
28
|
+
`skipped/payload_over_size_cap` -- never reported as `clean`.
|
|
29
|
+
|
|
30
|
+
- **`--strict` failed open on a non-dict payload.** It now refuses, like the
|
|
31
|
+
other four unusable-payload branches.
|
|
32
|
+
|
|
33
|
+
- **`prompt` was not a canonical tool**, so authoring `deny: prompt:*` -- the
|
|
34
|
+
exact rule this feature exists for -- printed "not a known canonical action".
|
|
35
|
+
|
|
36
|
+
- **kiro-cli v3 PascalCase prompt events** were uncovered; normalisation is now
|
|
37
|
+
shared by both Kiro adapters so the result does not depend on which adapter
|
|
38
|
+
won the routing race.
|
|
39
|
+
|
|
40
|
+
### Known limitations, stated plainly
|
|
41
|
+
|
|
42
|
+
- **kiro-cli does not enforce this event.** Driven on 2.16.2: a hook exiting 2
|
|
43
|
+
on `userPromptSubmit` is logged as failed and the prompt reaches the model
|
|
44
|
+
anyway; a structured stdout deny is also ignored. Control Zero SCANS and
|
|
45
|
+
RECORDS the prompt on this host. It does not block it. Nothing in this
|
|
46
|
+
release claims otherwise.
|
|
47
|
+
|
|
48
|
+
- **The raw-prompt marker does not reach the hosted dashboard.** It reaches the
|
|
49
|
+
DLP scanner and the LOCAL audit record; surfacing it remotely needs a backend
|
|
50
|
+
field and is not done here.
|
|
51
|
+
|
|
52
|
+
- **The Node SDK's equivalent fix is still unreachable as installed (#2208).**
|
|
53
|
+
This release fixes Python only.
|
|
54
|
+
|
|
55
|
+
## 1.13.10 (2026-07-30): DLP now covers prompts and tool output, not just tool arguments
|
|
56
|
+
|
|
57
|
+
Until this release, Control Zero scanned the ARGUMENTS of a tool call and
|
|
58
|
+
nothing else. A secret could reach a model or leave the machine by three other
|
|
59
|
+
routes that looked identical to a working setup. All three are closed here.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- **A secret pasted into a prompt was sent to the model unscanned (#1966).**
|
|
64
|
+
Prompt submission is now a scanned surface on the hosts that expose one
|
|
65
|
+
(Claude Code `UserPromptSubmit`, Gemini CLI `BeforeAgent`). Previously only
|
|
66
|
+
tool calls were checked, so pasting a key into the chat box egressed it.
|
|
67
|
+
|
|
68
|
+
- **Credentials from dashboard-configured DLP rules were written to the audit
|
|
69
|
+
log in cleartext (#1881 follow-up).** The scanner hashed matches only when
|
|
70
|
+
the rule category was the literal string `secret`; dashboard rules arrive as
|
|
71
|
+
`credential`, so those matches were stored verbatim. The audit trail was
|
|
72
|
+
keeping the secret it had just detected. Now gated on a named set of
|
|
73
|
+
value-bearing categories, so a category added later cannot reach cleartext.
|
|
74
|
+
|
|
75
|
+
- **Control Zero's own audit egress was shipping your raw tool input and
|
|
76
|
+
output (#1967 path 5).** `input_payload` / `output_payload` carried customer
|
|
77
|
+
content verbatim to our backend, and redaction was deferred to the backend --
|
|
78
|
+
i.e. applied after the bytes had already left your machine. They are now
|
|
79
|
+
scanned and redacted BEFORE the wire, every sensitive span rather than only
|
|
80
|
+
`mask`-action rules, and the payload is DROPPED rather than forwarded if the
|
|
81
|
+
scanner cannot run. `io_dlp_scan_status` is always set, so "not scanned" stays
|
|
82
|
+
distinguishable from "scanned and clean".
|
|
83
|
+
|
|
84
|
+
- **Spooled audit records were replayed without ever being scanned (#1967).**
|
|
85
|
+
Replay validated JSON shape, tool name and decision -- never payload content
|
|
86
|
+
-- then transmitted the stored bytes as-is, possibly days later on a
|
|
87
|
+
different network. Records written before this release are treated as
|
|
88
|
+
unscanned rather than trusted.
|
|
89
|
+
|
|
90
|
+
- **The auth cache dropped `key_type`, silently disabling the audit
|
|
91
|
+
anti-spoof guarantee.** `key_type == "extension"` is server-authoritative
|
|
92
|
+
for a row's `surface_class`; on every cache HIT the field was empty, so the
|
|
93
|
+
wire-supplied value was honoured instead. The guarantee held only on the
|
|
94
|
+
first request after a cache miss.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- **Sensitive data in tool OUTPUT is now detected and recorded (#1965).**
|
|
99
|
+
Reading a file that contains a secret is caught. The tool call is NOT
|
|
100
|
+
blocked -- an agent reading a file is normal work -- so this is deliberately
|
|
101
|
+
observe-and-record: one deduplicated message per tool call, and an audit row
|
|
102
|
+
carrying `effect=recorded_only` that is never labelled a block.
|
|
103
|
+
|
|
104
|
+
- **DLP runs before export to third-party observability (#1967).** Braintrust
|
|
105
|
+
and Langfuse were receiving full tool arguments and output previews with no
|
|
106
|
+
scan in between.
|
|
107
|
+
|
|
108
|
+
### Changed
|
|
109
|
+
|
|
110
|
+
- API-key creation rejects a `key_type` outside `{live, test}` with
|
|
111
|
+
`INVALID_CREDENTIAL_KIND` (400) instead of accepting any string and
|
|
112
|
+
interpolating it into the stored key prefix. Bulk extension-key prefixes are
|
|
113
|
+
limited to 1-8 characters of `[a-z0-9-]`, which is what the database column
|
|
114
|
+
actually fits.
|
|
115
|
+
|
|
116
|
+
### Notes for anyone writing a policy by hand
|
|
117
|
+
|
|
118
|
+
DLP rules go under `dlp_rules:`, and **every rule needs a `pattern:`**. A rule
|
|
119
|
+
without one is dropped silently. Our own published recipe used `dlp:` with
|
|
120
|
+
`pattern_group:` -- neither is read by anything -- so it configured no DLP at
|
|
121
|
+
all. That recipe is corrected, and CI now fails if a shipped example uses a key
|
|
122
|
+
the loader does not consume.
|
|
123
|
+
|
|
124
|
+
### Not in this release
|
|
125
|
+
|
|
126
|
+
Tool output is observed, not blocked. That is the intended behaviour, not a
|
|
127
|
+
limitation awaiting a fix: an agent reading a file is normal work, and blocking
|
|
128
|
+
it would make Control Zero unusable.
|
|
129
|
+
|
|
3
130
|
## 1.13.9 (2026-07-28): Kiro IDE hooks execute on 1.0.x and stay in the correct project
|
|
4
131
|
|
|
5
132
|
### Fixed
|
|
@@ -1119,7 +1246,7 @@ changes do not perform.
|
|
|
1119
1246
|
|
|
1120
1247
|
## 1.9.3 -- 2026-06-16 (posture release: empty-bundle OBSERVE + self-explaining no-rule-match deny, epic gh#1247)
|
|
1121
1248
|
|
|
1122
|
-
The consolidated **posture release** for epic gh#1247 (customer
|
|
1249
|
+
The consolidated **posture release** for epic gh#1247 (a customer).
|
|
1123
1250
|
Three customer-visible behaviors land together so the engine's "what
|
|
1124
1251
|
happens when a tool isn't covered by a rule" story is coherent across
|
|
1125
1252
|
both SDKs and the backend:
|
|
@@ -1134,7 +1261,7 @@ both SDKs and the backend:
|
|
|
1134
1261
|
|
|
1135
1262
|
- **Hosted no-rule-match deny is now self-explaining (folds gh#1257).** A
|
|
1136
1263
|
non-empty bundle with `default_action=deny` and zero rule matches (the
|
|
1137
|
-
exact shape of
|
|
1264
|
+
exact shape of that customer's "Db read only" allow-list, which excludes
|
|
1138
1265
|
`bash`) used to deny with the bare, bug-looking message
|
|
1139
1266
|
`No matching policy rule (fail-closed default)` -- no path out. The
|
|
1140
1267
|
deny reason now NAMES the unmatched action (e.g. `bash:find`) and the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.13.
|
|
3
|
+
Version: 1.13.11
|
|
4
4
|
Summary: AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup.
|
|
5
5
|
Project-URL: Homepage, https://controlzero.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.controlzero.ai
|
|
@@ -130,8 +130,8 @@ Templates available (`controlzero init -t <name>`):
|
|
|
130
130
|
- `autogen` — AutoGen starter policy
|
|
131
131
|
- `codex-cli` — Codex CLI hook starter
|
|
132
132
|
- `gemini-cli` — Gemini CLI hook starter
|
|
133
|
-
- `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE:
|
|
134
|
-
- `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (
|
|
133
|
+
- `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE: PRIVATE PREVIEW)
|
|
134
|
+
- `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (BETA)
|
|
135
135
|
|
|
136
136
|
## Loading a policy
|
|
137
137
|
|
|
@@ -75,8 +75,8 @@ Templates available (`controlzero init -t <name>`):
|
|
|
75
75
|
- `autogen` — AutoGen starter policy
|
|
76
76
|
- `codex-cli` — Codex CLI hook starter
|
|
77
77
|
- `gemini-cli` — Gemini CLI hook starter
|
|
78
|
-
- `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE:
|
|
79
|
-
- `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (
|
|
78
|
+
- `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE: PRIVATE PREVIEW)
|
|
79
|
+
- `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (BETA)
|
|
80
80
|
|
|
81
81
|
## Loading a policy
|
|
82
82
|
|
|
@@ -44,6 +44,14 @@ _CANONICAL_TOOLS: set[str] = {
|
|
|
44
44
|
# were in fact firing correctly (#1904). Warn-only path; no decision
|
|
45
45
|
# depends on this set.
|
|
46
46
|
"file_delete", "network",
|
|
47
|
+
# Prompt-submission pseudo-tool (#1966 / #2108). claude_code.py,
|
|
48
|
+
# gemini_cli.py, cursor.py and kiro.py all lift a submitted prompt into
|
|
49
|
+
# tool_name="prompt" so ONE operator-authored `prompt:*` rule governs every
|
|
50
|
+
# host. It was absent here, so writing exactly the rule the feature is for
|
|
51
|
+
# printed "not a known canonical action" -- a correct rule being told it is
|
|
52
|
+
# wrong, which is how an operator concludes the surface is unsupported and
|
|
53
|
+
# stops writing the rule. Warn-only path; no decision depends on this set.
|
|
54
|
+
"prompt",
|
|
47
55
|
# database aliases
|
|
48
56
|
"sql", "Database", "PostgreSQL", "MySQL", "postgres", "sqlite",
|
|
49
57
|
# Bash aliases
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Observe-only DLP scanning for data produced after a call starts.
|
|
2
|
+
|
|
3
|
+
This deliberately reuses :class:`DLPScanner`. It does not evaluate policy
|
|
4
|
+
effects and cannot return a deny: the data has already been produced.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from controlzero._internal.dlp_scanner import (
|
|
13
|
+
DLPScanner,
|
|
14
|
+
extract_text_from_args,
|
|
15
|
+
)
|
|
16
|
+
from controlzero._internal.enforcer import (
|
|
17
|
+
DLP_STATUS_CLEAN,
|
|
18
|
+
DLP_STATUS_FINDINGS,
|
|
19
|
+
DLP_STATUS_SKIPPED,
|
|
20
|
+
DLP_STATUS_UNAVAILABLE,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DLP_OUTPUT_EFFECT_RECORDED_ONLY = "recorded_only"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class DLPOutputObservation:
|
|
28
|
+
"""Terminal, non-enforcing result of scanning one output payload."""
|
|
29
|
+
|
|
30
|
+
scanned: bool
|
|
31
|
+
status: str
|
|
32
|
+
reason: str
|
|
33
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
findings: list[dict] = field(default_factory=list)
|
|
35
|
+
effect: str = DLP_OUTPUT_EFFECT_RECORDED_ONLY
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def observe_dlp_output(
|
|
39
|
+
scanner: Optional[DLPScanner],
|
|
40
|
+
output: Any,
|
|
41
|
+
) -> DLPOutputObservation:
|
|
42
|
+
"""Scan one complete output and collapse all findings into one observation.
|
|
43
|
+
|
|
44
|
+
The caller owns user messaging and audit emission. Returning one object
|
|
45
|
+
per invocation makes the deduplication boundary explicit: ten matches in
|
|
46
|
+
one payload are still one observation and therefore one notice.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
if scanner is None:
|
|
50
|
+
return DLPOutputObservation(
|
|
51
|
+
scanned=False,
|
|
52
|
+
status=DLP_STATUS_UNAVAILABLE,
|
|
53
|
+
reason="scanner_uninitialized",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
text = extract_text_from_args(output)
|
|
58
|
+
except Exception as exc: # observation must never affect the tool result
|
|
59
|
+
return DLPOutputObservation(
|
|
60
|
+
scanned=False,
|
|
61
|
+
status=DLP_STATUS_UNAVAILABLE,
|
|
62
|
+
reason="output_extraction_error",
|
|
63
|
+
details={"error_type": type(exc).__name__},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not text:
|
|
67
|
+
return DLPOutputObservation(
|
|
68
|
+
scanned=False,
|
|
69
|
+
status=DLP_STATUS_SKIPPED,
|
|
70
|
+
reason="no_scannable_output",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
matches = scanner.scan(text)
|
|
75
|
+
except Exception as exc: # observation must never affect the tool result
|
|
76
|
+
return DLPOutputObservation(
|
|
77
|
+
scanned=False,
|
|
78
|
+
status=DLP_STATUS_UNAVAILABLE,
|
|
79
|
+
reason="scanner_error",
|
|
80
|
+
details={"error_type": type(exc).__name__},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not matches:
|
|
84
|
+
return DLPOutputObservation(
|
|
85
|
+
scanned=True,
|
|
86
|
+
status=DLP_STATUS_CLEAN,
|
|
87
|
+
reason="scan_clean",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return DLPOutputObservation(
|
|
91
|
+
scanned=True,
|
|
92
|
+
status=DLP_STATUS_FINDINGS,
|
|
93
|
+
reason="scan_findings",
|
|
94
|
+
findings=scanner.get_findings_for_audit(matches),
|
|
95
|
+
)
|
|
@@ -11,7 +11,7 @@ Two pattern sources:
|
|
|
11
11
|
2. Custom rules: loaded from the policy file's `dlp_rules:` section
|
|
12
12
|
or pulled from backend for enrolled SDKs.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Matched sensitive values store a SHA-256 hash of the matched text, never the
|
|
15
15
|
plaintext. This is consistent with the browser extension's HMAC approach.
|
|
16
16
|
|
|
17
17
|
Pattern categories:
|
|
@@ -28,6 +28,36 @@ import re
|
|
|
28
28
|
from dataclasses import dataclass, field
|
|
29
29
|
from typing import Any, Optional
|
|
30
30
|
|
|
31
|
+
# Categories the backend can emit in SDK bundle rules. This is the union of
|
|
32
|
+
# dashboard-authored custom rule categories (validDLPCategories in
|
|
33
|
+
# dlp_handler.go) and canonical built-in detector categories
|
|
34
|
+
# (internal/dlpcatalog/patterns/*.json). Keep this as a named positive set:
|
|
35
|
+
# every accepted category carries a matched sensitive VALUE and must be hashed.
|
|
36
|
+
# A category introduced by a newer backend is rejected at scan time rather than
|
|
37
|
+
# silently taking a plaintext fallback.
|
|
38
|
+
BACKEND_DLP_VALUE_CATEGORIES = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"compliance",
|
|
41
|
+
"contact",
|
|
42
|
+
"credential",
|
|
43
|
+
"crypto",
|
|
44
|
+
"custom",
|
|
45
|
+
"document",
|
|
46
|
+
"financial",
|
|
47
|
+
"ip",
|
|
48
|
+
"medical",
|
|
49
|
+
"national_id",
|
|
50
|
+
"network",
|
|
51
|
+
"pii",
|
|
52
|
+
"secret",
|
|
53
|
+
"tax",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ``healthcare`` is the older SDK-local spelling used by the embedded NPI/DEA
|
|
58
|
+
# built-ins. The backend catalog spells the corresponding category ``medical``.
|
|
59
|
+
SENSITIVE_VALUE_CATEGORIES = BACKEND_DLP_VALUE_CATEGORIES | {"healthcare"}
|
|
60
|
+
|
|
31
61
|
# ---------------------------------------------------------------------------
|
|
32
62
|
# Built-in patterns grouped by category.
|
|
33
63
|
# All builtins use action="detect". Admins add action="block" via custom rules.
|
|
@@ -575,7 +605,7 @@ class DLPRule:
|
|
|
575
605
|
id: str
|
|
576
606
|
name: str
|
|
577
607
|
pattern: str
|
|
578
|
-
category: str
|
|
608
|
+
category: str
|
|
579
609
|
action: str # "detect", "block", "mask"
|
|
580
610
|
region: str = "global"
|
|
581
611
|
compiled: Optional[re.Pattern] = field(default=None, repr=False)
|
|
@@ -593,14 +623,14 @@ class DLPMatch:
|
|
|
593
623
|
rule_name: str
|
|
594
624
|
category: str
|
|
595
625
|
action: str
|
|
596
|
-
matched_text: str #
|
|
626
|
+
matched_text: str # SHA-256 hash of the matched sensitive value
|
|
597
627
|
offset: int
|
|
598
628
|
count: int = 1
|
|
599
629
|
# Length of the matched span in the SOURCE text. Carried so the
|
|
600
630
|
# masker can splice the exact span out of a string value without
|
|
601
631
|
# re-running the regex. Defaults to len(matched_text) so DLPMatch
|
|
602
|
-
# objects built by older callers (
|
|
603
|
-
#
|
|
632
|
+
# objects built by older callers (where matched_text may not be the raw
|
|
633
|
+
# value) still report a sensible
|
|
604
634
|
# length -- but the masker only ever uses the span length recorded at
|
|
605
635
|
# scan time, never the hash length.
|
|
606
636
|
length: int = 0
|
|
@@ -669,7 +699,7 @@ class DLPScanner:
|
|
|
669
699
|
"""Number of loaded rules."""
|
|
670
700
|
return len(self._rules)
|
|
671
701
|
|
|
672
|
-
def
|
|
702
|
+
def _scan_impl(self, text: str) -> list[DLPMatch]:
|
|
673
703
|
"""Scan text against all loaded DLP rules.
|
|
674
704
|
|
|
675
705
|
Args:
|
|
@@ -678,7 +708,7 @@ class DLPScanner:
|
|
|
678
708
|
|
|
679
709
|
Returns:
|
|
680
710
|
List of DLPMatch objects for each rule that matched.
|
|
681
|
-
|
|
711
|
+
``matched_text`` is a SHA-256 hash for every supported category.
|
|
682
712
|
"""
|
|
683
713
|
if not text or not self._rules:
|
|
684
714
|
return []
|
|
@@ -694,13 +724,19 @@ class DLPScanner:
|
|
|
694
724
|
for m in found:
|
|
695
725
|
raw_text = m.group(0)
|
|
696
726
|
|
|
697
|
-
#
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
727
|
+
# Every supported DLP category carries a sensitive matched
|
|
728
|
+
# VALUE. Enumerate the backend vocabulary positively, and
|
|
729
|
+
# fail loudly on forward drift so a newer category can never
|
|
730
|
+
# inherit a cleartext fallback (#1881, same failure shape as
|
|
731
|
+
# #1927).
|
|
732
|
+
if rule.category not in SENSITIVE_VALUE_CATEGORIES:
|
|
733
|
+
raise ValueError(
|
|
734
|
+
f"unknown DLP value category {rule.category!r}; "
|
|
735
|
+
"refusing to serialize matched text"
|
|
736
|
+
)
|
|
737
|
+
display_text = hashlib.sha256(
|
|
738
|
+
raw_text.encode("utf-8")
|
|
739
|
+
).hexdigest()
|
|
704
740
|
|
|
705
741
|
matches.append(
|
|
706
742
|
DLPMatch(
|
|
@@ -711,7 +747,7 @@ class DLPScanner:
|
|
|
711
747
|
matched_text=display_text,
|
|
712
748
|
offset=m.start(),
|
|
713
749
|
# Span length in the SOURCE text (NOT len(display_text),
|
|
714
|
-
# which
|
|
750
|
+
# which is a 64-char hash). The masker splices
|
|
715
751
|
# exactly [offset, offset+length) out of the string value.
|
|
716
752
|
length=len(raw_text),
|
|
717
753
|
)
|
|
@@ -719,6 +755,39 @@ class DLPScanner:
|
|
|
719
755
|
|
|
720
756
|
return matches
|
|
721
757
|
|
|
758
|
+
def scan(self, text: str) -> list[DLPMatch]:
|
|
759
|
+
"""Scan text on the ENFORCEMENT path -- "should this call be allowed".
|
|
760
|
+
|
|
761
|
+
A thin wrapper over :meth:`_scan_impl` on purpose. The egress
|
|
762
|
+
conformance matrix measures host-surface coverage by patching THIS
|
|
763
|
+
method, so anything that patches or spies on ``scan`` observes only
|
|
764
|
+
enforcement scanning. :meth:`scan_for_egress` goes straight to
|
|
765
|
+
``_scan_impl`` and is therefore invisible to that measurement -- which
|
|
766
|
+
is correct, because our own outbound hygiene is not host coverage and
|
|
767
|
+
counting it as such would inflate the published number.
|
|
768
|
+
"""
|
|
769
|
+
return self._scan_impl(text)
|
|
770
|
+
|
|
771
|
+
def scan_for_egress(self, text: str) -> list[DLPMatch]:
|
|
772
|
+
"""Scan text we are about to SEND OURSELVES, not text we are policing.
|
|
773
|
+
|
|
774
|
+
Same detection, deliberately a different entry point from :meth:`scan`.
|
|
775
|
+
|
|
776
|
+
These are two different jobs. ``scan`` answers "should this tool call be
|
|
777
|
+
allowed" -- a host-surface enforcement question. This answers "are we
|
|
778
|
+
about to put a customer's secret on our own wire" -- a hygiene question
|
|
779
|
+
about Control Zero's own egress (path 5, #1967).
|
|
780
|
+
|
|
781
|
+
Keeping them distinguishable matters beyond tidiness: the egress
|
|
782
|
+
conformance matrix measures host-surface coverage by spying on
|
|
783
|
+
``scan``. When redaction shared that entry point, a cell with no host
|
|
784
|
+
surface at all started reporting as "scanned", which would have
|
|
785
|
+
promoted a declaration and inflated the published coverage number on
|
|
786
|
+
the strength of our own outbound hygiene. The measurement has to
|
|
787
|
+
measure what it says it measures.
|
|
788
|
+
"""
|
|
789
|
+
return self._scan_impl(text)
|
|
790
|
+
|
|
722
791
|
def has_blocking_match(self, matches: list[DLPMatch]) -> bool:
|
|
723
792
|
"""Check if any match has action='block'."""
|
|
724
793
|
return any(m.action == "block" for m in matches)
|
|
@@ -836,12 +905,12 @@ class DLPScanner:
|
|
|
836
905
|
def get_findings_for_audit(self, matches: list[DLPMatch]) -> list[dict]:
|
|
837
906
|
"""Convert matches to audit-safe findings.
|
|
838
907
|
|
|
839
|
-
For
|
|
908
|
+
For detect/block matches, ``matched_text`` is the SHA-256 hash produced
|
|
909
|
+
by :meth:`scan`, for every supported sensitive-value category.
|
|
840
910
|
For mask-action matches, the raw value is REPLACED by the value-free
|
|
841
|
-
mask placeholder ("audit shows the masked value, never the
|
|
911
|
+
mask placeholder ("audit shows the masked value, never the value") --
|
|
842
912
|
consistent with the gateway, which renders a mask as a value-free
|
|
843
|
-
``[REDACTED-...]`` token everywhere.
|
|
844
|
-
matched text is included.
|
|
913
|
+
``[REDACTED-...]`` token everywhere.
|
|
845
914
|
"""
|
|
846
915
|
findings: list[dict] = []
|
|
847
916
|
for m in matches:
|
|
@@ -854,22 +923,26 @@ class DLPScanner:
|
|
|
854
923
|
if m.action == "mask":
|
|
855
924
|
# Mask is enforcement, not observation: the audit row records
|
|
856
925
|
# WHAT the value was masked TO (the value-free placeholder),
|
|
857
|
-
# never the raw bytes
|
|
858
|
-
#
|
|
926
|
+
# never the raw bytes. Detect/block findings carry a digest,
|
|
927
|
+
# while mask findings carry the actual replacement token.
|
|
859
928
|
entry["masked"] = True
|
|
860
929
|
entry["matched_text"] = mask_placeholder(m.rule_id)
|
|
861
|
-
|
|
862
|
-
#
|
|
930
|
+
else:
|
|
931
|
+
# scan() has already replaced every supported category's raw
|
|
932
|
+
# value with its SHA-256 digest.
|
|
863
933
|
entry["matched_text"] = m.matched_text
|
|
864
934
|
findings.append(entry)
|
|
865
935
|
return findings
|
|
866
936
|
|
|
867
937
|
|
|
868
|
-
def extract_text_from_args(args: Any) -> str:
|
|
938
|
+
def extract_text_from_args(args: Any, *, include_keys: bool = False) -> str:
|
|
869
939
|
"""Recursively extract all string values from a nested dict/list structure.
|
|
870
940
|
|
|
871
941
|
Handles nested dicts, lists, and mixed structures. Non-string leaf
|
|
872
|
-
values are converted to their string representation.
|
|
942
|
+
values are converted to their string representation. Mapping keys are
|
|
943
|
+
omitted by default because tool-argument keys are schema, not user data.
|
|
944
|
+
Egress callers that forward user-controlled mappings can set
|
|
945
|
+
``include_keys=True`` to scan every key that will cross the boundary too.
|
|
873
946
|
|
|
874
947
|
Args:
|
|
875
948
|
args: The tool arguments (typically a dict from JSON).
|
|
@@ -878,20 +951,27 @@ def extract_text_from_args(args: Any) -> str:
|
|
|
878
951
|
A single string with all extracted values joined by newlines.
|
|
879
952
|
"""
|
|
880
953
|
parts: list[str] = []
|
|
881
|
-
_extract_recursive(args, parts)
|
|
954
|
+
_extract_recursive(args, parts, include_keys=include_keys)
|
|
882
955
|
return "\n".join(parts)
|
|
883
956
|
|
|
884
957
|
|
|
885
|
-
def _extract_recursive(
|
|
958
|
+
def _extract_recursive(
|
|
959
|
+
obj: Any,
|
|
960
|
+
parts: list[str],
|
|
961
|
+
*,
|
|
962
|
+
include_keys: bool = False,
|
|
963
|
+
) -> None:
|
|
886
964
|
"""Walk a nested structure and collect string representations."""
|
|
887
965
|
if isinstance(obj, str):
|
|
888
966
|
parts.append(obj)
|
|
889
967
|
elif isinstance(obj, dict):
|
|
890
|
-
for
|
|
891
|
-
|
|
968
|
+
for key, value in obj.items():
|
|
969
|
+
if include_keys:
|
|
970
|
+
_extract_recursive(key, parts, include_keys=True)
|
|
971
|
+
_extract_recursive(value, parts, include_keys=include_keys)
|
|
892
972
|
elif isinstance(obj, (list, tuple)):
|
|
893
973
|
for item in obj:
|
|
894
|
-
_extract_recursive(item, parts)
|
|
974
|
+
_extract_recursive(item, parts, include_keys=include_keys)
|
|
895
975
|
elif obj is not None:
|
|
896
976
|
# Convert numbers, bools, etc. to string for scanning
|
|
897
977
|
parts.append(str(obj))
|