- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
357 lines
16 KiB
C#
357 lines
16 KiB
C#
// -----------------------------------------------------------------------------
|
|
// DeltaCommandGroup.cs
|
|
// Sprint: SPRINT_5100_0002_0003_delta_verdict_generator
|
|
// Description: CLI commands for delta verdict operations
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System.CommandLine;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using StellaOps.DeltaVerdict.Engine;
|
|
using StellaOps.DeltaVerdict.Models;
|
|
using StellaOps.DeltaVerdict.Oci;
|
|
using StellaOps.DeltaVerdict.Policy;
|
|
using StellaOps.DeltaVerdict.Serialization;
|
|
using StellaOps.DeltaVerdict.Signing;
|
|
|
|
namespace StellaOps.Cli.Commands;
|
|
|
|
public static class DeltaCommandGroup
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
WriteIndented = true,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
public static Command BuildDeltaCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var delta = new Command("delta", "Delta verdict operations");
|
|
|
|
delta.Add(BuildComputeCommand(verboseOption, cancellationToken));
|
|
delta.Add(BuildCheckCommand(verboseOption, cancellationToken));
|
|
delta.Add(BuildAttachCommand(verboseOption, cancellationToken));
|
|
delta.Add(BuildVerifyCommand(verboseOption, cancellationToken));
|
|
delta.Add(BuildPushCommand(verboseOption, cancellationToken));
|
|
|
|
return delta;
|
|
}
|
|
|
|
private static Command BuildComputeCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var baseOption = new Option<string>("--base") { Description = "Base verdict JSON file", Required = true };
|
|
var headOption = new Option<string>("--head") { Description = "Head verdict JSON file", Required = true };
|
|
var outputOption = new Option<string?>("--output") { Description = "Output delta JSON path" };
|
|
var signOption = new Option<bool>("--sign") { Description = "Sign delta verdict" };
|
|
var keyIdOption = new Option<string?>("--key-id") { Description = "Signing key identifier" };
|
|
var secretOption = new Option<string?>("--secret") { Description = "Base64 secret for HMAC signing" };
|
|
|
|
var compute = new Command("compute", "Compute delta between two verdicts");
|
|
compute.Add(baseOption);
|
|
compute.Add(headOption);
|
|
compute.Add(outputOption);
|
|
compute.Add(signOption);
|
|
compute.Add(keyIdOption);
|
|
compute.Add(secretOption);
|
|
compute.Add(verboseOption);
|
|
|
|
compute.SetAction(async (parseResult, _) =>
|
|
{
|
|
var basePath = parseResult.GetValue(baseOption) ?? string.Empty;
|
|
var headPath = parseResult.GetValue(headOption) ?? string.Empty;
|
|
var outputPath = parseResult.GetValue(outputOption);
|
|
var sign = parseResult.GetValue(signOption);
|
|
var keyId = parseResult.GetValue(keyIdOption) ?? "delta-dev";
|
|
var secret = parseResult.GetValue(secretOption);
|
|
|
|
var baseVerdict = VerdictSerializer.Deserialize(await File.ReadAllTextAsync(basePath, cancellationToken));
|
|
var headVerdict = VerdictSerializer.Deserialize(await File.ReadAllTextAsync(headPath, cancellationToken));
|
|
|
|
var engine = new DeltaComputationEngine();
|
|
var deltaVerdict = engine.ComputeDelta(baseVerdict, headVerdict);
|
|
deltaVerdict = DeltaVerdictSerializer.WithDigest(deltaVerdict);
|
|
|
|
if (sign)
|
|
{
|
|
var signer = new DeltaSigningService();
|
|
deltaVerdict = await signer.SignAsync(deltaVerdict, new SigningOptions
|
|
{
|
|
KeyId = keyId,
|
|
SecretBase64 = secret ?? Convert.ToBase64String("delta-dev-secret"u8.ToArray())
|
|
}, cancellationToken);
|
|
}
|
|
|
|
var json = DeltaVerdictSerializer.Serialize(deltaVerdict);
|
|
if (!string.IsNullOrWhiteSpace(outputPath))
|
|
{
|
|
await File.WriteAllTextAsync(outputPath, json, cancellationToken);
|
|
return 0;
|
|
}
|
|
|
|
Console.WriteLine(json);
|
|
return 0;
|
|
});
|
|
|
|
return compute;
|
|
}
|
|
|
|
private static Command BuildCheckCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var deltaOption = new Option<string>("--delta") { Description = "Delta verdict JSON file", Required = true };
|
|
var budgetOption = new Option<string?>("--budget") { Description = "Budget profile (prod|stage|dev) or JSON path", Arity = ArgumentArity.ZeroOrOne };
|
|
var outputOption = new Option<string?>("--output") { Description = "Output format (text|json)", Arity = ArgumentArity.ZeroOrOne };
|
|
|
|
var check = new Command("check", "Check delta against risk budget");
|
|
check.Add(deltaOption);
|
|
check.Add(budgetOption);
|
|
check.Add(outputOption);
|
|
check.Add(verboseOption);
|
|
|
|
check.SetAction(async (parseResult, _) =>
|
|
{
|
|
var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty;
|
|
var budgetValue = parseResult.GetValue(budgetOption);
|
|
var outputFormat = parseResult.GetValue(outputOption) ?? "text";
|
|
|
|
var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken));
|
|
var budget = await ResolveBudgetAsync(budgetValue, cancellationToken);
|
|
|
|
var evaluator = new RiskBudgetEvaluator();
|
|
var result = evaluator.Evaluate(delta, budget);
|
|
|
|
if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions));
|
|
}
|
|
else
|
|
{
|
|
var status = result.IsWithinBudget ? "[PASS]" : "[FAIL]";
|
|
Console.WriteLine($"{status} Delta Budget Check");
|
|
Console.WriteLine($" Total Changes: {result.Delta.Summary.TotalChanges}");
|
|
Console.WriteLine($" Magnitude: {result.Delta.Summary.Magnitude}");
|
|
|
|
if (result.Violations.Count > 0)
|
|
{
|
|
Console.WriteLine(" Violations:");
|
|
foreach (var violation in result.Violations)
|
|
{
|
|
Console.WriteLine($" - {violation.Category}: {violation.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
return result.IsWithinBudget ? 0 : 2;
|
|
});
|
|
|
|
return check;
|
|
}
|
|
|
|
private static Command BuildAttachCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var deltaOption = new Option<string>("--delta") { Description = "Delta verdict JSON file", Required = true };
|
|
var artifactOption = new Option<string>("--artifact") { Description = "OCI artifact reference", Required = true };
|
|
var outputOption = new Option<string?>("--output") { Description = "Output format (text|json)" };
|
|
|
|
var attach = new Command("attach", "Prepare OCI attachment metadata for delta verdict");
|
|
attach.Add(deltaOption);
|
|
attach.Add(artifactOption);
|
|
attach.Add(outputOption);
|
|
attach.Add(verboseOption);
|
|
|
|
attach.SetAction(async (parseResult, _) =>
|
|
{
|
|
var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty;
|
|
var artifactRef = parseResult.GetValue(artifactOption) ?? string.Empty;
|
|
var outputFormat = parseResult.GetValue(outputOption) ?? "json";
|
|
|
|
var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken));
|
|
var attacher = new DeltaOciAttacher();
|
|
var attachment = attacher.CreateAttachment(delta, artifactRef);
|
|
|
|
if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(attachment, JsonOptions));
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Delta OCI Attachment");
|
|
Console.WriteLine($" Artifact: {attachment.ArtifactReference}");
|
|
Console.WriteLine($" MediaType: {attachment.MediaType}");
|
|
Console.WriteLine($" PayloadBytes: {attachment.Payload.Length}");
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
|
|
return attach;
|
|
}
|
|
|
|
private static async Task<RiskBudget> ResolveBudgetAsync(string? budgetValue, CancellationToken cancellationToken)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(budgetValue) && File.Exists(budgetValue))
|
|
{
|
|
var json = await File.ReadAllTextAsync(budgetValue, cancellationToken);
|
|
return JsonSerializer.Deserialize<RiskBudget>(json, JsonOptions)
|
|
?? new RiskBudget();
|
|
}
|
|
|
|
return (budgetValue ?? "prod").ToLowerInvariant() switch
|
|
{
|
|
"dev" => new RiskBudget
|
|
{
|
|
MaxNewCriticalVulnerabilities = 2,
|
|
MaxNewHighVulnerabilities = 5,
|
|
MaxRiskScoreIncrease = 25,
|
|
MaxMagnitude = DeltaMagnitude.Large
|
|
},
|
|
"stage" => new RiskBudget
|
|
{
|
|
MaxNewCriticalVulnerabilities = 1,
|
|
MaxNewHighVulnerabilities = 3,
|
|
MaxRiskScoreIncrease = 15,
|
|
MaxMagnitude = DeltaMagnitude.Medium
|
|
},
|
|
_ => new RiskBudget
|
|
{
|
|
MaxNewCriticalVulnerabilities = 0,
|
|
MaxNewHighVulnerabilities = 1,
|
|
MaxRiskScoreIncrease = 5,
|
|
MaxMagnitude = DeltaMagnitude.Small
|
|
}
|
|
};
|
|
}
|
|
|
|
private static Command BuildVerifyCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var deltaOption = new Option<string>("--delta") { Description = "Delta verdict JSON file", Required = true };
|
|
var keyIdOption = new Option<string?>("--key-id") { Description = "Signing key identifier" };
|
|
var secretOption = new Option<string?>("--secret") { Description = "Base64 secret for HMAC verification" };
|
|
var outputOption = new Option<string?>("--output") { Description = "Output format (text|json)", Arity = ArgumentArity.ZeroOrOne };
|
|
|
|
var verify = new Command("verify", "Verify delta verdict signature");
|
|
verify.Add(deltaOption);
|
|
verify.Add(keyIdOption);
|
|
verify.Add(secretOption);
|
|
verify.Add(outputOption);
|
|
verify.Add(verboseOption);
|
|
|
|
verify.SetAction(async (parseResult, _) =>
|
|
{
|
|
var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty;
|
|
var keyId = parseResult.GetValue(keyIdOption) ?? "delta-dev";
|
|
var secret = parseResult.GetValue(secretOption);
|
|
var outputFormat = parseResult.GetValue(outputOption) ?? "text";
|
|
|
|
var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken));
|
|
|
|
var signer = new DeltaSigningService();
|
|
var result = await signer.VerifyAsync(delta, new VerificationOptions
|
|
{
|
|
KeyId = keyId,
|
|
SecretBase64 = secret ?? Convert.ToBase64String("delta-dev-secret"u8.ToArray())
|
|
}, cancellationToken);
|
|
|
|
if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(new
|
|
{
|
|
isValid = result.IsValid,
|
|
error = result.Error,
|
|
deltaDigest = delta.DeltaDigest
|
|
}, JsonOptions));
|
|
}
|
|
else
|
|
{
|
|
var status = result.IsValid ? "[PASS]" : "[FAIL]";
|
|
Console.WriteLine($"{status} Delta Signature Verification");
|
|
Console.WriteLine($" Delta Digest: {delta.DeltaDigest ?? "N/A"}");
|
|
Console.WriteLine($" Valid: {result.IsValid}");
|
|
if (!string.IsNullOrEmpty(result.Error))
|
|
{
|
|
Console.WriteLine($" Error: {result.Error}");
|
|
}
|
|
}
|
|
|
|
return result.IsValid ? 0 : 1;
|
|
});
|
|
|
|
return verify;
|
|
}
|
|
|
|
private static Command BuildPushCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var deltaOption = new Option<string>("--delta") { Description = "Delta verdict JSON file", Required = true };
|
|
var targetOption = new Option<string>("--target") { Description = "Target OCI artifact reference (e.g., registry.example.com/repo:tag)", Required = true };
|
|
var dryRunOption = new Option<bool>("--dry-run") { Description = "Preview push without executing" };
|
|
var outputOption = new Option<string?>("--output") { Description = "Output format (text|json)" };
|
|
|
|
var push = new Command("push", "Push delta verdict to OCI registry as referrer");
|
|
push.Add(deltaOption);
|
|
push.Add(targetOption);
|
|
push.Add(dryRunOption);
|
|
push.Add(outputOption);
|
|
push.Add(verboseOption);
|
|
|
|
push.SetAction(async (parseResult, _) =>
|
|
{
|
|
var deltaPath = parseResult.GetValue(deltaOption) ?? string.Empty;
|
|
var targetRef = parseResult.GetValue(targetOption) ?? string.Empty;
|
|
var dryRun = parseResult.GetValue(dryRunOption);
|
|
var outputFormat = parseResult.GetValue(outputOption) ?? "text";
|
|
|
|
var delta = DeltaVerdictSerializer.Deserialize(await File.ReadAllTextAsync(deltaPath, cancellationToken));
|
|
var attacher = new DeltaOciAttacher();
|
|
var attachment = attacher.CreateAttachment(delta, targetRef);
|
|
|
|
if (dryRun)
|
|
{
|
|
if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(new
|
|
{
|
|
dryRun = true,
|
|
artifact = attachment.ArtifactReference,
|
|
mediaType = attachment.MediaType,
|
|
payloadSize = attachment.Payload.Length,
|
|
annotations = attachment.Annotations
|
|
}, JsonOptions));
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[DRY-RUN] Delta OCI Push");
|
|
Console.WriteLine($" Target: {attachment.ArtifactReference}");
|
|
Console.WriteLine($" MediaType: {attachment.MediaType}");
|
|
Console.WriteLine($" PayloadSize: {attachment.Payload.Length} bytes");
|
|
Console.WriteLine($" Annotations:");
|
|
foreach (var (key, value) in attachment.Annotations)
|
|
{
|
|
Console.WriteLine($" {key}: {value}");
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// For actual push, we need to use the OCI pusher infrastructure
|
|
// This would require DI container setup; for CLI direct usage, output the attachment info
|
|
if (string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(attachment, JsonOptions));
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Delta OCI Push Prepared");
|
|
Console.WriteLine($" Target: {attachment.ArtifactReference}");
|
|
Console.WriteLine($" MediaType: {attachment.MediaType}");
|
|
Console.WriteLine($" PayloadSize: {attachment.Payload.Length} bytes");
|
|
Console.WriteLine(" Use 'oras push' or OCI-compliant tooling to complete the push.");
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
|
|
return push;
|
|
}
|
|
}
|