From a17b16723a44e1c6b1a5cdd6855446c48b6cb51c Mon Sep 17 00:00:00 2001 From: Gabe Stocco Date: Fri, 5 Apr 2019 14:00:31 -0700 Subject: [PATCH] CLI commands default to using timestamps (#75) * Default runid is now timestamp * Also fixed a crash when no filters.json file is present. * Adds default behavior for compare command * Add monitor and export-monitor support * Add support for export-collect * Simplifies code by adding a function to Database Manager * Fix get signature status issue. --- Cli/Program.cs | 187 ++++++++++++++++++++++---------- Lib/Objects/FileSystemObject.cs | 98 ++++++++--------- Lib/Utils/DatabaseManager.cs | 32 ++++++ Lib/Utils/Filter.cs | 8 +- 4 files changed, 217 insertions(+), 108 deletions(-) diff --git a/Cli/Program.cs b/Cli/Program.cs index dafa05fec..3194aa399 100644 --- a/Cli/Program.cs +++ b/Cli/Program.cs @@ -30,16 +30,16 @@ namespace AttackSurfaceAnalyzer.Cli [Verb("compare", HelpText = "Compare ASA executions and output a .html summary")] public class CompareCommandOptions { - [Option(Required = false, HelpText = "Name of output database", Default = "asa.sqlite")] + [Option(HelpText = "Name of output database", Default = "asa.sqlite")] public string DatabaseFilename { get; set; } - [Option(Required = true, HelpText = "First run (pre-install) identifier")] + [Option(HelpText = "First run (pre-install) identifier", Default = "Timestamps")] public string FirstRunId { get; set; } - [Option(Required = true, HelpText = "Second run (post-install) identifier")] + [Option(HelpText = "Second run (post-install) identifier", Default = "Timestamps")] public string SecondRunId { get; set; } - [Option(Required = false, HelpText = "Base name of output file", Default = "output")] + [Option(HelpText = "Base name of output file", Default = "output")] public string OutputBaseFilename { get; set; } [Option(Default = false, HelpText = "Increase logging verbosity")] @@ -49,16 +49,16 @@ public class CompareCommandOptions [Verb("export-collect", HelpText = "Compare ASA executions and output a .json report")] public class ExportCollectCommandOptions { - [Option(Required = false, HelpText = "Name of output database", Default = "asa.sqlite")] + [Option(HelpText = "Name of output database", Default = "asa.sqlite")] public string DatabaseFilename { get; set; } - [Option(Required = true, HelpText = "First run (pre-install) identifier")] + [Option(HelpText = "First run (pre-install) identifier", Default = "Timestamps")] public string FirstRunId { get; set; } - [Option(Required = true, HelpText = "Second run (post-install) identifier")] + [Option(HelpText = "Second run (post-install) identifier", Default = "Timestamps")] public string SecondRunId { get; set; } - [Option(Required = false, HelpText = "Directory to output to", Default = ".")] + [Option(HelpText = "Directory to output to", Default = ".")] public string OutputPath { get; set; } [Option(Default = false, HelpText = "Increase logging verbosity")] @@ -68,13 +68,13 @@ public class ExportCollectCommandOptions [Verb("export-monitor", HelpText = "Output a .json report for a monitor run")] public class ExportMonitorCommandOptions { - [Option(Required = false, HelpText = "Name of output database", Default = "asa.sqlite")] + [Option(HelpText = "Name of output database", Default = "asa.sqlite")] public string DatabaseFilename { get; set; } - [Option(Required = true, HelpText = "Monitor run identifier")] + [Option(HelpText = "Monitor run identifier", Default = "Timestamp")] public string RunId { get; set; } - [Option(Required = false, HelpText = "Directory to output to", Default = ".")] + [Option(HelpText = "Directory to output to", Default = ".")] public string OutputPath { get; set; } [Option(Default = false, HelpText = "Increase logging verbosity")] @@ -84,10 +84,10 @@ public class ExportMonitorCommandOptions [Verb("collect", HelpText = "Collect operating system metrics")] public class CollectCommandOptions { - [Option(Required = true, HelpText = "Identifies which run this is (used during comparison)")] + [Option(HelpText = "Identifies which run this is (used during comparison)", Default = "Timestamp")] public string RunId { get; set; } - [Option(Required = false, HelpText = "Name of output database", Default = "asa.sqlite")] + [Option(HelpText = "Name of output database", Default = "asa.sqlite")] public string DatabaseFilename { get; set; } [Option('c', "certificates", Required = false, HelpText = "Enable the certificate store collector")] @@ -129,10 +129,10 @@ public class CollectCommandOptions [Verb("monitor", HelpText = "Continue running and monitor activity")] public class MonitorCommandOptions { - [Option(Required = true, HelpText = "Identifies which run this is. Monitor output can be combined with collect output, but doesn't need to be compared.")] + [Option(HelpText = "Identifies which run this is. Monitor output can be combined with collect output, but doesn't need to be compared.", Default="Timestamp")] public string RunId { get; set; } - [Option(Required = false, HelpText = "Name of output database", Default = "asa.sqlite")] + [Option(HelpText = "Name of output database", Default = "asa.sqlite")] public string DatabaseFilename { get; set; } [Option('f', "file-system", Required = false, HelpText = "Enable the file system monitor. Unless -d is specified will monitor the entire file system.")] @@ -323,6 +323,25 @@ private static int RunExportCollectCommand(ExportCollectCommandOptions opts) Log.Debug("Entering RunExportCollectCommand"); DatabaseManager.SqliteFilename = opts.DatabaseFilename; + + if (opts.FirstRunId == "Timestamps" || opts.SecondRunId == "Timestamps") + { + List runIds = DatabaseManager.GetLatestRunIds(2, "collect"); + + if (runIds.Count < 2) + { + Log.Fatal("Couldn't determine latest two run ids. Can't continue."); + System.Environment.Exit(-1); + } + else + { + opts.SecondRunId = runIds.First(); + opts.FirstRunId = runIds.ElementAt(1); + } + } + + Log.Information("Comparing {0} vs {1}", opts.FirstRunId, opts.SecondRunId); + Dictionary StartEvent = new Dictionary(); StartEvent.Add("Version", Helpers.GetVersionString()); StartEvent.Add("OutputPathSet", (opts.OutputPath != null).ToString()); @@ -515,8 +534,27 @@ private static int RunExportMonitorCommand(ExportMonitorCommandOptions opts) #else Logger.Setup(false, opts.Verbose); #endif + DatabaseManager.SqliteFilename = opts.DatabaseFilename; + if (opts.RunId.Equals("Timestamp")) + { + + List runIds = DatabaseManager.GetLatestRunIds(1, "monitor"); + + if (runIds.Count < 1) + { + Log.Fatal("Couldn't determine latest run id. Can't continue."); + System.Environment.Exit(-1); + } + else + { + opts.RunId = runIds.First(); + } + } + + Log.Information("Exporting {0}", opts.RunId); + Dictionary StartEvent = new Dictionary(); StartEvent.Add("Version", Helpers.GetVersionString()); StartEvent.Add("OutputPathSet", (opts.OutputPath != null).ToString()); @@ -571,6 +609,10 @@ private static int RunMonitorCommand(MonitorCommandOptions opts) #endif AdminOrQuit(); Filter.LoadFilters(opts.FilterLocation); + if (opts.RunId.Equals("Timestamp")) + { + opts.RunId = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + } Dictionary StartEvent = new Dictionary(); StartEvent.Add("Version", Helpers.GetVersionString()); Telemetry.Client.TrackEvent("Begin monitoring", StartEvent); @@ -798,6 +840,9 @@ private static bool HasResults(string BaseRunId, string CompareRunId, RESULT_TYP public static Dictionary CompareRuns(CompareCommandOptions opts) { + Log.Information("Comparing {0} vs {1}",opts.FirstRunId,opts.SecondRunId); + + var results = new Dictionary { ["BeforeRunId"] = opts.FirstRunId, @@ -806,48 +851,50 @@ public static Dictionary CompareRuns(CompareCommandOptions opts) comparators = new List(); - var cmd = new SqliteCommand(SQL_GET_RESULT_TYPES, DatabaseManager.Connection, DatabaseManager.Transaction); - Log.Debug("Getting result types"); - cmd.Parameters.AddWithValue("@base_run_id", opts.FirstRunId); - cmd.Parameters.AddWithValue("@compare_run_id", opts.SecondRunId); + Dictionary count = new Dictionary() + { + { "File", 0 }, + { "Certificate", 0 }, + { "Registry", 0 }, + { "Port", 0 }, + { "Service", 0 }, + { "User", 0 } + }; - var count = new Dictionary() + using (var cmd = new SqliteCommand(SQL_GET_RESULT_TYPES, DatabaseManager.Connection, DatabaseManager.Transaction)) { - { "File", 0 }, - { "Certificate", 0 }, - { "Registry", 0 }, - { "Port", 0 }, - { "Service", 0 }, - { "User", 0 } - }; + Log.Debug("Getting result types"); + cmd.Parameters.AddWithValue("@base_run_id", opts.FirstRunId); + cmd.Parameters.AddWithValue("@compare_run_id", opts.SecondRunId); - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) + using (var reader = cmd.ExecuteReader()) { - if (int.Parse(reader["file_system"].ToString()) != 0) - { - count["File"]++; - } - if (int.Parse(reader["ports"].ToString()) != 0) - { - count["Port"]++; - } - if (int.Parse(reader["users"].ToString()) != 0) - { - count["User"]++; - } - if (int.Parse(reader["services"].ToString()) != 0) - { - count["Service"]++; - } - if (int.Parse(reader["registry"].ToString()) != 0) - { - count["Registry"]++; - } - if (int.Parse(reader["certificates"].ToString()) != 0) + while (reader.Read()) { - count["Certificate"]++; + if (int.Parse(reader["file_system"].ToString()) != 0) + { + count["File"]++; + } + if (int.Parse(reader["ports"].ToString()) != 0) + { + count["Port"]++; + } + if (int.Parse(reader["users"].ToString()) != 0) + { + count["User"]++; + } + if (int.Parse(reader["services"].ToString()) != 0) + { + count["Service"]++; + } + if (int.Parse(reader["registry"].ToString()) != 0) + { + count["Registry"]++; + } + if (int.Parse(reader["certificates"].ToString()) != 0) + { + count["Certificate"]++; + } } } } @@ -892,11 +939,13 @@ public static Dictionary CompareRuns(CompareCommandOptions opts) } c.Results.ToList().ForEach(x => results.Add(x.Key, x.Value)); } - cmd = new SqliteCommand(UPDATE_RUN_IN_RESULT_TABLE, DatabaseManager.Connection, DatabaseManager.Transaction); - cmd.Parameters.AddWithValue("@base_run_id", opts.FirstRunId); - cmd.Parameters.AddWithValue("@compare_run_id", opts.SecondRunId); - cmd.Parameters.AddWithValue("@status", RUN_STATUS.COMPLETED); - cmd.ExecuteNonQuery(); + using (var cmd = new SqliteCommand(UPDATE_RUN_IN_RESULT_TABLE, DatabaseManager.Connection, DatabaseManager.Transaction)) + { + cmd.Parameters.AddWithValue("@base_run_id", opts.FirstRunId); + cmd.Parameters.AddWithValue("@compare_run_id", opts.SecondRunId); + cmd.Parameters.AddWithValue("@status", RUN_STATUS.COMPLETED); + cmd.ExecuteNonQuery(); + } DatabaseManager.Commit(); return results; @@ -1042,9 +1091,12 @@ public static int RunCollectCommand(CollectCommandOptions opts) } Filter.LoadFilters(opts.FilterLocation); - Filter.DumpFilters(); DatabaseManager.SqliteFilename = opts.DatabaseFilename; + if (opts.RunId.Equals("Timestamp")) + { + opts.RunId = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + } if (opts.Overwrite) { @@ -1064,6 +1116,8 @@ public static int RunCollectCommand(CollectCommandOptions opts) } } + Log.Information("Starting run {0}", opts.RunId); + string INSERT_RUN = "insert into runs (run_id, file_system, ports, users, services, registry, certificates, type, timestamp, version) values (@run_id, @file_system, @ports, @users, @services, @registry, @certificates, @type, @timestamp, @version)"; using (var cmd = new SqliteCommand(INSERT_RUN, DatabaseManager.Connection, DatabaseManager.Transaction)) @@ -1192,6 +1246,23 @@ private static int RunCompareCommand(CompareCommandOptions opts) StartEvent.Add("Version", Helpers.GetVersionString()); Telemetry.Client.TrackEvent("Begin Compare Command", StartEvent); + + if (opts.FirstRunId == "Timestamps" || opts.SecondRunId == "Timestamps") + { + List runIds = DatabaseManager.GetLatestRunIds(2, "collect"); + + if (runIds.Count < 2) + { + Log.Fatal("Couldn't determine latest two run ids. Can't continue."); + System.Environment.Exit(-1); + } + else + { + opts.SecondRunId = runIds.First(); + opts.FirstRunId = runIds.ElementAt(1); + } + } + Log.Debug("Starting CompareRuns"); var results = CompareRuns(opts); diff --git a/Lib/Objects/FileSystemObject.cs b/Lib/Objects/FileSystemObject.cs index eb40aae9a..4303c52a2 100644 --- a/Lib/Objects/FileSystemObject.cs +++ b/Lib/Objects/FileSystemObject.cs @@ -32,66 +32,66 @@ public string SignatureStatus return "No signature required"; } + //try + //{ + // using (var ps = PowerShell.Create()) + // { + // var certStatus = ps.AddScript($"(Get-AuthenticodeSignature '{Path}').Status").Invoke().First(); + // if (certStatus == null || certStatus.Equals("NotSigned")) // lgtm[cs/hardcoded-credentials] + // { + // return "Not signed"; + // } + // else if (certStatus.Equals("Valid")) // lgtm [cs/hardcoded-credentials] + // { + // return "Valid"; + // } + // else + // { + // return $"Signature error: {certStatus}"; + // } + // } + //} + //catch(Exception ex) + //{ + // Log.Verbose(ex.StackTrace); try { - using (var ps = PowerShell.Create()) + var _path = Path.Replace("'", "`'"); + var process = new Process() { - var certStatus = ps.AddScript($"(Get-AuthenticodeSignature '{Path}').Status").Invoke().First(); - if (certStatus == null || certStatus.Equals("NotSigned")) // lgtm[cs/hardcoded-credentials] + StartInfo = new ProcessStartInfo() { - return "Not signed"; - } - else if (certStatus.Equals("Valid")) // lgtm [cs/hardcoded-credentials] - { - return "Valid"; - } - else - { - return $"Signature error: {certStatus}"; + FileName = "powershell.exe", + Arguments = string.Format("(Get-AuthenticodeSignature '{0}').Status", Path), + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, } + }; + process.Start(); + var certStatus = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (certStatus == null || certStatus.Equals("NotSigned")) //lgtm [cs/hardcoded-credentials] + { + return "Not signed"; } - } - catch(Exception ex) - { - Log.Debug(ex.StackTrace); - // Fall back to a call out to powershell.exe - try + else if (certStatus.Equals("Valid")) //lgtm [cs/hardcoded-credentials] { - var _path = Path.Replace("'", "`'"); - var process = new Process() - { - StartInfo = new ProcessStartInfo() - { - FileName = "powershell.exe", - Arguments = string.Format("(Get-AuthenticodeSignature '{0}').Status", Path), - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - } - }; - process.Start(); - var certStatus = process.StandardOutput.ReadToEnd().Trim(); - process.WaitForExit(); - - if (certStatus == null || certStatus.Equals("NotSigned")) //lgtm [cs/hardcoded-credentials] - { - return "Not signed"; - } - else if (certStatus.Equals("Valid")) //lgtm [cs/hardcoded-credentials] - { - return "Valid"; - } - else - { - return $"Signature error: {certStatus.Substring(0, Math.Min(15, certStatus.Length - 1))}"; - } + return "Valid"; } - catch(Exception ex2) + else { - Log.Debug(ex2.StackTrace); + return $"Signature error: {certStatus.Substring(0, Math.Min(15, certStatus.Length - 1))}"; } - return null; } + catch(Exception ex2) + { + Log.Debug("Failed to get signature status"); + Log.Debug(ex2.StackTrace); + } + return null; + } } diff --git a/Lib/Utils/DatabaseManager.cs b/Lib/Utils/DatabaseManager.cs index 2ecea2010..6482502c1 100644 --- a/Lib/Utils/DatabaseManager.cs +++ b/Lib/Utils/DatabaseManager.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; +using System.Collections.Generic; using Microsoft.Data.Sqlite; using Serilog; @@ -52,6 +53,7 @@ public static class DatabaseManager private static readonly string SQL_TRUNCATE_FILES_MONITORED = "delete from file_system_monitored where run_id=@run_id"; private static readonly string SQL_TRUNCATE_RUN = "delete from runs where run_id=@run_id"; + private static readonly string SQL_SELECT_LATEST_N_RUNS = "select run_id from runs where type = @type order by timestamp desc limit 0,@limit;"; public static SqliteConnection Connection; @@ -137,6 +139,36 @@ public static void Setup() } } + public static List GetLatestRunIds(int numberOfIds, string type) + { + + + List output = new List(); + using (var cmd = new SqliteCommand(SQL_SELECT_LATEST_N_RUNS, DatabaseManager.Connection)) + { + cmd.Parameters.AddWithValue("@type", type); + cmd.Parameters.AddWithValue("@limit", numberOfIds); + try + { + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + output.Add(reader["run_id"].ToString()); + } + } + } + catch (Exception e) + { + Log.Debug(e.GetType().ToString()); + Log.Debug(e.Message); + Log.Debug("Couldn't determine latest {0} run ids.",numberOfIds); + } + } + return output; + } + + public static SqliteTransaction Transaction { get diff --git a/Lib/Utils/Filter.cs b/Lib/Utils/Filter.cs index b477b2405..bf606c37d 100644 --- a/Lib/Utils/Filter.cs +++ b/Lib/Utils/Filter.cs @@ -33,6 +33,10 @@ public static string RuntimeString() public static bool IsFiltered(string Platform, string ScanType, string ItemType, string Property, string Target) { + if (config == null) + { + return false; + } if (IsFiltered(Platform, ScanType, ItemType, Property, "Include", Target)) { return false; @@ -135,6 +139,7 @@ public static void LoadFilters(string filterLoc = "filters.json") { config = (JObject)JToken.ReadFrom(reader); Log.Information("Loaded filters from {0}", filterLoc); + DumpFilters(); } if (config == null) { @@ -144,13 +149,14 @@ public static void LoadFilters(string filterLoc = "filters.json") catch (System.IO.FileNotFoundException) { //That's fine, we just don't have any filters to load + config = null; Log.Debug("{0} is missing (filter configuration file)", filterLoc); return; } catch (NullReferenceException) { - + config = null; Log.Debug("{0} is missing (filter configuration file)", filterLoc); return;