diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 3ca014b4..044d1db8 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - project: [Test] + project: [ProtectionScan, Test] runtime: [win-x86, win-x64, win-arm64, linux-x64, linux-arm64, osx-x64] framework: [net8.0] #[net20, net35, net40, net452, net472, net48, netcoreapp3.1, net5.0, net6.0, net7.0, net8.0] conf: [Release, Debug] diff --git a/BinaryObjectScanner.sln b/BinaryObjectScanner.sln index a3ae472d..69a17a5d 100644 --- a/BinaryObjectScanner.sln +++ b/BinaryObjectScanner.sln @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryObjectScanner", "BinaryObjectScanner\BinaryObjectScanner.csproj", "{341EA3F5-847C-4739-B86F-2B051FFE4EF2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtectionScan", "ProtectionScan\ProtectionScan.csproj", "{14CC56E0-7D56-497C-BF3D-4C06FA169831}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {341EA3F5-847C-4739-B86F-2B051FFE4EF2}.Debug|Any CPU.Build.0 = Debug|Any CPU {341EA3F5-847C-4739-B86F-2B051FFE4EF2}.Release|Any CPU.ActiveCfg = Release|Any CPU {341EA3F5-847C-4739-B86F-2B051FFE4EF2}.Release|Any CPU.Build.0 = Release|Any CPU + {14CC56E0-7D56-497C-BF3D-4C06FA169831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14CC56E0-7D56-497C-BF3D-4C06FA169831}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14CC56E0-7D56-497C-BF3D-4C06FA169831}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14CC56E0-7D56-497C-BF3D-4C06FA169831}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ProtectionScan/Options.cs b/ProtectionScan/Options.cs new file mode 100644 index 00000000..e14a2bb0 --- /dev/null +++ b/ProtectionScan/Options.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; + +namespace ProtectionScan +{ + /// + /// Set of options for the test executable + /// + internal sealed class Options + { + #region Properties + + /// + /// Enable debug output for relevant operations + /// + public bool Debug { get; private set; } = false; + + /// + /// Set of input paths to use for operations + /// + public List InputPaths { get; private set; } = []; + + /// + /// Scan archives during protection scanning + /// + public bool ScanArchives { get; private set; } = true; + + /// + /// Scan file contents during protection scanning + /// + public bool ScanContents { get; private set; } = true; + + /// + /// Scan game engines during protection scanning + /// + public bool ScanGameEngines { get; private set; } = true; + + /// + /// Scan packers during protection scanning + /// + public bool ScanPackers { get; private set; } = true; + + /// + /// Scan file paths during protection scanning + /// + public bool ScanPaths { get; private set; } = true; + + #endregion + + /// + /// Parse commandline arguments into an Options object + /// + public static Options? ParseOptions(string[] args) + { + // If we have invalid arguments + if (args == null || args.Length == 0) + return null; + + // Create an Options object + var options = new Options(); + + // Parse the options and paths + for (int index = 0; index < args.Length; index++) + { + string arg = args[index]; + switch (arg) + { + case "-?": + case "-h": + case "--help": + return null; + + case "-d": + case "--debug": + options.Debug = true; + break; + + case "-na": + case "--no-archives": + options.ScanArchives = false; + break; + + case "-nc": + case "--no-contents": + options.ScanContents = false; + break; + + case "-ng": + case "--no-game-engines": + options.ScanGameEngines = false; + break; + + case "-np": + case "--no-packers": + options.ScanPackers = false; + break; + + case "-ns": + case "--no-paths": + options.ScanPaths = false; + break; + + default: + options.InputPaths.Add(arg); + break; + } + } + + // Validate we have any input paths to work on + if (options.InputPaths.Count == 0) + { + Console.WriteLine("At least one path is required!"); + return null; + } + + return options; + } + + /// + /// Display help text + /// + public static void DisplayHelp() + { + Console.WriteLine("Protection Scanner"); + Console.WriteLine(); + Console.WriteLine("ProtectionScan.exe file|directory ..."); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine("-?, -h, --help Display this help text and quit"); + Console.WriteLine("-d, --debug Enable debug mode"); + Console.WriteLine("-nc, --no-contents Disable scanning for content checks"); + Console.WriteLine("-na, --no-archives Disable scanning archives"); + Console.WriteLine("-ng, --no-game-engines Disable scanning for game engines"); + Console.WriteLine("-np, --no-packers Disable scanning for packers"); + Console.WriteLine("-ns, --no-paths Disable scanning for path checks"); + } + } +} \ No newline at end of file diff --git a/ProtectionScan/Program.cs b/ProtectionScan/Program.cs new file mode 100644 index 00000000..353f356f --- /dev/null +++ b/ProtectionScan/Program.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Linq; +using BinaryObjectScanner; + +namespace ProtectionScan +{ + class Program + { + static void Main(string[] args) + { +#if NET462_OR_GREATER || NETCOREAPP + // Register the codepages + System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); +#endif + + // Create progress indicator + var fileProgress = new Progress(); + fileProgress.ProgressChanged += Changed; + + // Get the options from the arguments + var options = Options.ParseOptions(args); + + // If we have an invalid state + if (options == null) + { + Options.DisplayHelp(); + return; + } + + // Create scanner for all paths + var scanner = new Scanner( + options.ScanArchives, + options.ScanContents, + options.ScanGameEngines, + options.ScanPackers, + options.ScanPaths, + options.Debug, + fileProgress); + + // Loop through the input paths + foreach (string inputPath in options.InputPaths) + { + GetAndWriteProtections(scanner, inputPath); + } + } + + /// + /// Wrapper to get and log protections for a single path + /// + /// Scanner object to use + /// File or directory path + private static void GetAndWriteProtections(Scanner scanner, string path) + { + // An invalid path can't be scanned + if (!Directory.Exists(path) && !File.Exists(path)) + { + Console.WriteLine($"{path} does not exist, skipping..."); + return; + } + + try + { + var protections = scanner.GetProtections(path); + WriteProtectionResultFile(path, protections); + } + catch (Exception ex) + { + using var sw = new StreamWriter(File.OpenWrite($"exception-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.txt")); + sw.WriteLine(ex); + } + } + + /// + /// Write the protection results from a single path to file, if possible + /// + /// File or directory path + /// Dictionary of protections found, if any + private static void WriteProtectionResultFile(string path, ProtectionDictionary? protections) + { + if (protections == null) + { + Console.WriteLine($"No protections found for {path}"); + return; + } + + using var sw = new StreamWriter(File.OpenWrite($"protection-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.txt")); + foreach (string key in protections.Keys.OrderBy(k => k)) + { + // Skip over files with no protection + if (protections[key] == null || !protections[key].Any()) + continue; + + string line = $"{key}: {string.Join(", ", [.. protections[key].OrderBy(p => p)])}"; + Console.WriteLine(line); + sw.WriteLine(line); + } + } + + /// + /// Protection progress changed handler + /// + private static void Changed(object? source, ProtectionProgress value) + { + Console.WriteLine($"{value.Percentage * 100:N2}%: {value.Filename} - {value.Protection}"); + } + } +} diff --git a/ProtectionScan/ProtectionScan.csproj b/ProtectionScan/ProtectionScan.csproj new file mode 100644 index 00000000..20bfda3c --- /dev/null +++ b/ProtectionScan/ProtectionScan.csproj @@ -0,0 +1,37 @@ + + + + net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 + Exe + false + false + latest + enable + true + true + + + + + win-x86;win-x64 + + + win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64 + + + win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 + + + net6.0;net7.0;net8.0 + + + + + $(DefineConstants);WIN + + + + + + + \ No newline at end of file diff --git a/publish-nix.sh b/publish-nix.sh index 37bbe161..0c61ce07 100644 --- a/publish-nix.sh +++ b/publish-nix.sh @@ -69,6 +69,45 @@ if [ $NO_BUILD = false ]; then # Create Nuget Package dotnet pack BinaryObjectScanner/BinaryObjectScanner.csproj --output $BUILD_FOLDER + # Build ProtectionScan + for FRAMEWORK in "${FRAMEWORKS[@]}"; do + for RUNTIME in "${RUNTIMES[@]}"; do + # Output the current build + echo "===== Build ProtectionScan - $FRAMEWORK, $RUNTIME =====" + + # If we have an invalid combination of framework and runtime + if [[ ! $(echo ${VALID_CROSS_PLATFORM_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then + if [[ $(echo ${VALID_CROSS_PLATFORM_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then + echo "Skipped due to invalid combination" + continue + fi + fi + + # If we have Apple silicon but an unsupported framework + if [[ ! $(echo ${VALID_APPLE_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then + if [ $RUNTIME = "osx-arm64" ]; then + echo "Skipped due to no Apple Silicon support" + continue + fi + fi + + # Only .NET 5 and above can publish to a single file + if [[ $(echo ${SINGLE_FILE_CAPABLE[@]} | fgrep -w $FRAMEWORK) ]]; then + # Only include Debug if building all + if [ $USE_ALL = true ]; then + dotnet publish ProtectionScan/ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true + fi + dotnet publish ProtectionScan/ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false + else + # Only include Debug if building all + if [ $USE_ALL = true ]; then + dotnet publish ProtectionScan/ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT + fi + dotnet publish ProtectionScan/ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false + fi + done + done + # Build Test for FRAMEWORK in "${FRAMEWORKS[@]}"; do for RUNTIME in "${RUNTIMES[@]}"; do @@ -111,6 +150,50 @@ fi # Only create archives if requested if [ $NO_ARCHIVE = false ]; then + # Create ProtectionScan archives + for FRAMEWORK in "${FRAMEWORKS[@]}"; do + for RUNTIME in "${RUNTIMES[@]}"; do + # Output the current build + echo "===== Archive ProtectionScan - $FRAMEWORK, $RUNTIME =====" + + # If we have an invalid combination of framework and runtime + if [[ ! $(echo ${VALID_CROSS_PLATFORM_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then + if [[ $(echo ${VALID_CROSS_PLATFORM_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then + echo "Skipped due to invalid combination" + continue + fi + fi + + # If we have Apple silicon but an unsupported framework + if [[ ! $(echo ${VALID_APPLE_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then + if [ $RUNTIME = "osx-arm64" ]; then + echo "Skipped due to no Apple Silicon support" + continue + fi + fi + + # Only include Debug if building all + if [ $USE_ALL = true ]; then + cd $BUILD_FOLDER/ProtectionScan/bin/Debug/${FRAMEWORK}/${RUNTIME}/publish/ + if [[ $(echo ${NON_DLL_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then + zip -r $BUILD_FOLDER/ProtectionScan_${FRAMEWORK}_${RUNTIME}_debug.zip . -x 'CascLib.dll' -x 'mspack.dll' -x 'StormLib.dll' + elif [[ $(echo ${NON_DLL_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then + zip -r $BUILD_FOLDER/ProtectionScan_${FRAMEWORK}_${RUNTIME}_debug.zip . -x 'CascLib.dll' -x 'mspack.dll' -x 'StormLib.dll' + else + zip -r $BUILD_FOLDER/ProtectionScan_${FRAMEWORK}_${RUNTIME}_debug.zip . + fi + fi + cd $BUILD_FOLDER/ProtectionScan/bin/Release/${FRAMEWORK}/${RUNTIME}/publish/ + if [[ $(echo ${NON_DLL_FRAMEWORKS[@]} | fgrep -w $FRAMEWORK) ]]; then + zip -r $BUILD_FOLDER/ProtectionScan_${FRAMEWORK}_${RUNTIME}_release.zip . -x 'CascLib.dll' -x 'mspack.dll' -x 'StormLib.dll' + elif [[ $(echo ${NON_DLL_RUNTIMES[@]} | fgrep -w $RUNTIME) ]]; then + zip -r $BUILD_FOLDER/ProtectionScan_${FRAMEWORK}_${RUNTIME}_release.zip . -x 'CascLib.dll' -x 'mspack.dll' -x 'StormLib.dll' + else + zip -r $BUILD_FOLDER/ProtectionScan_${FRAMEWORK}_${RUNTIME}_release.zip . + fi + done + done + # Create Test archives for FRAMEWORK in "${FRAMEWORKS[@]}"; do for RUNTIME in "${RUNTIMES[@]}"; do diff --git a/publish-win.ps1 b/publish-win.ps1 index fd9474cb..a14d7f56 100644 --- a/publish-win.ps1 +++ b/publish-win.ps1 @@ -60,6 +60,42 @@ if (!$NO_BUILD.IsPresent) { # Create Nuget Package dotnet pack BinaryObjectScanner\BinaryObjectScanner.csproj --output $BUILD_FOLDER + # Build ProtectionScan + foreach ($FRAMEWORK in $FRAMEWORKS) { + foreach ($RUNTIME in $RUNTIMES) { + # Output the current build + Write-Host "===== Build ProtectionScan - $FRAMEWORK, $RUNTIME =====" + + # If we have an invalid combination of framework and runtime + if ($VALID_CROSS_PLATFORM_FRAMEWORKS -notcontains $FRAMEWORK -and $VALID_CROSS_PLATFORM_RUNTIMES -contains $RUNTIME) { + Write-Host "Skipped due to invalid combination" + continue + } + + # If we have Apple silicon but an unsupported framework + if ($VALID_APPLE_FRAMEWORKS -notcontains $FRAMEWORK -and $RUNTIME -eq 'osx-arm64') { + Write-Host "Skipped due to no Apple Silicon support" + continue + } + + # Only .NET 5 and above can publish to a single file + if ($SINGLE_FILE_CAPABLE -contains $FRAMEWORK) { + # Only include Debug if building all + if ($USE_ALL.IsPresent) { + dotnet publish ProtectionScan\ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true + } + dotnet publish ProtectionScan\ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false + } + else { + # Only include Debug if building all + if ($USE_ALL.IsPresent) { + dotnet publish ProtectionScan\ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Debug --self-contained true --version-suffix $COMMIT + } + dotnet publish ProtectionScan\ProtectionScan.csproj -f $FRAMEWORK -r $RUNTIME -c Release --self-contained true --version-suffix $COMMIT -p:DebugType=None -p:DebugSymbols=false + } + } + } + # Build Test foreach ($FRAMEWORK in $FRAMEWORKS) { foreach ($RUNTIME in $RUNTIMES) { @@ -99,6 +135,45 @@ if (!$NO_BUILD.IsPresent) { # Only create archives if requested if (!$NO_ARCHIVE.IsPresent) { + # Create ProtectionScan archives + foreach ($FRAMEWORK in $FRAMEWORKS) { + foreach ($RUNTIME in $RUNTIMES) { + # Output the current build + Write-Host "===== Archive ProtectionScan - $FRAMEWORK, $RUNTIME =====" + + # If we have an invalid combination of framework and runtime + if ($VALID_CROSS_PLATFORM_FRAMEWORKS -notcontains $FRAMEWORK -and $VALID_CROSS_PLATFORM_RUNTIMES -contains $RUNTIME) { + Write-Host "Skipped due to invalid combination" + continue + } + + # If we have Apple silicon but an unsupported framework + if ($VALID_APPLE_FRAMEWORKS -notcontains $FRAMEWORK -and $RUNTIME -eq 'osx-arm64') { + Write-Host "Skipped due to no Apple Silicon support" + continue + } + + # Only include Debug if building all + if ($USE_ALL.IsPresent) { + Set-Location -Path $BUILD_FOLDER\ProtectionScan\bin\Debug\${FRAMEWORK}\${RUNTIME}\publish\ + if ($NON_DLL_FRAMEWORKS -contains $FRAMEWORK -or $NON_DLL_RUNTIMES -contains $RUNTIME) { + 7z a -tzip -x'!CascLib.dll' -x'!mspack.dll' -x'!StormLib.dll' $BUILD_FOLDER\ProtectionScan_${FRAMEWORK}_${RUNTIME}_debug.zip * + } + else { + 7z a -tzip $BUILD_FOLDER\ProtectionScan_${FRAMEWORK}_${RUNTIME}_debug.zip * + } + } + + Set-Location -Path $BUILD_FOLDER\ProtectionScan\bin\Release\${FRAMEWORK}\${RUNTIME}\publish\ + if ($NON_DLL_FRAMEWORKS -contains $FRAMEWORK -or $NON_DLL_RUNTIMES -contains $RUNTIME) { + 7z a -tzip -x'!CascLib.dll' -x'!mspack.dll' -x'!StormLib.dll' $BUILD_FOLDER\ProtectionScan_${FRAMEWORK}_${RUNTIME}_release.zip * + } + else { + 7z a -tzip $BUILD_FOLDER\ProtectionScan_${FRAMEWORK}_${RUNTIME}_release.zip * + } + } + } + # Create Test archives foreach ($FRAMEWORK in $FRAMEWORKS) { foreach ($RUNTIME in $RUNTIMES) {