runbooks 0.2.5__py3-none-any.whl → 0.7.0__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 +2 -0
- jupyter-agent/.env.template +2 -0
- jupyter-agent/.gitattributes +35 -0
- jupyter-agent/.gradio/certificate.pem +31 -0
- jupyter-agent/README.md +16 -0
- jupyter-agent/__main__.log +8 -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/tmp/4ojbs8a02ir/jupyter-agent.ipynb +68 -0
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +91 -0
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +57 -0
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +53 -0
- jupyter-agent/tmp/jupyter-agent.ipynb +27 -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/README.md +337 -0
- runbooks/finops/__init__.py +86 -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/FAILED_SCRIPTS_TROUBLESHOOTING.md +619 -0
- runbooks/inventory/Inventory_Modules.py +6130 -0
- runbooks/inventory/LandingZone/delete_lz.py +1075 -0
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +738 -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/aws_organization.png +0 -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/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 +1004 -0
- runbooks/organizations/__init__.py +12 -0
- runbooks/organizations/manager.py +374 -0
- runbooks/security/README.md +447 -0
- runbooks/security/__init__.py +71 -0
- runbooks/{security_baseline → security}/checklist/alternate_contacts.py +8 -1
- runbooks/{security_baseline → security}/checklist/bucket_public_access.py +4 -1
- runbooks/{security_baseline → security}/checklist/cloudwatch_alarm_configuration.py +9 -2
- runbooks/{security_baseline → security}/checklist/guardduty_enabled.py +9 -2
- runbooks/{security_baseline → security}/checklist/multi_region_instance_usage.py +5 -1
- runbooks/{security_baseline → security}/checklist/root_access_key.py +6 -1
- runbooks/{security_baseline → security}/config-origin.json +1 -1
- runbooks/{security_baseline → security}/config.json +1 -1
- runbooks/{security_baseline → security}/permission.json +1 -1
- runbooks/{security_baseline → security}/report_generator.py +10 -2
- runbooks/{security_baseline → security}/report_template_en.html +7 -7
- runbooks/{security_baseline → security}/report_template_jp.html +7 -7
- runbooks/{security_baseline → security}/report_template_kr.html +12 -12
- runbooks/{security_baseline → security}/report_template_vn.html +7 -7
- runbooks/{security_baseline → security}/run_script.py +8 -2
- runbooks/{security_baseline → security}/security_baseline_tester.py +12 -4
- runbooks/{security_baseline → security}/utils/common.py +5 -1
- runbooks/utils/__init__.py +204 -0
- runbooks-0.7.0.dist-info/METADATA +375 -0
- runbooks-0.7.0.dist-info/RECORD +249 -0
- {runbooks-0.2.5.dist-info → runbooks-0.7.0.dist-info}/WHEEL +1 -1
- runbooks-0.7.0.dist-info/entry_points.txt +7 -0
- runbooks-0.7.0.dist-info/licenses/LICENSE +201 -0
- runbooks-0.7.0.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.5.dist-info/METADATA +0 -439
- runbooks-0.2.5.dist-info/RECORD +0 -61
- runbooks-0.2.5.dist-info/entry_points.txt +0 -3
- runbooks-0.2.5.dist-info/top_level.txt +0 -1
- /runbooks/{security_baseline/__init__.py → inventory/tests/script_test_data.py} +0 -0
- /runbooks/{security_baseline → security}/checklist/__init__.py +0 -0
- /runbooks/{security_baseline → security}/checklist/account_level_bucket_public_access.py +0 -0
- /runbooks/{security_baseline → security}/checklist/direct_attached_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_password_policy.py +0 -0
- /runbooks/{security_baseline → security}/checklist/iam_user_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/multi_region_trail.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_mfa.py +0 -0
- /runbooks/{security_baseline → security}/checklist/root_usage.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trail_enabled.py +0 -0
- /runbooks/{security_baseline → security}/checklist/trusted_advisor.py +0 -0
- /runbooks/{security_baseline → security}/utils/__init__.py +0 -0
- /runbooks/{security_baseline → security}/utils/enums.py +0 -0
- /runbooks/{security_baseline → security}/utils/language.py +0 -0
- /runbooks/{security_baseline → security}/utils/level_const.py +0 -0
- /runbooks/{security_baseline → security}/utils/permission_list.py +0 -0
@@ -0,0 +1,354 @@
|
|
1
|
+
"""
|
2
|
+
Tests for CFAT reporting functionality.
|
3
|
+
|
4
|
+
Tests report generation, export formats, and project management
|
5
|
+
integrations to ensure reliable multi-format output generation.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import csv
|
9
|
+
import json
|
10
|
+
import os
|
11
|
+
import tempfile
|
12
|
+
from io import StringIO
|
13
|
+
from pathlib import Path
|
14
|
+
|
15
|
+
import pytest
|
16
|
+
|
17
|
+
from runbooks.cfat.models import AssessmentReport, CheckStatus, Severity
|
18
|
+
from runbooks.cfat.reporting.exporters import (
|
19
|
+
AsanaExporter,
|
20
|
+
JiraExporter,
|
21
|
+
ServiceNowExporter,
|
22
|
+
get_exporter,
|
23
|
+
list_available_exporters,
|
24
|
+
)
|
25
|
+
from runbooks.cfat.tests import create_sample_assessment_report
|
26
|
+
|
27
|
+
|
28
|
+
@pytest.mark.reporting
|
29
|
+
class TestReportGeneration:
|
30
|
+
"""Test assessment report generation in various formats."""
|
31
|
+
|
32
|
+
def setup_method(self):
|
33
|
+
"""Set up test environment."""
|
34
|
+
self.report = create_sample_assessment_report(num_results=10)
|
35
|
+
self.temp_dir = tempfile.mkdtemp()
|
36
|
+
|
37
|
+
def teardown_method(self):
|
38
|
+
"""Clean up test environment."""
|
39
|
+
import shutil
|
40
|
+
|
41
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
42
|
+
|
43
|
+
def test_json_export(self):
|
44
|
+
"""Test JSON report generation."""
|
45
|
+
json_file = os.path.join(self.temp_dir, "test_report.json")
|
46
|
+
|
47
|
+
# Generate JSON report
|
48
|
+
self.report.to_json(json_file)
|
49
|
+
|
50
|
+
# Verify file was created
|
51
|
+
assert os.path.exists(json_file)
|
52
|
+
|
53
|
+
# Verify JSON content
|
54
|
+
with open(json_file, "r") as f:
|
55
|
+
data = json.load(f)
|
56
|
+
|
57
|
+
assert data["account_id"] == self.report.account_id
|
58
|
+
assert data["region"] == self.report.region
|
59
|
+
assert len(data["results"]) == len(self.report.results)
|
60
|
+
assert "summary" in data
|
61
|
+
assert data["summary"]["total_checks"] == self.report.summary.total_checks
|
62
|
+
|
63
|
+
def test_csv_export(self):
|
64
|
+
"""Test CSV report generation."""
|
65
|
+
csv_file = os.path.join(self.temp_dir, "test_report.csv")
|
66
|
+
|
67
|
+
# Generate CSV report
|
68
|
+
self.report.to_csv(csv_file)
|
69
|
+
|
70
|
+
# Verify file was created
|
71
|
+
assert os.path.exists(csv_file)
|
72
|
+
|
73
|
+
# Verify CSV content
|
74
|
+
with open(csv_file, "r") as f:
|
75
|
+
reader = csv.DictReader(f)
|
76
|
+
rows = list(reader)
|
77
|
+
|
78
|
+
assert len(rows) == len(self.report.results)
|
79
|
+
|
80
|
+
# Check required columns
|
81
|
+
expected_columns = ["finding_id", "check_name", "category", "status", "severity", "message", "execution_time"]
|
82
|
+
for col in expected_columns:
|
83
|
+
assert col in reader.fieldnames
|
84
|
+
|
85
|
+
# Verify data integrity
|
86
|
+
first_row = rows[0]
|
87
|
+
first_result = self.report.results[0]
|
88
|
+
assert first_row["finding_id"] == first_result.finding_id
|
89
|
+
assert first_row["check_name"] == first_result.check_name
|
90
|
+
assert first_row["status"] == first_result.status.value
|
91
|
+
|
92
|
+
def test_html_export(self):
|
93
|
+
"""Test HTML report generation."""
|
94
|
+
html_file = os.path.join(self.temp_dir, "test_report.html")
|
95
|
+
|
96
|
+
# Generate HTML report
|
97
|
+
self.report.to_html(html_file)
|
98
|
+
|
99
|
+
# Verify file was created
|
100
|
+
assert os.path.exists(html_file)
|
101
|
+
|
102
|
+
# Verify HTML content
|
103
|
+
with open(html_file, "r") as f:
|
104
|
+
html_content = f.read()
|
105
|
+
|
106
|
+
assert "<!DOCTYPE html>" in html_content
|
107
|
+
assert "Cloud Foundations Assessment Report" in html_content
|
108
|
+
assert self.report.account_id in html_content
|
109
|
+
assert str(self.report.summary.total_checks) in html_content
|
110
|
+
|
111
|
+
def test_markdown_export(self):
|
112
|
+
"""Test Markdown report generation."""
|
113
|
+
md_file = os.path.join(self.temp_dir, "test_report.md")
|
114
|
+
|
115
|
+
# Generate Markdown report
|
116
|
+
self.report.to_markdown(md_file)
|
117
|
+
|
118
|
+
# Verify file was created
|
119
|
+
assert os.path.exists(md_file)
|
120
|
+
|
121
|
+
# Verify Markdown content
|
122
|
+
with open(md_file, "r") as f:
|
123
|
+
md_content = f.read()
|
124
|
+
|
125
|
+
assert "# Cloud Foundations Assessment Report" in md_content
|
126
|
+
assert f"**Account:** {self.report.account_id}" in md_content
|
127
|
+
assert f"**Total Checks:** {self.report.summary.total_checks}" in md_content
|
128
|
+
assert f"**Compliance Score:** {self.report.summary.compliance_score}" in md_content
|
129
|
+
|
130
|
+
def test_export_with_failed_results(self):
|
131
|
+
"""Test export handling when there are failed results."""
|
132
|
+
# Create report with failed results
|
133
|
+
from runbooks.cfat.tests import create_sample_assessment_result
|
134
|
+
|
135
|
+
failed_result = create_sample_assessment_result(
|
136
|
+
finding_id="FAIL-001",
|
137
|
+
status=CheckStatus.FAIL,
|
138
|
+
severity=Severity.CRITICAL,
|
139
|
+
message="Critical security issue",
|
140
|
+
check_name="security_check",
|
141
|
+
)
|
142
|
+
|
143
|
+
# Add to report
|
144
|
+
self.report.results.append(failed_result)
|
145
|
+
|
146
|
+
# Test markdown export shows critical findings
|
147
|
+
md_file = os.path.join(self.temp_dir, "failed_report.md")
|
148
|
+
self.report.to_markdown(md_file)
|
149
|
+
|
150
|
+
with open(md_file, "r") as f:
|
151
|
+
content = f.read()
|
152
|
+
|
153
|
+
assert "🚨 Critical Findings" in content
|
154
|
+
assert "FAIL-001" in content
|
155
|
+
|
156
|
+
|
157
|
+
@pytest.mark.reporting
|
158
|
+
class TestProjectManagementExporters:
|
159
|
+
"""Test project management tool exporters."""
|
160
|
+
|
161
|
+
def setup_method(self):
|
162
|
+
"""Set up test environment."""
|
163
|
+
# Create report with some failed results for export
|
164
|
+
self.report = create_sample_assessment_report(num_results=5)
|
165
|
+
|
166
|
+
# Ensure we have some failed results
|
167
|
+
from runbooks.cfat.tests import create_sample_assessment_result
|
168
|
+
|
169
|
+
failed_result = create_sample_assessment_result(
|
170
|
+
finding_id="CRITICAL-001",
|
171
|
+
status=CheckStatus.FAIL,
|
172
|
+
severity=Severity.CRITICAL,
|
173
|
+
message="Critical security vulnerability requires immediate attention",
|
174
|
+
check_name="security_critical_check",
|
175
|
+
)
|
176
|
+
failed_result.recommendations = ["Enable MFA for root account", "Review IAM policies for least privilege"]
|
177
|
+
self.report.results.append(failed_result)
|
178
|
+
|
179
|
+
self.temp_dir = tempfile.mkdtemp()
|
180
|
+
|
181
|
+
def teardown_method(self):
|
182
|
+
"""Clean up test environment."""
|
183
|
+
import shutil
|
184
|
+
|
185
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
186
|
+
|
187
|
+
def test_jira_exporter(self):
|
188
|
+
"""Test Jira CSV export functionality."""
|
189
|
+
exporter = JiraExporter()
|
190
|
+
assert exporter.get_exporter_name() == "jira"
|
191
|
+
|
192
|
+
# Test export to file
|
193
|
+
jira_file = os.path.join(self.temp_dir, "jira_export.csv")
|
194
|
+
csv_content = exporter.export(self.report, jira_file)
|
195
|
+
|
196
|
+
# Verify file was created
|
197
|
+
assert os.path.exists(jira_file)
|
198
|
+
|
199
|
+
# Verify CSV content structure
|
200
|
+
assert isinstance(csv_content, str)
|
201
|
+
assert "Summary,Issue Type,Priority" in csv_content
|
202
|
+
assert "[CFAT]" in csv_content
|
203
|
+
|
204
|
+
# Parse CSV to verify structure
|
205
|
+
csv_reader = csv.DictReader(StringIO(csv_content))
|
206
|
+
rows = list(csv_reader)
|
207
|
+
|
208
|
+
# Should have at least one row for failed results
|
209
|
+
failed_results = self.report.get_failed_results()
|
210
|
+
assert len(rows) >= len(failed_results)
|
211
|
+
|
212
|
+
# Check required Jira columns
|
213
|
+
expected_columns = ["Summary", "Issue Type", "Priority", "Description", "Labels"]
|
214
|
+
for col in expected_columns:
|
215
|
+
assert col in csv_reader.fieldnames
|
216
|
+
|
217
|
+
def test_asana_exporter(self):
|
218
|
+
"""Test Asana CSV export functionality."""
|
219
|
+
exporter = AsanaExporter()
|
220
|
+
assert exporter.get_exporter_name() == "asana"
|
221
|
+
|
222
|
+
# Test export
|
223
|
+
asana_file = os.path.join(self.temp_dir, "asana_export.csv")
|
224
|
+
csv_content = exporter.export(self.report, asana_file)
|
225
|
+
|
226
|
+
# Verify file and content
|
227
|
+
assert os.path.exists(asana_file)
|
228
|
+
assert isinstance(csv_content, str)
|
229
|
+
assert "Name,Notes,Priority" in csv_content
|
230
|
+
|
231
|
+
# Parse and verify structure
|
232
|
+
csv_reader = csv.DictReader(StringIO(csv_content))
|
233
|
+
rows = list(csv_reader)
|
234
|
+
assert len(rows) > 0
|
235
|
+
|
236
|
+
# Check Asana-specific columns
|
237
|
+
expected_columns = ["Name", "Notes", "Priority", "Tags", "Due Date"]
|
238
|
+
for col in expected_columns:
|
239
|
+
assert col in csv_reader.fieldnames
|
240
|
+
|
241
|
+
def test_servicenow_exporter(self):
|
242
|
+
"""Test ServiceNow JSON export functionality."""
|
243
|
+
exporter = ServiceNowExporter()
|
244
|
+
assert exporter.get_exporter_name() == "servicenow"
|
245
|
+
|
246
|
+
# Test export
|
247
|
+
snow_file = os.path.join(self.temp_dir, "servicenow_export.json")
|
248
|
+
json_content = exporter.export(self.report, snow_file)
|
249
|
+
|
250
|
+
# Verify file and content
|
251
|
+
assert os.path.exists(snow_file)
|
252
|
+
assert isinstance(json_content, str)
|
253
|
+
|
254
|
+
# Parse JSON to verify structure
|
255
|
+
data = json.loads(json_content)
|
256
|
+
assert "incidents" in data
|
257
|
+
assert "metadata" in data
|
258
|
+
|
259
|
+
# Verify incident structure
|
260
|
+
incidents = data["incidents"]
|
261
|
+
assert len(incidents) > 0
|
262
|
+
|
263
|
+
incident = incidents[0]
|
264
|
+
required_fields = ["short_description", "description", "category", "priority", "impact", "urgency"]
|
265
|
+
for field in required_fields:
|
266
|
+
assert field in incident
|
267
|
+
|
268
|
+
# Verify metadata
|
269
|
+
metadata = data["metadata"]
|
270
|
+
assert metadata["account_id"] == self.report.account_id
|
271
|
+
assert "assessment_date" in metadata
|
272
|
+
|
273
|
+
def test_severity_mapping(self):
|
274
|
+
"""Test severity to priority mapping in exporters."""
|
275
|
+
jira_exporter = JiraExporter()
|
276
|
+
|
277
|
+
# Test severity mapping
|
278
|
+
assert jira_exporter._map_severity_to_priority(Severity.CRITICAL) == "Critical"
|
279
|
+
assert jira_exporter._map_severity_to_priority(Severity.WARNING) == "High"
|
280
|
+
assert jira_exporter._map_severity_to_priority(Severity.INFO) == "Medium"
|
281
|
+
|
282
|
+
def test_exporter_registry(self):
|
283
|
+
"""Test exporter registry functionality."""
|
284
|
+
# Test getting exporters by name
|
285
|
+
jira_exporter = get_exporter("jira")
|
286
|
+
assert isinstance(jira_exporter, JiraExporter)
|
287
|
+
|
288
|
+
asana_exporter = get_exporter("asana")
|
289
|
+
assert isinstance(asana_exporter, AsanaExporter)
|
290
|
+
|
291
|
+
snow_exporter = get_exporter("servicenow")
|
292
|
+
assert isinstance(snow_exporter, ServiceNowExporter)
|
293
|
+
|
294
|
+
# Test invalid exporter
|
295
|
+
invalid_exporter = get_exporter("invalid")
|
296
|
+
assert invalid_exporter is None
|
297
|
+
|
298
|
+
# Test listing available exporters
|
299
|
+
available = list_available_exporters()
|
300
|
+
assert "jira" in available
|
301
|
+
assert "asana" in available
|
302
|
+
assert "servicenow" in available
|
303
|
+
|
304
|
+
def test_export_with_no_failed_results(self):
|
305
|
+
"""Test exporters handle reports with no failed results gracefully."""
|
306
|
+
# Create report with only passed results
|
307
|
+
from runbooks.cfat.tests import create_sample_assessment_result
|
308
|
+
|
309
|
+
passed_report = create_sample_assessment_report(num_results=0)
|
310
|
+
passed_result = create_sample_assessment_result(status=CheckStatus.PASS, severity=Severity.INFO)
|
311
|
+
passed_report.results = [passed_result]
|
312
|
+
|
313
|
+
# Test Jira exporter
|
314
|
+
jira_exporter = JiraExporter()
|
315
|
+
jira_file = os.path.join(self.temp_dir, "jira_empty.csv")
|
316
|
+
csv_content = jira_exporter.export(passed_report, jira_file)
|
317
|
+
|
318
|
+
# Should still create file with headers but no data rows
|
319
|
+
assert os.path.exists(jira_file)
|
320
|
+
csv_reader = csv.DictReader(StringIO(csv_content))
|
321
|
+
rows = list(csv_reader)
|
322
|
+
assert len(rows) == 0 # No failed results to export
|
323
|
+
|
324
|
+
def test_large_export(self):
|
325
|
+
"""Test exporters handle large numbers of findings."""
|
326
|
+
# Create report with many failed results
|
327
|
+
large_report = create_sample_assessment_report(num_results=0)
|
328
|
+
|
329
|
+
from runbooks.cfat.tests import create_sample_assessment_result
|
330
|
+
|
331
|
+
for i in range(100):
|
332
|
+
failed_result = create_sample_assessment_result(
|
333
|
+
finding_id=f"LARGE-{i:03d}",
|
334
|
+
status=CheckStatus.FAIL,
|
335
|
+
severity=Severity.WARNING,
|
336
|
+
message=f"Large test finding {i}",
|
337
|
+
check_name=f"large_check_{i}",
|
338
|
+
)
|
339
|
+
large_report.results.append(failed_result)
|
340
|
+
|
341
|
+
# Test Jira export handles large data
|
342
|
+
jira_exporter = JiraExporter()
|
343
|
+
jira_file = os.path.join(self.temp_dir, "jira_large.csv")
|
344
|
+
csv_content = jira_exporter.export(large_report, jira_file)
|
345
|
+
|
346
|
+
# Verify all results were exported
|
347
|
+
csv_reader = csv.DictReader(StringIO(csv_content))
|
348
|
+
rows = list(csv_reader)
|
349
|
+
assert len(rows) == 100
|
350
|
+
|
351
|
+
# Verify file size is reasonable
|
352
|
+
file_size = os.path.getsize(jira_file)
|
353
|
+
assert file_size > 1000 # Should be substantial but not enormous
|
354
|
+
assert file_size < 1000000 # Should be less than 1MB
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ES2020",
|
4
|
+
"module": "ES2020",
|
5
|
+
"strict": true,
|
6
|
+
"preserveConstEnums": true,
|
7
|
+
"sourceMap": false,
|
8
|
+
"outDir": "./build",
|
9
|
+
"moduleResolution": "node",
|
10
|
+
"esModuleInterop": true,
|
11
|
+
"skipLibCheck": true,
|
12
|
+
"forceConsistentCasingInFileNames": true
|
13
|
+
},
|
14
|
+
"include": ["src/**/*", "./**/*.ts"],
|
15
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
16
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
//import * as path from 'path'
|
2
|
+
const path = require('path');
|
3
|
+
const nodeExternals = require('webpack-node-externals');
|
4
|
+
module.exports = {
|
5
|
+
target: 'node',
|
6
|
+
entry: './build/app.js',
|
7
|
+
externals: [nodeExternals()],
|
8
|
+
mode: 'production',
|
9
|
+
devtool: 'inline-source-map',
|
10
|
+
watch: false,
|
11
|
+
output: {
|
12
|
+
filename: 'cfat.js',
|
13
|
+
path: path.resolve(__dirname, 'dist'),
|
14
|
+
},
|
15
|
+
resolve: {
|
16
|
+
extensions: ['.ts', '.js'],
|
17
|
+
},
|
18
|
+
module: {
|
19
|
+
rules: [
|
20
|
+
{
|
21
|
+
test: /\.ts$/,
|
22
|
+
use: 'ts-loader',
|
23
|
+
exclude: /node_modules/,
|
24
|
+
},
|
25
|
+
],
|
26
|
+
},
|
27
|
+
};
|
runbooks/config.py
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
"""
|
2
|
+
Configuration management for CloudOps Runbooks.
|
3
|
+
|
4
|
+
This module handles loading, saving, and managing configuration settings
|
5
|
+
for the runbooks package, including AWS profiles, regions, and assessment settings.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Dict, Optional
|
11
|
+
|
12
|
+
import yaml
|
13
|
+
from loguru import logger
|
14
|
+
|
15
|
+
try:
|
16
|
+
from pydantic import BaseModel, Field
|
17
|
+
|
18
|
+
_HAS_PYDANTIC = True
|
19
|
+
except ImportError:
|
20
|
+
_HAS_PYDANTIC = False
|
21
|
+
|
22
|
+
# Fallback BaseModel
|
23
|
+
class BaseModel:
|
24
|
+
def __init__(self, **kwargs):
|
25
|
+
for key, value in kwargs.items():
|
26
|
+
setattr(self, key, value)
|
27
|
+
|
28
|
+
def model_dump(self, exclude_none=True):
|
29
|
+
result = {}
|
30
|
+
for key, value in self.__dict__.items():
|
31
|
+
if not exclude_none or value is not None:
|
32
|
+
result[key] = value
|
33
|
+
return result
|
34
|
+
|
35
|
+
def Field(default=None, default_factory=None, description=""):
|
36
|
+
if default_factory:
|
37
|
+
return default_factory()
|
38
|
+
return default
|
39
|
+
|
40
|
+
|
41
|
+
from runbooks.utils import ensure_directory
|
42
|
+
|
43
|
+
|
44
|
+
class RunbooksConfig(BaseModel):
|
45
|
+
"""Configuration model for CloudOps Runbooks."""
|
46
|
+
|
47
|
+
# AWS Configuration
|
48
|
+
aws_profile: str = Field(default="default", description="Default AWS profile")
|
49
|
+
aws_region: Optional[str] = Field(default=None, description="Default AWS region")
|
50
|
+
|
51
|
+
# Assessment Configuration
|
52
|
+
cfat_checks: Dict[str, bool] = Field(
|
53
|
+
default_factory=lambda: {
|
54
|
+
"cloudtrail": True,
|
55
|
+
"config": True,
|
56
|
+
"iam": True,
|
57
|
+
"vpc": True,
|
58
|
+
"ec2": True,
|
59
|
+
"organizations": True,
|
60
|
+
"control_tower": True,
|
61
|
+
"kms": True,
|
62
|
+
},
|
63
|
+
description="CFAT checks to run by default",
|
64
|
+
)
|
65
|
+
cfat_severity_threshold: str = Field(default="WARNING", description="Minimum severity for CFAT reports")
|
66
|
+
|
67
|
+
# Inventory Configuration
|
68
|
+
inventory_parallel: bool = Field(default=True, description="Run inventory collection in parallel")
|
69
|
+
inventory_cache_ttl: int = Field(default=3600, description="Inventory cache TTL in seconds")
|
70
|
+
inventory_include_costs: bool = Field(default=False, description="Include cost data in inventory")
|
71
|
+
|
72
|
+
# Output Configuration
|
73
|
+
default_output_format: str = Field(default="console", description="Default output format")
|
74
|
+
reports_directory: str = Field(default="reports", description="Directory for generated reports")
|
75
|
+
|
76
|
+
# Logging Configuration
|
77
|
+
log_level: str = Field(default="INFO", description="Default log level")
|
78
|
+
log_file: Optional[str] = Field(default=None, description="Log file path")
|
79
|
+
|
80
|
+
class Config:
|
81
|
+
"""Pydantic config."""
|
82
|
+
|
83
|
+
extra = "allow"
|
84
|
+
|
85
|
+
|
86
|
+
def get_default_config_path() -> Path:
|
87
|
+
"""
|
88
|
+
Get the default configuration file path.
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
Path to the default config file
|
92
|
+
"""
|
93
|
+
config_dir = Path.home() / ".runbooks"
|
94
|
+
ensure_directory(config_dir)
|
95
|
+
return config_dir / "config.yaml"
|
96
|
+
|
97
|
+
|
98
|
+
def load_config(config_path: Optional[Path] = None) -> RunbooksConfig:
|
99
|
+
"""
|
100
|
+
Load configuration from file or return default configuration.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
config_path: Path to configuration file
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
Loaded configuration object
|
107
|
+
"""
|
108
|
+
if config_path is None:
|
109
|
+
config_path = get_default_config_path()
|
110
|
+
|
111
|
+
if not config_path.exists():
|
112
|
+
logger.info(f"Config file not found at {config_path}, using defaults")
|
113
|
+
config = RunbooksConfig()
|
114
|
+
save_config(config, config_path)
|
115
|
+
return config
|
116
|
+
|
117
|
+
try:
|
118
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
119
|
+
config_data = yaml.safe_load(f) or {}
|
120
|
+
|
121
|
+
# Handle environment variable substitution
|
122
|
+
config_data = _substitute_env_vars(config_data)
|
123
|
+
|
124
|
+
config = RunbooksConfig(**config_data)
|
125
|
+
logger.debug(f"Loaded configuration from {config_path}")
|
126
|
+
return config
|
127
|
+
|
128
|
+
except Exception as e:
|
129
|
+
logger.error(f"Error loading config from {config_path}: {e}")
|
130
|
+
logger.info("Using default configuration")
|
131
|
+
return RunbooksConfig()
|
132
|
+
|
133
|
+
|
134
|
+
def save_config(config: RunbooksConfig, config_path: Optional[Path] = None) -> None:
|
135
|
+
"""
|
136
|
+
Save configuration to file.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
config: Configuration object to save
|
140
|
+
config_path: Path to save configuration file
|
141
|
+
"""
|
142
|
+
if config_path is None:
|
143
|
+
config_path = get_default_config_path()
|
144
|
+
|
145
|
+
try:
|
146
|
+
ensure_directory(config_path.parent)
|
147
|
+
|
148
|
+
config_dict = config.model_dump(exclude_none=True)
|
149
|
+
|
150
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
151
|
+
yaml.dump(config_dict, f, default_flow_style=False, indent=2, sort_keys=True)
|
152
|
+
|
153
|
+
logger.debug(f"Saved configuration to {config_path}")
|
154
|
+
|
155
|
+
except Exception as e:
|
156
|
+
logger.error(f"Error saving config to {config_path}: {e}")
|
157
|
+
raise
|
158
|
+
|
159
|
+
|
160
|
+
def update_config(updates: Dict[str, Any], config_path: Optional[Path] = None) -> RunbooksConfig:
|
161
|
+
"""
|
162
|
+
Update specific configuration values.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
updates: Dictionary of configuration updates
|
166
|
+
config_path: Path to configuration file
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
Updated configuration object
|
170
|
+
"""
|
171
|
+
config = load_config(config_path)
|
172
|
+
|
173
|
+
# Update configuration
|
174
|
+
for key, value in updates.items():
|
175
|
+
if hasattr(config, key):
|
176
|
+
setattr(config, key, value)
|
177
|
+
else:
|
178
|
+
logger.warning(f"Unknown configuration key: {key}")
|
179
|
+
|
180
|
+
save_config(config, config_path)
|
181
|
+
return config
|
182
|
+
|
183
|
+
|
184
|
+
def _substitute_env_vars(data: Any) -> Any:
|
185
|
+
"""
|
186
|
+
Recursively substitute environment variables in configuration data.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
data: Configuration data
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Data with environment variables substituted
|
193
|
+
"""
|
194
|
+
if isinstance(data, dict):
|
195
|
+
return {key: _substitute_env_vars(value) for key, value in data.items()}
|
196
|
+
elif isinstance(data, list):
|
197
|
+
return [_substitute_env_vars(item) for item in data]
|
198
|
+
elif isinstance(data, str):
|
199
|
+
# Simple environment variable substitution ${VAR_NAME}
|
200
|
+
if data.startswith("${") and data.endswith("}"):
|
201
|
+
env_var = data[2:-1]
|
202
|
+
return os.getenv(env_var, data)
|
203
|
+
return data
|
204
|
+
else:
|
205
|
+
return data
|
206
|
+
|
207
|
+
|
208
|
+
def get_aws_session_config(config: RunbooksConfig) -> Dict[str, str]:
|
209
|
+
"""
|
210
|
+
Get AWS session configuration from runbooks config.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
config: Runbooks configuration
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
Dictionary with AWS session parameters
|
217
|
+
"""
|
218
|
+
session_config = {"profile_name": config.aws_profile}
|
219
|
+
|
220
|
+
if config.aws_region:
|
221
|
+
session_config["region_name"] = config.aws_region
|
222
|
+
|
223
|
+
return session_config
|
224
|
+
|
225
|
+
|
226
|
+
def validate_config(config: RunbooksConfig) -> bool:
|
227
|
+
"""
|
228
|
+
Validate configuration settings.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
config: Configuration to validate
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
True if configuration is valid
|
235
|
+
"""
|
236
|
+
try:
|
237
|
+
# Validate AWS profile if specified
|
238
|
+
if config.aws_profile != "default":
|
239
|
+
from runbooks.utils import validate_aws_profile
|
240
|
+
|
241
|
+
if not validate_aws_profile(config.aws_profile):
|
242
|
+
logger.warning(f"AWS profile '{config.aws_profile}' not found")
|
243
|
+
|
244
|
+
# Validate log level
|
245
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
246
|
+
if config.log_level not in valid_levels:
|
247
|
+
logger.warning(f"Invalid log level '{config.log_level}', using INFO")
|
248
|
+
config.log_level = "INFO"
|
249
|
+
|
250
|
+
# Validate severity threshold
|
251
|
+
valid_severities = ["INFO", "WARNING", "CRITICAL"]
|
252
|
+
if config.cfat_severity_threshold not in valid_severities:
|
253
|
+
logger.warning(f"Invalid CFAT severity '{config.cfat_severity_threshold}', using WARNING")
|
254
|
+
config.cfat_severity_threshold = "WARNING"
|
255
|
+
|
256
|
+
return True
|
257
|
+
|
258
|
+
except Exception as e:
|
259
|
+
logger.error(f"Configuration validation failed: {e}")
|
260
|
+
return False
|