/// <summary> /// Returns a single XPath query string from the Events, Levels, and Sources found in a LogQuery object. /// </summary> /// <param name="log">The LogQuery object to be parsed.</param> /// <returns></returns> public static string BuildXPathQuery(GevLog.LogQuery log) { string queryStringPrefix = ""; string queryStringSuffix = ""; string queryStringSources = ""; string queryStringIds = ""; string queryStringLevels = ""; if (log.LevelFilter.Count > 0 || log.IdFilter.Count > 0 || log.SourceFilter.Count > 0) { queryStringPrefix = "*[System["; queryStringSuffix = "]]"; } else { return("*"); } if (log.SourceFilter.Count > 0) { queryStringSources = PrepareXPathStringFromList(ParseProviders(log.SourceFilter), "Provider[", "Name", "]", true); } if (log.IdFilter.Count > 0) { queryStringIds = PrepareXPathStringFromList(log.IdFilter, "(", "EventID", ")"); } if (log.LevelFilter.Count > 0) { queryStringLevels = PrepareXPathStringFromList(log.LevelFilter, "(", "Level", ")"); } List <string> queryStringList = new List <string>(); if (queryStringSources.Length > 0) { queryStringList.Add(queryStringSources); } if (queryStringIds.Length > 0) { queryStringList.Add(queryStringIds); } if (queryStringLevels.Length > 0) { queryStringList.Add(queryStringLevels); } // return the final string, when all is said and done it will look something like: // *[System[Provider[@Name='Microsoft-Windows-Ntfs' or @Name='Microsoft-Windows-Ntfs-UBPM' or @Name='Ntfs'] and (EventID=1 or EventID=2 or EventID=3) and (Level=1 or Level=2)]] return(queryStringPrefix + queryStringList.Aggregate((a, b) => a + " and " + b) + queryStringSuffix); }
/// <summary> Display the debugging information about command-line arguments </summary> private static void DisplayArguments(GevLog.LogQuery log) { Console.WriteLine($"\nParsed arguments:\n"); Console.WriteLine($"Path = \"{log.LogPath}\""); Console.WriteLine($"Outfile = \"{log.OutputFile}\""); Console.WriteLine($"Date = \"{log.DateRangeFilter}\""); Console.WriteLine($"Offset = \"{log.DateOffsetFilter}\""); Console.Write("Levels = \""); for (int i = 0; i < log.LevelFilter.Count; i++) { Console.Write(log.LevelFilter[i]); if (i < (log.LevelFilter.Count - 1)) { Console.Write(", "); } } Console.Write("\"\n"); Console.Write("IDs = \""); for (int i = 0; i < log.IdFilter.Count; i++) { Console.Write(log.IdFilter[i]); if (i < (log.IdFilter.Count - 1)) { Console.Write(", "); } } Console.Write("\"\n"); Console.WriteLine($"IDCount = \"{log.IdFilter.Count}\""); Console.Write("Sources = \""); for (int i = 0; i < log.SourceFilter.Count; i++) { Console.Write(log.SourceFilter[i]); if (i < (log.SourceFilter.Count - 1)) { Console.Write(", "); } } Console.Write("\""); Console.WriteLine($"\nSource count: {log.SourceFilter.Count}"); Console.Write("\n"); }
/// <summary> /// Parses command-line arguments. /// </summary> /// <param name="arguments">args[] from main()</param> /// <param name="log">LogQuery class which stores user filters parsed from arguments.</param> /// <returns>The LogQuery object.</returns> private static GevLog.LogQuery ParseArguments(string[] arguments, GevLog.LogQuery log) { // Check for help if (arguments.Contains("--help")) { DisplayHelp(); // This also aborts } // Dictionary which contains list of acceptable options as keys, and whether the // option requires arguments // For instance, in: `--path .\hello.evtx`, ".\hello.evtx" is an argument var argsDict = new Dictionary <string, bool>() { { "--path", true }, { "--debug", false }, { "--query", false }, { "--id", true }, { "--source", true }, { "--level", true }, { "--format", true }, { "--out-file", true }, { "--direction", true }, { "--max", true } }; for (int i = 0; i < arguments.Length; i++) { string key = arguments[i].ToLower(); if (argsDict.ContainsKey(key)) { string secondaryParameter = ""; // if the Value for the Key is "true", it means we need to look for arguments if (argsDict[key]) { secondaryParameter = arguments[i + 1]; i++; // In the event that a secondary parameter is needed and found, we need to increment // `i` again so the next iteration will find the next key, and not the previous // key's parameter } switch (key) { case "--": return(log); // Stop parsing arguments ///////////////////// Path ////////////////////// case "--path": string pathArgument = secondaryParameter; if (File.Exists(pathArgument)) { // Looks good, assign value log.LogPath = pathArgument; } else { if (File.Exists($"{pathArgument}.evtx")) { // Append .evtx log.LogPath = ($"{pathArgument}.evtx"); } else { // It's not valid AbortGev($"Invalid path: \"{pathArgument}\""); } } break; //////////////////// Debug ///////////////////// case "--debug": log.DebugMode = true; break; //////////////////// Query ///////////////////// case "--query": log.QuerySet = true; return(log); // Stop parsing arguments ///////////////////// IDs ////////////////////// case "--id": string idArgument = secondaryParameter; if (!string.IsNullOrEmpty(idArgument)) { if (idArgument.Split(',').Length > 5) { // Too many IDs (max 5), abort AbortGev("Too many IDs. Max 5."); } else { // Iterate through each foreach (var id in idArgument.Split(',')) { if (int.TryParse(id, out int idSubValue)) { if (idSubValue < 1 || idSubValue > 65535) { // It is an int, but it's outside 1-65535 AbortGev($"Invalid ID: \"{idSubValue}\" Please input a value between 1 and 65535."); } else { // All is good, add to list log.IdFilter.Add(idSubValue); } } else { // It isn't an int, abort AbortGev($"Invalid ID. \"{id}\" is not an integer."); } } } // Check to see we ended up with any ids if (log.IdFilter.Count < 1) { // Something broke (we had an --id flag but there was a problem parsing IDs) AbortGev("There was a problem parsing IDs. Valid IDs are separated by commas without spaces."); } } break; /////////////////// Sources //////////////////// case "--source": string sourceArgument = secondaryParameter; if (!string.IsNullOrEmpty(sourceArgument)) { if (sourceArgument.Split(',').Length > 5) { // Too many sources (max 5), abort AbortGev("Too many sources. Max 5."); } // Iterate through each and add to the source list foreach (var source in sourceArgument.Split(',')) { log.SourceFilter.Add(source); } // Check to see we ended up with any ids if (log.SourceFilter.Count < 1) { // Something broke (we had a --source flag but there was a problem parsing sources) AbortGev("There was a problem parsing sources. Valid sources are separated by commas without spaces."); } } break; //////////////////// Level ///////////////////// case "--level": string levelArgument = secondaryParameter; if (!String.IsNullOrEmpty(levelArgument)) { if (levelArgument.Split(',').Length > 5) { // Too many levels (max 5), abort AbortGev("Too many levels. Max 5."); } else { // Iterate through each foreach (var level in levelArgument.Split(',')) { if (int.TryParse(level, out int levelSubValue)) { if (levelSubValue < 1 || levelSubValue > 5) { // It is an int, but it's outside 1-5 AbortGev($"Invalid event level: \"{levelSubValue}\" Please input a value between 1 and 5."); } else { // All is good, add to list log.LevelFilter.Add(levelSubValue); } } else { // It isn't an int, abort AbortGev($"Invalid level. \"{level}\" is not an integer."); } } } // Check to see we ended up with any levels if (log.LevelFilter.Count < 1) { // Something broke (we had an --level flag but there was a problem parsing levels) AbortGev("There was a problem parsing IDs. Valid IDs are separated by commas without spaces."); } } break; /////////////////// Format ///////////////////// case "--format": string formatArgument = secondaryParameter; if (!string.IsNullOrEmpty(formatArgument)) { // Check for case-insensitive strings if (formatArgument.Equals("xml", StringComparison.InvariantCultureIgnoreCase)) { log.Format = "xml"; // xml } else if (formatArgument.Equals("html", StringComparison.InvariantCultureIgnoreCase)) { log.Format = "html"; // html } else if (formatArgument.Equals("text", StringComparison.InvariantCultureIgnoreCase)) { log.Format = "text"; // text } else if (formatArgument.Equals("json", StringComparison.InvariantCultureIgnoreCase)) { log.Format = "json"; // json } else { // They didn't supply 'xml', 'html', 'text', or 'json' AbortGev("There was a problem parsing format. Valid formats are 'xml', 'html', 'text', or 'json'."); } } break; ///////////////// Output File ////////////////// case "--out-file": string outfileArgument = secondaryParameter; if (!string.IsNullOrEmpty(outfileArgument)) { // Parse format: switch (log.Format) { case "text": // Defaults to .txt if (outfileArgument.EndsWith(".txt")) { log.OutputFile = outfileArgument; } else { log.OutputFile = (outfileArgument + ".txt"); } break; case "xml": // xml if (outfileArgument.EndsWith(".xml")) { log.OutputFile = outfileArgument; } else { log.OutputFile = (outfileArgument + ".xml"); } break; case "html": // html if (outfileArgument.EndsWith(".html")) { log.OutputFile = outfileArgument; } else { log.OutputFile = (outfileArgument + ".html"); } break; case "json": // json if (outfileArgument.EndsWith(".json")) { log.OutputFile = outfileArgument; } else { log.OutputFile = (outfileArgument + ".json"); } break; default: // Something broke AbortGev("There was an error when parsing output file."); break; } // If the file exists, back it up if (File.Exists(log.OutputFile)) { // Grab file name (base + extension) string fileBase = Path.GetFileNameWithoutExtension(log.OutputFile); string fileExt = Path.GetExtension(log.OutputFile); // Random number string randomNumber; Random random = new Random(); // Keep trying to rename the file until we get one that doesn't exist. do { // Create string to prepend string prependString = String.Empty; // Create random number between 1-10000 randomNumber = random.Next(10000).ToString(); // If the nmber is < 10000, prepend '0's to the string to make it 5 characters long. if (randomNumber.Length < 5) { for (int x = 0; x + randomNumber.Length < 5; x++) { prependString += "0"; } } log.OutputFile = (fileBase + "-" + prependString + randomNumber + fileExt); } while (File.Exists(log.OutputFile)); // Create it File.Create(log.OutputFile).Close(); } else { File.Create(log.OutputFile).Close(); } } break; ////////////////// Direction /////////////////// case "--direction": string directionArgument = secondaryParameter; if (!String.IsNullOrEmpty(directionArgument)) { if (int.TryParse(directionArgument, out int direction)) { if (direction < 1 || direction > 2) { // It is an int, but outside 1-2 AbortGev($"Invalid direction: \"{direction}\" Please input either 1 or 2."); } else { // Everything looks good, we've gotten a valid direction. log.Direction = direction; } } else { // It's not an integer AbortGev($"Invalid direction. \"{directionArgument}\" is not an integer."); } } break; //////////////// Maximum Events //////////////// case "--max": string maxEventsArgument = secondaryParameter; if (!String.IsNullOrEmpty(maxEventsArgument)) { if (int.TryParse(maxEventsArgument, out int maxEvents)) { if (maxEvents < 1) { // Doesn't make sense AbortGev($"Invalid maximum events: \"{maxEvents}\" Please input a number greater than 0."); } else { // Everything looks good, we've gotten a valid maximum Globals.MaxEvents = maxEvents; } } } break; } } else { // Unknown argument AbortGev($"Unknown argument {arguments[i]}"); } } // All done return(log); }
/// <summary> /// Outputs event log records /// </summary> /// <param name="record">Event log record to output</param> /// <param name="outFile">File to output it to</param> /// <param name="format">Type of output -> 1 = XML - 2 = HTML - 3 = TEXT</param> /// <returns>Number of records found so far</returns> private static void OutputRecords(List <GevLog.Record> eventLogRecords, GevLog.LogQuery log) { string outString = ""; /* JsonSerializer and XmlSerializer can turn the whole object into a JSON string * so we don't need to iterate through each record for "xml" or "json", but * we will for "text" and "html" */ /*==================== JSON ====================*/ if (log.Format == "json") { outString = JsonSerializer.Serialize(eventLogRecords, new JsonSerializerOptions() { WriteIndented = true }); } /*==================== XML ====================*/ else if (log.Format == "xml") { using (var stringWriter = new StringWriter()) { var xmlSerializer = new XmlSerializer(eventLogRecords.GetType()); xmlSerializer.Serialize(stringWriter, eventLogRecords); outString = stringWriter.ToString(); } } else { foreach (GevLog.Record record in eventLogRecords) { /*==================== Text ====================*/ if (log.Format == "text") { outString += $"---------------------------------------------------------------------------------\n" + $"Id : {record.Id}\n" + $"LevelDisplayName : {record.LevelDisplayName}\n" + $"Level : {record.Level}\n" + $"TimeCreated : {record.TimeCreated}\n" + $"ProviderName : {record.ProviderName}\n" + $"FormatDescription : {record.FormatDescription}\n" + $"Records : {Globals.TotalRecords}\n" + $"---------------------------------------------------------------------------------" + "\n\n"; } /*==================== HTML ====================*/ else if (log.Format == "html") { // This is specifically used by the gev_web application using a customized CSS style sheet. if (Globals.TotalRecords < 1) { // Formulate output and adds header outString += $"<table class=\"tg\" style=\"width:100%\">" + $" <colgroup>" + $" <col style=\"width:15%\">\n" + $" <col style=\"width:15%\">\n" + $" <col style=\"width:30%\">\n" + $" <col style=\"width:10%\">\n" + $" </colgroup>\n" + $" <tr>\n" + $" <th class=\"tg-h\">Level</th>\n" + $" <th class=\"tg-h\">Date</th>\n" + $" <th class=\"tg-h\">Source</th>\n" + $" <th class=\"tg-h\">EventID</th>\n" + $" </tr>\n" + $" <tr>\n" + $" <td class=\"tg-1\">{record.LevelDisplayName}</td>\n" + $" <td class=\"tg-1\">{record.TimeCreated}</td>\n" + $" <td class=\"tg-1\">{record.ProviderName}</td>\n" + $" <td class=\"tg-1\">{record.Id}</td>\n" + $" </tr>\n" + $" <tr>\n" + $" <td class=\"tg-2\" colspan=\"4\"><pre>{record.FormatDescription}</pre></td>\n" + $" </tr>\n" + $"</table>\n" + $"<br>\n"; } else { // Formulates output without header outString += $"<table class=\"tg\" style=\"width:100%\">" + $" <colgroup>" + $" <col style=\"width:15%\">\n" + $" <col style=\"width:15%\">\n" + $" <col style=\"width:30%\">\n" + $" <col style=\"width:10%\">\n" + $" </colgroup>\n" + $" <tr>\n" + $" <td class=\"tg-1\">{record.LevelDisplayName}</td>\n" + $" <td class=\"tg-1\">{record.TimeCreated}</td>\n" + $" <td class=\"tg-1\">{record.ProviderName}</td>\n" + $" <td class=\"tg-1\">{record.Id}</td>\n" + $" </tr>\n" + $" <tr>\n" + $" <td class=\"tg-2\" colspan=\"4\"><pre>{record.FormatDescription}</pre></td>\n" + $" </tr>\n" + $"</table>\n" + $"<br>\n"; } } /* Something bad did indeed happen */ else { AbortGev("Unable to determine format."); } } } // Either output the records to a file, or to the console if (!string.IsNullOrEmpty(log.OutputFile)) { // Write output to a file File.AppendAllText(log.OutputFile, outString); } else { // Write output to screen Console.Write($"{outString}\n"); } }
private static void Main(string[] args) { // Stopwatch for debugging Stopwatch stopWatch = new Stopwatch(); // Instantiate a new object that holds query information GevLog.LogQuery log = new GevLog.LogQuery() { LogPath = "" }; // Holds a list of all the records we have found var eventLogRecords = new List <GevLog.Record>(); // If user has not supplied any arguments, we'll display the help text if (args.Length == 0) { DisplayHelp(); } else { // parse arguments log = ParseArguments(args, log); } // DEBUG MODE if (log.DebugMode) { // Start the System.Diagnostics.Stopwatch to see how long the program takes to run stopWatch.Start(); // Print all arguments to the console DisplayArguments(log); } // Since we have parsed arguments, check to see if they supplied a path if (log.LogPath.Length < 1) { if (log.QuerySet) { AbortGev("Query flag was found, but no path was given. Please set the path before the query flag."); } else { AbortGev("No path to archived log found."); } } // If we have any Levels or IDs we'll need a prefix and suffix, otherwise the queryString should just be '*' string queryString = XPathBuilder.BuildXPathQuery(log); if (log.DebugMode) { Console.WriteLine($"\n{queryString}"); } // Get our records that match our XPath query EventRecords(LogRecordCollection(log.LogPath, log.Direction, queryString), eventLogRecords); // If there were no records found, we'll alert the user if (Globals.TotalRecords < 1) { Console.WriteLine("No records found matching search criteria."); } else { OutputRecords(eventLogRecords, log); // Metrics -- only for debugging if (log.DebugMode) { stopWatch.Stop(); Console.WriteLine("Program run in " + stopWatch.ElapsedMilliseconds.ToString() + "ms with " + Globals.TotalRecords + " records found."); } } }