awslabs.terraform-mcp-server 0.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.

Potentially problematic release.


This version of awslabs.terraform-mcp-server might be problematic. Click here for more details.

Files changed (32) hide show
  1. awslabs/__init__.py +2 -0
  2. awslabs/terraform_mcp_server/__init__.py +3 -0
  3. awslabs/terraform_mcp_server/impl/resources/__init__.py +11 -0
  4. awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +52 -0
  5. awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +55 -0
  6. awslabs/terraform_mcp_server/impl/tools/__init__.py +15 -0
  7. awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +206 -0
  8. awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +359 -0
  9. awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +677 -0
  10. awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +627 -0
  11. awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +444 -0
  12. awslabs/terraform_mcp_server/impl/tools/utils.py +558 -0
  13. awslabs/terraform_mcp_server/models/__init__.py +27 -0
  14. awslabs/terraform_mcp_server/models/models.py +260 -0
  15. awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +1224 -0
  16. awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +1020 -0
  17. awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +129 -0
  18. awslabs/terraform_mcp_server/server.py +329 -0
  19. awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +3125 -0
  20. awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +3833 -0
  21. awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +2523 -0
  22. awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +126 -0
  23. awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +198 -0
  24. awslabs/terraform_mcp_server/static/__init__.py +22 -0
  25. awslabs/terraform_mcp_server/tests/__init__.py +1 -0
  26. awslabs/terraform_mcp_server/tests/run_tests.sh +35 -0
  27. awslabs/terraform_mcp_server/tests/test_parameter_annotations.py +207 -0
  28. awslabs/terraform_mcp_server/tests/test_tool_implementations.py +309 -0
  29. awslabs_terraform_mcp_server-0.0.1.dist-info/METADATA +97 -0
  30. awslabs_terraform_mcp_server-0.0.1.dist-info/RECORD +32 -0
  31. awslabs_terraform_mcp_server-0.0.1.dist-info/WHEEL +4 -0
  32. awslabs_terraform_mcp_server-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,558 @@
1
+ """Utility functions for Terraform MCP server tools."""
2
+
3
+ import asyncio
4
+ import re
5
+ import requests
6
+ import time
7
+ import traceback
8
+ from awslabs.terraform_mcp_server.models import SubmoduleInfo, TerraformVariable
9
+ from loguru import logger
10
+ from typing import Any, Dict, List, Optional, Tuple
11
+
12
+
13
+ def clean_description(description: str) -> str:
14
+ """Remove emoji characters from description strings.
15
+
16
+ Args:
17
+ description: The module description text
18
+
19
+ Returns:
20
+ Cleaned description without emojis
21
+ """
22
+ # This regex pattern targets common emoji Unicode ranges
23
+ emoji_pattern = re.compile(
24
+ '['
25
+ '\U0001f1e0-\U0001f1ff' # flags (iOS)
26
+ '\U0001f300-\U0001f5ff' # symbols & pictographs
27
+ '\U0001f600-\U0001f64f' # emoticons
28
+ '\U0001f680-\U0001f6ff' # transport & map symbols
29
+ '\U0001f700-\U0001f77f' # alchemical symbols
30
+ '\U0001f780-\U0001f7ff' # Geometric Shapes
31
+ '\U0001f800-\U0001f8ff' # Supplemental Arrows-C
32
+ '\U0001f900-\U0001f9ff' # Supplemental Symbols and Pictographs
33
+ '\U0001fa00-\U0001fa6f' # Chess Symbols
34
+ '\U0001fa70-\U0001faff' # Symbols and Pictographs Extended-A
35
+ '\U00002702-\U000027b0' # Dingbats
36
+ ']+',
37
+ flags=re.UNICODE,
38
+ )
39
+
40
+ # Clean the description
41
+ return emoji_pattern.sub(r'', description).strip()
42
+
43
+
44
+ async def get_github_release_details(owner: str, repo: str) -> Dict[str, Any]:
45
+ """Fetch detailed release information from GitHub API.
46
+
47
+ Args:
48
+ owner: The GitHub repository owner
49
+ repo: The GitHub repository name
50
+
51
+ Returns:
52
+ Dictionary containing version details and cleaned version string
53
+ """
54
+ logger.info(f'Fetching GitHub release details for {owner}/{repo}')
55
+
56
+ # Try to get the latest release first
57
+ release_url = f'https://api.github.com/repos/{owner}/{repo}/releases/latest'
58
+ logger.debug(f'Making request to GitHub releases API: {release_url}')
59
+
60
+ try:
61
+ response = requests.get(release_url)
62
+ logger.debug(f'GitHub releases API response code: {response.status_code}')
63
+
64
+ if response.status_code == 200:
65
+ release_data = response.json()
66
+ logger.info(f'Found latest GitHub release: {release_data.get("tag_name")}')
67
+
68
+ # Extract just the requested fields (tag name and publish date)
69
+ version_details = {
70
+ 'tag_name': release_data.get('tag_name'),
71
+ 'published_at': release_data.get('published_at'),
72
+ }
73
+
74
+ # Use clean version for the module result
75
+ clean_version = release_data.get('tag_name', '')
76
+ if clean_version.startswith('v'):
77
+ clean_version = clean_version[1:]
78
+
79
+ logger.debug(f'Extracted version: {clean_version}')
80
+
81
+ return {'details': version_details, 'version': clean_version}
82
+ except Exception as ex:
83
+ logger.error(f'Error fetching GitHub release details: {ex}')
84
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
85
+
86
+ # Fallback to tags if no releases found
87
+ tags_url = f'https://api.github.com/repos/{owner}/{repo}/tags'
88
+ logger.debug(f'No releases found, trying tags: {tags_url}')
89
+
90
+ try:
91
+ response = requests.get(tags_url)
92
+ logger.debug(f'GitHub tags API response code: {response.status_code}')
93
+
94
+ if response.status_code == 200 and response.json():
95
+ tags_data = response.json()
96
+ if tags_data:
97
+ latest_tag = tags_data[0] # Tags are typically sorted newest first
98
+ logger.info(f'Found latest GitHub tag: {latest_tag.get("name")}')
99
+
100
+ version_details = {
101
+ 'tag_name': latest_tag.get('name'),
102
+ 'published_at': None, # Tags don't have publish dates in GitHub API
103
+ }
104
+
105
+ # Use clean version for the module result
106
+ clean_version = latest_tag.get('name', '')
107
+ if clean_version.startswith('v'):
108
+ clean_version = clean_version[1:]
109
+
110
+ logger.debug(f'Extracted version from tag: {clean_version}')
111
+
112
+ return {'details': version_details, 'version': clean_version}
113
+ except Exception as ex:
114
+ logger.error(f'Error fetching GitHub tags: {ex}')
115
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
116
+
117
+ # Return empty details if nothing was found
118
+ logger.warning('No GitHub release or tag information found')
119
+ return {'details': {}, 'version': ''}
120
+
121
+
122
+ async def get_submodules(owner: str, repo: str, branch: str = 'master') -> List[SubmoduleInfo]:
123
+ """Fetch submodules from a module's GitHub repository.
124
+
125
+ Args:
126
+ owner: GitHub repository owner
127
+ repo: GitHub repository name
128
+ branch: Branch name (default: master)
129
+
130
+ Returns:
131
+ List of SubmoduleInfo objects
132
+ """
133
+ logger.info(f'Checking for submodules in {owner}/{repo} ({branch} branch)')
134
+ submodules = []
135
+
136
+ # Check if modules directory exists
137
+ modules_url = f'https://api.github.com/repos/{owner}/{repo}/contents/modules?ref={branch}'
138
+ logger.debug(f'Checking for modules directory: {modules_url}')
139
+
140
+ try:
141
+ # Get list of directories in /modules
142
+ start_time = time.time()
143
+ response = requests.get(
144
+ modules_url,
145
+ headers={'Accept': 'application/vnd.github.v3+json'},
146
+ timeout=3.0, # Add timeout
147
+ )
148
+ logger.debug(f'GitHub API request took {time.time() - start_time:.2f} seconds')
149
+
150
+ if response.status_code == 404:
151
+ logger.debug(f'No modules directory found in {branch} branch')
152
+ return []
153
+
154
+ if response.status_code == 403:
155
+ logger.warning(f'GitHub API rate limit reached, status: {response.status_code}')
156
+ # Return empty list but don't fail completely
157
+ return []
158
+
159
+ if response.status_code != 200:
160
+ logger.warning(f'Failed to get modules directory: status {response.status_code}')
161
+ return []
162
+
163
+ modules_list = response.json()
164
+ if not isinstance(modules_list, list):
165
+ logger.warning('Unexpected API response format for modules listing')
166
+ return []
167
+
168
+ # Filter for directories only
169
+ submodule_dirs = [item for item in modules_list if item.get('type') == 'dir']
170
+ logger.info(f'Found {len(submodule_dirs)} potential submodules')
171
+
172
+ # Process submodules with concurrency limits
173
+ # Only process up to 5 submodules to avoid timeouts
174
+ max_submodules = min(len(submodule_dirs), 5)
175
+ logger.info(f'Processing {max_submodules} out of {len(submodule_dirs)} submodules')
176
+
177
+ # Process each submodule
178
+ for i, submodule in enumerate(submodule_dirs[:max_submodules]):
179
+ name = submodule.get('name')
180
+ path = submodule.get('path', f'modules/{name}')
181
+
182
+ # Create basic submodule info
183
+ submodule_info = SubmoduleInfo(
184
+ name=name,
185
+ path=path,
186
+ )
187
+
188
+ # Add a slight delay between API requests to avoid rate limiting
189
+ if i > 0:
190
+ await asyncio.sleep(0.2) # 200ms delay between requests
191
+
192
+ # Try to get README content
193
+ readme_url = (
194
+ f'https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}/README.md'
195
+ )
196
+ logger.debug(f'Fetching README for submodule {name}: {readme_url}')
197
+
198
+ try:
199
+ start_time = time.time()
200
+ readme_response = requests.get(readme_url, timeout=2.0) # Add timeout
201
+ logger.debug(f'README fetch took {time.time() - start_time:.2f} seconds')
202
+
203
+ if readme_response.status_code == 200:
204
+ readme_content = readme_response.text
205
+ # Truncate if too long
206
+ if len(readme_content) > 8000:
207
+ readme_content = (
208
+ readme_content[:8000] + '...\n[README truncated due to length]'
209
+ )
210
+
211
+ # Extract description from first paragraph if available
212
+ description = extract_description_from_readme(readme_content)
213
+ if description:
214
+ submodule_info.description = description
215
+
216
+ submodule_info.readme_content = readme_content
217
+ logger.debug(
218
+ f'Found README for submodule {name} ({len(readme_content)} chars)'
219
+ )
220
+ else:
221
+ logger.debug(
222
+ f'No README found for submodule {name}, status: {readme_response.status_code}'
223
+ )
224
+ # Try lowercase readme.md as fallback
225
+ lowercase_readme_url = f'https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}/readme.md'
226
+ logger.debug(f'Trying lowercase readme.md: {lowercase_readme_url}')
227
+
228
+ lowercase_response = requests.get(lowercase_readme_url, timeout=2.0)
229
+ if lowercase_response.status_code == 200:
230
+ readme_content = lowercase_response.text
231
+ if len(readme_content) > 8000:
232
+ readme_content = (
233
+ readme_content[:8000] + '...\n[README truncated due to length]'
234
+ )
235
+
236
+ description = extract_description_from_readme(readme_content)
237
+ if description:
238
+ submodule_info.description = description
239
+
240
+ submodule_info.readme_content = readme_content
241
+ logger.debug(
242
+ f'Found lowercase readme.md for {name} ({len(readme_content)} chars)'
243
+ )
244
+ except Exception as ex:
245
+ logger.error(f'Error fetching README for submodule {name}: {ex}')
246
+
247
+ # Add the submodule to our result list
248
+ submodules.append(submodule_info)
249
+
250
+ if len(submodule_dirs) > max_submodules:
251
+ logger.warning(
252
+ f'Only processed {max_submodules} out of {len(submodule_dirs)} submodules to avoid timeouts'
253
+ )
254
+
255
+ return submodules
256
+
257
+ except Exception as e:
258
+ logger.error(f'Error fetching submodules: {e}')
259
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
260
+ return []
261
+
262
+
263
+ def extract_description_from_readme(readme_content: str) -> Optional[str]:
264
+ """Extract a short description from the README content.
265
+
266
+ Args:
267
+ readme_content: The README markdown content
268
+
269
+ Returns:
270
+ Short description or None if not found
271
+ """
272
+ if not readme_content:
273
+ return None
274
+
275
+ # Try to find the first paragraph after any headings
276
+ lines = readme_content.split('\n')
277
+ paragraph_text = []
278
+
279
+ for line in lines:
280
+ # Skip headings, horizontal rules and blank lines
281
+ if line.startswith('#') or line.startswith('---') or not line.strip():
282
+ # If we already found a paragraph, return it
283
+ if paragraph_text:
284
+ break
285
+ continue
286
+
287
+ # Found text content, add to paragraph
288
+ paragraph_text.append(line)
289
+
290
+ # If this line ends a paragraph, break
291
+ if not line.endswith('\\') and len(paragraph_text) > 0:
292
+ break
293
+
294
+ if paragraph_text:
295
+ description = ' '.join(paragraph_text).strip()
296
+ # Limit to 200 chars max
297
+ if len(description) > 200:
298
+ description = description[:197] + '...'
299
+ return description
300
+
301
+ return None
302
+
303
+
304
+ def extract_outputs_from_readme(readme_content: str) -> List[Dict[str, str]]:
305
+ """Extract module outputs from the README content.
306
+
307
+ Looks for the Outputs section in the README, which is typically at the bottom
308
+ of the file and contains a table of outputs with descriptions.
309
+
310
+ Args:
311
+ readme_content: The README markdown content
312
+
313
+ Returns:
314
+ List of dictionaries containing output name and description
315
+ """
316
+ if not readme_content:
317
+ return []
318
+
319
+ outputs = []
320
+
321
+ # Find the Outputs section
322
+ lines = readme_content.split('\n')
323
+ in_outputs_section = False
324
+ in_outputs_table = False
325
+
326
+ for i, line in enumerate(lines):
327
+ # Look for Outputs heading
328
+ if re.match(r'^#+\s+Outputs?$', line, re.IGNORECASE):
329
+ in_outputs_section = True
330
+ continue
331
+
332
+ # If we're in the outputs section, look for the table header
333
+ if in_outputs_section and not in_outputs_table:
334
+ if '|' in line and ('Name' in line or 'Output' in line) and 'Description' in line:
335
+ in_outputs_table = True
336
+ continue
337
+
338
+ # If we're in the outputs table, parse each row
339
+ if in_outputs_section and in_outputs_table:
340
+ # Skip the table header separator line
341
+ if line.strip().startswith('|') and all(c in '|-: ' for c in line):
342
+ continue
343
+
344
+ # If we hit another heading or the table ends, stop parsing
345
+ if line.strip().startswith('#') or not line.strip() or '|' not in line:
346
+ break
347
+
348
+ # Parse the table row
349
+ if '|' in line:
350
+ parts = [part.strip() for part in line.split('|')]
351
+ if len(parts) >= 3: # Should have at least empty, name, description columns
352
+ name_part = parts[1].strip()
353
+ desc_part = parts[2].strip()
354
+
355
+ # Clean up any markdown formatting
356
+ name = re.sub(r'`(.*?)`', r'\1', name_part).strip()
357
+ description = re.sub(r'`(.*?)`', r'\1', desc_part).strip()
358
+
359
+ if name:
360
+ outputs.append({'name': name, 'description': description})
361
+
362
+ # If we didn't find a table, try looking for a list format
363
+ if not outputs and in_outputs_section:
364
+ for line in lines:
365
+ # If we hit another heading, stop parsing
366
+ if line.strip().startswith('#'):
367
+ break
368
+
369
+ # Look for list items that might be outputs
370
+ list_match = re.match(r'^[-*]\s+`([^`]+)`\s*[-:]\s*(.+)$', line)
371
+ if list_match:
372
+ name = list_match.group(1).strip()
373
+ description = list_match.group(2).strip()
374
+
375
+ outputs.append({'name': name, 'description': description})
376
+
377
+ logger.debug(f'Extracted {len(outputs)} outputs from README')
378
+ return outputs
379
+
380
+
381
+ async def get_variables_tf(
382
+ owner: str, repo: str, branch: str = 'main'
383
+ ) -> Tuple[Optional[str], Optional[List[TerraformVariable]]]:
384
+ """Fetch and parse the variables.tf file from a GitHub repository.
385
+
386
+ Args:
387
+ owner: GitHub repository owner
388
+ repo: GitHub repository name
389
+ branch: Branch name (default: main)
390
+
391
+ Returns:
392
+ Tuple containing the raw variables.tf content and a list of parsed TerraformVariable objects
393
+ """
394
+ logger.info(f'Fetching variables.tf from {owner}/{repo} ({branch} branch)')
395
+
396
+ # Try to get the variables.tf file
397
+ variables_url = f'https://raw.githubusercontent.com/{owner}/{repo}/{branch}/variables.tf'
398
+ logger.debug(f'Fetching variables.tf: {variables_url}')
399
+
400
+ try:
401
+ start_time = time.time()
402
+ response = requests.get(variables_url, timeout=3.0)
403
+ logger.debug(f'variables.tf fetch took {time.time() - start_time:.2f} seconds')
404
+
405
+ if response.status_code == 200:
406
+ variables_content = response.text
407
+ logger.info(f'Found variables.tf ({len(variables_content)} chars)')
408
+
409
+ # Parse the variables.tf file
410
+ variables = parse_variables_tf(variables_content)
411
+ logger.info(f'Parsed {len(variables)} variables from variables.tf')
412
+
413
+ return variables_content, variables
414
+ else:
415
+ logger.debug(
416
+ f'No variables.tf found at {branch} branch, status: {response.status_code}'
417
+ )
418
+
419
+ # Try master branch as fallback
420
+ if branch != 'master':
421
+ logger.debug('Trying master branch for variables.tf')
422
+ master_variables_url = (
423
+ f'https://raw.githubusercontent.com/{owner}/{repo}/master/variables.tf'
424
+ )
425
+ master_response = requests.get(master_variables_url, timeout=3.0)
426
+
427
+ if master_response.status_code == 200:
428
+ variables_content = master_response.text
429
+ logger.info(
430
+ f'Found variables.tf in master branch ({len(variables_content)} chars)'
431
+ )
432
+
433
+ # Parse the variables.tf file
434
+ variables = parse_variables_tf(variables_content)
435
+ logger.info(f'Parsed {len(variables)} variables from variables.tf')
436
+
437
+ return variables_content, variables
438
+ except Exception as ex:
439
+ logger.error(f'Error fetching variables.tf: {ex}')
440
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
441
+
442
+ return None, None
443
+
444
+
445
+ def parse_variables_tf(content: str) -> List[TerraformVariable]:
446
+ """Parse variables.tf content to extract variable definitions.
447
+
448
+ Args:
449
+ content: The content of the variables.tf file
450
+
451
+ Returns:
452
+ List of TerraformVariable objects
453
+ """
454
+ if not content:
455
+ return []
456
+
457
+ variables = []
458
+
459
+ # Simple regex pattern to match variable blocks
460
+ # This is a simplified approach and may not handle all complex HCL syntax
461
+ variable_blocks = re.finditer(r'variable\s+"([^"]+)"\s*{([^}]+)}', content, re.DOTALL)
462
+
463
+ for match in variable_blocks:
464
+ var_name = match.group(1)
465
+ var_block = match.group(2)
466
+
467
+ # Initialize variable with name
468
+ variable = TerraformVariable(name=var_name)
469
+
470
+ # Extract type
471
+ type_match = re.search(r'type\s*=\s*(.+?)($|\n)', var_block)
472
+ if type_match:
473
+ variable.type = type_match.group(1).strip()
474
+
475
+ # Extract description
476
+ desc_match = re.search(r'description\s*=\s*"([^"]+)"', var_block)
477
+ if desc_match:
478
+ variable.description = desc_match.group(1).strip()
479
+
480
+ # Check for default value
481
+ default_match = re.search(r'default\s*=\s*(.+?)($|\n)', var_block)
482
+ if default_match:
483
+ default_value = default_match.group(1).strip()
484
+ variable.default = default_value
485
+ variable.required = False
486
+
487
+ variables.append(variable)
488
+
489
+ return variables
490
+
491
+
492
+ # Security-related constants and utilities
493
+ # These are used to prevent command injection and other security issues
494
+
495
+
496
+ def get_dangerous_patterns() -> List[str]:
497
+ """Get a list of dangerous patterns for command injection detection.
498
+
499
+ Returns:
500
+ List of dangerous patterns to check for
501
+ """
502
+ # Dangerous patterns that could indicate command injection attempts
503
+ # Separated by platform for better organization and maintainability
504
+ patterns = [
505
+ '|',
506
+ ';',
507
+ '&',
508
+ '&&',
509
+ '||', # Command chaining
510
+ '>',
511
+ '>>',
512
+ '<', # Redirection
513
+ '`',
514
+ '$(', # Command substitution
515
+ '--', # Double dash options
516
+ 'rm',
517
+ 'mv',
518
+ 'cp', # Potentially dangerous commands
519
+ '/bin/',
520
+ '/usr/bin/', # Path references
521
+ '../',
522
+ './', # Directory traversal
523
+ # Unix/Linux specific dangerous patterns
524
+ 'sudo', # Privilege escalation
525
+ 'chmod',
526
+ 'chown', # File permission changes
527
+ 'su', # Switch user
528
+ 'bash',
529
+ 'sh',
530
+ 'zsh', # Shell execution
531
+ 'curl',
532
+ 'wget', # Network access
533
+ 'ssh',
534
+ 'scp', # Remote access
535
+ 'eval', # Command evaluation
536
+ 'exec', # Command execution
537
+ 'source', # Script sourcing
538
+ # Windows specific dangerous patterns
539
+ 'cmd',
540
+ 'powershell',
541
+ 'pwsh', # Command shells
542
+ 'net', # Network commands
543
+ 'reg', # Registry access
544
+ 'runas', # Privilege escalation
545
+ 'del',
546
+ 'rmdir', # File deletion
547
+ 'start', # Process execution
548
+ 'taskkill', # Process termination
549
+ 'sc', # Service control
550
+ 'schtasks', # Scheduled tasks
551
+ 'wmic', # WMI commands
552
+ '%SYSTEMROOT%',
553
+ '%WINDIR%', # System directories
554
+ '.bat',
555
+ '.cmd',
556
+ '.ps1', # Script files
557
+ ]
558
+ return patterns
@@ -0,0 +1,27 @@
1
+ from .models import (
2
+ ModuleSearchResult,
3
+ TerraformAWSProviderDocsResult,
4
+ TerraformAWSCCProviderDocsResult,
5
+ SubmoduleInfo,
6
+ TerraformExecutionRequest,
7
+ TerraformExecutionResult,
8
+ CheckovVulnerability,
9
+ CheckovScanRequest,
10
+ CheckovScanResult,
11
+ TerraformVariable,
12
+ TerraformOutput,
13
+ )
14
+
15
+ __all__ = [
16
+ 'ModuleSearchResult',
17
+ 'TerraformAWSProviderDocsResult',
18
+ 'TerraformAWSCCProviderDocsResult',
19
+ 'SubmoduleInfo',
20
+ 'TerraformExecutionRequest',
21
+ 'TerraformExecutionResult',
22
+ 'CheckovVulnerability',
23
+ 'CheckovScanRequest',
24
+ 'CheckovScanResult',
25
+ 'TerraformVariable',
26
+ 'TerraformOutput',
27
+ ]