awslabs.terraform-mcp-server 1.0.14__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.
Files changed (30) hide show
  1. awslabs/__init__.py +17 -0
  2. awslabs/terraform_mcp_server/__init__.py +17 -0
  3. awslabs/terraform_mcp_server/impl/resources/__init__.py +25 -0
  4. awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +66 -0
  5. awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +69 -0
  6. awslabs/terraform_mcp_server/impl/tools/__init__.py +33 -0
  7. awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +223 -0
  8. awslabs/terraform_mcp_server/impl/tools/execute_terragrunt_command.py +320 -0
  9. awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +376 -0
  10. awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +691 -0
  11. awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +641 -0
  12. awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +458 -0
  13. awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +349 -0
  14. awslabs/terraform_mcp_server/impl/tools/utils.py +572 -0
  15. awslabs/terraform_mcp_server/models/__init__.py +49 -0
  16. awslabs/terraform_mcp_server/models/models.py +381 -0
  17. awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +1240 -0
  18. awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +1039 -0
  19. awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +143 -0
  20. awslabs/terraform_mcp_server/server.py +440 -0
  21. awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +3125 -0
  22. awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +3833 -0
  23. awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +2523 -0
  24. awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +142 -0
  25. awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +330 -0
  26. awslabs/terraform_mcp_server/static/__init__.py +38 -0
  27. awslabs_terraform_mcp_server-1.0.14.dist-info/METADATA +166 -0
  28. awslabs_terraform_mcp_server-1.0.14.dist-info/RECORD +30 -0
  29. awslabs_terraform_mcp_server-1.0.14.dist-info/WHEEL +4 -0
  30. awslabs_terraform_mcp_server-1.0.14.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,320 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Implementation of Terragrunt command execution tool."""
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import subprocess
21
+ from awslabs.terraform_mcp_server.impl.tools.utils import get_dangerous_patterns
22
+ from awslabs.terraform_mcp_server.models import (
23
+ TerragruntExecutionRequest,
24
+ TerragruntExecutionResult,
25
+ )
26
+ from loguru import logger
27
+
28
+
29
+ async def execute_terragrunt_command_impl(
30
+ request: TerragruntExecutionRequest,
31
+ ) -> TerragruntExecutionResult:
32
+ """Execute Terragrunt workflow commands against an AWS account.
33
+
34
+ This tool runs Terragrunt commands (init, plan, validate, apply, destroy, run-all) in the
35
+ specified working directory, with optional variables and region settings.
36
+
37
+ Parameters:
38
+ request: Details about the Terragrunt command to execute
39
+
40
+ Returns:
41
+ A TerragruntExecutionResult object containing command output and status
42
+ """
43
+ logger.info(f"Executing 'terragrunt {request.command}' in {request.working_directory}")
44
+
45
+ # Helper function to clean output text
46
+ def clean_output_text(text: str) -> str:
47
+ """Clean output text by removing or replacing problematic Unicode characters.
48
+
49
+ Args:
50
+ text: The text to clean
51
+
52
+ Returns:
53
+ Cleaned text with ASCII-friendly replacements
54
+ """
55
+ if not text:
56
+ return text
57
+
58
+ # First remove ANSI escape sequences (color codes, cursor movement)
59
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
60
+ text = ansi_escape.sub('', text)
61
+
62
+ # Remove C0 and C1 control characters (except common whitespace)
63
+ control_chars = re.compile(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]')
64
+ text = control_chars.sub('', text)
65
+
66
+ # Replace HTML entities
67
+ html_entities = {
68
+ '->': '->', # Replace HTML arrow
69
+ '&lt;': '<', # Less than
70
+ '&gt;': '>', # Greater than
71
+ '&amp;': '&', # Ampersand
72
+ }
73
+ for entity, replacement in html_entities.items():
74
+ text = text.replace(entity, replacement)
75
+
76
+ # Replace box-drawing and other special Unicode characters with ASCII equivalents
77
+ unicode_chars = {
78
+ '\u2500': '-', # Horizontal line
79
+ '\u2502': '|', # Vertical line
80
+ '\u2514': '+', # Up and right
81
+ '\u2518': '+', # Up and left
82
+ '\u2551': '|', # Double vertical
83
+ '\u2550': '-', # Double horizontal
84
+ '\u2554': '+', # Double down and right
85
+ '\u2557': '+', # Double down and left
86
+ '\u255a': '+', # Double up and right
87
+ '\u255d': '+', # Double up and left
88
+ '\u256c': '+', # Double cross
89
+ '\u2588': '#', # Full block
90
+ '\u25cf': '*', # Black circle
91
+ '\u2574': '-', # Left box drawing
92
+ '\u2576': '-', # Right box drawing
93
+ '\u2577': '|', # Down box drawing
94
+ '\u2575': '|', # Up box drawing
95
+ }
96
+ for char, replacement in unicode_chars.items():
97
+ text = text.replace(char, replacement)
98
+
99
+ return text
100
+
101
+ # Set environment variables for AWS region if provided
102
+ env = os.environ.copy()
103
+ if request.aws_region:
104
+ env['AWS_REGION'] = request.aws_region
105
+
106
+ # Security check for command injection
107
+ allowed_commands = ['init', 'plan', 'validate', 'apply', 'destroy', 'output', 'run-all']
108
+ if request.command not in allowed_commands:
109
+ logger.error(f'Invalid Terragrunt command: {request.command}')
110
+ return TerragruntExecutionResult(
111
+ command=f'terragrunt {request.command}',
112
+ status='error',
113
+ error_message=f'Invalid Terragrunt command: {request.command}. Allowed commands are: {", ".join(allowed_commands)}',
114
+ working_directory=request.working_directory,
115
+ outputs=None,
116
+ affected_dirs=None,
117
+ )
118
+
119
+ # Validate that terragrunt_config is not used with run-all
120
+ if request.terragrunt_config and request.command == 'run-all':
121
+ logger.error('terragrunt_config cannot be used with run-all command')
122
+ return TerragruntExecutionResult(
123
+ command=f'terragrunt {request.command}',
124
+ status='error',
125
+ error_message='Invalid configuration: --terragrunt-config cannot be used with run-all command',
126
+ working_directory=request.working_directory,
127
+ outputs=None,
128
+ affected_dirs=None,
129
+ )
130
+
131
+ # Check for potentially dangerous characters or command injection attempts
132
+ dangerous_patterns = get_dangerous_patterns()
133
+ logger.debug(f'Checking for {len(dangerous_patterns)} dangerous patterns')
134
+
135
+ for pattern in dangerous_patterns:
136
+ if request.variables:
137
+ # Check if the pattern is in any of the variable values
138
+ for var_name, var_value in request.variables.items():
139
+ if pattern in str(var_value) or pattern in str(var_name):
140
+ logger.error(
141
+ f'Potentially dangerous pattern detected in variable {var_name}: {pattern}'
142
+ )
143
+ return TerragruntExecutionResult(
144
+ command=f'terragrunt {request.command}',
145
+ status='error',
146
+ error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in variable '{var_name}'",
147
+ working_directory=request.working_directory,
148
+ outputs=None,
149
+ affected_dirs=None,
150
+ )
151
+
152
+ # Check terragrunt_config for dangerous patterns
153
+ if request.terragrunt_config and pattern in str(request.terragrunt_config):
154
+ logger.error(f'Potentially dangerous pattern detected in terragrunt_config: {pattern}')
155
+ return TerragruntExecutionResult(
156
+ command=f'terragrunt {request.command}',
157
+ status='error',
158
+ error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in terragrunt_config",
159
+ working_directory=request.working_directory,
160
+ outputs=None,
161
+ affected_dirs=None,
162
+ )
163
+
164
+ # Also check include_dirs and exclude_dirs for dangerous patterns
165
+ if request.include_dirs:
166
+ for dir_path in request.include_dirs:
167
+ if pattern in str(dir_path):
168
+ logger.error(
169
+ f'Potentially dangerous pattern detected in include_dirs: {pattern}'
170
+ )
171
+ return TerragruntExecutionResult(
172
+ command=f'terragrunt {request.command}',
173
+ status='error',
174
+ error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in include_dirs",
175
+ working_directory=request.working_directory,
176
+ outputs=None,
177
+ affected_dirs=None,
178
+ )
179
+
180
+ if request.exclude_dirs:
181
+ for dir_path in request.exclude_dirs:
182
+ if pattern in str(dir_path):
183
+ logger.error(
184
+ f'Potentially dangerous pattern detected in exclude_dirs: {pattern}'
185
+ )
186
+ return TerragruntExecutionResult(
187
+ command=f'terragrunt {request.command}',
188
+ status='error',
189
+ error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in exclude_dirs",
190
+ working_directory=request.working_directory,
191
+ outputs=None,
192
+ affected_dirs=None,
193
+ )
194
+
195
+ # Build the command
196
+ base_cmd = ['terragrunt']
197
+
198
+ # Handle run-all command differently
199
+ if request.command == 'run-all':
200
+ base_cmd.append('run-all')
201
+ # The actual terraform command becomes the first argument
202
+ # Default to 'apply' if not specified in the command
203
+ base_cmd.append('apply')
204
+ else:
205
+ base_cmd.append(request.command)
206
+
207
+ # Add auto-approve flag for apply and destroy commands to make them non-interactive
208
+ if request.command in ['apply', 'destroy'] or (request.command == 'run-all'):
209
+ logger.info(f'Adding -auto-approve flag to {request.command} command')
210
+ base_cmd.append('-auto-approve')
211
+
212
+ # Add terragrunt_config if specified and not using run-all
213
+ if request.terragrunt_config:
214
+ logger.info(f'Using custom terragrunt config file: {request.terragrunt_config}')
215
+ base_cmd.append(f'--terragrunt-config={request.terragrunt_config}')
216
+
217
+ # Add variables only for commands that accept them (plan, apply, destroy, output)
218
+ if request.command in ['plan', 'apply', 'destroy', 'output', 'run-all'] and request.variables:
219
+ logger.info(f'Adding {len(request.variables)} variables to {request.command} command')
220
+ for key, value in request.variables.items():
221
+ base_cmd.append(f'-var={key}={value}')
222
+
223
+ # Add include-dirs if specified
224
+ if request.include_dirs and request.command == 'run-all':
225
+ for dir_path in request.include_dirs:
226
+ base_cmd.append(f'--queue-include-dir={dir_path}')
227
+
228
+ # Add exclude-dirs if specified
229
+ if request.exclude_dirs and request.command == 'run-all':
230
+ for dir_path in request.exclude_dirs:
231
+ base_cmd.append(f'--queue-exclude-dir={dir_path}')
232
+
233
+ # Execute command
234
+ try:
235
+ # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit
236
+ # Safe: Command is validated against allowlist, variables are checked for dangerous patterns,
237
+ # working_directory is user-controlled but subprocess uses cwd parameter (not shell injection)
238
+ process = subprocess.run( # noqa: B603 - Safe: allowlisted commands, validated variables, no shell injection
239
+ base_cmd, cwd=request.working_directory, capture_output=True, text=True, env=env
240
+ )
241
+
242
+ # Prepare the result
243
+ stdout = process.stdout
244
+ stderr = process.stderr if process.stderr else ''
245
+
246
+ # Clean output text if requested
247
+ if request.strip_ansi:
248
+ logger.debug('Cleaning command output text (ANSI codes and control characters)')
249
+ stdout = clean_output_text(stdout)
250
+ stderr = clean_output_text(stderr)
251
+
252
+ # Extract affected directories for run-all command
253
+ affected_dirs = None
254
+ if request.command == 'run-all':
255
+ affected_dirs = []
256
+ # Look for directory paths in the output
257
+ dir_pattern = re.compile(r'Module at\s+"([^"]+)"')
258
+ for match in dir_pattern.finditer(stdout):
259
+ affected_dirs.append(match.group(1))
260
+
261
+ result = {
262
+ 'command': f'terragrunt {request.command}',
263
+ 'status': 'success' if process.returncode == 0 else 'error',
264
+ 'return_code': process.returncode,
265
+ 'stdout': stdout,
266
+ 'stderr': stderr,
267
+ 'working_directory': request.working_directory,
268
+ 'outputs': None,
269
+ 'affected_dirs': affected_dirs,
270
+ }
271
+
272
+ # Get outputs if this was a successful apply or output command
273
+ if (
274
+ request.command in ['apply', 'output'] or (request.command == 'run-all')
275
+ ) and process.returncode == 0:
276
+ try:
277
+ logger.info('Getting Terragrunt outputs')
278
+ output_process = subprocess.run( # noqa: B603 - Safe: hardcoded terragrunt output command with no user input
279
+ ['terragrunt', 'output', '-json'],
280
+ cwd=request.working_directory,
281
+ capture_output=True,
282
+ text=True,
283
+ env=env,
284
+ )
285
+
286
+ if output_process.returncode == 0 and output_process.stdout:
287
+ # Get output and clean it if needed
288
+ output_stdout = output_process.stdout
289
+ if request.strip_ansi:
290
+ output_stdout = clean_output_text(output_stdout)
291
+
292
+ # Parse the JSON output
293
+ raw_outputs = json.loads(output_stdout)
294
+
295
+ # Process outputs to extract values from complex structure
296
+ processed_outputs = {}
297
+ for key, value in raw_outputs.items():
298
+ # Terraform outputs in JSON format have a nested structure
299
+ # with 'value', 'type', and sometimes 'sensitive'
300
+ if isinstance(value, dict) and 'value' in value:
301
+ processed_outputs[key] = value['value']
302
+ else:
303
+ processed_outputs[key] = value
304
+
305
+ result['outputs'] = processed_outputs
306
+ logger.info(f'Extracted {len(processed_outputs)} Terragrunt outputs')
307
+ except Exception as e:
308
+ logger.warning(f'Failed to get Terragrunt outputs: {e}')
309
+
310
+ # Return the output
311
+ return TerragruntExecutionResult(**result)
312
+ except Exception as e:
313
+ return TerragruntExecutionResult(
314
+ command=f'terragrunt {request.command}',
315
+ status='error',
316
+ error_message=str(e),
317
+ working_directory=request.working_directory,
318
+ outputs=None,
319
+ affected_dirs=None,
320
+ )