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.
- awslabs/__init__.py +17 -0
- awslabs/terraform_mcp_server/__init__.py +17 -0
- awslabs/terraform_mcp_server/impl/resources/__init__.py +25 -0
- awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +66 -0
- awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +69 -0
- awslabs/terraform_mcp_server/impl/tools/__init__.py +33 -0
- awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +223 -0
- awslabs/terraform_mcp_server/impl/tools/execute_terragrunt_command.py +320 -0
- awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +376 -0
- awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +691 -0
- awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +641 -0
- awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +458 -0
- awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +349 -0
- awslabs/terraform_mcp_server/impl/tools/utils.py +572 -0
- awslabs/terraform_mcp_server/models/__init__.py +49 -0
- awslabs/terraform_mcp_server/models/models.py +381 -0
- awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +1240 -0
- awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +1039 -0
- awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +143 -0
- awslabs/terraform_mcp_server/server.py +440 -0
- awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +3125 -0
- awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +3833 -0
- awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +2523 -0
- awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +142 -0
- awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +330 -0
- awslabs/terraform_mcp_server/static/__init__.py +38 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/METADATA +166 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/RECORD +30 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/WHEEL +4 -0
- 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
|
+
'<': '<', # Less than
|
|
55
|
+
'>': '>', # Greater than
|
|
56
|
+
'&': '&', # 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
|
+
)
|