ConfigurationWingetDscModuleUnitValidation.cpp (18600B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "ConfigurationWingetDscModuleUnitValidation.h" 5 #include "ExecutionContext.h" 6 #include <winrt/Microsoft.Management.Configuration.h> 7 8 using namespace winrt::Microsoft::Management::Configuration; 9 using namespace winrt::Windows::Foundation; 10 using namespace winrt::Windows::Foundation::Collections; 11 using namespace AppInstaller::Utility::literals; 12 13 namespace AppInstaller::CLI::Configuration 14 { 15 namespace 16 { 17 constexpr static std::string_view UnitType_WinGetSource = "WinGetSource"sv; 18 constexpr static std::string_view UnitType_WinGetPackage = "WinGetPackage"sv; 19 20 constexpr static std::string_view WellKnownSourceName_WinGet = "winget"sv; 21 constexpr static std::string_view WellKnownSourceName_MSStore = "msstore"sv; 22 23 constexpr static std::string_view ValueSetKey_TreatAsArray = "treatAsArray"sv; 24 25 constexpr static std::string_view WinGetSourceValueSetKey_Name = "name"sv; 26 constexpr static std::string_view WinGetSourceValueSetKey_Type = "type"sv; 27 constexpr static std::string_view WinGetSourceValueSetKey_Arg = "argument"sv; 28 constexpr static std::string_view WinGetSourceValueSetKey_Ensure = "ensure"sv; 29 constexpr static std::string_view WinGetSourceValueSetKey_Ensure_Present = "present"sv; 30 31 constexpr static std::string_view WinGetPackageValueSetKey_Id = "id"sv; 32 constexpr static std::string_view WinGetPackageValueSetKey_Version = "version"sv; 33 constexpr static std::string_view WinGetPackageValueSetKey_Source = "source"sv; 34 constexpr static std::string_view WinGetPackageValueSetKey_UseLatest = "useLatest"sv; 35 36 struct WinGetSource 37 { 38 std::string Name; 39 std::string Type; 40 std::string Arg; 41 bool Present = true; 42 43 bool Empty() 44 { 45 return Name.empty() && Arg.empty() && Type.empty(); 46 } 47 }; 48 49 std::string GetPropertyValueAsString(const winrt::Windows::Foundation::IInspectable& value) 50 { 51 IPropertyValue propertyValue = value.try_as<IPropertyValue>(); 52 if (propertyValue && propertyValue.Type() == PropertyType::String) 53 { 54 return Utility::ConvertToUTF8(propertyValue.GetString()); 55 } 56 57 return {}; 58 } 59 60 bool GetPropertyValueAsBoolean(const winrt::Windows::Foundation::IInspectable& value, bool defaultIfFailed = false) 61 { 62 IPropertyValue propertyValue = value.try_as<IPropertyValue>(); 63 if (propertyValue && propertyValue.Type() == PropertyType::Boolean) 64 { 65 return propertyValue.GetBoolean(); 66 } 67 68 return defaultIfFailed; 69 } 70 71 WinGetSource ParseWinGetSourceFromSettings(const ValueSet& settings) 72 { 73 WinGetSource result; 74 75 // Iterate through the value set as Powershell variables are case-insensitive. 76 for (auto const& settingsPair : settings) 77 { 78 auto settingsKey = Utility::ConvertToUTF8(settingsPair.Key()); 79 80 if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Name, settingsKey)) 81 { 82 result.Name = GetPropertyValueAsString(settingsPair.Value()); 83 } 84 else if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Type, settingsKey)) 85 { 86 result.Type = GetPropertyValueAsString(settingsPair.Value()); 87 } 88 else if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Arg, settingsKey)) 89 { 90 result.Arg = GetPropertyValueAsString(settingsPair.Value()); 91 } 92 else if (Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Ensure, settingsKey)) 93 { 94 result.Present = Utility::CaseInsensitiveEquals(WinGetSourceValueSetKey_Ensure_Present, GetPropertyValueAsString(settingsPair.Value())); 95 } 96 } 97 98 return result; 99 } 100 101 bool IsWellKnownSourceName(std::string_view sourceName) 102 { 103 return Utility::CaseInsensitiveEquals(WellKnownSourceName_WinGet, sourceName) || 104 Utility::CaseInsensitiveEquals(WellKnownSourceName_MSStore, sourceName); 105 } 106 107 bool ValidateWellKnownSource(const WinGetSource& source) 108 { 109 static std::vector<Repository::SourceDetails> wellKnownSourceDetails = 110 { 111 Repository::Source{ Repository::WellKnownSource::WinGet }.GetDetails(), 112 Repository::Source{ Repository::WellKnownSource::MicrosoftStore }.GetDetails(), 113 }; 114 115 for (auto const& wellKnownSource : wellKnownSourceDetails) 116 { 117 if (Utility::CaseInsensitiveEquals(wellKnownSource.Name, source.Name) && 118 Utility::CaseInsensitiveEquals(wellKnownSource.Arg, source.Arg) && 119 Utility::CaseInsensitiveEquals(wellKnownSource.Type, source.Type)) 120 { 121 return true; 122 } 123 } 124 125 return false; 126 } 127 128 struct WinGetPackage 129 { 130 std::string Id; 131 std::string Version; 132 std::string Source; 133 bool UseLatest = false; 134 135 bool Empty() 136 { 137 return Id.empty() && Version.empty() && Source.empty(); 138 } 139 }; 140 141 WinGetPackage ParseWinGetPackageFromSettings(const ValueSet& settings) 142 { 143 // Iterate through the value set as Powershell variables are case-insensitive. 144 WinGetPackage result; 145 for (auto const& settingsPair : settings) 146 { 147 auto settingsKey = Utility::ConvertToUTF8(settingsPair.Key()); 148 if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_Id, settingsKey)) 149 { 150 result.Id = GetPropertyValueAsString(settingsPair.Value()); 151 } 152 else if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_Version, settingsKey)) 153 { 154 result.Version = GetPropertyValueAsString(settingsPair.Value()); 155 } 156 else if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_Source, settingsKey)) 157 { 158 result.Source = GetPropertyValueAsString(settingsPair.Value()); 159 } 160 else if (Utility::CaseInsensitiveEquals(WinGetPackageValueSetKey_UseLatest, settingsKey)) 161 { 162 result.UseLatest = GetPropertyValueAsBoolean(settingsPair.Value()); 163 } 164 } 165 166 return result; 167 } 168 } 169 170 bool WingetDscModuleUnitValidator::ValidateConfigurationSetUnit(Execution::Context& context, const ConfigurationUnit& unit) 171 { 172 bool foundIssues = false; 173 auto details = unit.Details(); 174 auto unitType = Utility::ConvertToUTF8(details.UnitType()); 175 auto unitIntent = unit.Intent(); 176 177 if (Utility::CaseInsensitiveEquals(UnitType_WinGetSource, unitType)) 178 { 179 auto source = ParseWinGetSourceFromSettings(unit.Settings()); 180 181 // Validate basic semantics. 182 if (source.Name.empty()) 183 { 184 AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Name"); 185 context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSource }, "Name"_liv) << std::endl; 186 foundIssues = true; 187 } 188 if (source.Arg.empty() && source.Present) 189 { 190 AICLI_LOG(Config, Error, << "WinGetSource unit missing required arg: Argument"); 191 context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetSource }, "Argument"_liv) << std::endl; 192 foundIssues = true; 193 } 194 195 // Validate well known source or process 3rd party source. 196 if (IsWellKnownSourceName(source.Name)) 197 { 198 if (!ValidateWellKnownSource(source)) 199 { 200 AICLI_LOG(Config, Warning, << "WinGetSource conflicts with a well known source. Source: " << source.Name); 201 context.Reporter.Warn() << Resource::String::WinGetResourceUnitKnownSourceConfliction(Utility::LocIndView{ source.Name }) << std::endl; 202 foundIssues = true; 203 } 204 } 205 else 206 { 207 if (unitIntent == ConfigurationUnitIntent::Assert) 208 { 209 AICLI_LOG(Config, Warning, << "Asserting on 3rd party source: " << source.Name); 210 context.Reporter.Warn() << Resource::String::WinGetResourceUnitThirdPartySourceAssertion(Utility::LocIndView{ source.Name }) << std::endl; 211 foundIssues = true; 212 } 213 else if (unitIntent == ConfigurationUnitIntent::Apply) 214 { 215 // Add to dependency source map so it can be validated with later WinGetPackage source 216 m_dependenciesSourceAndUnitIdMap.emplace(Utility::FoldCase(std::string_view{ source.Name }), Utility::FoldCase(Utility::NormalizedString{ unit.Identifier() })); 217 } 218 } 219 } 220 else if (Utility::CaseInsensitiveEquals(UnitType_WinGetPackage, unitType)) 221 { 222 auto package = ParseWinGetPackageFromSettings(unit.Settings()); 223 if (package.Empty()) 224 { 225 AICLI_LOG(Config, Warning, << "Failed to parse WinGetPackage or empty content."); 226 context.Reporter.Warn() << Resource::String::WinGetResourceUnitEmptyContent(Utility::LocIndView{ UnitType_WinGetPackage }) << std::endl; 227 foundIssues = true; 228 } 229 // Validate basic semantics. 230 if (package.Id.empty()) 231 { 232 AICLI_LOG(Config, Error, << "WinGetPackage unit missing required arg: Id"); 233 context.Reporter.Error() << Resource::String::WinGetResourceUnitMissingRequiredArg(Utility::LocIndView{ UnitType_WinGetPackage }, "Id"_liv) << std::endl; 234 foundIssues = true; 235 } 236 if (package.Source.empty()) 237 { 238 AICLI_LOG(Config, Warning, << "WinGetPackage unit missing recommended arg: Source"); 239 context.Reporter.Warn() << Resource::String::WinGetResourceUnitMissingRecommendedArg(Utility::LocIndView{ UnitType_WinGetPackage }, "Source"_liv) << std::endl; 240 foundIssues = true; 241 } 242 if (package.UseLatest && !package.Version.empty()) 243 { 244 AICLI_LOG(Config, Warning, << "WinGetPackage unit both UseLatest and Version declared. Package: " << package.Id); 245 context.Reporter.Warn() << Resource::String::WinGetResourceUnitBothPackageVersionAndUseLatest(Utility::LocIndView{ package.Id }) << std::endl; 246 foundIssues = true; 247 } 248 // Validate dependency source is configured. 249 if (!package.Source.empty() && !IsWellKnownSourceName(package.Source)) 250 { 251 if (unitIntent == ConfigurationUnitIntent::Assert) 252 { 253 AICLI_LOG(Config, Warning, << "Asserting on a package that depends on a 3rd party source. Package: " << package.Id << " Source: " << package.Source); 254 context.Reporter.Warn() << Resource::String::WinGetResourceUnitThirdPartySourceAssertionForPackage(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl; 255 foundIssues = true; 256 } 257 else 258 { 259 auto dependencySourceItr = m_dependenciesSourceAndUnitIdMap.find(Utility::FoldCase(std::string_view{ package.Source })); 260 if (dependencySourceItr == m_dependenciesSourceAndUnitIdMap.end()) 261 { 262 AICLI_LOG(Config, Warning, << "WinGetPackage depends on a 3rd party source not previously configured. Package: " << package.Id << " Source: " << package.Source); 263 context.Reporter.Warn() << Resource::String::WinGetResourceUnitDependencySourceNotConfigured(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl; 264 foundIssues = true; 265 } 266 else 267 { 268 bool foundInUnitDependencies = false; 269 for (auto const& entry : unit.Dependencies()) 270 { 271 // The map contains normalized string, so just use direct comparison; 272 if (dependencySourceItr->second == Utility::FoldCase(Utility::NormalizedString{ entry })) 273 { 274 foundInUnitDependencies = true; 275 break; 276 } 277 } 278 if (!foundInUnitDependencies) 279 { 280 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); 281 context.Reporter.Warn() << Resource::String::WinGetResourceUnitDependencySourceNotDeclaredAsDependency(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl; 282 foundIssues = true; 283 } 284 } 285 } 286 } 287 // Validate package is found and version available. 288 try 289 { 290 Repository::Source source{ package.Source }; 291 if (!source) 292 { 293 AICLI_LOG(Config, Warning, << "Failed to open WinGet source. Package: " << package.Id << " Source: " << package.Source); 294 context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageSourceOpenFailed(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Source }) << std::endl; 295 foundIssues = true; 296 } 297 else 298 { 299 source.SetCaller("winget-cli-configuration-unit-module-validation"); 300 ProgressCallback empty; 301 source.Open(empty); 302 Repository::SearchRequest searchRequest; 303 searchRequest.Filters.emplace_back(Repository::PackageMatchFilter{ Repository::PackageMatchField::Id, Repository::MatchType::CaseInsensitive, package.Id }); 304 auto searchResult = source.Search(searchRequest); 305 if (searchResult.Matches.size() == 0) 306 { 307 AICLI_LOG(Config, Warning, << "WinGetPackage not found: " << package.Id); 308 context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageNotFound(Utility::LocIndView{ package.Id }) << std::endl; 309 foundIssues = true; 310 } 311 else if (searchResult.Matches.size() > 1) 312 { 313 AICLI_LOG(Config, Warning, << "More than one WinGetPackage found: " << package.Id); 314 context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageMultipleFound(Utility::LocIndView{ package.Id }) << std::endl; 315 foundIssues = true; 316 } 317 else 318 { 319 if (!package.Version.empty()) 320 { 321 std::shared_ptr<Repository::IPackage> availablePackage = searchResult.Matches.at(0).Package->GetAvailable().at(0); 322 auto versionKeys = availablePackage->GetVersionKeys(); 323 bool foundVersion = false; 324 for (auto const& versionKey : versionKeys) 325 { 326 if (versionKey.Version == Utility::NormalizedString(package.Version)) 327 { 328 foundVersion = true; 329 break; 330 } 331 } 332 if (!foundVersion) 333 { 334 AICLI_LOG(Config, Warning, << "WinGetPackage version not found. Package: " << package.Id << " Version: " << package.Version); 335 context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackageVersionNotFound(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Version }) << std::endl; 336 foundIssues = true; 337 } 338 if (versionKeys.size() == 1) 339 { 340 AICLI_LOG(Config, Warning, << "WinGetPackage version specified with only one version available: " << package.Id); 341 context.Reporter.Warn() << Resource::String::WinGetResourceUnitPackageVersionSpecifiedWithOnlyOnePackageVersion(Utility::LocIndView{ package.Id }, Utility::LocIndView{ package.Version }) << std::endl; 342 foundIssues = true; 343 } 344 } 345 } 346 } 347 } 348 catch (...) 349 { 350 AICLI_LOG(Config, Warning, << "Failed to validate WinGetPackage: " << package.Id); 351 context.Reporter.Warn() << Resource::String::WinGetResourceUnitFailedToValidatePackage(Utility::LocIndView{ package.Id }) << std::endl; 352 foundIssues = true; 353 } 354 } 355 356 return !foundIssues; 357 } 358 }