Skip to content

Commit

Permalink
(GH-563) Warn/Log Environment Variable Changes
Browse files Browse the repository at this point in the history
Detect environment variable changes during install/upgrade and
uninstall. Log all of the changes. Do not log the actual environment
variable values by default as they could contain sensitive data.
Instead inform the user about the need to flip a feature if they want
the value changes logged with a warning that it could disclose
sensitive data.

Also provide SET type information if the feature is turned on.

When environment variables have changed, inform the user they need to
take action to see the changes as Windows doesn't automatically update
command shells like PowerShell.exe and cmd.exe. Inform cmd.exe users
that they can simply type a single command to get their environment
updated.
  • Loading branch information
ferventcoder committed Jan 21, 2016
1 parent 79353e7 commit fe640fb
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 22 deletions.
3 changes: 3 additions & 0 deletions src/chocolatey/chocolatey.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
<Link>Properties\SolutionVersion.cs</Link>
</Compile>
<Compile Include="AssemblyExtensions.cs" />
<Compile Include="infrastructure.app\domain\GenericRegistryKey.cs" />
<Compile Include="infrastructure.app\domain\GenericRegistryValue.cs" />
<Compile Include="infrastructure.app\domain\RegistryValueKindType.cs" />
<Compile Include="infrastructure\commandline\ReadKeyTimeout.cs" />
<Compile Include="infrastructure\commands\Execute.cs" />
<Compile Include="GetChocolatey.cs" />
Expand Down
1 change: 1 addition & 0 deletions src/chocolatey/infrastructure.app/ApplicationParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions src/chocolatey/infrastructure.app/domain/GenericRegistryKey.cs
Original file line number Diff line number Diff line change
@@ -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<GenericRegistryKey> Keys { get; set; }
public IEnumerable<GenericRegistryValue> Values { get; set; }
public RegistryView View { get; set; }
}
}
53 changes: 53 additions & 0 deletions src/chocolatey/infrastructure.app/domain/GenericRegistryValue.cs
Original file line number Diff line number Diff line change
@@ -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<GenericRegistryValue>
{
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<GenericRegistryValue>.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())
;
}


}
}
29 changes: 29 additions & 0 deletions src/chocolatey/infrastructure.app/domain/RegistryValueKindType.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<GenericRegistryValue> environmentChanges;
IEnumerable<GenericRegistryValue> 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);

Expand Down Expand Up @@ -322,9 +328,11 @@ public ConcurrentDictionary<string, PackageResult> 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<PackageResult> action = null;
Expand Down Expand Up @@ -536,6 +544,8 @@ public ConcurrentDictionary<string, PackageResult> 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);
Expand Down Expand Up @@ -603,8 +613,14 @@ public ConcurrentDictionary<string, PackageResult> 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<GenericRegistryValue> environmentChanges;
IEnumerable<GenericRegistryValue> 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,
Expand Down Expand Up @@ -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;

Expand All @@ -832,5 +848,79 @@ private void remove_rollback_if_exists(PackageResult packageResult)
{
_nugetService.remove_rollback_directory_if_exists(packageResult.Name);
}

private IEnumerable<GenericRegistryValue> get_environment_before(ChocolateyConfiguration config, bool allowLogging = true)
{
if (config.Information.PlatformType != PlatformType.Windows) return Enumerable.Empty<GenericRegistryValue>();
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<GenericRegistryValue> environmentBefore, out IEnumerable<GenericRegistryValue> environmentChanges, out IEnumerable<GenericRegistryValue> environmentRemovals)
{
if (config.Information.PlatformType != PlatformType.Windows)
{
environmentChanges = Enumerable.Empty<GenericRegistryValue>();
environmentRemovals = Enumerable.Empty<GenericRegistryValue>();

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"
));
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenericRegistryValue> get_environment_values();
IEnumerable<GenericRegistryValue> get_added_changed_environment_differences(IEnumerable<GenericRegistryValue> before, IEnumerable<GenericRegistryValue> after);
IEnumerable<GenericRegistryValue> get_removed_environment_differences(IEnumerable<GenericRegistryValue> before, IEnumerable<GenericRegistryValue> after);
void save_to_file(Registry snapshot, string filePath);
Registry read_from_file(string filePath);
bool installer_value_exists(string keyPath, string value);
Expand Down
Loading

0 comments on commit fe640fb

Please sign in to comment.