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