diff --git a/src/AppInstallerCLITests/Runtime.cpp b/src/AppInstallerCLITests/Runtime.cpp index 3594b870c1..d52addcded 100644 --- a/src/AppInstallerCLITests/Runtime.cpp +++ b/src/AppInstallerCLITests/Runtime.cpp @@ -7,6 +7,7 @@ #include using namespace AppInstaller; +using namespace AppInstaller::Filesystem; using namespace AppInstaller::Runtime; using namespace TestCommon; diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index 92943625c0..f27b1738bc 100644 --- a/src/AppInstallerCLITests/TestHooks.h +++ b/src/AppInstallerCLITests/TestHooks.h @@ -28,7 +28,7 @@ namespace AppInstaller namespace Runtime { void TestHook_SetPathOverride(PathName target, const std::filesystem::path& path); - void TestHook_SetPathOverride(PathName target, const PathDetails& details); + void TestHook_SetPathOverride(PathName target, const Filesystem::PathDetails& details); void TestHook_ClearPathOverrides(); } diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index 08435990be..3c91b37667 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -441,7 +441,6 @@ true - @@ -468,7 +467,6 @@ - diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index 302521e268..3498d88dfb 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -144,9 +144,6 @@ Public\winget - - Public\winget - Public\winget @@ -311,9 +308,6 @@ Source Files - - Source Files - Source Files diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h index c388a3ba24..edc3127515 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include #include @@ -57,56 +58,15 @@ namespace AppInstaller::Runtime Max }; - // The principal that an ACE applies to. - enum class ACEPrincipal : uint32_t - { - CurrentUser, - Admins, - System, - }; - - // The permissions granted to a specific ACE. - enum class ACEPermissions : uint32_t - { - // This is not "Deny All", but rather, "Not mentioned" - None = 0x0, - Read = 0x1, - Write = 0x2, - Execute = 0x4, - ReadWrite = Read | Write, - ReadExecute = Read | Execute, - ReadWriteExecute = Read | Write | Execute, - // All means that full control will be granted - All = 0xFFFFFFFF - }; - - DEFINE_ENUM_FLAG_OPERATORS(ACEPermissions); - - // Information about a path that we use and how to set it up. - struct PathDetails - { - std::filesystem::path Path; - // Default to creating the directory with inherited ownership and permissions - bool Create = true; - std::optional Owner; - std::map ACL; - - // Shorthand for setting Owner and giving them ACEPermissions::All - void SetOwner(ACEPrincipal owner); - - // Determines if the ACL should be applied. - bool ShouldApplyACL() const; - - // Applies the ACL unconditionally. - void ApplyACL() const; - }; - // Gets the PathDetails used for the given path. // This is exposed primarily to allow for testing, GetPathTo should be preferred. - PathDetails GetPathDetailsFor(PathName path, bool forDisplay = false); + Filesystem::PathDetails GetPathDetailsFor(PathName path, bool forDisplay = false); // Gets the path to the requested location. - std::filesystem::path GetPathTo(PathName path, bool forDisplay = false); + inline std::filesystem::path GetPathTo(PathName path, bool forDisplay = false) + { + return Filesystem::GetPathTo(path, forDisplay); + } // Gets a new temp file path. std::filesystem::path GetNewTempFilePath(); diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp index 26d3de8359..570992ee42 100644 --- a/src/AppInstallerCommonCore/Runtime.cpp +++ b/src/AppInstallerCommonCore/Runtime.cpp @@ -5,9 +5,9 @@ #include "Public/AppInstallerLogging.h" #include "Public/AppInstallerRuntime.h" #include "Public/AppInstallerStrings.h" -#include "Public/winget/Filesystem.h" #include "Public/winget/UserSettings.h" #include "Public/winget/Registry.h" +#include #define WINGET_DEFAULT_LOG_DIRECTORY "DiagOutputDir" @@ -184,43 +184,6 @@ namespace AppInstaller::Runtime return result; } - DWORD AccessPermissionsFrom(ACEPermissions permissions) - { - DWORD result = 0; - - if (permissions == ACEPermissions::All) - { - result |= GENERIC_ALL; - } - else - { - if (WI_IsFlagSet(permissions, ACEPermissions::Read)) - { - result |= GENERIC_READ; - } - - if (WI_IsFlagSet(permissions, ACEPermissions::Write)) - { - result |= GENERIC_WRITE | FILE_DELETE_CHILD; - } - - if (WI_IsFlagSet(permissions, ACEPermissions::Execute)) - { - result |= GENERIC_EXECUTE; - } - } - - return result; - } - - // Contains the information about an ACE entry for a given principal. - struct ACEDetails - { - ACEPrincipal Principal; - PSID SID; - TRUSTEE_TYPE TrusteeType; - }; - // Try to replace LOCALAPPDATA first as it is the likely location, fall back to trying USERPROFILE. void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path) { @@ -238,99 +201,6 @@ namespace AppInstaller::Runtime s_runtimePathStateName.emplace(std::move(suitablePathPart)); } - void PathDetails::SetOwner(ACEPrincipal owner) - { - Owner = owner; - ACL[owner] = ACEPermissions::All; - } - - bool PathDetails::ShouldApplyACL() const - { - // Could be expanded to actually check the current owner/ACL on the path, but isn't worth it currently - return !ACL.empty(); - } - - void PathDetails::ApplyACL() const - { - bool hasCurrentUser = ACL.count(ACEPrincipal::CurrentUser) != 0; - bool hasSystem = ACL.count(ACEPrincipal::System) != 0; - - // Configuring permissions for both CurrentUser and SYSTEM while not having owner set as one of them is not valid because - // below we use only the owner permissions in the case of running as SYSTEM. - if ((hasCurrentUser && hasSystem) && - IsRunningAsSystem() && - (!Owner || (Owner.value() != ACEPrincipal::CurrentUser && Owner.value() != ACEPrincipal::System))) - { - THROW_HR(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); - } - - auto userToken = wil::get_token_information(); - auto adminSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); - auto systemSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID); - PSID ownerSID = nullptr; - - ACEDetails aceDetails[] = - { - { ACEPrincipal::CurrentUser, userToken->User.Sid, TRUSTEE_IS_USER }, - { ACEPrincipal::Admins, adminSID.get(), TRUSTEE_IS_WELL_KNOWN_GROUP}, - { ACEPrincipal::System, systemSID.get(), TRUSTEE_IS_USER}, - }; - - ULONG entriesCount = 0; - std::array explicitAccess; - - // If the current user is SYSTEM, we want to take either the owner or the only configured set of permissions. - // The check above should prevent us from getting into situations outside of the ones below. - std::optional principalToIgnore; - if (hasCurrentUser && hasSystem && EqualSid(userToken->User.Sid, systemSID.get())) - { - principalToIgnore = (Owner.value() == ACEPrincipal::CurrentUser ? ACEPrincipal::System : ACEPrincipal::CurrentUser); - } - - for (const auto& ace : aceDetails) - { - if (principalToIgnore && principalToIgnore.value() == ace.Principal) - { - continue; - } - - if (Owner && Owner.value() == ace.Principal) - { - ownerSID = ace.SID; - } - - auto itr = ACL.find(ace.Principal); - if (itr != ACL.end()) - { - EXPLICIT_ACCESS_W& entry = explicitAccess[entriesCount++]; - entry = {}; - - entry.grfAccessPermissions = AccessPermissionsFrom(itr->second); - entry.grfAccessMode = SET_ACCESS; - entry.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE; - - entry.Trustee.pMultipleTrustee = nullptr; - entry.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; - entry.Trustee.TrusteeForm = TRUSTEE_IS_SID; - entry.Trustee.TrusteeType = ace.TrusteeType; - entry.Trustee.ptstrName = reinterpret_cast(ace.SID); - } - } - - wil::unique_any acl; - THROW_IF_WIN32_ERROR(SetEntriesInAclW(entriesCount, explicitAccess.data(), nullptr, &acl)); - - std::wstring path = Path.wstring(); - SECURITY_INFORMATION securityInformation = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; - - if (ownerSID) - { - securityInformation |= OWNER_SECURITY_INFORMATION; - } - - THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(&path[0], SE_FILE_OBJECT, securityInformation, ownerSID, nullptr, acl.get(), nullptr)); - } - // Contains all of the paths that are common between the runtime contexts. PathDetails GetPathDetailsCommon(PathName path, bool forDisplay) { @@ -616,38 +486,6 @@ namespace AppInstaller::Runtime return result; } - std::filesystem::path GetPathTo(PathName path, bool forDisplay) - { - PathDetails details = GetPathDetailsFor(path, forDisplay); - - if (details.Create) - { - if (details.Path.is_absolute()) - { - if (std::filesystem::exists(details.Path) && !std::filesystem::is_directory(details.Path)) - { - std::filesystem::remove(details.Path); - } - - std::filesystem::create_directories(details.Path); - - // Set the ACLs on the directory if needed. We do this after creating the directory because an attacker could - // have created the directory beforehand so we must be able to place the correct ACL on any directory or fail - // to operate. - if (details.ShouldApplyACL()) - { - details.ApplyACL(); - } - } - else - { - AICLI_LOG(Core, Warning, << "GetPathTo directory creation requested for [" << path << "], but path was not absolute: " << details.Path); - } - } - - return std::move(details.Path); - } - std::filesystem::path GetNewTempFilePath() { GUID guid; diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj index ab1bfc2e5d..25c174ef83 100644 --- a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj +++ b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj @@ -411,6 +411,7 @@ + @@ -439,6 +440,7 @@ + NotUsing diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters index e36760fcf2..88a41d4769 100644 --- a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters +++ b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -131,6 +131,9 @@ Public\winget + + Public\winget + @@ -214,6 +217,9 @@ Source Files + + Source Files + diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp similarity index 56% rename from src/AppInstallerCommonCore/Filesystem.cpp rename to src/AppInstallerSharedLib/Filesystem.cpp index 2c5e5ad8a7..d3dfbb8c57 100644 --- a/src/AppInstallerCommonCore/Filesystem.cpp +++ b/src/AppInstallerSharedLib/Filesystem.cpp @@ -1,13 +1,56 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "Public/winget/Filesystem.h" #include "Public/AppInstallerStrings.h" -#include "public/winget/Filesystem.h" +#include "Public/AppInstallerLogging.h" +#include "Public/winget/Runtime.h" + +using namespace std::chrono_literals; +using namespace std::string_view_literals; +using namespace AppInstaller::Runtime; namespace AppInstaller::Filesystem { - using namespace std::chrono_literals; - using namespace std::string_view_literals; + namespace anon + { + // Contains the information about an ACE entry for a given principal. + struct ACEDetails + { + ACEPrincipal Principal; + PSID SID; + TRUSTEE_TYPE TrusteeType; + }; + + DWORD AccessPermissionsFrom(ACEPermissions permissions) + { + DWORD result = 0; + + if (permissions == ACEPermissions::All) + { + result |= GENERIC_ALL; + } + else + { + if (WI_IsFlagSet(permissions, ACEPermissions::Read)) + { + result |= GENERIC_READ; + } + + if (WI_IsFlagSet(permissions, ACEPermissions::Write)) + { + result |= GENERIC_WRITE | FILE_DELETE_CHILD; + } + + if (WI_IsFlagSet(permissions, ACEPermissions::Execute)) + { + result |= GENERIC_EXECUTE; + } + } + + return result; + } + } DWORD GetVolumeInformationFlagsByHandle(HANDLE anyFileHandle) { @@ -255,4 +298,127 @@ namespace AppInstaller::Filesystem } return Utility::ICUCaseInsensitiveEquals(Utility::ConvertToUTF8(volumeName1), Utility::ConvertToUTF8(volumeName2)); } -} \ No newline at end of file + + void PathDetails::SetOwner(ACEPrincipal owner) + { + Owner = owner; + ACL[owner] = ACEPermissions::All; + } + + bool PathDetails::ShouldApplyACL() const + { + // Could be expanded to actually check the current owner/ACL on the path, but isn't worth it currently + return !ACL.empty(); + } + + void PathDetails::ApplyACL() const + { + bool hasCurrentUser = ACL.count(ACEPrincipal::CurrentUser) != 0; + bool hasSystem = ACL.count(ACEPrincipal::System) != 0; + + // Configuring permissions for both CurrentUser and SYSTEM while not having owner set as one of them is not valid because + // below we use only the owner permissions in the case of running as SYSTEM. + if ((hasCurrentUser && hasSystem) && + IsRunningAsSystem() && + (!Owner || (Owner.value() != ACEPrincipal::CurrentUser && Owner.value() != ACEPrincipal::System))) + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + + auto userToken = wil::get_token_information(); + auto adminSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + auto systemSID = wil::make_static_sid(SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID); + PSID ownerSID = nullptr; + + anon::ACEDetails aceDetails[] = + { + { ACEPrincipal::CurrentUser, userToken->User.Sid, TRUSTEE_IS_USER }, + { ACEPrincipal::Admins, adminSID.get(), TRUSTEE_IS_WELL_KNOWN_GROUP}, + { ACEPrincipal::System, systemSID.get(), TRUSTEE_IS_USER}, + }; + + ULONG entriesCount = 0; + std::array explicitAccess; + + // If the current user is SYSTEM, we want to take either the owner or the only configured set of permissions. + // The check above should prevent us from getting into situations outside of the ones below. + std::optional principalToIgnore; + if (hasCurrentUser && hasSystem && EqualSid(userToken->User.Sid, systemSID.get())) + { + principalToIgnore = (Owner.value() == ACEPrincipal::CurrentUser ? ACEPrincipal::System : ACEPrincipal::CurrentUser); + } + + for (const auto& ace : aceDetails) + { + if (principalToIgnore && principalToIgnore.value() == ace.Principal) + { + continue; + } + + if (Owner && Owner.value() == ace.Principal) + { + ownerSID = ace.SID; + } + + auto itr = ACL.find(ace.Principal); + if (itr != ACL.end()) + { + EXPLICIT_ACCESS_W& entry = explicitAccess[entriesCount++]; + entry = {}; + + entry.grfAccessPermissions = anon::AccessPermissionsFrom(itr->second); + entry.grfAccessMode = SET_ACCESS; + entry.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE; + + entry.Trustee.pMultipleTrustee = nullptr; + entry.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; + entry.Trustee.TrusteeForm = TRUSTEE_IS_SID; + entry.Trustee.TrusteeType = ace.TrusteeType; + entry.Trustee.ptstrName = reinterpret_cast(ace.SID); + } + } + + wil::unique_any acl; + THROW_IF_WIN32_ERROR(SetEntriesInAclW(entriesCount, explicitAccess.data(), nullptr, &acl)); + + std::wstring path = Path.wstring(); + SECURITY_INFORMATION securityInformation = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; + + if (ownerSID) + { + securityInformation |= OWNER_SECURITY_INFORMATION; + } + + THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(&path[0], SE_FILE_OBJECT, securityInformation, ownerSID, nullptr, acl.get(), nullptr)); + } + + std::filesystem::path InitializeAndGetPathTo(PathDetails&& details) + { + if (details.Create) + { + if (details.Path.is_absolute()) + { + if (std::filesystem::exists(details.Path) && !std::filesystem::is_directory(details.Path)) + { + std::filesystem::remove(details.Path); + } + + std::filesystem::create_directories(details.Path); + + // Set the ACLs on the directory if needed. We do this after creating the directory because an attacker could + // have created the directory beforehand so we must be able to place the correct ACL on any directory or fail + // to operate. + if (details.ShouldApplyACL()) + { + details.ApplyACL(); + } + } + else + { + AICLI_LOG(Core, Warning, << "InitializeAndGetPathTo directory creation requested for path that was not absolute: " << details.Path); + } + } + + return std::move(details.Path); + } +} diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h similarity index 54% rename from src/AppInstallerCommonCore/Public/winget/Filesystem.h rename to src/AppInstallerSharedLib/Public/winget/Filesystem.h index 0e202ace16..6871713723 100644 --- a/src/AppInstallerCommonCore/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -2,6 +2,8 @@ // Licensed under the MIT License. #pragma once #include +#include +#include #include namespace AppInstaller::Filesystem @@ -46,4 +48,59 @@ namespace AppInstaller::Filesystem // Verifies that the paths are on the same volume. bool IsSameVolume(const std::filesystem::path& path1, const std::filesystem::path& path2); -} \ No newline at end of file + + // The principal that an ACE applies to. + enum class ACEPrincipal : uint32_t + { + CurrentUser, + Admins, + System, + }; + + // The permissions granted to a specific ACE. + enum class ACEPermissions : uint32_t + { + // This is not "Deny All", but rather, "Not mentioned" + None = 0x0, + Read = 0x1, + Write = 0x2, + Execute = 0x4, + ReadWrite = Read | Write, + ReadExecute = Read | Execute, + ReadWriteExecute = Read | Write | Execute, + // All means that full control will be granted + All = 0xFFFFFFFF + }; + + DEFINE_ENUM_FLAG_OPERATORS(ACEPermissions); + + // Information about a path that we use and how to set it up. + struct PathDetails + { + std::filesystem::path Path; + // Default to creating the directory with inherited ownership and permissions + bool Create = true; + std::optional Owner; + std::map ACL; + + // Shorthand for setting Owner and giving them ACEPermissions::All + void SetOwner(ACEPrincipal owner); + + // Determines if the ACL should be applied. + bool ShouldApplyACL() const; + + // Applies the ACL unconditionally. + void ApplyACL() const; + }; + + // Initializes from the given details and returns the path to it. + // The path is moved out of the details. + std::filesystem::path InitializeAndGetPathTo(PathDetails&& details); + + // Gets the path to the requested location. + template + std::filesystem::path GetPathTo(PathEnum path, bool forDisplay = false) + { + return InitializeAndGetPathTo(GetPathDetailsFor(path, forDisplay)); + } +} diff --git a/src/AppInstallerSharedLib/pch.h b/src/AppInstallerSharedLib/pch.h index f10f3dca38..9304b0556f 100644 --- a/src/AppInstallerSharedLib/pch.h +++ b/src/AppInstallerSharedLib/pch.h @@ -4,9 +4,11 @@ #define NOMINMAX #include +#include #include #include -#include +#include +#include #include #define YAML_DECLARE_STATIC @@ -31,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -57,4 +60,4 @@ #include #include #include -#include +#include diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs index 8085b2ea03..cd9ec0bb24 100644 --- a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs +++ b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -352,6 +352,8 @@ public void InstallModule(ModuleSpecification moduleSpecification) // Maybe is already there. if (!this.ValidateModule(moduleSpecification)) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Installing module: {moduleSpecification.Name} ..."); + // Ok, we have to get it. if (this.location == PowerShellConfigurationProcessorLocation.Custom) { @@ -360,14 +362,18 @@ public void InstallModule(ModuleSpecification moduleSpecification) throw new ArgumentNullException(nameof(this.customLocation)); } + this.OnDiagnostics(DiagnosticLevel.Verbose, $"... calling save module ..."); this.SaveModule(moduleSpecification, this.customLocation); } else { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"... calling install module ..."); using PowerShell pwsh = PowerShell.Create(this.Runspace); this.powerShellGet.InstallModule(pwsh, moduleSpecification, this.location == PowerShellConfigurationProcessorLocation.AllUsers); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); } + + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module installed."); } } @@ -508,19 +514,25 @@ public void SetLocation(PowerShellConfigurationProcessorLocation location, strin private bool ValidateModule(ModuleSpecification moduleSpecification) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Validating module: {moduleSpecification.Name} ..."); + var loadedModule = this.GetImportedModule(moduleSpecification); if (loadedModule is not null) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module is already imported."); return true; } var availableModule = this.GetAvailableModule(moduleSpecification); if (availableModule is not null) { + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module is available, importing ..."); this.ImportModule(moduleSpecification); + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module imported."); return true; } + this.OnDiagnostics(DiagnosticLevel.Verbose, $" ... module not found."); return false; }