From 6fb183a163102108a7fb02e4360b229eada53bce Mon Sep 17 00:00:00 2001 From: Sebastien Pouliot Date: Thu, 20 Oct 2022 16:43:48 -0400 Subject: [PATCH] Add `fingerprint` command, to both `assembly` and `appbundle` branches ... to uniquely identify assemblies and appbundles using `Info.plist` --- ChangeLog.md | 3 +- Commands/AppBundleFingerprintCommand.cs | 93 ++++++++++++++++++++ Commands/AssemblyFingerprintCommand.cs | 107 ++++++++++++++++++++++++ Program.cs | 4 + cilout.csproj | 1 + 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 Commands/AppBundleFingerprintCommand.cs create mode 100644 Commands/AssemblyFingerprintCommand.cs diff --git a/ChangeLog.md b/ChangeLog.md index ad67693..0c3da86 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,10 +1,11 @@ # ChangeLog -## 1.2.0 (unreleased) +## 1.2.0 (20 Oct 2022) ### Features - Add `appbundle` branch and [`is-trimmable`](https://github.com/spouliot/cilout/wiki/AppBundleIsTrimmable) command to detect any trimmable assembly inside the app bundle - Add `has-entrypoint` command, to both `assembly` and `appbundle` branches, to detect if the assembly has an entry point +- Add `fingerprint` command, to both `assembly` and `appbundle` branches, to uniquely identify assemblies and appbundle using `Info.plist` ### Updates - Updated Spectre.Console to 0.45.0 diff --git a/Commands/AppBundleFingerprintCommand.cs b/Commands/AppBundleFingerprintCommand.cs new file mode 100644 index 0000000..3841f6a --- /dev/null +++ b/Commands/AppBundleFingerprintCommand.cs @@ -0,0 +1,93 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Spectre.Console.Cli; +using Claunia.PropertyList; + +namespace CilOut.Commands; + +public sealed class AppBundleFingerprintCommand : Command { + + public sealed class Settings : AppBundleSettings { + + [Description ("Output using a diff-friendly, tab seperated values, format")] + [CommandOption ("-d|--diff-friendly")] + [DefaultValue (false)] + public bool DiffFriendly { get; init; } + } + + public override int Execute ([NotNull] CommandContext context, [NotNull] Settings settings) + { + var result = ReturnCodes.Success; + var appbundle = settings.AppBundle!; + SortedDictionary> fingerprints = new (); + foreach (var file in Directory.EnumerateFiles (appbundle, "*", SearchOption.AllDirectories)) { + switch (Path.GetExtension (file).ToLowerInvariant ()) { + case ".dll": + case ".exe": + var assembly_result = AssemblyFingerprintCommand.Fingerprint (file, out var minutiae); + if (assembly_result == ReturnCodes.Success) + fingerprints.Add (file, minutiae); + + if (assembly_result != ReturnCodes.Success) + result = assembly_result; + break; + case ".plist": + try { + fingerprints.Add (file, ParseInfoPlist (file)); + } + catch { + result = ReturnCodes.Failure; + } + break; + } + } + + foreach (var (file, minutiae) in fingerprints) { + var n = appbundle.Length; + if (file [n] == '/') + n++; + var assembly = file [n..]; + if (settings.DiffFriendly) { + AssemblyFingerprintCommand.TabSeparatedValues (assembly, minutiae); + } else { + AssemblyFingerprintCommand.ShowResult (assembly, result, minutiae); + } + } + + return (int) result; + } + + static SortedDictionary ParseInfoPlist (string filename) + { + SortedDictionary minutiae = new (); + NSDictionary? info = (NSDictionary) PropertyListParser.Parse (filename); + if (info is not null) { + foreach (var element in info) { + switch (element.Key) { + case "DTCompiler": + case "DTPlatformBuild": + case "DTPlatformName": + case "DTPlatformVersion": + case "DTSDKBuild": + case "DTSDKName": + case "DTXcode": + case "DTXcodeBuild": + minutiae.Add (element.Key, element.Value.ToString ()!); + break; + case "com.microsoft.ios": + case "com.microsoft.macos": + case "com.microsoft.maccatalyst": + case "com.microsoft.tvos": + case "com.microsoft.watchos": + NSDictionary? dict = (NSDictionary) element.Value; + if (dict is not null) { + foreach (var subelement in dict) + minutiae.Add (element.Key + "/" + subelement.Key, subelement.Value.ToString ()!); + } + break; + } + } + } + return minutiae; + } +} diff --git a/Commands/AssemblyFingerprintCommand.cs b/Commands/AssemblyFingerprintCommand.cs new file mode 100644 index 0000000..8267416 --- /dev/null +++ b/Commands/AssemblyFingerprintCommand.cs @@ -0,0 +1,107 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Mono.Cecil; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace CilOut.Commands; + +public sealed class AssemblyFingerprintCommand : Command { + + public sealed class Settings : AssemblySettings { + + [Description ("Output using a diff-friendly, tab seperated values, format")] + [CommandOption ("-d|--diff-friendly")] + [DefaultValue (false)] + public bool DiffFriendly { get; init; } + } + + public override int Execute ([NotNull] CommandContext context, [NotNull] Settings settings) + { + var assembly = settings.Assembly!; + var result = Fingerprint (assembly, out var minutiae); + + if (settings.DiffFriendly) { + TabSeparatedValues (Path.GetFileName (assembly), minutiae); + } else { + ShowResult (assembly, result, minutiae); + } + + return (int) result; + } + + public static void TabSeparatedValues (string assemblyName, SortedDictionary minutiae) + { + foreach (var (key, value) in minutiae) { + Console.Write (assemblyName); + Console.Write ('\t'); + Console.Write (key); + Console.Write ('\t'); + Console.WriteLine (value); + } + } + + public static void ShowResult (string assemblyName, ReturnCodes result, SortedDictionary minutiae) + { + var asm = assemblyName.EscapeMarkup (); + switch (result) { + case ReturnCodes.Success: + AnsiConsole.WriteLine ($"Assembly `{asm}` fingerprint is composed of"); + foreach (var (key, value) in minutiae) { + AnsiConsole.Write (key); + AnsiConsole.Write (" : '"); + AnsiConsole.Write (value); + AnsiConsole.WriteLine ('\''); + } + break; + case ReturnCodes.Failure: + AnsiConsole.MarkupLine ($"Assembly `{asm}` has [bold]no[/] minutiae."); + break; + case ReturnCodes.CouldNotReadAssembly: + AnsiConsole.MarkupLine ($"[red]Error: [/] Could not read assembly `{asm}`."); + break; + } + } + + public static ReturnCodes Fingerprint (string assembly, out SortedDictionary minutiae) + { + minutiae = new (); + try { + AssemblyDefinition ad = AssemblyDefinition.ReadAssembly (assembly); + // FullName includes [Assembly]Version, Culture and PublicKeyToken + minutiae.Add ("FullName", ad.FullName); + if (ad.HasCustomAttributes) { + foreach (var ca in ad.CustomAttributes) { + switch (ca.AttributeType.FullName) { + case "System.Reflection.AssemblyCompanyAttribute": + case "System.Reflection.AssemblyConfigurationAttribute": + case "System.Reflection.AssemblyFileVersionAttribute": // can differ from AssemblyVersion + case "System.Reflection.AssemblyInformationalVersionAttribute": + case "System.Runtime.Versioning.TargetFrameworkAttribute": + case "System.Runtime.Versioning.TargetPlatformAttribute": + case "System.Runtime.Versioning.SupportedOSPlatform": + minutiae.Add (ca.AttributeType.Name, GetConstructorArgument (ca, 0)); + break; + case "System.Reflection.AssemblyMetadataAttribute": + minutiae.Add (ca.AttributeType.Name + "." + GetConstructorArgument (ca, 0), GetConstructorArgument (ca, 1)); + break; + } + } + } + return minutiae.Count > 0 ? ReturnCodes.Success : ReturnCodes.Failure; + } catch (Exception e) { + if (Environment.GetEnvironmentVariable ("V") is not null) + AnsiConsole.WriteException (e); + return ReturnCodes.CouldNotReadAssembly; + } + } + + static string GetConstructorArgument (CustomAttribute ca, int n) + { + if (!ca.HasConstructorArguments) + return ""; + if (n >= ca.ConstructorArguments.Count) + return ""; + return ca.ConstructorArguments [0].Value.ToString () ?? ""; + } +} diff --git a/Program.cs b/Program.cs index 6e4e797..72dced4 100644 --- a/Program.cs +++ b/Program.cs @@ -5,6 +5,8 @@ config.Settings.ApplicationName = "cilout"; config.AddBranch ("assembly", add => { add.SetDescription ("Assembly to analyze"); + add.AddCommand ("fingerprint") + .WithDescription ("Generate a fingerprint of the assembly to detect future changes."); add.AddCommand ("is-trimmable") .WithDescription ("Check for the presence of [[assembly: AssemblyMetadata (\"Trimmable\", \"true\")]] inside the specified assembly."); add.AddCommand ("has-entrypoint") @@ -18,6 +20,8 @@ }); config.AddBranch ("appbundle", add => { add.SetDescription ("Application Bundle to analyze"); + add.AddCommand ("fingerprint") + .WithDescription ("Generate a fingerprint of the appbundle to detect future changes."); add.AddCommand ("is-trimmable") .WithDescription ("Check for the presence of [[assembly: AssemblyMetadata (\"Trimmable\", \"true\")]] inside any assemblies of the appbundle."); add.AddCommand ("has-entrypoint") diff --git a/cilout.csproj b/cilout.csproj index 95cd130..01e714e 100644 --- a/cilout.csproj +++ b/cilout.csproj @@ -27,6 +27,7 @@ +