awslabs.ccapi-mcp-server 1.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.ccapi-mcp-server might be problematic. Click here for more details.
- awslabs/__init__.py +16 -0
- awslabs/ccapi_mcp_server/__init__.py +17 -0
- awslabs/ccapi_mcp_server/aws_client.py +62 -0
- awslabs/ccapi_mcp_server/cloud_control_utils.py +120 -0
- awslabs/ccapi_mcp_server/context.py +37 -0
- awslabs/ccapi_mcp_server/errors.py +67 -0
- awslabs/ccapi_mcp_server/iac_generator.py +203 -0
- awslabs/ccapi_mcp_server/impl/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/tools/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/tools/explanation.py +325 -0
- awslabs/ccapi_mcp_server/impl/tools/infrastructure_generation.py +70 -0
- awslabs/ccapi_mcp_server/impl/tools/resource_operations.py +367 -0
- awslabs/ccapi_mcp_server/impl/tools/security_scanning.py +223 -0
- awslabs/ccapi_mcp_server/impl/tools/session_management.py +221 -0
- awslabs/ccapi_mcp_server/impl/utils/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/utils/validation.py +64 -0
- awslabs/ccapi_mcp_server/infrastructure_generator.py +160 -0
- awslabs/ccapi_mcp_server/models/__init__.py +13 -0
- awslabs/ccapi_mcp_server/models/models.py +118 -0
- awslabs/ccapi_mcp_server/schema_manager.py +219 -0
- awslabs/ccapi_mcp_server/server.py +733 -0
- awslabs/ccapi_mcp_server/static/__init__.py +13 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/METADATA +656 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/RECORD +28 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/WHEEL +4 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/entry_points.txt +2 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/LICENSE +175 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,367 @@
|
|
|
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
|
+
"""Resource operations implementation for CCAPI MCP server."""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from awslabs.ccapi_mcp_server.aws_client import get_aws_client
|
|
19
|
+
from awslabs.ccapi_mcp_server.cloud_control_utils import progress_event, validate_patch
|
|
20
|
+
from awslabs.ccapi_mcp_server.context import Context
|
|
21
|
+
from awslabs.ccapi_mcp_server.errors import ClientError, handle_aws_api_error
|
|
22
|
+
from awslabs.ccapi_mcp_server.impl.utils.validation import (
|
|
23
|
+
cleanup_workflow_tokens,
|
|
24
|
+
ensure_region_string,
|
|
25
|
+
validate_identifier,
|
|
26
|
+
validate_resource_type,
|
|
27
|
+
validate_workflow_token,
|
|
28
|
+
)
|
|
29
|
+
from awslabs.ccapi_mcp_server.models.models import (
|
|
30
|
+
CreateResourceRequest,
|
|
31
|
+
DeleteResourceRequest,
|
|
32
|
+
GetResourceRequest,
|
|
33
|
+
UpdateResourceRequest,
|
|
34
|
+
)
|
|
35
|
+
from os import environ
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_readonly_mode(aws_session_data: dict) -> None:
|
|
39
|
+
"""Check if server is in read-only mode and raise error if so."""
|
|
40
|
+
if Context.readonly_mode() or aws_session_data.get('readonly_mode', False):
|
|
41
|
+
raise ClientError('Server is in read-only mode')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_security_scanning() -> tuple[bool, str | None]:
|
|
45
|
+
"""Check if security scanning is enabled and return warning if disabled."""
|
|
46
|
+
security_scanning_enabled = environ.get('SECURITY_SCANNING', 'enabled').lower() == 'enabled'
|
|
47
|
+
security_warning = None
|
|
48
|
+
|
|
49
|
+
if not security_scanning_enabled:
|
|
50
|
+
security_warning = '⚠️ SECURITY SCANNING IS DISABLED. This MCP server is configured with SECURITY_SCANNING=disabled, which means resources will be created/updated WITHOUT automated security validation. For security best practices, consider enabling SECURITY_SCANNING in your MCP configuration or ensure other security scanning tools are in place.'
|
|
51
|
+
|
|
52
|
+
return security_scanning_enabled, security_warning
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_token_chain(
|
|
56
|
+
explained_token: str, security_scan_token: str, workflow_store: dict
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Validate that tokens are from the same workflow chain."""
|
|
59
|
+
if not explained_token or explained_token not in workflow_store:
|
|
60
|
+
raise ClientError('Invalid explained_token')
|
|
61
|
+
|
|
62
|
+
if not security_scan_token or security_scan_token not in workflow_store:
|
|
63
|
+
raise ClientError('Invalid security_scan_token')
|
|
64
|
+
|
|
65
|
+
# Security scan token must be created after explain token in same workflow
|
|
66
|
+
explained_data = workflow_store[explained_token]
|
|
67
|
+
security_data = workflow_store[security_scan_token]
|
|
68
|
+
|
|
69
|
+
# For now, just ensure both tokens exist and are valid types
|
|
70
|
+
if explained_data.get('type') != 'explained_properties':
|
|
71
|
+
raise ClientError('Invalid explained_token type')
|
|
72
|
+
|
|
73
|
+
if security_data.get('type') != 'security_scan':
|
|
74
|
+
raise ClientError('Invalid security_scan_token type')
|
|
75
|
+
|
|
76
|
+
# Set the parent relationship (security scan derives from explained token)
|
|
77
|
+
workflow_store[security_scan_token]['parent_token'] = explained_token
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def create_resource_impl(request: CreateResourceRequest, workflow_store: dict) -> dict:
|
|
81
|
+
"""Create an AWS resource implementation."""
|
|
82
|
+
validate_resource_type(request.resource_type)
|
|
83
|
+
|
|
84
|
+
# Check if security scanning is enabled
|
|
85
|
+
security_scanning_enabled, security_warning = check_security_scanning()
|
|
86
|
+
|
|
87
|
+
# Validate security scan token if security scanning is enabled
|
|
88
|
+
if security_scanning_enabled:
|
|
89
|
+
if not request.security_scan_token:
|
|
90
|
+
raise ClientError(
|
|
91
|
+
'Security scanning is enabled but no security_scan_token provided: run run_checkov() first and get user approval via approve_security_findings()'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Validate token chain
|
|
95
|
+
_validate_token_chain(request.explained_token, request.security_scan_token, workflow_store)
|
|
96
|
+
elif not security_scanning_enabled and not request.skip_security_check:
|
|
97
|
+
raise ClientError(
|
|
98
|
+
'Security scanning is disabled. You must set skip_security_check=True to proceed without security validation.'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Validate credentials token
|
|
102
|
+
cred_data = validate_workflow_token(request.credentials_token, 'credentials', workflow_store)
|
|
103
|
+
aws_session_data = cred_data['data']
|
|
104
|
+
if not aws_session_data.get('credentials_valid'):
|
|
105
|
+
raise ClientError('Invalid AWS credentials')
|
|
106
|
+
|
|
107
|
+
# Read-only mode check
|
|
108
|
+
check_readonly_mode(aws_session_data)
|
|
109
|
+
|
|
110
|
+
# CRITICAL SECURITY: Get properties from validated explained token only
|
|
111
|
+
workflow_data = validate_workflow_token(
|
|
112
|
+
request.explained_token, 'explained_properties', workflow_store
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Use ONLY the properties that were explained - no manual override possible
|
|
116
|
+
properties = workflow_data['data']['properties']
|
|
117
|
+
|
|
118
|
+
# Ensure region is a string, not a FieldInfo object
|
|
119
|
+
region_str = ensure_region_string(request.region) or 'us-east-1'
|
|
120
|
+
cloudcontrol_client = get_aws_client('cloudcontrol', region_str)
|
|
121
|
+
try:
|
|
122
|
+
response = cloudcontrol_client.create_resource(
|
|
123
|
+
TypeName=request.resource_type, DesiredState=json.dumps(properties)
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
raise handle_aws_api_error(e)
|
|
127
|
+
|
|
128
|
+
# Clean up consumed tokens after successful operation
|
|
129
|
+
cleanup_workflow_tokens(
|
|
130
|
+
workflow_store,
|
|
131
|
+
request.explained_token,
|
|
132
|
+
request.credentials_token,
|
|
133
|
+
request.security_scan_token or '',
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
result = progress_event(response['ProgressEvent'], None)
|
|
137
|
+
if security_warning:
|
|
138
|
+
result['security_warning'] = security_warning
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def update_resource_impl(request: UpdateResourceRequest, workflow_store: dict) -> dict:
|
|
143
|
+
"""Update an AWS resource implementation."""
|
|
144
|
+
validate_resource_type(request.resource_type)
|
|
145
|
+
validate_identifier(request.identifier)
|
|
146
|
+
|
|
147
|
+
if not request.patch_document:
|
|
148
|
+
raise ClientError('Please provide a patch document for the update')
|
|
149
|
+
|
|
150
|
+
# Validate credentials token
|
|
151
|
+
cred_data = validate_workflow_token(request.credentials_token, 'credentials', workflow_store)
|
|
152
|
+
aws_session_data = cred_data['data']
|
|
153
|
+
if not aws_session_data.get('credentials_valid'):
|
|
154
|
+
raise ClientError('Invalid AWS credentials')
|
|
155
|
+
|
|
156
|
+
# Check read-only mode
|
|
157
|
+
try:
|
|
158
|
+
check_readonly_mode(aws_session_data)
|
|
159
|
+
except ClientError:
|
|
160
|
+
raise ClientError(
|
|
161
|
+
'You have configured this tool in readonly mode. To make this change you will have to update your configuration.'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Check if security scanning is enabled
|
|
165
|
+
security_scanning_enabled, security_warning = check_security_scanning()
|
|
166
|
+
|
|
167
|
+
# Validate security scan token if security scanning is enabled
|
|
168
|
+
if security_scanning_enabled and not request.security_scan_token:
|
|
169
|
+
raise ClientError('Security scan token required (run run_checkov() first)')
|
|
170
|
+
|
|
171
|
+
# CRITICAL SECURITY: Validate explained token (already validated in token chain if security enabled)
|
|
172
|
+
if not security_scanning_enabled or request.skip_security_check:
|
|
173
|
+
validate_workflow_token(request.explained_token, 'explained_properties', workflow_store)
|
|
174
|
+
else:
|
|
175
|
+
# Token already validated in chain
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
validate_patch(request.patch_document)
|
|
179
|
+
# Ensure region is a string, not a FieldInfo object
|
|
180
|
+
region_str = ensure_region_string(request.region) or 'us-east-1'
|
|
181
|
+
cloudcontrol_client = get_aws_client('cloudcontrol', region_str)
|
|
182
|
+
|
|
183
|
+
# Convert patch document to JSON string for the API
|
|
184
|
+
patch_document_str = json.dumps(request.patch_document)
|
|
185
|
+
|
|
186
|
+
# Update the resource
|
|
187
|
+
try:
|
|
188
|
+
response = cloudcontrol_client.update_resource(
|
|
189
|
+
TypeName=request.resource_type,
|
|
190
|
+
Identifier=request.identifier,
|
|
191
|
+
PatchDocument=patch_document_str,
|
|
192
|
+
)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
raise handle_aws_api_error(e)
|
|
195
|
+
|
|
196
|
+
# Clean up consumed tokens after successful operation
|
|
197
|
+
cleanup_workflow_tokens(
|
|
198
|
+
workflow_store,
|
|
199
|
+
request.explained_token,
|
|
200
|
+
request.credentials_token,
|
|
201
|
+
request.security_scan_token or '',
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
result = progress_event(response['ProgressEvent'], None)
|
|
205
|
+
if security_warning:
|
|
206
|
+
result['security_warning'] = security_warning
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def delete_resource_impl(request: DeleteResourceRequest, workflow_store: dict) -> dict:
|
|
211
|
+
"""Delete an AWS resource implementation."""
|
|
212
|
+
validate_resource_type(request.resource_type)
|
|
213
|
+
validate_identifier(request.identifier)
|
|
214
|
+
|
|
215
|
+
if not request.confirmed:
|
|
216
|
+
raise ClientError(
|
|
217
|
+
'Please confirm the deletion by setting confirmed=True to proceed with resource deletion.'
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# CRITICAL SECURITY: Validate explained token to ensure deletion was explained
|
|
221
|
+
workflow_data = validate_workflow_token(
|
|
222
|
+
request.explained_token, 'explained_delete', workflow_store
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if workflow_data.get('operation') != 'delete':
|
|
226
|
+
raise ClientError('Invalid explained token: token was not generated for delete operation')
|
|
227
|
+
|
|
228
|
+
# Validate credentials token
|
|
229
|
+
cred_data = validate_workflow_token(request.credentials_token, 'credentials', workflow_store)
|
|
230
|
+
aws_session_data = cred_data['data']
|
|
231
|
+
if not aws_session_data.get('credentials_valid'):
|
|
232
|
+
raise ClientError('Invalid AWS credentials')
|
|
233
|
+
|
|
234
|
+
# Check read-only mode
|
|
235
|
+
try:
|
|
236
|
+
check_readonly_mode(aws_session_data)
|
|
237
|
+
except ClientError:
|
|
238
|
+
raise ClientError(
|
|
239
|
+
'You have configured this tool in readonly mode. To make this change you will have to update your configuration.'
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
cloudcontrol_client = get_aws_client('cloudcontrol', request.region or 'us-east-1')
|
|
243
|
+
try:
|
|
244
|
+
response = cloudcontrol_client.delete_resource(
|
|
245
|
+
TypeName=request.resource_type, Identifier=request.identifier
|
|
246
|
+
)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
raise handle_aws_api_error(e)
|
|
249
|
+
|
|
250
|
+
# Clean up consumed tokens after successful operation
|
|
251
|
+
cleanup_workflow_tokens(workflow_store, request.explained_token, request.credentials_token)
|
|
252
|
+
|
|
253
|
+
return progress_event(response['ProgressEvent'], None)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def get_resource_impl(
|
|
257
|
+
request: GetResourceRequest, workflow_store: dict | None = None
|
|
258
|
+
) -> dict:
|
|
259
|
+
"""Get details of a specific AWS resource implementation."""
|
|
260
|
+
validate_resource_type(request.resource_type)
|
|
261
|
+
validate_identifier(request.identifier)
|
|
262
|
+
|
|
263
|
+
cloudcontrol = get_aws_client('cloudcontrol', request.region or 'us-east-1')
|
|
264
|
+
try:
|
|
265
|
+
result = cloudcontrol.get_resource(
|
|
266
|
+
TypeName=request.resource_type, Identifier=request.identifier
|
|
267
|
+
)
|
|
268
|
+
properties_str = result['ResourceDescription']['Properties']
|
|
269
|
+
properties = (
|
|
270
|
+
json.loads(properties_str) if isinstance(properties_str, str) else properties_str
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
resource_info = {
|
|
274
|
+
'identifier': result['ResourceDescription']['Identifier'],
|
|
275
|
+
'properties': properties,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Add security analysis if requested
|
|
279
|
+
if request.analyze_security and workflow_store is not None:
|
|
280
|
+
# Import here to avoid circular imports
|
|
281
|
+
from awslabs.ccapi_mcp_server.impl.tools.explanation import explain_impl
|
|
282
|
+
from awslabs.ccapi_mcp_server.impl.tools.security_scanning import run_checkov_impl
|
|
283
|
+
from awslabs.ccapi_mcp_server.impl.tools.session_management import (
|
|
284
|
+
check_environment_variables_impl,
|
|
285
|
+
get_aws_session_info_impl,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
env_token = None
|
|
289
|
+
creds_token = None
|
|
290
|
+
gen_token = None
|
|
291
|
+
explained_token = None
|
|
292
|
+
security_scan_token = None
|
|
293
|
+
try:
|
|
294
|
+
# Get credentials token first
|
|
295
|
+
env_check = await check_environment_variables_impl(workflow_store)
|
|
296
|
+
env_token = env_check['environment_token']
|
|
297
|
+
session_info = await get_aws_session_info_impl(env_token, workflow_store)
|
|
298
|
+
creds_token = session_info['credentials_token']
|
|
299
|
+
|
|
300
|
+
# Use existing security analysis workflow
|
|
301
|
+
from awslabs.ccapi_mcp_server.impl.tools.infrastructure_generation import (
|
|
302
|
+
generate_infrastructure_code_impl_wrapper,
|
|
303
|
+
)
|
|
304
|
+
from awslabs.ccapi_mcp_server.models.models import (
|
|
305
|
+
GenerateInfrastructureCodeRequest,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
gen_request = GenerateInfrastructureCodeRequest(
|
|
309
|
+
resource_type=request.resource_type,
|
|
310
|
+
properties=properties or {},
|
|
311
|
+
credentials_token=creds_token or '',
|
|
312
|
+
region=request.region,
|
|
313
|
+
)
|
|
314
|
+
generated_code = await generate_infrastructure_code_impl_wrapper(
|
|
315
|
+
gen_request, workflow_store
|
|
316
|
+
)
|
|
317
|
+
gen_token = generated_code['generated_code_token']
|
|
318
|
+
|
|
319
|
+
from awslabs.ccapi_mcp_server.models.models import ExplainRequest
|
|
320
|
+
|
|
321
|
+
explain_request = ExplainRequest(
|
|
322
|
+
generated_code_token=gen_token or '', content=None
|
|
323
|
+
)
|
|
324
|
+
explained = await explain_impl(explain_request, workflow_store)
|
|
325
|
+
explained_token = explained['explained_token']
|
|
326
|
+
|
|
327
|
+
from awslabs.ccapi_mcp_server.models.models import RunCheckovRequest
|
|
328
|
+
|
|
329
|
+
checkov_request = RunCheckovRequest(explained_token=explained_token)
|
|
330
|
+
security_scan = await run_checkov_impl(checkov_request, workflow_store)
|
|
331
|
+
security_scan_token = security_scan.get('security_scan_token')
|
|
332
|
+
resource_info['security_analysis'] = security_scan
|
|
333
|
+
except Exception as e:
|
|
334
|
+
resource_info['security_analysis'] = {
|
|
335
|
+
'error': f'Security analysis failed: {str(e)}'
|
|
336
|
+
}
|
|
337
|
+
finally:
|
|
338
|
+
# Clean up security analysis tokens that aren't auto-consumed
|
|
339
|
+
# gen_token is consumed by explain(), so only clean remaining tokens
|
|
340
|
+
if workflow_store is not None:
|
|
341
|
+
cleanup_workflow_tokens(
|
|
342
|
+
workflow_store,
|
|
343
|
+
env_token or '',
|
|
344
|
+
creds_token or '',
|
|
345
|
+
explained_token or '',
|
|
346
|
+
security_scan_token or '',
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return resource_info
|
|
350
|
+
except Exception as e:
|
|
351
|
+
raise handle_aws_api_error(e)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def get_resource_request_status_impl(request_token: str, region: str | None = None) -> dict:
|
|
355
|
+
"""Get the status of a long running operation implementation."""
|
|
356
|
+
if not request_token:
|
|
357
|
+
raise ClientError('Please provide a request token to track the request')
|
|
358
|
+
|
|
359
|
+
cloudcontrol_client = get_aws_client('cloudcontrol', region or 'us-east-1')
|
|
360
|
+
try:
|
|
361
|
+
response = cloudcontrol_client.get_resource_request_status(
|
|
362
|
+
RequestToken=request_token,
|
|
363
|
+
)
|
|
364
|
+
except Exception as e:
|
|
365
|
+
raise handle_aws_api_error(e)
|
|
366
|
+
|
|
367
|
+
return progress_event(response['ProgressEvent'], response.get('HooksProgressEvent', None))
|
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
"""Security scanning implementation for CCAPI MCP server."""
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
import tempfile
|
|
22
|
+
import uuid
|
|
23
|
+
from awslabs.ccapi_mcp_server.errors import ClientError
|
|
24
|
+
from awslabs.ccapi_mcp_server.models.models import RunCheckovRequest
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_checkov_installed() -> dict:
|
|
28
|
+
"""Check if Checkov is available.
|
|
29
|
+
|
|
30
|
+
Since checkov is now a declared dependency, it should always be available.
|
|
31
|
+
This function mainly serves as a validation step.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dictionary with status information:
|
|
35
|
+
{
|
|
36
|
+
"installed": True/False,
|
|
37
|
+
"message": Description of what happened,
|
|
38
|
+
"needs_user_action": True/False
|
|
39
|
+
}
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
# Check if Checkov is available
|
|
43
|
+
subprocess.run(
|
|
44
|
+
['checkov', '--version'],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
check=True,
|
|
48
|
+
shell=False,
|
|
49
|
+
)
|
|
50
|
+
return {
|
|
51
|
+
'installed': True,
|
|
52
|
+
'message': 'Checkov is available',
|
|
53
|
+
'needs_user_action': False,
|
|
54
|
+
}
|
|
55
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
56
|
+
return {
|
|
57
|
+
'installed': False,
|
|
58
|
+
'message': 'Checkov is not available. This should not happen as checkov is a declared dependency. Please reinstall the package.',
|
|
59
|
+
'needs_user_action': True,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def run_security_analysis(resource_type: str, properties: dict) -> dict:
|
|
64
|
+
"""Simple security analysis function for test compatibility."""
|
|
65
|
+
return {'passed': True, 'message': 'Security analysis passed'}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def run_checkov_impl(request: RunCheckovRequest, workflow_store: dict) -> dict:
|
|
69
|
+
"""Run Checkov security and compliance scanner on server-stored CloudFormation template implementation."""
|
|
70
|
+
# Check if Checkov is installed
|
|
71
|
+
checkov_status = _check_checkov_installed()
|
|
72
|
+
if not checkov_status['installed']:
|
|
73
|
+
return {
|
|
74
|
+
'passed': False,
|
|
75
|
+
'error': 'Checkov is not installed',
|
|
76
|
+
'summary': {'error': 'Checkov not installed'},
|
|
77
|
+
'message': checkov_status['message'],
|
|
78
|
+
'requires_confirmation': checkov_status['needs_user_action'],
|
|
79
|
+
'options': [
|
|
80
|
+
{'option': 'install_help', 'description': 'Get help installing Checkov'},
|
|
81
|
+
{'option': 'proceed_without', 'description': 'Proceed without security checks'},
|
|
82
|
+
{'option': 'cancel', 'description': 'Cancel the operation'},
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# CRITICAL SECURITY: Validate explained token and get server-stored CloudFormation template
|
|
87
|
+
if request.explained_token not in workflow_store:
|
|
88
|
+
raise ClientError('Invalid explained token: you must call explain() first')
|
|
89
|
+
|
|
90
|
+
workflow_data = workflow_store[request.explained_token]
|
|
91
|
+
if workflow_data.get('type') != 'explained_properties':
|
|
92
|
+
raise ClientError('Invalid token type: expected explained_properties token from explain()')
|
|
93
|
+
|
|
94
|
+
# Get CloudFormation template from server-stored data (AI cannot override this)
|
|
95
|
+
cloudformation_template = workflow_data['data']['cloudformation_template']
|
|
96
|
+
resource_type = workflow_data['data']['properties'].get('Type', 'Unknown')
|
|
97
|
+
|
|
98
|
+
# Ensure content is a string for Checkov
|
|
99
|
+
if not isinstance(cloudformation_template, str):
|
|
100
|
+
try:
|
|
101
|
+
content = json.dumps(cloudformation_template)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return {
|
|
104
|
+
'passed': False,
|
|
105
|
+
'error': f'CloudFormation template must be valid JSON: {str(e)}',
|
|
106
|
+
'summary': {'error': 'Invalid CloudFormation template format'},
|
|
107
|
+
}
|
|
108
|
+
else:
|
|
109
|
+
content = cloudformation_template
|
|
110
|
+
|
|
111
|
+
# Create a temporary file with the CloudFormation template (always JSON)
|
|
112
|
+
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as temp_file:
|
|
113
|
+
temp_file.write(content.encode('utf-8'))
|
|
114
|
+
temp_file_path = temp_file.name
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# Build the checkov command with input validation
|
|
118
|
+
cmd = ['checkov', '-f', temp_file_path, '--output', 'json']
|
|
119
|
+
|
|
120
|
+
# Add framework if specified (validate against allowed frameworks)
|
|
121
|
+
if request.framework:
|
|
122
|
+
allowed_frameworks = [
|
|
123
|
+
'terraform',
|
|
124
|
+
'cloudformation',
|
|
125
|
+
'kubernetes',
|
|
126
|
+
'dockerfile',
|
|
127
|
+
'arm',
|
|
128
|
+
'all',
|
|
129
|
+
]
|
|
130
|
+
if request.framework in allowed_frameworks:
|
|
131
|
+
cmd.extend(['--framework', request.framework])
|
|
132
|
+
else:
|
|
133
|
+
return {
|
|
134
|
+
'passed': False,
|
|
135
|
+
'error': f'Invalid framework: {request.framework}. Allowed: {allowed_frameworks}',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Run checkov with shell=False for security
|
|
139
|
+
process = subprocess.run(cmd, capture_output=True, text=True, shell=False)
|
|
140
|
+
|
|
141
|
+
# Parse the output
|
|
142
|
+
if process.returncode == 0:
|
|
143
|
+
# All checks passed - generate security scan token
|
|
144
|
+
security_scan_token = f'sec_{str(uuid.uuid4())}'
|
|
145
|
+
|
|
146
|
+
workflow_store[security_scan_token] = {
|
|
147
|
+
'type': 'security_scan',
|
|
148
|
+
'data': {
|
|
149
|
+
'passed': True,
|
|
150
|
+
'scan_results': json.loads(process.stdout) if process.stdout else [],
|
|
151
|
+
'resource_type': resource_type,
|
|
152
|
+
'timestamp': str(datetime.datetime.now()),
|
|
153
|
+
},
|
|
154
|
+
'timestamp': datetime.datetime.now().isoformat(),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
'scan_status': 'PASSED',
|
|
159
|
+
'raw_failed_checks': [],
|
|
160
|
+
'raw_passed_checks': json.loads(process.stdout) if process.stdout else [],
|
|
161
|
+
'raw_summary': {'passed': True, 'message': 'All security checks passed'},
|
|
162
|
+
'resource_type': resource_type,
|
|
163
|
+
'timestamp': str(datetime.datetime.now()),
|
|
164
|
+
'security_scan_token': security_scan_token,
|
|
165
|
+
'message': 'Security checks passed. You can proceed with create_resource().',
|
|
166
|
+
}
|
|
167
|
+
elif process.returncode == 1: # Return code 1 means vulnerabilities were found
|
|
168
|
+
# Some checks failed
|
|
169
|
+
try:
|
|
170
|
+
results = json.loads(process.stdout) if process.stdout else {}
|
|
171
|
+
failed_checks = results.get('results', {}).get('failed_checks', [])
|
|
172
|
+
passed_checks = results.get('results', {}).get('passed_checks', [])
|
|
173
|
+
summary = results.get('summary', {})
|
|
174
|
+
|
|
175
|
+
# Security issues found - return results with security_scan_token
|
|
176
|
+
security_scan_token = f'sec_{str(uuid.uuid4())}'
|
|
177
|
+
|
|
178
|
+
workflow_store[security_scan_token] = {
|
|
179
|
+
'type': 'security_scan',
|
|
180
|
+
'data': {
|
|
181
|
+
'passed': False,
|
|
182
|
+
'scan_results': {
|
|
183
|
+
'failed_checks': failed_checks,
|
|
184
|
+
'passed_checks': passed_checks,
|
|
185
|
+
'summary': summary,
|
|
186
|
+
},
|
|
187
|
+
'resource_type': resource_type,
|
|
188
|
+
'timestamp': str(datetime.datetime.now()),
|
|
189
|
+
},
|
|
190
|
+
'timestamp': datetime.datetime.now().isoformat(),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
'scan_status': 'FAILED',
|
|
195
|
+
'raw_failed_checks': failed_checks,
|
|
196
|
+
'raw_passed_checks': passed_checks,
|
|
197
|
+
'raw_summary': summary,
|
|
198
|
+
'resource_type': resource_type,
|
|
199
|
+
'timestamp': str(datetime.datetime.now()),
|
|
200
|
+
'security_scan_token': security_scan_token,
|
|
201
|
+
'message': 'Security issues found. You can proceed with create_resource() if you approve.',
|
|
202
|
+
}
|
|
203
|
+
except json.JSONDecodeError:
|
|
204
|
+
# Handle case where output is not valid JSON
|
|
205
|
+
return {
|
|
206
|
+
'passed': False,
|
|
207
|
+
'error': 'Failed to parse Checkov output',
|
|
208
|
+
'stdout': process.stdout,
|
|
209
|
+
'stderr': process.stderr,
|
|
210
|
+
}
|
|
211
|
+
else:
|
|
212
|
+
# Error running checkov
|
|
213
|
+
return {
|
|
214
|
+
'passed': False,
|
|
215
|
+
'error': f'Checkov exited with code {process.returncode}',
|
|
216
|
+
'stderr': process.stderr,
|
|
217
|
+
}
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return {'passed': False, 'error': str(e), 'message': 'Failed to run Checkov'}
|
|
220
|
+
finally:
|
|
221
|
+
# Clean up the temporary file
|
|
222
|
+
if os.path.exists(temp_file_path):
|
|
223
|
+
os.unlink(temp_file_path)
|