Analyze Cross-Account Patterns in AWS Trusted Advisor Organizational View Reports Using Amazon Bedrock
Learn how to parse AWS Trusted Advisor Organizational View reports with Python and use Amazon Bedrock Structured Outputs to identify cross-account risk patterns, prioritize findings by blast radius, and produce actionable executive summaries across your AWS Organization.
Analyze Cross-Account Patterns in AWS Trusted Advisor Organizational View Reports Using Amazon Bedrock
If you manage multiple AWS accounts through AWS Organizations, you have likely enabled Trusted Advisor Organizational View to aggregate recommendations across your environment. The challenge is not generating the report. It is extracting actionable patterns from the data once you have it.
This article shows you how to parse a Trusted Advisor Organizational View report with Python and use Amazon Bedrock with Structured Outputs to identify cross-account patterns and produce a prioritized risk summary.
Prerequisites
Before you begin, confirm the following:
- AWS Trusted Advisor Organizational View is enabled in your management account. If you have not enabled it yet, follow Organizational view for AWS Trusted Advisor.
- You have downloaded at least one Organizational View report (JSON format) from the Trusted Advisor console.
- Your AWS account has Amazon Bedrock model access enabled for Amazon Nova Pro. To enable access, open the Amazon Bedrock console, choose Model access in the navigation pane, and request access to Amazon Nova Pro.
- Python 3.9 or later with boto3 installed.
- IAM permissions for
bedrock:InvokeModelon the Amazon Nova Pro model.
Important: The Organizational View report can only be created from the management account (or a delegated administrator for Trusted Advisor Priority). There is no API to trigger report creation programmatically. You create reports from the Trusted Advisor console and download them as a .zip file.
Support plan requirement: Member accounts with Basic Support have limited Trusted Advisor checks available. The report will only contain checks that each account's support plan covers.
Understanding the report structure
When you download an Organizational View report, you receive a .zip file containing three files:
summary.json— Aggregated check results per account and per category (security, performance, cost_optimizing, fault_tolerance, service_limits).schema.json— Maps check IDs to their column names, so you know what each check reports on.resources.json(orresources.csv) — Detailed per-resource findings across all accounts.
For reports larger than 5 MB, AWS splits the resources file into multiple parts (for example, resources-1.json, resources-2.json). Your code should handle this.
Here is a simplified example of summary.json:
{ "numAccounts": 3, "filtersApplied": { "accountIds": ["111122223333", "444455556666", "777788889999"], "checkIds": "All", "categories": ["security", "performance"], "statuses": "All", "regions": ["us-east-1", "us-west-2"] }, "categoryStatusMap": { "security": { "statusMap": { "ERROR": { "name": "Red", "count": 2 }, "OK": { "name": "Green", "count": 5 }, "WARN": { "name": "Yellow", "count": 3 } } } }, "accountStatusMap": { "111122223333": { "security": { "statusMap": { "ERROR": { "name": "Red", "count": 2 }, "WARN": { "name": "Yellow", "count": 1 } } } } } }
Step 1: Parse the report with Python
Start by loading and grouping the findings. This deterministic step structures the data before sending it to Bedrock.
import json import glob import os from collections import defaultdict def load_ta_report(report_dir): """Load all components of a TA Organizational View report.""" with open(os.path.join(report_dir, "summary.json")) as f: summary = json.load(f) with open(os.path.join(report_dir, "schema.json")) as f: schema = json.load(f) # Handle multiple resource files for large reports resource_files = glob.glob(os.path.join(report_dir, "resources*.json")) resources = [] for rf in resource_files: with open(rf) as f: data = json.load(f) if isinstance(data, list): resources.extend(data) else: resources.append(data) return summary, schema, resources def group_findings_by_pattern(summary): """Group findings by account and category for pattern analysis.""" account_status = summary.get("accountStatusMap", {}) num_accounts = summary.get("numAccounts", 0) patterns = { "total_accounts": num_accounts, "accounts_with_errors": [], "accounts_with_warnings": [], "category_summary": {}, "per_account_breakdown": {} } for account_id, categories in account_status.items(): account_errors = 0 account_warnings = 0 for category, status_data in categories.items(): if isinstance(status_data, dict) and "statusMap" in status_data: status_map = status_data["statusMap"] errors = status_map.get("ERROR", {}).get("count", 0) warnings = status_map.get("WARN", {}).get("count", 0) account_errors += errors account_warnings += warnings # Track category-level totals if category not in patterns["category_summary"]: patterns["category_summary"][category] = { "total_errors": 0, "total_warnings": 0, "affected_accounts": 0 } patterns["category_summary"][category]["total_errors"] += errors patterns["category_summary"][category]["total_warnings"] += warnings if errors > 0 or warnings > 0: patterns["category_summary"][category]["affected_accounts"] += 1 if account_errors > 0: patterns["accounts_with_errors"].append({ "account_id": account_id, "error_count": account_errors }) if account_warnings > 0: patterns["accounts_with_warnings"].append({ "account_id": account_id, "warning_count": account_warnings }) patterns["per_account_breakdown"][account_id] = { "errors": account_errors, "warnings": account_warnings } return patterns
Step 2: Analyze patterns with Amazon Bedrock
Now feed the grouped data to Amazon Bedrock using the Converse API with Structured Outputs. The model analyzes cross-account patterns and returns a schema-compliant JSON response.
import boto3 def analyze_with_bedrock(patterns, region="us-east-1"): """Use Amazon Bedrock to identify cross-account patterns.""" bedrock = boto3.client("bedrock-runtime", region_name=region) # Define the output schema for structured responses analysis_schema = { "type": "object", "properties": { "executive_summary": { "type": "string", "description": "2-3 sentence summary of organizational risk posture" }, "top_patterns": { "type": "array", "items": { "type": "object", "properties": { "pattern_name": {"type": "string"}, "severity": { "type": "string", "enum": ["critical", "high", "medium", "low"] }, "affected_account_count": {"type": "integer"}, "description": {"type": "string"}, "recommended_action": {"type": "string"} }, "required": [ "pattern_name", "severity", "affected_account_count", "description", "recommended_action" ] }, "description": "Top cross-account patterns ranked by organizational risk" }, "priority_accounts": { "type": "array", "items": { "type": "object", "properties": { "account_id": {"type": "string"}, "risk_level": { "type": "string", "enum": ["critical", "high", "medium", "low"] }, "reason": {"type": "string"} }, "required": ["account_id", "risk_level", "reason"] }, "description": "Accounts requiring immediate attention" } }, "required": ["executive_summary", "top_patterns", "priority_accounts"] } prompt = f"""Analyze these AWS Trusted Advisor findings across {patterns['total_accounts']} accounts in an AWS Organization. Identify the top cross-account patterns that represent organizational risk. Consider: 1. Which categories have the most errors across accounts? 2. Are errors concentrated in a few accounts or spread evenly? 3. Which patterns, if left unaddressed, have the highest blast radius? Rank patterns by organizational risk (blast radius multiplied by likelihood of impact). Identify accounts that need immediate attention. Data: - Category summary: {json.dumps(patterns['category_summary'])} - Accounts with errors (sorted by count): {json.dumps(sorted(patterns['accounts_with_errors'], key=lambda x: x['error_count'], reverse=True)[:20])} - Accounts with warnings: {json.dumps(sorted(patterns['accounts_with_warnings'], key=lambda x: x['warning_count'], reverse=True)[:20])} - Total accounts: {patterns['total_accounts']} - Accounts with at least one error: {len(patterns['accounts_with_errors'])} - Accounts with at least one warning: {len(patterns['accounts_with_warnings'])}""" response = bedrock.converse( modelId="us.amazon.nova-pro-v1:0", messages=[{ "role": "user", "content": [{"text": prompt}] }], inferenceConfig={ "maxTokens": 4096, "temperature": 0.1 }, outputConfig={ "textFormat": { "type": "json_schema", "structure": { "jsonSchema": { "schema": json.dumps(analysis_schema), "name": "ta_cross_account_analysis", "description": "Cross-account pattern analysis from Trusted Advisor data" } } } } ) result = json.loads( response["output"]["message"]["content"][0]["text"] ) return result
Step 3: Put it together
Combine parsing and analysis into a single script:
def main(): # Path to the unzipped TA Organizational View report report_dir = "./ta-org-report" print("Loading Trusted Advisor Organizational View report...") summary, schema, resources = load_ta_report(report_dir) print(f"Report covers {summary['numAccounts']} accounts") print(f"Categories: {list(summary.get('categoryStatusMap', {}).keys())}") print("\nGrouping findings by pattern...") patterns = group_findings_by_pattern(summary) print(f"Accounts with errors: {len(patterns['accounts_with_errors'])}") print(f"Accounts with warnings: {len(patterns['accounts_with_warnings'])}") print("\nAnalyzing with Amazon Bedrock (Nova Pro)...") analysis = analyze_with_bedrock(patterns) # Display results print("\n" + "=" * 60) print("CROSS-ACCOUNT PATTERN ANALYSIS") print("=" * 60) print(f"\n{analysis['executive_summary']}\n") print("-" * 60) print("TOP PATTERNS (ranked by organizational risk)") print("-" * 60) for i, pattern in enumerate(analysis["top_patterns"], 1): print(f"\n{i}. [{pattern['severity'].upper()}] {pattern['pattern_name']}") print(f" Affected accounts: {pattern['affected_account_count']}") print(f" {pattern['description']}") print(f" Action: {pattern['recommended_action']}") print("\n" + "-" * 60) print("PRIORITY ACCOUNTS") print("-" * 60) for account in analysis["priority_accounts"]: print(f"\n [{account['risk_level'].upper()}] {account['account_id']}") print(f" Reason: {account['reason']}") # Save full analysis to file with open("ta-analysis-output.json", "w") as f: json.dump(analysis, f, indent=2) print(f"\nFull analysis saved to ta-analysis-output.json") if __name__ == "__main__": main()
Handling large organizations
For organizations with hundreds of accounts, the report data may be large. Two strategies keep Bedrock context usage efficient:
Filter by category before analysis. If you only care about security patterns, filter the summary data to the security category before passing it to Bedrock:
# Analyze only security findings security_patterns = { k: v for k, v in patterns["category_summary"].items() if k == "security" }
Filter by OU. When creating the report in the Trusted Advisor console, select specific organizational units rather than the entire organization. This produces smaller, more focused reports.
Cost considerations
Amazon Bedrock charges per input and output token. A typical Trusted Advisor report for 50 accounts produces approximately 30,000 input tokens when passed to the model. The structured output response is approximately 2,000 tokens. At current Amazon Nova Pro pricing, this costs pennies per analysis run. See Amazon Bedrock Pricing for current rates.
For automated recurring analysis (for example, weekly), the cost remains negligible even at scale.
Optional: Automate with Lambda and EventBridge
You can automate this analysis by running it as an AWS Lambda function on a schedule:
- Store the TA report in Amazon S3 (manually or using the approach described in Using other AWS services to view Trusted Advisor reports).
- Create a Lambda function that loads the report from S3, runs the analysis, and publishes results to Amazon SNS or stores them in S3.
- Use Amazon EventBridge Scheduler to invoke the Lambda on a weekly cadence.
This gives you a recurring cross-account risk report delivered to your inbox without manual intervention.
What this approach adds beyond the raw report
The Organizational View report gives you data. This approach gives you analysis:
- Pattern recognition across categories. The model identifies when multiple findings correlate (for example, accounts with both open security groups and disabled CloudTrail suggest unmanaged environments).
- Natural language executive summary. Share the output with leadership without requiring them to interpret JSON or CSV files.
- Prioritization with reasoning. Patterns are ranked by blast radius and likelihood, not just finding count.
- Structured, parseable output. The JSON schema ensures consistent output that downstream tools can consume reliably.
Next steps
- Deploy the Trusted Advisor Organizational (TAO) Dashboard for ongoing visualization in Amazon QuickSight.
- Use the
ListOrganizationRecommendationsAPI for programmatic access to Trusted Advisor Priority recommendations (note: this API only supports prioritized recommendations, not all standard checks). - Extend the analysis schema to include cost savings estimates by adding the
pillarSpecificAggregates.costOptimizingfields from the report data.
Related resources
- Language
- English
Relevant content
- Accepted Answer
asked 3 years ago
AWS OFFICIALUpdated 23 days ago
AWS OFFICIALUpdated 2 years ago