controlzero 1.9.0__tar.gz → 1.9.1__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.9.0 → controlzero-1.9.1}/CHANGELOG.md +29 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/PKG-INFO +1 -1
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/__init__.py +1 -1
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/client.py +108 -24
- {controlzero-1.9.0 → controlzero-1.9.1}/pyproject.toml +1 -1
- controlzero-1.9.1/tests/test_hosted_local_audit_1247.py +217 -0
- controlzero-1.9.1/tests/test_log_options_ignored_hosted.py +50 -0
- controlzero-1.9.0/tests/test_log_options_ignored_hosted.py +0 -35
- {controlzero-1.9.0 → controlzero-1.9.1}/.gitignore +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/Dockerfile.test +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/LICENSE +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/README.md +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/types.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/audit_local.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/audit_remote.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/canonical.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/console.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/main.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/device.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/enrollment.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/error_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/errors.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/status.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/layout_migration.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/policy_loader.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_state.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/tamper.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/tracecontext.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/examples/hello_world.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/conftest.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/integrations/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/integrations/test_google.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/conftest.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_action_aliases.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_audit_remote.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_init.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_tail.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_test.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_validate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_conditions.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_conformance.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_console.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_credential_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_default_action.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_device.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_doctor.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_enrollment.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_error_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_glob_matching.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_install_hooks.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_kiro_install.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_log_rotation.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_migrate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_settings.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_quarantine.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_reason_code.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_refresh.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_secrets.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tamper.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tracecontext.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.1}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.1 -- 2026-06-16 (hosted-mode local audit log P0, epic gh#1247)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **(P0) Hosted mode never wrote the local `~/.controlzero/audit.log`.** After
|
|
8
|
+
`controlzero install claude-code --api-key cz_live_...` (hosted mode), tool
|
|
9
|
+
calls reached the dashboard (remote audit) but the local audit log stayed
|
|
10
|
+
frozen indefinitely -- even though the claude-code template and the install
|
|
11
|
+
success message both promise that every tool call is logged to
|
|
12
|
+
`~/.controlzero/audit.log`. Root cause: `Client.__init__` constructed the
|
|
13
|
+
`LocalAuditLogger` ONLY when no API key was set, so hosted mode left the
|
|
14
|
+
local sink `None` and `_audit_decision()` skipped the local write (allow AND
|
|
15
|
+
deny). The local log is now written in EVERY mode; the remote sink is layered
|
|
16
|
+
on top, not instead. A blocked (deny) call is now locally recorded too -- the
|
|
17
|
+
exact case a customer hit (Claude Code `Bash` calls denied by no-rule-match).
|
|
18
|
+
This also restores `cz debug-bundle` and the tamper hash-chain, which read
|
|
19
|
+
the local log. Regression test: `tests/test_hosted_local_audit_1247.py`.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- **Local audit log redacts PII/financial DLP plaintext.** Now that hosted mode
|
|
24
|
+
writes the local plaintext audit log, a DLP finding's raw `matched_text` (which
|
|
25
|
+
is plaintext for the pii/financial categories; the secret category is already
|
|
26
|
+
SHA-256 hashed) is stripped from the LOCAL row -- it would otherwise expose
|
|
27
|
+
PII/financial data to anyone who can read the file, content hosted mode kept
|
|
28
|
+
server-side only. The finding metadata (rule_id/category/location) is
|
|
29
|
+
preserved so the local log still records THAT a rule fired, and the remote
|
|
30
|
+
sink keeps full fidelity.
|
|
31
|
+
|
|
3
32
|
## 1.9.0 -- 2026-06-15 (Antigravity install CLI, epic gh#925)
|
|
4
33
|
|
|
5
34
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.1
|
|
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
|
|
@@ -36,7 +36,6 @@ import sys
|
|
|
36
36
|
import threading
|
|
37
37
|
import time
|
|
38
38
|
import uuid
|
|
39
|
-
import warnings
|
|
40
39
|
import asyncio
|
|
41
40
|
from datetime import datetime, timezone
|
|
42
41
|
from pathlib import Path
|
|
@@ -120,6 +119,53 @@ def _mask_api_key(api_key: Optional[str]) -> str:
|
|
|
120
119
|
return "***"
|
|
121
120
|
|
|
122
121
|
|
|
122
|
+
# DLP categories whose ``matched_text`` is PLAINTEXT in a finding (the secret
|
|
123
|
+
# category is already SHA-256 hashed by the scanner, so it is safe to persist
|
|
124
|
+
# locally). Anything in this set must have its raw matched value stripped
|
|
125
|
+
# before the finding is written to the local plaintext audit log. See
|
|
126
|
+
# ``_redact_local_dlp`` and the #1247 review note in ``_audit_decision``.
|
|
127
|
+
_DLP_PLAINTEXT_CATEGORIES = ("pii", "financial")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _redact_local_dlp(entry: dict) -> dict:
|
|
131
|
+
"""Return a copy of an audit entry safe to write to the LOCAL plaintext log.
|
|
132
|
+
|
|
133
|
+
The only field that carries raw argument-derived sensitive content is
|
|
134
|
+
``dlp_findings[*].matched_text`` for pii/financial categories. We strip the
|
|
135
|
+
raw value (replacing it with a redaction marker) while preserving every
|
|
136
|
+
other field of the finding -- rule_id, category, location, count, etc. --
|
|
137
|
+
so the local log still records THAT a DLP rule fired, just not the matched
|
|
138
|
+
value. Entries without DLP findings are returned unchanged (a cheap
|
|
139
|
+
identity for the overwhelmingly common case).
|
|
140
|
+
"""
|
|
141
|
+
findings = entry.get("dlp_findings")
|
|
142
|
+
if not findings:
|
|
143
|
+
return entry
|
|
144
|
+
needs_redaction = any(
|
|
145
|
+
isinstance(f, dict)
|
|
146
|
+
and f.get("category") in _DLP_PLAINTEXT_CATEGORIES
|
|
147
|
+
and "matched_text" in f
|
|
148
|
+
for f in findings
|
|
149
|
+
)
|
|
150
|
+
if not needs_redaction:
|
|
151
|
+
return entry
|
|
152
|
+
safe = dict(entry)
|
|
153
|
+
redacted_findings = []
|
|
154
|
+
for f in findings:
|
|
155
|
+
if (
|
|
156
|
+
isinstance(f, dict)
|
|
157
|
+
and f.get("category") in _DLP_PLAINTEXT_CATEGORIES
|
|
158
|
+
and "matched_text" in f
|
|
159
|
+
):
|
|
160
|
+
rf = dict(f)
|
|
161
|
+
rf["matched_text"] = "[redacted-local]"
|
|
162
|
+
redacted_findings.append(rf)
|
|
163
|
+
else:
|
|
164
|
+
redacted_findings.append(f)
|
|
165
|
+
safe["dlp_findings"] = redacted_findings
|
|
166
|
+
return safe
|
|
167
|
+
|
|
168
|
+
|
|
123
169
|
class Client:
|
|
124
170
|
"""The ControlZero policy client.
|
|
125
171
|
|
|
@@ -302,35 +348,60 @@ class Client:
|
|
|
302
348
|
|
|
303
349
|
self._tamper_state_dir = Path.home() / ".controlzero"
|
|
304
350
|
|
|
305
|
-
# Set up local audit logger
|
|
306
|
-
#
|
|
307
|
-
#
|
|
351
|
+
# Set up the local audit logger in EVERY mode.
|
|
352
|
+
#
|
|
353
|
+
# P0 regression (epic #1247, 2026-06-15): a customer who ran
|
|
354
|
+
# `controlzero install claude-code --api-key cz_live_...` saw audit
|
|
355
|
+
# rows reach the dashboard (remote bearer sink) but their local
|
|
356
|
+
# ~/.controlzero/audit.log stayed frozen -- it was last appended weeks
|
|
357
|
+
# earlier. Root cause: this block used to create the LocalAuditLogger
|
|
358
|
+
# ONLY when `not self._has_api_key`, so hosted mode left ``self._audit``
|
|
359
|
+
# None and ``_audit_decision`` skipped the local write entirely.
|
|
360
|
+
#
|
|
361
|
+
# That silently broke the day-one value prop the claude-code template
|
|
362
|
+
# AND the install command both promise verbatim:
|
|
363
|
+
# "every Claude Code tool call is logged to ~/.controlzero/audit.log"
|
|
364
|
+
# "Audit log: ~/.controlzero/audit.log"
|
|
365
|
+
# The promise is mode-independent, so the local sink must be too. Local
|
|
366
|
+
# audit is now ALWAYS on (allow AND deny, local AND hosted); the remote
|
|
367
|
+
# bearer/enrolled sink is layered ON TOP in hosted mode, not instead of
|
|
368
|
+
# the local file. The local log is also what `cz debug-bundle` and the
|
|
369
|
+
# tamper hash-chain depend on, so a frozen file degraded those too.
|
|
308
370
|
self._audit: Optional[LocalAuditLogger] = None
|
|
309
|
-
|
|
310
|
-
|
|
371
|
+
# In hosted mode the caller (e.g. the hook-check CLI) may not pass a
|
|
372
|
+
# log_path; default it to the canonical global audit log so a hosted
|
|
373
|
+
# install still writes the file the template/install message advertise.
|
|
374
|
+
effective_log_path = log_path
|
|
375
|
+
if self._has_api_key and log_path == "./controlzero.log":
|
|
376
|
+
effective_log_path = str(Path.home() / ".controlzero" / "audit.log")
|
|
377
|
+
try:
|
|
311
378
|
self._audit = LocalAuditLogger(
|
|
312
|
-
log_path=
|
|
379
|
+
log_path=effective_log_path,
|
|
313
380
|
rotation=log_rotation,
|
|
314
381
|
retention=log_retention,
|
|
315
382
|
compression=log_compression,
|
|
316
383
|
log_format=log_format,
|
|
317
384
|
)
|
|
318
|
-
|
|
319
|
-
#
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
385
|
+
except (OSError, PermissionError) as exc:
|
|
386
|
+
# The local audit log must never crash the client when the FAILURE
|
|
387
|
+
# IS ENVIRONMENTAL: an unwritable log path (read-only HOME, sandbox,
|
|
388
|
+
# full disk) falls back to no local sink. In hosted mode the remote
|
|
389
|
+
# sink still carries the trail; in pure-local mode there may be no
|
|
390
|
+
# remote sink, but the alternative -- crashing every guard() call --
|
|
391
|
+
# is worse, and the warning surfaces the cause.
|
|
392
|
+
#
|
|
393
|
+
# #1247 review (codex): this catch is deliberately NARROW. A broad
|
|
394
|
+
# ``except Exception`` here would also swallow a genuine logger bug
|
|
395
|
+
# or bad config and silently set _audit=None, which in pure-local
|
|
396
|
+
# mode would make audit vanish with no signal. Programming errors
|
|
397
|
+
# must propagate so they are caught in tests/CI, not in production.
|
|
398
|
+
self._audit = None
|
|
399
|
+
import logging as _logging
|
|
400
|
+
_logging.getLogger("controlzero.client").warning(
|
|
401
|
+
"controlzero: local audit log path unavailable (%s); "
|
|
402
|
+
"audit will be recorded remotely only",
|
|
403
|
+
exc,
|
|
326
404
|
)
|
|
327
|
-
if user_set_log_opts:
|
|
328
|
-
warnings.warn(
|
|
329
|
-
"controlzero: log_* options are ignored when an API key is set "
|
|
330
|
-
"(audit is managed server-side).",
|
|
331
|
-
UserWarning,
|
|
332
|
-
stacklevel=2,
|
|
333
|
-
)
|
|
334
405
|
|
|
335
406
|
# --- Hosted policy periodic refresh state --------------------------
|
|
336
407
|
#
|
|
@@ -1746,9 +1817,22 @@ class Client:
|
|
|
1746
1817
|
if "host_tool_name" in context:
|
|
1747
1818
|
entry["host_tool_name"] = context["host_tool_name"]
|
|
1748
1819
|
|
|
1749
|
-
# Local file first (when local audit is enabled)
|
|
1820
|
+
# Local file first (when local audit is enabled).
|
|
1821
|
+
#
|
|
1822
|
+
# #1247 review (codex): now that the local audit log is written in
|
|
1823
|
+
# hosted mode too, we must NOT regress the data-exposure posture.
|
|
1824
|
+
# ``dlp_findings[*].matched_text`` is PLAINTEXT for pii/financial
|
|
1825
|
+
# categories (only the secret category is already SHA-256 hashed --
|
|
1826
|
+
# see dlp_scanner). Previously hosted mode kept that argument-derived
|
|
1827
|
+
# sensitive content server-side only; persisting it to the local
|
|
1828
|
+
# plaintext ~/.controlzero/audit.log would leak PII/financial data to
|
|
1829
|
+
# anyone who can read the file. So the LOCAL row carries DLP findings
|
|
1830
|
+
# with the raw plaintext stripped (rule_id / category / location /
|
|
1831
|
+
# count are preserved so the local log still shows THAT a match fired,
|
|
1832
|
+
# just not the matched value). The REMOTE sinks keep full fidelity --
|
|
1833
|
+
# they ship over TLS to server-side storage exactly as before.
|
|
1750
1834
|
if self._audit is not None:
|
|
1751
|
-
self._audit.log(entry)
|
|
1835
|
+
self._audit.log(_redact_local_dlp(entry))
|
|
1752
1836
|
|
|
1753
1837
|
# Remote sink (enrolled machines: signed-request auth).
|
|
1754
1838
|
if self._remote_sink is not None:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "controlzero"
|
|
7
|
-
version = "1.9.
|
|
7
|
+
version = "1.9.1"
|
|
8
8
|
description = "AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Regression guard for epic #1247: hosted mode MUST write the local audit log.
|
|
2
|
+
|
|
3
|
+
P0 customer regression (2026-06-15): after
|
|
4
|
+
``controlzero install claude-code --api-key cz_live_...`` (hosted mode), a
|
|
5
|
+
customer's Claude Code tool calls reached the dashboard (remote bearer sink)
|
|
6
|
+
but their local ``~/.controlzero/audit.log`` stayed frozen for weeks. Root
|
|
7
|
+
cause: ``Client.__init__`` only constructed the ``LocalAuditLogger`` when NO
|
|
8
|
+
API key was set, so hosted mode left ``self._audit`` ``None`` and
|
|
9
|
+
``_audit_decision`` skipped the local write entirely.
|
|
10
|
+
|
|
11
|
+
That silently broke the day-one value prop the claude-code template AND the
|
|
12
|
+
``controlzero install`` message both promise verbatim: every tool call is
|
|
13
|
+
logged to ``~/.controlzero/audit.log``. The promise is mode-independent, so the
|
|
14
|
+
local sink must be too.
|
|
15
|
+
|
|
16
|
+
These tests pin the contract so the core audit path cannot silently regress
|
|
17
|
+
again:
|
|
18
|
+
|
|
19
|
+
* Hosted mode (api_key set) writes a LOCAL audit row for an ALLOW decision.
|
|
20
|
+
* Hosted mode writes a LOCAL audit row for a DENY decision (a blocked call
|
|
21
|
+
MUST be locally recorded -- this is the exact shape the customer hit:
|
|
22
|
+
Claude Code Bash calls denied by the policy engine).
|
|
23
|
+
* Hosted mode still ALSO ships the row remotely (bearer sink), i.e. the fix
|
|
24
|
+
layers local audit ON TOP of remote, it does not trade one for the other.
|
|
25
|
+
* Both rows carry ``client_name == "claude_code"`` so the dashboard + local
|
|
26
|
+
log attribute the call to the right host adapter.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
from unittest.mock import MagicMock, patch
|
|
33
|
+
|
|
34
|
+
from controlzero import Client
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _make_fake_bearer_sink():
|
|
38
|
+
"""A BearerAuditSink stand-in that records every entry instead of POSTing."""
|
|
39
|
+
sink = MagicMock()
|
|
40
|
+
sink.logged = []
|
|
41
|
+
sink.log.side_effect = lambda entry: sink.logged.append(entry)
|
|
42
|
+
sink.close = MagicMock()
|
|
43
|
+
return sink
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_local_audit_rows(log_path):
|
|
47
|
+
"""Parse the local audit log into a list of decoded JSON rows.
|
|
48
|
+
|
|
49
|
+
The LocalAuditLogger writes one JSON object per line (loguru ``{message}``
|
|
50
|
+
sink). Lines that are not pure JSON (loguru may not be installed, in which
|
|
51
|
+
case it falls back to a formatted stderr line) are skipped.
|
|
52
|
+
"""
|
|
53
|
+
rows = []
|
|
54
|
+
if not log_path.exists():
|
|
55
|
+
return rows
|
|
56
|
+
for line in log_path.read_text(encoding="utf-8").splitlines():
|
|
57
|
+
line = line.strip()
|
|
58
|
+
if not line:
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
rows.append(json.loads(line))
|
|
62
|
+
except json.JSONDecodeError:
|
|
63
|
+
continue
|
|
64
|
+
return rows
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _hosted_client(monkeypatch, tmp_path, *, allow: bool, fake_sink):
|
|
68
|
+
"""Build a hosted-mode Client whose evaluator returns a fixed allow/deny.
|
|
69
|
+
|
|
70
|
+
Patches the hosted bundle load + the bearer sink so no network is touched.
|
|
71
|
+
The evaluator is stubbed to return the requested effect deterministically,
|
|
72
|
+
independent of whatever the (mocked) bundle contains.
|
|
73
|
+
"""
|
|
74
|
+
monkeypatch.setenv("CONTROLZERO_API_KEY", "cz_live_1247_regression")
|
|
75
|
+
monkeypatch.delenv("CONTROLZERO_LOCAL_OVERRIDE", raising=False)
|
|
76
|
+
|
|
77
|
+
fake_parsed = MagicMock()
|
|
78
|
+
fake_parsed.payload = {"project_id": "proj-1247"}
|
|
79
|
+
|
|
80
|
+
log_path = tmp_path / "audit.log"
|
|
81
|
+
|
|
82
|
+
with patch(
|
|
83
|
+
"controlzero.audit_remote.BearerAuditSink", return_value=fake_sink
|
|
84
|
+
), patch(
|
|
85
|
+
"controlzero.hosted_policy.load_hosted_policy",
|
|
86
|
+
return_value=({"version": "1", "rules": [{"allow": "*"}]}, fake_parsed),
|
|
87
|
+
), patch(
|
|
88
|
+
"controlzero.hosted_policy.load_cached_bundle",
|
|
89
|
+
return_value=None,
|
|
90
|
+
), patch(
|
|
91
|
+
"controlzero.client.load_policy",
|
|
92
|
+
return_value=MagicMock(rules=[], settings=MagicMock()),
|
|
93
|
+
):
|
|
94
|
+
cz = Client(log_path=str(log_path))
|
|
95
|
+
|
|
96
|
+
# Stub the evaluator so the decision is deterministic regardless of bundle.
|
|
97
|
+
from controlzero._internal.enforcer import PolicyDecision
|
|
98
|
+
|
|
99
|
+
effect = "allow" if allow else "deny"
|
|
100
|
+
decision = PolicyDecision(
|
|
101
|
+
effect=effect,
|
|
102
|
+
reason="ok" if allow else "blocked by policy (no rule match)",
|
|
103
|
+
policy_id="rule-1" if allow else "synthetic:NO_RULE_MATCH",
|
|
104
|
+
)
|
|
105
|
+
cz._evaluator = MagicMock()
|
|
106
|
+
cz._evaluator.evaluate.return_value = decision
|
|
107
|
+
return cz, log_path
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_hosted_mode_writes_local_audit_on_allow(monkeypatch, tmp_path):
|
|
111
|
+
fake_sink = _make_fake_bearer_sink()
|
|
112
|
+
cz, log_path = _hosted_client(
|
|
113
|
+
monkeypatch, tmp_path, allow=True, fake_sink=fake_sink
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
# Hosted Client MUST have a live local audit sink (the regression was
|
|
117
|
+
# this being None whenever an API key was set).
|
|
118
|
+
assert cz._audit is not None, (
|
|
119
|
+
"hosted-mode Client must construct a local audit logger "
|
|
120
|
+
"(#1247: it used to be None when api_key was set)"
|
|
121
|
+
)
|
|
122
|
+
cz.guard(
|
|
123
|
+
"Bash",
|
|
124
|
+
args={"command": "echo hi"},
|
|
125
|
+
method="echo",
|
|
126
|
+
context={"client_name": "claude_code"},
|
|
127
|
+
)
|
|
128
|
+
finally:
|
|
129
|
+
cz.close()
|
|
130
|
+
|
|
131
|
+
rows = _read_local_audit_rows(log_path)
|
|
132
|
+
allow_rows = [r for r in rows if r.get("decision") == "allow"]
|
|
133
|
+
assert allow_rows, f"expected a local ALLOW audit row, got rows: {rows}"
|
|
134
|
+
row = allow_rows[-1]
|
|
135
|
+
assert row["tool"] == "Bash", row
|
|
136
|
+
assert row["client_name"] == "claude_code", row
|
|
137
|
+
assert row["policy_source"] == "hosted", row
|
|
138
|
+
|
|
139
|
+
# The fix layers local ON TOP of remote: the bearer sink still got the row.
|
|
140
|
+
remote_rows = [e for e in fake_sink.logged if e.get("mode") != "lifecycle"]
|
|
141
|
+
assert remote_rows, "hosted mode must still ship the row to the remote sink"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_hosted_mode_writes_local_audit_on_deny(monkeypatch, tmp_path):
|
|
145
|
+
"""The exact customer shape: a Claude Code call DENIED in hosted mode must
|
|
146
|
+
still produce a local audit row. A blocked call that leaves no local trace
|
|
147
|
+
is the worst case -- it is precisely the action an auditor needs to see."""
|
|
148
|
+
fake_sink = _make_fake_bearer_sink()
|
|
149
|
+
cz, log_path = _hosted_client(
|
|
150
|
+
monkeypatch, tmp_path, allow=False, fake_sink=fake_sink
|
|
151
|
+
)
|
|
152
|
+
try:
|
|
153
|
+
assert cz._audit is not None, "hosted-mode Client must have a local audit logger"
|
|
154
|
+
decision = cz.guard(
|
|
155
|
+
"Bash",
|
|
156
|
+
args={"command": "rm -rf /tmp/x"},
|
|
157
|
+
method="rm",
|
|
158
|
+
context={"client_name": "claude_code"},
|
|
159
|
+
)
|
|
160
|
+
assert decision.denied, "test setup expected a deny decision"
|
|
161
|
+
finally:
|
|
162
|
+
cz.close()
|
|
163
|
+
|
|
164
|
+
rows = _read_local_audit_rows(log_path)
|
|
165
|
+
deny_rows = [r for r in rows if r.get("decision") == "deny"]
|
|
166
|
+
assert deny_rows, f"expected a local DENY audit row, got rows: {rows}"
|
|
167
|
+
row = deny_rows[-1]
|
|
168
|
+
assert row["tool"] == "Bash", row
|
|
169
|
+
assert row["client_name"] == "claude_code", row
|
|
170
|
+
assert row["policy_source"] == "hosted", row
|
|
171
|
+
|
|
172
|
+
remote_rows = [e for e in fake_sink.logged if e.get("mode") != "lifecycle"]
|
|
173
|
+
assert remote_rows, "hosted mode must still ship the deny row to the remote sink"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_local_dlp_redaction_strips_plaintext_but_remote_keeps_it():
|
|
177
|
+
"""#1247 review (codex): the local plaintext audit row must NOT carry raw
|
|
178
|
+
pii/financial ``matched_text``. The remote sink keeps full fidelity.
|
|
179
|
+
|
|
180
|
+
Now that hosted mode writes the local audit.log, persisting a DLP match's
|
|
181
|
+
raw plaintext value to ~/.controlzero/audit.log would leak PII/financial
|
|
182
|
+
data to anyone who can read the file -- content hosted mode previously
|
|
183
|
+
kept server-side only. The redaction preserves the finding's metadata
|
|
184
|
+
(rule_id/category/location) so the local log still shows a match fired.
|
|
185
|
+
"""
|
|
186
|
+
from controlzero.client import _redact_local_dlp
|
|
187
|
+
|
|
188
|
+
entry = {
|
|
189
|
+
"decision": "deny",
|
|
190
|
+
"tool": "Bash",
|
|
191
|
+
"dlp_findings": [
|
|
192
|
+
{"rule_id": "ssn", "category": "pii", "matched_text": "123-45-6789", "location": "command"},
|
|
193
|
+
{"rule_id": "ccn", "category": "financial", "matched_text": "4111111111111111"},
|
|
194
|
+
# secret category is ALREADY hashed by the scanner -> safe, keep as-is.
|
|
195
|
+
{"rule_id": "aws", "category": "secret", "matched_text": "sha256:deadbeef"},
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
safe = _redact_local_dlp(entry)
|
|
200
|
+
|
|
201
|
+
# Local copy: pii/financial plaintext stripped, secret hash preserved.
|
|
202
|
+
by_rule = {f["rule_id"]: f for f in safe["dlp_findings"]}
|
|
203
|
+
assert by_rule["ssn"]["matched_text"] == "[redacted-local]"
|
|
204
|
+
assert by_rule["ccn"]["matched_text"] == "[redacted-local]"
|
|
205
|
+
assert by_rule["aws"]["matched_text"] == "sha256:deadbeef"
|
|
206
|
+
# Metadata preserved so the local log still records THAT a rule fired.
|
|
207
|
+
assert by_rule["ssn"]["category"] == "pii"
|
|
208
|
+
assert by_rule["ssn"]["location"] == "command"
|
|
209
|
+
|
|
210
|
+
# The ORIGINAL entry (which the remote sinks consume) is untouched: full
|
|
211
|
+
# fidelity ships to server-side storage exactly as before.
|
|
212
|
+
assert entry["dlp_findings"][0]["matched_text"] == "123-45-6789"
|
|
213
|
+
assert entry["dlp_findings"][1]["matched_text"] == "4111111111111111"
|
|
214
|
+
|
|
215
|
+
# No DLP findings -> identity (cheap common path).
|
|
216
|
+
plain = {"decision": "allow", "tool": "Read"}
|
|
217
|
+
assert _redact_local_dlp(plain) is plain
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Audit-log option behavior across hosted + pure-local modes.
|
|
2
|
+
|
|
3
|
+
History: before epic #1247 (2026-06-15), hosted mode (API key set) DISABLED the
|
|
4
|
+
local audit log entirely and warned that ``log_*`` options were ignored. That
|
|
5
|
+
was the regression -- a customer's ``~/.controlzero/audit.log`` froze while the
|
|
6
|
+
dashboard kept receiving rows. The fix makes the local audit log ALWAYS active
|
|
7
|
+
(see ``Client.__init__`` and ``tests/test_hosted_local_audit_1247.py``), so the
|
|
8
|
+
``log_*`` options are now HONORED in hosted mode and the old "ignored" warning
|
|
9
|
+
no longer fires.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
from controlzero import Client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_log_options_honored_in_hosted_mode(monkeypatch, tmp_path):
|
|
18
|
+
"""Hosted mode (api key set) now WRITES the local audit log, so the
|
|
19
|
+
log_* options take effect and the legacy 'ignored' warning is gone."""
|
|
20
|
+
monkeypatch.setenv("CONTROLZERO_API_KEY", "cz_test_fakekey")
|
|
21
|
+
custom_log = tmp_path / "custom.log"
|
|
22
|
+
with warnings.catch_warnings(record=True) as w:
|
|
23
|
+
warnings.simplefilter("always")
|
|
24
|
+
cz = Client(
|
|
25
|
+
policy={"rules": [{"allow": "*"}]},
|
|
26
|
+
log_path=str(custom_log),
|
|
27
|
+
log_rotation="1 MB",
|
|
28
|
+
)
|
|
29
|
+
log_warnings = [x for x in w if "log_*" in str(x.message)]
|
|
30
|
+
assert not log_warnings, (
|
|
31
|
+
"#1247: hosted mode now honors log_* options (local audit always "
|
|
32
|
+
f"on); no 'ignored' warning expected, got: {[str(x.message) for x in log_warnings]}"
|
|
33
|
+
)
|
|
34
|
+
# A local audit sink must exist in hosted mode now.
|
|
35
|
+
assert cz._audit is not None
|
|
36
|
+
cz.close()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_log_options_silent_in_pure_local(tmp_log):
|
|
40
|
+
with warnings.catch_warnings(record=True) as w:
|
|
41
|
+
warnings.simplefilter("always")
|
|
42
|
+
cz = Client(
|
|
43
|
+
policy={"rules": [{"allow": "*"}]},
|
|
44
|
+
log_path=str(tmp_log),
|
|
45
|
+
log_rotation="1 MB",
|
|
46
|
+
)
|
|
47
|
+
# No warning expected
|
|
48
|
+
log_warnings = [x for x in w if "log_*" in str(x.message)]
|
|
49
|
+
assert not log_warnings
|
|
50
|
+
cz.close()
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"""When API key is set (hybrid mode), log_* options are ignored with a warning."""
|
|
2
|
-
|
|
3
|
-
import warnings
|
|
4
|
-
|
|
5
|
-
from controlzero import Client
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_log_options_warn_in_hybrid_mode(monkeypatch):
|
|
9
|
-
"""Hybrid mode (api key + local policy): log_* options have no effect
|
|
10
|
-
because audit ships to remote, so we warn the user."""
|
|
11
|
-
monkeypatch.setenv("CONTROLZERO_API_KEY", "cz_test_fakekey")
|
|
12
|
-
with warnings.catch_warnings(record=True) as w:
|
|
13
|
-
warnings.simplefilter("always")
|
|
14
|
-
Client(
|
|
15
|
-
policy={"rules": [{"allow": "*"}]},
|
|
16
|
-
log_path="/tmp/custom.log",
|
|
17
|
-
log_rotation="1 MB",
|
|
18
|
-
)
|
|
19
|
-
assert any(
|
|
20
|
-
"log_*" in str(warning.message) for warning in w
|
|
21
|
-
), f"expected log_* warning, got: {[str(x.message) for x in w]}"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_log_options_silent_in_pure_local(tmp_log):
|
|
25
|
-
with warnings.catch_warnings(record=True) as w:
|
|
26
|
-
warnings.simplefilter("always")
|
|
27
|
-
cz = Client(
|
|
28
|
-
policy={"rules": [{"allow": "*"}]},
|
|
29
|
-
log_path=str(tmp_log),
|
|
30
|
-
log_rotation="1 MB",
|
|
31
|
-
)
|
|
32
|
-
# No warning expected
|
|
33
|
-
log_warnings = [x for x in w if "log_*" in str(x.message)]
|
|
34
|
-
assert not log_warnings
|
|
35
|
-
cz.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credentials_data/built_in.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|