feat(telemetry): add telemetry client and services for tracking events

- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint.
- Created TtfsTelemetryService for emitting specific telemetry events related to TTFS.
- Added tests for TelemetryClient to ensure event queuing and flushing functionality.
- Introduced models for reachability drift detection, including DriftResult and DriftedSink.
- Developed DriftApiService for interacting with the drift detection API.
- Updated FirstSignalCardComponent to emit telemetry events on signal appearance.
- Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
master
2025-12-18 16:19:16 +02:00
parent 00d2c99af9
commit 811f35cba7
114 changed files with 13702 additions and 268 deletions

View File

@@ -0,0 +1,133 @@
// -----------------------------------------------------------------------------
// InternalCallGraphTests.cs
// Sprint: SPRINT_3700_0003_0001_trigger_extraction
// Description: Unit tests for InternalCallGraph.
// -----------------------------------------------------------------------------
using StellaOps.Scanner.VulnSurfaces.CallGraph;
using StellaOps.Scanner.VulnSurfaces.Models;
using Xunit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public class InternalCallGraphTests
{
[Fact]
public void AddMethod_StoresMethod()
{
// Arrange
var graph = new InternalCallGraph
{
PackageId = "TestPackage",
Version = "1.0.0"
};
var method = new InternalMethodRef
{
MethodKey = "Namespace.Class::Method()",
Name = "Method",
DeclaringType = "Namespace.Class",
IsPublic = true
};
// Act
graph.AddMethod(method);
// Assert
Assert.True(graph.ContainsMethod("Namespace.Class::Method()"));
Assert.Equal(1, graph.MethodCount);
}
[Fact]
public void AddEdge_CreatesForwardAndReverseMapping()
{
// Arrange
var graph = new InternalCallGraph
{
PackageId = "TestPackage",
Version = "1.0.0"
};
var edge = new InternalCallEdge
{
Caller = "A::M1()",
Callee = "A::M2()"
};
// Act
graph.AddEdge(edge);
// Assert
Assert.Contains("A::M2()", graph.GetCallees("A::M1()"));
Assert.Contains("A::M1()", graph.GetCallers("A::M2()"));
Assert.Equal(1, graph.EdgeCount);
}
[Fact]
public void GetPublicMethods_ReturnsOnlyPublic()
{
// Arrange
var graph = new InternalCallGraph
{
PackageId = "TestPackage",
Version = "1.0.0"
};
graph.AddMethod(new InternalMethodRef
{
MethodKey = "A::Public()",
Name = "Public",
DeclaringType = "A",
IsPublic = true
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "A::Private()",
Name = "Private",
DeclaringType = "A",
IsPublic = false
});
// Act
var publicMethods = graph.GetPublicMethods().ToList();
// Assert
Assert.Single(publicMethods);
Assert.Equal("A::Public()", publicMethods[0].MethodKey);
}
[Fact]
public void GetCallees_EmptyForUnknownMethod()
{
// Arrange
var graph = new InternalCallGraph
{
PackageId = "TestPackage",
Version = "1.0.0"
};
// Act
var callees = graph.GetCallees("Unknown::Method()");
// Assert
Assert.Empty(callees);
}
[Fact]
public void GetMethod_ReturnsNullForUnknown()
{
// Arrange
var graph = new InternalCallGraph
{
PackageId = "TestPackage",
Version = "1.0.0"
};
// Act
var method = graph.GetMethod("Unknown::Method()");
// Assert
Assert.Null(method);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Scanner.VulnSurfaces.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.VulnSurfaces\StellaOps.Scanner.VulnSurfaces.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,292 @@
// -----------------------------------------------------------------------------
// TriggerMethodExtractorTests.cs
// Sprint: SPRINT_3700_0003_0001_trigger_extraction
// Description: Unit tests for TriggerMethodExtractor.
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.VulnSurfaces.CallGraph;
using StellaOps.Scanner.VulnSurfaces.Models;
using StellaOps.Scanner.VulnSurfaces.Triggers;
using Xunit;
namespace StellaOps.Scanner.VulnSurfaces.Tests;
public class TriggerMethodExtractorTests
{
private readonly TriggerMethodExtractor _extractor;
public TriggerMethodExtractorTests()
{
_extractor = new TriggerMethodExtractor(NullLogger<TriggerMethodExtractor>.Instance);
}
[Fact]
public async Task ExtractAsync_DirectPath_FindsTrigger()
{
// Arrange
var graph = CreateTestGraph();
// Public -> Internal -> Sink
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Namespace.Class::PublicMethod()",
Name = "PublicMethod",
DeclaringType = "Namespace.Class",
IsPublic = true
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Namespace.Class::InternalHelper()",
Name = "InternalHelper",
DeclaringType = "Namespace.Class",
IsPublic = false
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Namespace.Class::VulnerableSink(String)",
Name = "VulnerableSink",
DeclaringType = "Namespace.Class",
IsPublic = false
});
graph.AddEdge(new InternalCallEdge
{
Caller = "Namespace.Class::PublicMethod()",
Callee = "Namespace.Class::InternalHelper()"
});
graph.AddEdge(new InternalCallEdge
{
Caller = "Namespace.Class::InternalHelper()",
Callee = "Namespace.Class::VulnerableSink(String)"
});
var request = new TriggerExtractionRequest
{
SurfaceId = 1,
SinkMethodKeys = ["Namespace.Class::VulnerableSink(String)"],
Graph = graph
};
// Act
var result = await _extractor.ExtractAsync(request);
// Assert
Assert.True(result.Success);
Assert.Single(result.Triggers);
var trigger = result.Triggers[0];
Assert.Equal("Namespace.Class::PublicMethod()", trigger.TriggerMethodKey);
Assert.Equal("Namespace.Class::VulnerableSink(String)", trigger.SinkMethodKey);
Assert.Equal(2, trigger.Depth);
Assert.False(trigger.IsInterfaceExpansion);
}
[Fact]
public async Task ExtractAsync_NoPath_ReturnsEmpty()
{
// Arrange
var graph = CreateTestGraph();
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Namespace.Class::PublicMethod()",
Name = "PublicMethod",
DeclaringType = "Namespace.Class",
IsPublic = true
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Namespace.Class::UnreachableSink()",
Name = "UnreachableSink",
DeclaringType = "Namespace.Class",
IsPublic = false
});
// No edge between them
var request = new TriggerExtractionRequest
{
SurfaceId = 1,
SinkMethodKeys = ["Namespace.Class::UnreachableSink()"],
Graph = graph
};
// Act
var result = await _extractor.ExtractAsync(request);
// Assert
Assert.True(result.Success);
Assert.Empty(result.Triggers);
}
[Fact]
public async Task ExtractAsync_MultiplePublicMethods_FindsAllTriggers()
{
// Arrange
var graph = CreateTestGraph();
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Class::Api1()",
Name = "Api1",
DeclaringType = "Class",
IsPublic = true
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Class::Api2()",
Name = "Api2",
DeclaringType = "Class",
IsPublic = true
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "Class::Sink()",
Name = "Sink",
DeclaringType = "Class",
IsPublic = false
});
graph.AddEdge(new InternalCallEdge { Caller = "Class::Api1()", Callee = "Class::Sink()" });
graph.AddEdge(new InternalCallEdge { Caller = "Class::Api2()", Callee = "Class::Sink()" });
var request = new TriggerExtractionRequest
{
SurfaceId = 1,
SinkMethodKeys = ["Class::Sink()"],
Graph = graph
};
// Act
var result = await _extractor.ExtractAsync(request);
// Assert
Assert.True(result.Success);
Assert.Equal(2, result.Triggers.Count);
Assert.Contains(result.Triggers, t => t.TriggerMethodKey == "Class::Api1()");
Assert.Contains(result.Triggers, t => t.TriggerMethodKey == "Class::Api2()");
}
[Fact]
public async Task ExtractAsync_MaxDepthExceeded_DoesNotFindTrigger()
{
// Arrange
var graph = CreateTestGraph();
// Create a long chain: Public -> M1 -> M2 -> M3 -> M4 -> M5 -> Sink
graph.AddMethod(new InternalMethodRef
{
MethodKey = "C::Public()",
Name = "Public",
DeclaringType = "C",
IsPublic = true
});
for (int i = 1; i <= 5; i++)
{
graph.AddMethod(new InternalMethodRef
{
MethodKey = $"C::M{i}()",
Name = $"M{i}",
DeclaringType = "C",
IsPublic = false
});
}
graph.AddMethod(new InternalMethodRef
{
MethodKey = "C::Sink()",
Name = "Sink",
DeclaringType = "C",
IsPublic = false
});
graph.AddEdge(new InternalCallEdge { Caller = "C::Public()", Callee = "C::M1()" });
graph.AddEdge(new InternalCallEdge { Caller = "C::M1()", Callee = "C::M2()" });
graph.AddEdge(new InternalCallEdge { Caller = "C::M2()", Callee = "C::M3()" });
graph.AddEdge(new InternalCallEdge { Caller = "C::M3()", Callee = "C::M4()" });
graph.AddEdge(new InternalCallEdge { Caller = "C::M4()", Callee = "C::M5()" });
graph.AddEdge(new InternalCallEdge { Caller = "C::M5()", Callee = "C::Sink()" });
var request = new TriggerExtractionRequest
{
SurfaceId = 1,
SinkMethodKeys = ["C::Sink()"],
Graph = graph,
MaxDepth = 3 // Too shallow to reach sink
};
// Act
var result = await _extractor.ExtractAsync(request);
// Assert
Assert.True(result.Success);
Assert.Empty(result.Triggers);
}
[Fact]
public async Task ExtractAsync_VirtualMethod_ReducesConfidence()
{
// Arrange
var graph = CreateTestGraph();
graph.AddMethod(new InternalMethodRef
{
MethodKey = "C::Public()",
Name = "Public",
DeclaringType = "C",
IsPublic = true
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "C::Virtual()",
Name = "Virtual",
DeclaringType = "C",
IsPublic = false,
IsVirtual = true
});
graph.AddMethod(new InternalMethodRef
{
MethodKey = "C::Sink()",
Name = "Sink",
DeclaringType = "C",
IsPublic = false
});
graph.AddEdge(new InternalCallEdge { Caller = "C::Public()", Callee = "C::Virtual()" });
graph.AddEdge(new InternalCallEdge { Caller = "C::Virtual()", Callee = "C::Sink()" });
var request = new TriggerExtractionRequest
{
SurfaceId = 1,
SinkMethodKeys = ["C::Sink()"],
Graph = graph
};
// Act
var result = await _extractor.ExtractAsync(request);
// Assert
Assert.True(result.Success);
Assert.Single(result.Triggers);
Assert.True(result.Triggers[0].Confidence < 1.0);
}
private static InternalCallGraph CreateTestGraph()
{
return new InternalCallGraph
{
PackageId = "TestPackage",
Version = "1.0.0"
};
}
}