diff --git a/src/chocolatey/StringExtensions.cs b/src/chocolatey/StringExtensions.cs
index 4b830da846..5da1455870 100644
--- a/src/chocolatey/StringExtensions.cs
+++ b/src/chocolatey/StringExtensions.cs
@@ -17,10 +17,15 @@
namespace chocolatey
{
using System;
+ using System.Collections.Generic;
using System.Globalization;
+ using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
+ using System.Text;
using System.Text.RegularExpressions;
+ using System.Threading;
+ using System.Web.UI;
using infrastructure.app;
using infrastructure.logging;
@@ -50,6 +55,81 @@ public static string FormatWith(this string input, params object[] formatting)
}
}
+ ///
+ /// Splits any Newline elements and ensures that each line is no longer than the configured .
+ /// Lines longer than the specified line length will be split on the last non-letter or digit before the max length.
+ ///
+ /// The input to split any lines on.
+ /// The line prefix used for all lines not being the first line.
+ /// Maximum length of the line.
+ /// The splitted formatted lines.
+ /// Not recommended to be used in hot paths.
+ public static string SplitOnSpace(this string input, string linePrefix = "", int maxLineLength = 70)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ {
+ return string.Empty;
+ }
+
+ var sb = new StringBuilder(input.Length);
+ var firstLine = true;
+ var stack = new Stack(input.Split('\n').Reverse());
+
+ while (stack.Count > 0)
+ {
+ var currentLine = stack.Pop();
+
+ if (currentLine.Length <= maxLineLength)
+ {
+ if (!firstLine && !string.IsNullOrEmpty(currentLine))
+ {
+ sb.Append(linePrefix);
+ }
+
+ sb.AppendLine(currentLine.TrimEnd());
+ }
+ else
+ {
+ var index = 70 - 1;
+
+ for (; index >= 0; index--)
+ {
+ if (char.IsWhiteSpace(currentLine[index]) || !char.IsLetterOrDigit(currentLine[index]))
+ {
+ break;
+ }
+ }
+
+ if (index <= 0)
+ {
+ index = maxLineLength;
+ }
+
+ if (!firstLine)
+ {
+ sb.Append(linePrefix);
+ }
+
+ var subLine = currentLine.Substring(0, index);
+ sb.AppendLine(subLine.TrimEnd());
+
+ if (stack.Count > 0)
+ {
+ var nextLine = currentLine.Substring(index + 1).TrimStart() + stack.Pop();
+ stack.Push(nextLine);
+ }
+ else
+ {
+ stack.Push(currentLine.Substring(index + 1).TrimStart());
+ }
+ }
+
+ firstLine = false;
+ }
+
+ return sb.ToString();
+ }
+
///
/// Performs a trim only if the item is not null
///
diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj
index c4b9ab931a..cbc7c14524 100644
--- a/src/chocolatey/chocolatey.csproj
+++ b/src/chocolatey/chocolatey.csproj
@@ -1,4 +1,4 @@
-
+
@@ -241,6 +241,7 @@
+
diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs
index 122e6ddfa9..4f28197ce0 100644
--- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs
+++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs
@@ -74,6 +74,9 @@ public static class ApplicationParameters
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile, System.Environment.SpecialFolderOption.DoNotVerify)
: CommonAppDataChocolatey;
public static readonly string HttpCacheUserLocation = _fileSystem.CombinePaths(UserProfilePath, ".chocolatey", "http-cache");
+ // CommonAppDataChocolatey is always set to ProgramData\Chocolatey.
+ // So we append HttpCache to that name if it is possible.
+ public static readonly string HttpCacheSystemLocation = CommonAppDataChocolatey + "HttpCache";
public static readonly string HttpCacheLocation = GetHttpCacheLocation();
public static readonly string UserLicenseFileLocation = _fileSystem.CombinePaths(UserProfilePath, "chocolatey.license.xml");
@@ -112,9 +115,7 @@ private static string GetHttpCacheLocation()
{
if (ProcessInformation.IsElevated() || string.IsNullOrEmpty(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile, System.Environment.SpecialFolderOption.DoNotVerify)))
{
- // CommonAppDataChocolatey is always set to ProgramData\Chocolatey.
- // So we append HttpCache to that name if it is possible.
- return CommonAppDataChocolatey + "HttpCache";
+ return HttpCacheSystemLocation;
}
else
{
diff --git a/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs b/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs
index 334b8aa580..dc48004502 100644
--- a/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs
+++ b/src/chocolatey/infrastructure.app/registration/ChocolateyRegistrationModule.cs
@@ -82,7 +82,8 @@ public void RegisterDependencies(IContainerRegistrator registrator, ChocolateyCo
registrator.RegisterService(
typeof(GlobalConfigurationValidation),
- typeof(SystemStateValidation));
+ typeof(SystemStateValidation),
+ typeof(CacheFolderLockdownValidation));
// Rule registrations
registrator.RegisterService();
diff --git a/src/chocolatey/infrastructure.app/validations/CacheFolderValidationLockdown.cs b/src/chocolatey/infrastructure.app/validations/CacheFolderValidationLockdown.cs
new file mode 100644
index 0000000000..17cf157a6b
--- /dev/null
+++ b/src/chocolatey/infrastructure.app/validations/CacheFolderValidationLockdown.cs
@@ -0,0 +1,127 @@
+// Copyright © 2017 - 2023 Chocolatey Software, Inc
+// Copyright © 2011 - 2017 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.validations
+{
+ using System;
+ using System.Collections.Generic;
+ using chocolatey.infrastructure.app.configuration;
+ using chocolatey.infrastructure.filesystem;
+ using chocolatey.infrastructure.information;
+ using chocolatey.infrastructure.validations;
+
+ public sealed class CacheFolderLockdownValidation : IValidation
+ {
+ private readonly IFileSystem _fileSystem;
+
+ public CacheFolderLockdownValidation(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ public ICollection Validate(ChocolateyConfiguration config)
+ {
+ this.Log().Debug("Cache Folder Lockdown Checks:");
+
+ var result = new List();
+
+ if (!ProcessInformation.IsElevated() && !string.IsNullOrEmpty(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify)))
+ {
+ this.Log().Debug(" - Elevated State = Failed");
+
+ result.Add(new ValidationResult
+ {
+ ExitCode = 0,
+ Message = "User Cache directory is valid.",
+ Status = ValidationStatus.Success
+ });
+
+ return result;
+ }
+
+ this.Log().Debug(" - Elevated State = Checked");
+
+ var cacheFolderPath = ApplicationParameters.HttpCacheLocation;
+
+ if (_fileSystem.DirectoryExists(cacheFolderPath))
+ {
+ this.Log().Debug(" - Folder Exists = Checked");
+
+ if (_fileSystem.IsLockedDirectory(cacheFolderPath))
+ {
+ this.Log().Debug(" - Folder lockdown = Checked");
+
+ result.Add(new ValidationResult
+ {
+ ExitCode = 0,
+ Message = "System Cache directory is locked down to administrators.",
+ Status = ValidationStatus.Success
+ });
+ }
+ else
+ {
+ this.Log().Debug(" - Folder lockdown = Failed");
+
+ result.Add(new ValidationResult
+ {
+ ExitCode = 0,
+ Message = "System Cache directory is not locked down to administrators.\nRemove the directory '{0}' to have Chocolatey CLI create it with the proper permissions.".FormatWith(cacheFolderPath).SplitOnSpace(linePrefix: " "),
+ Status = ValidationStatus.Warning
+ });
+ }
+
+ return result;
+ }
+
+ this.Log().Debug(" - Folder Exists = Failed");
+
+ if (_fileSystem.LockDirectory(cacheFolderPath))
+ {
+ this.Log().Debug(" - Folder lockdown update = Success");
+
+ result.Add(new ValidationResult
+ {
+ ExitCode = 0,
+ Message = "System Cache directory successfullly created and locked down to administrators.",
+ Status = ValidationStatus.Success,
+ });
+ }
+ else
+ {
+ this.Log().Debug(" - Folder lockdown update = Failed");
+
+ result.Add(new ValidationResult
+ {
+ ExitCode = 1, // Should we error?
+ Message = "System Cache directory was not created, or could not be locked down to administrators.".SplitOnSpace(linePrefix: " "),
+ Status = ValidationStatus.Error
+ });
+ }
+
+ return result;
+ }
+
+#pragma warning disable IDE1006
+
+ [Obsolete("This overload is deprecated and will be removed in v3.")]
+ public ICollection validate(ChocolateyConfiguration config)
+ {
+ return Validate(config);
+ }
+
+#pragma warning restore IDE1006
+ }
+}
\ No newline at end of file
diff --git a/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs b/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
index 410b765340..048cba2866 100644
--- a/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
+++ b/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
@@ -23,6 +23,8 @@ namespace chocolatey.infrastructure.filesystem
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+ using System.Security.AccessControl;
+ using System.Security.Principal;
using System.Text;
using System.Threading;
using adapters;
@@ -646,19 +648,7 @@ public dynamic GetFileDirectoryInfo(string filePath)
public void CreateDirectory(string directoryPath)
{
- this.Log().Debug(ChocolateyLoggers.Verbose, () => "Attempting to create directory \"{0}\".".FormatWith(GetFullPath(directoryPath)));
- AllowRetries(
- () =>
- {
- try
- {
- Directory.CreateDirectory(directoryPath);
- }
- catch (IOException)
- {
- Alphaleonis.Win32.Filesystem.Directory.CreateDirectory(directoryPath);
- }
- });
+ CreateDirectory(directoryPath, isSilent: false);
}
public void MoveDirectory(string directoryPath, string newDirectoryPath)
@@ -762,19 +752,139 @@ public void EnsureDirectoryExists(string directoryPath)
EnsureDirectoryExists(directoryPath, false);
}
- private void EnsureDirectoryExists(string directoryPath, bool ignoreError)
+ public bool IsLockedDirectory(string directoryPath)
+ {
+ try
+ {
+ var permissions = Directory.GetAccessControl(directoryPath);
+
+ var rules = permissions.GetAccessRules(includeExplicit: true, includeInherited: true, typeof(NTAccount));
+ var builtinAdmins = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount));
+ var localSystem = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null).Translate(typeof(NTAccount));
+
+ foreach (FileSystemAccessRule rule in rules)
+ {
+ if (rule.IdentityReference != builtinAdmins && rule.IdentityReference != localSystem &&
+ AllowsAnyFlag(rule, FileSystemRights.CreateFiles, FileSystemRights.AppendData, FileSystemRights.WriteExtendedAttributes, FileSystemRights.WriteAttributes))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this.Log().Debug("Unable to read directory '{0}'.", directoryPath);
+ this.Log().Debug("Exception: {0}", ex.Message);
+
+ // If we do not have access, we assume the directory is locked.
+ return true;
+ }
+ }
+
+ public bool LockDirectory(string directoryPath)
+ {
+ try
+ {
+ EnsureDirectoryExists(directoryPath, ignoreError: false, isSilent: true);
+
+ this.Log().Debug(" - Folder Created = Success");
+
+ var permissions = Directory.GetAccessControl(directoryPath);
+
+ var rules = permissions.GetAccessRules(includeExplicit: true, includeInherited: true, typeof(NTAccount));
+
+ // We first need to remove all rules
+ foreach (FileSystemAccessRule rule in rules)
+ {
+ permissions.RemoveAccessRuleAll(rule);
+ }
+
+ this.Log().Debug(" - Pending normal access removed = Checked");
+
+ var bultinAdmins = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount));
+ var localsystem = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null).Translate(typeof(NTAccount));
+ var builtinUsers = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null).Translate(typeof(NTAccount));
+
+ var inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
+
+ permissions.SetAccessRule(new FileSystemAccessRule(bultinAdmins, FileSystemRights.FullControl, inheritanceFlags, PropagationFlags.None, AccessControlType.Allow));
+ permissions.SetAccessRule(new FileSystemAccessRule(localsystem, FileSystemRights.FullControl, inheritanceFlags, PropagationFlags.None, AccessControlType.Allow));
+ permissions.SetAccessRule(new FileSystemAccessRule(builtinUsers, FileSystemRights.ReadAndExecute, inheritanceFlags, PropagationFlags.None, AccessControlType.Allow));
+ this.Log().Debug(" - Pending write permissions for Administrators = Checked");
+
+ permissions.SetOwner(bultinAdmins);
+ this.Log().Debug(" - Pending Administrators as Owners = Checked");
+
+ permissions.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
+ this.Log().Debug(" - Pending removing inheritance with no copy = Checked");
+
+ Directory.SetAccessControl(directoryPath, permissions);
+ this.Log().Debug(" - Access Permissions updated = Success");
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this.Log().Debug(" - Access Permissions updated = Failure");
+ this.Log().Debug("Exception: {0}", ex.Message);
+
+ return false;
+ }
+ }
+
+ private bool AllowsAnyFlag(FileSystemAccessRule rule, params FileSystemRights[] flags)
+ {
+ foreach (var flag in flags)
+ {
+ if (rule.AccessControlType == AccessControlType.Allow && rule.FileSystemRights.HasFlag(flag))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void CreateDirectory(string directoryPath, bool isSilent)
+ {
+ if (!isSilent)
+ {
+ this.Log().Debug(ChocolateyLoggers.Verbose, () => "Attempting to create directory \"{0}\".".FormatWith(GetFullPath(directoryPath)));
+ }
+
+ AllowRetries(
+ () =>
+ {
+ try
+ {
+ Directory.CreateDirectory(directoryPath);
+ }
+ catch (IOException)
+ {
+ Alphaleonis.Win32.Filesystem.Directory.CreateDirectory(directoryPath);
+ }
+ }, isSilent: isSilent);
+ }
+
+ private void EnsureDirectoryExists(string directoryPath, bool ignoreError, bool isSilent = false)
{
if (!DirectoryExists(directoryPath))
{
try
{
- CreateDirectory(directoryPath);
+ CreateDirectory(directoryPath, isSilent);
}
catch (SystemException e)
{
if (!ignoreError)
{
- this.Log().Error("Cannot create directory \"{0}\". Error was:{1}{2}", GetFullPath(directoryPath), Environment.NewLine, e);
+ if (!isSilent)
+ {
+ this.Log().Error("Cannot create directory \"{0}\". Error was:{1}{2}", GetFullPath(directoryPath), Environment.NewLine, e);
+ }
+
throw;
}
}
diff --git a/src/chocolatey/infrastructure/filesystem/IFileSystem.cs b/src/chocolatey/infrastructure/filesystem/IFileSystem.cs
index 661cb51754..34c36c8de3 100644
--- a/src/chocolatey/infrastructure/filesystem/IFileSystem.cs
+++ b/src/chocolatey/infrastructure/filesystem/IFileSystem.cs
@@ -444,6 +444,10 @@ public interface IFileSystem
/// Should this method be silent? false by default
void DeleteDirectoryChecked(string directoryPath, bool recursive, bool overrideAttributes, bool isSilent);
+ bool IsLockedDirectory(string directoryPath);
+
+ bool LockDirectory(string directoryPath);
+
#endregion
///