regscale-cli 6.27.1.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 (53) 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 +41 -7
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/integrations/commercial/aws/scanner.py +7 -3
  10. regscale/integrations/commercial/microsoft_defender/defender_api.py +1 -1
  11. regscale/integrations/commercial/sicura/api.py +65 -29
  12. regscale/integrations/commercial/sicura/scanner.py +36 -7
  13. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  14. regscale/integrations/commercial/tenablev2/commands.py +4 -4
  15. regscale/integrations/commercial/tenablev2/scanner.py +1 -2
  16. regscale/integrations/commercial/wizv2/scanner.py +40 -16
  17. regscale/integrations/control_matcher.py +78 -23
  18. regscale/integrations/public/cci_importer.py +400 -9
  19. regscale/integrations/public/csam/csam.py +572 -763
  20. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  21. regscale/integrations/public/csam/csam_common.py +154 -0
  22. regscale/integrations/public/csam/csam_controls.py +432 -0
  23. regscale/integrations/public/csam/csam_poam.py +124 -0
  24. regscale/integrations/public/fedramp/click.py +17 -4
  25. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  26. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  27. regscale/integrations/scanner_integration.py +16 -1
  28. regscale/models/integration_models/aqua.py +2 -2
  29. regscale/models/integration_models/cisa_kev_data.json +121 -18
  30. regscale/models/integration_models/flat_file_importer/__init__.py +4 -6
  31. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  32. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
  33. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  34. regscale/models/platform.py +3 -0
  35. regscale/models/regscale_models/__init__.py +5 -0
  36. regscale/models/regscale_models/component.py +1 -1
  37. regscale/models/regscale_models/control_implementation.py +55 -24
  38. regscale/models/regscale_models/organization.py +3 -0
  39. regscale/models/regscale_models/regscale_model.py +17 -5
  40. regscale/models/regscale_models/security_plan.py +1 -0
  41. regscale/regscale.py +11 -1
  42. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
  43. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +53 -49
  44. tests/regscale/core/test_login.py +171 -4
  45. tests/regscale/integrations/commercial/test_sicura.py +0 -1
  46. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +86 -0
  47. tests/regscale/integrations/public/test_cci.py +596 -1
  48. tests/regscale/integrations/test_control_matcher.py +24 -0
  49. tests/regscale/models/test_control_implementation.py +118 -3
  50. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
  51. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
  52. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
  53. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.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.
@@ -706,16 +706,20 @@ Description: {description if isinstance(description, str) else ''}"""
706
706
 
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
- region = kwargs.get("region") or os.getenv("AWS_REGION", "us-east-1")
710
- if not aws_secret_key_id or not aws_secret_access_key:
709
+ region = kwargs.get("region") or os.getenv("AWS_REGION")
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."
714
717
  )
715
718
  if not region:
716
719
  logger.warning("AWS region not provided. Defaulting to 'us-east-1'.")
720
+ region = "us-east-1"
717
721
  session = boto3.Session(
718
- region_name=kwargs.get(region, "us-east-1"),
722
+ region_name=region,
719
723
  aws_access_key_id=aws_secret_key_id,
720
724
  aws_secret_access_key=aws_secret_access_key,
721
725
  aws_session_token=kwargs.get("aws_session_token"),
@@ -353,7 +353,7 @@ class DefenderApi:
353
353
 
354
354
  data = self.get_items_from_azure(url=url, parse_value=kwargs.get("parse_value", True))
355
355
  save_path = Path(
356
- os.path.join(ENTRA_SAVE_DIR, f"azure_entra_{endpoint_key}_{get_current_datetime('%Y%m%d')}.csv")
356
+ os.path.join(ENTRA_SAVE_DIR, f"azure_entra_{endpoint_key}_{get_current_datetime('%Y%m%d')}.xlsx")
357
357
  )
358
358
  save_data_to(file=save_path, data=data, transpose_data=False)
359
359
  return [save_path]
@@ -203,7 +203,6 @@ class SicuraAPI:
203
203
  FILTER_TYPE = "filter[type]"
204
204
  FILTER_REJECTED = "filter[rejected]"
205
205
  FILTER_TASK_ID = "filter[task_id]"
206
- csrf_token: Optional[str] = None
207
206
 
208
207
  def __init__(self):
209
208
  """
@@ -246,12 +245,9 @@ class SicuraAPI:
246
245
 
247
246
  if data:
248
247
  self.session.headers["Content-Type"] = "application/json"
249
- if not endpoint.endswith("/auth/token"):
250
- if not self.csrf_token:
251
- self.csrf_token = self.get_csrf_token()
252
- if self.csrf_token:
253
- self.session.headers["X-CSRF-TOKEN"] = str(self.csrf_token)
254
- self.session.headers["auth-token-signature"] = SicuraVariables.sicuraToken
248
+
249
+ # Always set the auth token signature for API authentication
250
+ self.session.headers["auth-token-signature"] = SicuraVariables.sicuraToken
255
251
 
256
252
  try:
257
253
  # Use the session object to maintain cookies between requests
@@ -313,22 +309,6 @@ class SicuraAPI:
313
309
  logging.error(f"Error validating response: {e}", exc_info=True)
314
310
  return None
315
311
 
316
- def get_csrf_token(self) -> Optional[AuthResponse]:
317
- """
318
- Get authentication token from Sicura API.
319
-
320
- :return: Authentication response
321
- :rtype: Optional[AuthResponse]
322
- :raises requests.exceptions.RequestException: If the request fails
323
- """
324
- try:
325
- response = self._make_request("GET", "/auth/token")
326
- self.csrf_token = response
327
- return response
328
- except requests.exceptions.RequestException as e:
329
- logger.error(f"Error getting authentication token: {e}", exc_info=True)
330
- raise
331
-
332
312
  class Device(SicuraModel):
333
313
  """Model for Sicura device information."""
334
314
 
@@ -384,11 +364,56 @@ class SicuraAPI:
384
364
  logger.error(f"Failed to get devices: {e}", exc_info=True)
385
365
  return []
386
366
 
367
+ def create_or_update_control_profile(self, profile_name: str, controls: list[dict]) -> Optional[dict]:
368
+ """
369
+ Create or update a control profile.
370
+
371
+ :param str profile_name: Name of the control profile
372
+ :param list[dict] controls: List of controls to add to the profile
373
+ :return: The control profile if successfully created or updated, None otherwise
374
+ :rtype: Optional[dict]
375
+ """
376
+ profile_id = None
377
+ profile_data = None
378
+ params = {"verbose": "true"}
379
+ try:
380
+ # see if the profile already exists
381
+ response = self._make_request("GET", "/api/jaeger/v1/control_profiles", params=params)
382
+ for profile in response:
383
+ if profile["name"] == profile_name:
384
+ profile_id = profile["id"]
385
+ break
386
+ payload = {
387
+ "name": profile_name,
388
+ "description": f"Profile for {profile_name} with {len(controls)} controls.",
389
+ "controls": controls,
390
+ }
391
+ if not profile_id:
392
+ crud_operation = "Created"
393
+ response = self._make_request("POST", "/api/jaeger/v1/control_profiles", data=payload, params=params)
394
+ profile_id = response
395
+ profile_data = self._make_request("GET", f"/api/jaeger/v1/control_profiles/{profile_id}", params=params)
396
+ else:
397
+ crud_operation = "Updated"
398
+ response = self._make_request(
399
+ "PUT", f"/api/jaeger/v1/control_profiles/{profile_id}", data=payload, params=params
400
+ )
401
+ profile_id = response["id"]
402
+ profile_data = response
403
+ logger.info(f"{crud_operation} control profile #{profile_id} in Sicura with {len(controls)} controls.")
404
+ return profile_data
405
+
406
+ return profile_id
407
+ except Exception as e:
408
+ logger.error(f"Failed to create or update control profile: {e}", exc_info=True)
409
+ return None
410
+
387
411
  def create_scan_task(
388
412
  self,
389
413
  device_id: int,
390
414
  platform: str,
391
- profile: SicuraProfile,
415
+ profile: Union[SicuraProfile, str],
416
+ author: Optional[str] = None,
392
417
  task_name: Optional[str] = None,
393
418
  scheduled_time: Optional[datetime.datetime] = None,
394
419
  ) -> Optional[str]:
@@ -398,6 +423,7 @@ class SicuraAPI:
398
423
  :param int device_id: ID of the device to scan
399
424
  :param str platform: Platform name (e.g., 'Red Hat Enterprise Linux 9')
400
425
  :param SicuraProfile profile: Scan profile name (e.g., 'I - Mission Critical Classified')
426
+ :param Optional[str] author: Author of the scan task (default: None)
401
427
  :param Optional[str] task_name: Name for the scan task (default: auto-generated)
402
428
  :param Optional[datetime.datetime] scheduled_time: When to run the scan (default: now)
403
429
  :return: Task ID if successful, None otherwise
@@ -425,6 +451,9 @@ class SicuraAPI:
425
451
  "scanAttributes": {"platform": platform, "profile": profile},
426
452
  }
427
453
 
454
+ if author:
455
+ payload["scanAttributes"]["author"] = author
456
+
428
457
  result = self._make_request("POST", "/api/jaeger/v1/tasks/", data=payload)
429
458
 
430
459
  if result:
@@ -483,14 +512,16 @@ class SicuraAPI:
483
512
  self,
484
513
  fqdn: str,
485
514
  platform: Optional[str] = None,
486
- profile: SicuraProfile = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
515
+ profile: Union[SicuraProfile, str] = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
516
+ author: Optional[str] = None,
487
517
  ) -> Optional[ScanReport]:
488
518
  """
489
519
  Get scan results for a specific device.
490
520
 
491
521
  :param str fqdn: Fully qualified domain name of the device
492
522
  :param Optional[str] platform: Platform name to filter results (e.g., 'Red Hat Enterprise Linux 9')
493
- :param SicuraProfile profile: Profile name to filter results (e.g., 'I - Mission Critical Classified')
523
+ :param Union[SicuraProfile, str] profile: Profile name to filter results, defaults to I - Mission Critical Classified
524
+ :param Optional[str] author: Author of the scan task (default: None)
494
525
  :return: Scan report containing device info and scan results, or None if not found
495
526
  :rtype: Optional[ScanReport]
496
527
  """
@@ -503,6 +534,9 @@ class SicuraAPI:
503
534
  if profile:
504
535
  params["profile"] = profile
505
536
 
537
+ if author:
538
+ params["author"] = author
539
+
506
540
  response = self._make_request("GET", "/api/jaeger/v1/nodes", params=params)
507
541
 
508
542
  # Handle 404 or empty response
@@ -560,7 +594,8 @@ class SicuraAPI:
560
594
  task_id: Union[int, str],
561
595
  fqdn: str,
562
596
  platform: Optional[str] = None,
563
- profile: SicuraProfile = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
597
+ profile: Union[SicuraProfile, str] = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
598
+ author: Optional[str] = None,
564
599
  max_wait_time: int = 600,
565
600
  poll_interval: int = 10,
566
601
  ) -> Optional[Union[ScanReport, Dict[str, Any]]]:
@@ -570,7 +605,8 @@ class SicuraAPI:
570
605
  :param Union[int, str] task_id: ID of the scan task to monitor
571
606
  :param str fqdn: Fully qualified domain name of the device
572
607
  :param Optional[str] platform: Platform name to filter results
573
- :param SicuraProfile profile: Profile name to filter results
608
+ :param Union[SicuraProfile, str] profile: Profile name to filter results, defaults to I - Mission Critical Classified
609
+ :param Optional[str] author: Author of the scan task (default: None)
574
610
  :param int max_wait_time: Maximum time to wait in seconds (default: 10 minutes)
575
611
  :param int poll_interval: Time between status checks in seconds (default: 10 seconds)
576
612
  :return: Scan results once the task is complete, or None if timeout or error
@@ -602,7 +638,7 @@ class SicuraAPI:
602
638
  logger.info(f"Scan task {task_id} completed successfully, fetching results...")
603
639
  # Wait a moment for results to be processed
604
640
  time.sleep(2)
605
- return self.get_scan_results(fqdn, platform, profile)
641
+ return self.get_scan_results(fqdn=fqdn, platform=platform, profile=profile, author=author)
606
642
  else:
607
643
  logger.error(f"Scan task {task_id} ended with status {latest_status}")
608
644
  return None
@@ -4,7 +4,7 @@
4
4
  RegScale Sicura Integration
5
5
  """
6
6
  import datetime
7
- from typing import Generator, Iterator, Any
7
+ from typing import Any, Generator, Iterator, Union
8
8
 
9
9
  from regscale.core.utils.date import date_str
10
10
  from regscale.integrations.commercial.sicura.api import SicuraAPI, ScanReport, SicuraProfile, Device, ScanResult
@@ -55,6 +55,8 @@ class SicuraIntegration(ScannerIntegration):
55
55
  """
56
56
  super().__init__(*args, **kwargs)
57
57
  self.api = SicuraAPI()
58
+ self.control_scan = False
59
+ self.control_scan_profile = None
58
60
 
59
61
  def fetch_findings(self, **kwargs) -> Generator[IntegrationFinding, None, None]:
60
62
  """
@@ -103,23 +105,28 @@ class SicuraIntegration(ScannerIntegration):
103
105
  return
104
106
 
105
107
  # Profiles to scan
106
- profiles = [SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED, SicuraProfile.LEVEL_1_SERVER]
108
+ profiles = [SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED]
109
+ if self.control_scan:
110
+ profiles = [self.control_scan_profile]
107
111
 
108
112
  for profile in profiles:
109
113
  yield from self._process_profile_findings(device, profile)
110
114
 
111
115
  def _process_profile_findings(
112
- self, device: Device, profile: SicuraProfile
116
+ self, device: Device, profile: Union[SicuraProfile, str]
113
117
  ) -> Generator[IntegrationFinding, None, None]:
114
118
  """
115
119
  Process findings for a device with a specific profile
116
120
 
117
121
  :param Device device: The device to process
118
- :param SicuraProfile profile: The profile to process
122
+ :param Union[SicuraProfile, str] profile: The profile to process
119
123
  :yield: IntegrationFinding objects
120
124
  :rtype: Generator[IntegrationFinding, None, None]
121
125
  """
122
- scan_report = self.api.get_scan_results(fqdn=device.fqdn, profile=profile)
126
+ if self.control_scan and profile == self.control_scan_profile:
127
+ scan_report = self.api.get_scan_results(fqdn=device.fqdn, profile=profile, author="control")
128
+ else:
129
+ scan_report = self.api.get_scan_results(fqdn=device.fqdn, profile=profile)
123
130
 
124
131
  if not scan_report:
125
132
  logger.warning(f"No scan results found for device: {device.fqdn} with profile: {profile}")
@@ -458,17 +465,21 @@ class SicuraIntegration(ScannerIntegration):
458
465
  :param Device device: The device to trigger a scan for
459
466
  :return: None
460
467
  """
468
+ profile = self.control_scan_profile if self.control_scan else SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED
469
+ author = "control" if self.control_scan else None
461
470
  task_id = self.api.create_scan_task(
462
471
  device_id=device.id,
463
472
  platform=device.platforms,
464
- profile=SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
473
+ profile=profile,
474
+ author=author,
465
475
  )
466
476
  if task_id:
467
477
  self.api.wait_for_scan_results(
468
478
  task_id=task_id,
469
479
  fqdn=device.fqdn,
470
480
  platform=device.platforms,
471
- profile=SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
481
+ profile=profile,
482
+ author=author,
472
483
  )
473
484
  else:
474
485
  logger.warning(f"Failed to create scan task for device {device.fqdn}")
@@ -480,6 +491,24 @@ class SicuraIntegration(ScannerIntegration):
480
491
  :param list[Device] devices: The devices to trigger scans for
481
492
  :return: None
482
493
  """
494
+ # get the SSP's controlImplementations
495
+ if control_imps := regscale_models.ControlImplementation.get_list_by_parent(
496
+ regscale_id=self.plan_id, regscale_module=regscale_models.SecurityPlan.get_module_slug()
497
+ ):
498
+ if profile := self.api.create_or_update_control_profile(
499
+ profile_name=f"regscale_ssp_id_{self.plan_id}",
500
+ controls=control_imps,
501
+ ):
502
+ self.control_scan = True
503
+ self.control_scan_profile = profile["name"]
504
+ else:
505
+ logger.warning("Failed to create or update control profile")
506
+ self.control_scan = False
507
+ self.control_scan_profile = None
508
+ else:
509
+ self.control_scan = False
510
+ self.control_scan_profile = None
511
+
483
512
  if len(devices) > 1:
484
513
  from regscale.utils.threading import ThreadManager
485
514
 
@@ -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
 
@@ -154,8 +154,8 @@ def io_sync_assets(regscale_ssp_id: int, tags: List[Tuple[str, str]] = None):
154
154
  try:
155
155
  from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
156
156
 
157
- integration = TenableIntegration(plan_id=regscale_ssp_id, tags=tags)
158
- integration.sync_assets(plan_id=regscale_ssp_id)
157
+ integration = TenableIntegration(plan_id=regscale_ssp_id)
158
+ integration.sync_assets(plan_id=regscale_ssp_id, tags=tags)
159
159
 
160
160
  console.print("[bold green]Tenable.io asset synchronization complete.[/bold green]")
161
161
  except Exception as e:
@@ -193,8 +193,8 @@ def io_sync_findings(
193
193
  try:
194
194
  from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
195
195
 
196
- integration = TenableIntegration(plan_id=regscale_ssp_id, tags=tags, scan_date=scan_date)
197
- integration.sync_findings(plan_id=regscale_ssp_id, severity=severity)
196
+ integration = TenableIntegration(plan_id=regscale_ssp_id, scan_date=scan_date)
197
+ integration.sync_findings(plan_id=regscale_ssp_id, severity=severity, tags=tags)
198
198
 
199
199
  console.print("[bold green]Tenable.io finding synchronization complete.[/bold green]")
200
200
  except Exception as e:
@@ -44,7 +44,7 @@ class TenableIntegration(ScannerIntegration):
44
44
  "low": regscale_models.IssueSeverity.Low,
45
45
  }
46
46
 
47
- def __init__(self, plan_id: int, tenant_id: int = 1, tags: Optional[List[str]] = None, **kwargs):
47
+ def __init__(self, plan_id: int, tenant_id: int = 1, **kwargs):
48
48
  """
49
49
  Initialize the TenableIntegration.
50
50
 
@@ -53,7 +53,6 @@ class TenableIntegration(ScannerIntegration):
53
53
  """
54
54
  super().__init__(plan_id, tenant_id, **kwargs)
55
55
  self.client = None
56
- self.tags = tags or []
57
56
  self.scan_date = kwargs.get("scan_date", get_current_datetime())
58
57
 
59
58
  def authenticate(self) -> None: