Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime observation timeline for a finding.
|
||||
/// </summary>
|
||||
public sealed record RuntimeTimeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Finding this timeline is for.
|
||||
/// </summary>
|
||||
public required Guid FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerable component being tracked.
|
||||
/// </summary>
|
||||
public required string ComponentPurl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time window start.
|
||||
/// </summary>
|
||||
public required DateTimeOffset WindowStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time window end.
|
||||
/// </summary>
|
||||
public required DateTimeOffset WindowEnd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Overall posture based on observations.
|
||||
/// </summary>
|
||||
public required RuntimePosture Posture { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Posture explanation.
|
||||
/// </summary>
|
||||
public required string PostureExplanation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time buckets with observation summaries.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<TimelineBucket> Buckets { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Significant events in the timeline.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<TimelineEvent> Events { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total observation count.
|
||||
/// </summary>
|
||||
public int TotalObservations => Buckets.Sum(b => b.ObservationCount);
|
||||
|
||||
/// <summary>
|
||||
/// Capture session digests.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<string> SessionDigests { get; init; }
|
||||
}
|
||||
|
||||
public enum RuntimePosture
|
||||
{
|
||||
/// <summary>No runtime data available.</summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>Runtime evidence supports the verdict.</summary>
|
||||
Supports,
|
||||
|
||||
/// <summary>Runtime evidence contradicts the verdict.</summary>
|
||||
Contradicts,
|
||||
|
||||
/// <summary>Runtime evidence is inconclusive.</summary>
|
||||
Inconclusive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A time bucket in the timeline.
|
||||
/// </summary>
|
||||
public sealed record TimelineBucket
|
||||
{
|
||||
/// <summary>
|
||||
/// Bucket start time.
|
||||
/// </summary>
|
||||
public required DateTimeOffset Start { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bucket end time.
|
||||
/// </summary>
|
||||
public required DateTimeOffset End { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of observations in this bucket.
|
||||
/// </summary>
|
||||
public required int ObservationCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Observation types in this bucket.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ObservationTypeSummary> ByType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether component was loaded in this bucket.
|
||||
/// </summary>
|
||||
public required bool ComponentLoaded { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether vulnerable code was executed.
|
||||
/// </summary>
|
||||
public bool? VulnerableCodeExecuted { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of observations by type.
|
||||
/// </summary>
|
||||
public sealed record ObservationTypeSummary
|
||||
{
|
||||
public required ObservationType Type { get; init; }
|
||||
public required int Count { get; init; }
|
||||
}
|
||||
|
||||
public enum ObservationType
|
||||
{
|
||||
LibraryLoad,
|
||||
Syscall,
|
||||
NetworkConnection,
|
||||
FileAccess,
|
||||
ProcessSpawn,
|
||||
SymbolResolution
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A significant event in the timeline.
|
||||
/// </summary>
|
||||
public sealed record TimelineEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Event timestamp.
|
||||
/// </summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event type.
|
||||
/// </summary>
|
||||
public required TimelineEventType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event description.
|
||||
/// </summary>
|
||||
public required string Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Significance level.
|
||||
/// </summary>
|
||||
public required EventSignificance Significance { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Related evidence digest.
|
||||
/// </summary>
|
||||
public string? EvidenceDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional details.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Details { get; init; }
|
||||
= new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public enum TimelineEventType
|
||||
{
|
||||
ComponentLoaded,
|
||||
ComponentUnloaded,
|
||||
VulnerableFunctionCalled,
|
||||
NetworkExposure,
|
||||
SyscallBlocked,
|
||||
ProcessForked,
|
||||
CaptureStarted,
|
||||
CaptureStopped
|
||||
}
|
||||
|
||||
public enum EventSignificance
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline;
|
||||
|
||||
public interface ITimelineBuilder
|
||||
{
|
||||
RuntimeTimeline Build(
|
||||
RuntimeEvidence evidence,
|
||||
string componentPurl,
|
||||
TimelineOptions options);
|
||||
}
|
||||
|
||||
public sealed class TimelineBuilder : ITimelineBuilder
|
||||
{
|
||||
public RuntimeTimeline Build(
|
||||
RuntimeEvidence evidence,
|
||||
string componentPurl,
|
||||
TimelineOptions options)
|
||||
{
|
||||
var windowStart = options.WindowStart ?? evidence.FirstObservation;
|
||||
var windowEnd = options.WindowEnd ?? evidence.LastObservation;
|
||||
|
||||
// Build time buckets
|
||||
var buckets = BuildBuckets(evidence, componentPurl, windowStart, windowEnd, options.BucketSize);
|
||||
|
||||
// Extract significant events
|
||||
var events = ExtractEvents(evidence, componentPurl);
|
||||
|
||||
// Determine posture
|
||||
var (posture, explanation) = DeterminePosture(buckets, events, componentPurl);
|
||||
|
||||
return new RuntimeTimeline
|
||||
{
|
||||
FindingId = Guid.Empty, // Set by caller
|
||||
ComponentPurl = componentPurl,
|
||||
WindowStart = windowStart,
|
||||
WindowEnd = windowEnd,
|
||||
Posture = posture,
|
||||
PostureExplanation = explanation,
|
||||
Buckets = buckets,
|
||||
Events = events.OrderBy(e => e.Timestamp).ToList(),
|
||||
SessionDigests = evidence.SessionDigests.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private List<TimelineBucket> BuildBuckets(
|
||||
RuntimeEvidence evidence,
|
||||
string componentPurl,
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset end,
|
||||
TimeSpan bucketSize)
|
||||
{
|
||||
var buckets = new List<TimelineBucket>();
|
||||
var current = start;
|
||||
|
||||
while (current < end)
|
||||
{
|
||||
var bucketEnd = current + bucketSize;
|
||||
if (bucketEnd > end) bucketEnd = end;
|
||||
|
||||
var observations = evidence.Observations
|
||||
.Where(o => o.Timestamp >= current && o.Timestamp < bucketEnd)
|
||||
.ToList();
|
||||
|
||||
var byType = observations
|
||||
.GroupBy(o => ClassifyObservation(o))
|
||||
.Select(g => new ObservationTypeSummary
|
||||
{
|
||||
Type = g.Key,
|
||||
Count = g.Count()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var componentLoaded = observations.Any(o =>
|
||||
o.Type == "library_load" &&
|
||||
o.Path?.Contains(ExtractComponentName(componentPurl)) == true);
|
||||
|
||||
buckets.Add(new TimelineBucket
|
||||
{
|
||||
Start = current,
|
||||
End = bucketEnd,
|
||||
ObservationCount = observations.Count,
|
||||
ByType = byType,
|
||||
ComponentLoaded = componentLoaded,
|
||||
VulnerableCodeExecuted = componentLoaded ? DetectVulnerableExecution(observations) : null
|
||||
});
|
||||
|
||||
current = bucketEnd;
|
||||
}
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
private List<TimelineEvent> ExtractEvents(RuntimeEvidence evidence, string componentPurl)
|
||||
{
|
||||
var events = new List<TimelineEvent>();
|
||||
var componentName = ExtractComponentName(componentPurl);
|
||||
|
||||
foreach (var obs in evidence.Observations)
|
||||
{
|
||||
if (obs.Type == "library_load" && obs.Path?.Contains(componentName) == true)
|
||||
{
|
||||
events.Add(new TimelineEvent
|
||||
{
|
||||
Timestamp = obs.Timestamp,
|
||||
Type = TimelineEventType.ComponentLoaded,
|
||||
Description = $"Component {componentName} loaded",
|
||||
Significance = EventSignificance.High,
|
||||
EvidenceDigest = obs.Digest,
|
||||
Details = new Dictionary<string, string>
|
||||
{
|
||||
["path"] = obs.Path ?? "",
|
||||
["process_id"] = obs.ProcessId.ToString()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (obs.Type == "network" && obs.Port is > 0 and < 1024)
|
||||
{
|
||||
events.Add(new TimelineEvent
|
||||
{
|
||||
Timestamp = obs.Timestamp,
|
||||
Type = TimelineEventType.NetworkExposure,
|
||||
Description = $"Network exposure on port {obs.Port}",
|
||||
Significance = EventSignificance.Critical,
|
||||
EvidenceDigest = obs.Digest
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add capture session events
|
||||
foreach (var session in evidence.Sessions)
|
||||
{
|
||||
events.Add(new TimelineEvent
|
||||
{
|
||||
Timestamp = session.StartTime,
|
||||
Type = TimelineEventType.CaptureStarted,
|
||||
Description = $"Capture session started ({session.Platform})",
|
||||
Significance = EventSignificance.Low
|
||||
});
|
||||
|
||||
if (session.EndTime.HasValue)
|
||||
{
|
||||
events.Add(new TimelineEvent
|
||||
{
|
||||
Timestamp = session.EndTime.Value,
|
||||
Type = TimelineEventType.CaptureStopped,
|
||||
Description = "Capture session stopped",
|
||||
Significance = EventSignificance.Low
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private static (RuntimePosture posture, string explanation) DeterminePosture(
|
||||
List<TimelineBucket> buckets,
|
||||
List<TimelineEvent> events,
|
||||
string componentPurl)
|
||||
{
|
||||
if (buckets.Count == 0 || buckets.All(b => b.ObservationCount == 0))
|
||||
{
|
||||
return (RuntimePosture.Unknown, "No runtime observations collected");
|
||||
}
|
||||
|
||||
var componentLoadedCount = buckets.Count(b => b.ComponentLoaded);
|
||||
var totalBuckets = buckets.Count;
|
||||
|
||||
if (componentLoadedCount == 0)
|
||||
{
|
||||
return (RuntimePosture.Supports,
|
||||
$"Component {ExtractComponentName(componentPurl)} was not loaded during observation window");
|
||||
}
|
||||
|
||||
var hasNetworkExposure = events.Any(e => e.Type == TimelineEventType.NetworkExposure);
|
||||
var hasVulnerableExecution = buckets.Any(b => b.VulnerableCodeExecuted == true);
|
||||
|
||||
if (hasVulnerableExecution || hasNetworkExposure)
|
||||
{
|
||||
return (RuntimePosture.Contradicts,
|
||||
"Runtime evidence shows component is actively used and exposed");
|
||||
}
|
||||
|
||||
if (componentLoadedCount < totalBuckets / 2)
|
||||
{
|
||||
return (RuntimePosture.Inconclusive,
|
||||
$"Component loaded in {componentLoadedCount}/{totalBuckets} time periods");
|
||||
}
|
||||
|
||||
return (RuntimePosture.Supports,
|
||||
"Component loaded but no evidence of vulnerable code execution");
|
||||
}
|
||||
|
||||
private static ObservationType ClassifyObservation(RuntimeObservation obs)
|
||||
{
|
||||
return obs.Type switch
|
||||
{
|
||||
"library_load" or "dlopen" => ObservationType.LibraryLoad,
|
||||
"syscall" => ObservationType.Syscall,
|
||||
"network" or "connect" => ObservationType.NetworkConnection,
|
||||
"file" or "open" => ObservationType.FileAccess,
|
||||
"fork" or "exec" => ObservationType.ProcessSpawn,
|
||||
"symbol" => ObservationType.SymbolResolution,
|
||||
_ => ObservationType.LibraryLoad
|
||||
};
|
||||
}
|
||||
|
||||
private static string ExtractComponentName(string purl)
|
||||
{
|
||||
// Extract name from PURL like pkg:npm/lodash@4.17.21
|
||||
var parts = purl.Split('/');
|
||||
var namePart = parts.LastOrDefault() ?? purl;
|
||||
return namePart.Split('@').FirstOrDefault() ?? namePart;
|
||||
}
|
||||
|
||||
private static bool? DetectVulnerableExecution(List<RuntimeObservation> observations)
|
||||
{
|
||||
// Check if any observation indicates vulnerable code path execution
|
||||
return observations.Any(o =>
|
||||
o.Type == "symbol" ||
|
||||
o.Attributes?.ContainsKey("vulnerable_function") == true);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TimelineOptions
|
||||
{
|
||||
public DateTimeOffset? WindowStart { get; init; }
|
||||
public DateTimeOffset? WindowEnd { get; init; }
|
||||
public TimeSpan BucketSize { get; init; } = TimeSpan.FromHours(1);
|
||||
}
|
||||
|
||||
// Simplified runtime evidence types for Timeline API
|
||||
public sealed record RuntimeEvidence
|
||||
{
|
||||
public required DateTimeOffset FirstObservation { get; init; }
|
||||
public required DateTimeOffset LastObservation { get; init; }
|
||||
public required IReadOnlyList<RuntimeObservation> Observations { get; init; }
|
||||
public required IReadOnlyList<RuntimeSession> Sessions { get; init; }
|
||||
public required IReadOnlyList<string> SessionDigests { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RuntimeObservation
|
||||
{
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public string? Path { get; init; }
|
||||
public int? Port { get; init; }
|
||||
public int ProcessId { get; init; }
|
||||
public string? Digest { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Attributes { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RuntimeSession
|
||||
{
|
||||
public required DateTimeOffset StartTime { get; init; }
|
||||
public DateTimeOffset? EndTime { get; init; }
|
||||
public required string Platform { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user