Files
git.stella-ops.org/src/Policy/StellaOps.PolicyDsl/DslCompletionProvider.cs
StellaOps Bot 2a06f780cf sprints work
2025-12-25 12:19:12 +02:00

555 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// -----------------------------------------------------------------------------
// DslCompletionProvider.cs
// Sprint: SPRINT_8200_0012_0003_policy_engine_integration
// Task: PINT-8200-019
// Description: Provides DSL autocomplete hints for score fields and other constructs
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
namespace StellaOps.PolicyDsl;
/// <summary>
/// Provides completion hints for the Stella Policy DSL.
/// This provider generates structured completion suggestions that can be used
/// by any editor client (Monaco, VS Code, etc.).
/// </summary>
public static class DslCompletionProvider
{
/// <summary>
/// Gets all available completion items grouped by category.
/// </summary>
public static DslCompletionCatalog GetCompletionCatalog() => DslCompletionCatalog.Instance;
/// <summary>
/// Gets completion items relevant for the given context.
/// </summary>
/// <param name="context">The completion context including cursor position and text.</param>
/// <returns>Filtered completion items relevant to the context.</returns>
public static ImmutableArray<DslCompletionItem> GetCompletionsForContext(DslCompletionContext context)
{
ArgumentNullException.ThrowIfNull(context);
var results = ImmutableArray.CreateBuilder<DslCompletionItem>();
var catalog = DslCompletionCatalog.Instance;
// Check for namespace prefix completion
if (context.TriggerText.EndsWith("score.", StringComparison.Ordinal))
{
results.AddRange(catalog.ScoreFields);
return results.ToImmutable();
}
if (context.TriggerText.EndsWith("sbom.", StringComparison.Ordinal))
{
results.AddRange(catalog.SbomFields);
return results.ToImmutable();
}
if (context.TriggerText.EndsWith("advisory.", StringComparison.Ordinal))
{
results.AddRange(catalog.AdvisoryFields);
return results.ToImmutable();
}
if (context.TriggerText.EndsWith("vex.", StringComparison.Ordinal))
{
results.AddRange(catalog.VexFields);
return results.ToImmutable();
}
if (context.TriggerText.EndsWith("signals.", StringComparison.Ordinal))
{
results.AddRange(catalog.SignalFields);
return results.ToImmutable();
}
if (context.TriggerText.EndsWith("reachability.", StringComparison.Ordinal))
{
results.AddRange(catalog.ReachabilityFields);
return results.ToImmutable();
}
// Check for value completion contexts
if (IsScoreBucketContext(context.TriggerText))
{
results.AddRange(catalog.ScoreBuckets);
return results.ToImmutable();
}
if (IsScoreFlagContext(context.TriggerText))
{
results.AddRange(catalog.ScoreFlags);
return results.ToImmutable();
}
if (IsVexStatusContext(context.TriggerText))
{
results.AddRange(catalog.VexStatuses);
return results.ToImmutable();
}
if (IsVexJustificationContext(context.TriggerText))
{
results.AddRange(catalog.VexJustifications);
return results.ToImmutable();
}
// Check for action context (after 'then' or 'else')
if (IsActionContext(context.TriggerText))
{
results.AddRange(catalog.Actions);
return results.ToImmutable();
}
// Default: return all top-level completions
results.AddRange(catalog.Keywords);
results.AddRange(catalog.Functions);
results.AddRange(catalog.Namespaces);
return results.ToImmutable();
}
private static bool IsScoreBucketContext(string text) =>
text.Contains("score.bucket", StringComparison.OrdinalIgnoreCase) &&
(text.EndsWith("== ", StringComparison.Ordinal) ||
text.EndsWith("!= ", StringComparison.Ordinal) ||
text.EndsWith("in [", StringComparison.Ordinal) ||
text.EndsWith("== \"", StringComparison.Ordinal));
private static bool IsScoreFlagContext(string text) =>
text.Contains("score.flags", StringComparison.OrdinalIgnoreCase) &&
(text.EndsWith("contains ", StringComparison.Ordinal) ||
text.EndsWith("contains \"", StringComparison.Ordinal) ||
text.EndsWith("in [", StringComparison.Ordinal));
private static bool IsVexStatusContext(string text) =>
text.Contains("status", StringComparison.OrdinalIgnoreCase) &&
(text.EndsWith("== ", StringComparison.Ordinal) ||
text.EndsWith(":= ", StringComparison.Ordinal) ||
text.EndsWith("!= ", StringComparison.Ordinal) ||
text.EndsWith("== \"", StringComparison.Ordinal) ||
text.EndsWith(":= \"", StringComparison.Ordinal));
private static bool IsVexJustificationContext(string text) =>
text.Contains("justification", StringComparison.OrdinalIgnoreCase) &&
(text.EndsWith("== ", StringComparison.Ordinal) ||
text.EndsWith("!= ", StringComparison.Ordinal) ||
text.EndsWith("== \"", StringComparison.Ordinal));
private static bool IsActionContext(string text)
{
var trimmed = text.TrimEnd();
return trimmed.EndsWith(" then", StringComparison.OrdinalIgnoreCase) ||
trimmed.EndsWith(" else", StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>
/// Context for completion requests.
/// </summary>
/// <param name="TriggerText">The text up to and including the cursor position.</param>
/// <param name="LineNumber">The 1-based line number of the cursor.</param>
/// <param name="Column">The 1-based column number of the cursor.</param>
public sealed record DslCompletionContext(
string TriggerText,
int LineNumber = 1,
int Column = 1);
/// <summary>
/// A single completion item.
/// </summary>
/// <param name="Label">The display label for the completion.</param>
/// <param name="Kind">The kind of completion (keyword, field, function, etc.).</param>
/// <param name="InsertText">The text to insert when the completion is accepted.</param>
/// <param name="Documentation">Documentation describing the completion item.</param>
/// <param name="Detail">Additional detail shown in the completion list.</param>
/// <param name="IsSnippet">Whether the insert text is a snippet with placeholders.</param>
public sealed record DslCompletionItem(
string Label,
DslCompletionKind Kind,
string InsertText,
string Documentation,
string? Detail = null,
bool IsSnippet = false);
/// <summary>
/// The kind of completion item.
/// </summary>
public enum DslCompletionKind
{
Keyword = 14,
Function = 1,
Field = 5,
Constant = 21,
Namespace = 9,
Snippet = 15,
}
/// <summary>
/// Catalog of all completion items, organized by category.
/// </summary>
public sealed class DslCompletionCatalog
{
/// <summary>
/// Singleton instance of the completion catalog.
/// </summary>
public static DslCompletionCatalog Instance { get; } = new();
private DslCompletionCatalog()
{
// Initialize all completion categories
Keywords = BuildKeywords();
Functions = BuildFunctions();
Namespaces = BuildNamespaces();
ScoreFields = BuildScoreFields();
ScoreBuckets = BuildScoreBuckets();
ScoreFlags = BuildScoreFlags();
SbomFields = BuildSbomFields();
AdvisoryFields = BuildAdvisoryFields();
VexFields = BuildVexFields();
VexStatuses = BuildVexStatuses();
VexJustifications = BuildVexJustifications();
SignalFields = BuildSignalFields();
ReachabilityFields = BuildReachabilityFields();
Actions = BuildActions();
}
/// <summary>DSL keywords (policy, rule, when, then, etc.).</summary>
public ImmutableArray<DslCompletionItem> Keywords { get; }
/// <summary>Built-in functions.</summary>
public ImmutableArray<DslCompletionItem> Functions { get; }
/// <summary>Top-level namespaces (score, sbom, advisory, etc.).</summary>
public ImmutableArray<DslCompletionItem> Namespaces { get; }
/// <summary>Score namespace fields.</summary>
public ImmutableArray<DslCompletionItem> ScoreFields { get; }
/// <summary>Score bucket values.</summary>
public ImmutableArray<DslCompletionItem> ScoreBuckets { get; }
/// <summary>Score flag values.</summary>
public ImmutableArray<DslCompletionItem> ScoreFlags { get; }
/// <summary>SBOM namespace fields.</summary>
public ImmutableArray<DslCompletionItem> SbomFields { get; }
/// <summary>Advisory namespace fields.</summary>
public ImmutableArray<DslCompletionItem> AdvisoryFields { get; }
/// <summary>VEX namespace fields.</summary>
public ImmutableArray<DslCompletionItem> VexFields { get; }
/// <summary>VEX status values.</summary>
public ImmutableArray<DslCompletionItem> VexStatuses { get; }
/// <summary>VEX justification values.</summary>
public ImmutableArray<DslCompletionItem> VexJustifications { get; }
/// <summary>Signal namespace fields.</summary>
public ImmutableArray<DslCompletionItem> SignalFields { get; }
/// <summary>Reachability namespace fields.</summary>
public ImmutableArray<DslCompletionItem> ReachabilityFields { get; }
/// <summary>Action keywords and patterns.</summary>
public ImmutableArray<DslCompletionItem> Actions { get; }
private static ImmutableArray<DslCompletionItem> BuildKeywords() =>
[
new("policy", DslCompletionKind.Keyword, "policy \"${1:PolicyName}\" syntax \"stella-dsl@1\" {\n\t$0\n}",
"Define a new policy document.", "Policy Declaration", true),
new("rule", DslCompletionKind.Keyword, "rule ${1:rule_name} priority ${2:10} {\n\twhen ${3:condition}\n\tthen ${4:action}\n\tbecause \"${5:rationale}\";\n}",
"Define a policy rule with when/then logic.", "Rule Definition", true),
new("when", DslCompletionKind.Keyword, "when ${1:condition}",
"Condition clause for rule execution.", "Rule Condition", true),
new("then", DslCompletionKind.Keyword, "then ${1:action}",
"Action clause executed when condition is true.", "Rule Action", true),
new("else", DslCompletionKind.Keyword, "else ${1:action}",
"Fallback action clause.", "Rule Else Action", true),
new("because", DslCompletionKind.Keyword, "because \"${1:rationale}\"",
"Mandatory rationale for status/severity changes.", "Rule Rationale", true),
new("metadata", DslCompletionKind.Keyword, "metadata {\n\tdescription = \"${1:description}\"\n\ttags = [$2]\n}",
"Define metadata for the policy.", "Metadata Section", true),
new("settings", DslCompletionKind.Keyword, "settings {\n\t${1:shadow} = ${2:true};\n}",
"Configure evaluation settings.", "Settings Section", true),
new("profile", DslCompletionKind.Keyword, "profile ${1:severity} {\n\t$0\n}",
"Define a profile block for scoring modifiers.", "Profile Section", true),
new("and", DslCompletionKind.Keyword, "and", "Logical AND operator."),
new("or", DslCompletionKind.Keyword, "or", "Logical OR operator."),
new("not", DslCompletionKind.Keyword, "not", "Logical NOT operator."),
new("in", DslCompletionKind.Keyword, "in", "Membership test operator."),
new("between", DslCompletionKind.Keyword, "between ${1:min} and ${2:max}",
"Range comparison operator.", "Range Check", true),
new("contains", DslCompletionKind.Keyword, "contains", "Array contains operator."),
];
private static ImmutableArray<DslCompletionItem> BuildFunctions() =>
[
new("normalize_cvss", DslCompletionKind.Function, "normalize_cvss(${1:advisory})",
"Parse advisory for CVSS data and return severity scalar.", "Advisory → SeverityScalar", true),
new("severity_band", DslCompletionKind.Function, "severity_band(\"${1:severity}\")",
"Normalise severity string to band.", "string → SeverityBand", true),
new("risk_score", DslCompletionKind.Function, "risk_score(${1:base}, ${2:modifier})",
"Calculate risk by multiplying severity × trust × reachability.", "Variadic", true),
new("exists", DslCompletionKind.Function, "exists(${1:expression})",
"Return true when value is non-null/empty.", "→ bool", true),
new("coalesce", DslCompletionKind.Function, "coalesce(${1:a}, ${2:b})",
"Return first non-null argument.", "→ value", true),
new("days_between", DslCompletionKind.Function, "days_between(${1:dateA}, ${2:dateB})",
"Calculate absolute day difference (UTC).", "→ int", true),
];
private static ImmutableArray<DslCompletionItem> BuildNamespaces() =>
[
new("score", DslCompletionKind.Namespace, "score",
"Evidence-weighted score object. Access via score.value, score.bucket, etc."),
new("sbom", DslCompletionKind.Namespace, "sbom",
"SBOM (Software Bill of Materials) data for the finding."),
new("advisory", DslCompletionKind.Namespace, "advisory",
"Security advisory information."),
new("vex", DslCompletionKind.Namespace, "vex",
"VEX (Vulnerability Exploitability eXchange) statements."),
new("severity", DslCompletionKind.Namespace, "severity",
"Severity information for the finding."),
new("signals", DslCompletionKind.Namespace, "signals",
"Signal data including trust scores and runtime evidence."),
new("reachability", DslCompletionKind.Namespace, "reachability",
"Reachability analysis results."),
new("entropy", DslCompletionKind.Namespace, "entropy",
"Entropy and uncertainty metrics."),
new("env", DslCompletionKind.Namespace, "env",
"Environment context (dev, staging, prod, etc.)."),
new("run", DslCompletionKind.Namespace, "run",
"Runtime context (policy ID, tenant, timestamp)."),
];
private static ImmutableArray<DslCompletionItem> BuildScoreFields() =>
[
// Core score value
new("value", DslCompletionKind.Field, "value",
"Numeric score value (0-100). Use in comparisons like: score.value >= 80",
"decimal"),
// Bucket access
new("bucket", DslCompletionKind.Field, "bucket",
"Score bucket: ActNow, ScheduleNext, Investigate, or Watchlist.",
"string"),
new("is_act_now", DslCompletionKind.Field, "is_act_now",
"True if bucket is ActNow (highest priority).",
"bool"),
new("is_schedule_next", DslCompletionKind.Field, "is_schedule_next",
"True if bucket is ScheduleNext.",
"bool"),
new("is_investigate", DslCompletionKind.Field, "is_investigate",
"True if bucket is Investigate.",
"bool"),
new("is_watchlist", DslCompletionKind.Field, "is_watchlist",
"True if bucket is Watchlist (lowest priority).",
"bool"),
// Individual dimension scores (0-1 normalized)
new("rch", DslCompletionKind.Field, "rch",
"Reachability dimension score (0-1 normalized). Alias: reachability",
"double"),
new("reachability", DslCompletionKind.Field, "reachability",
"Reachability dimension score (0-1 normalized). Alias: rch",
"double"),
new("rts", DslCompletionKind.Field, "rts",
"Runtime signal dimension score (0-1 normalized). Alias: runtime",
"double"),
new("runtime", DslCompletionKind.Field, "runtime",
"Runtime signal dimension score (0-1 normalized). Alias: rts",
"double"),
new("bkp", DslCompletionKind.Field, "bkp",
"Backport dimension score (0-1 normalized). Alias: backport",
"double"),
new("backport", DslCompletionKind.Field, "backport",
"Backport dimension score (0-1 normalized). Alias: bkp",
"double"),
new("xpl", DslCompletionKind.Field, "xpl",
"Exploit evidence dimension score (0-1 normalized). Alias: exploit",
"double"),
new("exploit", DslCompletionKind.Field, "exploit",
"Exploit evidence dimension score (0-1 normalized). Alias: xpl",
"double"),
new("src", DslCompletionKind.Field, "src",
"Source trust dimension score (0-1 normalized). Alias: source_trust",
"double"),
new("source_trust", DslCompletionKind.Field, "source_trust",
"Source trust dimension score (0-1 normalized). Alias: src",
"double"),
new("mit", DslCompletionKind.Field, "mit",
"Mitigation dimension score (0-1 normalized). Alias: mitigation",
"double"),
new("mitigation", DslCompletionKind.Field, "mitigation",
"Mitigation dimension score (0-1 normalized). Alias: mit",
"double"),
// Flags
new("flags", DslCompletionKind.Field, "flags",
"Array of score flags (e.g., \"kev\", \"live-signal\", \"vendor-na\").",
"string[]"),
// Metadata
new("policy_digest", DslCompletionKind.Field, "policy_digest",
"SHA-256 digest of the policy used for scoring.",
"string"),
new("calculated_at", DslCompletionKind.Field, "calculated_at",
"ISO 8601 timestamp when score was calculated.",
"DateTime"),
new("explanations", DslCompletionKind.Field, "explanations",
"Array of human-readable explanations for the score.",
"string[]"),
];
private static ImmutableArray<DslCompletionItem> BuildScoreBuckets() =>
[
new("ActNow", DslCompletionKind.Constant, "\"ActNow\"",
"Highest priority: immediate action required."),
new("ScheduleNext", DslCompletionKind.Constant, "\"ScheduleNext\"",
"High priority: schedule remediation soon."),
new("Investigate", DslCompletionKind.Constant, "\"Investigate\"",
"Medium priority: requires investigation."),
new("Watchlist", DslCompletionKind.Constant, "\"Watchlist\"",
"Low priority: monitor for changes."),
];
private static ImmutableArray<DslCompletionItem> BuildScoreFlags() =>
[
new("kev", DslCompletionKind.Constant, "\"kev\"",
"Known Exploited Vulnerability (CISA KEV list)."),
new("live-signal", DslCompletionKind.Constant, "\"live-signal\"",
"Runtime evidence detected active exploitation."),
new("vendor-na", DslCompletionKind.Constant, "\"vendor-na\"",
"Vendor confirms not affected."),
new("epss-high", DslCompletionKind.Constant, "\"epss-high\"",
"High EPSS probability score."),
new("reachable", DslCompletionKind.Constant, "\"reachable\"",
"Code is statically or dynamically reachable."),
new("unreachable", DslCompletionKind.Constant, "\"unreachable\"",
"Code is confirmed unreachable."),
new("backported", DslCompletionKind.Constant, "\"backported\"",
"Fix has been backported by vendor."),
];
private static ImmutableArray<DslCompletionItem> BuildSbomFields() =>
[
new("purl", DslCompletionKind.Field, "purl", "Package URL of the component."),
new("name", DslCompletionKind.Field, "name", "Component name."),
new("version", DslCompletionKind.Field, "version", "Component version."),
new("licenses", DslCompletionKind.Field, "licenses", "Component licenses."),
new("layerDigest", DslCompletionKind.Field, "layerDigest", "Container layer digest."),
new("tags", DslCompletionKind.Field, "tags", "Component tags."),
new("usedByEntrypoint", DslCompletionKind.Field, "usedByEntrypoint",
"Whether component is used by entrypoint."),
];
private static ImmutableArray<DslCompletionItem> BuildAdvisoryFields() =>
[
new("id", DslCompletionKind.Field, "id", "Advisory identifier."),
new("source", DslCompletionKind.Field, "source", "Advisory source (GHSA, OSV, etc.)."),
new("aliases", DslCompletionKind.Field, "aliases", "Advisory aliases (CVE, etc.)."),
new("severity", DslCompletionKind.Field, "severity", "Advisory severity."),
new("cvss", DslCompletionKind.Field, "cvss", "CVSS score."),
new("publishedAt", DslCompletionKind.Field, "publishedAt", "Publication date."),
new("modifiedAt", DslCompletionKind.Field, "modifiedAt", "Last modification date."),
];
private static ImmutableArray<DslCompletionItem> BuildVexFields() =>
[
new("status", DslCompletionKind.Field, "status", "VEX status."),
new("justification", DslCompletionKind.Field, "justification", "VEX justification."),
new("statementId", DslCompletionKind.Field, "statementId", "VEX statement ID."),
new("timestamp", DslCompletionKind.Field, "timestamp", "VEX timestamp."),
new("scope", DslCompletionKind.Field, "scope", "VEX scope."),
new("any", DslCompletionKind.Function, "any(${1:predicate})",
"True if any VEX statement satisfies the predicate.", "(Statement → bool) → bool", true),
new("all", DslCompletionKind.Function, "all(${1:predicate})",
"True if all VEX statements satisfy the predicate.", "(Statement → bool) → bool", true),
new("latest", DslCompletionKind.Function, "latest()",
"Return the lexicographically newest VEX statement.", "→ Statement", true),
new("count", DslCompletionKind.Function, "count(${1:predicate})",
"Count VEX statements matching predicate.", "→ int", true),
];
private static ImmutableArray<DslCompletionItem> BuildVexStatuses() =>
[
new("affected", DslCompletionKind.Constant, "\"affected\"",
"Component is affected by the vulnerability."),
new("not_affected", DslCompletionKind.Constant, "\"not_affected\"",
"Component is not affected."),
new("fixed", DslCompletionKind.Constant, "\"fixed\"",
"Vulnerability has been fixed."),
new("suppressed", DslCompletionKind.Constant, "\"suppressed\"",
"Finding is suppressed."),
new("under_investigation", DslCompletionKind.Constant, "\"under_investigation\"",
"Under investigation."),
new("escalated", DslCompletionKind.Constant, "\"escalated\"",
"Finding has been escalated."),
];
private static ImmutableArray<DslCompletionItem> BuildVexJustifications() =>
[
new("component_not_present", DslCompletionKind.Constant, "\"component_not_present\"",
"Component is not present in the product."),
new("vulnerable_code_not_present", DslCompletionKind.Constant, "\"vulnerable_code_not_present\"",
"Vulnerable code is not present."),
new("vulnerable_code_not_in_execute_path", DslCompletionKind.Constant, "\"vulnerable_code_not_in_execute_path\"",
"Vulnerable code is not in execution path."),
new("vulnerable_code_cannot_be_controlled_by_adversary", DslCompletionKind.Constant, "\"vulnerable_code_cannot_be_controlled_by_adversary\"",
"Vulnerable code cannot be controlled by adversary."),
new("inline_mitigations_already_exist", DslCompletionKind.Constant, "\"inline_mitigations_already_exist\"",
"Inline mitigations already exist."),
];
private static ImmutableArray<DslCompletionItem> BuildSignalFields() =>
[
new("trust_score", DslCompletionKind.Field, "trust_score",
"Trust score (01)."),
new("reachability.state", DslCompletionKind.Field, "reachability.state",
"Reachability state."),
new("reachability.score", DslCompletionKind.Field, "reachability.score",
"Reachability score (01)."),
new("entropy_penalty", DslCompletionKind.Field, "entropy_penalty",
"Entropy penalty (00.3)."),
new("uncertainty.level", DslCompletionKind.Field, "uncertainty.level",
"Uncertainty level (U1U3)."),
new("runtime_hits", DslCompletionKind.Field, "runtime_hits",
"Runtime hit indicator."),
];
private static ImmutableArray<DslCompletionItem> BuildReachabilityFields() =>
[
new("state", DslCompletionKind.Field, "state",
"Reachability state (reachable, unreachable, unknown)."),
new("score", DslCompletionKind.Field, "score",
"Reachability confidence score (01)."),
new("callchain", DslCompletionKind.Field, "callchain",
"Call chain evidence if reachable."),
new("tool", DslCompletionKind.Field, "tool",
"Tool that determined reachability."),
];
private static ImmutableArray<DslCompletionItem> BuildActions() =>
[
new("status :=", DslCompletionKind.Keyword, "status := \"${1:status}\"",
"Set the finding status.", "Status Assignment", true),
new("severity :=", DslCompletionKind.Keyword, "severity := ${1:expression}",
"Set the finding severity.", "Severity Assignment", true),
new("ignore", DslCompletionKind.Keyword, "ignore until ${1:date} because \"${2:rationale}\"",
"Temporarily suppress finding until date.", "Ignore Action", true),
new("escalate", DslCompletionKind.Keyword, "escalate to severity_band(\"${1:severity}\") when ${2:condition}",
"Escalate severity when condition is true.", "Escalate Action", true),
new("warn", DslCompletionKind.Keyword, "warn message \"${1:text}\"",
"Add warning verdict.", "Warn Action", true),
new("defer", DslCompletionKind.Keyword, "defer until ${1:condition}",
"Defer finding evaluation.", "Defer Action", true),
new("annotate", DslCompletionKind.Keyword, "annotate ${1:key} := ${2:value}",
"Add free-form annotation to explain payload.", "Annotate Action", true),
new("requireVex", DslCompletionKind.Keyword, "requireVex {\n\tvendors = [${1:\"Vendor\"}]\n\tjustifications = [${2:\"component_not_present\"}]\n}",
"Require matching VEX evidence.", "Require VEX Action", true),
];
}