runbooks 0.2.3__py3-none-any.whl → 0.6.1__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.
- conftest.py +26 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/app.py +256 -0
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +154 -0
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +123 -0
- jupyter-agent/requirements.txt +9 -0
- jupyter-agent/utils.py +409 -0
- runbooks/__init__.py +71 -3
- runbooks/__main__.py +13 -0
- runbooks/aws/ec2_describe_instances.py +1 -1
- runbooks/aws/ec2_run_instances.py +8 -2
- runbooks/aws/ec2_start_stop_instances.py +17 -4
- runbooks/aws/ec2_unused_volumes.py +5 -1
- runbooks/aws/s3_create_bucket.py +4 -2
- runbooks/aws/s3_list_objects.py +6 -1
- runbooks/aws/tagging_lambda_handler.py +13 -2
- runbooks/aws/tags.json +12 -0
- runbooks/base.py +353 -0
- runbooks/cfat/README.md +49 -0
- runbooks/cfat/__init__.py +74 -0
- runbooks/cfat/app.ts +644 -0
- runbooks/cfat/assessment/__init__.py +40 -0
- runbooks/cfat/assessment/asana-import.csv +39 -0
- runbooks/cfat/assessment/cfat-checks.csv +31 -0
- runbooks/cfat/assessment/cfat.txt +520 -0
- runbooks/cfat/assessment/collectors.py +200 -0
- runbooks/cfat/assessment/jira-import.csv +39 -0
- runbooks/cfat/assessment/runner.py +387 -0
- runbooks/cfat/assessment/validators.py +290 -0
- runbooks/cfat/cli.py +103 -0
- runbooks/cfat/docs/asana-import.csv +24 -0
- runbooks/cfat/docs/cfat-checks.csv +31 -0
- runbooks/cfat/docs/cfat.txt +335 -0
- runbooks/cfat/docs/checks-output.png +0 -0
- runbooks/cfat/docs/cloudshell-console-run.png +0 -0
- runbooks/cfat/docs/cloudshell-download.png +0 -0
- runbooks/cfat/docs/cloudshell-output.png +0 -0
- runbooks/cfat/docs/downloadfile.png +0 -0
- runbooks/cfat/docs/jira-import.csv +24 -0
- runbooks/cfat/docs/open-cloudshell.png +0 -0
- runbooks/cfat/docs/report-header.png +0 -0
- runbooks/cfat/models.py +1026 -0
- runbooks/cfat/package-lock.json +5116 -0
- runbooks/cfat/package.json +38 -0
- runbooks/cfat/report.py +496 -0
- runbooks/cfat/reporting/__init__.py +46 -0
- runbooks/cfat/reporting/exporters.py +337 -0
- runbooks/cfat/reporting/formatters.py +496 -0
- runbooks/cfat/reporting/templates.py +135 -0
- runbooks/cfat/run-assessment.sh +23 -0
- runbooks/cfat/runner.py +69 -0
- runbooks/cfat/src/actions/check-cloudtrail-existence.ts +43 -0
- runbooks/cfat/src/actions/check-config-existence.ts +37 -0
- runbooks/cfat/src/actions/check-control-tower.ts +37 -0
- runbooks/cfat/src/actions/check-ec2-existence.ts +46 -0
- runbooks/cfat/src/actions/check-iam-users.ts +50 -0
- runbooks/cfat/src/actions/check-legacy-cur.ts +30 -0
- runbooks/cfat/src/actions/check-org-cloudformation.ts +30 -0
- runbooks/cfat/src/actions/check-vpc-existence.ts +43 -0
- runbooks/cfat/src/actions/create-asanaimport.ts +14 -0
- runbooks/cfat/src/actions/create-backlog.ts +372 -0
- runbooks/cfat/src/actions/create-jiraimport.ts +15 -0
- runbooks/cfat/src/actions/create-report.ts +616 -0
- runbooks/cfat/src/actions/define-account-type.ts +51 -0
- runbooks/cfat/src/actions/get-enabled-org-policy-types.ts +40 -0
- runbooks/cfat/src/actions/get-enabled-org-services.ts +26 -0
- runbooks/cfat/src/actions/get-idc-info.ts +34 -0
- runbooks/cfat/src/actions/get-org-da-accounts.ts +34 -0
- runbooks/cfat/src/actions/get-org-details.ts +35 -0
- runbooks/cfat/src/actions/get-org-member-accounts.ts +44 -0
- runbooks/cfat/src/actions/get-org-ous.ts +35 -0
- runbooks/cfat/src/actions/get-regions.ts +22 -0
- runbooks/cfat/src/actions/zip-assessment.ts +27 -0
- runbooks/cfat/src/types/index.d.ts +147 -0
- runbooks/cfat/tests/__init__.py +141 -0
- runbooks/cfat/tests/test_cli.py +340 -0
- runbooks/cfat/tests/test_integration.py +290 -0
- runbooks/cfat/tests/test_models.py +505 -0
- runbooks/cfat/tests/test_reporting.py +354 -0
- runbooks/cfat/tsconfig.json +16 -0
- runbooks/cfat/webpack.config.cjs +27 -0
- runbooks/config.py +260 -0
- runbooks/finops/__init__.py +88 -0
- runbooks/finops/aws_client.py +245 -0
- runbooks/finops/cli.py +151 -0
- runbooks/finops/cost_processor.py +410 -0
- runbooks/finops/dashboard_runner.py +448 -0
- runbooks/finops/helpers.py +355 -0
- runbooks/finops/main.py +14 -0
- runbooks/finops/profile_processor.py +174 -0
- runbooks/finops/types.py +66 -0
- runbooks/finops/visualisations.py +80 -0
- runbooks/inventory/.gitignore +354 -0
- runbooks/inventory/ArgumentsClass.py +261 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/README.md +1320 -0
- runbooks/inventory/__init__.py +62 -0
- runbooks/inventory/account_class.py +532 -0
- runbooks/inventory/all_my_instances_wrapper.py +123 -0
- runbooks/inventory/aws_decorators.py +201 -0
- runbooks/inventory/cfn_move_stack_instances.py +1526 -0
- runbooks/inventory/check_cloudtrail_compliance.py +614 -0
- runbooks/inventory/check_controltower_readiness.py +1107 -0
- runbooks/inventory/check_landingzone_readiness.py +711 -0
- runbooks/inventory/cloudtrail.md +727 -0
- runbooks/inventory/collectors/__init__.py +20 -0
- runbooks/inventory/collectors/aws_compute.py +518 -0
- runbooks/inventory/collectors/aws_networking.py +275 -0
- runbooks/inventory/collectors/base.py +222 -0
- runbooks/inventory/core/__init__.py +19 -0
- runbooks/inventory/core/collector.py +303 -0
- runbooks/inventory/core/formatter.py +296 -0
- runbooks/inventory/delete_s3_buckets_objects.py +169 -0
- runbooks/inventory/discovery.md +81 -0
- runbooks/inventory/draw_org_structure.py +748 -0
- runbooks/inventory/ec2_vpc_utils.py +341 -0
- runbooks/inventory/find_cfn_drift_detection.py +272 -0
- runbooks/inventory/find_cfn_orphaned_stacks.py +719 -0
- runbooks/inventory/find_cfn_stackset_drift.py +733 -0
- runbooks/inventory/find_ec2_security_groups.py +669 -0
- runbooks/inventory/find_landingzone_versions.py +201 -0
- runbooks/inventory/find_vpc_flow_logs.py +1221 -0
- runbooks/inventory/inventory.sh +659 -0
- runbooks/inventory/list_cfn_stacks.py +558 -0
- runbooks/inventory/list_cfn_stackset_operation_results.py +252 -0
- runbooks/inventory/list_cfn_stackset_operations.py +734 -0
- runbooks/inventory/list_cfn_stacksets.py +453 -0
- runbooks/inventory/list_config_recorders_delivery_channels.py +681 -0
- runbooks/inventory/list_ds_directories.py +354 -0
- runbooks/inventory/list_ec2_availability_zones.py +286 -0
- runbooks/inventory/list_ec2_ebs_volumes.py +244 -0
- runbooks/inventory/list_ec2_instances.py +425 -0
- runbooks/inventory/list_ecs_clusters_and_tasks.py +562 -0
- runbooks/inventory/list_elbs_load_balancers.py +411 -0
- runbooks/inventory/list_enis_network_interfaces.py +526 -0
- runbooks/inventory/list_guardduty_detectors.py +568 -0
- runbooks/inventory/list_iam_policies.py +404 -0
- runbooks/inventory/list_iam_roles.py +518 -0
- runbooks/inventory/list_iam_saml_providers.py +359 -0
- runbooks/inventory/list_lambda_functions.py +882 -0
- runbooks/inventory/list_org_accounts.py +446 -0
- runbooks/inventory/list_org_accounts_users.py +354 -0
- runbooks/inventory/list_rds_db_instances.py +406 -0
- runbooks/inventory/list_route53_hosted_zones.py +318 -0
- runbooks/inventory/list_servicecatalog_provisioned_products.py +575 -0
- runbooks/inventory/list_sns_topics.py +360 -0
- runbooks/inventory/list_ssm_parameters.py +402 -0
- runbooks/inventory/list_vpc_subnets.py +433 -0
- runbooks/inventory/list_vpcs.py +422 -0
- runbooks/inventory/lockdown_cfn_stackset_role.py +224 -0
- runbooks/inventory/models/__init__.py +24 -0
- runbooks/inventory/models/account.py +192 -0
- runbooks/inventory/models/inventory.py +309 -0
- runbooks/inventory/models/resource.py +247 -0
- runbooks/inventory/recover_cfn_stack_ids.py +205 -0
- runbooks/inventory/requirements.txt +12 -0
- runbooks/inventory/run_on_multi_accounts.py +211 -0
- runbooks/inventory/tests/common_test_data.py +3661 -0
- runbooks/inventory/tests/common_test_functions.py +204 -0
- runbooks/inventory/tests/script_test_data.py +0 -0
- runbooks/inventory/tests/setup.py +24 -0
- runbooks/inventory/tests/src.py +18 -0
- runbooks/inventory/tests/test_cfn_describe_stacks.py +208 -0
- runbooks/inventory/tests/test_ec2_describe_instances.py +162 -0
- runbooks/inventory/tests/test_inventory_modules.py +55 -0
- runbooks/inventory/tests/test_lambda_list_functions.py +86 -0
- runbooks/inventory/tests/test_moto_integration_example.py +273 -0
- runbooks/inventory/tests/test_org_list_accounts.py +49 -0
- runbooks/inventory/update_aws_actions.py +173 -0
- runbooks/inventory/update_cfn_stacksets.py +1215 -0
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +294 -0
- runbooks/inventory/update_iam_roles_cross_accounts.py +478 -0
- runbooks/inventory/update_s3_public_access_block.py +539 -0
- runbooks/inventory/utils/__init__.py +23 -0
- runbooks/inventory/utils/aws_helpers.py +510 -0
- runbooks/inventory/utils/threading_utils.py +493 -0
- runbooks/inventory/utils/validation.py +682 -0
- runbooks/inventory/verify_ec2_security_groups.py +1430 -0
- runbooks/main.py +785 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security_baseline/README.md +324 -0
- runbooks/security_baseline/checklist/alternate_contacts.py +8 -1
- runbooks/security_baseline/checklist/bucket_public_access.py +4 -1
- runbooks/security_baseline/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/security_baseline/checklist/guardduty_enabled.py +9 -2
- runbooks/security_baseline/checklist/multi_region_instance_usage.py +5 -1
- runbooks/security_baseline/checklist/root_access_key.py +6 -1
- runbooks/security_baseline/config-origin.json +1 -1
- runbooks/security_baseline/config.json +1 -1
- runbooks/security_baseline/permission.json +1 -1
- runbooks/security_baseline/report_generator.py +10 -2
- runbooks/security_baseline/report_template_en.html +8 -8
- runbooks/security_baseline/report_template_jp.html +8 -8
- runbooks/security_baseline/report_template_kr.html +13 -13
- runbooks/security_baseline/report_template_vn.html +8 -8
- runbooks/security_baseline/requirements.txt +7 -0
- runbooks/security_baseline/run_script.py +8 -2
- runbooks/security_baseline/security_baseline_tester.py +10 -2
- runbooks/security_baseline/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.6.1.dist-info/METADATA +373 -0
- runbooks-0.6.1.dist-info/RECORD +237 -0
- {runbooks-0.2.3.dist-info → runbooks-0.6.1.dist-info}/WHEEL +1 -1
- runbooks-0.6.1.dist-info/entry_points.txt +7 -0
- runbooks-0.6.1.dist-info/licenses/LICENSE +201 -0
- runbooks-0.6.1.dist-info/top_level.txt +3 -0
- runbooks/python101/calculator.py +0 -34
- runbooks/python101/config.py +0 -1
- runbooks/python101/exceptions.py +0 -16
- runbooks/python101/file_manager.py +0 -218
- runbooks/python101/toolkit.py +0 -153
- runbooks-0.2.3.dist-info/METADATA +0 -435
- runbooks-0.2.3.dist-info/RECORD +0 -61
- runbooks-0.2.3.dist-info/entry_points.txt +0 -3
- runbooks-0.2.3.dist-info/top_level.txt +0 -1
@@ -0,0 +1,422 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
AWS VPC Inventory Collection
|
4
|
+
|
5
|
+
A comprehensive VPC discovery tool for multi-account AWS Organizations that provides
|
6
|
+
detailed network topology visibility across all accounts and regions. Supports
|
7
|
+
filtering for default VPCs to identify potential security risks.
|
8
|
+
|
9
|
+
**AWS API Mapping**: `boto3.client('ec2').describe_vpcs()`
|
10
|
+
|
11
|
+
Features:
|
12
|
+
- Multi-account VPC discovery via AWS Organizations
|
13
|
+
- Default VPC identification for security auditing
|
14
|
+
- CIDR block and association analysis
|
15
|
+
- VPC peering and gateway information
|
16
|
+
- Tag-based metadata collection
|
17
|
+
- Cross-region network topology mapping
|
18
|
+
|
19
|
+
Compatibility:
|
20
|
+
- AWS Organizations with cross-account roles
|
21
|
+
- AWS Control Tower managed accounts
|
22
|
+
- Standalone AWS accounts
|
23
|
+
- All AWS regions including opt-in regions
|
24
|
+
|
25
|
+
Example:
|
26
|
+
Discover all VPCs across organization:
|
27
|
+
```bash
|
28
|
+
python ec2_describe_vpcs.py --profile my-org-profile
|
29
|
+
```
|
30
|
+
|
31
|
+
Find only default VPCs for security audit:
|
32
|
+
```bash
|
33
|
+
python ec2_describe_vpcs.py --profile my-profile --default
|
34
|
+
```
|
35
|
+
|
36
|
+
Export VPC inventory to file:
|
37
|
+
```bash
|
38
|
+
python ec2_describe_vpcs.py --profile my-profile \\
|
39
|
+
--save vpc_inventory.json --output json
|
40
|
+
```
|
41
|
+
|
42
|
+
Requirements:
|
43
|
+
- IAM permissions: `ec2:DescribeVpcs`, `sts:AssumeRole`
|
44
|
+
- AWS Organizations access (for multi-account scanning)
|
45
|
+
- Python 3.8+ with required dependencies
|
46
|
+
|
47
|
+
Author:
|
48
|
+
AWS Cloud Foundations Team
|
49
|
+
|
50
|
+
Version:
|
51
|
+
2024.01.26
|
52
|
+
"""
|
53
|
+
|
54
|
+
import logging
|
55
|
+
import sys
|
56
|
+
from queue import Queue
|
57
|
+
|
58
|
+
# from tqdm.auto import tqdm
|
59
|
+
from threading import Thread
|
60
|
+
from time import time
|
61
|
+
|
62
|
+
import Inventory_Modules
|
63
|
+
from ArgumentsClass import CommonArguments
|
64
|
+
from botocore.exceptions import ClientError
|
65
|
+
from colorama import Fore, init
|
66
|
+
from Inventory_Modules import display_results, get_all_credentials
|
67
|
+
|
68
|
+
init()
|
69
|
+
__version__ = "2024.01.26"
|
70
|
+
|
71
|
+
|
72
|
+
##########################
|
73
|
+
def parse_args(args):
|
74
|
+
"""
|
75
|
+
Parse and validate command-line arguments for VPC network topology discovery.
|
76
|
+
|
77
|
+
Configures the argument parser with VPC-specific options including default VPC
|
78
|
+
detection for security auditing and comprehensive network inventory capabilities.
|
79
|
+
Uses the standardized CommonArguments framework for consistency.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
args (list): Command-line arguments to parse (typically sys.argv[1:])
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
argparse.Namespace: Parsed arguments containing:
|
86
|
+
- Profiles: AWS profiles for multi-account network discovery
|
87
|
+
- Regions: Target AWS regions for VPC enumeration
|
88
|
+
- AccessRoles: Cross-account roles for Organizations access
|
89
|
+
- pDefault: CRITICAL security flag for default VPC identification
|
90
|
+
- RootOnly: Limit to Organization Management Accounts
|
91
|
+
- Filename: Output file prefix for network topology export
|
92
|
+
- Other standard framework arguments
|
93
|
+
|
94
|
+
Security-Critical Argument:
|
95
|
+
--default: Identifies default VPCs which represent significant security risks:
|
96
|
+
- Often have overly permissive security groups
|
97
|
+
- May violate network segmentation policies
|
98
|
+
- Should be replaced with purpose-built VPCs
|
99
|
+
- Critical for compliance auditing (PCI DSS, SOC2)
|
100
|
+
- Essential for CIS benchmark compliance
|
101
|
+
|
102
|
+
Network Architecture Use Cases:
|
103
|
+
- Network topology mapping: Complete VPC inventory
|
104
|
+
- Security auditing: Default VPC identification and remediation
|
105
|
+
- CIDR planning: IP address space utilization analysis
|
106
|
+
- Compliance validation: Network segmentation verification
|
107
|
+
- Migration planning: Cross-account network architecture assessment
|
108
|
+
"""
|
109
|
+
parser = CommonArguments()
|
110
|
+
parser.my_parser.description = "We're going to find all vpcs within any of the accounts and regions we have access to, given the profile(s) provided."
|
111
|
+
parser.multiprofile()
|
112
|
+
parser.multiregion()
|
113
|
+
parser.extendedargs()
|
114
|
+
parser.rolestouse()
|
115
|
+
parser.rootOnly()
|
116
|
+
parser.timing()
|
117
|
+
parser.save_to_file()
|
118
|
+
parser.verbosity()
|
119
|
+
parser.version(__version__)
|
120
|
+
parser.my_parser.add_argument(
|
121
|
+
"--default",
|
122
|
+
dest="pDefault",
|
123
|
+
metavar="Looking for default VPCs only",
|
124
|
+
action="store_const",
|
125
|
+
default=False,
|
126
|
+
const=True,
|
127
|
+
help="Flag to determine whether we're looking for default VPCs only.",
|
128
|
+
)
|
129
|
+
return parser.my_parser.parse_args(args)
|
130
|
+
|
131
|
+
|
132
|
+
def find_all_vpcs(fAllCredentials, fDefaultOnly=False):
|
133
|
+
"""
|
134
|
+
Execute multi-threaded VPC discovery across AWS accounts and regions.
|
135
|
+
|
136
|
+
This is the core network topology discovery engine that performs concurrent
|
137
|
+
VPC enumeration across all provided AWS accounts and regions. Essential for
|
138
|
+
understanding network architecture, CIDR utilization, and security posture.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
fAllCredentials (list): List of credential dictionaries containing:
|
142
|
+
- AccountId: AWS account identifier
|
143
|
+
- Region: AWS region name
|
144
|
+
- AccessKeyId, SecretAccessKey, SessionToken: AWS credentials
|
145
|
+
- MgmtAccount: Management account identifier
|
146
|
+
- Success: Boolean flag indicating credential validation status
|
147
|
+
|
148
|
+
fDefaultOnly (bool, optional): Focus discovery on default VPCs only.
|
149
|
+
Critical for security auditing as default VPCs often violate
|
150
|
+
network security policies and compliance requirements.
|
151
|
+
|
152
|
+
Returns:
|
153
|
+
list: Comprehensive VPC inventory with network metadata:
|
154
|
+
- VpcId: AWS VPC identifier
|
155
|
+
- VpcName: VPC name from tags (or "No name defined")
|
156
|
+
- CIDR: VPC CIDR block (handles multiple CIDRs per VPC)
|
157
|
+
- IsDefault: Boolean indicating if VPC is account default
|
158
|
+
- AccountId: Source AWS account
|
159
|
+
- Region: Source AWS region
|
160
|
+
- MgmtAccount: Management account identifier
|
161
|
+
|
162
|
+
Threading Architecture:
|
163
|
+
- Uses Queue for thread-safe work distribution
|
164
|
+
- Worker thread pool for concurrent VPC discovery
|
165
|
+
- Progress tracking for large-scale network inventory
|
166
|
+
- Comprehensive error handling for account access failures
|
167
|
+
|
168
|
+
Network Analysis Features:
|
169
|
+
- CIDR block enumeration (handles secondary CIDR associations)
|
170
|
+
- Default VPC identification for security compliance
|
171
|
+
- Tag-based VPC naming and categorization
|
172
|
+
- Cross-account network topology mapping
|
173
|
+
- Regional network architecture visibility
|
174
|
+
|
175
|
+
Security Implications:
|
176
|
+
- Default VPCs represent significant security risks
|
177
|
+
- Often have permissive default security groups
|
178
|
+
- May violate network segmentation requirements
|
179
|
+
- Critical for PCI DSS and SOC2 compliance validation
|
180
|
+
- Essential for Zero Trust architecture assessment
|
181
|
+
|
182
|
+
Enterprise Use Cases:
|
183
|
+
- Network architecture documentation
|
184
|
+
- CIDR space planning and IP address management
|
185
|
+
- Compliance auditing and remediation planning
|
186
|
+
- Multi-account network segmentation validation
|
187
|
+
- Cloud migration network assessment
|
188
|
+
"""
|
189
|
+
|
190
|
+
class FindVPCs(Thread):
|
191
|
+
"""
|
192
|
+
Worker thread for concurrent VPC discovery and network topology analysis.
|
193
|
+
|
194
|
+
Each worker thread processes credential sets from the shared queue,
|
195
|
+
calls AWS EC2 VPC APIs to discover network infrastructure, and performs
|
196
|
+
detailed CIDR and default VPC analysis for security assessment.
|
197
|
+
|
198
|
+
Network Discovery Capabilities:
|
199
|
+
- VPC enumeration with metadata extraction
|
200
|
+
- CIDR block association analysis
|
201
|
+
- Default VPC identification for security auditing
|
202
|
+
- Tag-based VPC naming and categorization
|
203
|
+
- Cross-account network topology mapping
|
204
|
+
"""
|
205
|
+
|
206
|
+
def __init__(self, queue):
|
207
|
+
"""
|
208
|
+
Initialize worker thread with reference to shared work queue.
|
209
|
+
|
210
|
+
Args:
|
211
|
+
queue (Queue): Thread-safe queue containing VPC discovery work items
|
212
|
+
"""
|
213
|
+
Thread.__init__(self)
|
214
|
+
self.queue = queue
|
215
|
+
|
216
|
+
def run(self):
|
217
|
+
"""
|
218
|
+
Main worker thread execution loop for VPC network discovery.
|
219
|
+
|
220
|
+
Continuously processes credential sets from queue, performs VPC
|
221
|
+
discovery via AWS EC2 APIs, and aggregates network topology data
|
222
|
+
with comprehensive CIDR and security analysis.
|
223
|
+
"""
|
224
|
+
while True:
|
225
|
+
# Get VPC discovery work item from thread-safe queue
|
226
|
+
c_account_credentials, c_default, c_PlaceCount = self.queue.get()
|
227
|
+
logging.info(f"De-queued info for account number {c_account_credentials['AccountId']}")
|
228
|
+
|
229
|
+
try:
|
230
|
+
# Call AWS EC2 API to discover VPCs in this account/region
|
231
|
+
# find_account_vpcs2() handles DescribeVpcs API with optional default filtering
|
232
|
+
Vpcs = Inventory_Modules.find_account_vpcs2(c_account_credentials, c_default)
|
233
|
+
|
234
|
+
logging.info(
|
235
|
+
f"Account: {c_account_credentials['AccountId']} Region: {c_account_credentials['Region']} | Found {len(Vpcs['Vpcs'])} VPCs"
|
236
|
+
)
|
237
|
+
# Process discovered VPCs with comprehensive network metadata extraction
|
238
|
+
if "Vpcs" in Vpcs.keys() and len(Vpcs["Vpcs"]) > 0:
|
239
|
+
for y in range(len(Vpcs["Vpcs"])):
|
240
|
+
# Initialize VPC metadata with default values
|
241
|
+
VpcName = "No name defined" # Fallback for untagged VPCs
|
242
|
+
VpcId = Vpcs["Vpcs"][y]["VpcId"]
|
243
|
+
IsDefault = Vpcs["Vpcs"][y]["IsDefault"] # Critical for security assessment
|
244
|
+
CIDRBlockAssociationSet = Vpcs["Vpcs"][y]["CidrBlockAssociationSet"]
|
245
|
+
|
246
|
+
# Extract VPC name from tags for network documentation
|
247
|
+
# Proper VPC naming is essential for network governance
|
248
|
+
if "Tags" in Vpcs["Vpcs"][y]:
|
249
|
+
for z in range(len(Vpcs["Vpcs"][y]["Tags"])):
|
250
|
+
if Vpcs["Vpcs"][y]["Tags"][z]["Key"] == "Name":
|
251
|
+
VpcName = Vpcs["Vpcs"][y]["Tags"][z]["Value"]
|
252
|
+
|
253
|
+
# Handle multiple CIDR block associations per VPC
|
254
|
+
# AWS supports secondary CIDR blocks for IP space expansion
|
255
|
+
# Each CIDR gets its own record for accurate IP space tracking
|
256
|
+
for _ in range(len(CIDRBlockAssociationSet)):
|
257
|
+
# Create individual record for each CIDR association
|
258
|
+
# This enables precise CIDR space analysis and planning
|
259
|
+
AllVPCs.append(
|
260
|
+
{
|
261
|
+
# Organizational context
|
262
|
+
"MgmtAccount": c_account_credentials["MgmtAccount"],
|
263
|
+
"AccountId": c_account_credentials["AccountId"],
|
264
|
+
"Region": c_account_credentials["Region"],
|
265
|
+
# Network topology data
|
266
|
+
"CIDR": CIDRBlockAssociationSet[_]["CidrBlock"],
|
267
|
+
"VpcId": VpcId,
|
268
|
+
"VpcName": VpcName,
|
269
|
+
# Security-critical default VPC flag
|
270
|
+
"IsDefault": IsDefault,
|
271
|
+
}
|
272
|
+
)
|
273
|
+
else:
|
274
|
+
# No VPCs found in this account/region combination
|
275
|
+
continue
|
276
|
+
except KeyError as my_Error:
|
277
|
+
# Handle credential or account access configuration errors
|
278
|
+
logging.error(f"Account Access failed - trying to access {c_account_credentials['AccountId']}")
|
279
|
+
logging.info(f"Actual Error: {my_Error}")
|
280
|
+
# Continue processing other accounts despite this failure
|
281
|
+
pass
|
282
|
+
|
283
|
+
except AttributeError as my_Error:
|
284
|
+
# Handle profile configuration or credential format errors
|
285
|
+
logging.error(f"Error: Likely that one of the supplied profiles was wrong")
|
286
|
+
logging.warning(my_Error)
|
287
|
+
continue
|
288
|
+
|
289
|
+
except ClientError as my_Error:
|
290
|
+
# Handle AWS API authentication and authorization errors
|
291
|
+
if "AuthFailure" in str(my_Error):
|
292
|
+
logging.error(
|
293
|
+
f"Authorization Failure accessing account {c_account_credentials['AccountId']} in {c_account_credentials['Region']} region"
|
294
|
+
)
|
295
|
+
logging.warning(
|
296
|
+
f"It's possible that the region {c_account_credentials['Region']} hasn't been opted-into"
|
297
|
+
)
|
298
|
+
continue
|
299
|
+
else:
|
300
|
+
# Handle API throttling and other service errors
|
301
|
+
logging.error(f"Error: Likely throttling errors from too much activity")
|
302
|
+
logging.warning(my_Error)
|
303
|
+
continue
|
304
|
+
|
305
|
+
finally:
|
306
|
+
# Always mark work item as complete for queue management
|
307
|
+
self.queue.task_done()
|
308
|
+
|
309
|
+
###########
|
310
|
+
|
311
|
+
checkqueue = Queue()
|
312
|
+
|
313
|
+
AllVPCs = []
|
314
|
+
PlaceCount = 0
|
315
|
+
WorkerThreads = min(len(fAllCredentials), 25)
|
316
|
+
|
317
|
+
worker_threads = []
|
318
|
+
for x in range(WorkerThreads):
|
319
|
+
worker = FindVPCs(checkqueue)
|
320
|
+
# Setting daemon to False for proper cleanup
|
321
|
+
worker.daemon = False
|
322
|
+
worker.start()
|
323
|
+
worker_threads.append(worker)
|
324
|
+
|
325
|
+
for credential in fAllCredentials:
|
326
|
+
logging.info(f"Beginning to queue data - starting with {credential['AccountId']}")
|
327
|
+
print(
|
328
|
+
f"{ERASE_LINE}Checking {credential['AccountId']} in region {credential['Region']} - {PlaceCount + 1} / {len(fAllCredentials)}",
|
329
|
+
end="\r",
|
330
|
+
)
|
331
|
+
# for region in fRegionList:
|
332
|
+
try:
|
333
|
+
# I don't know why - but double parens are necessary below. If you remove them, only the first parameter is queued.
|
334
|
+
checkqueue.put((credential, fDefaultOnly, PlaceCount))
|
335
|
+
logging.info(f"Put credential: {credential}, Default: {fDefaultOnly}")
|
336
|
+
PlaceCount += 1
|
337
|
+
except ClientError as my_Error:
|
338
|
+
if "AuthFailure" in str(my_Error):
|
339
|
+
logging.error(
|
340
|
+
f"Authorization Failure accessing account {credential['AccountId']} in {credential['Region']} region"
|
341
|
+
)
|
342
|
+
logging.warning(f"It's possible that the region {credential['Region']} hasn't been opted-into")
|
343
|
+
pass
|
344
|
+
checkqueue.join()
|
345
|
+
return AllVPCs
|
346
|
+
|
347
|
+
|
348
|
+
##########################
|
349
|
+
if __name__ == "__main__":
|
350
|
+
args = parse_args(sys.argv[1:])
|
351
|
+
pProfiles = args.Profiles
|
352
|
+
pRegionList = args.Regions
|
353
|
+
pAccounts = args.Accounts
|
354
|
+
pRoles = args.AccessRoles
|
355
|
+
pSkipProfiles = args.SkipProfiles
|
356
|
+
pSkipAccounts = args.SkipAccounts
|
357
|
+
pRootOnly = args.RootOnly
|
358
|
+
pTiming = args.Time
|
359
|
+
pFilename = args.Filename
|
360
|
+
pDefault = args.pDefault
|
361
|
+
verbose = args.loglevel
|
362
|
+
logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)30s() ] %(message)s")
|
363
|
+
|
364
|
+
ERASE_LINE = "\x1b[2K"
|
365
|
+
|
366
|
+
begin_time = time()
|
367
|
+
|
368
|
+
NumVpcsFound = 0
|
369
|
+
NumRegions = 0
|
370
|
+
if pProfiles is not None:
|
371
|
+
print(f"Checking for VPCs in profile{'s' if len(pProfiles) > 1 else ''} {pProfiles}")
|
372
|
+
else:
|
373
|
+
print(f"Checking for VPCs in default profile")
|
374
|
+
|
375
|
+
# NumOfRootProfiles = 0
|
376
|
+
# Get credentials
|
377
|
+
AllCredentials = get_all_credentials(
|
378
|
+
pProfiles, pTiming, pSkipProfiles, pSkipAccounts, pRootOnly, pAccounts, pRegionList, pRoles
|
379
|
+
)
|
380
|
+
AllRegionsList = list(set([x["Region"] for x in AllCredentials]))
|
381
|
+
AllAccountList = list(set([x["AccountId"] for x in AllCredentials]))
|
382
|
+
# Find the VPCs
|
383
|
+
All_VPCs_Found = find_all_vpcs(AllCredentials, pDefault)
|
384
|
+
# Display results
|
385
|
+
display_dict = {
|
386
|
+
"MgmtAccount": {"DisplayOrder": 1, "Heading": "Mgmt Acct"},
|
387
|
+
"AccountId": {"DisplayOrder": 2, "Heading": "Acct Number"},
|
388
|
+
"Region": {"DisplayOrder": 3, "Heading": "Region"},
|
389
|
+
"VpcName": {"DisplayOrder": 4, "Heading": "VPC Name"},
|
390
|
+
"CIDR": {"DisplayOrder": 5, "Heading": "CIDR Block"},
|
391
|
+
"IsDefault": {"DisplayOrder": 6, "Heading": "Default VPC", "Condition": [True, 1, "1"]},
|
392
|
+
"VpcId": {"DisplayOrder": 7, "Heading": "VPC Id"},
|
393
|
+
}
|
394
|
+
|
395
|
+
logging.info(f"# of Regions: {len(AllRegionsList)}")
|
396
|
+
# logging.info(f"# of Management Accounts: {NumOfRootProfiles}")
|
397
|
+
logging.info(f"# of Child Accounts: {len(AllAccountList)}")
|
398
|
+
|
399
|
+
sorted_AllVPCs = sorted(
|
400
|
+
All_VPCs_Found, key=lambda d: (d["MgmtAccount"], d["AccountId"], d["Region"], d["VpcName"], d["CIDR"])
|
401
|
+
)
|
402
|
+
print()
|
403
|
+
display_results(sorted_AllVPCs, display_dict, None, pFilename)
|
404
|
+
|
405
|
+
# Threading cleanup is handled within find_all_vpcs function
|
406
|
+
|
407
|
+
print()
|
408
|
+
# checkqueue.join() marks all of the threads as done - so Checking is done
|
409
|
+
logging.info(f"Threads all done - took {time() - begin_time:.2f} seconds")
|
410
|
+
|
411
|
+
if pTiming:
|
412
|
+
print(ERASE_LINE)
|
413
|
+
print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
|
414
|
+
print(ERASE_LINE)
|
415
|
+
# Had to do this, because some of the VPCs that show up in the "sorted_AllVPCs" list are actually the same VPC, with a different CIDR range.
|
416
|
+
Num_of_unique_VPCs = len(set([x["VpcId"] for x in sorted_AllVPCs]))
|
417
|
+
print(
|
418
|
+
f"Found {Num_of_unique_VPCs}{' default' if pDefault else ''} Vpcs across {len(AllAccountList)} accounts across {len(AllRegionsList)} regions"
|
419
|
+
)
|
420
|
+
print()
|
421
|
+
print("Thank you for using this script.")
|
422
|
+
print()
|
@@ -0,0 +1,224 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import sys
|
5
|
+
|
6
|
+
import boto3
|
7
|
+
import Inventory_Modules
|
8
|
+
import simplejson as json
|
9
|
+
from account_class import aws_acct_access
|
10
|
+
from ArgumentsClass import CommonArguments
|
11
|
+
from botocore.exceptions import ClientError
|
12
|
+
|
13
|
+
__version__ = "2023.05.04"
|
14
|
+
|
15
|
+
parser = CommonArguments()
|
16
|
+
parser.singleprofile()
|
17
|
+
parser.singleregion()
|
18
|
+
parser.verbosity()
|
19
|
+
parser.version(__version__)
|
20
|
+
parser.my_parser.add_argument(
|
21
|
+
"-R",
|
22
|
+
"--access_rolename",
|
23
|
+
dest="pAccessRole",
|
24
|
+
default="AWSCloudFormationStackSetExecutionRole",
|
25
|
+
metavar="role to use for access to child accounts",
|
26
|
+
help="This parameter specifies the role that will allow this script to have access to the children accounts.",
|
27
|
+
)
|
28
|
+
parser.my_parser.add_argument(
|
29
|
+
"-t",
|
30
|
+
"--target_rolename",
|
31
|
+
dest="pTargetRole",
|
32
|
+
default="AWSCloudFormationStackSetExecutionRole",
|
33
|
+
metavar="role to change",
|
34
|
+
help="This parameter specifies the role to have its Trust Policy changed.",
|
35
|
+
)
|
36
|
+
parser.my_parser.add_argument(
|
37
|
+
"+f",
|
38
|
+
"--fix",
|
39
|
+
"+fix",
|
40
|
+
dest="pFix",
|
41
|
+
action="store_const",
|
42
|
+
const=True,
|
43
|
+
default=False,
|
44
|
+
help="This parameter determines whether to make any changes in child accounts.",
|
45
|
+
)
|
46
|
+
parser.my_parser.add_argument(
|
47
|
+
"+l",
|
48
|
+
"--lock",
|
49
|
+
"+lock",
|
50
|
+
dest="pLock",
|
51
|
+
action="store_const",
|
52
|
+
const=True,
|
53
|
+
default=False,
|
54
|
+
help="This parameter determines whether to lock the Trust Policy.",
|
55
|
+
)
|
56
|
+
parser.my_parser.add_argument(
|
57
|
+
"-s",
|
58
|
+
"--safety",
|
59
|
+
dest="pSafety",
|
60
|
+
action="store_const",
|
61
|
+
const=False,
|
62
|
+
default=True,
|
63
|
+
help="Adding this parameter will 'remove the safety' - by not including the principle running this script, which might mean you get locked out of making further changes.",
|
64
|
+
)
|
65
|
+
args = parser.my_parser.parse_args()
|
66
|
+
|
67
|
+
pProfile = args.Profile
|
68
|
+
pTargetRole = args.pTargetRole
|
69
|
+
pAccessRole = args.pAccessRole
|
70
|
+
pLock = args.pLock
|
71
|
+
pSafety = args.pSafety
|
72
|
+
pFix = args.pFix
|
73
|
+
verbose = args.loglevel
|
74
|
+
logging.basicConfig(level=args.loglevel, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
|
75
|
+
|
76
|
+
aws_acct = aws_acct_access(pProfile)
|
77
|
+
|
78
|
+
if not aws_acct.AccountType.lower() == "root":
|
79
|
+
print()
|
80
|
+
print(f"The profile {pProfile} does not represent an Org")
|
81
|
+
print("This script only works with org accounts.")
|
82
|
+
print()
|
83
|
+
sys.exit(1)
|
84
|
+
##########################
|
85
|
+
ERASE_LINE = "\x1b[2K"
|
86
|
+
##########################
|
87
|
+
|
88
|
+
print(f"We're using the {pAccessRole} role to gain access to the child accounts")
|
89
|
+
print(f"We're targeting the {pTargetRole} role to change its Trust Policy")
|
90
|
+
|
91
|
+
"""
|
92
|
+
1. Collect SSM parameters with the ARNs that should be in the permission
|
93
|
+
2. Create the TrustPolicy in JSON
|
94
|
+
3. Get a listing of all accounts that need to be updated
|
95
|
+
4. Connect to each account, and update the existing trust policy with the new policy
|
96
|
+
"""
|
97
|
+
# 1. Collect parameters with the ARNs that should be in the permission
|
98
|
+
# lock_down_arns_list=[]
|
99
|
+
allowed_arns = []
|
100
|
+
ssm_client = aws_acct.session.client("ssm")
|
101
|
+
param_list = ssm_client.describe_parameters(
|
102
|
+
ParameterFilters=[{"Key": "Name", "Option": "Contains", "Values": ["lock_down_role_arns_list"]}]
|
103
|
+
)["Parameters"]
|
104
|
+
if len(param_list) == 0:
|
105
|
+
print("You need to set the region (-r|--region) to the default region where the SSM parameters are stored.")
|
106
|
+
print("Otherwise, with no *allowed* arns, we would lock everything out from this role.")
|
107
|
+
print("Exiting...")
|
108
|
+
sys.exit(2)
|
109
|
+
for i in param_list:
|
110
|
+
response = param = ssm_client.get_parameter(Name=i["Name"])
|
111
|
+
logging.info(f"Adding {response['Parameter']['Value']} to the list for i: {i['Name']}")
|
112
|
+
allowed_arns.append(response["Parameter"]["Value"])
|
113
|
+
|
114
|
+
# 1.5 Find who is running the script and add their credential as a safety
|
115
|
+
Creds = Inventory_Modules.find_calling_identity(pProfile)
|
116
|
+
if pSafety:
|
117
|
+
allowed_arns.append(Creds["Arn"])
|
118
|
+
# 2. Create the Trust Policy in JSON
|
119
|
+
|
120
|
+
if pLock:
|
121
|
+
if pSafety and pFix:
|
122
|
+
logging.error("Locking down the Trust Policy to *only* the Lambda functions.")
|
123
|
+
elif pFix:
|
124
|
+
logging.error(f"Locking down the Trust Policy to the Lambda functions and {Creds['Arn']}.")
|
125
|
+
else:
|
126
|
+
logging.critical(
|
127
|
+
"While you asked us to lock things down, You didn't use the '+f' parameter, so we're not changing a thing."
|
128
|
+
)
|
129
|
+
Trust_Policy = {
|
130
|
+
"Version": "2012-10-17",
|
131
|
+
"Statement": [
|
132
|
+
{"Sid": "LambdaAccess", "Effect": "Allow", "Principal": {"AWS": allowed_arns}, "Action": "sts:AssumeRole"}
|
133
|
+
],
|
134
|
+
}
|
135
|
+
else:
|
136
|
+
Trust_Policy = {
|
137
|
+
"Version": "2012-10-17",
|
138
|
+
"Statement": [
|
139
|
+
{"Sid": "LambdaAccess", "Effect": "Allow", "Principal": {"AWS": allowed_arns}, "Action": "sts:AssumeRole"},
|
140
|
+
{
|
141
|
+
"Sid": "DevAccess",
|
142
|
+
"Effect": "Allow",
|
143
|
+
"Principal": {"AWS": [f"arn:aws:iam::{aws_acct.MgmtAccount}:root"]},
|
144
|
+
"Action": "sts:AssumeRole",
|
145
|
+
},
|
146
|
+
],
|
147
|
+
}
|
148
|
+
Trust_Policy_json = json.dumps(Trust_Policy)
|
149
|
+
# 3. Get a listing of all accounts that need to be updated and then ...
|
150
|
+
|
151
|
+
|
152
|
+
# 4. Connect to each account, and detach the existing policy, and apply the new policy
|
153
|
+
sts_client = aws_acct.session.client("sts")
|
154
|
+
TrustPoliciesChanged = 0
|
155
|
+
ErroredAccounts = []
|
156
|
+
for acct in aws_acct.ChildAccounts:
|
157
|
+
ConnectionSuccess = False
|
158
|
+
try:
|
159
|
+
role_arn = f"arn:aws:iam::{acct['AccountId']}:role/{pAccessRole}"
|
160
|
+
account_credentials = sts_client.assume_role(RoleArn=role_arn, RoleSessionName="RegistrationScript")[
|
161
|
+
"Credentials"
|
162
|
+
]
|
163
|
+
account_credentials["Account"] = acct["AccountId"]
|
164
|
+
logging.warning(f"Accessed Account {acct['AccountId']} using rolename {pAccessRole}")
|
165
|
+
ConnectionSuccess = True
|
166
|
+
except ClientError as my_Error:
|
167
|
+
logging.error(
|
168
|
+
f"Account {acct['AccountId']}, role {pTargetRole} was unavailable to change, so we couldn't access the role's Trust Policy"
|
169
|
+
)
|
170
|
+
logging.warning(my_Error)
|
171
|
+
ErroredAccounts.append(acct["AccountId"])
|
172
|
+
pass
|
173
|
+
if ConnectionSuccess:
|
174
|
+
try:
|
175
|
+
# detach policy from the role and attach the new policy
|
176
|
+
iam_session = boto3.Session(
|
177
|
+
aws_access_key_id=account_credentials["AccessKeyId"],
|
178
|
+
aws_secret_access_key=account_credentials["SecretAccessKey"],
|
179
|
+
aws_session_token=account_credentials["SessionToken"],
|
180
|
+
)
|
181
|
+
iam_client = iam_session.client("iam")
|
182
|
+
trustpolicyexisting = iam_client.get_role(RoleName=pTargetRole)
|
183
|
+
logging.info(
|
184
|
+
"Found Trust Policy %s in account %s for role %s"
|
185
|
+
% (json.dumps(trustpolicyexisting["Role"]["AssumeRolePolicyDocument"]), acct["AccountId"], pTargetRole)
|
186
|
+
)
|
187
|
+
if pFix:
|
188
|
+
trustpolicyupdate = iam_client.update_assume_role_policy(
|
189
|
+
RoleName=pTargetRole, PolicyDocument=Trust_Policy_json
|
190
|
+
)
|
191
|
+
TrustPoliciesChanged += 1
|
192
|
+
logging.error(f"Updated Trust Policy in Account {acct['AccountId']} for role {pTargetRole}")
|
193
|
+
trustpolicyexisting = iam_client.get_role(RoleName=pTargetRole)
|
194
|
+
logging.info(
|
195
|
+
"Updated Trust Policy %s in account %s for role %s"
|
196
|
+
% (
|
197
|
+
json.dumps(trustpolicyexisting["Role"]["AssumeRolePolicyDocument"]),
|
198
|
+
acct["AccountId"],
|
199
|
+
pTargetRole,
|
200
|
+
)
|
201
|
+
)
|
202
|
+
else:
|
203
|
+
logging.error(f"Account {acct['AccountId']} - no changes made")
|
204
|
+
except ClientError as my_Error:
|
205
|
+
logging.warning(my_Error)
|
206
|
+
pass
|
207
|
+
|
208
|
+
print(ERASE_LINE)
|
209
|
+
print(f"We found {len(aws_acct.ChildAccounts)} accounts under your organization")
|
210
|
+
if pLock and pFix:
|
211
|
+
print(f"We locked {TrustPoliciesChanged} Trust Policies")
|
212
|
+
elif not pLock and pFix:
|
213
|
+
print(f"We unlocked {TrustPoliciesChanged} Trust Policies")
|
214
|
+
else:
|
215
|
+
print(f"We didn't change {TrustPoliciesChanged} Trust Policies")
|
216
|
+
if len(ErroredAccounts) > 0:
|
217
|
+
print(f"We weren't able to access {len(ErroredAccounts)} accounts.")
|
218
|
+
if verbose < 50:
|
219
|
+
print("Here are the accounts that were not updated")
|
220
|
+
for i in ErroredAccounts:
|
221
|
+
print(i)
|
222
|
+
print()
|
223
|
+
print("Thanks for using the tool.")
|
224
|
+
print()
|
@@ -0,0 +1,24 @@
|
|
1
|
+
"""
|
2
|
+
Data models for AWS inventory system.
|
3
|
+
|
4
|
+
This module provides Pydantic-based models for representing AWS accounts,
|
5
|
+
resources, and inventory results with proper validation and serialization.
|
6
|
+
|
7
|
+
Models:
|
8
|
+
- account: AWS account representation and organization structure
|
9
|
+
- resource: Individual AWS resource models with metadata
|
10
|
+
- inventory: Inventory collection results and aggregations
|
11
|
+
"""
|
12
|
+
|
13
|
+
from runbooks.inventory.models.account import AWSAccount, OrganizationAccount
|
14
|
+
from runbooks.inventory.models.inventory import InventoryMetadata, InventoryResult
|
15
|
+
from runbooks.inventory.models.resource import AWSResource, ResourceMetadata
|
16
|
+
|
17
|
+
__all__ = [
|
18
|
+
"AWSAccount",
|
19
|
+
"OrganizationAccount",
|
20
|
+
"AWSResource",
|
21
|
+
"ResourceMetadata",
|
22
|
+
"InventoryResult",
|
23
|
+
"InventoryMetadata",
|
24
|
+
]
|