regscale-cli 6.27.2.0__py3-none-any.whl → 6.28.0.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 (140) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +11 -2
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/dev/cli.py +26 -0
  10. regscale/dev/version.py +72 -0
  11. regscale/integrations/commercial/__init__.py +15 -1
  12. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  13. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  14. regscale/integrations/commercial/amazon/common.py +48 -58
  15. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  16. regscale/integrations/commercial/aws/cli.py +3093 -55
  17. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  18. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  19. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  20. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  21. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  22. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  23. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  24. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  25. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  26. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  27. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  28. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  29. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  30. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  31. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  32. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  33. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  34. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  35. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  36. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  37. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  38. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  39. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  40. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  41. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  42. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  43. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  44. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  45. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  46. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  47. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  48. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  49. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  50. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  51. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  52. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  53. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  54. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  55. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  56. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  57. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  58. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  59. regscale/integrations/commercial/aws/scanner.py +853 -205
  60. regscale/integrations/commercial/aws/security_hub.py +319 -0
  61. regscale/integrations/commercial/aws/session_manager.py +282 -0
  62. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  63. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  64. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  65. regscale/integrations/compliance_integration.py +308 -38
  66. regscale/integrations/control_matcher.py +78 -23
  67. regscale/integrations/due_date_handler.py +3 -0
  68. regscale/integrations/public/csam/csam.py +572 -763
  69. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  70. regscale/integrations/public/csam/csam_common.py +154 -0
  71. regscale/integrations/public/csam/csam_controls.py +432 -0
  72. regscale/integrations/public/csam/csam_poam.py +124 -0
  73. regscale/integrations/public/fedramp/click.py +17 -4
  74. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  75. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  76. regscale/integrations/scanner_integration.py +415 -85
  77. regscale/models/integration_models/cisa_kev_data.json +80 -20
  78. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  79. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
  80. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  81. regscale/models/platform.py +3 -0
  82. regscale/models/regscale_models/__init__.py +5 -0
  83. regscale/models/regscale_models/assessment.py +2 -1
  84. regscale/models/regscale_models/component.py +1 -1
  85. regscale/models/regscale_models/control_implementation.py +55 -24
  86. regscale/models/regscale_models/control_objective.py +74 -5
  87. regscale/models/regscale_models/file.py +2 -0
  88. regscale/models/regscale_models/issue.py +2 -5
  89. regscale/models/regscale_models/organization.py +3 -0
  90. regscale/models/regscale_models/regscale_model.py +17 -5
  91. regscale/models/regscale_models/security_plan.py +1 -0
  92. regscale/regscale.py +11 -1
  93. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  94. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
  95. tests/regscale/core/test_login.py +171 -4
  96. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  97. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  98. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  99. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  100. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  101. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  102. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  103. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  104. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  105. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  106. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  107. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  108. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  109. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  110. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  111. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  112. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  113. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  114. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  115. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  116. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  117. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  118. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  119. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  120. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  121. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  122. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  123. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  124. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  125. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  126. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  127. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  128. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  129. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  130. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  131. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  132. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  133. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  134. tests/regscale/integrations/commercial/test_aws.py +55 -56
  135. tests/regscale/integrations/test_control_matcher.py +24 -0
  136. tests/regscale/models/test_control_implementation.py +118 -3
  137. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  138. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  139. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  140. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
regscale/core/login.py CHANGED
@@ -6,6 +6,7 @@ from os import getenv
6
6
  from typing import Optional, Tuple
7
7
  from urllib.parse import urljoin
8
8
 
9
+ from requests.exceptions import HTTPError
9
10
  from regscale.core.app.api import Api
10
11
  from regscale.core.app.utils.app_utils import error_and_exit
11
12
 
@@ -14,10 +15,11 @@ logger = logging.getLogger("regscale")
14
15
 
15
16
  def get_regscale_token(
16
17
  api: Api,
17
- username: str = getenv("REGSCALE_USER"),
18
- password: str = getenv("REGSCALE_PASSWORD"),
19
- domain: str = getenv("REGSCALE_DOMAIN"),
18
+ username: Optional[str] = getenv("REGSCALE_USER"),
19
+ password: Optional[str] = getenv("REGSCALE_PASSWORD"),
20
+ domain: Optional[str] = getenv("REGSCALE_DOMAIN"),
20
21
  mfa_token: Optional[str] = "",
22
+ app_id: Optional[int] = 1,
21
23
  ) -> Tuple[str, str]:
22
24
  """
23
25
  Authenticate with RegScale and return a token
@@ -27,6 +29,7 @@ def get_regscale_token(
27
29
  :param str password: a string defaulting to the envar REGSCALE_PASSWORD
28
30
  :param str domain: a string representing the RegScale domain, checks environment REGSCALE_DOMAIN
29
31
  :param Optional[str] mfa_token: MFA token to login with
32
+ :param Optional[int] app_id: The app ID to login with
30
33
  :raises EnvironmentError: if domain is not passed or retrieved
31
34
  :return: a tuple of user_id and auth_token
32
35
  :rtype: Tuple[str, str]
@@ -47,7 +50,19 @@ def get_regscale_token(
47
50
  logger.info("Logging into: %s", domain)
48
51
  # suggest structuring the login paths so that they all exist in one place
49
52
  url = urljoin(domain, "/api/authentication/login")
50
- response = api.post(url=url, json=auth, headers={})
53
+ try:
54
+ # Try to authenticate with the new API version
55
+ auth["appId"] = app_id
56
+ response = api.post(url=url, json=auth, headers={"X-Api-Version": "2.0"})
57
+ if response is None:
58
+ raise HTTPError("No response received from api.post(). Possible connection issue or internal error.")
59
+ response.raise_for_status()
60
+ app_id_compatible = True
61
+ except HTTPError:
62
+ # Fallback to the old API version
63
+ del auth["appId"]
64
+ response = api.post(url=url, json=auth, headers={})
65
+ app_id_compatible = False
51
66
  error_msg = "Unable to authenticate with RegScale. Please check your credentials."
52
67
  if response is None:
53
68
  logger.error("No response received from api.post(). Possible connection issue or internal error.")
@@ -63,4 +78,6 @@ def get_regscale_token(
63
78
  error_and_exit(f"{error_msg}\n{response.status_code}: {response.text}")
64
79
  if isinstance(response_dict, str):
65
80
  response_dict = json.loads(response_dict)
81
+ if app_id_compatible:
82
+ return response_dict["accessToken"]["id"], response_dict["accessToken"]["authToken"]
66
83
  return response_dict["id"], response_dict["auth_token"]
@@ -2,8 +2,10 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """Utility functions for handling date and datetime conversions"""
4
4
 
5
+ import calendar
5
6
  import datetime
6
7
  import logging
8
+ import re
7
9
  from typing import Any, List, Optional, Union
8
10
 
9
11
 
@@ -39,7 +41,8 @@ def date_str(date_object: Union[str, datetime.datetime, datetime.date, None], da
39
41
  return date_object.strftime(date_format)
40
42
 
41
43
  return date_object.isoformat()
42
- except Exception:
44
+ except (AttributeError, TypeError, ValueError) as e:
45
+ logger.debug(f"Error converting date object to string: {e}")
43
46
  return ""
44
47
 
45
48
 
@@ -82,6 +85,7 @@ def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None])
82
85
  def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.datetime]:
83
86
  """
84
87
  Convert a string, datetime, date, integer, or timestamp string to a datetime object.
88
+ If the day of the month is invalid (e.g., November 31), adjusts to the last valid day of that month.
85
89
 
86
90
  :param Union[str, datetime.datetime, datetime.date, int, None] date_str: The value to convert.
87
91
  :return: The datetime object.
@@ -93,6 +97,10 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
93
97
  try:
94
98
  return parse(date_str)
95
99
  except ParserError as e:
100
+ # Try to fix invalid day of month (e.g., 2023/11/31 -> 2023/11/30)
101
+ if fixed_date := _fix_invalid_day_of_month(date_str):
102
+ return fixed_date
103
+
96
104
  if date_str and str(date_str).lower() not in ["n/a", "none"]:
97
105
  logger.warning(f"Warning could not parse date string: {date_str}\n{e}")
98
106
  return None
@@ -105,6 +113,74 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
105
113
  return None
106
114
 
107
115
 
116
+ def _parse_date_components(match_groups: tuple) -> tuple[int, int, int]:
117
+ """
118
+ Parse year, month, day from regex match groups.
119
+
120
+ :param tuple match_groups: Tuple of matched groups from regex
121
+ :return: Tuple of (year, month, day)
122
+ :rtype: tuple[int, int, int]
123
+ """
124
+ if len(match_groups[0]) == 4: # First group is year (YYYY/MM/DD)
125
+ return int(match_groups[0]), int(match_groups[1]), int(match_groups[2])
126
+ # Last group is year (MM/DD/YYYY)
127
+ return int(match_groups[2]), int(match_groups[0]), int(match_groups[1])
128
+
129
+
130
+ def _adjust_invalid_day(year: int, month: int, day: int) -> Optional[datetime.datetime]:
131
+ """
132
+ Adjust invalid day of month to last valid day.
133
+
134
+ :param int year: Year
135
+ :param int month: Month
136
+ :param int day: Day
137
+ :return: Adjusted datetime or None if invalid
138
+ :rtype: Optional[datetime.datetime]
139
+ """
140
+ last_valid_day = calendar.monthrange(year, month)[1]
141
+ if day <= last_valid_day:
142
+ return None
143
+
144
+ logger.warning(f"Invalid day {day} for month {month}/{year}. Adjusting to last valid day: {last_valid_day}")
145
+ try:
146
+ return datetime.datetime(year, month, last_valid_day)
147
+ except ValueError:
148
+ return None
149
+
150
+
151
+ def _fix_invalid_day_of_month(date_str: str) -> Optional[datetime.datetime]:
152
+ """
153
+ Attempt to fix an invalid day of month in a date string.
154
+ For example, 2023/11/31 would become 2023/11/30.
155
+
156
+ :param str date_str: The date string to fix
157
+ :return: A datetime object with a valid day, or None if it can't be fixed
158
+ :rtype: Optional[datetime.datetime]
159
+ """
160
+ try:
161
+ patterns = [
162
+ r"(\d{4})[/-](\d{1,2})[/-](\d{1,2})", # YYYY/MM/DD or YYYY-MM-DD
163
+ r"(\d{1,2})[/-](\d{1,2})[/-](\d{4})", # MM/DD/YYYY or MM-DD-YYYY
164
+ ]
165
+
166
+ for pattern in patterns:
167
+ if not (match := re.search(pattern, date_str)):
168
+ continue
169
+
170
+ year, month, day = _parse_date_components(match.groups())
171
+
172
+ if not (1 <= month <= 12):
173
+ continue
174
+
175
+ if result := _adjust_invalid_day(year, month, day):
176
+ return result
177
+
178
+ return None
179
+ except (ValueError, TypeError, AttributeError, IndexError) as e:
180
+ logger.debug(f"Could not fix invalid day of month for: {date_str} - {e}")
181
+ return None
182
+
183
+
108
184
  def time_str(time_obj: Union[str, datetime.datetime, datetime.time]) -> str:
109
185
  """
110
186
  Convert a datetime/time object to a string.
regscale/dev/cli.py CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  import contextlib
4
4
  import os
5
+ import re
5
6
  import sys
6
7
 
8
+ from pathlib import Path
9
+
7
10
  import click
8
11
  from rich.console import Console
9
12
 
@@ -231,5 +234,28 @@ def update_docs(readme_token: str, confluence_user: str, confluence_token: str,
231
234
  update_confluence(api, confluence_url, root, file)
232
235
 
233
236
 
237
+ @cli.command()
238
+ @click.option("--version", "-v", type=click.STRING, help="The version to upgrade the CLI to use.")
239
+ @click.option("--current", "-c", is_flag=True, help="Get the current version of the CLI.")
240
+ def version(version: str, current: bool) -> None:
241
+ """Manage the version of the regscale-cli package."""
242
+ from regscale.dev.version import (
243
+ get_current_version,
244
+ update_fallback_version_in_version_py,
245
+ update_version_in_pyproject_toml,
246
+ )
247
+
248
+ if current:
249
+ print(get_current_version())
250
+ return
251
+
252
+ if not version:
253
+ print("❌ Please provide a version to upgrade to using the --version flag.")
254
+ return
255
+
256
+ update_version_in_pyproject_toml(version)
257
+ update_fallback_version_in_version_py(version)
258
+
259
+
234
260
  if __name__ == "__main__":
235
261
  cli()
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """Version management script for regscale-cli."""
3
+
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+
12
+ def update_version_in_pyproject_toml(version: str) -> None:
13
+ """
14
+ Update the version in pyproject.toml.
15
+
16
+ :param str version: The version to update to
17
+ """
18
+ pyproject_file = "pyproject.toml"
19
+
20
+ pyproject_path = Path(pyproject_file)
21
+ content = pyproject_path.read_text()
22
+
23
+ # Update the version
24
+ pattern = r'version\s*=\s*["\']([^"\']+)["\']'
25
+ replacement = f'version = "{version}"'
26
+
27
+ if re.search(pattern, content):
28
+ content = re.sub(pattern, replacement, content)
29
+ pyproject_path.write_text(content)
30
+ console.print(f"[green]Updated version to {version} in {pyproject_file}")
31
+ else:
32
+ console.print(f"[red]Could not find version in {pyproject_file}")
33
+
34
+
35
+ def update_fallback_version_in_version_py(version: str) -> None:
36
+ """
37
+ Update the fallback version in regscale/_version.py.
38
+
39
+ :param str version: The version to update to
40
+ """
41
+ version_py_path = Path("regscale/_version.py")
42
+ content = version_py_path.read_text()
43
+ pattern = r'return\s*["\'](\d+\.\d+\.\d+\.\d+)["\']\s*# fallback version'
44
+ replacement = f'return "{version}" # fallback version'
45
+ if re.search(pattern, content):
46
+ content = re.sub(pattern, replacement, content)
47
+ version_py_path.write_text(content)
48
+ console.print(f"[green]Updated fallback version to {version} in regscale/_version.py")
49
+ else:
50
+ console.print("[red]Could not find fallback version in regscale/_version.py")
51
+
52
+
53
+ def get_current_version() -> str:
54
+ """
55
+ Get the current version from the package.
56
+
57
+ :return: The current version
58
+ :rtype: str
59
+ """
60
+ try:
61
+ # Add the project root to Python path to ensure we can import regscale
62
+ project_root = Path(__file__).parent.parent
63
+ if str(project_root) not in sys.path:
64
+ sys.path.insert(0, str(project_root))
65
+
66
+ from regscale import __version__
67
+
68
+ return __version__
69
+ except ImportError as e:
70
+ console.print(f"[red]Could not import version from regscale package: {e}")
71
+ console.print("[yellow]Make sure you're running this script from the project root directory")
72
+ sys.exit(1)
@@ -43,13 +43,27 @@ 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
47
  "sync_findings_and_assets": "regscale.integrations.commercial.aws.cli.sync_findings_and_assets",
48
+ "sync_compliance": "regscale.integrations.commercial.aws.cli.sync_compliance",
49
+ "sync_config_compliance": "regscale.integrations.commercial.aws.cli.sync_config_compliance",
50
+ "sync_kms": "regscale.integrations.commercial.aws.cli.sync_kms",
51
+ "sync_org": "regscale.integrations.commercial.aws.cli.sync_org",
52
+ "sync_iam": "regscale.integrations.commercial.aws.cli.sync_iam",
53
+ "sync_guardduty": "regscale.integrations.commercial.aws.cli.sync_guardduty",
54
+ "sync_s3": "regscale.integrations.commercial.aws.cli.sync_s3",
55
+ "sync_cloudtrail": "regscale.integrations.commercial.aws.cli.sync_cloudtrail",
56
+ "sync_cloudwatch": "regscale.integrations.commercial.aws.cli.sync_cloudwatch",
57
+ "sync_ssm": "regscale.integrations.commercial.aws.cli.sync_ssm",
58
+ "inventory": "regscale.integrations.commercial.aws.cli.inventory",
59
+ "findings": "regscale.integrations.commercial.aws.cli.findings",
60
+ "auth": "regscale.integrations.commercial.aws.cli.auth",
47
61
  "inspector": "regscale.integrations.commercial.aws.cli.inspector",
48
62
  },
49
63
  name="aws",
50
64
  )
51
65
  def aws():
52
- """AWS Integrations"""
66
+ """AWS Integrations - Asset sync, findings, compliance, and inventory collection"""
53
67
  pass
54
68
 
55
69
 
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """RegScale AWS Integrations"""
4
+ import re
5
+ from datetime import datetime, timedelta
6
+ from typing import Any, Optional, Tuple
7
+
8
+ from botocore.client import BaseClient
9
+ from botocore.exceptions import ClientError
10
+ from dateutil import parser
11
+
12
+ from regscale.core.app.utils.app_utils import create_logger
13
+
14
+ logger = create_logger()
15
+
16
+ # Error message constants
17
+ ERROR_MSG_FETCHING_AWS_RESOURCES = "Unexpected error when fetching resources from AWS: %s"
18
+
19
+
20
+ def check_finding_severity(comment: Optional[str]) -> str:
21
+ """Check the severity of the finding
22
+
23
+ :param Optional[str] comment: Comment from AWS Security Hub finding
24
+ :return: Severity of the finding
25
+ :rtype: str
26
+ """
27
+ result = ""
28
+ match = re.search(r"(?<=Finding Severity: ).*", comment)
29
+ if match:
30
+ severity = match.group()
31
+ result = severity # Output: "High"
32
+ return result
33
+
34
+
35
+ def get_due_date(earliest_date_performed: datetime, days: int) -> datetime:
36
+ """Returns the due date for an issue
37
+
38
+ :param datetime earliest_date_performed: Earliest date performed
39
+ :param int days: Days to add to the earliest date performed
40
+ :return: Due date
41
+ :rtype: datetime
42
+ """
43
+ fmt = "%Y-%m-%dT%H:%M:%S.%fZ"
44
+ try:
45
+ due_date = datetime.strptime(earliest_date_performed, fmt) + timedelta(days=days)
46
+ except ValueError:
47
+ # Try to determine the date format from a string
48
+ due_date = parser.parse(earliest_date_performed) + timedelta(days)
49
+ return due_date
50
+
51
+
52
+ def determine_status_and_results(finding: Any) -> Tuple[str, Optional[str]]:
53
+ """
54
+ Determine Status and Results
55
+
56
+ :param Any finding: AWS Finding
57
+ :return: Status and Results
58
+ :rtype: Tuple[str, Optional[str]]
59
+ """
60
+ status = "Pass"
61
+ results = None
62
+ if "Compliance" in finding.keys():
63
+ status = "Fail" if finding["Compliance"]["Status"] == "FAILED" else "Pass"
64
+ results = ", ".join(finding.get("Compliance", {}).get("RelatedRequirements", [])) or "N/A"
65
+ if "FindingProviderFields" in finding.keys():
66
+ status = (
67
+ "Fail"
68
+ if finding.get("FindingProviderFields", {}).get("Severity", {}).get("Label", "")
69
+ in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
70
+ else "Pass"
71
+ )
72
+ if "PatchSummary" in finding.keys() and not results:
73
+ results = (
74
+ f"{finding.get('PatchSummary', {}).get('MissingCount', 0)} Missing Patch(s) of "
75
+ "{finding.get('PatchSummary', {}).get('InstalledCount', 0)}"
76
+ )
77
+ return status, results
78
+
79
+
80
+ def get_comments(finding: dict) -> str:
81
+ """
82
+ Get Comments
83
+
84
+ :param dict finding: AWS Finding
85
+ :return: Comments
86
+ :rtype: str
87
+ """
88
+ try:
89
+ return (
90
+ finding["Remediation"]["Recommendation"]["Text"]
91
+ + "<br></br>"
92
+ + finding["Remediation"]["Recommendation"]["Url"]
93
+ + "<br></br>"
94
+ + f"""Finding Severity: {finding["FindingProviderFields"]["Severity"]["Label"]}"""
95
+ )
96
+ except KeyError:
97
+ return "No remediation recommendation available"
98
+
99
+
100
+ def fetch_aws_findings(
101
+ aws_client: BaseClient, minimum_severity: Optional[str] = None, posture_management_only: bool = False
102
+ ) -> list:
103
+ """Fetch AWS Findings with optimized rate limiting and pagination
104
+
105
+ :param BaseClient aws_client: AWS Security Hub Client
106
+ :param Optional[str] minimum_severity: Minimum severity to filter (CRITICAL, HIGH, MEDIUM, LOW, INFORMATIONAL)
107
+ :param bool posture_management_only: If True, only fetch posture management findings (compliance checks)
108
+ :return: AWS Findings
109
+ :rtype: list
110
+ """
111
+ findings = []
112
+ try:
113
+ # Use optimized SecurityHubPuller for better performance
114
+ from regscale.integrations.commercial.aws.security_hub import SecurityHubPuller
115
+
116
+ # Extract region from the client
117
+ region = aws_client.meta.region_name
118
+
119
+ # Create SecurityHubPuller with same region as the client
120
+ puller = SecurityHubPuller(region_name=region)
121
+
122
+ # Use existing client instead of creating new one to maintain credentials
123
+ puller.client = aws_client
124
+
125
+ # Fetch all findings with optimized pagination and rate limiting
126
+ logger.info("Using optimized SecurityHubPuller for findings retrieval...")
127
+
128
+ # Determine severity labels if minimum_severity is provided
129
+ severity_labels = None
130
+ if minimum_severity:
131
+ severity_labels = SecurityHubPuller.get_severity_filters_from_minimum(minimum_severity)
132
+ logger.info(f"Applying minimum severity filter '{minimum_severity}': {severity_labels}")
133
+
134
+ # Fetch findings based on type requested
135
+ if posture_management_only:
136
+ logger.info("Fetching posture management findings only (security standards compliance checks)")
137
+ findings = puller.get_posture_management_findings(severity_labels=severity_labels)
138
+ elif severity_labels:
139
+ findings = puller.get_findings_by_severity(severity_labels=severity_labels)
140
+ else:
141
+ findings = puller.get_all_findings_with_retries()
142
+
143
+ logger.info(f"Successfully fetched {len(findings)} findings with rate limiting")
144
+
145
+ except ImportError:
146
+ # Fallback to original method if SecurityHubPuller not available
147
+ logger.warning("SecurityHubPuller not available, falling back to basic client")
148
+ findings = fallback_fetch_aws_findings(aws_client)
149
+ except ClientError as cex:
150
+ logger.error("Unexpected error: %s", cex)
151
+ except Exception as e:
152
+ logger.error("Error using SecurityHubPuller, falling back to basic client: %s", e)
153
+ findings = fallback_fetch_aws_findings(aws_client)
154
+
155
+ return findings
156
+
157
+
158
+ def fallback_fetch_aws_findings(aws_client: BaseClient) -> list:
159
+ """Fallback method to fetch AWS Findings without pagination
160
+
161
+ :param BaseClient aws_client: AWS Security Hub Client
162
+ :return: AWS Findings
163
+ :rtype: list
164
+ """
165
+ findings = []
166
+ try:
167
+ response = aws_client.get_findings()
168
+ findings = response.get("Findings", [])
169
+ except ClientError as cex:
170
+ create_logger().error(ERROR_MSG_FETCHING_AWS_RESOURCES, cex)
171
+ return findings
172
+
173
+
174
+ def fetch_aws_findings_v2(aws_client: BaseClient) -> list:
175
+ """Fetch AWS Findings
176
+
177
+ :param BaseClient aws_client: AWS Security Hub Client
178
+ :return: AWS Findings
179
+ :rtype: list
180
+ """
181
+ findings = []
182
+ try:
183
+ response = aws_client.get_findings_v2()
184
+ findings = response.get("Findings", [])
185
+ except ClientError as cex:
186
+ create_logger().error(ERROR_MSG_FETCHING_AWS_RESOURCES, cex)
187
+ return findings
188
+
189
+
190
+ def fetch_aws_resources(aws_client: BaseClient) -> list:
191
+ """Fetch AWS Resources
192
+
193
+ :param BaseClient aws_client: AWS Security Hub Client
194
+ :return: AWS Resources
195
+ :rtype: list
196
+ """
197
+ resources = []
198
+ try:
199
+ response = aws_client.get_resources_v2()
200
+ resources = response.get("Resources", [])
201
+ logger.info(f"Fetched {len(resources)} resources from Security Hub")
202
+ except ClientError as cex:
203
+ create_logger().error(ERROR_MSG_FETCHING_AWS_RESOURCES, cex)
204
+ return resources