File: Agents\NpmProvenanceCheckerTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Npm;
 
namespace Aspire.Cli.Tests.Agents;
 
public class NpmProvenanceCheckerTests
{
    [Fact]
    public void ParseProvenance_WithValidSlsaProvenance_ReturnsVerifiedWithData()
    {
        var json = BuildAttestationJson("https://github.com/microsoft/playwright-cli");
 
        var result = NpmProvenanceChecker.ParseProvenance(json);
 
        Assert.NotNull(result);
        Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Value.Outcome);
        Assert.Equal("https://github.com/microsoft/playwright-cli", result.Value.Provenance.SourceRepository);
        Assert.Equal(".github/workflows/publish.yml", result.Value.Provenance.WorkflowPath);
        Assert.Equal("https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", result.Value.Provenance.BuildType);
        Assert.Equal("https://github.com/actions/runner/github-hosted", result.Value.Provenance.BuilderId);
        Assert.Equal("refs/tags/v0.1.1", result.Value.Provenance.WorkflowRef);
    }
 
    [Fact]
    public void ParseProvenance_WithDifferentRepository_ReturnsVerifiedWithThatRepository()
    {
        var json = BuildAttestationJson("https://github.com/attacker/malicious-package");
 
        var result = NpmProvenanceChecker.ParseProvenance(json);
 
        Assert.NotNull(result);
        Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Value.Outcome);
        Assert.Equal("https://github.com/attacker/malicious-package", result.Value.Provenance.SourceRepository);
    }
 
    [Fact]
    public void ParseProvenance_WithNoSlsaPredicate_ReturnsSlsaProvenanceNotFound()
    {
        var json = """
        {
            "attestations": [
                {
                    "predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1",
                    "bundle": {
                        "dsseEnvelope": {
                            "payload": ""
                        }
                    }
                }
            ]
        }
        """;
 
        var result = NpmProvenanceChecker.ParseProvenance(json);
 
        Assert.NotNull(result);
        Assert.Equal(ProvenanceVerificationOutcome.SlsaProvenanceNotFound, result.Value.Outcome);
    }
 
    [Fact]
    public void ParseProvenance_WithEmptyAttestations_ReturnsSlsaProvenanceNotFound()
    {
        var json = """{"attestations": []}""";
 
        var result = NpmProvenanceChecker.ParseProvenance(json);
 
        Assert.NotNull(result);
        Assert.Equal(ProvenanceVerificationOutcome.SlsaProvenanceNotFound, result.Value.Outcome);
    }
 
    [Fact]
    public void ParseProvenance_WithMalformedJson_ThrowsException()
    {
        Assert.ThrowsAny<JsonException>(() => NpmProvenanceChecker.ParseProvenance("not json"));
    }
 
    [Fact]
    public void ParseProvenance_WithMissingWorkflowNode_ReturnsSourceRepositoryNotFound()
    {
        var statement = new JsonObject
        {
            ["_type"] = "https://in-toto.io/Statement/v1",
            ["predicateType"] = "https://slsa.dev/provenance/v1",
            ["predicate"] = new JsonObject
            {
                ["buildDefinition"] = new JsonObject
                {
                    ["externalParameters"] = new JsonObject()
                }
            }
        };
 
        var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(statement.ToJsonString()));
        var json = $$"""
        {
            "attestations": [
                {
                    "predicateType": "https://slsa.dev/provenance/v1",
                    "bundle": {
                        "dsseEnvelope": {
                            "payload": "{{payload}}"
                        }
                    }
                }
            ]
        }
        """;
 
        var result = NpmProvenanceChecker.ParseProvenance(json);
 
        Assert.NotNull(result);
        Assert.Equal(ProvenanceVerificationOutcome.SourceRepositoryNotFound, result.Value.Outcome);
    }
 
    [Fact]
    public void ParseProvenance_WithMissingPayload_ReturnsPayloadDecodeFailed()
    {
        var json = """
        {
            "attestations": [
                {
                    "predicateType": "https://slsa.dev/provenance/v1",
                    "bundle": {
                        "dsseEnvelope": {}
                    }
                }
            ]
        }
        """;
 
        var result = NpmProvenanceChecker.ParseProvenance(json);
 
        Assert.NotNull(result);
        Assert.Equal(ProvenanceVerificationOutcome.PayloadDecodeFailed, result.Value.Outcome);
    }
 
    [Fact]
    public async Task VerifyProvenanceAsync_WithMismatchedWorkflowRef_ReturnsWorkflowRefMismatch()
    {
        var json = BuildAttestationJson(
            "https://github.com/microsoft/playwright-cli",
            workflowRef: "refs/tags/v9.9.9");
 
        var handler = new TestHttpMessageHandler(json);
        var httpClient = new HttpClient(handler);
        var checker = new NpmProvenanceChecker(httpClient, Microsoft.Extensions.Logging.Abstractions.NullLogger<NpmProvenanceChecker>.Instance);
 
        var result = await checker.VerifyProvenanceAsync(
            "@playwright/cli",
            "0.1.1",
            "https://github.com/microsoft/playwright-cli",
            ".github/workflows/publish.yml",
            "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
            refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) &&
                       string.Equals(refInfo.Name, "v0.1.1", StringComparison.Ordinal),
            CancellationToken.None);
 
        Assert.Equal(ProvenanceVerificationOutcome.WorkflowRefMismatch, result.Outcome);
        Assert.Equal("refs/tags/v9.9.9", result.Provenance?.WorkflowRef);
    }
 
    [Fact]
    public async Task VerifyProvenanceAsync_WithMatchingWorkflowRef_ReturnsVerified()
    {
        var json = BuildAttestationJson(
            "https://github.com/microsoft/playwright-cli",
            workflowRef: "refs/tags/v0.1.1");
 
        var handler = new TestHttpMessageHandler(json);
        var httpClient = new HttpClient(handler);
        var checker = new NpmProvenanceChecker(httpClient, Microsoft.Extensions.Logging.Abstractions.NullLogger<NpmProvenanceChecker>.Instance);
 
        var result = await checker.VerifyProvenanceAsync(
            "@playwright/cli",
            "0.1.1",
            "https://github.com/microsoft/playwright-cli",
            ".github/workflows/publish.yml",
            "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
            refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) &&
                       string.Equals(refInfo.Name, "v0.1.1", StringComparison.Ordinal),
            CancellationToken.None);
 
        Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Outcome);
    }
 
    [Fact]
    public async Task VerifyProvenanceAsync_WithNullCallback_SkipsRefValidation()
    {
        var json = BuildAttestationJson(
            "https://github.com/microsoft/playwright-cli",
            workflowRef: "refs/tags/any-format-at-all");
 
        var handler = new TestHttpMessageHandler(json);
        var httpClient = new HttpClient(handler);
        var checker = new NpmProvenanceChecker(httpClient, Microsoft.Extensions.Logging.Abstractions.NullLogger<NpmProvenanceChecker>.Instance);
 
        var result = await checker.VerifyProvenanceAsync(
            "@playwright/cli",
            "0.1.1",
            "https://github.com/microsoft/playwright-cli",
            ".github/workflows/publish.yml",
            "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
            validateWorkflowRef: null,
            CancellationToken.None);
 
        Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Outcome);
    }
 
    private static string BuildAttestationJson(string sourceRepository, string workflowPath = ".github/workflows/publish.yml", string buildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", string workflowRef = "refs/tags/v0.1.1")
    {
        var statement = new JsonObject
        {
            ["_type"] = "https://in-toto.io/Statement/v1",
            ["predicateType"] = "https://slsa.dev/provenance/v1",
            ["predicate"] = new JsonObject
            {
                ["buildDefinition"] = new JsonObject
                {
                    ["buildType"] = buildType,
                    ["externalParameters"] = new JsonObject
                    {
                        ["workflow"] = new JsonObject
                        {
                            ["repository"] = sourceRepository,
                            ["path"] = workflowPath,
                            ["ref"] = workflowRef
                        }
                    }
                },
                ["runDetails"] = new JsonObject
                {
                    ["builder"] = new JsonObject
                    {
                        ["id"] = "https://github.com/actions/runner/github-hosted"
                    }
                }
            }
        };
 
        var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(statement.ToJsonString()));
 
        var attestationResponse = new JsonObject
        {
            ["attestations"] = new JsonArray
            {
                new JsonObject
                {
                    ["predicateType"] = "https://slsa.dev/provenance/v1",
                    ["bundle"] = new JsonObject
                    {
                        ["dsseEnvelope"] = new JsonObject
                        {
                            ["payload"] = payload
                        }
                    }
                }
            }
        };
 
        return attestationResponse.ToJsonString();
    }
 
    private sealed class TestHttpMessageHandler(string responseContent) : HttpMessageHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)
            {
                Content = new StringContent(responseContent, Encoding.UTF8, "application/json")
            });
        }
    }
 
    [Theory]
    [InlineData("refs/tags/v0.1.1", "tags", "v0.1.1")]
    [InlineData("refs/heads/main", "heads", "main")]
    [InlineData("refs/tags/@scope/pkg@1.0.0", "tags", "@scope/pkg@1.0.0")]
    [InlineData("refs/tags/release/1.0.0", "tags", "release/1.0.0")]
    public void WorkflowRefInfo_TryParse_ValidRefs_ParsesCorrectly(string raw, string expectedKind, string expectedName)
    {
        var success = WorkflowRefInfo.TryParse(raw, out var refInfo);
 
        Assert.True(success);
        Assert.NotNull(refInfo);
        Assert.Equal(raw, refInfo.Raw);
        Assert.Equal(expectedKind, refInfo.Kind);
        Assert.Equal(expectedName, refInfo.Name);
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("not-a-ref")]
    [InlineData("refs/")]
    [InlineData("refs/tags/")]
    public void WorkflowRefInfo_TryParse_InvalidRefs_ReturnsFalse(string? raw)
    {
        var success = WorkflowRefInfo.TryParse(raw, out var refInfo);
 
        Assert.False(success);
        Assert.Null(refInfo);
    }
}