Skip to content

Commit

Permalink
Added an integrity hash to lock file #2664 (#2665)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Dec 14, 2024
1 parent f7a4a61 commit 266da10
Show file tree
Hide file tree
Showing 23 changed files with 479 additions and 66 deletions.
6 changes: 4 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
},
{
"fileMatch": [
"/**/ps-rule.lock.json"
"/**/ps-rule.lock.json",
"/**/test.lock.json"
],
"url": "./schemas/PSRule-lock.schema.json"
}
Expand Down Expand Up @@ -135,5 +136,6 @@
"release/*"
],
"dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true,
"PSRule.options.path": "ps-rule-ci.yaml"
"PSRule.options.path": "ps-rule-ci.yaml",
"PSRule.lock.restore": true
}
7 changes: 7 additions & 0 deletions docs/CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

## Unreleased

What's changed since pre-release v3.0.0-B0342:

- General improvements:
- Added an integrity hash to lock file by @BernieWhite.
[#2664](https://github.com/microsoft/PSRule/issues/2664)
- The lock file now includes an integrity hash to ensures the restored module matches originally added module.

## v3.0.0-B0342 (pre-release)

What's changed since pre-release v3.0.0-B0340:
Expand Down
27 changes: 22 additions & 5 deletions docs/updates/v3.0.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
date: 2024-11-30
date: 2024-01-30
version: 3.0
---

# nnn nnn (v3.0)
# January 2025 (v3.0)

Welcome to the nnn nnn release of PSRule.
Welcome to the January 2025 release of PSRule.
There are many updates in this version that we hope you'll like, some of the key highlights include:

- [Official CLI support](#official-cli-support) — A new CLI experience for PSRule.
Expand All @@ -19,17 +19,29 @@ See the detailed change log [here](../CHANGELOG-v3.md).
While many of you have been using PSRule through PowerShell for some time,
we've been working on a new experience for those who prefer a CLI.

Additionally we wanted to improve the bootstrapping experience for PSRule in CI/CD pipelines.
Additionally, we wanted to improve the bootstrapping experience for PSRule in during development and CI/CD pipelines.

The new CLI runs on Windows, macOS, and Linux and is available as a standalone executable or can be installed as a .NET tool.

## Module lock file

We've introduced a new feature to help you manage the versions of modules used by PSRule.
The module lock file is a JSON-based file named `ps-rule.lock.json` that lists the modules and versions used by PSRule.
Initialize and commit the lock file to your repository to pin each module to a specific version.

If the lock file is present, PSRule will use the versions listed in the lock file instead of the latest available version.
When no lock file is present, PSRule will use the latest version of each module that meets any configured constraints.

This makes it easier to share and reproduce the versions of modules used during development and CI/CD pipelines.

This is a change to the previous behavior in CI/CD pipelines where PSRule:

- Would install the latest version of each module but only if no version was not already installed.
- Would not check if the installed version met constraints defined in the options file.

The lock file is supported by the CLI, GitHub Actions, Azure Pipelines, and Visual Studio Code extension.
When using PSRule from PowerShell, the lock file is ignored to prevent conflicts with PowerShell's built-in update mechanism.

## Visual Studio Code

### New home and identity
Expand All @@ -38,6 +50,8 @@ The Visual Studio Code (VSCode) extension for PSRule now lives side-by-side with

As part of this change we are now publishing the extension as a **verified** Microsoft extension with the ID `ps-rule.vscode-ps-rule`.

The new extension supports pre-release and stable releases managed through Visual Studio Code's extension marketplace.

We hope this will not only help the community to log issues and get help on the correct repository,
but also streamline how we deliver updates in the future.

Expand All @@ -49,10 +63,13 @@ Bringing together the code base is the first step in building an all improved ri

Previously to use PSRule within VSCode,
a prerequisite step was to install PowerShell on non-Windows OSs and then install PSRule through PowerShell.
Additionally, any required rules modules would also need to be installed through PowerShell.

We've done away with this approach entirely for the authoring experience in VSCode by providing native support in the extension.

This means you can now use PSRule in VSCode without needing to install PowerShell or PSRule on your machine.
This means you can now use PSRule in VSCode without needing to separately install PowerShell or PSRule on your machine.
The extension includes the necessary components to run PSRule and will install and cache required rule modules.

We've improved the experience by adding the ability to:

- Restore modules from the VSCode command palette.
Expand Down
5 changes: 3 additions & 2 deletions ps-rule.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"version": 1,
"modules": {
"PSRule.Rules.MSFT.OSS": {
"version": "1.1.0"
"version": "1.1.0",
"integrity": "sha512-4oEbkAT3VIQQlrDUOpB9qKkbNU5BMktvkDCriws4LgCMUiyUoYMcN0XovljAIW4FO0cmP7mP6A8Z7MPNGlgK7Q=="
}
}
}
}
5 changes: 5 additions & 0 deletions schemas/PSRule-lock.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
"title": "Module version",
"description": "The version of the module to use."
},
"integrity": {
"type": "string",
"title": "Module integrity",
"description": "The integrity hash of the module to use."
},
"includePrerelease": {
"type": "boolean",
"title": "Include prerelease",
Expand Down
8 changes: 8 additions & 0 deletions src/PSRule.CommandLine/ClientContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System.CommandLine.Invocation;
using PSRule.Configuration;
using PSRule.Options;
using PSRule.Pipeline.Dependencies;

namespace PSRule.CommandLine;

Expand All @@ -28,6 +30,7 @@ public ClientContext(InvocationContext invocation, string? option, bool verbose,
Host = new ClientHost(this, verbose, debug);
Option = GetOption(Host, option);
CachePath = Path;
IntegrityAlgorithm = Option.Execution.HashAlgorithm.GetValueOrDefault(ExecutionOption.Default.HashAlgorithm!.Value).ToIntegrityAlgorithm();
}

/// <summary>
Expand Down Expand Up @@ -66,6 +69,11 @@ public ClientContext(InvocationContext invocation, string? option, bool verbose,
/// </summary>
public string CachePath { get; }

/// <summary>
/// The default integrity algorithm to use.
/// </summary>
public IntegrityAlgorithm IntegrityAlgorithm { get; }

private static PSRuleOption GetOption(ClientHost host, string? path)
{
PSRuleOption.UseHostContext(host);
Expand Down
95 changes: 83 additions & 12 deletions src/PSRule.CommandLine/Commands/ModuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions
var idealVersion = await FindVersionAsync(module, null, targetVersion, null, cancellationToken);
if (idealVersion != null)
{
installedVersion = await InstallVersionAsync(clientContext, module, idealVersion, cancellationToken);
installedVersion = await InstallVersionAsync(clientContext, module, idealVersion, kv.Value.Integrity, cancellationToken);
}

if (pwsh.HadErrors || idealVersion == null || installedVersion == null)
Expand Down Expand Up @@ -112,7 +112,7 @@ public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions
var idealVersion = await FindVersionAsync(includeModule, moduleConstraint, null, null, cancellationToken);
if (idealVersion != null)
{
await InstallVersionAsync(clientContext, includeModule, idealVersion, cancellationToken);
await InstallVersionAsync(clientContext, includeModule, idealVersion, null, cancellationToken);
}
else if (idealVersion == null)
{
Expand Down Expand Up @@ -153,13 +153,18 @@ public static async Task<int> ModuleInitAsync(ModuleOptions operationOptions, Cl
var file = !operationOptions.Force ? LockFile.Read(null) : new LockFile();
using var pwsh = CreatePowerShell();

if (operationOptions.Force)
{
clientContext.LogVerbose(Messages.UsingForce);
}

// Add for any included modules.
if (clientContext.Option?.Include?.Module != null && clientContext.Option.Include.Module.Length > 0)
{
foreach (var includeModule in clientContext.Option.Include.Module)
{
// Skip modules already in the lock unless force is used.
if (file.Modules.TryGetValue(includeModule, out var lockEntry))
if (file.Modules.TryGetValue(includeModule, out var lockEntry) && !operationOptions.Force)
continue;

// Get a constraint if set from options.
Expand All @@ -173,14 +178,18 @@ public static async Task<int> ModuleInitAsync(ModuleOptions operationOptions, Cl
return ERROR_MODULE_FAILED_TO_FIND;
}

if (lockEntry?.Version == idealVersion)
if (lockEntry?.Version == idealVersion && !operationOptions.Force)
continue;

// invocation.Log(Messages.UsingModule, includeModule, idealVersion.ToString());
if (!IsInstalled(pwsh, includeModule, idealVersion, out _))
{
await InstallVersionAsync(clientContext, includeModule, idealVersion, null, cancellationToken);
}

file.Modules[includeModule] = new LockEntry
clientContext.LogVerbose(Messages.UsingModule, includeModule, idealVersion.ToString());
file.Modules[includeModule] = new LockEntry(idealVersion)
{
Version = idealVersion
Integrity = IntegrityBuilder.Build(clientContext.IntegrityAlgorithm, GetModulePath(clientContext, includeModule, idealVersion)),
};
}
}
Expand Down Expand Up @@ -224,6 +233,11 @@ public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, Cli
var requires = clientContext.Option.Requires.ToDictionary();
var file = LockFile.Read(null);

if (operationOptions.Force)
{
clientContext.LogVerbose(Messages.UsingForce);
}

using var pwsh = CreatePowerShell();
foreach (var module in operationOptions.Module)
{
Expand Down Expand Up @@ -253,11 +267,16 @@ public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, Cli
return ERROR_MODULE_FAILED_TO_FIND;
}

if (!IsInstalled(pwsh, module, idealVersion, out _))
{
await InstallVersionAsync(clientContext, module, idealVersion, null, cancellationToken);
}

clientContext.LogVerbose(Messages.UsingModule, module, idealVersion.ToString());
item = new LockEntry
item = new LockEntry(idealVersion)
{
Version = idealVersion,
IncludePrerelease = operationOptions.Prerelease && !idealVersion.Stable ? true : null,
Integrity = IntegrityBuilder.Build(clientContext.IntegrityAlgorithm, GetModulePath(clientContext, module, idealVersion)),
};
file.Modules[module] = item;
}
Expand Down Expand Up @@ -337,9 +356,15 @@ public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions,
if (idealVersion == kv.Value.Version)
continue;

if (!IsInstalled(pwsh, kv.Key, idealVersion, out _))
{
await InstallVersionAsync(clientContext, kv.Key, idealVersion, null, cancellationToken);
}

clientContext.LogVerbose(Messages.UsingModule, kv.Key, idealVersion.ToString());

kv.Value.Version = idealVersion;
kv.Value.Integrity = IntegrityBuilder.Build(clientContext.IntegrityAlgorithm, GetModulePath(clientContext, kv.Key, idealVersion));
kv.Value.IncludePrerelease = (kv.Value.IncludePrerelease.GetValueOrDefault(false) || operationOptions.Prerelease) && !idealVersion.Stable ? true : null;
file.Modules[kv.Key] = kv.Value;
}
Expand Down Expand Up @@ -470,7 +495,7 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H
return result;
}

private static async Task<SemanticVersion.Version?> InstallVersionAsync([DisallowNull] ClientContext context, [DisallowNull] string name, [DisallowNull] SemanticVersion.Version version, CancellationToken cancellationToken)
private static async Task<SemanticVersion.Version?> InstallVersionAsync([DisallowNull] ClientContext context, [DisallowNull] string name, [DisallowNull] SemanticVersion.Version version, LockEntryIntegrity? integrity, CancellationToken cancellationToken)
{
context.LogVerbose(Messages.RestoringModule, name, version);

Expand All @@ -495,14 +520,23 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H
var nuspecReader = await packageReader.GetNuspecReaderAsync(cancellationToken);

var modulePath = GetModulePath(context, name, version);
var tempPath = GetModuleTempPath(context, name, version);

// Remove existing module.
if (Directory.Exists(modulePath))
{
Directory.Delete(modulePath, true);
}

// Remove existing temp module.
if (Directory.Exists(tempPath))
{
Directory.Delete(tempPath, true);
}

var count = 0;
var files = packageReader.GetFiles();
packageReader.CopyFiles(modulePath, files, (name, targetPath, s) =>
packageReader.CopyFiles(tempPath, files, (name, targetPath, s) =>
{
if (ShouldIgnorePackageFile(name))
return null;
Expand All @@ -515,6 +549,36 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H
}, logger, cancellationToken);

// Check module path exists.
if (!Directory.Exists(tempPath))
return null;

if (integrity != null)
{
context.LogVerbose("Checking module integrity: {0} -- {1}", name, integrity.Hash);

var actualIntegrity = IntegrityBuilder.Build(integrity.Algorithm, tempPath);
if (!string.Equals(actualIntegrity.Hash, integrity.Hash))
{
context.LogVerbose("Module integrity check failed: {0} -- {1}", name, actualIntegrity.Hash);

// Clean up the temp path.
if (Directory.Exists(tempPath))
{
Directory.Delete(tempPath, true);
}

context.LogError(Messages.Error_504, name, version);
return null;
}
}

var parentDirectory = Directory.GetParent(modulePath)?.FullName;
if (!Directory.Exists(parentDirectory) && parentDirectory != null)
Directory.CreateDirectory(parentDirectory);

// Move the module to the final path.
Directory.Move(tempPath, modulePath);

if (!Directory.Exists(modulePath))
return null;

Expand All @@ -534,10 +598,17 @@ private static string GetModulePath(ClientContext context, string name, [Disallo
return Path.Combine(context.CachePath, MODULES_PATH, name, version.ToShortString());
}

private static string GetModuleTempPath(ClientContext context, string name, [DisallowNull] SemanticVersion.Version version)
{
return Path.Combine(context.CachePath, MODULES_PATH, string.Concat("temp-", name, "-", version.ToShortString()));
}

private static bool ShouldIgnorePackageFile(string name)
{
return string.Equals(name, "[Content_Types].xml", StringComparison.OrdinalIgnoreCase) ||
string.Equals(name, "_rels/.rels", StringComparison.OrdinalIgnoreCase);
string.Equals(name, "_rels/.rels", StringComparison.OrdinalIgnoreCase) ||
Path.GetExtension(name) == ".nuspec" ||
name.StartsWith("package/services/metadata/core-properties/", StringComparison.OrdinalIgnoreCase);
}

private static PowerShell CreatePowerShell()
Expand Down
18 changes: 18 additions & 0 deletions src/PSRule.CommandLine/Resources/Messages.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 266da10

Please sign in to comment.