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; public string Service { get; init; } = string.Empty; public int? DestinationPort { get; init; } public string Endpoint { 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 List Services { get; init; } = []; public List DestinationPorts { get; init; } = []; public List Endpoints { 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(), Services = ordered.Select(static attack => attack.Service).Where(static service => !string.IsNullOrWhiteSpace(service)).Distinct(StringComparer.OrdinalIgnoreCase).Order().ToList(), DestinationPorts = ordered.Select(static attack => attack.DestinationPort).Where(static port => port.HasValue).Select(static port => port!.Value).Distinct().Order().ToList(), Endpoints = ordered.Select(static attack => attack.Endpoint).Where(static endpoint => !string.IsNullOrWhiteSpace(endpoint)).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; 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 ReportedEventCount { get; init; } public bool EventsTruncated { 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 RansomwareBetaSummary RansomwareBeta { 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 RansomwareBetaSummary { public bool Enabled { get; init; } public bool AlertingEnabled { get; init; } public string State { get; init; } = "disabled"; public string Reason { get; init; } = "Ransomware beta is disabled."; public int LookbackMinutes { get; init; } public List Signals { get; init; } = []; public List Sensors { get; init; } = []; public List SmbSessions { get; init; } = []; public RansomwareFileChurnSummary FileChurn { get; init; } = new(); } internal sealed record RansomwareSensorStatus { public string Name { get; init; } = string.Empty; public bool Enabled { get; init; } public bool Available { get; init; } public string State { get; init; } = "unknown"; public int EventCount { get; init; } } internal sealed record RansomwareFileChurnSummary { public bool Enabled { get; init; } public bool DataAvailable { get; init; } public bool IsTruncated { get; init; } public int WindowMinutes { get; init; } public int FileOperationCount { get; init; } public int DeleteOperationCount { get; init; } public int WriteOperationCount { get; init; } public int DistinctProcessCount { get; init; } public List TopProcesses { get; init; } = []; } internal sealed record RansomwareFileChurnProcess { public string Process { get; init; } = string.Empty; public int DeleteOperationCount { get; init; } public int WriteOperationCount { get; init; } } internal sealed record RansomwareSmbSession { public string ClientComputerName { get; init; } = string.Empty; public string ClientUserName { get; init; } = string.Empty; public long SessionId { get; init; } public long OpenFileCount { get; init; } } internal sealed record RansomwareSignal { public DateTimeOffset Timestamp { get; init; } public string Category { get; init; } = string.Empty; public string Confidence { get; init; } = string.Empty; public string Process { get; init; } = string.Empty; public string Source { get; init; } = string.Empty; public long EventId { get; init; } public string Evidence { get; init; } = string.Empty; } 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."; }