Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -8,8 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Signals.Storage.Postgres\StellaOps.Signals.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="..\..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,24 +0,0 @@
|
||||
# Signals Storage Postgres Guild Charter
|
||||
|
||||
## Mission
|
||||
Provide deterministic, offline-first PostgreSQL persistence for Signals, including call graph storage/projection, unknowns registry/scoring, and reachability facts needed by Scanner and Policy.
|
||||
|
||||
## Scope
|
||||
- PostgreSQL schema owned by Signals (default schema: `signals`).
|
||||
- Embedded SQL migrations under `Migrations/*.sql`, executed via `AddStartupMigrations`.
|
||||
- Repository implementations under `Repositories/` (query + ingestion/sync).
|
||||
|
||||
## Required Reading
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/signals/reachability.md`
|
||||
- `docs/signals/callgraph-formats.md`
|
||||
- `docs/signals/runtime-facts.md`
|
||||
- `docs/signals/unknowns-registry.md`
|
||||
- Current sprint file: `docs/implplan/SPRINT_3102_0001_0001_postgres_callgraph_tables.md`
|
||||
|
||||
## Working Agreement
|
||||
1. Update task state to `DOING`/`DONE` in both `/docs/implplan/SPRINT_*.md` and local `TASKS.md`.
|
||||
2. Keep outputs deterministic: stable ordering, canonical JSON where applicable, UTC timestamps only.
|
||||
3. Prefer additive, non-breaking startup migrations; avoid long-running data rewrites at startup.
|
||||
4. Maintain offline posture: no network I/O, no external schema downloads.
|
||||
5. Changes must be covered by tests (integration preferred for migrations + repositories).
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>StellaOps.Signals.Storage.Postgres</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Migrations\V0000_001__extensions.sql" LogicalName="%(Filename)%(Extension)" />
|
||||
<EmbeddedResource Include="Migrations\V3102_001__callgraph_relational_tables.sql" LogicalName="%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,132 +1,351 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals", "StellaOps.Signals\StellaOps.Signals.csproj", "{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration", "..\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj", "{4933EA43-D891-4080-A644-5D14F680F6F1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions", "..\Authority\StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj", "{4B663300-18DB-44DA-95FB-7C2B02D7BF69}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions", "..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj", "{60D01EF6-9E65-447D-86DC-B140731B5513}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration", "..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj", "{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals.Tests", "__Tests\StellaOps.Signals.Tests\StellaOps.Signals.Tests.csproj", "{1AB74DBC-22F8-48B8-B921-2367FFD67866}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DF8EEADB-1C60-46D6-B271-5742FE8F33EC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4933EA43-D891-4080-A644-5D14F680F6F1}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4B663300-18DB-44DA-95FB-7C2B02D7BF69}.Release|x86.Build.0 = Release|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{16E1FBA9-18D0-4912-A36E-691C7BAE0CF7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Release|x64.Build.0 = Release|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{60D01EF6-9E65-447D-86DC-B140731B5513}.Release|x86.Build.0 = Release|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{2283F9AD-83C5-473E-BE71-FAD3A98FB0FE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F7541F2C-CA8E-4D8E-A5DF-06E2E8F87F42}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1AB74DBC-22F8-48B8-B921-2367FFD67866}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signals", "StellaOps.Signals", "{B0689D9E-3BDA-FABC-44A4-6923936DCDCD}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signals.Scheduler", "StellaOps.Signals.Scheduler", "{659FE810-EE9F-BDCF-6E27-ED41900D2EB3}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__External", "__External", "{5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authority", "Authority", "{C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority", "StellaOps.Authority", "{A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.Abstractions", "StellaOps.Auth.Abstractions", "{F2E6CB0E-DF77-1FAA-582B-62B040DF3848}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.ServerIntegration", "StellaOps.Auth.ServerIntegration", "{7E890DF9-B715-B6DF-2498-FD74DDA87D71}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority.Plugins.Abstractions", "StellaOps.Authority.Plugins.Abstractions", "{64689413-46D7-8499-68A6-B6367ACBC597}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Router", "Router", "{FC018E5B-1E2F-DE19-1E97-0C845058C469}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{1BE5B76C-B486-560B-6CB2-44C6537249AA}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Messaging", "StellaOps.Messaging", "{F4F1CBE2-1CDD-CAA4-41F0-266DB4677C05}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scheduler", "Scheduler", "{B24B448A-28D8-778E-DCC1-FCF4A0916DF5}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{BF1AF1AB-97A8-BD70-63F2-E028DE8EE90F}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scheduler.Models", "StellaOps.Scheduler.Models", "{3DB6D7AE-8187-5324-1208-D6090D5324C6}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scheduler.Queue", "StellaOps.Scheduler.Queue", "{9C593CC5-AC88-E301-C4BB-3B1F8853C7AD}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{1345DD29-BB3A-FB5F-4B3D-E29F6045A27A}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Canonical.Json", "StellaOps.Canonical.Json", "{79E122F4-2325-3E92-438E-5825A307B594}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Configuration", "StellaOps.Configuration", "{538E2D98-5325-3F54-BE74-EFE5FC1ECBD8}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography", "StellaOps.Cryptography", "{66557252-B5C4-664B-D807-07018C627474}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.DependencyInjection", "StellaOps.Cryptography.DependencyInjection", "{7203223D-FF02-7BEB-2798-D1639ACC01C4}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.CryptoPro", "StellaOps.Cryptography.Plugin.CryptoPro", "{3C69853C-90E3-D889-1960-3B9229882590}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.OpenSslGost", "StellaOps.Cryptography.Plugin.OpenSslGost", "{643E4D4C-BC96-A37F-E0EC-488127F0B127}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.Pkcs11Gost", "StellaOps.Cryptography.Plugin.Pkcs11Gost", "{6F2CA7F5-3E7C-C61B-94E6-E7DD1227B5B1}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.PqSoft", "StellaOps.Cryptography.Plugin.PqSoft", "{F04B7DBB-77A5-C978-B2DE-8C189A32AA72}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SimRemote", "StellaOps.Cryptography.Plugin.SimRemote", "{7C72F22A-20FF-DF5B-9191-6DFD0D497DB2}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SmRemote", "StellaOps.Cryptography.Plugin.SmRemote", "{C896CC0A-F5E6-9AA4-C582-E691441F8D32}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SmSoft", "StellaOps.Cryptography.Plugin.SmSoft", "{0AA3A418-AB45-CCA4-46D4-EEBFE011FECA}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.WineCsp", "StellaOps.Cryptography.Plugin.WineCsp", "{225D9926-4AE8-E539-70AD-8698E688F271}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.PluginLoader", "StellaOps.Cryptography.PluginLoader", "{D6E8E69C-F721-BBCB-8C39-9716D53D72AD}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.DependencyInjection", "StellaOps.DependencyInjection", "{589A43FD-8213-E9E3-6CFF-9CBA72D53E98}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.EfCore", "StellaOps.Infrastructure.EfCore", "{FCD529E0-DD17-6587-B29C-12D425C0AD0C}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.Postgres", "StellaOps.Infrastructure.Postgres", "{61B23570-4F2D-B060-BE1F-37995682E494}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Plugin", "StellaOps.Plugin", "{772B02B5-6280-E1D4-3E2E-248D0455C2FB}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.TestKit", "StellaOps.TestKit", "{8380A20C-A5B8-EE91-1A58-270323688CB9}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{90659617-4DF7-809A-4E5B-29BB5A98E8E1}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.Postgres.Testing", "StellaOps.Infrastructure.Postgres.Testing", "{CEDC2447-F717-3C95-7E08-F214D575A7B7}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{A5C98087-E847-D2C4-2143-20869479839D}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signals.Persistence", "StellaOps.Signals.Persistence", "{BDB22A3D-689E-6CEA-2118-426BB4184A9C}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{BB76B5A5-14BA-E317-828D-110B711D71F5}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signals.Persistence.Tests", "StellaOps.Signals.Persistence.Tests", "{3CD38B98-43EE-75A6-56B5-523053F0CAB0}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signals.Tests", "StellaOps.Signals.Tests", "{CCF37C5D-2762-B793-EC2B-4B3AF6C6BFB6}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj", "{55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj", "{ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj", "{97F94029-5419-6187-5A63-5C8FD9232FAE}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Canonical.Json", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj", "{AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj", "{92C62F7B-8028-6EE1-B71B-F45F459B8E97}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{F664A948-E352-5808-E780-77A03F19E93E}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.DependencyInjection", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj", "{FA83F778-5252-0B80-5555-E69F790322EA}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.CryptoPro", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.CryptoPro\StellaOps.Cryptography.Plugin.CryptoPro.csproj", "{C53E0895-879A-D9E6-0A43-24AD17A2F270}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.OpenSslGost", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj", "{0AED303F-69E6-238F-EF80-81985080EDB7}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.Pkcs11Gost", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj", "{2904D288-CE64-A565-2C46-C2E85A96A1EE}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.PqSoft", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.PqSoft\StellaOps.Cryptography.Plugin.PqSoft.csproj", "{A6667CC3-B77F-023E-3A67-05F99E9FF46A}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SimRemote", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SimRemote\StellaOps.Cryptography.Plugin.SimRemote.csproj", "{A26E2816-F787-F76B-1D6C-E086DD3E19CE}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SmRemote", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SmRemote\StellaOps.Cryptography.Plugin.SmRemote.csproj", "{B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SmSoft", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj", "{90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.WineCsp", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.WineCsp\StellaOps.Cryptography.Plugin.WineCsp.csproj", "{059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.PluginLoader", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.PluginLoader\StellaOps.Cryptography.PluginLoader.csproj", "{8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{632A1F0D-1BA5-C84B-B716-2BE638A92780}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.EfCore", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj", "{A63897D9-9531-989B-7309-E384BCFC2BB9}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj", "{8C594D82-3463-3367-4F06-900AC707753D}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres.Testing", "E:\dev\git.stella-ops.org\src\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj", "{52F400CD-D473-7A1F-7986-89011CD2A887}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Messaging", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj", "{97998C88-E6E1-D5E2-B632-537B58E00CBF}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{38A9EE9B-6FC8-93BC-0D43-2A906E678D66}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scheduler.Models", "E:\dev\git.stella-ops.org\src\Scheduler\__Libraries\StellaOps.Scheduler.Models\StellaOps.Scheduler.Models.csproj", "{1F372AB9-D8DD-D295-1D5E-CB5D454CBB24}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scheduler.Queue", "E:\dev\git.stella-ops.org\src\Scheduler\__Libraries\StellaOps.Scheduler.Queue\StellaOps.Scheduler.Queue.csproj", "{CB42DA2A-D081-A7B3-DE34-AC200FE30B6E}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals", "StellaOps.Signals\StellaOps.Signals.csproj", "{A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals.Persistence", "__Libraries\StellaOps.Signals.Persistence\StellaOps.Signals.Persistence.csproj", "{EB1A9331-4A47-4C55-8189-C219B35E1B19}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals.Persistence.Tests", "__Tests\StellaOps.Signals.Persistence.Tests\StellaOps.Signals.Persistence.Tests.csproj", "{4D014382-FB30-131A-F8A7-A14DB59403B7}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals.Scheduler", "StellaOps.Signals.Scheduler\StellaOps.Signals.Scheduler.csproj", "{B1872175-6B98-BD4B-7D14-4A5401DA78DD}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals.Tests", "__Tests\StellaOps.Signals.Tests\StellaOps.Signals.Tests.csproj", "{01EE35B6-00AA-EA31-F2BB-D8C68525CB59}"
|
||||
|
||||
EndProject
|
||||
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.TestKit", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj", "{AF043113-CCE3-59C1-DF71-9804155F26A8}"
|
||||
|
||||
EndProject
|
||||
|
||||
Global
|
||||
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
||||
Release|Any CPU = Release|Any CPU
|
||||
|
||||
EndGlobalSection
|
||||
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
|
||||
{55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
||||
{55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
|
||||
{55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
{55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
|
||||
{ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
||||
{ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
|
||||
{ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
{ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
|
||||
{97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
||||
{97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
|
||||
{97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
{97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
|
||||
{AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
||||
{AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
|
||||
{AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
{AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
|
||||
{92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
||||
{92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
|
||||
{92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
{92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
|
||||
{F664A948-E352-5808-E780-77A03F19E93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
||||
{F664A948-E352-5808-E780-77A03F19E93E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
|
||||
{F664A948-E352-5808-E780-77A03F19E93E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
|
||||
@@ -875,6 +875,7 @@ signalsGroup.MapPost("/reachability/recompute", async Task<IResult> (
|
||||
|
||||
app.Run();
|
||||
|
||||
// Make Program class public for WebApplicationFactory<Program> test support
|
||||
public partial class Program
|
||||
{
|
||||
internal static bool TryAuthorize(HttpContext httpContext, string requiredScope, bool fallbackAllowed, out IResult? failure)
|
||||
@@ -935,3 +936,5 @@ public partial class Program
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Primary Program partial declaration merged above
|
||||
|
||||
12
src/Signals/StellaOps.Signals/Properties/launchSettings.json
Normal file
12
src/Signals/StellaOps.Signals/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"StellaOps.Signals": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62543;http://localhost:62544"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,10 +208,10 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
return new CallgraphIngestResponse(
|
||||
document.Id,
|
||||
document.Artifact.Path,
|
||||
document.Artifact.Hash,
|
||||
document.Artifact.CasUri,
|
||||
document.GraphHash,
|
||||
document.Artifact.ManifestCasUri,
|
||||
document.Artifact.Hash ?? string.Empty,
|
||||
document.Artifact.CasUri ?? string.Empty,
|
||||
document.GraphHash ?? string.Empty,
|
||||
document.Artifact.ManifestCasUri ?? string.Empty,
|
||||
schemaVersion,
|
||||
document.Nodes.Count,
|
||||
document.Edges.Count,
|
||||
@@ -288,7 +288,7 @@ internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
||||
.Append(node.Flags).Append('|')
|
||||
.Append(Join(node.Evidence)).Append('|')
|
||||
.Append(JoinDict(node.Analyzer)).Append('|')
|
||||
.Append(JoinDict(node.Attributes))
|
||||
.Append(JoinDict(node.Attributes as IReadOnlyDictionary<string, string?>))
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
|
||||
var map = new Dictionary<(string From, string To), EdgeGateInfo>();
|
||||
foreach (var edge in document.Edges)
|
||||
{
|
||||
if (blocked.Contains((edge.SourceId, edge.TargetId)))
|
||||
if (blocked.Contains((edge.SourceId ?? string.Empty, edge.TargetId ?? string.Empty)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.37" />
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
// <copyright file="AirGapProbeLoader.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Probes;
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled CO-RE probe loader for air-gapped environments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Probes are compiled during CI and embedded in the assembly or loaded
|
||||
/// from a sealed probe bundle. This allows eBPF functionality without
|
||||
/// requiring build tools or internet access at runtime.
|
||||
/// </remarks>
|
||||
public sealed class AirGapProbeLoader : IAirGapProbeLoader
|
||||
{
|
||||
private const string ProbeResourcePrefix = "StellaOps.Signals.Ebpf.Probes.Compiled.";
|
||||
private const string ProbeBundleFileName = "ebpf-probes.bundle";
|
||||
private const string ManifestFileName = "manifest.json";
|
||||
|
||||
private readonly ILogger<AirGapProbeLoader> _logger;
|
||||
private readonly string _bundlePath;
|
||||
private readonly AirGapProbeLoaderOptions _options;
|
||||
private ProbeManifest? _manifest;
|
||||
private bool _initialized;
|
||||
|
||||
public AirGapProbeLoader(
|
||||
ILogger<AirGapProbeLoader> logger,
|
||||
AirGapProbeLoaderOptions? options = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_options = options ?? new AirGapProbeLoaderOptions();
|
||||
_bundlePath = _options.ProbeBundlePath ?? GetDefaultBundlePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the probe loader by loading the manifest.
|
||||
/// </summary>
|
||||
public async Task InitializeAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Initializing air-gap probe loader from {BundlePath}", _bundlePath);
|
||||
|
||||
// Try embedded resources first
|
||||
var embeddedManifest = await TryLoadEmbeddedManifestAsync(ct);
|
||||
if (embeddedManifest is not null)
|
||||
{
|
||||
_manifest = embeddedManifest;
|
||||
_logger.LogInformation(
|
||||
"Loaded {ProbeCount} probes from embedded resources",
|
||||
_manifest.Probes.Count);
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try bundle file
|
||||
if (File.Exists(_bundlePath))
|
||||
{
|
||||
_manifest = await LoadBundleManifestAsync(_bundlePath, ct);
|
||||
_logger.LogInformation(
|
||||
"Loaded {ProbeCount} probes from bundle at {BundlePath}",
|
||||
_manifest.Probes.Count,
|
||||
_bundlePath);
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// No probes available
|
||||
_manifest = new ProbeManifest { Probes = [] };
|
||||
_logger.LogWarning("No pre-compiled probes found. eBPF functionality will be limited.");
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets available probes for the current system.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ProbeInfo> GetAvailableProbes()
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
var kernelVersion = GetKernelVersion();
|
||||
|
||||
return _manifest!.Probes
|
||||
.Where(p => IsCompatible(p, kernelVersion))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a specific probe by name.
|
||||
/// </summary>
|
||||
public async Task<ProbeData> LoadProbeAsync(
|
||||
string probeName,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
var probeInfo = _manifest!.Probes
|
||||
.FirstOrDefault(p => p.Name.Equals(probeName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (probeInfo is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Probe not found: {probeName}");
|
||||
}
|
||||
|
||||
_logger.LogDebug("Loading probe {ProbeName} ({Size} bytes)", probeName, probeInfo.Size);
|
||||
|
||||
byte[] probeBytes;
|
||||
|
||||
// Try embedded resources
|
||||
probeBytes = await TryLoadEmbeddedProbeAsync(probeInfo.ResourcePath, ct);
|
||||
if (probeBytes.Length > 0)
|
||||
{
|
||||
return CreateProbeData(probeInfo, probeBytes);
|
||||
}
|
||||
|
||||
// Try bundle file
|
||||
probeBytes = await LoadFromBundleAsync(probeInfo.BundlePath, ct);
|
||||
if (probeBytes.Length > 0)
|
||||
{
|
||||
return CreateProbeData(probeInfo, probeBytes);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Could not load probe: {probeName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the integrity of all probes.
|
||||
/// </summary>
|
||||
public async Task<ProbeVerificationResult> VerifyIntegrityAsync(CancellationToken ct = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
var results = new List<(string Name, bool Valid, string? Error)>();
|
||||
|
||||
foreach (var probe in _manifest!.Probes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await LoadProbeAsync(probe.Name, ct);
|
||||
var actualHash = ComputeHash(data.Bytes);
|
||||
|
||||
if (!actualHash.Equals(probe.Sha256Hash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add((probe.Name, false, $"Hash mismatch: expected {probe.Sha256Hash}, got {actualHash}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add((probe.Name, true, null));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Add((probe.Name, false, ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return new ProbeVerificationResult
|
||||
{
|
||||
TotalProbes = results.Count,
|
||||
ValidProbes = results.Count(r => r.Valid),
|
||||
InvalidProbes = results.Where(r => !r.Valid).Select(r => (r.Name, r.Error!)).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a probe bundle for offline deployment.
|
||||
/// </summary>
|
||||
public static async Task CreateBundleAsync(
|
||||
string outputPath,
|
||||
IEnumerable<(string Name, byte[] Data, ProbeInfo Info)> probes,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
using var archive = ZipFile.Open(outputPath, ZipArchiveMode.Create);
|
||||
|
||||
var manifest = new ProbeManifest
|
||||
{
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Probes = [],
|
||||
};
|
||||
|
||||
foreach (var (name, data, info) in probes)
|
||||
{
|
||||
var entryName = $"probes/{name}.bpf.o";
|
||||
|
||||
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
|
||||
await using var stream = entry.Open();
|
||||
await stream.WriteAsync(data, ct);
|
||||
|
||||
manifest.Probes.Add(info with
|
||||
{
|
||||
BundlePath = entryName,
|
||||
Sha256Hash = ComputeHash(data),
|
||||
});
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
var manifestEntry = archive.CreateEntry(ManifestFileName, CompressionLevel.Optimal);
|
||||
await using var manifestStream = manifestEntry.Open();
|
||||
await System.Text.Json.JsonSerializer.SerializeAsync(manifestStream, manifest, cancellationToken: ct);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
throw new InvalidOperationException("Probe loader not initialized. Call InitializeAsync first.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ProbeManifest?> TryLoadEmbeddedManifestAsync(CancellationToken ct)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"{ProbeResourcePrefix}manifest.json";
|
||||
|
||||
await using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await System.Text.Json.JsonSerializer.DeserializeAsync<ProbeManifest>(stream, cancellationToken: ct);
|
||||
}
|
||||
|
||||
private async Task<ProbeManifest> LoadBundleManifestAsync(string bundlePath, CancellationToken ct)
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(bundlePath);
|
||||
var manifestEntry = archive.GetEntry(ManifestFileName);
|
||||
|
||||
if (manifestEntry is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Bundle missing manifest: {bundlePath}");
|
||||
}
|
||||
|
||||
await using var stream = manifestEntry.Open();
|
||||
return await System.Text.Json.JsonSerializer.DeserializeAsync<ProbeManifest>(stream, cancellationToken: ct)
|
||||
?? throw new InvalidOperationException("Invalid manifest");
|
||||
}
|
||||
|
||||
private async Task<byte[]> TryLoadEmbeddedProbeAsync(string? resourcePath, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(resourcePath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"{ProbeResourcePrefix}{resourcePath}";
|
||||
|
||||
await using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream, ct);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
private async Task<byte[]> LoadFromBundleAsync(string? bundleEntryPath, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bundleEntryPath) || !File.Exists(_bundlePath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
using var archive = ZipFile.OpenRead(_bundlePath);
|
||||
var entry = archive.GetEntry(bundleEntryPath);
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
await using var stream = entry.Open();
|
||||
using var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream, ct);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
private static ProbeData CreateProbeData(ProbeInfo info, byte[] bytes)
|
||||
{
|
||||
return new ProbeData
|
||||
{
|
||||
Name = info.Name,
|
||||
Bytes = bytes,
|
||||
BtfRequired = info.BtfRequired,
|
||||
MinKernelVersion = info.MinKernelVersion,
|
||||
ProbeTypes = info.ProbeTypes,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetDefaultBundlePath()
|
||||
{
|
||||
var locations = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, ProbeBundleFileName),
|
||||
Path.Combine("/usr/share/stellaops/probes", ProbeBundleFileName),
|
||||
Path.Combine("/opt/stellaops/probes", ProbeBundleFileName),
|
||||
};
|
||||
|
||||
return locations.FirstOrDefault(File.Exists) ?? locations[0];
|
||||
}
|
||||
|
||||
private static bool IsCompatible(ProbeInfo probe, Version? kernelVersion)
|
||||
{
|
||||
if (kernelVersion is null)
|
||||
{
|
||||
return true; // Can't determine, assume compatible
|
||||
}
|
||||
|
||||
if (!Version.TryParse(probe.MinKernelVersion, out var minVersion))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return kernelVersion >= minVersion;
|
||||
}
|
||||
|
||||
private static Version? GetKernelVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists("/proc/version"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var versionString = File.ReadAllText("/proc/version");
|
||||
// Extract version like "5.15.0-generic"
|
||||
var match = System.Text.RegularExpressions.Regex.Match(
|
||||
versionString,
|
||||
@"(\d+\.\d+\.\d+)");
|
||||
|
||||
if (match.Success && Version.TryParse(match.Groups[1].Value, out var version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ComputeHash(byte[] data)
|
||||
{
|
||||
var hashBytes = SHA256.HashData(data);
|
||||
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for air-gap probe loading.
|
||||
/// </summary>
|
||||
public interface IAirGapProbeLoader
|
||||
{
|
||||
Task InitializeAsync(CancellationToken ct = default);
|
||||
IReadOnlyList<ProbeInfo> GetAvailableProbes();
|
||||
Task<ProbeData> LoadProbeAsync(string probeName, CancellationToken ct = default);
|
||||
Task<ProbeVerificationResult> VerifyIntegrityAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for air-gap probe loader.
|
||||
/// </summary>
|
||||
public sealed record AirGapProbeLoaderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to the probe bundle file.
|
||||
/// </summary>
|
||||
public string? ProbeBundlePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use embedded probes.
|
||||
/// </summary>
|
||||
public bool UseEmbeddedProbes { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manifest of available probes.
|
||||
/// </summary>
|
||||
public sealed record ProbeManifest
|
||||
{
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public IList<ProbeInfo> Probes { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a probe.
|
||||
/// </summary>
|
||||
public sealed record ProbeInfo
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string MinKernelVersion { get; init; }
|
||||
public required bool BtfRequired { get; init; }
|
||||
public required IReadOnlyList<string> ProbeTypes { get; init; }
|
||||
public required long Size { get; init; }
|
||||
public required string Sha256Hash { get; init; }
|
||||
public string? ResourcePath { get; init; }
|
||||
public string? BundlePath { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loaded probe data.
|
||||
/// </summary>
|
||||
public sealed record ProbeData
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required byte[] Bytes { get; init; }
|
||||
public required bool BtfRequired { get; init; }
|
||||
public required string MinKernelVersion { get; init; }
|
||||
public required IReadOnlyList<string> ProbeTypes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of probe verification.
|
||||
/// </summary>
|
||||
public sealed record ProbeVerificationResult
|
||||
{
|
||||
public required int TotalProbes { get; init; }
|
||||
public required int ValidProbes { get; init; }
|
||||
public required IReadOnlyList<(string Name, string Error)> InvalidProbes { get; init; }
|
||||
public bool AllValid => ValidProbes == TotalProbes;
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
// <copyright file="CoreProbeLoader.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Probes;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signals.Ebpf.Services;
|
||||
|
||||
/// <summary>
|
||||
/// CO-RE (Compile Once, Run Everywhere) eBPF probe loader.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses pre-compiled BTF-enabled probes that work across kernel versions.
|
||||
/// Probes are embedded as resources and loaded at runtime.
|
||||
/// </remarks>
|
||||
public sealed class CoreProbeLoader : IEbpfProbeLoader
|
||||
{
|
||||
private const string FunctionTracerProbe = "function_tracer.bpf.o";
|
||||
private readonly ILogger<CoreProbeLoader> _logger;
|
||||
private readonly ISymbolResolver _symbolResolver;
|
||||
private readonly string _probeDirectory;
|
||||
private readonly Dictionary<Guid, ProbeSession> _sessions;
|
||||
private readonly object _sessionsLock = new();
|
||||
|
||||
public CoreProbeLoader(
|
||||
ILogger<CoreProbeLoader> logger,
|
||||
ISymbolResolver symbolResolver,
|
||||
string? probeDirectory = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_symbolResolver = symbolResolver;
|
||||
_probeDirectory = probeDirectory ?? GetDefaultProbeDirectory();
|
||||
_sessions = [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<EbpfProbeHandle> LoadAndAttachAsync(
|
||||
string containerId,
|
||||
RuntimeSignalOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
throw new PlatformNotSupportedException("eBPF probes require Linux.");
|
||||
}
|
||||
|
||||
var probeId = Guid.NewGuid();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Loading CO-RE probes for container {ContainerId}, probe session {ProbeId}",
|
||||
containerId,
|
||||
probeId);
|
||||
|
||||
// Get PIDs for the container
|
||||
var pids = await GetContainerPidsAsync(containerId, ct);
|
||||
|
||||
if (pids.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No processes found for container {containerId}");
|
||||
}
|
||||
|
||||
// Load the probe object file
|
||||
var probePath = Path.Combine(_probeDirectory, FunctionTracerProbe);
|
||||
if (!File.Exists(probePath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"eBPF probe object not found: {probePath}",
|
||||
probePath);
|
||||
}
|
||||
|
||||
// In real implementation, this would:
|
||||
// 1. Load BPF object using libbpf
|
||||
// 2. Create ring buffer map
|
||||
// 3. Attach uprobes to target functions
|
||||
// For this implementation, we simulate the loading
|
||||
|
||||
var session = new ProbeSession
|
||||
{
|
||||
ProbeId = probeId,
|
||||
ContainerId = containerId,
|
||||
TracedPids = pids,
|
||||
RingBuffer = new SimulatedRingBuffer(options.RingBufferSize),
|
||||
Options = options,
|
||||
AttachedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
_sessions[probeId] = session;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Attached probes to {PidCount} processes in container {ContainerId}",
|
||||
pids.Count,
|
||||
containerId);
|
||||
|
||||
return new EbpfProbeHandle
|
||||
{
|
||||
ProbeId = probeId,
|
||||
ContainerId = containerId,
|
||||
ProbeFds = [], // Would contain actual FDs in real impl
|
||||
RingBufferFd = 0,
|
||||
MapFds = new Dictionary<string, int>(),
|
||||
TracedPids = pids,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DetachAsync(EbpfProbeHandle handle, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Detaching probes for session {ProbeId}",
|
||||
handle.ProbeId);
|
||||
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (_sessions.Remove(handle.ProbeId, out var session))
|
||||
{
|
||||
session.RingBuffer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<ReadOnlyMemory<byte>> ReadEventsAsync(
|
||||
EbpfProbeHandle handle,
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
ProbeSession? session;
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (!_sessions.TryGetValue(handle.ProbeId, out session))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read events from ring buffer
|
||||
await foreach (var evt in session.RingBuffer.ReadAsync(ct))
|
||||
{
|
||||
yield return evt;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public (string? Symbol, string? Library, string? Purl) ResolveSymbol(int pid, ulong address)
|
||||
{
|
||||
return _symbolResolver.Resolve(pid, address);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetBufferUtilization(EbpfProbeHandle handle)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (_sessions.TryGetValue(handle.ProbeId, out var session))
|
||||
{
|
||||
return session.RingBuffer.Utilization;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetCpuOverhead(EbpfProbeHandle handle)
|
||||
{
|
||||
// In real implementation, would read from BPF stats
|
||||
return 0.1; // Simulated 0.1% overhead
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public long GetMemoryUsage(EbpfProbeHandle handle)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (_sessions.TryGetValue(handle.ProbeId, out var session))
|
||||
{
|
||||
return session.RingBuffer.Size;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var types = new List<ProbeType> { ProbeType.Uprobe, ProbeType.Uretprobe };
|
||||
|
||||
// Check for USDT support
|
||||
if (File.Exists("/sys/kernel/debug/tracing/uprobe_events"))
|
||||
{
|
||||
types.Add(ProbeType.Usdt);
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
private static string GetDefaultProbeDirectory()
|
||||
{
|
||||
// Look for probes in standard locations
|
||||
var locations = new[]
|
||||
{
|
||||
"/usr/share/stellaops/probes",
|
||||
"/opt/stellaops/probes",
|
||||
Path.Combine(AppContext.BaseDirectory, "probes"),
|
||||
};
|
||||
|
||||
foreach (var loc in locations)
|
||||
{
|
||||
if (Directory.Exists(loc))
|
||||
{
|
||||
return loc;
|
||||
}
|
||||
}
|
||||
|
||||
return locations[^1];
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<int>> GetContainerPidsAsync(
|
||||
string containerId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Try to find PIDs from cgroup
|
||||
var cgroupPath = $"/sys/fs/cgroup/system.slice/docker-{containerId}.scope/cgroup.procs";
|
||||
|
||||
if (!File.Exists(cgroupPath))
|
||||
{
|
||||
// Try containerd path
|
||||
cgroupPath = $"/sys/fs/cgroup/system.slice/containerd-{containerId}.scope/cgroup.procs";
|
||||
}
|
||||
|
||||
if (!File.Exists(cgroupPath))
|
||||
{
|
||||
// Fall back to /proc scanning
|
||||
return await ScanProcForContainerAsync(containerId, ct);
|
||||
}
|
||||
|
||||
var pids = new List<int>();
|
||||
await foreach (var line in File.ReadLinesAsync(cgroupPath, ct))
|
||||
{
|
||||
if (int.TryParse(line, out var pid))
|
||||
{
|
||||
pids.Add(pid);
|
||||
}
|
||||
}
|
||||
|
||||
return pids;
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<int>> ScanProcForContainerAsync(
|
||||
string containerId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var pids = new List<int>();
|
||||
|
||||
if (!Directory.Exists("/proc"))
|
||||
{
|
||||
return pids;
|
||||
}
|
||||
|
||||
foreach (var dir in Directory.GetDirectories("/proc"))
|
||||
{
|
||||
var name = Path.GetFileName(dir);
|
||||
if (!int.TryParse(name, out var pid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cgroupFile = Path.Combine(dir, "cgroup");
|
||||
if (!File.Exists(cgroupFile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(cgroupFile, ct);
|
||||
if (content.Contains(containerId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
pids.Add(pid);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Process may have exited
|
||||
}
|
||||
}
|
||||
|
||||
return pids;
|
||||
}
|
||||
|
||||
private sealed class ProbeSession
|
||||
{
|
||||
public required Guid ProbeId { get; init; }
|
||||
public required string ContainerId { get; init; }
|
||||
public required IReadOnlyList<int> TracedPids { get; init; }
|
||||
public required SimulatedRingBuffer RingBuffer { get; init; }
|
||||
public required RuntimeSignalOptions Options { get; init; }
|
||||
public required DateTimeOffset AttachedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulated ring buffer for development/testing.
|
||||
/// In production, this would be backed by actual BPF ring buffer.
|
||||
/// </summary>
|
||||
private sealed class SimulatedRingBuffer : IDisposable
|
||||
{
|
||||
private readonly int _size;
|
||||
private readonly System.Threading.Channels.Channel<ReadOnlyMemory<byte>> _channel;
|
||||
private bool _disposed;
|
||||
|
||||
public SimulatedRingBuffer(int size)
|
||||
{
|
||||
_size = size;
|
||||
_channel = System.Threading.Channels.Channel.CreateBounded<ReadOnlyMemory<byte>>(
|
||||
new System.Threading.Channels.BoundedChannelOptions(1000)
|
||||
{
|
||||
FullMode = System.Threading.Channels.BoundedChannelFullMode.DropOldest,
|
||||
});
|
||||
}
|
||||
|
||||
public int Size => _size;
|
||||
|
||||
public double Utilization => 0.1; // Simulated
|
||||
|
||||
public async IAsyncEnumerable<ReadOnlyMemory<byte>> ReadAsync(
|
||||
[EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
await foreach (var item in _channel.Reader.ReadAllAsync(ct))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_channel.Writer.TryComplete();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbol resolver interface.
|
||||
/// </summary>
|
||||
public interface ISymbolResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves an address to symbol information.
|
||||
/// </summary>
|
||||
(string? Symbol, string? Library, string? Purl) Resolve(int pid, ulong address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF symbol resolver using /proc/pid/maps and symbol tables.
|
||||
/// </summary>
|
||||
public sealed class ElfSymbolResolver : ISymbolResolver
|
||||
{
|
||||
private readonly ILogger<ElfSymbolResolver> _logger;
|
||||
private readonly Dictionary<int, ProcessMaps> _processCache;
|
||||
private readonly object _cacheLock = new();
|
||||
|
||||
public ElfSymbolResolver(ILogger<ElfSymbolResolver> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_processCache = [];
|
||||
}
|
||||
|
||||
public (string? Symbol, string? Library, string? Purl) Resolve(int pid, ulong address)
|
||||
{
|
||||
try
|
||||
{
|
||||
var maps = GetProcessMaps(pid);
|
||||
|
||||
// Find the mapping containing this address
|
||||
foreach (var mapping in maps.Mappings)
|
||||
{
|
||||
if (address >= mapping.StartAddress && address < mapping.EndAddress)
|
||||
{
|
||||
var offset = address - mapping.StartAddress + mapping.FileOffset;
|
||||
|
||||
// In real impl, would read ELF symbol table
|
||||
return (null, mapping.Pathname, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to resolve symbol for PID {Pid} address {Address:X16}", pid, address);
|
||||
}
|
||||
|
||||
return (null, null, null);
|
||||
}
|
||||
|
||||
private ProcessMaps GetProcessMaps(int pid)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (_processCache.TryGetValue(pid, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
var maps = ParseMaps(pid);
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_processCache[pid] = maps;
|
||||
}
|
||||
|
||||
return maps;
|
||||
}
|
||||
|
||||
private static ProcessMaps ParseMaps(int pid)
|
||||
{
|
||||
var mapsPath = $"/proc/{pid}/maps";
|
||||
var mappings = new List<MemoryMapping>();
|
||||
|
||||
if (!File.Exists(mapsPath))
|
||||
{
|
||||
return new ProcessMaps { Mappings = mappings };
|
||||
}
|
||||
|
||||
foreach (var line in File.ReadLines(mapsPath))
|
||||
{
|
||||
// Parse: address perms offset dev inode pathname
|
||||
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var addrParts = parts[0].Split('-');
|
||||
if (addrParts.Length != 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(addrParts[0], System.Globalization.NumberStyles.HexNumber, null, out var start))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(addrParts[1], System.Globalization.NumberStyles.HexNumber, null, out var end))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = ulong.TryParse(parts[2], System.Globalization.NumberStyles.HexNumber, null, out var offset);
|
||||
|
||||
var pathname = parts.Length > 5 ? parts[5] : null;
|
||||
|
||||
mappings.Add(new MemoryMapping
|
||||
{
|
||||
StartAddress = start,
|
||||
EndAddress = end,
|
||||
FileOffset = offset,
|
||||
Pathname = pathname,
|
||||
});
|
||||
}
|
||||
|
||||
return new ProcessMaps { Mappings = mappings };
|
||||
}
|
||||
|
||||
private sealed record ProcessMaps
|
||||
{
|
||||
public required IReadOnlyList<MemoryMapping> Mappings { get; init; }
|
||||
}
|
||||
|
||||
private sealed record MemoryMapping
|
||||
{
|
||||
public required ulong StartAddress { get; init; }
|
||||
public required ulong EndAddress { get; init; }
|
||||
public required ulong FileOffset { get; init; }
|
||||
public string? Pathname { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// <copyright file="IEbpfProbeLoader.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Probes;
|
||||
|
||||
using StellaOps.Signals.Ebpf.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and manages eBPF probes for runtime signal collection.
|
||||
/// </summary>
|
||||
public interface IEbpfProbeLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads and attaches eBPF probes to a container's processes.
|
||||
/// </summary>
|
||||
/// <param name="containerId">Container ID to attach to.</param>
|
||||
/// <param name="options">Collection options.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Handle to the attached probes.</returns>
|
||||
Task<EbpfProbeHandle> LoadAndAttachAsync(
|
||||
string containerId,
|
||||
RuntimeSignalOptions options,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Detaches and unloads probes.
|
||||
/// </summary>
|
||||
/// <param name="handle">Probe handle.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task DetachAsync(EbpfProbeHandle handle, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads events from the probe ring buffer.
|
||||
/// </summary>
|
||||
/// <param name="handle">Probe handle.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Async enumerable of raw event data.</returns>
|
||||
IAsyncEnumerable<ReadOnlyMemory<byte>> ReadEventsAsync(
|
||||
EbpfProbeHandle handle,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a function address to symbol, library, and PURL.
|
||||
/// </summary>
|
||||
/// <param name="pid">Process ID.</param>
|
||||
/// <param name="address">Function address.</param>
|
||||
/// <returns>Tuple of (symbol, library, purl) or nulls if unresolved.</returns>
|
||||
(string? Symbol, string? Library, string? Purl) ResolveSymbol(int pid, ulong address);
|
||||
|
||||
/// <summary>
|
||||
/// Gets ring buffer utilization as a percentage (0.0-1.0).
|
||||
/// </summary>
|
||||
double GetBufferUtilization(EbpfProbeHandle handle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets CPU overhead percentage from probes.
|
||||
/// </summary>
|
||||
double GetCpuOverhead(EbpfProbeHandle handle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets memory usage in bytes.
|
||||
/// </summary>
|
||||
long GetMemoryUsage(EbpfProbeHandle handle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets supported probe types on this system.
|
||||
/// </summary>
|
||||
IReadOnlyList<ProbeType> GetSupportedProbeTypes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle to loaded eBPF probes.
|
||||
/// </summary>
|
||||
public sealed record EbpfProbeHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique probe session ID.
|
||||
/// </summary>
|
||||
public required Guid ProbeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Container ID probes are attached to.
|
||||
/// </summary>
|
||||
public required string ContainerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File descriptors for attached probes.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<int> ProbeFds { get; init; } = Array.Empty<int>();
|
||||
|
||||
/// <summary>
|
||||
/// Ring buffer file descriptor.
|
||||
/// </summary>
|
||||
internal int RingBufferFd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Map file descriptors.
|
||||
/// </summary>
|
||||
internal IReadOnlyDictionary<string, int> MapFds { get; init; } = new Dictionary<string, int>();
|
||||
|
||||
/// <summary>
|
||||
/// Process IDs being traced.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<int> TracedPids { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// <copyright file="RuntimeCallEvent.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Schema;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when a function call is observed via eBPF.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This record is the deserialized form of events from the eBPF ring buffer.
|
||||
/// The schema is designed for efficient serialization and deterministic ordering.
|
||||
/// </remarks>
|
||||
public sealed record RuntimeCallEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique event identifier.
|
||||
/// </summary>
|
||||
public required Guid EventId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Container ID where the call was observed.
|
||||
/// </summary>
|
||||
public required string ContainerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Process ID within the container.
|
||||
/// </summary>
|
||||
public required int Pid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Thread ID.
|
||||
/// </summary>
|
||||
public required int Tid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp in nanoseconds since boot.
|
||||
/// </summary>
|
||||
public required ulong TimestampNs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Called function symbol name (if resolved).
|
||||
/// </summary>
|
||||
public string? Symbol { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Called function address.
|
||||
/// </summary>
|
||||
public required ulong FunctionAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Call stack (addresses from bottom to top).
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ulong> StackTrace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime type (native, jvm, node, python, dotnet, go).
|
||||
/// </summary>
|
||||
public required RuntimeType RuntimeType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Library/module containing the function.
|
||||
/// </summary>
|
||||
public string? Library { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL if resolvable.
|
||||
/// </summary>
|
||||
public string? Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when this event was received by the collector.
|
||||
/// </summary>
|
||||
public DateTimeOffset ReceivedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime type detected from process characteristics.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum RuntimeType
|
||||
{
|
||||
/// <summary>Native binary (ELF/PE/Mach-O).</summary>
|
||||
Native = 0,
|
||||
|
||||
/// <summary>Java Virtual Machine.</summary>
|
||||
Jvm = 1,
|
||||
|
||||
/// <summary>Node.js / V8.</summary>
|
||||
Node = 2,
|
||||
|
||||
/// <summary>Python interpreter.</summary>
|
||||
Python = 3,
|
||||
|
||||
/// <summary>.NET runtime (CoreCLR).</summary>
|
||||
DotNet = 4,
|
||||
|
||||
/// <summary>Go runtime.</summary>
|
||||
Go = 5,
|
||||
|
||||
/// <summary>Ruby interpreter.</summary>
|
||||
Ruby = 6,
|
||||
|
||||
/// <summary>Unknown or unidentified runtime.</summary>
|
||||
Unknown = 255,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Observed call path from runtime signals.
|
||||
/// </summary>
|
||||
public sealed record ObservedCallPath
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbols in the call path (entry point to vulnerable function).
|
||||
/// </summary>
|
||||
public required IReadOnlyList<string> Symbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of times this path was observed.
|
||||
/// </summary>
|
||||
public required int ObservationCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package URL if resolvable.
|
||||
/// </summary>
|
||||
public string? Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime type where this path was observed.
|
||||
/// </summary>
|
||||
public RuntimeType RuntimeType { get; init; } = RuntimeType.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// First observation timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset FirstObservedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Last observation timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset LastObservedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of runtime signals collected for a container.
|
||||
/// </summary>
|
||||
public sealed record RuntimeSignalSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Container ID.
|
||||
/// </summary>
|
||||
public required string ContainerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When signal collection started.
|
||||
/// </summary>
|
||||
public required DateTimeOffset StartedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When signal collection stopped.
|
||||
/// </summary>
|
||||
public required DateTimeOffset StoppedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total events captured.
|
||||
/// </summary>
|
||||
public required long TotalEvents { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated call paths.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ObservedCallPath> CallPaths { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique symbols observed.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<string> ObservedSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Events that were dropped due to rate limiting.
|
||||
/// </summary>
|
||||
public long DroppedEvents { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime types detected in this container.
|
||||
/// </summary>
|
||||
public IReadOnlyList<RuntimeType> DetectedRuntimes { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about signal collection.
|
||||
/// </summary>
|
||||
public sealed record SignalStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Total events received.
|
||||
/// </summary>
|
||||
public required long TotalEvents { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Events per second (current rate).
|
||||
/// </summary>
|
||||
public required double EventsPerSecond { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique call paths observed.
|
||||
/// </summary>
|
||||
public required int UniqueCallPaths { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ring buffer utilization percentage.
|
||||
/// </summary>
|
||||
public required double BufferUtilization { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Events dropped due to rate limiting.
|
||||
/// </summary>
|
||||
public required long DroppedEvents { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CPU overhead percentage from eBPF probes.
|
||||
/// </summary>
|
||||
public double CpuOverheadPercent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Memory usage in bytes for signal collection.
|
||||
/// </summary>
|
||||
public long MemoryUsageBytes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// <copyright file="IRuntimeSignalCollector.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Services;
|
||||
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Collects runtime function call signals via eBPF probes.
|
||||
/// </summary>
|
||||
public interface IRuntimeSignalCollector
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts collecting runtime signals for a container.
|
||||
/// </summary>
|
||||
/// <param name="containerId">Container ID to attach probes to.</param>
|
||||
/// <param name="options">Collection options.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Handle to the collection session.</returns>
|
||||
Task<SignalCollectionHandle> StartCollectionAsync(
|
||||
string containerId,
|
||||
RuntimeSignalOptions options,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Stops collection and returns aggregated signals.
|
||||
/// </summary>
|
||||
/// <param name="handle">Collection handle from <see cref="StartCollectionAsync"/>.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Summary of collected signals.</returns>
|
||||
Task<RuntimeSignalSummary> StopCollectionAsync(
|
||||
SignalCollectionHandle handle,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets current signal statistics without stopping collection.
|
||||
/// </summary>
|
||||
/// <param name="handle">Collection handle.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Current statistics.</returns>
|
||||
Task<SignalStatistics> GetStatisticsAsync(
|
||||
SignalCollectionHandle handle,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if eBPF is supported on the current system.
|
||||
/// </summary>
|
||||
/// <returns>True if eBPF probes can be loaded.</returns>
|
||||
bool IsSupported();
|
||||
|
||||
/// <summary>
|
||||
/// Gets available probe types on this system.
|
||||
/// </summary>
|
||||
/// <returns>List of supported probe types.</returns>
|
||||
IReadOnlyList<ProbeType> GetSupportedProbeTypes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle to an active signal collection session.
|
||||
/// </summary>
|
||||
public sealed record SignalCollectionHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique session identifier.
|
||||
/// </summary>
|
||||
public required Guid SessionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Container ID being monitored.
|
||||
/// </summary>
|
||||
public required string ContainerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When collection started.
|
||||
/// </summary>
|
||||
public required DateTimeOffset StartedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Options used for this session.
|
||||
/// </summary>
|
||||
public required RuntimeSignalOptions Options { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for runtime signal collection.
|
||||
/// </summary>
|
||||
public sealed record RuntimeSignalOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Target functions to trace (by symbol pattern).
|
||||
/// Empty list means trace all functions (not recommended in production).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> TargetSymbols { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Maximum events per second (rate limiting).
|
||||
/// </summary>
|
||||
public int MaxEventsPerSecond { get; init; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Collection duration limit. Null means unlimited.
|
||||
/// </summary>
|
||||
public TimeSpan? MaxDuration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime types to instrument.
|
||||
/// </summary>
|
||||
public IReadOnlyList<RuntimeType> RuntimeTypes { get; init; } =
|
||||
[RuntimeType.Native, RuntimeType.Node, RuntimeType.Python];
|
||||
|
||||
/// <summary>
|
||||
/// Whether to resolve symbols from addresses.
|
||||
/// </summary>
|
||||
public bool ResolveSymbols { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum stack depth to capture.
|
||||
/// </summary>
|
||||
public int MaxStackDepth { get; init; } = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Ring buffer size in bytes.
|
||||
/// </summary>
|
||||
public int RingBufferSize { get; init; } = 256 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Sample rate (1 = every call, 10 = every 10th call).
|
||||
/// </summary>
|
||||
public int SampleRate { get; init; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of eBPF probes available.
|
||||
/// </summary>
|
||||
public enum ProbeType
|
||||
{
|
||||
/// <summary>User-space function entry probe.</summary>
|
||||
Uprobe,
|
||||
|
||||
/// <summary>User-space function return probe.</summary>
|
||||
Uretprobe,
|
||||
|
||||
/// <summary>User-space static tracepoint.</summary>
|
||||
Usdt,
|
||||
|
||||
/// <summary>Kernel tracepoint.</summary>
|
||||
Tracepoint,
|
||||
|
||||
/// <summary>Kernel kprobe.</summary>
|
||||
Kprobe,
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
// <copyright file="RuntimeSignalCollector.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Services;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signals.Ebpf.Probes;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Collects runtime function call signals via eBPF probes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation uses the CO-RE (Compile Once, Run Everywhere) model
|
||||
/// to load pre-compiled eBPF probes that work across different kernel versions.
|
||||
/// </remarks>
|
||||
public sealed class RuntimeSignalCollector : IRuntimeSignalCollector, IDisposable
|
||||
{
|
||||
private readonly ILogger<RuntimeSignalCollector> _logger;
|
||||
private readonly IEbpfProbeLoader _probeLoader;
|
||||
private readonly ConcurrentDictionary<Guid, CollectionSession> _activeSessions;
|
||||
private readonly bool _isSupported;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeSignalCollector(
|
||||
ILogger<RuntimeSignalCollector> logger,
|
||||
IEbpfProbeLoader probeLoader)
|
||||
{
|
||||
_logger = logger;
|
||||
_probeLoader = probeLoader;
|
||||
_activeSessions = new ConcurrentDictionary<Guid, CollectionSession>();
|
||||
_isSupported = CheckEbpfSupport();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SignalCollectionHandle> StartCollectionAsync(
|
||||
string containerId,
|
||||
RuntimeSignalOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(containerId);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
if (!_isSupported)
|
||||
{
|
||||
throw new PlatformNotSupportedException(
|
||||
"eBPF is not supported on this platform. Linux 5.8+ with BTF enabled is required.");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting signal collection for container {ContainerId} with {SymbolCount} target symbols",
|
||||
containerId,
|
||||
options.TargetSymbols.Count);
|
||||
|
||||
var sessionId = Guid.NewGuid();
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
// Load and attach eBPF probes
|
||||
var probeHandle = await _probeLoader.LoadAndAttachAsync(
|
||||
containerId,
|
||||
options,
|
||||
ct);
|
||||
|
||||
var session = new CollectionSession
|
||||
{
|
||||
SessionId = sessionId,
|
||||
ContainerId = containerId,
|
||||
StartedAt = startedAt,
|
||||
Options = options,
|
||||
ProbeHandle = probeHandle,
|
||||
Events = new ConcurrentQueue<RuntimeCallEvent>(),
|
||||
TotalEvents = 0,
|
||||
DroppedEvents = 0,
|
||||
};
|
||||
|
||||
// Start event processing
|
||||
session.ProcessingCts = new CancellationTokenSource();
|
||||
session.ProcessingTask = ProcessEventsAsync(session, session.ProcessingCts.Token);
|
||||
|
||||
if (!_activeSessions.TryAdd(sessionId, session))
|
||||
{
|
||||
await _probeLoader.DetachAsync(probeHandle, ct);
|
||||
throw new InvalidOperationException("Failed to register collection session.");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Signal collection started for container {ContainerId}, session {SessionId}",
|
||||
containerId,
|
||||
sessionId);
|
||||
|
||||
return new SignalCollectionHandle
|
||||
{
|
||||
SessionId = sessionId,
|
||||
ContainerId = containerId,
|
||||
StartedAt = startedAt,
|
||||
Options = options,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RuntimeSignalSummary> StopCollectionAsync(
|
||||
SignalCollectionHandle handle,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handle);
|
||||
|
||||
if (!_activeSessions.TryRemove(handle.SessionId, out var session))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No active session found for {handle.SessionId}");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Stopping signal collection for session {SessionId}",
|
||||
handle.SessionId);
|
||||
|
||||
// Stop event processing
|
||||
await session.ProcessingCts.CancelAsync();
|
||||
try
|
||||
{
|
||||
await session.ProcessingTask.WaitAsync(TimeSpan.FromSeconds(5), ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.LogWarning("Event processing did not stop gracefully");
|
||||
}
|
||||
|
||||
// Detach probes
|
||||
await _probeLoader.DetachAsync(session.ProbeHandle, ct);
|
||||
|
||||
var stoppedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
// Aggregate call paths
|
||||
var callPaths = AggregateCallPaths(session.Events);
|
||||
var observedSymbols = ExtractUniqueSymbols(session.Events);
|
||||
var detectedRuntimes = DetectRuntimes(session.Events);
|
||||
|
||||
session.ProcessingCts.Dispose();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Signal collection stopped for session {SessionId}: {EventCount} events, {CallPathCount} call paths",
|
||||
handle.SessionId,
|
||||
session.TotalEvents,
|
||||
callPaths.Count);
|
||||
|
||||
return new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = handle.ContainerId,
|
||||
StartedAt = session.StartedAt,
|
||||
StoppedAt = stoppedAt,
|
||||
TotalEvents = session.TotalEvents,
|
||||
CallPaths = callPaths,
|
||||
ObservedSymbols = observedSymbols,
|
||||
DroppedEvents = session.DroppedEvents,
|
||||
DetectedRuntimes = detectedRuntimes,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SignalStatistics> GetStatisticsAsync(
|
||||
SignalCollectionHandle handle,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handle);
|
||||
|
||||
if (!_activeSessions.TryGetValue(handle.SessionId, out var session))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No active session found for {handle.SessionId}");
|
||||
}
|
||||
|
||||
var elapsed = DateTimeOffset.UtcNow - session.StartedAt;
|
||||
var eventsPerSecond = elapsed.TotalSeconds > 0
|
||||
? session.TotalEvents / elapsed.TotalSeconds
|
||||
: 0;
|
||||
|
||||
var uniqueCallPaths = CountUniqueCallPaths(session.Events);
|
||||
var bufferUtilization = _probeLoader.GetBufferUtilization(session.ProbeHandle);
|
||||
|
||||
return Task.FromResult(new SignalStatistics
|
||||
{
|
||||
TotalEvents = session.TotalEvents,
|
||||
EventsPerSecond = eventsPerSecond,
|
||||
UniqueCallPaths = uniqueCallPaths,
|
||||
BufferUtilization = bufferUtilization,
|
||||
DroppedEvents = session.DroppedEvents,
|
||||
CpuOverheadPercent = _probeLoader.GetCpuOverhead(session.ProbeHandle),
|
||||
MemoryUsageBytes = _probeLoader.GetMemoryUsage(session.ProbeHandle),
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSupported() => _isSupported;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes()
|
||||
{
|
||||
if (!_isSupported)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return _probeLoader.GetSupportedProbeTypes();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (sessionId, session) in _activeSessions)
|
||||
{
|
||||
try
|
||||
{
|
||||
session.ProcessingCts.Cancel();
|
||||
session.ProcessingCts.Dispose();
|
||||
_probeLoader.DetachAsync(session.ProbeHandle, CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error disposing session {SessionId}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
_activeSessions.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private static bool CheckEbpfSupport()
|
||||
{
|
||||
// eBPF is only supported on Linux 5.8+ with BTF
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for BTF support by looking for /sys/kernel/btf/vmlinux
|
||||
return File.Exists("/sys/kernel/btf/vmlinux");
|
||||
}
|
||||
|
||||
private async Task ProcessEventsAsync(CollectionSession session, CancellationToken ct)
|
||||
{
|
||||
var rateLimiter = new RateLimiter(session.Options.MaxEventsPerSecond);
|
||||
|
||||
await foreach (var rawEvent in _probeLoader.ReadEventsAsync(session.ProbeHandle, ct))
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!rateLimiter.TryAcquire())
|
||||
{
|
||||
Interlocked.Increment(ref session.DroppedEvents);
|
||||
continue;
|
||||
}
|
||||
|
||||
var callEvent = ParseEvent(rawEvent, session.ContainerId, session.Options);
|
||||
if (callEvent is not null)
|
||||
{
|
||||
session.Events.Enqueue(callEvent);
|
||||
Interlocked.Increment(ref session.TotalEvents);
|
||||
|
||||
// Limit memory usage by trimming old events
|
||||
while (session.Events.Count > 100_000 && session.Events.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeCallEvent? ParseEvent(
|
||||
ReadOnlyMemory<byte> rawEvent,
|
||||
string containerId,
|
||||
RuntimeSignalOptions options)
|
||||
{
|
||||
// Parse the binary event from eBPF ring buffer
|
||||
// This is a simplified implementation - real impl would use MemoryMarshal
|
||||
if (rawEvent.Length < 32)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var span = rawEvent.Span;
|
||||
|
||||
var timestampNs = BitConverter.ToUInt64(span[..8]);
|
||||
var pid = BitConverter.ToInt32(span[8..12]);
|
||||
var tid = BitConverter.ToInt32(span[12..16]);
|
||||
var functionAddress = BitConverter.ToUInt64(span[16..24]);
|
||||
var stackDepth = BitConverter.ToInt32(span[24..28]);
|
||||
var runtimeType = (RuntimeType)span[28];
|
||||
|
||||
// Parse stack trace
|
||||
var stackTrace = new List<ulong>(Math.Min(stackDepth, options.MaxStackDepth));
|
||||
var stackOffset = 32;
|
||||
for (var i = 0; i < stackDepth && i < options.MaxStackDepth; i++)
|
||||
{
|
||||
if (stackOffset + 8 > rawEvent.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
stackTrace.Add(BitConverter.ToUInt64(span[stackOffset..(stackOffset + 8)]));
|
||||
stackOffset += 8;
|
||||
}
|
||||
|
||||
// Resolve symbol if enabled
|
||||
string? symbol = null;
|
||||
string? library = null;
|
||||
string? purl = null;
|
||||
|
||||
if (options.ResolveSymbols)
|
||||
{
|
||||
(symbol, library, purl) = _probeLoader.ResolveSymbol(
|
||||
pid, functionAddress);
|
||||
}
|
||||
|
||||
return new RuntimeCallEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
ContainerId = containerId,
|
||||
Pid = pid,
|
||||
Tid = tid,
|
||||
TimestampNs = timestampNs,
|
||||
Symbol = symbol,
|
||||
FunctionAddress = functionAddress,
|
||||
StackTrace = stackTrace,
|
||||
RuntimeType = runtimeType,
|
||||
Library = library,
|
||||
Purl = purl,
|
||||
ReceivedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ObservedCallPath> AggregateCallPaths(
|
||||
ConcurrentQueue<RuntimeCallEvent> events)
|
||||
{
|
||||
var pathCounts = new Dictionary<string, (List<string> Symbols, int Count, string? Purl, RuntimeType Runtime, DateTimeOffset First, DateTimeOffset Last)>();
|
||||
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (evt.Symbol is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create path key from stack symbols
|
||||
var symbols = evt.StackTrace
|
||||
.Select(addr => addr.ToString("X16"))
|
||||
.Prepend(evt.Symbol)
|
||||
.ToList();
|
||||
|
||||
var pathKey = string.Join("->", symbols);
|
||||
|
||||
if (pathCounts.TryGetValue(pathKey, out var existing))
|
||||
{
|
||||
pathCounts[pathKey] = (
|
||||
existing.Symbols,
|
||||
existing.Count + 1,
|
||||
existing.Purl ?? evt.Purl,
|
||||
existing.Runtime,
|
||||
existing.First,
|
||||
evt.ReceivedAt);
|
||||
}
|
||||
else
|
||||
{
|
||||
pathCounts[pathKey] = (symbols, 1, evt.Purl, evt.RuntimeType, evt.ReceivedAt, evt.ReceivedAt);
|
||||
}
|
||||
}
|
||||
|
||||
return pathCounts.Values
|
||||
.OrderByDescending(p => p.Count)
|
||||
.Take(1000) // Limit to top 1000 paths
|
||||
.Select(p => new ObservedCallPath
|
||||
{
|
||||
Symbols = p.Symbols,
|
||||
ObservationCount = p.Count,
|
||||
Purl = p.Purl,
|
||||
RuntimeType = p.Runtime,
|
||||
FirstObservedAt = p.First,
|
||||
LastObservedAt = p.Last,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ExtractUniqueSymbols(
|
||||
ConcurrentQueue<RuntimeCallEvent> events)
|
||||
{
|
||||
return events
|
||||
.Where(e => e.Symbol is not null)
|
||||
.Select(e => e.Symbol!)
|
||||
.Distinct()
|
||||
.OrderBy(s => s, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<RuntimeType> DetectRuntimes(
|
||||
ConcurrentQueue<RuntimeCallEvent> events)
|
||||
{
|
||||
return events
|
||||
.Select(e => e.RuntimeType)
|
||||
.Where(r => r != RuntimeType.Unknown)
|
||||
.Distinct()
|
||||
.OrderBy(r => r)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static int CountUniqueCallPaths(ConcurrentQueue<RuntimeCallEvent> events)
|
||||
{
|
||||
var pathKeys = new HashSet<string>();
|
||||
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (evt.Symbol is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pathKey = $"{evt.Symbol}->{string.Join("->", evt.StackTrace.Take(3))}";
|
||||
pathKeys.Add(pathKey);
|
||||
}
|
||||
|
||||
return pathKeys.Count;
|
||||
}
|
||||
|
||||
private sealed class CollectionSession
|
||||
{
|
||||
public required Guid SessionId { get; init; }
|
||||
public required string ContainerId { get; init; }
|
||||
public required DateTimeOffset StartedAt { get; init; }
|
||||
public required RuntimeSignalOptions Options { get; init; }
|
||||
public required EbpfProbeHandle ProbeHandle { get; init; }
|
||||
public required ConcurrentQueue<RuntimeCallEvent> Events { get; init; }
|
||||
public long TotalEvents;
|
||||
public long DroppedEvents;
|
||||
public CancellationTokenSource ProcessingCts { get; set; } = null!;
|
||||
public Task ProcessingTask { get; set; } = null!;
|
||||
}
|
||||
|
||||
private sealed class RateLimiter
|
||||
{
|
||||
private readonly int _maxPerSecond;
|
||||
private long _count;
|
||||
private long _windowStart;
|
||||
|
||||
public RateLimiter(int maxPerSecond)
|
||||
{
|
||||
_maxPerSecond = maxPerSecond;
|
||||
_windowStart = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
public bool TryAcquire()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
if (now > Interlocked.Read(ref _windowStart))
|
||||
{
|
||||
Interlocked.Exchange(ref _windowStart, now);
|
||||
Interlocked.Exchange(ref _count, 0);
|
||||
}
|
||||
|
||||
return Interlocked.Increment(ref _count) <= _maxPerSecond;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Signals.Ebpf</RootNamespace>
|
||||
<AssemblyName>StellaOps.Signals.Ebpf</AssemblyName>
|
||||
<Description>eBPF runtime signal collection for StellaOps</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace StellaOps.Signals.Persistence.EfCore.Context;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core DbContext for Signals module.
|
||||
/// This is a stub that will be scaffolded from the PostgreSQL database.
|
||||
/// </summary>
|
||||
public class SignalsDbContext : DbContext
|
||||
{
|
||||
public SignalsDbContext(DbContextOptions<SignalsDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema("signals");
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Infrastructure.Postgres.Migrations;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Signals.Services;
|
||||
|
||||
namespace StellaOps.Signals.Persistence.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring Signals persistence services.
|
||||
/// </summary>
|
||||
public static class SignalsPersistenceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Signals PostgreSQL persistence 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 AddSignalsPersistence(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = "Postgres:Signals")
|
||||
{
|
||||
services.Configure<PostgresOptions>(configuration.GetSection(sectionName));
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.AddSingleton<SignalsDataSource>();
|
||||
|
||||
services.AddStartupMigrations(
|
||||
SignalsDataSource.DefaultSchemaName,
|
||||
"Signals.Persistence",
|
||||
typeof(SignalsDataSource).Assembly);
|
||||
|
||||
// Register repositories
|
||||
services.AddSingleton<ICallgraphRepository, PostgresCallgraphRepository>();
|
||||
services.AddSingleton<IReachabilityFactRepository, PostgresReachabilityFactRepository>();
|
||||
services.AddSingleton<IUnknownsRepository, PostgresUnknownsRepository>();
|
||||
services.AddSingleton<IReachabilityStoreRepository, PostgresReachabilityStoreRepository>();
|
||||
services.AddSingleton<IDeploymentRefsRepository, PostgresDeploymentRefsRepository>();
|
||||
services.AddSingleton<IGraphMetricsRepository, PostgresGraphMetricsRepository>();
|
||||
services.AddSingleton<ICallGraphQueryRepository, PostgresCallGraphQueryRepository>();
|
||||
services.AddSingleton<ICallGraphProjectionRepository, PostgresCallGraphProjectionRepository>();
|
||||
|
||||
services.AddSingleton<ICallGraphSyncService, CallGraphSyncService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Signals PostgreSQL persistence 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 AddSignalsPersistence(
|
||||
this IServiceCollection services,
|
||||
Action<PostgresOptions> configureOptions)
|
||||
{
|
||||
services.Configure(configureOptions);
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.AddSingleton<SignalsDataSource>();
|
||||
|
||||
services.AddStartupMigrations(
|
||||
SignalsDataSource.DefaultSchemaName,
|
||||
"Signals.Persistence",
|
||||
typeof(SignalsDataSource).Assembly);
|
||||
|
||||
// Register repositories
|
||||
services.AddSingleton<ICallgraphRepository, PostgresCallgraphRepository>();
|
||||
services.AddSingleton<IReachabilityFactRepository, PostgresReachabilityFactRepository>();
|
||||
services.AddSingleton<IUnknownsRepository, PostgresUnknownsRepository>();
|
||||
services.AddSingleton<IReachabilityStoreRepository, PostgresReachabilityStoreRepository>();
|
||||
services.AddSingleton<IDeploymentRefsRepository, PostgresDeploymentRefsRepository>();
|
||||
services.AddSingleton<IGraphMetricsRepository, PostgresGraphMetricsRepository>();
|
||||
services.AddSingleton<ICallGraphQueryRepository, PostgresCallGraphQueryRepository>();
|
||||
services.AddSingleton<ICallGraphProjectionRepository, PostgresCallGraphProjectionRepository>();
|
||||
|
||||
services.AddSingleton<ICallGraphSyncService, CallGraphSyncService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
-- Signals Schema Migration 001: Initial Schema (Compacted)
|
||||
-- Consolidated from V0000_001__extensions.sql, V1102_001__unknowns_scoring_schema.sql,
|
||||
-- V1105_001__deploy_refs_graph_metrics.sql, and V3102_001__callgraph_relational_tables.sql
|
||||
-- for 1.0.0 release
|
||||
-- Creates the signals schema for call graphs, reachability, unknowns scoring, and runtime facts
|
||||
|
||||
-- ============================================================================
|
||||
-- Extensions
|
||||
-- ============================================================================
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- ============================================================================
|
||||
-- Schema Creation
|
||||
-- ============================================================================
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS signals;
|
||||
|
||||
-- ============================================================================
|
||||
-- Scan Tracking
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.scans (
|
||||
scan_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
artifact_digest TEXT NOT NULL,
|
||||
repo_uri TEXT,
|
||||
commit_sha TEXT,
|
||||
sbom_digest TEXT,
|
||||
policy_digest TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ,
|
||||
error_message TEXT,
|
||||
|
||||
CONSTRAINT scans_artifact_sbom_unique UNIQUE (artifact_digest, sbom_digest)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_scans_status ON signals.scans(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_scans_artifact ON signals.scans(artifact_digest);
|
||||
CREATE INDEX IF NOT EXISTS idx_scans_commit ON signals.scans(commit_sha) WHERE commit_sha IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_scans_created ON signals.scans(created_at DESC);
|
||||
|
||||
COMMENT ON TABLE signals.scans IS 'Tracks scan context for call graph analysis';
|
||||
|
||||
-- ============================================================================
|
||||
-- Artifacts
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.artifacts (
|
||||
artifact_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
artifact_key TEXT NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('assembly', 'jar', 'module', 'binary', 'script')),
|
||||
sha256 TEXT NOT NULL,
|
||||
purl TEXT,
|
||||
build_id TEXT,
|
||||
file_path TEXT,
|
||||
size_bytes BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT artifacts_scan_key_unique UNIQUE (scan_id, artifact_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artifacts_scan ON signals.artifacts(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_artifacts_sha256 ON signals.artifacts(sha256);
|
||||
CREATE INDEX IF NOT EXISTS idx_artifacts_purl ON signals.artifacts(purl) WHERE purl IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_artifacts_build_id ON signals.artifacts(build_id) WHERE build_id IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE signals.artifacts IS 'Individual artifacts (assemblies, JARs, modules) within a scan';
|
||||
|
||||
-- ============================================================================
|
||||
-- Call Graph Nodes
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.cg_nodes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
node_id TEXT NOT NULL,
|
||||
artifact_key TEXT,
|
||||
symbol_key TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'unknown'
|
||||
CHECK (visibility IN ('public', 'internal', 'protected', 'private', 'unknown')),
|
||||
is_entrypoint_candidate BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
purl TEXT,
|
||||
symbol_digest TEXT,
|
||||
flags INT NOT NULL DEFAULT 0,
|
||||
attributes JSONB,
|
||||
|
||||
CONSTRAINT cg_nodes_scan_node_unique UNIQUE (scan_id, node_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_nodes_scan ON signals.cg_nodes(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_nodes_symbol_key ON signals.cg_nodes(symbol_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_nodes_purl ON signals.cg_nodes(purl) WHERE purl IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_nodes_entrypoint ON signals.cg_nodes(scan_id, is_entrypoint_candidate)
|
||||
WHERE is_entrypoint_candidate = TRUE;
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_nodes_symbol_fts ON signals.cg_nodes
|
||||
USING gin(to_tsvector('simple', symbol_key));
|
||||
|
||||
COMMENT ON TABLE signals.cg_nodes IS 'Individual nodes (symbols) in call graphs';
|
||||
COMMENT ON COLUMN signals.cg_nodes.visibility IS 'Symbol visibility: public, internal, protected, private, unknown';
|
||||
COMMENT ON COLUMN signals.cg_nodes.flags IS 'Bitfield for node properties (static, virtual, async, etc.)';
|
||||
|
||||
-- ============================================================================
|
||||
-- Call Graph Edges
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.cg_edges (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
from_node_id TEXT NOT NULL,
|
||||
to_node_id TEXT NOT NULL,
|
||||
kind SMALLINT NOT NULL DEFAULT 0, -- 0=static, 1=heuristic, 2=runtime
|
||||
reason SMALLINT NOT NULL DEFAULT 0, -- EdgeReason enum value
|
||||
weight REAL NOT NULL DEFAULT 1.0,
|
||||
offset_bytes INT,
|
||||
is_resolved BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
provenance TEXT,
|
||||
|
||||
CONSTRAINT cg_edges_unique UNIQUE (scan_id, from_node_id, to_node_id, kind, reason)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_edges_scan ON signals.cg_edges(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_edges_from ON signals.cg_edges(scan_id, from_node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_edges_to ON signals.cg_edges(scan_id, to_node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cg_edges_traversal ON signals.cg_edges(scan_id, from_node_id)
|
||||
INCLUDE (to_node_id, kind, weight);
|
||||
|
||||
COMMENT ON TABLE signals.cg_edges IS 'Call edges between nodes in the call graph';
|
||||
COMMENT ON COLUMN signals.cg_edges.kind IS 'Edge kind: 0=static, 1=heuristic, 2=runtime';
|
||||
COMMENT ON COLUMN signals.cg_edges.reason IS 'EdgeReason enum value explaining why this edge exists';
|
||||
|
||||
-- ============================================================================
|
||||
-- Entrypoints
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.entrypoints (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
node_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN (
|
||||
'http', 'grpc', 'cli', 'job', 'event', 'message_queue',
|
||||
'timer', 'test', 'main', 'module_init', 'static_constructor', 'unknown'
|
||||
)),
|
||||
framework TEXT,
|
||||
route TEXT,
|
||||
http_method TEXT,
|
||||
phase TEXT NOT NULL DEFAULT 'runtime'
|
||||
CHECK (phase IN ('module_init', 'app_start', 'runtime', 'shutdown')),
|
||||
order_idx INT NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT entrypoints_scan_node_unique UNIQUE (scan_id, node_id, kind)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_entrypoints_scan ON signals.entrypoints(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_entrypoints_kind ON signals.entrypoints(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_entrypoints_route ON signals.entrypoints(route) WHERE route IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE signals.entrypoints IS 'Framework-aware entrypoints detected in the call graph';
|
||||
COMMENT ON COLUMN signals.entrypoints.phase IS 'Execution phase: module_init, app_start, runtime, shutdown';
|
||||
|
||||
-- ============================================================================
|
||||
-- Symbol-to-Component Mapping
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.symbol_component_map (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
node_id TEXT NOT NULL,
|
||||
purl TEXT NOT NULL,
|
||||
mapping_kind TEXT NOT NULL CHECK (mapping_kind IN (
|
||||
'exact', 'assembly', 'namespace', 'heuristic'
|
||||
)),
|
||||
confidence REAL NOT NULL DEFAULT 1.0,
|
||||
evidence JSONB,
|
||||
|
||||
CONSTRAINT symbol_component_map_unique UNIQUE (scan_id, node_id, purl)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_symbol_component_scan ON signals.symbol_component_map(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_symbol_component_purl ON signals.symbol_component_map(purl);
|
||||
CREATE INDEX IF NOT EXISTS idx_symbol_component_node ON signals.symbol_component_map(scan_id, node_id);
|
||||
|
||||
COMMENT ON TABLE signals.symbol_component_map IS 'Maps symbols to SBOM components for vulnerability correlation';
|
||||
COMMENT ON COLUMN signals.symbol_component_map.mapping_kind IS 'How the mapping was determined: exact, assembly, namespace, heuristic';
|
||||
|
||||
-- ============================================================================
|
||||
-- Reachability Results
|
||||
-- ============================================================================
|
||||
|
||||
-- Component-level reachability status
|
||||
CREATE TABLE IF NOT EXISTS signals.reachability_components (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
purl TEXT NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 0, -- ReachabilityStatus enum
|
||||
lattice_state TEXT,
|
||||
confidence REAL NOT NULL DEFAULT 0,
|
||||
why JSONB,
|
||||
evidence JSONB,
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT reachability_components_unique UNIQUE (scan_id, purl)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reachability_components_scan ON signals.reachability_components(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reachability_components_purl ON signals.reachability_components(purl);
|
||||
CREATE INDEX IF NOT EXISTS idx_reachability_components_status ON signals.reachability_components(status);
|
||||
|
||||
COMMENT ON TABLE signals.reachability_components IS 'Component-level reachability status for each scan';
|
||||
|
||||
-- CVE-level reachability findings
|
||||
CREATE TABLE IF NOT EXISTS signals.reachability_findings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
cve_id TEXT NOT NULL,
|
||||
purl TEXT NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 0,
|
||||
lattice_state TEXT,
|
||||
confidence REAL NOT NULL DEFAULT 0,
|
||||
path_witness TEXT[],
|
||||
why JSONB,
|
||||
evidence JSONB,
|
||||
spine_id UUID,
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT reachability_findings_unique UNIQUE (scan_id, cve_id, purl)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reachability_findings_scan ON signals.reachability_findings(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reachability_findings_cve ON signals.reachability_findings(cve_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reachability_findings_purl ON signals.reachability_findings(purl);
|
||||
CREATE INDEX IF NOT EXISTS idx_reachability_findings_status ON signals.reachability_findings(status);
|
||||
|
||||
COMMENT ON TABLE signals.reachability_findings IS 'CVE-level reachability findings with path witnesses';
|
||||
COMMENT ON COLUMN signals.reachability_findings.path_witness IS 'Array of node IDs forming the reachability path';
|
||||
|
||||
-- ============================================================================
|
||||
-- Runtime Samples
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.runtime_samples (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
scan_id UUID NOT NULL REFERENCES signals.scans(scan_id) ON DELETE CASCADE,
|
||||
collected_at TIMESTAMPTZ NOT NULL,
|
||||
env_hash TEXT,
|
||||
timestamp TIMESTAMPTZ NOT NULL,
|
||||
pid INT,
|
||||
thread_id INT,
|
||||
frames TEXT[] NOT NULL,
|
||||
weight REAL NOT NULL DEFAULT 1.0,
|
||||
container_id TEXT,
|
||||
pod_name TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_runtime_samples_scan ON signals.runtime_samples(scan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_runtime_samples_collected ON signals.runtime_samples(collected_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_runtime_samples_frames ON signals.runtime_samples USING gin(frames);
|
||||
|
||||
COMMENT ON TABLE signals.runtime_samples IS 'Stack trace samples from runtime evidence collection';
|
||||
|
||||
-- ============================================================================
|
||||
-- Deployment References (Popularity Scoring)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.deploy_refs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
purl TEXT NOT NULL,
|
||||
purl_version TEXT,
|
||||
image_id TEXT NOT NULL,
|
||||
image_digest TEXT,
|
||||
environment TEXT NOT NULL DEFAULT 'unknown'
|
||||
CONSTRAINT chk_environment CHECK (environment IN ('production', 'staging', 'development', 'test', 'unknown')),
|
||||
namespace TEXT,
|
||||
cluster TEXT,
|
||||
region TEXT,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_deploy_refs_purl_image_env UNIQUE (purl, image_id, environment)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_deploy_refs_purl ON signals.deploy_refs(purl);
|
||||
CREATE INDEX IF NOT EXISTS idx_deploy_refs_purl_version ON signals.deploy_refs(purl, purl_version)
|
||||
WHERE purl_version IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_deploy_refs_last_seen ON signals.deploy_refs(last_seen_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_deploy_refs_environment ON signals.deploy_refs(environment);
|
||||
CREATE INDEX IF NOT EXISTS idx_deploy_refs_active ON signals.deploy_refs(purl, last_seen_at)
|
||||
WHERE last_seen_at > NOW() - INTERVAL '30 days';
|
||||
|
||||
COMMENT ON TABLE signals.deploy_refs IS 'Tracks package deployments across images and environments for popularity scoring (P factor).';
|
||||
COMMENT ON COLUMN signals.deploy_refs.purl IS 'Package URL (PURL) identifier, e.g., pkg:npm/lodash@4.17.21';
|
||||
COMMENT ON COLUMN signals.deploy_refs.environment IS 'Deployment environment: production (highest weight), staging, development, test, unknown';
|
||||
COMMENT ON COLUMN signals.deploy_refs.first_seen_at IS 'When this package was first observed in this image/environment';
|
||||
COMMENT ON COLUMN signals.deploy_refs.last_seen_at IS 'Most recent observation timestamp; used for active deployment filtering';
|
||||
|
||||
-- ============================================================================
|
||||
-- Graph Metrics (Centrality Scoring)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.graph_metrics (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
node_id TEXT NOT NULL,
|
||||
callgraph_id TEXT NOT NULL,
|
||||
node_type TEXT NOT NULL DEFAULT 'symbol'
|
||||
CONSTRAINT chk_node_type CHECK (node_type IN ('symbol', 'package', 'function', 'class', 'method')),
|
||||
degree_centrality INT NOT NULL DEFAULT 0,
|
||||
in_degree INT NOT NULL DEFAULT 0,
|
||||
out_degree INT NOT NULL DEFAULT 0,
|
||||
betweenness_centrality FLOAT NOT NULL DEFAULT 0.0,
|
||||
closeness_centrality FLOAT,
|
||||
eigenvector_centrality FLOAT,
|
||||
normalized_betweenness FLOAT
|
||||
CONSTRAINT chk_norm_betweenness CHECK (normalized_betweenness IS NULL OR (normalized_betweenness >= 0.0 AND normalized_betweenness <= 1.0)),
|
||||
normalized_degree FLOAT
|
||||
CONSTRAINT chk_norm_degree CHECK (normalized_degree IS NULL OR (normalized_degree >= 0.0 AND normalized_degree <= 1.0)),
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
computation_duration_ms INT,
|
||||
algorithm_version TEXT NOT NULL DEFAULT '1.0',
|
||||
total_nodes INT,
|
||||
total_edges INT,
|
||||
|
||||
CONSTRAINT uq_graph_metrics_node_graph UNIQUE (node_id, callgraph_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_graph_metrics_node ON signals.graph_metrics(node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_graph_metrics_callgraph ON signals.graph_metrics(callgraph_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_graph_metrics_betweenness ON signals.graph_metrics(betweenness_centrality DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_graph_metrics_computed ON signals.graph_metrics(computed_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_graph_metrics_high_centrality ON signals.graph_metrics(callgraph_id, normalized_betweenness DESC)
|
||||
WHERE normalized_betweenness > 0.5;
|
||||
|
||||
COMMENT ON TABLE signals.graph_metrics IS 'Stores computed graph centrality metrics for call graph nodes (C factor).';
|
||||
COMMENT ON COLUMN signals.graph_metrics.node_id IS 'Symbol identifier from call graph, matches SymbolId format';
|
||||
COMMENT ON COLUMN signals.graph_metrics.betweenness_centrality IS 'Raw betweenness centrality: number of shortest paths passing through this node';
|
||||
COMMENT ON COLUMN signals.graph_metrics.normalized_betweenness IS 'Betweenness normalized to 0.0-1.0 range: raw / max(raw) across graph';
|
||||
COMMENT ON COLUMN signals.graph_metrics.algorithm_version IS 'Version of centrality algorithm used (e.g., "brandes-1.0")';
|
||||
|
||||
-- ============================================================================
|
||||
-- Unknowns Table (Full Scoring Schema)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signals.unknowns (
|
||||
id TEXT NOT NULL,
|
||||
subject_key TEXT NOT NULL,
|
||||
callgraph_id TEXT,
|
||||
symbol_id TEXT,
|
||||
code_id TEXT,
|
||||
purl TEXT,
|
||||
purl_version TEXT,
|
||||
edge_from TEXT,
|
||||
edge_to TEXT,
|
||||
reason TEXT,
|
||||
-- Scoring factors (range: 0.0 - 1.0)
|
||||
popularity_p FLOAT DEFAULT 0.0
|
||||
CONSTRAINT chk_popularity_range CHECK (popularity_p >= 0.0 AND popularity_p <= 1.0),
|
||||
deployment_count INT DEFAULT 0,
|
||||
exploit_potential_e FLOAT DEFAULT 0.0
|
||||
CONSTRAINT chk_exploit_range CHECK (exploit_potential_e >= 0.0 AND exploit_potential_e <= 1.0),
|
||||
uncertainty_u FLOAT DEFAULT 0.0
|
||||
CONSTRAINT chk_uncertainty_range CHECK (uncertainty_u >= 0.0 AND uncertainty_u <= 1.0),
|
||||
centrality_c FLOAT DEFAULT 0.0
|
||||
CONSTRAINT chk_centrality_range CHECK (centrality_c >= 0.0 AND centrality_c <= 1.0),
|
||||
degree_centrality INT DEFAULT 0,
|
||||
betweenness_centrality FLOAT DEFAULT 0.0,
|
||||
staleness_s FLOAT DEFAULT 0.0
|
||||
CONSTRAINT chk_staleness_range CHECK (staleness_s >= 0.0 AND staleness_s <= 1.0),
|
||||
days_since_analysis INT DEFAULT 0,
|
||||
-- Composite score and band
|
||||
score FLOAT DEFAULT 0.0
|
||||
CONSTRAINT chk_score_range CHECK (score >= 0.0 AND score <= 1.0),
|
||||
band TEXT DEFAULT 'cold'
|
||||
CONSTRAINT chk_band_value CHECK (band IN ('hot', 'warm', 'cold')),
|
||||
-- JSONB columns
|
||||
unknown_flags JSONB DEFAULT '{}'::jsonb,
|
||||
normalization_trace JSONB,
|
||||
-- Rescan scheduling
|
||||
rescan_attempts INT DEFAULT 0,
|
||||
last_rescan_result TEXT,
|
||||
next_scheduled_rescan TIMESTAMPTZ,
|
||||
last_analyzed_at TIMESTAMPTZ,
|
||||
-- Graph slice reference
|
||||
graph_slice_hash BYTEA,
|
||||
evidence_set_hash BYTEA,
|
||||
callgraph_attempt_hash BYTEA,
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (subject_key, id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_unknowns_band ON signals.unknowns(band);
|
||||
CREATE INDEX IF NOT EXISTS idx_unknowns_score_desc ON signals.unknowns(score DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_unknowns_band_score ON signals.unknowns(band, score DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_unknowns_next_rescan ON signals.unknowns(next_scheduled_rescan)
|
||||
WHERE next_scheduled_rescan IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_unknowns_hot_band ON signals.unknowns(score DESC)
|
||||
WHERE band = 'hot';
|
||||
CREATE INDEX IF NOT EXISTS idx_unknowns_purl ON signals.unknowns(purl);
|
||||
CREATE INDEX IF NOT EXISTS idx_unknowns_flags_gin ON signals.unknowns USING GIN (unknown_flags);
|
||||
|
||||
COMMENT ON COLUMN signals.unknowns.popularity_p IS
|
||||
'Deployment impact score (P). Formula: min(1, log10(1 + deployments)/log10(1 + 100))';
|
||||
COMMENT ON COLUMN signals.unknowns.exploit_potential_e IS
|
||||
'Exploit consequence potential (E). Based on CVE severity, KEV status.';
|
||||
COMMENT ON COLUMN signals.unknowns.uncertainty_u IS
|
||||
'Uncertainty density (U). Aggregated from flags: no_provenance(0.30), version_range(0.25), conflicting_feeds(0.20), missing_vector(0.15), unreachable_source(0.10)';
|
||||
COMMENT ON COLUMN signals.unknowns.centrality_c IS
|
||||
'Graph centrality (C). Normalized betweenness centrality.';
|
||||
COMMENT ON COLUMN signals.unknowns.staleness_s IS
|
||||
'Evidence staleness (S). Formula: min(1, age_days / 14)';
|
||||
COMMENT ON COLUMN signals.unknowns.score IS
|
||||
'Composite score: clamp01(wP*P + wE*E + wU*U + wC*C + wS*S). Default weights: wP=0.25, wE=0.25, wU=0.25, wC=0.15, wS=0.10';
|
||||
COMMENT ON COLUMN signals.unknowns.band IS
|
||||
'Triage band. HOT (>=0.70): immediate rescan. WARM (0.40-0.69): scheduled 12-72h. COLD (<0.40): weekly batch.';
|
||||
COMMENT ON COLUMN signals.unknowns.unknown_flags IS
|
||||
'JSONB flags: {no_provenance_anchor, version_range, conflicting_feeds, missing_vector, unreachable_source_advisory, dynamic_call_target, external_assembly}';
|
||||
COMMENT ON COLUMN signals.unknowns.normalization_trace IS
|
||||
'JSONB trace of scoring computation for audit/debugging. Includes raw values, normalized values, weights, and formula.';
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Views
|
||||
-- ============================================================================
|
||||
|
||||
-- Deployment counts per package (for popularity scoring)
|
||||
CREATE OR REPLACE VIEW signals.deploy_counts AS
|
||||
SELECT
|
||||
purl,
|
||||
COUNT(DISTINCT image_id) as image_count,
|
||||
COUNT(DISTINCT environment) as env_count,
|
||||
COUNT(*) as total_deployments,
|
||||
MAX(last_seen_at) as last_deployment,
|
||||
MIN(first_seen_at) as first_deployment
|
||||
FROM signals.deploy_refs
|
||||
WHERE last_seen_at > NOW() - INTERVAL '30 days'
|
||||
GROUP BY purl;
|
||||
|
||||
COMMENT ON VIEW signals.deploy_counts IS
|
||||
'Aggregated deployment counts per package for popularity scoring. Only includes active deployments (last 30 days).';
|
||||
|
||||
-- High-centrality nodes per graph
|
||||
CREATE OR REPLACE VIEW signals.high_centrality_nodes AS
|
||||
SELECT
|
||||
callgraph_id,
|
||||
node_id,
|
||||
node_type,
|
||||
betweenness_centrality,
|
||||
normalized_betweenness,
|
||||
degree_centrality,
|
||||
computed_at
|
||||
FROM signals.graph_metrics
|
||||
WHERE normalized_betweenness > 0.5
|
||||
ORDER BY callgraph_id, normalized_betweenness DESC;
|
||||
|
||||
COMMENT ON VIEW signals.high_centrality_nodes IS
|
||||
'Nodes with normalized betweenness > 0.5, sorted by centrality within each graph.';
|
||||
|
||||
-- ============================================================================
|
||||
-- Materialized Views for Analytics
|
||||
-- ============================================================================
|
||||
|
||||
-- Daily scan statistics
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS signals.scan_stats_daily AS
|
||||
SELECT
|
||||
DATE_TRUNC('day', created_at) AS day,
|
||||
COUNT(*) AS total_scans,
|
||||
COUNT(*) FILTER (WHERE status = 'completed') AS completed_scans,
|
||||
COUNT(*) FILTER (WHERE status = 'failed') AS failed_scans,
|
||||
AVG(EXTRACT(EPOCH FROM (completed_at - created_at))) FILTER (WHERE status = 'completed') AS avg_duration_seconds
|
||||
FROM signals.scans
|
||||
GROUP BY DATE_TRUNC('day', created_at)
|
||||
ORDER BY day DESC;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_scan_stats_daily_day ON signals.scan_stats_daily(day);
|
||||
|
||||
-- CVE reachability summary
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS signals.cve_reachability_summary AS
|
||||
SELECT
|
||||
cve_id,
|
||||
COUNT(DISTINCT scan_id) AS affected_scans,
|
||||
COUNT(DISTINCT purl) AS affected_components,
|
||||
COUNT(*) FILTER (WHERE status = 2) AS reachable_count, -- REACHABLE_STATIC
|
||||
COUNT(*) FILTER (WHERE status = 3) AS proven_count, -- REACHABLE_PROVEN
|
||||
COUNT(*) FILTER (WHERE status = 0) AS unreachable_count,
|
||||
AVG(confidence) AS avg_confidence,
|
||||
MAX(computed_at) AS last_updated
|
||||
FROM signals.reachability_findings
|
||||
GROUP BY cve_id;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_cve_reachability_summary_cve ON signals.cve_reachability_summary(cve_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- Refresh Function for Materialized Views
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION signals.refresh_analytics_views()
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY signals.scan_stats_daily;
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY signals.cve_reachability_summary;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION signals.refresh_analytics_views IS
|
||||
'Refreshes all analytics materialized views concurrently';
|
||||
@@ -0,0 +1,23 @@
|
||||
# Archived Pre-1.0 Migrations
|
||||
|
||||
This directory contains the original migrations that were compacted into `001_initial_schema.sql`
|
||||
for the 1.0.0 release.
|
||||
|
||||
## Original Files
|
||||
- `V0000_001__extensions.sql` - PostgreSQL extensions (pgcrypto)
|
||||
- `V1102_001__unknowns_scoring_schema.sql` - Unknowns scoring columns extension
|
||||
- `V1105_001__deploy_refs_graph_metrics.sql` - Deployment references and graph metrics tables
|
||||
- `V3102_001__callgraph_relational_tables.sql` - Call graph relational tables (scans, artifacts, cg_nodes, cg_edges, entrypoints, etc.)
|
||||
|
||||
## Why Archived
|
||||
Pre-1.0, the schema evolved incrementally with Flyway-style naming. For 1.0.0, migrations were:
|
||||
- Renamed to standard `NNN_description.sql` format
|
||||
- Compacted into a single initial schema
|
||||
- This simplifies new deployments and provides a cleaner upgrade path
|
||||
|
||||
## For Existing Deployments
|
||||
If upgrading from pre-1.0, run the reset script directly with psql:
|
||||
```bash
|
||||
psql -h <host> -U <user> -d <db> -f devops/scripts/migrations-reset-pre-1.0.sql
|
||||
```
|
||||
This updates `schema_migrations` to recognize the compacted schema.
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for querying call graph data across scans.
|
||||
@@ -12,7 +12,7 @@ using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="ICallGraphProjectionRepository"/>.
|
||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for querying call graph data across scans.
|
||||
@@ -5,7 +5,7 @@ using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="ICallgraphRepository"/>.
|
||||
@@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IDeploymentRefsRepository"/>.
|
||||
@@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IGraphMetricsRepository"/>.
|
||||
@@ -5,7 +5,7 @@ using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IReachabilityFactRepository"/>.
|
||||
@@ -6,7 +6,7 @@ using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Models.ReachabilityStore;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IReachabilityStoreRepository"/>.
|
||||
@@ -6,7 +6,7 @@ using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
namespace StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IUnknownsRepository"/>.
|
||||
@@ -5,9 +5,9 @@ using StellaOps.Infrastructure.Postgres.Migrations;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
using StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres;
|
||||
namespace StellaOps.Signals.Persistence.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring Signals PostgreSQL storage services.
|
||||
@@ -4,7 +4,7 @@ using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres;
|
||||
namespace StellaOps.Signals.Persistence.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL data source for Signals module.
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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.Signals.Persistence</RootNamespace>
|
||||
<AssemblyName>StellaOps.Signals.Persistence</AssemblyName>
|
||||
<Description>Consolidated persistence layer for StellaOps Signals module</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
|
||||
<PackageReference Include="Npgsql" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Signals\StellaOps.Signals.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,316 @@
|
||||
// <copyright file="EbpfSignalMergerTests.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Reachability.Runtime;
|
||||
using StellaOps.Scanner.Reachability.Slices;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="EbpfSignalMerger"/>.
|
||||
/// </summary>
|
||||
public sealed class EbpfSignalMergerTests
|
||||
{
|
||||
private readonly EbpfSignalMerger _merger;
|
||||
private readonly RuntimeStaticMerger _baseMerger;
|
||||
|
||||
public EbpfSignalMergerTests()
|
||||
{
|
||||
_baseMerger = new RuntimeStaticMerger();
|
||||
_merger = new EbpfSignalMerger(
|
||||
_baseMerger,
|
||||
NullLogger<EbpfSignalMerger>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_WithNoSignals_ReturnsSameGraph()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
|
||||
var result = _merger.Merge(graph, null);
|
||||
|
||||
Assert.Same(graph, result.MergedGraph);
|
||||
Assert.Empty(result.Evidence);
|
||||
Assert.Equal(2, result.Statistics.StaticEdgeCount);
|
||||
Assert.Equal(0, result.Statistics.RuntimeEventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_WithEmptySignals_ReturnsSameGraph()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var signals = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 0,
|
||||
CallPaths = [],
|
||||
ObservedSymbols = [],
|
||||
};
|
||||
|
||||
var result = _merger.Merge(graph, signals);
|
||||
|
||||
Assert.Same(graph, result.MergedGraph);
|
||||
Assert.Empty(result.Evidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_WithMatchingSignals_CreatesConfirmedEvidence()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var signals = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 100,
|
||||
CallPaths = new List<ObservedCallPath>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbols = ["main", "processRequest"],
|
||||
ObservationCount = 50,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
},
|
||||
},
|
||||
ObservedSymbols = ["main", "processRequest"],
|
||||
};
|
||||
|
||||
var result = _merger.Merge(graph, signals);
|
||||
|
||||
Assert.NotEmpty(result.Evidence);
|
||||
Assert.Contains(result.Evidence, e =>
|
||||
e.Type == RuntimeEvidenceType.RuntimeConfirmed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_WithRuntimeOnlyPath_CreatesRuntimeOnlyEvidence()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var signals = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 100,
|
||||
CallPaths = new List<ObservedCallPath>
|
||||
{
|
||||
new()
|
||||
{
|
||||
// Path not in static graph
|
||||
Symbols = ["dynamic_dispatch", "hidden_method"],
|
||||
ObservationCount = 20,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
},
|
||||
},
|
||||
ObservedSymbols = ["dynamic_dispatch", "hidden_method"],
|
||||
};
|
||||
|
||||
var result = _merger.Merge(graph, signals);
|
||||
|
||||
Assert.Contains(result.Evidence, e =>
|
||||
e.Type == RuntimeEvidenceType.RuntimeOnly);
|
||||
Assert.True(result.Statistics.RuntimeOnlyPathCount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_WithDetectedRuntimes_CreatesRuntimeDetectedEvidence()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var signals = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 100,
|
||||
CallPaths = [],
|
||||
ObservedSymbols = [],
|
||||
DetectedRuntimes = [RuntimeType.Node, RuntimeType.Python],
|
||||
};
|
||||
|
||||
var result = _merger.Merge(graph, signals);
|
||||
|
||||
Assert.Contains(result.Evidence, e =>
|
||||
e.Type == RuntimeEvidenceType.RuntimeDetected &&
|
||||
e.RuntimeType == "Node");
|
||||
Assert.Contains(result.Evidence, e =>
|
||||
e.Type == RuntimeEvidenceType.RuntimeDetected &&
|
||||
e.RuntimeType == "Python");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePath_WithValidPath_ReturnsConfirmed()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var path = new ObservedCallPath
|
||||
{
|
||||
Symbols = ["main", "processRequest"],
|
||||
ObservationCount = 10,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow,
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
var result = _merger.ValidatePath(graph, path);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(PathType.Confirmed, result.PathType);
|
||||
Assert.Equal(1.0, result.MatchRatio);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePath_WithUnknownPath_ReturnsRuntimeOnly()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var path = new ObservedCallPath
|
||||
{
|
||||
Symbols = ["unknown", "method"],
|
||||
ObservationCount = 10,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow,
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
var result = _merger.ValidatePath(graph, path);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(PathType.RuntimeOnly, result.PathType);
|
||||
Assert.Equal(0.0, result.MatchRatio);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatePath_WithShortPath_ReturnsInvalid()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var path = new ObservedCallPath
|
||||
{
|
||||
Symbols = ["single"],
|
||||
ObservationCount = 10,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow,
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
var result = _merger.ValidatePath(graph, path);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(PathType.Invalid, result.PathType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_StatisticsAreAccurate()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var signals = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 500,
|
||||
CallPaths = new List<ObservedCallPath>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbols = ["main", "processRequest"],
|
||||
ObservationCount = 100,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
},
|
||||
},
|
||||
ObservedSymbols = ["main", "processRequest"],
|
||||
DroppedEvents = 10,
|
||||
};
|
||||
|
||||
var result = _merger.Merge(graph, signals);
|
||||
|
||||
Assert.Equal(2, result.Statistics.StaticEdgeCount);
|
||||
Assert.Equal(500, result.Statistics.RuntimeEventCount);
|
||||
Assert.Equal(1, result.Statistics.CallPathCount);
|
||||
Assert.Equal(10, result.Statistics.DroppedEventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeEvidence_ContainsContainerId()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var signals = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "my-container-id",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 100,
|
||||
CallPaths = new List<ObservedCallPath>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbols = ["main", "processRequest"],
|
||||
ObservationCount = 10,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
},
|
||||
},
|
||||
ObservedSymbols = [],
|
||||
};
|
||||
|
||||
var result = _merger.Merge(graph, signals);
|
||||
|
||||
Assert.All(result.Evidence, e =>
|
||||
Assert.Equal("my-container-id", e.ContainerId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceSource_IsEbpf()
|
||||
{
|
||||
var graph = CreateTestGraph();
|
||||
var signals = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-123",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 100,
|
||||
CallPaths = new List<ObservedCallPath>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbols = ["main", "processRequest"],
|
||||
ObservationCount = 10,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
},
|
||||
},
|
||||
ObservedSymbols = [],
|
||||
};
|
||||
|
||||
var result = _merger.Merge(graph, signals);
|
||||
|
||||
Assert.All(result.Evidence, e =>
|
||||
Assert.Equal(EvidenceSource.Ebpf, e.Source));
|
||||
}
|
||||
|
||||
private static RichGraph CreateTestGraph()
|
||||
{
|
||||
return new RichGraph(
|
||||
Nodes: new List<RichGraphNode>
|
||||
{
|
||||
new("main", "main", null, null, "native", "entrypoint", null, null, null, null, null),
|
||||
new("processRequest", "processRequest", null, null, "native", "function", null, null, null, null, null),
|
||||
new("handleError", "handleError", null, null, "native", "function", null, null, null, null, null),
|
||||
},
|
||||
Edges: new List<RichGraphEdge>
|
||||
{
|
||||
new("main", "processRequest", "call", null, null, null, 1.0, null),
|
||||
new("processRequest", "handleError", "call", null, null, null, 0.8, null),
|
||||
},
|
||||
Roots: new List<RichGraphRoot>
|
||||
{
|
||||
new("main", "main", "entrypoint")
|
||||
},
|
||||
Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// <copyright file="RuntimeSignalCollectorTests.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Signals.Ebpf.Probes;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using StellaOps.Signals.Ebpf.Services;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RuntimeSignalCollector"/>.
|
||||
/// </summary>
|
||||
public sealed class RuntimeSignalCollectorTests
|
||||
{
|
||||
private readonly RuntimeSignalCollector _collector;
|
||||
private readonly MockProbeLoader _probeLoader;
|
||||
|
||||
public RuntimeSignalCollectorTests()
|
||||
{
|
||||
_probeLoader = new MockProbeLoader();
|
||||
_collector = new RuntimeSignalCollector(
|
||||
NullLogger<RuntimeSignalCollector>.Instance,
|
||||
_probeLoader);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSupported_ReturnsCorrectValue()
|
||||
{
|
||||
// On non-Linux systems, eBPF is not supported
|
||||
var isSupported = _collector.IsSupported();
|
||||
|
||||
// This will be false on Windows/macOS test runners
|
||||
Assert.True(isSupported == false || Environment.OSVersion.Platform == PlatformID.Unix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSupportedProbeTypes_ReturnsEmptyOnUnsupportedPlatform()
|
||||
{
|
||||
var probeTypes = _collector.GetSupportedProbeTypes();
|
||||
|
||||
// On non-Linux, should be empty
|
||||
if (!_collector.IsSupported())
|
||||
{
|
||||
Assert.Empty(probeTypes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeCallEvent_HasCorrectProperties()
|
||||
{
|
||||
var evt = new RuntimeCallEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
ContainerId = "container-123",
|
||||
Pid = 1234,
|
||||
Tid = 5678,
|
||||
TimestampNs = 1000000000,
|
||||
Symbol = "malloc",
|
||||
FunctionAddress = 0x7fff12345678,
|
||||
StackTrace = [0x7fff00001000, 0x7fff00002000],
|
||||
RuntimeType = RuntimeType.Native,
|
||||
Library = "libc.so.6",
|
||||
Purl = "pkg:deb/ubuntu/libc6@2.31",
|
||||
};
|
||||
|
||||
Assert.Equal("container-123", evt.ContainerId);
|
||||
Assert.Equal(1234, evt.Pid);
|
||||
Assert.Equal("malloc", evt.Symbol);
|
||||
Assert.Equal(RuntimeType.Native, evt.RuntimeType);
|
||||
Assert.Equal(2, evt.StackTrace.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeSignalSummary_HasCorrectProperties()
|
||||
{
|
||||
var summary = new RuntimeSignalSummary
|
||||
{
|
||||
ContainerId = "container-456",
|
||||
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
StoppedAt = DateTimeOffset.UtcNow,
|
||||
TotalEvents = 1000,
|
||||
CallPaths = new List<ObservedCallPath>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbols = ["main", "processRequest", "vulnerable_func"],
|
||||
ObservationCount = 50,
|
||||
Purl = "pkg:npm/lodash@4.17.20",
|
||||
},
|
||||
},
|
||||
ObservedSymbols = ["main", "processRequest", "vulnerable_func"],
|
||||
DroppedEvents = 10,
|
||||
DetectedRuntimes = [RuntimeType.Node],
|
||||
};
|
||||
|
||||
Assert.Equal(1000, summary.TotalEvents);
|
||||
Assert.Single(summary.CallPaths);
|
||||
Assert.Equal(3, summary.CallPaths[0].Symbols.Count);
|
||||
Assert.Equal(50, summary.CallPaths[0].ObservationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeSignalOptions_HasDefaultValues()
|
||||
{
|
||||
var options = new RuntimeSignalOptions();
|
||||
|
||||
Assert.Empty(options.TargetSymbols);
|
||||
Assert.Equal(10000, options.MaxEventsPerSecond);
|
||||
Assert.Null(options.MaxDuration);
|
||||
Assert.True(options.ResolveSymbols);
|
||||
Assert.Equal(16, options.MaxStackDepth);
|
||||
Assert.Equal(256 * 1024, options.RingBufferSize);
|
||||
Assert.Equal(1, options.SampleRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignalStatistics_CapturesMetrics()
|
||||
{
|
||||
var stats = new SignalStatistics
|
||||
{
|
||||
TotalEvents = 5000,
|
||||
EventsPerSecond = 250.5,
|
||||
UniqueCallPaths = 42,
|
||||
BufferUtilization = 0.35,
|
||||
DroppedEvents = 5,
|
||||
CpuOverheadPercent = 0.1,
|
||||
MemoryUsageBytes = 1024 * 1024,
|
||||
};
|
||||
|
||||
Assert.Equal(5000, stats.TotalEvents);
|
||||
Assert.Equal(250.5, stats.EventsPerSecond);
|
||||
Assert.Equal(42, stats.UniqueCallPaths);
|
||||
Assert.Equal(0.35, stats.BufferUtilization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObservedCallPath_TracksObservations()
|
||||
{
|
||||
var path = new ObservedCallPath
|
||||
{
|
||||
Symbols = ["entry", "middle", "vulnerable"],
|
||||
ObservationCount = 100,
|
||||
Purl = "pkg:golang/example.com/pkg@1.0.0",
|
||||
RuntimeType = RuntimeType.Go,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
LastObservedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
Assert.Equal(3, path.Symbols.Count);
|
||||
Assert.Equal(100, path.ObservationCount);
|
||||
Assert.Equal(RuntimeType.Go, path.RuntimeType);
|
||||
Assert.True(path.LastObservedAt > path.FirstObservedAt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(RuntimeType.Native, "Native")]
|
||||
[InlineData(RuntimeType.Jvm, "Jvm")]
|
||||
[InlineData(RuntimeType.Node, "Node")]
|
||||
[InlineData(RuntimeType.Python, "Python")]
|
||||
[InlineData(RuntimeType.DotNet, "DotNet")]
|
||||
[InlineData(RuntimeType.Go, "Go")]
|
||||
[InlineData(RuntimeType.Ruby, "Ruby")]
|
||||
[InlineData(RuntimeType.Unknown, "Unknown")]
|
||||
public void RuntimeType_HasCorrectStringRepresentation(RuntimeType type, string expected)
|
||||
{
|
||||
Assert.Equal(expected, type.ToString());
|
||||
}
|
||||
|
||||
private sealed class MockProbeLoader : IEbpfProbeLoader
|
||||
{
|
||||
public Task<EbpfProbeHandle> LoadAndAttachAsync(
|
||||
string containerId,
|
||||
RuntimeSignalOptions options,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(new EbpfProbeHandle
|
||||
{
|
||||
ProbeId = Guid.NewGuid(),
|
||||
ContainerId = containerId,
|
||||
TracedPids = [],
|
||||
});
|
||||
}
|
||||
|
||||
public Task DetachAsync(EbpfProbeHandle handle, CancellationToken ct = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async IAsyncEnumerable<ReadOnlyMemory<byte>> ReadEventsAsync(
|
||||
EbpfProbeHandle handle,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
await Task.Yield();
|
||||
yield break;
|
||||
}
|
||||
|
||||
public (string? Symbol, string? Library, string? Purl) ResolveSymbol(int pid, ulong address)
|
||||
=> (null, null, null);
|
||||
|
||||
public double GetBufferUtilization(EbpfProbeHandle handle) => 0.0;
|
||||
|
||||
public double GetCpuOverhead(EbpfProbeHandle handle) => 0.0;
|
||||
|
||||
public long GetMemoryUsage(EbpfProbeHandle handle) => 0;
|
||||
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes() => [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Signals.Ebpf\StellaOps.Signals.Ebpf.csproj" />
|
||||
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -4,13 +4,14 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MicrosoftOptions = Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Signals.Services;
|
||||
using StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for callgraph projection to relational tables.
|
||||
@@ -6,12 +6,13 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MicrosoftOptions = Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Signals.Services;
|
||||
using StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
[Collection(SignalsPostgresCollection.Name)]
|
||||
public sealed class CallGraphSyncServiceTests : IAsyncLifetime
|
||||
@@ -2,11 +2,12 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MicrosoftOptions = Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Storage.Postgres.Repositories;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
[Collection(SignalsPostgresCollection.Name)]
|
||||
public sealed class PostgresCallgraphRepositoryTests : IAsyncLifetime
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Reflection;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
using StellaOps.Signals.Persistence.Postgres;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL integration test fixture for the Signals module.
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Signals.Persistence\StellaOps.Signals.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
@@ -136,7 +136,6 @@ public class CallgraphIngestionServiceTests
|
||||
if (request.ManifestContent is not null)
|
||||
{
|
||||
using var manifestMs = new MemoryStream();
|
||||
using StellaOps.TestKit;
|
||||
request.ManifestContent.CopyTo(manifestMs);
|
||||
manifests[request.Hash] = manifestMs.ToArray();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -244,7 +244,6 @@ public class EdgeBundleIngestionServiceTests
|
||||
using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1)));
|
||||
using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2)));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
await _service.IngestAsync(TestTenantId, stream1, null);
|
||||
await _service.IngestAsync(TestTenantId, stream2, null);
|
||||
|
||||
@@ -321,7 +321,7 @@ public class InMemoryEvidenceWeightPolicyProviderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_RemovesAllPolicies()
|
||||
public async Task Clear_RemovesAllPolicies()
|
||||
{
|
||||
var provider = new InMemoryEvidenceWeightPolicyProvider();
|
||||
provider.SetPolicy(new EvidenceWeightPolicy
|
||||
@@ -339,7 +339,7 @@ public class InMemoryEvidenceWeightPolicyProviderTests
|
||||
|
||||
provider.Clear();
|
||||
|
||||
provider.PolicyExistsAsync(null, "production").Result.Should().BeFalse();
|
||||
provider.PolicyExistsAsync(null, "development").Result.Should().BeFalse();
|
||||
(await provider.PolicyExistsAsync(null, "production")).Should().BeFalse();
|
||||
(await provider.PolicyExistsAsync(null, "development")).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
// Without MIT, sum of weights = 0.95 (default) → 95%
|
||||
result.Score.Should().BeGreaterOrEqualTo(90);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(90);
|
||||
result.Bucket.Should().Be(ScoreBucket.ActNow);
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
result.Score.Should().BeLessOrEqualTo(15);
|
||||
result.Score.Should().BeLessThanOrEqualTo(15);
|
||||
result.Caps.NotAffectedCap.Should().BeTrue();
|
||||
result.Flags.Should().Contain("vendor-na");
|
||||
}
|
||||
@@ -229,7 +229,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
result.Score.Should().BeGreaterOrEqualTo(60);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(60);
|
||||
result.Caps.RuntimeFloor.Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
// Since RTS >= 0.8, runtime floor should apply (floor at 60)
|
||||
result.Score.Should().BeGreaterOrEqualTo(60);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(60);
|
||||
result.Caps.RuntimeFloor.Should().BeTrue();
|
||||
// Speculative cap shouldn't apply because RTS > 0
|
||||
result.Caps.SpeculativeCap.Should().BeFalse();
|
||||
@@ -311,7 +311,7 @@ public class EvidenceWeightedScoreCalculatorTests
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Score.Should().BeGreaterOrEqualTo(0);
|
||||
result.Score.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -57,7 +57,7 @@ public class GroundTruthValidatorTests
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_HasValidUncertaintyTiers(string samplePath, GroundTruthDocument document)
|
||||
public void GroundTruth_HasValidUncertaintyTiers(string _samplePath, GroundTruthDocument document)
|
||||
{
|
||||
if (document.ExpectedUncertainty is null)
|
||||
{
|
||||
@@ -139,7 +139,7 @@ public class GroundTruthValidatorTests
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(GetGroundTruthSamples))]
|
||||
public void GroundTruth_EntryPointsHaveValidPhases(string samplePath, GroundTruthDocument document)
|
||||
public void GroundTruth_EntryPointsHaveValidPhases(string _samplePath, GroundTruthDocument document)
|
||||
{
|
||||
var validPhases = new[] { "load", "init", "runtime", "main", "fini" };
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ public class ReachabilityLatticeStateExtensionsTests
|
||||
[InlineData(null, ReachabilityLatticeState.Unknown)]
|
||||
public void FromCode_ReturnsExpectedState(string? code, ReachabilityLatticeState expected)
|
||||
{
|
||||
Assert.Equal(expected, ReachabilityLatticeStateExtensions.FromCode(code));
|
||||
Assert.Equal(expected, ReachabilityLatticeStateExtensions.FromCode(code!));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
@@ -88,7 +88,6 @@ public class ReachabilityUnionIngestionServiceTests
|
||||
private static string ComputeSha(string content)
|
||||
{
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
using StellaOps.TestKit;
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||
return Convert.ToHexString(sha.ComputeHash(bytes)).ToLowerInvariant();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Linq;
|
||||
@@ -48,7 +48,6 @@ public class RouterEventsPublisherTests
|
||||
var options = CreateOptions();
|
||||
var handler = new StubHandler(HttpStatusCode.InternalServerError, "boom");
|
||||
using var httpClient = new HttpClient(handler) { BaseAddress = new Uri(options.Events.Router.BaseUrl) };
|
||||
using StellaOps.TestKit;
|
||||
var logger = new ListLogger<RouterEventsPublisher>();
|
||||
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System);
|
||||
var publisher = new RouterEventsPublisher(builder, options, httpClient, logger);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO.Compression;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -252,7 +252,6 @@ public class RuntimeFactsBatchIngestionTests
|
||||
public async Task<StoredRuntimeFactsArtifact> SaveAsync(RuntimeFactsArtifactSaveRequest request, Stream content, CancellationToken cancellationToken)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using StellaOps.TestKit;
|
||||
await content.CopyToAsync(ms, cancellationToken);
|
||||
|
||||
var artifact = new StoredRuntimeFactsArtifact(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -48,7 +48,6 @@ public sealed class SimpleJsonCallgraphParserGateTests
|
||||
|
||||
var parser = new SimpleJsonCallgraphParser("csharp");
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json), writable: false);
|
||||
using StellaOps.TestKit;
|
||||
var parsed = await parser.ParseAsync(stream, CancellationToken.None);
|
||||
|
||||
parsed.Edges.Should().ContainSingle();
|
||||
|
||||
@@ -6,24 +6,27 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<!-- Disable Concelier shared test infra to avoid pulling unrelated projects into the Signals test graph -->
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<!-- FsCheck for property-based testing (EvidenceWeightedScore) -->
|
||||
<PackageReference Include="FsCheck" Version="3.0.0-rc3" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="3.0.0-rc3" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<!-- Verify for snapshot testing (EvidenceWeightedScore) -->
|
||||
<PackageReference Include="Verify.Xunit" Version="28.7.2" />
|
||||
<PackageReference Include="Verify.Xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -310,7 +310,6 @@ public class UnknownsDecayServiceTests
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
using StellaOps.TestKit;
|
||||
cts.Cancel();
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
|
||||
Reference in New Issue
Block a user