diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index e648b29052..9cb61a21b2 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -82,6 +82,9 @@ Properties\SolutionVersion.cs + + + diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index 2435440b31..a80e7dff8a 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -102,6 +102,7 @@ public static class Features public static readonly string AllowGlobalConfirmation = "allowGlobalConfirmation"; public static readonly string FailOnStandardError = "failOnStandardError"; public static readonly string UsePowerShellHost = "powershellHost"; + public static readonly string LogEnvironmentValues = "logEnvironmentValues"; } public static class Messages diff --git a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs index 9a8177189b..f279805b19 100644 --- a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs +++ b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs @@ -189,6 +189,7 @@ private static void set_feature_flags(ChocolateyConfiguration config, ConfigFile config.Features.FailOnAutoUninstaller = set_feature_flag(ApplicationParameters.Features.FailOnAutoUninstaller, configFileSettings, defaultEnabled: false, description: "Fail if automatic uninstaller fails."); config.Features.FailOnStandardError = set_feature_flag(ApplicationParameters.Features.FailOnStandardError, configFileSettings, defaultEnabled: false, description: "Fail if install provider writes to stderr."); config.Features.UsePowerShellHost = set_feature_flag(ApplicationParameters.Features.UsePowerShellHost, configFileSettings, defaultEnabled: true, description: "Use Chocolatey's built-in PowerShell host."); + config.Features.LogEnvironmentValues = set_feature_flag(ApplicationParameters.Features.LogEnvironmentValues, configFileSettings, defaultEnabled: false, description: "Log Environment Values - will log values of environment before and after install (could disclose sensitive data)."); config.PromptForConfirmation = !set_feature_flag(ApplicationParameters.Features.AllowGlobalConfirmation, configFileSettings, defaultEnabled: false, description: "Prompt for confirmation in scripts or bypass."); } diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index 495c70ce95..d13a1b1489 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -337,6 +337,7 @@ public sealed class FeaturesConfiguration public bool FailOnAutoUninstaller { get; set; } public bool FailOnStandardError { get; set; } public bool UsePowerShellHost { get; set; } + public bool LogEnvironmentValues { get; set; } } //todo: retrofit other command configs this way diff --git a/src/chocolatey/infrastructure.app/domain/GenericRegistryKey.cs b/src/chocolatey/infrastructure.app/domain/GenericRegistryKey.cs new file mode 100644 index 0000000000..c42c96f03a --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/GenericRegistryKey.cs @@ -0,0 +1,28 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + using System.Collections.Generic; + using Microsoft.Win32; + + public class GenericRegistryKey + { + public string Name { get; set; } + public IEnumerable Keys { get; set; } + public IEnumerable Values { get; set; } + public RegistryView View { get; set; } + } +} diff --git a/src/chocolatey/infrastructure.app/domain/GenericRegistryValue.cs b/src/chocolatey/infrastructure.app/domain/GenericRegistryValue.cs new file mode 100644 index 0000000000..5c54c5c385 --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/GenericRegistryValue.cs @@ -0,0 +1,53 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + using System; + + public class GenericRegistryValue : IEquatable + { + public string ParentKeyName { get; set; } + public string Name { get; set; } + public string Value { get; set; } + public RegistryValueKindType Type { get; set; } + + public override int GetHashCode() + { + return ParentKeyName.GetHashCode() + & Name.GetHashCode() + & Value.GetHashCode() + & Type.GetHashCode(); + } + + public override bool Equals(object obj) + { + return Equals(obj as GenericRegistryValue); + } + + bool IEquatable.Equals(GenericRegistryValue other) + { + if (other == null) return false; + + return ParentKeyName.is_equal_to(other.ParentKeyName) + && Name.is_equal_to(other.Name) + && Value.is_equal_to(other.Value) + && Type.to_string().is_equal_to(other.Type.to_string()) + ; + } + + + } +} diff --git a/src/chocolatey/infrastructure.app/domain/RegistryValueKindType.cs b/src/chocolatey/infrastructure.app/domain/RegistryValueKindType.cs new file mode 100644 index 0000000000..aa8479a078 --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/RegistryValueKindType.cs @@ -0,0 +1,29 @@ +// Copyright © 2011 - Present RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + public enum RegistryValueKindType + { + None = -1, + Unknown = 0, + String = 1, + ExpandString = 2, + Binary = 3, + DWord = 4, + MultiString = 7, + QWord = 11, + } +} diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index f62b4616b4..0809f65bd6 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -249,7 +249,8 @@ public void handle_package_result(PackageResult packageResult, ChocolateyConfigu { if (!config.SkipPackageInstallProvider) { - var before = _registryService.get_installer_keys(); + var installersBefore = _registryService.get_installer_keys(); + var environmentBefore = get_environment_before(config, allowLogging: false); var powerShellRan = _powershellService.install(config, packageResult); if (powerShellRan) @@ -258,22 +259,27 @@ public void handle_package_result(PackageResult packageResult, ChocolateyConfigu if (config.Information.PlatformType == PlatformType.Windows) CommandExecutor.execute_static("shutdown", "/a", config.CommandExecutionTimeoutSeconds, _fileSystem.get_current_directory(), (s, e) => { }, (s, e) => { }, false, false); } - var difference = _registryService.get_differences(before, _registryService.get_installer_keys()); - if (difference.RegistryKeys.Count != 0) + var installersDifferences = _registryService.get_installer_key_differences(installersBefore, _registryService.get_installer_keys()); + if (installersDifferences.RegistryKeys.Count != 0) { //todo v1 - determine the installer type and write it to the snapshot //todo v1 - note keys passed in - pkgInfo.RegistrySnapshot = difference; + pkgInfo.RegistrySnapshot = installersDifferences; - var key = difference.RegistryKeys.FirstOrDefault(); + var key = installersDifferences.RegistryKeys.FirstOrDefault(); if (key != null && key.HasQuietUninstall) { pkgInfo.HasSilentUninstall = true; } } + + IEnumerable environmentChanges; + IEnumerable environmentRemovals; + get_environment_after(config, environmentBefore, out environmentChanges, out environmentRemovals); + //todo: record this with package info } - _filesService.ensure_compatible_file_attributes(packageResult,config); + _filesService.ensure_compatible_file_attributes(packageResult, config); _configTransformService.run(packageResult, config); pkgInfo.FilesSnapshot = _filesService.capture_package_files(packageResult, config); @@ -322,9 +328,11 @@ public ConcurrentDictionary install_run(ChocolateyConfigu Environment.ExitCode = 1; return packageInstalls; } - + this.Log().Info(@"By installing you accept licenses for the packages."); + get_environment_before(config, allowLogging: true); + foreach (var packageConfig in set_config_from_package_names_and_packages_config(config, packageInstalls).or_empty_list_if_null()) { Action action = null; @@ -536,6 +544,8 @@ public ConcurrentDictionary upgrade_run(ChocolateyConfigu action = (packageResult) => handle_package_result(packageResult, config, CommandNameType.upgrade); } + get_environment_before(config, allowLogging: true); + var packageUpgrades = perform_source_runner_function(config, r => r.upgrade_run(config, action)); var upgradeFailures = packageUpgrades.Count(p => !p.Value.Success); @@ -603,8 +613,14 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi action = (packageResult) => handle_package_uninstall(packageResult, config); } + var environmentBefore = get_environment_before(config); + var packageUninstalls = perform_source_runner_function(config, r => r.uninstall_run(config, action)); + IEnumerable environmentChanges; + IEnumerable environmentRemovals; + get_environment_after(config, environmentBefore, out environmentChanges, out environmentRemovals); + var uninstallFailures = packageUninstalls.Count(p => !p.Value.Success); this.Log().Warn(() => @"{0}{1} uninstalled {2}/{3} packages. {4} packages failed.{0} See the log for details ({5}).".format_with( Environment.NewLine, @@ -807,7 +823,7 @@ private void rollback_previous_version(ChocolateyConfiguration config, PackageRe } rollbackDirectory = _fileSystem.get_full_path(rollbackDirectory); - + if (string.IsNullOrWhiteSpace(rollbackDirectory) || !_fileSystem.directory_exists(rollbackDirectory)) return; if (!rollbackDirectory.StartsWith(ApplicationParameters.PackageBackupLocation) || rollbackDirectory.is_equal_to(ApplicationParameters.PackageBackupLocation)) return; @@ -832,5 +848,79 @@ private void remove_rollback_if_exists(PackageResult packageResult) { _nugetService.remove_rollback_directory_if_exists(packageResult.Name); } + + private IEnumerable get_environment_before(ChocolateyConfiguration config, bool allowLogging = true) + { + if (config.Information.PlatformType != PlatformType.Windows) return Enumerable.Empty(); + var environmentBefore = _registryService.get_environment_values(); + + if (allowLogging && config.Features.LogEnvironmentValues) + { + this.Log().Debug("Current environment values (may contain sensitive data):"); + foreach (var environmentValue in environmentBefore.or_empty_list_if_null()) + { + this.Log().Debug(@" * '{0}'='{1}' ('{2}')".format_with( + environmentValue.Name.escape_curly_braces(), + environmentValue.Value.escape_curly_braces(), + environmentValue.ParentKeyName.to_lower().Contains("hkey_current_user") ? "User" : "Machine")); + } + } + return environmentBefore; + } + + private void get_environment_after(ChocolateyConfiguration config, IEnumerable environmentBefore, out IEnumerable environmentChanges, out IEnumerable environmentRemovals) + { + if (config.Information.PlatformType != PlatformType.Windows) + { + environmentChanges = Enumerable.Empty(); + environmentRemovals = Enumerable.Empty(); + + return; + } + + var environmentAfer = _registryService.get_environment_values(); + environmentChanges = _registryService.get_added_changed_environment_differences(environmentBefore, environmentAfer); + environmentRemovals = _registryService.get_removed_environment_differences(environmentBefore, environmentAfer); + var hasEnvironmentChanges = environmentChanges.Count() != 0; + var hasEnvironmentRemovals = environmentRemovals.Count() != 0; + if (hasEnvironmentChanges || hasEnvironmentRemovals) + { + this.Log().Info(ChocolateyLoggers.Important,@"Environment Vars (like PATH) have changed. Close/reopen your shell to + see the changes (or in cmd.exe just type `refreshenv`)."); + + if (!config.Features.LogEnvironmentValues) + { + this.Log().Debug(@"Logging of values is not turned on by default because it + could potentially expose sensitive data. If you understand the risk, + please see `choco feature -h` for information to turn it on."); + } + + if (hasEnvironmentChanges) + { + this.Log().Debug(@"The following values have been added/changed (may contain sensitive data):"); + foreach (var difference in environmentChanges.or_empty_list_if_null()) + { + this.Log().Debug(@" * {0}='{1}' ({2})".format_with( + difference.Name.escape_curly_braces(), + config.Features.LogEnvironmentValues ? difference.Value.escape_curly_braces() : "[REDACTED]", + difference.ParentKeyName.to_lower().Contains("hkey_current_user") ? "User" : "Machine" + )); + } + } + + if (hasEnvironmentRemovals) + { + this.Log().Debug(@"The following values have been removed:"); + foreach (var difference in environmentRemovals.or_empty_list_if_null()) + { + this.Log().Debug(@" * {0}='{1}' ({2})".format_with( + difference.Name.escape_curly_braces(), + config.Features.LogEnvironmentValues ? difference.Value.escape_curly_braces() : "[REDACTED]", + difference.ParentKeyName.to_lower().Contains("hkey_current_user") ? "User": "Machine" + )); + } + } + } + } } } diff --git a/src/chocolatey/infrastructure.app/services/IRegistryService.cs b/src/chocolatey/infrastructure.app/services/IRegistryService.cs index 47adab543a..f84449c1de 100644 --- a/src/chocolatey/infrastructure.app/services/IRegistryService.cs +++ b/src/chocolatey/infrastructure.app/services/IRegistryService.cs @@ -15,13 +15,18 @@ namespace chocolatey.infrastructure.app.services { + using System.Collections.Generic; using Microsoft.Win32; + using domain; using Registry = domain.Registry; public interface IRegistryService { Registry get_installer_keys(); - Registry get_differences(Registry before, Registry after); + Registry get_installer_key_differences(Registry before, Registry after); + IEnumerable get_environment_values(); + IEnumerable get_added_changed_environment_differences(IEnumerable before, IEnumerable after); + IEnumerable get_removed_environment_differences(IEnumerable before, IEnumerable after); void save_to_file(Registry snapshot, string filePath); Registry read_from_file(string filePath); bool installer_value_exists(string keyPath, string value); diff --git a/src/chocolatey/infrastructure.app/services/RegistryService.cs b/src/chocolatey/infrastructure.app/services/RegistryService.cs index 226685633d..56ba7d16dd 100644 --- a/src/chocolatey/infrastructure.app/services/RegistryService.cs +++ b/src/chocolatey/infrastructure.app/services/RegistryService.cs @@ -16,6 +16,7 @@ namespace chocolatey.infrastructure.app.services { using System; + using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -37,20 +38,30 @@ public sealed class RegistryService : IRegistryService private readonly IFileSystem _fileSystem; private readonly bool _logOutput = false; + private const string UNINSTALLER_KEY_NAME = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + private const string USER_ENVIRONMENT_REGISTRY_KEY_NAME = "Environment"; + private const string MACHINE_ENVIRONMENT_REGISTRY_KEY_NAME = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; + public RegistryService(IXmlService xmlService, IFileSystem fileSystem) { _xmlService = xmlService; _fileSystem = fileSystem; } - private void add_key(IList keys, RegistryHive hive, RegistryView view) + private RegistryKey open_key(RegistryHive hive, RegistryView view) { - FaultTolerance.try_catch_with_logging_exception( - () => keys.Add(RegistryKey.OpenBaseKey(hive, view)), + return FaultTolerance.try_catch_with_logging_exception( + () => RegistryKey.OpenBaseKey(hive, view), "Could not open registry hive '{0}' for view '{1}'".format_with(hive.to_string(), view.to_string()), logWarningInsteadOfError: true); } + private void add_key(IList keys, RegistryHive hive, RegistryView view) + { + var key = open_key(hive, view); + if (key != null) keys.Add(key); + } + public Registry get_installer_keys() { var snapshot = new Registry(); @@ -70,7 +81,7 @@ public Registry get_installer_keys() foreach (var registryKey in keys) { var uninstallKey = FaultTolerance.try_catch_with_logging_exception( - () => registryKey.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), + () => registryKey.OpenSubKey(UNINSTALLER_KEY_NAME, RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), "Could not open uninstall subkey for key '{0}'".format_with(registryKey.Name), logWarningInsteadOfError: true); @@ -108,15 +119,15 @@ public void evaluate_keys(RegistryKey key, Registry snapshot) FaultTolerance.try_catch_with_logging_exception( () => + { + foreach (var subKeyName in key.GetSubKeyNames()) { - foreach (var subKeyName in key.GetSubKeyNames()) - { - FaultTolerance.try_catch_with_logging_exception( - () => evaluate_keys(key.OpenSubKey(subKeyName, RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), snapshot), - "Failed to open subkey named '{0}' for '{1}', likely due to permissions".format_with(subKeyName, key.Name), - logWarningInsteadOfError: true); - } - }, + FaultTolerance.try_catch_with_logging_exception( + () => evaluate_keys(key.OpenSubKey(subKeyName, RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), snapshot), + "Failed to open subkey named '{0}' for '{1}', likely due to permissions".format_with(subKeyName, key.Name), + logWarningInsteadOfError: true); + } + }, "Failed to open subkeys for '{0}', likely due to permissions".format_with(key.Name), logWarningInsteadOfError: true); @@ -227,7 +238,7 @@ public void evaluate_keys(RegistryKey key, Registry snapshot) key.Dispose(); } - public Registry get_differences(Registry before, Registry after) + public Registry get_installer_key_differences(Registry before, Registry after) { //var difference = after.RegistryKeys.Where(r => !before.RegistryKeys.Contains(r)).ToList(); return new Registry(after.User, after.RegistryKeys.Except(before.RegistryKeys).ToList()); @@ -253,6 +264,62 @@ public Registry read_from_file(string filePath) return _xmlService.deserialize(filePath); } + private void get_values(RegistryKey key, string subKeyName, IList values, bool expandValues) + { + if (key != null) + { + var subKey = FaultTolerance.try_catch_with_logging_exception( + () => key.OpenSubKey(subKeyName, RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), + "Could not open uninstall subkey for key '{0}'".format_with(key.Name), + logWarningInsteadOfError: true); + + if (subKey != null) + { + foreach (var valueName in subKey.GetValueNames()) + { + values.Add(new GenericRegistryValue + { + Name = valueName, + ParentKeyName = subKey.Name, + Type = (RegistryValueKindType)Enum.Parse(typeof(RegistryValueKindType), subKey.GetValueKind(valueName).to_string(), ignoreCase:true), + Value = subKey.GetValue(valueName, expandValues ? RegistryValueOptions.None : RegistryValueOptions.DoNotExpandEnvironmentNames).to_string(), + }); + } + } + } + } + + public IEnumerable get_environment_values() + { + IList environmentValues = new List(); + + get_values(open_key(RegistryHive.CurrentUser, RegistryView.Default), USER_ENVIRONMENT_REGISTRY_KEY_NAME, environmentValues, expandValues: false); + get_values(open_key(RegistryHive.LocalMachine, RegistryView.Default), MACHINE_ENVIRONMENT_REGISTRY_KEY_NAME, environmentValues, expandValues: false); + + return environmentValues; + } + + public IEnumerable get_added_changed_environment_differences(IEnumerable before, IEnumerable after) + { + return after.Except(before).ToList(); + } + + public IEnumerable get_removed_environment_differences(IEnumerable before, IEnumerable after) + { + var removals = new List(); + + foreach (var beforeValue in before.or_empty_list_if_null()) + { + var afterValue = after.FirstOrDefault(a => a.Name.is_equal_to(beforeValue.Name) && a.ParentKeyName.is_equal_to(beforeValue.ParentKeyName)); + if (afterValue == null) + { + removals.Add(beforeValue); + } + } + + return removals; + } + public RegistryKey get_key(RegistryHive hive, string subKeyPath) { IList keyLocations = new List(); @@ -278,4 +345,5 @@ public RegistryKey get_key(RegistryHive hive, string subKeyPath) return null; } } + } \ No newline at end of file