Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -0,0 +1,430 @@
// =============================================================================
// WebhookTestHelper.cs
// Sprint: SPRINT_20251229_019 - Integration E2E Validation
// Description: Utility class for webhook testing operations
// =============================================================================
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.Integration.E2E.Integrations.Helpers;
/// <summary>
/// Provides utility methods for webhook testing, including signature generation
/// and payload manipulation.
/// </summary>
public static class WebhookTestHelper
{
#region Signature Generation
/// <summary>
/// Generates an HMAC-SHA256 signature for a webhook payload.
/// </summary>
/// <param name="payload">The webhook payload.</param>
/// <param name="secret">The webhook secret.</param>
/// <param name="prefix">Optional prefix for the signature (e.g., "sha256=").</param>
/// <returns>The generated signature.</returns>
public static string GenerateHmacSha256Signature(string payload, string secret, string prefix = "sha256=")
{
var secretBytes = Encoding.UTF8.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
using var hmac = new HMACSHA256(secretBytes);
var hash = hmac.ComputeHash(payloadBytes);
return prefix + Convert.ToHexStringLower(hash);
}
/// <summary>
/// Generates a GitHub-style webhook signature.
/// </summary>
public static string GenerateGitHubSignature(string payload, string secret)
{
return GenerateHmacSha256Signature(payload, secret, "sha256=");
}
/// <summary>
/// Generates a GitLab-style webhook token.
/// GitLab uses X-Gitlab-Token header with the secret directly.
/// </summary>
public static string GenerateGitLabToken(string secret)
{
return secret;
}
/// <summary>
/// Generates a Gitea-style webhook signature.
/// </summary>
public static string GenerateGiteaSignature(string payload, string secret)
{
return GenerateHmacSha256Signature(payload, secret, "sha256=");
}
/// <summary>
/// Generates a Harbor-style webhook signature.
/// </summary>
public static string GenerateHarborSignature(string payload, string secret)
{
return GenerateHmacSha256Signature(payload, secret, "sha256=");
}
/// <summary>
/// Generates a Docker Hub-style webhook signature.
/// </summary>
public static string GenerateDockerHubSignature(string payload, string secret)
{
return GenerateHmacSha256Signature(payload, secret, "sha256=");
}
#endregion
#region Payload Manipulation
/// <summary>
/// Modifies a JSON payload by updating a specific field.
/// </summary>
/// <param name="payload">The original JSON payload.</param>
/// <param name="jsonPath">Dot-separated path to the field (e.g., "repository.name").</param>
/// <param name="newValue">The new value to set.</param>
/// <returns>The modified payload.</returns>
public static string ModifyPayloadField(string payload, string jsonPath, object newValue)
{
var doc = JsonDocument.Parse(payload);
var root = doc.RootElement;
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(payload, JsonOptions)
?? throw new InvalidOperationException("Failed to parse payload");
SetNestedValue(dict, jsonPath.Split('.'), newValue);
return JsonSerializer.Serialize(dict, JsonOptions);
}
/// <summary>
/// Corrupts a payload by modifying its hash/digest field.
/// </summary>
public static string CorruptPayloadDigest(string payload)
{
// Try common digest field names
var digestFields = new[] { "digest", "image-digest", "sha", "hash" };
foreach (var field in digestFields)
{
if (payload.Contains($"\"{field}\""))
{
// Replace any sha256 digest with a corrupted one
return System.Text.RegularExpressions.Regex.Replace(
payload,
@"sha256:[a-f0-9]{64}",
"sha256:0000000000000000000000000000000000000000000000000000000000000000");
}
}
return payload;
}
/// <summary>
/// Creates a minimal valid webhook payload for testing.
/// </summary>
public static string CreateMinimalPayload(string provider, string eventType = "push")
{
return provider.ToLowerInvariant() switch
{
"harbor" => CreateMinimalHarborPayload(),
"dockerhub" => CreateMinimalDockerHubPayload(),
"acr" => CreateMinimalAcrPayload(),
"ecr" => CreateMinimalEcrPayload(),
"gcr" => CreateMinimalGcrPayload(),
"ghcr" => CreateMinimalGhcrPayload(),
"github" => CreateMinimalGitHubPushPayload(),
"gitlab" => CreateMinimalGitLabPushPayload(),
"gitea" => CreateMinimalGiteaPushPayload(),
_ => throw new ArgumentException($"Unknown provider: {provider}")
};
}
#endregion
#region Minimal Payload Generators
private static string CreateMinimalHarborPayload()
{
return JsonSerializer.Serialize(new
{
type = "PUSH_ARTIFACT",
occur_at = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
@operator = "test",
event_data = new
{
resources = new[]
{
new
{
digest = "sha256:test123",
tag = "latest",
resource_url = "harbor.example.com/library/test:latest"
}
},
repository = new
{
name = "test",
repo_full_name = "library/test"
}
}
}, JsonOptions);
}
private static string CreateMinimalDockerHubPayload()
{
return JsonSerializer.Serialize(new
{
push_data = new
{
tag = "latest",
pushed_at = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
},
repository = new
{
repo_name = "stellaops/test",
name = "test",
@namespace = "stellaops"
}
}, JsonOptions);
}
private static string CreateMinimalAcrPayload()
{
return JsonSerializer.Serialize(new
{
id = Guid.NewGuid().ToString(),
action = "push",
target = new
{
repository = "stellaops/test",
tag = "latest",
digest = "sha256:test123"
}
}, JsonOptions);
}
private static string CreateMinimalEcrPayload()
{
return JsonSerializer.Serialize(new Dictionary<string, object>
{
["version"] = "0",
["id"] = Guid.NewGuid().ToString(),
["detail-type"] = "ECR Image Action",
["source"] = "aws.ecr",
["detail"] = new Dictionary<string, object>
{
["action-type"] = "PUSH",
["repository-name"] = "stellaops/test",
["image-tag"] = "latest"
}
}, JsonOptions);
}
private static string CreateMinimalGcrPayload()
{
var innerData = new { action = "INSERT", tag = "latest", digest = "sha256:test123" };
var base64Data = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(innerData)));
return JsonSerializer.Serialize(new
{
message = new
{
data = base64Data,
messageId = "gcr-test-123"
}
}, JsonOptions);
}
private static string CreateMinimalGhcrPayload()
{
return JsonSerializer.Serialize(new
{
action = "published",
package = new
{
name = "test-package",
package_type = "container",
package_version = new
{
version = "v1.0.0"
}
},
repository = new
{
full_name = "stellaops/test"
}
}, JsonOptions);
}
private static string CreateMinimalGitHubPushPayload()
{
return JsonSerializer.Serialize(new
{
@ref = "refs/heads/main",
after = "abc123",
repository = new
{
full_name = "stellaops/test",
name = "test",
default_branch = "main"
},
pusher = new { name = "test-user" },
sender = new { login = "test-user", type = "User" },
commits = Array.Empty<object>()
}, JsonOptions);
}
private static string CreateMinimalGitLabPushPayload()
{
return JsonSerializer.Serialize(new
{
object_kind = "push",
@ref = "refs/heads/main",
after = "abc123",
project = new
{
path_with_namespace = "stellaops/test",
name = "test",
default_branch = "main"
},
user_name = "test-user",
commits = Array.Empty<object>()
}, JsonOptions);
}
private static string CreateMinimalGiteaPushPayload()
{
return JsonSerializer.Serialize(new
{
@ref = "refs/heads/main",
after = "abc123",
repository = new
{
full_name = "stellaops/test",
name = "test",
default_branch = "main"
},
pusher = new { login = "test-user" },
sender = new { login = "test-user" },
commits = Array.Empty<object>()
}, JsonOptions);
}
#endregion
#region Validation Helpers
/// <summary>
/// Validates that a webhook payload contains required fields.
/// </summary>
public static bool ValidateRequiredFields(string payload, params string[] fields)
{
try
{
var doc = JsonDocument.Parse(payload);
var root = doc.RootElement;
foreach (var field in fields)
{
if (!HasNestedProperty(root, field.Split('.')))
{
return false;
}
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Extracts a field value from a JSON payload.
/// </summary>
public static string? ExtractField(string payload, string jsonPath)
{
try
{
var doc = JsonDocument.Parse(payload);
var element = doc.RootElement;
foreach (var part in jsonPath.Split('.'))
{
if (!element.TryGetProperty(part, out element))
{
return null;
}
}
return element.ValueKind == JsonValueKind.String
? element.GetString()
: element.GetRawText();
}
catch
{
return null;
}
}
#endregion
#region Private Helpers
private static void SetNestedValue(Dictionary<string, object> dict, string[] path, object value)
{
var current = dict;
for (var i = 0; i < path.Length - 1; i++)
{
if (!current.TryGetValue(path[i], out var next))
{
next = new Dictionary<string, object>();
current[path[i]] = next;
}
if (next is JsonElement jsonElement)
{
var nested = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonElement.GetRawText());
if (nested != null)
{
current[path[i]] = nested;
current = nested;
}
}
else if (next is Dictionary<string, object> nestedDict)
{
current = nestedDict;
}
}
current[path[^1]] = value;
}
private static bool HasNestedProperty(JsonElement element, string[] path)
{
var current = element;
foreach (var part in path)
{
if (current.ValueKind != JsonValueKind.Object ||
!current.TryGetProperty(part, out current))
{
return false;
}
}
return true;
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
#endregion
}