248 lines
8.4 KiB
C#
248 lines
8.4 KiB
C#
// -----------------------------------------------------------------------------
|
|
// AdvisoryCacheKeys.cs
|
|
// Sprint: SPRINT_8200_0013_0001_GW_valkey_advisory_cache
|
|
// Task: VCACHE-8200-004, VCACHE-8200-005, VCACHE-8200-006, VCACHE-8200-007, VCACHE-8200-008
|
|
// Description: Key schema for Concelier Valkey cache
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace StellaOps.Concelier.Cache.Valkey;
|
|
|
|
/// <summary>
|
|
/// Static class for generating Valkey cache keys for canonical advisories.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Key Schema:
|
|
/// <code>
|
|
/// advisory:{merge_hash} -> JSON(CanonicalAdvisory) - TTL based on interest_score
|
|
/// rank:hot -> ZSET { merge_hash: interest_score } - max 10,000 entries
|
|
/// by:purl:{normalized_purl} -> SET { merge_hash, ... } - TTL 24h
|
|
/// by:cve:{cve_id} -> STRING merge_hash - TTL 24h
|
|
/// cache:stats:hits -> INCR counter
|
|
/// cache:stats:misses -> INCR counter
|
|
/// cache:warmup:last -> STRING ISO8601 timestamp
|
|
/// </code>
|
|
/// </remarks>
|
|
public static class AdvisoryCacheKeys
|
|
{
|
|
/// <summary>
|
|
/// Default key prefix for all cache keys.
|
|
/// </summary>
|
|
public const string DefaultPrefix = "concelier:";
|
|
public const int MaxPurlKeyLength = 500;
|
|
|
|
/// <summary>
|
|
/// Key for advisory by merge hash.
|
|
/// Format: {prefix}advisory:{mergeHash}
|
|
/// </summary>
|
|
public static string Advisory(string mergeHash, string prefix = DefaultPrefix)
|
|
=> $"{prefix}advisory:{mergeHash}";
|
|
|
|
/// <summary>
|
|
/// Key for the hot advisory sorted set.
|
|
/// Format: {prefix}rank:hot
|
|
/// </summary>
|
|
public static string HotSet(string prefix = DefaultPrefix)
|
|
=> $"{prefix}rank:hot";
|
|
|
|
/// <summary>
|
|
/// Key for PURL index set.
|
|
/// Format: {prefix}by:purl:{normalizedPurl}
|
|
/// </summary>
|
|
/// <param name="purl">The PURL (will be normalized).</param>
|
|
/// <param name="prefix">Key prefix.</param>
|
|
public static string ByPurl(string purl, string prefix = DefaultPrefix)
|
|
=> $"{prefix}by:purl:{NormalizePurl(purl)}";
|
|
|
|
/// <summary>
|
|
/// Key for CVE mapping.
|
|
/// Format: {prefix}by:cve:{cveId}
|
|
/// </summary>
|
|
/// <param name="cve">The CVE identifier (case-insensitive).</param>
|
|
/// <param name="prefix">Key prefix.</param>
|
|
public static string ByCve(string cve, string prefix = DefaultPrefix)
|
|
=> $"{prefix}by:cve:{cve.ToUpperInvariant()}";
|
|
|
|
/// <summary>
|
|
/// Key for cache hit counter.
|
|
/// Format: {prefix}cache:stats:hits
|
|
/// </summary>
|
|
public static string StatsHits(string prefix = DefaultPrefix)
|
|
=> $"{prefix}cache:stats:hits";
|
|
|
|
/// <summary>
|
|
/// Key for cache miss counter.
|
|
/// Format: {prefix}cache:stats:misses
|
|
/// </summary>
|
|
public static string StatsMisses(string prefix = DefaultPrefix)
|
|
=> $"{prefix}cache:stats:misses";
|
|
|
|
/// <summary>
|
|
/// Key for last warmup timestamp.
|
|
/// Format: {prefix}cache:warmup:last
|
|
/// </summary>
|
|
public static string WarmupLast(string prefix = DefaultPrefix)
|
|
=> $"{prefix}cache:warmup:last";
|
|
|
|
/// <summary>
|
|
/// Key for warmup lock (for distributed coordination).
|
|
/// Format: {prefix}cache:warmup:lock
|
|
/// </summary>
|
|
public static string WarmupLock(string prefix = DefaultPrefix)
|
|
=> $"{prefix}cache:warmup:lock";
|
|
|
|
/// <summary>
|
|
/// Key for total cached advisories gauge.
|
|
/// Format: {prefix}cache:stats:count
|
|
/// </summary>
|
|
public static string StatsCount(string prefix = DefaultPrefix)
|
|
=> $"{prefix}cache:stats:count";
|
|
|
|
/// <summary>
|
|
/// Pattern to match all advisory keys (for scanning/cleanup).
|
|
/// Format: {prefix}advisory:*
|
|
/// </summary>
|
|
public static string AdvisoryPattern(string prefix = DefaultPrefix)
|
|
=> $"{prefix}advisory:*";
|
|
|
|
/// <summary>
|
|
/// Pattern to match all PURL index keys (for scanning/cleanup).
|
|
/// Format: {prefix}by:purl:*
|
|
/// </summary>
|
|
public static string PurlIndexPattern(string prefix = DefaultPrefix)
|
|
=> $"{prefix}by:purl:*";
|
|
|
|
/// <summary>
|
|
/// Pattern to match all CVE mapping keys (for scanning/cleanup).
|
|
/// Format: {prefix}by:cve:*
|
|
/// </summary>
|
|
public static string CveMappingPattern(string prefix = DefaultPrefix)
|
|
=> $"{prefix}by:cve:*";
|
|
|
|
/// <summary>
|
|
/// Normalizes a PURL for use as a cache key.
|
|
/// </summary>
|
|
/// <param name="purl">The PURL to normalize.</param>
|
|
/// <returns>Normalized PURL safe for use in cache keys.</returns>
|
|
/// <remarks>
|
|
/// Normalization:
|
|
/// 1. Lowercase the entire PURL
|
|
/// 2. Replace special characters that may cause issues in keys
|
|
/// 3. Truncate very long PURLs to prevent oversized keys
|
|
/// </remarks>
|
|
public static string NormalizePurl(string purl)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(purl))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
// Normalize to lowercase
|
|
var normalized = purl.ToLowerInvariant();
|
|
|
|
// Replace characters that could cause issues in Redis keys
|
|
// Redis keys should avoid spaces and some special chars for simplicity
|
|
var sb = new StringBuilder(normalized.Length);
|
|
foreach (var c in normalized)
|
|
{
|
|
// Allow alphanumeric, standard PURL chars: : / @ . - _ %
|
|
if (char.IsLetterOrDigit(c) ||
|
|
c is ':' or '/' or '@' or '.' or '-' or '_' or '%')
|
|
{
|
|
sb.Append(c);
|
|
}
|
|
else
|
|
{
|
|
// Replace other chars with underscore
|
|
sb.Append('_');
|
|
}
|
|
}
|
|
|
|
var normalizedKey = sb.ToString();
|
|
if (normalizedKey.Length <= MaxPurlKeyLength)
|
|
{
|
|
return normalizedKey;
|
|
}
|
|
|
|
var hash = ComputeHash(normalizedKey);
|
|
var prefixLength = Math.Max(0, MaxPurlKeyLength - hash.Length - 1);
|
|
if (prefixLength == 0)
|
|
{
|
|
return hash;
|
|
}
|
|
|
|
return normalizedKey[..prefixLength] + "-" + hash;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the merge hash from an advisory key.
|
|
/// </summary>
|
|
/// <param name="key">The full advisory key.</param>
|
|
/// <param name="prefix">The key prefix used.</param>
|
|
/// <returns>The merge hash, or null if key doesn't match expected format.</returns>
|
|
public static string? ExtractMergeHash(string key, string prefix = DefaultPrefix)
|
|
{
|
|
var expectedStart = $"{prefix}advisory:";
|
|
if (key.StartsWith(expectedStart, StringComparison.Ordinal))
|
|
{
|
|
return key[expectedStart.Length..];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the PURL from a PURL index key.
|
|
/// </summary>
|
|
/// <param name="key">The full PURL index key.</param>
|
|
/// <param name="prefix">The key prefix used.</param>
|
|
/// <returns>The normalized PURL, or null if key doesn't match expected format.</returns>
|
|
public static string? ExtractPurl(string key, string prefix = DefaultPrefix)
|
|
{
|
|
var expectedStart = $"{prefix}by:purl:";
|
|
if (key.StartsWith(expectedStart, StringComparison.Ordinal))
|
|
{
|
|
return key[expectedStart.Length..];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the CVE from a CVE mapping key.
|
|
/// </summary>
|
|
/// <param name="key">The full CVE mapping key.</param>
|
|
/// <param name="prefix">The key prefix used.</param>
|
|
/// <returns>The CVE identifier, or null if key doesn't match expected format.</returns>
|
|
public static string? ExtractCve(string key, string prefix = DefaultPrefix)
|
|
{
|
|
var expectedStart = $"{prefix}by:cve:";
|
|
if (key.StartsWith(expectedStart, StringComparison.Ordinal))
|
|
{
|
|
return key[expectedStart.Length..];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static string ComputeHash(string value)
|
|
{
|
|
using var sha = SHA256.Create();
|
|
var bytes = Encoding.UTF8.GetBytes(value);
|
|
var hash = sha.ComputeHash(bytes);
|
|
return ToLowerHex(hash);
|
|
}
|
|
|
|
private static string ToLowerHex(byte[] bytes)
|
|
{
|
|
const string hex = "0123456789abcdef";
|
|
var chars = new char[bytes.Length * 2];
|
|
for (var i = 0; i < bytes.Length; i++)
|
|
{
|
|
var b = bytes[i];
|
|
chars[i * 2] = hex[b >> 4];
|
|
chars[i * 2 + 1] = hex[b & 0xF];
|
|
}
|
|
return new string(chars);
|
|
}
|
|
}
|