regscale-cli 6.20.10.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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +12 -5
- regscale/core/app/internal/set_permissions.py +58 -27
- regscale/integrations/commercial/__init__.py +1 -2
- regscale/integrations/commercial/amazon/common.py +79 -2
- regscale/integrations/commercial/aws/cli.py +183 -9
- regscale/integrations/commercial/aws/scanner.py +544 -9
- regscale/integrations/commercial/cpe.py +18 -1
- regscale/integrations/commercial/nessus/scanner.py +2 -0
- regscale/integrations/commercial/sonarcloud.py +35 -36
- regscale/integrations/commercial/synqly/ticketing.py +51 -0
- regscale/integrations/commercial/tenablev2/jsonl_scanner.py +2 -1
- regscale/integrations/commercial/wizv2/async_client.py +10 -3
- regscale/integrations/commercial/wizv2/click.py +102 -26
- regscale/integrations/commercial/wizv2/constants.py +249 -1
- regscale/integrations/commercial/wizv2/issue.py +2 -2
- regscale/integrations/commercial/wizv2/parsers.py +3 -2
- regscale/integrations/commercial/wizv2/policy_compliance.py +1858 -0
- regscale/integrations/commercial/wizv2/scanner.py +15 -21
- regscale/integrations/commercial/wizv2/utils.py +258 -85
- regscale/integrations/commercial/wizv2/variables.py +4 -3
- regscale/integrations/compliance_integration.py +1455 -0
- regscale/integrations/integration_override.py +15 -6
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/markdown_parser.py +7 -1
- regscale/integrations/scanner_integration.py +193 -37
- regscale/models/app_models/__init__.py +1 -0
- regscale/models/integration_models/amazon_models/inspector_scan.py +32 -57
- regscale/models/integration_models/aqua.py +92 -78
- regscale/models/integration_models/cisa_kev_data.json +117 -5
- regscale/models/integration_models/defenderimport.py +64 -59
- regscale/models/integration_models/ecr_models/ecr.py +100 -147
- regscale/models/integration_models/flat_file_importer/__init__.py +52 -38
- regscale/models/integration_models/ibm.py +29 -47
- regscale/models/integration_models/nexpose.py +156 -68
- regscale/models/integration_models/prisma.py +46 -66
- regscale/models/integration_models/qualys.py +99 -93
- regscale/models/integration_models/snyk.py +229 -158
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/veracode.py +15 -20
- regscale/{integrations/commercial/wizv2/models.py → models/integration_models/wizv2.py} +4 -12
- regscale/models/integration_models/xray.py +276 -82
- regscale/models/regscale_models/control_implementation.py +14 -12
- regscale/models/regscale_models/file.py +4 -0
- regscale/models/regscale_models/issue.py +123 -0
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/rbac.py +22 -0
- regscale/models/regscale_models/regscale_model.py +4 -2
- regscale/models/regscale_models/security_plan.py +1 -1
- regscale/utils/graphql_client.py +3 -1
- {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/METADATA +9 -9
- {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/RECORD +64 -60
- tests/fixtures/test_fixture.py +58 -2
- tests/regscale/core/test_app.py +5 -3
- tests/regscale/core/test_version_regscale.py +5 -3
- tests/regscale/integrations/test_integration_mapping.py +522 -40
- tests/regscale/integrations/test_issue_due_date.py +1 -1
- tests/regscale/integrations/test_update_finding_dates.py +336 -0
- tests/regscale/integrations/test_wiz_policy_compliance_affected_controls.py +154 -0
- tests/regscale/models/test_asset.py +406 -50
- {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.10.0.dist-info → regscale_cli-6.21.1.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.20.10.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 = [
|
|
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 = [
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
+
)
|