@jaguilar87/gaia-ops 1.0.0
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.
- package/CHANGELOG.md +315 -0
- package/CLAUDE.md +154 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/agents/aws-troubleshooter.md +50 -0
- package/agents/claude-architect.md +821 -0
- package/agents/devops-developer.md +92 -0
- package/agents/gcp-troubleshooter.md +50 -0
- package/agents/gitops-operator.md +360 -0
- package/agents/terraform-architect.md +289 -0
- package/bin/gaia-init.js +620 -0
- package/commands/architect.md +97 -0
- package/commands/restore-session.md +87 -0
- package/commands/save-session.md +88 -0
- package/commands/session-status.md +61 -0
- package/commands/speckit.add-task.md +144 -0
- package/commands/speckit.analyze-task.md +65 -0
- package/commands/speckit.implement.md +96 -0
- package/commands/speckit.init.md +237 -0
- package/commands/speckit.plan.md +88 -0
- package/commands/speckit.specify.md +161 -0
- package/commands/speckit.tasks.md +188 -0
- package/config/AGENTS.md +162 -0
- package/config/agent-catalog.md +604 -0
- package/config/context-contracts.md +682 -0
- package/config/git-standards.md +674 -0
- package/config/git_standards.json +69 -0
- package/config/orchestration-workflow.md +735 -0
- package/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_kubectl_security.cpython-312.pyc +0 -0
- package/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/__pycache__/subagent_stop.cpython-312.pyc +0 -0
- package/hooks/post_tool_use.py +463 -0
- package/hooks/pre_kubectl_security.py +205 -0
- package/hooks/pre_tool_use.py +530 -0
- package/hooks/session_start.py +315 -0
- package/hooks/subagent_stop.py +549 -0
- package/index.js +92 -0
- package/package.json +59 -0
- package/speckit/README.en.md +648 -0
- package/speckit/README.md +353 -0
- package/speckit/governance.md +169 -0
- package/speckit/scripts/check-prerequisites.sh +194 -0
- package/speckit/scripts/common.sh +126 -0
- package/speckit/scripts/create-new-feature.sh +131 -0
- package/speckit/scripts/init.sh +42 -0
- package/speckit/scripts/setup-plan.sh +95 -0
- package/speckit/scripts/update-agent-context.sh +718 -0
- package/speckit/templates/adr-template.md +118 -0
- package/speckit/templates/agent-file-template.md +23 -0
- package/speckit/templates/plan-template.md +233 -0
- package/speckit/templates/spec-template.md +116 -0
- package/speckit/templates/tasks-template-bkp.md +136 -0
- package/speckit/templates/tasks-template.md +345 -0
- package/templates/CLAUDE.template.md +170 -0
- package/templates/code-examples/approval_gate_workflow.py +141 -0
- package/templates/code-examples/clarification_workflow.py +94 -0
- package/templates/code-examples/commit_validation.py +86 -0
- package/templates/project-context.template.json +126 -0
- package/templates/settings.template.json +307 -0
- package/tools/__pycache__/agent_router.cpython-312.pyc +0 -0
- package/tools/__pycache__/approval_gate.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_engine.cpython-312.pyc +0 -0
- package/tools/__pycache__/clarify_patterns.cpython-312.pyc +0 -0
- package/tools/__pycache__/commit_validator.cpython-312.pyc +0 -0
- package/tools/__pycache__/context_section_reader.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_dashboard.cpython-312.pyc +0 -0
- package/tools/__pycache__/routing_feedback.cpython-312.pyc +0 -0
- package/tools/__pycache__/semantic_matcher.cpython-312.pyc +0 -0
- package/tools/__pycache__/task_manager.cpython-312.pyc +0 -0
- package/tools/agent_capabilities.json +231 -0
- package/tools/agent_invoker_helper.py +239 -0
- package/tools/agent_router.py +730 -0
- package/tools/approval_gate.py +318 -0
- package/tools/clarify_engine.py +511 -0
- package/tools/clarify_patterns.py +356 -0
- package/tools/commit_validator.py +338 -0
- package/tools/context_provider.py +181 -0
- package/tools/context_section_reader.py +301 -0
- package/tools/demo_clarify.py +104 -0
- package/tools/generate_embeddings.py +168 -0
- package/tools/quicktriage_aws_troubleshooter.sh +45 -0
- package/tools/quicktriage_devops_developer.sh +38 -0
- package/tools/quicktriage_gcp_troubleshooter.sh +51 -0
- package/tools/quicktriage_gitops_operator.sh +47 -0
- package/tools/quicktriage_terraform_architect.sh +40 -0
- package/tools/semantic_matcher.py +222 -0
- package/tools/task_manager.py +547 -0
- package/tools/task_manager_README.md +395 -0
- package/tools/task_manager_example.py +215 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pre-tool use hook for Claude Code Agent System
|
|
4
|
+
Implements security policy gates, tier-based command filtering,
|
|
5
|
+
and routing metadata verification with centralized capabilities
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
15
|
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
16
|
+
|
|
17
|
+
from pre_kubectl_security import validate_gitops_workflow
|
|
18
|
+
|
|
19
|
+
# Configure logging
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.INFO,
|
|
22
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
23
|
+
)
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
class SecurityTier:
|
|
27
|
+
"""Security tier definitions for command classification"""
|
|
28
|
+
|
|
29
|
+
T0_READ_ONLY = "T0" # describe, get, show, list operations
|
|
30
|
+
T1_VALIDATION = "T1" # validate, plan, template, lint operations
|
|
31
|
+
T2_DRY_RUN = "T2" # --dry-run, --plan-only operations
|
|
32
|
+
T3_BLOCKED = "T3" # apply, reconcile, deploy operations
|
|
33
|
+
|
|
34
|
+
class PolicyEngine:
|
|
35
|
+
"""Policy engine for command validation and security enforcement"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
# Load agent capabilities for dynamic routing verification
|
|
39
|
+
self.capabilities = self._load_capabilities()
|
|
40
|
+
self.skills = self.capabilities.get("routing_matrix", {}).get("skills", {})
|
|
41
|
+
self.integration_config = self.capabilities.get("integration_metadata", {})
|
|
42
|
+
|
|
43
|
+
self.blocked_commands = [
|
|
44
|
+
# Terraform destructive operations
|
|
45
|
+
r"terraform\s+apply(?!\s+--help)",
|
|
46
|
+
r"terraform\s+destroy",
|
|
47
|
+
r"terragrunt\s+apply(?!\s+--help)",
|
|
48
|
+
r"terragrunt\s+destroy",
|
|
49
|
+
|
|
50
|
+
# Kubernetes write operations
|
|
51
|
+
r"kubectl\s+apply(?!\s+.*--dry-run)",
|
|
52
|
+
r"kubectl\s+create(?!\s+.*--dry-run)",
|
|
53
|
+
r"kubectl\s+delete",
|
|
54
|
+
r"kubectl\s+patch",
|
|
55
|
+
r"kubectl\s+replace(?!\s+.*--dry-run)",
|
|
56
|
+
|
|
57
|
+
# Helm write operations
|
|
58
|
+
r"helm\s+install(?!\s+.*--dry-run)",
|
|
59
|
+
r"helm\s+upgrade(?!\s+.*--dry-run)",
|
|
60
|
+
r"helm\s+uninstall",
|
|
61
|
+
r"helm\s+delete",
|
|
62
|
+
|
|
63
|
+
# Flux write operations
|
|
64
|
+
r"flux\s+reconcile(?!\s+.*--dry-run)",
|
|
65
|
+
r"flux\s+create",
|
|
66
|
+
r"flux\s+delete",
|
|
67
|
+
|
|
68
|
+
# GCP write operations
|
|
69
|
+
r"gcloud\s+.*\s+(create|update|delete|patch)",
|
|
70
|
+
r"gcloud\s+(create|update|delete|patch)",
|
|
71
|
+
|
|
72
|
+
# AWS write operations
|
|
73
|
+
r"aws\s+.*\s+(create|update|delete|put)",
|
|
74
|
+
|
|
75
|
+
# Docker write operations
|
|
76
|
+
r"docker\s+build",
|
|
77
|
+
r"docker\s+push",
|
|
78
|
+
r"docker\s+run(?!\s+.*--rm)",
|
|
79
|
+
|
|
80
|
+
# Git write operations (unless explicitly allowed)
|
|
81
|
+
r"git\s+push(?!\s+--dry-run)",
|
|
82
|
+
r"git\s+commit(?!\s+.*--dry-run)",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
self.ask_commands = [
|
|
86
|
+
r"terragrunt\s+apply",
|
|
87
|
+
r"terraform\s+apply",
|
|
88
|
+
r"git\s+push",
|
|
89
|
+
r"git\s+commit",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
self.allowed_read_operations = [
|
|
93
|
+
# Terraform read operations
|
|
94
|
+
r"terraform\s+(fmt|validate|plan|show|output|version)",
|
|
95
|
+
r"terragrunt\s+(plan|output|validate)",
|
|
96
|
+
|
|
97
|
+
# Kubernetes read operations
|
|
98
|
+
r"kubectl\s+(get|describe|logs|explain|version)",
|
|
99
|
+
r"kubectl\s+.*--dry-run(=client)?",
|
|
100
|
+
|
|
101
|
+
# Helm read operations
|
|
102
|
+
r"helm\s+(template|lint|list|status|version)",
|
|
103
|
+
r"helm\s+.*--dry-run",
|
|
104
|
+
|
|
105
|
+
# Flux read operations
|
|
106
|
+
r"flux\s+(check|get|version)",
|
|
107
|
+
|
|
108
|
+
# GCP read operations
|
|
109
|
+
r"gcloud\s+.*\s+(describe|list|show|get)",
|
|
110
|
+
r"gcloud\s+(describe|list|show|get)",
|
|
111
|
+
|
|
112
|
+
# AWS read operations
|
|
113
|
+
r"aws\s+.*\s+(describe|list|get)",
|
|
114
|
+
|
|
115
|
+
# General utilities
|
|
116
|
+
r"ls|pwd|cd|cat|head|tail|grep|find|which",
|
|
117
|
+
r"echo|printf",
|
|
118
|
+
|
|
119
|
+
# Python scripts (session management)
|
|
120
|
+
r"python3?\s+.*session.*\.py",
|
|
121
|
+
r"python3?\s+\.claude/session/scripts/.*\.py",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# Commands that require credentials to be loaded
|
|
125
|
+
self.credential_required_patterns = [
|
|
126
|
+
r"kubectl\s+(?!version)", # kubectl (except version)
|
|
127
|
+
r"flux\s+(?!version)", # flux (except version)
|
|
128
|
+
r"helm\s+(?!version)", # helm (except version)
|
|
129
|
+
r"gcloud\s+container\s+", # gcloud container
|
|
130
|
+
r"gcloud\s+sql\s+", # gcloud sql
|
|
131
|
+
r"gcloud\s+redis\s+", # gcloud redis
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
def check_credentials_required(self, command: str) -> Tuple[bool, str]:
|
|
135
|
+
"""
|
|
136
|
+
Check if command requires credentials and provide guidance
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
(requires_creds: bool, warning_message: str)
|
|
140
|
+
"""
|
|
141
|
+
# Check if command requires credentials
|
|
142
|
+
for pattern in self.credential_required_patterns:
|
|
143
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
144
|
+
# Check if credentials are being loaded in the command
|
|
145
|
+
if "gcloud auth" in command or "gcloud config" in command:
|
|
146
|
+
# Auth commands don't need pre-loading
|
|
147
|
+
return False, ""
|
|
148
|
+
|
|
149
|
+
if "source" in command and "load-cluster-credentials.sh" in command:
|
|
150
|
+
# Credentials are being loaded via cluster-specific script
|
|
151
|
+
return False, ""
|
|
152
|
+
|
|
153
|
+
# Check if using optimized scripts that auto-load credentials
|
|
154
|
+
if any(script in command for script in ["k8s-health.sh", "flux-status.sh"]):
|
|
155
|
+
# These scripts auto-load credentials, no warning needed
|
|
156
|
+
return False, ""
|
|
157
|
+
|
|
158
|
+
# Command requires credentials but not loading them
|
|
159
|
+
warning = (
|
|
160
|
+
"โ ๏ธ This command requires GCP/Kubernetes credentials to be loaded.\n\n"
|
|
161
|
+
"Recommended patterns:\n"
|
|
162
|
+
" 1. Load credentials inline:\n"
|
|
163
|
+
" gcloud auth application-default login && kubectl ...\n\n"
|
|
164
|
+
" 2. Use gcloud container clusters get-credentials first:\n"
|
|
165
|
+
" gcloud container clusters get-credentials <cluster> --region <region> && kubectl ...\n\n"
|
|
166
|
+
" 3. Ensure KUBECONFIG is set for kubectl/helm/flux commands\n"
|
|
167
|
+
)
|
|
168
|
+
return True, warning
|
|
169
|
+
|
|
170
|
+
return False, ""
|
|
171
|
+
|
|
172
|
+
def classify_command_tier(self, command: str) -> str:
|
|
173
|
+
"""Classify command into security tier"""
|
|
174
|
+
|
|
175
|
+
# Check for blocked operations first
|
|
176
|
+
for pattern in self.blocked_commands:
|
|
177
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
178
|
+
return SecurityTier.T3_BLOCKED
|
|
179
|
+
|
|
180
|
+
# Check for dry-run operations
|
|
181
|
+
if "--dry-run" in command or "--plan-only" in command:
|
|
182
|
+
return SecurityTier.T2_DRY_RUN
|
|
183
|
+
|
|
184
|
+
# Check for validation operations
|
|
185
|
+
validation_patterns = [
|
|
186
|
+
r"validate", r"plan", r"template", r"lint", r"check", r"fmt"
|
|
187
|
+
]
|
|
188
|
+
for pattern in validation_patterns:
|
|
189
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
190
|
+
return SecurityTier.T1_VALIDATION
|
|
191
|
+
|
|
192
|
+
# Check for read operations
|
|
193
|
+
for pattern in self.allowed_read_operations:
|
|
194
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
195
|
+
return SecurityTier.T0_READ_ONLY
|
|
196
|
+
|
|
197
|
+
# Default to blocked for unknown commands
|
|
198
|
+
return SecurityTier.T3_BLOCKED
|
|
199
|
+
|
|
200
|
+
@retry(
|
|
201
|
+
stop=stop_after_attempt(3),
|
|
202
|
+
wait=wait_exponential(multiplier=1, min=1, max=5),
|
|
203
|
+
reraise=True
|
|
204
|
+
)
|
|
205
|
+
def _load_capabilities_with_retry(self, capabilities_file: Path) -> Dict[str, Any]:
|
|
206
|
+
"""Load capabilities file with retry logic"""
|
|
207
|
+
try:
|
|
208
|
+
with open(capabilities_file, 'r', encoding='utf-8') as f:
|
|
209
|
+
data = json.load(f)
|
|
210
|
+
|
|
211
|
+
# Validate structure
|
|
212
|
+
if not isinstance(data, dict):
|
|
213
|
+
raise ValueError("Capabilities file must be a JSON object")
|
|
214
|
+
|
|
215
|
+
logger.debug(f"Successfully loaded capabilities from {capabilities_file}")
|
|
216
|
+
return data
|
|
217
|
+
|
|
218
|
+
except FileNotFoundError:
|
|
219
|
+
logger.error(f"Capabilities file not found: {capabilities_file}")
|
|
220
|
+
raise
|
|
221
|
+
except json.JSONDecodeError as e:
|
|
222
|
+
logger.error(f"Invalid JSON in capabilities file: {e}")
|
|
223
|
+
raise
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"Unexpected error loading capabilities: {e}")
|
|
226
|
+
raise
|
|
227
|
+
|
|
228
|
+
def _load_capabilities(self) -> Dict[str, Any]:
|
|
229
|
+
"""Load agent capabilities configuration with robust error handling"""
|
|
230
|
+
capabilities_file = Path(__file__).parent.parent / "tools" / "agent_capabilities.json"
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if not capabilities_file.exists():
|
|
234
|
+
logger.warning(f"Capabilities file does not exist: {capabilities_file}")
|
|
235
|
+
return self._get_fallback_capabilities()
|
|
236
|
+
|
|
237
|
+
# Try to load with retry
|
|
238
|
+
return self._load_capabilities_with_retry(capabilities_file)
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.warning(f"Could not load agent capabilities after retries: {e}")
|
|
242
|
+
logger.info("Using fallback capabilities configuration")
|
|
243
|
+
return self._get_fallback_capabilities()
|
|
244
|
+
|
|
245
|
+
def _inspect_script_content(self, script_path: str) -> Tuple[bool, str, Optional[str]]:
|
|
246
|
+
"""Inspects script content for blocked or sensitive commands."""
|
|
247
|
+
try:
|
|
248
|
+
with open(script_path, 'r') as f:
|
|
249
|
+
for line_num, line in enumerate(f, 1):
|
|
250
|
+
line = line.strip()
|
|
251
|
+
if not line or line.startswith('#'):
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Check against blocked commands
|
|
255
|
+
for pattern in self.blocked_commands:
|
|
256
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
257
|
+
return False, SecurityTier.T3_BLOCKED, f"Script contains blocked command on line {line_num}: '{line}'"
|
|
258
|
+
|
|
259
|
+
# Check against commands requiring approval
|
|
260
|
+
for pattern in self.ask_commands:
|
|
261
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
262
|
+
# This doesn't directly trigger 'ask', but blocks it for safety.
|
|
263
|
+
# The Claude Code environment itself will trigger 'ask' for the direct command.
|
|
264
|
+
# The hook's job is to prevent unnoticed execution inside a script.
|
|
265
|
+
return False, SecurityTier.T3_BLOCKED, f"Script contains sensitive command requiring direct approval on line {line_num}: '{line}'. Execute it directly, not from a script."
|
|
266
|
+
|
|
267
|
+
except FileNotFoundError:
|
|
268
|
+
return False, SecurityTier.T3_BLOCKED, f"Script file not found: {script_path}"
|
|
269
|
+
except Exception as e:
|
|
270
|
+
return False, SecurityTier.T3_BLOCKED, f"Error reading script file {script_path}: {e}"
|
|
271
|
+
|
|
272
|
+
return True, SecurityTier.T0_READ_ONLY, "Script content seems safe."
|
|
273
|
+
|
|
274
|
+
def verify_routing_metadata(self, tool_name: str, command: str, bundle_metadata: Optional[Dict] = None) -> Tuple[bool, str]:
|
|
275
|
+
"""Verify routing metadata against bundle expectations"""
|
|
276
|
+
|
|
277
|
+
if not self.integration_config.get("handshake_validation", {}).get("verify_agent_match", False):
|
|
278
|
+
return True, "Routing verification disabled"
|
|
279
|
+
|
|
280
|
+
if not bundle_metadata:
|
|
281
|
+
# Try to load from environment or session
|
|
282
|
+
bundle_metadata = self._get_current_bundle_metadata()
|
|
283
|
+
|
|
284
|
+
if not bundle_metadata:
|
|
285
|
+
return True, "No bundle metadata available"
|
|
286
|
+
|
|
287
|
+
suggested_agent = bundle_metadata.get("suggested_agent")
|
|
288
|
+
expected_tier = bundle_metadata.get("security_tier")
|
|
289
|
+
|
|
290
|
+
# Classify current command
|
|
291
|
+
actual_tier = self.classify_command_tier(command)
|
|
292
|
+
|
|
293
|
+
# Check tier compatibility
|
|
294
|
+
if expected_tier and actual_tier != expected_tier:
|
|
295
|
+
if self.integration_config.get("handshake_validation", {}).get("require_approval_on_mismatch", False):
|
|
296
|
+
return False, f"Tier mismatch: expected {expected_tier}, got {actual_tier}"
|
|
297
|
+
else:
|
|
298
|
+
logger.warning(f"Tier mismatch detected but proceeding: expected {expected_tier}, got {actual_tier}")
|
|
299
|
+
|
|
300
|
+
# Log routing decision for audit
|
|
301
|
+
if self.integration_config.get("handshake_validation", {}).get("log_routing_decisions", False):
|
|
302
|
+
logger.info(f"Routing verification - Agent: {suggested_agent}, Tier: {expected_tier} -> {actual_tier}")
|
|
303
|
+
|
|
304
|
+
return True, "Routing verification passed"
|
|
305
|
+
|
|
306
|
+
def _get_current_bundle_metadata(self) -> Optional[Dict]:
|
|
307
|
+
"""Get current bundle metadata from session or environment"""
|
|
308
|
+
try:
|
|
309
|
+
# Try to get from environment variable
|
|
310
|
+
bundle_data = os.environ.get("CLAUDE_TASK_METADATA")
|
|
311
|
+
if bundle_data:
|
|
312
|
+
return json.loads(bundle_data)
|
|
313
|
+
|
|
314
|
+
# Try to get from session active context
|
|
315
|
+
session_dir = Path(__file__).parent.parent / "session" / "active"
|
|
316
|
+
context_file = session_dir / "context.json"
|
|
317
|
+
if context_file.exists():
|
|
318
|
+
with open(context_file) as f:
|
|
319
|
+
context = json.load(f)
|
|
320
|
+
return context.get("current_task_metadata")
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.debug(f"Could not load bundle metadata: {e}")
|
|
323
|
+
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
def validate_command(self, tool_name: str, command: str) -> Tuple[bool, str, str]:
|
|
327
|
+
"""
|
|
328
|
+
Validate command against security policies
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
(is_allowed: bool, tier: str, reason: str)
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
# Validate inputs
|
|
335
|
+
if not isinstance(tool_name, str):
|
|
336
|
+
logger.error(f"Invalid tool_name type: {type(tool_name)}")
|
|
337
|
+
return False, SecurityTier.T3_BLOCKED, "Invalid tool name"
|
|
338
|
+
|
|
339
|
+
if not isinstance(command, str):
|
|
340
|
+
logger.error(f"Invalid command type: {type(command)}")
|
|
341
|
+
return False, SecurityTier.T3_BLOCKED, "Invalid command"
|
|
342
|
+
|
|
343
|
+
# Skip validation for non-bash tools
|
|
344
|
+
if tool_name.lower() != "bash":
|
|
345
|
+
return True, SecurityTier.T0_READ_ONLY, "Non-bash tool allowed"
|
|
346
|
+
|
|
347
|
+
# Handle empty commands
|
|
348
|
+
if not command or not command.strip():
|
|
349
|
+
logger.warning("Empty command provided")
|
|
350
|
+
return False, SecurityTier.T3_BLOCKED, "Empty command not allowed"
|
|
351
|
+
|
|
352
|
+
# Check if command is a script execution
|
|
353
|
+
script_match = re.match(r"^\s*(bash|sh)\s+([\w\-\./_]+)", command)
|
|
354
|
+
if script_match:
|
|
355
|
+
script_path = script_match.group(2)
|
|
356
|
+
is_allowed, tier, reason = self._inspect_script_content(script_path)
|
|
357
|
+
if not is_allowed:
|
|
358
|
+
return is_allowed, tier, reason
|
|
359
|
+
|
|
360
|
+
# Enforce GitOps security rules for cluster-related commands
|
|
361
|
+
if any(keyword in command for keyword in ("kubectl", "helm", "flux")):
|
|
362
|
+
try:
|
|
363
|
+
gitops_validation = validate_gitops_workflow(command)
|
|
364
|
+
except Exception as exc:
|
|
365
|
+
logger.error(f"GitOps validation failed: {exc}", exc_info=True)
|
|
366
|
+
return (
|
|
367
|
+
False,
|
|
368
|
+
SecurityTier.T3_BLOCKED,
|
|
369
|
+
"GitOps security validation error; blocking command for safety"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if not gitops_validation.get("allowed", False):
|
|
373
|
+
suggestions = gitops_validation.get("suggestions") or []
|
|
374
|
+
suggestion_text = ""
|
|
375
|
+
if suggestions:
|
|
376
|
+
suggestion_text = "\n\nSuggestions:\n - " + "\n - ".join(suggestions)
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
False,
|
|
380
|
+
SecurityTier.T3_BLOCKED,
|
|
381
|
+
f"GitOps policy violation: {gitops_validation.get('reason', 'operation not permitted')}"
|
|
382
|
+
f"{suggestion_text}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if gitops_validation.get("severity") in ("warning", "high"):
|
|
386
|
+
logger.warning(
|
|
387
|
+
"GitOps validation warning for command '%s': %s",
|
|
388
|
+
command,
|
|
389
|
+
gitops_validation.get("reason", "unspecified reason")
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Check if credentials are required (provide warning, don't block)
|
|
393
|
+
requires_creds, creds_warning = self.check_credentials_required(command)
|
|
394
|
+
if requires_creds:
|
|
395
|
+
logger.info(f"Credential warning issued for command: {command[:100]}")
|
|
396
|
+
# Log warning but don't block (allow command to proceed)
|
|
397
|
+
# User will see authentication error if credentials are actually missing
|
|
398
|
+
|
|
399
|
+
tier = self.classify_command_tier(command)
|
|
400
|
+
|
|
401
|
+
if tier == SecurityTier.T3_BLOCKED:
|
|
402
|
+
return False, tier, f"Command blocked by security policy: {command[:100]}"
|
|
403
|
+
|
|
404
|
+
# Verify routing metadata if enabled
|
|
405
|
+
routing_allowed, routing_reason = self.verify_routing_metadata(tool_name, command)
|
|
406
|
+
if not routing_allowed:
|
|
407
|
+
logger.warning(f"Routing verification failed: {routing_reason}")
|
|
408
|
+
return False, tier, f"Routing verification failed: {routing_reason}"
|
|
409
|
+
|
|
410
|
+
# Include credential warning in reason if present
|
|
411
|
+
final_reason = f"Command allowed in tier {tier} ({routing_reason})"
|
|
412
|
+
if requires_creds and creds_warning:
|
|
413
|
+
final_reason = f"{final_reason}\n\n{creds_warning}"
|
|
414
|
+
|
|
415
|
+
return True, tier, final_reason
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.error(f"Error during command validation: {e}", exc_info=True)
|
|
419
|
+
# Fail closed - if validation errors, block the command
|
|
420
|
+
return False, SecurityTier.T3_BLOCKED, f"Validation error: {str(e)}"
|
|
421
|
+
|
|
422
|
+
def pre_tool_use_hook(tool_name: str, parameters: Dict) -> Optional[str]:
|
|
423
|
+
"""
|
|
424
|
+
Pre-tool use hook implementation with robust error handling
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
tool_name: Name of the tool being invoked
|
|
428
|
+
parameters: Tool parameters
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
None if allowed, error message if blocked
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
# Validate inputs
|
|
435
|
+
if not isinstance(tool_name, str):
|
|
436
|
+
logger.error(f"Invalid tool_name type: {type(tool_name)}")
|
|
437
|
+
return "๐ซ Error: Invalid tool name provided"
|
|
438
|
+
|
|
439
|
+
if not isinstance(parameters, dict):
|
|
440
|
+
logger.error(f"Invalid parameters type: {type(parameters)}")
|
|
441
|
+
return "๐ซ Error: Invalid parameters provided"
|
|
442
|
+
|
|
443
|
+
policy_engine = PolicyEngine()
|
|
444
|
+
|
|
445
|
+
# Extract command from parameters
|
|
446
|
+
command = ""
|
|
447
|
+
if tool_name.lower() == "bash":
|
|
448
|
+
command = parameters.get("command", "")
|
|
449
|
+
|
|
450
|
+
# Validate command exists for bash tool
|
|
451
|
+
if not command:
|
|
452
|
+
logger.warning("Bash tool invoked without command")
|
|
453
|
+
return "๐ซ Error: Bash tool requires a command"
|
|
454
|
+
|
|
455
|
+
# Validate command
|
|
456
|
+
is_allowed, tier, reason = policy_engine.validate_command(tool_name, command)
|
|
457
|
+
|
|
458
|
+
# Log the decision with structured data
|
|
459
|
+
log_data = {
|
|
460
|
+
"tool": tool_name,
|
|
461
|
+
"command": command[:100] if command else "N/A",
|
|
462
|
+
"tier": tier,
|
|
463
|
+
"allowed": is_allowed,
|
|
464
|
+
"reason": reason
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if not is_allowed:
|
|
468
|
+
logger.warning(f"BLOCKED: {json.dumps(log_data)}")
|
|
469
|
+
return (
|
|
470
|
+
f"๐ซ Command blocked by security policy\n\n"
|
|
471
|
+
f"Tier: {tier}\n"
|
|
472
|
+
f"Reason: {reason}\n\n"
|
|
473
|
+
f"๐ก Use validation or read-only alternatives instead:"
|
|
474
|
+
f"\n - terraform plan (instead of apply)"
|
|
475
|
+
f"\n - kubectl get/describe (instead of apply/delete)"
|
|
476
|
+
f"\n - --dry-run flag for testing changes\n"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
logger.info(f"ALLOWED: {json.dumps(log_data)}")
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
except Exception as e:
|
|
483
|
+
logger.error(f"Unexpected error in pre_tool_use_hook: {e}", exc_info=True)
|
|
484
|
+
# Fail closed - return error message
|
|
485
|
+
return f"๐ซ Error during security validation: {str(e)}\n\nPlease contact DevOps team."
|
|
486
|
+
|
|
487
|
+
def main():
|
|
488
|
+
"""CLI interface for testing the policy engine"""
|
|
489
|
+
|
|
490
|
+
if len(sys.argv) < 2:
|
|
491
|
+
print("Usage: python pre_tool_use.py <command>")
|
|
492
|
+
print(" python pre_tool_use.py --test")
|
|
493
|
+
sys.exit(1)
|
|
494
|
+
|
|
495
|
+
if sys.argv[1] == "--test":
|
|
496
|
+
# Run test cases
|
|
497
|
+
policy_engine = PolicyEngine()
|
|
498
|
+
|
|
499
|
+
test_cases = [
|
|
500
|
+
("terraform validate", True, SecurityTier.T1_VALIDATION),
|
|
501
|
+
("terraform apply", False, SecurityTier.T3_BLOCKED),
|
|
502
|
+
("kubectl get pods", True, SecurityTier.T0_READ_ONLY),
|
|
503
|
+
("kubectl apply -f manifest.yaml", False, SecurityTier.T3_BLOCKED),
|
|
504
|
+
("kubectl apply -f manifest.yaml --dry-run=client", True, SecurityTier.T2_DRY_RUN),
|
|
505
|
+
("helm template myapp", True, SecurityTier.T1_VALIDATION),
|
|
506
|
+
("helm install myapp", False, SecurityTier.T3_BLOCKED),
|
|
507
|
+
("gcloud container clusters describe my-cluster", True, SecurityTier.T0_READ_ONLY),
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
print("๐งช Testing Policy Engine...")
|
|
511
|
+
for command, expected_allowed, expected_tier in test_cases:
|
|
512
|
+
is_allowed, tier, reason = policy_engine.validate_command("bash", command)
|
|
513
|
+
status = "โ
" if is_allowed == expected_allowed and tier == expected_tier else "โ"
|
|
514
|
+
print(f"{status} {command}: {tier} ({'ALLOWED' if is_allowed else 'BLOCKED'})")
|
|
515
|
+
|
|
516
|
+
print("โจ Test completed")
|
|
517
|
+
|
|
518
|
+
else:
|
|
519
|
+
# Test specific command
|
|
520
|
+
command = " ".join(sys.argv[1:])
|
|
521
|
+
result = pre_tool_use_hook("bash", {"command": command})
|
|
522
|
+
|
|
523
|
+
if result:
|
|
524
|
+
print(f"โ BLOCKED: {result}")
|
|
525
|
+
sys.exit(1)
|
|
526
|
+
else:
|
|
527
|
+
print(f"โ
ALLOWED: {command}")
|
|
528
|
+
|
|
529
|
+
if __name__ == "__main__":
|
|
530
|
+
main()
|