From 21b1188cb374b55fb35fc5464a6bbbc8bbf8e80a Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Wed, 2 Apr 2025 14:30:56 -0700 Subject: [PATCH 1/7] Add HTML log option --- SnaffCore/Concurrency/SnafflerMessageType.cs | 7 +- SnaffCore/Config/Options.cs | 3 +- Snaffler/Config.cs | 4 + Snaffler/SnaffleRunner.cs | 103 +++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/SnaffCore/Concurrency/SnafflerMessageType.cs b/SnaffCore/Concurrency/SnafflerMessageType.cs index ac3c0a78..c70432b0 100644 --- a/SnaffCore/Concurrency/SnafflerMessageType.cs +++ b/SnaffCore/Concurrency/SnafflerMessageType.cs @@ -1,10 +1,15 @@ -namespace SnaffCore.Concurrency +using System.ComponentModel; + +namespace SnaffCore.Concurrency { public enum SnafflerMessageType { Error, + [Description("Share")] ShareResult, + [Description("Dir")] DirResult, + [Description("File")] FileResult, Finish, Info, diff --git a/SnaffCore/Config/Options.cs b/SnaffCore/Config/Options.cs index 78c66282..aed95e6c 100644 --- a/SnaffCore/Config/Options.cs +++ b/SnaffCore/Config/Options.cs @@ -7,7 +7,8 @@ namespace SnaffCore.Config public enum LogType { Plain = 0, - JSON = 1 + JSON = 1, + HTML = 2 } public partial class Options diff --git a/Snaffler/Config.cs b/Snaffler/Config.cs index 0a7e9a57..5f66161a 100644 --- a/Snaffler/Config.cs +++ b/Snaffler/Config.cs @@ -173,6 +173,10 @@ private static Options ParseImpl(string[] args) { parsedConfig.LogType = LogType.JSON; } + else if (logType.Value.ToLower() == "html") + { + parsedConfig.LogType = LogType.HTML; + } else { Mq.Info("Invalid type argument passed (" + logType.Value + ") defaulting to plaintext"); diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index 4d01882d..6967a75d 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.Text.RegularExpressions; using System.Threading; +using System.ComponentModel; namespace Snaffler { @@ -185,6 +186,10 @@ public void Run(string[] args) }; logfile.Layout = jsonLayout; } + else if (Options.LogType == LogType.HTML) + { + logfile.Layout = "${longdate}${event-properties:htmlFields:objectPath=DateTime}${level}${event-properties:htmlFields:objectPath=Type}${message}"; + } } // Apply config @@ -247,6 +252,10 @@ private bool HandleOutput() { ProcessMessageJSON(message); } + else if (Options.LogType == LogType.HTML) + { + ProcessMessageHTML(message); + } // catch terminating messages and bail out of the master 'while' loop if ((message.Type == SnafflerMessageType.Fatal) || (message.Type == SnafflerMessageType.Finish)) @@ -364,6 +373,73 @@ private void ProcessMessageJSON(SnafflerMessage message) } } + private void ProcessMessageHTML(SnafflerMessage message) + { + // standardized time formatting, UTC + string datetime = String.Format("{1}{0}{2:u}{0}", Options.Separator, hostString(), message.DateTime.ToUniversalTime()); + + // get a more user friendly name for a type variant if provided + var typeVariantField = message.Type.GetType().GetField(message.Type.ToString()); + var typeVariantAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(typeVariantField, typeof(DescriptionAttribute)); + string userReadableType = typeVariantAttribute == null ? message.Type.ToString() : typeVariantAttribute.Description; + + var htmlFields = new { DateTime = datetime, Type = userReadableType }; + + switch (message.Type) + { + case SnafflerMessageType.Trace: + //Logger.Trace(message); + Logger.WithProperty("htmlFields", htmlFields).Trace(message.Message); + break; + case SnafflerMessageType.Degub: + //Logger.Debug(message); + Logger.WithProperty("htmlFields", htmlFields).Debug(message.Message); + break; + case SnafflerMessageType.Info: + //Logger.Info(message); + Logger.WithProperty("htmlFields", htmlFields).Info(message.Message); + break; + case SnafflerMessageType.FileResult: + //Logger.Warn(message); + Logger.WithProperty("htmlFields", htmlFields).Warn(FileResultLogFromMessage(message)); + break; + case SnafflerMessageType.DirResult: + //Logger.Warn(message); + Logger.WithProperty("htmlFields", htmlFields).Warn(DirResultLogFromMessage(message)); + break; + case SnafflerMessageType.ShareResult: + //Logger.Warn(message); + Logger.WithProperty("htmlFields", htmlFields).Warn(ShareResultLogFromMessage(message)); + break; + case SnafflerMessageType.Error: + //Logger.Error(message); + Logger.WithProperty("htmlFields", htmlFields).Error(message.Message); + break; + case SnafflerMessageType.Fatal: + //Logger.Fatal(message); + Logger.WithProperty("htmlFields", htmlFields).Fatal(message.Message); + if (Debugger.IsAttached) + { + Console.ReadKey(); + } + break; + case SnafflerMessageType.Finish: + Logger.Info("Snaffler out."); + + if (Debugger.IsAttached) + { + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + } + if (Options.LogType == LogType.HTML) + { + Logger.Info("Normalising output, please wait..."); + FixHTMLOutput(); + } + break; + } + } + public string ShareResultLogFromMessage(SnafflerMessage message) { string sharePath = message.ShareResult.SharePath; @@ -579,5 +655,32 @@ private void FixJSONOutput() //Delete the temporary file. File.Delete(Options.LogFilePath + ".tmp"); } + + private void FixHTMLOutput() + { + //Rename the log file temporarily + File.Move(Options.LogFilePath, Options.LogFilePath + ".tmp"); + + //Prepare the normalised file + using (StreamWriter file = new StreamWriter(Options.LogFilePath)) + { + //Write the start of the surrounding template that we need + file.Write("Snaffler Logs
"); + + //Open the original file + using (FileStream sourceStream = new FileStream(Options.LogFilePath + ".tmp", FileMode.Open, FileAccess.Read)) + using (StreamReader sourceReader = new StreamReader(sourceStream)) + { + //Write the original content + file.Write(sourceReader.ReadToEnd()); + } + + //Write the end of the surrounding template that we need + file.Write("
TimestampDateTimeLevelTypeMessage
"); + } + + //Delete the temporary file + File.Delete(Options.LogFilePath + ".tmp"); + } } } From 19153a717b9f9527c5d162794d212ad0b04e5066 Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Thu, 3 Apr 2025 21:15:09 -0700 Subject: [PATCH 2/7] Refactor permissions checking and logging, add option to skip scanning files, add option to log directories even if not marked for snaffling. --- SnaffCore/Classifiers/DirClassifier.cs | 21 +++++- SnaffCore/Classifiers/EffectiveAccess.cs | 69 ++++++++++++++++++++ SnaffCore/Classifiers/FileResult.cs | 68 +------------------- SnaffCore/Classifiers/ShareClassifier.cs | 7 +- SnaffCore/Config/Options.cs | 2 + SnaffCore/ShareFind/ShareFinder.cs | 17 ++--- SnaffCore/TreeWalk/TreeWalker.cs | 81 ++++++++++++------------ Snaffler/Config.cs | 18 +++++- Snaffler/SnaffleRunner.cs | 27 ++++++-- 9 files changed, 182 insertions(+), 128 deletions(-) diff --git a/SnaffCore/Classifiers/DirClassifier.cs b/SnaffCore/Classifiers/DirClassifier.cs index 34eb5fd2..564a1484 100644 --- a/SnaffCore/Classifiers/DirClassifier.cs +++ b/SnaffCore/Classifiers/DirClassifier.cs @@ -1,4 +1,7 @@ -using SnaffCore.Concurrency; +using System.IO; +using SnaffCore.Classifiers.EffectiveAccess; +using SnaffCore.Concurrency; +using static SnaffCore.Config.Options; namespace SnaffCore.Classifiers { @@ -20,6 +23,11 @@ public DirResult ClassifyDir(string dir) Triage = ClassifierRule.Triage, ScanDir = true, }; + + DirectoryInfo dirInfo = new DirectoryInfo(dir); + EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); + dirResult.RwStatus = effPerms.CanRw(dirInfo); + // check if it matches TextClassifier textClassifier = new TextClassifier(ClassifierRule); TextResult textResult = textClassifier.TextMatch(dir); @@ -30,6 +38,10 @@ public DirResult ClassifyDir(string dir) { case MatchAction.Discard: dirResult.ScanDir = false; + if (MyOptions.LogEverything) + { + Mq.DirResult(dirResult); + } return dirResult; case MatchAction.Snaffle: dirResult.Triage = ClassifierRule.Triage; @@ -40,6 +52,12 @@ public DirResult ClassifyDir(string dir) return null; } } + + if (MyOptions.LogEverything) + { + Mq.DirResult(dirResult); + } + return dirResult; } } @@ -48,6 +66,7 @@ public class DirResult { public bool ScanDir { get; set; } public string DirPath { get; set; } + public RwStatus RwStatus { get; set; } public Triage Triage { get; set; } } } diff --git a/SnaffCore/Classifiers/EffectiveAccess.cs b/SnaffCore/Classifiers/EffectiveAccess.cs index 834f572a..74b7c5c7 100644 --- a/SnaffCore/Classifiers/EffectiveAccess.cs +++ b/SnaffCore/Classifiers/EffectiveAccess.cs @@ -1,5 +1,10 @@  +using System; +using System.IO; +using System.Security.AccessControl; +using System.Security.Principal; + namespace SnaffCore.Classifiers.EffectiveAccess { public class RwStatus @@ -9,4 +14,68 @@ public class RwStatus public bool CanModify { get; set; } } + public class EffectivePermissions + { + private readonly string _username; + + public EffectivePermissions(string username) + { + _username = username; + } + + public RwStatus CanRw(AuthorizationRuleCollection acl) + { + RwStatus rwStatus = new RwStatus(); + + foreach (FileSystemAccessRule rule in acl) + { + if (rule.IdentityReference.Value.Equals(_username, StringComparison.OrdinalIgnoreCase)) + { + if (((rule.FileSystemRights & FileSystemRights.Read) == FileSystemRights.Read) || + ((rule.FileSystemRights & FileSystemRights.ReadAndExecute) == FileSystemRights.ReadAndExecute) || + ((rule.FileSystemRights & FileSystemRights.ReadData) == FileSystemRights.ReadData) || + ((rule.FileSystemRights & FileSystemRights.ListDirectory) == FileSystemRights.ListDirectory)) + { + rwStatus.CanRead = true; + } + if (((rule.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) || + ((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || + ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || + ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || + ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions) || + ((rule.FileSystemRights & FileSystemRights.AppendData) == FileSystemRights.AppendData) || + ((rule.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData) || + ((rule.FileSystemRights & FileSystemRights.CreateFiles) == FileSystemRights.CreateFiles) || + ((rule.FileSystemRights & FileSystemRights.CreateDirectories) == FileSystemRights.CreateDirectories)) + { + rwStatus.CanWrite = true; + } + if (((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || + ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || + ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || + ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions)) + { + rwStatus.CanModify = true; + } + } + } + + + return rwStatus; + } + + public RwStatus CanRw(FileInfo fileInfo) + { + FileSecurity fileSecurity = fileInfo.GetAccessControl(); + AuthorizationRuleCollection acl = fileSecurity.GetAccessRules(true, true, typeof(NTAccount)); + return CanRw(acl); + } + + public RwStatus CanRw(DirectoryInfo dirInfo) + { + DirectorySecurity dirSecurity = dirInfo.GetAccessControl(); + AuthorizationRuleCollection acl = dirSecurity.GetAccessRules(true, true, typeof(NTAccount)); + return CanRw(acl); + } + } } diff --git a/SnaffCore/Classifiers/FileResult.cs b/SnaffCore/Classifiers/FileResult.cs index 4865eea4..72752786 100644 --- a/SnaffCore/Classifiers/FileResult.cs +++ b/SnaffCore/Classifiers/FileResult.cs @@ -14,19 +14,8 @@ public class FileResult public FileResult(FileInfo fileInfo) { - //EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); - - // get an aggressively simplified version of the file's ACL - //this.RwStatus = effPerms.CanRw(fileInfo); - try - { - File.OpenRead(fileInfo.FullName); - this.RwStatus = new RwStatus() { CanRead = true, CanModify = false, CanWrite = false }; - } - catch (Exception e) - { - this.RwStatus = new RwStatus() { CanModify = false, CanRead = false, CanWrite = false }; - } + EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); + this.RwStatus = effPerms.CanRw(fileInfo); // nasty debug this.FileInfo = fileInfo; @@ -56,58 +45,5 @@ public void SnaffleFile(FileInfo fileInfo, string snafflePath) Directory.CreateDirectory(snaffleDirPath); File.Copy(sourcePath, (Path.Combine(snafflePath, cleanedPath)), true); } - - /* - public static EffectivePermissions.RwStatus CanRw(FileInfo fileInfo) - { - BlockingMq Mq = BlockingMq.GetMq(); - - try - { - EffectivePermissions.RwStatus rwStatus = new EffectivePermissions.RwStatus { CanWrite = false, CanRead = false, CanModify = false }; - EffectivePermissions effPerms = new EffectivePermissions(); - string dir = fileInfo.DirectoryName; - - // we hard code this otherwise it tries to do some madness where it uses RPC with a share server to check file access, then fails if you're not admin on that host. - string hostname = "localhost"; - - string whoami = WindowsIdentity.GetCurrent().Name; - - string[] accessStrings = effPerms.GetEffectivePermissions(fileInfo, whoami); - - string[] readRights = new string[] { "Read", "ReadAndExecute", "ReadData", "ListDirectory" }; - string[] writeRights = new string[] { "Write", "Modify", "FullControl", "TakeOwnership", "ChangePermissions", "AppendData", "WriteData", "CreateFiles", "CreateDirectories" }; - string[] modifyRights = new string[] { "Modify", "FullControl", "TakeOwnership", "ChangePermissions" }; - - foreach (string access in accessStrings) - { - if (access == "FullControl") - { - rwStatus.CanModify = true; - rwStatus.CanRead = true; - rwStatus.CanWrite = true; - } - if (readRights.Contains(access)){ - rwStatus.CanRead = true; - } - if (writeRights.Contains(access)) - { - rwStatus.CanWrite = true; - } - if (modifyRights.Contains(access)) - { - rwStatus.CanModify = true; - } - } - - return rwStatus; - } - catch (Exception e) - { - Mq.Error(e.ToString()); - return new EffectivePermissions.RwStatus { CanWrite = false, CanRead = false }; ; - } - } - */ } } \ No newline at end of file diff --git a/SnaffCore/Classifiers/ShareClassifier.cs b/SnaffCore/Classifiers/ShareClassifier.cs index 4e6e7d15..9df4c49e 100644 --- a/SnaffCore/Classifiers/ShareClassifier.cs +++ b/SnaffCore/Classifiers/ShareClassifier.cs @@ -1,4 +1,5 @@ -using SnaffCore.Concurrency; +using SnaffCore.Classifiers.EffectiveAccess; +using SnaffCore.Concurrency; using System; using System.IO; using static SnaffCore.Config.Options; @@ -76,9 +77,7 @@ public class ShareResult public string SharePath { get; set; } public string ShareComment { get; set; } public bool Listable { get; set; } - public bool RootWritable { get; set; } - public bool RootReadable { get; set; } - public bool RootModifyable { get; set; } + public RwStatus RwStatus { get; set; } public Triage Triage { get; set; } = Triage.Gray; } } \ No newline at end of file diff --git a/SnaffCore/Config/Options.cs b/SnaffCore/Config/Options.cs index 78c66282..c4489735 100644 --- a/SnaffCore/Config/Options.cs +++ b/SnaffCore/Config/Options.cs @@ -23,6 +23,8 @@ public partial class Options public bool ScanSysvol { get; set; } = true; public bool ScanNetlogon { get; set; } = true; public bool ScanFoundShares { get; set; } = true; + public bool ScanFoundFiles { get; set; } = true; + public bool LogEverything { get; set; } = false; public int InterestLevel { get; set; } = 0; public bool DfsOnly { get; set; } = false; public bool DfsShareDiscovery { get; set; } = false; diff --git a/SnaffCore/ShareFind/ShareFinder.cs b/SnaffCore/ShareFind/ShareFinder.cs index 4d1395c1..74202ed5 100644 --- a/SnaffCore/ShareFind/ShareFinder.cs +++ b/SnaffCore/ShareFind/ShareFinder.cs @@ -19,7 +19,7 @@ public class ShareFinder private BlockingMq Mq { get; set; } private BlockingStaticTaskScheduler TreeTaskScheduler { get; set; } private TreeWalker TreeWalker { get; set; } - //private EffectivePermissions effectivePermissions { get; set; } = new EffectivePermissions(MyOptions.CurrentUser); + private EffectivePermissions EffectivePermissions { get; set; } = new EffectivePermissions(MyOptions.CurrentUser); public ShareFinder() { @@ -162,19 +162,14 @@ internal void GetComputerShares(string computer) try { DirectoryInfo dirInfo = new DirectoryInfo(shareResult.SharePath); + RwStatus rwStatus = EffectivePermissions.CanRw(dirInfo); + shareResult.RwStatus = rwStatus; - //EffectivePermissions.RwStatus rwStatus = effectivePermissions.CanRw(dirInfo); - - shareResult.RootModifyable = false; - shareResult.RootWritable = false; - shareResult.RootReadable = true; - - /* - if (rwStatus.CanWrite || rwStatus.CanModify) + if (rwStatus.CanWrite || rwStatus.CanModify) { - triage = Triage.Yellow; + shareResult.Triage = Triage.Yellow; } - */ + } catch (System.UnauthorizedAccessException e) { diff --git a/SnaffCore/TreeWalk/TreeWalker.cs b/SnaffCore/TreeWalk/TreeWalker.cs index c3cf3fc3..3be741e4 100644 --- a/SnaffCore/TreeWalk/TreeWalker.cs +++ b/SnaffCore/TreeWalk/TreeWalker.cs @@ -27,52 +27,55 @@ public void WalkTree(string currentDir) { // Walks a tree checking files and generating results as it goes. - if (!Directory.Exists(currentDir)) - { - return; - } + if (!Directory.Exists(currentDir)) + { + return; + } - try + if (MyOptions.ScanFoundFiles) { - string[] files = Directory.GetFiles(currentDir); - // check if we actually like the files - foreach (string file in files) + try { - FileTaskScheduler.New(() => + string[] files = Directory.GetFiles(currentDir); + // check if we actually like the files + foreach (string file in files) { - try + FileTaskScheduler.New(() => { - FileScanner.ScanFile(file); - } - catch (Exception e) - { - Mq.Error("Exception in FileScanner task for file " + file); - Mq.Trace(e.ToString()); - } - }); + try + { + FileScanner.ScanFile(file); + } + catch (Exception e) + { + Mq.Error("Exception in FileScanner task for file " + file); + Mq.Trace(e.ToString()); + } + }); + } + } + catch (UnauthorizedAccessException) + { + //Mq.Trace(e.ToString()); + //continue; + } + catch (DirectoryNotFoundException) + { + //Mq.Trace(e.ToString()); + //continue; + } + catch (IOException) + { + //Mq.Trace(e.ToString()); + //continue; + } + catch (Exception e) + { + Mq.Degub(e.ToString()); + //continue; } } - catch (UnauthorizedAccessException) - { - //Mq.Trace(e.ToString()); - //continue; - } - catch (DirectoryNotFoundException) - { - //Mq.Trace(e.ToString()); - //continue; - } - catch (IOException) - { - //Mq.Trace(e.ToString()); - //continue; - } - catch (Exception e) - { - Mq.Degub(e.ToString()); - //continue; - } - + try { string[] subDirs = Directory.GetDirectories(currentDir); diff --git a/Snaffler/Config.cs b/Snaffler/Config.cs index 0a7e9a57..7125d3cb 100644 --- a/Snaffler/Config.cs +++ b/Snaffler/Config.cs @@ -100,13 +100,17 @@ private static Options ParseImpl(string[] args) SwitchArgument dfsArg = new SwitchArgument('f', "dfs", "Limits Snaffler to finding file shares via DFS, for \"OPSEC\" reasons.", false); SwitchArgument findSharesOnlyArg = new SwitchArgument('a', "sharesonly", "Stops after finding shares, doesn't walk their filesystems.", false); + SwitchArgument findFoldersOnlyArg = new SwitchArgument('g', "foldersonly", + "Stops after finding folders, doesn't scan files.", false); + SwitchArgument logEverything = new SwitchArgument('w', "logeverything", + "Log everything.", false); ValueArgument compExclusionArg = new ValueArgument('k', "exclusions", "Path to a file containing a list of computers to exclude from scanning."); ValueArgument compTargetArg = new ValueArgument('n', "comptarget", "List of computers in a file(e.g C:\targets.txt), a single Computer (or comma separated list) to target."); ValueArgument ruleDirArg = new ValueArgument('p', "rulespath", "Path to a directory full of toml-formatted rules. Snaffler will load all of these in place of the default ruleset."); ValueArgument logType = new ValueArgument('t', "logtype", "Type of log you would like to output. Currently supported options are plain and JSON. Defaults to plain."); ValueArgument timeOutArg = new ValueArgument('e', "timeout", "Interval between status updates (in minutes) also acts as a timeout for AD data to be gathered via LDAP. Turn this knob up if you aren't getting any computers from AD when you run Snaffler through a proxy or other slow link. Default = 5"); - // list of letters i haven't used yet: gnqw + // list of letters i haven't used yet: q CommandLineParser.CommandLineParser parser = new CommandLineParser.CommandLineParser(); parser.Arguments.Add(timeOutArg); @@ -127,6 +131,8 @@ private static Options ParseImpl(string[] args) parser.Arguments.Add(tsvArg); parser.Arguments.Add(dfsArg); parser.Arguments.Add(findSharesOnlyArg); + parser.Arguments.Add(findFoldersOnlyArg); + parser.Arguments.Add(logEverything); parser.Arguments.Add(maxThreadsArg); parser.Arguments.Add(compTargetArg); parser.Arguments.Add(ruleDirArg); @@ -258,6 +264,16 @@ private static Options ParseImpl(string[] args) { parsedConfig.ScanFoundShares = false; } + if (findFoldersOnlyArg.Parsed) + { + parsedConfig.ScanFoundFiles = false; + } + + if (logEverything.Parsed) + { + parsedConfig.LogEverything = true; + } + if (maxThreadsArg.Parsed) { parsedConfig.MaxThreads = maxThreadsArg.Value; diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index 4d01882d..f23dfb3f 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -65,7 +65,7 @@ public void Run(string[] args) { fileResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{2}" + Options.Separator + "{3}" + Options.Separator + "{4}" + Options.Separator + "{5}" + Options.Separator + "{6}" + Options.Separator + "{7:u}" + Options.Separator + "{8}" + Options.Separator + "{9}"; shareResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{2}"; - dirResultTemplate = "{0}" + Options.Separator + "{1}"; + dirResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{3}"; } // otherwise just do the normal thing else @@ -73,7 +73,7 @@ public void Run(string[] args) // treat all as strings except LastWriteTime {6} fileResultTemplate = "{{{0}}}<{1}|{2}{3}{4}|{5}|{6}|{7:u}>({8}) {9}"; shareResultTemplate = "{{{0}}}<{1}>({2}) {3}"; - dirResultTemplate = "{{{0}}}({1})"; + dirResultTemplate = "{{{0}}}<{1}>({2})"; } //------------------------------------------ // set up new fangled logging @@ -371,15 +371,15 @@ public string ShareResultLogFromMessage(SnafflerMessage message) string shareComment = message.ShareResult.ShareComment; string rwString = ""; - if (message.ShareResult.RootReadable) + if (message.ShareResult.RwStatus.CanRead) { rwString = rwString + "R"; } - if (message.ShareResult.RootWritable) + if (message.ShareResult.RwStatus.CanWrite) { rwString = rwString + "W"; } - if (message.ShareResult.RootModifyable) + if (message.ShareResult.RwStatus.CanModify) { rwString = rwString + "M"; } @@ -391,7 +391,22 @@ public string DirResultLogFromMessage(SnafflerMessage message) { string sharePath = message.DirResult.DirPath; string triage = message.DirResult.Triage.ToString(); - return string.Format(dirResultTemplate, triage, sharePath); + + string rwString = ""; + if (message.DirResult.RwStatus.CanRead) + { + rwString = rwString + "R"; + } + if (message.DirResult.RwStatus.CanWrite) + { + rwString = rwString + "W"; + } + if (message.DirResult.RwStatus.CanModify) + { + rwString = rwString + "M"; + } + + return string.Format(dirResultTemplate, triage, sharePath, rwString); } public string FileResultLogFromMessage(SnafflerMessage message) From c2a24e902b3b65a2dd0a72bb8a3ad2e80032abcd Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Sat, 5 Apr 2025 17:31:58 -0700 Subject: [PATCH 3/7] Refactor Logging --- SnaffCore/Classifiers/EffectiveAccess.cs | 23 +- SnaffCore/Concurrency/SnafflerMessage.cs | 218 ++++++++++++ SnaffCore/Config/Options.cs | 1 + Snaffler/Config.cs | 10 +- Snaffler/SnaffleRunner.cs | 424 ++++------------------- Snaffler/Snaffler.csproj | 1 + 6 files changed, 323 insertions(+), 354 deletions(-) diff --git a/SnaffCore/Classifiers/EffectiveAccess.cs b/SnaffCore/Classifiers/EffectiveAccess.cs index 74b7c5c7..f91d634e 100644 --- a/SnaffCore/Classifiers/EffectiveAccess.cs +++ b/SnaffCore/Classifiers/EffectiveAccess.cs @@ -12,6 +12,17 @@ public class RwStatus public bool CanRead { get; set; } public bool CanWrite { get; set; } public bool CanModify { get; set; } + + public override string ToString() + { + char[] rwChars = { '-', '-', '-' }; + + if (CanRead) rwChars[0] = 'R'; + if (CanWrite) rwChars[1] = 'W'; + if (CanModify) rwChars[2] = 'M'; + + return new string(rwChars); + } } public class EffectivePermissions @@ -31,14 +42,14 @@ public RwStatus CanRw(AuthorizationRuleCollection acl) { if (rule.IdentityReference.Value.Equals(_username, StringComparison.OrdinalIgnoreCase)) { - if (((rule.FileSystemRights & FileSystemRights.Read) == FileSystemRights.Read) || + if ((rwStatus.CanRead != true) && (((rule.FileSystemRights & FileSystemRights.Read) == FileSystemRights.Read) || ((rule.FileSystemRights & FileSystemRights.ReadAndExecute) == FileSystemRights.ReadAndExecute) || ((rule.FileSystemRights & FileSystemRights.ReadData) == FileSystemRights.ReadData) || - ((rule.FileSystemRights & FileSystemRights.ListDirectory) == FileSystemRights.ListDirectory)) + ((rule.FileSystemRights & FileSystemRights.ListDirectory) == FileSystemRights.ListDirectory))) { rwStatus.CanRead = true; } - if (((rule.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) || + if ((rwStatus.CanWrite != true) && (((rule.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) || ((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || @@ -46,14 +57,14 @@ public RwStatus CanRw(AuthorizationRuleCollection acl) ((rule.FileSystemRights & FileSystemRights.AppendData) == FileSystemRights.AppendData) || ((rule.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData) || ((rule.FileSystemRights & FileSystemRights.CreateFiles) == FileSystemRights.CreateFiles) || - ((rule.FileSystemRights & FileSystemRights.CreateDirectories) == FileSystemRights.CreateDirectories)) + ((rule.FileSystemRights & FileSystemRights.CreateDirectories) == FileSystemRights.CreateDirectories))) { rwStatus.CanWrite = true; } - if (((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || + if ((rwStatus.CanModify != true) && (((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || - ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions)) + ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions))) { rwStatus.CanModify = true; } diff --git a/SnaffCore/Concurrency/SnafflerMessage.cs b/SnaffCore/Concurrency/SnafflerMessage.cs index 943c70af..93b1b051 100644 --- a/SnaffCore/Concurrency/SnafflerMessage.cs +++ b/SnaffCore/Concurrency/SnafflerMessage.cs @@ -1,8 +1,20 @@ using SnaffCore.Classifiers; using System; +using System.ComponentModel; +using System.Text.RegularExpressions; +using static SnaffCore.Config.Options; namespace SnaffCore.Concurrency { + public struct SnafflerMessageComponents + { + public string DateTime; + public string Type; + public string Message; + public string Triage; + public string Permissions; + } + public class SnafflerMessage { public DateTime DateTime { get; set; } @@ -11,5 +23,211 @@ public class SnafflerMessage public FileResult FileResult { get; set; } public ShareResult ShareResult { get; set; } public DirResult DirResult { get; set; } + + private static int _typePaddingLength; + private static int _triagePaddingLength; + private static string _hostString; + + private static int GetTypePaddingLength() + { + if (_typePaddingLength == 0) + { + int typePaddingLength = 0; + var enumValues = Enum.GetValues(typeof(SnafflerMessageType)); + + foreach (var enumValue in enumValues) + { + var fieldInfo = typeof(SnafflerMessageType).GetField(enumValue.ToString()); + var descriptionAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute)); + string readableName = descriptionAttribute == null ? enumValue.ToString() : descriptionAttribute.Description; + + if (readableName.Length > typePaddingLength) + { + typePaddingLength = readableName.Length; + } + } + + _typePaddingLength = typePaddingLength; + } + + return _typePaddingLength; + } + + private static int GetTriagePaddingLength() + { + if (_triagePaddingLength == 0) + { + int triagePaddingLength = 0; + var enumValues = Enum.GetValues(typeof(Triage)); + + foreach (var enumValue in enumValues) + { + var fieldInfo = typeof(Triage).GetField(enumValue.ToString()); + var descriptionAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute)); + string readableName = descriptionAttribute == null ? enumValue.ToString() : descriptionAttribute.Description; + + if (readableName.Length > triagePaddingLength) + { + triagePaddingLength = readableName.Length; + } + } + + _triagePaddingLength = triagePaddingLength; + } + + return _triagePaddingLength; + } + + private static string GetHostString() + { + if (string.IsNullOrWhiteSpace(_hostString)) + { + _hostString = "[" + System.Security.Principal.WindowsIdentity.GetCurrent().Name + "@" + System.Net.Dns.GetHostName() + "]"; + } + + return _hostString; + } + + // get a more user friendly name for a type variant if provided + private string GetUserReadableType() + { + var typeVariantField = Type.GetType().GetField(Type.ToString()); + var typeVariantAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(typeVariantField, typeof(DescriptionAttribute)); + return typeVariantAttribute == null ? Type.ToString() : typeVariantAttribute.Description; + } + + private static String BytesToString(long byteCount) + { + string[] suf = { "B", "kB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB + if (byteCount == 0) + return "0" + suf[0]; + long bytes = Math.Abs(byteCount); + int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + double num = Math.Round(bytes / Math.Pow(1024, place), 1); + return (Math.Sign(byteCount) * num) + suf[place]; + } + + public SnafflerMessageComponents ToStringComponents() + { + return ToStringComponents(true, true); + } + + public SnafflerMessageComponents ToStringComponents(bool includeTriage, bool includePermissions) + { + + // this is everything that stays the same between message types + string dateTime = String.Format("{1}{0}{2:u}", MyOptions.Separator, GetHostString(), DateTime.ToUniversalTime()); + string readableType = GetUserReadableType(); + string formattedMessage = Message; + + string triageString = ""; + string permissionsString = ""; + + switch (Type) + { + case SnafflerMessageType.FileResult: + string fileResultTemplate = MyOptions.LogTSV ? "{1}{0}{2}{0}{3}{0}{4}{0}{5}{0}{6}" : "{1}{0}<{2}|{3}|{4}|{5:u}>{0}{6}"; + + try + { + string matchedclassifier = FileResult.MatchedRule.RuleName; + DateTime modifiedStamp = FileResult.FileInfo.LastWriteTime.ToUniversalTime(); + + string matchedstring = ""; + + long fileSize = FileResult.FileInfo.Length; + + string fileSizeString; + + // TSV output will probably be machine-consumed. Don't pretty it up. + if (MyOptions.LogTSV) + { + fileSizeString = fileSize.ToString(); + } + else + { + fileSizeString = BytesToString(fileSize); + } + + string filepath = FileResult.FileInfo.FullName; + + string matchcontext = ""; + if (FileResult.TextResult != null) + { + matchedstring = FileResult.TextResult.MatchedStrings[0]; + matchcontext = FileResult.TextResult.MatchContext; + matchcontext = Regex.Replace(matchcontext, @"\r\n?|\n", "\\n"); // Replace newlines with \n for consistent log lines + } + + triageString = FileResult.MatchedRule.Triage.ToString(); + permissionsString = FileResult.RwStatus.ToString(); + + formattedMessage = string.Format(fileResultTemplate, MyOptions.Separator, filepath, matchedclassifier, matchedstring, fileSizeString, modifiedStamp, matchcontext); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + Console.WriteLine(FileResult.FileInfo.FullName); + } + break; + case SnafflerMessageType.DirResult: + triageString = DirResult.Triage.ToString(); + permissionsString = DirResult.RwStatus.ToString(); + formattedMessage = DirResult.DirPath; + break; + case SnafflerMessageType.ShareResult: + string shareResultTemplate = MyOptions.LogTSV ? "{1}{0}{2}" : "{1}{2}"; + + triageString = ShareResult.Triage.ToString(); + permissionsString = ShareResult.RwStatus.ToString(); + + // this lets us do conditional formatting, we don't want angled brackets around a blank comment + string shareComment = ShareResult.ShareComment; + if (ShareResult.ShareComment.Length > 0) { + shareComment = MyOptions.Separator + "<" + shareComment + ">"; + } + + formattedMessage = string.Format(shareResultTemplate, MyOptions.Separator, ShareResult.SharePath, shareComment); + break; + } + + return new SnafflerMessageComponents + { + DateTime = dateTime, + Type = readableType, + Message = formattedMessage, + Triage = triageString, + Permissions = permissionsString, + }; + } + + public static string StringFromComponents(SnafflerMessageComponents components) + { + return StringFromComponents(components, true, true); + } + + public static string StringFromComponents(SnafflerMessageComponents components, bool includeTriage, bool includePermissions) + { + string paddedType = ("[" + components.Type + "]"); + if (!MyOptions.LogTSV) paddedType = paddedType.PadRight(GetTypePaddingLength() + 2); + + string triageString = ""; + if (includeTriage && components.Triage.Length > 0) + { + triageString = ("[" + components.Triage + "]"); + if (!MyOptions.LogTSV) triageString = triageString.PadRight(GetTriagePaddingLength() + 2); + triageString += MyOptions.Separator; + } + + string permissionsString = (includePermissions && components.Permissions.Length > 0) ? String.Format(MyOptions.LogTSV ? "{1}{0}" : "[{1}]{0}", MyOptions.Separator, components.Permissions) : ""; + + return String.Format("{1}{0}{2}{0}{3}{4}{0}{5}", MyOptions.Separator, components.DateTime, paddedType, triageString, permissionsString, components.Message); + } + + public override string ToString() + { + SnafflerMessageComponents components = ToStringComponents(); + return SnafflerMessage.StringFromComponents(components); + } } } \ No newline at end of file diff --git a/SnaffCore/Config/Options.cs b/SnaffCore/Config/Options.cs index 65154ed5..9016db80 100644 --- a/SnaffCore/Config/Options.cs +++ b/SnaffCore/Config/Options.cs @@ -26,6 +26,7 @@ public partial class Options public bool ScanFoundShares { get; set; } = true; public bool ScanFoundFiles { get; set; } = true; public bool LogEverything { get; set; } = false; + public bool NoColorLogs { get; set; } = false; public int InterestLevel { get; set; } = 0; public bool DfsOnly { get; set; } = false; public bool DfsShareDiscovery { get; set; } = false; diff --git a/Snaffler/Config.cs b/Snaffler/Config.cs index 63431c9c..b0fd4d8d 100644 --- a/Snaffler/Config.cs +++ b/Snaffler/Config.cs @@ -104,13 +104,15 @@ private static Options ParseImpl(string[] args) "Stops after finding folders, doesn't scan files.", false); SwitchArgument logEverything = new SwitchArgument('w', "logeverything", "Log everything.", false); + SwitchArgument noColorLogs = new SwitchArgument('q', "nocolor", + "Do not color logs.", false); ValueArgument compExclusionArg = new ValueArgument('k', "exclusions", "Path to a file containing a list of computers to exclude from scanning."); ValueArgument compTargetArg = new ValueArgument('n', "comptarget", "List of computers in a file(e.g C:\targets.txt), a single Computer (or comma separated list) to target."); ValueArgument ruleDirArg = new ValueArgument('p', "rulespath", "Path to a directory full of toml-formatted rules. Snaffler will load all of these in place of the default ruleset."); ValueArgument logType = new ValueArgument('t', "logtype", "Type of log you would like to output. Currently supported options are plain and JSON. Defaults to plain."); ValueArgument timeOutArg = new ValueArgument('e', "timeout", "Interval between status updates (in minutes) also acts as a timeout for AD data to be gathered via LDAP. Turn this knob up if you aren't getting any computers from AD when you run Snaffler through a proxy or other slow link. Default = 5"); - // list of letters i haven't used yet: q + // list of letters i haven't used yet: CommandLineParser.CommandLineParser parser = new CommandLineParser.CommandLineParser(); parser.Arguments.Add(timeOutArg); @@ -133,6 +135,7 @@ private static Options ParseImpl(string[] args) parser.Arguments.Add(findSharesOnlyArg); parser.Arguments.Add(findFoldersOnlyArg); parser.Arguments.Add(logEverything); + parser.Arguments.Add(noColorLogs); parser.Arguments.Add(maxThreadsArg); parser.Arguments.Add(compTargetArg); parser.Arguments.Add(ruleDirArg); @@ -278,6 +281,11 @@ private static Options ParseImpl(string[] args) parsedConfig.LogEverything = true; } + if (noColorLogs.Parsed) + { + parsedConfig.NoColorLogs = true; + } + if (maxThreadsArg.Parsed) { parsedConfig.MaxThreads = maxThreadsArg.Value; diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index cd10e388..69ef8299 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -12,6 +12,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.ComponentModel; +using System.Web; namespace Snaffler { @@ -22,26 +23,8 @@ public class SnaffleRunner private LogLevel LogLevel { get; set; } private Options Options { get; set; } - private string _hostString; - - private string fileResultTemplate { get; set; } - private string shareResultTemplate { get; set; } - private string dirResultTemplate { get; set; } - - private string hostString() - { - if (string.IsNullOrWhiteSpace(_hostString)) - { - _hostString = "[" + System.Security.Principal.WindowsIdentity.GetCurrent().Name + "@" + System.Net.Dns.GetHostName() + "]"; - } - - return _hostString; - } - public void Run(string[] args) { - // prime the hoststring lazy instantiator - hostString(); // print the thing PrintBanner(); // set up the message queue for operation @@ -61,27 +44,12 @@ public void Run(string[] args) return; } - // set up the TSV output if the flag is set - if (Options.LogTSV) - { - fileResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{2}" + Options.Separator + "{3}" + Options.Separator + "{4}" + Options.Separator + "{5}" + Options.Separator + "{6}" + Options.Separator + "{7:u}" + Options.Separator + "{8}" + Options.Separator + "{9}"; - shareResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{2}"; - dirResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{3}"; - } - // otherwise just do the normal thing - else - { - // treat all as strings except LastWriteTime {6} - fileResultTemplate = "{{{0}}}<{1}|{2}{3}{4}|{5}|{6}|{7:u}>({8}) {9}"; - shareResultTemplate = "{{{0}}}<{1}>({2}) {3}"; - dirResultTemplate = "{{{0}}}<{1}>({2})"; - } //------------------------------------------ // set up new fangled logging //------------------------------------------ LoggingConfiguration nlogConfig = new LoggingConfiguration(); nlogConfig.Variables["encoding"] = "utf8"; - ColoredConsoleTarget logconsole = null; + TargetWithLayoutHeaderAndFooter logconsole = null; FileTarget logfile = null; ParseLogLevelString(Options.LogLevelString); @@ -89,58 +57,54 @@ public void Run(string[] args) // Targets where to log to: File and Console if (Options.LogToConsole) { - logconsole = new ColoredConsoleTarget("logconsole") + if (Options.NoColorLogs) + { + logconsole = new ConsoleTarget("logconsole"); + } + else { - DetectOutputRedirected = true, - UseDefaultRowHighlightingRules = false, - WordHighlightingRules = + logconsole = new ColoredConsoleTarget("logconsole") { - new ConsoleWordHighlightingRule("{Green}", ConsoleOutputColor.DarkGreen, - ConsoleOutputColor.White), - new ConsoleWordHighlightingRule("{Yellow}", ConsoleOutputColor.DarkYellow, - ConsoleOutputColor.White), - new ConsoleWordHighlightingRule("{Red}", ConsoleOutputColor.DarkRed, - ConsoleOutputColor.White), - new ConsoleWordHighlightingRule("{Black}", ConsoleOutputColor.Black, - ConsoleOutputColor.White), - - new ConsoleWordHighlightingRule("[Trace]", ConsoleOutputColor.DarkGray, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Degub]", ConsoleOutputColor.Gray, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Info]", ConsoleOutputColor.White, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Error]", ConsoleOutputColor.Magenta, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Fatal]", ConsoleOutputColor.Red, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[File]", ConsoleOutputColor.Green, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Share]", ConsoleOutputColor.Yellow, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule + DetectOutputRedirected = true, + UseDefaultRowHighlightingRules = false, + WordHighlightingRules = { - CompileRegex = true, - Regex = @"<.*\|.*\|.*\|.*?>", - ForegroundColor = ConsoleOutputColor.Cyan, - BackgroundColor = ConsoleOutputColor.Black - }, - new ConsoleWordHighlightingRule - { - CompileRegex = true, - Regex = @"^\d\d\d\d-\d\d\-\d\d \d\d:\d\d:\d\d [\+-]\d\d:\d\d ", - ForegroundColor = ConsoleOutputColor.DarkGray, - BackgroundColor = ConsoleOutputColor.Black - }, - new ConsoleWordHighlightingRule - { - CompileRegex = true, - Regex = @"\((?:[^\)]*\)){1}", - ForegroundColor = ConsoleOutputColor.DarkMagenta, - BackgroundColor = ConsoleOutputColor.Black + new ConsoleWordHighlightingRule("[Green]", ConsoleOutputColor.Green, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Yellow]", ConsoleOutputColor.Yellow, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Red]", ConsoleOutputColor.Red, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Black]", ConsoleOutputColor.White, + ConsoleOutputColor.Black), + + new ConsoleWordHighlightingRule("[Trace]", ConsoleOutputColor.DarkGray, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Degub]", ConsoleOutputColor.Gray, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Info]", ConsoleOutputColor.White, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Error]", ConsoleOutputColor.Magenta, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Fatal]", ConsoleOutputColor.Red, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[File]", ConsoleOutputColor.DarkCyan, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Share]", ConsoleOutputColor.Cyan, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Dir]", ConsoleOutputColor.Blue, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule + { + CompileRegex = true, + Regex = @"<.*?>", + ForegroundColor = ConsoleOutputColor.Cyan, + BackgroundColor = ConsoleOutputColor.Black + } } - } - }; + }; + } + if (LogLevel == LogLevel.Warn) { nlogConfig.AddRule(LogLevel.Warn, LogLevel.Warn, logconsole); @@ -188,7 +152,7 @@ public void Run(string[] args) } else if (Options.LogType == LogType.HTML) { - logfile.Layout = "${longdate}${event-properties:htmlFields:objectPath=DateTime}${level}${event-properties:htmlFields:objectPath=Type}${message}"; + logfile.Layout = "${longdate}${event-properties:htmlFields:objectPath=DateTime}${level}${event-properties:htmlFields:objectPath=Type}${event-properties:htmlFields:objectPath=Triage}${event-properties:htmlFields:objectPath=Permissions}${event-properties:htmlFields:objectPath=Message}"; } } @@ -216,6 +180,10 @@ public void Run(string[] args) exit = true; } } + + if (Options.LogType == LogType.JSON) FixJSONOutput(); + if (Options.LogType == LogType.HTML) FixHTMLOutput(); + return; } catch (Exception e) @@ -244,18 +212,7 @@ private bool HandleOutput() BlockingMq Mq = BlockingMq.GetMq(); foreach (SnafflerMessage message in Mq.Q.GetConsumingEnumerable()) { - if (Options.LogType == LogType.Plain) - { - ProcessMessage(message); - } - else if (Options.LogType == LogType.JSON) - { - ProcessMessageJSON(message); - } - else if (Options.LogType == LogType.HTML) - { - ProcessMessageHTML(message); - } + ProcessMessage(message); // catch terminating messages and bail out of the master 'while' loop if ((message.Type == SnafflerMessageType.Fatal) || (message.Type == SnafflerMessageType.Finish)) @@ -268,285 +225,69 @@ private bool HandleOutput() private void ProcessMessage(SnafflerMessage message) { - // standardized time formatting, UTC - string datetime = String.Format("{1}{0}{2:u}{0}", Options.Separator, hostString(), message.DateTime.ToUniversalTime()); + Logger messageLogger = Logger; + string formattedMessageString; - switch (message.Type) + switch (Options.LogType) { - case SnafflerMessageType.Trace: - Logger.Trace(datetime + "[Trace]" + Options.Separator + message.Message); + case LogType.Plain: + formattedMessageString = message.ToString(); break; - case SnafflerMessageType.Degub: - Logger.Debug(datetime + "[Degub]" + Options.Separator + message.Message); - break; - case SnafflerMessageType.Info: - Logger.Info(datetime + "[Info]" + Options.Separator + message.Message); - break; - case SnafflerMessageType.FileResult: - Logger.Warn(datetime + "[File]" + Options.Separator + FileResultLogFromMessage(message)); + case LogType.JSON: + formattedMessageString = message.ToString(); + messageLogger = messageLogger.WithProperty("SnafflerMessage", message); break; - case SnafflerMessageType.DirResult: - Logger.Warn(datetime + "[Dir]" + Options.Separator + DirResultLogFromMessage(message)); - break; - case SnafflerMessageType.ShareResult: - Logger.Warn(datetime + "[Share]" + Options.Separator + ShareResultLogFromMessage(message)); - break; - case SnafflerMessageType.Error: - Logger.Error(datetime + "[Error]" + Options.Separator + message.Message); - break; - case SnafflerMessageType.Fatal: - Logger.Fatal(datetime + "[Fatal]" + Options.Separator + message.Message); - if (Debugger.IsAttached) - { - Console.ReadKey(); - } - break; - case SnafflerMessageType.Finish: - Logger.Info("Snaffler out."); - - if (Debugger.IsAttached) - { - Console.WriteLine("Press any key to exit."); - Console.ReadKey(); - } - break; - } - } + case LogType.HTML: + SnafflerMessageComponents components = message.ToStringComponents(); - private void ProcessMessageJSON(SnafflerMessage message) - { - // standardized time formatting, UTC - string datetime = String.Format("{1}{0}{2:u}{0}", Options.Separator, hostString(), message.DateTime.ToUniversalTime()); - - switch (message.Type) - { - case SnafflerMessageType.Trace: - //Logger.Trace(message); - Logger.Trace(datetime + "[Trace]" + Options.Separator + message.Message, message); - break; - case SnafflerMessageType.Degub: - //Logger.Debug(message); - Logger.Debug(datetime + "[Degub]" + Options.Separator + message.Message, message); - break; - case SnafflerMessageType.Info: - //Logger.Info(message); - Logger.Info(datetime + "[Info]" + Options.Separator + message.Message, message); - break; - case SnafflerMessageType.FileResult: - //Logger.Warn(message); - Logger.Warn(datetime + "[File]" + Options.Separator + FileResultLogFromMessage(message), message); - break; - case SnafflerMessageType.DirResult: - //Logger.Warn(message); - Logger.Warn(datetime + "[Dir]" + Options.Separator + DirResultLogFromMessage(message), message); - break; - case SnafflerMessageType.ShareResult: - //Logger.Warn(message); - Logger.Warn(datetime + "[Share]" + Options.Separator + ShareResultLogFromMessage(message), message); - break; - case SnafflerMessageType.Error: - //Logger.Error(message); - Logger.Error(datetime + "[Error]" + Options.Separator + message.Message, message); - break; - case SnafflerMessageType.Fatal: - //Logger.Fatal(message); - Logger.Fatal(datetime + "[Fatal]" + Options.Separator + message.Message, message); - if (Debugger.IsAttached) - { - Console.ReadKey(); - } + formattedMessageString = SnafflerMessage.StringFromComponents(components); + messageLogger = messageLogger.WithProperty("htmlFields", new { components.DateTime, components.Type, components.Triage, components.Permissions, Message = HttpUtility.HtmlEncode(components.Message) }); break; - case SnafflerMessageType.Finish: - Logger.Info("Snaffler out."); - - if (Debugger.IsAttached) - { - Console.WriteLine("Press any key to exit."); - Console.ReadKey(); - } - if (Options.LogType == LogType.JSON) - { - Logger.Info("Normalising output, please wait..."); - FixJSONOutput(); - } + default: + // this should be unreachable but whatever + formattedMessageString = message.ToString(); break; } - } - - private void ProcessMessageHTML(SnafflerMessage message) - { - // standardized time formatting, UTC - string datetime = String.Format("{1}{0}{2:u}{0}", Options.Separator, hostString(), message.DateTime.ToUniversalTime()); - - // get a more user friendly name for a type variant if provided - var typeVariantField = message.Type.GetType().GetField(message.Type.ToString()); - var typeVariantAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(typeVariantField, typeof(DescriptionAttribute)); - string userReadableType = typeVariantAttribute == null ? message.Type.ToString() : typeVariantAttribute.Description; - - var htmlFields = new { DateTime = datetime, Type = userReadableType }; switch (message.Type) { case SnafflerMessageType.Trace: - //Logger.Trace(message); - Logger.WithProperty("htmlFields", htmlFields).Trace(message.Message); + messageLogger.Trace(formattedMessageString); break; case SnafflerMessageType.Degub: - //Logger.Debug(message); - Logger.WithProperty("htmlFields", htmlFields).Debug(message.Message); + messageLogger.Debug(formattedMessageString); break; case SnafflerMessageType.Info: - //Logger.Info(message); - Logger.WithProperty("htmlFields", htmlFields).Info(message.Message); + messageLogger.Info(formattedMessageString); break; case SnafflerMessageType.FileResult: - //Logger.Warn(message); - Logger.WithProperty("htmlFields", htmlFields).Warn(FileResultLogFromMessage(message)); - break; case SnafflerMessageType.DirResult: - //Logger.Warn(message); - Logger.WithProperty("htmlFields", htmlFields).Warn(DirResultLogFromMessage(message)); - break; case SnafflerMessageType.ShareResult: - //Logger.Warn(message); - Logger.WithProperty("htmlFields", htmlFields).Warn(ShareResultLogFromMessage(message)); + messageLogger.Warn(formattedMessageString); break; case SnafflerMessageType.Error: - //Logger.Error(message); - Logger.WithProperty("htmlFields", htmlFields).Error(message.Message); + messageLogger.Error(formattedMessageString); break; case SnafflerMessageType.Fatal: - //Logger.Fatal(message); - Logger.WithProperty("htmlFields", htmlFields).Fatal(message.Message); + messageLogger.Fatal(formattedMessageString); if (Debugger.IsAttached) { Console.ReadKey(); } break; case SnafflerMessageType.Finish: - Logger.Info("Snaffler out."); - + messageLogger.Info("Snaffler out."); + if (Debugger.IsAttached) { Console.WriteLine("Press any key to exit."); Console.ReadKey(); } - if (Options.LogType == LogType.HTML) - { - Logger.Info("Normalising output, please wait..."); - FixHTMLOutput(); - } - break; - } - } - - public string ShareResultLogFromMessage(SnafflerMessage message) - { - string sharePath = message.ShareResult.SharePath; - string triage = message.ShareResult.Triage.ToString(); - string shareComment = message.ShareResult.ShareComment; - - string rwString = ""; - if (message.ShareResult.RwStatus.CanRead) - { - rwString = rwString + "R"; - } - if (message.ShareResult.RwStatus.CanWrite) - { - rwString = rwString + "W"; - } - if (message.ShareResult.RwStatus.CanModify) - { - rwString = rwString + "M"; - } - - return string.Format(shareResultTemplate, triage, sharePath, rwString, shareComment); - } - - public string DirResultLogFromMessage(SnafflerMessage message) - { - string sharePath = message.DirResult.DirPath; - string triage = message.DirResult.Triage.ToString(); - string rwString = ""; - if (message.DirResult.RwStatus.CanRead) - { - rwString = rwString + "R"; - } - if (message.DirResult.RwStatus.CanWrite) - { - rwString = rwString + "W"; - } - if (message.DirResult.RwStatus.CanModify) - { - rwString = rwString + "M"; + break; } - - return string.Format(dirResultTemplate, triage, sharePath, rwString); } - public string FileResultLogFromMessage(SnafflerMessage message) - { - try - { - string matchedclassifier = message.FileResult.MatchedRule.RuleName; - string triageString = message.FileResult.MatchedRule.Triage.ToString(); - DateTime modifiedStamp = message.FileResult.FileInfo.LastWriteTime.ToUniversalTime(); - - string canread = ""; - if (message.FileResult.RwStatus.CanRead) - { - canread = "R"; - } - - string canwrite = ""; - if (message.FileResult.RwStatus.CanWrite) - { - canwrite = "W"; - } - - string canmodify = ""; - if (message.FileResult.RwStatus.CanModify) - { - canmodify = "M"; - } - - string matchedstring = ""; - - long fileSize = message.FileResult.FileInfo.Length; - - string fileSizeString; - - // TSV output will probably be machine-consumed. Don't pretty it up. - if (Options.LogTSV) - { - fileSizeString = fileSize.ToString(); - } - else - { - fileSizeString = BytesToString(fileSize); - } - - string filepath = message.FileResult.FileInfo.FullName; - - string matchcontext = ""; - if (message.FileResult.TextResult != null) - { - matchedstring = message.FileResult.TextResult.MatchedStrings[0]; - matchcontext = message.FileResult.TextResult.MatchContext; - matchcontext = Regex.Replace(matchcontext, @"\r\n?|\n", "\\n"); // Replace newlines with \n for consistent log lines - } - - return string.Format(fileResultTemplate, triageString, matchedclassifier, canread, canwrite, canmodify, matchedstring, fileSizeString, modifiedStamp, - filepath, matchcontext); - } - catch (Exception e) - { - Console.WriteLine(e.ToString()); - Console.WriteLine(message.FileResult.FileInfo.FullName); - return ""; - } - } private void ParseLogLevelString(string logLevelString) { switch (logLevelString.ToLower()) @@ -579,17 +320,6 @@ private void ParseLogLevelString(string logLevelString) } } - private static String BytesToString(long byteCount) - { - string[] suf = { "B", "kB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB - if (byteCount == 0) - return "0" + suf[0]; - long bytes = Math.Abs(byteCount); - int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); - double num = Math.Round(bytes / Math.Pow(1024, place), 1); - return (Math.Sign(byteCount) * num) + suf[place]; - } - public void WriteColor(string textToWrite, ConsoleColor fgColor) { Console.ForegroundColor = fgColor; @@ -680,7 +410,7 @@ private void FixHTMLOutput() using (StreamWriter file = new StreamWriter(Options.LogFilePath)) { //Write the start of the surrounding template that we need - file.Write("Snaffler Logs
"); + file.Write("Snaffler Logs
TimestampDateTimeLevelTypeMessage
"); //Open the original file using (FileStream sourceStream = new FileStream(Options.LogFilePath + ".tmp", FileMode.Open, FileAccess.Read)) diff --git a/Snaffler/Snaffler.csproj b/Snaffler/Snaffler.csproj index f84fdba9..e49e62a3 100644 --- a/Snaffler/Snaffler.csproj +++ b/Snaffler/Snaffler.csproj @@ -63,6 +63,7 @@ + From 55e42c409e973d4f30fd7b4d9fce75db0979954f Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Sat, 5 Apr 2025 20:38:31 -0400 Subject: [PATCH 4/7] Update README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 490b4c0e..e6355b9c 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ The key incantations are: `-a` Skips file enumeration, just gives you a list of listable shares on the target hosts. +`-g` Skips file enumeration, just gives you a list of shares and folders on the target hosts. (combine with `-w` for the most useful results) + `-u` Makes Snaffler pull a list of account names from AD, choose the ones that look most-interesting, and then use them in a search rule. `-d` Domain to search for computers to search for shares on to search for files in. Easy. @@ -74,12 +76,16 @@ The key incantations are: `-z` Path to a config file that defines all of the above, and much much more! See below for more details. Give it `-z generate` to generate a sample config file called `.\default.toml`. -`-t` Type of log you would like to output. Currently supported options are plain and JSON. Defaults to plain. +`-t` Type of log you would like to output. Currently supported options are plain, JSON, and HTML. Defaults to plain. `-x` Max number of threads to use. Don't set it below 4 or shit will break. `-p` Path to a directory full of .toml formatted rules. Snaffler will load all of these in place of the default ruleset. +`-w` Log everything (currently just logs directories walked but not marked for snaffling) + +`-q` Disable coloring of console output + ## What does any of this log output mean? Hopefully this annotated example will help: From f8db4f2ed267b671b5f2c7f3b1fc20de131e9174 Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Sat, 5 Apr 2025 20:40:52 -0400 Subject: [PATCH 5/7] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 490b4c0e..ea3c1d55 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ The key incantations are: `-a` Skips file enumeration, just gives you a list of listable shares on the target hosts. +`-g` Skips file enumeration, just gives you a list of shares and folders on the target hosts. (combine with `-w `for the most useful results) + `-u` Makes Snaffler pull a list of account names from AD, choose the ones that look most-interesting, and then use them in a search rule. `-d` Domain to search for computers to search for shares on to search for files in. Easy. @@ -80,6 +82,8 @@ The key incantations are: `-p` Path to a directory full of .toml formatted rules. Snaffler will load all of these in place of the default ruleset. +`-w` Log everything (currently just logs directories walked but not marked for snaffling) + ## What does any of this log output mean? Hopefully this annotated example will help: From 3a28c4b506d8ed2fdb97f3e084ee5bb889a37fef Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Mon, 7 Apr 2025 20:25:15 -0700 Subject: [PATCH 6/7] Move unauthorized exception handling into canRw --- SnaffCore/Classifiers/EffectiveAccess.cs | 57 +++++++++++++----------- SnaffCore/ShareFind/ShareFinder.cs | 18 +++----- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/SnaffCore/Classifiers/EffectiveAccess.cs b/SnaffCore/Classifiers/EffectiveAccess.cs index 74b7c5c7..0c7b0c37 100644 --- a/SnaffCore/Classifiers/EffectiveAccess.cs +++ b/SnaffCore/Classifiers/EffectiveAccess.cs @@ -27,39 +27,42 @@ public RwStatus CanRw(AuthorizationRuleCollection acl) { RwStatus rwStatus = new RwStatus(); - foreach (FileSystemAccessRule rule in acl) + try { - if (rule.IdentityReference.Value.Equals(_username, StringComparison.OrdinalIgnoreCase)) + foreach (FileSystemAccessRule rule in acl) { - if (((rule.FileSystemRights & FileSystemRights.Read) == FileSystemRights.Read) || - ((rule.FileSystemRights & FileSystemRights.ReadAndExecute) == FileSystemRights.ReadAndExecute) || - ((rule.FileSystemRights & FileSystemRights.ReadData) == FileSystemRights.ReadData) || - ((rule.FileSystemRights & FileSystemRights.ListDirectory) == FileSystemRights.ListDirectory)) + if (rule.IdentityReference.Value.Equals(_username, StringComparison.OrdinalIgnoreCase)) { - rwStatus.CanRead = true; - } - if (((rule.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) || - ((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || - ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || - ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || - ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions) || - ((rule.FileSystemRights & FileSystemRights.AppendData) == FileSystemRights.AppendData) || - ((rule.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData) || - ((rule.FileSystemRights & FileSystemRights.CreateFiles) == FileSystemRights.CreateFiles) || - ((rule.FileSystemRights & FileSystemRights.CreateDirectories) == FileSystemRights.CreateDirectories)) - { - rwStatus.CanWrite = true; - } - if (((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || - ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || - ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || - ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions)) - { - rwStatus.CanModify = true; + if (((rule.FileSystemRights & FileSystemRights.Read) == FileSystemRights.Read) || + ((rule.FileSystemRights & FileSystemRights.ReadAndExecute) == FileSystemRights.ReadAndExecute) || + ((rule.FileSystemRights & FileSystemRights.ReadData) == FileSystemRights.ReadData) || + ((rule.FileSystemRights & FileSystemRights.ListDirectory) == FileSystemRights.ListDirectory)) + { + rwStatus.CanRead = true; + } + if (((rule.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) || + ((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || + ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || + ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || + ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions) || + ((rule.FileSystemRights & FileSystemRights.AppendData) == FileSystemRights.AppendData) || + ((rule.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData) || + ((rule.FileSystemRights & FileSystemRights.CreateFiles) == FileSystemRights.CreateFiles) || + ((rule.FileSystemRights & FileSystemRights.CreateDirectories) == FileSystemRights.CreateDirectories)) + { + rwStatus.CanWrite = true; + } + if (((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || + ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || + ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || + ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions)) + { + rwStatus.CanModify = true; + } } } } - + catch (UnauthorizedAccessException) { } return rwStatus; } diff --git a/SnaffCore/ShareFind/ShareFinder.cs b/SnaffCore/ShareFind/ShareFinder.cs index 74202ed5..31b41619 100644 --- a/SnaffCore/ShareFind/ShareFinder.cs +++ b/SnaffCore/ShareFind/ShareFinder.cs @@ -159,21 +159,13 @@ internal void GetComputerShares(string computer) // Share is readable, report as green (the old default/min of the Triage enum ) shareResult.Triage = Triage.Green; - try - { - DirectoryInfo dirInfo = new DirectoryInfo(shareResult.SharePath); - RwStatus rwStatus = EffectivePermissions.CanRw(dirInfo); - shareResult.RwStatus = rwStatus; + DirectoryInfo dirInfo = new DirectoryInfo(shareResult.SharePath); + RwStatus rwStatus = EffectivePermissions.CanRw(dirInfo); + shareResult.RwStatus = rwStatus; - if (rwStatus.CanWrite || rwStatus.CanModify) - { - shareResult.Triage = Triage.Yellow; - } - - } - catch (System.UnauthorizedAccessException e) + if (rwStatus.CanWrite || rwStatus.CanModify) { - Mq.Error("Failed to get permissions on " + shareResult.SharePath); + shareResult.Triage = Triage.Yellow; } if (MyOptions.ScanFoundShares) From 511c28095453395a90ca57929a3eff767128f8ce Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Mon, 7 Apr 2025 21:05:44 -0700 Subject: [PATCH 7/7] Initialize RwStatus as default on share results --- SnaffCore/Classifiers/ShareClassifier.cs | 9 ++++++++- SnaffCore/ShareFind/ShareFinder.cs | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/SnaffCore/Classifiers/ShareClassifier.cs b/SnaffCore/Classifiers/ShareClassifier.cs index 9df4c49e..153d4eec 100644 --- a/SnaffCore/Classifiers/ShareClassifier.cs +++ b/SnaffCore/Classifiers/ShareClassifier.cs @@ -33,11 +33,18 @@ public bool ClassifyShare(string share) // in this context snaffle means 'send a report up the queue, and scan the share further' if (IsShareReadable(share)) { + // is this supposed to be here? + DirectoryInfo shareInfo = new DirectoryInfo(share); + + EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); + RwStatus rwStatus = effPerms.CanRw(shareInfo); + ShareResult shareResult = new ShareResult() { Triage = ClassifierRule.Triage, Listable = true, - SharePath = share + SharePath = share, + RwStatus = rwStatus }; Mq.ShareResult(shareResult); } diff --git a/SnaffCore/ShareFind/ShareFinder.cs b/SnaffCore/ShareFind/ShareFinder.cs index 31b41619..0732a765 100644 --- a/SnaffCore/ShareFind/ShareFinder.cs +++ b/SnaffCore/ShareFind/ShareFinder.cs @@ -94,7 +94,8 @@ internal void GetComputerShares(string computer) { Listable = true, SharePath = shareName, - ShareComment = hostShareInfo.shi1_remark.ToString() + ShareComment = hostShareInfo.shi1_remark.ToString(), + RwStatus = new RwStatus() }; // Try to find this computer+share in the list of DFS targets
TimestampDateTimeLevelTypeTriagePermissionsMessage