From 327448e1920c98b70dfc443575350c458f41394b Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:10:30 -0500 Subject: [PATCH 1/7] Adding structure. --- .../Actions/ControlTowerActions.csproj | 17 ++++ .../Actions/ControlTowerWrapper.cs | 93 ++++++++++++++++++ .../ControlTower/Actions/HelloControlTower.cs | 64 ++++++++++++ dotnetv4/ControlTower/Actions/Usings.cs | 13 +++ .../ControlTower/ControlTowerExamples.sln | 47 +++++++++ dotnetv4/ControlTower/README.md | 98 +++++++++++++++++++ .../ControlTowerBasics.csproj | 26 +++++ .../Tests/ControlTowerTests.csproj | 35 +++++++ dotnetv4/ControlTower/Tests/Usings.cs | 7 ++ 9 files changed, 400 insertions(+) create mode 100644 dotnetv4/ControlTower/Actions/ControlTowerActions.csproj create mode 100644 dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs create mode 100644 dotnetv4/ControlTower/Actions/HelloControlTower.cs create mode 100644 dotnetv4/ControlTower/Actions/Usings.cs create mode 100644 dotnetv4/ControlTower/ControlTowerExamples.sln create mode 100644 dotnetv4/ControlTower/README.md create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj create mode 100644 dotnetv4/ControlTower/Tests/ControlTowerTests.csproj create mode 100644 dotnetv4/ControlTower/Tests/Usings.cs diff --git a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj new file mode 100644 index 00000000000..b2276e2a524 --- /dev/null +++ b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj @@ -0,0 +1,17 @@ + + + + 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..870c87d17a2 --- /dev/null +++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper] +using System.Net; + +namespace ControlTowerActions; + +/// +/// Methods to perform AWS Control Tower actions. +/// +public class ControlTowerWrapper +{ + private readonly IAmazonControlTower _controlTowerService; + + /// + /// Constructor for the wrapper class containing AWS Control Tower actions. + /// + /// The AWS Control Tower client object. + public ControlTowerWrapper(IAmazonControlTower controlTowerService) + { + _controlTowerService = controlTowerService; + } + + // snippet-start:[ControlTower.dotnetv4.ListLandingZones] + /// + /// List the AWS Control Tower landing zones for an account. + /// + /// A list of LandingZoneSummary objects. + public async Task> ListLandingZonesAsync() + { + var landingZones = new List(); + + var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest()); + + await foreach (var response in landingZonesPaginator.Responses) + { + landingZones.AddRange(response.LandingZones); + } + + return landingZones; + } + + // snippet-end:[ControlTower.dotnetv4.ListLandingZones] + + // snippet-start:[ControlTower.dotnetv4.GetLandingZone] + /// + /// Get details about a specific landing zone. + /// + /// The landing zone identifier. + /// The landing zone details. + public async Task GetLandingZoneAsync(string landingZoneIdentifier) + { + var request = new GetLandingZoneRequest + { + LandingZoneIdentifier = landingZoneIdentifier + }; + + var response = await _controlTowerService.GetLandingZoneAsync(request); + return response.LandingZone; + } + + // snippet-end:[ControlTower.dotnetv4.GetLandingZone] + + // 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) + { + 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; + } + + // snippet-end:[ControlTower.dotnetv4.ListEnabledControls] +} + +// 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..d77c6dee60b --- /dev/null +++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[ControlTower.dotnetv4.HelloControlTower] + +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace ControlTowerActions; + +/// +/// A class that introduces the AWS Control Tower by listing the +/// landing zones 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() + .AddTransient() + ) + .Build(); + + logger = LoggerFactory.Create(builder => { builder.AddConsole(); }) + .CreateLogger(); + + var amazonClient = host.Services.GetRequiredService(); + + Console.Clear(); + Console.WriteLine("Hello AWS Control Tower."); + Console.WriteLine("Let's get a list of your AWS Control Tower landing zones."); + + var landingZones = new List(); + + var landingZonesPaginator = amazonClient.Paginators.ListLandingZones(new ListLandingZonesRequest()); + + await foreach (var response in landingZonesPaginator.Responses) + { + landingZones.AddRange(response.LandingZones); + } + + if (landingZones.Count > 0) + { + landingZones.ForEach(landingZone => + { + Console.WriteLine($"{landingZone.Arn}\t{landingZone.Status}"); + }); + } + else + { + Console.WriteLine("No landing zones were found."); + } + } +} + +// snippet-end:[ControlTower.dotnetv4.HelloControlTower] \ No newline at end of file diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs new file mode 100644 index 00000000000..4624044bcbf --- /dev/null +++ b/dotnetv4/ControlTower/Actions/Usings.cs @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[ControlTower.dotnetv4.Usings] +global using Amazon.ControlTower; +global using Amazon.ControlTower.Model; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging.Console; +global using Microsoft.Extensions.Logging.Debug; + +// snippet-end:[ControlTower.dotnetv4.Usings] \ 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..33f5b2ce51c --- /dev/null +++ b/dotnetv4/ControlTower/README.md @@ -0,0 +1,98 @@ +# AWS Control Tower code examples for the SDK for .NET + +## Overview + +Shows how to use the AWS SDK for .NET to work with AWS Control Tower. + + + + +*AWS Control Tower provides a pre-configured multi-account environment based on best practices to help organizations set up a secure, compliant, multi-account AWS environment.* + +## ⚠ 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#L15) (`ListLandingZones`) + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [GetLandingZone](Actions/ControlTowerWrapper.cs#L35) +- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L50) +- [ListLandingZones](Actions/ControlTowerWrapper.cs#L20) + + + + +## Run the examples + +### Instructions + +For general instructions to run the examples, see the [README](../../README.md#building-and-running-the-code-examples) in the `dotnetv4` folder. + +Some projects might include a settings file. Before compiling the project, you can change these settings to match your account and preferred Region. Alternatively, add a settings.local.json file with your preferred settings, which will be loaded automatically when the application runs. + +After the example compiles, you can run it from the command line. To do so, navigate to the folder that contains the .csproj file and run the following command: + +``` +dotnet run +``` + +Alternatively, you can run the example from within your IDE. + + + + +#### Hello AWS Control Tower + +This example shows you how to get started using AWS Control Tower. + +``` +dotnet run +``` + + + + +### 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) +- [AWS SDK for .NET AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/ControlTower/NControlTower.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 \ 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..779f4a83a70 --- /dev/null +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + PreserveNewest + settings.json + + + + \ 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..0e5a3a20150 --- /dev/null +++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj @@ -0,0 +1,35 @@ + + + + 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 From 1b983c61d4a057102c8a78a57ab9eaa79e19c0c1 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:39:25 -0500 Subject: [PATCH 2/7] Add Scenario files. --- .../Actions/ControlTowerActions.csproj | 1 + .../Actions/ControlTowerWrapper.cs | 468 ++++++++++++++++-- .../ControlTower/Actions/HelloControlTower.cs | 3 +- dotnetv4/ControlTower/Actions/Usings.cs | 2 + .../ControlTower_Basics/ControlTowerBasics.cs | 265 ++++++++++ .../ControlTowerBasics.csproj | 2 + .../Scenarios/ControlTower_Basics/Usings.cs | 5 + .../ControlTower_Basics/settings.json | 6 + .../Tests/ControlTowerBasicsTests.cs | 79 +++ .../Tests/ControlTowerTests.csproj | 1 + dotnetv4/ControlTower/Tests/testsettings.json | 6 + python/example_code/controltower/README.md | 134 +++++ .../controltower/controltower_wrapper.py | 453 +++++++++++++++++ .../controltower/hello/hello_controltower.py | 40 ++ .../controltower/requirements.txt | 4 + .../controltower/scenario_controltower.py | 315 ++++++++++++ .../controltower/test/conftest.py | 73 +++ .../controltower/test/test_scenario_run.py | 266 ++++++++++ 18 files changed, 2094 insertions(+), 29 deletions(-) create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs create mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json create mode 100644 dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs create mode 100644 dotnetv4/ControlTower/Tests/testsettings.json create mode 100644 python/example_code/controltower/README.md create mode 100644 python/example_code/controltower/controltower_wrapper.py create mode 100644 python/example_code/controltower/hello/hello_controltower.py create mode 100644 python/example_code/controltower/requirements.txt create mode 100644 python/example_code/controltower/scenario_controltower.py create mode 100644 python/example_code/controltower/test/conftest.py create mode 100644 python/example_code/controltower/test/test_scenario_run.py diff --git a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj index b2276e2a524..d989156a2c3 100644 --- a/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj +++ b/dotnetv4/ControlTower/Actions/ControlTowerActions.csproj @@ -9,6 +9,7 @@ + diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs index 870c87d17a2..2121133689d 100644 --- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs +++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper] -using System.Net; namespace ControlTowerActions; @@ -12,14 +11,17 @@ namespace ControlTowerActions; 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. - public ControlTowerWrapper(IAmazonControlTower controlTowerService) + /// The AWS Control Catalog client object. + public ControlTowerWrapper(IAmazonControlTower controlTowerService, IAmazonControlCatalog controlCatalogService) { _controlTowerService = controlTowerService; + _controlCatalogService = controlCatalogService; } // snippet-start:[ControlTower.dotnetv4.ListLandingZones] @@ -29,38 +31,270 @@ public ControlTowerWrapper(IAmazonControlTower controlTowerService) /// A list of LandingZoneSummary objects. public async Task> ListLandingZonesAsync() { - var landingZones = new List(); + try + { + var landingZones = new List(); + + var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest()); - var landingZonesPaginator = _controlTowerService.Paginators.ListLandingZones(new ListLandingZonesRequest()); + await foreach (var response in landingZonesPaginator.Responses) + { + landingZones.AddRange(response.LandingZones); + } - await foreach (var response in landingZonesPaginator.Responses) + return landingZones; + } + catch (AmazonControlTowerException ex) { - landingZones.AddRange(response.LandingZones); + Console.WriteLine($"Couldn't list landing zones. Here's why: {ex.ErrorCode}: {ex.Message}"); + throw; } - - return landingZones; } // snippet-end:[ControlTower.dotnetv4.ListLandingZones] - // snippet-start:[ControlTower.dotnetv4.GetLandingZone] + // snippet-start:[ControlTower.dotnetv4.ListBaselines] /// - /// Get details about a specific landing zone. + /// List all baselines. /// - /// The landing zone identifier. - /// The landing zone details. - public async Task GetLandingZoneAsync(string landingZoneIdentifier) + /// A list of baseline summaries. + public async Task> ListBaselinesAsync() { - var request = new GetLandingZoneRequest + try { - LandingZoneIdentifier = landingZoneIdentifier - }; + var baselines = new List(); + + var baselinesPaginator = _controlTowerService.Paginators.ListBaselines(new ListBaselinesRequest()); - var response = await _controlTowerService.GetLandingZoneAsync(request); - return response.LandingZone; + 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.GetLandingZone] + // 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 (Amazon.ControlTower.Model.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] /// @@ -70,24 +304,202 @@ public async Task GetLandingZoneAsync(string landingZoneIdent /// A list of enabled control summaries. public async Task> ListEnabledControlsAsync(string targetIdentifier) { - var request = new ListEnabledControlsRequest + try { - TargetIdentifier = targetIdentifier - }; + var request = new ListEnabledControlsRequest + { + TargetIdentifier = targetIdentifier + }; + + var enabledControls = new List(); - var enabledControls = new List(); + var enabledControlsPaginator = _controlTowerService.Paginators.ListEnabledControls(request); - var enabledControlsPaginator = _controlTowerService.Paginators.ListEnabledControls(request); + await foreach (var response in enabledControlsPaginator.Responses) + { + enabledControls.AddRange(response.EnabledControls); + } - await foreach (var response in enabledControlsPaginator.Responses) + return enabledControls; + } + catch (Amazon.ControlTower.Model.ResourceNotFoundException ex) when (ex.Message.Contains("not registered with AWS Control Tower")) { - enabledControls.AddRange(response.EnabledControls); + 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; } - - return enabledControls; } // 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 index d77c6dee60b..b46920240f4 100644 --- a/dotnetv4/ControlTower/Actions/HelloControlTower.cs +++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs @@ -25,6 +25,7 @@ static async Task Main(string[] args) .AddFilter("Microsoft", LogLevel.Trace)) .ConfigureServices((_, services) => services.AddAWSService() + .AddAWSService() .AddTransient() ) .Build(); @@ -51,7 +52,7 @@ static async Task Main(string[] args) { landingZones.ForEach(landingZone => { - Console.WriteLine($"{landingZone.Arn}\t{landingZone.Status}"); + Console.WriteLine($"Landing Zone \t{landingZone.Arn}"); }); } else diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs index 4624044bcbf..e0bb67d1aeb 100644 --- a/dotnetv4/ControlTower/Actions/Usings.cs +++ b/dotnetv4/ControlTower/Actions/Usings.cs @@ -4,6 +4,8 @@ // snippet-start:[ControlTower.dotnetv4.Usings] global using Amazon.ControlTower; global using Amazon.ControlTower.Model; +global using Amazon.ControlCatalog; +global using Amazon.ControlCatalog.Model; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs new file mode 100644 index 00000000000..5a8640b89fa --- /dev/null +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs @@ -0,0 +1,265 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[ControlTower.dotnetv4.ControlTowerBasics] + +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 +{ + private static ILogger logger = null!; + private static IAmazonOrganizations orgClient = null!; + private static string? ouArn; + private static bool useLandingZone = false; + + 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(); + + var wrapper = host.Services.GetRequiredService(); + orgClient = host.Services.GetRequiredService(); + var stsClient = host.Services.GetRequiredService(); + + 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 (Console.ReadLine()?.ToLower() == "y") + { + 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 (Console.ReadLine()?.ToLower() == "y") + { + 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 (Console.ReadLine()?.ToLower() == "y") + { + 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 (Console.ReadLine()?.ToLower() == "y") + { + 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 (Console.ReadLine()?.ToLower() == "y") + { + 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 (Console.ReadLine()?.ToLower() == "y") + { + 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}"); + } + } + + 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; + } +} + +// 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 index 779f4a83a70..78d5b798214 100644 --- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.csproj @@ -9,6 +9,8 @@ + + diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs new file mode 100644 index 00000000000..c8cc76f2bfa --- /dev/null +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +global using Amazon.ControlTower; +global using Amazon.ControlCatalog; \ No newline at end of file diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json new file mode 100644 index 00000000000..9a17f2b6ffc --- /dev/null +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json @@ -0,0 +1,6 @@ +{ + "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345", + "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED", + "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline", + "BaselineVersion": "1.0" +} \ 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..bf2533a0807 --- /dev/null +++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.ControlTower; +using Amazon.ControlCatalog; +using Microsoft.Extensions.Configuration; +using Moq; +using ControlTowerActions; + +namespace ControlTowerTests; + +/// +/// Tests for the ControlTowerWrapper class. +/// +public class ControlTowerBasicsTests +{ + private readonly IConfiguration _configuration; + + /// + /// Constructor for the test class. + /// + public ControlTowerBasicsTests() + { + _configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("testsettings.json") // Load test settings from JSON file. + .AddJsonFile("testsettings.local.json", + true) // Load local test settings from JSON file. + .Build(); + } + + /// + /// Test that ListLandingZonesAsync returns a list. + /// + [Fact] + public async Task ListLandingZonesAsync_ShouldReturnList() + { + // Arrange + var mockControlTowerService = new Mock(); + var mockControlCatalogService = new Mock(); + var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object); + + // Act & Assert + var exception = await Record.ExceptionAsync(() => wrapper.ListLandingZonesAsync()); + Assert.Null(exception); + } + + /// + /// Test that ListBaselinesAsync returns a list. + /// + [Fact] + public async Task ListBaselinesAsync_ShouldReturnList() + { + // Arrange + var mockControlTowerService = new Mock(); + var mockControlCatalogService = new Mock(); + var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object); + + // Act & Assert + var exception = await Record.ExceptionAsync(() => wrapper.ListBaselinesAsync()); + Assert.Null(exception); + } + + /// + /// Test that ListControlsAsync returns a list. + /// + [Fact] + public async Task ListControlsAsync_ShouldReturnList() + { + // Arrange + var mockControlTowerService = new Mock(); + var mockControlCatalogService = new Mock(); + var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object); + + // Act & Assert + var exception = await Record.ExceptionAsync(() => wrapper.ListControlsAsync()); + Assert.Null(exception); + } +} \ No newline at end of file diff --git a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj index 0e5a3a20150..caec21bafe6 100644 --- a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj +++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj @@ -10,6 +10,7 @@ + diff --git a/dotnetv4/ControlTower/Tests/testsettings.json b/dotnetv4/ControlTower/Tests/testsettings.json new file mode 100644 index 00000000000..9a17f2b6ffc --- /dev/null +++ b/dotnetv4/ControlTower/Tests/testsettings.json @@ -0,0 +1,6 @@ +{ + "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345", + "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED", + "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline", + "BaselineVersion": "1.0" +} \ No newline at end of file diff --git a/python/example_code/controltower/README.md b/python/example_code/controltower/README.md new file mode 100644 index 00000000000..0246c301ca9 --- /dev/null +++ b/python/example_code/controltower/README.md @@ -0,0 +1,134 @@ +# AWS Control Tower code examples for the SDK for Python + +## Overview + +Shows how to use the AWS SDK for Python (Boto3) 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 `python` folder. + +Install the packages required by these examples by running the following in a virtual environment: + +``` +python -m pip install -r requirements.txt +``` + + +Before running the example, set up a landing zone in order to run the baseline and control management sections. +Follow the instructions provided by the [quick start](https://docs.aws.amazon.com/controltower/latest/userguide/quick-start.html) guide. + + +### Get started + +- [Hello AWS Control Tower](hello/hello_controltower.py#L4) (`ListBaselines`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](scenario_controltower.py) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [DisableBaseline](controltower_wrapper.py#L365) +- [DisableControl](controltower_wrapper.py#L240) +- [EnableBaseline](controltower_wrapper.py#L64) +- [EnableControl](controltower_wrapper.py#L143) +- [GetControlOperation](controltower_wrapper.py#L186) +- [ListBaselines](controltower_wrapper.py#L36) +- [ListEnabledBaselines](controltower_wrapper.py#L305) +- [ListEnabledControls](controltower_wrapper.py#L401) +- [ListLandingZones](controltower_wrapper.py#L278) +- [ResetEnabledBaseline](controltower_wrapper.py#L332) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello AWS Control Tower + +This example shows you how to get started using AWS Control Tower. + +``` +python hello/hello_controltower.py +``` + +#### 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. + + + + +Start the example by running the following at a command prompt: + +``` +python scenario_controltower.py +``` + + + + + + +### 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 `python` 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 Python AWS Control Tower reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/controltower/controltower_wrapper.py b/python/example_code/controltower/controltower_wrapper.py new file mode 100644 index 00000000000..8034b5ebb14 --- /dev/null +++ b/python/example_code/controltower/controltower_wrapper.py @@ -0,0 +1,453 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging +import boto3 +import time + +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.controltower.ControlTowerWrapper.class] +# snippet-start:[python.example_code.controltower.ControlTowerWrapper.decl] + + +class ControlTowerWrapper: + """Encapsulates AWS Control Tower and Control Catalog functionality.""" + + def __init__(self, controltower_client, controlcatalog_client): + """ + :param controltower_client: A Boto3 Amazon ControlTower client. + :param controlcatalog_client: A Boto3 Amazon ControlCatalog client. + """ + self.controltower_client = controltower_client + self.controlcatalog_client = controlcatalog_client + + @classmethod + def from_client(cls): + controltower_client = boto3.client("controltower") + controlcatalog_client = boto3.client("controlcatalog") + return cls(controltower_client, controlcatalog_client) + + # snippet-end:[python.example_code.controltower.ControlTowerWrapper.decl] + + # snippet-start:[python.example_code.controltower.ListBaselines] + def list_baselines(self): + """ + Lists all baselines. + + :return: List of baselines. + :raises ClientError: If the listing operation fails. + """ + try: + paginator = self.controltower_client.get_paginator("list_baselines") + baselines = [] + for page in paginator.paginate(): + baselines.extend(page["baselines"]) + return baselines + + except ClientError as err: + if err.response["Error"]["Code"] == "AccessDeniedException": + logger.error( + "Access denied. Please ensure you have the necessary permissions." + ) + else: + logger.error( + "Couldn't list baselines. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.ListBaselines] + + # snippet-start:[python.example_code.controltower.EnableBaseline] + def enable_baseline( + self, + target_identifier, + identity_center_baseline, + baseline_identifier, + baseline_version, + ): + """ + Enables a baseline for the specified target if it's not already enabled. + + :param target_identifier: The ARN of the target. + :param baseline_identifier: The identifier of baseline to enable. + :param identity_center_baseline: The identifier of identity center baseline if it is enabled. + :param baseline_version: The version of baseline to enable. + :return: The enabled baseline ARN or None if already enabled. + :raises ClientError: If enabling the baseline fails for reasons other than it being already enabled. + """ + try: + response = self.controltower_client.enable_baseline( + baselineIdentifier=baseline_identifier, + baselineVersion=baseline_version, + targetIdentifier=target_identifier, + parameters=[ + { + "key": "IdentityCenterEnabledBaselineArn", + "value": identity_center_baseline, + } + ], + ) + + operation_id = response["operationIdentifier"] + while True: + status = self.get_baseline_operation(operation_id) + print(f"Baseline operation status: {status}") + if status in ["SUCCEEDED", "FAILED"]: + break + time.sleep(30) + + return response["arn"] + except ClientError as err: + if err.response["Error"]["Code"] == "ValidationException": + if "already enabled" in err.response["Error"]["Message"]: + print("Baseline is already enabled for this target") + return None + else: + print( + "Unable to enable baseline due to validation exception: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + logger.error( + "Couldn't enable baseline. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.EnableBaseline] + + # snippet-start:[python.example_code.controltower.ListControls] + def list_controls(self): + """ + Lists all controls in the Control Tower control catalog. + + :return: List of controls. + :raises ClientError: If the listing operation fails. + """ + try: + paginator = self.controlcatalog_client.get_paginator("list_controls") + controls = [] + for page in paginator.paginate(): + controls.extend(page["Controls"]) + return controls + + except ClientError as err: + if err.response["Error"]["Code"] == "AccessDeniedException": + logger.error( + "Access denied. Please ensure you have the necessary permissions." + ) + else: + logger.error( + "Couldn't list controls. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.ListControls] + + # snippet-start:[python.example_code.controltower.EnableControl] + def enable_control(self, control_arn, target_identifier): + """ + Enables a control for a specified target. + + :param control_arn: The ARN of the control to enable. + :param target_identifier: The identifier of the target (e.g., OU ARN). + :return: The operation ID. + :raises ClientError: If enabling the control fails. + """ + try: + print(control_arn) + print(target_identifier) + response = self.controltower_client.enable_control( + controlIdentifier=control_arn, targetIdentifier=target_identifier + ) + + operation_id = response["operationIdentifier"] + while True: + status = self.get_control_operation(operation_id) + print(f"Control operation status: {status}") + if status in ["SUCCEEDED", "FAILED"]: + break + time.sleep(30) + + return operation_id + + except ClientError as err: + if ( + err.response["Error"]["Code"] == "ValidationException" + and "already enabled" in err.response["Error"]["Message"] + ): + logger.info("Control is already enabled for this target") + return None + logger.error( + "Couldn't enable control. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.EnableControl] + + # snippet-start:[python.example_code.controltower.GetControlOperation] + def get_control_operation(self, operation_id): + """ + Gets the status of a control operation. + + :param operation_id: The ID of the control operation. + :return: The operation status. + :raises ClientError: If getting the operation status fails. + """ + try: + response = self.controltower_client.get_control_operation( + operationIdentifier=operation_id + ) + return response["controlOperation"]["status"] + except ClientError as err: + if err.response["Error"]["Code"] == "ResourceNotFoundException": + logger.error("Operation not found.") + else: + logger.error( + "Couldn't get control operation status. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.GetControlOperation] + + # snippet-start:[python.example_code.controltower.GetBaselineOperation] + def get_baseline_operation(self, operation_id): + """ + Gets the status of a baseline operation. + + :param operation_id: The ID of the baseline operation. + :return: The operation status. + :raises ClientError: If getting the operation status fails. + """ + try: + response = self.controltower_client.get_baseline_operation( + operationIdentifier=operation_id + ) + return response["baselineOperation"]["status"] + except ClientError as err: + if err.response["Error"]["Code"] == "ResourceNotFoundException": + logger.error("Operation not found.") + else: + logger.error( + "Couldn't get baseline operation status. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.GetBaselineOperation] + + # snippet-start:[python.example_code.controltower.DisableControl] + def disable_control(self, control_arn, target_identifier): + """ + Disables a control for a specified target. + + :param control_arn: The ARN of the control to disable. + :param target_identifier: The identifier of the target (e.g., OU ARN). + :return: The operation ID. + :raises ClientError: If disabling the control fails. + """ + try: + response = self.controltower_client.disable_control( + controlIdentifier=control_arn, targetIdentifier=target_identifier + ) + + operation_id = response["operationIdentifier"] + while True: + status = self.get_control_operation(operation_id) + print(f"Control operation status: {status}") + if status in ["SUCCEEDED", "FAILED"]: + break + time.sleep(30) + + return operation_id + except ClientError as err: + if err.response["Error"]["Code"] == "ResourceNotFoundException": + logger.error("Control not found.") + else: + logger.error( + "Couldn't disable control. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.DisableControl] + + # snippet-start:[python.example_code.controltower.ListLandingZones] + def list_landing_zones(self): + """ + Lists all landing zones. + + :return: List of landing zones. + :raises ClientError: If the listing operation fails. + """ + try: + paginator = self.controltower_client.get_paginator("list_landing_zones") + landing_zones = [] + for page in paginator.paginate(): + landing_zones.extend(page["landingZones"]) + return landing_zones + + except ClientError as err: + if err.response["Error"]["Code"] == "AccessDeniedException": + logger.error( + "Access denied. Please ensure you have the necessary permissions." + ) + else: + logger.error( + "Couldn't list landing zones. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.ListLandingZones] + + # snippet-start:[python.example_code.controltower.ListEnabledBaselines] + def list_enabled_baselines(self): + """ + Lists all enabled baselines. + + :return: List of enabled baselines. + :raises ClientError: If the listing operation fails. + """ + try: + paginator = self.controltower_client.get_paginator("list_enabled_baselines") + enabled_baselines = [] + for page in paginator.paginate(): + enabled_baselines.extend(page["enabledBaselines"]) + return enabled_baselines + + except ClientError as err: + if err.response["Error"]["Code"] == "ResourceNotFoundException": + logger.error("Target not found.") + else: + logger.error( + "Couldn't list enabled baselines. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.ListEnabledBaselines] + + # snippet-start:[python.example_code.controltower.ResetEnabledBaseline] + def reset_enabled_baseline(self, enabled_baseline_identifier): + """ + Resets an enabled baseline for a specific target. + + :param enabled_baseline_identifier: The identifier of the enabled baseline to reset. + :return: The operation ID. + :raises ClientError: If resetting the baseline fails. + """ + try: + response = self.controltower_client.reset_enabled_baseline( + enabledBaselineIdentifier=enabled_baseline_identifier + ) + operation_id = response["operationIdentifier"] + while True: + status = self.get_baseline_operation(operation_id) + print(f"Baseline operation status: {status}") + if status in ["SUCCEEDED", "FAILED"]: + break + time.sleep(30) + return operation_id + except ClientError as err: + if err.response["Error"]["Code"] == "ResourceNotFoundException": + logger.error("Target not found.") + else: + logger.error( + "Couldn't reset enabled baseline. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.ResetEnabledBaseline] + + # snippet-start:[python.example_code.controltower.DisableBaseline] + def disable_baseline(self, enabled_baseline_identifier): + """ + Disables a baseline for a specific target and waits for the operation to complete. + + :param enabled_baseline_identifier: The identifier of the baseline to disable. + :return: The operation ID. + :raises ClientError: If disabling the baseline fails. + """ + try: + response = self.controltower_client.disable_baseline( + enabledBaselineIdentifier=enabled_baseline_identifier + ) + + operation_id = response["operationIdentifier"] + while True: + status = self.get_baseline_operation(operation_id) + print(f"Baseline operation status: {status}") + if status in ["SUCCEEDED", "FAILED"]: + break + time.sleep(30) + + return response["operationIdentifier"] + except ClientError as err: + if err.response["Error"]["Code"] == "ConflictException": + print( + f"Conflict disabling baseline: {err.response['Error']['Message']}. Skipping disable step." + ) + return None + else: + logger.error( + "Couldn't disable baseline. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.DisableBaseline] + + # snippet-start:[python.example_code.controltower.ListEnabledControls] + def list_enabled_controls(self, target_identifier): + """ + Lists all enabled controls for a specific target. + + :param target_identifier: The identifier of the target (e.g., OU ARN). + :return: List of enabled controls. + :raises ClientError: If the listing operation fails. + """ + try: + paginator = self.controltower_client.get_paginator("list_enabled_controls") + enabled_controls = [] + for page in paginator.paginate(targetIdentifier=target_identifier): + enabled_controls.extend(page["enabledControls"]) + return enabled_controls + + except ClientError as err: + if err.response["Error"]["Code"] == "AccessDeniedException": + logger.error( + "Access denied. Please ensure you have the necessary permissions." + ) + else: + logger.error( + "Couldn't list enabled controls. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.controltower.ListEnabledControls] + + +# snippet-end:[python.example_code.controltower.ControlTowerWrapper.class] diff --git a/python/example_code/controltower/hello/hello_controltower.py b/python/example_code/controltower/hello/hello_controltower.py new file mode 100644 index 00000000000..6a4d0dc3c0e --- /dev/null +++ b/python/example_code/controltower/hello/hello_controltower.py @@ -0,0 +1,40 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start:[python.example_code.controltower.Hello] +import boto3 + + +def hello_controltower(controltower_client): + """ + Use the AWS SDK for Python (Boto3) to create an AWS Control Tower client + and list all available baselines. + This example uses the default settings specified in your shared credentials + and config files. + + :param controltower_client: A Boto3 AWS Control Tower Client object. This object wraps + the low-level AWS Control Tower service API. + """ + print("Hello, AWS Control Tower! Let's list available baselines:\n") + paginator = controltower_client.get_paginator("list_baselines") + page_iterator = paginator.paginate() + + baseline_names: [str] = [] + try: + for page in page_iterator: + for baseline in page["baselines"]: + baseline_names.append(baseline["name"]) + + print(f"{len(baseline_names)} baseline(s) retrieved.") + for baseline_name in baseline_names: + print(f"\t{baseline_name}") + + except controltower_client.exceptions.AccessDeniedException: + print("Access denied. Please ensure you have the necessary permissions.") + except Exception as e: + print(f"An error occurred: {str(e)}") + + +if __name__ == "__main__": + hello_controltower(boto3.client("controltower")) +# snippet-end:[python.example_code.controltower.Hello] diff --git a/python/example_code/controltower/requirements.txt b/python/example_code/controltower/requirements.txt new file mode 100644 index 00000000000..e74f0c584b9 --- /dev/null +++ b/python/example_code/controltower/requirements.txt @@ -0,0 +1,4 @@ +boto3>=1.26.79 +pytest>=7.2.1 +qrcode>=7.4.2 +pycognito>=2022.12.0 diff --git a/python/example_code/controltower/scenario_controltower.py b/python/example_code/controltower/scenario_controltower.py new file mode 100644 index 00000000000..83804892c52 --- /dev/null +++ b/python/example_code/controltower/scenario_controltower.py @@ -0,0 +1,315 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + + +import logging +import sys +import time + +import boto3 +from botocore.exceptions import ClientError + +from controltower_wrapper import ControlTowerWrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append("../..") +import demo_tools.question as q # noqa + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.controltower.ControlTowerScenario] +class ControlTowerScenario: + stack_name = "" + + def __init__(self, controltower_wrapper, org_client): + """ + :param controltower_wrapper: An instance of the ControlTowerWrapper class. + :param org_client: A Boto3 Organization client. + """ + self.controltower_wrapper = controltower_wrapper + self.org_client = org_client + self.stack = None + self.ou_id = None + self.ou_arn = None + self.account_id = None + self.landing_zone_id = None + self.use_landing_zone = False + + def run_scenario(self): + print("-" * 88) + print( + "\tWelcome to the AWS Control Tower with ControlCatalog example scenario." + ) + print("-" * 88) + + print( + "This demo will walk you through working with AWS Control Tower for landing zones," + ) + print("managing baselines, and working with controls.") + + self.account_id = boto3.client("sts").get_caller_identity()["Account"] + + print( + "Some demo operations require the use of a landing zone. " + "\nYou can use an existing landing zone or opt out of these operations in the demo." + "\nFor instructions on how to set up a landing zone, " + "\nsee https://docs.aws.amazon.com/controltower/latest/userguide/getting-started-from-console.html" + ) + # List available landing zones + landing_zones = self.controltower_wrapper.list_landing_zones() + if landing_zones: + print("\nAvailable Landing Zones:") + for i, lz in enumerate(landing_zones, 1): + print(f"{i} {lz['arn']})") + + # Ask if user wants to use the first landing zone in the list + if q.ask( + f"Do you want to use the first landing zone in the list ({landing_zones[0]['arn']})? (y/n) ", + q.is_yesno, + ): + self.use_landing_zone = True + self.landing_zone_id = landing_zones[0]["arn"] + print(f"Using landing zone ID: {self.landing_zone_id})") + # Set up organization and get Sandbox OU ID. + sandbox_ou_id = self.setup_organization() + # Store the OU ID for use in the CloudFormation template. + self.ou_id = sandbox_ou_id + elif q.ask( + f"Do you want to use a different existing Landing Zone for this demo? (y/n) ", + q.is_yesno, + ): + self.use_landing_zone = True + self.landing_zone_id = q.ask("Enter landing zone id: ", q.non_empty) + # Set up organization and get Sandbox OU ID. + sandbox_ou_id = self.setup_organization() + # Store the OU ID for use in the CloudFormation template. + self.ou_id = sandbox_ou_id + + # List and Enable Baseline. + print("\nManaging Baselines:") + control_tower_baseline = None + identity_center_baseline = None + baselines = self.controltower_wrapper.list_baselines() + print("\nListing available Baselines:") + for baseline in baselines: + if baseline["name"] == "AWSControlTowerBaseline": + control_tower_baseline = baseline + print(f"{baseline['name']}") + + if self.use_landing_zone: + print("\nListing enabled baselines:") + enabled_baselines = self.controltower_wrapper.list_enabled_baselines() + for baseline in enabled_baselines: + # If the Identity Center baseline is enabled, the identifier must be used for other baselines. + if "baseline/LN25R72TTG6IGPTQ" in baseline["baselineIdentifier"]: + identity_center_baseline = baseline + print(f"{baseline['baselineIdentifier']}") + + if q.ask( + f"Do you want to enable the Control Tower Baseline? (y/n) ", + q.is_yesno, + ): + print("\nEnabling Control Tower Baseline.") + ic_baseline_arn = ( + identity_center_baseline["arn"] + if identity_center_baseline + else None + ) + baseline_arn = self.controltower_wrapper.enable_baseline( + self.ou_arn, ic_baseline_arn, control_tower_baseline["arn"], "4.0" + ) + if baseline_arn: + print(f"Enabled baseline ARN: {baseline_arn}") + else: + # Find the enabled baseline so we can reset it. + for enabled_baseline in enabled_baselines: + if ( + enabled_baseline["baselineIdentifier"] + == control_tower_baseline["arn"] + ): + baseline_arn = enabled_baseline["arn"] + print("No change, the selected baseline was already enabled.") + + if q.ask( + f"Do you want to reset the Control Tower Baseline? (y/n) ", + q.is_yesno, + ): + print(f"\nResetting Control Tower Baseline. {baseline_arn}") + operation_id = self.controltower_wrapper.reset_enabled_baseline( + baseline_arn + ) + print(f"\nReset baseline operation id {operation_id}.") + + if baseline_arn and q.ask( + f"Do you want to disable the Control Tower Baseline? (y/n) ", + q.is_yesno, + ): + print(f"Disabling baseline ARN: {baseline_arn}") + operation_id = self.controltower_wrapper.disable_baseline( + baseline_arn + ) + print(f"\nDisabled baseline operation id {operation_id}.") + + # List and Enable Controls. + print("\nManaging Controls:") + controls = self.controltower_wrapper.list_controls() + print("\nListing first 5 available Controls:") + for i, control in enumerate(controls[:5], 1): + print(f"{i}. {control['Name']} - {control['Arn']}") + + if self.use_landing_zone: + target_ou = self.ou_arn + enabled_controls = self.controltower_wrapper.list_enabled_controls( + target_ou + ) + print("\nListing enabled controls:") + for i, control in enumerate(enabled_controls, 1): + print(f"{i}. {control['controlIdentifier']}") + + # Enable first non-enabled control as an example. + enabled_control_arns = [control["arn"] for control in enabled_controls] + control_arn = next( + control["Arn"] + for control in controls + if control["Arn"] not in enabled_control_arns + ) + + if control_arn and q.ask( + f"Do you want to enable the control {control_arn}? (y/n) ", + q.is_yesno, + ): + print(f"\nEnabling control: {control_arn}") + operation_id = self.controltower_wrapper.enable_control( + control_arn, target_ou + ) + + if operation_id: + print(f"Enabled control with operation id {operation_id}") + else: + print("Control is already enabled for this target") + + if q.ask( + f"Do you want to disable the control? (y/n) ", + q.is_yesno, + ): + print("\nDisabling the control...") + operation_id = self.controltower_wrapper.disable_control( + control_arn, target_ou + ) + print(f"Disable operation ID: {operation_id}") + + print("\nThis concludes the example scenario.") + + print("Thanks for watching!") + print("-" * 88) + + def setup_organization(self): + """ + Checks if the current account is part of an organization and creates one if needed. + Also ensures a Sandbox OU exists and returns its ID. + + :return: The ID of the Sandbox OU + """ + print("\nChecking organization status...") + + try: + # Check if account is part of an organization + org_response = self.org_client.describe_organization() + org_id = org_response["Organization"]["Id"] + print(f"Account is part of organization: {org_id}") + + except ClientError as error: + if error.response["Error"]["Code"] == "AWSOrganizationsNotInUseException": + print("No organization found. Creating a new organization...") + try: + create_response = self.org_client.create_organization( + FeatureSet="ALL" + ) + org_id = create_response["Organization"]["Id"] + print(f"Created new organization: {org_id}") + + # Wait for organization to be available. + waiter = self.org_client.get_waiter("organization_active") + waiter.wait( + Organization=org_id, + WaiterConfig={"Delay": 5, "MaxAttempts": 12}, + ) + + except ClientError as create_error: + logger.error( + "Couldn't create organization. Here's why: %s: %s", + create_error.response["Error"]["Code"], + create_error.response["Error"]["Message"], + ) + raise + else: + logger.error( + "Couldn't describe organization. Here's why: %s: %s", + error.response["Error"]["Code"], + error.response["Error"]["Message"], + ) + raise + + # Look for Sandbox OU. + sandbox_ou_id = None + paginator = self.org_client.get_paginator( + "list_organizational_units_for_parent" + ) + + try: + # Get root ID first. + roots = self.org_client.list_roots()["Roots"] + if not roots: + raise ValueError("No root found in organization") + root_id = roots[0]["Id"] + + # Search for existing Sandbox OU. + print("Checking for Sandbox OU...") + for page in paginator.paginate(ParentId=root_id): + for ou in page["OrganizationalUnits"]: + if ou["Name"] == "Sandbox": + sandbox_ou_id = ou["Id"] + self.ou_arn = ou["Arn"] + print(f"Found existing Sandbox OU: {sandbox_ou_id}") + break + if sandbox_ou_id: + break + + # Create Sandbox OU if it doesn't exist. + if not sandbox_ou_id: + print("Creating Sandbox OU...") + create_ou_response = self.org_client.create_organizational_unit( + ParentId=root_id, Name="Sandbox" + ) + sandbox_ou_id = create_ou_response["OrganizationalUnit"]["Id"] + print(f"Created new Sandbox OU: {sandbox_ou_id}") + + # Wait for OU to be available. + waiter = self.org_client.get_waiter("organizational_unit_active") + waiter.wait( + OrganizationalUnitId=sandbox_ou_id, + WaiterConfig={"Delay": 5, "MaxAttempts": 12}, + ) + + except ClientError as error: + logger.error( + "Couldn't set up Sandbox OU. Here's why: %s: %s", + error.response["Error"]["Code"], + error.response["Error"]["Message"], + ) + raise + + return sandbox_ou_id + + +if __name__ == "__main__": + try: + org = boto3.client("organizations") + control_tower_wrapper = ControlTowerWrapper.from_client() + + scenario = ControlTowerScenario(control_tower_wrapper, org) + scenario.run_scenario() + except Exception: + logging.exception("Something went wrong with the scenario.") +# snippet-end:[python.example_code.controltower.ControlTowerScenario] diff --git a/python/example_code/controltower/test/conftest.py b/python/example_code/controltower/test/conftest.py new file mode 100644 index 00000000000..7307c5c8313 --- /dev/null +++ b/python/example_code/controltower/test/conftest.py @@ -0,0 +1,73 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Contains common test fixtures used to run unit tests. +""" + +import sys +import os +import boto3 +import pytest + +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Add relative path to include ControlTowerWrapper. +sys.path.append(script_dir) +sys.path.append(os.path.dirname(script_dir)) +import scenario_controltower +from controltower_wrapper import ControlTowerWrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append(os.path.join(script_dir, "../..")) + +from test_tools.fixtures.common import * + + +class ScenarioData: + def __init__( + self, + controltower_client, + controlcatalog_client, + organizations_client, + controltower_stubber, + controlcatalog_stubber, + organizations_stubber, + ): + self.controltower_client = controltower_client + self.controlcatalog_client = controlcatalog_client + self.organizations_client = organizations_client + self.controltower_stubber = controltower_stubber + self.controlcatalog_stubber = controlcatalog_stubber + self.organizations_stubber = organizations_stubber + self.scenario = scenario_controltower.ControlTowerScenario( + controltower_wrapper=ControlTowerWrapper( + self.controltower_client, self.controlcatalog_client + ), + org_client=self.organizations_client, + ) + + +@pytest.fixture +def scenario_data(make_stubber): + controltower_client = boto3.client("controltower") + controlcatalog_client = boto3.client("controlcatalog") + organizations_client = boto3.client("organizations") + + controltower_stubber = make_stubber(controltower_client) + controlcatalog_stubber = make_stubber(controlcatalog_client) + organizations_stubber = make_stubber(organizations_client) + + return ScenarioData( + controltower_client, + controlcatalog_client, + organizations_client, + controltower_stubber, + controlcatalog_stubber, + organizations_stubber, + ) + + +@pytest.fixture +def mock_wait(monkeypatch): + return diff --git a/python/example_code/controltower/test/test_scenario_run.py b/python/example_code/controltower/test/test_scenario_run.py new file mode 100644 index 00000000000..5b54e68809d --- /dev/null +++ b/python/example_code/controltower/test/test_scenario_run.py @@ -0,0 +1,266 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Unit tests for the run_scenario method in scenario_controltower.py. +""" + +import pytest +from botocore.exceptions import ClientError +import datetime +import boto3 + +from example_code.controltower.controltower_wrapper import ControlTowerWrapper +from example_code.controltower.scenario_controltower import ControlTowerScenario + + +class MockManager: + def __init__(self, stub_runner, scenario_data, input_mocker): + self.scenario_data = scenario_data + self.account_id = "123456789012" + self.org_id = "o-exampleorgid" + self.root_id = "r-examplerootid" + self.sandbox_ou_id = "ou-exampleouid123456" + self.sandbox_ou_arn = ( + "arn:aws:organizations::123456789012:ou/o-exampleorgid/ou-exampleouid" + ) + self.landing_zone_arn = ( + "arn:aws:controltower:us-east-1:123456789012:landingzone/lz-example" + ) + self.operation_id = "op-1234567890abcdef01234567890abcdef" + self.baseline_operation_id = "op-1234567890abcdef01234567890abcdef" + self.stack_id = ( + "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abcdef" + ) + self.baseline_arn = "arn:aws:controltower:us-east-1:123456789012:baseline/AWSControlTowerBaseline" + self.enabled_baseline_arn = "arn:aws:controltower:us-east-1:123456789012:baseline/AWSControlTowerBaseline/isenabled" + self.control_arn = ( + "arn:aws:controlcatalog:us-east-1:123456789012:control/aws-control-1234" + ) + self.control_arn_enabled = ( + "arn:aws:controlcatalog:us-east-1:123456789012:control/aws-control-5678" + ) + + self.landing_zones = [{"arn": self.landing_zone_arn}] + + self.baselines = [{"name": "AWSControlTowerBaseline", "arn": self.baseline_arn}] + + self.enabled_baselines = [ + { + "targetIdentifier": self.sandbox_ou_arn, + "baselineIdentifier": self.enabled_baseline_arn, + "arn": self.baseline_arn, + "statusSummary": { + "status": "SUCCEEDED", + "lastOperationIdentifier": self.baseline_operation_id, + }, + } + ] + + self.controls = [ + { + "Arn": self.control_arn, + "Name": "TestControl1", + "Description": "Test control description", + } + ] + + self.enabled_controls = [ + { + "arn": self.control_arn_enabled, + "controlIdentifier": self.control_arn_enabled, + "statusSummary": { + "status": "SUCCEEDED", + "lastOperationIdentifier": self.baseline_operation_id, + }, + "targetIdentifier": self.sandbox_ou_id, + } + ] + + self.stub_runner = stub_runner + self.input_mocker = input_mocker + + def setup_stubs(self, error, stop_on, monkeypatch): + """Setup stubs for the scenario""" + # Mock user inputs + answers = [ + "y", # Use first landing zone in the list. + "y", # Enable baseline. + "y", # Reset baseline. + "y", # Disable baseline. + "y", # Enable control. + "y", # Disable control. + ] + self.input_mocker.mock_answers(answers) + + # Mock STS get_caller_identity + def mock_get_caller_identity(): + return {"Account": self.account_id} + + monkeypatch.setattr( + boto3.client("sts"), "get_caller_identity", mock_get_caller_identity + ) + + with self.stub_runner(error, stop_on) as runner: + # List landing zones + runner.add( + self.scenario_data.controltower_stubber.stub_list_landing_zones, + self.landing_zones, + ) + + # Organization setup + runner.add( + self.scenario_data.organizations_stubber.stub_describe_organization, + self.org_id, + ) + runner.add( + self.scenario_data.organizations_stubber.stub_list_roots, + [{"Id": self.root_id, "Name": "Root"}], + ) + runner.add( + self.scenario_data.organizations_stubber.stub_list_organizational_units_for_parent, + self.root_id, + [ + { + "Id": self.sandbox_ou_id, + "Name": "Sandbox", + "Arn": self.sandbox_ou_arn, + } + ], + ) + + # List and enable baselines + runner.add( + self.scenario_data.controltower_stubber.stub_list_baselines, + self.baselines, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_list_enabled_baselines, + self.enabled_baselines, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_enable_baseline, + self.baseline_arn, + "4.0", + self.sandbox_ou_arn, + self.enabled_baseline_arn, + self.baseline_operation_id, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_get_baseline_operation, + self.baseline_operation_id, + "SUCCEEDED", + ) + runner.add( + self.scenario_data.controltower_stubber.stub_reset_enabled_baseline, + self.enabled_baseline_arn, + self.baseline_operation_id, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_get_baseline_operation, + self.baseline_operation_id, + "SUCCEEDED", + ) + runner.add( + self.scenario_data.controltower_stubber.stub_disable_baseline, + self.enabled_baseline_arn, + self.baseline_operation_id, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_get_baseline_operation, + self.baseline_operation_id, + "SUCCEEDED", + ) + + # List and enable controls + runner.add( + self.scenario_data.controlcatalog_stubber.stub_list_controls, + self.controls, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_list_enabled_controls, + self.sandbox_ou_arn, + self.enabled_controls, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_enable_control, + self.control_arn, + self.sandbox_ou_arn, + self.operation_id, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_get_control_operation, + self.operation_id, + "SUCCEEDED", + ) + runner.add( + self.scenario_data.controltower_stubber.stub_disable_control, + self.control_arn, + self.sandbox_ou_arn, + self.operation_id, + ) + runner.add( + self.scenario_data.controltower_stubber.stub_get_control_operation, + self.operation_id, + "SUCCEEDED", + ) + + def setup_integ(self, error, stop_on): + """Set up the scenario for an integration test.""" + # Mock user inputs for using the suggested landing zone + answers = [ + "n", # Use first landing zone in the list. + "n", # Enable baseline. + ] + self.stub_runner = None + self.input_mocker.mock_answers(answers) + + +@pytest.fixture +def mock_mgr(stub_runner, scenario_data, input_mocker): + return MockManager(stub_runner, scenario_data, input_mocker) + + +# Define ANY constant for template body matching +ANY = object() + + +def test_run_scenario(mock_mgr, capsys, monkeypatch): + """Test the scenario that uses the suggested landing zone.""" + mock_mgr.setup_stubs(None, None, monkeypatch) + + # Run the scenario + mock_mgr.scenario_data + mock_mgr.scenario_data.scenario.run_scenario() + + # Verify the scenario completed successfully + captured = capsys.readouterr() + assert "This concludes the example scenario." in captured.out + + +@pytest.mark.integ +def test_run_scenario_integ(input_mocker, capsys): + """Test the scenario with an integration test.""" + answers = [ + "n", # Run the sections that don't require a landing zone. + "n", + ] + + input_mocker.mock_answers(answers) + controltower_client = boto3.client("controltower") + controlcatalog_client = boto3.client("controlcatalog") + organizations_client = boto3.client("organizations") + + scenario = ControlTowerScenario( + controltower_wrapper=ControlTowerWrapper( + controltower_client, controlcatalog_client + ), + org_client=organizations_client, + ) + + # Run the scenario + scenario.run_scenario() + + # Verify the scenario completed successfully + captured = capsys.readouterr() + assert "This concludes the example scenario." in captured.out From df6842738930ff852b31ec6b3d71287f612569d0 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:36:28 -0500 Subject: [PATCH 3/7] Update tests. --- .../Actions/ControlTowerWrapper.cs | 2 + .../ControlTower_Basics/ControlTowerBasics.cs | 60 +++-- .../Tests/ControlTowerBasicsTests.cs | 218 +++++++++++++++--- .../Tests/ControlTowerTests.csproj | 1 + 4 files changed, 228 insertions(+), 53 deletions(-) diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs index 2121133689d..8024080b2a4 100644 --- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs +++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs @@ -3,6 +3,8 @@ // snippet-start:[ControlTower.dotnetv4.ControlTowerWrapper] +using ValidationException = Amazon.ControlTower.Model.ValidationException; + namespace ControlTowerActions; /// diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs index 5a8640b89fa..b72e684272b 100644 --- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs @@ -20,12 +20,19 @@ namespace ControlTowerBasics; /// public class ControlTowerBasics { - private static ILogger logger = null!; - private static IAmazonOrganizations orgClient = null!; + 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; - static async Task Main(string[] args) + /// + /// 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) => @@ -40,10 +47,18 @@ static async Task Main(string[] args) logger = LoggerFactory.Create(builder => { builder.AddConsole(); }) .CreateLogger(); - var wrapper = host.Services.GetRequiredService(); - orgClient = host.Services.GetRequiredService(); - var stsClient = host.Services.GetRequiredService(); + 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)); @@ -71,7 +86,7 @@ static async Task Main(string[] args) } Console.Write($"\nDo you want to use the first landing zone in the list ({landingZones[0].Arn})? (y/n): "); - if (Console.ReadLine()?.ToLower() == "y") + if (GetUserConfirmation()) { useLandingZone = true; Console.WriteLine($"Using landing zone: {landingZones[0].Arn}"); @@ -108,7 +123,7 @@ static async Task Main(string[] args) if (controlTowerBaseline != null) { Console.Write("\nDo you want to enable the Control Tower Baseline? (y/n): "); - if (Console.ReadLine()?.ToLower() == "y") + if (GetUserConfirmation()) { Console.WriteLine("\nEnabling Control Tower Baseline."); var icBaselineArn = identityCenterBaseline?.Arn; @@ -138,7 +153,7 @@ static async Task Main(string[] args) if (baselineArn != null) { Console.Write("\nDo you want to reset the Control Tower Baseline? (y/n): "); - if (Console.ReadLine()?.ToLower() == "y") + if (GetUserConfirmation()) { Console.WriteLine($"\nResetting Control Tower Baseline: {baselineArn}"); var operationId = await wrapper.ResetEnabledBaselineAsync(baselineArn); @@ -146,7 +161,7 @@ static async Task Main(string[] args) } Console.Write("\nDo you want to disable the Control Tower Baseline? (y/n): "); - if (Console.ReadLine()?.ToLower() == "y") + if (GetUserConfirmation()) { Console.WriteLine($"Disabling baseline ARN: {baselineArn}"); var operationId = await wrapper.DisableBaselineAsync(baselineArn); @@ -189,16 +204,16 @@ await wrapper.EnableBaselineAsync(ouArn, if (controlArn != null) { Console.Write($"\nDo you want to enable the control {controlArn}? (y/n): "); - if (Console.ReadLine()?.ToLower() == "y") + 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 (Console.ReadLine()?.ToLower() == "y") + if (GetUserConfirmation()) { Console.WriteLine("\nDisabling the control..."); var disableOpId = await wrapper.DisableControlAsync(controlArn, ouArn); @@ -220,10 +235,14 @@ await wrapper.EnableBaselineAsync(ouArn, } } + /// + /// 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()); @@ -241,11 +260,11 @@ private static async Task SetupOrganizationAsync() // 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..."); @@ -260,6 +279,15 @@ private static async Task SetupOrganizationAsync() 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/Tests/ControlTowerBasicsTests.cs b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs index bf2533a0807..69ef212ff29 100644 --- a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs +++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs @@ -1,16 +1,24 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using Amazon.ControlTower; 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.Configuration; +using Microsoft.Extensions.Logging; using Moq; -using ControlTowerActions; namespace ControlTowerTests; /// -/// Tests for the ControlTowerWrapper class. +/// Integration tests for the AWS Control Tower Basics scenario. /// public class ControlTowerBasicsTests { @@ -23,57 +31,193 @@ public ControlTowerBasicsTests() { _configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("testsettings.json") // Load test settings from JSON file. - .AddJsonFile("testsettings.local.json", - true) // Load local test settings from JSON file. .Build(); } /// - /// Test that ListLandingZonesAsync returns a list. + /// Verifies the scenario with an integration test. No errors should be logged. /// - [Fact] - public async Task ListLandingZonesAsync_ShouldReturnList() + /// Async task. + //[Fact] + //[Trait("Category", "Integration")] + public async Task TestScenarioIntegration() { // Arrange - var mockControlTowerService = new Mock(); - var mockControlCatalogService = new Mock(); - var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object); + ControlTowerBasics.ControlTowerBasics.isInteractive = false; + var loggerScenarioMock = new Mock>(); - // Act & Assert - var exception = await Record.ExceptionAsync(() => wrapper.ListLandingZonesAsync()); - Assert.Null(exception); - } + loggerScenarioMock.Setup(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>() + )); - /// - /// Test that ListBaselinesAsync returns a list. - /// - [Fact] - public async Task ListBaselinesAsync_ShouldReturnList() - { - // Arrange - var mockControlTowerService = new Mock(); - var mockControlCatalogService = new Mock(); - var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object); + // Act + ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object; + await ControlTowerBasics.ControlTowerBasics.Main(new string[] { "" }); - // Act & Assert - var exception = await Record.ExceptionAsync(() => wrapper.ListBaselinesAsync()); - Assert.Null(exception); + // 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 that ListControlsAsync returns a list. + /// Scenario test using mocked AWS service clients. /// + /// Async task. [Fact] - public async Task ListControlsAsync_ShouldReturnList() + [Trait("Category", "Unit")] + public async Task TestScenarioLogic() { // Arrange - var mockControlTowerService = new Mock(); - var mockControlCatalogService = new Mock(); - var wrapper = new ControlTowerWrapper(mockControlTowerService.Object, mockControlCatalogService.Object); + var mockControlTower = new Mock(); + var mockControlCatalog = new Mock(); + var mockOrganizations = new Mock(); + var mockSts = new Mock(); + + // 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); + + // Setup individual service method mocks + 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.DisableBaselineAsync(It.IsAny(), default)) + .ReturnsAsync(new DisableBaselineResponse { 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" }); + + 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>(); + + 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; + + // Act + await ControlTowerBasics.ControlTowerBasics.RunScenario(); - // Act & Assert - var exception = await Record.ExceptionAsync(() => wrapper.ListControlsAsync()); - Assert.Null(exception); + // 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); } } \ No newline at end of file diff --git a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj index caec21bafe6..44b2474f876 100644 --- a/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj +++ b/dotnetv4/ControlTower/Tests/ControlTowerTests.csproj @@ -14,6 +14,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive From 8b1518bbdfe2ce2578483c7d78937d1681c423e7 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:30:27 -0500 Subject: [PATCH 4/7] Updates to test and formatting. --- .../Actions/ControlTowerWrapper.cs | 2 +- dotnetv4/ControlTower/Actions/Usings.cs | 4 +- .../ControlTower_Basics/ControlTowerBasics.cs | 6 +- .../Scenarios/ControlTower_Basics/Usings.cs | 2 +- .../Tests/ControlTowerBasicsTests.cs | 157 ++++++++++++------ dotnetv4/ControlTower/Tests/testsettings.json | 6 - 6 files changed, 112 insertions(+), 65 deletions(-) delete mode 100644 dotnetv4/ControlTower/Tests/testsettings.json diff --git a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs index 8024080b2a4..9cafb90648d 100644 --- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs +++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs @@ -160,7 +160,7 @@ public async Task> ListEnabledBaselinesAsync() return response.Arn; } - catch (Amazon.ControlTower.Model.ValidationException ex) when (ex.Message.Contains("already enabled")) + catch (ValidationException ex) when (ex.Message.Contains("already enabled")) { Console.WriteLine("Baseline is already enabled for this target"); return null; diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs index e0bb67d1aeb..5d29be62880 100644 --- a/dotnetv4/ControlTower/Actions/Usings.cs +++ b/dotnetv4/ControlTower/Actions/Usings.cs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // snippet-start:[ControlTower.dotnetv4.Usings] -global using Amazon.ControlTower; -global using Amazon.ControlTower.Model; global using Amazon.ControlCatalog; global using Amazon.ControlCatalog.Model; +global using Amazon.ControlTower; +global using Amazon.ControlTower.Model; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs index b72e684272b..bf7487c4bdc 100644 --- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs @@ -47,9 +47,9 @@ public static async Task Main(string[] args) logger = LoggerFactory.Create(builder => { builder.AddConsole(); }) .CreateLogger(); - wrapper ??= host.Services.GetRequiredService(); - orgClient ??= host.Services.GetRequiredService(); - stsClient ??= host.Services.GetRequiredService(); + wrapper = host.Services.GetRequiredService(); + orgClient = host.Services.GetRequiredService(); + stsClient = host.Services.GetRequiredService(); await RunScenario(); } diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs index c8cc76f2bfa..e683f0911fd 100644 --- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs @@ -1,5 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +global using Amazon.ControlCatalog; global using Amazon.ControlTower; -global using Amazon.ControlCatalog; \ No newline at end of file diff --git a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs index 69ef212ff29..3aac260da5b 100644 --- a/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs +++ b/dotnetv4/ControlTower/Tests/ControlTowerBasicsTests.cs @@ -11,7 +11,6 @@ using Amazon.SecurityToken; using Amazon.SecurityToken.Model; using ControlTowerActions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Moq; @@ -22,24 +21,12 @@ namespace ControlTowerTests; /// public class ControlTowerBasicsTests { - private readonly IConfiguration _configuration; - - /// - /// Constructor for the test class. - /// - public ControlTowerBasicsTests() - { - _configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .Build(); - } - /// /// Verifies the scenario with an integration test. No errors should be logged. /// /// Async task. - //[Fact] - //[Trait("Category", "Integration")] + [Fact] + [Trait("Category", "Integration")] public async Task TestScenarioIntegration() { // Arrange @@ -56,7 +43,13 @@ public async Task TestScenarioIntegration() // Act ControlTowerBasics.ControlTowerBasics.logger = loggerScenarioMock.Object; - await ControlTowerBasics.ControlTowerBasics.Main(new string[] { "" }); + + 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( @@ -83,9 +76,82 @@ public async Task TestScenarioLogic() 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 { @@ -95,6 +161,8 @@ public async Task TestScenarioLogic() 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); @@ -159,11 +227,27 @@ public async Task TestScenarioLogic() mockControlCatalog.Setup(x => x.Paginators.ListControls(It.IsAny())) .Returns(mockControlsPaginator.Object); - // Setup individual service method mocks - 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.DisableBaselineAsync(It.IsAny(), default)) - .ReturnsAsync(new DisableBaselineResponse { OperationIdentifier = "12345678-1234-1234-1234-123456789012" }); + // 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)) @@ -188,36 +272,5 @@ public async Task TestScenarioLogic() // Setup STS mocks mockSts.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), default)) .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" }); - - 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>(); - - 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; - - // 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); } } \ No newline at end of file diff --git a/dotnetv4/ControlTower/Tests/testsettings.json b/dotnetv4/ControlTower/Tests/testsettings.json deleted file mode 100644 index 9a17f2b6ffc..00000000000 --- a/dotnetv4/ControlTower/Tests/testsettings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345", - "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED", - "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline", - "BaselineVersion": "1.0" -} \ No newline at end of file From bc7e69d62ce640e8e7ed2b6c10b3bb7c97f814fe Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:09:06 -0500 Subject: [PATCH 5/7] Updates to scenario. --- .doc_gen/metadata/controltower_metadata.yaml | 121 ++++++++++++++++++ .../Actions/ControlTowerWrapper.cs | 4 + .../ControlTower/Actions/HelloControlTower.cs | 46 ++++--- dotnetv4/ControlTower/Actions/Usings.cs | 15 --- dotnetv4/ControlTower/README.md | 75 +++++++---- .../ControlTower_Basics/ControlTowerBasics.cs | 10 +- .../Scenarios/ControlTower_Basics/Usings.cs | 5 - .../ControlTower_Basics/settings.json | 6 - dotnetv4/DotNetV4Examples.sln | 26 ++++ 9 files changed, 233 insertions(+), 75 deletions(-) delete mode 100644 dotnetv4/ControlTower/Actions/Usings.cs delete mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs delete mode 100644 dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json 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/ControlTowerWrapper.cs b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs index 9cafb90648d..baff9a65f54 100644 --- a/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs +++ b/dotnetv4/ControlTower/Actions/ControlTowerWrapper.cs @@ -3,6 +3,10 @@ // 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; diff --git a/dotnetv4/ControlTower/Actions/HelloControlTower.cs b/dotnetv4/ControlTower/Actions/HelloControlTower.cs index b46920240f4..bab047bf390 100644 --- a/dotnetv4/ControlTower/Actions/HelloControlTower.cs +++ b/dotnetv4/ControlTower/Actions/HelloControlTower.cs @@ -3,13 +3,20 @@ // 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 -/// landing zones for the account. +/// available baselines for the account. /// public class HelloControlTower { @@ -25,8 +32,6 @@ static async Task Main(string[] args) .AddFilter("Microsoft", LogLevel.Trace)) .ConfigureServices((_, services) => services.AddAWSService() - .AddAWSService() - .AddTransient() ) .Build(); @@ -36,28 +41,33 @@ static async Task Main(string[] args) var amazonClient = host.Services.GetRequiredService(); Console.Clear(); - Console.WriteLine("Hello AWS Control Tower."); - Console.WriteLine("Let's get a list of your AWS Control Tower landing zones."); + Console.WriteLine("Hello, AWS Control Tower! Let's list available baselines:"); + Console.WriteLine(); - var landingZones = new List(); + var baselines = new List(); - var landingZonesPaginator = amazonClient.Paginators.ListLandingZones(new ListLandingZonesRequest()); - - await foreach (var response in landingZonesPaginator.Responses) + try { - landingZones.AddRange(response.LandingZones); - } + var baselinesPaginator = amazonClient.Paginators.ListBaselines(new ListBaselinesRequest()); - if (landingZones.Count > 0) - { - landingZones.ForEach(landingZone => + await foreach (var response in baselinesPaginator.Responses) { - Console.WriteLine($"Landing Zone \t{landingZone.Arn}"); - }); + 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."); } - else + catch (Exception ex) { - Console.WriteLine("No landing zones were found."); + Console.WriteLine($"An error occurred: {ex.Message}"); } } } diff --git a/dotnetv4/ControlTower/Actions/Usings.cs b/dotnetv4/ControlTower/Actions/Usings.cs deleted file mode 100644 index 5d29be62880..00000000000 --- a/dotnetv4/ControlTower/Actions/Usings.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// snippet-start:[ControlTower.dotnetv4.Usings] -global using Amazon.ControlCatalog; -global using Amazon.ControlCatalog.Model; -global using Amazon.ControlTower; -global using Amazon.ControlTower.Model; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using Microsoft.Extensions.Logging.Console; -global using Microsoft.Extensions.Logging.Debug; - -// snippet-end:[ControlTower.dotnetv4.Usings] \ No newline at end of file diff --git a/dotnetv4/ControlTower/README.md b/dotnetv4/ControlTower/README.md index 33f5b2ce51c..9fd6f547974 100644 --- a/dotnetv4/ControlTower/README.md +++ b/dotnetv4/ControlTower/README.md @@ -1,13 +1,13 @@ -# AWS Control Tower code examples for the SDK for .NET +# AWS Control Tower code examples for the SDK for .NET (v4) ## Overview -Shows how to use the AWS SDK for .NET to work with AWS Control Tower. +Shows how to use the AWS SDK for .NET (v4) to work with AWS Control Tower. -*AWS Control Tower provides a pre-configured multi-account environment based on best practices to help organizations set up a secure, compliant, multi-account AWS environment.* +_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 @@ -23,22 +23,40 @@ Shows how to use the AWS SDK for .NET to work with AWS Control Tower. ### Prerequisites -For prerequisites, see the [README](../../README.md#Prerequisites) in the `dotnetv4` folder. +For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder. + ### Get started -- [Hello AWS Control Tower](Actions/HelloControlTower.cs#L15) (`ListLandingZones`) +- [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. -- [GetLandingZone](Actions/ControlTowerWrapper.cs#L35) -- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L50) -- [ListLandingZones](Actions/ControlTowerWrapper.cs#L20) +- [DisableBaseline](Actions/ControlTowerWrapper.cs#L177) +- [DisableControl](Actions/ControlTowerWrapper.cs#L397) +- [EnableBaseline](Actions/ControlTowerWrapper.cs#L116) +- [EnableControl](Actions/ControlTowerWrapper.cs#L341) +- [GetBaselineOperation](Actions/ControlTowerWrapper.cs#L269) +- [GetControlOperation](Actions/ControlTowerWrapper.cs#L445) +- [ListBaselines](Actions/ControlTowerWrapper.cs#L58) +- [ListEnabledBaselines](Actions/ControlTowerWrapper.cs#L87) +- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L301) +- [ListLandingZones](Actions/ControlTowerWrapper.cs#L29) +- [ResetEnabledBaseline](Actions/ControlTowerWrapper.cs#L223) + @@ -47,17 +65,6 @@ Code excerpts that show you how to call individual service functions. ### Instructions -For general instructions to run the examples, see the [README](../../README.md#building-and-running-the-code-examples) in the `dotnetv4` folder. - -Some projects might include a settings file. Before compiling the project, you can change these settings to match your account and preferred Region. Alternatively, add a settings.local.json file with your preferred settings, which will be loaded automatically when the application runs. - -After the example compiles, you can run it from the command line. To do so, navigate to the folder that contains the .csproj file and run the following command: - -``` -dotnet run -``` - -Alternatively, you can run the example from within your IDE. @@ -66,18 +73,32 @@ Alternatively, you can run the example from within your IDE. This example shows you how to get started using AWS Control Tower. -``` -dotnet run -``` - - +#### 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. + +To find instructions for running these tests, see the [README](../README.md#Tests) +in the `dotnetv4` folder. + + @@ -86,7 +107,7 @@ To find instructions for running these tests, see the [README](../../README.md#T - [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) -- [AWS SDK for .NET AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/ControlTower/NControlTower.html) +- [SDK for .NET (v4) AWS Control Tower reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/Controltower/NControltower.html) @@ -95,4 +116,4 @@ To find instructions for running these tests, see the [README](../../README.md#T Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs index bf7487c4bdc..9d5e3e212da 100644 --- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs +++ b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/ControlTowerBasics.cs @@ -3,6 +3,8 @@ // snippet-start:[ControlTower.dotnetv4.ControlTowerBasics] +using Amazon.ControlCatalog; +using Amazon.ControlTower; using Amazon.ControlTower.Model; using Amazon.Organizations; using Amazon.Organizations.Model; @@ -67,7 +69,7 @@ public static async Task RunScenario() try { - var accountId = (await stsClient.GetCallerIdentityAsync(new GetCallerIdentityRequest())).Account; + 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."); @@ -76,7 +78,7 @@ public static async Task RunScenario() 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(); + var landingZones = await wrapper!.ListLandingZonesAsync(); if (landingZones.Count > 0) { Console.WriteLine("\nAvailable Landing Zones:"); @@ -245,14 +247,14 @@ private static async Task SetupOrganizationAsync() try { - var orgResponse = await orgClient.DescribeOrganizationAsync(new DescribeOrganizationRequest()); + 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 createResponse = await orgClient!.CreateOrganizationAsync(new CreateOrganizationRequest { FeatureSet = OrganizationFeatureSet.ALL }); var orgId = createResponse.Organization.Id; Console.WriteLine($"Created new organization: {orgId}"); } diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs deleted file mode 100644 index e683f0911fd..00000000000 --- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/Usings.cs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -global using Amazon.ControlCatalog; -global using Amazon.ControlTower; diff --git a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json b/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json deleted file mode 100644 index 9a17f2b6ffc..00000000000 --- a/dotnetv4/ControlTower/Scenarios/ControlTower_Basics/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-example12345/ou-example12345", - "ControlArn": "arn:aws:controltower:us-east-1::control/AWS-GR_AUDIT_BUCKET_ENCRYPTION_ENABLED", - "BaselineIdentifier": "arn:aws:controltower:us-east-1::baseline/AWSControlTowerBaseline", - "BaselineVersion": "1.0" -} \ 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} From cd4a89c3e22b5bb2adf4db52ec5e2fae0e7b025b Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:42:13 -0500 Subject: [PATCH 6/7] Update README.md --- dotnetv4/ControlTower/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/dotnetv4/ControlTower/README.md b/dotnetv4/ControlTower/README.md index 9fd6f547974..226fdc22170 100644 --- a/dotnetv4/ControlTower/README.md +++ b/dotnetv4/ControlTower/README.md @@ -45,17 +45,17 @@ Code examples that show you how to perform the essential operations within a ser Code excerpts that show you how to call individual service functions. -- [DisableBaseline](Actions/ControlTowerWrapper.cs#L177) -- [DisableControl](Actions/ControlTowerWrapper.cs#L397) -- [EnableBaseline](Actions/ControlTowerWrapper.cs#L116) -- [EnableControl](Actions/ControlTowerWrapper.cs#L341) -- [GetBaselineOperation](Actions/ControlTowerWrapper.cs#L269) -- [GetControlOperation](Actions/ControlTowerWrapper.cs#L445) -- [ListBaselines](Actions/ControlTowerWrapper.cs#L58) -- [ListEnabledBaselines](Actions/ControlTowerWrapper.cs#L87) -- [ListEnabledControls](Actions/ControlTowerWrapper.cs#L301) -- [ListLandingZones](Actions/ControlTowerWrapper.cs#L29) -- [ResetEnabledBaseline](Actions/ControlTowerWrapper.cs#L223) +- [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) From 445bc9390cd7a7bf3fbf6c2f5cee185ef87d09f4 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:51:43 -0500 Subject: [PATCH 7/7] Fix up missing line in python README. --- python/example_code/controltower/README.md | 1 + 1 file changed, 1 insertion(+) 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)