diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 17e9e7583..5f2a7b06f 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -520,9 +520,18 @@ private List InstallPackage( continue; } - if (!Utils.TryParsePSDataFile(moduleManifest, _cmdletPassedIn, out Hashtable parsedMetadataHashtable)) + if (!Utils.TryReadManifestFile( + manifestFilePath: moduleManifest, + manifestInfo: out Hashtable parsedMetadataHashtable, + error: out Exception manifestReadError)) { - // Ran into errors parsing the module manifest file which was found in Utils.ParseModuleManifest() and written. + WriteError( + new ErrorRecord( + exception: manifestReadError, + errorId: "ManifestFileReadParseError", + errorCategory: ErrorCategory.ReadError, + this)); + continue; } diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 98c64a346..cb9bb3510 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -320,18 +320,25 @@ protected override void ProcessRecord() Hashtable pkgsInFile = null; try { - if (_resourceFileType.Equals(ResourceFileType.JsonFile)) + switch (_resourceFileType) { - pkgsInFile = Utils.ConvertJsonToHashtable(this, requiredResourceFileStream); - } - else - { - // must be a .psd1 file - if (!Utils.TryParsePSDataFile(_requiredResourceFile, this, out pkgsInFile)) - { - // Ran into errors parsing the .psd1 file which was found in Utils.TryParsePSDataFile() and written. - return; - } + case ResourceFileType.JsonFile: + pkgsInFile = Utils.ConvertJsonToHashtable(this, requiredResourceFileStream); + break; + + case ResourceFileType.PSDataFile: + if (!Utils.TryReadRequiredResourceFile( + resourceFilePath: _requiredResourceFile, + out pkgsInFile, + out Exception error)) + { + throw error; + } + break; + + case ResourceFileType.UnknownFile: + throw new PSInvalidOperationException( + message: "Unkown file type. Required resource file must be either a json or psd1 data file."); } } catch (Exception) diff --git a/src/code/PublishPSResource.cs b/src/code/PublishPSResource.cs index c3c465e63..f10648451 100644 --- a/src/code/PublishPSResource.cs +++ b/src/code/PublishPSResource.cs @@ -477,12 +477,19 @@ private string CreateNuspec( // a module will still need the module manifest to be parsed. if (!isScript) { - // Parse the module manifest and *replace* the passed-in metadata with the module manifest metadata. - if (!Utils.TryParsePSDataFile( - moduleFileInfo: filePath, - cmdletPassedIn: this, - parsedMetadataHashtable: out parsedMetadataHash)) + // Use the parsed module manifest data as 'parsedMetadataHash' instead of the passed-in data. + if (!Utils.TryReadManifestFile( + manifestFilePath: filePath, + manifestInfo: out parsedMetadataHash, + error: out Exception manifestReadError)) { + WriteError( + new ErrorRecord( + exception: manifestReadError, + errorId: "ManifestFileReadParseForNuspecError", + errorCategory: ErrorCategory.ReadError, + this)); + return string.Empty; } } diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 9ab09b3ce..af3687a1f 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -25,7 +25,7 @@ internal static class Utils { #region String fields - public static readonly string[] EmptyStrArray = Array.Empty(); + public static readonly string[] EmptyStrArray = Array.Empty(); public const string PSDataFileExt = ".psd1"; private const string ConvertJsonToHashtableScript = @" param ( @@ -728,53 +728,94 @@ private static void GetStandardPlatformPaths( #endregion - #region Manifest methods + #region PSDataFile parsing - public static bool TryParsePSDataFile( - string moduleFileInfo, - PSCmdlet cmdletPassedIn, - out Hashtable parsedMetadataHashtable) + private static readonly string[] ManifestFileVariables = new string[] { "PSEdition", "PSScriptRoot" }; + + /// + /// Read psd1 manifest file contents and return as Hashtable object. + /// + /// File path to manfiest psd1 file. + /// Hashtable of manifest file contents. + /// Error exception on failure. + /// True on success. + public static bool TryReadManifestFile( + string manifestFilePath, + out Hashtable manifestInfo, + out Exception error) { - parsedMetadataHashtable = new Hashtable(); - bool successfullyParsed = false; + return TryReadPSDataFile( + filePath: manifestFilePath, + allowedVariables: ManifestFileVariables, + allowedCommands: Utils.EmptyStrArray, + allowEnvironmentVariables: false, + out manifestInfo, + out error); + } - // A script will already have the metadata parsed into the parsedMetadatahash, - // a module will still need the module manifest to be parsed. - if (moduleFileInfo.EndsWith(PSDataFileExt, StringComparison.OrdinalIgnoreCase)) - { - // Parse the module manifest - var ast = Parser.ParseFile( - moduleFileInfo, - out Token[] tokens, - out ParseError[] errors); + /// + /// Read psd1 required resource file contents and return as Hashtable object. + /// + /// File path to required resource psd1 file. + /// Hashtable of required resource file contents. + /// Error exception on failure. + /// True on success. + public static bool TryReadRequiredResourceFile( + string resourceFilePath, + out Hashtable resourceInfo, + out Exception error) + { + return TryReadPSDataFile( + filePath: resourceFilePath, + allowedVariables: Utils.EmptyStrArray, + allowedCommands: Utils.EmptyStrArray, + allowEnvironmentVariables: false, + out resourceInfo, + out error); + } - if (errors.Length > 0) + private static bool TryReadPSDataFile( + string filePath, + string[] allowedVariables, + string[] allowedCommands, + bool allowEnvironmentVariables, + out Hashtable dataFileInfo, + out Exception error) + { + try + { + if (filePath is null) { - var message = String.Format("Could not parse '{0}' as a PowerShell data file.", moduleFileInfo); - var ex = new ArgumentException(message); - var psdataParseError = new ErrorRecord(ex, "psdataParseError", ErrorCategory.ParserError, null); - cmdletPassedIn.WriteError(psdataParseError); - return successfullyParsed; + throw new PSArgumentNullException(nameof(filePath)); } - else + + string contents = System.IO.File.ReadAllText(filePath); + var scriptBlock = System.Management.Automation.ScriptBlock.Create(contents); + + // Ensure that the content script block is safe to convert into a PSDataFile Hashtable. + // This will throw for unsafe content. + scriptBlock.CheckRestrictedLanguage( + allowedCommands: allowedCommands, + allowedVariables: allowedVariables, + allowEnvironmentVariables: allowEnvironmentVariables); + + // Convert contents into PSDataFile Hashtable by executing content as script. + object result = scriptBlock.InvokeReturnAsIs(); + if (result is PSObject psObject) { - var data = ast.Find(a => a is HashtableAst, false); - if (data != null) - { - parsedMetadataHashtable = (Hashtable)data.SafeGetValue(); - successfullyParsed = true; - } - else - { - var message = String.Format("Could not parse as PowerShell data file-- no hashtable root for file '{0}'", moduleFileInfo); - var ex = new ArgumentException(message); - var psdataParseError = new ErrorRecord(ex, "psdataParseError", ErrorCategory.ParserError, null); - cmdletPassedIn.WriteError(psdataParseError); - } + result = psObject.BaseObject; } - } - return successfullyParsed; + dataFileInfo = (Hashtable) result; + error = null; + return true; + } + catch (Exception ex) + { + dataFileInfo = null; + error = ex; + return false; + } } #endregion @@ -1128,117 +1169,117 @@ public static Collection InvokeScriptWithHost( } #endregion Methods - } - + } + #endregion - + #region AuthenticodeSignature - - internal static class AuthenticodeSignature - { - #region Methods - - internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath, PSCmdlet cmdletPassedIn, out ErrorRecord errorRecord) - { - errorRecord = null; - - // Because authenticode and catalog verifications are only applicable on Windows, we allow all packages by default to be installed on unix systems. - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return true; - } - - // Check that the catalog file is signed properly - string catalogFilePath = Path.Combine(tempDirNameVersion, pkgName + ".cat"); - if (File.Exists(catalogFilePath)) - { - // Run catalog validation - Collection TestFileCatalogResult = new Collection(); - string moduleBasePath = tempDirNameVersion; - try - { - // By default "Test-FileCatalog will look through all files in the provided directory, -FilesToSkip allows us to ignore specific files - TestFileCatalogResult = cmdletPassedIn.InvokeCommand.InvokeScript( - script: @"param ( - [string] $moduleBasePath, - [string] $catalogFilePath - ) - $catalogValidation = Test-FileCatalog -Path $moduleBasePath -CatalogFilePath $CatalogFilePath ` - -FilesToSkip '*.nupkg','*.nuspec', '*.nupkg.metadata', '*.nupkg.sha512' ` - -Detailed -ErrorAction SilentlyContinue - - if ($catalogValidation.Status.ToString() -eq 'valid' -and $catalogValidation.Signature.Status -eq 'valid') { - return $true - } - else { - return $false - } - ", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { moduleBasePath, catalogFilePath }); - } - catch (Exception e) - { - errorRecord = new ErrorRecord(new ArgumentException(e.Message), "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn); - return false; - } - - bool catalogValidation = (TestFileCatalogResult[0] != null) ? (bool)TestFileCatalogResult[0].BaseObject : false; - if (!catalogValidation) - { - var exMessage = String.Format("The catalog file '{0}' is invalid.", pkgName + ".cat"); - var ex = new ArgumentException(exMessage); - - errorRecord = new ErrorRecord(ex, "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn); - return false; - } - } - - Collection authenticodeSignature = new Collection(); - try - { - string[] listOfExtensions = { "*.ps1", "*.psd1", "*.psm1", "*.mof", "*.cat", "*.ps1xml" }; - authenticodeSignature = cmdletPassedIn.InvokeCommand.InvokeScript( - script: @"param ( - [string] $tempDirNameVersion, - [string[]] $listOfExtensions - ) - Get-ChildItem $tempDirNameVersion -Recurse -Include $listOfExtensions | Get-AuthenticodeSignature -ErrorAction SilentlyContinue", - useNewScope: true, - writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, - input: null, - args: new object[] { tempDirNameVersion, listOfExtensions }); - } - catch (Exception e) - { - errorRecord = new ErrorRecord(new ArgumentException(e.Message), "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn); - return false; - } - - // If the authenticode signature is not valid, return false - if (authenticodeSignature.Any() && authenticodeSignature[0] != null) - { - foreach (var sign in authenticodeSignature) - { - Signature signature = (Signature)sign.BaseObject; - if (!signature.Status.Equals(SignatureStatus.Valid)) - { - var exMessage = String.Format("The signature for '{0}' is '{1}.", pkgName, signature.Status.ToString()); - var ex = new ArgumentException(exMessage); - errorRecord = new ErrorRecord(ex, "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn); - - return false; - } - } - } - - return true; - } - - #endregion - } - + + internal static class AuthenticodeSignature + { + #region Methods + + internal static bool CheckAuthenticodeSignature(string pkgName, string tempDirNameVersion, VersionRange versionRange, List pathsToSearch, string installPath, PSCmdlet cmdletPassedIn, out ErrorRecord errorRecord) + { + errorRecord = null; + + // Because authenticode and catalog verifications are only applicable on Windows, we allow all packages by default to be installed on unix systems. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return true; + } + + // Check that the catalog file is signed properly + string catalogFilePath = Path.Combine(tempDirNameVersion, pkgName + ".cat"); + if (File.Exists(catalogFilePath)) + { + // Run catalog validation + Collection TestFileCatalogResult = new Collection(); + string moduleBasePath = tempDirNameVersion; + try + { + // By default "Test-FileCatalog will look through all files in the provided directory, -FilesToSkip allows us to ignore specific files + TestFileCatalogResult = cmdletPassedIn.InvokeCommand.InvokeScript( + script: @"param ( + [string] $moduleBasePath, + [string] $catalogFilePath + ) + $catalogValidation = Test-FileCatalog -Path $moduleBasePath -CatalogFilePath $CatalogFilePath ` + -FilesToSkip '*.nupkg','*.nuspec', '*.nupkg.metadata', '*.nupkg.sha512' ` + -Detailed -ErrorAction SilentlyContinue + + if ($catalogValidation.Status.ToString() -eq 'valid' -and $catalogValidation.Signature.Status -eq 'valid') { + return $true + } + else { + return $false + } + ", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { moduleBasePath, catalogFilePath }); + } + catch (Exception e) + { + errorRecord = new ErrorRecord(new ArgumentException(e.Message), "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn); + return false; + } + + bool catalogValidation = (TestFileCatalogResult[0] != null) ? (bool)TestFileCatalogResult[0].BaseObject : false; + if (!catalogValidation) + { + var exMessage = String.Format("The catalog file '{0}' is invalid.", pkgName + ".cat"); + var ex = new ArgumentException(exMessage); + + errorRecord = new ErrorRecord(ex, "TestFileCatalogError", ErrorCategory.InvalidResult, cmdletPassedIn); + return false; + } + } + + Collection authenticodeSignature = new Collection(); + try + { + string[] listOfExtensions = { "*.ps1", "*.psd1", "*.psm1", "*.mof", "*.cat", "*.ps1xml" }; + authenticodeSignature = cmdletPassedIn.InvokeCommand.InvokeScript( + script: @"param ( + [string] $tempDirNameVersion, + [string[]] $listOfExtensions + ) + Get-ChildItem $tempDirNameVersion -Recurse -Include $listOfExtensions | Get-AuthenticodeSignature -ErrorAction SilentlyContinue", + useNewScope: true, + writeToPipeline: System.Management.Automation.Runspaces.PipelineResultTypes.None, + input: null, + args: new object[] { tempDirNameVersion, listOfExtensions }); + } + catch (Exception e) + { + errorRecord = new ErrorRecord(new ArgumentException(e.Message), "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn); + return false; + } + + // If the authenticode signature is not valid, return false + if (authenticodeSignature.Any() && authenticodeSignature[0] != null) + { + foreach (var sign in authenticodeSignature) + { + Signature signature = (Signature)sign.BaseObject; + if (!signature.Status.Equals(SignatureStatus.Valid)) + { + var exMessage = String.Format("The signature for '{0}' is '{1}.", pkgName, signature.Status.ToString()); + var ex = new ArgumentException(exMessage); + errorRecord = new ErrorRecord(ex, "GetAuthenticodeSignatureError", ErrorCategory.InvalidResult, cmdletPassedIn); + + return false; + } + } + } + + return true; + } + + #endregion + } + #endregion }