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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -0
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/login.py +4 -1
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/login.py +21 -4
- regscale/core/utils/date.py +77 -1
- regscale/integrations/commercial/aws/scanner.py +4 -1
- regscale/integrations/commercial/synqly/query_builder.py +4 -1
- regscale/integrations/control_matcher.py +78 -23
- regscale/integrations/public/csam/csam.py +572 -763
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +17 -4
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
- regscale/integrations/public/fedramp/poam/scanner.py +74 -7
- regscale/integrations/scanner_integration.py +16 -1
- regscale/models/integration_models/cisa_kev_data.json +49 -19
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
- regscale/models/platform.py +3 -0
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/component.py +1 -1
- regscale/models/regscale_models/control_implementation.py +55 -24
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +17 -5
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/regscale.py +11 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +40 -36
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/test_control_matcher.py +24 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/top_level.txt +0 -0
regscale/core/utils/date.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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'--
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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}
|