namespace OCSentinelCli; internal sealed record AttackEvent { public DateTimeOffset Timestamp { get; init; } public string SourceIp { get; init; } = string.Empty; public long InstanceId { get; init; } public string Target { get; init; } = string.Empty; public string Username { get; init; } = string.Empty; public string Source { get; init; } = string.Empty; } internal sealed record AggregatedAttack { public string SourceIp { get; init; } = string.Empty; public int Count { get; init; } public DateTimeOffset FirstSeenLocal { get; init; } public DateTimeOffset LastSeenLocal { get; init; } public string RateLabel { get; init; } = string.Empty; public List Targets { get; init; } = []; public List Usernames { get; init; } = []; public List Sources { get; init; } = []; public static AggregatedAttack FromGroup(IGrouping group) { List ordered = group.OrderBy(static attack => attack.Timestamp).ToList(); DateTimeOffset firstSeen = ordered[0].Timestamp; DateTimeOffset lastSeen = ordered[^1].Timestamp; return new AggregatedAttack { SourceIp = group.Key, Count = ordered.Count, FirstSeenLocal = firstSeen, LastSeenLocal = lastSeen, RateLabel = FormatRate(ordered.Count, firstSeen, lastSeen), Targets = ordered.Select(static attack => attack.Target).Distinct(StringComparer.OrdinalIgnoreCase).Order().ToList(), Usernames = ordered.Select(static attack => attack.Username).Distinct(StringComparer.OrdinalIgnoreCase).Order().ToList(), Sources = ordered.Select(static attack => attack.Source).Distinct(StringComparer.OrdinalIgnoreCase).Order().ToList() }; } private static string FormatRate(int count, DateTimeOffset firstSeen, DateTimeOffset lastSeen) { int seconds = (int)Math.Max(1, (lastSeen - firstSeen).TotalSeconds); if (seconds <= 1) { return $"{count * 60}/min"; } if (seconds <= 60) { return $"{count * 60 / seconds}/min"; } if (seconds <= 3600) { return $"{count * 3600 / seconds}/hour"; } return $"{count * 86400 / seconds}/day"; } } internal sealed record ScanResult { public string SchemaVersion { get; init; } = "2.0"; public string MachineName { get; init; } = string.Empty; // Populated only for runs launched by NinjaOne automation. public NinjaOneContext NinjaOne { get; init; } = new(); public DateTimeOffset GeneratedAtLocal { get; init; } public DateTimeOffset GeneratedAtUtc { get; init; } public string ClientVersion { get; init; } = string.Empty; public int LookbackDays { get; init; } public int TotalEvents { get; init; } public int UniqueIpCount { get; init; } public string AlertState { get; init; } = "ok"; public string AlertReason { get; init; } = "No thresholds exceeded."; public string BaseAlertState { get; init; } = "ok"; public string BaseAlertReason { get; init; } = "No thresholds exceeded."; public VulnerabilityCorrelationSummary VulnerabilityCorrelation { get; init; } = new(); public ScanRuntimeMetadata Runtime { get; init; } = new(); public List Events { get; init; } = []; public List TopSources { get; init; } = []; public List Errors { get; init; } = []; } internal sealed record NinjaOneContext { public string OrganizationId { get; init; } = string.Empty; public string OrganizationName { get; init; } = string.Empty; public string MachineId { get; init; } = string.Empty; public string NodeId { get; init; } = string.Empty; public string LocationId { get; init; } = string.Empty; public string LocationName { get; init; } = string.Empty; } internal sealed record ScanRuntimeMetadata { public DateTimeOffset StartedAtUtc { get; init; } public DateTimeOffset FinishedAtUtc { get; init; } public bool UploadAttempted { get; init; } public bool UploadSucceeded { get; init; } public string UploadStatus { get; init; } = "not-attempted"; public int QueuedReportCount { get; init; } public DateTimeOffset? LastSuccessfulUploadUtc { get; init; } public string LastUploadError { get; init; } = string.Empty; } internal sealed record VulnerabilityFinding { public string DeviceName { get; init; } = string.Empty; public string CveId { get; init; } = string.Empty; public string Severity { get; init; } = string.Empty; public double? CvssScore { get; init; } public string Remediation { get; init; } = string.Empty; } internal sealed record VulnerabilityCorrelationSummary { public string SourcePath { get; init; } = string.Empty; public int TotalCount { get; init; } public int CriticalCount { get; init; } public int HighCvssCount { get; init; } public List Findings { get; init; } = []; public static VulnerabilityCorrelationSummary Empty(string sourcePath = "") { return new VulnerabilityCorrelationSummary { SourcePath = sourcePath }; } public static VulnerabilityCorrelationSummary FromFindings(string sourcePath, List findings) { int criticalCount = findings.Count(f => string.Equals(f.Severity, "critical", StringComparison.OrdinalIgnoreCase) || string.Equals(f.Severity, "high", StringComparison.OrdinalIgnoreCase)); int highCvssCount = findings.Count(f => f.CvssScore.HasValue && f.CvssScore.Value >= 8.0); return new VulnerabilityCorrelationSummary { SourcePath = sourcePath, TotalCount = findings.Count, CriticalCount = criticalCount, HighCvssCount = highCvssCount, Findings = findings }; } } internal sealed record CorrelationAssessment { public string FinalAlertState { get; init; } = "ok"; public string CorrelationReason { get; init; } = "No CVE correlation applied."; }