regscale-cli 6.21.0.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 (37) hide show
  1. regscale/_version.py +1 -1
  2. regscale/integrations/commercial/__init__.py +1 -2
  3. regscale/integrations/commercial/amazon/common.py +79 -2
  4. regscale/integrations/commercial/aws/cli.py +183 -9
  5. regscale/integrations/commercial/aws/scanner.py +544 -9
  6. regscale/integrations/commercial/cpe.py +18 -1
  7. regscale/integrations/commercial/tenablev2/jsonl_scanner.py +2 -1
  8. regscale/integrations/commercial/wizv2/async_client.py +10 -3
  9. regscale/integrations/commercial/wizv2/click.py +102 -26
  10. regscale/integrations/commercial/wizv2/constants.py +249 -1
  11. regscale/integrations/commercial/wizv2/issue.py +2 -2
  12. regscale/integrations/commercial/wizv2/parsers.py +3 -2
  13. regscale/integrations/commercial/wizv2/policy_compliance.py +1858 -0
  14. regscale/integrations/commercial/wizv2/scanner.py +15 -21
  15. regscale/integrations/commercial/wizv2/utils.py +258 -85
  16. regscale/integrations/commercial/wizv2/variables.py +4 -3
  17. regscale/integrations/compliance_integration.py +1455 -0
  18. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  19. regscale/integrations/public/fedramp/markdown_parser.py +7 -1
  20. regscale/integrations/scanner_integration.py +30 -2
  21. regscale/models/app_models/__init__.py +1 -0
  22. regscale/models/integration_models/cisa_kev_data.json +73 -4
  23. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  24. regscale/{integrations/commercial/wizv2/models.py → models/integration_models/wizv2.py} +4 -12
  25. regscale/models/regscale_models/file.py +4 -0
  26. regscale/models/regscale_models/issue.py +123 -0
  27. regscale/models/regscale_models/regscale_model.py +4 -2
  28. regscale/models/regscale_models/security_plan.py +1 -1
  29. regscale/utils/graphql_client.py +3 -1
  30. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.1.0.dist-info}/METADATA +9 -9
  31. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.1.0.dist-info}/RECORD +37 -34
  32. tests/regscale/core/test_version_regscale.py +5 -3
  33. tests/regscale/integrations/test_wiz_policy_compliance_affected_controls.py +154 -0
  34. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.1.0.dist-info}/LICENSE +0 -0
  35. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.1.0.dist-info}/WHEEL +0 -0
  36. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.1.0.dist-info}/entry_points.txt +0 -0
  37. {regscale_cli-6.21.0.0.dist-info → regscale_cli-6.21.1.0.dist-info}/top_level.txt +0 -0
@@ -24,9 +24,11 @@ from .inventory import AWSInventoryCollector
24
24
 
25
25
  logger = logging.getLogger("regscale")
26
26
 
27
- # Constants for file paths
27
+ # Constants for file paths:
28
28
  INVENTORY_FILE_PATH = os.path.join("artifacts", "aws", "inventory.json")
29
+ FINDINGS_FILE_PATH = os.path.join("artifacts", "aws", "findings.json")
29
30
  CACHE_TTL_SECONDS = 8 * 60 * 60 # 8 hours in seconds
31
+ EC_INSTANCES = "EC2 Instances"
30
32
 
31
33
 
32
34
  class AWSInventoryIntegration(ScannerIntegration):
@@ -56,6 +58,8 @@ class AWSInventoryIntegration(ScannerIntegration):
56
58
  """
57
59
  super().__init__(plan_id=plan_id, kwargs=kwargs)
58
60
  self.collector: Optional[AWSInventoryCollector] = None
61
+ self.discovered_assets: List[IntegrationAsset] = []
62
+ self.processed_asset_identifiers: set = set() # Track processed assets to avoid duplicates
59
63
 
60
64
  def authenticate(
61
65
  self,
@@ -199,7 +203,6 @@ class AWSInventoryIntegration(ScannerIntegration):
199
203
  :yield: Iterator[IntegrationAsset]
200
204
  """
201
205
  inventory = self.fetch_aws_data_if_needed(region, aws_access_key_id, aws_secret_access_key, aws_session_token)
202
-
203
206
  # Process each asset type using the corresponding parser
204
207
  asset_configs = self.get_asset_configs()
205
208
 
@@ -256,13 +259,13 @@ class AWSInventoryIntegration(ScannerIntegration):
256
259
  asset_type = regscale_models.AssetType.VM
257
260
  asset_category = regscale_models.AssetCategory.Hardware
258
261
  component_type = regscale_models.ComponentType.Hardware
259
- component_names = ["EC2 Instances"]
262
+ component_names = [EC_INSTANCES]
260
263
  else:
261
264
  operating_system = regscale_models.AssetOperatingSystem.Linux
262
265
  asset_type = regscale_models.AssetType.VM
263
266
  asset_category = regscale_models.AssetCategory.Hardware
264
267
  component_type = regscale_models.ComponentType.Hardware
265
- component_names = ["EC2 Instances"]
268
+ component_names = [EC_INSTANCES]
266
269
 
267
270
  os_version = image_info.get("Description", "")
268
271
 
@@ -692,8 +695,8 @@ Description: {description if isinstance(description, str) else ''}"""
692
695
 
693
696
  def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
694
697
  """
695
- Fetch security findings.
696
- Currently, we're only doing inventory collection.
698
+ Fetch security findings from AWS Security Hub.
699
+ Also discovers assets from the finding resources during processing.
697
700
 
698
701
  :yield: Iterator[IntegrationFinding]
699
702
  """
@@ -703,7 +706,7 @@ Description: {description if isinstance(description, str) else ''}"""
703
706
 
704
707
  aws_secret_key_id = kwargs.get("aws_access_key_id") or os.getenv("AWS_ACCESS_KEY_ID")
705
708
  aws_secret_access_key = kwargs.get("aws_secret_access_key") or os.getenv("AWS_SECRET_ACCESS_KEY")
706
- region = kwargs.get("region")
709
+ region = kwargs.get("region") or os.getenv("AWS_REGION", "us-east-1")
707
710
  if not aws_secret_key_id or not aws_secret_access_key:
708
711
  raise ValueError(
709
712
  "AWS Access Key ID and Secret Access Key are required.\nPlease update in environment "
@@ -719,10 +722,123 @@ Description: {description if isinstance(description, str) else ''}"""
719
722
  )
720
723
  client = session.client("securityhub")
721
724
  aws_findings = fetch_aws_findings(aws_client=client)
725
+ # Note: Resources are extracted directly from findings, so separate resource fetch not needed
726
+ # Reset discovered assets for this run
727
+ self.discovered_assets.clear()
728
+ self.processed_asset_identifiers.clear()
729
+
722
730
  self.num_findings_to_process = len(aws_findings)
723
731
  for finding in aws_findings:
724
732
  yield from iter(self.parse_finding(finding))
725
733
 
734
+ # Log discovered assets count
735
+ if self.discovered_assets:
736
+ logger.info(f"Discovered {len(self.discovered_assets)} assets from Security Hub findings")
737
+
738
+ def get_discovered_assets(self) -> Iterator[IntegrationAsset]:
739
+ """
740
+ Get assets discovered from Security Hub findings.
741
+
742
+ :return: Iterator of discovered assets
743
+ :rtype: Iterator[IntegrationAsset]
744
+ """
745
+ logger.info(f"Yielding {len(self.discovered_assets)} discovered assets from findings")
746
+ for asset in self.discovered_assets:
747
+ yield asset
748
+
749
+ def sync_findings_and_assets(self, **kwargs) -> tuple[int, int]:
750
+ """
751
+ Sync both findings and discovered assets from AWS Security Hub.
752
+ First discovers assets from findings, creates them, then processes findings.
753
+
754
+ :return: Tuple of (findings_processed, assets_processed)
755
+ :rtype: tuple[int, int]
756
+ """
757
+ logger.info("Starting AWS Security Hub findings and assets sync...")
758
+
759
+ # First, fetch findings to discover assets (but don't sync findings yet)
760
+ logger.info("Discovering assets from AWS Security Hub findings...")
761
+
762
+ # Reset discovered assets for this run
763
+ self.discovered_assets.clear()
764
+ self.processed_asset_identifiers.clear()
765
+
766
+ # Fetch findings to discover assets - store them to avoid re-fetching
767
+ findings_list = list(self.fetch_findings(**kwargs))
768
+
769
+ # Sync the discovered assets first
770
+ if self.discovered_assets:
771
+ logger.info(f"Creating {len(self.discovered_assets)} assets discovered from findings...")
772
+ self.num_assets_to_process = len(self.discovered_assets)
773
+ assets_processed = self.update_regscale_assets(self.get_discovered_assets())
774
+ logger.info(f"Successfully created {assets_processed} assets")
775
+ else:
776
+ logger.info("No assets discovered from findings")
777
+ assets_processed = 0
778
+
779
+ # Now process the findings we already fetched (avoid double-fetching)
780
+ logger.info("Now syncing findings with created assets...")
781
+ findings_processed = self.update_regscale_findings(findings_list)
782
+
783
+ return findings_processed, assets_processed
784
+
785
+ def get_configured_issue_status(self) -> IssueStatus:
786
+ """
787
+ Get the configured issue status from the configuration.
788
+
789
+ :return: The configured issue status
790
+ :rtype: IssueStatus
791
+ """
792
+ try:
793
+ configured_status = self.app.config["issues"]["amazon"]["status"]
794
+ if configured_status.lower() == "open":
795
+ return IssueStatus.Open
796
+ elif configured_status.lower() == "closed":
797
+ return IssueStatus.Closed
798
+ else:
799
+ logger.warning(f"Unknown configured status '{configured_status}', defaulting to Open")
800
+ return IssueStatus.Open
801
+ except KeyError:
802
+ logger.warning("No status configuration found for amazon issues, defaulting to Open")
803
+ return IssueStatus.Open
804
+
805
+ def should_process_finding_by_severity(self, severity: str) -> bool:
806
+ """
807
+ Check if a finding should be processed based on the configured minimum severity.
808
+
809
+ :param str severity: The severity level of the finding
810
+ :return: True if the finding should be processed, False otherwise
811
+ :rtype: bool
812
+ """
813
+ try:
814
+ min_severity = self.app.config["issues"]["amazon"]["minimumSeverity"].upper()
815
+ except KeyError:
816
+ logger.warning("No minimumSeverity configuration found for amazon issues, processing all findings")
817
+ return True
818
+
819
+ # Define severity hierarchy (higher number = more severe)
820
+ severity_levels = {
821
+ "INFORMATIONAL": 0,
822
+ "INFO": 0,
823
+ "LOW": 1,
824
+ "MEDIUM": 2,
825
+ "MODERATE": 2,
826
+ "HIGH": 3,
827
+ "CRITICAL": 4,
828
+ }
829
+
830
+ finding_severity_level = severity_levels.get(severity.upper(), 0)
831
+ min_severity_level = severity_levels.get(min_severity, 1) # Default to LOW if not found
832
+
833
+ should_process = finding_severity_level >= min_severity_level
834
+
835
+ if not should_process:
836
+ logger.debug(
837
+ f"Filtering out finding with severity '{severity}' (level {finding_severity_level}) - below minimum '{min_severity}' (level {min_severity_level})"
838
+ )
839
+
840
+ return should_process
841
+
726
842
  @staticmethod
727
843
  def get_baseline(resource: dict) -> str:
728
844
  """
@@ -763,6 +879,7 @@ Description: {description if isinstance(description, str) else ''}"""
763
879
  def parse_finding(self, finding: dict) -> list[IntegrationFinding]:
764
880
  """
765
881
  Parse AWS Security Hub to RegScale IntegrationFinding format.
882
+ Also collects assets from the finding resources for later processing.
766
883
 
767
884
  :param dict finding: AWS Security Hub finding
768
885
  :return: RegScale IntegrationFinding
@@ -771,6 +888,14 @@ Description: {description if isinstance(description, str) else ''}"""
771
888
  findings = []
772
889
  try:
773
890
  for resource in finding["Resources"]:
891
+ # Parse resource to asset and add to discovered assets (avoiding duplicates)
892
+ asset = self.parse_resource_to_asset(resource, finding)
893
+ if asset and asset.identifier not in self.processed_asset_identifiers:
894
+ self.discovered_assets.append(asset)
895
+ self.processed_asset_identifiers.add(asset.identifier)
896
+ logger.debug(f"Discovered asset from finding: {asset.name} ({asset.identifier})")
897
+
898
+ # Continue with finding processing as before
774
899
  status, results = determine_status_and_results(finding)
775
900
  comments = get_comments(finding)
776
901
  severity = check_finding_severity(comments)
@@ -779,6 +904,11 @@ Description: {description if isinstance(description, str) else ''}"""
779
904
  friendly_sev = "high"
780
905
  elif severity in ["MEDIUM", "MODERATE"]:
781
906
  friendly_sev = "moderate"
907
+
908
+ # Filter findings based on minimum severity configuration
909
+ if not self.should_process_finding_by_severity(severity):
910
+ logger.debug(f"Skipping finding with severity '{severity}' - below minimum threshold")
911
+ continue
782
912
  try:
783
913
  days = self.app.config["issues"]["amazon"][friendly_sev]
784
914
  except KeyError:
@@ -787,17 +917,23 @@ Description: {description if isinstance(description, str) else ''}"""
787
917
  due_date = datetime_str(get_due_date(date_str(finding["CreatedAt"]), days))
788
918
 
789
919
  plugin_name = next(iter(finding.get("Types", [])))
920
+ # Create a unique plugin_id using the finding ID to ensure each finding creates a separate issue
921
+ finding_id = finding.get("Id", "")
922
+ # Extract just the finding UUID from the full ARN for a cleaner ID
923
+ finding_uuid = finding_id.split("/")[-1] if "/" in finding_id else finding_id.split(":")[-1]
924
+ plugin_id = f"{plugin_name.replace(' ', '_').replace('/', '_').replace(':', '_')}_{finding_uuid}"
925
+
790
926
  findings.append(
791
927
  IntegrationFinding(
792
928
  asset_identifier=self.extract_name_from_arn(resource["Id"]),
793
- external_id=self.extract_name_from_arn(resource["Id"]),
929
+ external_id=finding_id, # Use the full finding ID as external_id for uniqueness
794
930
  control_labels=[], # Determine how to populate this
795
931
  title=finding["Title"],
796
932
  category="SecurityHub",
797
933
  issue_title=finding["Title"],
798
934
  severity=self.finding_severity_map.get(severity),
799
935
  description=finding["Description"],
800
- status=IssueStatus.Open if status == "Fail" else IssueStatus.Closed,
936
+ status=self.get_configured_issue_status(),
801
937
  checklist_status=self.get_checklist_status(status),
802
938
  vulnerability_number="",
803
939
  results=results,
@@ -809,6 +945,7 @@ Description: {description if isinstance(description, str) else ''}"""
809
945
  date_created=date_str(finding["CreatedAt"]),
810
946
  due_date=due_date,
811
947
  plugin_name=plugin_name,
948
+ plugin_id=plugin_id, # Add the sanitized plugin_id
812
949
  baseline=self.get_baseline(resource),
813
950
  observations=comments,
814
951
  gaps="",
@@ -822,3 +959,401 @@ Description: {description if isinstance(description, str) else ''}"""
822
959
  logger.error(f"Error parsing AWS Security Hub finding: {str(e)}", exc_info=True)
823
960
 
824
961
  return findings
962
+
963
+ def parse_resource_to_asset(self, resource: dict, finding: dict) -> Optional[IntegrationAsset]:
964
+ """
965
+ Parse AWS Security Hub resource to RegScale IntegrationAsset format.
966
+
967
+ :param dict resource: AWS Security Hub resource from finding
968
+ :param dict finding: AWS Security Hub finding for additional context
969
+ :return: RegScale IntegrationAsset or None if resource type not supported
970
+ :rtype: Optional[IntegrationAsset]
971
+ """
972
+ try:
973
+ resource_type = resource.get("Type", "")
974
+ resource_id = resource.get("Id", "")
975
+
976
+ if not resource_type or not resource_id:
977
+ logger.warning("Resource missing Type or Id, skipping asset creation")
978
+ return None
979
+
980
+ # Map resource types to parser methods
981
+ parser_map = {
982
+ "AwsEc2SecurityGroup": self._parse_security_group_resource,
983
+ "AwsEc2Subnet": self._parse_subnet_resource,
984
+ "AwsIamUser": self._parse_iam_user_resource,
985
+ "AwsEc2Instance": self._parse_ec2_instance_resource,
986
+ "AwsS3Bucket": self._parse_s3_bucket_resource,
987
+ "AwsRdsDbInstance": self._parse_rds_instance_resource,
988
+ "AwsLambdaFunction": self._parse_lambda_function_resource,
989
+ "AwsEcrRepository": self._parse_ecr_repository_resource,
990
+ }
991
+
992
+ parser_method = parser_map.get(resource_type)
993
+ if parser_method:
994
+ return parser_method(resource, finding)
995
+ else:
996
+ # Create a generic asset for unsupported resource types
997
+ return self._parse_generic_resource(resource)
998
+
999
+ except Exception as e:
1000
+ logger.error(f"Error parsing resource to asset: {str(e)}", exc_info=True)
1001
+ return None
1002
+
1003
+ def _parse_security_group_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1004
+ """Parse AWS EC2 Security Group resource to IntegrationAsset."""
1005
+ details = resource.get("Details", {}).get("AwsEc2SecurityGroup", {})
1006
+ resource_id = resource.get("Id", "")
1007
+ region = resource.get("Region", "us-east-1")
1008
+ # Tags also available in details
1009
+
1010
+ # Extract security group ID from ARN
1011
+ sg_id = self.extract_name_from_arn(resource_id) or details.get("GroupId", "")
1012
+ group_name = details.get("GroupName", sg_id)
1013
+
1014
+ name = f"Security Group: {group_name}"
1015
+ description = f"AWS EC2 Security Group {group_name} ({sg_id})"
1016
+
1017
+ # Build notes with security group rules
1018
+ notes_parts = []
1019
+ if ingress_rules := details.get("IpPermissions", []):
1020
+ notes_parts.append(f"Ingress Rules: {len(ingress_rules)}")
1021
+ if egress_rules := details.get("IpPermissionsEgress", []):
1022
+ notes_parts.append(f"Egress Rules: {len(egress_rules)}")
1023
+ if vpc_id := details.get("VpcId"):
1024
+ notes_parts.append(f"VPC: {vpc_id}")
1025
+
1026
+ notes = "; ".join(notes_parts) if notes_parts else "AWS Security Group"
1027
+
1028
+ # Create console URI
1029
+ uri = f"https://console.aws.amazon.com/ec2/v2/home?region={region}#SecurityGroups:groupId={sg_id}"
1030
+
1031
+ return IntegrationAsset(
1032
+ name=name,
1033
+ identifier=sg_id,
1034
+ asset_type=regscale_models.AssetType.Firewall, # Security groups act like firewalls
1035
+ asset_category=regscale_models.AssetCategory.Software,
1036
+ component_type=regscale_models.ComponentType.Software,
1037
+ component_names=["Security Groups"],
1038
+ parent_id=self.plan_id,
1039
+ parent_module="securityplans",
1040
+ status=regscale_models.AssetStatus.Active,
1041
+ description=description,
1042
+ location=region,
1043
+ notes=notes,
1044
+ manufacturer="AWS",
1045
+ aws_identifier=sg_id,
1046
+ vlan_id=details.get("VpcId"),
1047
+ uri=uri,
1048
+ source_data=resource,
1049
+ is_virtual=True,
1050
+ )
1051
+
1052
+ def _parse_subnet_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1053
+ """Parse AWS EC2 Subnet resource to IntegrationAsset."""
1054
+ details = resource.get("Details", {}).get("AwsEc2Subnet", {})
1055
+ resource_id = resource.get("Id", "")
1056
+ region = resource.get("Region", "us-east-1")
1057
+
1058
+ subnet_id = self.extract_name_from_arn(resource_id) or details.get("SubnetId", "")
1059
+ cidr_block = details.get("CidrBlock", "")
1060
+ az = details.get("AvailabilityZone", "")
1061
+
1062
+ name = f"Subnet: {subnet_id}"
1063
+ if cidr_block:
1064
+ name += f" ({cidr_block})"
1065
+
1066
+ description = f"AWS EC2 Subnet {subnet_id} in {az}"
1067
+
1068
+ # Build notes with subnet details
1069
+ notes_parts = []
1070
+ if cidr_block:
1071
+ notes_parts.append(f"CIDR: {cidr_block}")
1072
+ if az:
1073
+ notes_parts.append(f"AZ: {az}")
1074
+ if available_ips := details.get("AvailableIpAddressCount"):
1075
+ notes_parts.append(f"Available IPs: {available_ips}")
1076
+ if details.get("MapPublicIpOnLaunch"):
1077
+ notes_parts.append("Auto-assigns public IP")
1078
+
1079
+ notes = "; ".join(notes_parts) if notes_parts else "AWS Subnet"
1080
+
1081
+ # Create console URI
1082
+ uri = f"https://console.aws.amazon.com/vpc/home?region={region}#SubnetDetails:subnetId={subnet_id}"
1083
+
1084
+ return IntegrationAsset(
1085
+ name=name,
1086
+ identifier=subnet_id,
1087
+ asset_type=regscale_models.AssetType.NetworkRouter, # Subnets are network infrastructure
1088
+ asset_category=regscale_models.AssetCategory.Hardware,
1089
+ component_type=regscale_models.ComponentType.Hardware,
1090
+ component_names=["Subnets"],
1091
+ parent_id=self.plan_id,
1092
+ parent_module="securityplans",
1093
+ status=regscale_models.AssetStatus.Active,
1094
+ description=description,
1095
+ location=region,
1096
+ notes=notes,
1097
+ manufacturer="AWS",
1098
+ aws_identifier=subnet_id,
1099
+ vlan_id=details.get("VpcId"),
1100
+ uri=uri,
1101
+ source_data=resource,
1102
+ is_virtual=True,
1103
+ is_public_facing=details.get("MapPublicIpOnLaunch", False),
1104
+ )
1105
+
1106
+ def _parse_iam_user_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1107
+ """Parse AWS IAM User resource to IntegrationAsset."""
1108
+ resource_id = resource.get("Id", "")
1109
+ region = resource.get("Region", "us-east-1")
1110
+
1111
+ # Extract username from ARN
1112
+ username = self.extract_name_from_arn(resource_id) or "Unknown User"
1113
+
1114
+ name = f"IAM User: {username}"
1115
+ description = f"AWS IAM User {username}"
1116
+
1117
+ # Create console URI
1118
+ uri = f"https://console.aws.amazon.com/iam/home?region={region}#/users/{username}"
1119
+
1120
+ return IntegrationAsset(
1121
+ name=name,
1122
+ identifier=username,
1123
+ asset_type=regscale_models.AssetType.Other, # IAM users don't fit standard asset types
1124
+ asset_category=regscale_models.AssetCategory.Software,
1125
+ component_type=regscale_models.ComponentType.Software,
1126
+ component_names=["IAM Users"],
1127
+ parent_id=self.plan_id,
1128
+ parent_module="securityplans",
1129
+ status=regscale_models.AssetStatus.Active,
1130
+ description=description,
1131
+ location=region,
1132
+ notes="AWS IAM User Account",
1133
+ manufacturer="AWS",
1134
+ aws_identifier=username,
1135
+ uri=uri,
1136
+ source_data=resource,
1137
+ is_virtual=True,
1138
+ )
1139
+
1140
+ def _parse_ec2_instance_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1141
+ """Parse AWS EC2 Instance resource to IntegrationAsset."""
1142
+ details = resource.get("Details", {}).get("AwsEc2Instance", {})
1143
+ resource_id = resource.get("Id", "")
1144
+ region = resource.get("Region", "us-east-1")
1145
+ tags = resource.get("Tags", {})
1146
+
1147
+ instance_id = self.extract_name_from_arn(resource_id) or details.get("InstanceId", "")
1148
+ instance_type = details.get("Type", "")
1149
+
1150
+ # Try to get a friendly name from tags
1151
+ friendly_name = tags.get("Name", instance_id)
1152
+ name = f"EC2: {friendly_name}"
1153
+ if instance_type:
1154
+ name += f" ({instance_type})"
1155
+
1156
+ description = f"AWS EC2 Instance {instance_id}"
1157
+
1158
+ # Create console URI
1159
+ uri = f"https://console.aws.amazon.com/ec2/v2/home?region={region}#InstanceDetails:instanceId={instance_id}"
1160
+
1161
+ return IntegrationAsset(
1162
+ name=name,
1163
+ identifier=instance_id,
1164
+ asset_type=regscale_models.AssetType.VM,
1165
+ asset_category=regscale_models.AssetCategory.Hardware,
1166
+ component_type=regscale_models.ComponentType.Hardware,
1167
+ component_names=[EC_INSTANCES],
1168
+ parent_id=self.plan_id,
1169
+ parent_module="securityplans",
1170
+ status=regscale_models.AssetStatus.Active,
1171
+ description=description,
1172
+ location=region,
1173
+ notes=f"AWS EC2 Instance - {instance_type}",
1174
+ model=instance_type,
1175
+ manufacturer="AWS",
1176
+ aws_identifier=instance_id,
1177
+ vlan_id=details.get("SubnetId"),
1178
+ uri=uri,
1179
+ source_data=resource,
1180
+ is_virtual=True,
1181
+ )
1182
+
1183
+ def _parse_s3_bucket_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1184
+ """Parse AWS S3 Bucket resource to IntegrationAsset."""
1185
+ details = resource.get("Details", {}).get("AwsS3Bucket", {})
1186
+ resource_id = resource.get("Id", "")
1187
+ region = resource.get("Region", "us-east-1")
1188
+
1189
+ bucket_name = self.extract_name_from_arn(resource_id) or details.get("Name", "")
1190
+
1191
+ name = f"S3 Bucket: {bucket_name}"
1192
+ description = f"AWS S3 Bucket {bucket_name}"
1193
+
1194
+ # Create console URI
1195
+ uri = f"https://s3.console.aws.amazon.com/s3/buckets/{bucket_name}?region={region}"
1196
+
1197
+ return IntegrationAsset(
1198
+ name=name,
1199
+ identifier=bucket_name,
1200
+ asset_type=regscale_models.AssetType.Other, # S3 buckets are storage, closest to Other
1201
+ asset_category=regscale_models.AssetCategory.Software,
1202
+ component_type=regscale_models.ComponentType.Software,
1203
+ component_names=["S3 Buckets"],
1204
+ parent_id=self.plan_id,
1205
+ parent_module="securityplans",
1206
+ status=regscale_models.AssetStatus.Active,
1207
+ description=description,
1208
+ location=region,
1209
+ notes="AWS S3 Storage Bucket",
1210
+ manufacturer="AWS",
1211
+ aws_identifier=bucket_name,
1212
+ uri=uri,
1213
+ source_data=resource,
1214
+ is_virtual=True,
1215
+ )
1216
+
1217
+ def _parse_rds_instance_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1218
+ """Parse AWS RDS Instance resource to IntegrationAsset."""
1219
+ details = resource.get("Details", {}).get("AwsRdsDbInstance", {})
1220
+ resource_id = resource.get("Id", "")
1221
+ region = resource.get("Region", "us-east-1")
1222
+
1223
+ db_identifier = self.extract_name_from_arn(resource_id) or details.get("DbInstanceIdentifier", "")
1224
+ db_class = details.get("DbInstanceClass", "")
1225
+ engine = details.get("Engine", "")
1226
+
1227
+ name = f"RDS: {db_identifier}"
1228
+ if engine:
1229
+ name += f" ({engine})"
1230
+
1231
+ description = f"AWS RDS Database Instance {db_identifier}"
1232
+
1233
+ # Create console URI
1234
+ uri = f"https://console.aws.amazon.com/rds/home?region={region}#database:id={db_identifier}"
1235
+
1236
+ return IntegrationAsset(
1237
+ name=name,
1238
+ identifier=db_identifier,
1239
+ asset_type=regscale_models.AssetType.VM, # RDS instances are virtual database servers
1240
+ asset_category=regscale_models.AssetCategory.Software,
1241
+ component_type=regscale_models.ComponentType.Software,
1242
+ component_names=["RDS Instances"],
1243
+ parent_id=self.plan_id,
1244
+ parent_module="securityplans",
1245
+ status=regscale_models.AssetStatus.Active,
1246
+ description=description,
1247
+ location=region,
1248
+ notes=f"AWS RDS Database - {engine} {db_class}",
1249
+ model=db_class,
1250
+ software_name=engine,
1251
+ manufacturer="AWS",
1252
+ aws_identifier=db_identifier,
1253
+ uri=uri,
1254
+ source_data=resource,
1255
+ is_virtual=True,
1256
+ )
1257
+
1258
+ def _parse_lambda_function_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1259
+ """Parse AWS Lambda Function resource to IntegrationAsset."""
1260
+ details = resource.get("Details", {}).get("AwsLambdaFunction", {})
1261
+ resource_id = resource.get("Id", "")
1262
+ region = resource.get("Region", "us-east-1")
1263
+
1264
+ function_name = self.extract_name_from_arn(resource_id) or details.get("FunctionName", "")
1265
+ runtime = details.get("Runtime", "")
1266
+
1267
+ name = f"Lambda: {function_name}"
1268
+ if runtime:
1269
+ name += f" ({runtime})"
1270
+
1271
+ description = f"AWS Lambda Function {function_name}"
1272
+
1273
+ # Create console URI
1274
+ uri = f"https://console.aws.amazon.com/lambda/home?region={region}#/functions/{function_name}"
1275
+
1276
+ return IntegrationAsset(
1277
+ name=name,
1278
+ identifier=function_name,
1279
+ asset_type=regscale_models.AssetType.Other, # Lambda functions are serverless, closest to Other
1280
+ asset_category=regscale_models.AssetCategory.Software,
1281
+ component_type=regscale_models.ComponentType.Software,
1282
+ component_names=["Lambda Functions"],
1283
+ parent_id=self.plan_id,
1284
+ parent_module="securityplans",
1285
+ status=regscale_models.AssetStatus.Active,
1286
+ description=description,
1287
+ location=region,
1288
+ notes=f"AWS Lambda Function - {runtime}",
1289
+ software_name=runtime,
1290
+ manufacturer="AWS",
1291
+ aws_identifier=function_name,
1292
+ uri=uri,
1293
+ source_data=resource,
1294
+ is_virtual=True,
1295
+ )
1296
+
1297
+ def _parse_ecr_repository_resource(self, resource: dict, finding: dict) -> IntegrationAsset:
1298
+ """Parse AWS ECR Repository resource to IntegrationAsset."""
1299
+ details = resource.get("Details", {}).get("AwsEcrRepository", {})
1300
+ resource_id = resource.get("Id", "")
1301
+ region = resource.get("Region", "us-east-1")
1302
+
1303
+ repo_name = self.extract_name_from_arn(resource_id) or details.get("RepositoryName", "")
1304
+
1305
+ name = f"ECR Repository: {repo_name}"
1306
+ description = f"AWS ECR Container Repository {repo_name}"
1307
+
1308
+ # Create console URI
1309
+ uri = f"https://console.aws.amazon.com/ecr/repositories/{repo_name}?region={region}"
1310
+
1311
+ return IntegrationAsset(
1312
+ name=name,
1313
+ identifier=repo_name,
1314
+ asset_type=regscale_models.AssetType.Other, # ECR repositories are container registries
1315
+ asset_category=regscale_models.AssetCategory.Software,
1316
+ component_type=regscale_models.ComponentType.Software,
1317
+ component_names=["ECR Repositories"],
1318
+ parent_id=self.plan_id,
1319
+ parent_module="securityplans",
1320
+ status=regscale_models.AssetStatus.Active,
1321
+ description=description,
1322
+ location=region,
1323
+ notes="AWS ECR Container Repository",
1324
+ manufacturer="AWS",
1325
+ aws_identifier=repo_name,
1326
+ uri=uri,
1327
+ source_data=resource,
1328
+ is_virtual=True,
1329
+ )
1330
+
1331
+ def _parse_generic_resource(self, resource: dict) -> IntegrationAsset:
1332
+ """Parse generic AWS resource to IntegrationAsset."""
1333
+ resource_type = resource.get("Type", "Unknown")
1334
+ resource_id = resource.get("Id", "")
1335
+ region = resource.get("Region", "us-east-1")
1336
+
1337
+ identifier = self.extract_name_from_arn(resource_id) or resource_id
1338
+
1339
+ name = f"{resource_type}: {identifier}"
1340
+ description = f"AWS {resource_type} {identifier}"
1341
+
1342
+ return IntegrationAsset(
1343
+ name=name,
1344
+ identifier=identifier,
1345
+ asset_type=regscale_models.AssetType.Other,
1346
+ asset_category=regscale_models.AssetCategory.Software,
1347
+ component_type=regscale_models.ComponentType.Software,
1348
+ component_names=[f"{resource_type}s"],
1349
+ parent_id=self.plan_id,
1350
+ parent_module="securityplans",
1351
+ status=regscale_models.AssetStatus.Active,
1352
+ description=description,
1353
+ location=region,
1354
+ notes=f"AWS {resource_type}",
1355
+ manufacturer="AWS",
1356
+ aws_identifier=identifier,
1357
+ source_data=resource,
1358
+ is_virtual=True,
1359
+ )