From 0d6c75b6a7aad1bfe616e8a0eba4befaa1d42148 Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Wed, 26 Nov 2025 21:40:19 +0100 Subject: [PATCH 1/9] docs: enhance and add missing documentation --- .../FileValidatorConfiguration.cs | 4 +- .../Exceptions/EmptyFileException.cs | 14 +++- .../Exceptions/InvalidFileSizeException.cs | 9 ++- .../InvalidOpenDocumentFormatException.cs | 12 +++ .../InvalidOpenXmlFormatException.cs | 14 +++- .../Exceptions/InvalidSignatureException.cs | 9 ++- .../Exceptions/UnsupportedFileException.cs | 10 ++- src/ByteGuard.FileValidator/FileExtensions.cs | 77 ++++++++++++++++++- src/ByteGuard.FileValidator/FileValidator.cs | 66 ++++++++-------- .../Models/FileDefinition.cs | 4 +- .../ByteGuard.FileValidator.Tests.Unit.csproj | 1 + .../ConfigurationValidatorTests.cs | 6 +- .../FileValidatorTests.cs | 2 +- 13 files changed, 178 insertions(+), 50 deletions(-) diff --git a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs index 2549cd9..4b60682 100644 --- a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs +++ b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs @@ -42,7 +42,7 @@ public class FileValidatorConfiguration /// /// /// - public string FriendlyFileSizeLimit { get; set; } + public string FriendlyFileSizeLimit { get; set; } = default!; /// /// Whether to throw an exception if an unsupported/invalid file is encountered. Defaults to true. @@ -56,4 +56,4 @@ public class FileValidatorConfiguration /// public bool ThrowExceptionOnInvalidFile { get; set; } = true; } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs b/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs index 13c3882..9c0d66b 100644 --- a/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs @@ -7,17 +7,29 @@ namespace ByteGuard.FileValidator.Exceptions /// public class EmptyFileException : Exception { + /// + /// Construct a new to indicate that the provided file does not have any content. + /// public EmptyFileException() : this("The provided file does not have any content.") { } + /// + /// Construct a new to indicate that the provided file does not have any content. + /// + /// Custom exception message. public EmptyFileException(string message) : base(message) { } + /// + /// Construct a new to indicate that the provided file does not have any content. + /// + /// Custom exception message. + /// Inner exception. public EmptyFileException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs index bde401a..f704c12 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs @@ -8,13 +8,20 @@ namespace ByteGuard.FileValidator.Exceptions /// public class InvalidFileSizeException : Exception { + /// + /// Construct a new to indicate that the provided file does not adhere to the configured limit. + /// public InvalidFileSizeException() : base("File size is larger than the configured file size limit.") { } + /// + /// Construct a new to indicate that the provided file does not adhere to the configured limit. + /// + /// Custom exception message. public InvalidFileSizeException(string message) : base(message) { } } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs index 0b64530..021015a 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs @@ -8,15 +8,27 @@ namespace ByteGuard.FileValidator.Exceptions /// /// public class InvalidOpenDocumentFormatException : Exception { + /// + /// Construct a new to indicate that the provided file does not adhere to the OpenDocument format specification. + /// public InvalidOpenDocumentFormatException() : base("Invalid Open Document Format file.") { } + /// + /// Construct a new to indicate that the provided file does not adhere to the OpenDocument format specification. + /// + /// Custom exception message. public InvalidOpenDocumentFormatException(string message) : base(message) { } + /// + /// Construct a new to indicate that the provided file does not adhere to the OpenDocument format specification. + /// + /// Custom exception message. + /// Inner exception. public InvalidOpenDocumentFormatException(string message, Exception innerException) : base(message, innerException) { } diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs index 478959e..e285d77 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs @@ -8,17 +8,29 @@ namespace ByteGuard.FileValidator.Exceptions /// public class InvalidOpenXmlFormatException : Exception { + /// + /// Construct a new to indicate that the provided file does not adhere to the Microsoft Open XML format specification. + /// public InvalidOpenXmlFormatException() : base("Invalid Open XML file.") { } + /// + /// Construct a new to indicate that the provided file does not adhere to the Microsoft Open XML format specification. + /// + /// Custom exception message. public InvalidOpenXmlFormatException(string message) : base(message) { } + /// + /// Construct a new to indicate that the provided file does not adhere to the Microsoft Open XML format specification. + /// + /// Custom exception message. + /// Inner exception. public InvalidOpenXmlFormatException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs index 630aaeb..8786681 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs @@ -7,13 +7,20 @@ namespace ByteGuard.FileValidator.Exceptions /// public class InvalidSignatureException : Exception { + /// + /// Construct a new to indicate that the provided file signature does not match the file extension. + /// public InvalidSignatureException() : base("File signature does not match the expected signature for the given file type.") { } + /// + /// Construct a new to indicate that the provided file signature does not match the file extension. + /// + /// Custom exception message. public InvalidSignatureException(string message) : base(message) { } } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs b/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs index 5ab5e47..b3ad7d2 100644 --- a/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs @@ -7,14 +7,20 @@ namespace ByteGuard.FileValidator.Exceptions /// public class UnsupportedFileException : Exception { - + /// + /// Construct a new to indicate that the file is not supported as per the configured supported file extensions. + /// public UnsupportedFileException() : base("File type is not supported.") { } + /// + /// Construct a new to indicate that the file is not supported as per the configured supported file extensions. + /// + /// Custom exception message. public UnsupportedFileException(string message) : base(message) { } } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/FileExtensions.cs b/src/ByteGuard.FileValidator/FileExtensions.cs index 7822705..c3f619b 100644 --- a/src/ByteGuard.FileValidator/FileExtensions.cs +++ b/src/ByteGuard.FileValidator/FileExtensions.cs @@ -5,24 +5,99 @@ /// public static class FileExtensions { + /// + /// Joint Photographic Experts Group. + /// public const string Jpeg = ".jpeg"; + + /// + /// Joint Photographic Experts Group. + /// public const string Jpg = ".jpg"; + + /// + /// Joint Photographic Experts Group. + /// public const string Jpe = ".jpe"; + + /// + /// Portable Document Format. + /// public const string Pdf = ".pdf"; + + /// + /// Portable Network Graphics. + /// public const string Png = ".png"; + + /// + /// Bitmap. + /// public const string Bmp = ".bmp"; + + /// + /// Microsoft Word Document. + /// public const string Doc = ".doc"; + + /// + /// Microsoft Word Open XML Document. + /// public const string Docx = ".docx"; + + /// + /// OpenDocument Text. + /// public const string Odt = ".odt"; + + /// + /// Rich Text Format. + /// public const string Rtf = ".rtf"; + + /// + /// Microsoft Excel Spreadsheet. + /// public const string Xls = ".xls"; + + /// + /// Microsoft Excel Open XML Spreadsheet. + /// public const string Xlsx = ".xlsx"; + + /// + /// Microsoft PowerPoint Open XML Presentation. + /// public const string Pptx = ".pptx"; + + /// + /// MPEG-4 Audio. + /// public const string M4a = ".m4a"; + + /// + /// Apple QuickTime Movie. + /// public const string Mov = ".mov"; + + /// + /// Audio Video Interleave. + /// public const string Avi = ".avi"; + + /// + /// MP3 Audio. + /// public const string Mp3 = ".mp3"; + + /// + /// MPEG-4 Video. + /// public const string Mp4 = ".mp4"; + + /// + /// Waveform Audio File Format. + /// public const string Wav = ".wav"; } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/FileValidator.cs b/src/ByteGuard.FileValidator/FileValidator.cs index c01c3c1..b6a9dcb 100644 --- a/src/ByteGuard.FileValidator/FileValidator.cs +++ b/src/ByteGuard.FileValidator/FileValidator.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Packaging; using ByteGuard.FileValidator.Configuration; using ByteGuard.FileValidator.Exceptions; using ByteGuard.FileValidator.Models; @@ -241,17 +237,17 @@ public class FileValidator /// /// File validator configuration instance. /// - private readonly FileValidatorConfiguration configuration; + private readonly FileValidatorConfiguration _configuration; /// - /// Instantiate a new instance of the file validator with the given configuration. + /// Instantiate a new instance of the file validator. /// /// Configuration including which files should be supported and whether an exception should be thrown when encountering an invalid file. public FileValidator(FileValidatorConfiguration configuration) { ConfigurationValidator.ThrowIfInvalid(configuration); - this.configuration = configuration; + _configuration = configuration; } /// @@ -260,7 +256,7 @@ public FileValidator(FileValidatorConfiguration configuration) /// All supported file types as per the configuration. public List GetSupportedFileTypes() { - return configuration.SupportedFileTypes; + return _configuration.SupportedFileTypes; } /// @@ -277,7 +273,7 @@ public bool IsValidFile(string fileName, byte[] content) // Validate file type. if (!IsValidFileType(fileName)) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new UnsupportedFileException(); } @@ -288,7 +284,7 @@ public bool IsValidFile(string fileName, byte[] content) // Validate file size. if (!HasValidSize(content)) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new UnsupportedFileException(); } @@ -299,7 +295,7 @@ public bool IsValidFile(string fileName, byte[] content) // Validate file signature. if (!HasValidSignature(fileName, content)) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidSignatureException(); } @@ -310,7 +306,7 @@ public bool IsValidFile(string fileName, byte[] content) // Validate Open XML conformance for specific file types. if (IsOpenXmlFormat(fileName) && !IsValidOpenXmlDocument(fileName, content)) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenXmlFormatException(); } @@ -321,7 +317,7 @@ public bool IsValidFile(string fileName, byte[] content) // Validate Open Document Format (ODF) for specific file types. if (IsOpenDocumentFormat(fileName) && !IsValidOpenDocumentFormat(fileName, content)) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenDocumentFormatException(); } @@ -398,10 +394,10 @@ public bool IsValidFile(string filePath) public bool IsValidFileType(string fileName) { var extension = Path.GetExtension(fileName).ToLowerInvariant(); - var isSupported = configuration.SupportedFileTypes.Contains(extension, StringComparer.InvariantCultureIgnoreCase) && + var isSupported = _configuration.SupportedFileTypes.Contains(extension, StringComparer.InvariantCultureIgnoreCase) && SupportedFileDefinitions.Any(fd => fd.FileType.Equals(extension, StringComparison.InvariantCultureIgnoreCase)); - if (!isSupported && configuration.ThrowExceptionOnInvalidFile) + if (!isSupported && _configuration.ThrowExceptionOnInvalidFile) { throw new UnsupportedFileException(); } @@ -449,7 +445,7 @@ public bool HasValidSignature(string fileName, byte[] content) fd.FileType.Equals(extension, StringComparison.InvariantCultureIgnoreCase)); if (fileDefinition == null) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new UnsupportedFileException(); } @@ -465,7 +461,7 @@ public bool HasValidSignature(string fileName, byte[] content) using (var pdfValidator = new PdfValidator(content)) { var isValidPdf = pdfValidator.IsValidPdfSignature(); - if (!isValidPdf && configuration.ThrowExceptionOnInvalidFile) + if (!isValidPdf && _configuration.ThrowExceptionOnInvalidFile) { throw new InvalidSignatureException(); } @@ -481,7 +477,7 @@ public bool HasValidSignature(string fileName, byte[] content) // Check whether the content is valid according to the primary header signature length. if (content.Length < signatureEnd) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidSignatureException("File content is too short to contain a valid signature."); } @@ -497,7 +493,7 @@ public bool HasValidSignature(string fileName, byte[] content) // Might as well return early as the subtype check is irrelevant if the primary signature is invalid. if (!result) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidSignatureException(); } @@ -515,7 +511,7 @@ public bool HasValidSignature(string fileName, byte[] content) // Check whether the content is valid according to the primary header signature length. if (content.Length < subtypeSignatureEnd) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidSignatureException("File content is too short to contain a valid subtype signature."); } @@ -529,7 +525,7 @@ public bool HasValidSignature(string fileName, byte[] content) result = fileDefinition.ValidSubtypeSignatures.Any(signature => subtypeHeaderBytes.Take(signature.Length).SequenceEqual(signature)); } - if (!result && configuration.ThrowExceptionOnInvalidFile) + if (!result && _configuration.ThrowExceptionOnInvalidFile) { throw new InvalidSignatureException(); } @@ -605,9 +601,9 @@ public bool HasValidSignature(string filePath) /// >Thrown if the file size is greater than the configured file size limit and is enabled public bool HasValidSize(byte[] content) { - var isBelowLimit = content.Length <= configuration.FileSizeLimit; + var isBelowLimit = content.Length <= _configuration.FileSizeLimit; - if (configuration.ThrowExceptionOnInvalidFile && !isBelowLimit) + if (_configuration.ThrowExceptionOnInvalidFile && !isBelowLimit) { throw new InvalidFileSizeException(); } @@ -628,9 +624,9 @@ public bool HasValidSize(byte[] content) /// >Thrown if the file size is greater than the configured file size limit and is enabled public bool HasValidSize(Stream stream) { - var isBelowLimit = stream.Length <= configuration.FileSizeLimit; + var isBelowLimit = stream.Length <= _configuration.FileSizeLimit; - if (configuration.ThrowExceptionOnInvalidFile && !isBelowLimit) + if (_configuration.ThrowExceptionOnInvalidFile && !isBelowLimit) { throw new InvalidFileSizeException(); } @@ -701,7 +697,7 @@ public bool IsValidOpenXmlDocument(string fileName, byte[] content) // If we are not expecting this file type to be an Open XML file, we can just return false. if (!IsOpenXmlFormat(fileName)) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenXmlFormatException("The provided file extension is not recognized as an Open XML document."); } @@ -735,7 +731,7 @@ public bool IsValidOpenXmlDocument(string fileName, byte[] content) } } - if (configuration.ThrowExceptionOnInvalidFile && !isValid) + if (_configuration.ThrowExceptionOnInvalidFile && !isValid) { throw new InvalidOpenXmlFormatException(); } @@ -744,7 +740,7 @@ public bool IsValidOpenXmlDocument(string fileName, byte[] content) } catch (InvalidDataException e) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenXmlFormatException("The provided file is not a valid Open XML file. See inner exception for details.", e); } @@ -754,7 +750,7 @@ public bool IsValidOpenXmlDocument(string fileName, byte[] content) catch (FileFormatException e) { // Thrown if the content is corrupt. - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenXmlFormatException("File content appears to be corrupt. Se inner exception for details.", e); } @@ -764,7 +760,7 @@ public bool IsValidOpenXmlDocument(string fileName, byte[] content) catch (OpenXmlPackageException e) { // Thrown if the content is not valid Open XML. - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenXmlFormatException("Content does not appear to be valid Open XML format. See inner exception for details.", e); } @@ -774,7 +770,7 @@ public bool IsValidOpenXmlDocument(string fileName, byte[] content) catch (InvalidOpenXmlFormatException) { // Exceptions throw from within the Open XML format validator. - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw; } @@ -872,7 +868,7 @@ public bool IsValidOpenDocumentFormat(string fileName, byte[] content) // If we are not expecting this file type to be an Open XML file, we can just return false. if (!IsOpenDocumentFormat(fileName)) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenDocumentFormatException("The provided file extension is not recognized as an Open Document Format document."); } @@ -896,7 +892,7 @@ public bool IsValidOpenDocumentFormat(string fileName, byte[] content) } } - if (configuration.ThrowExceptionOnInvalidFile && !isValid) + if (_configuration.ThrowExceptionOnInvalidFile && !isValid) { throw new InvalidOpenDocumentFormatException(); } @@ -905,7 +901,7 @@ public bool IsValidOpenDocumentFormat(string fileName, byte[] content) } catch (InvalidDataException e) { - if (configuration.ThrowExceptionOnInvalidFile) + if (_configuration.ThrowExceptionOnInvalidFile) { throw new InvalidOpenDocumentFormatException("The provided file is not a valid Open Document Format file. See inner exception for details.", e); } diff --git a/src/ByteGuard.FileValidator/Models/FileDefinition.cs b/src/ByteGuard.FileValidator/Models/FileDefinition.cs index 5392924..efebd82 100644 --- a/src/ByteGuard.FileValidator/Models/FileDefinition.cs +++ b/src/ByteGuard.FileValidator/Models/FileDefinition.cs @@ -10,7 +10,7 @@ internal class FileDefinition /// /// File type in question (e.g. .jpw, .png, .pdf, etc). /// - public string FileType { get; set; } + public string FileType { get; set; } = default!; /// /// Valid header signatures. @@ -46,4 +46,4 @@ internal class FileDefinition /// public List ValidSubtypeSignatures { get; set; } = new List(); } -} \ No newline at end of file +} diff --git a/tests/ByteGuard.FileValidator.Tests.Unit/ByteGuard.FileValidator.Tests.Unit.csproj b/tests/ByteGuard.FileValidator.Tests.Unit/ByteGuard.FileValidator.Tests.Unit.csproj index b1ab82c..340f3d1 100644 --- a/tests/ByteGuard.FileValidator.Tests.Unit/ByteGuard.FileValidator.Tests.Unit.csproj +++ b/tests/ByteGuard.FileValidator.Tests.Unit/ByteGuard.FileValidator.Tests.Unit.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + false false true diff --git a/tests/ByteGuard.FileValidator.Tests.Unit/ConfigurationValidatorTests.cs b/tests/ByteGuard.FileValidator.Tests.Unit/ConfigurationValidatorTests.cs index d738579..62ee7e8 100644 --- a/tests/ByteGuard.FileValidator.Tests.Unit/ConfigurationValidatorTests.cs +++ b/tests/ByteGuard.FileValidator.Tests.Unit/ConfigurationValidatorTests.cs @@ -9,7 +9,7 @@ public class ConfigurationValidatorTests public void ThrowIfInvalid_ConfigurationIsNull_ShouldThrowArgumentNullException() { // Act - Action act = () => new FileValidator(null); + Action act = () => new FileValidator(null!); // Act & Assert Assert.Throws(act); @@ -21,7 +21,7 @@ public void ThrowIfInvalid_SupportedFileTypesIsNull_ShouldThrowArgumentException // Arrange var config = new FileValidatorConfiguration { - SupportedFileTypes = null + SupportedFileTypes = null! }; // Act @@ -95,4 +95,4 @@ public void ThrowIfInvalid_FileSizeLimitIsLessThanOrEqualToZero_ShouldThrowArgum Assert.Throws(act); } -} \ No newline at end of file +} diff --git a/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs b/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs index a0ca844..a7a2048 100644 --- a/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs +++ b/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs @@ -10,7 +10,7 @@ public class FileValidatorTests public void Constructor_ShouldThrowArgumentNullException_WhenConfigurationIsNotProvided() { // Act - Action act = () => new FileValidator(null); + Action act = () => new FileValidator(null!); // Assert Assert.Throws(act); From 100ee43ccf0627acde85dc185a688e43624608ef Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 20:29:33 +0100 Subject: [PATCH 2/9] feat: add antimalware abstraction --- .../FileValidatorConfiguration.cs | 15 +++--- .../Exceptions/MalwareDetectedException.cs | 33 ++++++++++++ src/ByteGuard.FileValidator/FileValidator.cs | 20 ++++++++ .../Scanners/IAntimalwareScanner.cs | 19 +++++++ .../FileValidatorTests.cs | 50 +++++++++++++++++++ 5 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 src/ByteGuard.FileValidator/Exceptions/MalwareDetectedException.cs create mode 100644 src/ByteGuard.FileValidator/Scanners/IAntimalwareScanner.cs diff --git a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs index 4b60682..eba7caf 100644 --- a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs +++ b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using ByteGuard.FileValidator.Exceptions; +using ByteGuard.FileValidator.Scanners; namespace ByteGuard.FileValidator.Configuration { @@ -47,13 +46,11 @@ public class FileValidatorConfiguration /// /// Whether to throw an exception if an unsupported/invalid file is encountered. Defaults to true. /// - /// - /// Will throw the following exceptions: - ///
    - ///
  • if the given file type is not supported according to the .
  • - ///
  • if the file signature does not match the valid signatures for the given file type.
  • - ///
- ///
public bool ThrowExceptionOnInvalidFile { get; set; } = true; + + /// + /// Optional antimalware scanner to use during file validation. + /// + public IAntimalwareScanner? AntimalwareScanner { get; set; } = null; } } diff --git a/src/ByteGuard.FileValidator/Exceptions/MalwareDetectedException.cs b/src/ByteGuard.FileValidator/Exceptions/MalwareDetectedException.cs new file mode 100644 index 0000000..315faac --- /dev/null +++ b/src/ByteGuard.FileValidator/Exceptions/MalwareDetectedException.cs @@ -0,0 +1,33 @@ +namespace ByteGuard.FileValidator.Exceptions +{ + /// + /// Exception type used specifically when a given file is detected as being malware from antimalware scanners. + /// + public class MalwareDetectedException : Exception + { + /// + /// Construct a new to indicate that the antimalware scanner detected malware in the file. + /// + public MalwareDetectedException() + : this("The antimalware scanner reported malware detection in the given file.") + { + } + + /// + /// Construct a new to indicate that the antimalware scanner detected malware in the file. + /// + /// Custom exception message. + public MalwareDetectedException(string message) : base(message) + { + } + + /// + /// Construct a new to indicate that the antimalware scanner detected malware in the file. + /// + /// Custom exception message. + /// Inner exception. + public MalwareDetectedException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/ByteGuard.FileValidator/FileValidator.cs b/src/ByteGuard.FileValidator/FileValidator.cs index b6a9dcb..0d0aeb6 100644 --- a/src/ByteGuard.FileValidator/FileValidator.cs +++ b/src/ByteGuard.FileValidator/FileValidator.cs @@ -325,6 +325,26 @@ public bool IsValidFile(string fileName, byte[] content) return false; } + // Validate antimalware scan if configured. + if (_configuration.AntimalwareScanner != null) + { + using (var memoryStream = new MemoryStream(content)) + { + memoryStream.Seek(0, SeekOrigin.Begin); + + var isClean = _configuration.AntimalwareScanner.IsClean(memoryStream, fileName); + if (!isClean) + { + if (_configuration.ThrowExceptionOnInvalidFile) + { + throw new MalwareDetectedException(); + } + + return false; + } + } + } + return true; } diff --git a/src/ByteGuard.FileValidator/Scanners/IAntimalwareScanner.cs b/src/ByteGuard.FileValidator/Scanners/IAntimalwareScanner.cs new file mode 100644 index 0000000..572bd72 --- /dev/null +++ b/src/ByteGuard.FileValidator/Scanners/IAntimalwareScanner.cs @@ -0,0 +1,19 @@ +namespace ByteGuard.FileValidator.Scanners; + +/// +/// An abstraction for antimalware scanners. +/// +public interface IAntimalwareScanner +{ + /// + /// Whether the given file is clean from malicious content based on an antimalware scanner. + /// + /// + /// Implementers should let exception propagate in case the antimalware scanner is not reachable or any other error occurs. + /// The will catch those exceptions and handle them appropriately. + /// + /// Stream content of the file. + /// ile name including extension (e.g. my-file.jpg). + /// true if the antimalware scanner did not find anything, false otherwise. + public bool IsClean(Stream contentStream, string fileName); +} diff --git a/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs b/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs index a7a2048..97644cc 100644 --- a/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs +++ b/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs @@ -1,6 +1,8 @@ using System.Reflection; using ByteGuard.FileValidator.Configuration; using ByteGuard.FileValidator.Exceptions; +using ByteGuard.FileValidator.Scanners; +using NSubstitute; namespace ByteGuard.FileValidator.Tests.Unit; @@ -1059,4 +1061,52 @@ public void IsValidFile_InvalidFileSize_ShouldThrowInvalidFileSizeException(byte // Assert Assert.Throws(act); } + + [Fact(DisplayName = "IsValidFile(string, byte[]) should throw MalwareDetectedException when an antimalware scanner is registered and malware is detected and ThrowExceptionOnInvalidFile is true")] + public void IsValidFile_AntimalwareScannerDetectsMalwareAndThrowExceptionOnInvalidFileIsTrue_ShouldThrowMalwareDetectedException() + { + // Arrange + var mockAntimalwareScanner = Substitute.For(); + mockAntimalwareScanner.IsClean(Arg.Any(), Arg.Any()).Returns(false); // Simulate malware detection + + var config = new FileValidatorConfiguration + { + SupportedFileTypes = [".pdf"], + FileSizeLimit = ByteSize.MegaBytes(25), + ThrowExceptionOnInvalidFile = true, + AntimalwareScanner = mockAntimalwareScanner + }; + var fileValidator = new FileValidator(config); + var fileBytes = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D }; // Valid PDF signature + + // Act + Action act = () => fileValidator.IsValidFile("test.pdf", fileBytes); + + // Assert + Assert.Throws(act); + } + + [Fact(DisplayName = "IsValidFile(string, byte[]) should return false when an antimalware scanner is registered and malware is detected and ThrowExceptionOnInvalidFile is false")] + public void IsValidFile_AntimalwareScannerDetectsMalwareAndThrowExceptionOnInvalidFileIsFalse_ShouldReturnFalse() + { + // Arrange + var mockAntimalwareScanner = Substitute.For(); + mockAntimalwareScanner.IsClean(Arg.Any(), Arg.Any()).Returns(false); // Simulate malware detection + + var config = new FileValidatorConfiguration + { + SupportedFileTypes = [".pdf"], + FileSizeLimit = ByteSize.MegaBytes(25), + ThrowExceptionOnInvalidFile = false, + AntimalwareScanner = mockAntimalwareScanner + }; + var fileValidator = new FileValidator(config); + var fileBytes = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D }; // Valid PDF signature + + // Act + var actual = fileValidator.IsValidFile("test.pdf", fileBytes); + + // Assert + Assert.False(actual); + } } From 5ba28a65e63dd06236f1ec50bd2644ae644d5896 Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 20:34:02 +0100 Subject: [PATCH 3/9] refactor: remove unused friendly file size limit Thing configuration key should've never been in the core package, but is instead specific for the AspNetCore package. Does require the aspnetcore package to be updated and ensure reference to new versuon of the core package. --- .../FileValidatorConfiguration.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs index eba7caf..25390af 100644 --- a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs +++ b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfiguration.cs @@ -25,24 +25,6 @@ public class FileValidatorConfiguration /// public long FileSizeLimit { get; set; } = -1; - /// - /// Maximum file size limit in string representation (e.g. "25MB", "2 GB", etc.). - /// - /// - /// Defines the file size limit of files. See for conversion help. - /// Will be ignored if is defined. - /// Spacing ("25 MB" vs. "25MB") is irrelevant. - /// Supported string representation are: - ///
    - ///
  • B: Bytes
  • - ///
  • KB: Kilobytes
  • - ///
  • MB: Megabytes
  • - ///
  • GB: Gigabytes
  • - ///
- ///
- ///
- public string FriendlyFileSizeLimit { get; set; } = default!; - /// /// Whether to throw an exception if an unsupported/invalid file is encountered. Defaults to true. /// From 2030ba787662fca472a3a3dc2944adaec8fbd366 Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 21:10:05 +0100 Subject: [PATCH 4/9] refactor: remove specific support for net48 This is suported un netstandard2.0, an we're not doing anything special in net48 other that what we already do in netstandard2.0. --- src/ByteGuard.FileValidator/ByteGuard.FileValidator.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ByteGuard.FileValidator/ByteGuard.FileValidator.csproj b/src/ByteGuard.FileValidator/ByteGuard.FileValidator.csproj index 8e23ca1..aedc7cc 100644 --- a/src/ByteGuard.FileValidator/ByteGuard.FileValidator.csproj +++ b/src/ByteGuard.FileValidator/ByteGuard.FileValidator.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net48;net8.0;net9.0;net10.0 + netstandard2.0;net8.0;net9.0;net10.0 ByteGuard Contributors, detilium ByteGuard File Validator is a security-focused .NET library for validating user-supplied files, providing a configurable API to help you enforce safe and consistent file handling across your applications. https://github.com/ByteGuard-HQ/byteguard-file-validator-net From 0910f4fc1c155a2e68d9d18e0175ad5cc679a206 Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 21:13:15 +0100 Subject: [PATCH 5/9] refactor: add malware specific functions to validator --- .../Exceptions/AntimalwareScannerException.cs | 46 ++++++++ src/ByteGuard.FileValidator/FileValidator.cs | 107 ++++++++++++++++-- 2 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 src/ByteGuard.FileValidator/Exceptions/AntimalwareScannerException.cs diff --git a/src/ByteGuard.FileValidator/Exceptions/AntimalwareScannerException.cs b/src/ByteGuard.FileValidator/Exceptions/AntimalwareScannerException.cs new file mode 100644 index 0000000..11cad3e --- /dev/null +++ b/src/ByteGuard.FileValidator/Exceptions/AntimalwareScannerException.cs @@ -0,0 +1,46 @@ +namespace ByteGuard.FileValidator.Exceptions +{ + /// + /// Exception type used specifically when a an antimalware scanner failed to scane the file. + /// + public class AntimalwareScannerException : Exception + { + /// + /// Default expcetion message for this expection type, if no custom message is provided. + /// + private const string defaultMessage = "An error occurred while scanning the file with the antimalware scanner."; + + /// + /// Initializes a new instance of the class indicating that an error occured during the scan. + /// + public AntimalwareScannerException() + : this(defaultMessage) + { + } + + /// + /// Initializes a new instance of the class indicating that an error occured during the scan. + /// + /// Custom exception message. + public AntimalwareScannerException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class indicating that an error occured during the scan. + /// + /// Custom exception message. + /// Inner exception. + public AntimalwareScannerException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class indicating that an error occured during the scan. + /// + /// Inner exception. + public AntimalwareScannerException(Exception innerException) : base(defaultMessage, innerException) + { + } + } +} diff --git a/src/ByteGuard.FileValidator/FileValidator.cs b/src/ByteGuard.FileValidator/FileValidator.cs index 0d0aeb6..828c65e 100644 --- a/src/ByteGuard.FileValidator/FileValidator.cs +++ b/src/ByteGuard.FileValidator/FileValidator.cs @@ -328,20 +328,15 @@ public bool IsValidFile(string fileName, byte[] content) // Validate antimalware scan if configured. if (_configuration.AntimalwareScanner != null) { - using (var memoryStream = new MemoryStream(content)) + var isClean = IsMalwareClean(fileName, content); + if (!isClean) { - memoryStream.Seek(0, SeekOrigin.Begin); - - var isClean = _configuration.AntimalwareScanner.IsClean(memoryStream, fileName); - if (!isClean) + if (_configuration.ThrowExceptionOnInvalidFile) { - if (_configuration.ThrowExceptionOnInvalidFile) - { - throw new MalwareDetectedException(); - } - - return false; + throw new MalwareDetectedException(); } + + return false; } } @@ -982,6 +977,96 @@ public bool IsValidOpenDocumentFormat(string filePath) } } + /// + /// Whether the given file is clean according to the configured antimalware scanner. + /// + /// File name including extension (e.g. my-file.odt). + /// Byte content of the file. + /// true if the no malware was detected in the file from the configured antimalware scanner, false otherwise. + /// Thrown if no antimalware scanner has been configured for the FileValidator. + /// Thrown if malware was detected in the file and is enabled. + /// Thrown if the configured antimalware scanner encountered an error while scanning the file for malware. + public bool IsMalwareClean(string fileName, byte[] content) + { + if (_configuration.AntimalwareScanner is null) + { + throw new InvalidOperationException("No antimalware scanner has been configured for the FileValidator."); + } + + using (var memoryStream = new MemoryStream(content)) + { + return IsMalwareClean(fileName, memoryStream); + } + } + + /// + /// Whether the given file is clean according to the configured antimalware scanner. + /// + /// File name including extension (e.g. my-file.odt). + /// Stream content of the file. + /// true if the no malware was detected in the file from the configured antimalware scanner, false otherwise. + /// Thrown if no antimalware scanner has been configured for the FileValidator. + /// Thrown if malware was detected in the file and is enabled. + /// Thrown if the configured antimalware scanner encountered an error while scanning the file for malware. + public bool IsMalwareClean(string fileName, Stream stream) + { + if (_configuration.AntimalwareScanner is null) + { + throw new InvalidOperationException("No antimalware scanner has been configured for the FileValidator."); + } + + stream.Seek(0, SeekOrigin.Begin); + + try + { + var isClean = _configuration.AntimalwareScanner.IsClean(stream, fileName); + if (!isClean) + { + if (_configuration.ThrowExceptionOnInvalidFile) + { + throw new MalwareDetectedException(); + } + + return false; + } + + return true; + } + catch (Exception ex) + { + throw new AntimalwareScannerException(ex); + } + } + + /// + /// Whether the given file is clean according to the configured antimalware scanner. + /// + /// Full path to the file including filename and extension (e.g. C:\temp\my-file.odt). + /// true if the no malware was detected in the file from the configured antimalware scanner, false otherwise. + /// Thrown if no antimalware scanner has been configured for the FileValidator. + /// Thrown if the is null or whitespace. + /// Thrown if malware was detected in the file and is enabled. + /// Thrown if the configured antimalware scanner encountered an error while scanning the file for malware. + public bool IsMalwareClean(string filePath) + { + if (_configuration.AntimalwareScanner is null) + { + throw new InvalidOperationException("No antimalware scanner has been configured for the FileValidator."); + } + + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentNullException(nameof(filePath), "File path cannot be null or empty."); + } + + using (var fileStream = File.OpenRead(filePath)) + { + var fileName = Path.GetFileName(filePath); + + return IsValidOpenDocumentFormat(fileName, fileStream); + } + } + /// /// Whether the given file type is expected to be an Open XML file. /// From d4da852f07774b76034a6d59acf318af2944311b Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 21:20:12 +0100 Subject: [PATCH 6/9] refactor: remove unused usings --- src/ByteGuard.FileValidator/ByteSize.cs | 3 +-- .../Configuration/ConfigurationValidator.cs | 6 ++---- .../Configuration/FileValidatorConfigurationBuilder.cs | 6 ++---- .../Exceptions/EmptyFileException.cs | 4 +--- .../Exceptions/InvalidFileSizeException.cs | 5 +---- .../Exceptions/InvalidOpenDocumentFormatException.cs | 2 -- .../Exceptions/InvalidOpenXmlFormatException.cs | 4 +--- .../Exceptions/InvalidSignatureException.cs | 4 +--- .../Exceptions/UnsupportedFileException.cs | 4 +--- src/ByteGuard.FileValidator/Models/FileDefinition.cs | 4 +--- .../Validators/OpenDocumentFormatValidator.cs | 2 -- .../Validators/OpenXmlFormatValidator.cs | 3 --- src/ByteGuard.FileValidator/Validators/PdfValidator.cs | 4 +--- 13 files changed, 12 insertions(+), 39 deletions(-) diff --git a/src/ByteGuard.FileValidator/ByteSize.cs b/src/ByteGuard.FileValidator/ByteSize.cs index 14c01cb..1d94935 100644 --- a/src/ByteGuard.FileValidator/ByteSize.cs +++ b/src/ByteGuard.FileValidator/ByteSize.cs @@ -1,5 +1,4 @@ -using System; -using System.Globalization; +using System.Globalization; namespace ByteGuard.FileValidator { diff --git a/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs b/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs index c0e0301..ff59372 100644 --- a/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs +++ b/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using ByteGuard.FileValidator.Exceptions; +using ByteGuard.FileValidator.Exceptions; namespace ByteGuard.FileValidator.Configuration { @@ -49,4 +47,4 @@ public static void ThrowIfInvalid(FileValidatorConfiguration configuration) } } } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs index 0de1375..97f015e 100644 --- a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs +++ b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace ByteGuard.FileValidator.Configuration +namespace ByteGuard.FileValidator.Configuration { /// /// File validator configurations fluent API builder. @@ -61,4 +59,4 @@ public FileValidatorConfiguration Build() return configuration; } } -} \ No newline at end of file +} diff --git a/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs b/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs index 9c0d66b..b76f917 100644 --- a/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ByteGuard.FileValidator.Exceptions +namespace ByteGuard.FileValidator.Exceptions { /// /// Exception type used specifically when a given file does not contain any content. diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs index f704c12..3935b51 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidFileSizeException.cs @@ -1,7 +1,4 @@ -using System; -using System.Runtime.Serialization; - -namespace ByteGuard.FileValidator.Exceptions +namespace ByteGuard.FileValidator.Exceptions { /// /// Exception type used specifically when a given file exceeds the configured file size limit. diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs index 021015a..ae1e557 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenDocumentFormatException.cs @@ -1,5 +1,3 @@ -using System; - namespace ByteGuard.FileValidator.Exceptions { /// diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs index e285d77..bce9463 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidOpenXmlFormatException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ByteGuard.FileValidator.Exceptions +namespace ByteGuard.FileValidator.Exceptions { /// /// Exception type used specifically when a given file, which is expected to be an Open XML file, does not adhere diff --git a/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs b/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs index 8786681..89ea67f 100644 --- a/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/InvalidSignatureException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ByteGuard.FileValidator.Exceptions +namespace ByteGuard.FileValidator.Exceptions { /// /// Exception type used specifically when a given file is invalid based on signature. diff --git a/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs b/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs index b3ad7d2..d5a206a 100644 --- a/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs +++ b/src/ByteGuard.FileValidator/Exceptions/UnsupportedFileException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ByteGuard.FileValidator.Exceptions +namespace ByteGuard.FileValidator.Exceptions { /// /// Exception type used specifically when a given file is unsupported. diff --git a/src/ByteGuard.FileValidator/Models/FileDefinition.cs b/src/ByteGuard.FileValidator/Models/FileDefinition.cs index efebd82..2700c2f 100644 --- a/src/ByteGuard.FileValidator/Models/FileDefinition.cs +++ b/src/ByteGuard.FileValidator/Models/FileDefinition.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace ByteGuard.FileValidator.Models +namespace ByteGuard.FileValidator.Models { /// /// Definition of filetype, their valid signatures, and potentially their valid subtype signatures. diff --git a/src/ByteGuard.FileValidator/Validators/OpenDocumentFormatValidator.cs b/src/ByteGuard.FileValidator/Validators/OpenDocumentFormatValidator.cs index 6cc8131..9bee276 100644 --- a/src/ByteGuard.FileValidator/Validators/OpenDocumentFormatValidator.cs +++ b/src/ByteGuard.FileValidator/Validators/OpenDocumentFormatValidator.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.IO.Compression; namespace ByteGuard.FileValidator.Validators diff --git a/src/ByteGuard.FileValidator/Validators/OpenXmlFormatValidator.cs b/src/ByteGuard.FileValidator/Validators/OpenXmlFormatValidator.cs index d723a92..594e46e 100644 --- a/src/ByteGuard.FileValidator/Validators/OpenXmlFormatValidator.cs +++ b/src/ByteGuard.FileValidator/Validators/OpenXmlFormatValidator.cs @@ -1,9 +1,6 @@ using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Validation; -using System; -using System.IO; -using System.Linq; using ByteGuard.FileValidator.Exceptions; namespace ByteGuard.FileValidator.Validators diff --git a/src/ByteGuard.FileValidator/Validators/PdfValidator.cs b/src/ByteGuard.FileValidator/Validators/PdfValidator.cs index 8a589f4..e154104 100644 --- a/src/ByteGuard.FileValidator/Validators/PdfValidator.cs +++ b/src/ByteGuard.FileValidator/Validators/PdfValidator.cs @@ -1,6 +1,4 @@ -using System; -using System.IO; -using System.Text; +using System.Text; using ByteGuard.FileValidator.Exceptions; namespace ByteGuard.FileValidator.Validators From 39be64c4d5ab1c5a9b9b5d39c987e26f8bcf11b4 Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 21:22:33 +0100 Subject: [PATCH 7/9] feat: enable setting an antimalware scanner using the configuration builder --- .../FileValidatorConfigurationBuilder.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs index 97f015e..2d6060c 100644 --- a/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs +++ b/src/ByteGuard.FileValidator/Configuration/FileValidatorConfigurationBuilder.cs @@ -1,4 +1,6 @@ -namespace ByteGuard.FileValidator.Configuration +using ByteGuard.FileValidator.Scanners; + +namespace ByteGuard.FileValidator.Configuration { /// /// File validator configurations fluent API builder. @@ -8,6 +10,7 @@ public class FileValidatorConfigurationBuilder private readonly List supportedFileTypes = new List(); private bool throwOnInvalidFiles = true; private long fileSizeLimit = ByteSize.MegaBytes(25); + private IAntimalwareScanner? antimalwareScanner = null; /// /// Allow specific file types (extensions) to be validated. @@ -41,6 +44,22 @@ public FileValidatorConfigurationBuilder SetFileSizeLimit(long inFileSizeLimit) return this; } + /// + /// Add an antimalware scanner. + /// + /// Antimalware scanner to use. + /// Thrown when the provided scanner is null. + public FileValidatorConfigurationBuilder AddAntimalwareScanner(IAntimalwareScanner scanner) + { + if (scanner == null) + { + throw new ArgumentNullException(nameof(scanner)); + } + + antimalwareScanner = scanner; + return this; + } + /// /// Build configuration. /// @@ -51,7 +70,8 @@ public FileValidatorConfiguration Build() { SupportedFileTypes = supportedFileTypes, ThrowExceptionOnInvalidFile = throwOnInvalidFiles, - FileSizeLimit = fileSizeLimit + FileSizeLimit = fileSizeLimit, + AntimalwareScanner = antimalwareScanner }; ConfigurationValidator.ThrowIfInvalid(configuration); From d7e17e3d3d75f0d03b325a4033a321921222d12b Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 21:25:02 +0100 Subject: [PATCH 8/9] docs: update readme to include documentation on antimalware scanners --- README.md | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fd3dcb7..5a219e5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ It helps you enforce consistent file upload rules by checking: - File size limits - File signatures (magic numbers) to detect spoofed types - Specification conformance for Office Open XML / Open Document Formats (`.docx`, `.xlsx`, `.pptx`, `.odt`) +- Malware scan result using a varity of scanners (_requires the addition of a specific ByteGuard.FileValidator scanner package_) > ⚠️ **Important:** This library should be part of a **defense-in-depth** strategy. It does not replace antivirus scanning, sandboxing, or other security controls. @@ -17,6 +18,7 @@ It does not replace antivirus scanning, sandboxing, or other security controls. - ✅ Validate files by **size** - ✅ Validate files by **signature (_magic-numbers_)** - ✅ Validate files by **specification conformance** for archive-based formats (_Open XML and Open Document Formats_) +- ✅ **Ensure no malware** through a variety of antimalware scanners - ✅ Validate using file path, `Stream`, or `byte[]` - ✅ Configure which file types to support - ✅ Configure whether to **throw exceptions** or simply return a boolean @@ -25,13 +27,15 @@ It does not replace antivirus scanning, sandboxing, or other security controls. ## Getting Started ### Installation -This package is published and installed via NuGet. +This package is published and installed via [NuGet](https://www.nuget.org/packages/ByteGuard.FileValidator). Reference the package in your project: ```bash dotnet add package ByteGuard.FileValidator ``` +### Antimalware scanners +In order to use the antimalware scanning capabilities, ensure you have a ByteGuard.FileValidator antimalware package referenced as well. Youo can find the relevant scanner package on NuGet under the namespace `ByteGuard.FileValidator.Scanners`. ## Usage ### Basic validation @@ -39,9 +43,10 @@ dotnet add package ByteGuard.FileValidator ```csharp var configuration = new FileValidatorConfiguration { - SupportedFileTypes = [FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png], - FileSizeLimit = ByteSize.MegaBytes(25), - ThrowExceptionOnInvalidFile = false + SupportedFileTypes = [FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png], + FileSizeLimit = ByteSize.MegaBytes(25), + ThrowExceptionOnInvalidFile = false, + AntimalwareScanner = new SpecificAntimalwareScanner(scannerOptions) }; var fileValidator = new FileValidator(configuration); @@ -52,10 +57,11 @@ var isValid = fileValidator.IsValidFile("example.pdf", fileStream); ```csharp var configuration = new FileValidatorConfigurationBuilder() - .AllowFileTypes(FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png) - .SetFileSizeLimit(ByteSize.MegaBytes(25)) - .SetThrowExceptionOnInvalidFile(false) - .Build(); + .AllowFileTypes(FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png) + .SetFileSizeLimit(ByteSize.MegaBytes(25)) + .SetThrowExceptionOnInvalidFile(false) + .AddAntimalwareScanner(new SpecificAntimalwareScanner(scannerOptions)) + .Build(); var fileValidator = new FileValidator(configuration); var isValid = fileValidator.IsValidFile("example.pdf", fileStream); @@ -72,6 +78,7 @@ The `FileValidator` class provides methods to validate specific aspects of a fil > 2. File size validation > 3. Signature (magic-number) validation > 4. Optional Open XML / Open Document Format specification conformance validation (for supported types) +> 5. Optional antimalware scanning with a compatible scanning package ```csharp bool isExtensionValid = fileValidator.IsValidFileType(fileName); @@ -79,6 +86,7 @@ bool isFileSizeValid = fileValidator.HasValidSize(fileStream); bool isSignatureValid = fileValidator.HasValidSignature(fileName, fileStream); bool isOpenXmlValid = fileValidator.IsValidOpenXmlDocument(fileName, fileStream); bool isOpenDocumentFormatValid = fileValidator.IsValidOpenDocumentFormat(fileName, fileStream); +bool isMalwareClean = fileValidator.IsMalwareClean(fileName, fileStream); ``` ### Example @@ -92,7 +100,8 @@ public async Task Upload(IFormFile file) { SupportedFileTypes = [FileExtensions.Pdf, FileExtensions.Docx], FileSizeLimit = ByteSize.MegaBytes(10), - ThrowExceptionOnInvalidFile = false + ThrowExceptionOnInvalidFile = false, + AntimalwareScanner = new SpecificAntimalwareScanner(scannerOptions) }; var validator = new FileValidator(configuration); @@ -132,9 +141,10 @@ The following file extensions are supported by the `FileValidator`: `IsValidFile` always validates: -- File extension (against `SupportedFileTypes`) -- File size (against `FileSizeLimit`) -- File signature (magic number) +- File extension (_against `SupportedFileTypes`_) +- File size (_against `FileSizeLimit`_) +- File signature (_magic number_) +- Malware scan result (_if an antimalware scanner has been configured_) For some formats, additional checks are performed: @@ -143,11 +153,13 @@ For some formats, additional checks are performed: - File size - Signature - Specification conformance + - Malware scan result - **Other binary formats** (e.g. images, audio, video such as `.jpg`, `.png`, `.mp3`, `.mp4`): - Extension - File size - Signature + - Malware scan result ## Configuration Options @@ -158,6 +170,7 @@ The `FileValidatorConfiguration` supports: | `SupportedFileTypes` | Yes | N/A | A list of allowed file extensions (e.g., `.pdf`, `.jpg`).
Use the predefined constants in `FileExtensions` for supported types. | | `FileSizeLimit` | Yes | N/A | Maximum permitted size of files.
Use the static `ByteSize` class provided with this package, to simplify your limit. | | `ThrowExceptionOnInvalidFile` | No | `true` | Whether to throw an exception on invalid files or return `false`. | +| `AntimalwareScanner` | No | N/A | An antimalware scanner used to scan the given file for potential malware. | ### Exceptions @@ -171,6 +184,7 @@ When `ThrowExceptionOnInvalidFile` is set to `true`, validation functions will t | `InvalidSignatureException` | Thrown when the file's signature does not match the expected signature for its type. | | `InvalidOpenXmlFormatException` | Thrown when the internal structure of an Open XML file is invalid (`.docx`, `.xlsx`, `.pptx`, etc.). | | `InvalidOpenDocumentFormatException` | Thrown when the specification conformance of an Open Document Format file is invalid (`.odt`, etc.). | +| `MalwareDetectedException` | Thrown when the configured antimalware scanner detected malware in the file from a scan result. | ## When to use this package From f13207439316c45b84fd42a4c61b55c4707e0005 Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Thu, 27 Nov 2025 21:34:26 +0100 Subject: [PATCH 9/9] fix: incorrect exception handling implementation --- src/ByteGuard.FileValidator/FileValidator.cs | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/ByteGuard.FileValidator/FileValidator.cs b/src/ByteGuard.FileValidator/FileValidator.cs index 828c65e..2fe58e2 100644 --- a/src/ByteGuard.FileValidator/FileValidator.cs +++ b/src/ByteGuard.FileValidator/FileValidator.cs @@ -1017,25 +1017,27 @@ public bool IsMalwareClean(string fileName, Stream stream) stream.Seek(0, SeekOrigin.Begin); + bool isClean; try { - var isClean = _configuration.AntimalwareScanner.IsClean(stream, fileName); - if (!isClean) - { - if (_configuration.ThrowExceptionOnInvalidFile) - { - throw new MalwareDetectedException(); - } - - return false; - } - - return true; + isClean = _configuration.AntimalwareScanner.IsClean(stream, fileName); } catch (Exception ex) { throw new AntimalwareScannerException(ex); } + + if (!isClean) + { + if (_configuration.ThrowExceptionOnInvalidFile) + { + throw new MalwareDetectedException(); + } + + return false; + } + + return true; } ///