langchain-agentx-python 0.1__py3-none-any.whl
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.
- langchain_agentx/__init__.py +46 -0
- langchain_agentx/command/__init__.py +28 -0
- langchain_agentx/command/builtin/__init__.py +25 -0
- langchain_agentx/command/builtin/clear.py +33 -0
- langchain_agentx/command/builtin/compact.py +33 -0
- langchain_agentx/command/builtin/memory.py +37 -0
- langchain_agentx/command/builtin/reload_plugins.py +42 -0
- langchain_agentx/command/context.py +30 -0
- langchain_agentx/command/dispatcher.py +183 -0
- langchain_agentx/command/registry.py +110 -0
- langchain_agentx/command/result.py +25 -0
- langchain_agentx/command/types.py +41 -0
- langchain_agentx/config/__init__.py +14 -0
- langchain_agentx/loop/__init__.py +47 -0
- langchain_agentx/loop/config/__init__.py +20 -0
- langchain_agentx/loop/config/agent_config.py +66 -0
- langchain_agentx/loop/config/agent_loop_config.py +72 -0
- langchain_agentx/loop/config/model_context_resolver.py +105 -0
- langchain_agentx/loop/config/runtime_settings.py +50 -0
- langchain_agentx/loop/config/token_estimator.py +133 -0
- langchain_agentx/loop/context/__init__.py +66 -0
- langchain_agentx/loop/context/blocking_guard.py +97 -0
- langchain_agentx/loop/context/compaction_service.py +60 -0
- langchain_agentx/loop/context/message_utils.py +56 -0
- langchain_agentx/loop/context/pipeline.py +127 -0
- langchain_agentx/loop/context/settings.py +103 -0
- langchain_agentx/loop/context/stages/__init__.py +29 -0
- langchain_agentx/loop/context/stages/autocompact.py +140 -0
- langchain_agentx/loop/context/stages/base.py +32 -0
- langchain_agentx/loop/context/stages/collapse.py +76 -0
- langchain_agentx/loop/context/stages/microcompact.py +76 -0
- langchain_agentx/loop/context/stages/noop.py +33 -0
- langchain_agentx/loop/context/stages/snip.py +71 -0
- langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
- langchain_agentx/loop/context/types.py +79 -0
- langchain_agentx/loop/exit/__init__.py +1 -0
- langchain_agentx/loop/exit/exit_logic.py +320 -0
- langchain_agentx/loop/exit/reason_codes.py +39 -0
- langchain_agentx/loop/graph/__init__.py +5 -0
- langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
- langchain_agentx/loop/graph/factory.py +1409 -0
- langchain_agentx/loop/graph/graph_edges.py +820 -0
- langchain_agentx/loop/hook/__init__.py +48 -0
- langchain_agentx/loop/hook/async_hook_runner.py +62 -0
- langchain_agentx/loop/hook/config.py +280 -0
- langchain_agentx/loop/hook/engine.py +321 -0
- langchain_agentx/loop/hook/executors/__init__.py +9 -0
- langchain_agentx/loop/hook/executors/agent.py +107 -0
- langchain_agentx/loop/hook/executors/command.py +230 -0
- langchain_agentx/loop/hook/executors/http.py +114 -0
- langchain_agentx/loop/hook/executors/prompt.py +92 -0
- langchain_agentx/loop/hook/graph_wiring.py +134 -0
- langchain_agentx/loop/hook/registry.py +262 -0
- langchain_agentx/loop/hook/trust.py +43 -0
- langchain_agentx/loop/hook/types.py +110 -0
- langchain_agentx/loop/injection/__init__.py +13 -0
- langchain_agentx/loop/injection/dedup.py +74 -0
- langchain_agentx/loop/loop_abort.py +36 -0
- langchain_agentx/loop/model/__init__.py +1 -0
- langchain_agentx/loop/model/model_node.py +648 -0
- langchain_agentx/loop/model/model_nodes.py +661 -0
- langchain_agentx/loop/model/orphan_tool_results.py +38 -0
- langchain_agentx/loop/model/retrier.py +307 -0
- langchain_agentx/loop/model/retry_bridge.py +58 -0
- langchain_agentx/loop/model/retry_events.py +35 -0
- langchain_agentx/loop/model/retry_policy.py +56 -0
- langchain_agentx/loop/model/schema_and_format.py +153 -0
- langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
- langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
- langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
- langchain_agentx/loop/prompt/__init__.py +95 -0
- langchain_agentx/loop/prompt/builder.py +61 -0
- langchain_agentx/loop/prompt/builtin.py +218 -0
- langchain_agentx/loop/prompt/compact.py +408 -0
- langchain_agentx/loop/prompt/sections.py +120 -0
- langchain_agentx/loop/runtime/__init__.py +19 -0
- langchain_agentx/loop/runtime/context.py +34 -0
- langchain_agentx/loop/runtime/context_factory.py +107 -0
- langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
- langchain_agentx/loop/subagent/__init__.py +53 -0
- langchain_agentx/loop/subagent/async_runner.py +215 -0
- langchain_agentx/loop/subagent/context.py +209 -0
- langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
- langchain_agentx/loop/subagent/graph.py +72 -0
- langchain_agentx/loop/subagent/orchestrator.py +391 -0
- langchain_agentx/loop/subagent/progress.py +30 -0
- langchain_agentx/loop/subagent/prompt.py +52 -0
- langchain_agentx/loop/subagent/runner.py +504 -0
- langchain_agentx/loop/subagent/transcript.py +172 -0
- langchain_agentx/memory/__init__.py +2 -0
- langchain_agentx/memory/instruction/__init__.py +12 -0
- langchain_agentx/memory/instruction/loader.py +325 -0
- langchain_agentx/memory/instruction/resolver.py +24 -0
- langchain_agentx/memory/instruction/runtime.py +83 -0
- langchain_agentx/memory/instruction/sections.py +83 -0
- langchain_agentx/memory/instruction/types.py +59 -0
- langchain_agentx/memory/memdir/__init__.py +77 -0
- langchain_agentx/memory/memdir/age.py +36 -0
- langchain_agentx/memory/memdir/agent_memory.py +380 -0
- langchain_agentx/memory/memdir/extractor.py +309 -0
- langchain_agentx/memory/memdir/loader.py +187 -0
- langchain_agentx/memory/memdir/paths.py +63 -0
- langchain_agentx/memory/memdir/recall.py +45 -0
- langchain_agentx/memory/memdir/runtime.py +43 -0
- langchain_agentx/memory/memdir/scan.py +135 -0
- langchain_agentx/memory/memdir/types.py +104 -0
- langchain_agentx/memory/session/__init__.py +76 -0
- langchain_agentx/memory/session/compact_bridge.py +208 -0
- langchain_agentx/memory/session/prompts.py +172 -0
- langchain_agentx/memory/session/session_memory.py +282 -0
- langchain_agentx/observability/__init__.py +67 -0
- langchain_agentx/observability/evaluation/__init__.py +17 -0
- langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
- langchain_agentx/observability/evaluation/checkers/base.py +34 -0
- langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
- langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
- langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
- langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
- langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
- langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
- langchain_agentx/observability/evaluation/service.py +102 -0
- langchain_agentx/observability/evaluation/state.py +32 -0
- langchain_agentx/observability/evaluation/store.py +258 -0
- langchain_agentx/observability/events/__init__.py +15 -0
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
- langchain_agentx/observability/logging/__init__.py +15 -0
- langchain_agentx/observability/logging/debug_burst.py +95 -0
- langchain_agentx/observability/logging/logging_config.py +178 -0
- langchain_agentx/observability/logging/logging_contract.py +65 -0
- langchain_agentx/observability/replay/__init__.py +35 -0
- langchain_agentx/observability/replay/cli.py +91 -0
- langchain_agentx/observability/replay/service.py +83 -0
- langchain_agentx/observability/replay/store.py +278 -0
- langchain_agentx/observability/replay/ui.py +47 -0
- langchain_agentx/observability/trace/__init__.py +25 -0
- langchain_agentx/observability/trace/collector.py +560 -0
- langchain_agentx/observability/trace/event_emitter.py +183 -0
- langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
- langchain_agentx/observability/trace/models.py +144 -0
- langchain_agentx/observability/trace/sqlite_store.py +873 -0
- langchain_agentx/observability/trace/trace_callback.py +295 -0
- langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
- langchain_agentx/plugin/__init__.py +26 -0
- langchain_agentx/plugin/builtin.py +53 -0
- langchain_agentx/plugin/config.py +113 -0
- langchain_agentx/plugin/loader.py +386 -0
- langchain_agentx/plugin/manifest.py +154 -0
- langchain_agentx/plugin/registries.py +211 -0
- langchain_agentx/plugin/types.py +142 -0
- langchain_agentx/provider/__init__.py +27 -0
- langchain_agentx/provider/anthropic.py +121 -0
- langchain_agentx/provider/compatible_chat_openai.py +86 -0
- langchain_agentx/provider/env.py +45 -0
- langchain_agentx/provider/model_profile.py +156 -0
- langchain_agentx/provider/openai.py +89 -0
- langchain_agentx/session/__init__.py +17 -0
- langchain_agentx/session/agent_session.py +320 -0
- langchain_agentx/session/conversation_factory.py +87 -0
- langchain_agentx/session/conversation_recovery.py +156 -0
- langchain_agentx/session/conversation_session.py +198 -0
- langchain_agentx/session/factory.py +143 -0
- langchain_agentx/session/protocol.py +25 -0
- langchain_agentx/task_runtime/__init__.py +113 -0
- langchain_agentx/task_runtime/core/__init__.py +51 -0
- langchain_agentx/task_runtime/core/ids.py +33 -0
- langchain_agentx/task_runtime/core/interfaces.py +115 -0
- langchain_agentx/task_runtime/core/notification_priority.py +19 -0
- langchain_agentx/task_runtime/core/types.py +136 -0
- langchain_agentx/task_runtime/integrations/__init__.py +33 -0
- langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
- langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
- langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
- langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
- langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
- langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
- langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
- langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
- langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
- langchain_agentx/task_runtime/output/__init__.py +5 -0
- langchain_agentx/task_runtime/output/sink.py +64 -0
- langchain_agentx/task_runtime/policy/__init__.py +11 -0
- langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
- langchain_agentx/task_runtime/queue/__init__.py +5 -0
- langchain_agentx/task_runtime/queue/in_memory.py +55 -0
- langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
- langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
- langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
- langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
- langchain_agentx/task_runtime/store/__init__.py +6 -0
- langchain_agentx/task_runtime/store/in_memory.py +81 -0
- langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
- langchain_agentx/task_runtime/tasks/__init__.py +76 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
- langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
- langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
- langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
- langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
- langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
- langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
- langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
- langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
- langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
- langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
- langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
- langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
- langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
- langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
- langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
- langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
- langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
- langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
- langchain_agentx/tool_runtime/__init__.py +90 -0
- langchain_agentx/tool_runtime/adapter.py +365 -0
- langchain_agentx/tool_runtime/base.py +319 -0
- langchain_agentx/tool_runtime/errors.py +190 -0
- langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
- langchain_agentx/tool_runtime/loader.py +195 -0
- langchain_agentx/tool_runtime/models.py +260 -0
- langchain_agentx/tool_runtime/permission_context.py +78 -0
- langchain_agentx/tool_runtime/pipeline.py +621 -0
- langchain_agentx/tool_runtime/policy.py +447 -0
- langchain_agentx/tool_runtime/registry.py +81 -0
- langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
- langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
- langchain_agentx/tool_runtime/resolvers/background.py +32 -0
- langchain_agentx/tool_runtime/resolvers/base.py +20 -0
- langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
- langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
- langchain_agentx/tool_runtime/session_store.py +132 -0
- langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
- langchain_agentx/tool_runtime/state_bridge.py +164 -0
- langchain_agentx/tools/__init__.py +26 -0
- langchain_agentx/tools/agent/__init__.py +9 -0
- langchain_agentx/tools/agent/backend.py +53 -0
- langchain_agentx/tools/agent/built_in/__init__.py +19 -0
- langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
- langchain_agentx/tools/agent/built_in/explore.py +80 -0
- langchain_agentx/tools/agent/built_in/general.py +57 -0
- langchain_agentx/tools/agent/built_in/plan.py +89 -0
- langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
- langchain_agentx/tools/agent/built_in/verification.py +120 -0
- langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
- langchain_agentx/tools/agent/cwd_resolution.py +119 -0
- langchain_agentx/tools/agent/limits.py +26 -0
- langchain_agentx/tools/agent/loader.py +270 -0
- langchain_agentx/tools/agent/models.py +85 -0
- langchain_agentx/tools/agent/prompt.py +120 -0
- langchain_agentx/tools/agent/registry/__init__.py +18 -0
- langchain_agentx/tools/agent/registry/config.py +29 -0
- langchain_agentx/tools/agent/registry/registry.py +47 -0
- langchain_agentx/tools/agent/scope.py +137 -0
- langchain_agentx/tools/agent/tool.py +256 -0
- langchain_agentx/tools/bash/__init__.py +9 -0
- langchain_agentx/tools/bash/ast_security.py +571 -0
- langchain_agentx/tools/bash/backend.py +1447 -0
- langchain_agentx/tools/bash/bash_hardening.py +734 -0
- langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
- langchain_agentx/tools/bash/cwd_reporter.py +95 -0
- langchain_agentx/tools/bash/limits.py +71 -0
- langchain_agentx/tools/bash/mode_validation.py +282 -0
- langchain_agentx/tools/bash/models.py +131 -0
- langchain_agentx/tools/bash/observability.py +148 -0
- langchain_agentx/tools/bash/output_utils.py +200 -0
- langchain_agentx/tools/bash/path_security.py +2429 -0
- langchain_agentx/tools/bash/prompt.py +68 -0
- langchain_agentx/tools/bash/read_only_validation.py +589 -0
- langchain_agentx/tools/bash/result_presenter.py +324 -0
- langchain_agentx/tools/bash/sandbox_decision.py +133 -0
- langchain_agentx/tools/bash/security.py +311 -0
- langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
- langchain_agentx/tools/bash/sed_validation.py +163 -0
- langchain_agentx/tools/bash/semantics.py +111 -0
- langchain_agentx/tools/bash/session_manager.py +205 -0
- langchain_agentx/tools/bash/session_runtime.py +290 -0
- langchain_agentx/tools/bash/shell_locator.py +191 -0
- langchain_agentx/tools/bash/task_runtime.py +91 -0
- langchain_agentx/tools/bash/tool.py +939 -0
- langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
- langchain_agentx/tools/glob/__init__.py +9 -0
- langchain_agentx/tools/glob/models.py +57 -0
- langchain_agentx/tools/glob/pagination.py +30 -0
- langchain_agentx/tools/glob/prompt.py +24 -0
- langchain_agentx/tools/glob/rg_list_backend.py +139 -0
- langchain_agentx/tools/glob/rg_pattern.py +44 -0
- langchain_agentx/tools/glob/tool.py +327 -0
- langchain_agentx/tools/grep/__init__.py +7 -0
- langchain_agentx/tools/grep/backend.py +375 -0
- langchain_agentx/tools/grep/models.py +127 -0
- langchain_agentx/tools/grep/prompt.py +30 -0
- langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
- langchain_agentx/tools/grep/tool.py +475 -0
- langchain_agentx/tools/read/__init__.py +9 -0
- langchain_agentx/tools/read/backend.py +415 -0
- langchain_agentx/tools/read/limits.py +67 -0
- langchain_agentx/tools/read/models.py +156 -0
- langchain_agentx/tools/read/prompt.py +73 -0
- langchain_agentx/tools/read/tool.py +494 -0
- langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
- langchain_agentx/tools/skill/__init__.py +4 -0
- langchain_agentx/tools/skill/argument_substitution.py +80 -0
- langchain_agentx/tools/skill/loader.py +196 -0
- langchain_agentx/tools/skill/models.py +88 -0
- langchain_agentx/tools/skill/policy.py +80 -0
- langchain_agentx/tools/skill/prompt.py +35 -0
- langchain_agentx/tools/skill/tool.py +222 -0
- langchain_agentx/utils/__init__.py +0 -0
- langchain_agentx/utils/cwd.py +124 -0
- langchain_agentx/utils/host_platform.py +112 -0
- langchain_agentx/utils/path_hierarchy.py +48 -0
- langchain_agentx/utils/path_user_input.py +66 -0
- langchain_agentx/utils/rg_executable.py +18 -0
- langchain_agentx/utils/subprocess_text.py +101 -0
- langchain_agentx/utils/temp_paths.py +77 -0
- langchain_agentx/utils/unc_path.py +25 -0
- langchain_agentx/utils/win_reserved_paths.py +51 -0
- langchain_agentx/workflow/__init__.py +7 -0
- langchain_agentx/workflow/base.py +97 -0
- langchain_agentx/workflow/batch.py +55 -0
- langchain_agentx/workflow/dag.py +54 -0
- langchain_agentx/workspace/__init__.py +13 -0
- langchain_agentx/workspace/config.py +140 -0
- langchain_agentx/workspace/path_key_normalizer.py +30 -0
- langchain_agentx/workspace/resolver.py +74 -0
- langchain_agentx/workspace/validators.py +41 -0
- langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
- langchain_agentx_python-0.1.dist-info/METADATA +513 -0
- langchain_agentx_python-0.1.dist-info/RECORD +354 -0
- langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
- langchain_agentx_python-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/bash_hardening.py — BashRuntimeTool 模块3:安全攻防加固
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
在 AST 结构审批、路径授权、PolicyEngine 之外,补上 command-string 层面的
|
|
6
|
+
防绕过检查,避免 env / wrapper / quoting / git-internal 等模式绕过主权限链。
|
|
7
|
+
|
|
8
|
+
协作者(对照 CC 更大攻防规则库方向扩展):
|
|
9
|
+
1. BashCommandCanonicalizer — env/wrapper 固定点归一化
|
|
10
|
+
2. BashEnvSecurityChecker — 高危环境变量(含 JVM/SSH/TLS 劫持面)
|
|
11
|
+
3. BashUnicodeRiskChecker — 零宽 / RTL 等不可见字符混淆
|
|
12
|
+
4. BashExecutionContextChecker — sudo/chroot/nsenter 等执行上下文切换
|
|
13
|
+
5. BashCrossPlatformBinaryChecker — wsl/cmd/powershell 等跨边界执行
|
|
14
|
+
6. BashRemoteShellPipelineChecker — curl|bash、base64|sh 等「下载/解码→解释器」链
|
|
15
|
+
7. BashShellRiskChecker — quoting / heredoc / UNC / WebDAV 等
|
|
16
|
+
8. BashGitCliInjectionChecker — git -c hooksPath / sshCommand 等 CLI 注入
|
|
17
|
+
9. BashGitSecurityChecker — .git 路径、bare、compound cd 等
|
|
18
|
+
|
|
19
|
+
观测:
|
|
20
|
+
BashSecurityHardener.audit_explain() 返回结构化快照,便于与 CC 式调试输出对齐。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import shlex
|
|
28
|
+
import sys
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from langchain_agentx.tool_runtime.models import AuthorizationDecision
|
|
33
|
+
from langchain_agentx.utils.host_platform import get_host_platform
|
|
34
|
+
|
|
35
|
+
from .ast_security import BashAstAnalysis, BashWrapperStripper
|
|
36
|
+
from .path_security import BashPathExtractorRegistry
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class BashHardeningCheckResult:
|
|
41
|
+
behavior: str
|
|
42
|
+
message: str | None = None
|
|
43
|
+
policy_id: str | None = None
|
|
44
|
+
ask_prompt: str | None = None
|
|
45
|
+
rule_code: str | None = None
|
|
46
|
+
category: str | None = None
|
|
47
|
+
extra: dict[str, Any] | None = None
|
|
48
|
+
|
|
49
|
+
def to_decision(self) -> AuthorizationDecision:
|
|
50
|
+
meta: dict[str, Any] | None = None
|
|
51
|
+
if self.rule_code or self.category or self.extra:
|
|
52
|
+
bh: dict[str, Any] = {}
|
|
53
|
+
if self.rule_code:
|
|
54
|
+
bh["rule_code"] = self.rule_code
|
|
55
|
+
if self.category:
|
|
56
|
+
bh["category"] = self.category
|
|
57
|
+
if self.extra:
|
|
58
|
+
bh.update(self.extra)
|
|
59
|
+
meta = {"bash_hardening": bh}
|
|
60
|
+
return AuthorizationDecision(
|
|
61
|
+
behavior=self.behavior,
|
|
62
|
+
message=self.message,
|
|
63
|
+
policy_id=self.policy_id,
|
|
64
|
+
ask_prompt=self.ask_prompt,
|
|
65
|
+
metadata=meta,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BashCommandCanonicalizer:
|
|
70
|
+
"""对命令做 env/wrapper 固定点归一化,供策略匹配使用。"""
|
|
71
|
+
|
|
72
|
+
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
|
|
73
|
+
|
|
74
|
+
def __init__(self, wrapper_stripper: BashWrapperStripper | None = None) -> None:
|
|
75
|
+
self._wrapper_stripper = wrapper_stripper or BashWrapperStripper()
|
|
76
|
+
|
|
77
|
+
def canonicalize(self, command: str) -> str:
|
|
78
|
+
tokens = self._split(command)
|
|
79
|
+
if not tokens:
|
|
80
|
+
return command.strip()
|
|
81
|
+
|
|
82
|
+
previous: list[str] | None = None
|
|
83
|
+
current = list(tokens)
|
|
84
|
+
while current != previous and current:
|
|
85
|
+
previous = list(current)
|
|
86
|
+
current = self._strip_assignments(current)
|
|
87
|
+
current = self._wrapper_stripper.strip(current)
|
|
88
|
+
|
|
89
|
+
return " ".join(current) if current else command.strip()
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _split(command: str) -> list[str]:
|
|
93
|
+
try:
|
|
94
|
+
return shlex.split(command)
|
|
95
|
+
except ValueError:
|
|
96
|
+
return command.split()
|
|
97
|
+
|
|
98
|
+
def _strip_assignments(self, tokens: list[str]) -> list[str]:
|
|
99
|
+
index = 0
|
|
100
|
+
while index < len(tokens) and self._ENV_ASSIGNMENT_RE.match(tokens[index]):
|
|
101
|
+
index += 1
|
|
102
|
+
return tokens[index:]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BashEnvSecurityChecker:
|
|
106
|
+
"""检测前导环境变量中的高风险执行劫持面。"""
|
|
107
|
+
|
|
108
|
+
SAFE_ENV_VARS = frozenset({
|
|
109
|
+
"TERM", "COLORTERM", "NO_COLOR", "FORCE_COLOR", "CLICOLOR",
|
|
110
|
+
"LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TZ",
|
|
111
|
+
"COLUMNS", "LINES", "CI",
|
|
112
|
+
})
|
|
113
|
+
BINARY_HIJACK_VARS = frozenset({
|
|
114
|
+
"PATH", "LD_PRELOAD", "LD_LIBRARY_PATH", "DYLD_INSERT_LIBRARIES",
|
|
115
|
+
"DYLD_LIBRARY_PATH", "PYTHONPATH", "PYTHONHOME", "PYTHONSTARTUP",
|
|
116
|
+
"RUBYLIB", "RUBYOPT", "PERL5LIB", "PERL5OPT", "NODE_PATH",
|
|
117
|
+
"NODE_OPTIONS", "GIT_EXEC_PATH", "GIT_CONFIG", "GIT_CONFIG_GLOBAL",
|
|
118
|
+
"GIT_CONFIG_SYSTEM", "BASH_ENV", "ENV", "SHELLOPTS", "IFS",
|
|
119
|
+
"CDPATH", "PROMPT_COMMAND",
|
|
120
|
+
"JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS", "JDK_JAVA_OPTIONS",
|
|
121
|
+
"GIT_SSH_COMMAND", "GIT_SSH",
|
|
122
|
+
"LD_AUDIT", "OPENSSL_CONF", "SSLKEYLOGFILE",
|
|
123
|
+
})
|
|
124
|
+
_ENV_ASSIGNMENT_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
|
|
125
|
+
|
|
126
|
+
def collect_modified_vars(self, command: str) -> set[str]:
|
|
127
|
+
tokens = self._split(command)
|
|
128
|
+
if not tokens:
|
|
129
|
+
return set()
|
|
130
|
+
|
|
131
|
+
result: set[str] = set()
|
|
132
|
+
index = 0
|
|
133
|
+
while index < len(tokens):
|
|
134
|
+
match = self._ENV_ASSIGNMENT_RE.match(tokens[index])
|
|
135
|
+
if match:
|
|
136
|
+
result.add(match.group(1))
|
|
137
|
+
index += 1
|
|
138
|
+
continue
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
if index < len(tokens) and tokens[index] == "env":
|
|
142
|
+
index += 1
|
|
143
|
+
while index < len(tokens):
|
|
144
|
+
token = tokens[index]
|
|
145
|
+
if token == "--":
|
|
146
|
+
index += 1
|
|
147
|
+
break
|
|
148
|
+
if token == "-u" and index + 1 < len(tokens):
|
|
149
|
+
result.add(tokens[index + 1])
|
|
150
|
+
index += 2
|
|
151
|
+
continue
|
|
152
|
+
if token in {"-i", "-0", "-v"}:
|
|
153
|
+
index += 1
|
|
154
|
+
continue
|
|
155
|
+
match = self._ENV_ASSIGNMENT_RE.match(token)
|
|
156
|
+
if match:
|
|
157
|
+
result.add(match.group(1))
|
|
158
|
+
index += 1
|
|
159
|
+
continue
|
|
160
|
+
break
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
def evaluate(self, command: str) -> BashHardeningCheckResult | None:
|
|
164
|
+
modified_vars = self.collect_modified_vars(command)
|
|
165
|
+
dangerous = sorted(var for var in modified_vars if var in self.BINARY_HIJACK_VARS)
|
|
166
|
+
if not dangerous:
|
|
167
|
+
return None
|
|
168
|
+
joined = ", ".join(dangerous)
|
|
169
|
+
return BashHardeningCheckResult(
|
|
170
|
+
behavior="ask",
|
|
171
|
+
message=(
|
|
172
|
+
"This command modifies high-risk environment variables that can change "
|
|
173
|
+
f"which binary or config is used: {joined}."
|
|
174
|
+
),
|
|
175
|
+
policy_id="bash_env_hardening",
|
|
176
|
+
ask_prompt=(
|
|
177
|
+
"High-risk environment variable override detected.\n"
|
|
178
|
+
f"Variables: {joined}\n"
|
|
179
|
+
"These variables can alter binary resolution or runtime configuration, "
|
|
180
|
+
"so explicit approval is required."
|
|
181
|
+
),
|
|
182
|
+
rule_code="env.binary_hijack",
|
|
183
|
+
category="env",
|
|
184
|
+
extra={"variables": dangerous},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _split(command: str) -> list[str]:
|
|
189
|
+
try:
|
|
190
|
+
return shlex.split(command)
|
|
191
|
+
except ValueError:
|
|
192
|
+
return command.split()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class BashUnicodeRiskChecker:
|
|
196
|
+
"""零宽字符 / RTL 控制符等不可见混淆(跨平台 Unicode 特例)。"""
|
|
197
|
+
|
|
198
|
+
_INVISIBLE_OR_RTL = re.compile(r"[\u200b\u200c\u200d\ufeff\u202a-\u202e]")
|
|
199
|
+
|
|
200
|
+
def evaluate(self, command: str) -> BashHardeningCheckResult | None:
|
|
201
|
+
if not self._INVISIBLE_OR_RTL.search(command):
|
|
202
|
+
return None
|
|
203
|
+
return BashHardeningCheckResult(
|
|
204
|
+
behavior="ask",
|
|
205
|
+
message="Command contains invisible Unicode characters that can obfuscate intent.",
|
|
206
|
+
policy_id="bash_unicode_obfuscation",
|
|
207
|
+
ask_prompt=(
|
|
208
|
+
"Invisible Unicode characters (zero-width / bidi controls) detected.\n"
|
|
209
|
+
"These can hide flags or path segments; explicit approval is required."
|
|
210
|
+
),
|
|
211
|
+
rule_code="shell.unicode_invisible",
|
|
212
|
+
category="shell",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class BashExecutionContextChecker:
|
|
217
|
+
"""sudo / chroot / namespace 等改变真实执行身份或根目录。"""
|
|
218
|
+
|
|
219
|
+
_CTX = re.compile(
|
|
220
|
+
r"(?:^|[;&|]|\s|&&|\|\|)"
|
|
221
|
+
r"(?:sudo|doas|su\b|runuser|chroot|nsenter|strace|ltrace|"
|
|
222
|
+
r"pkexec|firejail|bwrap|bubblewrap|unshare|systemd-run|machinectl)\s",
|
|
223
|
+
re.IGNORECASE,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def evaluate(self, command: str) -> BashHardeningCheckResult | None:
|
|
227
|
+
if not self._CTX.search(command):
|
|
228
|
+
return None
|
|
229
|
+
return BashHardeningCheckResult(
|
|
230
|
+
behavior="ask",
|
|
231
|
+
message="Command uses a privilege or namespace wrapper that can change execution semantics.",
|
|
232
|
+
policy_id="bash_execution_context",
|
|
233
|
+
ask_prompt=(
|
|
234
|
+
"Privilege / chroot / namespace / tracer wrapper detected.\n"
|
|
235
|
+
"These can bypass normal permission and sandbox assumptions; approve explicitly."
|
|
236
|
+
),
|
|
237
|
+
rule_code="exec.context.privilege_or_namespace",
|
|
238
|
+
category="execution_context",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class BashCrossPlatformBinaryChecker:
|
|
243
|
+
"""跨 OS 边界执行(Windows / WSL / PowerShell),与 POSIX 沙盒假设不一致。"""
|
|
244
|
+
|
|
245
|
+
_CROSS = re.compile(
|
|
246
|
+
r"(?:^|\s)(?:wsl(?:\.exe)?|cmd(?:\.exe)?|powershell(?:\.exe)?|pwsh(?:\.exe)?)\s",
|
|
247
|
+
re.IGNORECASE,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def evaluate(self, command: str) -> BashHardeningCheckResult | None:
|
|
251
|
+
if not self._CROSS.search(command):
|
|
252
|
+
return None
|
|
253
|
+
return BashHardeningCheckResult(
|
|
254
|
+
behavior="ask",
|
|
255
|
+
message="Command invokes a cross-platform shell binary (WSL/cmd/PowerShell), which requires review.",
|
|
256
|
+
policy_id="bash_cross_platform_shell",
|
|
257
|
+
ask_prompt=(
|
|
258
|
+
"Cross-platform shell invocation detected (wsl/cmd/powershell/pwsh).\n"
|
|
259
|
+
"Paths and permission models differ from POSIX bash; explicit approval is required."
|
|
260
|
+
),
|
|
261
|
+
rule_code="platform.cross_shell",
|
|
262
|
+
category="platform",
|
|
263
|
+
extra={
|
|
264
|
+
"sys_platform_raw": sys.platform,
|
|
265
|
+
"host_platform": get_host_platform().value,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class BashRemoteShellPipelineChecker:
|
|
271
|
+
"""远程拉取 / 解码后直接进入解释器的高风险链。"""
|
|
272
|
+
|
|
273
|
+
_FETCH_PIPE_SHELL = re.compile(
|
|
274
|
+
r"(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:bash|sh|zsh|dash)\b",
|
|
275
|
+
re.IGNORECASE,
|
|
276
|
+
)
|
|
277
|
+
_B64_PIPE_SHELL = re.compile(
|
|
278
|
+
r"base64\b[^\n|]*(?:-d|--decode)[^\n|]*\|\s*(?:bash|sh)\b",
|
|
279
|
+
re.IGNORECASE,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def evaluate(self, command: str) -> BashHardeningCheckResult | None:
|
|
283
|
+
if self._FETCH_PIPE_SHELL.search(command):
|
|
284
|
+
return BashHardeningCheckResult(
|
|
285
|
+
behavior="ask",
|
|
286
|
+
message="Pipeline fetches remote content directly into a shell interpreter.",
|
|
287
|
+
policy_id="bash_remote_pipe_shell",
|
|
288
|
+
ask_prompt=(
|
|
289
|
+
"curl/wget/fetch piped into bash/sh detected.\n"
|
|
290
|
+
"This is a common remote-code execution pattern; explicit approval is required."
|
|
291
|
+
),
|
|
292
|
+
rule_code="network.fetch_pipe_interpreter",
|
|
293
|
+
category="network",
|
|
294
|
+
)
|
|
295
|
+
if self._B64_PIPE_SHELL.search(command):
|
|
296
|
+
return BashHardeningCheckResult(
|
|
297
|
+
behavior="ask",
|
|
298
|
+
message="Pipeline decodes base64 directly into a shell interpreter.",
|
|
299
|
+
policy_id="bash_remote_pipe_shell",
|
|
300
|
+
ask_prompt=(
|
|
301
|
+
"base64 decode piped into bash/sh detected.\n"
|
|
302
|
+
"This can hide payload from static review; explicit approval is required."
|
|
303
|
+
),
|
|
304
|
+
rule_code="network.base64_pipe_interpreter",
|
|
305
|
+
category="network",
|
|
306
|
+
)
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class BashGitCliInjectionChecker:
|
|
311
|
+
"""git -c 覆盖 hooks / SSH 等敏感运行时配置。"""
|
|
312
|
+
|
|
313
|
+
_DANGEROUS_GIT_C = re.compile(
|
|
314
|
+
r"\bgit\s+(?:-\S+\s+)*-c\s+\S*(hooksPath|sshCommand|uploadpack|receivepack)\S*=",
|
|
315
|
+
re.IGNORECASE,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def evaluate(self, command: str) -> BashHardeningCheckResult | None:
|
|
319
|
+
if not re.search(r"\bgit\b", command):
|
|
320
|
+
return None
|
|
321
|
+
if not self._DANGEROUS_GIT_C.search(command):
|
|
322
|
+
return None
|
|
323
|
+
return BashHardeningCheckResult(
|
|
324
|
+
behavior="ask",
|
|
325
|
+
message="Git command overrides sensitive runtime settings via `-c` (hooks/ssh/pack).",
|
|
326
|
+
policy_id="bash_git_cli_injection",
|
|
327
|
+
ask_prompt=(
|
|
328
|
+
"git -c override for hooksPath/sshCommand/uploadpack/receivepack detected.\n"
|
|
329
|
+
"These can redirect hooks or transport; explicit approval is required."
|
|
330
|
+
),
|
|
331
|
+
rule_code="git.cli_runtime_override",
|
|
332
|
+
category="git",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class BashShellRiskChecker:
|
|
337
|
+
"""检测 quoting / heredoc / UNC / WebDAV / eval / process substitution 等模式。"""
|
|
338
|
+
|
|
339
|
+
_ANSI_C_QUOTING_RE = re.compile(r"\$'[^']*'")
|
|
340
|
+
_LOCALE_QUOTING_RE = re.compile(r'\$"[^"]*"')
|
|
341
|
+
_EMPTY_QUOTES_BEFORE_DASH_RE = re.compile(r"(?:^|\s)(?:''|\"\")+\s*-")
|
|
342
|
+
_ADJACENT_QUOTED_DASH_RE = re.compile(r'(?:""|\'\')+[\'"]-')
|
|
343
|
+
_CONSECUTIVE_QUOTES_RE = re.compile(r"(?:^|\s)['\"]{3,}")
|
|
344
|
+
_HEREDOC_IN_SUBSTITUTION_RE = re.compile(
|
|
345
|
+
r"(\$\([^)]*<<-?\s*['\"]?[A-Za-z0-9_]+|`[^`]*<<-?\s*['\"]?[A-Za-z0-9_]+)"
|
|
346
|
+
)
|
|
347
|
+
_UNC_BACKSLASH_RE = re.compile(r"\\\\[^\\/\s]+\\[^\\/\s]+")
|
|
348
|
+
_UNC_DEVICE_RE = re.compile(r"//\?/UNC/", re.IGNORECASE)
|
|
349
|
+
_WEBDAV_RE = re.compile(r"\b(?:dav|davs|webdav|smb)://", re.IGNORECASE)
|
|
350
|
+
_EVAL_OR_SOURCE_RE = re.compile(
|
|
351
|
+
r"(?:^|\s)(?:eval|source)\s",
|
|
352
|
+
)
|
|
353
|
+
_PROCESS_SUBST_RE = re.compile(r"[<>]\(")
|
|
354
|
+
|
|
355
|
+
def pattern_rules(
|
|
356
|
+
self,
|
|
357
|
+
) -> list[tuple[str, re.Pattern[str], str, str]]:
|
|
358
|
+
return [
|
|
359
|
+
(
|
|
360
|
+
"ansi_c_quote",
|
|
361
|
+
self._ANSI_C_QUOTING_RE,
|
|
362
|
+
"Command contains ANSI-C quoting which can hide characters.",
|
|
363
|
+
"bash_shell_obfuscation",
|
|
364
|
+
),
|
|
365
|
+
(
|
|
366
|
+
"locale_quote",
|
|
367
|
+
self._LOCALE_QUOTING_RE,
|
|
368
|
+
"Command contains locale quoting which can hide characters.",
|
|
369
|
+
"bash_shell_obfuscation",
|
|
370
|
+
),
|
|
371
|
+
(
|
|
372
|
+
"empty_quotes_dash",
|
|
373
|
+
self._EMPTY_QUOTES_BEFORE_DASH_RE,
|
|
374
|
+
"Command contains empty quotes before a dash, which can obfuscate flags.",
|
|
375
|
+
"bash_shell_obfuscation",
|
|
376
|
+
),
|
|
377
|
+
(
|
|
378
|
+
"adjacent_quotes_dash",
|
|
379
|
+
self._ADJACENT_QUOTED_DASH_RE,
|
|
380
|
+
"Command contains empty quote pairs adjacent to a quoted dash.",
|
|
381
|
+
"bash_shell_obfuscation",
|
|
382
|
+
),
|
|
383
|
+
(
|
|
384
|
+
"consecutive_quotes",
|
|
385
|
+
self._CONSECUTIVE_QUOTES_RE,
|
|
386
|
+
"Command contains suspicious consecutive quote characters at word start.",
|
|
387
|
+
"bash_shell_obfuscation",
|
|
388
|
+
),
|
|
389
|
+
(
|
|
390
|
+
"heredoc_in_subst",
|
|
391
|
+
self._HEREDOC_IN_SUBSTITUTION_RE,
|
|
392
|
+
"Command contains heredoc content inside command substitution, which requires review.",
|
|
393
|
+
"bash_heredoc_substitution",
|
|
394
|
+
),
|
|
395
|
+
(
|
|
396
|
+
"unc_backslash",
|
|
397
|
+
self._UNC_BACKSLASH_RE,
|
|
398
|
+
"Command references a UNC path, which may trigger remote filesystem access.",
|
|
399
|
+
"bash_remote_path_risk",
|
|
400
|
+
),
|
|
401
|
+
(
|
|
402
|
+
"unc_device",
|
|
403
|
+
self._UNC_DEVICE_RE,
|
|
404
|
+
"Command references a Windows UNC device path, which requires approval.",
|
|
405
|
+
"bash_remote_path_risk",
|
|
406
|
+
),
|
|
407
|
+
(
|
|
408
|
+
"webdav",
|
|
409
|
+
self._WEBDAV_RE,
|
|
410
|
+
"Command references a WebDAV/SMB-style remote path, which requires approval.",
|
|
411
|
+
"bash_remote_path_risk",
|
|
412
|
+
),
|
|
413
|
+
(
|
|
414
|
+
"eval_source",
|
|
415
|
+
self._EVAL_OR_SOURCE_RE,
|
|
416
|
+
"Command uses eval/source/. which can execute arbitrary strings.",
|
|
417
|
+
"bash_dynamic_eval",
|
|
418
|
+
),
|
|
419
|
+
(
|
|
420
|
+
"process_substitution",
|
|
421
|
+
self._PROCESS_SUBST_RE,
|
|
422
|
+
"Command uses process substitution (<(...) / >(...)), which can hide side effects.",
|
|
423
|
+
"bash_process_substitution",
|
|
424
|
+
),
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
def evaluate(self, command: str) -> BashHardeningCheckResult | None:
|
|
428
|
+
for rule_id, pattern, message, policy_id in self.pattern_rules():
|
|
429
|
+
if pattern.search(command):
|
|
430
|
+
return BashHardeningCheckResult(
|
|
431
|
+
behavior="ask",
|
|
432
|
+
message=message,
|
|
433
|
+
policy_id=policy_id,
|
|
434
|
+
ask_prompt=message,
|
|
435
|
+
rule_code=f"shell.{rule_id}",
|
|
436
|
+
category="shell",
|
|
437
|
+
)
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
def probe(self, command: str) -> list[dict[str, Any]]:
|
|
441
|
+
out: list[dict[str, Any]] = []
|
|
442
|
+
for rule_id, pattern, message, policy_id in self.pattern_rules():
|
|
443
|
+
out.append(
|
|
444
|
+
{
|
|
445
|
+
"rule_id": rule_id,
|
|
446
|
+
"policy_id": policy_id,
|
|
447
|
+
"matched": bool(pattern.search(command)),
|
|
448
|
+
"message": message,
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
return out
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class BashGitSecurityChecker:
|
|
455
|
+
"""检测 git-internal 路径、裸仓库和仓库边界混淆。"""
|
|
456
|
+
|
|
457
|
+
def __init__(self, extractor_registry: BashPathExtractorRegistry | None = None) -> None:
|
|
458
|
+
self._extractor_registry = extractor_registry or BashPathExtractorRegistry()
|
|
459
|
+
|
|
460
|
+
def evaluate_command(
|
|
461
|
+
self,
|
|
462
|
+
*,
|
|
463
|
+
command: str,
|
|
464
|
+
analysis: BashAstAnalysis,
|
|
465
|
+
cwd: str,
|
|
466
|
+
) -> BashHardeningCheckResult | None:
|
|
467
|
+
normalized = set(analysis.normalized_base_commands)
|
|
468
|
+
if "git" in normalized:
|
|
469
|
+
risky_subcommand = self._git_risky_subcommand(command)
|
|
470
|
+
if risky_subcommand is not None:
|
|
471
|
+
return BashHardeningCheckResult(
|
|
472
|
+
behavior="ask",
|
|
473
|
+
message=(
|
|
474
|
+
"Git command may mutate repository or remote state "
|
|
475
|
+
f"(`git {risky_subcommand}`), explicit approval is required."
|
|
476
|
+
),
|
|
477
|
+
policy_id="bash_git_high_risk_subcommand",
|
|
478
|
+
ask_prompt=(
|
|
479
|
+
f"High-risk git subcommand detected: git {risky_subcommand}\n"
|
|
480
|
+
"This operation can modify refs/history/remotes, approve explicitly."
|
|
481
|
+
),
|
|
482
|
+
rule_code="git.high_risk_subcommand",
|
|
483
|
+
category="git",
|
|
484
|
+
extra={"subcommand": risky_subcommand},
|
|
485
|
+
)
|
|
486
|
+
if self._is_git_internal_dir(cwd):
|
|
487
|
+
return BashHardeningCheckResult(
|
|
488
|
+
behavior="ask",
|
|
489
|
+
message="Running git from inside a .git internal directory requires approval.",
|
|
490
|
+
policy_id="bash_git_workspace_boundary",
|
|
491
|
+
ask_prompt=(
|
|
492
|
+
"Git command is running from inside a .git internal directory.\n"
|
|
493
|
+
"This can change repository internals in non-obvious ways, so approval is required."
|
|
494
|
+
),
|
|
495
|
+
rule_code="git.cwd_inside_dot_git",
|
|
496
|
+
category="git",
|
|
497
|
+
)
|
|
498
|
+
if self._looks_like_bare_repo(cwd):
|
|
499
|
+
return BashHardeningCheckResult(
|
|
500
|
+
behavior="ask",
|
|
501
|
+
message="Running git inside a bare repository requires approval.",
|
|
502
|
+
policy_id="bash_git_workspace_boundary",
|
|
503
|
+
ask_prompt=(
|
|
504
|
+
"Git command appears to target a bare repository.\n"
|
|
505
|
+
"Bare repos expose internal refs/config directly, so approval is required."
|
|
506
|
+
),
|
|
507
|
+
rule_code="git.bare_repo_cwd",
|
|
508
|
+
category="git",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
if "git" in normalized and self._has_cd_into_git_internal(analysis, cwd):
|
|
512
|
+
return BashHardeningCheckResult(
|
|
513
|
+
behavior="ask",
|
|
514
|
+
message="Compound command changes into a git-internal directory before running git.",
|
|
515
|
+
policy_id="bash_git_workspace_boundary",
|
|
516
|
+
ask_prompt=(
|
|
517
|
+
"This command changes into a git-internal directory and then runs git.\n"
|
|
518
|
+
"Explicit approval is required to avoid repository-boundary confusion."
|
|
519
|
+
),
|
|
520
|
+
rule_code="git.cd_into_internal_then_git",
|
|
521
|
+
category="git",
|
|
522
|
+
)
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
def _git_risky_subcommand(self, command: str) -> str | None:
|
|
526
|
+
tokens = self._split(command)
|
|
527
|
+
if not tokens:
|
|
528
|
+
return None
|
|
529
|
+
if os.path.basename(tokens[0]) != "git":
|
|
530
|
+
return None
|
|
531
|
+
subcommand = ""
|
|
532
|
+
for token in tokens[1:]:
|
|
533
|
+
if token.startswith("-"):
|
|
534
|
+
continue
|
|
535
|
+
subcommand = token
|
|
536
|
+
break
|
|
537
|
+
if not subcommand:
|
|
538
|
+
return None
|
|
539
|
+
risky = {
|
|
540
|
+
"push", "fetch", "pull", "clone", "remote", "submodule", "config",
|
|
541
|
+
"update-ref", "rebase", "reset", "checkout", "switch", "worktree",
|
|
542
|
+
"commit", "cherry-pick", "revert", "merge", "apply", "am",
|
|
543
|
+
"notes", "tag", "branch", "stash",
|
|
544
|
+
}
|
|
545
|
+
if subcommand in risky:
|
|
546
|
+
if subcommand == "remote" and any(t in {"-v", "show"} for t in tokens[2:]):
|
|
547
|
+
return None
|
|
548
|
+
return subcommand
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
def evaluate_segment(
|
|
552
|
+
self,
|
|
553
|
+
*,
|
|
554
|
+
command: str,
|
|
555
|
+
analysis: BashAstAnalysis,
|
|
556
|
+
cwd: str,
|
|
557
|
+
) -> BashHardeningCheckResult | None:
|
|
558
|
+
path_info = self._extractor_registry.extract(command, analysis)
|
|
559
|
+
if path_info is None:
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
for target in path_info.path_targets:
|
|
563
|
+
resolved = self._resolve_path(target.raw_path, cwd)
|
|
564
|
+
if self._is_git_internal_path(resolved):
|
|
565
|
+
if target.operation_type in {"write", "create"}:
|
|
566
|
+
return BashHardeningCheckResult(
|
|
567
|
+
behavior="deny",
|
|
568
|
+
message="Direct writes to git-internal paths are not allowed.",
|
|
569
|
+
policy_id="bash_git_internal_path",
|
|
570
|
+
rule_code="git.internal_path_write",
|
|
571
|
+
category="git",
|
|
572
|
+
extra={"path": resolved},
|
|
573
|
+
)
|
|
574
|
+
return BashHardeningCheckResult(
|
|
575
|
+
behavior="ask",
|
|
576
|
+
message="Direct access to git-internal paths requires approval.",
|
|
577
|
+
policy_id="bash_git_internal_path",
|
|
578
|
+
ask_prompt=(
|
|
579
|
+
f"Direct access to git-internal path detected: {resolved}\n"
|
|
580
|
+
"This requires approval because repository internals may contain hooks or config."
|
|
581
|
+
),
|
|
582
|
+
rule_code="git.internal_path_access",
|
|
583
|
+
category="git",
|
|
584
|
+
extra={"path": resolved},
|
|
585
|
+
)
|
|
586
|
+
return None
|
|
587
|
+
|
|
588
|
+
def _has_cd_into_git_internal(self, analysis: BashAstAnalysis, cwd: str) -> bool:
|
|
589
|
+
if "git" not in analysis.normalized_base_commands:
|
|
590
|
+
return False
|
|
591
|
+
for simple in analysis.simple_commands:
|
|
592
|
+
if simple.normalized_base_command != "cd":
|
|
593
|
+
continue
|
|
594
|
+
tokens = self._split(simple.text)
|
|
595
|
+
if len(tokens) < 2:
|
|
596
|
+
continue
|
|
597
|
+
resolved = self._resolve_path(tokens[1], cwd)
|
|
598
|
+
if self._is_git_internal_dir(resolved) or self._looks_like_bare_repo(resolved):
|
|
599
|
+
return True
|
|
600
|
+
return False
|
|
601
|
+
|
|
602
|
+
@staticmethod
|
|
603
|
+
def _split(command: str) -> list[str]:
|
|
604
|
+
try:
|
|
605
|
+
return shlex.split(command)
|
|
606
|
+
except ValueError:
|
|
607
|
+
return command.split()
|
|
608
|
+
|
|
609
|
+
@staticmethod
|
|
610
|
+
def _resolve_path(raw_path: str, cwd: str) -> str:
|
|
611
|
+
expanded = os.path.expanduser(raw_path.strip("\"'"))
|
|
612
|
+
absolute = expanded if os.path.isabs(expanded) else os.path.join(cwd, expanded)
|
|
613
|
+
return os.path.realpath(absolute)
|
|
614
|
+
|
|
615
|
+
@staticmethod
|
|
616
|
+
def _is_git_internal_path(path: str) -> bool:
|
|
617
|
+
normalized = os.path.normpath(path)
|
|
618
|
+
parts = normalized.split(os.sep)
|
|
619
|
+
if ".git" in parts:
|
|
620
|
+
return True
|
|
621
|
+
basename = os.path.basename(normalized)
|
|
622
|
+
return basename == ".gitmodules"
|
|
623
|
+
|
|
624
|
+
@staticmethod
|
|
625
|
+
def _is_git_internal_dir(path: str) -> bool:
|
|
626
|
+
normalized = os.path.normpath(path)
|
|
627
|
+
parts = normalized.split(os.sep)
|
|
628
|
+
return ".git" in parts
|
|
629
|
+
|
|
630
|
+
@staticmethod
|
|
631
|
+
def _looks_like_bare_repo(path: str) -> bool:
|
|
632
|
+
return (
|
|
633
|
+
os.path.isdir(path)
|
|
634
|
+
and os.path.isfile(os.path.join(path, "HEAD"))
|
|
635
|
+
and os.path.isfile(os.path.join(path, "config"))
|
|
636
|
+
and os.path.isdir(os.path.join(path, "objects"))
|
|
637
|
+
and os.path.isdir(os.path.join(path, "refs"))
|
|
638
|
+
and not os.path.isdir(os.path.join(path, ".git"))
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class BashSecurityHardener:
|
|
643
|
+
"""模块 3 总协作者:在 AST/path/policy 链路之外追加防绕过检查。"""
|
|
644
|
+
|
|
645
|
+
def __init__(
|
|
646
|
+
self,
|
|
647
|
+
*,
|
|
648
|
+
canonicalizer: BashCommandCanonicalizer | None = None,
|
|
649
|
+
env_checker: BashEnvSecurityChecker | None = None,
|
|
650
|
+
unicode_checker: BashUnicodeRiskChecker | None = None,
|
|
651
|
+
exec_ctx_checker: BashExecutionContextChecker | None = None,
|
|
652
|
+
cross_platform_checker: BashCrossPlatformBinaryChecker | None = None,
|
|
653
|
+
remote_pipe_checker: BashRemoteShellPipelineChecker | None = None,
|
|
654
|
+
git_cli_checker: BashGitCliInjectionChecker | None = None,
|
|
655
|
+
shell_checker: BashShellRiskChecker | None = None,
|
|
656
|
+
git_checker: BashGitSecurityChecker | None = None,
|
|
657
|
+
) -> None:
|
|
658
|
+
self._canonicalizer = canonicalizer or BashCommandCanonicalizer()
|
|
659
|
+
self._env_checker = env_checker or BashEnvSecurityChecker()
|
|
660
|
+
self._unicode_checker = unicode_checker or BashUnicodeRiskChecker()
|
|
661
|
+
self._exec_ctx_checker = exec_ctx_checker or BashExecutionContextChecker()
|
|
662
|
+
self._cross_platform_checker = cross_platform_checker or BashCrossPlatformBinaryChecker()
|
|
663
|
+
self._remote_pipe_checker = remote_pipe_checker or BashRemoteShellPipelineChecker()
|
|
664
|
+
self._git_cli_checker = git_cli_checker or BashGitCliInjectionChecker()
|
|
665
|
+
self._shell_checker = shell_checker or BashShellRiskChecker()
|
|
666
|
+
self._git_checker = git_checker or BashGitSecurityChecker()
|
|
667
|
+
|
|
668
|
+
def canonicalize_for_policy(self, command: str) -> str:
|
|
669
|
+
canonical = self._canonicalizer.canonicalize(command)
|
|
670
|
+
return canonical or command
|
|
671
|
+
|
|
672
|
+
def check_command(
|
|
673
|
+
self,
|
|
674
|
+
*,
|
|
675
|
+
command: str,
|
|
676
|
+
analysis: BashAstAnalysis,
|
|
677
|
+
cwd: str,
|
|
678
|
+
) -> AuthorizationDecision | None:
|
|
679
|
+
for checker in (
|
|
680
|
+
self._env_checker.evaluate(command),
|
|
681
|
+
self._unicode_checker.evaluate(command),
|
|
682
|
+
self._exec_ctx_checker.evaluate(command),
|
|
683
|
+
self._cross_platform_checker.evaluate(command),
|
|
684
|
+
self._remote_pipe_checker.evaluate(command),
|
|
685
|
+
self._shell_checker.evaluate(command),
|
|
686
|
+
self._git_cli_checker.evaluate(command),
|
|
687
|
+
self._git_checker.evaluate_command(command=command, analysis=analysis, cwd=cwd),
|
|
688
|
+
):
|
|
689
|
+
if checker is not None:
|
|
690
|
+
return checker.to_decision()
|
|
691
|
+
return None
|
|
692
|
+
|
|
693
|
+
def check_segment(
|
|
694
|
+
self,
|
|
695
|
+
*,
|
|
696
|
+
command: str,
|
|
697
|
+
analysis: BashAstAnalysis,
|
|
698
|
+
cwd: str,
|
|
699
|
+
) -> AuthorizationDecision | None:
|
|
700
|
+
result = self._git_checker.evaluate_segment(command=command, analysis=analysis, cwd=cwd)
|
|
701
|
+
return result.to_decision() if result is not None else None
|
|
702
|
+
|
|
703
|
+
def audit_explain(
|
|
704
|
+
self,
|
|
705
|
+
*,
|
|
706
|
+
command: str,
|
|
707
|
+
analysis: BashAstAnalysis | None = None,
|
|
708
|
+
cwd: str = "",
|
|
709
|
+
) -> dict[str, Any]:
|
|
710
|
+
"""
|
|
711
|
+
结构化攻防审计快照(不阻断执行):用于日志、调试与 UI 对齐 CC 式可观测性。
|
|
712
|
+
"""
|
|
713
|
+
modified = sorted(self._env_checker.collect_modified_vars(command))
|
|
714
|
+
dangerous = sorted(
|
|
715
|
+
v for v in modified if v in self._env_checker.BINARY_HIJACK_VARS
|
|
716
|
+
)
|
|
717
|
+
first_block: dict[str, Any] | None = None
|
|
718
|
+
if analysis is not None:
|
|
719
|
+
d = self.check_command(command=command, analysis=analysis, cwd=cwd or os.getcwd())
|
|
720
|
+
if d is not None:
|
|
721
|
+
first_block = {
|
|
722
|
+
"behavior": d.behavior,
|
|
723
|
+
"policy_id": d.policy_id,
|
|
724
|
+
"metadata": d.metadata,
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
"canonical_command": self.canonicalize_for_policy(command),
|
|
728
|
+
"sys_platform_raw": sys.platform,
|
|
729
|
+
"host_platform": get_host_platform().value,
|
|
730
|
+
"modified_env_vars": modified,
|
|
731
|
+
"dangerous_env_vars": dangerous,
|
|
732
|
+
"would_block_or_ask": first_block,
|
|
733
|
+
"shell_pattern_probe": self._shell_checker.probe(command),
|
|
734
|
+
}
|