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,86 @@
|
|
1
|
+
import pytest
|
2
|
+
from common_test_data import CredentialResponseData, get_all_my_functions_test_result_dict
|
3
|
+
from common_test_functions import _amend_create_boto3_session, _amend_make_api_call, _amend_make_api_call_specific
|
4
|
+
from lambda_list_functions import collect_all_my_functions, fix_my_functions
|
5
|
+
|
6
|
+
# CredentialList = test_get_all_credentials()
|
7
|
+
# This is what the response from the tested function should look like, given the parameters supplied
|
8
|
+
|
9
|
+
"""
|
10
|
+
Pass in the parameters as dictionary with operation names and what test results I should get back
|
11
|
+
"""
|
12
|
+
function_parameters = {"CredentialList": CredentialResponseData, "pFragments": ["python3.9", "Metric"], "pverbose": 20}
|
13
|
+
|
14
|
+
|
15
|
+
@pytest.mark.parametrize(
|
16
|
+
"parameters, test_value_dict",
|
17
|
+
[
|
18
|
+
(function_parameters, get_all_my_functions_test_result_dict),
|
19
|
+
# (RootOnlyParams, ),
|
20
|
+
# str(1993),
|
21
|
+
# json.dumps({"SecretString": "my-secret"}),
|
22
|
+
# json.dumps([2, 3, 5, 7, 11, 13, 17, 19]),
|
23
|
+
# KeyError("How dare you touch my secret!"),
|
24
|
+
# ValueError("Oh my goodness you even have the guts to repeat it!!!"),
|
25
|
+
],
|
26
|
+
)
|
27
|
+
def test_all_my_functions(parameters, test_value_dict, mocker):
|
28
|
+
"""
|
29
|
+
Description:
|
30
|
+
@param parameters: the expected parameters into the function
|
31
|
+
@param mocker: the mock object
|
32
|
+
@return:
|
33
|
+
"""
|
34
|
+
CredentialList = parameters["CredentialList"]
|
35
|
+
pFragments = parameters["pFragments"]
|
36
|
+
verbose = parameters["pverbose"]
|
37
|
+
test_data = {"FunctionName": "all_my_functions", "AccountSpecific": True, "RegionSpecific": True}
|
38
|
+
# _amend_create_boto3_session(test_data, mocker)
|
39
|
+
# pass
|
40
|
+
# _amend_make_api_call(test_data, test_value_dict, mocker)
|
41
|
+
_amend_make_api_call_specific(test_data, test_value_dict, mocker)
|
42
|
+
|
43
|
+
# if isinstance(test_value, Exception):
|
44
|
+
# print("Expected Error...")
|
45
|
+
# with pytest.raises(type(test_value)) as error:
|
46
|
+
# all_my_functions(CredentialList, pFragments, verbose)
|
47
|
+
# result = error
|
48
|
+
# else:
|
49
|
+
result = collect_all_my_functions(CredentialList, pFragments, verbose)
|
50
|
+
|
51
|
+
print("Result:", result)
|
52
|
+
print()
|
53
|
+
|
54
|
+
|
55
|
+
"""
|
56
|
+
import boto3
|
57
|
+
from unittest.mock import patch, MagicMock
|
58
|
+
|
59
|
+
|
60
|
+
# Your application code
|
61
|
+
def get_lambda_functions():
|
62
|
+
session = boto3.Session('LZ1', region_name='us-east-2')
|
63
|
+
lambda_client = session.client('lambda')
|
64
|
+
return lambda_client.list_functions()
|
65
|
+
|
66
|
+
|
67
|
+
# Your mock method
|
68
|
+
def mock_list_functions(*args, **kwargs):
|
69
|
+
print("Inside the mocked function!")
|
70
|
+
# Access the session info here (if it's passed or globally accessible)
|
71
|
+
print()
|
72
|
+
creds = args[1]['test'].get_credentials()
|
73
|
+
return {'Functions': [{'FunctionName': 'mocked_function_1'}, {'FunctionName': 'mocked_function_2'}]}
|
74
|
+
|
75
|
+
|
76
|
+
# Your test code
|
77
|
+
def test_get_lambda_functions():
|
78
|
+
with patch.object(boto3.Session, 'client') as mock_client:
|
79
|
+
mock_lambda_client = MagicMock()
|
80
|
+
addl_params = {'test': boto3.Session(), 'account': '11111'}
|
81
|
+
mock_lambda_client.list_functions = mock_list_functions('lambda', addl_params)
|
82
|
+
mock_client.return_value = mock_lambda_client
|
83
|
+
|
84
|
+
result = get_lambda_functions()
|
85
|
+
assert result == {'Functions': [{'FunctionName': 'mocked_function_1'}, {'FunctionName': 'mocked_function_2'}]}
|
86
|
+
"""
|
@@ -0,0 +1,273 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Integration Test Example for AWS Cloud Foundations Inventory Scripts
|
4
|
+
|
5
|
+
This demonstrates how to use moto for AWS service mocking in inventory script testing.
|
6
|
+
This follows the KISS principle - simple, focused tests that validate core functionality.
|
7
|
+
|
8
|
+
**Test Strategy:**
|
9
|
+
- Use moto to mock AWS services (no real AWS calls)
|
10
|
+
- Test individual functions in isolation
|
11
|
+
- Focus on core logic validation
|
12
|
+
- Demonstrate integration testing patterns
|
13
|
+
|
14
|
+
**Markers:**
|
15
|
+
- @pytest.mark.integration: Integration tests using moto
|
16
|
+
- @pytest.mark.aws_service: AWS service-specific tests
|
17
|
+
- @pytest.mark.inventory: Inventory collection tests
|
18
|
+
"""
|
19
|
+
|
20
|
+
import boto3
|
21
|
+
import pytest
|
22
|
+
from moto import mock_aws
|
23
|
+
|
24
|
+
# Test Markers
|
25
|
+
pytestmark = [pytest.mark.integration, pytest.mark.aws_service, pytest.mark.inventory]
|
26
|
+
|
27
|
+
|
28
|
+
class TestEC2IntegrationWithMoto:
|
29
|
+
"""
|
30
|
+
Integration tests for EC2-related inventory scripts using moto.
|
31
|
+
|
32
|
+
These tests demonstrate how to mock AWS EC2 service for testing
|
33
|
+
inventory collection without requiring real AWS credentials.
|
34
|
+
"""
|
35
|
+
|
36
|
+
@mock_aws
|
37
|
+
def test_ec2_describe_instances_with_mocked_service(self):
|
38
|
+
"""
|
39
|
+
Test EC2 instance discovery with mocked EC2 service.
|
40
|
+
|
41
|
+
This test demonstrates:
|
42
|
+
- Moto EC2 service mocking
|
43
|
+
- Instance creation and discovery
|
44
|
+
- Basic inventory collection patterns
|
45
|
+
"""
|
46
|
+
# Create mock EC2 client
|
47
|
+
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
48
|
+
|
49
|
+
# Create test instances
|
50
|
+
response = ec2_client.run_instances(
|
51
|
+
ImageId="ami-12345678",
|
52
|
+
MinCount=2,
|
53
|
+
MaxCount=2,
|
54
|
+
InstanceType="t2.micro",
|
55
|
+
TagSpecifications=[
|
56
|
+
{
|
57
|
+
"ResourceType": "instance",
|
58
|
+
"Tags": [
|
59
|
+
{"Key": "Environment", "Value": "test"},
|
60
|
+
{"Key": "Project", "Value": "inventory-testing"},
|
61
|
+
],
|
62
|
+
}
|
63
|
+
],
|
64
|
+
)
|
65
|
+
|
66
|
+
# Test instance discovery
|
67
|
+
instances_response = ec2_client.describe_instances()
|
68
|
+
reservations = instances_response["Reservations"]
|
69
|
+
|
70
|
+
# Validate results
|
71
|
+
assert len(reservations) == 1, "Should have one reservation"
|
72
|
+
instances = reservations[0]["Instances"]
|
73
|
+
assert len(instances) == 2, "Should have two instances"
|
74
|
+
|
75
|
+
# Validate instance properties
|
76
|
+
for instance in instances:
|
77
|
+
assert instance["InstanceType"] == "t2.micro"
|
78
|
+
assert instance["State"]["Name"] == "running"
|
79
|
+
|
80
|
+
# Check tags
|
81
|
+
tags = {tag["Key"]: tag["Value"] for tag in instance.get("Tags", [])}
|
82
|
+
assert tags["Environment"] == "test"
|
83
|
+
assert tags["Project"] == "inventory-testing"
|
84
|
+
|
85
|
+
@mock_aws
|
86
|
+
def test_vpc_discovery_with_moto(self):
|
87
|
+
"""
|
88
|
+
Test VPC discovery with mocked EC2 service.
|
89
|
+
|
90
|
+
Demonstrates VPC and subnet inventory collection patterns.
|
91
|
+
"""
|
92
|
+
ec2_client = boto3.client("ec2", region_name="us-west-2")
|
93
|
+
|
94
|
+
# Create test VPC
|
95
|
+
vpc_response = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
|
96
|
+
vpc_id = vpc_response["Vpc"]["VpcId"]
|
97
|
+
|
98
|
+
# Tag the VPC
|
99
|
+
ec2_client.create_tags(
|
100
|
+
Resources=[vpc_id], Tags=[{"Key": "Name", "Value": "test-vpc"}, {"Key": "Environment", "Value": "testing"}]
|
101
|
+
)
|
102
|
+
|
103
|
+
# Create test subnet
|
104
|
+
subnet_response = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.1.0/24")
|
105
|
+
subnet_id = subnet_response["Subnet"]["SubnetId"]
|
106
|
+
|
107
|
+
# Test VPC discovery
|
108
|
+
vpcs_response = ec2_client.describe_vpcs()
|
109
|
+
vpcs = vpcs_response["Vpcs"]
|
110
|
+
|
111
|
+
# Validate VPC discovery
|
112
|
+
test_vpc = next((vpc for vpc in vpcs if vpc["VpcId"] == vpc_id), None)
|
113
|
+
assert test_vpc is not None, "Test VPC should be discovered"
|
114
|
+
assert test_vpc["CidrBlock"] == "10.0.0.0/16"
|
115
|
+
|
116
|
+
# Test subnet discovery
|
117
|
+
subnets_response = ec2_client.describe_subnets(Filters=[{"Name": "vpc-id", "Values": [vpc_id]}])
|
118
|
+
subnets = subnets_response["Subnets"]
|
119
|
+
|
120
|
+
assert len(subnets) == 1, "Should find one subnet"
|
121
|
+
assert subnets[0]["SubnetId"] == subnet_id
|
122
|
+
assert subnets[0]["CidrBlock"] == "10.0.1.0/24"
|
123
|
+
|
124
|
+
|
125
|
+
class TestOrganizationsIntegrationWithMoto:
|
126
|
+
"""
|
127
|
+
Integration tests for AWS Organizations functionality using moto.
|
128
|
+
|
129
|
+
These tests demonstrate testing patterns for organization structure
|
130
|
+
discovery and policy management.
|
131
|
+
"""
|
132
|
+
|
133
|
+
@mock_aws
|
134
|
+
def test_organizations_basic_structure(self):
|
135
|
+
"""
|
136
|
+
Test basic AWS Organizations structure discovery.
|
137
|
+
|
138
|
+
Note: moto's Organizations support is limited, but this demonstrates
|
139
|
+
the testing pattern for when more features become available.
|
140
|
+
"""
|
141
|
+
org_client = boto3.client("organizations", region_name="us-east-1")
|
142
|
+
|
143
|
+
try:
|
144
|
+
# Attempt to create organization (may not be fully supported in moto)
|
145
|
+
response = org_client.create_organization(FeatureSet="ALL")
|
146
|
+
|
147
|
+
# Test root discovery
|
148
|
+
roots_response = org_client.list_roots()
|
149
|
+
roots = roots_response["Roots"]
|
150
|
+
|
151
|
+
assert len(roots) >= 1, "Should have at least one root"
|
152
|
+
|
153
|
+
except Exception as e:
|
154
|
+
# Expected - moto has limited Organizations support
|
155
|
+
# This demonstrates the test structure for when support improves
|
156
|
+
pytest.skip(f"Moto Organizations support limited: {e}")
|
157
|
+
|
158
|
+
|
159
|
+
class TestIAMIntegrationWithMoto:
|
160
|
+
"""
|
161
|
+
Integration tests for IAM-related inventory scripts using moto.
|
162
|
+
|
163
|
+
Demonstrates testing patterns for IAM role and policy discovery.
|
164
|
+
"""
|
165
|
+
|
166
|
+
@mock_aws
|
167
|
+
def test_iam_role_discovery(self):
|
168
|
+
"""
|
169
|
+
Test IAM role discovery with mocked IAM service.
|
170
|
+
|
171
|
+
This demonstrates testing patterns for IAM inventory collection.
|
172
|
+
"""
|
173
|
+
iam_client = boto3.client("iam", region_name="us-east-1")
|
174
|
+
|
175
|
+
# Create test role
|
176
|
+
import json
|
177
|
+
|
178
|
+
assume_role_policy = {
|
179
|
+
"Version": "2012-10-17",
|
180
|
+
"Statement": [
|
181
|
+
{"Effect": "Allow", "Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole"}
|
182
|
+
],
|
183
|
+
}
|
184
|
+
|
185
|
+
iam_client.create_role(
|
186
|
+
RoleName="test-inventory-role",
|
187
|
+
AssumeRolePolicyDocument=json.dumps(assume_role_policy),
|
188
|
+
Description="Test role for inventory testing",
|
189
|
+
Tags=[{"Key": "Environment", "Value": "test"}, {"Key": "Purpose", "Value": "inventory-testing"}],
|
190
|
+
)
|
191
|
+
|
192
|
+
# Test role discovery
|
193
|
+
roles_response = iam_client.list_roles()
|
194
|
+
roles = roles_response["Roles"]
|
195
|
+
|
196
|
+
# Find test role
|
197
|
+
test_role = next((role for role in roles if role["RoleName"] == "test-inventory-role"), None)
|
198
|
+
|
199
|
+
assert test_role is not None, "Test role should be discovered"
|
200
|
+
assert test_role["Description"] == "Test role for inventory testing"
|
201
|
+
|
202
|
+
@mock_aws
|
203
|
+
def test_iam_policy_discovery(self):
|
204
|
+
"""
|
205
|
+
Test IAM managed policy discovery patterns.
|
206
|
+
"""
|
207
|
+
iam_client = boto3.client("iam", region_name="us-east-1")
|
208
|
+
|
209
|
+
# Create test policy
|
210
|
+
import json
|
211
|
+
|
212
|
+
policy_document = {
|
213
|
+
"Version": "2012-10-17",
|
214
|
+
"Statement": [{"Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::test-bucket/*"}],
|
215
|
+
}
|
216
|
+
|
217
|
+
policy_response = iam_client.create_policy(
|
218
|
+
PolicyName="test-inventory-policy",
|
219
|
+
PolicyDocument=json.dumps(policy_document),
|
220
|
+
Description="Test policy for inventory testing",
|
221
|
+
)
|
222
|
+
|
223
|
+
policy_arn = policy_response["Policy"]["Arn"]
|
224
|
+
|
225
|
+
# Test policy discovery
|
226
|
+
policies_response = iam_client.list_policies(Scope="Local")
|
227
|
+
policies = policies_response["Policies"]
|
228
|
+
|
229
|
+
# Find test policy
|
230
|
+
test_policy = next((policy for policy in policies if policy["Arn"] == policy_arn), None)
|
231
|
+
|
232
|
+
assert test_policy is not None, "Test policy should be discovered"
|
233
|
+
assert test_policy["PolicyName"] == "test-inventory-policy"
|
234
|
+
|
235
|
+
|
236
|
+
class TestArgumentParsingValidation:
|
237
|
+
"""
|
238
|
+
Unit tests for CLI argument parsing validation.
|
239
|
+
|
240
|
+
These tests validate argument parsing logic without requiring AWS services.
|
241
|
+
"""
|
242
|
+
|
243
|
+
def test_common_argument_patterns(self):
|
244
|
+
"""
|
245
|
+
Test common argument parsing patterns used across inventory scripts.
|
246
|
+
|
247
|
+
This demonstrates testing CLI argument parsing in isolation.
|
248
|
+
"""
|
249
|
+
# Test data for common argument patterns
|
250
|
+
test_cases = [
|
251
|
+
{
|
252
|
+
"args": ["--profile", "test-profile", "--verbose"],
|
253
|
+
"expected_profile": "test-profile",
|
254
|
+
"expected_verbose": True,
|
255
|
+
},
|
256
|
+
{
|
257
|
+
"args": ["--region", "us-west-2", "--output", "json"],
|
258
|
+
"expected_region": "us-west-2",
|
259
|
+
"expected_output": "json",
|
260
|
+
},
|
261
|
+
]
|
262
|
+
|
263
|
+
# This would normally test actual argument parsing functions
|
264
|
+
# For now, demonstrates the testing pattern
|
265
|
+
for case in test_cases:
|
266
|
+
# Mock argument parsing would go here
|
267
|
+
assert isinstance(case["args"], list)
|
268
|
+
assert len(case["args"]) >= 2
|
269
|
+
|
270
|
+
|
271
|
+
if __name__ == "__main__":
|
272
|
+
# Allow running individual test file
|
273
|
+
pytest.main([__file__, "-v"])
|
@@ -0,0 +1,49 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
from botocore import client
|
5
|
+
from common_test_data import cli_provided_parameters1, cli_provided_parameters2, get_all_my_orgs_test_result_dict
|
6
|
+
from common_test_functions import AWSAccount_from_AWSKeyID, _amend_make_api_call, _amend_make_api_call_orig
|
7
|
+
from org_list_accounts import all_my_orgs
|
8
|
+
|
9
|
+
|
10
|
+
@pytest.mark.parametrize(
|
11
|
+
"parameters,test_value_dict",
|
12
|
+
[
|
13
|
+
(cli_provided_parameters1, get_all_my_orgs_test_result_dict),
|
14
|
+
(cli_provided_parameters2, get_all_my_orgs_test_result_dict),
|
15
|
+
# str(1993),
|
16
|
+
# json.dumps({"SecretString": "my-secret"}),
|
17
|
+
# json.dumps([2, 3, 5, 7, 11, 13, 17, 19]),
|
18
|
+
# KeyError("How dare you touch my secret!"),
|
19
|
+
# ValueError("Oh my goodness you even have the guts to repeat it!!!"),
|
20
|
+
],
|
21
|
+
)
|
22
|
+
def test_get_account_data(parameters, test_value_dict, mocker):
|
23
|
+
pProfiles = parameters["pProfiles"]
|
24
|
+
pSkipProfiles = parameters["pSkipProfiles"]
|
25
|
+
pAccountList = parameters["pAccountList"]
|
26
|
+
pTiming = parameters["pTiming"]
|
27
|
+
pRootOnly = parameters["pRootOnly"]
|
28
|
+
pSaveFilename = parameters["pSaveFilename"]
|
29
|
+
pShortform = parameters["pShortform"]
|
30
|
+
pverbose = parameters["pverbose"]
|
31
|
+
test_data = {"FunctionName": "all_my_orgs", "AccountSpecific": True, "RegionSpecific": True}
|
32
|
+
# _amend_make_api_call_orig(pProfiles[0], test_value, mocker)
|
33
|
+
_amend_make_api_call(test_data, test_value_dict, mocker)
|
34
|
+
|
35
|
+
# if isinstance(test_value_dict, Exception):
|
36
|
+
# print("Expected Error...")
|
37
|
+
# with pytest.raises(type(test_value_dict)) as error:
|
38
|
+
# all_my_orgs(pProfiles, pSkipProfiles, pAccountList, pTiming, pRootOnly, pSaveFilename, pShortform, pverbose)
|
39
|
+
# result = error
|
40
|
+
# else:
|
41
|
+
|
42
|
+
# During the run of this test, the profile provided MUST match something real on the user's desktop,
|
43
|
+
# otherwise the script thinks the profile is unmatched and doesn't try to use it.
|
44
|
+
# In further testing, I'll have to figure out how to mock the session call, since it's not being caught by the "make_api_call" function...
|
45
|
+
result = all_my_orgs(
|
46
|
+
pProfiles, pSkipProfiles, pAccountList, pTiming, pRootOnly, pSaveFilename, pShortform, pverbose
|
47
|
+
)
|
48
|
+
|
49
|
+
print("Result:", result)
|
@@ -0,0 +1,173 @@
|
|
1
|
+
import logging
|
2
|
+
import sys
|
3
|
+
from datetime import datetime
|
4
|
+
from os.path import split
|
5
|
+
from queue import Queue
|
6
|
+
from threading import Thread
|
7
|
+
from time import time
|
8
|
+
|
9
|
+
import boto3
|
10
|
+
from ArgumentsClass import CommonArguments
|
11
|
+
from colorama import Fore, Style, init
|
12
|
+
from tqdm.auto import tqdm, trange
|
13
|
+
|
14
|
+
__version__ = "2024.04.24"
|
15
|
+
|
16
|
+
from tqdm import trange
|
17
|
+
|
18
|
+
init()
|
19
|
+
|
20
|
+
|
21
|
+
##################
|
22
|
+
# Functions
|
23
|
+
##################
|
24
|
+
|
25
|
+
|
26
|
+
def parse_args(f_arguments):
|
27
|
+
"""
|
28
|
+
Description: Parses the arguments passed into the script
|
29
|
+
@param f_arguments: args represents the list of arguments passed in
|
30
|
+
@return: returns an object namespace that contains the individualized parameters passed in
|
31
|
+
"""
|
32
|
+
script_path, script_name = split(sys.argv[0])
|
33
|
+
parser = CommonArguments()
|
34
|
+
parser.my_parser.description = "We're going to find all API actions within the available AWS services."
|
35
|
+
parser.timing()
|
36
|
+
parser.save_to_file()
|
37
|
+
parser.verbosity()
|
38
|
+
parser.version(__version__)
|
39
|
+
local = parser.my_parser.add_argument_group(script_name, "Parameters specific to this script")
|
40
|
+
# local.add_argument(
|
41
|
+
# "-s", "--status",
|
42
|
+
# dest="pStatus",
|
43
|
+
# choices=['running', 'stopped'],
|
44
|
+
# type=str,
|
45
|
+
# default=None,
|
46
|
+
# help="Whether you want to limit the instances returned to either 'running', 'stopped'. Default is both")
|
47
|
+
return parser.my_parser.parse_args(f_arguments)
|
48
|
+
|
49
|
+
|
50
|
+
def random_string(stringLength=10):
|
51
|
+
"""
|
52
|
+
Description: Generates a random string
|
53
|
+
@param stringLength: to determine the length of the random number generated
|
54
|
+
@return: returns a random string of characters of length "stringlength"
|
55
|
+
"""
|
56
|
+
import random
|
57
|
+
import string
|
58
|
+
|
59
|
+
# Generate a random string of fixed length
|
60
|
+
letters = string.ascii_lowercase
|
61
|
+
randomstring = "".join(random.choice(letters) for _ in range(stringLength))
|
62
|
+
return randomstring
|
63
|
+
|
64
|
+
|
65
|
+
def get_aws_actions():
|
66
|
+
"""
|
67
|
+
Description: Gets all the actions for all the services in AWS
|
68
|
+
@return: list of actions
|
69
|
+
"""
|
70
|
+
|
71
|
+
class AWSActionThread(Thread):
|
72
|
+
def __init__(self, queue):
|
73
|
+
Thread.__init__(self)
|
74
|
+
self.queue = queue
|
75
|
+
|
76
|
+
def run(self):
|
77
|
+
while True:
|
78
|
+
# existing code to get actions for a service
|
79
|
+
c_service = self.queue.get()
|
80
|
+
client = boto3.client(c_service)
|
81
|
+
try:
|
82
|
+
list_of_operations = client.meta.method_to_api_mapping.keys()
|
83
|
+
# print(f"{ERASE_LINE}Checking actions for {c_service}", end='\r', flush=True)
|
84
|
+
for operation in tqdm(list_of_operations, desc=f"actions for {c_service}", leave=False):
|
85
|
+
action_list.append({"Service": c_service, "Operation": operation})
|
86
|
+
except AttributeError as myError:
|
87
|
+
print(myError)
|
88
|
+
action_list.append({"Service": c_service, "Operation": None})
|
89
|
+
pass
|
90
|
+
finally:
|
91
|
+
self.queue.task_done()
|
92
|
+
|
93
|
+
checkqueue = Queue()
|
94
|
+
action_list = []
|
95
|
+
workerthreads = 10
|
96
|
+
|
97
|
+
for x in trange(
|
98
|
+
workerthreads,
|
99
|
+
desc=f"Creating {workerthreads} worker threads",
|
100
|
+
leave=False,
|
101
|
+
# , position=0
|
102
|
+
):
|
103
|
+
worker = AWSActionThread(checkqueue)
|
104
|
+
worker.daemon = True
|
105
|
+
worker.start()
|
106
|
+
|
107
|
+
# Create a thread for every service.
|
108
|
+
print(f"Capturing all available AWS Services...")
|
109
|
+
for service in tqdm(
|
110
|
+
list_of_services,
|
111
|
+
desc=f"Checking actions for each service",
|
112
|
+
# , position=0
|
113
|
+
):
|
114
|
+
# for service in list_of_services:
|
115
|
+
checkqueue.put(service)
|
116
|
+
checkqueue.join()
|
117
|
+
|
118
|
+
return action_list
|
119
|
+
|
120
|
+
|
121
|
+
def save_file(file_to_save_to: str = None, input_data: list = None):
|
122
|
+
"""
|
123
|
+
Description: Saves the data to a file
|
124
|
+
@param file_to_save_to:
|
125
|
+
@param input_data:
|
126
|
+
@return: None
|
127
|
+
"""
|
128
|
+
if input_data is None:
|
129
|
+
print(f"Input data cannot be none. Exiting...")
|
130
|
+
raise SystemExit(1)
|
131
|
+
elif len(input_data) == 0:
|
132
|
+
print(f"No data to save. Exiting...")
|
133
|
+
raise SystemExit(1)
|
134
|
+
|
135
|
+
if file_to_save_to is None:
|
136
|
+
print(f"No filename provided to save data.\nUsing a randomized name.")
|
137
|
+
file_to_save_to = random_string(15) + ".txt"
|
138
|
+
else:
|
139
|
+
logging.info(f"Saving data to {file_to_save_to}")
|
140
|
+
with open(file_to_save_to, "w", encoding="utf-8") as f:
|
141
|
+
for item in input_data:
|
142
|
+
f.write(f"{item['Service']}:{item['Operation']}\n")
|
143
|
+
return file_to_save_to
|
144
|
+
|
145
|
+
|
146
|
+
##################
|
147
|
+
# Main
|
148
|
+
##################
|
149
|
+
ERASE_LINE = "\x1b[2K"
|
150
|
+
begin_time = time()
|
151
|
+
|
152
|
+
if __name__ == "__main__":
|
153
|
+
arguments = parse_args(sys.argv[1:])
|
154
|
+
pTiming = arguments.Time
|
155
|
+
file_to_save = arguments.Filename
|
156
|
+
verbose = arguments.loglevel
|
157
|
+
logging.basicConfig(level=verbose, format="[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s")
|
158
|
+
logging.getLogger("boto3").setLevel(logging.CRITICAL)
|
159
|
+
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
160
|
+
logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
|
161
|
+
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
162
|
+
|
163
|
+
list_of_services = boto3.Session().get_available_services()
|
164
|
+
all_actions = get_aws_actions()
|
165
|
+
if pTiming:
|
166
|
+
print(ERASE_LINE)
|
167
|
+
print(f"{Fore.GREEN}This script took {time() - begin_time:.2f} seconds{Fore.RESET}")
|
168
|
+
print(ERASE_LINE)
|
169
|
+
print(f"Found {len(all_actions)} actions across {len(list_of_services)} services")
|
170
|
+
filename = save_file(file_to_save, all_actions)
|
171
|
+
print(f"Saved to {filename}")
|
172
|
+
print("Thank you for using this script")
|
173
|
+
print()
|