controlzero 1.13.0__tar.gz → 1.13.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.13.0 → controlzero-1.13.1}/CHANGELOG.md +16 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/PKG-INFO +1 -1
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/__init__.py +1 -1
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/enforcer.py +24 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/hook_extractors.py +455 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/main.py +26 -1
- {controlzero-1.13.0 → controlzero-1.13.1}/pyproject.toml +1 -1
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_glob_matching.py +86 -0
- controlzero-1.13.1/tests/test_shell_semantic_class.py +244 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/.gitignore +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/Dockerfile.test +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/LICENSE +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/README.md +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/__main__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/_messages.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/condition_ops.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/io_capture_producer.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/source_labels.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/_internal/types.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/audit_local.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/audit_remote.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/canonical.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/agent_config.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/console.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/exec_cmd.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hook_entry.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/cursor.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/cursor_cli.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/output.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/client.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/device.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/enrollment.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/error_codes.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/errors.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hitl/status.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hook_integrity.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/google.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/layout_migration.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/policy_loader.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/secrets/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/secrets/reference.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/secrets/resolver.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/secrets/secretstr.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_state.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/tamper.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/controlzero/tracecontext.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/examples/hello_world.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/conftest.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/integrations/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/integrations/test_google.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/__init__.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/conftest.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_action_aliases.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_adapter_interface.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_agy_docs_no_exit2_1373.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_antigravity_envelope_parity.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_antigravity_ga_blockers_1248.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_antigravity_tool_vocab_1303.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_audit_project_id_attribution_pr2.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_audit_remote.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_audit_remote_io_capture.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_bundle_cache_freshness_1303.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_capability_matrix_doc.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_hook.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_hook_entry.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_init.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_tail.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_test.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cli_validate.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_condition_operators_1495.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_conditions.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_config_format_parity_1303.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_conformance.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_console.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_credential_hook.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_cursor_adapter.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_default_action.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_deny_rule_reconciliation_1011.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_device.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_dlp_mask.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_doctor.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_enrollment.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_error_codes.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_failopen_1303.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hook_integrity.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hook_mask_failclosed.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_install_hooks.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_io_capture_producer.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_kiro_install.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_localized_reason.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_log_rotation.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_migrate.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_part3_active_policy_count_1303.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_per_client_isolation_1011.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_policy_load_failclosed_f3.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_policy_settings.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_pr4_install_messaging.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_pr4_output.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_pr4_status.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_probe_audit_isolation.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_quarantine.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_reason_code.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_refresh.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_secrets.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_secrets_reference_resolver.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_source_labels.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_tamper.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_tracecontext.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_update_cmd.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tests/test_upgrade_nudge.py +0 -0
- {controlzero-1.13.0 → controlzero-1.13.1}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.1 (2026-06-27): shell semantic-class enforcement
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Shell semantic-class extractor (#437/#1459).** The enforcer maps a shell
|
|
8
|
+
command's first meaningful token to a semantic class
|
|
9
|
+
(`file_write`/`file_delete`/`file_read`/`network`), so a `deny: file_write`
|
|
10
|
+
rule also blocks shell-mediated file operations (e.g. `Bash(touch x)` ->
|
|
11
|
+
`file_write`). Additive layer after the per-command action, most-dangerous-wins
|
|
12
|
+
(`file_delete > network > file_write > file_read`), byte-identical to the Node
|
|
13
|
+
and Go SDKs (shared `parity-cases.json`, 118 cases). Closes the cross-OS
|
|
14
|
+
bypass where a denied `Write` was retried as a PowerShell/`cmd`/Unix file
|
|
15
|
+
write through the Bash tool. Unwraps `sh -c`/`powershell -Command`/`cmd /c`
|
|
16
|
+
launchers and prefix wrappers; recurses `find -delete`/`find -exec`;
|
|
17
|
+
whole-line scan for `.NET` method calls and `/dev/tcp/`.
|
|
18
|
+
|
|
3
19
|
## 1.13.0 (2026-06-26): policy-condition operator vocab
|
|
4
20
|
|
|
5
21
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.13.
|
|
3
|
+
Version: 1.13.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
|
|
@@ -470,6 +470,30 @@ class PolicyEvaluator:
|
|
|
470
470
|
cls = sql_semantic_class(sql_text)
|
|
471
471
|
if cls:
|
|
472
472
|
semantic_action = f"{tool}:{cls}"
|
|
473
|
+
# Shell semantic-class layer (#437): the SAME additive pattern for
|
|
474
|
+
# shell-mediated file/network ops. When the tool is the canonical
|
|
475
|
+
# shell (`Bash`) and no class was provided, derive it from the
|
|
476
|
+
# command and promote it into the file_write/file_delete/file_read/
|
|
477
|
+
# network NAMESPACE (`<class>:<class>`) -- NOT `Bash:<class>` --
|
|
478
|
+
# so a rule `deny: file_write` (normalized `file_write:*`) ALSO
|
|
479
|
+
# blocks `Bash(touch x)`. Existing `Bash:*` rules are unaffected
|
|
480
|
+
# because the per-command `action` is always tried first. The
|
|
481
|
+
# hook-check CLI provides the class via
|
|
482
|
+
# ``context['action_semantic_class']``; this is the on-the-fly
|
|
483
|
+
# path for direct ``guard("Bash", args={"command": ...})`` callers.
|
|
484
|
+
if not semantic_action and tool == "Bash" and args:
|
|
485
|
+
from controlzero._internal.hook_extractors import shell_semantic_class
|
|
486
|
+
cmd_text = None
|
|
487
|
+
if isinstance(args, dict):
|
|
488
|
+
for _key in ("command", "CommandLine"):
|
|
489
|
+
_v = args.get(_key)
|
|
490
|
+
if isinstance(_v, str) and _v:
|
|
491
|
+
cmd_text = _v
|
|
492
|
+
break
|
|
493
|
+
if cmd_text:
|
|
494
|
+
cls = shell_semantic_class(cmd_text)
|
|
495
|
+
if cls:
|
|
496
|
+
semantic_action = f"{cls}:{cls}"
|
|
473
497
|
candidate_actions: list[str] = [action]
|
|
474
498
|
if semantic_action and semantic_action != action:
|
|
475
499
|
candidate_actions.append(semantic_action)
|
|
@@ -161,6 +161,97 @@ _SHELL_DANGER_ORDER = (
|
|
|
161
161
|
)
|
|
162
162
|
_SHELL_DANGER_RANK = {cmd: i for i, cmd in enumerate(_SHELL_DANGER_ORDER)}
|
|
163
163
|
|
|
164
|
+
# Mapping from shell command basename -> canonical semantic class (#437).
|
|
165
|
+
#
|
|
166
|
+
# A second, portable layer parallel to most_dangerous_shell_command's
|
|
167
|
+
# per-command method. A shell-mediated file or network operation is
|
|
168
|
+
# promoted into the file_write / file_delete / file_read / network tool
|
|
169
|
+
# namespace so a rule written against `file_write` (or `file_write:*`)
|
|
170
|
+
# ALSO blocks `Bash(touch x)` -- not just `Write`. Existing `Bash:*`
|
|
171
|
+
# rules are unaffected: this class is an ADDITIVE candidate the
|
|
172
|
+
# evaluator tries AFTER the per-command action (mirrors how the SQL
|
|
173
|
+
# semantic class layers on top of the per-keyword method).
|
|
174
|
+
#
|
|
175
|
+
# Keys are lowercased; the lookup lowercases the basenamed first token,
|
|
176
|
+
# so PowerShell PascalCase cmdlets (New-Item) and any case mix resolve.
|
|
177
|
+
# Covers Unix coreutils, PowerShell cmdlets + their built-in aliases, and
|
|
178
|
+
# cmd.exe builtins, because on Windows most agents spawn PowerShell/cmd and
|
|
179
|
+
# wrap commands via the Bash tool (the #1459 production bypass:
|
|
180
|
+
# `Bash("New-Item ... | Out-Null; Set-Content ...")`). Shell output
|
|
181
|
+
# redirection (`>`/`>>`) and .NET method calls are detected separately and
|
|
182
|
+
# ALSO classify. DELIBERATELY EXCLUDED to avoid denying common-benign
|
|
183
|
+
# commands: bare `sc` (sc.exe service control), `mkdir`/`md`, `echo`,
|
|
184
|
+
# `dir`/`ls`/`gci`, `set`, and arg-aware verbs (`certutil`, `sed`, `dd`,
|
|
185
|
+
# `robocopy`, `awk`, `tar`, `openssl`, `git`, `rsync`) handled in a follow-up.
|
|
186
|
+
_SHELL_CLASS_MAP = {
|
|
187
|
+
# ---- file_write: create / append / copy / move-dest / rename ----
|
|
188
|
+
# Unix
|
|
189
|
+
"touch": "file_write", "tee": "file_write", "cp": "file_write",
|
|
190
|
+
"mv": "file_write", "install": "file_write", "ln": "file_write",
|
|
191
|
+
"truncate": "file_write", "sponge": "file_write", "split": "file_write",
|
|
192
|
+
"patch": "file_write",
|
|
193
|
+
# PowerShell cmdlets + aliases (sc EXCLUDED: sc.exe collision)
|
|
194
|
+
"new-item": "file_write", "ni": "file_write",
|
|
195
|
+
"set-content": "file_write", "add-content": "file_write", "ac": "file_write",
|
|
196
|
+
"out-file": "file_write", "tee-object": "file_write",
|
|
197
|
+
"copy-item": "file_write", "cpi": "file_write",
|
|
198
|
+
"move-item": "file_write", "mi": "file_write",
|
|
199
|
+
"rename-item": "file_write", "rni": "file_write",
|
|
200
|
+
"export-csv": "file_write", "epcsv": "file_write",
|
|
201
|
+
"export-clixml": "file_write",
|
|
202
|
+
# cmd.exe
|
|
203
|
+
"copy": "file_write", "xcopy": "file_write", "move": "file_write",
|
|
204
|
+
"ren": "file_write", "rename": "file_write", "mklink": "file_write",
|
|
205
|
+
# ---- file_delete: irreversible removal / truncate-to-empty ----
|
|
206
|
+
# Unix
|
|
207
|
+
"rm": "file_delete", "rmdir": "file_delete", "unlink": "file_delete",
|
|
208
|
+
"shred": "file_delete", "srm": "file_delete", "wipe": "file_delete",
|
|
209
|
+
"trash": "file_delete", "trash-put": "file_delete",
|
|
210
|
+
# PowerShell
|
|
211
|
+
"remove-item": "file_delete", "ri": "file_delete",
|
|
212
|
+
"clear-content": "file_delete", "clc": "file_delete",
|
|
213
|
+
"clear-item": "file_delete", "cli": "file_delete",
|
|
214
|
+
"remove-itemproperty": "file_delete", "rp": "file_delete",
|
|
215
|
+
# cmd.exe
|
|
216
|
+
"del": "file_delete", "erase": "file_delete", "rd": "file_delete",
|
|
217
|
+
"format": "file_delete",
|
|
218
|
+
# ---- file_read: disclosure ----
|
|
219
|
+
# Unix
|
|
220
|
+
"cat": "file_read", "less": "file_read", "more": "file_read",
|
|
221
|
+
"head": "file_read", "tail": "file_read", "grep": "file_read",
|
|
222
|
+
"od": "file_read", "hexdump": "file_read", "strings": "file_read",
|
|
223
|
+
"cut": "file_read", "tac": "file_read", "nl": "file_read",
|
|
224
|
+
"rev": "file_read", "diff": "file_read", "cmp": "file_read",
|
|
225
|
+
"paste": "file_read", "wc": "file_read",
|
|
226
|
+
# PowerShell
|
|
227
|
+
"get-content": "file_read", "gc": "file_read",
|
|
228
|
+
"select-string": "file_read", "sls": "file_read",
|
|
229
|
+
"import-csv": "file_read", "ipcsv": "file_read",
|
|
230
|
+
"import-clixml": "file_read",
|
|
231
|
+
# cmd.exe readers. `type` EXCLUDED: in Bash/Zsh `type foo` is benign command
|
|
232
|
+
# introspection (`type node`), only a file read in cmd/PowerShell -- needs
|
|
233
|
+
# shell context (follow-up). cmd `find` EXCLUDED (ambiguous with Unix find).
|
|
234
|
+
"fc": "file_read", "comp": "file_read", "findstr": "file_read",
|
|
235
|
+
# ---- network: exfiltration / remote transfer / download ----
|
|
236
|
+
# cross-shell
|
|
237
|
+
"curl": "network", "wget": "network", "nc": "network", "ncat": "network",
|
|
238
|
+
"socat": "network", "telnet": "network", "ssh": "network", "scp": "network",
|
|
239
|
+
"sftp": "network", "gh": "network", "ftp": "network", "tftp": "network",
|
|
240
|
+
"smbclient": "network", "lftp": "network", "aria2c": "network",
|
|
241
|
+
# PowerShell (certutil/bitsadmin EXCLUDED: arg-aware, follow-up)
|
|
242
|
+
"invoke-webrequest": "network", "iwr": "network",
|
|
243
|
+
"invoke-restmethod": "network", "irm": "network",
|
|
244
|
+
"start-bitstransfer": "network",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Class danger ordering: lowest index wins when a compound command's
|
|
248
|
+
# segments resolve to different classes. file_delete (irreversible) >
|
|
249
|
+
# network (exfiltration) > file_write (mutation) > file_read
|
|
250
|
+
# (disclosure). A pipeline `curl evil | rm -rf /` therefore resolves to
|
|
251
|
+
# file_delete so a deny-file_delete rule catches the worst class.
|
|
252
|
+
_SHELL_CLASS_DANGER_ORDER = ("file_delete", "network", "file_write", "file_read")
|
|
253
|
+
_SHELL_CLASS_DANGER_RANK = {c: i for i, c in enumerate(_SHELL_CLASS_DANGER_ORDER)}
|
|
254
|
+
|
|
164
255
|
|
|
165
256
|
# ---------------------------------------------------------------------------
|
|
166
257
|
# SQL literal/comment stripping
|
|
@@ -440,6 +531,63 @@ def _first_token_basename(segment: str) -> str:
|
|
|
440
531
|
return token
|
|
441
532
|
|
|
442
533
|
|
|
534
|
+
def _has_file_redirect(segment: str) -> bool:
|
|
535
|
+
"""True if ``segment`` redirects output to a FILE (``>`` or ``>>``).
|
|
536
|
+
|
|
537
|
+
A grammar-free scan mirroring the deliberately dumb shell tokenizer
|
|
538
|
+
(#437): a ``>`` is a file redirect unless it is part of a
|
|
539
|
+
comparison / arrow (``->``, ``=>``, ``<>``) or its target is a
|
|
540
|
+
file-descriptor duplication (``2>&1``, ``>&2``). Quote handling is
|
|
541
|
+
intentionally absent, so ``echo "a > b"`` is conservatively
|
|
542
|
+
classified as a write -- the safe failure direction for a deny rule.
|
|
543
|
+
|
|
544
|
+
Byte-identical to the Node ``hasFileRedirect`` and Go
|
|
545
|
+
``hasFileRedirect`` implementations.
|
|
546
|
+
"""
|
|
547
|
+
n = len(segment)
|
|
548
|
+
i = 0
|
|
549
|
+
# Depth of Bash test / arithmetic contexts. A ``>`` inside ``[[ ... ]]``
|
|
550
|
+
# or ``(( ... ))`` is a string/arith comparison, never a file redirect, so
|
|
551
|
+
# ``[[ "$v" > "1.2" ]]`` must not be classified as a write (would wrongly
|
|
552
|
+
# deny a legitimate conditional under ``deny: file_write``).
|
|
553
|
+
test_depth = 0
|
|
554
|
+
while i < n:
|
|
555
|
+
two = segment[i:i + 2]
|
|
556
|
+
if two == "[[" or two == "((":
|
|
557
|
+
test_depth += 1
|
|
558
|
+
i += 2
|
|
559
|
+
continue
|
|
560
|
+
if two == "]]" or two == "))":
|
|
561
|
+
if test_depth > 0:
|
|
562
|
+
test_depth -= 1
|
|
563
|
+
i += 2
|
|
564
|
+
continue
|
|
565
|
+
if segment[i] != ">":
|
|
566
|
+
i += 1
|
|
567
|
+
continue
|
|
568
|
+
if test_depth > 0:
|
|
569
|
+
i += 1
|
|
570
|
+
continue
|
|
571
|
+
prev = segment[i - 1] if i > 0 else ""
|
|
572
|
+
# ``->``, ``=>``, ``<>`` are operators / dup-source, not a redirect.
|
|
573
|
+
if prev in ("-", "=", "<"):
|
|
574
|
+
i += 1
|
|
575
|
+
continue
|
|
576
|
+
j = i + 1
|
|
577
|
+
# Consume the second ``>`` of an append redirect (``>>``).
|
|
578
|
+
if j < n and segment[j] == ">":
|
|
579
|
+
j += 1
|
|
580
|
+
# Skip whitespace between the operator and its target.
|
|
581
|
+
while j < n and segment[j] in (" ", "\t"):
|
|
582
|
+
j += 1
|
|
583
|
+
# A real file redirect has a target that is not ``&`` (which
|
|
584
|
+
# would be an fd duplication like ``2>&1`` / ``>&2``, not a file).
|
|
585
|
+
if j < n and segment[j] != "&":
|
|
586
|
+
return True
|
|
587
|
+
i = j
|
|
588
|
+
return False
|
|
589
|
+
|
|
590
|
+
|
|
443
591
|
def most_dangerous_shell_command(cmd: str) -> str:
|
|
444
592
|
"""Return the most dangerous shell command across every segment.
|
|
445
593
|
|
|
@@ -491,6 +639,312 @@ def most_dangerous_shell_command(cmd: str) -> str:
|
|
|
491
639
|
return first_segment_token
|
|
492
640
|
|
|
493
641
|
|
|
642
|
+
# --- #1459 hardening: bounded wrapper/launcher unwrap ----------------------
|
|
643
|
+
# The extractor still does NOT parse shell grammar. It unwraps a fixed,
|
|
644
|
+
# auditable set of evasion forms so a `deny: file_write` rule is not defeated
|
|
645
|
+
# by `sh -c 'touch x'`, `env touch x`, `sudo rm`, `xargs rm`, `busybox touch`,
|
|
646
|
+
# or `find . -delete`. Forms outside this set (interpreter `-c` with arbitrary
|
|
647
|
+
# code, base64|sh, eval of variables, `-EncodedCommand`) remain documented
|
|
648
|
+
# known limitations. Byte-identical across Python/Node/Go; the adversarial
|
|
649
|
+
# cases are pinned in ``shell_semantic_class_cases`` in ``parity-cases.json``.
|
|
650
|
+
_SHELL_LAUNCHERS = frozenset({"sh", "bash", "zsh", "dash", "ash", "ksh", "su"})
|
|
651
|
+
_POWERSHELL_LAUNCHERS = frozenset({"powershell", "pwsh"})
|
|
652
|
+
_CMD_LAUNCHERS = frozenset({"cmd"})
|
|
653
|
+
# Prefix wrappers consume only flags/operands before the real command. Wrappers
|
|
654
|
+
# that take a POSITIONAL arg first (flock <lockfile>, chroot <dir>, script <file>)
|
|
655
|
+
# are excluded -- they need positional handling and would otherwise misread the
|
|
656
|
+
# arg as the command.
|
|
657
|
+
_SHELL_PREFIX_WRAPPERS = frozenset({
|
|
658
|
+
"sudo", "doas", "env", "command", "builtin", "exec", "nohup", "setsid",
|
|
659
|
+
"time", "nice", "ionice", "stdbuf", "timeout", "xargs", "busybox",
|
|
660
|
+
"watch", "unshare", "nsenter", "parallel",
|
|
661
|
+
})
|
|
662
|
+
_POWERSHELL_CMD_FLAGS = frozenset({"-command", "-c"})
|
|
663
|
+
_CMD_LAUNCHER_FLAGS = frozenset({"/c", "/k"})
|
|
664
|
+
_MAX_SHELL_UNWRAP_DEPTH = 5
|
|
665
|
+
|
|
666
|
+
# Whole-line substring scan (NOT first-token): PowerShell .NET method calls and
|
|
667
|
+
# bash /dev/tcp sockets bypass the verb layer. Lowercased substring -> class.
|
|
668
|
+
_DOTNET_WRITE_SUBSTRINGS = (
|
|
669
|
+
"[system.io.file]::writealltext", "[system.io.file]::appendalltext",
|
|
670
|
+
"[system.io.file]::writeallbytes", "[system.io.file]::create",
|
|
671
|
+
"[system.io.file]::copy", "[system.io.file]::move",
|
|
672
|
+
"streamwriter", "filestream",
|
|
673
|
+
)
|
|
674
|
+
_DOTNET_READ_SUBSTRINGS = (
|
|
675
|
+
"[system.io.file]::readalltext", "[system.io.file]::readallbytes",
|
|
676
|
+
"[system.io.file]::openread", "streamreader",
|
|
677
|
+
)
|
|
678
|
+
_DOTNET_DELETE_SUBSTRINGS = (
|
|
679
|
+
"[system.io.file]::delete", "[system.io.directory]::delete",
|
|
680
|
+
)
|
|
681
|
+
_NETWORK_SUBSTRINGS = (
|
|
682
|
+
".downloadstring", ".downloadfile", ".uploadfile",
|
|
683
|
+
"net.webclient", "net.sockets", "/dev/tcp/", "/dev/udp/",
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _whole_line_class(cmd_lower: str) -> str:
|
|
688
|
+
"""Most-dangerous class from a lowercased whole-command substring scan,
|
|
689
|
+
catching .NET method calls (`[System.IO.File]::WriteAllText`, StreamWriter)
|
|
690
|
+
and bash sockets (`/dev/tcp/`) that are not the first token. ``""`` if none."""
|
|
691
|
+
best_rank: Optional[int] = None
|
|
692
|
+
best_class: str = ""
|
|
693
|
+
|
|
694
|
+
def consider(cls: str) -> None:
|
|
695
|
+
nonlocal best_rank, best_class
|
|
696
|
+
rank = _SHELL_CLASS_DANGER_RANK[cls]
|
|
697
|
+
if best_rank is None or rank < best_rank:
|
|
698
|
+
best_rank = rank
|
|
699
|
+
best_class = cls
|
|
700
|
+
|
|
701
|
+
if any(s in cmd_lower for s in _DOTNET_DELETE_SUBSTRINGS):
|
|
702
|
+
consider("file_delete")
|
|
703
|
+
if any(s in cmd_lower for s in _NETWORK_SUBSTRINGS):
|
|
704
|
+
consider("network")
|
|
705
|
+
if any(s in cmd_lower for s in _DOTNET_WRITE_SUBSTRINGS):
|
|
706
|
+
consider("file_write")
|
|
707
|
+
if any(s in cmd_lower for s in _DOTNET_READ_SUBSTRINGS):
|
|
708
|
+
consider("file_read")
|
|
709
|
+
return best_class
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _cmd_launcher_script(tokens: list) -> Optional[str]:
|
|
713
|
+
"""Script after cmd.exe's ``/c`` or ``/k`` (`cmd /c <command>`), dequoted."""
|
|
714
|
+
for j, t in enumerate(tokens):
|
|
715
|
+
if t.lower() in _CMD_LAUNCHER_FLAGS:
|
|
716
|
+
if j + 1 < len(tokens):
|
|
717
|
+
return _strip_matched_quotes(" ".join(tokens[j + 1:]))
|
|
718
|
+
return None
|
|
719
|
+
return None
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _basename_lower(token: str) -> str:
|
|
723
|
+
idx = token.rfind("/")
|
|
724
|
+
base = token[idx + 1:] if idx >= 0 else token
|
|
725
|
+
return base.lower()
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _is_env_assignment(token: str) -> bool:
|
|
729
|
+
"""True for a ``VAR=value`` env-assignment prefix token."""
|
|
730
|
+
eq = token.find("=")
|
|
731
|
+
if eq <= 0:
|
|
732
|
+
return False
|
|
733
|
+
name = token[:eq]
|
|
734
|
+
if not (name[0].isalpha() or name[0] == "_"):
|
|
735
|
+
return False
|
|
736
|
+
return all(c.isalnum() or c == "_" for c in name)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
# Wrapper option flags that take a SEPARATE operand token, so the operand is
|
|
740
|
+
# skipped too (``sudo -u root rm``, ``xargs -I {} rm``, ``nice -n 10 rm``).
|
|
741
|
+
# Deliberately small + unambiguous: ``-i`` (env ignore-env, no operand) is
|
|
742
|
+
# excluded so ``env -i touch x`` still classifies ``touch``.
|
|
743
|
+
_WRAPPER_VALUE_FLAGS = frozenset({"-u", "--user", "-g", "--group", "-I", "-n", "-p"})
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _is_duration_or_number(token: str) -> bool:
|
|
747
|
+
"""A bare number or a duration like ``5`` / ``5s`` (timeout's operand)."""
|
|
748
|
+
if not token:
|
|
749
|
+
return False
|
|
750
|
+
body = token[:-1] if (token[-1] in "smhd" and len(token) >= 2) else token
|
|
751
|
+
if not body:
|
|
752
|
+
return False
|
|
753
|
+
has_digit = False
|
|
754
|
+
for c in body:
|
|
755
|
+
if c.isdigit():
|
|
756
|
+
has_digit = True
|
|
757
|
+
elif c != ".":
|
|
758
|
+
return False
|
|
759
|
+
return has_digit
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _is_wrapper_skip_arg(token: str) -> bool:
|
|
763
|
+
"""A token consumed while skipping a prefix wrapper's own args: an option
|
|
764
|
+
(``-x``), a ``VAR=val`` assignment, or a duration/number (``timeout 5s``)."""
|
|
765
|
+
if not token:
|
|
766
|
+
return True
|
|
767
|
+
if token[0] == "-":
|
|
768
|
+
return True
|
|
769
|
+
if _is_env_assignment(token):
|
|
770
|
+
return True
|
|
771
|
+
return _is_duration_or_number(token)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _launcher_script(tokens: list) -> Optional[str]:
|
|
775
|
+
"""Script after a shell launcher's ``-c`` flag, including grouped flags
|
|
776
|
+
like ``-ec`` / ``-lc`` (the rest of the segment, dequoted), or ``None``."""
|
|
777
|
+
for j, t in enumerate(tokens):
|
|
778
|
+
low = t.lower()
|
|
779
|
+
is_c = low == "-c" or (
|
|
780
|
+
len(low) >= 2
|
|
781
|
+
and low[0] == "-"
|
|
782
|
+
and low[1] != "-"
|
|
783
|
+
and low.endswith("c")
|
|
784
|
+
and all(ch.isalpha() for ch in low[1:])
|
|
785
|
+
)
|
|
786
|
+
if is_c:
|
|
787
|
+
if j + 1 < len(tokens):
|
|
788
|
+
return _strip_matched_quotes(" ".join(tokens[j + 1:]))
|
|
789
|
+
return None
|
|
790
|
+
return None
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _strip_matched_quotes(s: str) -> str:
|
|
794
|
+
s = s.strip()
|
|
795
|
+
if len(s) >= 2 and s[0] == s[-1] and s[0] in ("'", '"'):
|
|
796
|
+
return s[1:-1]
|
|
797
|
+
return s
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _script_after_flag(tokens: list, flags) -> Optional[str]:
|
|
801
|
+
"""The script string following a standalone ``-c`` / ``-Command`` token
|
|
802
|
+
(the rest of the segment, dequoted), or ``None`` if absent."""
|
|
803
|
+
for j, t in enumerate(tokens):
|
|
804
|
+
if t.lower() in flags:
|
|
805
|
+
if j + 1 < len(tokens):
|
|
806
|
+
return _strip_matched_quotes(" ".join(tokens[j + 1:]))
|
|
807
|
+
return None
|
|
808
|
+
return None
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _classify_find(tokens: list, depth: int) -> str:
|
|
812
|
+
"""find(1): ``-delete`` is a delete; ``-exec/-execdir CMD ... ;`` classifies
|
|
813
|
+
CMD by recursing through the same unwrap (so ``-exec sh -c 'rm x'`` and
|
|
814
|
+
``-exec sudo rm`` are caught); a plain search find has no class."""
|
|
815
|
+
if "-delete" in tokens:
|
|
816
|
+
return "file_delete"
|
|
817
|
+
for flag in ("-exec", "-execdir"):
|
|
818
|
+
if flag in tokens:
|
|
819
|
+
j = tokens.index(flag)
|
|
820
|
+
rest = []
|
|
821
|
+
for t in tokens[j + 1:]:
|
|
822
|
+
if t in (";", "\\;", "+"):
|
|
823
|
+
break
|
|
824
|
+
rest.append(t)
|
|
825
|
+
if rest and depth < _MAX_SHELL_UNWRAP_DEPTH:
|
|
826
|
+
return _classify_shell_segment(" ".join(rest), depth + 1)
|
|
827
|
+
return ""
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _classify_shell_segment(seg: str, depth: int) -> str:
|
|
831
|
+
"""Most-dangerous class for one already-split segment, after unwrapping a
|
|
832
|
+
bounded set of launchers/prefixes. ``""`` when nothing classifies."""
|
|
833
|
+
best_rank: Optional[int] = None
|
|
834
|
+
best_class: str = ""
|
|
835
|
+
|
|
836
|
+
def consider(cls: str) -> None:
|
|
837
|
+
nonlocal best_rank, best_class
|
|
838
|
+
if not cls:
|
|
839
|
+
return
|
|
840
|
+
rank = _SHELL_CLASS_DANGER_RANK[cls]
|
|
841
|
+
if best_rank is None or rank < best_rank:
|
|
842
|
+
best_rank = rank
|
|
843
|
+
best_class = cls
|
|
844
|
+
|
|
845
|
+
if _has_file_redirect(seg):
|
|
846
|
+
consider("file_write")
|
|
847
|
+
|
|
848
|
+
tokens = seg.split()
|
|
849
|
+
i = 0
|
|
850
|
+
while i < len(tokens) and _is_env_assignment(tokens[i]):
|
|
851
|
+
i += 1
|
|
852
|
+
guard = 0
|
|
853
|
+
while i < len(tokens) and guard < 8:
|
|
854
|
+
guard += 1
|
|
855
|
+
base = _basename_lower(tokens[i])
|
|
856
|
+
if base in _SHELL_LAUNCHERS:
|
|
857
|
+
sub = _launcher_script(tokens[i:])
|
|
858
|
+
if sub is not None and depth < _MAX_SHELL_UNWRAP_DEPTH:
|
|
859
|
+
consider(_shell_semantic_class_inner(sub, depth + 1))
|
|
860
|
+
break
|
|
861
|
+
if base in _POWERSHELL_LAUNCHERS:
|
|
862
|
+
sub = _script_after_flag(tokens[i:], _POWERSHELL_CMD_FLAGS)
|
|
863
|
+
if sub is not None and depth < _MAX_SHELL_UNWRAP_DEPTH:
|
|
864
|
+
consider(_shell_semantic_class_inner(sub, depth + 1))
|
|
865
|
+
break
|
|
866
|
+
if base in _CMD_LAUNCHERS:
|
|
867
|
+
sub = _cmd_launcher_script(tokens[i:])
|
|
868
|
+
if sub is not None and depth < _MAX_SHELL_UNWRAP_DEPTH:
|
|
869
|
+
consider(_shell_semantic_class_inner(sub, depth + 1))
|
|
870
|
+
break
|
|
871
|
+
if base == "find":
|
|
872
|
+
consider(_classify_find(tokens[i:], depth))
|
|
873
|
+
break
|
|
874
|
+
if base in _SHELL_PREFIX_WRAPPERS:
|
|
875
|
+
i += 1
|
|
876
|
+
while i < len(tokens) and _is_wrapper_skip_arg(tokens[i]):
|
|
877
|
+
flag = tokens[i]
|
|
878
|
+
i += 1
|
|
879
|
+
# skip the operand of a value-taking flag (sudo -u USER, etc.)
|
|
880
|
+
if flag in _WRAPPER_VALUE_FLAGS and i < len(tokens) and not tokens[i].startswith("-"):
|
|
881
|
+
i += 1
|
|
882
|
+
continue
|
|
883
|
+
consider(_SHELL_CLASS_MAP.get(base, ""))
|
|
884
|
+
break
|
|
885
|
+
|
|
886
|
+
return best_class
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _shell_semantic_class_inner(cmd: str, depth: int) -> str:
|
|
890
|
+
if not isinstance(cmd, str) or not cmd.strip():
|
|
891
|
+
return ""
|
|
892
|
+
if depth > _MAX_SHELL_UNWRAP_DEPTH:
|
|
893
|
+
return ""
|
|
894
|
+
best_rank: Optional[int] = None
|
|
895
|
+
best_class: str = ""
|
|
896
|
+
|
|
897
|
+
def consider(cls: str) -> None:
|
|
898
|
+
nonlocal best_rank, best_class
|
|
899
|
+
if not cls:
|
|
900
|
+
return
|
|
901
|
+
rank = _SHELL_CLASS_DANGER_RANK[cls]
|
|
902
|
+
if best_rank is None or rank < best_rank:
|
|
903
|
+
best_rank = rank
|
|
904
|
+
best_class = cls
|
|
905
|
+
|
|
906
|
+
# Whole-line scan first (.NET method calls / /dev/tcp are not first tokens).
|
|
907
|
+
consider(_whole_line_class(cmd.lower()))
|
|
908
|
+
for seg in _split_shell_segments(cmd):
|
|
909
|
+
consider(_classify_shell_segment(seg, depth))
|
|
910
|
+
return best_class
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def shell_semantic_class(cmd: str) -> str:
|
|
914
|
+
"""Return the most-dangerous shell semantic class across every segment.
|
|
915
|
+
|
|
916
|
+
Maps shell-mediated file and network operations to one of
|
|
917
|
+
``file_write | file_delete | file_read | network`` (or ``""`` when
|
|
918
|
+
no segment classifies). Closes the #437 bypass where a policy rule
|
|
919
|
+
``deny: file_write`` blocked ``Write(...)`` but NOT the equivalent
|
|
920
|
+
``Bash(touch x)`` / ``PowerShell(New-Item ...)`` because the shell
|
|
921
|
+
was treated as an opaque tool.
|
|
922
|
+
|
|
923
|
+
Per segment the class is the more-dangerous of:
|
|
924
|
+
|
|
925
|
+
- the basenamed, lowercased first command token looked up in
|
|
926
|
+
``_SHELL_CLASS_MAP`` (``rm`` -> file_delete, ``curl`` -> network,
|
|
927
|
+
``New-Item`` -> file_write), and
|
|
928
|
+
- ``file_write`` when the segment redirects output to a file
|
|
929
|
+
(``>`` / ``>>``), so ``cat a > b`` is a write, not a read.
|
|
930
|
+
|
|
931
|
+
Compound commands (pipes, ``;``, ``&&``, ``||``, command
|
|
932
|
+
substitution) reuse :func:`_split_shell_segments` -- the SAME
|
|
933
|
+
tokenizer :func:`most_dangerous_shell_command` uses -- and the
|
|
934
|
+
most-dangerous class across all segments wins (file_delete >
|
|
935
|
+
network > file_write > file_read).
|
|
936
|
+
|
|
937
|
+
Empty / non-string / unrecognized input -> ``""``. The shell grammar
|
|
938
|
+
is deliberately not parsed (no quote stripping, no alias expansion);
|
|
939
|
+
see the spec README "Known limitations".
|
|
940
|
+
|
|
941
|
+
Byte-identical to the Node ``shellSemanticClass`` and Go
|
|
942
|
+
``ShellSemanticClass`` implementations; parity is asserted by
|
|
943
|
+
``shell_semantic_class_cases`` in ``parity-cases.json``.
|
|
944
|
+
"""
|
|
945
|
+
return _shell_semantic_class_inner(cmd, 0)
|
|
946
|
+
|
|
947
|
+
|
|
494
948
|
def identity(v: str) -> str:
|
|
495
949
|
"""Return ``v`` unchanged. Used for browser ``action``."""
|
|
496
950
|
if not isinstance(v, str):
|
|
@@ -646,6 +1100,7 @@ __all__ = [
|
|
|
646
1100
|
"most_dangerous_shell_command",
|
|
647
1101
|
"most_dangerous_sql_keyword",
|
|
648
1102
|
"resolve_canonical_tool",
|
|
1103
|
+
"shell_semantic_class",
|
|
649
1104
|
"sql_semantic_class",
|
|
650
1105
|
"tool_name_as_method",
|
|
651
1106
|
]
|
|
@@ -29,7 +29,11 @@ from controlzero._internal.enforcer import (
|
|
|
29
29
|
REASON_CODE_MACHINE_QUARANTINED,
|
|
30
30
|
REASON_CODE_RULE_MATCH,
|
|
31
31
|
)
|
|
32
|
-
from controlzero._internal.hook_extractors import
|
|
32
|
+
from controlzero._internal.hook_extractors import (
|
|
33
|
+
extract_method,
|
|
34
|
+
shell_semantic_class,
|
|
35
|
+
sql_semantic_class,
|
|
36
|
+
)
|
|
33
37
|
from controlzero.cli import agent_config
|
|
34
38
|
from controlzero.cli import output as cli_output
|
|
35
39
|
from controlzero.cli.output import Severity
|
|
@@ -1611,6 +1615,27 @@ def hook_check(policy: Optional[str], strict_flag: Optional[bool]):
|
|
|
1611
1615
|
semantic_class = sql_semantic_class(sql_text)
|
|
1612
1616
|
if semantic_class:
|
|
1613
1617
|
semantic_action = f"{canonical_tool}:{semantic_class}"
|
|
1618
|
+
elif canonical_tool == "Bash":
|
|
1619
|
+
# Shell semantic-class layer (#437): promote a shell-mediated
|
|
1620
|
+
# file/network op into the file_write/file_delete/file_read/
|
|
1621
|
+
# network NAMESPACE so a `deny: file_write` rule ALSO blocks
|
|
1622
|
+
# `Bash(touch x)` / `PowerShell(New-Item ...)`. The action form is
|
|
1623
|
+
# `<class>:<class>` (NOT `Bash:<class>`) so the normalized
|
|
1624
|
+
# `file_write:*` rule matches. Additive: the per-command
|
|
1625
|
+
# `Bash:<cmd>` action is always evaluated first, so existing
|
|
1626
|
+
# `Bash:*` rules are unaffected. Reads `command` (Claude Code /
|
|
1627
|
+
# Gemini CLI) or `CommandLine` (Antigravity run_command).
|
|
1628
|
+
cmd_text = None
|
|
1629
|
+
if isinstance(_tool_args_dict, dict):
|
|
1630
|
+
for _key in ("command", "CommandLine"):
|
|
1631
|
+
_v = _tool_args_dict.get(_key)
|
|
1632
|
+
if isinstance(_v, str) and _v:
|
|
1633
|
+
cmd_text = _v
|
|
1634
|
+
break
|
|
1635
|
+
if cmd_text:
|
|
1636
|
+
semantic_class = shell_semantic_class(cmd_text)
|
|
1637
|
+
if semantic_class:
|
|
1638
|
+
semantic_action = f"{semantic_class}:{semantic_class}"
|
|
1614
1639
|
|
|
1615
1640
|
# Unenrolled first-run carve-out.
|
|
1616
1641
|
#
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "controlzero"
|
|
7
|
-
version = "1.13.
|
|
7
|
+
version = "1.13.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"}
|