global_services.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. """
  2. Global Service Scanners
  3. Scans CloudFront Distributions, Route 53 Hosted Zones, ACM Certificates, and WAF Web ACLs.
  4. These are global services that are not region-specific.
  5. Requirements:
  6. - 5.1: Scan global AWS services using boto3
  7. - 5.2: Scan global resources regardless of selected regions
  8. """
  9. import boto3
  10. from typing import List, Dict, Any
  11. import logging
  12. from app.scanners.base import ResourceData
  13. from app.scanners.utils import retry_with_backoff
  14. logger = logging.getLogger(__name__)
  15. class GlobalServiceScanner:
  16. """Scanner for global AWS resources"""
  17. @staticmethod
  18. @retry_with_backoff()
  19. def scan_cloudfront_distributions(session: boto3.Session, account_id: str, region: str) -> List[ResourceData]:
  20. """
  21. Scan CloudFront Distributions (global service).
  22. Attributes (vertical layout - one table per distribution):
  23. CloudFront ID, Domain Name, CNAME, Origin Domain Name,
  24. Origin Protocol Policy, Viewer Protocol Policy,
  25. Allowed HTTP Methods, Cached HTTP Methods
  26. """
  27. resources = []
  28. # CloudFront is a global service, always use us-east-1
  29. cf_client = session.client('cloudfront', region_name='us-east-1')
  30. try:
  31. paginator = cf_client.get_paginator('list_distributions')
  32. for page in paginator.paginate():
  33. distribution_list = page.get('DistributionList', {})
  34. for dist in distribution_list.get('Items', []):
  35. dist_id = dist.get('Id', '')
  36. # Get aliases (CNAMEs)
  37. aliases = dist.get('Aliases', {}).get('Items', [])
  38. # Get origin info
  39. origins = dist.get('Origins', {}).get('Items', [])
  40. origin_domain = ''
  41. origin_protocol = ''
  42. if origins:
  43. origin = origins[0]
  44. origin_domain = origin.get('DomainName', '')
  45. custom_origin = origin.get('CustomOriginConfig', {})
  46. if custom_origin:
  47. origin_protocol = custom_origin.get('OriginProtocolPolicy', '')
  48. else:
  49. origin_protocol = 'S3'
  50. # Get default cache behavior
  51. default_behavior = dist.get('DefaultCacheBehavior', {})
  52. viewer_protocol = default_behavior.get('ViewerProtocolPolicy', '')
  53. allowed_methods = default_behavior.get('AllowedMethods', {}).get('Items', [])
  54. cached_methods = default_behavior.get('AllowedMethods', {}).get('CachedMethods', {}).get('Items', [])
  55. resources.append(ResourceData(
  56. account_id=account_id,
  57. region='global',
  58. service='cloudfront',
  59. resource_type='Distribution',
  60. resource_id=dist.get('ARN', dist_id),
  61. name=dist_id,
  62. attributes={
  63. 'CloudFront ID': dist_id,
  64. 'Domain Name': dist.get('DomainName', ''),
  65. 'CNAME': ', '.join(aliases) if aliases else 'N/A',
  66. 'Origin Domain Name': origin_domain,
  67. 'Origin Protocol Policy': origin_protocol,
  68. 'Viewer Protocol Policy': viewer_protocol,
  69. 'Allowed HTTP Methods': ', '.join(allowed_methods),
  70. 'Cached HTTP Methods': ', '.join(cached_methods)
  71. }
  72. ))
  73. except Exception as e:
  74. logger.warning(f"Failed to scan CloudFront distributions: {str(e)}")
  75. return resources
  76. @staticmethod
  77. @retry_with_backoff()
  78. def scan_route53_hosted_zones(session: boto3.Session, account_id: str, region: str) -> List[ResourceData]:
  79. """
  80. Scan Route 53 Hosted Zones (global service).
  81. Attributes (horizontal layout):
  82. Zone ID, Name, Type, Record Count
  83. """
  84. resources = []
  85. # Route 53 is a global service
  86. route53_client = session.client('route53', region_name='us-east-1')
  87. try:
  88. paginator = route53_client.get_paginator('list_hosted_zones')
  89. for page in paginator.paginate():
  90. for zone in page.get('HostedZones', []):
  91. zone_id = zone.get('Id', '').replace('/hostedzone/', '')
  92. zone_name = zone.get('Name', '')
  93. # Determine zone type
  94. zone_type = 'Private' if zone.get('Config', {}).get('PrivateZone') else 'Public'
  95. resources.append(ResourceData(
  96. account_id=account_id,
  97. region='global',
  98. service='route53',
  99. resource_type='Hosted Zone',
  100. resource_id=zone_id,
  101. name=zone_name,
  102. attributes={
  103. 'Zone ID': zone_id,
  104. 'Name': zone_name,
  105. 'Type': zone_type,
  106. 'Record Count': str(zone.get('ResourceRecordSetCount', 0))
  107. }
  108. ))
  109. except Exception as e:
  110. logger.warning(f"Failed to scan Route 53 hosted zones: {str(e)}")
  111. return resources
  112. @staticmethod
  113. @retry_with_backoff()
  114. def scan_acm_certificates(session: boto3.Session, account_id: str, region: str) -> List[ResourceData]:
  115. """
  116. Scan ACM Certificates (regional service).
  117. Attributes (horizontal layout): Domain name, Additional names
  118. """
  119. resources = []
  120. # ACM is a regional service
  121. acm_client = session.client('acm', region_name=region)
  122. try:
  123. paginator = acm_client.get_paginator('list_certificates')
  124. for page in paginator.paginate():
  125. for cert in page.get('CertificateSummaryList', []):
  126. domain_name = cert.get('DomainName', '')
  127. cert_arn = cert.get('CertificateArn', '')
  128. # Get additional names (Subject Alternative Names)
  129. additional_names = ''
  130. try:
  131. cert_detail = acm_client.describe_certificate(CertificateArn=cert_arn)
  132. sans = cert_detail.get('Certificate', {}).get('SubjectAlternativeNames', [])
  133. # Filter out the main domain name from SANs
  134. additional = [san for san in sans if san != domain_name]
  135. additional_names = ', '.join(additional) if additional else ''
  136. except Exception:
  137. pass
  138. resources.append(ResourceData(
  139. account_id=account_id,
  140. region=region,
  141. service='acm',
  142. resource_type='Certificate',
  143. resource_id=cert_arn,
  144. name=domain_name,
  145. attributes={
  146. 'Domain name': domain_name,
  147. 'Additional names': additional_names
  148. }
  149. ))
  150. except Exception as e:
  151. logger.warning(f"Failed to scan ACM certificates in {region}: {str(e)}")
  152. return resources
  153. @staticmethod
  154. @retry_with_backoff()
  155. def scan_waf_web_acls(session: boto3.Session, account_id: str, region: str) -> List[ResourceData]:
  156. """
  157. Scan WAF Web ACLs (global service for CloudFront).
  158. Attributes (horizontal layout):
  159. WebACL Name, Scope, Rules Count, Associated Resources
  160. """
  161. resources = []
  162. # Scan WAFv2 global (CloudFront) Web ACLs
  163. wafv2_client = session.client('wafv2', region_name='us-east-1')
  164. try:
  165. # List CloudFront Web ACLs (CLOUDFRONT scope)
  166. response = wafv2_client.list_web_acls(Scope='CLOUDFRONT')
  167. for acl in response.get('WebACLs', []):
  168. acl_name = acl.get('Name', '')
  169. acl_id = acl.get('Id', '')
  170. acl_arn = acl.get('ARN', '')
  171. # Get Web ACL details for rules count
  172. rules_count = 0
  173. associated_resources = []
  174. try:
  175. acl_response = wafv2_client.get_web_acl(
  176. Name=acl_name,
  177. Scope='CLOUDFRONT',
  178. Id=acl_id
  179. )
  180. web_acl = acl_response.get('WebACL', {})
  181. rules_count = len(web_acl.get('Rules', []))
  182. # Get associated resources
  183. resources_response = wafv2_client.list_resources_for_web_acl(
  184. WebACLArn=acl_arn
  185. )
  186. for resource_arn in resources_response.get('ResourceArns', []):
  187. # Extract resource name from ARN
  188. resource_name = resource_arn.split('/')[-1]
  189. associated_resources.append(resource_name)
  190. except Exception as e:
  191. logger.debug(f"Failed to get WAF ACL details: {str(e)}")
  192. resources.append(ResourceData(
  193. account_id=account_id,
  194. region='global',
  195. service='waf',
  196. resource_type='Web ACL',
  197. resource_id=acl_arn,
  198. name=acl_name,
  199. attributes={
  200. 'WebACL Name': acl_name,
  201. 'Scope': 'CLOUDFRONT',
  202. 'Rules Count': str(rules_count),
  203. 'Associated Resources': ', '.join(associated_resources) if associated_resources else 'None'
  204. }
  205. ))
  206. except Exception as e:
  207. logger.warning(f"Failed to scan WAFv2 Web ACLs: {str(e)}")
  208. # Also scan regional WAF Web ACLs
  209. try:
  210. response = wafv2_client.list_web_acls(Scope='REGIONAL')
  211. for acl in response.get('WebACLs', []):
  212. acl_name = acl.get('Name', '')
  213. acl_id = acl.get('Id', '')
  214. acl_arn = acl.get('ARN', '')
  215. rules_count = 0
  216. associated_resources = []
  217. try:
  218. acl_response = wafv2_client.get_web_acl(
  219. Name=acl_name,
  220. Scope='REGIONAL',
  221. Id=acl_id
  222. )
  223. web_acl = acl_response.get('WebACL', {})
  224. rules_count = len(web_acl.get('Rules', []))
  225. resources_response = wafv2_client.list_resources_for_web_acl(
  226. WebACLArn=acl_arn
  227. )
  228. for resource_arn in resources_response.get('ResourceArns', []):
  229. resource_name = resource_arn.split('/')[-1]
  230. associated_resources.append(resource_name)
  231. except Exception as e:
  232. logger.debug(f"Failed to get WAF ACL details: {str(e)}")
  233. resources.append(ResourceData(
  234. account_id=account_id,
  235. region='global',
  236. service='waf',
  237. resource_type='Web ACL',
  238. resource_id=acl_arn,
  239. name=acl_name,
  240. attributes={
  241. 'WebACL Name': acl_name,
  242. 'Scope': 'REGIONAL',
  243. 'Rules Count': str(rules_count),
  244. 'Associated Resources': ', '.join(associated_resources) if associated_resources else 'None'
  245. }
  246. ))
  247. except Exception as e:
  248. logger.warning(f"Failed to scan regional WAFv2 Web ACLs: {str(e)}")
  249. return resources