Reduce default output width when listing certificates (#132)

When running `sestest list-certificates`, the friendly name takes up a lot of horizontal space, causing lines to wrap and making the output hard to read.

This change defaults to displaying only the DNS name, but allowing an `extra-columns` argument for specifying that subject and/or friendly names should also be displayed.

This also addresses the point raised in #130 that it should be easy to see certificates by DNS name so that it should be easy to scan for, e.g. localhost.
This commit is contained in:
peterbom 2019-02-28 11:52:49 +13:00 коммит произвёл Bevan Arps
Родитель 75ad9da767
Коммит b78d82385a
4 изменённых файлов: 125 добавлений и 50 удалений

Просмотреть файл

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
namespace Microsoft.Azure.Batch.SoftwareEntitlement.Common
@ -24,5 +25,24 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement.Common
}
}
}
/// <summary>
/// Attempt to parse an Enum value from a string in a case-insensitive manner.
/// </summary>
/// <typeparam name="TEnum">The type of the enum</typeparam>
/// <param name="value">The string value</param>
/// <returns>
/// A <see cref="Result{TOk,TError}"/> containing the enum value, or a string describing the parse error.
/// </returns>
public static Result<TEnum, string> ParseEnum<TEnum>(this string value) where TEnum : struct, Enum
{
if (Enum.TryParse<TEnum>(value, true, out var result))
{
// Successfully parsed the string
return result;
}
return $"Failed to parse {value} into {typeof(TEnum).Name}";
}
}
}

Просмотреть файл

@ -29,20 +29,21 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
/// <returns>Results of execution (0 = success).</returns>
public Task<int> Execute(ListCertificatesCommandLine commandLine)
{
var showCertsResult = TryParseShow(commandLine.Show);
var executeResult =
from showCerts in TryParseShow(commandLine.Show)
join extraColumns in TryParseExtraColumns(commandLine.ExtraColumns) on true equals true
select Execute(showCerts, extraColumns.ToList());
var exitCode = showCertsResult
.OnOk(Execute)
.Merge(errors =>
{
Logger.LogErrors(errors);
return ResultCodes.Failed;
});
var exitCode = executeResult.Merge(errors =>
{
Logger.LogErrors(errors);
return ResultCodes.Failed;
});
return Task.FromResult(exitCode);
}
private int Execute(ShowCertificates showCerts)
private int Execute(ShowCertificates showCerts, IList<ExtraColumn> extraColumns)
{
var now = DateTime.Now;
@ -65,7 +66,8 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
"Found {0} non-expired certificates with private keys that allow both encryption and signing",
candidates.Where(c => now < c.NotAfter
&& c.SupportsUse(X509KeyUsageFlags.DigitalSignature)
&& c.SupportsUse(X509KeyUsageFlags.DataEncipherment)));
&& c.SupportsUse(X509KeyUsageFlags.DataEncipherment)),
extraColumns);
}
if (showCerts == ShowCertificates.All
@ -76,7 +78,8 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
"Found {0} non-expired certificates with private keys that allow signing but not encryption",
candidates.Where(c => now < c.NotAfter
&& c.SupportsUse(X509KeyUsageFlags.DigitalSignature)
&& !c.SupportsUse(X509KeyUsageFlags.DataEncipherment)));
&& !c.SupportsUse(X509KeyUsageFlags.DataEncipherment)),
extraColumns);
}
if (showCerts == ShowCertificates.All
@ -87,7 +90,8 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
"Found {0} non-expired certificates with private keys that allow encryption but not signing",
candidates.Where(c => now < c.NotAfter
&& !c.SupportsUse(X509KeyUsageFlags.DigitalSignature)
&& c.SupportsUse(X509KeyUsageFlags.DataEncipherment)));
&& c.SupportsUse(X509KeyUsageFlags.DataEncipherment)),
extraColumns);
}
if (showCerts == ShowCertificates.All
@ -97,7 +101,8 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
"Found {0} non-expired certificates with private keys that allow neither encryption nor signing",
candidates.Where(c => now < c.NotAfter
&& !c.SupportsUse(X509KeyUsageFlags.DigitalSignature)
&& !c.SupportsUse(X509KeyUsageFlags.DataEncipherment)));
&& !c.SupportsUse(X509KeyUsageFlags.DataEncipherment)),
extraColumns);
}
if (showCerts == ShowCertificates.All
@ -115,26 +120,32 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
LogCertificates(
"Found {0} non-expired certificates with private keys that allow server authentication and are verified",
serverAuthCerts.Where(c => c.IsVerified).Select(c => c.Cert));
serverAuthCerts.Where(c => c.IsVerified).Select(c => c.Cert),
extraColumns);
LogCertificates(
"Found {0} non-expired certificates with private keys that allow server authentication and are NOT verified",
serverAuthCerts.Where(c => !c.IsVerified).Select(c => c.Cert));
serverAuthCerts.Where(c => !c.IsVerified).Select(c => c.Cert),
extraColumns);
}
if (showCerts == ShowCertificates.All)
{
LogCertificates(
"Found {0} expired certificates",
candidates.Where(c => now >= c.NotAfter));
candidates.Where(c => now >= c.NotAfter),
extraColumns);
}
return 0;
}
private void LogCertificates(string title, IEnumerable<X509Certificate2> certificates)
private void LogCertificates(
string title,
IEnumerable<X509Certificate2> certificates,
IList<ExtraColumn> extraColumns)
{
var rows = certificates.Select(DescribeCertificate).ToList();
var rows = certificates.Select(c => DescribeCertificate(c, extraColumns)).ToList();
if (rows.Count == 0)
{
@ -142,14 +153,30 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
return;
}
rows.Insert(0, new List<string>
var headers = new List<string>
{
"Name",
"Friendly Name",
"DNS Name",
"Thumbprint",
"Not Before",
"Not After"
});
};
foreach (var extraColumn in extraColumns)
{
switch (extraColumn)
{
case ExtraColumn.SubjectName:
headers.Add("Subject Name");
break;
case ExtraColumn.FriendlyName:
headers.Add("Friendly Name");
break;
default:
throw new InvalidOperationException($"Unexpected {typeof(ExtraColumn).Name} value: {extraColumn}");
}
}
rows.Insert(0, headers);
Logger.LogInformation("");
Logger.LogInformation(title, rows.Count - 1);
@ -158,8 +185,35 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
}
private static IList<string> DescribeCertificate(X509Certificate2 cert)
private static IList<string> DescribeCertificate(
X509Certificate2 cert,
IList<ExtraColumn> extraColumns)
{
const string dateFormat = "s";
var result = new List<string>
{
cert.GetNameInfo(X509NameType.DnsName, forIssuer: false),
cert.Thumbprint,
cert.NotBefore.ToString(dateFormat, CultureInfo.InvariantCulture),
cert.NotAfter.ToString(dateFormat, CultureInfo.InvariantCulture)
};
foreach (var extraColumn in extraColumns)
{
switch (extraColumn)
{
case ExtraColumn.SubjectName:
result.Add(cert.SubjectName.Name);
break;
case ExtraColumn.FriendlyName:
result.Add(cert.FriendlyName);
break;
default:
throw new InvalidOperationException($"Unexpected {typeof(ExtraColumn).Name} value: {extraColumn}");
}
}
var status = string.Empty;
if (cert.NotAfter < DateTime.Now)
{
@ -170,17 +224,9 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
status = "(not yet active)";
}
const string dateFormat = "s";
result.Add(status);
return new List<string>
{
cert.SubjectName.Name,
cert.FriendlyName,
cert.Thumbprint,
cert.NotBefore.ToString(dateFormat, CultureInfo.InvariantCulture),
cert.NotAfter.ToString(dateFormat, CultureInfo.InvariantCulture),
status
};
return result;
}
private static Result<ShowCertificates, ErrorSet> TryParseShow(string show)
@ -190,14 +236,8 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
return ShowCertificates.NonExpired;
}
if (Enum.TryParse<ShowCertificates>(show, true, out var result))
{
// Successfully parsed the string
return result;
}
return ErrorSet.Create(
$"Failed to recognize '{show}'; valid choices are: `nonexpired` (default), 'forsigning', 'forencrypting', 'expired', 'forserverauth', and 'all'.");
return show.ParseEnum<ShowCertificates>().OnError(_ => ErrorSet.Create(
$"Failed to recognize '{show}'; valid choices are: `nonexpired` (default), 'forsigning', 'forencrypting', 'forserverauth', 'expired', and 'all'."));
}
[Flags]
@ -210,5 +250,22 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
Expired = 16,
All = NonExpired | Expired | ForSigning | ForEncrypting | ForServerAuth
}
private static Result<IEnumerable<ExtraColumn>, ErrorSet> TryParseExtraColumns(
IEnumerable<string> extraColumnNames)
{
var results =
from colName in extraColumnNames
select colName.ParseEnum<ExtraColumn>().OnError(_ => ErrorSet.Create(
$"Failed to recognize '{colName}'; valid choices are: 'subjectname' and 'friendlyname'."));
return results.Reduce();
}
private enum ExtraColumn
{
SubjectName,
FriendlyName
}
}
}

Просмотреть файл

@ -1,3 +1,4 @@
using System.Collections.Generic;
using CommandLine;
namespace Microsoft.Azure.Batch.SoftwareEntitlement
@ -7,5 +8,8 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
{
[Option(HelpText = "Which certificates should be shown? (one of `nonexpired` (default), 'forsigning', 'forencrypting', 'expired', 'forserverauth', and 'all').")]
public string Show { get; set; }
[Option("extra-columns", HelpText = "Which extra columns should be shown? (comma separated, one or more of 'subjectname', and 'friendlyname')", Separator = ',')]
public IList<string> ExtraColumns { get; set; } = new List<string>();
}
}

Просмотреть файл

@ -190,14 +190,8 @@ namespace Microsoft.Azure.Batch.SoftwareEntitlement
return defaultLevel;
}
if (Enum.TryParse<LogLevel>(level, true, out var result))
{
// Successfully parsed the string
return result;
}
return ErrorSet.Create(
$"Failed to recognize {purpose} log level '{level}'; valid choices are: error, warning, information, and debug.");
return purpose.ParseEnum<LogLevel>().OnError(_ => ErrorSet.Create(
$"Failed to recognize {purpose} log level '{level}'; valid choices are: error, warning, information, and debug."));
}
}
}