feat: Implement vulnerability token signing and verification utilities
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added VulnTokenSigner for signing JWT tokens with specified algorithms and keys. - Introduced VulnTokenUtilities for resolving tenant and subject claims, and sanitizing context dictionaries. - Created VulnTokenVerificationUtilities for parsing tokens, verifying signatures, and deserializing payloads. - Developed VulnWorkflowAntiForgeryTokenIssuer for issuing anti-forgery tokens with configurable options. - Implemented VulnWorkflowAntiForgeryTokenVerifier for verifying anti-forgery tokens and validating payloads. - Added AuthorityVulnerabilityExplorerOptions to manage configuration for vulnerability explorer features. - Included tests for FilesystemPackRunDispatcher to ensure proper job handling under egress policy restrictions.
This commit is contained in:
@@ -1,128 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
static EventWebhookEndpointTests()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Feedser__HmacSecret", FeedserSecret);
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Feedser__Enabled", "true");
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Vexer__HmacSecret", VexerSecret);
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Vexer__Enabled", "true");
|
||||
}
|
||||
|
||||
private const string FeedserSecret = "feedser-secret";
|
||||
private const string VexerSecret = "vexer-secret";
|
||||
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public EventWebhookEndpointTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FeedserWebhook_AcceptsValidSignature()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
var payload = new
|
||||
{
|
||||
exportId = "feedser-exp-1",
|
||||
changedProductKeys = new[] { "pkg:rpm/openssl", "pkg:deb/nginx" },
|
||||
kev = new[] { "CVE-2024-0001" },
|
||||
window = new { from = DateTimeOffset.UtcNow.AddHours(-1), to = DateTimeOffset.UtcNow }
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/events/feedser-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(FeedserSecret, json));
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FeedserWebhook_RejectsInvalidSignature()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
var payload = new
|
||||
{
|
||||
exportId = "feedser-exp-2",
|
||||
changedProductKeys = new[] { "pkg:nuget/log4net" }
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/events/feedser-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", "sha256=invalid");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VexerWebhook_HonoursRateLimit()
|
||||
{
|
||||
using var restrictedFactory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Scheduler:Events:Webhooks:Vexer:RateLimitRequests"] = "1",
|
||||
["Scheduler:Events:Webhooks:Vexer:RateLimitWindowSeconds"] = "60"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
using var client = restrictedFactory.CreateClient();
|
||||
var payload = new
|
||||
{
|
||||
exportId = "vexer-exp-1",
|
||||
changedClaims = new[]
|
||||
{
|
||||
new { productKey = "pkg:deb/openssl", vulnerabilityId = "CVE-2024-1234", status = "affected" }
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var first = new HttpRequestMessage(HttpMethod.Post, "/events/vexer-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
first.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(VexerSecret, json));
|
||||
var firstResponse = await client.SendAsync(first);
|
||||
Assert.Equal(HttpStatusCode.Accepted, firstResponse.StatusCode);
|
||||
|
||||
using var second = new HttpRequestMessage(HttpMethod.Post, "/events/vexer-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
second.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(VexerSecret, json));
|
||||
var secondResponse = await client.SendAsync(second);
|
||||
Assert.Equal((HttpStatusCode)429, secondResponse.StatusCode);
|
||||
Assert.True(secondResponse.Headers.Contains("Retry-After"));
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string secret, string payload)
|
||||
{
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||
return "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
static EventWebhookEndpointTests()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Conselier__HmacSecret", ConselierSecret);
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Conselier__Enabled", "true");
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Excitor__HmacSecret", ExcitorSecret);
|
||||
Environment.SetEnvironmentVariable("Scheduler__Events__Webhooks__Excitor__Enabled", "true");
|
||||
}
|
||||
|
||||
private const string ConselierSecret = "conselier-secret";
|
||||
private const string ExcitorSecret = "excitor-secret";
|
||||
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public EventWebhookEndpointTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConselierWebhook_AcceptsValidSignature()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
var payload = new
|
||||
{
|
||||
exportId = "conselier-exp-1",
|
||||
changedProductKeys = new[] { "pkg:rpm/openssl", "pkg:deb/nginx" },
|
||||
kev = new[] { "CVE-2024-0001" },
|
||||
window = new { from = DateTimeOffset.UtcNow.AddHours(-1), to = DateTimeOffset.UtcNow }
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/events/conselier-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(ConselierSecret, json));
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConselierWebhook_RejectsInvalidSignature()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
var payload = new
|
||||
{
|
||||
exportId = "conselier-exp-2",
|
||||
changedProductKeys = new[] { "pkg:nuget/log4net" }
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/events/conselier-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
request.Headers.TryAddWithoutValidation("X-Scheduler-Signature", "sha256=invalid");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExcitorWebhook_HonoursRateLimit()
|
||||
{
|
||||
using var restrictedFactory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Scheduler:Events:Webhooks:Excitor:RateLimitRequests"] = "1",
|
||||
["Scheduler:Events:Webhooks:Excitor:RateLimitWindowSeconds"] = "60"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
using var client = restrictedFactory.CreateClient();
|
||||
var payload = new
|
||||
{
|
||||
exportId = "excitor-exp-1",
|
||||
changedClaims = new[]
|
||||
{
|
||||
new { productKey = "pkg:deb/openssl", vulnerabilityId = "CVE-2024-1234", status = "affected" }
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var first = new HttpRequestMessage(HttpMethod.Post, "/events/excitor-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
first.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(ExcitorSecret, json));
|
||||
var firstResponse = await client.SendAsync(first);
|
||||
Assert.Equal(HttpStatusCode.Accepted, firstResponse.StatusCode);
|
||||
|
||||
using var second = new HttpRequestMessage(HttpMethod.Post, "/events/excitor-export")
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
second.Headers.TryAddWithoutValidation("X-Scheduler-Signature", ComputeSignature(ExcitorSecret, json));
|
||||
var secondResponse = await client.SendAsync(second);
|
||||
Assert.Equal((HttpStatusCode)429, secondResponse.StatusCode);
|
||||
Assert.True(secondResponse.Headers.Contains("Retry-After"));
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string secret, string payload)
|
||||
{
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||
return "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scheduler.WebService.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class SchedulerWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string?>("Scheduler:Authority:Enabled", "false"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Cartographer:Webhook:Enabled", "false"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:GraphJobs:Enabled", "false"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Feedser:Enabled", "true"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Feedser:HmacSecret", "feedser-secret"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Feedser:RateLimitRequests", "20"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Feedser:RateLimitWindowSeconds", "60"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Vexer:Enabled", "true"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Vexer:HmacSecret", "vexer-secret"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Vexer:RateLimitRequests", "20"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Vexer:RateLimitWindowSeconds", "60")
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.Configure<SchedulerEventsOptions>(options =>
|
||||
{
|
||||
options.Webhooks ??= new SchedulerInboundWebhooksOptions();
|
||||
options.Webhooks.Feedser ??= SchedulerWebhookOptions.CreateDefault("feedser");
|
||||
options.Webhooks.Vexer ??= SchedulerWebhookOptions.CreateDefault("vexer");
|
||||
options.Webhooks.Feedser.HmacSecret = "feedser-secret";
|
||||
options.Webhooks.Feedser.Enabled = true;
|
||||
options.Webhooks.Vexer.HmacSecret = "vexer-secret";
|
||||
options.Webhooks.Vexer.Enabled = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scheduler.WebService.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class SchedulerWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string?>("Scheduler:Authority:Enabled", "false"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Cartographer:Webhook:Enabled", "false"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:GraphJobs:Enabled", "false"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Conselier:Enabled", "true"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Conselier:HmacSecret", "conselier-secret"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Conselier:RateLimitRequests", "20"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Conselier:RateLimitWindowSeconds", "60"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:Enabled", "true"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:HmacSecret", "excitor-secret"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:RateLimitRequests", "20"),
|
||||
new KeyValuePair<string, string?>("Scheduler:Events:Webhooks:Excitor:RateLimitWindowSeconds", "60")
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.Configure<SchedulerEventsOptions>(options =>
|
||||
{
|
||||
options.Webhooks ??= new SchedulerInboundWebhooksOptions();
|
||||
options.Webhooks.Conselier ??= SchedulerWebhookOptions.CreateDefault("conselier");
|
||||
options.Webhooks.Excitor ??= SchedulerWebhookOptions.CreateDefault("excitor");
|
||||
options.Webhooks.Conselier.HmacSecret = "conselier-secret";
|
||||
options.Webhooks.Conselier.Enabled = true;
|
||||
options.Webhooks.Excitor.HmacSecret = "excitor-secret";
|
||||
options.Webhooks.Excitor.Enabled = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user