aws-inventory-manager 0.13.2__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 aws-inventory-manager might be problematic. Click here for more details.

Files changed (145) hide show
  1. aws_inventory_manager-0.13.2.dist-info/LICENSE +21 -0
  2. aws_inventory_manager-0.13.2.dist-info/METADATA +1226 -0
  3. aws_inventory_manager-0.13.2.dist-info/RECORD +145 -0
  4. aws_inventory_manager-0.13.2.dist-info/WHEEL +5 -0
  5. aws_inventory_manager-0.13.2.dist-info/entry_points.txt +2 -0
  6. aws_inventory_manager-0.13.2.dist-info/top_level.txt +1 -0
  7. src/__init__.py +3 -0
  8. src/aws/__init__.py +11 -0
  9. src/aws/client.py +128 -0
  10. src/aws/credentials.py +191 -0
  11. src/aws/rate_limiter.py +177 -0
  12. src/cli/__init__.py +12 -0
  13. src/cli/config.py +130 -0
  14. src/cli/main.py +3626 -0
  15. src/config_service/__init__.py +21 -0
  16. src/config_service/collector.py +346 -0
  17. src/config_service/detector.py +256 -0
  18. src/config_service/resource_type_mapping.py +328 -0
  19. src/cost/__init__.py +5 -0
  20. src/cost/analyzer.py +226 -0
  21. src/cost/explorer.py +209 -0
  22. src/cost/reporter.py +237 -0
  23. src/delta/__init__.py +5 -0
  24. src/delta/calculator.py +206 -0
  25. src/delta/differ.py +185 -0
  26. src/delta/formatters.py +272 -0
  27. src/delta/models.py +154 -0
  28. src/delta/reporter.py +234 -0
  29. src/models/__init__.py +21 -0
  30. src/models/config_diff.py +135 -0
  31. src/models/cost_report.py +87 -0
  32. src/models/deletion_operation.py +104 -0
  33. src/models/deletion_record.py +97 -0
  34. src/models/delta_report.py +122 -0
  35. src/models/efs_resource.py +80 -0
  36. src/models/elasticache_resource.py +90 -0
  37. src/models/group.py +318 -0
  38. src/models/inventory.py +133 -0
  39. src/models/protection_rule.py +123 -0
  40. src/models/report.py +288 -0
  41. src/models/resource.py +111 -0
  42. src/models/security_finding.py +102 -0
  43. src/models/snapshot.py +122 -0
  44. src/restore/__init__.py +20 -0
  45. src/restore/audit.py +175 -0
  46. src/restore/cleaner.py +461 -0
  47. src/restore/config.py +209 -0
  48. src/restore/deleter.py +976 -0
  49. src/restore/dependency.py +254 -0
  50. src/restore/safety.py +115 -0
  51. src/security/__init__.py +0 -0
  52. src/security/checks/__init__.py +0 -0
  53. src/security/checks/base.py +56 -0
  54. src/security/checks/ec2_checks.py +88 -0
  55. src/security/checks/elasticache_checks.py +149 -0
  56. src/security/checks/iam_checks.py +102 -0
  57. src/security/checks/rds_checks.py +140 -0
  58. src/security/checks/s3_checks.py +95 -0
  59. src/security/checks/secrets_checks.py +96 -0
  60. src/security/checks/sg_checks.py +142 -0
  61. src/security/cis_mapper.py +97 -0
  62. src/security/models.py +53 -0
  63. src/security/reporter.py +174 -0
  64. src/security/scanner.py +87 -0
  65. src/snapshot/__init__.py +6 -0
  66. src/snapshot/capturer.py +451 -0
  67. src/snapshot/filter.py +259 -0
  68. src/snapshot/inventory_storage.py +236 -0
  69. src/snapshot/report_formatter.py +250 -0
  70. src/snapshot/reporter.py +189 -0
  71. src/snapshot/resource_collectors/__init__.py +5 -0
  72. src/snapshot/resource_collectors/apigateway.py +140 -0
  73. src/snapshot/resource_collectors/backup.py +136 -0
  74. src/snapshot/resource_collectors/base.py +81 -0
  75. src/snapshot/resource_collectors/cloudformation.py +55 -0
  76. src/snapshot/resource_collectors/cloudwatch.py +109 -0
  77. src/snapshot/resource_collectors/codebuild.py +69 -0
  78. src/snapshot/resource_collectors/codepipeline.py +82 -0
  79. src/snapshot/resource_collectors/dynamodb.py +65 -0
  80. src/snapshot/resource_collectors/ec2.py +240 -0
  81. src/snapshot/resource_collectors/ecs.py +215 -0
  82. src/snapshot/resource_collectors/efs_collector.py +102 -0
  83. src/snapshot/resource_collectors/eks.py +200 -0
  84. src/snapshot/resource_collectors/elasticache_collector.py +79 -0
  85. src/snapshot/resource_collectors/elb.py +126 -0
  86. src/snapshot/resource_collectors/eventbridge.py +156 -0
  87. src/snapshot/resource_collectors/iam.py +188 -0
  88. src/snapshot/resource_collectors/kms.py +111 -0
  89. src/snapshot/resource_collectors/lambda_func.py +139 -0
  90. src/snapshot/resource_collectors/rds.py +109 -0
  91. src/snapshot/resource_collectors/route53.py +86 -0
  92. src/snapshot/resource_collectors/s3.py +105 -0
  93. src/snapshot/resource_collectors/secretsmanager.py +70 -0
  94. src/snapshot/resource_collectors/sns.py +68 -0
  95. src/snapshot/resource_collectors/sqs.py +82 -0
  96. src/snapshot/resource_collectors/ssm.py +160 -0
  97. src/snapshot/resource_collectors/stepfunctions.py +74 -0
  98. src/snapshot/resource_collectors/vpcendpoints.py +79 -0
  99. src/snapshot/resource_collectors/waf.py +159 -0
  100. src/snapshot/storage.py +351 -0
  101. src/storage/__init__.py +21 -0
  102. src/storage/audit_store.py +419 -0
  103. src/storage/database.py +294 -0
  104. src/storage/group_store.py +749 -0
  105. src/storage/inventory_store.py +320 -0
  106. src/storage/resource_store.py +413 -0
  107. src/storage/schema.py +288 -0
  108. src/storage/snapshot_store.py +346 -0
  109. src/utils/__init__.py +12 -0
  110. src/utils/export.py +305 -0
  111. src/utils/hash.py +60 -0
  112. src/utils/logging.py +63 -0
  113. src/utils/pagination.py +41 -0
  114. src/utils/paths.py +51 -0
  115. src/utils/progress.py +41 -0
  116. src/utils/unsupported_resources.py +306 -0
  117. src/web/__init__.py +5 -0
  118. src/web/app.py +97 -0
  119. src/web/dependencies.py +69 -0
  120. src/web/routes/__init__.py +1 -0
  121. src/web/routes/api/__init__.py +18 -0
  122. src/web/routes/api/charts.py +156 -0
  123. src/web/routes/api/cleanup.py +186 -0
  124. src/web/routes/api/filters.py +253 -0
  125. src/web/routes/api/groups.py +305 -0
  126. src/web/routes/api/inventories.py +80 -0
  127. src/web/routes/api/queries.py +202 -0
  128. src/web/routes/api/resources.py +379 -0
  129. src/web/routes/api/snapshots.py +314 -0
  130. src/web/routes/api/views.py +260 -0
  131. src/web/routes/pages.py +198 -0
  132. src/web/services/__init__.py +1 -0
  133. src/web/templates/base.html +949 -0
  134. src/web/templates/components/navbar.html +31 -0
  135. src/web/templates/components/sidebar.html +104 -0
  136. src/web/templates/pages/audit_logs.html +86 -0
  137. src/web/templates/pages/cleanup.html +279 -0
  138. src/web/templates/pages/dashboard.html +227 -0
  139. src/web/templates/pages/diff.html +175 -0
  140. src/web/templates/pages/error.html +30 -0
  141. src/web/templates/pages/groups.html +721 -0
  142. src/web/templates/pages/queries.html +246 -0
  143. src/web/templates/pages/resources.html +2251 -0
  144. src/web/templates/pages/snapshot_detail.html +271 -0
  145. src/web/templates/pages/snapshots.html +429 -0
@@ -0,0 +1,102 @@
1
+ """Security finding model for representing detected security issues in AWS resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any, Dict, Optional
8
+
9
+
10
+ class Severity(Enum):
11
+ """Security finding severity levels."""
12
+
13
+ CRITICAL = "critical"
14
+ HIGH = "high"
15
+ MEDIUM = "medium"
16
+ LOW = "low"
17
+
18
+
19
+ @dataclass
20
+ class SecurityFinding:
21
+ """Represents a detected security misconfiguration in an AWS resource.
22
+
23
+ Attributes:
24
+ resource_arn: AWS ARN of the resource with the security issue
25
+ finding_type: Type of security issue (e.g., "public_s3_bucket", "open_security_group")
26
+ severity: Severity level of the finding
27
+ description: Human-readable description of the issue
28
+ remediation: Guidance on how to fix the issue
29
+ cis_control: Optional CIS AWS Foundations Benchmark control ID (e.g., "2.1.5")
30
+ metadata: Additional context-specific metadata
31
+ """
32
+
33
+ resource_arn: str
34
+ finding_type: str
35
+ severity: Severity
36
+ description: str
37
+ remediation: str
38
+ cis_control: Optional[str] = None
39
+ metadata: Optional[Dict[str, Any]] = None
40
+
41
+ def __post_init__(self) -> None:
42
+ """Validate SecurityFinding fields after initialization."""
43
+ # Validate ARN format (basic check)
44
+ if not self.resource_arn or not self.resource_arn.startswith("arn:"):
45
+ raise ValueError(f"Invalid ARN format: {self.resource_arn}")
46
+
47
+ # Validate severity is Severity enum
48
+ if not isinstance(self.severity, Severity):
49
+ raise ValueError(f"Invalid severity type: {type(self.severity)}. Must be Severity enum.")
50
+
51
+ # Validate required string fields are not empty
52
+ if not self.finding_type:
53
+ raise ValueError("finding_type cannot be empty")
54
+ if not self.description:
55
+ raise ValueError("description cannot be empty")
56
+ if not self.remediation:
57
+ raise ValueError("remediation cannot be empty")
58
+
59
+ def to_dict(self) -> Dict[str, Any]:
60
+ """Convert SecurityFinding to dictionary representation.
61
+
62
+ Returns:
63
+ Dictionary with all finding attributes
64
+ """
65
+ return {
66
+ "resource_arn": self.resource_arn,
67
+ "finding_type": self.finding_type,
68
+ "severity": self.severity.value,
69
+ "description": self.description,
70
+ "remediation": self.remediation,
71
+ "cis_control": self.cis_control,
72
+ "metadata": self.metadata or {},
73
+ }
74
+
75
+ @classmethod
76
+ def from_dict(cls, data: Dict[str, Any]) -> SecurityFinding:
77
+ """Create SecurityFinding from dictionary representation.
78
+
79
+ Args:
80
+ data: Dictionary with finding attributes
81
+
82
+ Returns:
83
+ SecurityFinding instance
84
+
85
+ Raises:
86
+ ValueError: If severity value is invalid
87
+ """
88
+ severity_str = data.get("severity", "").lower()
89
+ try:
90
+ severity = Severity(severity_str)
91
+ except ValueError:
92
+ raise ValueError(f"Invalid severity value: {severity_str}")
93
+
94
+ return cls(
95
+ resource_arn=data["resource_arn"],
96
+ finding_type=data["finding_type"],
97
+ severity=severity,
98
+ description=data["description"],
99
+ remediation=data["remediation"],
100
+ cis_control=data.get("cis_control"),
101
+ metadata=data.get("metadata"),
102
+ )
src/models/snapshot.py ADDED
@@ -0,0 +1,122 @@
1
+ """Snapshot data model representing a point-in-time inventory of AWS resources."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ @dataclass
9
+ class Snapshot:
10
+ """Represents a point-in-time inventory of AWS resources.
11
+
12
+ This serves as the baseline reference for delta tracking and cost analysis.
13
+
14
+ Schema Versions:
15
+ - v1.0: Basic snapshot with config_hash only
16
+ - v1.1: Added raw_config for drift analysis support
17
+ """
18
+
19
+ name: str
20
+ created_at: datetime
21
+ account_id: str
22
+ regions: List[str]
23
+ resources: List[Any] # List[Resource] - avoiding circular import
24
+ is_active: bool = True
25
+ resource_count: int = 0
26
+ service_counts: Dict[str, int] = field(default_factory=dict)
27
+ metadata: Dict[str, Any] = field(default_factory=dict)
28
+ filters_applied: Optional[Dict[str, Any]] = None
29
+ total_resources_before_filter: Optional[int] = None
30
+ inventory_name: str = "default" # Name of inventory this snapshot belongs to
31
+ schema_version: str = "1.1" # Schema version for forward/backward compatibility
32
+
33
+ def __post_init__(self) -> None:
34
+ """Calculate derived fields after initialization."""
35
+ if self.resource_count == 0:
36
+ self.resource_count = len(self.resources)
37
+
38
+ if not self.service_counts:
39
+ self._calculate_service_counts()
40
+
41
+ def _calculate_service_counts(self) -> None:
42
+ """Calculate resource counts by service type."""
43
+ counts: Dict[str, int] = {}
44
+ for resource in self.resources:
45
+ service = resource.resource_type.split(":")[0] if ":" in resource.resource_type else resource.resource_type
46
+ counts[service] = counts.get(service, 0) + 1
47
+ self.service_counts = counts
48
+
49
+ def to_dict(self) -> Dict[str, Any]:
50
+ """Convert snapshot to dictionary for serialization."""
51
+ return {
52
+ "schema_version": self.schema_version,
53
+ "name": self.name,
54
+ "created_at": self.created_at.isoformat(),
55
+ "account_id": self.account_id,
56
+ "regions": self.regions,
57
+ "is_active": self.is_active,
58
+ "resource_count": self.resource_count,
59
+ "service_counts": self.service_counts,
60
+ "metadata": self.metadata,
61
+ "filters_applied": self.filters_applied,
62
+ "total_resources_before_filter": self.total_resources_before_filter,
63
+ "inventory_name": self.inventory_name,
64
+ "resources": [r.to_dict() for r in self.resources],
65
+ }
66
+
67
+ @classmethod
68
+ def from_dict(cls, data: Dict[str, Any]) -> "Snapshot":
69
+ """Create snapshot from dictionary.
70
+
71
+ Supports both v1.0 and v1.1+ snapshot formats for backward compatibility.
72
+
73
+ Note: This requires Resource class to be imported at call time
74
+ to avoid circular imports.
75
+ """
76
+ from .resource import Resource
77
+
78
+ # Handle created_at being either string or datetime (PyYAML can auto-parse)
79
+ created_at = data["created_at"]
80
+ if isinstance(created_at, str):
81
+ created_at = datetime.fromisoformat(created_at)
82
+
83
+ return cls(
84
+ name=data["name"],
85
+ created_at=created_at,
86
+ account_id=data["account_id"],
87
+ regions=data["regions"],
88
+ resources=[Resource.from_dict(r) for r in data["resources"]],
89
+ is_active=data.get("is_active", True),
90
+ resource_count=data.get("resource_count", 0),
91
+ service_counts=data.get("service_counts", {}),
92
+ metadata=data.get("metadata", {}),
93
+ filters_applied=data.get("filters_applied"),
94
+ total_resources_before_filter=data.get("total_resources_before_filter"),
95
+ inventory_name=data.get("inventory_name", "default"), # Default for backward compatibility
96
+ schema_version=data.get("schema_version", "1.0"), # Default to 1.0 for old snapshots
97
+ )
98
+
99
+ def validate(self) -> bool:
100
+ """Validate snapshot data integrity.
101
+
102
+ Returns:
103
+ True if valid, raises ValueError if invalid
104
+ """
105
+ import re
106
+
107
+ # Validate name format (alphanumeric, hyphens, underscores)
108
+ if not re.match(r"^[a-zA-Z0-9_-]+$", self.name):
109
+ raise ValueError(
110
+ f"Invalid snapshot name: {self.name}. "
111
+ f"Must contain only alphanumeric characters, hyphens, and underscores."
112
+ )
113
+
114
+ # Validate account ID (12-digit string)
115
+ if not re.match(r"^\d{12}$", self.account_id):
116
+ raise ValueError(f"Invalid AWS account ID: {self.account_id}. Must be a 12-digit string.")
117
+
118
+ # Validate regions list is not empty
119
+ if not self.regions:
120
+ raise ValueError("Snapshot must include at least one AWS region.")
121
+
122
+ return True
@@ -0,0 +1,20 @@
1
+ """Resource cleanup/restoration module.
2
+
3
+ This module provides functionality to delete AWS resources created after a baseline
4
+ snapshot, with comprehensive safety protections.
5
+
6
+ Classes:
7
+ ResourceCleaner: Main orchestrator for restore operations
8
+ DependencyResolver: Dependency graph construction and deletion ordering
9
+ SafetyChecker: Protection rule evaluation
10
+ AuditStorage: Audit log storage and retrieval
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = [
16
+ "ResourceCleaner",
17
+ "DependencyResolver",
18
+ "SafetyChecker",
19
+ "AuditStorage",
20
+ ]
src/restore/audit.py ADDED
@@ -0,0 +1,175 @@
1
+ """Audit storage for deletion operations.
2
+
3
+ Stores and retrieves audit logs in YAML format for compliance and troubleshooting.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import yaml
13
+
14
+ from src.models.deletion_operation import DeletionOperation
15
+ from src.models.deletion_record import DeletionRecord
16
+
17
+
18
+ class AuditStorage:
19
+ """Audit log storage and retrieval.
20
+
21
+ Stores deletion operation audit logs as YAML files organized by year/month.
22
+ Supports querying operations by date range and retrieving detailed operation logs.
23
+
24
+ Storage structure:
25
+ ~/.snapshots/audit-logs/
26
+ 2025/
27
+ 11/
28
+ operation-op_123.yaml
29
+ operation-op_456.yaml
30
+
31
+ Attributes:
32
+ storage_dir: Base directory for audit logs
33
+ """
34
+
35
+ def __init__(self, storage_dir: Optional[str] = None) -> None:
36
+ """Initialize audit storage.
37
+
38
+ Args:
39
+ storage_dir: Base directory for audit logs (default: ~/.snapshots/audit-logs)
40
+ """
41
+ if storage_dir is None:
42
+ storage_dir = str(Path.home() / ".snapshots" / "audit-logs")
43
+
44
+ self.storage_dir = Path(storage_dir)
45
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
46
+
47
+ def log_operation(self, operation: DeletionOperation, records: list[DeletionRecord]) -> None:
48
+ """Log deletion operation to audit storage.
49
+
50
+ Creates YAML file with operation metadata and all deletion records.
51
+ Overwrites existing log if operation ID already exists.
52
+
53
+ Args:
54
+ operation: Deletion operation to log
55
+ records: List of deletion records for this operation
56
+ """
57
+ # Create year/month directory structure
58
+ year = operation.timestamp.year
59
+ month = operation.timestamp.month
60
+ year_month_dir = self.storage_dir / str(year) / f"{month:02d}"
61
+ year_month_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ # Create audit log structure
64
+ audit_data = {
65
+ "metadata": {
66
+ "version": "1.0",
67
+ "log_type": "resource_deletion",
68
+ "created_at": datetime.utcnow().isoformat() + "Z",
69
+ },
70
+ "operation": {
71
+ "operation_id": operation.operation_id,
72
+ "baseline_snapshot": operation.baseline_snapshot,
73
+ "timestamp": operation.timestamp.isoformat() + "Z",
74
+ "aws_profile": operation.aws_profile,
75
+ "account_id": operation.account_id,
76
+ "mode": operation.mode.value,
77
+ "status": operation.status.value,
78
+ "filters": operation.filters,
79
+ "total_resources": operation.total_resources,
80
+ "succeeded_count": operation.succeeded_count,
81
+ "failed_count": operation.failed_count,
82
+ "skipped_count": operation.skipped_count,
83
+ "started_at": operation.started_at.isoformat() + "Z" if operation.started_at else None,
84
+ "completed_at": operation.completed_at.isoformat() + "Z" if operation.completed_at else None,
85
+ "duration_seconds": operation.duration_seconds,
86
+ },
87
+ "records": [
88
+ {
89
+ "record_id": record.record_id,
90
+ "operation_id": record.operation_id,
91
+ "resource_arn": record.resource_arn,
92
+ "resource_id": record.resource_id,
93
+ "resource_type": record.resource_type,
94
+ "region": record.region,
95
+ "timestamp": record.timestamp.isoformat() + "Z",
96
+ "status": record.status.value,
97
+ "error_code": record.error_code,
98
+ "error_message": record.error_message,
99
+ "protection_reason": record.protection_reason,
100
+ "deletion_tier": record.deletion_tier,
101
+ "tags": record.tags,
102
+ "estimated_monthly_cost": record.estimated_monthly_cost,
103
+ }
104
+ for record in records
105
+ ],
106
+ }
107
+
108
+ # Write YAML file
109
+ audit_file = year_month_dir / f"operation-{operation.operation_id}.yaml"
110
+ with open(audit_file, "w") as f:
111
+ yaml.dump(audit_data, f, default_flow_style=False, sort_keys=False)
112
+
113
+ def get_operation(self, operation_id: str) -> Optional[dict]:
114
+ """Retrieve operation audit log by ID.
115
+
116
+ Args:
117
+ operation_id: Operation ID to retrieve
118
+
119
+ Returns:
120
+ Audit log dictionary if found, None otherwise
121
+ """
122
+ # Search all year/month directories
123
+ for year_dir in self.storage_dir.glob("*"):
124
+ if not year_dir.is_dir():
125
+ continue
126
+
127
+ for month_dir in year_dir.glob("*"):
128
+ if not month_dir.is_dir():
129
+ continue
130
+
131
+ audit_file = month_dir / f"operation-{operation_id}.yaml"
132
+ if audit_file.exists():
133
+ with open(audit_file, "r") as f:
134
+ return yaml.safe_load(f)
135
+
136
+ return None
137
+
138
+ def query_operations(self, since: Optional[datetime] = None, until: Optional[datetime] = None) -> list[dict]:
139
+ """Query operations within date range.
140
+
141
+ Args:
142
+ since: Start date (inclusive), None for all
143
+ until: End date (inclusive), None for all
144
+
145
+ Returns:
146
+ List of operation audit logs matching criteria
147
+ """
148
+ results = []
149
+
150
+ # Search all year/month directories
151
+ for year_dir in sorted(self.storage_dir.glob("*")):
152
+ if not year_dir.is_dir():
153
+ continue
154
+
155
+ for month_dir in sorted(year_dir.glob("*")):
156
+ if not month_dir.is_dir():
157
+ continue
158
+
159
+ for audit_file in sorted(month_dir.glob("operation-*.yaml")):
160
+ with open(audit_file, "r") as f:
161
+ audit_data = yaml.safe_load(f)
162
+
163
+ # Parse timestamp
164
+ timestamp_str = audit_data["operation"]["timestamp"]
165
+ timestamp = datetime.fromisoformat(timestamp_str.rstrip("Z"))
166
+
167
+ # Filter by date range
168
+ if since and timestamp < since:
169
+ continue
170
+ if until and timestamp > until:
171
+ continue
172
+
173
+ results.append(audit_data)
174
+
175
+ return results