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) {