using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using BurnOutSharp.FileType; using BurnOutSharp.Utilities; using static BurnOutSharp.Utilities.Dictionary; namespace BurnOutSharp { public class Scanner { #region Options /// /// Determines whether archives are decompressed and scanned /// public bool ScanArchives { get; private set; } /// /// Determines if content matches are used or not /// public bool ScanContents { get; private set; } /// /// Determines if packers are counted as detected protections or not /// public bool ScanPackers { get; private set; } /// /// Determines if path matches are used or not /// public bool ScanPaths { get; private set; } /// /// Determines if debug information is output or not /// public bool IncludeDebug { get; private set; } #endregion /// /// Optional progress callback during scanning /// private readonly IProgress fileProgress; /// /// Constructor /// /// Enable scanning archive contents /// Enable including content detections in output /// Enable including packers in output /// Enable including path detections in output /// Enable including debug information /// Optional progress callback public Scanner(bool scanArchives, bool scanContents, bool scanPackers, bool scanPaths, bool includeDebug, IProgress fileProgress = null) { ScanArchives = scanArchives; ScanContents = scanContents; ScanPackers = scanPackers; ScanPaths = scanPaths; IncludeDebug = includeDebug; this.fileProgress = fileProgress; // Register the codepages Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } #region Scanning /// /// Scan a single path and get all found protections /// /// Path to scan /// Dictionary of list of strings representing the found protections public ConcurrentDictionary> GetProtections(string path) { return GetProtections(new List { path }); } /// /// Scan the list of paths and get all found protections /// /// Dictionary of list of strings representing the found protections public ConcurrentDictionary> GetProtections(List paths) { // If we have no paths, we can't scan if (paths == null || !paths.Any()) return null; // Set a starting starting time for debug output DateTime startTime = DateTime.UtcNow; // Checkpoint this.fileProgress?.Report(new ProtectionProgress(null, 0, null)); // Temp variables for reporting string tempFilePath = Path.GetTempPath(); string tempFilePathWithGuid = Path.Combine(tempFilePath, Guid.NewGuid().ToString()); // Loop through each path and get the returned values var protections = new ConcurrentDictionary>(); foreach (string path in paths) { // Directories scan each internal file individually if (Directory.Exists(path)) { // Enumerate all files at first for easier access var files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).ToList(); // Scan for path-detectable protections if (ScanPaths) { var directoryPathProtections = GetDirectoryPathProtections(path, files); AppendToDictionary(protections, directoryPathProtections); } // Scan each file in directory separately for (int i = 0; i < files.Count; i++) { // Get the current file string file = files.ElementAt(i); // Get the reportable file name string reportableFileName = file; if (reportableFileName.StartsWith(tempFilePath)) reportableFileName = reportableFileName.Substring(tempFilePathWithGuid.Length); // Checkpoint this.fileProgress?.Report(new ProtectionProgress(reportableFileName, i / (float)files.Count, "Checking file" + (file != reportableFileName ? " from archive" : string.Empty))); // Scan for path-detectable protections if (ScanPaths) { var filePathProtections = GetFilePathProtections(file); AppendToDictionary(protections, filePathProtections); } // Scan for content-detectable protections var fileProtections = GetInternalProtections(file); if (fileProtections != null && fileProtections.Any()) { foreach (string key in fileProtections.Keys) { if (!protections.ContainsKey(key)) protections[key] = new ConcurrentQueue(); protections[key].AddRange(fileProtections[key]); } } // Checkpoint protections.TryGetValue(file, out ConcurrentQueue fullProtectionList); string fullProtection = (fullProtectionList != null && fullProtectionList.Any() ? string.Join(", ", fullProtectionList) : null); this.fileProgress?.Report(new ProtectionProgress(reportableFileName, (i + 1) / (float)files.Count, fullProtection ?? string.Empty)); } } // Scan a single file by itself else if (File.Exists(path)) { // Get the reportable file name string reportableFileName = path; if (reportableFileName.StartsWith(tempFilePath)) reportableFileName = reportableFileName.Substring(tempFilePathWithGuid.Length); // Checkpoint this.fileProgress?.Report(new ProtectionProgress(reportableFileName, 0, "Checking file" + (path != reportableFileName ? " from archive" : string.Empty))); // Scan for path-detectable protections if (ScanPaths) { var filePathProtections = GetFilePathProtections(path); AppendToDictionary(protections, filePathProtections); } // Scan for content-detectable protections var fileProtections = GetInternalProtections(path); if (fileProtections != null && fileProtections.Any()) { foreach (string key in fileProtections.Keys) { if (!protections.ContainsKey(key)) protections[key] = new ConcurrentQueue(); protections[key].AddRange(fileProtections[key]); } } // Checkpoint protections.TryGetValue(path, out ConcurrentQueue fullProtectionList); string fullProtection = (fullProtectionList != null && fullProtectionList.Any() ? string.Join(", ", fullProtectionList) : null); this.fileProgress?.Report(new ProtectionProgress(reportableFileName, 1, fullProtection ?? string.Empty)); } // Throw on an invalid path else { Console.WriteLine($"{path} is not a directory or file, skipping..."); //throw new FileNotFoundException($"{path} is not a directory or file, skipping..."); } } // Clear out any empty keys ClearEmptyKeys(protections); // If we're in debug, output the elasped time to console if (IncludeDebug) Console.WriteLine($"Time elapsed: {DateTime.UtcNow.Subtract(startTime)}"); return protections; } /// /// Get the path-detectable protections associated with a single path /// /// Path of the directory to scan /// Files contained within /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetDirectoryPathProtections(string path, List files) { // Create an empty queue for protections var protections = new ConcurrentQueue(); // Preprocess the list of files files = files.Select(f => f.Replace('\\', '/')).ToList(); // Iterate through all path checks Parallel.ForEach(ScanningClasses.PathCheckClasses, pathCheckClass => { ConcurrentQueue protection = pathCheckClass.CheckDirectoryPath(path, files); if (protection != null) protections.AddRange(protection); }); // Create and return the dictionary return new ConcurrentDictionary> { [path] = protections }; } /// /// Get the path-detectable protections associated with a single path /// /// Path of the file to scan /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetFilePathProtections(string path) { // Create an empty queue for protections var protections = new ConcurrentQueue(); // Iterate through all path checks Parallel.ForEach(ScanningClasses.PathCheckClasses, pathCheckClass => { string protection = pathCheckClass.CheckFilePath(path.Replace("\\", "/")); if (!string.IsNullOrWhiteSpace(protection)) protections.Enqueue(protection); }); // Create and return the dictionary return new ConcurrentDictionary> { [path] = protections }; } /// /// Get the content-detectable protections associated with a single path /// /// Path to the file to scan /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetInternalProtections(string file) { // Quick sanity check before continuing if (!File.Exists(file)) return null; // Open the file and begin scanning try { using (FileStream fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) { return GetInternalProtections(file, fs); } } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); var protections = new ConcurrentDictionary>(); AppendToDictionary(protections, file, IncludeDebug ? ex.ToString() : "[Exception opening file, please try again]"); ClearEmptyKeys(protections); return protections; } } /// /// Get the content-detectable protections associated with a single path /// /// Name of the source file of the stream, for tracking /// Stream to scan the contents of /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetInternalProtections(string fileName, Stream stream) { // Quick sanity check before continuing if (stream == null || !stream.CanRead || !stream.CanSeek) return null; // Initialize the protections found var protections = new ConcurrentDictionary>(); // Get the extension for certain checks string extension = Path.GetExtension(fileName).ToLower().TrimStart('.'); // Open the file and begin scanning try { // Get the first 16 bytes for matching byte[] magic = new byte[16]; try { stream.Read(magic, 0, 16); stream.Seek(0, SeekOrigin.Begin); } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); return null; } // Get the file type either from magic number or extension SupportedFileType fileType = Tools.Utilities.GetFileType(magic); if (fileType == SupportedFileType.UNKNOWN) fileType = Tools.Utilities.GetFileType(extension); // If we still got unknown, just return null if (fileType == SupportedFileType.UNKNOWN) return null; // Create a scannable for the given file type var scannable = Tools.Utilities.CreateScannable(fileType); if (scannable == null) return null; #region Non-Archive File Types // If we're scanning file contents if (ScanContents) { // AACS media key block if (scannable is AACSMediaKeyBlock) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } // BD+ SVM if (scannable is BDPlusSVM) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } // Executable if (scannable is Executable) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } // LDSCRYPT if (scannable is LDSCRYPT) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } // PLJ if (scannable is PLJ) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } // SFFS if (scannable is SFFS) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } // Text-based files if (scannable is Textfile) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } } #endregion #region Archive File Types // If we're scanning archives if (ScanArchives) { // 7-Zip archive if (scannable is SevenZip) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // BFPK archive if (scannable is BFPK) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // BSP if (scannable is BSP) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // BZip2 if (scannable is BZip2) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // CFB if (fileName != null && scannable is CFB) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // GCF if (scannable is GCF) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // GZIP if (scannable is GZIP) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // InstallShield Archive V3 (Z) if (fileName != null && scannable is InstallShieldArchiveV3) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // InstallShield Cabinet if (fileName != null && scannable is InstallShieldCAB) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // Microsoft Cabinet if (fileName != null && scannable is MicrosoftCAB) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // Microsoft LZ if (fileName != null && scannable is MicrosoftLZ) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // MoPaQ archive if (fileName != null && scannable is MPQ) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // PAK if (fileName != null && scannable is PAK) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // PFF if (fileName != null && scannable is PFF) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // PKZIP archive (and derivatives) if (scannable is PKZIP) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // RAR archive if (scannable is RAR) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // SGA if (scannable is SGA) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // Tape Archive if (scannable is TapeArchive) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // VBSP if (fileName != null && scannable is VBSP) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // VPK if (fileName != null && scannable is VPK) { var subProtections = scannable.Scan(this, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // WAD if (scannable is WAD) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // XZ if (scannable is XZ) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } // XZP if (scannable is XZP) { var subProtections = scannable.Scan(this, stream, fileName); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); } } #endregion } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); AppendToDictionary(protections, fileName, IncludeDebug ? ex.ToString() : "[Exception opening file, please try again]"); } // Clear out any empty keys ClearEmptyKeys(protections); return protections; } #endregion } }