Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ env:
jobs:
build:

# We need to run on Windows as we're supporting .NET Framework
# We need to run on Windows as we're supporting .NET Framework and using MIcrosoft AMSI
runs-on: windows-latest

permissions:
Expand Down
7 changes: 5 additions & 2 deletions ByteGuard.FileValidator.Scanners.Amsi.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
<File Path="Directory.Version.props" />
<File Path="LICENSE" />
<File Path="README.md" />
<File Path=".github/workflows/ci.yml" />
</Folder>
<Folder Name="/tests/" />
<Project Path="src/ByteGuard.FileValidator.Scanners.Amsi/ByteGuard.FileValidator.Scanners.Amsi.csproj" />
<Folder Name="/tests/">
<Project Path="tests/ByteGuard.FileValidator.Scanner.Amsi.Tests.Unit/ByteGuard.FileValidator.Scanner.Amsi.Tests.Unit.csproj" />
</Folder>
<Project Path="src/ByteGuard.FileValidator.Scanner.Amsi/ByteGuard.FileValidator.Scanner.Amsi.csproj" />
</Solution>
2 changes: 1 addition & 1 deletion Directory.Version.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<VersionPrefix>0.1.0</VersionPrefix>
<VersionPrefix>1.0.0</VersionPrefix>
</PropertyGroup>
</Project>
111 changes: 110 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,111 @@

# ByteGuard.FileValidator.Scanners.Amsi ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.FileValidator.Scanners.Amsi)
# ByteGuard.FileValidator.Scanners.Amsi ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.FileValidator.Scanners.Amsi)

`ByteGuard.FileValidator.Scanners.Amsi` is a Microsoft Antimalware Scan Interface (AMSI) specific antimalware scanner implementation for [`ByteGuard.FileValidator`](https://www.nuget.org/packages/ByteGuard.FileValidator).

AMSI is a Windows API that allows applications to submit content for scanning and receive a verdict from the installed antimalware provider. It lets you route files through the OS antimalware engine (_for example Microsoft Defender or any other AMSI-integrated AV_) before they’re accepted by your application.

> ⚠️ **Important:** This package is one layer in a defense-in-depth strategy.
> It does **not** replace endpoint protection, sandboxing, input validation, or other security controls.

> ⚠️ **Important:** This package uses the Microsoft Antimalware Scan Interface (AMSI) and will submit content to the installed antimalware engine on the host (_e.g., Microsoft Defender_). Malicious samples or test files (_such as the EICAR test file_) may trigger alerts and incidents in your security monitoring. Make sure your security/operations team is aware of this integration before running tests in shared or production environments.

## Features

- **AMSI-based** implementation of `IAntimalwareScanner` for `ByteGuard.FileValidator`
- Works with **any AMSI-compatible antivirus** installed on the host machine

## Prerequisites

- **Operating system**
- Windows 10+ or Windows Server 2016+ (_AMSI is available and supported on these versions_).
- **Antimalware**
- An AMSI-integrated antimalware engine installed and enabled (_e.g. Microsoft Defender Antivirus_).
- **Core packages**
- [`ByteGuard.FileValidator`](https://www.nuget.org/packages/ByteGuard.FileValidator)

## Getting Started

### Installation
This package is published and installed via [NuGet](https://www.nuget.org/packages/ByteGuard.FileValidator.Scanners.Amsi).

Reference the package in your project:
```bash
dotnet add package ByteGuard.FileValidator.Scanners.Amsi
```

## Usage

```csharp
using ByteGuard.FileValidator;
using ByteGuard.FileValidator.Scanners.Amsi;

var amsiConfig = new AmsiScannerConfiguration()
{
ApplicationName = "MyApplication"
};

var amsiScanner = new AmsiAntimalwareScanner(amsiConfig);

var configuration = //...;
var fileValidator = new FileValidator(configuration, amsiScanner);

var isValid = fileValidator.IsValidFile(fileStream, fileName);
```

The `FileValidator` will automatically scan the file once provided as argument, and whenever using either `IsValidFile` or `IsMalwareClean` functions.

## Configuration
`AmsiScannerConfiguration` supports the following settings:

| Settings | Required | Description |
| -- | -- | -- |
| `ApplicationName` | Yes | The logical name used when registering the AMSI session (_helps AV engines with context_). |

### Example
```csharp
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
using var stream = file.OpenReadStream();

var amsiConfig = new AmsiScannerConfiguration()
{
ApplicationName = "MyApplication"
};

var amsiScanner = new AmsiAntimalwareScanner(amsiConfig);

var configuration = //...
var validator = new FileValidator(configuration, amsiScanner);

if (!validator.IsValidFile(file.FileName, stream))
{
return BadRequest("Invalid or unsupported file.");
}

// Proceed with processing/saving...

return Ok();
}
```

### Testing the AMSI integration

If you verify the integration using known test signatures (for example, the EICAR test file), be aware that:

- The installed AV engine may quarantine or block the file.
- Alerts may be raised and forwarded to your SIEM / security team.
- In tightly monitored environments, you should coordinate with your security team before running such tests.


## Security notes & limitations

- AMSI relies on the underlying antimalware provider for detection. If the provider is disabled, misconfigured, or missing signatures, detection quality will be affected.
- Attackers constantly attempt to evade or disable AMSI; **treat AMSI as a signal, not as a guarantee**.
- **Always** combine this package with:
- Principle of least privilege for storage and processing
- Endpoint protection and monitoring

## License
_ByteGuard.FileValidator.Scanners.Amsi is Copyright © ByteGuard Contributors - Provided under the MIT license._
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions src/ByteGuard.FileValidator.Scanner.Amsi/AmsiAntimalwareScanner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.ComponentModel;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using ByteGuard.FileValidator.Scanner.Amsi.Integration;
using ByteGuard.FileValidator.Scanners;

namespace ByteGuard.FileValidator.Scanner.Amsi;

/// <summary>
/// Microsoft Antimalware Scan Interface (AMSI) antimalware scanner implementation.
/// </summary>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public class AmsiAntimalwareScanner : IAntimalwareScanner<AmsiScannerConfiguration>
{
private readonly AmsiScannerConfiguration _configuration;

/// <summary>
/// Initializes a new instance of the <see cref="AmsiAntimalwareScanner"/> class.
/// </summary>
/// <param name="configuration">Scanner configuration.</param>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="configuration"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown if the <paramref name="configuration.ApplicationName"/> is <c>null></c> or whitespace.</exception>
public AmsiAntimalwareScanner(AmsiScannerConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}

if (string.IsNullOrWhiteSpace(configuration.ApplicationName))
{
throw new ArgumentException("ApplicationName must be provided in the configuration.", nameof(configuration));
}

_configuration = configuration;
}

/// <summary>
/// Scans the provided content stream for malware using Windows Antimalware Scan Interface (AMSI).
/// </summary>
/// <param name="contentStream">Content stream.</param>
/// <param name="fileName">Filename.</param>
/// <returns><c>true</c> if no malware was detected in the file, <c>false</c> otherwise.</returns>
/// <exception cref="Win32Exception">Thrown if the AMSI API call fails.</exception>
public bool IsClean(Stream contentStream, string fileName)
{
var content = ReadStreamToByteArray(contentStream);

using var context = AmsiContext.Create(_configuration.ApplicationName);
using var session = context.CreateSession();

var isMalware = session.IsMalware(content, fileName);
return !isMalware;
}

private byte[] ReadStreamToByteArray(Stream stream)
{
using var ms = new MemoryStream();
stream.CopyTo(ms);

return ms.ToArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net481;net8.0;net9.0;net10.0</TargetFrameworks>
<Authors>ByteGuard Contributors</Authors>
<Description>ByteGuard File Validator Microsoft Antimalware Scan Interface (AMSI) antimalware scanner.</Description>
<PackageProjectUrl>https://github.com/ByteGuard-HQ/byteguard-file-validator-scanner-amsi</PackageProjectUrl>
<RepositoryUrl>https://github.com/ByteGuard-HQ/byteguard-file-validator-scanner-amsi</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>byteguard file-validator scanner amsi</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<Copyright>Copyright © ByteGuard Contributors</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\assets\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="ByteGuard.FileValidator" Version="1.1.0" />
</ItemGroup>



</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace ByteGuard.FileValidator.Scanner.Amsi;

/// <summary>
/// Configuration for the Microsoft Antimalware Scan Interface (AMSI) scanner.
/// </summary>
public class AmsiScannerConfiguration
{
/// <summary>
/// Name of the application integrating with Microsoft Antimalware Scan Interface (AMSI).
/// </summary>
/// <remarks>
/// The name, version, or GUID string of the app calling the AMSI API.
/// </remarks>
public string ApplicationName { get; set; } = default!;
}
88 changes: 88 additions & 0 deletions src/ByteGuard.FileValidator.Scanner.Amsi/Integration/Amsi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Runtime.InteropServices;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif

namespace ByteGuard.FileValidator.Scanner.Amsi.Integration;

/// <summary>
/// Dynamically loaded Antimalware Scan Interface from System32. Will only work on Windows clients and servers.
/// </summary>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
internal static class Amsi
{
/// <summary>
/// Initialize the AMSI API.
/// </summary>
/// <param name="appName">The name, version, or GUID string of the app calling the AMSI API.</param>
/// <param name="amsiContext">Out generated AMSI context that must be passed to all subsequent calls to the AMSI API.</param>
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("Amsi.dll", EntryPoint = "AmsiInitialize", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiInitialize([MarshalAs(UnmanagedType.LPWStr)] string appName, out AmsiContextSafeHandle amsiContext);

/// <summary>
/// Remove the instance of the AMSI API that was originally opened by <see cref="AmsiInitialize"/>.
/// </summary>
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("Amsi.dll", EntryPoint = "AmsiUninitialize", CallingConvention = CallingConvention.StdCall)]
internal static extern void AmsiUninitialize(IntPtr amsiContext);

/// <summary>
/// Opens a session within which multiple scan requests can be correlated.
/// </summary>
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
/// <param name="session">Out generated AMSI session that must be passed to all subsequent calls to the AMSI API within the session.</param>
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("Amsi.dll", EntryPoint = "AmsiOpenSession", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiOpenSession(AmsiContextSafeHandle amsiContext, out AmsiSessionSafeHandle session);

/// <summary>
/// Close a session that was opened by <see cref="AmsiOpenSession"/>.
/// </summary>
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
/// <param name="session">The session handle that was initially received from <see cref="AmsiOpenSession"/>.</param>
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("Amsi.dll", EntryPoint = "AmsiCloseSession", CallingConvention = CallingConvention.StdCall)]
internal static extern void AmsiCloseSession(AmsiContextSafeHandle amsiContext, IntPtr session);

/// <summary>
/// Scans a string for malware.
/// </summary>
/// <remarks>
/// It's recommended to use <see cref="AmsiResultIsMalware"/> to interpret the returning result.
/// </remarks>
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
/// <param name="payload">The string to be scanned.</param>
/// <param name="contentName">The filename, URL, unique script ID, or similar of the content being scanned.</param>
/// <param name="session">If multiple scan requests are to be correlated within a session, set session to the session handle that was initially received from <see cref="AmsiOpenSession"/>. Otherwise, set session to <c>null</c>.</param>
/// <param name="result">Out result of the scan (see <see cref="AmsiResult"/>).</param>
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("Amsi.dll", EntryPoint = "AmsiScanString", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiScanString(AmsiContextSafeHandle amsiContext, [In, MarshalAs(UnmanagedType.LPWStr)] string payload, [In, MarshalAs(UnmanagedType.LPWStr)] string contentName, AmsiSessionSafeHandle session, out AmsiResult result);

/// <summary>
/// Scans a buffer-full of content for malware.
/// </summary>
/// <remarks>
/// It's recommended to use <see cref="AmsiResultIsMalware"/> to interpret the returning result.
/// </remarks>
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
/// <param name="buffer">The buffer from which to read the data to be scanned.</param>
/// <param name="length">The length, in bytes, of the data to be read from buffer.</param>
/// <param name="contentName">The filename, URL, unique script ID, or similar of the content being scanned.</param>
/// <param name="session">If multiple scan requests are to be correlated within a session, set session to the session handle that was initially received from <see cref="AmsiOpenSession"/>. Otherwise, set session to <c>null</c>.</param>
/// <param name="result">Out result of the scan (see <see cref="AmsiResult"/>).</param>
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("Amsi.dll", EntryPoint = "AmsiScanBuffer", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiScanBuffer(AmsiContextSafeHandle amsiContext, byte[] buffer, uint length, [In, MarshalAs(UnmanagedType.LPWStr)] string contentName, AmsiSessionSafeHandle session, out AmsiResult result);

/// <summary>
/// Determines if the result of a scan indicates that the content should be blocked.
/// </summary>
/// <param name="result">Result of a scan from either <see cref="AmsiScanString"/> or <see cref="AmsiScanBuffer"/>.</param>
/// <returns><c>true</c> if the result is considered a malware detection, <c>false</c> otherwise.</returns>
internal static bool AmsiResultIsMalware(AmsiResult result) => result >= AmsiResult.AMSI_RESULT_DETECTED;
}
Loading