feat: Add Promotion-Time Attestations for Stella Ops
- Introduced a new document for promotion-time attestations, detailing the purpose, predicate schema, producer workflow, verification flow, APIs, and security considerations. - Implemented the `stella.ops/promotion@v1` predicate schema to capture promotion evidence including image digest, SBOM/VEX artifacts, and Rekor proof. - Defined producer responsibilities and workflows for CLI orchestration, signer responsibilities, and Export Center integration. - Added verification steps for auditors to validate promotion attestations offline. feat: Create Symbol Manifest v1 Specification - Developed a specification for Symbol Manifest v1 to provide a deterministic format for publishing debug symbols and source maps. - Defined the manifest structure, including schema, entries, source maps, toolchain, and provenance. - Outlined upload and verification processes, resolve APIs, runtime proxy, caching, and offline bundle generation. - Included security considerations and related tasks for implementation. chore: Add Ruby Analyzer with Git Sources - Created a Gemfile and Gemfile.lock for Ruby analyzer with dependencies on git-gem, httparty, and path-gem. - Implemented main application logic to utilize the defined gems and output their versions. - Added expected JSON output for the Ruby analyzer to validate the integration of the new gems and their functionalities. - Developed internal observation classes for Ruby packages, runtime edges, and capabilities, including serialization logic for observations. test: Add tests for Ruby Analyzer - Created test fixtures for Ruby analyzer, including Gemfile, Gemfile.lock, main application, and expected JSON output. - Ensured that the tests validate the correct integration and functionality of the Ruby analyzer with the specified gems.
This commit is contained in:
@@ -574,12 +574,12 @@ internal sealed class PolicyParser
|
||||
PolicyExpression expr = new PolicyIdentifierExpression(identifier.Text, identifier.Span);
|
||||
while (true)
|
||||
{
|
||||
if (Match(TokenKind.Dot))
|
||||
{
|
||||
var member = Consume(TokenKind.Identifier, "Expected identifier after '.'.", "expression.member");
|
||||
expr = new PolicyMemberAccessExpression(expr, member.Text, new SourceSpan(expr.Span.Start, member.Span.End));
|
||||
continue;
|
||||
}
|
||||
if (Match(TokenKind.Dot))
|
||||
{
|
||||
var member = ConsumeIdentifier("Expected identifier after '.'.", "expression.member");
|
||||
expr = new PolicyMemberAccessExpression(expr, member.Text, new SourceSpan(expr.Span.Start, member.Span.End));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Match(TokenKind.LeftParen))
|
||||
{
|
||||
@@ -609,12 +609,26 @@ internal sealed class PolicyParser
|
||||
break;
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private bool Match(TokenKind kind)
|
||||
{
|
||||
if (Check(kind))
|
||||
return expr;
|
||||
}
|
||||
|
||||
private DslToken ConsumeIdentifier(string message, string path)
|
||||
{
|
||||
if (Check(TokenKind.Identifier) || IsKeywordIdentifier(Current.Kind))
|
||||
{
|
||||
return Advance();
|
||||
}
|
||||
|
||||
diagnostics.Add(PolicyIssue.Error(PolicyDslDiagnosticCodes.UnexpectedToken, message, path));
|
||||
return Advance();
|
||||
}
|
||||
|
||||
private static bool IsKeywordIdentifier(TokenKind kind) =>
|
||||
kind == TokenKind.KeywordSource;
|
||||
|
||||
private bool Match(TokenKind kind)
|
||||
{
|
||||
if (Check(kind))
|
||||
{
|
||||
Advance();
|
||||
return true;
|
||||
|
||||
@@ -11,13 +11,13 @@ internal sealed record PolicyEvaluationRequest(
|
||||
PolicyIrDocument Document,
|
||||
PolicyEvaluationContext Context);
|
||||
|
||||
internal sealed record PolicyEvaluationContext(
|
||||
PolicyEvaluationSeverity Severity,
|
||||
PolicyEvaluationEnvironment Environment,
|
||||
PolicyEvaluationAdvisory Advisory,
|
||||
PolicyEvaluationVexEvidence Vex,
|
||||
PolicyEvaluationSbom Sbom,
|
||||
PolicyEvaluationExceptions Exceptions);
|
||||
internal sealed record PolicyEvaluationContext(
|
||||
PolicyEvaluationSeverity Severity,
|
||||
PolicyEvaluationEnvironment Environment,
|
||||
PolicyEvaluationAdvisory Advisory,
|
||||
PolicyEvaluationVexEvidence Vex,
|
||||
PolicyEvaluationSbom Sbom,
|
||||
PolicyEvaluationExceptions Exceptions);
|
||||
|
||||
internal sealed record PolicyEvaluationSeverity(string Normalized, decimal? Score = null);
|
||||
|
||||
@@ -43,10 +43,28 @@ internal sealed record PolicyEvaluationVexStatement(
|
||||
string StatementId,
|
||||
DateTimeOffset? Timestamp = null);
|
||||
|
||||
internal sealed record PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
|
||||
{
|
||||
public bool HasTag(string tag) => Tags.Contains(tag);
|
||||
}
|
||||
internal sealed record PolicyEvaluationSbom(
|
||||
ImmutableHashSet<string> Tags,
|
||||
ImmutableArray<PolicyEvaluationComponent> Components)
|
||||
{
|
||||
public PolicyEvaluationSbom(ImmutableHashSet<string> Tags)
|
||||
: this(Tags, ImmutableArray<PolicyEvaluationComponent>.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public static readonly PolicyEvaluationSbom Empty = new(
|
||||
ImmutableHashSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase),
|
||||
ImmutableArray<PolicyEvaluationComponent>.Empty);
|
||||
|
||||
public bool HasTag(string tag) => Tags.Contains(tag);
|
||||
}
|
||||
|
||||
internal sealed record PolicyEvaluationComponent(
|
||||
string Name,
|
||||
string Version,
|
||||
string Type,
|
||||
string? Purl,
|
||||
ImmutableDictionary<string, string> Metadata);
|
||||
|
||||
internal sealed record PolicyEvaluationResult(
|
||||
bool Matched,
|
||||
|
||||
@@ -98,10 +98,20 @@ internal sealed class PolicyExpressionEvaluator
|
||||
return sbom.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is ImmutableDictionary<string, object?> dict && dict.TryGetValue(member.Member, out var value))
|
||||
{
|
||||
return new EvaluationValue(value);
|
||||
}
|
||||
if (raw is ComponentScope componentScope)
|
||||
{
|
||||
return componentScope.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is RubyComponentScope rubyScope)
|
||||
{
|
||||
return rubyScope.Get(member.Member);
|
||||
}
|
||||
|
||||
if (raw is ImmutableDictionary<string, object?> dict && dict.TryGetValue(member.Member, out var value))
|
||||
{
|
||||
return new EvaluationValue(value);
|
||||
}
|
||||
|
||||
if (raw is PolicyEvaluationVexStatement stmt)
|
||||
{
|
||||
@@ -129,47 +139,51 @@ internal sealed class PolicyExpressionEvaluator
|
||||
}
|
||||
}
|
||||
|
||||
if (invocation.Target is PolicyMemberAccessExpression member && member.Target is PolicyIdentifierExpression root)
|
||||
{
|
||||
if (root.Name == "vex")
|
||||
{
|
||||
var vex = Evaluate(member.Target, scope);
|
||||
if (vex.Raw is VexScope vexScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"any" => new EvaluationValue(vexScope.Any(invocation.Arguments, scope)),
|
||||
"latest" => new EvaluationValue(vexScope.Latest()),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (root.Name == "sbom")
|
||||
{
|
||||
var sbom = Evaluate(member.Target, scope);
|
||||
if (sbom.Raw is SbomScope sbomScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"has_tag" => sbomScope.HasTag(invocation.Arguments, scope, this),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (root.Name == "advisory")
|
||||
{
|
||||
var advisory = Evaluate(member.Target, scope);
|
||||
if (advisory.Raw is AdvisoryScope advisoryScope)
|
||||
{
|
||||
return advisoryScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
if (invocation.Target is PolicyMemberAccessExpression member)
|
||||
{
|
||||
var targetValue = Evaluate(member.Target, scope);
|
||||
var targetRaw = targetValue.Raw;
|
||||
if (targetRaw is RubyComponentScope rubyScope)
|
||||
{
|
||||
return rubyScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
|
||||
if (targetRaw is ComponentScope componentScope)
|
||||
{
|
||||
return componentScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
|
||||
if (member.Target is PolicyIdentifierExpression root)
|
||||
{
|
||||
if (root.Name == "vex" && targetRaw is VexScope vexScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"any" => new EvaluationValue(vexScope.Any(invocation.Arguments, scope)),
|
||||
"latest" => new EvaluationValue(vexScope.Latest()),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
if (root.Name == "sbom" && targetRaw is SbomScope sbomScope)
|
||||
{
|
||||
return member.Member switch
|
||||
{
|
||||
"has_tag" => sbomScope.HasTag(invocation.Arguments, scope, this),
|
||||
"any_component" => sbomScope.AnyComponent(invocation.Arguments, scope, this),
|
||||
_ => EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
if (root.Name == "advisory" && targetRaw is AdvisoryScope advisoryScope)
|
||||
{
|
||||
return advisoryScope.Invoke(member.Member, invocation.Arguments, scope, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
|
||||
private EvaluationValue EvaluateIndexer(PolicyIndexerExpression indexer, EvaluationScope scope)
|
||||
{
|
||||
@@ -428,31 +442,322 @@ internal sealed class PolicyExpressionEvaluator
|
||||
this.sbom = sbom;
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
if (member.Equals("tags", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new EvaluationValue(sbom.Tags.ToImmutableArray<object?>());
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
|
||||
public EvaluationValue HasTag(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
var tag = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
if (string.IsNullOrWhiteSpace(tag))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
return new EvaluationValue(sbom.HasTag(tag!));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class VexScope
|
||||
{
|
||||
private readonly PolicyExpressionEvaluator evaluator;
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
if (member.Equals("tags", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new EvaluationValue(sbom.Tags.ToImmutableArray<object?>());
|
||||
}
|
||||
|
||||
if (member.Equals("components", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new EvaluationValue(sbom.Components
|
||||
.Select(component => (object?)new ComponentScope(component))
|
||||
.ToImmutableArray());
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
|
||||
public EvaluationValue HasTag(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
var tag = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
if (string.IsNullOrWhiteSpace(tag))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
return new EvaluationValue(sbom.HasTag(tag!));
|
||||
}
|
||||
|
||||
public EvaluationValue AnyComponent(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
if (arguments.Length == 0 || sbom.Components.IsDefaultOrEmpty)
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
var predicate = arguments[0];
|
||||
foreach (var component in sbom.Components)
|
||||
{
|
||||
var locals = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["component"] = new ComponentScope(component),
|
||||
};
|
||||
|
||||
if (component.Type.Equals("gem", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
locals["ruby"] = new RubyComponentScope(component);
|
||||
}
|
||||
|
||||
var nestedScope = EvaluationScope.FromLocals(scope.Globals, locals);
|
||||
if (evaluator.EvaluateBoolean(predicate, nestedScope))
|
||||
{
|
||||
return EvaluationValue.True;
|
||||
}
|
||||
}
|
||||
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ComponentScope
|
||||
{
|
||||
private readonly PolicyEvaluationComponent component;
|
||||
|
||||
public ComponentScope(PolicyEvaluationComponent component)
|
||||
{
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
return member.ToLowerInvariant() switch
|
||||
{
|
||||
"name" => new EvaluationValue(component.Name),
|
||||
"version" => new EvaluationValue(component.Version),
|
||||
"type" => new EvaluationValue(component.Type),
|
||||
"purl" => new EvaluationValue(component.Purl),
|
||||
"metadata" => new EvaluationValue(component.Metadata),
|
||||
_ => component.Metadata.TryGetValue(member, out var value)
|
||||
? new EvaluationValue(value)
|
||||
: EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
public EvaluationValue Invoke(string member, ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
if (member.Equals("has_metadata", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var key = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
return new EvaluationValue(component.Metadata.ContainsKey(key!));
|
||||
}
|
||||
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RubyComponentScope
|
||||
{
|
||||
private readonly PolicyEvaluationComponent component;
|
||||
private readonly ImmutableHashSet<string> groups;
|
||||
|
||||
public RubyComponentScope(PolicyEvaluationComponent component)
|
||||
{
|
||||
this.component = component;
|
||||
groups = ParseGroups(component.Metadata);
|
||||
}
|
||||
|
||||
public EvaluationValue Get(string member)
|
||||
{
|
||||
return member.ToLowerInvariant() switch
|
||||
{
|
||||
"groups" => new EvaluationValue(groups.Select(value => (object?)value).ToImmutableArray()),
|
||||
"declaredonly" => new EvaluationValue(IsDeclaredOnly()),
|
||||
"source" => new EvaluationValue(GetSource() ?? string.Empty),
|
||||
_ => component.Metadata.TryGetValue(member, out var value)
|
||||
? new EvaluationValue(value)
|
||||
: EvaluationValue.Null,
|
||||
};
|
||||
}
|
||||
|
||||
public EvaluationValue Invoke(string member, ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
switch (member.ToLowerInvariant())
|
||||
{
|
||||
case "group":
|
||||
{
|
||||
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
return new EvaluationValue(name is not null && groups.Contains(name));
|
||||
}
|
||||
case "groups":
|
||||
return new EvaluationValue(groups.Select(value => (object?)value).ToImmutableArray());
|
||||
case "declared_only":
|
||||
return new EvaluationValue(IsDeclaredOnly());
|
||||
case "source":
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
{
|
||||
return new EvaluationValue(GetSource() ?? string.Empty);
|
||||
}
|
||||
|
||||
var requested = evaluator.Evaluate(arguments[0], scope).AsString();
|
||||
if (string.IsNullOrWhiteSpace(requested))
|
||||
{
|
||||
return EvaluationValue.False;
|
||||
}
|
||||
|
||||
var kind = GetSourceKind();
|
||||
return new EvaluationValue(string.Equals(kind, requested, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
case "capability":
|
||||
{
|
||||
var name = arguments.Length > 0 ? evaluator.Evaluate(arguments[0], scope).AsString() : null;
|
||||
return new EvaluationValue(HasCapability(name));
|
||||
}
|
||||
case "capability_any":
|
||||
{
|
||||
var capabilities = EvaluateAsStringSet(arguments, scope, evaluator);
|
||||
return new EvaluationValue(capabilities.Any(HasCapability));
|
||||
}
|
||||
default:
|
||||
return EvaluationValue.Null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasCapability(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = name.Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (component.Metadata.TryGetValue($"capability.{normalized}", out var value))
|
||||
{
|
||||
return IsTruthy(value);
|
||||
}
|
||||
|
||||
if (normalized.StartsWith("scheduler.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var group = normalized.Substring("scheduler.".Length);
|
||||
var schedulerList = component.Metadata.TryGetValue("capability.scheduler", out var listValue)
|
||||
? listValue
|
||||
: null;
|
||||
return ContainsDelimitedValue(schedulerList, group);
|
||||
}
|
||||
|
||||
if (normalized.Equals("scheduler", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var schedulerList = component.Metadata.TryGetValue("capability.scheduler", out var listValue)
|
||||
? listValue
|
||||
: null;
|
||||
return !string.IsNullOrWhiteSpace(schedulerList);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsDeclaredOnly()
|
||||
{
|
||||
return component.Metadata.TryGetValue("declaredOnly", out var value) && IsTruthy(value);
|
||||
}
|
||||
|
||||
private string? GetSource()
|
||||
{
|
||||
return component.Metadata.TryGetValue("source", out var value) ? value : null;
|
||||
}
|
||||
|
||||
private string? GetSourceKind()
|
||||
{
|
||||
var source = GetSource();
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
source = source.Trim();
|
||||
if (source.StartsWith("git:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "git";
|
||||
}
|
||||
|
||||
if (source.StartsWith("path:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "path";
|
||||
}
|
||||
|
||||
if (source.StartsWith("vendor-cache", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "vendor-cache";
|
||||
}
|
||||
|
||||
if (source.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
|| source.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "registry";
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> ParseGroups(ImmutableDictionary<string, string> metadata)
|
||||
{
|
||||
if (!metadata.TryGetValue("groups", out var value) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return ImmutableHashSet<string>.Empty;
|
||||
}
|
||||
|
||||
var groups = value
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(static g => !string.IsNullOrWhiteSpace(g))
|
||||
.Select(static g => g.Trim())
|
||||
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static bool ContainsDelimitedValue(string? delimited, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(delimited) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return delimited
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Any(entry => entry.Equals(value, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsTruthy(string? value)
|
||||
{
|
||||
return value is not null
|
||||
&& (value.Equals("true", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("1", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> EvaluateAsStringSet(ImmutableArray<PolicyExpression> arguments, EvaluationScope scope, PolicyExpressionEvaluator evaluator)
|
||||
{
|
||||
var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
var evaluated = evaluator.Evaluate(argument, scope).Raw;
|
||||
switch (evaluated)
|
||||
{
|
||||
case ImmutableArray<object?> array:
|
||||
foreach (var item in array)
|
||||
{
|
||||
if (item is string text && !string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
builder.Add(text.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case string text when !string.IsNullOrWhiteSpace(text):
|
||||
builder.Add(text.Trim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class VexScope
|
||||
{
|
||||
private readonly PolicyExpressionEvaluator evaluator;
|
||||
private readonly PolicyEvaluationVexEvidence vex;
|
||||
|
||||
public VexScope(PolicyExpressionEvaluator evaluator, PolicyEvaluationVexEvidence vex)
|
||||
|
||||
Reference in New Issue
Block a user