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
src/security/models.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Security scan result model for aggregating security findings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from ..models.security_finding import SecurityFinding, Severity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SecurityScanResult:
|
|
14
|
+
"""Aggregates security findings from scanning a snapshot.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
findings: List of all security findings
|
|
18
|
+
total_findings: Total count of findings
|
|
19
|
+
snapshot_name: Name of the snapshot that was scanned
|
|
20
|
+
scan_timestamp: When the scan was executed
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
snapshot_name: str
|
|
24
|
+
scan_timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
25
|
+
findings: List[SecurityFinding] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def total_findings(self) -> int:
|
|
29
|
+
"""Get total count of findings.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Number of findings in the result
|
|
33
|
+
"""
|
|
34
|
+
return len(self.findings)
|
|
35
|
+
|
|
36
|
+
def add_finding(self, finding: SecurityFinding) -> None:
|
|
37
|
+
"""Add a security finding to the result.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
finding: SecurityFinding to add
|
|
41
|
+
"""
|
|
42
|
+
self.findings.append(finding)
|
|
43
|
+
|
|
44
|
+
def get_findings_by_severity(self, severity: Severity) -> List[SecurityFinding]:
|
|
45
|
+
"""Get all findings matching a specific severity level.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
severity: Severity level to filter by
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of findings with the specified severity
|
|
52
|
+
"""
|
|
53
|
+
return [f for f in self.findings if f.severity == severity]
|
src/security/reporter.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Security findings reporter with multiple output formats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from src.models.security_finding import SecurityFinding, Severity
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SecurityReporter:
|
|
17
|
+
"""Report security findings in various formats (terminal, JSON, CSV)."""
|
|
18
|
+
|
|
19
|
+
def format_terminal(self, findings: List[SecurityFinding]) -> str:
|
|
20
|
+
"""Format findings for terminal output using Rich.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
findings: List of security findings
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Formatted string for terminal display
|
|
27
|
+
"""
|
|
28
|
+
if not findings:
|
|
29
|
+
return "No security findings detected."
|
|
30
|
+
|
|
31
|
+
# Create a Rich table
|
|
32
|
+
table = Table(title="Security Findings")
|
|
33
|
+
table.add_column("Severity", style="bold")
|
|
34
|
+
table.add_column("Resource")
|
|
35
|
+
table.add_column("Finding Type")
|
|
36
|
+
table.add_column("Description")
|
|
37
|
+
|
|
38
|
+
for finding in findings:
|
|
39
|
+
# Color code severity
|
|
40
|
+
severity_str = finding.severity.value.upper()
|
|
41
|
+
if finding.severity == Severity.CRITICAL:
|
|
42
|
+
severity_display = f"[red]{severity_str}[/red]"
|
|
43
|
+
elif finding.severity == Severity.HIGH:
|
|
44
|
+
severity_display = f"[orange1]{severity_str}[/orange1]"
|
|
45
|
+
elif finding.severity == Severity.MEDIUM:
|
|
46
|
+
severity_display = f"[yellow]{severity_str}[/yellow]"
|
|
47
|
+
else:
|
|
48
|
+
severity_display = f"[cyan]{severity_str}[/cyan]"
|
|
49
|
+
|
|
50
|
+
# Truncate description if too long
|
|
51
|
+
description = finding.description
|
|
52
|
+
if len(description) > 60:
|
|
53
|
+
description = description[:57] + "..."
|
|
54
|
+
|
|
55
|
+
# Extract resource ID from ARN for display
|
|
56
|
+
resource_display = (
|
|
57
|
+
finding.resource_arn.split("/")[-1]
|
|
58
|
+
if "/" in finding.resource_arn
|
|
59
|
+
else finding.resource_arn.split(":")[-1]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
table.add_row(
|
|
63
|
+
severity_display,
|
|
64
|
+
resource_display,
|
|
65
|
+
finding.finding_type,
|
|
66
|
+
description,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Render table to string
|
|
70
|
+
console = Console()
|
|
71
|
+
with console.capture() as capture:
|
|
72
|
+
console.print(table)
|
|
73
|
+
|
|
74
|
+
return capture.get()
|
|
75
|
+
|
|
76
|
+
def export_json(self, findings: List[SecurityFinding], filepath: str) -> None:
|
|
77
|
+
"""Export findings to JSON format.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
findings: List of security findings
|
|
81
|
+
filepath: Output file path
|
|
82
|
+
"""
|
|
83
|
+
# Generate summary statistics
|
|
84
|
+
summary = self.generate_summary(findings)
|
|
85
|
+
|
|
86
|
+
# Convert findings to dictionaries
|
|
87
|
+
findings_data = [finding.to_dict() for finding in findings]
|
|
88
|
+
|
|
89
|
+
output = {
|
|
90
|
+
"findings": findings_data,
|
|
91
|
+
"summary": summary,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Write to file
|
|
95
|
+
output_path = Path(filepath)
|
|
96
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
with open(output_path, "w") as f:
|
|
99
|
+
json.dump(output, f, indent=2)
|
|
100
|
+
|
|
101
|
+
def export_csv(self, findings: List[SecurityFinding], filepath: str) -> None:
|
|
102
|
+
"""Export findings to CSV format.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
findings: List of security findings
|
|
106
|
+
filepath: Output file path
|
|
107
|
+
"""
|
|
108
|
+
output_path = Path(filepath)
|
|
109
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
# Define CSV columns
|
|
112
|
+
fieldnames = [
|
|
113
|
+
"resource_arn",
|
|
114
|
+
"finding_type",
|
|
115
|
+
"severity",
|
|
116
|
+
"description",
|
|
117
|
+
"remediation",
|
|
118
|
+
"cis_control",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
with open(output_path, "w", newline="") as f:
|
|
122
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
123
|
+
writer.writeheader()
|
|
124
|
+
|
|
125
|
+
for finding in findings:
|
|
126
|
+
row = {
|
|
127
|
+
"resource_arn": finding.resource_arn,
|
|
128
|
+
"finding_type": finding.finding_type,
|
|
129
|
+
"severity": finding.severity.value,
|
|
130
|
+
"description": finding.description,
|
|
131
|
+
"remediation": finding.remediation,
|
|
132
|
+
"cis_control": finding.cis_control or "",
|
|
133
|
+
}
|
|
134
|
+
writer.writerow(row)
|
|
135
|
+
|
|
136
|
+
def group_by_severity(self, findings: List[SecurityFinding]) -> Dict[Severity, List[SecurityFinding]]:
|
|
137
|
+
"""Group findings by severity level.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
findings: List of security findings
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dictionary mapping Severity to lists of findings
|
|
144
|
+
"""
|
|
145
|
+
grouped: Dict[Severity, List[SecurityFinding]] = {
|
|
146
|
+
Severity.CRITICAL: [],
|
|
147
|
+
Severity.HIGH: [],
|
|
148
|
+
Severity.MEDIUM: [],
|
|
149
|
+
Severity.LOW: [],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for finding in findings:
|
|
153
|
+
grouped[finding.severity].append(finding)
|
|
154
|
+
|
|
155
|
+
return grouped
|
|
156
|
+
|
|
157
|
+
def generate_summary(self, findings: List[SecurityFinding]) -> dict:
|
|
158
|
+
"""Generate summary statistics for findings.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
findings: List of security findings
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dictionary with counts by severity
|
|
165
|
+
"""
|
|
166
|
+
grouped = self.group_by_severity(findings)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"total_findings": len(findings),
|
|
170
|
+
"critical_count": len(grouped[Severity.CRITICAL]),
|
|
171
|
+
"high_count": len(grouped[Severity.HIGH]),
|
|
172
|
+
"medium_count": len(grouped[Severity.MEDIUM]),
|
|
173
|
+
"low_count": len(grouped[Severity.LOW]),
|
|
174
|
+
}
|
src/security/scanner.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Security scanner for detecting misconfigurations in AWS snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import inspect
|
|
7
|
+
import pkgutil
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from ..models.security_finding import Severity
|
|
11
|
+
from ..models.snapshot import Snapshot
|
|
12
|
+
from .checks.base import SecurityCheck
|
|
13
|
+
from .models import SecurityScanResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SecurityScanner:
|
|
17
|
+
"""Scans AWS snapshots for security misconfigurations.
|
|
18
|
+
|
|
19
|
+
The scanner automatically discovers and loads all security check modules
|
|
20
|
+
from the checks package and executes them against snapshots.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
"""Initialize the security scanner and load all security checks."""
|
|
25
|
+
self.checks: List[SecurityCheck] = []
|
|
26
|
+
self._load_checks()
|
|
27
|
+
|
|
28
|
+
def _load_checks(self) -> None:
|
|
29
|
+
"""Dynamically load all security check modules from the checks package.
|
|
30
|
+
|
|
31
|
+
Discovers all Python modules in src/security/checks/, finds SecurityCheck
|
|
32
|
+
subclasses, and instantiates them.
|
|
33
|
+
"""
|
|
34
|
+
from . import checks
|
|
35
|
+
|
|
36
|
+
# Discover all check modules
|
|
37
|
+
for importer, modname, ispkg in pkgutil.iter_modules(checks.__path__):
|
|
38
|
+
# Skip the base module and __init__
|
|
39
|
+
if modname in ("base", "__init__"):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
# Import the module
|
|
43
|
+
try:
|
|
44
|
+
module = importlib.import_module(f".checks.{modname}", package="src.security")
|
|
45
|
+
|
|
46
|
+
# Find all SecurityCheck subclasses in the module
|
|
47
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
48
|
+
# Check if it's a subclass of SecurityCheck (but not SecurityCheck itself)
|
|
49
|
+
if issubclass(obj, SecurityCheck) and obj is not SecurityCheck:
|
|
50
|
+
# Instantiate and add to checks list
|
|
51
|
+
check_instance = obj()
|
|
52
|
+
self.checks.append(check_instance)
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
# Log the error but continue loading other checks
|
|
56
|
+
print(f"Warning: Failed to load check module {modname}: {e}")
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
def scan(self, snapshot: Snapshot, severity_filter: Optional[Severity] = None) -> SecurityScanResult:
|
|
60
|
+
"""Scan a snapshot for security misconfigurations.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
snapshot: Snapshot to scan
|
|
64
|
+
severity_filter: Optional severity level to filter findings (only include findings
|
|
65
|
+
matching this severity)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
SecurityScanResult containing all findings from the scan
|
|
69
|
+
"""
|
|
70
|
+
result = SecurityScanResult(snapshot_name=snapshot.name)
|
|
71
|
+
|
|
72
|
+
# Execute all checks on the snapshot
|
|
73
|
+
for check in self.checks:
|
|
74
|
+
try:
|
|
75
|
+
findings = check.execute(snapshot)
|
|
76
|
+
|
|
77
|
+
# Add findings to result, applying severity filter if specified
|
|
78
|
+
for finding in findings:
|
|
79
|
+
if severity_filter is None or finding.severity == severity_filter:
|
|
80
|
+
result.add_finding(finding)
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
# Log the error but continue with other checks
|
|
84
|
+
print(f"Warning: Check {check.check_id} failed: {e}")
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
return result
|
src/snapshot/__init__.py
ADDED