diff --git a/.doc_gen/metadata/controltower_metadata.yaml b/.doc_gen/metadata/controltower_metadata.yaml
index 3cf49427046..083079b4db1 100644
--- a/.doc_gen/metadata/controltower_metadata.yaml
+++ b/.doc_gen/metadata/controltower_metadata.yaml
@@ -12,6 +12,14 @@ controltower_Hello:
- description:
snippet_tags:
- python.example_code.controltower.Hello
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.HelloControlTower
services:
controltower: {ListBaselines}
@@ -26,6 +34,14 @@ controltower_ListBaselines:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListBaselines
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListBaselines
services:
controltower: {ListBaselines}
@@ -40,6 +56,14 @@ controltower_ListEnabledBaselines:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListEnabledBaselines
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListEnabledBaselines
services:
controltower: {ListEnabledBaselines}
@@ -54,6 +78,14 @@ controltower_EnableBaseline:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.EnableBaseline
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.EnableBaseline
services:
controltower: {EnableBaseline}
@@ -68,6 +100,14 @@ controltower_ResetEnabledBaseline:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ResetEnabledBaseline
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ResetEnabledBaseline
services:
controltower: {ResetEnabledBaseline}
@@ -82,6 +122,14 @@ controltower_DisableBaseline:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.DisableBaseline
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.DisableBaseline
services:
controltower: {DisableBaseline}
@@ -96,6 +144,14 @@ controltower_ListEnabledControls:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListEnabledControls
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListEnabledControls
services:
controltower: {ListEnabledControls}
@@ -110,6 +166,14 @@ controltower_EnableControl:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.EnableControl
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.EnableControl
services:
controltower: {EnableControl}
@@ -124,6 +188,14 @@ controltower_GetControlOperation:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.GetControlOperation
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.GetControlOperation
services:
controltower: {GetControlOperation}
@@ -138,6 +210,14 @@ controltower_DisableControl:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.DisableControl
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.DisableControl
services:
controltower: {DisableControl}
@@ -152,9 +232,39 @@ controltower_ListLandingZones:
snippet_tags:
- python.example_code.controltower.ControlTowerWrapper.decl
- python.example_code.controltower.ListLandingZones
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.ListLandingZones
services:
controltower: {ListLandingZones}
+controltower_GetBaselineOperation:
+ languages:
+ Python:
+ versions:
+ - sdk_version: 3
+ github: python/example_code/controltower
+ excerpts:
+ - description:
+ snippet_tags:
+ - python.example_code.controltower.ControlTowerWrapper.decl
+ - python.example_code.controltower.GetBaselineOperation
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description:
+ snippet_tags:
+ - ControlTower.dotnetv4.GetBaselineOperation
+ services:
+ controltower: {GetBaselineOperation}
+
controltower_Scenario:
synopsis_list:
- List landing zones.
@@ -172,5 +282,16 @@ controltower_Scenario:
snippet_tags:
- python.example_code.controltower.ControlTowerScenario
- python.example_code.controltower.ControlTowerWrapper.class
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/ControlTower
+ excerpts:
+ - description: Run an interactive scenario demonstrating &CTowerlong; features.
+ snippet_tags:
+ - ControlTower.dotnetv4.ControlTowerBasics
+ - description: Wrapper methods that are called by the scenario to manage &AUR; actions.
+ snippet_tags:
+ - ControlTower.dotnetv4.ControlTowerWrapper
services:
controltower: {CreateLandingZone, DeleteLandingZone, ListBaselines, ListEnabledBaselines, EnableBaseline, ResetEnabledBaseline, DisableBaseline, EnableControl, GetControlOperation, DisableControl, GetLandingZoneOperation, ListLandingZones, ListEnabledControls}
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
new file mode 100644
index 00000000000..d989156a2c3
--- /dev/null
+++ b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
new file mode 100644
index 00000000000..baff9a65f54
--- /dev/null
+++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs
@@ -0,0 +1,511 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper]
+
+using Amazon.ControlCatalog;
+using Amazon.ControlCatalog.Model;
+using Amazon.ControlTower;
+using Amazon.ControlTower.Model;
+using ValidationException = Amazon.ControlTower.Model.ValidationException;
+
+namespace ControlTowerActions;
+
+///
+/// Methods to perform AWS Control Tower actions.
+///
+public class ControlTowerWrapper
+{
+ private readonly IAmazonControlTower _controlTowerService;
+ private readonly IAmazonControlCatalog _controlCatalogService;
+
+ ///
+ /// Constructor for the wrapper class containing AWS Control Tower actions.
+ ///
+ /// The AWS Control Tower client object.
+ /// The AWS Control Catalog client object.
+ public ControlTowerWrapper(IAmazonControlTower controlTowerService, IAmazonControlCatalog controlCatalogService)
+ {
+ _controlTowerService = controlTowerService;
+ _controlCatalogService = controlCatalogService;
+ }
+
+ // snippet-start:[ControlTower.dotnetv4.ListLandingZones]
+ ///
+ /// List the AWS Control Tower landing zones for an account.
+ ///
+ /// A list of LandingZoneSummary objects.
+ public async Task> ListLandingZonesAsync()
+ {
+ try
+ {
+ var landingZones = new List();
+
+ var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest());
+
+ await foreach (var response in landingZonesPaginator.Responses)
+ {
+ landingZones.AddRange(response.LandingZones);
+ }
+
+ return landingZones;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't list landing zones. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListLandingZones]
+
+ // snippet-start:[ControlTower.dotnetv4.ListBaselines]
+ ///
+ /// List all baselines.
+ ///
+ /// A list of baseline summaries.
+ public async Task> ListBaselinesAsync()
+ {
+ try
+ {
+ var baselines = new List();
+
+ var baselinesPaginator = _controlTowerService.Paginators.ListBaselines(new ListBaselinesRequest());
+
+ await foreach (var response in baselinesPaginator.Responses)
+ {
+ baselines.AddRange(response.Baselines);
+ }
+
+ return baselines;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't list baselines. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListBaselines]
+
+ // snippet-start:[ControlTower.dotnetv4.ListEnabledBaselines]
+ ///
+ /// List all enabled baselines.
+ ///
+ /// A list of enabled baseline summaries.
+ public async Task> ListEnabledBaselinesAsync()
+ {
+ try
+ {
+ var enabledBaselines = new List();
+
+ var enabledBaselinesPaginator = _controlTowerService.Paginators.ListEnabledBaselines(new ListEnabledBaselinesRequest());
+
+ await foreach (var response in enabledBaselinesPaginator.Responses)
+ {
+ enabledBaselines.AddRange(response.EnabledBaselines);
+ }
+
+ return enabledBaselines;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't list enabled baselines. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListEnabledBaselines]
+
+ // snippet-start:[ControlTower.dotnetv4.EnableBaseline]
+ ///
+ /// Enable a baseline for the specified target.
+ ///
+ /// The ARN of the target.
+ /// The identifier of baseline to enable.
+ /// The version of baseline to enable.
+ /// The identifier of identity center baseline if it is enabled.
+ /// The enabled baseline ARN or null if already enabled.
+ public async Task EnableBaselineAsync(string targetIdentifier, string baselineIdentifier, string baselineVersion, string identityCenterBaseline)
+ {
+ try
+ {
+ var parameters = new List
+ {
+ new EnabledBaselineParameter
+ {
+ Key = "IdentityCenterEnabledBaselineArn",
+ Value = identityCenterBaseline
+ }
+ };
+
+ var request = new EnableBaselineRequest
+ {
+ BaselineIdentifier = baselineIdentifier,
+ BaselineVersion = baselineVersion,
+ TargetIdentifier = targetIdentifier,
+ Parameters = parameters
+ };
+
+ var response = await _controlTowerService.EnableBaselineAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetBaselineOperationAsync(operationId);
+ Console.WriteLine($"Baseline operation status: {status}");
+ if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return response.Arn;
+ }
+ catch (ValidationException ex) when (ex.Message.Contains("already enabled"))
+ {
+ Console.WriteLine("Baseline is already enabled for this target");
+ return null;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't enable baseline. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.EnableBaseline]
+
+ // snippet-start:[ControlTower.dotnetv4.DisableBaseline]
+ ///
+ /// Disable a baseline for a specific target and wait for the operation to complete.
+ ///
+ /// The identifier of the baseline to disable.
+ /// The operation ID or null if there was a conflict.
+ public async Task DisableBaselineAsync(string enabledBaselineIdentifier)
+ {
+ try
+ {
+ var request = new DisableBaselineRequest
+ {
+ EnabledBaselineIdentifier = enabledBaselineIdentifier
+ };
+
+ var response = await _controlTowerService.DisableBaselineAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetBaselineOperationAsync(operationId);
+ Console.WriteLine($"Baseline operation status: {status}");
+ if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (ConflictException ex)
+ {
+ Console.WriteLine($"Conflict disabling baseline: {ex.Message}. Skipping disable step.");
+ return null;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't disable baseline. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.DisableBaseline]
+
+ // snippet-start:[ControlTower.dotnetv4.ResetEnabledBaseline]
+ ///
+ /// Reset an enabled baseline for a specific target.
+ ///
+ /// The identifier of the enabled baseline to reset.
+ /// The operation ID.
+ public async Task ResetEnabledBaselineAsync(string enabledBaselineIdentifier)
+ {
+ try
+ {
+ var request = new ResetEnabledBaselineRequest
+ {
+ EnabledBaselineIdentifier = enabledBaselineIdentifier
+ };
+
+ var response = await _controlTowerService.ResetEnabledBaselineAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetBaselineOperationAsync(operationId);
+ Console.WriteLine($"Baseline operation status: {status}");
+ if (status == BaselineOperationStatus.SUCCEEDED || status == BaselineOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Target not found, unable to reset enabled baseline.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't reset enabled baseline. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ResetEnabledBaseline]
+
+ // snippet-start:[ControlTower.dotnetv4.GetBaselineOperation]
+ ///
+ /// Get the status of a baseline operation.
+ ///
+ /// The ID of the baseline operation.
+ /// The operation status.
+ public async Task GetBaselineOperationAsync(string operationId)
+ {
+ try
+ {
+ var request = new GetBaselineOperationRequest
+ {
+ OperationIdentifier = operationId
+ };
+
+ var response = await _controlTowerService.GetBaselineOperationAsync(request);
+ return response.BaselineOperation.Status;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Operation not found.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't get baseline operation status. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.GetBaselineOperation]
+
+ // snippet-start:[ControlTower.dotnetv4.ListEnabledControls]
+ ///
+ /// List enabled controls for a target organizational unit.
+ ///
+ /// The target organizational unit identifier.
+ /// A list of enabled control summaries.
+ public async Task> ListEnabledControlsAsync(string targetIdentifier)
+ {
+ try
+ {
+ var request = new ListEnabledControlsRequest
+ {
+ TargetIdentifier = targetIdentifier
+ };
+
+ var enabledControls = new List();
+
+ var enabledControlsPaginator = _controlTowerService.Paginators.ListEnabledControls(request);
+
+ await foreach (var response in enabledControlsPaginator.Responses)
+ {
+ enabledControls.AddRange(response.EnabledControls);
+ }
+
+ return enabledControls;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException ex) when (ex.Message.Contains("not registered with AWS Control Tower"))
+ {
+ Console.WriteLine("AWS Control Tower must be enabled to work with enabling controls.");
+ return new List();
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't list enabled controls. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListEnabledControls]
+
+ // snippet-start:[ControlTower.dotnetv4.EnableControl]
+ ///
+ /// Enable a control for a specified target.
+ ///
+ /// The ARN of the control to enable.
+ /// The identifier of the target (e.g., OU ARN).
+ /// The operation ID or null if already enabled.
+ public async Task EnableControlAsync(string controlArn, string targetIdentifier)
+ {
+ try
+ {
+ Console.WriteLine(controlArn);
+ Console.WriteLine(targetIdentifier);
+
+ var request = new EnableControlRequest
+ {
+ ControlIdentifier = controlArn,
+ TargetIdentifier = targetIdentifier
+ };
+
+ var response = await _controlTowerService.EnableControlAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetControlOperationAsync(operationId);
+ Console.WriteLine($"Control operation status: {status}");
+ if (status == ControlOperationStatus.SUCCEEDED || status == ControlOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (Amazon.ControlTower.Model.ValidationException ex) when (ex.Message.Contains("already enabled"))
+ {
+ Console.WriteLine("Control is already enabled for this target");
+ return null;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException ex) when (ex.Message.Contains("not registered with AWS Control Tower"))
+ {
+ Console.WriteLine("AWS Control Tower must be enabled to work with enabling controls.");
+ return null;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't enable control. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.EnableControl]
+
+ // snippet-start:[ControlTower.dotnetv4.DisableControl]
+ ///
+ /// Disable a control for a specified target.
+ ///
+ /// The ARN of the control to disable.
+ /// The identifier of the target (e.g., OU ARN).
+ /// The operation ID.
+ public async Task DisableControlAsync(string controlArn, string targetIdentifier)
+ {
+ try
+ {
+ var request = new DisableControlRequest
+ {
+ ControlIdentifier = controlArn,
+ TargetIdentifier = targetIdentifier
+ };
+
+ var response = await _controlTowerService.DisableControlAsync(request);
+ var operationId = response.OperationIdentifier;
+
+ // Wait for operation to complete
+ while (true)
+ {
+ var status = await GetControlOperationAsync(operationId);
+ Console.WriteLine($"Control operation status: {status}");
+ if (status == ControlOperationStatus.SUCCEEDED || status == ControlOperationStatus.FAILED)
+ {
+ break;
+ }
+ await Task.Delay(30000); // Wait 30 seconds
+ }
+
+ return operationId;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Control not found.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't disable control. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.DisableControl]
+
+ // snippet-start:[ControlTower.dotnetv4.GetControlOperation]
+ ///
+ /// Get the status of a control operation.
+ ///
+ /// The ID of the control operation.
+ /// The operation status.
+ public async Task GetControlOperationAsync(string operationId)
+ {
+ try
+ {
+ var request = new GetControlOperationRequest
+ {
+ OperationIdentifier = operationId
+ };
+
+ var response = await _controlTowerService.GetControlOperationAsync(request);
+ return response.ControlOperation.Status;
+ }
+ catch (Amazon.ControlTower.Model.ResourceNotFoundException)
+ {
+ Console.WriteLine("Operation not found.");
+ throw;
+ }
+ catch (AmazonControlTowerException ex)
+ {
+ Console.WriteLine($"Couldn't get control operation status. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.GetControlOperation]
+
+ // snippet-start:[ControlTower.dotnetv4.ListControls]
+ ///
+ /// List all controls in the Control Tower control catalog.
+ ///
+ /// A list of control summaries.
+ public async Task> ListControlsAsync()
+ {
+ try
+ {
+ var controls = new List();
+
+ var controlsPaginator = _controlCatalogService.Paginators.ListControls(new Amazon.ControlCatalog.Model.ListControlsRequest());
+
+ await foreach (var response in controlsPaginator.Responses)
+ {
+ controls.AddRange(response.Controls);
+ }
+
+ return controls;
+ }
+ catch (AmazonControlCatalogException ex)
+ {
+ Console.WriteLine($"Couldn't list controls. Here's why: {ex.ErrorCode}: {ex.Message}");
+ throw;
+ }
+ }
+
+ // snippet-end:[ControlTower.dotnetv4.ListControls]
+}
+
+// snippet-end:[ControlTower.dotnetv4.ControlTowerWrapper]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Actions/HelloControlTower.cs b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
new file mode 100644
index 00000000000..bab047bf390
--- /dev/null
+++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs
@@ -0,0 +1,75 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// snippet-start:[ControlTower.dotnetv4.HelloControlTower]
+
+using Amazon.ControlTower;
+using Amazon.ControlTower.Model;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Logging.Debug;
+using LogLevel = Microsoft.Extensions.Logging.LogLevel;
+
+namespace ControlTowerActions;
+
+///
+/// A class that introduces the AWS Control Tower by listing the
+/// available baselines for the account.
+///
+public class HelloControlTower
+{
+ private static ILogger logger = null!;
+
+ static async Task Main(string[] args)
+ {
+ // Set up dependency injection for AWS Control Tower.
+ using var host = Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(logging =>
+ logging.AddFilter("System", LogLevel.Debug)
+ .AddFilter("Microsoft", LogLevel.Information)
+ .AddFilter("Microsoft", LogLevel.Trace))
+ .ConfigureServices((_, services) =>
+ services.AddAWSService()
+ )
+ .Build();
+
+ logger = LoggerFactory.Create(builder => { builder.AddConsole(); })
+ .CreateLogger();
+
+ var amazonClient = host.Services.GetRequiredService();
+
+ Console.Clear();
+ Console.WriteLine("Hello, AWS Control Tower! Let's list available baselines:");
+ Console.WriteLine();
+
+ var baselines = new List();
+
+ try
+ {
+ var baselinesPaginator = amazonClient.Paginators.ListBaselines(new ListBaselinesRequest());
+
+ await foreach (var response in baselinesPaginator.Responses)
+ {
+ baselines.AddRange(response.Baselines);
+ }
+
+ Console.WriteLine($"{baselines.Count} baseline(s) retrieved.");
+ foreach (var baseline in baselines)
+ {
+ Console.WriteLine($"\t{baseline.Name}");
+ }
+ }
+ catch (Amazon.ControlTower.Model.AccessDeniedException)
+ {
+ Console.WriteLine("Access denied. Please ensure you have the necessary permissions.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"An error occurred: {ex.Message}");
+ }
+ }
+}
+
+// snippet-end:[ControlTower.dotnetv4.HelloControlTower]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/ControlTowerExamples.sln b/dotnetv4/ControlTower/ControlTowerExamples.sln
new file mode 100644
index 00000000000..b064902934c
--- /dev/null
+++ b/dotnetv4/ControlTower/ControlTowerExamples.sln
@@ -0,0 +1,47 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32630.192
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{7907FB6A-1353-4735-95DC-EEC5DF8C0649}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{B987097B-189C-4D0B-99BC-E67CD705BCA0}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5455D423-2AFC-4BC6-B79D-9DC4270D8F7D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerActions", "Actions\ControlTowerActions.csproj", "{796910FA-6E94-460B-8CB4-97DF01B9ADC8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerBasics", "Scenarios\ControlTower_Basics\ControlTowerBasics.csproj", "{B1731AE1-381F-4044-BEBE-269FF7E24B1F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlTowerTests", "Tests\ControlTowerTests.csproj", "{6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {796910FA-6E94-460B-8CB4-97DF01B9ADC8} = {7907FB6A-1353-4735-95DC-EEC5DF8C0649}
+ {B1731AE1-381F-4044-BEBE-269FF7E24B1F} = {B987097B-189C-4D0B-99BC-E67CD705BCA0}
+ {6046A2FC-6A39-4C2D-8DD9-AA3740B17B88} = {5455D423-2AFC-4BC6-B79D-9DC4270D8F7D}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {870D888D-5C8B-4057-8722-F73ECF38E513}
+ EndGlobalSection
+EndGlobal
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/README.md b/dotnetv4/ControlTower/README.md
new file mode 100644
index 00000000000..226fdc22170
--- /dev/null
+++ b/dotnetv4/ControlTower/README.md
@@ -0,0 +1,119 @@
+# AWS Control Tower code examples for the SDK for .NET (v4)
+
+## Overview
+
+Shows how to use the AWS SDK for .NET (v4) to work with AWS Control Tower.
+
+
+
+
+_AWS Control Tower enables you to enforce and manage governance rules for security, operations, and compliance at scale across all your organizations and accounts._
+
+## ⚠ Important
+
+* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/).
+* Running the tests might result in charges to your AWS account.
+* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege).
+* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services).
+
+
+
+
+## Code examples
+
+### Prerequisites
+
+For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder.
+
+
+
+
+
+### Get started
+
+- [Hello AWS Control Tower](Actions/HelloControlTower.cs#L4) (`ListBaselines`)
+
+
+### Basics
+
+Code examples that show you how to perform the essential operations within a service.
+
+- [Learn the basics](Scenarios/ControlTower_Basics/ControlTowerBasics.cs)
+
+
+### Single actions
+
+Code excerpts that show you how to call individual service functions.
+
+- [DisableBaseline](Actions/ControlTowerWrapper.cs#L181)
+- [DisableControl](Actions/ControlTowerWrapper.cs#L401)
+- [EnableBaseline](Actions/ControlTowerWrapper.cs#L120)
+- [EnableControl](Actions/ControlTowerWrapper.cs#L345)
+- [GetBaselineOperation](Actions/ControlTowerWrapper.cs#L273)
+- [GetControlOperation](Actions/ControlTowerWrapper.cs#L449)
+- [ListBaselines](Actions/ControlTowerWrapper.cs#L62)
+- [ListEnabledBaselines](Actions/ControlTowerWrapper.cs#L91)
+- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L305)
+- [ListLandingZones](Actions/ControlTowerWrapper.cs#L33)
+- [ResetEnabledBaseline](Actions/ControlTowerWrapper.cs#L227)
+
+
+
+
+
+## Run the examples
+
+### Instructions
+
+
+
+
+
+#### Hello AWS Control Tower
+
+This example shows you how to get started using AWS Control Tower.
+
+
+#### Learn the basics
+
+This example shows you how to do the following:
+
+- List landing zones.
+- List, enable, get, reset, and disable baselines.
+- List, enable, get, and disable controls.
+
+
+
+
+
+
+
+
+
+### Tests
+
+⚠ Running tests might result in charges to your AWS account.
+
+
+To find instructions for running these tests, see the [README](../README.md#Tests)
+in the `dotnetv4` folder.
+
+
+
+
+
+
+## Additional resources
+
+- [AWS Control Tower User Guide](https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html)
+- [AWS Control Tower API Reference](https://docs.aws.amazon.com/controltower/latest/APIReference/Welcome.html)
+- [SDK for .NET (v4) AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/Controltower/NControltower.html)
+
+
+
+
+---
+
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+SPDX-License-Identifier: Apache-2.0
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
new file mode 100644
index 00000000000..9d5e3e212da
--- /dev/null
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs
@@ -0,0 +1,295 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// snippet-start:[ControlTower.dotnetv4.ControlTowerBasics]
+
+using Amazon.ControlCatalog;
+using Amazon.ControlTower;
+using Amazon.ControlTower.Model;
+using Amazon.Organizations;
+using Amazon.Organizations.Model;
+using Amazon.SecurityToken;
+using Amazon.SecurityToken.Model;
+using ControlTowerActions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ControlTowerBasics;
+
+///
+/// Scenario class for AWS Control Tower basics.
+///
+public class ControlTowerBasics
+{
+ public static bool isInteractive = true;
+ public static ILogger logger = null!;
+ public static IAmazonOrganizations? orgClient = null;
+ public static IAmazonSecurityTokenService? stsClient = null;
+ public static ControlTowerWrapper? wrapper = null;
+ private static string? ouArn;
+ private static bool useLandingZone = false;
+
+ ///
+ /// Main entry point for the AWS Control Tower basics scenario.
+ ///
+ /// Command line arguments.
+ public static async Task Main(string[] args)
+ {
+ using var host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices((_, services) =>
+ services.AddAWSService()
+ .AddAWSService()
+ .AddAWSService()
+ .AddAWSService()
+ .AddTransient()
+ )
+ .Build();
+
+ logger = LoggerFactory.Create(builder => { builder.AddConsole(); })
+ .CreateLogger();
+
+ wrapper = host.Services.GetRequiredService();
+ orgClient = host.Services.GetRequiredService();
+ stsClient = host.Services.GetRequiredService();
+
+ await RunScenario();
+ }
+
+ ///
+ /// Runs the example scenario.
+ ///
+ public static async Task RunScenario()
+ {
+ Console.WriteLine(new string('-', 88));
+ Console.WriteLine("\tWelcome to the AWS Control Tower with ControlCatalog example scenario.");
+ Console.WriteLine(new string('-', 88));
+ Console.WriteLine("This demo will walk you through working with AWS Control Tower for landing zones,");
+ Console.WriteLine("managing baselines, and working with controls.");
+
+ try
+ {
+ var accountId = (await stsClient!.GetCallerIdentityAsync(new GetCallerIdentityRequest())).Account;
+ Console.WriteLine($"\nAccount ID: {accountId}");
+
+ Console.WriteLine("\nSome demo operations require the use of a landing zone.");
+ Console.WriteLine("You can use an existing landing zone or opt out of these operations in the demo.");
+ Console.WriteLine("For instructions on how to set up a landing zone,");
+ Console.WriteLine("see https://docs.aws.amazon.com/controltower/latest/userguide/getting-started-from-console.html");
+
+ // List available landing zones
+ var landingZones = await wrapper!.ListLandingZonesAsync();
+ if (landingZones.Count > 0)
+ {
+ Console.WriteLine("\nAvailable Landing Zones:");
+ for (int i = 0; i < landingZones.Count; i++)
+ {
+ Console.WriteLine($"{i + 1}. {landingZones[i].Arn}");
+ }
+
+ Console.Write($"\nDo you want to use the first landing zone in the list ({landingZones[0].Arn})? (y/n): ");
+ if (GetUserConfirmation())
+ {
+ useLandingZone = true;
+ Console.WriteLine($"Using landing zone: {landingZones[0].Arn}");
+ ouArn = await SetupOrganizationAsync();
+ }
+ }
+
+ // Managing Baselines
+ Console.WriteLine("\nManaging Baselines:");
+ var baselines = await wrapper.ListBaselinesAsync();
+ Console.WriteLine("\nListing available Baselines:");
+ BaselineSummary? controlTowerBaseline = null;
+ foreach (var baseline in baselines)
+ {
+ if (baseline.Name == "AWSControlTowerBaseline")
+ controlTowerBaseline = baseline;
+ Console.WriteLine($" - {baseline.Name}");
+ }
+
+ EnabledBaselineSummary? identityCenterBaseline = null;
+ string? baselineArn = null;
+
+ if (useLandingZone && ouArn != null)
+ {
+ Console.WriteLine("\nListing enabled baselines:");
+ var enabledBaselines = await wrapper.ListEnabledBaselinesAsync();
+ foreach (var baseline in enabledBaselines)
+ {
+ if (baseline.BaselineIdentifier.Contains("baseline/LN25R72TTG6IGPTQ"))
+ identityCenterBaseline = baseline;
+ Console.WriteLine($" - {baseline.BaselineIdentifier}");
+ }
+
+ if (controlTowerBaseline != null)
+ {
+ Console.Write("\nDo you want to enable the Control Tower Baseline? (y/n): ");
+ if (GetUserConfirmation())
+ {
+ Console.WriteLine("\nEnabling Control Tower Baseline.");
+ var icBaselineArn = identityCenterBaseline?.Arn;
+ baselineArn = await wrapper.EnableBaselineAsync(ouArn,
+ controlTowerBaseline.Arn, "4.0", icBaselineArn ?? "");
+ var alreadyEnabled = false;
+ if (baselineArn != null)
+ {
+ Console.WriteLine($"Enabled baseline ARN: {baselineArn}");
+ }
+ else
+ {
+ // Find the enabled baseline
+ foreach (var enabled in enabledBaselines)
+ {
+ if (enabled.BaselineIdentifier == controlTowerBaseline.Arn)
+ {
+ baselineArn = enabled.Arn;
+ break;
+ }
+ }
+
+ alreadyEnabled = true;
+ Console.WriteLine("No change, the selected baseline was already enabled.");
+ }
+
+ if (baselineArn != null)
+ {
+ Console.Write("\nDo you want to reset the Control Tower Baseline? (y/n): ");
+ if (GetUserConfirmation())
+ {
+ Console.WriteLine($"\nResetting Control Tower Baseline: {baselineArn}");
+ var operationId = await wrapper.ResetEnabledBaselineAsync(baselineArn);
+ Console.WriteLine($"Reset baseline operation id: {operationId}");
+ }
+
+ Console.Write("\nDo you want to disable the Control Tower Baseline? (y/n): ");
+ if (GetUserConfirmation())
+ {
+ Console.WriteLine($"Disabling baseline ARN: {baselineArn}");
+ var operationId = await wrapper.DisableBaselineAsync(baselineArn);
+ Console.WriteLine($"Disabled baseline operation id: {operationId}");
+ if (alreadyEnabled)
+ {
+ Console.WriteLine($"\nRe-enabling Control Tower Baseline: {baselineArn}");
+ // Re-enable the Control Tower baseline if it was originally enabled.
+ await wrapper.EnableBaselineAsync(ouArn,
+ controlTowerBaseline.Arn, "4.0", icBaselineArn ?? "");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Managing Controls
+ Console.WriteLine("\nManaging Controls:");
+ var controls = await wrapper.ListControlsAsync();
+ Console.WriteLine("\nListing first 5 available Controls:");
+ for (int i = 0; i < Math.Min(5, controls.Count); i++)
+ {
+ Console.WriteLine($"{i + 1}. {controls[i].Name} - {controls[i].Arn}");
+ }
+
+ if (useLandingZone && ouArn != null)
+ {
+ var enabledControls = await wrapper.ListEnabledControlsAsync(ouArn);
+ Console.WriteLine("\nListing enabled controls:");
+ for (int i = 0; i < enabledControls.Count; i++)
+ {
+ Console.WriteLine($"{i + 1}. {enabledControls[i].ControlIdentifier}");
+ }
+
+ // Find first non-enabled control
+ var enabledControlArns = enabledControls.Select(c => c.Arn).ToHashSet();
+ var controlArn = controls.FirstOrDefault(c => !enabledControlArns.Contains(c.Arn))?.Arn;
+
+ if (controlArn != null)
+ {
+ Console.Write($"\nDo you want to enable the control {controlArn}? (y/n): ");
+ if (GetUserConfirmation())
+ {
+ Console.WriteLine($"\nEnabling control: {controlArn}");
+ var operationId = await wrapper.EnableControlAsync(controlArn, ouArn);
+ if (operationId != null)
+ {
+ Console.WriteLine($"Enabled control with operation id: {operationId}");
+
+ Console.Write("\nDo you want to disable the control? (y/n): ");
+ if (GetUserConfirmation())
+ {
+ Console.WriteLine("\nDisabling the control...");
+ var disableOpId = await wrapper.DisableControlAsync(controlArn, ouArn);
+ Console.WriteLine($"Disable operation ID: {disableOpId}");
+ }
+ }
+ }
+ }
+ }
+
+ Console.WriteLine("\nThis concludes the example scenario.");
+ Console.WriteLine("Thanks for watching!");
+ Console.WriteLine(new string('-', 88));
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An error occurred during the Control Tower scenario.");
+ Console.WriteLine($"An error occurred: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Sets up AWS Organizations and creates or finds a Sandbox OU.
+ ///
+ /// The ARN of the Sandbox organizational unit.
+ private static async Task SetupOrganizationAsync()
+ {
+ Console.WriteLine("\nChecking organization status...");
+
+ try
+ {
+ var orgResponse = await orgClient!.DescribeOrganizationAsync(new DescribeOrganizationRequest());
+ var orgId = orgResponse.Organization.Id;
+ Console.WriteLine($"Account is part of organization: {orgId}");
+ }
+ catch (AWSOrganizationsNotInUseException)
+ {
+ Console.WriteLine("No organization found. Creating a new organization...");
+ var createResponse = await orgClient!.CreateOrganizationAsync(new CreateOrganizationRequest { FeatureSet = OrganizationFeatureSet.ALL });
+ var orgId = createResponse.Organization.Id;
+ Console.WriteLine($"Created new organization: {orgId}");
+ }
+
+ // Look for Sandbox OU
+ var roots = await orgClient.ListRootsAsync(new ListRootsRequest());
+ var rootId = roots.Roots[0].Id;
+
+ Console.WriteLine("Checking for Sandbox OU...");
+ var ous = await orgClient.ListOrganizationalUnitsForParentAsync(new ListOrganizationalUnitsForParentRequest { ParentId = rootId });
+ var sandboxOu = ous.OrganizationalUnits.FirstOrDefault(ou => ou.Name == "Sandbox");
+
+ if (sandboxOu == null)
+ {
+ Console.WriteLine("Creating Sandbox OU...");
+ var createOuResponse = await orgClient.CreateOrganizationalUnitAsync(new CreateOrganizationalUnitRequest { ParentId = rootId, Name = "Sandbox" });
+ sandboxOu = createOuResponse.OrganizationalUnit;
+ Console.WriteLine($"Created new Sandbox OU: {sandboxOu.Id}");
+ }
+ else
+ {
+ Console.WriteLine($"Found existing Sandbox OU: {sandboxOu.Id}");
+ }
+
+ return sandboxOu.Arn;
+ }
+
+ ///
+ /// Gets user confirmation by waiting for input or returning true if not interactive.
+ ///
+ /// True if user enters 'y' or if isInteractive is false, otherwise false.
+ private static bool GetUserConfirmation()
+ {
+ return Console.ReadLine()?.ToLower() == "y" || !isInteractive;
+ }
+}
+
+// snippet-end:[ControlTower.dotnetv4.ControlTowerBasics]
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
new file mode 100644
index 00000000000..78d5b798214
--- /dev/null
+++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ settings.json
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
new file mode 100644
index 00000000000..3aac260da5b
--- /dev/null
+++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs
@@ -0,0 +1,276 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.ControlCatalog;
+using Amazon.ControlCatalog.Model;
+using Amazon.ControlTower;
+using Amazon.ControlTower.Model;
+using Amazon.Organizations;
+using Amazon.Organizations.Model;
+using Amazon.Runtime;
+using Amazon.SecurityToken;
+using Amazon.SecurityToken.Model;
+using ControlTowerActions;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace ControlTowerTests;
+
+///
+/// Integration tests for the AWS Control Tower Basics scenario.
+///
+public class ControlTowerBasicsTests
+{
+ ///
+ /// Verifies the scenario with an integration test. No errors should be logged.
+ ///
+ /// Async task.
+ [Fact]
+ [Trait("Category", "Integration")]
+ public async Task TestScenarioIntegration()
+ {
+ // Arrange
+ ControlTowerBasics.ControlTowerBasics.isInteractive = false;
+ var loggerScenarioMock = new Mock>();
+
+ loggerScenarioMock.Setup(logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()
+ ));
+
+ // Act
+ ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
+
+ ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(new AmazonControlTowerClient(), new AmazonControlCatalogClient());
+ ControlTowerBasics.ControlTowerBasics.orgClient = new AmazonOrganizationsClient();
+ ControlTowerBasics.ControlTowerBasics.stsClient = new AmazonSecurityTokenServiceClient();
+
+
+ await ControlTowerBasics.ControlTowerBasics.RunScenario();
+
+ // Assert no errors logged
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
+ }
+
+ ///
+ /// Scenario test using mocked AWS service clients.
+ ///
+ /// Async task.
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task TestScenarioLogic()
+ {
+ // Arrange
+ var mockControlTower = new Mock();
+ var mockControlCatalog = new Mock();
+ var mockOrganizations = new Mock();
+ var mockSts = new Mock();
+
+ SetupMocks(mockControlTower, mockControlCatalog, mockOrganizations, mockSts);
+
+ ControlTowerBasics.ControlTowerBasics.isInteractive = false;
+ ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object);
+ ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object;
+ ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object;
+
+ var loggerScenarioMock = new Mock>();
+ ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
+
+ // Act
+ await ControlTowerBasics.ControlTowerBasics.RunScenario();
+
+ // Assert no errors logged
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
+ }
+
+ ///
+ /// Test scenario with ControlTowerException.
+ ///
+ /// Async task.
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task TestScenarioWithException()
+ {
+ // Arrange
+ var mockControlTower = new Mock();
+ var mockControlCatalog = new Mock();
+ var mockOrganizations = new Mock();
+ var mockSts = new Mock();
+
+ SetupMocks(mockControlTower, mockControlCatalog, mockOrganizations, mockSts, throwException: true);
+
+ ControlTowerBasics.ControlTowerBasics.isInteractive = false;
+ ControlTowerBasics.ControlTowerBasics.wrapper = new ControlTowerWrapper(mockControlTower.Object, mockControlCatalog.Object);
+ ControlTowerBasics.ControlTowerBasics.orgClient = mockOrganizations.Object;
+ ControlTowerBasics.ControlTowerBasics.stsClient = mockSts.Object;
+
+ var loggerScenarioMock = new Mock>();
+ ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object;
+
+ // Act
+ await ControlTowerBasics.ControlTowerBasics.RunScenario();
+
+ // Assert the error is logged.
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+
+ ///
+ /// Set up the mocks for testing.
+ ///
+ /// Mock ControlTower client.
+ /// Mock ControlCatalog client.
+ /// Mock Organizations client.
+ /// Mock Sts client.
+ /// True to force an exception.
+ private void SetupMocks(Mock mockControlTower, Mock mockControlCatalog, Mock mockOrganizations, Mock mockSts, bool throwException = false)
+ {
+ // Setup paginator mocks
+ var mockLandingZonesPaginator = new Mock();
+ var mockLandingZonesEnumerable = new Mock>();
+
+ mockLandingZonesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListLandingZonesResponse { LandingZones =
+ new List
+ {
+ new LandingZoneSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:landingzone/test-lz" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+
+
+ mockLandingZonesPaginator.Setup(x => x.Responses).Returns(mockLandingZonesEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListLandingZones(It.IsAny()))
+ .Returns(mockLandingZonesPaginator.Object);
+
+ var mockBaselinesPaginator = new Mock();
+ var mockBaselinesEnumerable = new Mock>();
+ mockBaselinesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListBaselinesResponse { Baselines =
+ new List
+ {
+ new BaselineSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:baseline/test-baseline", Name = "AWSControlTowerBaseline" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockBaselinesPaginator.Setup(x => x.Responses).Returns(mockBaselinesEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListBaselines(It.IsAny()))
+ .Returns(mockBaselinesPaginator.Object);
+
+ var mockEnabledBaselinesPaginator = new Mock();
+ var mockEnabledBaselinesEnumerable = new Mock>();
+ mockEnabledBaselinesEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListEnabledBaselinesResponse { EnabledBaselines =
+ new List
+ {
+ new EnabledBaselineSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-enabled", BaselineIdentifier = "baseline/LN25R72TTG6IGPTQ" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockEnabledBaselinesPaginator.Setup(x => x.Responses).Returns(mockEnabledBaselinesEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListEnabledBaselines(It.IsAny()))
+ .Returns(mockEnabledBaselinesPaginator.Object);
+
+ var mockEnabledControlsPaginator = new Mock();
+ var mockEnabledControlsEnumerable = new Mock>();
+ mockEnabledControlsEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new ListEnabledControlsResponse { EnabledControls =
+ new List
+ {
+ new EnabledControlSummary { Arn = "arn:aws:controltower:us-east-1:123456789012:control/test-control", ControlIdentifier = "arn:aws:controltower:us-east-1:123456789012:control/test-control-identifier" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockEnabledControlsPaginator.Setup(x => x.Responses).Returns(mockEnabledControlsEnumerable.Object);
+ mockControlTower.Setup(x => x.Paginators.ListEnabledControls(It.IsAny()))
+ .Returns(mockEnabledControlsPaginator.Object);
+
+ var mockControlsPaginator = new Mock();
+ var mockControlsEnumerable = new Mock>();
+ mockControlsEnumerable.Setup(x => x.GetAsyncEnumerator(CancellationToken.None))
+ .Returns(new List
+ {
+ new Amazon.ControlCatalog.Model.ListControlsResponse { Controls =
+ new List
+ {
+ new Amazon.ControlCatalog.Model.ControlSummary { Arn = "arn:aws:controlcatalog:us-east-1::control/ABCDEFG1234567", Name = "Test Control" }
+ } }
+ }.ToAsyncEnumerable().GetAsyncEnumerator());
+ mockControlsPaginator.Setup(x => x.Responses).Returns(mockControlsEnumerable.Object);
+ mockControlCatalog.Setup(x => x.Paginators.ListControls(It.IsAny()))
+ .Returns(mockControlsPaginator.Object);
+
+ // Force an exception that should end the scenario.
+ if (throwException)
+ {
+ mockControlTower.Setup(x =>
+ x.DisableBaselineAsync(It.IsAny(), default))
+ .Throws(new AmazonControlTowerException("Test exception"));
+ }
+ else
+ {
+ mockControlTower.Setup(x => x.DisableBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new DisableBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ }
+ mockControlTower.Setup(x =>
+ x.EnableBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new EnableBaselineResponse
+ {
+ Arn =
+ "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/test-baseline",
+ OperationIdentifier = "12345678-1234-1234-1234-123456789012"
+ });
+
+ mockControlTower.Setup(x => x.ResetEnabledBaselineAsync(It.IsAny(), default))
+ .ReturnsAsync(new ResetEnabledBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.GetBaselineOperationAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetBaselineOperationResponse { BaselineOperation = new BaselineOperation { Status = BaselineOperationStatus.SUCCEEDED } });
+ mockControlTower.Setup(x => x.EnableControlAsync(It.IsAny(), default))
+ .ReturnsAsync(new EnableControlResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.DisableControlAsync(It.IsAny(), default))
+ .ReturnsAsync(new DisableControlResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" });
+ mockControlTower.Setup(x => x.GetControlOperationAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetControlOperationResponse { ControlOperation = new ControlOperation { Status = ControlOperationStatus.SUCCEEDED } });
+ mockControlTower.Setup(x => x.GetLandingZoneAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetLandingZoneResponse { LandingZone = new LandingZoneDetail() });
+
+ // Setup Organizations mocks
+ mockOrganizations.Setup(x => x.DescribeOrganizationAsync(It.IsAny(), default))
+ .ReturnsAsync(new DescribeOrganizationResponse { Organization = new Organization { Id = "o-test123456" } });
+ mockOrganizations.Setup(x => x.ListRootsAsync(It.IsAny(), default))
+ .ReturnsAsync(new ListRootsResponse { Roots = new List { new Root { Id = "r-test123", Arn = "arn:aws:organizations::123456789012:root/o-test123456/r-test123" } } });
+ mockOrganizations.Setup(x => x.ListOrganizationalUnitsForParentAsync(It.IsAny(), default))
+ .ReturnsAsync(new ListOrganizationalUnitsForParentResponse { OrganizationalUnits = new List { new OrganizationalUnit { Id = "ou-test1234-abcd5678", Name = "Sandbox", Arn = "arn:aws:organizations::123456789012:ou/o-test123456/ou-test1234-abcd5678" } } });
+
+ // Setup STS mocks
+ mockSts.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), default))
+ .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" });
+ }
+}
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
new file mode 100644
index 00000000000..44b2474f876
--- /dev/null
+++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+ PreserveNewest
+ testsettings.json
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv4/ControlTower/Tests/Usings.cs b/dotnetv4/ControlTower/Tests/Usings.cs
new file mode 100644
index 00000000000..0f64a5599c7
--- /dev/null
+++ b/dotnetv4/ControlTower/Tests/Usings.cs
@@ -0,0 +1,7 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+global using Xunit;
+
+// Optional.
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
\ No newline at end of file
diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln
index 55584997279..57acbb70c60 100644
--- a/dotnetv4/DotNetV4Examples.sln
+++ b/dotnetv4/DotNetV4Examples.sln
@@ -137,6 +137,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECSScenario", "ECS\ECSScena
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECSActions", "ECS\ECSActions\ECSActions.csproj", "{7485EAED-F81C-4119-BABC-E009A21ACE46}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ControlTower", "ControlTower", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerTests", "ControlTower\Tests\ControlTowerTests.csproj", "{43C5E98B-5EC4-9F2B-2676-8F1E34969855}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{6BE1D9A4-1832-49F5-8682-6DEE4A7D6232}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerBasics", "ControlTower\Scenarios\ControlTower_Basics\ControlTowerBasics.csproj", "{6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlTowerActions", "ControlTower\Actions\ControlTowerActions.csproj", "{9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -335,6 +345,18 @@ Global
{7485EAED-F81C-4119-BABC-E009A21ACE46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7485EAED-F81C-4119-BABC-E009A21ACE46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7485EAED-F81C-4119-BABC-E009A21ACE46}.Release|Any CPU.Build.0 = Release|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -398,6 +420,10 @@ Global
{3F159C49-3DE7-42F5-AF14-E64C03AF19E8} = {EE6D1933-1E38-406A-B691-446326310D1F}
{D44D50E1-EC65-4A1C-AAA1-C360E4FC563F} = {EE6D1933-1E38-406A-B691-446326310D1F}
{7485EAED-F81C-4119-BABC-E009A21ACE46} = {EE6D1933-1E38-406A-B691-446326310D1F}
+ {43C5E98B-5EC4-9F2B-2676-8F1E34969855} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6} = {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232}
+ {9D601495-FDBA-C852-4ACB-EC54EDC9B3E5} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA}
diff --git a/python/example_code/controltower/README.md b/python/example_code/controltower/README.md
index 596edf11dee..726705ff9ef 100644
--- a/python/example_code/controltower/README.md
+++ b/python/example_code/controltower/README.md
@@ -56,6 +56,7 @@ Code excerpts that show you how to call individual service functions.
- [DisableControl](controltower_wrapper.py#L263)
- [EnableBaseline](controltower_wrapper.py#L69)
- [EnableControl](controltower_wrapper.py#L159)
+- [GetBaselineOperation](controltower_wrapper.py#L236)
- [GetControlOperation](controltower_wrapper.py#L209)
- [ListBaselines](controltower_wrapper.py#L39)
- [ListEnabledBaselines](controltower_wrapper.py#L330)