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,145 @@
|
|
|
1
|
+
src/__init__.py,sha256=DMK4jB1kbfaPjMeyFvozB7wOQmpsX41md4fINtVJsyk,75
|
|
2
|
+
src/aws/__init__.py,sha256=FcFY2cn8ZQCnActrZYH1hD0x4uFkM9bMBDOmuHA3_mw,253
|
|
3
|
+
src/aws/client.py,sha256=CjqCxEsbgxQYWx_HF6g9_W-pfXl7yr1Z9armXyzuYpM,4089
|
|
4
|
+
src/aws/credentials.py,sha256=mxj8lQf7TRG5dznQJ37rGQqfB-viHh_5VCt1C4xgqG0,6374
|
|
5
|
+
src/aws/rate_limiter.py,sha256=b3dV4A9ftUsGgVMOQmv3IVO0Fkym2o1QDHKpFOdSYbg,5464
|
|
6
|
+
src/cli/__init__.py,sha256=zstPcm000JdpuUuMRuqL2TkC-bnCpm0syHjAyu8ELcY,387
|
|
7
|
+
src/cli/config.py,sha256=to_hVDxQh0WNANPDQKYlnZzCsFS6_bnrE745wZgRQe4,4286
|
|
8
|
+
src/cli/main.py,sha256=7g1AFowwBXZQj0PrShQxq3FlLTRwUp6e7U-CiezMPFY,147763
|
|
9
|
+
src/config_service/__init__.py,sha256=fqYfceBP_kPnp8PjYW9D_FqsWqrrOzsQTftBgp7-aTE,585
|
|
10
|
+
src/config_service/collector.py,sha256=uW6m1feceYCOgANGWvxhbw23etcaqSQpmdIu-DTsWuk,12254
|
|
11
|
+
src/config_service/detector.py,sha256=BoJC32DXN2f8x8U8RiOmjkGtnrTQrRuO568FHcMt_xU,8531
|
|
12
|
+
src/config_service/resource_type_mapping.py,sha256=DyxmPcP0vQ2ouZ0hRS-oXUcViSNYSfzwXYuvoUu8cu8,8726
|
|
13
|
+
src/cost/__init__.py,sha256=WpTwsWAm8wyCkB65gsCnzu89-r5G_hgrbL9zTqvQ8j8,122
|
|
14
|
+
src/cost/analyzer.py,sha256=02WFkZErD-zfu1Jp8UDym3wTjuB0c3l5jadZmbGbBJk,10201
|
|
15
|
+
src/cost/explorer.py,sha256=c4Yj7w5gmvoeD9iY7hseinj3Ij-g9Rh8YXWYZVkO6u4,6847
|
|
16
|
+
src/cost/reporter.py,sha256=JPisT4C8YIt3vaQ1wD4VOAsRu5STNhp5Grs0ugQ0zA4,9609
|
|
17
|
+
src/delta/__init__.py,sha256=ypHqn-VKswWWisVQC1sY_23VqHC_LIaSdQv6HIwYU4Y,120
|
|
18
|
+
src/delta/calculator.py,sha256=5KKUKYiCQdTtfqB84llwej52Ev0r9B5SZfJ4aNuq37E,7833
|
|
19
|
+
src/delta/differ.py,sha256=-7MESqWyO8srPhs9OlsLJjYx57qPwQRh93GsatqnoW8,6692
|
|
20
|
+
src/delta/formatters.py,sha256=xhzph_iLGLD8yyflYCMTFZqWPmW9tB7hZsVHsgQDrzM,9209
|
|
21
|
+
src/delta/models.py,sha256=_biiC136e-C2GljlIeW7ql5vC47Ss1M8a-jgw9K9rFg,5103
|
|
22
|
+
src/delta/reporter.py,sha256=yqzof9snkJyJiOeGoLAla2pmnFNIPlR3brg7xrstpNA,8775
|
|
23
|
+
src/models/__init__.py,sha256=5DFL_8ZgLE28EG8ORumMT3EfhFAxXrFa3yzQpQBSQxc,533
|
|
24
|
+
src/models/config_diff.py,sha256=elh4oUgoln_sDC6lmkqfSd8k1pX9oqBPqTEgTksw5f8,4237
|
|
25
|
+
src/models/cost_report.py,sha256=BzyWrMewHau7EpQyLm4WoesVY3WDPBOtGVdk_H8USuM,3079
|
|
26
|
+
src/models/deletion_operation.py,sha256=bb2TiY_UsHEBk-B6qp1_miXOePTphhIAWRMnhYrh3k8,3468
|
|
27
|
+
src/models/deletion_record.py,sha256=OxA3oDRKhGct_D0apaOBg5lPQqc5CdICswpXipCrnes,3311
|
|
28
|
+
src/models/delta_report.py,sha256=TD0Jk2I9D5Fw2oXEkN6oUwMzGeIVfQzsRY-LK_MR8to,4854
|
|
29
|
+
src/models/efs_resource.py,sha256=Jq8bzDKEYo8vB6M-Zz1RVowteD0nyxUhF8C6gumWe7E,2665
|
|
30
|
+
src/models/elasticache_resource.py,sha256=WRHOjZ7m4cT7OwfL8GQp5Ays9S8i9RLGynkAr5aAB3o,3201
|
|
31
|
+
src/models/group.py,sha256=suBBtO4ipRjZP1XptNfcMMkERcAtYTYf3YIkcYYRoeU,11452
|
|
32
|
+
src/models/inventory.py,sha256=hEg5qQy_K8HvgtM7AyU1y1MX6c9xzsOefbCShCaH8PI,5157
|
|
33
|
+
src/models/protection_rule.py,sha256=DjGJ6il9y6fPe5WTZzlq0BJwfAOyQf6vEwmT-SjDYsc,3929
|
|
34
|
+
src/models/report.py,sha256=Qb9OKdPNRFxcfxk-9vFEnVctKaVWgg4aP-QTEBgJlxo,9288
|
|
35
|
+
src/models/resource.py,sha256=jTpdJabrn0KRU6aGEiJZyUtZFTvXIDNgPDWx5MmIVZs,3882
|
|
36
|
+
src/models/security_finding.py,sha256=RyTobHFg6n0eomCDVuL89zYevpmSInneZ4RtDHtms9U,3451
|
|
37
|
+
src/models/snapshot.py,sha256=-kj9LYSrZSfSY44sc-TyJBNfjCYgegol8uCTXSJbNx8,4841
|
|
38
|
+
src/restore/__init__.py,sha256=iTghVKlfBxzzIxaun_AafOXHNLbyMwimJGtSwZZIyYY,563
|
|
39
|
+
src/restore/audit.py,sha256=XQH7iBrEFfSsewBesa677DZw1YXaqLqbvwByDq2vuTc,6538
|
|
40
|
+
src/restore/cleaner.py,sha256=o_4jeLgHIku54rKkiNNVowdVw5LY8UdxTT9ikQnnAx0,16589
|
|
41
|
+
src/restore/config.py,sha256=6MiqaIo_0x5V3F8XRKTvu24ij15IUiuQku39otySU7s,6536
|
|
42
|
+
src/restore/deleter.py,sha256=mqsg0w6vWeGBqVcvz82VjsHTeAltgS65Kevxv0eie_o,41483
|
|
43
|
+
src/restore/dependency.py,sha256=WNEgJAQHqpOF6ir9cjiUtGVbckQfRvToOWeJCtzR76U,9190
|
|
44
|
+
src/restore/safety.py,sha256=_LIlkcJdCJCXx9aopZuRrd18R8Ag-rv5G-xG20TOf14,3971
|
|
45
|
+
src/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
+
src/security/cis_mapper.py,sha256=rLSQobH1a1fa_tc9nQpaOPGSx1Wt7vlomxSyyyNwdqI,3147
|
|
47
|
+
src/security/models.py,sha256=IASZUo9_35W3nwnehmpiW-X19WaJANYOpB_OUHEltHk,1579
|
|
48
|
+
src/security/reporter.py,sha256=oe84rm0-FRhUgIHERyozkWIJpg7LIA-TPsxG8FiZOGw,5543
|
|
49
|
+
src/security/scanner.py,sha256=JGWHYZJ2VHXzas6nvsqSlkd3PsU6dg6VnAfJZABmvEY,3293
|
|
50
|
+
src/security/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
|
+
src/security/checks/base.py,sha256=2e-i55dVO-0N8squB6vUg5DEPZnTvW6XPpLPQLW5s0I,1371
|
|
52
|
+
src/security/checks/ec2_checks.py,sha256=hUO1cTN-zd45WEGy7xZIzb7LMOjdggTc8YfLyEVA38M,3113
|
|
53
|
+
src/security/checks/elasticache_checks.py,sha256=CGkF-6e5Z7iCKj3dvghyMsU9aF9abnLs7Xu7MPFvy4I,6108
|
|
54
|
+
src/security/checks/iam_checks.py,sha256=k-kSf4zmVZ7svrajt_-jPW5bDGyLrswAX2E9KhfqXbU,3952
|
|
55
|
+
src/security/checks/rds_checks.py,sha256=oRmflzwLf3_JeMEJt-pCoPKGFgwC2BdItzuUQYyyXEo,5112
|
|
56
|
+
src/security/checks/s3_checks.py,sha256=usHrGBmmIZc4PEDSMRRE1H8A_NwUxV2kiIVeLaPCauc,3387
|
|
57
|
+
src/security/checks/secrets_checks.py,sha256=2bJY0Hqb1PwUYid5XZCC6y4hgWle3sRtwYNVh4jmUBw,3631
|
|
58
|
+
src/security/checks/sg_checks.py,sha256=BmipY31beybajxSbS_HoDkSXFYTQFhjXWNxuYYPEP2U,4731
|
|
59
|
+
src/snapshot/__init__.py,sha256=trHfRe_8jxjQVb48cDLVH3gs2Y7YQtsX2gFL4OB9qRc,237
|
|
60
|
+
src/snapshot/capturer.py,sha256=DZqDnyeum9m4AzSH-jj6wrz5I3QpHcdAHWRAE-LL4qc,17975
|
|
61
|
+
src/snapshot/filter.py,sha256=UPVTi23NHJYKFTD4ss8PazDHh-NIC2lWLvjJnr9Y7QY,9158
|
|
62
|
+
src/snapshot/inventory_storage.py,sha256=eOwGzYhUrvnv_usgEaUF-0_cDGGf21k-vjRRKP7Z5Hs,7739
|
|
63
|
+
src/snapshot/report_formatter.py,sha256=WR7Adqq5FjJqu3iIWAnOZ9yp6AuJH1kA6uKKpT7jrT4,8266
|
|
64
|
+
src/snapshot/reporter.py,sha256=CZp5fj6RcHoD5a3-1unwq5KKIxrQA38rLWCjspnyvvI,6628
|
|
65
|
+
src/snapshot/storage.py,sha256=29zyNelW4QeH0C5E-GYnD5N4sQvyXtiZgPG8OYw4R7c,11826
|
|
66
|
+
src/snapshot/resource_collectors/__init__.py,sha256=QSJiStvbmqd_uCI6c4T03-3PMb0UmxAROZwVHPk25gc,94
|
|
67
|
+
src/snapshot/resource_collectors/apigateway.py,sha256=X916NdfRzUMNmMuyJdB_BrgTu1p7P7NwslPxWcPW4CU,4748
|
|
68
|
+
src/snapshot/resource_collectors/backup.py,sha256=mWySnU_47Se88uJN33scZovpUYPYwGRNMo3ur_yyqT0,4795
|
|
69
|
+
src/snapshot/resource_collectors/base.py,sha256=kpn0gRbf6PBb_1CA-0e6lnaI7jCm7mXPEsFwVNGSY-k,2369
|
|
70
|
+
src/snapshot/resource_collectors/cloudformation.py,sha256=YqMwzEF9neoXebRRfN0s8BT8ex66hWOPJQXk0aaLmxQ,1892
|
|
71
|
+
src/snapshot/resource_collectors/cloudwatch.py,sha256=JW4RMWl4hD_NTuAWtOV4v5FIPRY-opKuJeIecKxmzWI,4095
|
|
72
|
+
src/snapshot/resource_collectors/codebuild.py,sha256=VQjw-KSu4lA5kN7ci63vaphpPIgY4ZelYaFWTV7uD7M,2485
|
|
73
|
+
src/snapshot/resource_collectors/codepipeline.py,sha256=-1Dait_7Yzz-a_B9OtbsKNNzovbns0YKuFSifY0b5OU,3372
|
|
74
|
+
src/snapshot/resource_collectors/dynamodb.py,sha256=QRtEH5uLwrqF0yBIeHX_RLnOYovSnbcdTy6oo4xg9ZY,2463
|
|
75
|
+
src/snapshot/resource_collectors/ec2.py,sha256=hvor4Z0ofznR8sLvrK_OdvNh7-mvuDZeDOXPr1-X-qg,8441
|
|
76
|
+
src/snapshot/resource_collectors/ecs.py,sha256=90uj0FBU9uF03OZ7RkkS1Mc3g5YSz0v2hWYCOYS5NXw,8379
|
|
77
|
+
src/snapshot/resource_collectors/efs_collector.py,sha256=58ouqtCF16bxDO66-M1LojyVpJASyhv6D7RJ2KQC3DQ,4015
|
|
78
|
+
src/snapshot/resource_collectors/eks.py,sha256=T7Rh4ij3frxYgIfAcZNk0xNpoahwBfsQWcs14xcB5rE,7746
|
|
79
|
+
src/snapshot/resource_collectors/elasticache_collector.py,sha256=g7mVa9ZlE0DBITnA-i565REZRpumWMnqZuLVHcVO0Gw,2830
|
|
80
|
+
src/snapshot/resource_collectors/elb.py,sha256=af3IeKUv_FUfcLwYcnDajh4_4eSWtelvoazKCptehnQ,5125
|
|
81
|
+
src/snapshot/resource_collectors/eventbridge.py,sha256=abHxh0Iob6YrumdzmFugpRGWFqE0uVRVt89gajdq5q4,6062
|
|
82
|
+
src/snapshot/resource_collectors/iam.py,sha256=N5QQO7E5opV-3CiOuoW_W0x-m_2alOjYs0-Q_loS-rQ,6732
|
|
83
|
+
src/snapshot/resource_collectors/kms.py,sha256=ioJUO2C7XawrGGnvpNA1pVT0GdHDI4pmbeqpACgf7Dc,4524
|
|
84
|
+
src/snapshot/resource_collectors/lambda_func.py,sha256=bjlWj_orHlU7vVT91c9ZcEAsXSPB7f4jgUWAMfa3C1w,5289
|
|
85
|
+
src/snapshot/resource_collectors/rds.py,sha256=Q4kdYZ4eDyLwAVTTM49PWh8Gg1NII-NBiY4_h5YafKw,4127
|
|
86
|
+
src/snapshot/resource_collectors/route53.py,sha256=oCFoEba-f80cf6e4R69Ovzp0eDgAZl_nNx4AkZ1L7zk,3311
|
|
87
|
+
src/snapshot/resource_collectors/s3.py,sha256=de90KYNHZ3b9UZ0csJlrw-Z5p1VgtU4sHyF0-5eft34,3749
|
|
88
|
+
src/snapshot/resource_collectors/secretsmanager.py,sha256=0daVuZE_VEk185boKM09Pc0A7E4b83jRztWYNMQm6bM,2673
|
|
89
|
+
src/snapshot/resource_collectors/sns.py,sha256=rsRyA95iBkildU2ytNeKihxOzwnwpUGkgZON2MOLTrY,2539
|
|
90
|
+
src/snapshot/resource_collectors/sqs.py,sha256=BK4LZJWjP7y2sItVEnn1-OHmDyNlhKlp--Bs2YIw7Mk,3112
|
|
91
|
+
src/snapshot/resource_collectors/ssm.py,sha256=EhyeGfK_17cqhDGuEM-KBg6TpAYj4kSNUvuNnfpeWes,6258
|
|
92
|
+
src/snapshot/resource_collectors/stepfunctions.py,sha256=I8TYniEzSX2jKZbSX6WI02eet5uP5ekn63uKnMaStEc,2950
|
|
93
|
+
src/snapshot/resource_collectors/vpcendpoints.py,sha256=tclcLPRxFmE5vwQQPwFQv2nbOc-4qVFnUaSJ4oDMU7w,2994
|
|
94
|
+
src/snapshot/resource_collectors/waf.py,sha256=HUNshQ_WHbIK3nAyNwhb21uHkMC7ywSOke28b-5ECRg,6461
|
|
95
|
+
src/storage/__init__.py,sha256=4LrT-bm45zLK6VUyKsx3jyK_L270A48t-w2-0ceb9PM,554
|
|
96
|
+
src/storage/audit_store.py,sha256=zO7L9o-8N31PsiLJ72wvmodoDrFtFeya05Ebfes8a5Y,14123
|
|
97
|
+
src/storage/database.py,sha256=j-IJ4LiNs7PgXH0L3FtMlmbId1TaGjNCcYjgn3dt7sQ,9465
|
|
98
|
+
src/storage/group_store.py,sha256=6sCqKwFTNHUOin08V1AXlN2scam2khSwf_cgisvD-M0,24832
|
|
99
|
+
src/storage/inventory_store.py,sha256=hRxdL2oqDTvtIuyNiR9IoBFjH1jOe3DV43C8Ybt-aTU,10601
|
|
100
|
+
src/storage/resource_store.py,sha256=MB8JVvlq5zZJcjLbIL6R6dtPfqK6hpV0JhzSjiofez0,13111
|
|
101
|
+
src/storage/schema.py,sha256=PFDVbC12RGh-VP1CNLqFc6V_QCmblGkf63VCaHuPKGU,10274
|
|
102
|
+
src/storage/snapshot_store.py,sha256=TI87NQmpMRG1lXcx8HvcKGPJuOyhzH0WmaUK0C2blZc,11855
|
|
103
|
+
src/utils/__init__.py,sha256=1qjjL3wxB7ye6s3AT4h5hQ9Xgi7y0WllUMmTpiZWC54,284
|
|
104
|
+
src/utils/export.py,sha256=lBUxLGkvtdzTFq2VKRmHgf18ZROiorHGGHlbeG7ixxc,8929
|
|
105
|
+
src/utils/hash.py,sha256=w6jJqNR64IEcgMIrIY-M1QXuo_CkH0sbT8I6JcFQJqo,1747
|
|
106
|
+
src/utils/logging.py,sha256=D39u9xrDSr_HlqedKa2yz-vmbCKulNAkJNVfG571UXY,2558
|
|
107
|
+
src/utils/pagination.py,sha256=BoMk2lP65-tGP1U6aUAuJPU7M-JroSvyfpScLKcGYxg,1153
|
|
108
|
+
src/utils/paths.py,sha256=EgO41-XE1KOmF0drY2YlJkzNQIb3WPsrGtPvsc0hVqY,1607
|
|
109
|
+
src/utils/progress.py,sha256=H8_6AtVsw-_-P40E57ECofkFw0v1Lq6uEVSofuxCXxY,936
|
|
110
|
+
src/utils/unsupported_resources.py,sha256=wN2fkN4Vpq2X_CXhpN2K0ewojeghrY3H4ARAL6lvCk4,10349
|
|
111
|
+
src/web/__init__.py,sha256=JIdxc2oP_DBW6BK8MFeYMCGGI-DO1K0ybUkML36Dg1A,100
|
|
112
|
+
src/web/app.py,sha256=G-cVVCakvBI3ErsHfE_RYznrjm6dnOeTbd0x6glypT4,2819
|
|
113
|
+
src/web/dependencies.py,sha256=Fp2J05-9rFYshs-fN_mlTPFPqpOsp5V3ilI5ReqwW6c,1736
|
|
114
|
+
src/web/routes/__init__.py,sha256=sFFDPD60smOOMalYMdiSFy0cmTM8co00A3ZZiP-915Q,26
|
|
115
|
+
src/web/routes/pages.py,sha256=TZjTMgQw88cpakJsYf4D3HdCTC5PgdoWXLH-zXg6oCs,5717
|
|
116
|
+
src/web/routes/api/__init__.py,sha256=q1ysdpJ1-3-Ux5sM3eH9nJCXya5AjeB077KnClVj1CI,712
|
|
117
|
+
src/web/routes/api/charts.py,sha256=V-2MtmsihptC9eikpw_u2VbJIyhMl-stUEO6QT27mSA,4136
|
|
118
|
+
src/web/routes/api/cleanup.py,sha256=27HOlGZJa4lh5cbuBWuVbtUgji7YVka-ojjswG-vhz8,6695
|
|
119
|
+
src/web/routes/api/filters.py,sha256=ILQmkxtpQjRh1XsRSmzet62Mfg-uVfl6UfLCDKzEqhM,7708
|
|
120
|
+
src/web/routes/api/groups.py,sha256=5ipRMBU8xpQHYtu29aJwfOmHYUMhu3kZWNxWVIRrQvg,8890
|
|
121
|
+
src/web/routes/api/inventories.py,sha256=dN2WSLlQB2HrDwOM9XdpM3dpoC2zDDE1V8O-Anculv4,2541
|
|
122
|
+
src/web/routes/api/queries.py,sha256=vETfCAaLk_HX9IQNbmKpc5XD4-cbs3UYrQA_uGjzkYw,5444
|
|
123
|
+
src/web/routes/api/resources.py,sha256=C2lXj3FqFmptIeUTWChJEEG8Z5MC_R-LSPUBrSX1tJg,12958
|
|
124
|
+
src/web/routes/api/snapshots.py,sha256=rZHifGAB8abop1iungV8ubYE7bwMf5u55zyypbHy-uo,9860
|
|
125
|
+
src/web/routes/api/views.py,sha256=MSFr6woupQA3-ftwv0zhwNadlYlQAsBzTjfb3o-KwGg,7843
|
|
126
|
+
src/web/services/__init__.py,sha256=CdQNRtr3v5VA5wpZ2d4SqhxwtfaDacxs29IYrVlaCFg,28
|
|
127
|
+
src/web/templates/base.html,sha256=EueO9AnupiS52R-jmLU35DkUC_7Bel24QyGRWVFaUK0,34109
|
|
128
|
+
src/web/templates/components/navbar.html,sha256=GX-rtm2vDSAcvY-lGaXvemTT04m9X5OIm9W60STl4Do,1997
|
|
129
|
+
src/web/templates/components/sidebar.html,sha256=TCclf5c3ss0f_D9P4H7R_uJLoqUxkUzTInCJZQ1f5No,9131
|
|
130
|
+
src/web/templates/pages/audit_logs.html,sha256=0shhlDlQ0Ghlxqu7wlAbSvEwv15G1V3gSratDVu1_nY,4930
|
|
131
|
+
src/web/templates/pages/cleanup.html,sha256=SzJPTY10ax_9T1XEM0juFLf1b5dyHcKHPQTIAUn-0cg,16204
|
|
132
|
+
src/web/templates/pages/dashboard.html,sha256=t6FKnpdkk6Um4JrtE5IJKF81KRMifVttCp09HjddXGs,10581
|
|
133
|
+
src/web/templates/pages/diff.html,sha256=UeRqKd6Op2XXNkNg916km7KICMPngh8FVxiaSQuxoO8,8489
|
|
134
|
+
src/web/templates/pages/error.html,sha256=rCnXvdqhrXQX8RungpL35q6iAU77ntQTnC-B52zYkdo,1715
|
|
135
|
+
src/web/templates/pages/groups.html,sha256=N2Bj44Bx5hTiXap9b_dIPjan_CS_9pAqJeJYAs5o-z0,43176
|
|
136
|
+
src/web/templates/pages/queries.html,sha256=sf167CjkCQmx0jZQsEorgSO2N3zVyISdEIpxkhZVr7E,12443
|
|
137
|
+
src/web/templates/pages/resources.html,sha256=fygTKEcER7e_L2jDKZD6G3MewWFWBvIHGMbBxW8GqMY,123801
|
|
138
|
+
src/web/templates/pages/snapshot_detail.html,sha256=hHoM_smOhVnDhsqHRrjv_Bk3V2WsUcsjOhFEy6qqDDs,12457
|
|
139
|
+
src/web/templates/pages/snapshots.html,sha256=3b47IQgkN4mxJpqjUaJ_lPqYUqM2Icz44Ad8CesZpvM,22536
|
|
140
|
+
aws_inventory_manager-0.13.2.dist-info/LICENSE,sha256=-lY65BqqcGV9QVjIoTpYEB0Jaddm9j-cS5ICDRBRQo0,1071
|
|
141
|
+
aws_inventory_manager-0.13.2.dist-info/METADATA,sha256=F4QepgckeuJ9n529xrQrLlBBztr7uzSuNTchDF_sxtw,42531
|
|
142
|
+
aws_inventory_manager-0.13.2.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
143
|
+
aws_inventory_manager-0.13.2.dist-info/entry_points.txt,sha256=Ktdhto-PER5BtwWKYBtU_-FS3ngiGtAGGeoLzRW2qUw,44
|
|
144
|
+
aws_inventory_manager-0.13.2.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
145
|
+
aws_inventory_manager-0.13.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
src
|
src/__init__.py
ADDED
src/aws/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""AWS client utilities and wrappers."""
|
|
2
|
+
|
|
3
|
+
from .client import create_boto_client
|
|
4
|
+
from .credentials import validate_credentials
|
|
5
|
+
from .rate_limiter import RateLimiter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"create_boto_client",
|
|
9
|
+
"validate_credentials",
|
|
10
|
+
"RateLimiter",
|
|
11
|
+
]
|
src/aws/client.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Boto3 client wrapper with retry configuration and error handling."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
from botocore.config import Config
|
|
8
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Aggressive retry configuration for batch operations
|
|
14
|
+
DEFAULT_RETRY_CONFIG = Config(
|
|
15
|
+
retries={"max_attempts": 10, "mode": "adaptive"}, # Adaptive retry mode (boto3 1.16+)
|
|
16
|
+
max_pool_connections=50,
|
|
17
|
+
connect_timeout=60,
|
|
18
|
+
read_timeout=60,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_boto_client(
|
|
23
|
+
service_name: str,
|
|
24
|
+
region_name: str = "us-east-1",
|
|
25
|
+
profile_name: Optional[str] = None,
|
|
26
|
+
retry_config: Optional[Config] = None,
|
|
27
|
+
) -> Any:
|
|
28
|
+
"""Create boto3 client with retry configuration and error handling.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
service_name: AWS service name (e.g., 'ec2', 'iam', 'lambda')
|
|
32
|
+
region_name: AWS region (default: 'us-east-1')
|
|
33
|
+
profile_name: AWS profile name from ~/.aws/config (optional)
|
|
34
|
+
retry_config: Custom botocore Config object (optional)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Configured boto3 client
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
NoCredentialsError: If AWS credentials are not found
|
|
41
|
+
ClientError: If client creation fails
|
|
42
|
+
"""
|
|
43
|
+
if retry_config is None:
|
|
44
|
+
retry_config = DEFAULT_RETRY_CONFIG
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Create session (with or without profile)
|
|
48
|
+
if profile_name:
|
|
49
|
+
session = boto3.Session(profile_name=profile_name)
|
|
50
|
+
client = session.client(service_name, region_name=region_name, config=retry_config)
|
|
51
|
+
else:
|
|
52
|
+
client = boto3.client(service_name, region_name=region_name, config=retry_config)
|
|
53
|
+
|
|
54
|
+
logger.debug(f"Created {service_name} client for region {region_name}")
|
|
55
|
+
return client
|
|
56
|
+
|
|
57
|
+
except NoCredentialsError:
|
|
58
|
+
logger.error("AWS credentials not found")
|
|
59
|
+
raise
|
|
60
|
+
except ClientError as e:
|
|
61
|
+
logger.error(f"Failed to create {service_name} client: {e}")
|
|
62
|
+
raise
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Unexpected error creating {service_name} client: {e}")
|
|
65
|
+
raise
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_enabled_regions(profile_name: Optional[str] = None) -> List[str]: # type: ignore
|
|
69
|
+
"""Get list of enabled AWS regions for the account.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
profile_name: AWS profile name (optional)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of enabled region names
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
ec2_client = create_boto_client("ec2", region_name="us-east-1", profile_name=profile_name)
|
|
79
|
+
response = ec2_client.describe_regions(AllRegions=False) # Only enabled regions
|
|
80
|
+
regions = [region["RegionName"] for region in response["Regions"]]
|
|
81
|
+
logger.info(f"Found {len(regions)} enabled regions")
|
|
82
|
+
return regions
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(f"Could not retrieve enabled regions: {e}")
|
|
85
|
+
# Return default set of common regions
|
|
86
|
+
return [
|
|
87
|
+
"us-east-1",
|
|
88
|
+
"us-east-2",
|
|
89
|
+
"us-west-1",
|
|
90
|
+
"us-west-2",
|
|
91
|
+
"eu-west-1",
|
|
92
|
+
"eu-central-1",
|
|
93
|
+
"ap-southeast-1",
|
|
94
|
+
"ap-northeast-1",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_client_connection(client: Any) -> bool:
|
|
99
|
+
"""Test if a boto3 client can connect to AWS.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
client: Boto3 client instance
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if connection successful, False otherwise
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
# Different services have different test methods
|
|
109
|
+
service_name = client._service_model.service_name
|
|
110
|
+
|
|
111
|
+
if service_name == "sts":
|
|
112
|
+
client.get_caller_identity()
|
|
113
|
+
elif service_name == "ec2":
|
|
114
|
+
client.describe_regions(MaxResults=1)
|
|
115
|
+
elif service_name == "iam":
|
|
116
|
+
client.list_users(MaxItems=1)
|
|
117
|
+
elif service_name == "lambda":
|
|
118
|
+
client.list_functions(MaxItems=1)
|
|
119
|
+
elif service_name == "s3":
|
|
120
|
+
client.list_buckets()
|
|
121
|
+
else:
|
|
122
|
+
# Generic test - just try to get service metadata
|
|
123
|
+
client._make_api_call("ListObjects", {})
|
|
124
|
+
|
|
125
|
+
return True
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.debug(f"Client connection test failed: {e}")
|
|
128
|
+
return False
|
src/aws/credentials.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""AWS credential validation and permission checking."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CredentialValidationError(Exception):
|
|
13
|
+
"""Raised when AWS credentials are invalid or missing."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_credentials(profile_name: Optional[str] = None) -> Dict[str, Any]:
|
|
19
|
+
"""Validate AWS credentials and return caller identity information.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
profile_name: AWS profile name from ~/.aws/config (optional)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Dictionary with account ID, user ID, and ARN
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
CredentialValidationError: If credentials are invalid or missing
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
if profile_name:
|
|
32
|
+
session = boto3.Session(profile_name=profile_name)
|
|
33
|
+
sts_client = session.client("sts")
|
|
34
|
+
else:
|
|
35
|
+
sts_client = boto3.client("sts")
|
|
36
|
+
|
|
37
|
+
# Get caller identity to validate credentials
|
|
38
|
+
identity = sts_client.get_caller_identity()
|
|
39
|
+
|
|
40
|
+
logger.debug(f"Validated credentials for account {identity['Account']}")
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"account_id": identity["Account"],
|
|
44
|
+
"user_id": identity["UserId"],
|
|
45
|
+
"arn": identity["Arn"],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
except NoCredentialsError:
|
|
49
|
+
error_msg = (
|
|
50
|
+
"AWS credentials not found. Please configure credentials using one of these methods:\n"
|
|
51
|
+
" 1. Run: aws configure\n"
|
|
52
|
+
" 2. Set environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY\n"
|
|
53
|
+
" 3. Use --profile option with a configured profile\n\n"
|
|
54
|
+
"For more info: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html"
|
|
55
|
+
)
|
|
56
|
+
logger.error("No AWS credentials found")
|
|
57
|
+
raise CredentialValidationError(error_msg)
|
|
58
|
+
|
|
59
|
+
except PartialCredentialsError as e:
|
|
60
|
+
error_msg = f"Incomplete AWS credentials: {e}"
|
|
61
|
+
logger.error(error_msg)
|
|
62
|
+
raise CredentialValidationError(error_msg)
|
|
63
|
+
|
|
64
|
+
except ClientError as e:
|
|
65
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
|
66
|
+
if error_code == "InvalidClientTokenId":
|
|
67
|
+
error_msg = "AWS credentials are invalid. Please check your access key ID."
|
|
68
|
+
elif error_code == "SignatureDoesNotMatch":
|
|
69
|
+
error_msg = "AWS credentials signature mismatch. Please check your secret access key."
|
|
70
|
+
elif error_code == "ExpiredToken":
|
|
71
|
+
error_msg = "AWS credentials have expired. Please refresh your temporary credentials."
|
|
72
|
+
else:
|
|
73
|
+
error_msg = f"AWS credential validation failed: {e}"
|
|
74
|
+
|
|
75
|
+
logger.error(error_msg)
|
|
76
|
+
raise CredentialValidationError(error_msg)
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
error_msg = f"Unexpected error validating credentials: {e}"
|
|
80
|
+
logger.error(error_msg)
|
|
81
|
+
raise CredentialValidationError(error_msg)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_required_permissions(
|
|
85
|
+
profile_name: Optional[str] = None, required_actions: Optional[List[str]] = None
|
|
86
|
+
) -> Dict[str, bool]:
|
|
87
|
+
"""Check if credentials have required IAM permissions.
|
|
88
|
+
|
|
89
|
+
Note: This is a best-effort check using IAM policy simulation.
|
|
90
|
+
Some permissions may not be accurately detected.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
profile_name: AWS profile name (optional)
|
|
94
|
+
required_actions: List of IAM actions to check (e.g., ['ec2:DescribeInstances'])
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dictionary mapping action names to permission status (True/False)
|
|
98
|
+
"""
|
|
99
|
+
if required_actions is None:
|
|
100
|
+
# Default minimum required permissions for snapshot operations
|
|
101
|
+
required_actions = [
|
|
102
|
+
"ec2:DescribeInstances",
|
|
103
|
+
"ec2:DescribeRegions",
|
|
104
|
+
"iam:ListRoles",
|
|
105
|
+
"lambda:ListFunctions",
|
|
106
|
+
"s3:ListAllMyBuckets",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# Get caller identity first
|
|
111
|
+
identity = validate_credentials(profile_name)
|
|
112
|
+
|
|
113
|
+
if profile_name:
|
|
114
|
+
session = boto3.Session(profile_name=profile_name)
|
|
115
|
+
iam_client = session.client("iam")
|
|
116
|
+
else:
|
|
117
|
+
iam_client = boto3.client("iam")
|
|
118
|
+
|
|
119
|
+
results = {}
|
|
120
|
+
|
|
121
|
+
# Try to simulate policy for each action
|
|
122
|
+
for action in required_actions:
|
|
123
|
+
try:
|
|
124
|
+
response = iam_client.simulate_principal_policy(
|
|
125
|
+
PolicySourceArn=identity["arn"],
|
|
126
|
+
ActionNames=[action],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Check if action is allowed
|
|
130
|
+
eval_results = response.get("EvaluationResults", [])
|
|
131
|
+
if eval_results:
|
|
132
|
+
decision = eval_results[0].get("EvalDecision", "deny")
|
|
133
|
+
results[action] = decision.lower() == "allowed"
|
|
134
|
+
else:
|
|
135
|
+
results[action] = False
|
|
136
|
+
|
|
137
|
+
except ClientError as e:
|
|
138
|
+
# If simulation fails (e.g., lack of iam:SimulatePrincipalPolicy permission),
|
|
139
|
+
# we can't determine the permission status
|
|
140
|
+
logger.debug(f"Could not check permission for {action}: {e}")
|
|
141
|
+
results[action] = None # Unknown
|
|
142
|
+
|
|
143
|
+
return results
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning(f"Permission check failed: {e}")
|
|
147
|
+
return {action: None for action in required_actions} # type: ignore
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_account_id(profile_name: Optional[str] = None) -> str:
|
|
151
|
+
"""Get AWS account ID for the current credentials.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
profile_name: AWS profile name (optional)
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
12-digit AWS account ID
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
CredentialValidationError: If credentials are invalid
|
|
161
|
+
"""
|
|
162
|
+
identity = validate_credentials(profile_name)
|
|
163
|
+
return identity["account_id"]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_credential_summary(profile_name: Optional[str] = None) -> str:
|
|
167
|
+
"""Get a human-readable summary of current AWS credentials.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
profile_name: AWS profile name (optional)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Formatted string with credential information
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
identity = validate_credentials(profile_name)
|
|
177
|
+
|
|
178
|
+
summary = f"""
|
|
179
|
+
AWS Credentials Valid
|
|
180
|
+
Account ID: {identity['account_id']}
|
|
181
|
+
User/Role: {identity['arn'].split('/')[-1]}
|
|
182
|
+
ARN: {identity['arn']}
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if profile_name:
|
|
186
|
+
summary += f"Profile: {profile_name}\n"
|
|
187
|
+
|
|
188
|
+
return summary.strip()
|
|
189
|
+
|
|
190
|
+
except CredentialValidationError as e:
|
|
191
|
+
return f"Credential Validation Failed:\n{e}"
|
src/aws/rate_limiter.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Rate limiter utility using token bucket algorithm."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Service-specific rate limits (calls per second)
|
|
12
|
+
# Based on AWS API throttling limits
|
|
13
|
+
SERVICE_RATE_LIMITS: Dict[str, float] = {
|
|
14
|
+
"iam": 5.0, # IAM has strict rate limits (global service)
|
|
15
|
+
"cloudformation": 2.0, # CloudFormation is particularly slow
|
|
16
|
+
"sts": 10.0, # STS is also rate-limited
|
|
17
|
+
"default": 10.0, # Conservative default for other services
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RateLimiter:
|
|
22
|
+
"""Token bucket rate limiter for controlling API call frequency.
|
|
23
|
+
|
|
24
|
+
This prevents hitting AWS API rate limits by throttling client-side
|
|
25
|
+
before the request is even made.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, rate: float):
|
|
29
|
+
"""Initialize rate limiter.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
rate: Maximum number of calls per second
|
|
33
|
+
"""
|
|
34
|
+
self.rate = rate
|
|
35
|
+
self.tokens = rate
|
|
36
|
+
self.last_update = time.time()
|
|
37
|
+
self.lock = Lock()
|
|
38
|
+
|
|
39
|
+
logger.debug(f"Initialized rate limiter with rate {rate} calls/sec")
|
|
40
|
+
|
|
41
|
+
def acquire(self, blocking: bool = True) -> bool:
|
|
42
|
+
"""Acquire permission to make an API call.
|
|
43
|
+
|
|
44
|
+
This method will block until a token is available (if blocking=True)
|
|
45
|
+
or return immediately (if blocking=False).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
blocking: If True, wait until token available. If False, return immediately.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if token acquired, False if blocking=False and no token available
|
|
52
|
+
"""
|
|
53
|
+
with self.lock:
|
|
54
|
+
now = time.time()
|
|
55
|
+
elapsed = now - self.last_update
|
|
56
|
+
|
|
57
|
+
# Refill tokens based on elapsed time
|
|
58
|
+
self.tokens = min(self.rate, self.tokens + elapsed * self.rate)
|
|
59
|
+
self.last_update = now
|
|
60
|
+
|
|
61
|
+
if self.tokens >= 1:
|
|
62
|
+
# Token available
|
|
63
|
+
self.tokens -= 1
|
|
64
|
+
return True
|
|
65
|
+
else:
|
|
66
|
+
# No token available
|
|
67
|
+
if not blocking:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
# Calculate how long to sleep
|
|
71
|
+
sleep_time = (1 - self.tokens) / self.rate
|
|
72
|
+
logger.debug(f"Rate limiter sleeping for {sleep_time:.3f}s")
|
|
73
|
+
|
|
74
|
+
# Sleep outside the lock to allow other threads
|
|
75
|
+
time.sleep(sleep_time)
|
|
76
|
+
|
|
77
|
+
# Acquire token after sleeping
|
|
78
|
+
with self.lock:
|
|
79
|
+
self.tokens = 0
|
|
80
|
+
self.last_update = time.time()
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def try_acquire(self) -> bool:
|
|
84
|
+
"""Try to acquire a token without blocking.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if token acquired, False otherwise
|
|
88
|
+
"""
|
|
89
|
+
return self.acquire(blocking=False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ServiceRateLimiter:
|
|
93
|
+
"""Manages rate limiters for different AWS services."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, rate_limits: Optional[Dict[str, float]] = None):
|
|
96
|
+
"""Initialize service rate limiter.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
rate_limits: Dictionary mapping service names to rates (optional)
|
|
100
|
+
"""
|
|
101
|
+
self.rate_limits = rate_limits or SERVICE_RATE_LIMITS
|
|
102
|
+
self._limiters: Dict[str, RateLimiter] = {}
|
|
103
|
+
self._lock = Lock()
|
|
104
|
+
|
|
105
|
+
def get_limiter(self, service_name: str) -> RateLimiter:
|
|
106
|
+
"""Get or create a rate limiter for a service.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
service_name: AWS service name (e.g., 'iam', 'ec2')
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
RateLimiter instance for the service
|
|
113
|
+
"""
|
|
114
|
+
with self._lock:
|
|
115
|
+
if service_name not in self._limiters:
|
|
116
|
+
rate = self.rate_limits.get(service_name, self.rate_limits["default"])
|
|
117
|
+
self._limiters[service_name] = RateLimiter(rate)
|
|
118
|
+
logger.debug(f"Created rate limiter for {service_name} ({rate} calls/sec)")
|
|
119
|
+
|
|
120
|
+
return self._limiters[service_name]
|
|
121
|
+
|
|
122
|
+
def acquire(self, service_name: str, blocking: bool = True) -> bool:
|
|
123
|
+
"""Acquire permission to call an AWS service API.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
service_name: AWS service name
|
|
127
|
+
blocking: Whether to block until token available
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if token acquired
|
|
131
|
+
"""
|
|
132
|
+
limiter = self.get_limiter(service_name)
|
|
133
|
+
return limiter.acquire(blocking=blocking)
|
|
134
|
+
|
|
135
|
+
def try_acquire(self, service_name: str) -> bool:
|
|
136
|
+
"""Try to acquire permission without blocking.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
service_name: AWS service name
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if token acquired, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
return self.acquire(service_name, blocking=False)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Global service rate limiter instance
|
|
148
|
+
_global_limiter: Optional[ServiceRateLimiter] = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_global_rate_limiter() -> ServiceRateLimiter:
|
|
152
|
+
"""Get the global service rate limiter instance.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Global ServiceRateLimiter instance
|
|
156
|
+
"""
|
|
157
|
+
global _global_limiter
|
|
158
|
+
if _global_limiter is None:
|
|
159
|
+
_global_limiter = ServiceRateLimiter()
|
|
160
|
+
return _global_limiter
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def rate_limited_call(service_name: str, func, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
164
|
+
"""Execute a function with rate limiting applied.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
service_name: AWS service name for rate limiting
|
|
168
|
+
func: Function to call
|
|
169
|
+
*args: Positional arguments to pass to func
|
|
170
|
+
**kwargs: Keyword arguments to pass to func
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Result of func(*args, **kwargs)
|
|
174
|
+
"""
|
|
175
|
+
limiter = get_global_rate_limiter()
|
|
176
|
+
limiter.acquire(service_name)
|
|
177
|
+
return func(*args, **kwargs)
|