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,136 @@
|
|
|
1
|
+
"""AWS Backup 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 BackupCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS Backup resources."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "backup"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect AWS Backup resources.
|
|
19
|
+
|
|
20
|
+
Collects:
|
|
21
|
+
- Backup Plans
|
|
22
|
+
- Backup Vaults
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
List of AWS Backup resources
|
|
26
|
+
"""
|
|
27
|
+
resources = []
|
|
28
|
+
|
|
29
|
+
# Collect Backup Plans
|
|
30
|
+
resources.extend(self._collect_backup_plans())
|
|
31
|
+
|
|
32
|
+
# Collect Backup Vaults
|
|
33
|
+
resources.extend(self._collect_backup_vaults())
|
|
34
|
+
|
|
35
|
+
self.logger.debug(f"Collected {len(resources)} AWS Backup resources in {self.region}")
|
|
36
|
+
return resources
|
|
37
|
+
|
|
38
|
+
def _collect_backup_plans(self) -> List[Resource]:
|
|
39
|
+
"""Collect Backup Plans.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of Backup Plan resources
|
|
43
|
+
"""
|
|
44
|
+
resources = []
|
|
45
|
+
client = self._create_client()
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
paginator = client.get_paginator("list_backup_plans")
|
|
49
|
+
for page in paginator.paginate():
|
|
50
|
+
for plan_summary in page.get("BackupPlansList", []):
|
|
51
|
+
plan_id = plan_summary["BackupPlanId"]
|
|
52
|
+
plan_name = plan_summary["BackupPlanName"]
|
|
53
|
+
plan_arn = plan_summary["BackupPlanArn"]
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Get detailed plan info
|
|
57
|
+
plan_response = client.get_backup_plan(BackupPlanId=plan_id)
|
|
58
|
+
plan = plan_response.get("BackupPlan", {})
|
|
59
|
+
|
|
60
|
+
# Get tags
|
|
61
|
+
tags = {}
|
|
62
|
+
try:
|
|
63
|
+
tag_response = client.list_tags(ResourceArn=plan_arn)
|
|
64
|
+
tags = tag_response.get("Tags", {})
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.logger.debug(f"Could not get tags for backup plan {plan_name}: {e}")
|
|
67
|
+
|
|
68
|
+
# Extract creation date
|
|
69
|
+
created_at = plan_summary.get("CreationDate")
|
|
70
|
+
|
|
71
|
+
# Create resource
|
|
72
|
+
resource = Resource(
|
|
73
|
+
arn=plan_arn,
|
|
74
|
+
resource_type="AWS::Backup::BackupPlan",
|
|
75
|
+
name=plan_name,
|
|
76
|
+
region=self.region,
|
|
77
|
+
tags=tags,
|
|
78
|
+
config_hash=compute_config_hash(plan),
|
|
79
|
+
created_at=created_at,
|
|
80
|
+
raw_config=plan,
|
|
81
|
+
)
|
|
82
|
+
resources.append(resource)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.logger.debug(f"Error processing backup plan {plan_name}: {e}")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
self.logger.error(f"Error collecting Backup Plans in {self.region}: {e}")
|
|
90
|
+
|
|
91
|
+
return resources
|
|
92
|
+
|
|
93
|
+
def _collect_backup_vaults(self) -> List[Resource]:
|
|
94
|
+
"""Collect Backup Vaults.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of Backup Vault resources
|
|
98
|
+
"""
|
|
99
|
+
resources = []
|
|
100
|
+
client = self._create_client()
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
paginator = client.get_paginator("list_backup_vaults")
|
|
104
|
+
for page in paginator.paginate():
|
|
105
|
+
for vault in page.get("BackupVaultList", []):
|
|
106
|
+
vault_name = vault["BackupVaultName"]
|
|
107
|
+
vault_arn = vault["BackupVaultArn"]
|
|
108
|
+
|
|
109
|
+
# Get tags
|
|
110
|
+
tags = {}
|
|
111
|
+
try:
|
|
112
|
+
tag_response = client.list_tags(ResourceArn=vault_arn)
|
|
113
|
+
tags = tag_response.get("Tags", {})
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.logger.debug(f"Could not get tags for backup vault {vault_name}: {e}")
|
|
116
|
+
|
|
117
|
+
# Extract creation date
|
|
118
|
+
created_at = vault.get("CreationDate")
|
|
119
|
+
|
|
120
|
+
# Create resource
|
|
121
|
+
resource = Resource(
|
|
122
|
+
arn=vault_arn,
|
|
123
|
+
resource_type="AWS::Backup::BackupVault",
|
|
124
|
+
name=vault_name,
|
|
125
|
+
region=self.region,
|
|
126
|
+
tags=tags,
|
|
127
|
+
config_hash=compute_config_hash(vault),
|
|
128
|
+
created_at=created_at,
|
|
129
|
+
raw_config=vault,
|
|
130
|
+
)
|
|
131
|
+
resources.append(resource)
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.logger.error(f"Error collecting Backup Vaults in {self.region}: {e}")
|
|
135
|
+
|
|
136
|
+
return resources
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Base resource collector interface."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
|
|
9
|
+
from ...models.resource import Resource
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseResourceCollector(ABC):
|
|
15
|
+
"""Abstract base class for AWS resource collectors.
|
|
16
|
+
|
|
17
|
+
Each AWS service (EC2, IAM, Lambda, etc.) should implement this interface
|
|
18
|
+
to provide a consistent way of collecting resources.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, session: boto3.Session, region: str):
|
|
22
|
+
"""Initialize the collector.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
session: Boto3 session with AWS credentials
|
|
26
|
+
region: AWS region to collect from (may be ignored for global services)
|
|
27
|
+
"""
|
|
28
|
+
self.session = session
|
|
29
|
+
self.region = region
|
|
30
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def collect(self) -> List[Resource]:
|
|
34
|
+
"""Collect all resources for this service.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of Resource instances
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
Exception: If collection fails (should be handled by caller)
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def service_name(self) -> str:
|
|
47
|
+
"""Return the AWS service name (e.g., 'ec2', 'iam', 'lambda')."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_global_service(self) -> bool:
|
|
52
|
+
"""Return True if this is a global service (like IAM).
|
|
53
|
+
|
|
54
|
+
Global services should only be collected once, not per-region.
|
|
55
|
+
"""
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def _create_client(self, service_name: Optional[str] = None): # type: ignore
|
|
59
|
+
"""Create a boto3 client for this service.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
service_name: Service name override (defaults to self.service_name)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Boto3 client instance
|
|
66
|
+
"""
|
|
67
|
+
from ...aws.client import create_boto_client
|
|
68
|
+
|
|
69
|
+
svc = service_name or self.service_name
|
|
70
|
+
profile = self.session.profile_name if hasattr(self.session, "profile_name") else None
|
|
71
|
+
|
|
72
|
+
return create_boto_client(service_name=svc, region_name=self.region, profile_name=profile)
|
|
73
|
+
|
|
74
|
+
def _get_account_id(self) -> str:
|
|
75
|
+
"""Get the AWS account ID from the session.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
12-digit AWS account ID
|
|
79
|
+
"""
|
|
80
|
+
sts = self._create_client("sts")
|
|
81
|
+
return sts.get_caller_identity()["Account"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""CloudFormation 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 CloudFormationCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS CloudFormation resources (stacks)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "cloudformation"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect CloudFormation resources.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of CloudFormation stacks
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
client = self._create_client()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
paginator = client.get_paginator("describe_stacks")
|
|
28
|
+
for page in paginator.paginate():
|
|
29
|
+
for stack in page.get("Stacks", []):
|
|
30
|
+
stack_name = stack["StackName"]
|
|
31
|
+
stack_id = stack["StackId"]
|
|
32
|
+
|
|
33
|
+
# Extract tags
|
|
34
|
+
tags = {}
|
|
35
|
+
for tag in stack.get("Tags", []):
|
|
36
|
+
tags[tag["Key"]] = tag["Value"]
|
|
37
|
+
|
|
38
|
+
# Create resource
|
|
39
|
+
resource = Resource(
|
|
40
|
+
arn=stack_id, # Stack ID is actually the ARN
|
|
41
|
+
resource_type="AWS::CloudFormation::Stack",
|
|
42
|
+
name=stack_name,
|
|
43
|
+
region=self.region,
|
|
44
|
+
tags=tags,
|
|
45
|
+
config_hash=compute_config_hash(stack),
|
|
46
|
+
created_at=stack.get("CreationTime"),
|
|
47
|
+
raw_config=stack,
|
|
48
|
+
)
|
|
49
|
+
resources.append(resource)
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
self.logger.error(f"Error collecting CloudFormation stacks in {self.region}: {e}")
|
|
53
|
+
|
|
54
|
+
self.logger.debug(f"Collected {len(resources)} CloudFormation stacks in {self.region}")
|
|
55
|
+
return resources
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""CloudWatch 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 CloudWatchCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS CloudWatch resources (alarms, log groups)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "cloudwatch"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect CloudWatch resources.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of CloudWatch resources
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
account_id = self._get_account_id()
|
|
25
|
+
|
|
26
|
+
# Collect alarms
|
|
27
|
+
resources.extend(self._collect_alarms(account_id))
|
|
28
|
+
|
|
29
|
+
# Collect log groups
|
|
30
|
+
resources.extend(self._collect_log_groups(account_id))
|
|
31
|
+
|
|
32
|
+
self.logger.debug(f"Collected {len(resources)} CloudWatch resources in {self.region}")
|
|
33
|
+
return resources
|
|
34
|
+
|
|
35
|
+
def _collect_alarms(self, account_id: str) -> List[Resource]:
|
|
36
|
+
"""Collect CloudWatch alarms."""
|
|
37
|
+
resources = []
|
|
38
|
+
client = self._create_client()
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
paginator = client.get_paginator("describe_alarms")
|
|
42
|
+
for page in paginator.paginate():
|
|
43
|
+
for alarm in page.get("MetricAlarms", []) + page.get("CompositeAlarms", []):
|
|
44
|
+
alarm_name = alarm["AlarmName"]
|
|
45
|
+
alarm_arn = alarm["AlarmArn"]
|
|
46
|
+
|
|
47
|
+
# Extract alarm type
|
|
48
|
+
alarm_type = "AWS::CloudWatch::Alarm"
|
|
49
|
+
if "CompositeAlarms" in str(type(alarm)):
|
|
50
|
+
alarm_type = "AWS::CloudWatch::CompositeAlarm"
|
|
51
|
+
|
|
52
|
+
# Create resource (CloudWatch alarms don't support tags directly)
|
|
53
|
+
resource = Resource(
|
|
54
|
+
arn=alarm_arn,
|
|
55
|
+
resource_type=alarm_type,
|
|
56
|
+
name=alarm_name,
|
|
57
|
+
region=self.region,
|
|
58
|
+
tags={},
|
|
59
|
+
config_hash=compute_config_hash(alarm),
|
|
60
|
+
created_at=alarm.get("AlarmConfigurationUpdatedTimestamp"),
|
|
61
|
+
raw_config=alarm,
|
|
62
|
+
)
|
|
63
|
+
resources.append(resource)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.logger.error(f"Error collecting CloudWatch alarms in {self.region}: {e}")
|
|
67
|
+
|
|
68
|
+
return resources
|
|
69
|
+
|
|
70
|
+
def _collect_log_groups(self, account_id: str) -> List[Resource]:
|
|
71
|
+
"""Collect CloudWatch log groups."""
|
|
72
|
+
resources = []
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
logs_client = self._create_client("logs")
|
|
76
|
+
|
|
77
|
+
paginator = logs_client.get_paginator("describe_log_groups")
|
|
78
|
+
for page in paginator.paginate():
|
|
79
|
+
for log_group in page.get("logGroups", []):
|
|
80
|
+
log_group_name = log_group["logGroupName"]
|
|
81
|
+
|
|
82
|
+
# Build ARN
|
|
83
|
+
arn = log_group.get("arn", f"arn:aws:logs:{self.region}:{account_id}:log-group:{log_group_name}")
|
|
84
|
+
|
|
85
|
+
# Get tags
|
|
86
|
+
tags = {}
|
|
87
|
+
try:
|
|
88
|
+
tag_response = logs_client.list_tags_log_group(logGroupName=log_group_name)
|
|
89
|
+
tags = tag_response.get("tags", {})
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self.logger.debug(f"Could not get tags for log group {log_group_name}: {e}")
|
|
92
|
+
|
|
93
|
+
# Create resource
|
|
94
|
+
resource = Resource(
|
|
95
|
+
arn=arn,
|
|
96
|
+
resource_type="AWS::Logs::LogGroup",
|
|
97
|
+
name=log_group_name,
|
|
98
|
+
region=self.region,
|
|
99
|
+
tags=tags,
|
|
100
|
+
config_hash=compute_config_hash(log_group),
|
|
101
|
+
created_at=None, # Log groups don't expose creation timestamp easily
|
|
102
|
+
raw_config=log_group,
|
|
103
|
+
)
|
|
104
|
+
resources.append(resource)
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
self.logger.error(f"Error collecting CloudWatch log groups in {self.region}: {e}")
|
|
108
|
+
|
|
109
|
+
return resources
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""CodeBuild 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 CodeBuildCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS CodeBuild resources."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "codebuild"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect CodeBuild projects.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of CodeBuild project resources
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
client = self._create_client()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
paginator = client.get_paginator("list_projects")
|
|
28
|
+
project_names = []
|
|
29
|
+
for page in paginator.paginate():
|
|
30
|
+
project_names.extend(page.get("projects", []))
|
|
31
|
+
|
|
32
|
+
# Get detailed info for projects (in batches of 100)
|
|
33
|
+
for i in range(0, len(project_names), 100):
|
|
34
|
+
batch = project_names[i : i + 100]
|
|
35
|
+
try:
|
|
36
|
+
response = client.batch_get_projects(names=batch)
|
|
37
|
+
for project in response.get("projects", []):
|
|
38
|
+
project_name = project["name"]
|
|
39
|
+
project_arn = project["arn"]
|
|
40
|
+
|
|
41
|
+
# Extract tags
|
|
42
|
+
tags = {}
|
|
43
|
+
for tag in project.get("tags", []):
|
|
44
|
+
tags[tag["key"]] = tag["value"]
|
|
45
|
+
|
|
46
|
+
# Extract creation date
|
|
47
|
+
created_at = project.get("created")
|
|
48
|
+
|
|
49
|
+
# Create resource
|
|
50
|
+
resource = Resource(
|
|
51
|
+
arn=project_arn,
|
|
52
|
+
resource_type="AWS::CodeBuild::Project",
|
|
53
|
+
name=project_name,
|
|
54
|
+
region=self.region,
|
|
55
|
+
tags=tags,
|
|
56
|
+
config_hash=compute_config_hash(project),
|
|
57
|
+
created_at=created_at,
|
|
58
|
+
raw_config=project,
|
|
59
|
+
)
|
|
60
|
+
resources.append(resource)
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.logger.debug(f"Error processing project batch: {e}")
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.logger.error(f"Error collecting CodeBuild projects in {self.region}: {e}")
|
|
67
|
+
|
|
68
|
+
self.logger.debug(f"Collected {len(resources)} CodeBuild projects in {self.region}")
|
|
69
|
+
return resources
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""CodePipeline 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 CodePipelineCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS CodePipeline resources."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "codepipeline"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect CodePipeline pipelines.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of CodePipeline resources
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
client = self._create_client()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
paginator = client.get_paginator("list_pipelines")
|
|
28
|
+
for page in paginator.paginate():
|
|
29
|
+
for pipeline_summary in page.get("pipelines", []):
|
|
30
|
+
pipeline_name = pipeline_summary["name"]
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# Get detailed pipeline info
|
|
34
|
+
pipeline_response = client.get_pipeline(name=pipeline_name)
|
|
35
|
+
pipeline = pipeline_response.get("pipeline", {})
|
|
36
|
+
metadata = pipeline_response.get("metadata", {})
|
|
37
|
+
|
|
38
|
+
# Get tags
|
|
39
|
+
tags = {}
|
|
40
|
+
try:
|
|
41
|
+
# Build ARN for tags
|
|
42
|
+
pipeline_arn = metadata.get("pipelineArn", "")
|
|
43
|
+
if pipeline_arn:
|
|
44
|
+
tag_response = client.list_tags_for_resource(resourceArn=pipeline_arn)
|
|
45
|
+
for tag in tag_response.get("tags", []):
|
|
46
|
+
tags[tag["key"]] = tag["value"]
|
|
47
|
+
except Exception as e:
|
|
48
|
+
self.logger.debug(f"Could not get tags for pipeline {pipeline_name}: {e}")
|
|
49
|
+
|
|
50
|
+
# Extract creation/update dates
|
|
51
|
+
created_at = metadata.get("created")
|
|
52
|
+
|
|
53
|
+
# Build ARN
|
|
54
|
+
arn = metadata.get(
|
|
55
|
+
"pipelineArn", f"arn:aws:codepipeline:{self.region}:*:pipeline/{pipeline_name}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Create resource (exclude large stage/action definitions for size)
|
|
59
|
+
config = {k: v for k, v in pipeline.items() if k not in ["stages"]}
|
|
60
|
+
config["stageCount"] = len(pipeline.get("stages", []))
|
|
61
|
+
|
|
62
|
+
resource = Resource(
|
|
63
|
+
arn=arn,
|
|
64
|
+
resource_type="AWS::CodePipeline::Pipeline",
|
|
65
|
+
name=pipeline_name,
|
|
66
|
+
region=self.region,
|
|
67
|
+
tags=tags,
|
|
68
|
+
config_hash=compute_config_hash(config),
|
|
69
|
+
created_at=created_at,
|
|
70
|
+
raw_config=config,
|
|
71
|
+
)
|
|
72
|
+
resources.append(resource)
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.logger.debug(f"Error processing pipeline {pipeline_name}: {e}")
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
self.logger.error(f"Error collecting CodePipeline pipelines in {self.region}: {e}")
|
|
80
|
+
|
|
81
|
+
self.logger.debug(f"Collected {len(resources)} CodePipeline pipelines in {self.region}")
|
|
82
|
+
return resources
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""DynamoDB 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 DynamoDBCollector(BaseResourceCollector):
|
|
11
|
+
"""Collector for AWS DynamoDB resources (tables)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def service_name(self) -> str:
|
|
15
|
+
return "dynamodb"
|
|
16
|
+
|
|
17
|
+
def collect(self) -> List[Resource]:
|
|
18
|
+
"""Collect DynamoDB resources.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of DynamoDB tables
|
|
22
|
+
"""
|
|
23
|
+
resources = []
|
|
24
|
+
client = self._create_client()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
paginator = client.get_paginator("list_tables")
|
|
28
|
+
for page in paginator.paginate():
|
|
29
|
+
for table_name in page.get("TableNames", []):
|
|
30
|
+
try:
|
|
31
|
+
# Get table details
|
|
32
|
+
table_response = client.describe_table(TableName=table_name)
|
|
33
|
+
table = table_response.get("Table", {})
|
|
34
|
+
|
|
35
|
+
table_arn = table.get("TableArn")
|
|
36
|
+
|
|
37
|
+
# Get tags
|
|
38
|
+
tags = {}
|
|
39
|
+
try:
|
|
40
|
+
tag_response = client.list_tags_of_resource(ResourceArn=table_arn)
|
|
41
|
+
tags = {tag["Key"]: tag["Value"] for tag in tag_response.get("Tags", [])}
|
|
42
|
+
except Exception as e:
|
|
43
|
+
self.logger.debug(f"Could not get tags for DynamoDB table {table_name}: {e}")
|
|
44
|
+
|
|
45
|
+
# Create resource
|
|
46
|
+
resource = Resource(
|
|
47
|
+
arn=table_arn,
|
|
48
|
+
resource_type="AWS::DynamoDB::Table",
|
|
49
|
+
name=table_name,
|
|
50
|
+
region=self.region,
|
|
51
|
+
tags=tags,
|
|
52
|
+
config_hash=compute_config_hash(table),
|
|
53
|
+
created_at=table.get("CreationDateTime"),
|
|
54
|
+
raw_config=table,
|
|
55
|
+
)
|
|
56
|
+
resources.append(resource)
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self.logger.debug(f"Could not get details for DynamoDB table {table_name}: {e}")
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.logger.error(f"Error collecting DynamoDB tables in {self.region}: {e}")
|
|
63
|
+
|
|
64
|
+
self.logger.debug(f"Collected {len(resources)} DynamoDB tables in {self.region}")
|
|
65
|
+
return resources
|