up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
13
ops/crypto/sim-crypto-service/Dockerfile
Normal file
13
ops/crypto/sim-crypto-service/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY SimCryptoService.csproj .
|
||||
RUN dotnet restore
|
||||
COPY . .
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8080
|
||||
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
ENTRYPOINT ["dotnet", "SimCryptoService.dll"]
|
||||
128
ops/crypto/sim-crypto-service/Program.cs
Normal file
128
ops/crypto/sim-crypto-service/Program.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
|
||||
// Static key material for simulations (not for production use).
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var ecdsaPublic = ecdsa.ExportSubjectPublicKeyInfo();
|
||||
|
||||
byte[] Sign(string message, string algorithm)
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes(message);
|
||||
var lower = algorithm.Trim().ToLowerInvariant();
|
||||
var upper = algorithm.Trim().ToUpperInvariant();
|
||||
|
||||
if (lower is "pq.dilithium3" or "pq.falcon512" or "pq.sim" || upper is "DILITHIUM3" or "FALCON512")
|
||||
{
|
||||
return HMACSHA256.HashData(Encoding.UTF8.GetBytes("pq-sim-key"), data);
|
||||
}
|
||||
|
||||
if (lower is "ru.magma.sim" or "ru.kuznyechik.sim" || upper is "GOST12-256" or "GOST12-512")
|
||||
{
|
||||
return HMACSHA256.HashData(Encoding.UTF8.GetBytes("gost-sim-key"), data);
|
||||
}
|
||||
|
||||
if (lower is "sm.sim" or "sm2.sim" || upper is "SM2")
|
||||
{
|
||||
return HMACSHA256.HashData(Encoding.UTF8.GetBytes("sm-sim-key"), data);
|
||||
}
|
||||
|
||||
return ecdsa.SignData(data, HashAlgorithmName.SHA256);
|
||||
}
|
||||
|
||||
bool Verify(string message, string algorithm, byte[] signature)
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes(message);
|
||||
var lower = algorithm.Trim().ToLowerInvariant();
|
||||
var upper = algorithm.Trim().ToUpperInvariant();
|
||||
|
||||
if (lower is "pq.dilithium3" or "pq.falcon512" or "pq.sim" || upper is "DILITHIUM3" or "FALCON512")
|
||||
{
|
||||
return CryptographicOperations.FixedTimeEquals(HMACSHA256.HashData(Encoding.UTF8.GetBytes("pq-sim-key"), data), signature);
|
||||
}
|
||||
|
||||
if (lower is "ru.magma.sim" or "ru.kuznyechik.sim" || upper is "GOST12-256" or "GOST12-512")
|
||||
{
|
||||
return CryptographicOperations.FixedTimeEquals(HMACSHA256.HashData(Encoding.UTF8.GetBytes("gost-sim-key"), data), signature);
|
||||
}
|
||||
|
||||
if (lower is "sm.sim" or "sm2.sim" || upper is "SM2")
|
||||
{
|
||||
return CryptographicOperations.FixedTimeEquals(HMACSHA256.HashData(Encoding.UTF8.GetBytes("sm-sim-key"), data), signature);
|
||||
}
|
||||
|
||||
return ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256);
|
||||
}
|
||||
|
||||
app.MapPost("/sign", (SignRequest request) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Algorithm) || string.IsNullOrWhiteSpace(request.Message))
|
||||
{
|
||||
return Results.BadRequest("Algorithm and message are required.");
|
||||
}
|
||||
|
||||
var sig = Sign(request.Message, request.Algorithm);
|
||||
return Results.Json(new SignResponse(Convert.ToBase64String(sig), request.Algorithm));
|
||||
});
|
||||
|
||||
app.MapPost("/verify", (VerifyRequest request) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Algorithm) || string.IsNullOrWhiteSpace(request.Message) || string.IsNullOrWhiteSpace(request.SignatureBase64))
|
||||
{
|
||||
return Results.BadRequest("Algorithm, message, and signature are required.");
|
||||
}
|
||||
|
||||
var sig = Convert.FromBase64String(request.SignatureBase64);
|
||||
var ok = Verify(request.Message, request.Algorithm, sig);
|
||||
return Results.Json(new VerifyResponse(ok, request.Algorithm));
|
||||
});
|
||||
|
||||
app.MapGet("/keys", () =>
|
||||
{
|
||||
return Results.Json(new KeysResponse(
|
||||
Convert.ToBase64String(ecdsaPublic),
|
||||
"nistp256",
|
||||
new[]
|
||||
{
|
||||
"pq.sim",
|
||||
"DILITHIUM3",
|
||||
"FALCON512",
|
||||
"ru.magma.sim",
|
||||
"ru.kuznyechik.sim",
|
||||
"GOST12-256",
|
||||
"GOST12-512",
|
||||
"sm.sim",
|
||||
"SM2",
|
||||
"fips.sim",
|
||||
"eidas.sim",
|
||||
"kcmvp.sim",
|
||||
"world.sim"
|
||||
}));
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
public record SignRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record SignResponse(
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record VerifyRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record VerifyResponse(
|
||||
[property: JsonPropertyName("ok")] bool Ok,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
public record KeysResponse(
|
||||
[property: JsonPropertyName("public_key_b64")] string PublicKeyBase64,
|
||||
[property: JsonPropertyName("curve")] string Curve,
|
||||
[property: JsonPropertyName("simulated_providers")] IEnumerable<string> Providers);
|
||||
32
ops/crypto/sim-crypto-service/README.md
Normal file
32
ops/crypto/sim-crypto-service/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Sim Crypto Service · 2025-12-11
|
||||
|
||||
Minimal HTTP service to simulate sovereign crypto providers when licensed hardware or certified modules are unavailable.
|
||||
|
||||
## Endpoints
|
||||
- `POST /sign` — body: `{"message":"<string>","algorithm":"<id>"}`; returns `{"signature_b64":"...","algorithm":"<id>"}`.
|
||||
- `POST /verify` — body: `{"message":"<string>","algorithm":"<id>","signature_b64":"..."}`; returns `{"ok":true/false,"algorithm":"<id>"}`.
|
||||
- `GET /keys` — returns public key info for simulated providers.
|
||||
|
||||
## Supported simulated provider IDs
|
||||
- GOST: `GOST12-256`, `GOST12-512`, `ru.magma.sim`, `ru.kuznyechik.sim` — deterministic HMAC-SHA256.
|
||||
- SM: `SM2`, `sm.sim`, `sm2.sim` — deterministic HMAC-SHA256.
|
||||
- PQ: `DILITHIUM3`, `FALCON512`, `pq.sim` — deterministic HMAC-SHA256.
|
||||
- FIPS/eIDAS/KCMVP/world: `ES256`, `ES384`, `ES512`, `fips.sim`, `eidas.sim`, `kcmvp.sim`, `world.sim` — ECDSA P-256 with a static key.
|
||||
|
||||
## Build & run
|
||||
```bash
|
||||
dotnet run -c Release --project ops/crypto/sim-crypto-service/SimCryptoService.csproj
|
||||
# or
|
||||
docker build -t sim-crypto -f ops/crypto/sim-crypto-service/Dockerfile ops/crypto/sim-crypto-service
|
||||
docker run --rm -p 8080:8080 sim-crypto
|
||||
```
|
||||
|
||||
## Wiring
|
||||
- Set `STELLAOPS_CRYPTO_ENABLE_SIM=1` to append `sim.crypto.remote` to the registry preference order.
|
||||
- Point the provider at the service: `STELLAOPS_CRYPTO_SIM_URL=http://localhost:8080` (or bind `StellaOps:Crypto:Sim:BaseAddress` in config).
|
||||
- `SimRemoteProviderOptions.Algorithms` already includes the IDs above; extend if you need extra aliases.
|
||||
|
||||
## Notes
|
||||
- Replaces the legacy SM-only simulator; use this unified service for SM, PQ, GOST, and FIPS/eIDAS/KCMVP placeholders.
|
||||
- Deterministic HMAC for SM/PQ/GOST; static ECDSA key for the rest. Not for production use.
|
||||
- No licensed binaries are shipped; everything is BCL-only.
|
||||
10
ops/crypto/sim-crypto-service/SimCryptoService.csproj
Normal file
10
ops/crypto/sim-crypto-service/SimCryptoService.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<PackageTargetFallback></PackageTargetFallback>
|
||||
<AssetTargetFallback></AssetTargetFallback>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
85
ops/crypto/sim-crypto-smoke/Program.cs
Normal file
85
ops/crypto/sim-crypto-smoke/Program.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var baseUrl = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_SIM_URL") ?? "http://localhost:8080";
|
||||
var algList = Environment.GetEnvironmentVariable("SIM_ALGORITHMS")?
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
: new[] { "SM2", "pq.sim", "ES256" };
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(baseUrl) };
|
||||
|
||||
static async Task<(bool Ok, string Error)> SignAndVerify(HttpClient client, string algorithm, string message, CancellationToken ct)
|
||||
{
|
||||
var signPayload = new SignRequest(message, algorithm);
|
||||
var signResponse = await client.PostAsJsonAsync("/sign", signPayload, ct).ConfigureAwait(false);
|
||||
if (!signResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"sign failed: {(int)signResponse.StatusCode} {signResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var signResult = await signResponse.Content.ReadFromJsonAsync<SignResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (signResult is null || string.IsNullOrWhiteSpace(signResult.SignatureBase64))
|
||||
{
|
||||
return (false, "sign returned empty payload");
|
||||
}
|
||||
|
||||
var verifyPayload = new VerifyRequest(message, signResult.SignatureBase64, algorithm);
|
||||
var verifyResponse = await client.PostAsJsonAsync("/verify", verifyPayload, ct).ConfigureAwait(false);
|
||||
if (!verifyResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return (false, $"verify failed: {(int)verifyResponse.StatusCode} {verifyResponse.ReasonPhrase}");
|
||||
}
|
||||
|
||||
var verifyResult = await verifyResponse.Content.ReadFromJsonAsync<VerifyResponse>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (verifyResult?.Ok is not true)
|
||||
{
|
||||
return (false, "verify returned false");
|
||||
}
|
||||
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
var failures = new List<string>();
|
||||
|
||||
foreach (var alg in algList)
|
||||
{
|
||||
var (ok, error) = await SignAndVerify(client, alg, "stellaops-sim-smoke", cts.Token);
|
||||
if (!ok)
|
||||
{
|
||||
failures.Add($"{alg}: {error}");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[ok] {alg} via {baseUrl}");
|
||||
}
|
||||
|
||||
if (failures.Count > 0)
|
||||
{
|
||||
Console.Error.WriteLine("Simulation smoke failed:");
|
||||
foreach (var f in failures)
|
||||
{
|
||||
Console.Error.WriteLine($" - {f}");
|
||||
}
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
Console.WriteLine("Simulation smoke passed.");
|
||||
|
||||
internal sealed record SignRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record SignResponse(
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record VerifyRequest(
|
||||
[property: JsonPropertyName("message")] string Message,
|
||||
[property: JsonPropertyName("signature_b64")] string SignatureBase64,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
|
||||
internal sealed record VerifyResponse(
|
||||
[property: JsonPropertyName("ok")] bool Ok,
|
||||
[property: JsonPropertyName("algorithm")] string Algorithm);
|
||||
9
ops/crypto/sim-crypto-smoke/SimCryptoSmoke.csproj
Normal file
9
ops/crypto/sim-crypto-smoke/SimCryptoSmoke.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
278
ops/devops/risk-bundle/build-bundle.sh
Normal file
278
ops/devops/risk-bundle/build-bundle.sh
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env bash
|
||||
# Risk Bundle Builder Script
|
||||
# RISK-BUNDLE-69-002: CI/offline kit pipeline integration
|
||||
#
|
||||
# Usage: build-bundle.sh --output <dir> [--fixtures-only] [--include-osv]
|
||||
#
|
||||
# This script builds a risk bundle for offline kit distribution.
|
||||
# In --fixtures-only mode, it generates a deterministic fixture bundle
|
||||
# suitable for CI testing without requiring live provider data.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
# Defaults
|
||||
OUTPUT_DIR=""
|
||||
FIXTURES_ONLY=false
|
||||
INCLUDE_OSV=false
|
||||
BUNDLE_ID=""
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--fixtures-only)
|
||||
FIXTURES_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--include-osv)
|
||||
INCLUDE_OSV=true
|
||||
shift
|
||||
;;
|
||||
--bundle-id)
|
||||
BUNDLE_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: build-bundle.sh --output <dir> [--fixtures-only] [--include-osv] [--bundle-id <id>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --output <dir> Output directory for bundle artifacts (required)"
|
||||
echo " --fixtures-only Use fixture data instead of live provider downloads"
|
||||
echo " --include-osv Include OSV providers (larger bundle)"
|
||||
echo " --bundle-id <id> Custom bundle ID (default: auto-generated)"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required arguments
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
echo "Error: --output is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate bundle ID if not provided
|
||||
if [[ -z "$BUNDLE_ID" ]]; then
|
||||
BUNDLE_ID="risk-bundle-$(date -u +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
|
||||
echo "=== Risk Bundle Builder ==="
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo "Bundle ID: $BUNDLE_ID"
|
||||
echo "Fixtures only: $FIXTURES_ONLY"
|
||||
echo "Include OSV: $INCLUDE_OSV"
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Create temporary working directory
|
||||
WORK_DIR=$(mktemp -d)
|
||||
trap "rm -rf $WORK_DIR" EXIT
|
||||
|
||||
echo ""
|
||||
echo "=== Preparing provider data ==="
|
||||
|
||||
# Provider directories
|
||||
mkdir -p "$WORK_DIR/providers/cisa-kev"
|
||||
mkdir -p "$WORK_DIR/providers/first-epss"
|
||||
mkdir -p "$WORK_DIR/manifests"
|
||||
mkdir -p "$WORK_DIR/signatures"
|
||||
|
||||
# Fixed timestamp for deterministic builds (2024-01-01 00:00:00 UTC)
|
||||
FIXED_TIMESTAMP="2024-01-01T00:00:00Z"
|
||||
FIXED_EPOCH=1704067200
|
||||
|
||||
if [[ "$FIXTURES_ONLY" == "true" ]]; then
|
||||
echo "Using fixture data..."
|
||||
|
||||
# Create CISA KEV fixture (mandatory provider)
|
||||
cat > "$WORK_DIR/providers/cisa-kev/snapshot" <<'EOF'
|
||||
{
|
||||
"catalogVersion": "2024.12.11",
|
||||
"dateReleased": "2024-12-11T00:00:00Z",
|
||||
"count": 3,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cveID": "CVE-2024-0001",
|
||||
"vendorProject": "Example Vendor",
|
||||
"product": "Example Product",
|
||||
"vulnerabilityName": "Example Vulnerability 1",
|
||||
"dateAdded": "2024-01-15",
|
||||
"shortDescription": "Test vulnerability for CI fixtures",
|
||||
"requiredAction": "Apply updates per vendor instructions",
|
||||
"dueDate": "2024-02-05",
|
||||
"knownRansomwareCampaignUse": "Unknown"
|
||||
},
|
||||
{
|
||||
"cveID": "CVE-2024-0002",
|
||||
"vendorProject": "Another Vendor",
|
||||
"product": "Another Product",
|
||||
"vulnerabilityName": "Example Vulnerability 2",
|
||||
"dateAdded": "2024-02-01",
|
||||
"shortDescription": "Another test vulnerability",
|
||||
"requiredAction": "Apply updates per vendor instructions",
|
||||
"dueDate": "2024-02-22",
|
||||
"knownRansomwareCampaignUse": "Known"
|
||||
},
|
||||
{
|
||||
"cveID": "CVE-2024-0003",
|
||||
"vendorProject": "Third Vendor",
|
||||
"product": "Third Product",
|
||||
"vulnerabilityName": "Example Vulnerability 3",
|
||||
"dateAdded": "2024-03-01",
|
||||
"shortDescription": "Third test vulnerability",
|
||||
"requiredAction": "Apply updates per vendor instructions",
|
||||
"dueDate": "2024-03-22",
|
||||
"knownRansomwareCampaignUse": "Unknown"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create FIRST EPSS fixture (optional provider)
|
||||
cat > "$WORK_DIR/providers/first-epss/snapshot" <<'EOF'
|
||||
{
|
||||
"model_version": "v2024.01.01",
|
||||
"score_date": "2024-12-11",
|
||||
"scores": [
|
||||
{"cve": "CVE-2024-0001", "epss": 0.00043, "percentile": 0.08},
|
||||
{"cve": "CVE-2024-0002", "epss": 0.00156, "percentile": 0.45},
|
||||
{"cve": "CVE-2024-0003", "epss": 0.00089, "percentile": 0.21}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Include OSV if requested
|
||||
if [[ "$INCLUDE_OSV" == "true" ]]; then
|
||||
mkdir -p "$WORK_DIR/providers/osv"
|
||||
cat > "$WORK_DIR/providers/osv/snapshot" <<'EOF'
|
||||
{
|
||||
"source": "osv",
|
||||
"updated": "2024-12-11T00:00:00Z",
|
||||
"advisories": [
|
||||
{"id": "GHSA-test-0001", "modified": "2024-01-15T00:00:00Z", "aliases": ["CVE-2024-0001"]},
|
||||
{"id": "GHSA-test-0002", "modified": "2024-02-01T00:00:00Z", "aliases": ["CVE-2024-0002"]}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
else
|
||||
echo "Live provider download not yet implemented"
|
||||
echo "Use --fixtures-only for CI testing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Computing hashes ==="
|
||||
|
||||
# Compute hashes for each provider file
|
||||
CISA_HASH=$(sha256sum "$WORK_DIR/providers/cisa-kev/snapshot" | cut -d' ' -f1)
|
||||
EPSS_HASH=$(sha256sum "$WORK_DIR/providers/first-epss/snapshot" | cut -d' ' -f1)
|
||||
|
||||
echo "cisa-kev hash: $CISA_HASH"
|
||||
echo "first-epss hash: $EPSS_HASH"
|
||||
|
||||
PROVIDERS_JSON="[
|
||||
{\"providerId\": \"cisa-kev\", \"digest\": \"sha256:$CISA_HASH\", \"snapshotDate\": \"$FIXED_TIMESTAMP\", \"optional\": false},
|
||||
{\"providerId\": \"first-epss\", \"digest\": \"sha256:$EPSS_HASH\", \"snapshotDate\": \"$FIXED_TIMESTAMP\", \"optional\": true}"
|
||||
|
||||
if [[ "$INCLUDE_OSV" == "true" ]]; then
|
||||
OSV_HASH=$(sha256sum "$WORK_DIR/providers/osv/snapshot" | cut -d' ' -f1)
|
||||
echo "osv hash: $OSV_HASH"
|
||||
PROVIDERS_JSON="$PROVIDERS_JSON,
|
||||
{\"providerId\": \"osv\", \"digest\": \"sha256:$OSV_HASH\", \"snapshotDate\": \"$FIXED_TIMESTAMP\", \"optional\": true}"
|
||||
fi
|
||||
|
||||
PROVIDERS_JSON="$PROVIDERS_JSON
|
||||
]"
|
||||
|
||||
# Compute inputs hash (hash of all provider hashes sorted)
|
||||
INPUTS_HASH=$(echo -n "$CISA_HASH$EPSS_HASH" | sha256sum | cut -d' ' -f1)
|
||||
echo "inputs hash: $INPUTS_HASH"
|
||||
|
||||
echo ""
|
||||
echo "=== Creating manifest ==="
|
||||
|
||||
# Create provider manifest
|
||||
cat > "$WORK_DIR/manifests/provider-manifest.json" <<EOF
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"bundleId": "$BUNDLE_ID",
|
||||
"createdAt": "$FIXED_TIMESTAMP",
|
||||
"inputsHash": "sha256:$INPUTS_HASH",
|
||||
"providers": $PROVIDERS_JSON
|
||||
}
|
||||
EOF
|
||||
|
||||
MANIFEST_HASH=$(sha256sum "$WORK_DIR/manifests/provider-manifest.json" | cut -d' ' -f1)
|
||||
echo "manifest hash: $MANIFEST_HASH"
|
||||
|
||||
# Create DSSE signature for manifest (using HMAC with test key for fixtures)
|
||||
if [[ "$FIXTURES_ONLY" == "true" ]]; then
|
||||
# Create stub DSSE envelope for fixtures
|
||||
MANIFEST_B64=$(base64 -w0 "$WORK_DIR/manifests/provider-manifest.json" 2>/dev/null || base64 "$WORK_DIR/manifests/provider-manifest.json")
|
||||
cat > "$WORK_DIR/signatures/provider-manifest.dsse" <<EOF
|
||||
{
|
||||
"payloadType": "application/vnd.stellaops.risk-bundle.manifest+json",
|
||||
"payload": "$MANIFEST_B64",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "risk-bundle-hmac-fixtures",
|
||||
"sig": "fixture-signature-for-ci-testing"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Building tar.gz bundle ==="
|
||||
|
||||
# Create the tar archive with deterministic ordering and timestamps
|
||||
cd "$WORK_DIR"
|
||||
|
||||
# Create file list in deterministic order
|
||||
find . -type f | sort > /tmp/bundle-files.txt
|
||||
|
||||
# Create tar with fixed mtime
|
||||
tar --mtime="@$FIXED_EPOCH" \
|
||||
--sort=name \
|
||||
--owner=0 --group=0 \
|
||||
--numeric-owner \
|
||||
-cvf "$OUTPUT_DIR/risk-bundle.tar" \
|
||||
-T /tmp/bundle-files.txt
|
||||
|
||||
# Compress with gzip (deterministic)
|
||||
gzip -n -9 < "$OUTPUT_DIR/risk-bundle.tar" > "$OUTPUT_DIR/risk-bundle.tar.gz"
|
||||
rm "$OUTPUT_DIR/risk-bundle.tar"
|
||||
|
||||
# Copy manifest to output for easy access
|
||||
cp "$WORK_DIR/manifests/provider-manifest.json" "$OUTPUT_DIR/manifest.json"
|
||||
|
||||
# Compute bundle hash
|
||||
BUNDLE_HASH=$(sha256sum "$OUTPUT_DIR/risk-bundle.tar.gz" | cut -d' ' -f1)
|
||||
|
||||
echo ""
|
||||
echo "=== Build complete ==="
|
||||
echo "Bundle: $OUTPUT_DIR/risk-bundle.tar.gz"
|
||||
echo "Bundle hash: $BUNDLE_HASH"
|
||||
echo "Manifest: $OUTPUT_DIR/manifest.json"
|
||||
echo "Manifest hash: $MANIFEST_HASH"
|
||||
|
||||
# Create checksum file
|
||||
echo "$BUNDLE_HASH risk-bundle.tar.gz" > "$OUTPUT_DIR/risk-bundle.tar.gz.sha256"
|
||||
|
||||
echo ""
|
||||
echo "=== Artifacts ==="
|
||||
ls -la "$OUTPUT_DIR"
|
||||
332
ops/devops/risk-bundle/verify-bundle.sh
Normal file
332
ops/devops/risk-bundle/verify-bundle.sh
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env bash
|
||||
# Risk Bundle Verification Script
|
||||
# RISK-BUNDLE-69-002: CI/offline kit pipeline integration
|
||||
#
|
||||
# Usage: verify-bundle.sh <bundle-path> [--signature <sig-path>] [--strict] [--json]
|
||||
#
|
||||
# This script verifies a risk bundle for integrity and correctness.
|
||||
# Exit codes:
|
||||
# 0 - Bundle is valid
|
||||
# 1 - Bundle is invalid or verification failed
|
||||
# 2 - Input error (missing file, bad arguments)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Defaults
|
||||
BUNDLE_PATH=""
|
||||
SIGNATURE_PATH=""
|
||||
STRICT_MODE=false
|
||||
JSON_OUTPUT=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--signature)
|
||||
SIGNATURE_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--strict)
|
||||
STRICT_MODE=true
|
||||
shift
|
||||
;;
|
||||
--json)
|
||||
JSON_OUTPUT=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: verify-bundle.sh <bundle-path> [--signature <sig-path>] [--strict] [--json]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " <bundle-path> Path to risk-bundle.tar.gz (required)"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --signature <sig-path> Path to detached signature file"
|
||||
echo " --strict Fail on any warning (e.g., missing optional providers)"
|
||||
echo " --json Output results as JSON"
|
||||
echo ""
|
||||
echo "Exit codes:"
|
||||
echo " 0 - Bundle is valid"
|
||||
echo " 1 - Bundle is invalid"
|
||||
echo " 2 - Input error"
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1"
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$BUNDLE_PATH" ]]; then
|
||||
BUNDLE_PATH="$1"
|
||||
else
|
||||
echo "Unexpected argument: $1"
|
||||
exit 2
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required arguments
|
||||
if [[ -z "$BUNDLE_PATH" ]]; then
|
||||
echo "Error: bundle path is required"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "$BUNDLE_PATH" ]]; then
|
||||
echo "Error: bundle not found: $BUNDLE_PATH"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Create temporary extraction directory
|
||||
WORK_DIR=$(mktemp -d)
|
||||
trap "rm -rf $WORK_DIR" EXIT
|
||||
|
||||
# Initialize result tracking
|
||||
ERRORS=()
|
||||
WARNINGS=()
|
||||
BUNDLE_ID=""
|
||||
BUNDLE_VERSION=""
|
||||
PROVIDER_COUNT=0
|
||||
MANDATORY_FOUND=false
|
||||
|
||||
log_error() {
|
||||
ERRORS+=("$1")
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo "ERROR: $1" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
WARNINGS+=("$1")
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo "WARNING: $1" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
log_info() {
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
log_info "=== Risk Bundle Verification ==="
|
||||
log_info "Bundle: $BUNDLE_PATH"
|
||||
log_info ""
|
||||
|
||||
# Step 1: Verify bundle can be extracted
|
||||
log_info "=== Step 1: Extract bundle ==="
|
||||
if ! tar -tzf "$BUNDLE_PATH" > /dev/null 2>&1; then
|
||||
log_error "Bundle is not a valid tar.gz archive"
|
||||
if [[ "$JSON_OUTPUT" == "true" ]]; then
|
||||
echo "{\"valid\": false, \"errors\": [\"Bundle is not a valid tar.gz archive\"]}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tar -xzf "$BUNDLE_PATH" -C "$WORK_DIR"
|
||||
log_info "Bundle extracted successfully"
|
||||
|
||||
# Step 2: Check required structure
|
||||
log_info ""
|
||||
log_info "=== Step 2: Verify structure ==="
|
||||
|
||||
REQUIRED_FILES=(
|
||||
"manifests/provider-manifest.json"
|
||||
)
|
||||
|
||||
for file in "${REQUIRED_FILES[@]}"; do
|
||||
if [[ ! -f "$WORK_DIR/$file" ]]; then
|
||||
log_error "Missing required file: $file"
|
||||
else
|
||||
log_info "Found: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 3: Parse and validate manifest
|
||||
log_info ""
|
||||
log_info "=== Step 3: Validate manifest ==="
|
||||
|
||||
MANIFEST_FILE="$WORK_DIR/manifests/provider-manifest.json"
|
||||
if [[ -f "$MANIFEST_FILE" ]]; then
|
||||
# Extract manifest fields using basic parsing (portable)
|
||||
if command -v jq &> /dev/null; then
|
||||
BUNDLE_ID=$(jq -r '.bundleId // empty' "$MANIFEST_FILE")
|
||||
BUNDLE_VERSION=$(jq -r '.version // empty' "$MANIFEST_FILE")
|
||||
INPUTS_HASH=$(jq -r '.inputsHash // empty' "$MANIFEST_FILE")
|
||||
PROVIDER_COUNT=$(jq '.providers | length' "$MANIFEST_FILE")
|
||||
|
||||
log_info "Bundle ID: $BUNDLE_ID"
|
||||
log_info "Version: $BUNDLE_VERSION"
|
||||
log_info "Inputs Hash: $INPUTS_HASH"
|
||||
log_info "Provider count: $PROVIDER_COUNT"
|
||||
else
|
||||
# Fallback to grep-based parsing
|
||||
BUNDLE_ID=$(grep -o '"bundleId"[[:space:]]*:[[:space:]]*"[^"]*"' "$MANIFEST_FILE" | cut -d'"' -f4 || echo "")
|
||||
log_info "Bundle ID: $BUNDLE_ID (jq not available - limited parsing)"
|
||||
fi
|
||||
|
||||
# Validate required fields
|
||||
if [[ -z "$BUNDLE_ID" ]]; then
|
||||
log_error "Manifest missing bundleId"
|
||||
fi
|
||||
else
|
||||
log_error "Manifest file not found"
|
||||
fi
|
||||
|
||||
# Step 4: Verify provider files
|
||||
log_info ""
|
||||
log_info "=== Step 4: Verify provider files ==="
|
||||
|
||||
# Check for mandatory provider (cisa-kev)
|
||||
CISA_KEV_FILE="$WORK_DIR/providers/cisa-kev/snapshot"
|
||||
if [[ -f "$CISA_KEV_FILE" ]]; then
|
||||
log_info "Found mandatory provider: cisa-kev"
|
||||
MANDATORY_FOUND=true
|
||||
|
||||
# Verify hash if jq is available
|
||||
if command -v jq &> /dev/null && [[ -f "$MANIFEST_FILE" ]]; then
|
||||
EXPECTED_HASH=$(jq -r '.providers[] | select(.providerId == "cisa-kev") | .digest' "$MANIFEST_FILE" | sed 's/sha256://')
|
||||
ACTUAL_HASH=$(sha256sum "$CISA_KEV_FILE" | cut -d' ' -f1)
|
||||
|
||||
if [[ "$EXPECTED_HASH" == "$ACTUAL_HASH" ]]; then
|
||||
log_info " Hash verified: $ACTUAL_HASH"
|
||||
else
|
||||
log_error "cisa-kev hash mismatch: expected $EXPECTED_HASH, got $ACTUAL_HASH"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_error "Missing mandatory provider: cisa-kev"
|
||||
fi
|
||||
|
||||
# Check optional providers
|
||||
EPSS_FILE="$WORK_DIR/providers/first-epss/snapshot"
|
||||
if [[ -f "$EPSS_FILE" ]]; then
|
||||
log_info "Found optional provider: first-epss"
|
||||
|
||||
if command -v jq &> /dev/null && [[ -f "$MANIFEST_FILE" ]]; then
|
||||
EXPECTED_HASH=$(jq -r '.providers[] | select(.providerId == "first-epss") | .digest' "$MANIFEST_FILE" | sed 's/sha256://')
|
||||
ACTUAL_HASH=$(sha256sum "$EPSS_FILE" | cut -d' ' -f1)
|
||||
|
||||
if [[ "$EXPECTED_HASH" == "$ACTUAL_HASH" ]]; then
|
||||
log_info " Hash verified: $ACTUAL_HASH"
|
||||
else
|
||||
log_error "first-epss hash mismatch: expected $EXPECTED_HASH, got $ACTUAL_HASH"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warning "Optional provider not found: first-epss"
|
||||
fi
|
||||
|
||||
OSV_FILE="$WORK_DIR/providers/osv/snapshot"
|
||||
if [[ -f "$OSV_FILE" ]]; then
|
||||
log_info "Found optional provider: osv"
|
||||
else
|
||||
log_warning "Optional provider not found: osv (this is OK unless --include-osv was specified)"
|
||||
fi
|
||||
|
||||
# Step 5: Verify DSSE signature (if present)
|
||||
log_info ""
|
||||
log_info "=== Step 5: Check signatures ==="
|
||||
|
||||
DSSE_FILE="$WORK_DIR/signatures/provider-manifest.dsse"
|
||||
if [[ -f "$DSSE_FILE" ]]; then
|
||||
log_info "Found manifest DSSE signature"
|
||||
|
||||
# Basic DSSE structure check
|
||||
if command -v jq &> /dev/null; then
|
||||
PAYLOAD_TYPE=$(jq -r '.payloadType // empty' "$DSSE_FILE")
|
||||
SIG_COUNT=$(jq '.signatures | length' "$DSSE_FILE")
|
||||
|
||||
if [[ "$PAYLOAD_TYPE" == "application/vnd.stellaops.risk-bundle.manifest+json" ]]; then
|
||||
log_info " Payload type: $PAYLOAD_TYPE (valid)"
|
||||
else
|
||||
log_warning "Unexpected payload type: $PAYLOAD_TYPE"
|
||||
fi
|
||||
|
||||
log_info " Signature count: $SIG_COUNT"
|
||||
fi
|
||||
else
|
||||
log_warning "No DSSE signature found"
|
||||
fi
|
||||
|
||||
# Check detached bundle signature
|
||||
if [[ -n "$SIGNATURE_PATH" ]]; then
|
||||
if [[ -f "$SIGNATURE_PATH" ]]; then
|
||||
log_info "Found detached bundle signature: $SIGNATURE_PATH"
|
||||
# TODO: Implement actual signature verification
|
||||
else
|
||||
log_error "Specified signature file not found: $SIGNATURE_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 6: Summarize results
|
||||
log_info ""
|
||||
log_info "=== Verification Summary ==="
|
||||
|
||||
ERROR_COUNT=${#ERRORS[@]}
|
||||
WARNING_COUNT=${#WARNINGS[@]}
|
||||
|
||||
if [[ "$JSON_OUTPUT" == "true" ]]; then
|
||||
# Output JSON result
|
||||
ERRORS_JSON=$(printf '%s\n' "${ERRORS[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]")
|
||||
WARNINGS_JSON=$(printf '%s\n' "${WARNINGS[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"valid": $([[ $ERROR_COUNT -eq 0 ]] && echo "true" || echo "false"),
|
||||
"bundleId": "$BUNDLE_ID",
|
||||
"version": "$BUNDLE_VERSION",
|
||||
"providerCount": $PROVIDER_COUNT,
|
||||
"mandatoryProviderFound": $MANDATORY_FOUND,
|
||||
"errorCount": $ERROR_COUNT,
|
||||
"warningCount": $WARNING_COUNT,
|
||||
"errors": $ERRORS_JSON,
|
||||
"warnings": $WARNINGS_JSON
|
||||
}
|
||||
EOF
|
||||
else
|
||||
log_info "Bundle ID: $BUNDLE_ID"
|
||||
log_info "Errors: $ERROR_COUNT"
|
||||
log_info "Warnings: $WARNING_COUNT"
|
||||
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Errors:"
|
||||
for err in "${ERRORS[@]}"; do
|
||||
echo " - $err"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $WARNING_COUNT -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Warnings:"
|
||||
for warn in "${WARNINGS[@]}"; do
|
||||
echo " - $warn"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine exit code
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo ""
|
||||
echo "RESULT: INVALID"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$STRICT_MODE" == "true" && $WARNING_COUNT -gt 0 ]]; then
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo ""
|
||||
echo "RESULT: INVALID (strict mode - warnings treated as errors)"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo ""
|
||||
echo "RESULT: VALID"
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user