To check GuardDuty is enabled in all Regions, call ListDetectors in every enabled Region. No detector ID means GuardDuty was never turned on there. If there is one, GetDetector returns its Status (ENABLED or DISABLED, which is how a suspended detector shows) and the list of protection features that are on. GuardDuty is Regional, so one Region’s answer tells you nothing about the rest.
Teams usually turn on GuardDuty in the Region they work in and assume that covers the account. It doesn’t: each Region needs its own detector, and a compromised credential used in a Region nobody watches produces no findings at all. This example is for engineers who want to check GuardDuty is enabled in all Regions, see which protection plans are running, and close the gaps in a controlled way.
You’ll get a TypeScript script for the AWS SDK for JavaScript v3 that reports every Region and changes nothing unless you pass --apply. It follows the same report-then-apply pattern as the rest of our AWS SDK v3 security and cost examples.
Why does GuardDuty have to be enabled per Region?
A GuardDuty detector is the resource that represents the service in one account and one Region, and you can have only one per account per Region. It analyzes the logs of that Region: CloudTrail management events, VPC flow logs and DNS query logs for foundational threat detection, plus whatever protection plans you add. GuardDuty reads these directly, so you don’t need to turn on VPC flow logs yourself, and your own log filtering doesn’t change what it sees. It doesn’t keep those events for you, though; for a long-term record, run the script to check CloudTrail is logging in every AWS Region as well.
That per-Region design matters most in the Regions you don’t use. Activity there, such as instances launched with a leaked access key, is exactly what nobody would otherwise notice, and a detector that isn’t there can’t raise a finding. GuardDuty findings also reach Security Hub CSPM only in Regions where both are on; the script to check Security Hub is enabled in all Regions lists those Regions and their standards.
How does a delegated administrator change the picture?
In AWS Organizations, the management account can make one member the delegated GuardDuty administrator. That setting is also Regional: the administrator manages members only in the Regions where it was designated, and it must be the same account in every Region. So an organization can have full coverage in us-east-1 and none in ap-south-1. The script shows the administrator account for each detector, which makes those gaps easy to spot. For member accounts, coverage in each Region comes from the administrator’s auto-enable preference (NEW or ALL), so fix gaps there rather than account by account.
What does the script do?
- Lists Regions
DescribeRegionsreturns the Regions enabled for your account, or you pass--regions=. - Finds the detector
ListDetectorsreturns zero or one detector ID per Region. - Reads its status and features
GetDetectorreturnsStatusand aFeatureslist with names such asCLOUD_TRAIL,DNS_LOGS,FLOW_LOGS,S3_DATA_EVENTS,RDS_LOGIN_EVENTSandRUNTIME_MONITORING. - Shows who manages it
GetAdministratorAccountreturns the administrator account ID and relationship status, if any. - Creates missing detectors only on requestWith
--apply,CreateDetectorwithEnable: truein each Region that has none. It never re-enables or changes an existing detector.
Prerequisites
- Node.js 18 or later, npm and
tsx. - The
@aws-sdk/client-guarddutyand@aws-sdk/client-ec2packages. - An AWS profile for the account you’re checking. The guide to AWS SDK v3 credential providers for SSO and assumed roles covers the options.
Which IAM permissions does it need?
The first statement covers the report. The other two are only for --apply: GuardDuty creates the AWSServiceRoleForAmazonGuardDuty service-linked role the first time it’s enabled, so the caller needs permission to create that role. Replace 123456789012 with your account ID.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReportDetectors",
"Effect": "Allow",
"Action": [
"ec2:DescribeRegions",
"guardduty:ListDetectors",
"guardduty:GetDetector",
"guardduty:GetAdministratorAccount"
],
"Resource": "*"
},
{
"Sid": "CreateDetector",
"Effect": "Allow",
"Action": "guardduty:CreateDetector",
"Resource": "*"
},
{
"Sid": "GuardDutyServiceLinkedRole",
"Effect": "Allow",
"Action": "iam:CreateServiceLinkedRole",
"Resource": "arn:aws:iam::123456789012:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty",
"Condition": { "StringLike": { "iam:AWSServiceName": "guardduty.amazonaws.com" } }
}
]
}
The IAM policy generator for TypeScript AWS code drafts a starting point from any SDK v3 script. Keep the last two statements out of a role you only use for audits.
The full script to check GuardDuty is enabled in all Regions
// check-guardduty-enabled-in-all-regions.ts
// Reports the GuardDuty detector in every enabled Region: missing, suspended or enabled, which
// protection features are on, and the delegated administrator account if one manages it.
// Report-only by default. --apply creates a detector (with GuardDuty's default features) in each
// Region that has none. It never re-enables, changes or deletes an existing detector.
// Usage: npx tsx check-guardduty-enabled-in-all-regions.ts [--regions=us-east-1,eu-west-1] [--apply]
import { EC2Client, DescribeRegionsCommand } from "@aws-sdk/client-ec2";
import {
GuardDutyClient,
CreateDetectorCommand,
GetAdministratorAccountCommand,
GetDetectorCommand,
ListDetectorsCommand,
} from "@aws-sdk/client-guardduty";
const args = process.argv.slice(2);
const apply = args.includes("--apply");
const regionArg = args.find((a) => a.startsWith("--regions="))?.split("=")[1];
interface Row { Region: string; Detector: string; Status: string; Admin: string; FeaturesOn: string; FeaturesOff: string; Verdict: 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 short = (name: string): string => name.toLowerCase().replace(/_/g, "-");
const trim = (s: string): string => (s.length > 50 ? `${s.slice(0, 47)}...` : s);
async function checkRegion(region: string): Promise<Row> {
const gd = new GuardDutyClient({ region });
const ids = (await gd.send(new ListDetectorsCommand({}))).DetectorIds ?? []; // at most one per account per Region
if (!ids.length) {
if (!apply) return { Region: region, Detector: "-", Status: "NONE", Admin: "-", FeaturesOn: "-", FeaturesOff: "-", Verdict: "NOT ENABLED" };
const created = await gd.send(new CreateDetectorCommand({ Enable: true }));
return { Region: region, Detector: created.DetectorId ?? "?", Status: "ENABLED", Admin: "-", FeaturesOn: "defaults", FeaturesOff: "-", Verdict: "detector created" };
}
const id = ids[0];
const det = await gd.send(new GetDetectorCommand({ DetectorId: id }));
const on = (det.Features ?? []).filter((f) => f.Status === "ENABLED").map((f) => short(f.Name ?? ""));
const off = (det.Features ?? []).filter((f) => f.Status === "DISABLED").map((f) => short(f.Name ?? ""));
let admin = "-";
try {
const adm = await gd.send(new GetAdministratorAccountCommand({ DetectorId: id }));
if (adm.Administrator?.AccountId) admin = `${adm.Administrator.AccountId} (${adm.Administrator.RelationshipStatus ?? "?"})`;
} catch {
admin = "unknown";
}
const enabled = det.Status === "ENABLED";
return {
Region: region,
Detector: id,
Status: det.Status ?? "?",
Admin: admin,
FeaturesOn: trim(on.join(", ")) || "-",
FeaturesOff: off.join(", ") || "-",
Verdict: enabled ? "ok" : "SUSPENDED: not monitoring",
};
}
async function main(): Promise<void> {
const rows: Row[] = [];
for (const region of await listRegions()) {
try {
rows.push(await checkRegion(region));
} catch (err) {
rows.push({ Region: region, Detector: "?", Status: "?", Admin: "?", FeaturesOn: "?", FeaturesOff: "?", Verdict: `error: ${err instanceof Error ? err.name : String(err)}` });
}
}
console.table(rows);
const missing = rows.filter((r) => r.Verdict === "NOT ENABLED").length;
const suspended = rows.filter((r) => r.Verdict.startsWith("SUSPENDED")).length;
console.log(`${rows.length} Regions checked; ${missing} without a detector, ${suspended} suspended.`);
if (!apply) console.log(`Report only: nothing changed. --apply would create a detector in ${missing} Region(s).`);
if (missing || suspended) process.exitCode = 2;
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
The script exits with code 2 when any Region is missing a detector or has a suspended one, so a scheduled job can alert on it.
How do you run it?
npm install @aws-sdk/client-guardduty @aws-sdk/client-ec2
npm install --save-dev tsx typescript
# Report only, every enabled Region
AWS_PROFILE=readonly npx tsx check-guardduty-enabled-in-all-regions.ts
# Create detectors where none exist, in two Regions
AWS_PROFILE=security-admin npx tsx check-guardduty-enabled-in-all-regions.ts --regions=ap-south-1,sa-east-1 --apply
Sample output
┌─────────┬──────────────┬────────────────────────────────────┬────────────┬──────────────────────────┬──────────────────────────────────────────────────────┬──────────────────────┬─────────────────────────────┐
│ (index) │ Region │ Detector │ Status │ Admin │ FeaturesOn │ FeaturesOff │ Verdict │
├─────────┼──────────────┼────────────────────────────────────┼────────────┼──────────────────────────┼──────────────────────────────────────────────────────┼──────────────────────┼─────────────────────────────┤
│ 0 │ 'ap-south-1' │ '-' │ 'NONE' │ '-' │ '-' │ '-' │ 'NOT ENABLED' │
│ 1 │ 'eu-west-1' │ 'b2c4a6e8f0d1c3e5a7b9d1f3e5a7c9b1' │ 'ENABLED' │ '111122223333 (Enabled)' │ 'cloud-trail, dns-logs, flow-logs, s3-data-event...' │ 'runtime-monitoring' │ 'ok' │
│ 2 │ 'us-east-1' │ 'a1b3c5d7e9f1a3b5c7d9e1f3a5b7c9d1' │ 'ENABLED' │ '111122223333 (Enabled)' │ 'cloud-trail, dns-logs, flow-logs, s3-data-event...' │ 'runtime-monitoring' │ 'ok' │
│ 3 │ 'us-west-2' │ 'c3d5e7f9a1b3c5d7e9f1a3b5c7d9e1f3' │ 'DISABLED' │ '-' │ 'cloud-trail, dns-logs, flow-logs' │ 's3-data-events' │ 'SUSPENDED: not monitoring' │
└─────────┴──────────────┴────────────────────────────────────┴────────────┴──────────────────────────┴──────────────────────────────────────────────────────┴──────────────────────┴─────────────────────────────┘
4 Regions checked; 1 without a detector, 1 suspended.
Report only: nothing changed. --apply would create a detector in 1 Region(s).
IDs are illustrative. eu-west-1 and us-east-1 are managed by the delegated administrator; ap-south-1 was never enabled, and us-west-2 is suspended with no administrator, so someone turned it off by hand.
What happens when you enable GuardDuty with –apply?
A new detector starts analyzing that Region’s logs straight away. Because the script calls CreateDetector without a Features list, GuardDuty turns on all optional features available in that Region except Runtime Monitoring, which needs agents and is left for you to decide. If you only want foundational threat detection at first, pass a Features list that sets the plans you don’t want to DISABLED.
Suspended detectors are reported, not changed. A suspended detector keeps its existing findings but stops monitoring and generating new ones, and you aren’t charged for GuardDuty while it’s suspended (Malware Protection for S3 is the exception and keeps billing). Re-enable it in the console or with UpdateDetector once you know why it was suspended. Disabling is different: it deletes the findings and configuration in that Region, and they can’t be recovered.
What will it cost?
GuardDuty is billed on usage, not per detector. Foundational threat detection is charged by the volume of CloudTrail management events, VPC flow logs and DNS logs analyzed, and each protection plan is charged on its own usage; rates vary by data source and Region. The GuardDuty usage and cost estimation guide (checked September 2026) describes a 30-day free trial per account for most protection plans, including foundational threat detection. During the trial, the hourly usage metrics GuardDuty publishes in the AWS/GuardDuty CloudWatch namespace give you the numbers to plug into the AWS Pricing Calculator. After the trial, the script to get last month’s AWS cost broken down by service shows what GuardDuty actually costs you.
Troubleshooting
- A Region shows
error: ...instead of a verdict. GuardDuty may not be available in that Region, or an SCP denies the call. The script records the error name and keeps going. AccessDeniedExceptiononCreateDetector. Usually the missingiam:CreateServiceLinkedRolestatement. The guide to troubleshoot AWS IAM access denied errors step by step shows how to read the error.- The administrator account can’t suspend GuardDuty. All member accounts must be disassociated or deleted before an administrator can suspend or disable it. That’s a safeguard, not a bug.
- Admin shows
unknown. The profile lacksguardduty:GetAdministratorAccount; the rest of the row is still accurate.
GuardDuty spots suspicious activity; closing the doors it usually comes through is a separate job. The scripts to find public EBS and RDS snapshots, find security groups open to the internet on common ports and find public Lambda function URLs cover three of them. The script to find publicly accessible RDS instances covers a fourth: databases with an internet-facing endpoint.
Ask ChatWithCloud instead
You can also ask ChatWithCloud “Is GuardDuty enabled in this Region, and which features are on?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your AWS profile and explains the result, as in the guide to analyze AWS security posture from the terminal with an AI CLI. It works in one profile and Region per session, so a multi-Region check means one session per Region, and it runs generated code without a confirmation step. Connect ChatWithCloud with a read-only AWS profile for questions like this, and read the ChatWithCloud security model for what leaves your machine.
Frequently asked questions
Is GuardDuty a global or Regional service?
Regional. You need a detector in each Region you want monitored, and a delegated administrator has to be set up in each Region too.
How do I check if GuardDuty is enabled with the AWS SDK?
Call ListDetectors in the Region. An empty list means it isn’t enabled; otherwise GetDetector returns ENABLED or DISABLED.
Does suspending GuardDuty delete findings?
No. Suspending keeps existing findings and stops new ones. Disabling deletes findings and configuration in that Region.
Does enabling GuardDuty in a new Region start a free trial?
GuardDuty’s documentation describes a 30-day free trial per account for most protection plans, and the GuardDuty console’s usage page shows your usage and estimated cost while it runs.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud