up
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.Excititor.Storage.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL data source for the Excititor (VEX) module.
|
||||
/// Manages connections with tenant context for VEX statements and dependency graphs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Excititor module handles high-volume graph data (nodes/edges) and requires
|
||||
/// optimized queries for graph traversal and VEX consensus computation.
|
||||
/// </remarks>
|
||||
public sealed class ExcititorDataSource : DataSourceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Default schema name for Excititor/VEX tables.
|
||||
/// </summary>
|
||||
public const string DefaultSchemaName = "vex";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Excititor data source.
|
||||
/// </summary>
|
||||
public ExcititorDataSource(IOptions<PostgresOptions> options, ILogger<ExcititorDataSource> logger)
|
||||
: base(CreateOptions(options.Value), logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ModuleName => "Excititor";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureDataSourceBuilder(NpgsqlDataSourceBuilder builder)
|
||||
{
|
||||
base.ConfigureDataSourceBuilder(builder);
|
||||
// Configure for high-throughput graph operations
|
||||
}
|
||||
|
||||
private static PostgresOptions CreateOptions(PostgresOptions baseOptions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseOptions.SchemaName))
|
||||
{
|
||||
baseOptions.SchemaName = DefaultSchemaName;
|
||||
}
|
||||
return baseOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
-- VEX Schema Migration 001: Initial Schema
|
||||
-- Creates the vex schema for VEX statements and dependency graphs
|
||||
|
||||
-- Create schema
|
||||
CREATE SCHEMA IF NOT EXISTS vex;
|
||||
|
||||
-- Projects table
|
||||
CREATE TABLE IF NOT EXISTS vex.projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
repository_url TEXT,
|
||||
default_branch TEXT,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT,
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_projects_tenant ON vex.projects(tenant_id);
|
||||
|
||||
-- Graph revisions table
|
||||
CREATE TABLE IF NOT EXISTS vex.graph_revisions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id UUID NOT NULL REFERENCES vex.projects(id) ON DELETE CASCADE,
|
||||
revision_id TEXT NOT NULL UNIQUE,
|
||||
parent_revision_id TEXT,
|
||||
sbom_digest TEXT NOT NULL,
|
||||
feed_snapshot_id TEXT,
|
||||
policy_version TEXT,
|
||||
node_count INT NOT NULL DEFAULT 0,
|
||||
edge_count INT NOT NULL DEFAULT 0,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_graph_revisions_project ON vex.graph_revisions(project_id);
|
||||
CREATE INDEX idx_graph_revisions_revision ON vex.graph_revisions(revision_id);
|
||||
CREATE INDEX idx_graph_revisions_created ON vex.graph_revisions(project_id, created_at DESC);
|
||||
|
||||
-- Graph nodes table (BIGSERIAL for high volume)
|
||||
CREATE TABLE IF NOT EXISTS vex.graph_nodes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
graph_revision_id UUID NOT NULL REFERENCES vex.graph_revisions(id) ON DELETE CASCADE,
|
||||
node_key TEXT NOT NULL,
|
||||
node_type TEXT NOT NULL,
|
||||
purl TEXT,
|
||||
name TEXT,
|
||||
version TEXT,
|
||||
attributes JSONB NOT NULL DEFAULT '{}',
|
||||
UNIQUE(graph_revision_id, node_key)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_graph_nodes_revision ON vex.graph_nodes(graph_revision_id);
|
||||
CREATE INDEX idx_graph_nodes_key ON vex.graph_nodes(graph_revision_id, node_key);
|
||||
CREATE INDEX idx_graph_nodes_purl ON vex.graph_nodes(purl);
|
||||
CREATE INDEX idx_graph_nodes_type ON vex.graph_nodes(graph_revision_id, node_type);
|
||||
|
||||
-- Graph edges table (BIGSERIAL for high volume)
|
||||
CREATE TABLE IF NOT EXISTS vex.graph_edges (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
graph_revision_id UUID NOT NULL REFERENCES vex.graph_revisions(id) ON DELETE CASCADE,
|
||||
from_node_id BIGINT NOT NULL REFERENCES vex.graph_nodes(id) ON DELETE CASCADE,
|
||||
to_node_id BIGINT NOT NULL REFERENCES vex.graph_nodes(id) ON DELETE CASCADE,
|
||||
edge_type TEXT NOT NULL,
|
||||
attributes JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_graph_edges_revision ON vex.graph_edges(graph_revision_id);
|
||||
CREATE INDEX idx_graph_edges_from ON vex.graph_edges(from_node_id);
|
||||
CREATE INDEX idx_graph_edges_to ON vex.graph_edges(to_node_id);
|
||||
|
||||
-- VEX statements table
|
||||
CREATE TABLE IF NOT EXISTS vex.statements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
project_id UUID REFERENCES vex.projects(id),
|
||||
graph_revision_id UUID REFERENCES vex.graph_revisions(id),
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
product_id TEXT,
|
||||
status TEXT NOT NULL CHECK (status IN (
|
||||
'not_affected', 'affected', 'fixed', 'under_investigation'
|
||||
)),
|
||||
justification TEXT CHECK (justification IN (
|
||||
'component_not_present', 'vulnerable_code_not_present',
|
||||
'vulnerable_code_not_in_execute_path', 'vulnerable_code_cannot_be_controlled_by_adversary',
|
||||
'inline_mitigations_already_exist'
|
||||
)),
|
||||
impact_statement TEXT,
|
||||
action_statement TEXT,
|
||||
action_statement_timestamp TIMESTAMPTZ,
|
||||
first_issued TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
source TEXT,
|
||||
source_url TEXT,
|
||||
evidence JSONB NOT NULL DEFAULT '{}',
|
||||
provenance JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_by TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_statements_tenant ON vex.statements(tenant_id);
|
||||
CREATE INDEX idx_statements_project ON vex.statements(project_id);
|
||||
CREATE INDEX idx_statements_revision ON vex.statements(graph_revision_id);
|
||||
CREATE INDEX idx_statements_vuln ON vex.statements(vulnerability_id);
|
||||
CREATE INDEX idx_statements_status ON vex.statements(tenant_id, status);
|
||||
|
||||
-- VEX observations table
|
||||
CREATE TABLE IF NOT EXISTS vex.observations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
statement_id UUID REFERENCES vex.statements(id) ON DELETE CASCADE,
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
product_id TEXT NOT NULL,
|
||||
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
observer TEXT NOT NULL,
|
||||
observation_type TEXT NOT NULL,
|
||||
confidence NUMERIC(3,2),
|
||||
details JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, vulnerability_id, product_id, observer, observation_type)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_observations_tenant ON vex.observations(tenant_id);
|
||||
CREATE INDEX idx_observations_statement ON vex.observations(statement_id);
|
||||
CREATE INDEX idx_observations_vuln ON vex.observations(vulnerability_id, product_id);
|
||||
|
||||
-- Linksets table
|
||||
CREATE TABLE IF NOT EXISTS vex.linksets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
source_type TEXT NOT NULL,
|
||||
source_url TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
filter JSONB NOT NULL DEFAULT '{}',
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_linksets_tenant ON vex.linksets(tenant_id);
|
||||
CREATE INDEX idx_linksets_enabled ON vex.linksets(tenant_id, enabled, priority DESC);
|
||||
|
||||
-- Linkset events table
|
||||
CREATE TABLE IF NOT EXISTS vex.linkset_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
linkset_id UUID NOT NULL REFERENCES vex.linksets(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
statement_count INT NOT NULL DEFAULT 0,
|
||||
error_message TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_linkset_events_linkset ON vex.linkset_events(linkset_id);
|
||||
CREATE INDEX idx_linkset_events_created ON vex.linkset_events(created_at);
|
||||
|
||||
-- Consensus table (VEX consensus state)
|
||||
CREATE TABLE IF NOT EXISTS vex.consensus (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
product_id TEXT NOT NULL,
|
||||
consensus_status TEXT NOT NULL,
|
||||
contributing_statements UUID[] NOT NULL DEFAULT '{}',
|
||||
confidence NUMERIC(3,2),
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
UNIQUE(tenant_id, vulnerability_id, product_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_consensus_tenant ON vex.consensus(tenant_id);
|
||||
CREATE INDEX idx_consensus_vuln ON vex.consensus(vulnerability_id, product_id);
|
||||
|
||||
-- Consensus holds table
|
||||
CREATE TABLE IF NOT EXISTS vex.consensus_holds (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
consensus_id UUID NOT NULL REFERENCES vex.consensus(id) ON DELETE CASCADE,
|
||||
hold_type TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
held_by TEXT NOT NULL,
|
||||
held_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
released_at TIMESTAMPTZ,
|
||||
released_by TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_consensus_holds_consensus ON vex.consensus_holds(consensus_id);
|
||||
CREATE INDEX idx_consensus_holds_active ON vex.consensus_holds(consensus_id, released_at)
|
||||
WHERE released_at IS NULL;
|
||||
|
||||
-- Unknown snapshots table
|
||||
CREATE TABLE IF NOT EXISTS vex.unknowns_snapshots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
project_id UUID REFERENCES vex.projects(id),
|
||||
graph_revision_id UUID REFERENCES vex.graph_revisions(id),
|
||||
snapshot_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
unknown_count INT NOT NULL DEFAULT 0,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_unknowns_snapshots_tenant ON vex.unknowns_snapshots(tenant_id);
|
||||
CREATE INDEX idx_unknowns_snapshots_project ON vex.unknowns_snapshots(project_id);
|
||||
|
||||
-- Unknown items table
|
||||
CREATE TABLE IF NOT EXISTS vex.unknown_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
snapshot_id UUID NOT NULL REFERENCES vex.unknowns_snapshots(id) ON DELETE CASCADE,
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
product_id TEXT,
|
||||
reason TEXT NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_unknown_items_snapshot ON vex.unknown_items(snapshot_id);
|
||||
CREATE INDEX idx_unknown_items_vuln ON vex.unknown_items(vulnerability_id);
|
||||
|
||||
-- Evidence manifests table
|
||||
CREATE TABLE IF NOT EXISTS vex.evidence_manifests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
statement_id UUID REFERENCES vex.statements(id) ON DELETE CASCADE,
|
||||
manifest_type TEXT NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
content JSONB NOT NULL,
|
||||
source TEXT,
|
||||
collected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_evidence_manifests_tenant ON vex.evidence_manifests(tenant_id);
|
||||
CREATE INDEX idx_evidence_manifests_statement ON vex.evidence_manifests(statement_id);
|
||||
|
||||
-- CVSS receipts table
|
||||
CREATE TABLE IF NOT EXISTS vex.cvss_receipts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
statement_id UUID REFERENCES vex.statements(id) ON DELETE CASCADE,
|
||||
vulnerability_id TEXT NOT NULL,
|
||||
cvss_version TEXT NOT NULL,
|
||||
vector_string TEXT NOT NULL,
|
||||
base_score NUMERIC(3,1) NOT NULL,
|
||||
environmental_score NUMERIC(3,1),
|
||||
temporal_score NUMERIC(3,1),
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
metadata JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cvss_receipts_tenant ON vex.cvss_receipts(tenant_id);
|
||||
CREATE INDEX idx_cvss_receipts_statement ON vex.cvss_receipts(statement_id);
|
||||
CREATE INDEX idx_cvss_receipts_vuln ON vex.cvss_receipts(vulnerability_id);
|
||||
|
||||
-- Attestations table
|
||||
CREATE TABLE IF NOT EXISTS vex.attestations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
statement_id UUID REFERENCES vex.statements(id),
|
||||
subject_digest TEXT NOT NULL,
|
||||
predicate_type TEXT NOT NULL,
|
||||
predicate JSONB NOT NULL,
|
||||
signature TEXT,
|
||||
signature_algorithm TEXT,
|
||||
signed_by TEXT,
|
||||
signed_at TIMESTAMPTZ,
|
||||
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
verified_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_attestations_tenant ON vex.attestations(tenant_id);
|
||||
CREATE INDEX idx_attestations_statement ON vex.attestations(statement_id);
|
||||
CREATE INDEX idx_attestations_subject ON vex.attestations(subject_digest);
|
||||
|
||||
-- Timeline events table
|
||||
CREATE TABLE IF NOT EXISTS vex.timeline_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
project_id UUID REFERENCES vex.projects(id),
|
||||
statement_id UUID REFERENCES vex.statements(id),
|
||||
event_type TEXT NOT NULL,
|
||||
event_data JSONB NOT NULL DEFAULT '{}',
|
||||
actor TEXT,
|
||||
correlation_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_timeline_events_tenant ON vex.timeline_events(tenant_id);
|
||||
CREATE INDEX idx_timeline_events_project ON vex.timeline_events(project_id);
|
||||
CREATE INDEX idx_timeline_events_statement ON vex.timeline_events(statement_id);
|
||||
CREATE INDEX idx_timeline_events_created ON vex.timeline_events(tenant_id, created_at);
|
||||
CREATE INDEX idx_timeline_events_correlation ON vex.timeline_events(correlation_id);
|
||||
|
||||
-- Update timestamp function
|
||||
CREATE OR REPLACE FUNCTION vex.update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Triggers
|
||||
CREATE TRIGGER trg_projects_updated_at
|
||||
BEFORE UPDATE ON vex.projects
|
||||
FOR EACH ROW EXECUTE FUNCTION vex.update_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_linksets_updated_at
|
||||
BEFORE UPDATE ON vex.linksets
|
||||
FOR EACH ROW EXECUTE FUNCTION vex.update_updated_at();
|
||||
|
||||
CREATE TRIGGER trg_statements_updated_at
|
||||
BEFORE UPDATE ON vex.statements
|
||||
FOR EACH ROW EXECUTE FUNCTION vex.update_updated_at();
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace StellaOps.Excititor.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a project entity in the vex schema.
|
||||
/// </summary>
|
||||
public sealed class ProjectEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique project identifier.
|
||||
/// </summary>
|
||||
public required Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant this project belongs to.
|
||||
/// </summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Project name (unique per tenant).
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Display name.
|
||||
/// </summary>
|
||||
public string? DisplayName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Project description.
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository URL.
|
||||
/// </summary>
|
||||
public string? RepositoryUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Default branch name.
|
||||
/// </summary>
|
||||
public string? DefaultBranch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Project settings as JSON.
|
||||
/// </summary>
|
||||
public string Settings { get; init; } = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// Project metadata as JSON.
|
||||
/// </summary>
|
||||
public string Metadata { get; init; } = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// When the project was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the project was last updated.
|
||||
/// </summary>
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// User who created the project.
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
namespace StellaOps.Excititor.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// VEX status values per OpenVEX specification.
|
||||
/// </summary>
|
||||
public enum VexStatus
|
||||
{
|
||||
/// <summary>Product is not affected by the vulnerability.</summary>
|
||||
NotAffected,
|
||||
/// <summary>Product is affected by the vulnerability.</summary>
|
||||
Affected,
|
||||
/// <summary>Vulnerability is fixed in this product version.</summary>
|
||||
Fixed,
|
||||
/// <summary>Vulnerability is under investigation.</summary>
|
||||
UnderInvestigation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX justification codes per OpenVEX specification.
|
||||
/// </summary>
|
||||
public enum VexJustification
|
||||
{
|
||||
/// <summary>The vulnerable component is not present.</summary>
|
||||
ComponentNotPresent,
|
||||
/// <summary>The vulnerable code is not present.</summary>
|
||||
VulnerableCodeNotPresent,
|
||||
/// <summary>The vulnerable code is not in execute path.</summary>
|
||||
VulnerableCodeNotInExecutePath,
|
||||
/// <summary>The vulnerable code cannot be controlled by adversary.</summary>
|
||||
VulnerableCodeCannotBeControlledByAdversary,
|
||||
/// <summary>Inline mitigations already exist.</summary>
|
||||
InlineMitigationsAlreadyExist
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a VEX statement entity in the vex schema.
|
||||
/// </summary>
|
||||
public sealed class VexStatementEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique statement identifier.
|
||||
/// </summary>
|
||||
public required Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant this statement belongs to.
|
||||
/// </summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Project this statement applies to.
|
||||
/// </summary>
|
||||
public Guid? ProjectId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Graph revision this statement is associated with.
|
||||
/// </summary>
|
||||
public Guid? GraphRevisionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability ID (CVE, GHSA, etc.).
|
||||
/// </summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Product identifier (PURL or product key).
|
||||
/// </summary>
|
||||
public string? ProductId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX status.
|
||||
/// </summary>
|
||||
public required VexStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for not_affected status.
|
||||
/// </summary>
|
||||
public VexJustification? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Impact statement describing effects.
|
||||
/// </summary>
|
||||
public string? ImpactStatement { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Action statement describing remediation.
|
||||
/// </summary>
|
||||
public string? ActionStatement { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When action statement was issued.
|
||||
/// </summary>
|
||||
public DateTimeOffset? ActionStatementTimestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When statement was first issued.
|
||||
/// </summary>
|
||||
public DateTimeOffset FirstIssued { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When statement was last updated.
|
||||
/// </summary>
|
||||
public DateTimeOffset LastUpdated { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source of the statement.
|
||||
/// </summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to source document.
|
||||
/// </summary>
|
||||
public string? SourceUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence supporting the statement as JSON.
|
||||
/// </summary>
|
||||
public string Evidence { get; init; } = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// Provenance information as JSON.
|
||||
/// </summary>
|
||||
public string Provenance { get; init; } = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// Statement metadata as JSON.
|
||||
/// </summary>
|
||||
public string Metadata { get; init; } = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// User who created the statement.
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using StellaOps.Excititor.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Excititor.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for VEX statement operations.
|
||||
/// </summary>
|
||||
public interface IVexStatementRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new VEX statement.
|
||||
/// </summary>
|
||||
Task<VexStatementEntity> CreateAsync(VexStatementEntity statement, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a VEX statement by ID.
|
||||
/// </summary>
|
||||
Task<VexStatementEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets VEX statements for a vulnerability.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexStatementEntity>> GetByVulnerabilityAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets VEX statements for a product.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexStatementEntity>> GetByProductAsync(
|
||||
string tenantId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets VEX statements for a project.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexStatementEntity>> GetByProjectAsync(
|
||||
string tenantId,
|
||||
Guid projectId,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets VEX statements by status.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<VexStatementEntity>> GetByStatusAsync(
|
||||
string tenantId,
|
||||
VexStatus status,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a VEX statement.
|
||||
/// </summary>
|
||||
Task<bool> UpdateAsync(VexStatementEntity statement, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a VEX statement.
|
||||
/// </summary>
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective VEX status for a vulnerability/product combination.
|
||||
/// Applies lattice logic for status precedence.
|
||||
/// </summary>
|
||||
Task<VexStatementEntity?> GetEffectiveStatementAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Excititor.Storage.Postgres.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Excititor.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for VEX statement operations.
|
||||
/// </summary>
|
||||
public sealed class VexStatementRepository : RepositoryBase<ExcititorDataSource>, IVexStatementRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new VEX statement repository.
|
||||
/// </summary>
|
||||
public VexStatementRepository(ExcititorDataSource dataSource, ILogger<VexStatementRepository> logger)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VexStatementEntity> CreateAsync(VexStatementEntity statement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO vex.statements (
|
||||
id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
source, source_url, evidence, provenance, metadata, created_by
|
||||
)
|
||||
VALUES (
|
||||
@id, @tenant_id, @project_id, @graph_revision_id, @vulnerability_id, @product_id,
|
||||
@status, @justification, @impact_statement, @action_statement, @action_statement_timestamp,
|
||||
@source, @source_url, @evidence::jsonb, @provenance::jsonb, @metadata::jsonb, @created_by
|
||||
)
|
||||
RETURNING id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
first_issued, last_updated, source, source_url,
|
||||
evidence::text, provenance::text, metadata::text, created_by
|
||||
""";
|
||||
|
||||
await using var connection = await DataSource.OpenConnectionAsync(statement.TenantId, "writer", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
|
||||
AddStatementParameters(command, statement);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return MapStatement(reader);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VexStatementEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
first_issued, last_updated, source, source_url,
|
||||
evidence::text, provenance::text, metadata::text, created_by
|
||||
FROM vex.statements
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
tenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
},
|
||||
MapStatement,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<VexStatementEntity>> GetByVulnerabilityAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
first_issued, last_updated, source, source_url,
|
||||
evidence::text, provenance::text, metadata::text, created_by
|
||||
FROM vex.statements
|
||||
WHERE tenant_id = @tenant_id AND vulnerability_id = @vulnerability_id
|
||||
ORDER BY last_updated DESC, id
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "vulnerability_id", vulnerabilityId);
|
||||
},
|
||||
MapStatement,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<VexStatementEntity>> GetByProductAsync(
|
||||
string tenantId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
first_issued, last_updated, source, source_url,
|
||||
evidence::text, provenance::text, metadata::text, created_by
|
||||
FROM vex.statements
|
||||
WHERE tenant_id = @tenant_id AND product_id = @product_id
|
||||
ORDER BY last_updated DESC, id
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "product_id", productId);
|
||||
},
|
||||
MapStatement,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<VexStatementEntity>> GetByProjectAsync(
|
||||
string tenantId,
|
||||
Guid projectId,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
first_issued, last_updated, source, source_url,
|
||||
evidence::text, provenance::text, metadata::text, created_by
|
||||
FROM vex.statements
|
||||
WHERE tenant_id = @tenant_id AND project_id = @project_id
|
||||
ORDER BY last_updated DESC, id
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "project_id", projectId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
},
|
||||
MapStatement,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<VexStatementEntity>> GetByStatusAsync(
|
||||
string tenantId,
|
||||
VexStatus status,
|
||||
int limit = 100,
|
||||
int offset = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
first_issued, last_updated, source, source_url,
|
||||
evidence::text, provenance::text, metadata::text, created_by
|
||||
FROM vex.statements
|
||||
WHERE tenant_id = @tenant_id AND status = @status
|
||||
ORDER BY last_updated DESC, id
|
||||
LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
|
||||
return await QueryAsync(
|
||||
tenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "status", StatusToString(status));
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
},
|
||||
MapStatement,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateAsync(VexStatementEntity statement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE vex.statements
|
||||
SET status = @status,
|
||||
justification = @justification,
|
||||
impact_statement = @impact_statement,
|
||||
action_statement = @action_statement,
|
||||
action_statement_timestamp = @action_statement_timestamp,
|
||||
source = @source,
|
||||
source_url = @source_url,
|
||||
evidence = @evidence::jsonb,
|
||||
provenance = @provenance::jsonb,
|
||||
metadata = @metadata::jsonb
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
|
||||
var rows = await ExecuteAsync(
|
||||
statement.TenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", statement.TenantId);
|
||||
AddParameter(cmd, "id", statement.Id);
|
||||
AddParameter(cmd, "status", StatusToString(statement.Status));
|
||||
AddParameter(cmd, "justification", statement.Justification.HasValue
|
||||
? JustificationToString(statement.Justification.Value)
|
||||
: null);
|
||||
AddParameter(cmd, "impact_statement", statement.ImpactStatement);
|
||||
AddParameter(cmd, "action_statement", statement.ActionStatement);
|
||||
AddParameter(cmd, "action_statement_timestamp", statement.ActionStatementTimestamp);
|
||||
AddParameter(cmd, "source", statement.Source);
|
||||
AddParameter(cmd, "source_url", statement.SourceUrl);
|
||||
AddJsonbParameter(cmd, "evidence", statement.Evidence);
|
||||
AddJsonbParameter(cmd, "provenance", statement.Provenance);
|
||||
AddJsonbParameter(cmd, "metadata", statement.Metadata);
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM vex.statements WHERE tenant_id = @tenant_id AND id = @id";
|
||||
|
||||
var rows = await ExecuteAsync(
|
||||
tenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VexStatementEntity?> GetEffectiveStatementAsync(
|
||||
string tenantId,
|
||||
string vulnerabilityId,
|
||||
string productId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// VEX lattice precedence: fixed > not_affected > affected > under_investigation
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, project_id, graph_revision_id, vulnerability_id, product_id,
|
||||
status, justification, impact_statement, action_statement, action_statement_timestamp,
|
||||
first_issued, last_updated, source, source_url,
|
||||
evidence::text, provenance::text, metadata::text, created_by
|
||||
FROM vex.statements
|
||||
WHERE tenant_id = @tenant_id
|
||||
AND vulnerability_id = @vulnerability_id
|
||||
AND product_id = @product_id
|
||||
ORDER BY
|
||||
CASE status
|
||||
WHEN 'fixed' THEN 1
|
||||
WHEN 'not_affected' THEN 2
|
||||
WHEN 'affected' THEN 3
|
||||
WHEN 'under_investigation' THEN 4
|
||||
END,
|
||||
last_updated DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
return await QuerySingleOrDefaultAsync(
|
||||
tenantId,
|
||||
sql,
|
||||
cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "vulnerability_id", vulnerabilityId);
|
||||
AddParameter(cmd, "product_id", productId);
|
||||
},
|
||||
MapStatement,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void AddStatementParameters(NpgsqlCommand command, VexStatementEntity statement)
|
||||
{
|
||||
AddParameter(command, "id", statement.Id);
|
||||
AddParameter(command, "tenant_id", statement.TenantId);
|
||||
AddParameter(command, "project_id", statement.ProjectId);
|
||||
AddParameter(command, "graph_revision_id", statement.GraphRevisionId);
|
||||
AddParameter(command, "vulnerability_id", statement.VulnerabilityId);
|
||||
AddParameter(command, "product_id", statement.ProductId);
|
||||
AddParameter(command, "status", StatusToString(statement.Status));
|
||||
AddParameter(command, "justification", statement.Justification.HasValue
|
||||
? JustificationToString(statement.Justification.Value)
|
||||
: null);
|
||||
AddParameter(command, "impact_statement", statement.ImpactStatement);
|
||||
AddParameter(command, "action_statement", statement.ActionStatement);
|
||||
AddParameter(command, "action_statement_timestamp", statement.ActionStatementTimestamp);
|
||||
AddParameter(command, "source", statement.Source);
|
||||
AddParameter(command, "source_url", statement.SourceUrl);
|
||||
AddJsonbParameter(command, "evidence", statement.Evidence);
|
||||
AddJsonbParameter(command, "provenance", statement.Provenance);
|
||||
AddJsonbParameter(command, "metadata", statement.Metadata);
|
||||
AddParameter(command, "created_by", statement.CreatedBy);
|
||||
}
|
||||
|
||||
private static VexStatementEntity MapStatement(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
ProjectId = GetNullableGuid(reader, 2),
|
||||
GraphRevisionId = GetNullableGuid(reader, 3),
|
||||
VulnerabilityId = reader.GetString(4),
|
||||
ProductId = GetNullableString(reader, 5),
|
||||
Status = ParseStatus(reader.GetString(6)),
|
||||
Justification = ParseJustification(GetNullableString(reader, 7)),
|
||||
ImpactStatement = GetNullableString(reader, 8),
|
||||
ActionStatement = GetNullableString(reader, 9),
|
||||
ActionStatementTimestamp = GetNullableDateTimeOffset(reader, 10),
|
||||
FirstIssued = reader.GetFieldValue<DateTimeOffset>(11),
|
||||
LastUpdated = reader.GetFieldValue<DateTimeOffset>(12),
|
||||
Source = GetNullableString(reader, 13),
|
||||
SourceUrl = GetNullableString(reader, 14),
|
||||
Evidence = reader.GetString(15),
|
||||
Provenance = reader.GetString(16),
|
||||
Metadata = reader.GetString(17),
|
||||
CreatedBy = GetNullableString(reader, 18)
|
||||
};
|
||||
|
||||
private static string StatusToString(VexStatus status) => status switch
|
||||
{
|
||||
VexStatus.NotAffected => "not_affected",
|
||||
VexStatus.Affected => "affected",
|
||||
VexStatus.Fixed => "fixed",
|
||||
VexStatus.UnderInvestigation => "under_investigation",
|
||||
_ => throw new ArgumentException($"Unknown VEX status: {status}", nameof(status))
|
||||
};
|
||||
|
||||
private static VexStatus ParseStatus(string status) => status switch
|
||||
{
|
||||
"not_affected" => VexStatus.NotAffected,
|
||||
"affected" => VexStatus.Affected,
|
||||
"fixed" => VexStatus.Fixed,
|
||||
"under_investigation" => VexStatus.UnderInvestigation,
|
||||
_ => throw new ArgumentException($"Unknown VEX status: {status}", nameof(status))
|
||||
};
|
||||
|
||||
private static string JustificationToString(VexJustification justification) => justification switch
|
||||
{
|
||||
VexJustification.ComponentNotPresent => "component_not_present",
|
||||
VexJustification.VulnerableCodeNotPresent => "vulnerable_code_not_present",
|
||||
VexJustification.VulnerableCodeNotInExecutePath => "vulnerable_code_not_in_execute_path",
|
||||
VexJustification.VulnerableCodeCannotBeControlledByAdversary => "vulnerable_code_cannot_be_controlled_by_adversary",
|
||||
VexJustification.InlineMitigationsAlreadyExist => "inline_mitigations_already_exist",
|
||||
_ => throw new ArgumentException($"Unknown VEX justification: {justification}", nameof(justification))
|
||||
};
|
||||
|
||||
private static VexJustification? ParseJustification(string? justification) => justification switch
|
||||
{
|
||||
null => null,
|
||||
"component_not_present" => VexJustification.ComponentNotPresent,
|
||||
"vulnerable_code_not_present" => VexJustification.VulnerableCodeNotPresent,
|
||||
"vulnerable_code_not_in_execute_path" => VexJustification.VulnerableCodeNotInExecutePath,
|
||||
"vulnerable_code_cannot_be_controlled_by_adversary" => VexJustification.VulnerableCodeCannotBeControlledByAdversary,
|
||||
"inline_mitigations_already_exist" => VexJustification.InlineMitigationsAlreadyExist,
|
||||
_ => throw new ArgumentException($"Unknown VEX justification: {justification}", nameof(justification))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Excititor.Storage.Postgres.Repositories;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.Excititor.Storage.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring Excititor PostgreSQL storage services.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Excititor PostgreSQL storage services.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configuration">Configuration root.</param>
|
||||
/// <param name="sectionName">Configuration section name for PostgreSQL options.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddExcititorPostgresStorage(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = "Postgres:Excititor")
|
||||
{
|
||||
services.Configure<PostgresOptions>(sectionName, configuration.GetSection(sectionName));
|
||||
services.AddSingleton<ExcititorDataSource>();
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IVexStatementRepository, VexStatementRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Excititor PostgreSQL storage services with explicit options.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configureOptions">Options configuration action.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddExcititorPostgresStorage(
|
||||
this IServiceCollection services,
|
||||
Action<PostgresOptions> configureOptions)
|
||||
{
|
||||
services.Configure(configureOptions);
|
||||
services.AddSingleton<ExcititorDataSource>();
|
||||
|
||||
// Register repositories
|
||||
services.AddScoped<IVexStatementRepository, VexStatementRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Excititor.Storage.Postgres</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Migrations\**\*.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user