regscale-cli 6.20.10.0__py3-none-any.whl → 6.21.1.0__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 regscale-cli might be problematic. Click here for more details.

Files changed (64) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +12 -5
  3. regscale/core/app/internal/set_permissions.py +58 -27
  4. regscale/integrations/commercial/__init__.py +1 -2
  5. regscale/integrations/commercial/amazon/common.py +79 -2
  6. regscale/integrations/commercial/aws/cli.py +183 -9
  7. regscale/integrations/commercial/aws/scanner.py +544 -9
  8. regscale/integrations/commercial/cpe.py +18 -1
  9. regscale/integrations/commercial/nessus/scanner.py +2 -0
  10. regscale/integrations/commercial/sonarcloud.py +35 -36
  11. regscale/integrations/commercial/synqly/ticketing.py +51 -0
  12. regscale/integrations/commercial/tenablev2/jsonl_scanner.py +2 -1
  13. regscale/integrations/commercial/wizv2/async_client.py +10 -3
  14. regscale/integrations/commercial/wizv2/click.py +102 -26
  15. regscale/integrations/commercial/wizv2/constants.py +249 -1
  16. regscale/integrations/commercial/wizv2/issue.py +2 -2
  17. regscale/integrations/commercial/wizv2/parsers.py +3 -2
  18. regscale/integrations/commercial/wizv2/policy_compliance.py +1858 -0
  19. regscale/integrations/commercial/wizv2/scanner.py +15 -21
  20. regscale/integrations/commercial/wizv2/utils.py +258 -85
  21. regscale/integrations/commercial/wizv2/variables.py +4 -3
  22. regscale/integrations/compliance_integration.py +1455 -0
  23. regscale/integrations/integration_override.py +15 -6
  24. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  25. regscale/integrations/public/fedramp/markdown_parser.py +7 -1
  26. regscale/integrations/scanner_integration.py +193 -37
  27. regscale/models/app_models/__init__.py +1 -0
  28. regscale/models/integration_models/amazon_models/inspector_scan.py +32 -57
  29. regscale/models/integration_models/aqua.py +92 -78
  30. regscale/models/integration_models/cisa_kev_data.json +117 -5
  31. regscale/models/integration_models/defenderimport.py +64 -59
  32. regscale/models/integration_models/ecr_models/ecr.py +100 -147
  33. regscale/models/integration_models/flat_file_importer/__init__.py +52 -38
  34. regscale/models/integration_models/ibm.py +29 -47
  35. regscale/models/integration_models/nexpose.py +156 -68
  36. regscale/models/integration_models/prisma.py +46 -66
  37. regscale/models/integration_models/qualys.py +99 -93
  38. regscale/models/integration_models/snyk.py +229 -158
  39. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  40. regscale/models/integration_models/veracode.py +15 -20
  41. regscale/{integrations/commercial/wizv2/models.py → models/integration_models/wizv2.py} +4 -12
  42. regscale/models/integration_models/xray.py +276 -82
  43. regscale/models/regscale_models/control_implementation.py +14 -12
  44. regscale/models/regscale_models/file.py +4 -0
  45. regscale/models/regscale_models/issue.py +123 -0
  46. regscale/models/regscale_models/milestone.py +1 -1
  47. regscale/models/regscale_models/rbac.py +22 -0
  48. regscale/models/regscale_models/regscale_model.py +4 -2
  49. regscale/models/regscale_models/security_plan.py +1 -1
  50. regscale/utils/graphql_client.py +3 -1
  51. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/METADATA +9 -9
  52. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/RECORD +64 -60
  53. tests/fixtures/test_fixture.py +58 -2
  54. tests/regscale/core/test_app.py +5 -3
  55. tests/regscale/core/test_version_regscale.py +5 -3
  56. tests/regscale/integrations/test_integration_mapping.py +522 -40
  57. tests/regscale/integrations/test_issue_due_date.py +1 -1
  58. tests/regscale/integrations/test_update_finding_dates.py +336 -0
  59. tests/regscale/integrations/test_wiz_policy_compliance_affected_controls.py +154 -0
  60. tests/regscale/models/test_asset.py +406 -50
  61. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/LICENSE +0 -0
  62. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/WHEEL +0 -0
  63. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/entry_points.txt +0 -0
  64. {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/top_level.txt +0 -0
regscale/_version.py CHANGED
@@ -33,7 +33,7 @@ def get_version_from_pyproject() -> str:
33
33
  return match.group(1)
34
34
  except Exception:
35
35
  pass
36
- return "6.20.10.0" # fallback version
36
+ return "6.21.1.0" # fallback version
37
37
 
38
38
 
39
39
  __version__ = get_version_from_pyproject()
@@ -245,6 +245,7 @@ class Application(metaclass=Singleton):
245
245
  "snowPassword": "<snowPassword>",
246
246
  "snowUrl": "<mySnowUrl>",
247
247
  "snowUserName": "<snowUserName>",
248
+ "sonarUrl": "https://sonarcloud.io",
248
249
  "sonarToken": "<mySonarToken>",
249
250
  "tenableAccessKey": "<tenableAccessKeyGoesHere>",
250
251
  "tenableSecretKey": "<tenableSecretKeyGoesHere>",
@@ -295,7 +296,10 @@ class Application(metaclass=Singleton):
295
296
  self.running_in_airflow = os.getenv("REGSCALE_AIRFLOW") == "true"
296
297
  if isinstance(config, str):
297
298
  config = self._read_config_from_str(config)
298
- self.config = self._gen_config(config)
299
+ if self.running_in_airflow:
300
+ self.config = self._fetch_config_from_regscale(config)
301
+ else:
302
+ self.config = self._gen_config(config)
299
303
  self.os = platform.system()
300
304
  self.input_host = ""
301
305
  # Ensure maxThreads is an integer for ThreadManager
@@ -536,10 +540,6 @@ class Application(metaclass=Singleton):
536
540
  self.logger.debug("Successfully retrieved config from Click context.")
537
541
  return click_config
538
542
 
539
- if self.running_in_airflow:
540
- if airflow_config := self._get_airflow_config(config):
541
- self.logger.debug("Successfully retrieved config from Airflow.")
542
- return airflow_config
543
543
  try:
544
544
  if config and self.local_config:
545
545
  self.logger.debug(f"Config provided as :\n{type(config)}")
@@ -572,6 +572,13 @@ class Application(metaclass=Singleton):
572
572
  return config
573
573
 
574
574
  def _get_airflow_config(self, config: Optional[Union[dict, str]] = None) -> Optional[dict]:
575
+ """
576
+ Get config from Airflow DAG config, or from the environment variables if not provided.
577
+
578
+ :param Optional[Union[dict, str]] config: Configuration dictionary, defaults to None
579
+ :return: Configuration dictionary
580
+ :rtype: Optional[dict]
581
+ """
575
582
  if config:
576
583
  self.logger.debug(f"Received config from Airflow as: {type(config)}")
577
584
  # check to see if config is a string because airflow can pass a string instead of a dict
@@ -1,11 +1,9 @@
1
1
  import click
2
2
  from pathlib import Path
3
3
  import os
4
- from openpyxl import Workbook, load_workbook
5
- from openpyxl.styles import Protection, Font, NamedStyle
6
- from openpyxl.worksheet.worksheet import Worksheet
4
+ from rich.progress import track
5
+ from openpyxl import Workbook
7
6
  from openpyxl.worksheet.datavalidation import DataValidation
8
- from openpyxl import Workbook, load_workbook
9
7
  from openpyxl.utils.dataframe import dataframe_to_rows
10
8
 
11
9
  from regscale.core.app.logz import create_logger
@@ -118,38 +116,71 @@ def import_permissions_workbook(file: Path):
118
116
  """
119
117
  # Read in the spreadsheet
120
118
  records = get_records(file=file)
121
- for record in records:
119
+ for index in track(
120
+ range(len(records)),
121
+ description="Processing rbacs...",
122
+ ):
123
+ record = records[index]
124
+ # Check the records:
125
+
122
126
  if not Group.get_object(record["group_id"]):
123
127
  logger.error(f"Group {record['group_id']} doesn't exist in this instance. Skipping row")
124
128
  continue
125
129
 
126
- regscale_module_id = Modules.get_module_to_id(record["regscale_module"])
127
- regscale_id = record["regscale_id"]
128
- if record[READ_UPDATE] == "R":
129
- permissions = 1
130
- elif record[READ_UPDATE] == "RU":
131
- permissions = 2
132
- else:
133
- permissions = 0
134
-
135
- # Set the record to private
136
130
  my_class = Modules.module_to_class(record["regscale_module"])
137
- obj = my_class.get_object(regscale_id)
131
+ obj = my_class.get_object(record["regscale_id"])
138
132
  if not obj:
139
- logger.error(f"RegScale {record['regscale_module']} record {regscale_id} doesn't exist. Skipping row")
133
+ logger.error(
134
+ f"RegScale {record['regscale_module']} record {record['regscale_id']} doesn't exist. Skipping row"
135
+ )
140
136
  continue
141
137
 
142
- # Add the permission
143
- RBAC.add(
144
- module_id=regscale_module_id,
145
- parent_id=regscale_id,
146
- group_id=record["group_id"],
147
- permission_type=permissions,
138
+ # Set the permissions
139
+ set_permissions(record=record)
140
+
141
+
142
+ def set_permissions(record: dict):
143
+ """
144
+ Sets the permissions for each record
145
+
146
+ :param dict record: permissions dictionary
147
+ """
148
+
149
+ if record[READ_UPDATE] == "R":
150
+ permissions = 1
151
+ elif record[READ_UPDATE] == "RU":
152
+ permissions = 2
153
+ else:
154
+ permissions = 0
155
+
156
+ # Add the permission
157
+ if not RBAC.add(
158
+ module_id=Modules.get_module_to_id(record["regscale_module"]),
159
+ parent_id=record["regscale_id"],
160
+ group_id=record["group_id"],
161
+ permission_type=permissions,
162
+ ):
163
+ logger.warning(
164
+ f"Failed to set permissions for {record['regscale_module']} {record['regscale_id']} for group {record['group_id']}"
165
+ )
166
+ return
167
+
168
+ if not RBAC.public(
169
+ module_id=Modules.get_module_to_id(record["regscale_module"]),
170
+ parent_id=record["regscale_id"],
171
+ is_public=0 if record[PUBLIC_PRIVATE] == "private" else 1,
172
+ ):
173
+ logger.warning(
174
+ f"Failed to set public/private for {record['regscale_module']} {record['regscale_id']} for group {record['group_id']}"
148
175
  )
149
- RBAC.public(
150
- module_id=regscale_module_id,
151
- parent_id=regscale_id,
152
- is_public=0 if record[PUBLIC_PRIVATE] == "private" else 1,
176
+ return
177
+
178
+ if not RBAC.reset(
179
+ module_id=Modules.get_module_to_id(record["regscale_module"]),
180
+ parent_id=record["regscale_id"],
181
+ ):
182
+ logger.warning(
183
+ f"Failed to proliferate permissions for {record['regscale_module']} {record['regscale_id']} for group {record['group_id']}"
153
184
  )
154
185
 
155
186
 
@@ -43,9 +43,8 @@ show_mapping(aqua, "aqua")
43
43
  cls=LazyGroup,
44
44
  lazy_subcommands={
45
45
  "sync_assets": "regscale.integrations.commercial.aws.cli.sync_assets",
46
- "sync_findings": "regscale.integrations.commercial.aws.cli.sync_findings",
46
+ "sync_findings_and_assets": "regscale.integrations.commercial.aws.cli.sync_findings_and_assets",
47
47
  "inspector": "regscale.integrations.commercial.aws.cli.inspector",
48
- "inventory": "regscale.integrations.commercial.aws.cli.inventory",
49
48
  },
50
49
  name="aws",
51
50
  )
@@ -11,6 +11,8 @@ from dateutil import parser
11
11
 
12
12
  from regscale.core.app.utils.app_utils import create_logger
13
13
 
14
+ logger = create_logger()
15
+
14
16
 
15
17
  def check_finding_severity(comment: Optional[str]) -> str:
16
18
  """Check the severity of the finding
@@ -93,6 +95,63 @@ def get_comments(finding: dict) -> str:
93
95
 
94
96
 
95
97
  def fetch_aws_findings(aws_client: BaseClient) -> list:
98
+ """Fetch AWS Findings with optimized rate limiting and pagination
99
+
100
+ :param BaseClient aws_client: AWS Security Hub Client
101
+ :return: AWS Findings
102
+ :rtype: list
103
+ """
104
+ findings = []
105
+ try:
106
+ # Use optimized SecurityHubPuller for better performance
107
+ from regscale.integrations.commercial.aws.security_hub import SecurityHubPuller
108
+
109
+ # Extract credentials from the client to create SecurityHubPuller
110
+ session = aws_client._client_config.__dict__.get("_user_provided_options", {})
111
+ region = session.meta.region_name
112
+
113
+ # Create SecurityHubPuller with same credentials as the client
114
+ puller = SecurityHubPuller(region_name=region)
115
+
116
+ # Use existing client instead of creating new one to maintain credentials
117
+ puller.client = aws_client
118
+
119
+ # Fetch all findings with optimized pagination and rate limiting
120
+ logger.info("Using optimized SecurityHubPuller for findings retrieval...")
121
+ findings = puller.get_all_findings_with_retries()
122
+
123
+ logger.info(f"Successfully fetched {len(findings)} findings with rate limiting")
124
+
125
+ except ImportError:
126
+ # Fallback to original method if SecurityHubPuller not available
127
+ logger.warning("SecurityHubPuller not available, falling back to basic client")
128
+ findings = fallback_fetch_aws_findings(aws_client)
129
+ except ClientError as cex:
130
+ logger.error("Unexpected error: %s", cex)
131
+ except Exception as e:
132
+ logger.error("Error using SecurityHubPuller, falling back to basic client: %s", e)
133
+ findings = fallback_fetch_aws_findings(aws_client)
134
+
135
+ return findings
136
+
137
+
138
+ def fallback_fetch_aws_findings(aws_client: BaseClient) -> list:
139
+ """Fallback method to fetch AWS Findings without pagination
140
+
141
+ :param BaseClient aws_client: AWS Security Hub Client
142
+ :return: AWS Findings
143
+ :rtype: list
144
+ """
145
+ findings = []
146
+ try:
147
+ response = aws_client.get_findings()
148
+ findings = response.get("Findings", [])
149
+ except ClientError as cex:
150
+ create_logger().error("Unexpected error when fetching resources from AWS: %s", cex)
151
+ return findings
152
+
153
+
154
+ def fetch_aws_findings_v2(aws_client: BaseClient) -> list:
96
155
  """Fetch AWS Findings
97
156
 
98
157
  :param BaseClient aws_client: AWS Security Hub Client
@@ -101,7 +160,25 @@ def fetch_aws_findings(aws_client: BaseClient) -> list:
101
160
  """
102
161
  findings = []
103
162
  try:
104
- findings = aws_client.get_findings()["Findings"]
163
+ response = aws_client.get_findings_v2()
164
+ findings = response.get("Findings", [])
105
165
  except ClientError as cex:
106
- create_logger().error("Unexpected error: %s", cex)
166
+ create_logger().error("Unexpected error when fetching resources from AWS: %s", cex)
107
167
  return findings
168
+
169
+
170
+ def fetch_aws_resources(aws_client: BaseClient) -> list:
171
+ """Fetch AWS Resources
172
+
173
+ :param BaseClient aws_client: AWS Security Hub Client
174
+ :return: AWS Resources
175
+ :rtype: list
176
+ """
177
+ resources = []
178
+ try:
179
+ response = aws_client.get_resources_v2()
180
+ resources = response.get("Resources", [])
181
+ logger.info(f"Fetched {len(resources)} resources from Security Hub")
182
+ except ClientError as cex:
183
+ create_logger().error("Unexpected error when fetching resources from AWS: %s", cex)
184
+ return resources
@@ -265,13 +265,13 @@ def import_aws_scans(
265
265
  "--region",
266
266
  type=str,
267
267
  default=os.environ.get("AWS_REGION", "us-east-1"),
268
- help="AWS region to collect inventory from. Default is us-east-1.",
268
+ help="AWS region to collect findings from",
269
269
  )
270
270
  @click.option(
271
271
  "--regscale_id",
272
272
  "--id",
273
273
  type=click.INT,
274
- help="RegScale will create and update assets as children of this record.",
274
+ help="RegScale will create and update findings as children of this record.",
275
275
  required=True,
276
276
  )
277
277
  @click.option(
@@ -286,7 +286,7 @@ def import_aws_scans(
286
286
  type=str,
287
287
  required=False,
288
288
  help="AWS secret access key",
289
- envvar="AWS_SECRET_ACCESS_KEY",
289
+ default=os.getenv("AWS_SECRET_ACCESS_KEY"),
290
290
  )
291
291
  @click.option(
292
292
  "--aws_session_token",
@@ -302,21 +302,195 @@ def sync_findings(
302
302
  aws_secret_access_key: Optional[str] = None,
303
303
  aws_session_token: Optional[str] = None,
304
304
  ) -> None:
305
- """Sync AWS Security Hub Findings."""
305
+ """
306
+ Sync AWS Security Hub findings to RegScale.
307
+
308
+ This command fetches findings from AWS Security Hub and creates/updates
309
+ corresponding issues in RegScale.
310
+ """
311
+ try:
312
+ logger.info("Starting AWS Security Hub findings sync to RegScale...")
313
+ from .scanner import AWSInventoryIntegration
314
+
315
+ scanner = AWSInventoryIntegration(plan_id=regscale_id)
316
+ findings_processed = scanner.sync_findings(
317
+ plan_id=regscale_id,
318
+ region=region,
319
+ aws_access_key_id=aws_access_key_id,
320
+ aws_secret_access_key=aws_secret_access_key,
321
+ aws_session_token=aws_session_token,
322
+ )
323
+ logger.info(f"AWS Security Hub findings sync completed successfully. Processed {findings_processed} findings.")
324
+ except Exception as e:
325
+ logger.error(f"Error syncing AWS Security Hub findings: {e}", exc_info=True)
326
+ raise click.ClickException(str(e))
327
+
328
+
329
+ @awsv2.command(name="sync_findings_and_assets")
330
+ @click.option(
331
+ "--region",
332
+ type=str,
333
+ default=os.environ.get("AWS_REGION", "us-east-1"),
334
+ help="AWS region to collect findings and assets from",
335
+ )
336
+ @click.option(
337
+ "--regscale_id",
338
+ "--id",
339
+ type=click.INT,
340
+ help="RegScale will create and update findings and assets as children of this record.",
341
+ required=True,
342
+ )
343
+ @click.option(
344
+ "--aws_access_key_id",
345
+ type=str,
346
+ required=False,
347
+ help="AWS access key ID",
348
+ envvar="AWS_ACCESS_KEY_ID",
349
+ )
350
+ @click.option(
351
+ "--aws_secret_access_key",
352
+ type=str,
353
+ required=False,
354
+ help="AWS secret access key",
355
+ default=os.getenv("AWS_SECRET_ACCESS_KEY"),
356
+ )
357
+ @click.option(
358
+ "--aws_session_token",
359
+ type=click.STRING,
360
+ required=False,
361
+ help="AWS Session ID",
362
+ default=os.environ.get("AWS_SESSION_TOKEN"),
363
+ )
364
+ def sync_findings_and_assets(
365
+ region: str,
366
+ regscale_id: int,
367
+ aws_access_key_id: Optional[str] = None,
368
+ aws_secret_access_key: Optional[str] = None,
369
+ aws_session_token: Optional[str] = None,
370
+ ) -> None:
371
+ """
372
+ Sync AWS Security Hub findings and automatically discovered assets to RegScale.
373
+
374
+ This command fetches findings from AWS Security Hub, creates/updates corresponding
375
+ issues in RegScale, and also creates assets for the resources referenced in the findings.
376
+ This provides a comprehensive view by creating both the security findings and the
377
+ underlying AWS resources they reference.
378
+ """
306
379
  try:
307
- logger.info("Starting AWS findings sync to RegScale...")
380
+ logger.info("Starting AWS Security Hub findings and assets sync to RegScale...")
308
381
  from .scanner import AWSInventoryIntegration
309
382
 
310
383
  scanner = AWSInventoryIntegration(plan_id=regscale_id)
311
- scanner.sync_findings(
384
+ findings_processed, assets_processed = scanner.sync_findings_and_assets(
312
385
  plan_id=regscale_id,
313
386
  region=region,
314
387
  aws_access_key_id=aws_access_key_id,
315
388
  aws_secret_access_key=aws_secret_access_key,
316
389
  aws_session_token=aws_session_token,
317
390
  )
318
- if not scanner.errors:
319
- logger.info("AWS finding sync completed successfully.")
391
+ logger.info(
392
+ f"AWS Security Hub sync completed successfully. "
393
+ f"Processed {findings_processed} findings and {assets_processed} assets."
394
+ )
395
+ except Exception as e:
396
+ logger.error(f"Error syncing AWS Security Hub findings and assets: {e}", exc_info=True)
397
+ raise click.ClickException(str(e))
398
+
399
+
400
+ @awsv2.group()
401
+ def findings():
402
+ """AWS Security Hub findings commands."""
403
+ pass
404
+
405
+
406
+ @findings.command(name="collect")
407
+ @click.option(
408
+ "--region",
409
+ type=str,
410
+ default=os.getenv("AWS_REGION", "us-east-1"),
411
+ help="AWS region to collect findings from. Default is us-east-1.",
412
+ )
413
+ @click.option(
414
+ "--aws_access_key_id",
415
+ type=str,
416
+ required=False,
417
+ help="AWS access key ID",
418
+ envvar="AWS_ACCESS_KEY_ID",
419
+ )
420
+ @click.option(
421
+ "--aws_secret_access_key",
422
+ type=str,
423
+ required=False,
424
+ help="AWS secret access key",
425
+ envvar="AWS_SECRET_ACCESS_KEY",
426
+ )
427
+ @click.option(
428
+ "--aws_session_token",
429
+ type=click.STRING,
430
+ required=False,
431
+ help="AWS Session ID",
432
+ default=os.environ.get("AWS_SESSION_TOKEN"),
433
+ )
434
+ @click.option(
435
+ "--output",
436
+ type=click.Path(dir_okay=False, writable=True),
437
+ help="Output file path (JSON format). Default: artifacts/aws/findings.json",
438
+ required=False,
439
+ )
440
+ def collect_findings(
441
+ region: str,
442
+ aws_access_key_id: Optional[str],
443
+ aws_secret_access_key: Optional[str],
444
+ aws_session_token: Optional[str],
445
+ output: Optional[str],
446
+ ) -> None:
447
+ """
448
+ Collect AWS Security Hub findings.
449
+
450
+ This command fetches findings from AWS Security Hub and displays them to stdout
451
+ or saves them to a JSON file. The findings include security issues, compliance
452
+ violations, and other security-related information from AWS Security Hub.
453
+
454
+ If no output file is specified, findings will be saved to artifacts/aws/findings.json
455
+ by default. Use --output - to display to stdout instead.
456
+ """
457
+ try:
458
+ import boto3
459
+ from regscale.integrations.commercial.amazon.common import fetch_aws_findings
460
+ from regscale.models import DateTimeEncoder
461
+
462
+ logger.info("Collecting AWS Security Hub findings...")
463
+
464
+ # Create AWS session
465
+ session = boto3.Session(
466
+ region_name=region,
467
+ aws_access_key_id=aws_access_key_id,
468
+ aws_secret_access_key=aws_secret_access_key,
469
+ aws_session_token=aws_session_token,
470
+ )
471
+ client = session.client("securityhub")
472
+
473
+ # Fetch findings
474
+ findings = fetch_aws_findings(aws_client=client)
475
+
476
+ logger.info(f"AWS Security Hub findings collected successfully. Found {len(findings)} finding(s).")
477
+
478
+ # Default output path
479
+ if output is None:
480
+ output = os.path.join("artifacts", "aws", "findings.json")
481
+
482
+ if output == "-":
483
+ # Output to stdout
484
+ click.echo(json.dumps(findings, indent=2, cls=DateTimeEncoder))
485
+ else:
486
+ # Save to file
487
+ # Ensure the artifacts directory exists
488
+ os.makedirs(os.path.dirname(output), exist_ok=True)
489
+
490
+ with open(output, "w", encoding="utf-8") as f:
491
+ json.dump(findings, f, indent=2, cls=DateTimeEncoder)
492
+ logger.info(f"Findings saved to {output}")
493
+
320
494
  except Exception as e:
321
- logger.error(f"Error syncing AWS finding(s): {e}", exc_info=True)
495
+ logger.error(f"Error collecting AWS Security Hub findings: {e}", exc_info=True)
322
496
  raise click.ClickException(str(e))