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.
- aws_inventory_manager-0.13.2.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.13.2.dist-info/METADATA +1226 -0
- aws_inventory_manager-0.13.2.dist-info/RECORD +145 -0
- aws_inventory_manager-0.13.2.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.13.2.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.13.2.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 +3626 -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/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 +451 -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/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 +749 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +413 -0
- src/storage/schema.py +288 -0
- src/storage/snapshot_store.py +346 -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 +379 -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 +949 -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 +2251 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- 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
|
src/restore/__init__.py
ADDED
|
@@ -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
|