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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user