Skip to content

Commit 8e82a7a

Browse files
authored
Merge pull request #1 from ByteGuard-HQ/dev
Release v1.0.0
2 parents cf63292 + 5c950c6 commit 8e82a7a

16 files changed

Lines changed: 608 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ env:
1414
jobs:
1515
build:
1616

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

2020
permissions:

ByteGuard.FileValidator.Scanners.Amsi.slnx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
<File Path="Directory.Version.props" />
66
<File Path="LICENSE" />
77
<File Path="README.md" />
8+
<File Path=".github/workflows/ci.yml" />
89
</Folder>
9-
<Folder Name="/tests/" />
10-
<Project Path="src/ByteGuard.FileValidator.Scanners.Amsi/ByteGuard.FileValidator.Scanners.Amsi.csproj" />
10+
<Folder Name="/tests/">
11+
<Project Path="tests/ByteGuard.FileValidator.Scanner.Amsi.Tests.Unit/ByteGuard.FileValidator.Scanner.Amsi.Tests.Unit.csproj" />
12+
</Folder>
13+
<Project Path="src/ByteGuard.FileValidator.Scanner.Amsi/ByteGuard.FileValidator.Scanner.Amsi.csproj" />
1114
</Solution>

Directory.Version.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<VersionPrefix>0.1.0</VersionPrefix>
3+
<VersionPrefix>1.0.0</VersionPrefix>
44
</PropertyGroup>
55
</Project>

README.md

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,111 @@
11

2-
# ByteGuard.FileValidator.Scanners.Amsi ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.FileValidator.Scanners.Amsi)
2+
# ByteGuard.FileValidator.Scanners.Amsi ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.FileValidator.Scanners.Amsi)
3+
4+
`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).
5+
6+
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.
7+
8+
> ⚠️ **Important:** This package is one layer in a defense-in-depth strategy.
9+
> It does **not** replace endpoint protection, sandboxing, input validation, or other security controls.
10+
11+
> ⚠️ **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.
12+
13+
## Features
14+
15+
- **AMSI-based** implementation of `IAntimalwareScanner` for `ByteGuard.FileValidator`
16+
- Works with **any AMSI-compatible antivirus** installed on the host machine
17+
18+
## Prerequisites
19+
20+
- **Operating system**
21+
- Windows 10+ or Windows Server 2016+ (_AMSI is available and supported on these versions_).
22+
- **Antimalware**
23+
- An AMSI-integrated antimalware engine installed and enabled (_e.g. Microsoft Defender Antivirus_).
24+
- **Core packages**
25+
- [`ByteGuard.FileValidator`](https://www.nuget.org/packages/ByteGuard.FileValidator)
26+
27+
## Getting Started
28+
29+
### Installation
30+
This package is published and installed via [NuGet](https://www.nuget.org/packages/ByteGuard.FileValidator.Scanners.Amsi).
31+
32+
Reference the package in your project:
33+
```bash
34+
dotnet add package ByteGuard.FileValidator.Scanners.Amsi
35+
```
36+
37+
## Usage
38+
39+
```csharp
40+
using ByteGuard.FileValidator;
41+
using ByteGuard.FileValidator.Scanners.Amsi;
42+
43+
var amsiConfig = new AmsiScannerConfiguration()
44+
{
45+
ApplicationName = "MyApplication"
46+
};
47+
48+
var amsiScanner = new AmsiAntimalwareScanner(amsiConfig);
49+
50+
var configuration = //...;
51+
var fileValidator = new FileValidator(configuration, amsiScanner);
52+
53+
var isValid = fileValidator.IsValidFile(fileStream, fileName);
54+
```
55+
56+
The `FileValidator` will automatically scan the file once provided as argument, and whenever using either `IsValidFile` or `IsMalwareClean` functions.
57+
58+
## Configuration
59+
`AmsiScannerConfiguration` supports the following settings:
60+
61+
| Settings | Required | Description |
62+
| -- | -- | -- |
63+
| `ApplicationName` | Yes | The logical name used when registering the AMSI session (_helps AV engines with context_). |
64+
65+
### Example
66+
```csharp
67+
[HttpPost("upload")]
68+
public async Task<IActionResult> Upload(IFormFile file)
69+
{
70+
using var stream = file.OpenReadStream();
71+
72+
var amsiConfig = new AmsiScannerConfiguration()
73+
{
74+
ApplicationName = "MyApplication"
75+
};
76+
77+
var amsiScanner = new AmsiAntimalwareScanner(amsiConfig);
78+
79+
var configuration = //...
80+
var validator = new FileValidator(configuration, amsiScanner);
81+
82+
if (!validator.IsValidFile(file.FileName, stream))
83+
{
84+
return BadRequest("Invalid or unsupported file.");
85+
}
86+
87+
// Proceed with processing/saving...
88+
89+
return Ok();
90+
}
91+
```
92+
93+
### Testing the AMSI integration
94+
95+
If you verify the integration using known test signatures (for example, the EICAR test file), be aware that:
96+
97+
- The installed AV engine may quarantine or block the file.
98+
- Alerts may be raised and forwarded to your SIEM / security team.
99+
- In tightly monitored environments, you should coordinate with your security team before running such tests.
100+
101+
102+
## Security notes & limitations
103+
104+
- AMSI relies on the underlying antimalware provider for detection. If the provider is disabled, misconfigured, or missing signatures, detection quality will be affected.
105+
- Attackers constantly attempt to evade or disable AMSI; **treat AMSI as a signal, not as a guarantee**.
106+
- **Always** combine this package with:
107+
- Principle of least privilege for storage and processing
108+
- Endpoint protection and monitoring
109+
110+
## License
111+
_ByteGuard.FileValidator.Scanners.Amsi is Copyright © ByteGuard Contributors - Provided under the MIT license._

assets/icon.png

6.92 KB
Loading
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.ComponentModel;
2+
#if NET5_0_OR_GREATER
3+
using System.Runtime.Versioning;
4+
#endif
5+
using ByteGuard.FileValidator.Scanner.Amsi.Integration;
6+
using ByteGuard.FileValidator.Scanners;
7+
8+
namespace ByteGuard.FileValidator.Scanner.Amsi;
9+
10+
/// <summary>
11+
/// Microsoft Antimalware Scan Interface (AMSI) antimalware scanner implementation.
12+
/// </summary>
13+
#if NET5_0_OR_GREATER
14+
[SupportedOSPlatform("windows")]
15+
#endif
16+
public class AmsiAntimalwareScanner : IAntimalwareScanner<AmsiScannerConfiguration>
17+
{
18+
private readonly AmsiScannerConfiguration _configuration;
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="AmsiAntimalwareScanner"/> class.
22+
/// </summary>
23+
/// <param name="configuration">Scanner configuration.</param>
24+
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="configuration"/> is <c>null</c>.</exception>
25+
/// <exception cref="ArgumentException">Thrown if the <paramref name="configuration.ApplicationName"/> is <c>null></c> or whitespace.</exception>
26+
public AmsiAntimalwareScanner(AmsiScannerConfiguration configuration)
27+
{
28+
if (configuration == null)
29+
{
30+
throw new ArgumentNullException(nameof(configuration));
31+
}
32+
33+
if (string.IsNullOrWhiteSpace(configuration.ApplicationName))
34+
{
35+
throw new ArgumentException("ApplicationName must be provided in the configuration.", nameof(configuration));
36+
}
37+
38+
_configuration = configuration;
39+
}
40+
41+
/// <summary>
42+
/// Scans the provided content stream for malware using Windows Antimalware Scan Interface (AMSI).
43+
/// </summary>
44+
/// <param name="contentStream">Content stream.</param>
45+
/// <param name="fileName">Filename.</param>
46+
/// <returns><c>true</c> if no malware was detected in the file, <c>false</c> otherwise.</returns>
47+
/// <exception cref="Win32Exception">Thrown if the AMSI API call fails.</exception>
48+
public bool IsClean(Stream contentStream, string fileName)
49+
{
50+
var content = ReadStreamToByteArray(contentStream);
51+
52+
using var context = AmsiContext.Create(_configuration.ApplicationName);
53+
using var session = context.CreateSession();
54+
55+
var isMalware = session.IsMalware(content, fileName);
56+
return !isMalware;
57+
}
58+
59+
private byte[] ReadStreamToByteArray(Stream stream)
60+
{
61+
using var ms = new MemoryStream();
62+
stream.CopyTo(ms);
63+
64+
return ms.ToArray();
65+
}
66+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netstandard2.0;net481;net8.0;net9.0;net10.0</TargetFrameworks>
5+
<Authors>ByteGuard Contributors</Authors>
6+
<Description>ByteGuard File Validator Microsoft Antimalware Scan Interface (AMSI) antimalware scanner.</Description>
7+
<PackageProjectUrl>https://github.com/ByteGuard-HQ/byteguard-file-validator-scanner-amsi</PackageProjectUrl>
8+
<RepositoryUrl>https://github.com/ByteGuard-HQ/byteguard-file-validator-scanner-amsi</RepositoryUrl>
9+
<RepositoryType>git</RepositoryType>
10+
<PackageTags>byteguard file-validator scanner amsi</PackageTags>
11+
<PackageReadmeFile>README.md</PackageReadmeFile>
12+
<PackageIcon>icon.png</PackageIcon>
13+
<Copyright>Copyright © ByteGuard Contributors</Copyright>
14+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
15+
</PropertyGroup>
16+
17+
<ItemGroup>
18+
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
19+
<None Include="..\..\assets\icon.png" Pack="true" PackagePath="\" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<PackageReference Include="ByteGuard.FileValidator" Version="1.1.0" />
24+
</ItemGroup>
25+
26+
27+
28+
</Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace ByteGuard.FileValidator.Scanner.Amsi;
2+
3+
/// <summary>
4+
/// Configuration for the Microsoft Antimalware Scan Interface (AMSI) scanner.
5+
/// </summary>
6+
public class AmsiScannerConfiguration
7+
{
8+
/// <summary>
9+
/// Name of the application integrating with Microsoft Antimalware Scan Interface (AMSI).
10+
/// </summary>
11+
/// <remarks>
12+
/// The name, version, or GUID string of the app calling the AMSI API.
13+
/// </remarks>
14+
public string ApplicationName { get; set; } = default!;
15+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System.Runtime.InteropServices;
2+
#if NET5_0_OR_GREATER
3+
using System.Runtime.Versioning;
4+
#endif
5+
6+
namespace ByteGuard.FileValidator.Scanner.Amsi.Integration;
7+
8+
/// <summary>
9+
/// Dynamically loaded Antimalware Scan Interface from System32. Will only work on Windows clients and servers.
10+
/// </summary>
11+
#if NET5_0_OR_GREATER
12+
[SupportedOSPlatform("windows")]
13+
#endif
14+
internal static class Amsi
15+
{
16+
/// <summary>
17+
/// Initialize the AMSI API.
18+
/// </summary>
19+
/// <param name="appName">The name, version, or GUID string of the app calling the AMSI API.</param>
20+
/// <param name="amsiContext">Out generated AMSI context that must be passed to all subsequent calls to the AMSI API.</param>
21+
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
22+
[DllImport("Amsi.dll", EntryPoint = "AmsiInitialize", CallingConvention = CallingConvention.StdCall)]
23+
internal static extern int AmsiInitialize([MarshalAs(UnmanagedType.LPWStr)] string appName, out AmsiContextSafeHandle amsiContext);
24+
25+
/// <summary>
26+
/// Remove the instance of the AMSI API that was originally opened by <see cref="AmsiInitialize"/>.
27+
/// </summary>
28+
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
29+
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
30+
[DllImport("Amsi.dll", EntryPoint = "AmsiUninitialize", CallingConvention = CallingConvention.StdCall)]
31+
internal static extern void AmsiUninitialize(IntPtr amsiContext);
32+
33+
/// <summary>
34+
/// Opens a session within which multiple scan requests can be correlated.
35+
/// </summary>
36+
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
37+
/// <param name="session">Out generated AMSI session that must be passed to all subsequent calls to the AMSI API within the session.</param>
38+
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
39+
[DllImport("Amsi.dll", EntryPoint = "AmsiOpenSession", CallingConvention = CallingConvention.StdCall)]
40+
internal static extern int AmsiOpenSession(AmsiContextSafeHandle amsiContext, out AmsiSessionSafeHandle session);
41+
42+
/// <summary>
43+
/// Close a session that was opened by <see cref="AmsiOpenSession"/>.
44+
/// </summary>
45+
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
46+
/// <param name="session">The session handle that was initially received from <see cref="AmsiOpenSession"/>.</param>
47+
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
48+
[DllImport("Amsi.dll", EntryPoint = "AmsiCloseSession", CallingConvention = CallingConvention.StdCall)]
49+
internal static extern void AmsiCloseSession(AmsiContextSafeHandle amsiContext, IntPtr session);
50+
51+
/// <summary>
52+
/// Scans a string for malware.
53+
/// </summary>
54+
/// <remarks>
55+
/// It's recommended to use <see cref="AmsiResultIsMalware"/> to interpret the returning result.
56+
/// </remarks>
57+
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
58+
/// <param name="payload">The string to be scanned.</param>
59+
/// <param name="contentName">The filename, URL, unique script ID, or similar of the content being scanned.</param>
60+
/// <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>
61+
/// <param name="result">Out result of the scan (see <see cref="AmsiResult"/>).</param>
62+
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
63+
[DllImport("Amsi.dll", EntryPoint = "AmsiScanString", CallingConvention = CallingConvention.StdCall)]
64+
internal static extern int AmsiScanString(AmsiContextSafeHandle amsiContext, [In, MarshalAs(UnmanagedType.LPWStr)] string payload, [In, MarshalAs(UnmanagedType.LPWStr)] string contentName, AmsiSessionSafeHandle session, out AmsiResult result);
65+
66+
/// <summary>
67+
/// Scans a buffer-full of content for malware.
68+
/// </summary>
69+
/// <remarks>
70+
/// It's recommended to use <see cref="AmsiResultIsMalware"/> to interpret the returning result.
71+
/// </remarks>
72+
/// <param name="amsiContext">The context handle that was initially received from <see cref="AmsiInitialize"/>.</param>
73+
/// <param name="buffer">The buffer from which to read the data to be scanned.</param>
74+
/// <param name="length">The length, in bytes, of the data to be read from buffer.</param>
75+
/// <param name="contentName">The filename, URL, unique script ID, or similar of the content being scanned.</param>
76+
/// <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>
77+
/// <param name="result">Out result of the scan (see <see cref="AmsiResult"/>).</param>
78+
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
79+
[DllImport("Amsi.dll", EntryPoint = "AmsiScanBuffer", CallingConvention = CallingConvention.StdCall)]
80+
internal static extern int AmsiScanBuffer(AmsiContextSafeHandle amsiContext, byte[] buffer, uint length, [In, MarshalAs(UnmanagedType.LPWStr)] string contentName, AmsiSessionSafeHandle session, out AmsiResult result);
81+
82+
/// <summary>
83+
/// Determines if the result of a scan indicates that the content should be blocked.
84+
/// </summary>
85+
/// <param name="result">Result of a scan from either <see cref="AmsiScanString"/> or <see cref="AmsiScanBuffer"/>.</param>
86+
/// <returns><c>true</c> if the result is considered a malware detection, <c>false</c> otherwise.</returns>
87+
internal static bool AmsiResultIsMalware(AmsiResult result) => result >= AmsiResult.AMSI_RESULT_DETECTED;
88+
}

0 commit comments

Comments
 (0)