regscale-cli 6.27.2.0__py3-none-any.whl → 6.27.3.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 (40) 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/login.py +21 -4
  7. regscale/core/utils/date.py +77 -1
  8. regscale/integrations/commercial/aws/scanner.py +4 -1
  9. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  10. regscale/integrations/control_matcher.py +78 -23
  11. regscale/integrations/public/csam/csam.py +572 -763
  12. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  13. regscale/integrations/public/csam/csam_common.py +154 -0
  14. regscale/integrations/public/csam/csam_controls.py +432 -0
  15. regscale/integrations/public/csam/csam_poam.py +124 -0
  16. regscale/integrations/public/fedramp/click.py +17 -4
  17. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  18. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  19. regscale/integrations/scanner_integration.py +16 -1
  20. regscale/models/integration_models/cisa_kev_data.json +49 -19
  21. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  22. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
  23. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  24. regscale/models/platform.py +3 -0
  25. regscale/models/regscale_models/__init__.py +5 -0
  26. regscale/models/regscale_models/component.py +1 -1
  27. regscale/models/regscale_models/control_implementation.py +55 -24
  28. regscale/models/regscale_models/organization.py +3 -0
  29. regscale/models/regscale_models/regscale_model.py +17 -5
  30. regscale/models/regscale_models/security_plan.py +1 -0
  31. regscale/regscale.py +11 -1
  32. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
  33. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +40 -36
  34. tests/regscale/core/test_login.py +171 -4
  35. tests/regscale/integrations/test_control_matcher.py +24 -0
  36. tests/regscale/models/test_control_implementation.py +118 -3
  37. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
  38. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
  39. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
  40. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/top_level.txt +0 -0
@@ -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.
@@ -707,7 +707,10 @@ Description: {description if isinstance(description, str) else ''}"""
707
707
  aws_secret_key_id = kwargs.get("aws_access_key_id") or os.getenv("AWS_ACCESS_KEY_ID")
708
708
  aws_secret_access_key = kwargs.get("aws_secret_access_key") or os.getenv("AWS_SECRET_ACCESS_KEY")
709
709
  region = kwargs.get("region") or os.getenv("AWS_REGION")
710
- if not aws_secret_key_id or not aws_secret_access_key:
710
+ profile = kwargs.get("profile")
711
+
712
+ # Profile or credentials required
713
+ if not profile and (not aws_secret_key_id or not aws_secret_access_key):
711
714
  raise ValueError(
712
715
  "AWS Access Key ID and Secret Access Key are required.\nPlease update in environment "
713
716
  "variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or pass as arguments."
@@ -353,13 +353,16 @@ def _display_filter_result(provider: str, filters: List[str]):
353
353
  for i, filter_str in enumerate(filters, 1):
354
354
  click.echo(f" {i}. {filter_str}")
355
355
 
356
+ connector = provider.split("_")[0]
357
+ filter_flag = "asset_filter" if connector == "vulnerabilities" else "filter"
358
+
356
359
  click.echo("\nComplete command:")
357
360
  provider_name = provider.replace(provider.split("_")[0] + "_", "")
358
361
 
359
362
  # Build command with semicolon-separated filters
360
363
  if filters:
361
364
  filter_string = ";".join(filters)
362
- filter_option = f'--filter "{filter_string}"'
365
+ filter_option = f'--{filter_flag} "{filter_string}"'
363
366
  else:
364
367
  filter_option = ""
365
368
 
@@ -7,7 +7,7 @@ across different RegScale entities based on control ID strings.
7
7
 
8
8
  import logging
9
9
  import re
10
- from typing import Dict, List, Optional, Tuple, Union
10
+ from typing import Dict, List, Optional, Tuple
11
11
 
12
12
  from regscale.core.app.api import Api
13
13
  from regscale.core.app.application import Application
@@ -42,14 +42,15 @@ class ControlMatcher:
42
42
  self._catalog_cache: Dict[int, List[SecurityControl]] = {}
43
43
  self._control_impl_cache: Dict[Tuple[int, str], Dict[str, ControlImplementation]] = {}
44
44
 
45
- def parse_control_id(self, control_string: str) -> Optional[str]:
45
+ @staticmethod
46
+ def parse_control_id(control_string: str) -> Optional[str]:
46
47
  """
47
48
  Parse a control ID string and extract the standardized control identifier.
48
49
 
49
50
  Handles various formats:
50
- - NIST format: AC-1, AC-1(1), AC-1.1
51
+ - NIST format: AC-1, AC-1(1), AC-1.1, AC-1(a), AC-1.a
51
52
  - With leading zeros: AC-01, AC-17(02)
52
- - With spaces: AC-1 (1), AC-02 (04)
53
+ - With spaces: AC-1 (1), AC-02 (04), AC-1 (a)
53
54
  - With text: "Access Control AC-1"
54
55
  - Multiple controls: "AC-1, AC-2"
55
56
 
@@ -64,12 +65,12 @@ class ControlMatcher:
64
65
  control_string = control_string.strip().upper()
65
66
 
66
67
  # Common NIST control ID pattern
67
- # Matches: AC-1, AC-01, AC-1(1), AC-1(01), AC-1 (1), AC-1.1, AC-1.01, etc.
68
+ # Matches: AC-1, AC-01, AC-1(1), AC-1(01), AC-1 (1), AC-1.1, AC-1.01, AC-1(a), AC-1.a, etc.
68
69
  # Allows optional whitespace before and inside parentheses
69
- pattern = r"([A-Z]{2,3}-\d+(?:\s*\(\s*\d+\s*\)|\.\d+)?)"
70
+ # Now includes letter parts (a, b, c) in addition to numeric parts
71
+ pattern = r"([A-Z]{2,3}-\d+(?:\s*\(\s*(?:\d+|[A-Z])\s*\)|\.(?:\d+|[A-Z]))?)"
70
72
 
71
- matches = re.findall(pattern, control_string)
72
- if matches:
73
+ if matches := re.findall(pattern, control_string):
73
74
  # Normalize parentheses to dots for consistency and remove spaces
74
75
  control_id = matches[0]
75
76
  control_id = control_id.replace(" ", "") # Remove all spaces
@@ -84,7 +85,9 @@ class ControlMatcher:
84
85
  if "." in number_part:
85
86
  main_num, enhancement = number_part.split(".", 1)
86
87
  main_num = str(int(main_num))
87
- enhancement = str(int(enhancement))
88
+ # Only normalize if enhancement is numeric, preserve letters as-is
89
+ if enhancement.isdigit():
90
+ enhancement = str(int(enhancement))
88
91
  control_id = f"{family}-{main_num}.{enhancement}"
89
92
  else:
90
93
  main_num = str(int(number_part))
@@ -295,6 +298,64 @@ class ControlMatcher:
295
298
  main_num = str(int(number_part))
296
299
  return f"{family}-{main_num}"
297
300
 
301
+ @staticmethod
302
+ def _generate_simple_variations(family: str, main_num: str) -> set:
303
+ """
304
+ Generate variations for simple control IDs without enhancements.
305
+
306
+ :param str family: Control family (e.g., AC, SI)
307
+ :param str main_num: Main control number
308
+ :return: Set of variations
309
+ :rtype: set
310
+ """
311
+ main_int = int(main_num)
312
+ return {
313
+ f"{family}-{main_int}",
314
+ f"{family}-{main_int:02d}",
315
+ }
316
+
317
+ @staticmethod
318
+ def _generate_letter_enhancement_variations(family: str, main_num: str, enhancement: str) -> set:
319
+ """
320
+ Generate variations for control IDs with letter-based enhancements.
321
+
322
+ :param str family: Control family (e.g., AC, SI)
323
+ :param str main_num: Main control number
324
+ :param str enhancement: Letter enhancement (e.g., a, b)
325
+ :return: Set of variations
326
+ :rtype: set
327
+ """
328
+ main_int = int(main_num)
329
+ variations = set()
330
+
331
+ for main_fmt in [str(main_int), f"{main_int:02d}"]:
332
+ variations.add(f"{family}-{main_fmt}.{enhancement}")
333
+ variations.add(f"{family}-{main_fmt}({enhancement})")
334
+
335
+ return variations
336
+
337
+ @staticmethod
338
+ def _generate_numeric_enhancement_variations(family: str, main_num: str, enhancement: str) -> set:
339
+ """
340
+ Generate variations for control IDs with numeric enhancements.
341
+
342
+ :param str family: Control family (e.g., AC, SI)
343
+ :param str main_num: Main control number
344
+ :param str enhancement: Numeric enhancement
345
+ :return: Set of variations
346
+ :rtype: set
347
+ """
348
+ main_int = int(main_num)
349
+ enh_int = int(enhancement)
350
+ variations = set()
351
+
352
+ for main_fmt in [str(main_int), f"{main_int:02d}"]:
353
+ for enh_fmt in [str(enh_int), f"{enh_int:02d}"]:
354
+ variations.add(f"{family}-{main_fmt}.{enh_fmt}")
355
+ variations.add(f"{family}-{main_fmt}({enh_fmt})")
356
+
357
+ return variations
358
+
298
359
  def _get_control_id_variations(self, control_id: str) -> set:
299
360
  """
300
361
  Generate all valid variations of a control ID (with and without leading zeros).
@@ -302,6 +363,7 @@ class ControlMatcher:
302
363
  Examples:
303
364
  - AC-1 -> {AC-1, AC-01}
304
365
  - AC-17.2 -> {AC-17.2, AC-17.02, AC-17(2), AC-17(02)}
366
+ - AC-1.a -> {AC-1.a, AC-01.a, AC-1(a), AC-01(a)}
305
367
 
306
368
  :param str control_id: The control ID to generate variations for
307
369
  :return: Set of all valid variations
@@ -311,8 +373,6 @@ class ControlMatcher:
311
373
  if not parsed:
312
374
  return set()
313
375
 
314
- variations = set()
315
-
316
376
  # Split by '-' to get family and number parts
317
377
  parts = parsed.split("-")
318
378
  if len(parts) != 2:
@@ -324,19 +384,14 @@ class ControlMatcher:
324
384
  # Handle enhancement notation
325
385
  if "." in number_part:
326
386
  main_num, enhancement = number_part.split(".", 1)
327
- main_int = int(main_num)
328
- enh_int = int(enhancement)
329
-
330
- # Generate all combinations: with/without leading zeros, dot/parenthesis notation
331
- for main_fmt in [str(main_int), f"{main_int:02d}"]:
332
- for enh_fmt in [str(enh_int), f"{enh_int:02d}"]:
333
- variations.add(f"{family}-{main_fmt}.{enh_fmt}")
334
- variations.add(f"{family}-{main_fmt}({enh_fmt})")
387
+
388
+ # Check if enhancement is a letter (a, b, c, etc.) or a number
389
+ if enhancement.isalpha():
390
+ variations = self._generate_letter_enhancement_variations(family, main_num, enhancement)
391
+ else:
392
+ variations = self._generate_numeric_enhancement_variations(family, main_num, enhancement)
335
393
  else:
336
- # Just main control number
337
- main_int = int(number_part)
338
- variations.add(f"{family}-{main_int}")
339
- variations.add(f"{family}-{main_int:02d}")
394
+ variations = self._generate_simple_variations(family, number_part)
340
395
 
341
396
  # Add uppercase versions to ensure consistency
342
397
  return {v.upper() for v in variations}