up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (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
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (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:
@@ -0,0 +1,71 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// Extension for decision- and scoring-specific replay tokens.
|
||||
/// </summary>
|
||||
public static class DecisionReplayTokenExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a replay token for a triage decision.
|
||||
/// </summary>
|
||||
public static ReplayToken GenerateForDecision(
|
||||
this IReplayTokenGenerator generator,
|
||||
string alertId,
|
||||
string actorId,
|
||||
string decisionStatus,
|
||||
IEnumerable<string> evidenceHashes,
|
||||
string? policyContext,
|
||||
string? rulesVersion)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(generator);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(alertId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(actorId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(decisionStatus);
|
||||
ArgumentNullException.ThrowIfNull(evidenceHashes);
|
||||
|
||||
var request = new ReplayTokenRequest
|
||||
{
|
||||
InputHashes = new[] { alertId },
|
||||
EvidenceHashes = evidenceHashes.ToList(),
|
||||
RulesVersion = rulesVersion,
|
||||
AdditionalContext = new Dictionary<string, string>
|
||||
{
|
||||
["actor_id"] = actorId,
|
||||
["decision_status"] = decisionStatus,
|
||||
["policy_context"] = policyContext ?? string.Empty
|
||||
}
|
||||
};
|
||||
|
||||
return generator.Generate(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a replay token for unknowns scoring.
|
||||
/// </summary>
|
||||
public static ReplayToken GenerateForScoring(
|
||||
this IReplayTokenGenerator generator,
|
||||
string subjectKey,
|
||||
IEnumerable<string> feedManifests,
|
||||
string scoringConfigVersion,
|
||||
IEnumerable<string> inputHashes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(generator);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
|
||||
ArgumentNullException.ThrowIfNull(feedManifests);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scoringConfigVersion);
|
||||
ArgumentNullException.ThrowIfNull(inputHashes);
|
||||
|
||||
var request = new ReplayTokenRequest
|
||||
{
|
||||
FeedManifests = feedManifests.ToList(),
|
||||
ScoringConfigVersion = scoringConfigVersion,
|
||||
InputHashes = inputHashes.ToList(),
|
||||
AdditionalContext = new Dictionary<string, string>
|
||||
{
|
||||
["subject_key"] = subjectKey
|
||||
}
|
||||
};
|
||||
|
||||
return generator.Generate(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// Generates deterministic replay tokens for audit and reproducibility.
|
||||
/// </summary>
|
||||
public interface IReplayTokenGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a replay token from the given inputs.
|
||||
/// </summary>
|
||||
/// <param name="request">The inputs to hash.</param>
|
||||
/// <returns>A deterministic replay token.</returns>
|
||||
ReplayToken Generate(ReplayTokenRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that inputs match a previously generated token.
|
||||
/// </summary>
|
||||
bool Verify(ReplayToken token, ReplayTokenRequest request);
|
||||
}
|
||||
18
src/__Libraries/StellaOps.Audit.ReplayToken/README.md
Normal file
18
src/__Libraries/StellaOps.Audit.ReplayToken/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# StellaOps.Audit.ReplayToken
|
||||
|
||||
Deterministic replay token generation used to make triage decisions and scoring reproducible and audit-ready.
|
||||
|
||||
## Token format
|
||||
|
||||
`replay:v<version>:<algorithm>:<sha256_hex>`
|
||||
|
||||
Example:
|
||||
|
||||
`replay:v1.0:SHA-256:0123abcd...`
|
||||
|
||||
## Usage
|
||||
|
||||
- Create a `ReplayTokenRequest` with feed/rules/policy/input digests.
|
||||
- Call `IReplayTokenGenerator.Generate(request)` to get a stable token value.
|
||||
- Store the token’s `Canonical` string alongside immutable decision events.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// Generates CLI snippets for one-click reproduce functionality.
|
||||
/// </summary>
|
||||
public sealed class ReplayCliSnippetGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a CLI command to reproduce a decision.
|
||||
/// </summary>
|
||||
public string GenerateDecisionReplay(
|
||||
ReplayToken token,
|
||||
string alertId,
|
||||
string? feedManifestUri = null,
|
||||
string? policyVersion = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(token);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(alertId);
|
||||
|
||||
var parts = new List<string>
|
||||
{
|
||||
"stellaops",
|
||||
"replay",
|
||||
"decision",
|
||||
$"--token {token.Value}",
|
||||
$"--alert-id {alertId}"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(feedManifestUri))
|
||||
{
|
||||
parts.Add($"--feed-manifest {feedManifestUri.Trim()}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(policyVersion))
|
||||
{
|
||||
parts.Add($"--policy-version {policyVersion.Trim()}");
|
||||
}
|
||||
|
||||
return string.Join(" \\\n+ ", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a CLI command to reproduce unknowns scoring.
|
||||
/// </summary>
|
||||
public string GenerateScoringReplay(
|
||||
ReplayToken token,
|
||||
string subjectKey,
|
||||
string? configVersion = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(token);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
|
||||
|
||||
var parts = new List<string>
|
||||
{
|
||||
"stellaops",
|
||||
"replay",
|
||||
"scoring",
|
||||
$"--token {token.Value}",
|
||||
$"--subject {subjectKey}"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(configVersion))
|
||||
{
|
||||
parts.Add($"--config-version {configVersion.Trim()}");
|
||||
}
|
||||
|
||||
return string.Join(" \\\n+ ", parts);
|
||||
}
|
||||
}
|
||||
94
src/__Libraries/StellaOps.Audit.ReplayToken/ReplayToken.cs
Normal file
94
src/__Libraries/StellaOps.Audit.ReplayToken/ReplayToken.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// A deterministic, content-addressable replay token.
|
||||
/// </summary>
|
||||
public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
{
|
||||
public const string Scheme = "replay";
|
||||
public const string DefaultAlgorithm = "SHA-256";
|
||||
public const string DefaultVersion = "1.0";
|
||||
|
||||
/// <summary>
|
||||
/// The token value (SHA-256 hash in hex).
|
||||
/// </summary>
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm used for hashing.
|
||||
/// </summary>
|
||||
public string Algorithm { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the token generation algorithm.
|
||||
/// </summary>
|
||||
public string Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when token was generated.
|
||||
/// </summary>
|
||||
public DateTimeOffset GeneratedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Canonical representation for storage.
|
||||
/// </summary>
|
||||
public string Canonical => $"{Scheme}:v{Version}:{Algorithm}:{Value}";
|
||||
|
||||
public ReplayToken(string value, DateTimeOffset generatedAt, string? algorithm = null, string? version = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Token value cannot be empty.", nameof(value));
|
||||
}
|
||||
|
||||
Value = value.Trim();
|
||||
GeneratedAt = generatedAt;
|
||||
Algorithm = string.IsNullOrWhiteSpace(algorithm) ? DefaultAlgorithm : algorithm.Trim();
|
||||
Version = string.IsNullOrWhiteSpace(version) ? DefaultVersion : version.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a canonical token string.
|
||||
/// </summary>
|
||||
public static ReplayToken Parse(string canonical)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(canonical))
|
||||
{
|
||||
throw new ArgumentException("Token cannot be empty.", nameof(canonical));
|
||||
}
|
||||
|
||||
var parts = canonical.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 4 || !string.Equals(parts[0], Scheme, StringComparison.Ordinal))
|
||||
{
|
||||
throw new FormatException($"Invalid replay token format: {canonical}");
|
||||
}
|
||||
|
||||
var versionPart = parts[1];
|
||||
if (!versionPart.StartsWith("v", StringComparison.Ordinal) || versionPart.Length <= 1)
|
||||
{
|
||||
throw new FormatException($"Invalid replay token version: {canonical}");
|
||||
}
|
||||
|
||||
var version = versionPart[1..];
|
||||
var algorithm = parts[2];
|
||||
var value = parts[3];
|
||||
|
||||
return new ReplayToken(value, DateTimeOffset.UnixEpoch, algorithm, version);
|
||||
}
|
||||
|
||||
public override string ToString() => Canonical;
|
||||
|
||||
public bool Equals(ReplayToken? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is ReplayToken other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// Inputs for replay token generation.
|
||||
/// </summary>
|
||||
public sealed class ReplayTokenRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Feed manifest hashes (advisory sources).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> FeedManifests { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Rule set version identifier.
|
||||
/// </summary>
|
||||
public string? RulesVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rule set content hash.
|
||||
/// </summary>
|
||||
public string? RulesHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Lattice policy version identifier.
|
||||
/// </summary>
|
||||
public string? LatticePolicyVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Lattice policy content hash.
|
||||
/// </summary>
|
||||
public string? LatticePolicyHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Input artifact hashes (SBOMs, images, etc.).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> InputHashes { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Scoring configuration version.
|
||||
/// </summary>
|
||||
public string? ScoringConfigVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence artifact hashes.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> EvidenceHashes { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Additional context for extensibility.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> AdditionalContext { get; init; } = new Dictionary<string, string>();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddReplayTokenServices(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.TryAddSingleton<IReplayTokenGenerator, Sha256ReplayTokenGenerator>();
|
||||
services.TryAddSingleton<ReplayCliSnippetGenerator>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddReplayTokenServices(this IServiceCollection services, TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
services.AddSingleton(timeProvider);
|
||||
services.TryAddSingleton<IReplayTokenGenerator, Sha256ReplayTokenGenerator>();
|
||||
services.TryAddSingleton<ReplayCliSnippetGenerator>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// Generates replay tokens using SHA-256 hashing with deterministic canonicalization.
|
||||
/// </summary>
|
||||
public sealed class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public Sha256ReplayTokenGenerator(ICryptoHash cryptoHash, TimeProvider timeProvider)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
}
|
||||
|
||||
public ReplayToken Generate(ReplayTokenRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var canonical = Canonicalize(request);
|
||||
var hashHex = ComputeHash(canonical);
|
||||
|
||||
return new ReplayToken(hashHex, _timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
public bool Verify(ReplayToken token, ReplayTokenRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(token);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var computed = Generate(request);
|
||||
return string.Equals(token.Value, computed.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string ComputeHash(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return _cryptoHash.ComputeHashHex(bytes, HashAlgorithms.Sha256);
|
||||
}
|
||||
|
||||
private static string? NormalizeValue(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
private static List<string> NormalizeSortedList(IReadOnlyList<string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var normalized = values
|
||||
.Where(static x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(static x => x.Trim())
|
||||
.OrderBy(static x => x, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> NormalizeSortedDictionary(IReadOnlyDictionary<string, string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var normalized = values
|
||||
.Where(static kvp => !string.IsNullOrWhiteSpace(kvp.Key))
|
||||
.Select(static kvp => new KeyValuePair<string, string>(kvp.Key.Trim(), kvp.Value?.Trim() ?? string.Empty))
|
||||
.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)
|
||||
.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.Ordinal);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produces deterministic canonical representation of inputs.
|
||||
/// </summary>
|
||||
private static string Canonicalize(ReplayTokenRequest request)
|
||||
{
|
||||
var canonical = new CanonicalReplayInput
|
||||
{
|
||||
Version = ReplayToken.DefaultVersion,
|
||||
FeedManifests = NormalizeSortedList(request.FeedManifests),
|
||||
RulesVersion = NormalizeValue(request.RulesVersion),
|
||||
RulesHash = NormalizeValue(request.RulesHash),
|
||||
LatticePolicyVersion = NormalizeValue(request.LatticePolicyVersion),
|
||||
LatticePolicyHash = NormalizeValue(request.LatticePolicyHash),
|
||||
InputHashes = NormalizeSortedList(request.InputHashes),
|
||||
ScoringConfigVersion = NormalizeValue(request.ScoringConfigVersion),
|
||||
EvidenceHashes = NormalizeSortedList(request.EvidenceHashes),
|
||||
AdditionalContext = NormalizeSortedDictionary(request.AdditionalContext)
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(canonical, JsonOptions);
|
||||
}
|
||||
|
||||
private sealed class CanonicalReplayInput
|
||||
{
|
||||
public required string Version { get; init; }
|
||||
public required List<string> FeedManifests { get; init; }
|
||||
public string? RulesVersion { get; init; }
|
||||
public string? RulesHash { get; init; }
|
||||
public string? LatticePolicyVersion { get; init; }
|
||||
public string? LatticePolicyHash { get; init; }
|
||||
public required List<string> InputHashes { get; init; }
|
||||
public string? ScoringConfigVersion { get; init; }
|
||||
public required List<string> EvidenceHashes { get; init; }
|
||||
public required Dictionary<string, string> AdditionalContext { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Audit.ReplayToken</RootNamespace>
|
||||
<Description>Deterministic replay token generation for audit and reproducibility</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -12,5 +12,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\\..\\..\\third_party\\forks\\AlexMAS.GostCryptography\\Source\\GostCryptography\\GostCryptography.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Plugin\\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Plugin\\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Plugin\\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Plugin\\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user