From a471e287a94f28662252f797ae61737b4ee64e01 Mon Sep 17 00:00:00 2001
From: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date: Thu, 14 Dec 2023 13:35:11 -0800
Subject: [PATCH] Allow winget configure from https location and extend winget
configure validate for winget resource units (#3833)
---
azure-pipelines.yml | 2 +-
src/AppInstallerCLI.sln | 1 +
.../AppInstallerCLICore.vcxproj | 2 +
.../Commands/ConfigureCommand.cpp | 2 +-
.../Commands/ConfigureShowCommand.cpp | 2 +-
.../Commands/ConfigureTestCommand.cpp | 2 +-
.../Commands/ConfigureValidateCommand.cpp | 3 +-
...igurationWingetDscModuleUnitValidation.cpp | 396 ++++++++++++++++++
...nfigurationWingetDscModuleUnitValidation.h | 30 ++
src/AppInstallerCLICore/Resources.h | 17 +
.../Workflows/ConfigurationFlow.cpp | 126 +++++-
.../Workflows/ConfigurationFlow.h | 6 +
.../Workflows/WorkflowBase.cpp | 44 ++
.../Workflows/WorkflowBase.h | 16 +
.../AppInstallerCLIE2ETests.csproj | 17 -
.../ConfigureCommand.cs | 12 +
.../ConfigureShowCommand.cs | 11 +
.../ConfigureTestCommand.cs | 11 +
.../ConfigureValidateCommand.cs | 106 +++++
...sourceValidate_DependencySourceMissing.yml | 13 +
.../WinGetDscResourceValidate_Good.yml | 24 ++
...GetDscResourceValidate_PackageNotFound.yml | 24 ++
...esourceValidate_PackageVersionNotFound.yml | 24 ++
...etDscResourceValidate_SourceOpenFailed.yml | 24 ++
...onSpecifiedWithOnlyOneVersionAvailable.yml | 24 ++
...Validate_VersionSpecifiedWithUseLatest.yml | 25 ++
.../Shared/Strings/en-us/winget.resw | 70 +++-
.../Public/AppInstallerDownloader.h | 1 +
src/LocalhostWebServer/Program.cs | 41 +-
.../Run-LocalhostWebServer.ps1 | 7 +-
src/LocalhostWebServer/Startup.cs | 3 +
.../Common/WinGetIntegrity.cs | 2 +-
32 files changed, 1050 insertions(+), 38 deletions(-)
create mode 100644 src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp
create mode 100644 src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.h
create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_DependencySourceMissing.yml
create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_Good.yml
create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageNotFound.yml
create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_PackageVersionNotFound.yml
create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_SourceOpenFailed.yml
create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithOnlyOneVersionAvailable.yml
create mode 100644 src/AppInstallerCLIE2ETests/TestData/Configuration/WinGetDscResourceValidate_VersionSpecifiedWithUseLatest.yml
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 829c86206a..0c01ebb798 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -222,7 +222,7 @@ jobs:
- template: templates/e2e-setup.yml
parameters:
sourceDir: $(Build.SourcesDirectory)
- localhostWebServerArgs: '-BuildRoot $(buildOutDir)\LocalhostWebServer -StaticFileRoot $(Agent.TempDirectory)\TestLocalIndex -LocalSourceJson $(Build.SourcesDirectory)\src\AppInstallerCLIE2ETests\TestData\localsource.json -SourceCert $(Build.SourcesDirectory)\src\AppInstallerCLIE2ETests\TestData\AppInstallerTest.cer'
+ localhostWebServerArgs: '-BuildRoot $(buildOutDir)\LocalhostWebServer -StaticFileRoot $(Agent.TempDirectory)\TestLocalIndex -LocalSourceJson $(Build.SourcesDirectory)\src\AppInstallerCLIE2ETests\TestData\localsource.json -TestDataPath $(Build.SourcesDirectory)\src\AppInstallerCLIE2ETests\TestData -SourceCert $(Build.SourcesDirectory)\src\AppInstallerCLIE2ETests\TestData\AppInstallerTest.cer'
- template: templates/e2e-test.template.yml
parameters:
diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln
index 138174f68e..25ff8cbf9e 100644
--- a/src/AppInstallerCLI.sln
+++ b/src/AppInstallerCLI.sln
@@ -147,6 +147,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Detours", "Xlang\UndockedRe
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{8E43F982-40D5-4DF1-9044-C08047B5F43B}"
ProjectSection(SolutionItems) = preProject
+ ..\templates\e2e-setup.yml = ..\templates\e2e-setup.yml
..\templates\e2e-test.template.yml = ..\templates\e2e-test.template.yml
EndProjectSection
EndProject
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
index 11d44ba3a9..9a94874dff 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
@@ -382,6 +382,7 @@
+
@@ -440,6 +441,7 @@
+
diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp
index ba5fff1857..3b8f6daed6 100644
--- a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp
@@ -71,7 +71,7 @@ namespace AppInstaller::CLI
{
context <<
VerifyIsFullPackage <<
- VerifyFile(Execution::Args::Type::ConfigurationFile) <<
+ VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) <<
CreateConfigurationProcessor <<
OpenConfigurationSet <<
ShowConfigurationSet <<
diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp
index 284a7654e1..c575cc349a 100644
--- a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp
@@ -37,7 +37,7 @@ namespace AppInstaller::CLI
void ConfigureShowCommand::ExecuteInternal(Execution::Context& context) const
{
context <<
- VerifyFile(Execution::Args::Type::ConfigurationFile) <<
+ VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) <<
CreateConfigurationProcessor <<
OpenConfigurationSet <<
ShowConfigurationSet;
diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp
index 63e8d56428..bfc53f78a9 100644
--- a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp
@@ -38,7 +38,7 @@ namespace AppInstaller::CLI
{
context <<
VerifyIsFullPackage <<
- VerifyFile(Execution::Args::Type::ConfigurationFile) <<
+ VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) <<
CreateConfigurationProcessor <<
OpenConfigurationSet <<
ShowConfigurationSet <<
diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp
index 83b17e5d91..024b450e7a 100644
--- a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp
@@ -37,11 +37,12 @@ namespace AppInstaller::CLI
{
context <<
VerifyIsFullPackage <<
- VerifyFile(Execution::Args::Type::ConfigurationFile) <<
+ VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) <<
CreateConfigurationProcessor <<
OpenConfigurationSet <<
ValidateConfigurationSetSemantics <<
ValidateConfigurationSetUnitProcessors <<
+ ValidateConfigurationSetUnitContents <<
ValidateAllGoodMessage;
}
diff --git a/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp b/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp
new file mode 100644
index 0000000000..1e556b888c
--- /dev/null
+++ b/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp
@@ -0,0 +1,396 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "ConfigurationWingetDscModuleUnitValidation.h"
+#include "ExecutionContext.h"
+#include
+
+using namespace winrt::Microsoft::Management::Configuration;
+using namespace winrt::Windows::Foundation;
+using namespace winrt::Windows::Foundation::Collections;
+using namespace AppInstaller::Utility::literals;
+
+namespace AppInstaller::CLI::Configuration
+{
+ namespace
+ {
+ constexpr static std::string_view UnitType_WinGetSources = "WinGetSources"sv;
+ constexpr static std::string_view UnitType_WinGetPackage = "WinGetPackage"sv;
+
+ constexpr static std::string_view WellKnownSourceName_WinGet = "winget"sv;
+ constexpr static std::string_view WellKnownSourceName_MSStore = "msstore"sv;
+
+ constexpr static std::string_view ValueSetKey_TreatAsArray = "treatAsArray"sv;
+
+ constexpr static std::string_view WinGetSourcesValueSetKey_Sources = "sources"sv;
+ constexpr static std::string_view WinGetSourcesValueSetKey_SourceName = "name"sv;
+ constexpr static std::string_view WinGetSourcesValueSetKey_SourceType = "type"sv;
+ constexpr static std::string_view WinGetSourcesValueSetKey_SourceArg = "arg"sv;
+
+ constexpr static std::string_view WinGetPackageValueSetKey_Id = "id"sv;
+ constexpr static std::string_view WinGetPackageValueSetKey_Version = "version"sv;
+ constexpr static std::string_view WinGetPackageValueSetKey_Source = "source"sv;
+ constexpr static std::string_view WinGetPackageValueSetKey_UseLatest = "useLatest"sv;
+
+ struct WinGetSource
+ {
+ std::string Name;
+ std::string Type;
+ std::string Arg;
+
+ bool Empty()
+ {
+ return Name.empty() && Arg.empty() && Type.empty();
+ }
+ };
+
+ std::string GetPropertyValueAsString(const winrt::Windows::Foundation::IInspectable& value)
+ {
+ IPropertyValue propertyValue = value.try_as();
+ if (propertyValue && propertyValue.Type() == PropertyType::String)
+ {
+ return Utility::ConvertToUTF8(propertyValue.GetString());
+ }
+
+ return {};
+ }
+
+ bool GetPropertyValueAsBoolean(const winrt::Windows::Foundation::IInspectable& value, bool defaultIfFailed = false)
+ {
+ IPropertyValue propertyValue = value.try_as();
+ if (propertyValue && propertyValue.Type() == PropertyType::Boolean)
+ {
+ return propertyValue.GetBoolean();
+ }
+
+ return defaultIfFailed;
+ }
+
+ std::vector ParseWinGetSourcesFromSettings(const ValueSet& settings)
+ {
+ // Iterate through the value set as Powershell variables are case insensitive.
+ std::vector result;
+ for (auto const& settingsPair : settings)
+ {
+ auto settingsKey = Utility::ConvertToUTF8(settingsPair.Key());
+ if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_Sources, settingsKey))
+ {
+ auto sources = settingsPair.Value().try_as();
+ if (!sources)
+ {
+ return {};
+ }
+ bool isArray = false;
+ for (auto const& sourcesPair : sources)
+ {
+ if (Utility::CaseInsensitiveEquals(ValueSetKey_TreatAsArray, Utility::ConvertToUTF8(sourcesPair.Key())))
+ {
+ isArray = true;
+ }
+ else
+ {
+ auto source = sourcesPair.Value().try_as();
+ if (source)
+ {
+ WinGetSource wingetSource;
+ for (auto const& sourcePair : source)
+ {
+ auto sourceKey = Utility::ConvertToUTF8(sourcePair.Key());
+ if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_SourceName, sourceKey))
+ {
+ wingetSource.Name = GetPropertyValueAsString(sourcePair.Value());
+ }
+ else if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_SourceType, sourceKey))
+ {
+ wingetSource.Type = GetPropertyValueAsString(sourcePair.Value());
+ }
+ else if (Utility::CaseInsensitiveEquals(WinGetSourcesValueSetKey_SourceArg, sourceKey))
+ {
+ wingetSource.Arg = GetPropertyValueAsString(sourcePair.Value());
+ }
+ }
+
+ if (!wingetSource.Empty())
+ {
+ result.emplace_back(std::move(wingetSource));
+ }
+ }
+ }
+ }
+
+ if (!isArray)
+ {
+ return {};
+ }
+
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ bool IsWellKnownSourceName(std::string_view sourceName)
+ {
+ return Utility::CaseInsensitiveEquals(WellKnownSourceName_WinGet, sourceName) ||
+ Utility::CaseInsensitiveEquals(WellKnownSourceName_MSStore, sourceName);
+ }
+
+ bool ValidateWellKnownSource(const WinGetSource& source)
+ {
+ static std::vector wellKnownSourceDetails =
+ {
+ Repository::Source{ Repository::WellKnownSource::WinGet }.GetDetails(),
+ Repository::Source{ Repository::WellKnownSource::MicrosoftStore }.GetDetails(),
+ };
+
+ for (auto const& wellKnownSource : wellKnownSourceDetails)
+ {
+ if (Utility::CaseInsensitiveEquals(wellKnownSource.Name, source.Name) &&
+ Utility::CaseInsensitiveEquals(wellKnownSource.Arg, source.Arg) &&
+ Utility::CaseInsensitiveEquals(wellKnownSource.Type, source.Type))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ struct WinGetPackage
+ {
+ std::string Id;
+ std::string Version;
+ std::string Source;
+ bool UseLatest = false;
+
+ bool Empty()
+ {
+ return Id.empty() && Version.empty() && Source.empty();
+ }
+ };
+
+ WinGetPackage ParseWinGetPackageFromSettings(const ValueSet& settings)
+ {
+ // Iterate through the value set as Powershell variables are case insensitive.
+ WinGetPackage result;
+ for (auto const& settingsPair : settings)
+ {
+ auto settingsKey = Utility::ConvertToUTF8(settingsPair.Key());
+ if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_Id, settingsKey))
+ {
+ result.Id = GetPropertyValueAsString(settingsPair.Value());
+ }
+ else if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_Version, settingsKey))
+ {
+ result.Version = GetPropertyValueAsString(settingsPair.Value());
+ }
+ else if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_Source, settingsKey))
+ {
+ result.Source = GetPropertyValueAsString(settingsPair.Value());
+ }
+ else if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_UseLatest, settingsKey))
+ {
+ result.UseLatest = GetPropertyValueAsBoolean(settingsPair.Value());
+ }
+ }
+
+ return result;
+ }
+ }
+
+ bool WingetDscModuleUnitValidator::ValidateConfigurationSetUnit(Execution::Context& context, const ConfigurationUnit& unit)
+ {
+ bool foundIssues = false;
+ auto details = unit.Details();
+ auto unitType = Utility::ConvertToUTF8(details.UnitType());
+ auto unitIntent = unit.Intent();
+
+ if (Utility::CaseInsensitiveEquals(UnitType_WinGetSources, unitType))
+ {
+ auto sources = ParseWinGetSourcesFromSettings(unit.Settings());
+ if (sources.size() == 0)
+ {
+ AICLI_LOG(Config, Warning, << "Failed to parse WinGetSources or empty content.");
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitEmptyContent(Utility::LocIndView{ UnitType_WinGetSources }) << std::endl;
+ foundIssues = true;
+ }
+ for (auto const& source : sources)
+ {
+ // Validate basic semantics.
+ if (source.Name.empty())
+ {
+ AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Name");
+ context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSources }, "Name"_liv) << std::endl;
+ foundIssues = true;
+ }
+ if (source.Arg.empty())
+ {
+ AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Arg");
+ context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSources }, "Arg"_liv) << std::endl;
+ foundIssues = true;
+ }
+
+ // Validate well known source or process 3rd party source.
+ if (IsWellKnownSourceName(source.Name))
+ {
+ if (!ValidateWellKnownSource(source))
+ {
+ AICLI_LOG(Config, Warning, << "WinGetSource conflicts with a well known source. Source: " << source.Name);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitKnownSourceConfliction(Utility::LocIndView{ source.Name }) << std::endl;
+ foundIssues = true;
+ }
+ }
+ else
+ {
+ if (unitIntent == ConfigurationUnitIntent::Assert)
+ {
+ AICLI_LOG(Config, Warning, << "Asserting on 3rd party source: " << source.Name);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitThirdPartySourceAssertion(Utility::LocIndView{ source.Name }) << std::endl;
+ foundIssues = true;
+ }
+ else if (unitIntent == ConfigurationUnitIntent::Apply)
+ {
+ // Add to dependency source map so it can be validated with later WinGetPackage source
+ m_dependenciesSourceAndUnitIdMap.emplace(Utility::FoldCase(std::string_view{ source.Name }), Utility::FoldCase(Utility::NormalizedString{ unit.Identifier() }));
+ }
+ }
+ }
+ }
+ else if (Utility::CaseInsensitiveEquals(UnitType_WinGetPackage, unitType))
+ {
+ auto package = ParseWinGetPackageFromSettings(unit.Settings());
+ if (package.Empty())
+ {
+ AICLI_LOG(Config, Warning, << "Failed to parse WinGetPackage or empty content.");
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitEmptyContent(Utility::LocIndView{ UnitType_WinGetPackage }) << std::endl;
+ foundIssues = true;
+ }
+ // Validate basic semantics.
+ if (package.Id.empty())
+ {
+ AICLI_LOG(Config, Error, << "WinGetPackage unit missing required arg: Id");
+ context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetPackage }, "Id"_liv) << std::endl;
+ foundIssues = true;
+ }
+ if (package.Source.empty())
+ {
+ AICLI_LOG(Config, Warning, << "WinGetPackage unit missing recommended arg: Source");
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitMissingRecommendedArg(Utility::LocIndView{ UnitType_WinGetPackage }, "Source"_liv) << std::endl;
+ foundIssues = true;
+ }
+ if (package.UseLatest && !package.Version.empty())
+ {
+ AICLI_LOG(Config, Error, << "WinGetPackage unit both UseLatest and Version declared. Package: " << package.Id);
+ context.Reporter.Error() << Resource::String::WinGetResourceUnitBothPackageVersionAndUseLatest(Utility::LocIndView{ package.Id }) << std::endl;
+ foundIssues = true;
+ }
+ // Validate dependency source is configured.
+ if (!package.Source.empty() && !IsWellKnownSourceName(package.Source))
+ {
+ if (unitIntent == ConfigurationUnitIntent::Assert)
+ {
+ AICLI_LOG(Config, Warning, << "Asserting on a package that depends on a 3rd party source. Package: " << package.Id << " Source: " << package.Source);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitThirdPartySourceAssertionForPackage(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl;
+ foundIssues = true;
+ }
+ else
+ {
+ auto dependencySourceItr = m_dependenciesSourceAndUnitIdMap.find(Utility::FoldCase(std::string_view{ package.Source }));
+ if (dependencySourceItr == m_dependenciesSourceAndUnitIdMap.end())
+ {
+ AICLI_LOG(Config, Warning, << "WinGetPackage depends on a 3rd party source not previously configured. Package: " << package.Id << " Source: " << package.Source);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitDependencySourceNotConfigured(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl;
+ foundIssues = true;
+ }
+ else
+ {
+ bool foundInUnitDependencies = false;
+ for (auto const& entry : unit.Dependencies())
+ {
+ // The map contains normalized string, so just use direct comparison;
+ if (dependencySourceItr->second == Utility::FoldCase(Utility::NormalizedString{ entry }))
+ {
+ foundInUnitDependencies = true;
+ break;
+ }
+ }
+ if (!foundInUnitDependencies)
+ {
+ AICLI_LOG(Config, Warning, << "WinGetPackage depends on a 3rd party source. It is recommended to add the WinGetSources unit configuring the source to the unit's dependsOn list. Package: " << package.Id << " Source: " << package.Source);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitDependencySourceNotDeclaredAsDependency(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl;
+ foundIssues = true;
+ }
+ }
+ }
+ }
+ // Validate package is found and version available.
+ try
+ {
+ Repository::Source source{ package.Source };
+ if (!source)
+ {
+ AICLI_LOG(Config, Warning, << "Failed to open WinGet source. Package: " << package.Id << " Source: " << package.Source);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageSourceOpenFailed(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl;
+ foundIssues = true;
+ }
+ else
+ {
+ ProgressCallback empty;
+ source.Open(empty);
+ Repository::SearchRequest searchRequest;
+ searchRequest.Filters.emplace_back(Repository::PackageMatchFilter{ Repository::PackageMatchField::Id, Repository::MatchType::CaseInsensitive, package.Id });
+ auto searchResult = source.Search(searchRequest);
+ if (searchResult.Matches.size() == 0)
+ {
+ AICLI_LOG(Config, Warning, << "WinGetPackage not found: " << package.Id);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageNotFound(Utility::LocIndView{ package.Id }) << std::endl;
+ foundIssues = true;
+ }
+ else if (searchResult.Matches.size() > 1)
+ {
+ AICLI_LOG(Config, Warning, << "More than one WinGetPackage found: " << package.Id);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageMultipleFound(Utility::LocIndView{ package.Id }) << std::endl;
+ foundIssues = true;
+ }
+ else
+ {
+ if (!package.Version.empty())
+ {
+ auto versionKeys = searchResult.Matches.at(0).Package->GetAvailableVersionKeys(Repository::PinBehavior::IgnorePins);
+ bool foundVersion = false;
+ for (auto const& versionKey : versionKeys)
+ {
+ if (versionKey.Version == Utility::NormalizedString(package.Version))
+ {
+ foundVersion = true;
+ break;
+ }
+ }
+ if (!foundVersion)
+ {
+ AICLI_LOG(Config, Warning, << "WinGetPackage version not found. Package: " << package.Id << " Version: " << package.Version);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageVersionNotFound(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Version }) << std::endl;
+ foundIssues = true;
+ }
+ if (versionKeys.size() == 1)
+ {
+ AICLI_LOG(Config, Warning, << "WinGetPackage version specified with only one version available: " << package.Id);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitPackageVersionSpecifiedWithOnlyOnePackageVersion(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Version }) << std::endl;
+ foundIssues = true;
+ }
+ }
+ }
+ }
+ }
+ catch (...)
+ {
+ AICLI_LOG(Config, Warning, << "Failed to validate WinGetPackage: " << package.Id);
+ context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackage(Utility::LocIndView{ package.Id }) << std::endl;
+ foundIssues = true;
+ }
+ }
+
+ return !foundIssues;
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.h b/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.h
new file mode 100644
index 0000000000..3a553ce175
--- /dev/null
+++ b/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.h
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include