|
1 | 1 | import os |
| 2 | +import time |
2 | 3 | from glob import glob |
3 | 4 |
|
4 | 5 | from celery.utils.log import get_task_logger |
5 | 6 | from config.django.base import DJANGO_FINDINGS_BATCH_SIZE |
| 7 | +from django.db import OperationalError |
6 | 8 | from tasks.utils import batched |
7 | 9 |
|
8 | 10 | from api.db_router import READ_REPLICA_ALIAS, MainRouter |
9 | | -from api.db_utils import rls_transaction |
| 11 | +from api.db_utils import REPLICA_MAX_ATTEMPTS, REPLICA_RETRY_BASE_DELAY, rls_transaction |
10 | 12 | from api.models import Finding, Integration, Provider |
11 | 13 | from api.utils import initialize_prowler_integration, initialize_prowler_provider |
12 | 14 | from prowler.lib.outputs.asff.asff import ASFF |
|
17 | 19 | from prowler.lib.outputs.ocsf.ocsf import OCSF |
18 | 20 | from prowler.providers.aws.aws_provider import AwsProvider |
19 | 21 | from prowler.providers.aws.lib.s3.s3 import S3 |
20 | | -from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub |
21 | | -from prowler.providers.common.models import Connection |
22 | 22 | from prowler.providers.aws.lib.security_hub.exceptions.exceptions import ( |
23 | 23 | SecurityHubNoEnabledRegionsError, |
24 | 24 | ) |
| 25 | +from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub |
| 26 | +from prowler.providers.common.models import Connection |
25 | 27 |
|
26 | 28 | logger = get_task_logger(__name__) |
27 | 29 |
|
@@ -291,96 +293,130 @@ def upload_security_hub_integration( |
291 | 293 | total_findings_sent[integration.id] = 0 |
292 | 294 |
|
293 | 295 | # Process findings in batches to avoid memory issues |
| 296 | + max_attempts = REPLICA_MAX_ATTEMPTS if READ_REPLICA_ALIAS else 1 |
294 | 297 | has_findings = False |
295 | 298 | batch_number = 0 |
296 | 299 |
|
297 | | - with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): |
298 | | - qs = ( |
299 | | - Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id) |
300 | | - .order_by("uid") |
301 | | - .iterator() |
302 | | - ) |
303 | | - |
304 | | - for batch, _ in batched(qs, DJANGO_FINDINGS_BATCH_SIZE): |
305 | | - batch_number += 1 |
306 | | - has_findings = True |
| 300 | + for attempt in range(1, max_attempts + 1): |
| 301 | + read_alias = None |
| 302 | + if READ_REPLICA_ALIAS: |
| 303 | + read_alias = ( |
| 304 | + READ_REPLICA_ALIAS |
| 305 | + if attempt < max_attempts |
| 306 | + else MainRouter.default_db |
| 307 | + ) |
307 | 308 |
|
308 | | - # Transform findings for this batch |
309 | | - transformed_findings = [ |
310 | | - FindingOutput.transform_api_finding( |
311 | | - finding, prowler_provider |
| 309 | + try: |
| 310 | + batch_number = 0 |
| 311 | + has_findings = False |
| 312 | + with rls_transaction( |
| 313 | + tenant_id, |
| 314 | + using=read_alias, |
| 315 | + retry_on_replica=False, |
| 316 | + ): |
| 317 | + qs = ( |
| 318 | + Finding.all_objects.filter( |
| 319 | + tenant_id=tenant_id, scan_id=scan_id |
| 320 | + ) |
| 321 | + .order_by("uid") |
| 322 | + .iterator() |
312 | 323 | ) |
313 | | - for finding in batch |
314 | | - ] |
315 | | - |
316 | | - # Convert to ASFF format |
317 | | - asff_transformer = ASFF( |
318 | | - findings=transformed_findings, |
319 | | - file_path="", |
320 | | - file_extension="json", |
321 | | - ) |
322 | | - asff_transformer.transform(transformed_findings) |
323 | 324 |
|
324 | | - # Get the batch of ASFF findings |
325 | | - batch_asff_findings = asff_transformer.data |
| 325 | + for batch, _ in batched(qs, DJANGO_FINDINGS_BATCH_SIZE): |
| 326 | + batch_number += 1 |
| 327 | + has_findings = True |
326 | 328 |
|
327 | | - if batch_asff_findings: |
328 | | - # Create Security Hub client for first batch or reuse existing |
329 | | - if not security_hub_client: |
330 | | - connected, security_hub = ( |
331 | | - get_security_hub_client_from_integration( |
332 | | - integration, tenant_id, batch_asff_findings |
| 329 | + # Transform findings for this batch |
| 330 | + transformed_findings = [ |
| 331 | + FindingOutput.transform_api_finding( |
| 332 | + finding, prowler_provider |
333 | 333 | ) |
| 334 | + for finding in batch |
| 335 | + ] |
| 336 | + |
| 337 | + # Convert to ASFF format |
| 338 | + asff_transformer = ASFF( |
| 339 | + findings=transformed_findings, |
| 340 | + file_path="", |
| 341 | + file_extension="json", |
334 | 342 | ) |
| 343 | + asff_transformer.transform(transformed_findings) |
| 344 | + |
| 345 | + # Get the batch of ASFF findings |
| 346 | + batch_asff_findings = asff_transformer.data |
| 347 | + |
| 348 | + if batch_asff_findings: |
| 349 | + # Create Security Hub client for first batch or reuse existing |
| 350 | + if not security_hub_client: |
| 351 | + connected, security_hub = ( |
| 352 | + get_security_hub_client_from_integration( |
| 353 | + integration, |
| 354 | + tenant_id, |
| 355 | + batch_asff_findings, |
| 356 | + ) |
| 357 | + ) |
335 | 358 |
|
336 | | - if not connected: |
337 | | - if isinstance( |
338 | | - security_hub.error, |
339 | | - SecurityHubNoEnabledRegionsError, |
340 | | - ): |
341 | | - logger.warning( |
342 | | - f"Security Hub integration {integration.id} has no enabled regions" |
| 359 | + if not connected: |
| 360 | + if isinstance( |
| 361 | + security_hub.error, |
| 362 | + SecurityHubNoEnabledRegionsError, |
| 363 | + ): |
| 364 | + logger.warning( |
| 365 | + f"Security Hub integration {integration.id} has no enabled regions" |
| 366 | + ) |
| 367 | + else: |
| 368 | + logger.error( |
| 369 | + f"Security Hub connection failed for integration {integration.id}: " |
| 370 | + f"{security_hub.error}" |
| 371 | + ) |
| 372 | + break # Skip this integration |
| 373 | + |
| 374 | + security_hub_client = security_hub |
| 375 | + logger.info( |
| 376 | + f"Sending {'fail' if send_only_fails else 'all'} findings to Security Hub via " |
| 377 | + f"integration {integration.id}" |
343 | 378 | ) |
344 | 379 | else: |
345 | | - logger.error( |
346 | | - f"Security Hub connection failed for integration {integration.id}: " |
347 | | - f"{security_hub.error}" |
| 380 | + # Update findings in existing client for this batch |
| 381 | + security_hub_client._findings_per_region = ( |
| 382 | + security_hub_client.filter( |
| 383 | + batch_asff_findings, |
| 384 | + send_only_fails, |
| 385 | + ) |
348 | 386 | ) |
349 | | - break # Skip this integration |
350 | 387 |
|
351 | | - security_hub_client = security_hub |
352 | | - logger.info( |
353 | | - f"Sending {'fail' if send_only_fails else 'all'} findings to Security Hub via " |
354 | | - f"integration {integration.id}" |
355 | | - ) |
356 | | - else: |
357 | | - # Update findings in existing client for this batch |
358 | | - security_hub_client._findings_per_region = ( |
359 | | - security_hub_client.filter( |
360 | | - batch_asff_findings, send_only_fails |
361 | | - ) |
362 | | - ) |
| 388 | + # Send this batch to Security Hub |
| 389 | + try: |
| 390 | + findings_sent = security_hub_client.batch_send_to_security_hub() |
| 391 | + total_findings_sent[integration.id] += ( |
| 392 | + findings_sent |
| 393 | + ) |
363 | 394 |
|
364 | | - # Send this batch to Security Hub |
365 | | - try: |
366 | | - findings_sent = ( |
367 | | - security_hub_client.batch_send_to_security_hub() |
368 | | - ) |
369 | | - total_findings_sent[integration.id] += findings_sent |
| 395 | + if findings_sent > 0: |
| 396 | + logger.debug( |
| 397 | + f"Sent batch {batch_number} with {findings_sent} findings to Security Hub" |
| 398 | + ) |
| 399 | + except Exception as batch_error: |
| 400 | + logger.error( |
| 401 | + f"Failed to send batch {batch_number} to Security Hub: {str(batch_error)}" |
| 402 | + ) |
370 | 403 |
|
371 | | - if findings_sent > 0: |
372 | | - logger.debug( |
373 | | - f"Sent batch {batch_number} with {findings_sent} findings to Security Hub" |
374 | | - ) |
375 | | - except Exception as batch_error: |
376 | | - logger.error( |
377 | | - f"Failed to send batch {batch_number} to Security Hub: {str(batch_error)}" |
378 | | - ) |
| 404 | + # Clear memory after processing each batch |
| 405 | + asff_transformer._data.clear() |
| 406 | + del batch_asff_findings |
| 407 | + del transformed_findings |
| 408 | + |
| 409 | + break |
| 410 | + except OperationalError as e: |
| 411 | + if attempt == max_attempts: |
| 412 | + raise |
379 | 413 |
|
380 | | - # Clear memory after processing each batch |
381 | | - asff_transformer._data.clear() |
382 | | - del batch_asff_findings |
383 | | - del transformed_findings |
| 414 | + delay = REPLICA_RETRY_BASE_DELAY * (2 ** (attempt - 1)) |
| 415 | + logger.info( |
| 416 | + "RLS query failed during Security Hub integration " |
| 417 | + f"(attempt {attempt}/{max_attempts}), retrying in {delay}s. Error: {e}" |
| 418 | + ) |
| 419 | + time.sleep(delay) |
384 | 420 |
|
385 | 421 | if not has_findings: |
386 | 422 | logger.info( |
|
0 commit comments