Skip to content

Commit

Permalink
Add secret rotation tool (#5564)
Browse files Browse the repository at this point in the history
* Add secret rotation tool and documentation
  • Loading branch information
hallipr authored Feb 27, 2023
1 parent 54e579a commit aa46c1c
Show file tree
Hide file tree
Showing 50 changed files with 3,140 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<IsPackable>true</IsPackable>
<PackAsTool>true</PackAsTool>
<ToolCommandName>secrets</ToolCommandName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.21.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Configuration\Azure.Sdk.Tools.SecretRotation.Configuration.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Core\Azure.Sdk.Tools.SecretRotation.Core.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.AzureActiveDirectory\Azure.Sdk.Tools.SecretRotation.Stores.AzureActiveDirectory.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps\Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.Generic\Azure.Sdk.Tools.SecretRotation.Stores.Generic.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.KeyVault\Azure.Sdk.Tools.SecretRotation.Stores.KeyVault.csproj" />
</ItemGroup>

<!-- Hack to allow winforms dependency in a tools package -->
<!-- See https://github.com/dotnet/sdk/issues/12055 -->
<Target Name="HackBeforePackToolValidation" BeforeTargets="_PackToolValidation">
<PropertyGroup>
<TargetPlatformIdentifier></TargetPlatformIdentifier>
<UseWindowsForms>false</UseWindowsForms>
</PropertyGroup>
</Target>

<Target Name="HackAfterPackToolValidation" AfterTargets="_PackToolValidation">
<PropertyGroup>
<TargetPlatformIdentifier>Windows</TargetPlatformIdentifier>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using Azure.Sdk.Tools.SecretRotation.Configuration;
using Azure.Sdk.Tools.SecretRotation.Core;

namespace Azure.Sdk.Tools.SecretRotation.Cli.Commands;

public class RotateCommand : RotationCommandBase
{
private readonly Option<bool> allOption = new(new[] { "--all", "-a" }, "Rotate all secrets");
private readonly Option<string[]> secretsOption = new(new[] { "--secrets", "-s" }, "Rotate only the specified secrets");
private readonly Option<bool> expiringOption = new(new[] { "--expiring", "-e" }, "Only rotate expiring secrets");
private readonly Option<bool> whatIfOption = new(new[] { "--dry-run", "-d" }, "Preview the changes that will be made without submitting them.");

public RotateCommand() : base("rotate", "Rotate one, expiring or all secrets")
{
AddOption(this.expiringOption);
AddOption(this.whatIfOption);
AddOption(this.allOption);
AddOption(this.secretsOption);
AddValidator(ValidateOptions);
}

protected override async Task HandleCommandAsync(ILogger logger, RotationConfiguration rotationConfiguration,
InvocationContext invocationContext)
{
bool onlyRotateExpiring = invocationContext.ParseResult.GetValueForOption(this.expiringOption);
bool all = invocationContext.ParseResult.GetValueForOption(this.allOption);
bool whatIf = invocationContext.ParseResult.GetValueForOption(this.whatIfOption);

var timeProvider = new TimeProvider();

IEnumerable<RotationPlan> plans;

if (all)
{
plans = rotationConfiguration.GetAllRotationPlans(logger, timeProvider);
}
else
{
string[] secretNames = invocationContext.ParseResult.GetValueForOption(this.secretsOption)!;

plans = rotationConfiguration.GetRotationPlans(logger, secretNames, timeProvider);
}

foreach (RotationPlan plan in plans)
{
await plan.ExecuteAsync(onlyRotateExpiring, whatIf);
}
}

private void ValidateOptions(CommandResult commandResult)
{
bool secretsUsed = commandResult.FindResultFor(this.secretsOption) is not null;
bool allUsed = commandResult.FindResultFor(this.allOption) is not null;

if (!(secretsUsed || allUsed))
{
commandResult.ErrorMessage = "Either the --secrets or the --all option must be provided.";
}

if (secretsUsed && allUsed)
{
commandResult.ErrorMessage = "The --secrets and --all options cannot both be provided.";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using Azure.Identity;
using Azure.Sdk.Tools.SecretRotation.Configuration;
using Azure.Sdk.Tools.SecretRotation.Core;
using Azure.Sdk.Tools.SecretRotation.Stores.AzureActiveDirectory;
using Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps;
using Azure.Sdk.Tools.SecretRotation.Stores.Generic;
using Azure.Sdk.Tools.SecretRotation.Stores.KeyVault;
using Microsoft.Extensions.Logging.Console;

namespace Azure.Sdk.Tools.SecretRotation.Cli.Commands;

public abstract class RotationCommandBase : Command
{
private readonly Option<string> configOption = new(new[] { "--config", "-c" }, "Configuration path")
{
IsRequired = true,
Arity = ArgumentArity.ExactlyOne
};

private readonly Option<bool> verboseOption = new(new[] { "--verbose", "-v" }, "Verbose output");

protected RotationCommandBase(string name, string description) : base(name, description)
{
AddOption(this.configOption);
AddOption(this.verboseOption);
this.SetHandler(ParseAndHandleCommandAsync);
}

protected abstract Task HandleCommandAsync(ILogger logger, RotationConfiguration rotationConfiguration,
InvocationContext invocationContext);

private async Task ParseAndHandleCommandAsync(InvocationContext invocationContext)
{
string configPath = invocationContext.ParseResult.GetValueForOption(this.configOption)!;
bool verbose = invocationContext.ParseResult.GetValueForOption(this.verboseOption);

LogLevel logLevel = verbose ? LogLevel.Trace : LogLevel.Information;

ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder
.AddConsoleFormatter<SimplerConsoleFormatter, ConsoleFormatterOptions>()
.AddConsole(options => options.FormatterName = SimplerConsoleFormatter.FormatterName)
.SetMinimumLevel(logLevel));

ILogger logger = loggerFactory.CreateLogger(string.Empty);

logger.LogDebug("Parsing configuration");

// TODO: Pass a logger to the token so it can verbose log getting tokens.
var tokenCredential = new AzureCliCredential();

IDictionary<string, Func<StoreConfiguration, SecretStore>> secretStoreFactories =
GetDefaultSecretStoreFactories(tokenCredential, logger);

// TODO: Pass a logger to RotationConfiguration so it can verbose log when reading from files.
RotationConfiguration rotationConfiguration = RotationConfiguration.From(configPath, secretStoreFactories);

await HandleCommandAsync(logger, rotationConfiguration, invocationContext);
}

private static IDictionary<string, Func<StoreConfiguration, SecretStore>> GetDefaultSecretStoreFactories(
AzureCliCredential tokenCredential, ILogger logger)
{
return new Dictionary<string, Func<StoreConfiguration, SecretStore>>
{
[RandomStringGenerator.MappingKey] = RandomStringGenerator.GetSecretStoreFactory(logger),
[KeyVaultSecretStore.MappingKey] = KeyVaultSecretStore.GetSecretStoreFactory(tokenCredential, logger),
[KeyVaultCertificateStore.MappingKey] = KeyVaultCertificateStore.GetSecretStoreFactory(tokenCredential, logger),
[ManualActionStore.MappingKey] = ManualActionStore.GetSecretStoreFactory(logger, new ConsoleValueProvider()),
[ServiceConnectionParameterStore.MappingKey] = ServiceConnectionParameterStore.GetSecretStoreFactory(tokenCredential, logger),
[AadApplicationSecretStore.MappingKey] = AadApplicationSecretStore.GetSecretStoreFactory(tokenCredential, logger)
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.CommandLine.Invocation;
using System.Text.Json;
using Azure.Sdk.Tools.SecretRotation.Configuration;
using Azure.Sdk.Tools.SecretRotation.Core;

namespace Azure.Sdk.Tools.SecretRotation.Cli.Commands;

public class StatusCommand : RotationCommandBase
{
public StatusCommand() : base("status", "Show secret rotation status")
{
}

protected override async Task HandleCommandAsync(ILogger logger, RotationConfiguration rotationConfiguration,
InvocationContext invocationContext)
{
var timeProvider = new TimeProvider();
IEnumerable<RotationPlan> plans = rotationConfiguration.GetAllRotationPlans(logger, timeProvider);

foreach (RotationPlan plan in plans)
{
Console.WriteLine();
Console.WriteLine($"{plan.Name}:");

RotationPlanStatus status = await plan.GetStatusAsync();
Console.WriteLine(JsonSerializer.Serialize(status, new JsonSerializerOptions { WriteIndented = true }));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Text;
using System.Windows.Forms;
using Azure.Sdk.Tools.SecretRotation.Stores.Generic;

namespace Azure.Sdk.Tools.SecretRotation.Cli;

internal class ConsoleValueProvider : IUserValueProvider
{
public string GetValue(string prompt, bool secret)
{
Console.Write($"{prompt}: ");
StringBuilder keysPressed = new();
while (true)
{
ConsoleKeyInfo key = Console.ReadKey(secret);

switch (key.Key)
{
case ConsoleKey.Enter:
Console.WriteLine();
return keysPressed.ToString();
case ConsoleKey.Backspace:
{
if (keysPressed.Length > 0)
{
Console.Write(' ');
Console.Write(key.KeyChar);
keysPressed.Length -= 1;
}

break;
}
default:
keysPressed.Append(key.KeyChar);
break;
}
}
}

public void PromptUser(string prompt, string? oldValue, string? newValue)
{
Console.WriteLine();
Console.WriteLine(prompt);

bool promptNew = !string.IsNullOrEmpty(newValue);
bool promptOld = !string.IsNullOrEmpty(oldValue);

if (promptOld)
{
Console.WriteLine("Press Ctrl-O to copy the old value to the clipboard.");
}

if (promptNew)
{
Console.WriteLine("Press Ctrl-N to copy the new value to the clipboard.");
}

Console.WriteLine("Press Enter to continue.");

while (true)
{
ConsoleKeyInfo key = Console.ReadKey(true);

if (key is { Key: ConsoleKey.Enter })
{
break;
}

if (promptOld && key is { Modifiers: ConsoleModifiers.Control, Key: ConsoleKey.O })
{
SetClipboard(oldValue!);
}

if (promptNew && key is { Modifiers: ConsoleModifiers.Control, Key: ConsoleKey.N })
{
SetClipboard(newValue!);
}
}
}

private static void SetClipboard(string value)
{
Thread thread = new(() => Clipboard.SetText(value));
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Global using directives

global using Microsoft.Extensions.Logging;
Loading

0 comments on commit aa46c1c

Please sign in to comment.