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,376 @@
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 Checkov scan tools."""
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
+ CheckovScanRequest,
24
+ CheckovScanResult,
25
+ CheckovVulnerability,
26
+ )
27
+ from loguru import logger
28
+ from typing import Any, Dict, List, Tuple
29
+
30
+
31
+ def _clean_output_text(text: str) -> str:
32
+ """Clean output text by removing or replacing problematic Unicode characters.
33
+
34
+ Args:
35
+ text: The text to clean
36
+
37
+ Returns:
38
+ Cleaned text with ASCII-friendly replacements
39
+ """
40
+ if not text:
41
+ return text
42
+
43
+ # First remove ANSI escape sequences (color codes, cursor movement)
44
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
45
+ text = ansi_escape.sub('', text)
46
+
47
+ # Remove C0 and C1 control characters (except common whitespace)
48
+ control_chars = re.compile(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]')
49
+ text = control_chars.sub('', text)
50
+
51
+ # Replace HTML entities
52
+ html_entities = {
53
+ '->': '->', # Replace HTML arrow
54
+ '&lt;': '<', # Less than
55
+ '&gt;': '>', # Greater than
56
+ '&amp;': '&', # Ampersand
57
+ }
58
+ for entity, replacement in html_entities.items():
59
+ text = text.replace(entity, replacement)
60
+
61
+ # Replace box-drawing and other special Unicode characters with ASCII equivalents
62
+ unicode_chars = {
63
+ '\u2500': '-', # Horizontal line
64
+ '\u2502': '|', # Vertical line
65
+ '\u2514': '+', # Up and right
66
+ '\u2518': '+', # Up and left
67
+ '\u2551': '|', # Double vertical
68
+ '\u2550': '-', # Double horizontal
69
+ '\u2554': '+', # Double down and right
70
+ '\u2557': '+', # Double down and left
71
+ '\u255a': '+', # Double up and right
72
+ '\u255d': '+', # Double up and left
73
+ '\u256c': '+', # Double cross
74
+ '\u2588': '#', # Full block
75
+ '\u25cf': '*', # Black circle
76
+ '\u2574': '-', # Left box drawing
77
+ '\u2576': '-', # Right box drawing
78
+ '\u2577': '|', # Down box drawing
79
+ '\u2575': '|', # Up box drawing
80
+ }
81
+ for char, replacement in unicode_chars.items():
82
+ text = text.replace(char, replacement)
83
+
84
+ return text
85
+
86
+
87
+ def _ensure_checkov_installed() -> bool:
88
+ """Ensure Checkov is installed, and install it if not.
89
+
90
+ Returns:
91
+ True if Checkov is installed or was successfully installed, False otherwise
92
+ """
93
+ try:
94
+ # Check if Checkov is already installed
95
+ subprocess.run( # noqa: B603 - Safe: hardcoded command with no user input
96
+ ['checkov', '--version'],
97
+ capture_output=True,
98
+ text=True,
99
+ check=False,
100
+ )
101
+ logger.info('Checkov is already installed')
102
+ return True
103
+ except FileNotFoundError:
104
+ logger.warning('Checkov not found, attempting to install')
105
+ try:
106
+ # Install Checkov using pip
107
+ subprocess.run( # noqa: B603 - Safe: hardcoded pip install command with no user input
108
+ ['pip', 'install', 'checkov'],
109
+ capture_output=True,
110
+ text=True,
111
+ check=True,
112
+ )
113
+ logger.info('Successfully installed Checkov')
114
+ return True
115
+ except subprocess.CalledProcessError as e:
116
+ logger.error(f'Failed to install Checkov: {e}')
117
+ return False
118
+
119
+
120
+ def _parse_checkov_json_output(output: str) -> Tuple[List[CheckovVulnerability], Dict[str, Any]]:
121
+ """Parse Checkov JSON output into structured vulnerability data.
122
+
123
+ Args:
124
+ output: JSON output from Checkov scan
125
+
126
+ Returns:
127
+ Tuple of (list of vulnerabilities, summary dictionary)
128
+ """
129
+ try:
130
+ data = json.loads(output)
131
+ vulnerabilities = []
132
+ summary = {
133
+ 'passed': 0,
134
+ 'failed': 0,
135
+ 'skipped': 0,
136
+ 'parsing_errors': 0,
137
+ 'resource_count': 0,
138
+ }
139
+
140
+ # Extract summary information
141
+ if 'summary' in data:
142
+ summary = data['summary']
143
+
144
+ # Process check results
145
+ if 'results' in data and 'failed_checks' in data['results']:
146
+ for check in data['results']['failed_checks']:
147
+ vuln = CheckovVulnerability(
148
+ id=check.get('check_id', 'UNKNOWN'),
149
+ type=check.get('check_type', 'terraform'),
150
+ resource=check.get('resource', 'UNKNOWN'),
151
+ file_path=check.get('file_path', 'UNKNOWN'),
152
+ line=check.get('file_line_range', [0, 0])[0],
153
+ description=check.get('check_name', 'UNKNOWN'),
154
+ guideline=check.get('guideline', None),
155
+ severity=(check.get('severity', 'MEDIUM') or 'MEDIUM').upper(),
156
+ fixed=False,
157
+ fix_details=None,
158
+ )
159
+ vulnerabilities.append(vuln)
160
+
161
+ return vulnerabilities, summary
162
+ except json.JSONDecodeError as e:
163
+ logger.error(f'Failed to parse Checkov JSON output: {e}')
164
+ return [], {'error': 'Failed to parse JSON output'}
165
+
166
+
167
+ async def run_checkov_scan_impl(request: CheckovScanRequest) -> CheckovScanResult:
168
+ """Run Checkov scan on Terraform code.
169
+
170
+ Args:
171
+ request: Details about the Checkov scan to execute
172
+
173
+ Returns:
174
+ A CheckovScanResult object containing scan results and vulnerabilities
175
+ """
176
+ logger.info(f'Running Checkov scan in {request.working_directory}')
177
+
178
+ # Ensure Checkov is installed
179
+ if not _ensure_checkov_installed():
180
+ return CheckovScanResult(
181
+ status='error',
182
+ working_directory=request.working_directory,
183
+ error_message='Failed to install Checkov. Please install it manually with: pip install checkov',
184
+ vulnerabilities=[],
185
+ summary={},
186
+ raw_output=None,
187
+ )
188
+
189
+ # Security checks for parameters
190
+
191
+ # Check framework parameter for allowed values
192
+ allowed_frameworks = ['terraform', 'cloudformation', 'kubernetes', 'dockerfile', 'arm', 'all']
193
+ if request.framework not in allowed_frameworks:
194
+ logger.error(f'Security violation: Invalid framework: {request.framework}')
195
+ return CheckovScanResult(
196
+ status='error',
197
+ working_directory=request.working_directory,
198
+ error_message=f"Security violation: Invalid framework '{request.framework}'. Allowed frameworks are: {', '.join(allowed_frameworks)}",
199
+ vulnerabilities=[],
200
+ summary={},
201
+ raw_output=None,
202
+ )
203
+
204
+ # Check output_format parameter for allowed values
205
+ allowed_output_formats = [
206
+ 'cli',
207
+ 'csv',
208
+ 'cyclonedx',
209
+ 'cyclonedx_json',
210
+ 'spdx',
211
+ 'json',
212
+ 'junitxml',
213
+ 'github_failed_only',
214
+ 'gitlab_sast',
215
+ 'sarif',
216
+ ]
217
+ if request.output_format not in allowed_output_formats:
218
+ logger.error(f'Security violation: Invalid output format: {request.output_format}')
219
+ return CheckovScanResult(
220
+ status='error',
221
+ working_directory=request.working_directory,
222
+ error_message=f"Security violation: Invalid output format '{request.output_format}'. Allowed formats are: {', '.join(allowed_output_formats)}",
223
+ vulnerabilities=[],
224
+ summary={},
225
+ raw_output=None,
226
+ )
227
+
228
+ # Check for command injection patterns in check_ids and skip_check_ids
229
+ dangerous_patterns = get_dangerous_patterns()
230
+ logger.debug(f'Checking for {len(dangerous_patterns)} dangerous patterns')
231
+
232
+ if request.check_ids:
233
+ for check_id in request.check_ids:
234
+ for pattern in dangerous_patterns:
235
+ if pattern in check_id:
236
+ logger.error(
237
+ f"Security violation: Potentially dangerous pattern '{pattern}' in check_id: {check_id}"
238
+ )
239
+ return CheckovScanResult(
240
+ status='error',
241
+ working_directory=request.working_directory,
242
+ error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in check_id",
243
+ vulnerabilities=[],
244
+ summary={},
245
+ raw_output=None,
246
+ )
247
+
248
+ if request.skip_check_ids:
249
+ for skip_id in request.skip_check_ids:
250
+ for pattern in dangerous_patterns:
251
+ if pattern in skip_id:
252
+ logger.error(
253
+ f"Security violation: Potentially dangerous pattern '{pattern}' in skip_check_id: {skip_id}"
254
+ )
255
+ return CheckovScanResult(
256
+ status='error',
257
+ working_directory=request.working_directory,
258
+ error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in skip_check_id",
259
+ vulnerabilities=[],
260
+ summary={},
261
+ raw_output=None,
262
+ )
263
+
264
+ # Build the command
265
+ # Convert working_directory to absolute path if it's not already
266
+ working_dir = request.working_directory
267
+ if not os.path.isabs(working_dir):
268
+ # Get the current working directory of the MCP server
269
+ current_dir = os.getcwd()
270
+ # Go up to the project root directory (assuming we're in src/terraform-mcp-server/awslabs/terraform_mcp_server)
271
+ project_root = os.path.abspath(os.path.join(current_dir, '..', '..', '..', '..'))
272
+ # Join with the requested working directory
273
+ working_dir = os.path.abspath(os.path.join(project_root, working_dir))
274
+
275
+ logger.info(f'Using absolute working directory: {working_dir}')
276
+ cmd = ['checkov', '--quiet', '-d', working_dir]
277
+
278
+ # Add framework if specified
279
+ if request.framework:
280
+ cmd.extend(['--framework', request.framework])
281
+
282
+ # Add specific check IDs if provided
283
+ if request.check_ids:
284
+ cmd.extend(['--check', ','.join(request.check_ids)])
285
+
286
+ # Add skip check IDs if provided
287
+ if request.skip_check_ids:
288
+ cmd.extend(['--skip-check', ','.join(request.skip_check_ids)])
289
+
290
+ # Set output format
291
+ cmd.extend(['--output', request.output_format])
292
+
293
+ # Execute command
294
+ try:
295
+ logger.info(f'Executing command: {" ".join(cmd)}')
296
+ # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit
297
+ # Safe: All user inputs are validated above - framework/output_format use allowlists,
298
+ # check_ids/skip_check_ids are validated for dangerous patterns, working_dir is path-normalized
299
+ process = subprocess.run( # noqa: B603 - Safe: validated inputs, allowlisted commands, no shell injection
300
+ cmd,
301
+ capture_output=True,
302
+ text=True,
303
+ )
304
+
305
+ # Clean output text
306
+ stdout = _clean_output_text(process.stdout)
307
+ stderr = _clean_output_text(process.stderr)
308
+
309
+ # Debug logging
310
+ logger.info(f'Checkov return code: {process.returncode}')
311
+ logger.info(f'Checkov stdout: {stdout}')
312
+ logger.info(f'Checkov stderr: {stderr}')
313
+
314
+ # Parse results if JSON output was requested
315
+ vulnerabilities = []
316
+ summary = {}
317
+ if request.output_format == 'json' and stdout:
318
+ vulnerabilities, summary = _parse_checkov_json_output(stdout)
319
+
320
+ # For non-JSON output, try to parse vulnerabilities from the text output
321
+ elif stdout and process.returncode == 1: # Return code 1 means vulnerabilities were found
322
+ # Simple regex to extract failed checks from CLI output
323
+ failed_checks = re.findall(
324
+ r'Check: (CKV\w*_\d+).*?FAILED for resource: ([\w\.]+).*?File: ([\w\/\.-]+):(\d+)',
325
+ stdout,
326
+ re.DOTALL,
327
+ )
328
+ for check_id, resource, file_path, line in failed_checks:
329
+ vuln = CheckovVulnerability(
330
+ id=check_id,
331
+ type='terraform',
332
+ resource=resource,
333
+ file_path=file_path,
334
+ line=int(line),
335
+ description=f'Failed check: {check_id}',
336
+ guideline=None,
337
+ severity='MEDIUM',
338
+ fixed=False,
339
+ fix_details=None,
340
+ )
341
+ vulnerabilities.append(vuln)
342
+
343
+ # Extract summary counts
344
+ passed_match = re.search(r'Passed checks: (\d+)', stdout)
345
+ failed_match = re.search(r'Failed checks: (\d+)', stdout)
346
+ skipped_match = re.search(r'Skipped checks: (\d+)', stdout)
347
+
348
+ summary = {
349
+ 'passed': int(passed_match.group(1)) if passed_match else 0,
350
+ 'failed': int(failed_match.group(1)) if failed_match else 0,
351
+ 'skipped': int(skipped_match.group(1)) if skipped_match else 0,
352
+ }
353
+
354
+ # Prepare the result - consider it a success even if vulnerabilities were found
355
+ # A return code of 1 from Checkov means vulnerabilities were found, not an error
356
+ is_error = process.returncode not in [0, 1]
357
+ result = CheckovScanResult(
358
+ status='error' if is_error else 'success',
359
+ return_code=process.returncode,
360
+ working_directory=request.working_directory,
361
+ vulnerabilities=vulnerabilities,
362
+ summary=summary,
363
+ raw_output=stdout,
364
+ )
365
+
366
+ return result
367
+ except Exception as e:
368
+ logger.error(f'Error running Checkov scan: {e}')
369
+ return CheckovScanResult(
370
+ status='error',
371
+ working_directory=request.working_directory,
372
+ error_message=str(e),
373
+ vulnerabilities=[],
374
+ summary={},
375
+ raw_output=None,
376
+ )