controlzero 1.13.9__tar.gz → 1.13.10__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.10}/CHANGELOG.md +66 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/PKG-INFO +3 -3
- {controlzero-1.13.9 → controlzero-1.13.10}/README.md +2 -2
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/__init__.py +1 -1
- controlzero-1.13.10/controlzero/_internal/dlp_output_observer.py +95 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/dlp_scanner.py +110 -30
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/enforcer.py +16 -11
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/audit_remote.py +197 -2
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/claude_code.py +263 -1
- controlzero-1.13.10/controlzero/cli/hosts/gemini_cli.py +221 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/main.py +480 -62
- controlzero-1.13.10/controlzero/cli/maturity.py +72 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/setup_wizard.py +294 -30
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/spool_cmd.py +1 -1
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/client.py +149 -29
- controlzero-1.13.10/controlzero/integrations/_export_dlp.py +171 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/braintrust.py +32 -11
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/task.py +20 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langfuse.py +68 -22
- {controlzero-1.13.9 → controlzero-1.13.10}/pyproject.toml +1 -1
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/claude_code.json +14 -1
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/gemini_cli.json +13 -2
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_hook.py +84 -0
- controlzero-1.13.10/tests/test_dlp_audit_hashing_1881.py +88 -0
- controlzero-1.13.10/tests/test_dlp_egress_conformance_matrix.py +1091 -0
- controlzero-1.13.10/tests/test_dlp_path5_audit_egress_1967.py +174 -0
- controlzero-1.13.10/tests/test_dlp_paths_8_9_observe_1967.py +170 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_scanner.py +17 -14
- controlzero-1.13.10/tests/test_feature_parity_maturity.py +187 -0
- controlzero-1.13.10/tests/test_integration_export_dlp_1967.py +294 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_pr4_install_messaging.py +5 -1
- controlzero-1.13.10/tests/test_prompt_submit_dlp_1966.py +421 -0
- controlzero-1.13.10/tests/test_setup_wizard.py +504 -0
- controlzero-1.13.9/controlzero/cli/hosts/gemini_cli.py +0 -114
- controlzero-1.13.9/tests/test_setup_wizard.py +0 -196
- {controlzero-1.13.9 → controlzero-1.13.10}/.gitignore +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/Dockerfile.test +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/LICENSE +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/__main__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/_messages.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/condition_ops.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/dlp_remediation.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/io_capture_producer.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/source_labels.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/types.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/audit_local.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/canonical.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/agent_config.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/console.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/enforcement.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/exec_cmd.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hook_entry.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/cursor.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/cursor_cli.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/output.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/device.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/enrollment.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/error_codes.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/errors.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/status.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hook_integrity.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/layout_migration.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/policy_loader.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/reference.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/resolver.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/secretstr.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_state.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/tamper.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/tracecontext.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/examples/hello_world.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/ruff.toml +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/conftest.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/antigravity.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/codex_cli.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/cursor.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/kiro.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/integrations/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/integrations/test_google.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/__init__.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/conftest.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_action_aliases.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_adapter_contracts.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_adapter_interface.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_agy_docs_no_exit2_1373.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_envelope_parity.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_ga_blockers_1248.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_tool_vocab_1303.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_api_url_config.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_project_id_attribution_pr2.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_remote.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_remote_io_capture.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_bundle_cache_freshness_1303.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_capability_matrix_doc.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_coverage.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_hook_entry.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_init.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_tail.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_test.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_validate.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_condition_operators_1495.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_conditions.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_config_format_parity_1303.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_conformance.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_console.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_credential_hook.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cross_adapter_shell_enforcement.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cursor_adapter.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dashboard_shell_template_1904.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_default_action.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_deny_rule_reconciliation_1011.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_deny_wins_default_catchall_1620.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_device.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_agent_remediation_1938.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_mask.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_mask_hosts_1938.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_origin_contract.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_scan_coverage_1927_1931.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_doctor.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_enrollment.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_error_codes.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_failopen_1303.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_git_target_1734.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_glob_matching.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_command_pinning_1705.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_integrity.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_mask_failclosed.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_warn_surfaces_reason_1903.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_install_api_url.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_install_hooks.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_io_capture_producer.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_agent_harness_routing.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_cli_v210_hooks.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_ide_v2_hooks_1944.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_install.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_install_standardisation.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_version_schemes.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_localized_reason.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_log_rotation.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_migrate.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_part3_active_policy_count_1303.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_per_client_isolation_1011.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_load_failclosed_f3.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_settings.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_pr4_output.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_pr4_status.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_probe_audit_isolation.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_quarantine.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_reason_code.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_reason_code_hitl_grant_1596.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_refresh.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_secrets.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_secrets_reference_resolver.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_shell_semantic_class.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_source_labels.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tamper.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tls_ca_bundle.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tracecontext.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_update_cmd.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_upgrade_nudge.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_version_aware_fail_closed.py +0 -0
- {controlzero-1.13.9 → controlzero-1.13.10}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.10 (2026-07-30): DLP now covers prompts and tool output, not just tool arguments
|
|
4
|
+
|
|
5
|
+
Until this release, Control Zero scanned the ARGUMENTS of a tool call and
|
|
6
|
+
nothing else. A secret could reach a model or leave the machine by three other
|
|
7
|
+
routes that looked identical to a working setup. All three are closed here.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **A secret pasted into a prompt was sent to the model unscanned (#1966).**
|
|
12
|
+
Prompt submission is now a scanned surface on the hosts that expose one
|
|
13
|
+
(Claude Code `UserPromptSubmit`, Gemini CLI `BeforeAgent`). Previously only
|
|
14
|
+
tool calls were checked, so pasting a key into the chat box egressed it.
|
|
15
|
+
|
|
16
|
+
- **Credentials from dashboard-configured DLP rules were written to the audit
|
|
17
|
+
log in cleartext (#1881 follow-up).** The scanner hashed matches only when
|
|
18
|
+
the rule category was the literal string `secret`; dashboard rules arrive as
|
|
19
|
+
`credential`, so those matches were stored verbatim. The audit trail was
|
|
20
|
+
keeping the secret it had just detected. Now gated on a named set of
|
|
21
|
+
value-bearing categories, so a category added later cannot reach cleartext.
|
|
22
|
+
|
|
23
|
+
- **Spooled audit records were replayed without ever being scanned (#1967).**
|
|
24
|
+
Replay validated JSON shape, tool name and decision -- never payload content
|
|
25
|
+
-- then transmitted the stored bytes as-is, possibly days later on a
|
|
26
|
+
different network. Records written before this release are treated as
|
|
27
|
+
unscanned rather than trusted.
|
|
28
|
+
|
|
29
|
+
- **The auth cache dropped `key_type`, silently disabling the audit
|
|
30
|
+
anti-spoof guarantee.** `key_type == "extension"` is server-authoritative
|
|
31
|
+
for a row's `surface_class`; on every cache HIT the field was empty, so the
|
|
32
|
+
wire-supplied value was honoured instead. The guarantee held only on the
|
|
33
|
+
first request after a cache miss.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **Sensitive data in tool OUTPUT is now detected and recorded (#1965).**
|
|
38
|
+
Reading a file that contains a secret is caught. The tool call is NOT
|
|
39
|
+
blocked -- an agent reading a file is normal work -- so this is deliberately
|
|
40
|
+
observe-and-record: one deduplicated message per tool call, and an audit row
|
|
41
|
+
carrying `effect=recorded_only` that is never labelled a block.
|
|
42
|
+
|
|
43
|
+
- **DLP runs before export to third-party observability (#1967).** Braintrust
|
|
44
|
+
and Langfuse were receiving full tool arguments and output previews with no
|
|
45
|
+
scan in between.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- API-key creation rejects a `key_type` outside `{live, test}` with
|
|
50
|
+
`INVALID_CREDENTIAL_KIND` (400) instead of accepting any string and
|
|
51
|
+
interpolating it into the stored key prefix. Bulk extension-key prefixes are
|
|
52
|
+
limited to 1-8 characters of `[a-z0-9-]`, which is what the database column
|
|
53
|
+
actually fits.
|
|
54
|
+
|
|
55
|
+
### Notes for anyone writing a policy by hand
|
|
56
|
+
|
|
57
|
+
DLP rules go under `dlp_rules:`, and **every rule needs a `pattern:`**. A rule
|
|
58
|
+
without one is dropped silently. Our own published recipe used `dlp:` with
|
|
59
|
+
`pattern_group:` -- neither is read by anything -- so it configured no DLP at
|
|
60
|
+
all. That recipe is corrected, and CI now fails if a shipped example uses a key
|
|
61
|
+
the loader does not consume.
|
|
62
|
+
|
|
63
|
+
### Not in this release
|
|
64
|
+
|
|
65
|
+
Tool output is observed, not blocked. Control Zero's own audit egress (the path
|
|
66
|
+
where we ship your data to our backend) is still unscanned; that is tracked and
|
|
67
|
+
is the last uncovered egress path.
|
|
68
|
+
|
|
3
69
|
## 1.13.9 (2026-07-28): Kiro IDE hooks execute on 1.0.x and stay in the correct project
|
|
4
70
|
|
|
5
71
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.13.
|
|
3
|
+
Version: 1.13.10
|
|
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
|
|
|
@@ -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))
|
|
@@ -634,6 +634,16 @@ class PolicyEvaluator:
|
|
|
634
634
|
"""Set or replace the DLP scanner."""
|
|
635
635
|
self._dlp_scanner = scanner
|
|
636
636
|
|
|
637
|
+
def observe_output(self, output):
|
|
638
|
+
"""DLP-scan already-produced data without changing its disposition.
|
|
639
|
+
|
|
640
|
+
Kept on the evaluator so this path uses the exact scanner instance
|
|
641
|
+
configured for argument DLP, including built-ins and policy rules.
|
|
642
|
+
"""
|
|
643
|
+
from controlzero._internal.dlp_output_observer import observe_dlp_output
|
|
644
|
+
|
|
645
|
+
return observe_dlp_output(self._dlp_scanner, output)
|
|
646
|
+
|
|
637
647
|
def set_default_action(self, action: str) -> None:
|
|
638
648
|
"""Replace the no-match default action.
|
|
639
649
|
|
|
@@ -1447,16 +1457,11 @@ class PolicyEvaluator:
|
|
|
1447
1457
|
residual = self._dlp_scanner.scan(masked_text) if masked_text else []
|
|
1448
1458
|
if self._dlp_scanner.has_masking_match(residual):
|
|
1449
1459
|
# Build the deny's audit findings from ONLY the residual
|
|
1450
|
-
# MASK-action matches
|
|
1451
|
-
#
|
|
1452
|
-
# value) would otherwise have get_findings_for_audit echo its
|
|
1453
|
-
# RAW matched_text into the audit row -- raw PII in the audit
|
|
1454
|
-
# even on a block is a #202 leak (codex re-review of #1427).
|
|
1460
|
+
# MASK-action matches so the row records the exact fail-closed
|
|
1461
|
+
# condition, not unrelated detect findings on the same bytes.
|
|
1455
1462
|
# Mask findings render as the value-free [REDACTED-...]
|
|
1456
|
-
# placeholder
|
|
1457
|
-
#
|
|
1458
|
-
# appear here: a block hit is handled earlier and short-circuits
|
|
1459
|
-
# to deny before the mask branch.)
|
|
1463
|
+
# placeholder. (Block-action matches cannot appear here: a
|
|
1464
|
+
# block hit short-circuits before the mask branch.)
|
|
1460
1465
|
residual_mask_only = [m for m in residual if m.action == "mask"]
|
|
1461
1466
|
offending = residual_mask_only[0]
|
|
1462
1467
|
return PolicyDecision(
|
|
@@ -1479,8 +1484,8 @@ class PolicyEvaluator:
|
|
|
1479
1484
|
# Audit the firing MASK rule(s) WITHOUT echoing the raw
|
|
1480
1485
|
# value: get_findings_for_audit renders mask findings as the
|
|
1481
1486
|
# value-free placeholder, so the deny row records that a
|
|
1482
|
-
# mask rule fired, never the
|
|
1483
|
-
#
|
|
1487
|
+
# mask rule fired, never the sensitive value. Unrelated
|
|
1488
|
+
# detect findings are deliberately excluded from this path.
|
|
1484
1489
|
dlp_findings=self._dlp_scanner.get_findings_for_audit(
|
|
1485
1490
|
residual_mask_only
|
|
1486
1491
|
),
|