Skip to content

Analyze Cross-Account Patterns in AWS Trusted Advisor Organizational View Reports Using Amazon Bedrock

9 minute read
Content level: Intermediate
0

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:InvokeModel on 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 (or resources.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:

  1. Store the TA report in Amazon S3 (manually or using the approach described in Using other AWS services to view Trusted Advisor reports).
  2. Create a Lambda function that loads the report from S3, runs the analysis, and publishes results to Amazon SNS or stores them in S3.
  3. 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 ListOrganizationRecommendations API 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.costOptimizing fields from the report data.

Related resources