diff --git a/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs b/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs index 3c727c1eea5..dbc2f1cfdd5 100644 --- a/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs +++ b/src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs @@ -31,7 +31,7 @@ internal sealed class AspireSkillsInstaller( internal const string GitHubRepository = "microsoft/aspire-skills"; internal const string ExpectedSourceRepository = $"https://github.com/{GitHubRepository}"; internal const string ExpectedWorkflowPath = ".github/workflows/publish.yml"; - internal const string ExpectedBuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1"; + internal const string ExpectedBuildType = "https://actions.github.io/buildtypes/workflow/v1"; internal const string DisablePackageValidationKey = "disableAspireSkillsPackageValidation"; internal const string VersionOverrideKey = "aspireSkillsVersion"; internal const string MaxCacheAgeKey = "AspireSkillsMaxCacheAgeSeconds"; diff --git a/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs b/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs index 9aa79f45fce..f17750521e0 100644 --- a/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs +++ b/tests/Aspire.Cli.Tests/Agents/AspireSkillsInstallerTests.cs @@ -19,6 +19,8 @@ namespace Aspire.Cli.Tests.Agents; public class AspireSkillsInstallerTests { + private const string GitHubReleaseAssetBuildType = "https://actions.github.io/buildtypes/workflow/v1"; + [Fact] public async Task InstallAsync_WhenValidBundleIsCached_UsesCacheWithoutNetwork() { @@ -120,6 +122,11 @@ public async Task InstallAsync_WhenGitHubReleaseAssetIsAvailable_UsesGitHub() Assert.Equal(AspireSkillsInstallStatus.Installed, result.Status); Assert.NotNull(result.Bundle); Assert.True(attestationVerifier.VerifyCalled); + Assert.Equal(AspireSkillsInstaller.GitHubRepository, attestationVerifier.Repository); + Assert.Equal(AspireSkillsInstaller.ExpectedSourceRepository, attestationVerifier.ExpectedSourceRepository); + Assert.Equal(AspireSkillsInstaller.ExpectedWorkflowPath, attestationVerifier.ExpectedWorkflowPath); + Assert.Equal(GitHubReleaseAssetBuildType, attestationVerifier.ExpectedBuildType); + Assert.Equal(AspireSkillsInstaller.Version, attestationVerifier.ExpectedVersion); Assert.NotNull(releaseRequestUri); Assert.NotNull(assetRequestUri); Assert.Contains("/microsoft/aspire-skills/releases/tags/v0.0.1", releaseRequestUri.AbsolutePath); @@ -298,6 +305,16 @@ private sealed class TestGitHubArtifactAttestationVerifier : IGitHubArtifactAtte { public bool VerifyCalled { get; private set; } + public string? Repository { get; private set; } + + public string? ExpectedSourceRepository { get; private set; } + + public string? ExpectedWorkflowPath { get; private set; } + + public string? ExpectedBuildType { get; private set; } + + public string? ExpectedVersion { get; private set; } + public ProvenanceVerificationResult Result { get; init; } = new() { Outcome = ProvenanceVerificationOutcome.Verified, @@ -314,6 +331,12 @@ public Task VerifyAsync( CancellationToken cancellationToken) { VerifyCalled = true; + Repository = repository; + ExpectedSourceRepository = expectedSourceRepository; + ExpectedWorkflowPath = expectedWorkflowPath; + ExpectedBuildType = expectedBuildType; + ExpectedVersion = expectedVersion; + return Task.FromResult(Result); } } diff --git a/tests/Aspire.Cli.Tests/Agents/SigstoreNpmProvenanceCheckerTests.cs b/tests/Aspire.Cli.Tests/Agents/SigstoreNpmProvenanceCheckerTests.cs index 0be6846b93a..89ad1ed48bc 100644 --- a/tests/Aspire.Cli.Tests/Agents/SigstoreNpmProvenanceCheckerTests.cs +++ b/tests/Aspire.Cli.Tests/Agents/SigstoreNpmProvenanceCheckerTests.cs @@ -9,6 +9,8 @@ namespace Aspire.Cli.Tests.Agents; public class SigstoreNpmProvenanceCheckerTests { + private const string GitHubReleaseAssetBuildType = "https://actions.github.io/buildtypes/workflow/v1"; + #region ExtractSlsaBundleJson Tests [Fact] @@ -259,6 +261,59 @@ public void VerifyProvenanceFields_WithAllFieldsMatching_ReturnsVerified() Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Outcome); } + [Fact] + public void VerifyProvenanceFields_WithGitHubReleaseAssetBuildType_ReturnsVerified() + { + var provenance = new NpmProvenanceData + { + SourceRepository = "https://github.com/microsoft/aspire-skills", + WorkflowPath = ".github/workflows/publish.yml", + BuildType = GitHubReleaseAssetBuildType, + WorkflowRef = "refs/tags/v0.0.1", + BuilderId = "https://github.com/microsoft/aspire-skills/.github/workflows/publish.yml@refs/tags/v0.0.1" + }; + + var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields( + provenance, + "https://github.com/microsoft/aspire-skills", + ".github/workflows/publish.yml", + GitHubReleaseAssetBuildType, + refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) && + string.Equals(refInfo.Name, "v0.0.1", StringComparison.Ordinal)); + + Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Outcome); + } + + [Theory] + [InlineData("https://github.com/evil/aspire-skills", ".github/workflows/publish.yml", "refs/tags/v0.0.1", nameof(ProvenanceVerificationOutcome.SourceRepositoryMismatch))] + [InlineData("https://github.com/microsoft/aspire-skills", ".github/workflows/evil.yml", "refs/tags/v0.0.1", nameof(ProvenanceVerificationOutcome.WorkflowMismatch))] + [InlineData("https://github.com/microsoft/aspire-skills", ".github/workflows/publish.yml", "refs/heads/main", nameof(ProvenanceVerificationOutcome.WorkflowRefMismatch))] + public void VerifyProvenanceFields_WithGitHubReleaseAssetBuildType_RejectsUnexpectedProvenance( + string sourceRepository, + string workflowPath, + string workflowRef, + string expectedOutcome) + { + var provenance = new NpmProvenanceData + { + SourceRepository = sourceRepository, + WorkflowPath = workflowPath, + BuildType = GitHubReleaseAssetBuildType, + WorkflowRef = workflowRef, + BuilderId = "https://github.com/microsoft/aspire-skills/.github/workflows/publish.yml@refs/tags/v0.0.1" + }; + + var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields( + provenance, + "https://github.com/microsoft/aspire-skills", + ".github/workflows/publish.yml", + GitHubReleaseAssetBuildType, + refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) && + string.Equals(refInfo.Name, "v0.0.1", StringComparison.Ordinal)); + + Assert.Equal(Enum.Parse(expectedOutcome), result.Outcome); + } + [Fact] public void VerifyProvenanceFields_WithSourceRepoMismatch_ReturnsSourceRepositoryMismatch() {