winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }