diff --git a/Lib/Utils/AssemblyInfo.cs b/Lib/Utils/AssemblyInfo.cs
index 763048f7..cbdde62b 100644
--- a/Lib/Utils/AssemblyInfo.cs
+++ b/Lib/Utils/AssemblyInfo.cs
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License.
using System.Resources;
+using System.Runtime.CompilerServices;
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("AsaTests")]
namespace Microsoft.CST.AttackSurfaceAnalyzer.Utils
{
diff --git a/Lib/Utils/MonoPosixAssemblyResolver.cs b/Lib/Utils/MonoPosixAssemblyResolver.cs
new file mode 100644
index 00000000..8016b713
--- /dev/null
+++ b/Lib/Utils/MonoPosixAssemblyResolver.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License.
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.Loader;
+
+namespace Microsoft.CST.AttackSurfaceAnalyzer.Utils
+{
+ ///
+ /// Fallback resolver for the Mono.Posix.NETStandard assembly.
+ ///
+ /// Mono.Posix.NETStandard 1.0.0 only ships its managed assembly under
+ /// runtimes/{rid}/lib/netstandard2.0/ and does not provide a win-arm64 runtime asset
+ /// (only win-x64, win-x86, linux-* and osx). Because the default host resolves that
+ /// assembly per-RID, on Windows ARM64 it cannot be found and a FileNotFoundException is
+ /// thrown whenever code that references Mono.Unix is JIT compiled (for example during
+ /// file system collection or monitoring).
+ ///
+ /// The managed assembly is platform independent IL, so we resolve it from one of the
+ /// runtime folders that actually ships with the application. This only runs when the
+ /// default resolution fails, so it has no effect on platforms where the assembly already
+ /// resolves normally.
+ ///
+ internal static class MonoPosixAssemblyResolver
+ {
+ internal const string MonoPosixAssemblyName = "Mono.Posix.NETStandard";
+
+ // RID-specific subfolders to probe, ordered by preference. win-arm64 is listed first so a
+ // matching asset is preferred should one ever ship (the 1.0.0 package does not provide one);
+ // the remaining RIDs all ship a managed Mono.Posix.NETStandard.dll that is identical IL.
+ internal static readonly string[] CandidateRuntimeIdentifiers = new[]
+ {
+ "win-arm64",
+ "win-x64",
+ "win-x86",
+ "linux-x64",
+ "linux-arm64",
+ "osx"
+ };
+
+ [ModuleInitializer]
+ internal static void Initialize()
+ {
+ AssemblyLoadContext.Default.Resolving += ResolveMonoPosix;
+ }
+
+ internal static Assembly? ResolveMonoPosix(AssemblyLoadContext context, AssemblyName assemblyName)
+ {
+ var path = FindAssemblyPath(AppContext.BaseDirectory, assemblyName?.Name);
+ if (path is null)
+ {
+ return null;
+ }
+
+ return context.LoadFromAssemblyPath(path);
+ }
+
+ ///
+ /// Returns the path to a shipped managed Mono.Posix.NETStandard.dll under the given base
+ /// directory, or null if the requested assembly is not Mono.Posix.NETStandard or no
+ /// shipped copy could be found.
+ ///
+ internal static string? FindAssemblyPath(string baseDirectory, string? assemblyName)
+ {
+ if (string.IsNullOrEmpty(baseDirectory) || !string.Equals(assemblyName, MonoPosixAssemblyName, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ foreach (var rid in CandidateRuntimeIdentifiers)
+ {
+ var candidate = Path.Combine(baseDirectory, "runtimes", rid, "lib", "netstandard2.0", $"{MonoPosixAssemblyName}.dll");
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Tests/MonoPosixAssemblyResolverTests.cs b/Tests/MonoPosixAssemblyResolverTests.cs
new file mode 100644
index 00000000..7077168f
--- /dev/null
+++ b/Tests/MonoPosixAssemblyResolverTests.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License.
+using Microsoft.CST.AttackSurfaceAnalyzer.Utils;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.IO;
+using System.Runtime.Loader;
+
+namespace Microsoft.CST.AttackSurfaceAnalyzer.Tests
+{
+ ///
+ /// Tests for , which provides the fallback used to
+ /// load Mono.Posix.NETStandard on RIDs (such as win-arm64) for which the package ships no
+ /// runtime asset.
+ ///
+ [TestClass]
+ public class MonoPosixAssemblyResolverTests
+ {
+ [TestMethod]
+ public void FindAssemblyPathReturnsNullForUnrelatedAssembly()
+ {
+ var dir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName;
+ try
+ {
+ Assert.IsNull(MonoPosixAssemblyResolver.FindAssemblyPath(dir, "System.Text.Json"));
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void FindAssemblyPathReturnsNullWhenNoRuntimeAssetShipped()
+ {
+ var dir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName;
+ try
+ {
+ Assert.IsNull(MonoPosixAssemblyResolver.FindAssemblyPath(dir, MonoPosixAssemblyResolver.MonoPosixAssemblyName));
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void FindAssemblyPathLocatesShippedRuntimeAsset()
+ {
+ var baseDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName;
+ try
+ {
+ // Simulate the shipped layout for an RID other than the (missing) win-arm64.
+ var ridDir = Path.Combine(baseDir, "runtimes", "win-x64", "lib", "netstandard2.0");
+ Directory.CreateDirectory(ridDir);
+ var expected = Path.Combine(ridDir, $"{MonoPosixAssemblyResolver.MonoPosixAssemblyName}.dll");
+ File.WriteAllText(expected, string.Empty);
+
+ var found = MonoPosixAssemblyResolver.FindAssemblyPath(baseDir, MonoPosixAssemblyResolver.MonoPosixAssemblyName);
+
+ Assert.AreEqual(expected, found);
+ }
+ finally
+ {
+ Directory.Delete(baseDir, true);
+ }
+ }
+
+ [TestMethod]
+ public void ResolverIsRegisteredAndMonoPosixLoads()
+ {
+ // The module initializer should have registered the resolver, and Mono.Posix.NETStandard
+ // should be loadable (either via the default host on supported RIDs or via the fallback).
+ var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(
+ new System.Reflection.AssemblyName(MonoPosixAssemblyResolver.MonoPosixAssemblyName));
+
+ Assert.IsNotNull(assembly);
+ Assert.IsNotNull(assembly.GetType("Mono.Unix.UnixFileInfo"));
+ }
+ }
+}