aws-inventory-manager 0.17.12__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.
- aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
- aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
- aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/aws/__init__.py +11 -0
- src/aws/client.py +128 -0
- src/aws/credentials.py +191 -0
- src/aws/rate_limiter.py +177 -0
- src/cli/__init__.py +12 -0
- src/cli/config.py +130 -0
- src/cli/main.py +4046 -0
- src/cloudtrail/__init__.py +5 -0
- src/cloudtrail/query.py +642 -0
- src/config_service/__init__.py +21 -0
- src/config_service/collector.py +346 -0
- src/config_service/detector.py +256 -0
- src/config_service/resource_type_mapping.py +328 -0
- src/cost/__init__.py +5 -0
- src/cost/analyzer.py +226 -0
- src/cost/explorer.py +209 -0
- src/cost/reporter.py +237 -0
- src/delta/__init__.py +5 -0
- src/delta/calculator.py +206 -0
- src/delta/differ.py +185 -0
- src/delta/formatters.py +272 -0
- src/delta/models.py +154 -0
- src/delta/reporter.py +234 -0
- src/matching/__init__.py +6 -0
- src/matching/config.py +52 -0
- src/matching/normalizer.py +450 -0
- src/matching/prompts.py +33 -0
- src/models/__init__.py +21 -0
- src/models/config_diff.py +135 -0
- src/models/cost_report.py +87 -0
- src/models/deletion_operation.py +104 -0
- src/models/deletion_record.py +97 -0
- src/models/delta_report.py +122 -0
- src/models/efs_resource.py +80 -0
- src/models/elasticache_resource.py +90 -0
- src/models/group.py +318 -0
- src/models/inventory.py +133 -0
- src/models/protection_rule.py +123 -0
- src/models/report.py +288 -0
- src/models/resource.py +111 -0
- src/models/security_finding.py +102 -0
- src/models/snapshot.py +122 -0
- src/restore/__init__.py +20 -0
- src/restore/audit.py +175 -0
- src/restore/cleaner.py +461 -0
- src/restore/config.py +209 -0
- src/restore/deleter.py +976 -0
- src/restore/dependency.py +254 -0
- src/restore/safety.py +115 -0
- src/security/__init__.py +0 -0
- src/security/checks/__init__.py +0 -0
- src/security/checks/base.py +56 -0
- src/security/checks/ec2_checks.py +88 -0
- src/security/checks/elasticache_checks.py +149 -0
- src/security/checks/iam_checks.py +102 -0
- src/security/checks/rds_checks.py +140 -0
- src/security/checks/s3_checks.py +95 -0
- src/security/checks/secrets_checks.py +96 -0
- src/security/checks/sg_checks.py +142 -0
- src/security/cis_mapper.py +97 -0
- src/security/models.py +53 -0
- src/security/reporter.py +174 -0
- src/security/scanner.py +87 -0
- src/snapshot/__init__.py +6 -0
- src/snapshot/capturer.py +453 -0
- src/snapshot/filter.py +259 -0
- src/snapshot/inventory_storage.py +236 -0
- src/snapshot/report_formatter.py +250 -0
- src/snapshot/reporter.py +189 -0
- src/snapshot/resource_collectors/__init__.py +5 -0
- src/snapshot/resource_collectors/apigateway.py +140 -0
- src/snapshot/resource_collectors/backup.py +136 -0
- src/snapshot/resource_collectors/base.py +81 -0
- src/snapshot/resource_collectors/cloudformation.py +55 -0
- src/snapshot/resource_collectors/cloudwatch.py +109 -0
- src/snapshot/resource_collectors/codebuild.py +69 -0
- src/snapshot/resource_collectors/codepipeline.py +82 -0
- src/snapshot/resource_collectors/dynamodb.py +65 -0
- src/snapshot/resource_collectors/ec2.py +240 -0
- src/snapshot/resource_collectors/ecs.py +215 -0
- src/snapshot/resource_collectors/efs_collector.py +102 -0
- src/snapshot/resource_collectors/eks.py +200 -0
- src/snapshot/resource_collectors/elasticache_collector.py +79 -0
- src/snapshot/resource_collectors/elb.py +126 -0
- src/snapshot/resource_collectors/eventbridge.py +156 -0
- src/snapshot/resource_collectors/glue.py +199 -0
- src/snapshot/resource_collectors/iam.py +188 -0
- src/snapshot/resource_collectors/kms.py +111 -0
- src/snapshot/resource_collectors/lambda_func.py +139 -0
- src/snapshot/resource_collectors/rds.py +109 -0
- src/snapshot/resource_collectors/route53.py +86 -0
- src/snapshot/resource_collectors/s3.py +105 -0
- src/snapshot/resource_collectors/secretsmanager.py +70 -0
- src/snapshot/resource_collectors/sns.py +68 -0
- src/snapshot/resource_collectors/sqs.py +82 -0
- src/snapshot/resource_collectors/ssm.py +160 -0
- src/snapshot/resource_collectors/stepfunctions.py +74 -0
- src/snapshot/resource_collectors/vpcendpoints.py +79 -0
- src/snapshot/resource_collectors/waf.py +159 -0
- src/snapshot/storage.py +351 -0
- src/storage/__init__.py +21 -0
- src/storage/audit_store.py +419 -0
- src/storage/database.py +294 -0
- src/storage/group_store.py +763 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +416 -0
- src/storage/schema.py +339 -0
- src/storage/snapshot_store.py +363 -0
- src/utils/__init__.py +12 -0
- src/utils/export.py +305 -0
- src/utils/hash.py +60 -0
- src/utils/logging.py +63 -0
- src/utils/pagination.py +41 -0
- src/utils/paths.py +51 -0
- src/utils/progress.py +41 -0
- src/utils/unsupported_resources.py +306 -0
- src/web/__init__.py +5 -0
- src/web/app.py +97 -0
- src/web/dependencies.py +69 -0
- src/web/routes/__init__.py +1 -0
- src/web/routes/api/__init__.py +18 -0
- src/web/routes/api/charts.py +156 -0
- src/web/routes/api/cleanup.py +186 -0
- src/web/routes/api/filters.py +253 -0
- src/web/routes/api/groups.py +305 -0
- src/web/routes/api/inventories.py +80 -0
- src/web/routes/api/queries.py +202 -0
- src/web/routes/api/resources.py +393 -0
- src/web/routes/api/snapshots.py +314 -0
- src/web/routes/api/views.py +260 -0
- src/web/routes/pages.py +198 -0
- src/web/services/__init__.py +1 -0
- src/web/templates/base.html +955 -0
- src/web/templates/components/navbar.html +31 -0
- src/web/templates/components/sidebar.html +104 -0
- src/web/templates/pages/audit_logs.html +86 -0
- src/web/templates/pages/cleanup.html +279 -0
- src/web/templates/pages/dashboard.html +227 -0
- src/web/templates/pages/diff.html +175 -0
- src/web/templates/pages/error.html +30 -0
- src/web/templates/pages/groups.html +721 -0
- src/web/templates/pages/queries.html +246 -0
- src/web/templates/pages/resources.html +2429 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- src/web/templates/pages/snapshots.html +429 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""RDS resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RDSCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS RDS resources (instances, clusters, snapshots)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "rds"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect RDS resources.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of RDS resources
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
account_id = self._get_account_id()
|
|
25
|
+
|
|
26
|
+
# Collect DB instances
|
|
27
|
+
resources.extend(self._collect_db_instances(account_id))
|
|
28
|
+
|
|
29
|
+
# Collect DB clusters (Aurora)
|
|
30
|
+
resources.extend(self._collect_db_clusters(account_id))
|
|
31
|
+
|
|
32
|
+
self.logger.debug(f"Collected {len(resources)} RDS resources in {self.region}")
|
|
33
|
+
return resources
|
|
34
|
+
|
|
35
|
+
def _collect_db_instances(self, account_id: str) -> List[Resource]:
|
|
36
|
+
"""Collect RDS DB instances."""
|
|
37
|
+
resources = []
|
|
38
|
+
client = self._create_client()
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
paginator = client.get_paginator("describe_db_instances")
|
|
42
|
+
for page in paginator.paginate():
|
|
43
|
+
for db_instance in page["DBInstances"]:
|
|
44
|
+
db_id = db_instance["DBInstanceIdentifier"]
|
|
45
|
+
db_arn = db_instance["DBInstanceArn"]
|
|
46
|
+
|
|
47
|
+
# Extract tags
|
|
48
|
+
tags = {}
|
|
49
|
+
try:
|
|
50
|
+
tag_response = client.list_tags_for_resource(ResourceName=db_arn)
|
|
51
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("TagList", [])}
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.logger.debug(f"Could not get tags for DB instance {db_id}: {e}")
|
|
54
|
+
|
|
55
|
+
# Create resource
|
|
56
|
+
resource = Resource(
|
|
57
|
+
arn=db_arn,
|
|
58
|
+
resource_type="AWS::RDS::DBInstance",
|
|
59
|
+
name=db_id,
|
|
60
|
+
region=self.region,
|
|
61
|
+
tags=tags,
|
|
62
|
+
config_hash=compute_config_hash(db_instance),
|
|
63
|
+
created_at=db_instance.get("InstanceCreateTime"),
|
|
64
|
+
raw_config=db_instance,
|
|
65
|
+
)
|
|
66
|
+
resources.append(resource)
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
self.logger.error(f"Error collecting RDS DB instances in {self.region}: {e}")
|
|
70
|
+
|
|
71
|
+
return resources
|
|
72
|
+
|
|
73
|
+
def _collect_db_clusters(self, account_id: str) -> List[Resource]:
|
|
74
|
+
"""Collect RDS DB clusters (Aurora)."""
|
|
75
|
+
resources = []
|
|
76
|
+
client = self._create_client()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
paginator = client.get_paginator("describe_db_clusters")
|
|
80
|
+
for page in paginator.paginate():
|
|
81
|
+
for db_cluster in page["DBClusters"]:
|
|
82
|
+
cluster_id = db_cluster["DBClusterIdentifier"]
|
|
83
|
+
cluster_arn = db_cluster["DBClusterArn"]
|
|
84
|
+
|
|
85
|
+
# Extract tags
|
|
86
|
+
tags = {}
|
|
87
|
+
try:
|
|
88
|
+
tag_response = client.list_tags_for_resource(ResourceName=cluster_arn)
|
|
89
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("TagList", [])}
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self.logger.debug(f"Could not get tags for DB cluster {cluster_id}: {e}")
|
|
92
|
+
|
|
93
|
+
# Create resource
|
|
94
|
+
resource = Resource(
|
|
95
|
+
arn=cluster_arn,
|
|
96
|
+
resource_type="AWS::RDS::DBCluster",
|
|
97
|
+
name=cluster_id,
|
|
98
|
+
region=self.region,
|
|
99
|
+
tags=tags,
|
|
100
|
+
config_hash=compute_config_hash(db_cluster),
|
|
101
|
+
created_at=db_cluster.get("ClusterCreateTime"),
|
|
102
|
+
raw_config=db_cluster,
|
|
103
|
+
)
|
|
104
|
+
resources.append(resource)
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
self.logger.error(f"Error collecting RDS DB clusters in {self.region}: {e}")
|
|
108
|
+
|
|
109
|
+
return resources
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Route53 resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Route53Collector(BaseResourceCollector):
|
|
11
|
+
"""Collector for Amazon Route53 resources (Hosted Zones)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "route53"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def is_global_service(self) -> bool:
|
|
19
|
+
"""Route53 is a global service."""
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
def collect(self) -> List[Resource]:
|
|
23
|
+
"""Collect Route53 resources.
|
|
24
|
+
|
|
25
|
+
Collects:
|
|
26
|
+
- Hosted Zones (public and private)
|
|
27
|
+
- Resource Record Sets within each zone
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of Route53 hosted zone resources
|
|
31
|
+
"""
|
|
32
|
+
resources = []
|
|
33
|
+
client = self._create_client()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Collect hosted zones
|
|
37
|
+
paginator = client.get_paginator("list_hosted_zones")
|
|
38
|
+
for page in paginator.paginate():
|
|
39
|
+
for zone in page.get("HostedZones", []):
|
|
40
|
+
zone_id = zone["Id"].split("/")[-1] # Extract ID from '/hostedzone/Z123'
|
|
41
|
+
zone_name = zone["Name"]
|
|
42
|
+
|
|
43
|
+
# Get tags
|
|
44
|
+
tags = {}
|
|
45
|
+
try:
|
|
46
|
+
tag_response = client.list_tags_for_resource(ResourceType="hostedzone", ResourceId=zone_id)
|
|
47
|
+
for tag in tag_response.get("Tags", []):
|
|
48
|
+
tags[tag["Key"]] = tag["Value"]
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.logger.debug(f"Could not get tags for hosted zone {zone_id}: {e}")
|
|
51
|
+
|
|
52
|
+
# Get record count and additional details
|
|
53
|
+
try:
|
|
54
|
+
zone_details = client.get_hosted_zone(Id=zone["Id"])
|
|
55
|
+
hosted_zone_info = zone_details.get("HostedZone", {})
|
|
56
|
+
# Merge basic info with detailed info
|
|
57
|
+
full_zone = {**zone, **hosted_zone_info}
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self.logger.debug(f"Could not get details for hosted zone {zone_id}: {e}")
|
|
60
|
+
full_zone = zone
|
|
61
|
+
|
|
62
|
+
# Build ARN (Route53 hosted zones use a specific ARN format)
|
|
63
|
+
# Note: ARNs for Route53 are not standard across all operations
|
|
64
|
+
arn = f"arn:aws:route53:::hostedzone/{zone_id}"
|
|
65
|
+
|
|
66
|
+
# Route53 doesn't provide creation timestamp, use None
|
|
67
|
+
created_at = None
|
|
68
|
+
|
|
69
|
+
# Create resource
|
|
70
|
+
resource = Resource(
|
|
71
|
+
arn=arn,
|
|
72
|
+
resource_type="AWS::Route53::HostedZone",
|
|
73
|
+
name=zone_name,
|
|
74
|
+
region="global", # Route53 is global
|
|
75
|
+
tags=tags,
|
|
76
|
+
config_hash=compute_config_hash(full_zone),
|
|
77
|
+
created_at=created_at,
|
|
78
|
+
raw_config=full_zone,
|
|
79
|
+
)
|
|
80
|
+
resources.append(resource)
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.logger.error(f"Error collecting Route53 hosted zones: {e}")
|
|
84
|
+
|
|
85
|
+
self.logger.debug(f"Collected {len(resources)} Route53 hosted zones")
|
|
86
|
+
return resources
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""S3 resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class S3Collector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS S3 buckets.
|
|
12
|
+
|
|
13
|
+
Note: S3 is a global service but buckets are accessed via regional endpoints.
|
|
14
|
+
We only collect once (in us-east-1) to avoid duplicates.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def service_name(self) -> str:
|
|
19
|
+
return "s3"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def is_global_service(self) -> bool:
|
|
23
|
+
# S3 is global, so only collect once
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
def collect(self) -> List[Resource]:
|
|
27
|
+
"""Collect S3 buckets.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of S3 bucket resources
|
|
31
|
+
"""
|
|
32
|
+
resources = []
|
|
33
|
+
client = self._create_client()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# List all buckets
|
|
37
|
+
response = client.list_buckets()
|
|
38
|
+
|
|
39
|
+
for bucket in response.get("Buckets", []):
|
|
40
|
+
bucket_name = bucket["Name"]
|
|
41
|
+
creation_date = bucket.get("CreationDate")
|
|
42
|
+
|
|
43
|
+
# Get bucket location to determine region
|
|
44
|
+
try:
|
|
45
|
+
location_response = client.get_bucket_location(Bucket=bucket_name)
|
|
46
|
+
location = location_response.get("LocationConstraint")
|
|
47
|
+
# None means us-east-1
|
|
48
|
+
bucket_region = location if location else "us-east-1"
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.logger.debug(f"Could not get location for bucket {bucket_name}: {e}")
|
|
51
|
+
bucket_region = "unknown"
|
|
52
|
+
|
|
53
|
+
# Get bucket tags
|
|
54
|
+
tags = {}
|
|
55
|
+
try:
|
|
56
|
+
tag_response = client.get_bucket_tagging(Bucket=bucket_name)
|
|
57
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("TagSet", [])}
|
|
58
|
+
except client.exceptions.NoSuchTagSet:
|
|
59
|
+
# Bucket has no tags
|
|
60
|
+
pass
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.logger.debug(f"Could not get tags for bucket {bucket_name}: {e}")
|
|
63
|
+
|
|
64
|
+
# Get additional bucket configuration for config hash
|
|
65
|
+
bucket_config = {
|
|
66
|
+
"Name": bucket_name,
|
|
67
|
+
"CreationDate": creation_date,
|
|
68
|
+
"Region": bucket_region,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Try to get versioning, encryption, etc.
|
|
72
|
+
try:
|
|
73
|
+
versioning = client.get_bucket_versioning(Bucket=bucket_name)
|
|
74
|
+
bucket_config["Versioning"] = versioning.get("Status", "Disabled")
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
encryption = client.get_bucket_encryption(Bucket=bucket_name)
|
|
80
|
+
bucket_config["Encryption"] = encryption.get("ServerSideEncryptionConfiguration")
|
|
81
|
+
except Exception:
|
|
82
|
+
# No encryption configured
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Build ARN
|
|
86
|
+
arn = f"arn:aws:s3:::{bucket_name}"
|
|
87
|
+
|
|
88
|
+
# Create resource
|
|
89
|
+
resource = Resource(
|
|
90
|
+
arn=arn,
|
|
91
|
+
resource_type="AWS::S3::Bucket",
|
|
92
|
+
name=bucket_name,
|
|
93
|
+
region=bucket_region,
|
|
94
|
+
tags=tags,
|
|
95
|
+
config_hash=compute_config_hash(bucket_config),
|
|
96
|
+
created_at=creation_date,
|
|
97
|
+
raw_config=bucket_config,
|
|
98
|
+
)
|
|
99
|
+
resources.append(resource)
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self.logger.error(f"Error collecting S3 buckets: {e}")
|
|
103
|
+
|
|
104
|
+
self.logger.debug(f"Collected {len(resources)} S3 buckets")
|
|
105
|
+
return resources
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Secrets Manager resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SecretsManagerCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS Secrets Manager resources."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "secretsmanager"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect Secrets Manager secrets.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of Secrets Manager secret resources
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
client = self._create_client()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
paginator = client.get_paginator("list_secrets")
|
|
28
|
+
for page in paginator.paginate():
|
|
29
|
+
for secret in page.get("SecretList", []):
|
|
30
|
+
secret_name = secret["Name"]
|
|
31
|
+
secret_arn = secret["ARN"]
|
|
32
|
+
|
|
33
|
+
# Get full secret details (but not the actual secret value)
|
|
34
|
+
try:
|
|
35
|
+
secret_details = client.describe_secret(SecretId=secret_arn)
|
|
36
|
+
# Use detailed info for config hash
|
|
37
|
+
config_data = secret_details
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self.logger.debug(f"Could not get details for secret {secret_name}: {e}")
|
|
40
|
+
config_data = secret
|
|
41
|
+
|
|
42
|
+
# Extract tags
|
|
43
|
+
tags = {}
|
|
44
|
+
for tag in secret.get("Tags", []):
|
|
45
|
+
tags[tag["Key"]] = tag["Value"]
|
|
46
|
+
|
|
47
|
+
# Extract creation date
|
|
48
|
+
created_at = secret.get("CreatedDate")
|
|
49
|
+
|
|
50
|
+
# Create resource (without secret value for security)
|
|
51
|
+
# Remove sensitive fields if present
|
|
52
|
+
safe_config = {k: v for k, v in config_data.items() if k not in ["SecretString", "SecretBinary"]}
|
|
53
|
+
|
|
54
|
+
resource = Resource(
|
|
55
|
+
arn=secret_arn,
|
|
56
|
+
resource_type="AWS::SecretsManager::Secret",
|
|
57
|
+
name=secret_name,
|
|
58
|
+
region=self.region,
|
|
59
|
+
tags=tags,
|
|
60
|
+
config_hash=compute_config_hash(safe_config),
|
|
61
|
+
created_at=created_at,
|
|
62
|
+
raw_config=safe_config,
|
|
63
|
+
)
|
|
64
|
+
resources.append(resource)
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.error(f"Error collecting Secrets Manager secrets in {self.region}: {e}")
|
|
68
|
+
|
|
69
|
+
self.logger.debug(f"Collected {len(resources)} Secrets Manager secrets in {self.region}")
|
|
70
|
+
return resources
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""SNS resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SNSCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS SNS resources (topics)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "sns"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect SNS resources.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of SNS topics
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
client = self._create_client()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
paginator = client.get_paginator("list_topics")
|
|
28
|
+
for page in paginator.paginate():
|
|
29
|
+
for topic in page.get("Topics", []):
|
|
30
|
+
topic_arn = topic["TopicArn"]
|
|
31
|
+
|
|
32
|
+
# Get topic name from ARN
|
|
33
|
+
topic_name = topic_arn.split(":")[-1]
|
|
34
|
+
|
|
35
|
+
# Get topic attributes
|
|
36
|
+
try:
|
|
37
|
+
attrs_response = client.get_topic_attributes(TopicArn=topic_arn)
|
|
38
|
+
attributes = attrs_response.get("Attributes", {})
|
|
39
|
+
|
|
40
|
+
# Get tags
|
|
41
|
+
tags = {}
|
|
42
|
+
try:
|
|
43
|
+
tag_response = client.list_tags_for_resource(ResourceArn=topic_arn)
|
|
44
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("Tags", [])}
|
|
45
|
+
except Exception as e:
|
|
46
|
+
self.logger.debug(f"Could not get tags for SNS topic {topic_name}: {e}")
|
|
47
|
+
|
|
48
|
+
# Create resource
|
|
49
|
+
resource = Resource(
|
|
50
|
+
arn=topic_arn,
|
|
51
|
+
resource_type="AWS::SNS::Topic",
|
|
52
|
+
name=topic_name,
|
|
53
|
+
region=self.region,
|
|
54
|
+
tags=tags,
|
|
55
|
+
config_hash=compute_config_hash(attributes),
|
|
56
|
+
created_at=None, # SNS topics don't expose creation date
|
|
57
|
+
raw_config=attributes,
|
|
58
|
+
)
|
|
59
|
+
resources.append(resource)
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.logger.debug(f"Could not get attributes for SNS topic {topic_name}: {e}")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.logger.error(f"Error collecting SNS topics in {self.region}: {e}")
|
|
66
|
+
|
|
67
|
+
self.logger.debug(f"Collected {len(resources)} SNS topics in {self.region}")
|
|
68
|
+
return resources
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""SQS resource collector."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from ...models.resource import Resource
|
|
7
|
+
from ...utils.hash import compute_config_hash
|
|
8
|
+
from .base import BaseResourceCollector
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SQSCollector(BaseResourceCollector):
|
|
12
|
+
"""Collector for AWS SQS resources (queues)."""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def service_name(self) -> str:
|
|
16
|
+
return "sqs"
|
|
17
|
+
|
|
18
|
+
def collect(self) -> List[Resource]:
|
|
19
|
+
"""Collect SQS resources.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of SQS queues
|
|
23
|
+
"""
|
|
24
|
+
resources = []
|
|
25
|
+
account_id = self._get_account_id()
|
|
26
|
+
client = self._create_client()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
# List all queue URLs
|
|
30
|
+
response = client.list_queues()
|
|
31
|
+
queue_urls = response.get("QueueUrls", [])
|
|
32
|
+
|
|
33
|
+
for queue_url in queue_urls:
|
|
34
|
+
try:
|
|
35
|
+
# Get queue attributes
|
|
36
|
+
attrs_response = client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"])
|
|
37
|
+
attributes = attrs_response.get("Attributes", {})
|
|
38
|
+
|
|
39
|
+
# Get queue name from URL
|
|
40
|
+
queue_name = queue_url.split("/")[-1]
|
|
41
|
+
|
|
42
|
+
# Build ARN
|
|
43
|
+
queue_arn = attributes.get("QueueArn", f"arn:aws:sqs:{self.region}:{account_id}:{queue_name}")
|
|
44
|
+
|
|
45
|
+
# Get tags
|
|
46
|
+
tags = {}
|
|
47
|
+
try:
|
|
48
|
+
tag_response = client.list_queue_tags(QueueUrl=queue_url)
|
|
49
|
+
tags = tag_response.get("Tags", {})
|
|
50
|
+
except Exception as e:
|
|
51
|
+
self.logger.debug(f"Could not get tags for SQS queue {queue_name}: {e}")
|
|
52
|
+
|
|
53
|
+
# Convert CreatedTimestamp from epoch seconds to datetime
|
|
54
|
+
created_at = None
|
|
55
|
+
created_timestamp = attributes.get("CreatedTimestamp")
|
|
56
|
+
if created_timestamp:
|
|
57
|
+
try:
|
|
58
|
+
created_at = datetime.fromtimestamp(int(created_timestamp), tz=timezone.utc)
|
|
59
|
+
except (ValueError, OSError) as e:
|
|
60
|
+
self.logger.debug(f"Could not parse CreatedTimestamp for {queue_name}: {e}")
|
|
61
|
+
|
|
62
|
+
# Create resource
|
|
63
|
+
resource = Resource(
|
|
64
|
+
arn=queue_arn,
|
|
65
|
+
resource_type="AWS::SQS::Queue",
|
|
66
|
+
name=queue_name,
|
|
67
|
+
region=self.region,
|
|
68
|
+
tags=tags,
|
|
69
|
+
config_hash=compute_config_hash(attributes),
|
|
70
|
+
created_at=created_at,
|
|
71
|
+
raw_config=attributes,
|
|
72
|
+
)
|
|
73
|
+
resources.append(resource)
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self.logger.debug(f"Could not get attributes for SQS queue {queue_url}: {e}")
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
self.logger.error(f"Error collecting SQS queues in {self.region}: {e}")
|
|
80
|
+
|
|
81
|
+
self.logger.debug(f"Collected {len(resources)} SQS queues in {self.region}")
|
|
82
|
+
return resources
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Systems Manager resource collector."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from ...models.resource import Resource
|
|
6
|
+
from ...utils.hash import compute_config_hash
|
|
7
|
+
from .base import BaseResourceCollector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SSMCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS Systems Manager resources (Parameter Store, Documents)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "ssm"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect Systems Manager resources.
|
|
19
|
+
|
|
20
|
+
Collects:
|
|
21
|
+
- Parameter Store parameters
|
|
22
|
+
- SSM Documents (custom documents only)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
List of Systems Manager resources
|
|
26
|
+
"""
|
|
27
|
+
resources = []
|
|
28
|
+
|
|
29
|
+
# Collect Parameter Store parameters
|
|
30
|
+
resources.extend(self._collect_parameters())
|
|
31
|
+
|
|
32
|
+
# Collect SSM Documents
|
|
33
|
+
resources.extend(self._collect_documents())
|
|
34
|
+
|
|
35
|
+
self.logger.debug(f"Collected {len(resources)} Systems Manager resources in {self.region}")
|
|
36
|
+
return resources
|
|
37
|
+
|
|
38
|
+
def _collect_parameters(self) -> List[Resource]:
|
|
39
|
+
"""Collect Parameter Store parameters.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of Parameter resources
|
|
43
|
+
"""
|
|
44
|
+
resources = []
|
|
45
|
+
client = self._create_client()
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
paginator = client.get_paginator("describe_parameters")
|
|
49
|
+
for page in paginator.paginate():
|
|
50
|
+
for param in page.get("Parameters", []):
|
|
51
|
+
param_name = param["Name"]
|
|
52
|
+
param_type = param["Type"]
|
|
53
|
+
|
|
54
|
+
# Build ARN
|
|
55
|
+
# Get account ID from session
|
|
56
|
+
sts_client = self.session.client("sts")
|
|
57
|
+
account_id = sts_client.get_caller_identity()["Account"]
|
|
58
|
+
arn = f"arn:aws:ssm:{self.region}:{account_id}:parameter{param_name}"
|
|
59
|
+
|
|
60
|
+
# Get tags
|
|
61
|
+
tags = {}
|
|
62
|
+
try:
|
|
63
|
+
tag_response = client.list_tags_for_resource(ResourceType="Parameter", ResourceId=param_name)
|
|
64
|
+
for tag in tag_response.get("TagList", []):
|
|
65
|
+
tags[tag["Key"]] = tag["Value"]
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.debug(f"Could not get tags for parameter {param_name}: {e}")
|
|
68
|
+
|
|
69
|
+
# Get parameter details (metadata only, not the actual value for SecureString)
|
|
70
|
+
try:
|
|
71
|
+
# For SecureString, we don't want to expose the value
|
|
72
|
+
if param_type == "SecureString":
|
|
73
|
+
# Use describe_parameters data only
|
|
74
|
+
config = param
|
|
75
|
+
else:
|
|
76
|
+
# For String and StringList, we can get the value
|
|
77
|
+
param_details = client.get_parameter(Name=param_name, WithDecryption=False)
|
|
78
|
+
config = {
|
|
79
|
+
**param,
|
|
80
|
+
"Value": param_details["Parameter"].get("Value"),
|
|
81
|
+
}
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.logger.debug(f"Could not get details for parameter {param_name}: {e}")
|
|
84
|
+
config = param
|
|
85
|
+
|
|
86
|
+
# Extract last modified date as creation date proxy
|
|
87
|
+
created_at = param.get("LastModifiedDate")
|
|
88
|
+
|
|
89
|
+
# Create resource
|
|
90
|
+
resource = Resource(
|
|
91
|
+
arn=arn,
|
|
92
|
+
resource_type="AWS::SSM::Parameter",
|
|
93
|
+
name=param_name,
|
|
94
|
+
region=self.region,
|
|
95
|
+
tags=tags,
|
|
96
|
+
config_hash=compute_config_hash(config),
|
|
97
|
+
created_at=created_at,
|
|
98
|
+
raw_config=config,
|
|
99
|
+
)
|
|
100
|
+
resources.append(resource)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self.logger.error(f"Error collecting SSM parameters in {self.region}: {e}")
|
|
104
|
+
|
|
105
|
+
return resources
|
|
106
|
+
|
|
107
|
+
def _collect_documents(self) -> List[Resource]:
|
|
108
|
+
"""Collect SSM Documents (custom documents only).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of SSM Document resources
|
|
112
|
+
"""
|
|
113
|
+
resources = []
|
|
114
|
+
client = self._create_client()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# Only collect custom documents (not AWS-owned)
|
|
118
|
+
paginator = client.get_paginator("list_documents")
|
|
119
|
+
for page in paginator.paginate(Filters=[{"Key": "Owner", "Values": ["Self"]}]): # Only user-owned documents
|
|
120
|
+
for doc in page.get("DocumentIdentifiers", []):
|
|
121
|
+
doc_name = doc["Name"]
|
|
122
|
+
|
|
123
|
+
# Get document details
|
|
124
|
+
try:
|
|
125
|
+
doc_details = client.describe_document(Name=doc_name)["Document"]
|
|
126
|
+
|
|
127
|
+
# Build ARN
|
|
128
|
+
arn = doc_details.get("DocumentArn", doc_name)
|
|
129
|
+
|
|
130
|
+
# Get tags
|
|
131
|
+
tags = {}
|
|
132
|
+
for tag in doc_details.get("Tags", []):
|
|
133
|
+
tags[tag["Key"]] = tag["Value"]
|
|
134
|
+
|
|
135
|
+
# Extract creation date
|
|
136
|
+
created_at = doc_details.get("CreatedDate")
|
|
137
|
+
|
|
138
|
+
# Create resource (without the actual document content to keep size manageable)
|
|
139
|
+
config = {k: v for k, v in doc_details.items() if k != "Content"}
|
|
140
|
+
|
|
141
|
+
resource = Resource(
|
|
142
|
+
arn=arn,
|
|
143
|
+
resource_type="AWS::SSM::Document",
|
|
144
|
+
name=doc_name,
|
|
145
|
+
region=self.region,
|
|
146
|
+
tags=tags,
|
|
147
|
+
config_hash=compute_config_hash(config),
|
|
148
|
+
created_at=created_at,
|
|
149
|
+
raw_config=config,
|
|
150
|
+
)
|
|
151
|
+
resources.append(resource)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
self.logger.debug(f"Error processing document {doc_name}: {e}")
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
self.logger.error(f"Error collecting SSM documents in {self.region}: {e}")
|
|
159
|
+
|
|
160
|
+
return resources
|