Check Security Hub Is Enabled in Every AWS Region

A dark control room with a wall of monitors showing network and security dashboards

Photo by Jakub Żerdzicki on Unsplash

To check Security Hub is enabled in all Regions, loop over the Regions from DescribeRegions and call DescribeHub in each one. A Region where Security Hub CSPM is off answers with InvalidAccessException. Where it’s on, GetEnabledStandards lists the active standards, and GetFindingAggregator tells you whether that Region’s findings reach your home Region.

Security Hub is a regional service. Turning it on in the Region you work in leaves every other Region unscored, and a Region nobody watches is exactly where a forgotten test stack or a compromised key tends to show up. This example is for engineers who want to check Security Hub is enabled in all Regions of an account from a script, and see at the same time which standards run there and whether findings are aggregated.

You’ll get a TypeScript script for the AWS SDK for JavaScript v3 that prints one row per Region and changes nothing unless you pass --apply. It follows the same report-then-apply pattern as the rest of our collection of AWS SDK v3 practical examples. The guide to analyzing your AWS security posture with an AI CLI names Security Hub as one of the signals to look at; this script is the per-Region check behind that.

Security Hub CSPM or the new Security Hub: which one are you checking?

In June 2025 AWS renamed the original service to AWS Security Hub CSPM (cloud security posture management), and in December 2025 a new, unified AWS Security Hub became generally available. They share the securityhub API namespace but use different resources:

Service What it does API to check it
Security Hub CSPM Runs control checks from standards such as AWS Foundational Security Best Practices and the CIS benchmark, and collects findings in the ASFF format DescribeHub
Security Hub (unified) Correlates findings from GuardDuty, Inspector, Macie and CSPM into prioritized exposures, using the OCSF format DescribeSecurityHubV2

Enabling the unified Security Hub turns on Security Hub CSPM for posture management, so CSPM is the baseline you want everywhere. The script reports both, but its pass/fail verdict and --apply are about CSPM. Both are regional: AWS’s FAQ says you must enable Security Hub in each Region to see findings for that Region.

The CIS Amazon Web Services Foundations Benchmark also asks for it: version 4.0.1 has “Ensure AWS Security Hub is enabled” as recommendation 4.16.

What does the script do?

  1. Lists RegionsDescribeRegions returns the Regions enabled for your account, or you pass --regions=.
  2. Checks the CSPM hubDescribeHub in each Region. Success means enabled and gives SubscribedAt; InvalidAccessException means off.
  3. Lists standardsGetEnabledStandards (paginated) returns each subscription with its status: READY, PENDING, INCOMPLETE, FAILED or DELETING.
  4. Checks the unified hubDescribeSecurityHubV2; ResourceNotFoundException means it’s off in that Region.
  5. Reads aggregationListFindingAggregators and GetFindingAggregator return the home Region, the linking mode and the Region list, from which the script marks each Region as linked or not.
  6. Enables, if askedWith --apply, EnableSecurityHub in every Region where CSPM is off.

Prerequisites

Which IAM permissions does it need?

The first statement is read-only and is all the report needs. The second is only for --apply: enabling Security Hub CSPM creates the AWSServiceRoleForSecurityHub service-linked role the first time.

security-hub-check-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReportSecurityHubStatus",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "securityhub:DescribeHub",
        "securityhub:GetEnabledStandards",
        "securityhub:DescribeSecurityHubV2",
        "securityhub:ListFindingAggregators",
        "securityhub:GetFindingAggregator"
      ],
      "Resource": "*"
    },
    {
      "Sid": "EnableSecurityHubOnlyWithApply",
      "Effect": "Allow",
      "Action": "securityhub:EnableSecurityHub",
      "Resource": "*"
    },
    {
      "Sid": "SecurityHubServiceLinkedRole",
      "Effect": "Allow",
      "Action": "iam:CreateServiceLinkedRole",
      "Resource": "arn:aws:iam::*:role/aws-service-role/securityhub.amazonaws.com/AWSServiceRoleForSecurityHub",
      "Condition": { "StringEquals": { "iam:AWSServiceName": "securityhub.amazonaws.com" } }
    }
  ]
}

Leave the last two statements out of an audit role. The IAM policy generator for TypeScript SDK code drafts a similar starting point from any SDK v3 script you write.

The script to check Security Hub in all Regions

check-security-hub-enabled-all-regions.ts

// check-security-hub-enabled-all-regions.ts
// Reports, for every enabled Region: whether Security Hub CSPM is on, which standards are enabled,
// whether the newer unified Security Hub is on, and whether the Region is linked to cross-Region aggregation.
// Changes nothing unless you pass --apply, which enables Security Hub CSPM where it is off.
// Usage: npx tsx check-security-hub-enabled-all-regions.ts [--regions=us-east-1,eu-west-1] [--apply] [--no-default-standards]
import { EC2Client, DescribeRegionsCommand } from "@aws-sdk/client-ec2";
import {
  SecurityHubClient,
  DescribeHubCommand,
  DescribeSecurityHubV2Command,
  EnableSecurityHubCommand,
  GetFindingAggregatorCommand,
  ListFindingAggregatorsCommand,
  paginateGetEnabledStandards,
} from "@aws-sdk/client-securityhub";

const args = process.argv.slice(2);
const apply = args.includes("--apply");
const defaultStandards = !args.includes("--no-default-standards");
const regionArg = args.find((a) => a.startsWith("--regions="))?.split("=")[1];

interface Aggregation {
  home: string;
  mode: string;
  regions: string[];
}

interface Row {
  Region: string;
  CSPM: string;
  Since: string;
  Standards: string;
  UnifiedHub: string;
  Aggregation: string;
}

async function listRegions(): Promise<string[]> {
  if (regionArg) return regionArg.split(",").map((r) => r.trim()).filter(Boolean);
  const out = await new EC2Client({}).send(new DescribeRegionsCommand({})); // enabled Regions only
  return (out.Regions ?? []).map((r) => r.RegionName ?? "").filter(Boolean).sort();
}

const errName = (err: unknown) => (err instanceof Error ? err.name : String(err));

// Security Hub CSPM answers InvalidAccessException ("not subscribed") when it isn't enabled in the Region.
async function describeHub(client: SecurityHubClient) {
  try {
    const hub = await client.send(new DescribeHubCommand({}));
    return { on: true as const, since: hub.SubscribedAt?.slice(0, 10) ?? "?" };
  } catch (err) {
    if (errName(err) === "InvalidAccessException" || errName(err) === "ResourceNotFoundException") {
      return { on: false as const, since: "" };
    }
    throw err;
  }
}

// Short names such as "aws-foundational-security-best-practices/v/1.0.0 (READY)".
async function enabledStandards(client: SecurityHubClient): Promise<string> {
  const names: string[] = [];
  for await (const page of paginateGetEnabledStandards({ client }, {})) {
    for (const s of page.StandardsSubscriptions ?? []) {
      const arn = s.StandardsArn ?? "";
      const short = arn.split(/:(?:standards|ruleset)\//)[1] ?? arn;
      names.push(`${short} (${s.StandardsStatus})`);
    }
  }
  return names.length ? names.join("; ") : "none";
}

// The unified Security Hub (GA December 2025) has its own resource and API.
async function unifiedHub(client: SecurityHubClient): Promise<string> {
  try {
    const out = await client.send(new DescribeSecurityHubV2Command({}));
    return `on since ${out.SubscribedAt?.slice(0, 10) ?? "?"}`;
  } catch (err) {
    return errName(err) === "ResourceNotFoundException" ? "off" : `unknown (${errName(err)})`;
  }
}

async function aggregation(client: SecurityHubClient): Promise<Aggregation | undefined> {
  const list = await client.send(new ListFindingAggregatorsCommand({}));
  const arn = list.FindingAggregators?.[0]?.FindingAggregatorArn;
  if (!arn) return undefined;
  const agg = await client.send(new GetFindingAggregatorCommand({ FindingAggregatorArn: arn }));
  return { home: agg.FindingAggregationRegion ?? "?", mode: agg.RegionLinkingMode ?? "?", regions: agg.Regions ?? [] };
}

function linkState(region: string, agg: Aggregation | undefined): string {
  if (!agg) return "no aggregator";
  if (region === agg.home) return "HOME Region";
  const listed = agg.regions.includes(region);
  if (agg.mode === "ALL_REGIONS") return "linked";
  if (agg.mode === "ALL_REGIONS_EXCEPT_SPECIFIED") return listed ? "excluded" : "linked";
  if (agg.mode === "SPECIFIED_REGIONS") return listed ? "linked" : "not linked";
  return "not linked";
}

async function main(): Promise<void> {
  const regions = await listRegions();
  const rows: Row[] = [];
  const off: string[] = [];
  let agg: Aggregation | undefined;
  let aggChecked = false;

  for (const region of regions) {
    const client = new SecurityHubClient({ region });
    try {
      const hub = await describeHub(client);
      if (hub.on && !aggChecked) {
        agg = await aggregation(client); // callable from any Region where the hub is enabled
        aggChecked = true;
      }
      if (!hub.on) off.push(region);
      rows.push({
        Region: region,
        CSPM: hub.on ? "ENABLED" : "OFF",
        Since: hub.since,
        Standards: hub.on ? await enabledStandards(client) : "-",
        UnifiedHub: await unifiedHub(client),
        Aggregation: "",
      });
    } catch (err) {
      rows.push({ Region: region, CSPM: `ERROR ${errName(err)}`, Since: "", Standards: "", UnifiedHub: "", Aggregation: "" });
    }
  }
  for (const row of rows) row.Aggregation = row.CSPM === "ENABLED" ? linkState(row.Region, agg) : "-";

  console.table(rows);
  console.log(agg
    ? `Cross-Region aggregation: home ${agg.home}, mode ${agg.mode}${agg.regions.length ? ` (${agg.regions.join(", ")})` : ""}`
    : "Cross-Region aggregation: not configured");
  console.log(`${off.length} of ${regions.length} Regions without Security Hub CSPM: ${off.join(", ") || "none"}`);

  if (apply && off.length) {
    for (const region of off) {
      try {
        await new SecurityHubClient({ region }).send(new EnableSecurityHubCommand({
          EnableDefaultStandards: defaultStandards,
          ControlFindingGenerator: "SECURITY_CONTROL",
        }));
        console.log(`Enabled Security Hub CSPM in ${region}${defaultStandards ? " with the default standards" : ""}`);
      } catch (err) {
        console.error(`Could not enable in ${region}: ${errName(err)} ${err instanceof Error ? err.message : ""}`);
        process.exitCode = 1;
      }
    }
  } else if (off.length) {
    console.log("Run again with --apply to enable Security Hub CSPM in those Regions.");
    process.exitCode = 2;
  }
}

main().catch((err) => {
  console.error(err instanceof Error ? `${err.name}: ${err.message}` : err);
  process.exit(1);
});

Aggregation is read once, from the first Region where the hub is on, because an account has at most one finding aggregator and ListFindingAggregators works from any Region. ControlFindingGenerator: "SECURITY_CONTROL" turns on consolidated control findings, so a control that appears in two standards produces one finding instead of two.

How do you run it?

Terminal

npm install @aws-sdk/client-securityhub @aws-sdk/client-ec2
npm install --save-dev tsx typescript

# Report every enabled Region
AWS_PROFILE=security-audit npx tsx check-security-hub-enabled-all-regions.ts

# Enable CSPM where it's off, without the two default standards
AWS_PROFILE=security-admin npx tsx check-security-hub-enabled-all-regions.ts --apply --no-default-standards

The script exits with code 2 when any Region is off and you didn’t pass --apply, which makes it easy to fail a scheduled pipeline job when a Region regresses.

Sample output

Output

┌─────────┬──────────────┬───────────┬──────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────┬───────────────┐
│ (index) │ Region       │ CSPM      │ Since        │ Standards                                                                                                 │ UnifiedHub            │ Aggregation   │
├─────────┼──────────────┼───────────┼──────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────┼───────────────┤
│ 0       │ 'ap-south-1' │ 'OFF'     │ ''           │ '-'                                                                                                       │ 'off'                 │ '-'           │
│ 1       │ 'eu-west-1'  │ 'ENABLED' │ '2024-02-12' │ 'aws-foundational-security-best-practices/v/1.0.0 (READY)'                                                │ 'off'                 │ 'linked'      │
│ 2       │ 'us-east-1'  │ 'ENABLED' │ '2023-06-01' │ 'aws-foundational-security-best-practices/v/1.0.0 (READY); cis-aws-foundations-benchmark/v/3.0.0 (READY)' │ 'on since 2026-01-20' │ 'HOME Region' │
│ 3       │ 'us-west-2'  │ 'ENABLED' │ '2025-08-30' │ 'none'                                                                                                    │ 'off'                 │ 'not linked'  │
└─────────┴──────────────┴───────────┴──────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────┴───────────────┘
Cross-Region aggregation: home us-east-1, mode SPECIFIED_REGIONS (eu-west-1)
1 of 4 Regions without Security Hub CSPM: ap-south-1
Run again with --apply to enable Security Hub CSPM in those Regions.

The account and dates are illustrative. Three different problems show up here. ap-south-1 has no hub at all. us-west-2 has a hub but no standards, so it collects findings from integrated services but runs no control checks. And because aggregation only links eu-west-1, anything found in us-west-2 never reaches the home Region your team looks at.

What does –apply turn on, and what will it cost?

EnableSecurityHub creates the hub in the current Region and, unless EnableDefaultStandards is false, subscribes to AWS Foundational Security Best Practices and CIS AWS Foundations Benchmark v1.2.0. Newer CIS versions are available but not enabled by default; enable them with BatchEnableStandards or in the console once you’ve picked one.

Security Hub CSPM is billed on usage, not per hub. As of September 2026 its pricing page lists three dimensions: security checks, finding ingestion events (with a monthly free allowance) and automation rule evaluations, plus a 30-day free trial for every account in each Region where it’s enabled. Rates vary by Region and volume, so treat the trial as your measurement period, then check what it costs with the script to get last month’s AWS bill broken down by service. A monthly AWS budget alert created with SDK v3 catches a surprise before the invoice does.

Organizations: if your organization uses Security Hub CSPM central configuration, member accounts are managed by the delegated administrator’s configuration policies. Enable Regions there instead of with --apply in each member, or the next policy run can undo your change.

To fix the aggregation gap, call UpdateFindingAggregator from the home Region with RegionLinkingMode: "ALL_REGIONS", which also links Regions you enable later.

Troubleshooting

  • ERROR AccessDeniedException in some Regions only. A service control policy that limits aws:RequestedRegion blocks calls outside the approved Regions. Pass those Regions with --regions=, or treat the denied ones as covered by the SCP.
  • UnifiedHub shows unknown (AccessDeniedException). The profile lacks securityhub:DescribeSecurityHubV2; add it to your audit policy.
  • A standard is stuck in INCOMPLETE. Some controls couldn’t be enabled, usually because AWS Config isn’t recording the resource types they need.
  • --apply fails with AccessDeniedException. Usually the missing iam:CreateServiceLinkedRole statement, or an SCP that denies securityhub:*. The guide to troubleshoot AWS IAM access denied errors step by step shows how to tell which.

Security Hub scores configuration; it doesn’t replace detection or logging. Pair it with the scripts to check GuardDuty threat detection is on in every Region and to check a multi-Region CloudTrail trail is logging. For the one account-wide identity check none of these cover, run the script to check the AWS root user for MFA and access keys.

Ask ChatWithCloud instead

ChatWithCloud turns a plain-English question into AWS SDK for JavaScript v2 code, runs it on your machine with your AWS profile, and sends the JSON result to the AI model to write the answer. Ask “Is Security Hub enabled here, and which standards are on?” and it can call DescribeHub and GetEnabledStandards. It uses one profile and Region per session, so the multi-Region sweep is still a job for the script. It also runs generated code without a confirmation step, so connect ChatWithCloud with a read-only AWS profile and read the ChatWithCloud security model and data flow first. Because v2 reached end of support on 8 September 2025, APIs added after that date, such as parts of the unified Security Hub API, may not be available to it.

Frequently asked questions

How do I check if Security Hub is enabled with the AWS CLI?

Run aws securityhub describe-hub --region <region>. It returns the hub ARN and SubscribedAt when enabled, and an InvalidAccessException saying the account isn’t subscribed when it’s off. Repeat per Region.

Do I need Security Hub in every Region if I use cross-Region aggregation?

Yes. Aggregation only replicates findings from Regions where Security Hub is enabled to the home Region; it doesn’t run checks in Regions where it’s off.

Does enabling the new Security Hub also enable Security Hub CSPM?

Yes. AWS’s FAQ says enabling the unified Security Hub automatically enables Security Hub CSPM in the account for posture management.

Is Security Hub free?

There is a 30-day free trial per account per Region. After that you pay per security check, per finding ingestion event above the free allowance, and per automation rule evaluation.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud