A:增加详细软件信息接口

This commit is contained in:
jimmy committed 2025-08-10 17:34:01 +08:00
1 parent 1732d279d9
commit 1f62e9cf51
7 files changed
+330 -29

No files matched your search

+6 -1
View File
@@ -9,10 +9,15 @@ public class AppInfo
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// 软件 ID = 路径 + 文件签名发布者 + 进程名 (文件签名发布者 可无) /// 软件 ID(Hash版本,用于关联和索引)
/// </summary> /// </summary>
public string AppId { get; set; } public string AppId { get; set; }
/// <summary>
/// 原始软件 ID = 路径 + 文件签名发布者 + 进程名 (文件签名发布者 可无)
/// </summary>
public string OriginalAppId { get; set; }
/// <summary> /// <summary>
/// 名字 /// 名字
/// </summary> /// </summary>
@@ -51,6 +51,7 @@ public class NetVizorDbContext : IDisposable
CREATE TABLE IF NOT EXISTS AppInfo ( CREATE TABLE IF NOT EXISTS AppInfo (
Id INTEGER PRIMARY KEY AUTOINCREMENT, Id INTEGER PRIMARY KEY AUTOINCREMENT,
AppId TEXT NOT NULL, AppId TEXT NOT NULL,
OriginalAppId TEXT,
Name TEXT NOT NULL, Name TEXT NOT NULL,
Path TEXT NOT NULL, Path TEXT NOT NULL,
Version TEXT, Version TEXT,
@@ -340,6 +341,9 @@ public class NetVizorDbContext : IDisposable
Log.Information($"AppSetting表迁移完成,恢复了 {existingSettings.Count()} 条记录"); Log.Information($"AppSetting表迁移完成,恢复了 {existingSettings.Count()} 条记录");
} }
// 检查并迁移AppInfo表 - 添加OriginalAppId字段
await MigrateAppInfoTableAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -347,6 +351,36 @@ public class NetVizorDbContext : IDisposable
} }
} }
private async Task MigrateAppInfoTableAsync()
{
try
{
var connection = Connection;
var columns = await GetTableColumnsAsync("AppInfo");
// 检查是否需要添加OriginalAppId字段
if (!columns.Contains("OriginalAppId"))
{
Log.Information("检测到AppInfo表缺少OriginalAppId字段,开始迁移...");
// 添加OriginalAppId字段
await connection.ExecuteAsync("ALTER TABLE AppInfo ADD COLUMN OriginalAppId TEXT");
// 为现有数据生成OriginalAppId(使用AppId作为默认值)
await connection.ExecuteAsync(@"
UPDATE AppInfo
SET OriginalAppId = COALESCE(Path || '|' || COALESCE(Company, '') || '|' || Name, AppId)
WHERE OriginalAppId IS NULL OR OriginalAppId = ''");
Log.Information("AppInfo表OriginalAppId字段添加完成");
}
}
catch (Exception ex)
{
Log.Warning("迁移AppInfo表时出错,将继续使用现有结构", ex);
}
}
private async Task<List<string>> GetTableColumnsAsync(string tableName) private async Task<List<string>> GetTableColumnsAsync(string tableName)
{ {
try try
@@ -48,8 +48,8 @@ public class AppInfoRepository : IAppInfoRepository
app.DeleteTime = 0; app.DeleteTime = 0;
const string sql = @" const string sql = @"
INSERT INTO AppInfo (AppId, Name, Path, Version, Company, Base64Icon, Hash, InsertTime, UpdateTime, DeleteTime) INSERT INTO AppInfo (AppId, OriginalAppId, Name, Path, Version, Company, Base64Icon, Hash, InsertTime, UpdateTime, DeleteTime)
VALUES (@AppId, @Name, @Path, @Version, @Company, @Base64Icon, @Hash, @InsertTime, @UpdateTime, @DeleteTime)"; VALUES (@AppId, @OriginalAppId, @Name, @Path, @Version, @Company, @Base64Icon, @Hash, @InsertTime, @UpdateTime, @DeleteTime)";
var result = await _context.Connection.ExecuteAsync(sql, app); var result = await _context.Connection.ExecuteAsync(sql, app);
@@ -67,7 +67,7 @@ public class AppInfoRepository : IAppInfoRepository
const string sql = @" const string sql = @"
UPDATE AppInfo UPDATE AppInfo
SET AppId = @AppId, Name = @Name, Path = @Path, Version = @Version, Company = @Company, SET AppId = @AppId, OriginalAppId = @OriginalAppId, Name = @Name, Path = @Path, Version = @Version, Company = @Company,
Base64Icon = @Base64Icon, Hash = @Hash, UpdateTime = @UpdateTime Base64Icon = @Base64Icon, Hash = @Hash, UpdateTime = @UpdateTime
WHERE Id = @Id"; WHERE Id = @Id";
@@ -1,3 +1,4 @@
using System.Data;
using Data.Models; using Data.Models;
using Dapper; using Dapper;
@@ -122,13 +123,27 @@ public class NetworkRepository : INetworkRepository
public async Task<IEnumerable<AppNetwork>> GetAppNetworkByTimeRangeAsync(string appId, long startTime, long endTime) public async Task<IEnumerable<AppNetwork>> GetAppNetworkByTimeRangeAsync(string appId, long startTime, long endTime)
{ {
// 清理 appId,去掉多余的空白和引号
if (!string.IsNullOrEmpty(appId))
{
appId = appId.Trim().Trim('"');
}
const string sql = @" const string sql = @"
SELECT * FROM AppNetwork SELECT * FROM AppNetwork
WHERE AppId = @AppId AND Timestamp >= @StartTime AND Timestamp <= @EndTime WHERE AppId = @AppId
ORDER BY Timestamp DESC"; AND CAST(Timestamp AS INTEGER) >= @StartTime
AND CAST(Timestamp AS INTEGER) <= @EndTime
ORDER BY CAST(Timestamp AS INTEGER) DESC";
return await _context.Connection.QueryAsync<AppNetwork>(sql, var parameters = new DynamicParameters();
new { AppId = appId, StartTime = startTime, EndTime = endTime }); parameters.Add("@AppId", appId, DbType.String); // 强制 TEXT 匹配
parameters.Add("@StartTime", startTime, DbType.Int64);
parameters.Add("@EndTime", endTime, DbType.Int64);
var appNetworks = await _context.Connection.QueryAsync<AppNetwork>(sql, parameters);
Console.WriteLine($"[Result] Found {appNetworks.Count()} rows.");
return appNetworks;
} }
#endregion #endregion
@@ -141,7 +156,7 @@ public class NetworkRepository : INetworkRepository
string sql; string sql;
object parameters; object parameters;
if (string.IsNullOrEmpty(networkCardGuid)) if (string.IsNullOrEmpty(networkCardGuid))
{ {
// 获取所有网卡数据 // 获取所有网卡数据
@@ -169,7 +184,7 @@ public class NetworkRepository : INetworkRepository
{ {
string sql; string sql;
object parameters; object parameters;
if (string.IsNullOrEmpty(networkCardGuid)) if (string.IsNullOrEmpty(networkCardGuid))
{ {
// 获取所有网卡数据 // 获取所有网卡数据
@@ -339,7 +354,7 @@ public class NetworkRepository : INetworkRepository
string sql; string sql;
object parameters; object parameters;
if (string.IsNullOrEmpty(networkCardGuid)) if (string.IsNullOrEmpty(networkCardGuid))
{ {
// 获取所有网卡数据 // 获取所有网卡数据
@@ -387,7 +402,7 @@ public class NetworkRepository : INetworkRepository
string sql; string sql;
object parameters; object parameters;
if (string.IsNullOrEmpty(networkCardGuid)) if (string.IsNullOrEmpty(networkCardGuid))
{ {
// 获取所有网卡数据 // 获取所有网卡数据
@@ -435,7 +450,7 @@ public class NetworkRepository : INetworkRepository
string sql; string sql;
object parameters; object parameters;
if (string.IsNullOrEmpty(networkCardGuid)) if (string.IsNullOrEmpty(networkCardGuid))
{ {
// 获取所有网卡数据 // 获取所有网卡数据
@@ -815,4 +830,4 @@ public class IpTrafficInfo
public int ConnectionCount { get; set; } public int ConnectionCount { get; set; }
} }
#endregion #endregion
@@ -1,4 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using Application.Utils; using Application.Utils;
using Common.Logger; using Common.Logger;
using Common.Utils; using Common.Utils;
@@ -29,8 +31,8 @@ public class NetworkDataCollectionService : IDisposable
// ETW数据缓存(只保留最活跃的前100条) // ETW数据缓存(只保留最活跃的前100条)
private readonly ConcurrentDictionary<string, AppNetworkData> _activeConnections = new(); private readonly ConcurrentDictionary<string, AppNetworkData> _activeConnections = new();
// AppId生成工具 // AppId生成工具 - 缓存原始AppId
private readonly ConcurrentDictionary<int, string> _processAppIdCache = new(); private readonly ConcurrentDictionary<int, string> _processOriginalAppIdCache = new();
public NetworkDataCollectionService(DatabaseWriteManager writeManager) public NetworkDataCollectionService(DatabaseWriteManager writeManager)
{ {
@@ -133,7 +135,7 @@ public class NetworkDataCollectionService : IDisposable
try try
{ {
// 生成或获取AppId // 生成或获取AppId
var appId = await GetOrGenerateAppIdAsync(app.ProcessId, app.ProgramInfo); var (appId, originalAppId) = await GetOrGenerateAppIdAsync(app.ProcessId, app.ProgramInfo);
if (string.IsNullOrEmpty(appId)) if (string.IsNullOrEmpty(appId))
{ {
@@ -151,6 +153,7 @@ public class NetworkDataCollectionService : IDisposable
var appInfo = new AppInfo var appInfo = new AppInfo
{ {
AppId = appId, AppId = appId,
OriginalAppId = originalAppId,
Name = app.ProgramInfo.ProcessName ?? "Unknown", Name = app.ProgramInfo.ProcessName ?? "Unknown",
Path = app.ProgramInfo.MainModulePath ?? "", Path = app.ProgramInfo.MainModulePath ?? "",
Version = app.ProgramInfo.Version ?? "", Version = app.ProgramInfo.Version ?? "",
@@ -236,12 +239,14 @@ public class NetworkDataCollectionService : IDisposable
/// <summary> /// <summary>
/// 获取或生成应用程序ID /// 获取或生成应用程序ID
/// </summary> /// </summary>
private async Task<string> GetOrGenerateAppIdAsync(int processId, Infrastructure.Models.ProgramInfo programInfo) /// <returns>返回元组(HashAppId, OriginalAppId)</returns>
private async Task<(string appId, string originalAppId)> GetOrGenerateAppIdAsync(int processId, Infrastructure.Models.ProgramInfo programInfo)
{ {
// 先检查缓存 // 先检查缓存
if (_processAppIdCache.TryGetValue(processId, out var cachedAppId)) if (_processOriginalAppIdCache.TryGetValue(processId, out var cachedOriginalAppId))
{ {
return cachedAppId; var cachedHashAppId = GenerateHashAppId(cachedOriginalAppId);
return (cachedHashAppId, cachedOriginalAppId);
} }
try try
@@ -255,25 +260,30 @@ public class NetworkDataCollectionService : IDisposable
if (programInfo == null) if (programInfo == null)
{ {
Log.Warning($"无法获取进程 {processId} 的信息"); Log.Warning($"无法获取进程 {processId} 的信息");
return null; return (null, null);
} }
// 生成AppId: 路径 + 文件签名发布者 + 进程名 // 生成原始AppId: 路径 + 文件签名发布者 + 进程名
var path = programInfo.MainModulePath ?? ""; var path = programInfo.MainModulePath ?? "";
var publisher = programInfo.CompanyName ?? ""; var publisher = programInfo.CompanyName ?? "";
var processName = programInfo.ProcessName ?? "Unknown"; var processName = programInfo.ProcessName ?? "Unknown";
var appId = $"{path}|{publisher}|{processName}"; var originalAppId = $"{path}|{publisher}|{processName}";
// 生成Hash版本的AppId(SHA256前16位)
var hashAppId = GenerateHashAppId(originalAppId);
// 缓存AppId // 缓存原始AppId
_processAppIdCache.TryAdd(processId, appId); _processOriginalAppIdCache.TryAdd(processId, originalAppId);
return appId; Log.Debug($"生成AppId: {hashAppId} (原始: {originalAppId.Substring(0, Math.Min(50, originalAppId.Length))}...)");
return (hashAppId, originalAppId);
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Warning($"生成进程 {processId} 的AppId时出错", ex); Log.Warning($"生成进程 {processId} 的AppId时出错", ex);
return null; return (null, null);
} }
} }
@@ -286,7 +296,7 @@ public class NetworkDataCollectionService : IDisposable
{ {
ActiveNetworkInterfaces = _networkInterfaces.Count, ActiveNetworkInterfaces = _networkInterfaces.Count,
LastNetworkInterfaceUpdate = _lastNetworkInterfaceUpdate, LastNetworkInterfaceUpdate = _lastNetworkInterfaceUpdate,
CachedProcessCount = _processAppIdCache.Count, CachedProcessCount = _processOriginalAppIdCache.Count,
ActiveConnectionsCount = _activeConnections.Count ActiveConnectionsCount = _activeConnections.Count
}; };
} }
@@ -333,6 +343,35 @@ public class NetworkDataCollectionService : IDisposable
} }
} }
/// <summary>
/// 生成Hash版本的AppId
/// </summary>
/// <param name="originalAppId">原始AppId</param>
/// <returns>Hash版本的AppId(SHA256前16位)</returns>
private static string GenerateHashAppId(string originalAppId)
{
if (string.IsNullOrEmpty(originalAppId))
{
return "unknown";
}
try
{
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(originalAppId));
// 取前8字节(16位十六进制字符)
var hashHex = Convert.ToHexString(hashBytes[..8]).ToLowerInvariant();
return hashHex;
}
catch
{
// 如果Hash生成失败,使用原始AppId的哈希码
return Math.Abs(originalAppId.GetHashCode()).ToString("x8");
}
}
public void Dispose() public void Dispose()
{ {
Log.Information("正在关闭网络数据收集服务..."); Log.Information("正在关闭网络数据收集服务...");
@@ -19,6 +19,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6f90c6c13479b8c2a5be98f3d75dfc3bd885a055652d8a32904ca2448132949e_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6f90c6c13479b8c2a5be98f3d75dfc3bd885a055652d8a32904ca2448132949e_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpListener_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F28febf18c0c64165a91fffc7144abdc786908_003F39_003Fa4eb9e55_003FHttpListener_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpListener_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F28febf18c0c64165a91fffc7144abdc786908_003F39_003Fa4eb9e55_003FHttpListener_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIcon_002Ecs_002Fl_003AC_0021_003FUsers_003Fcharlatans_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F946b37ae1f1648b7a334a81e4b6ce11c64330_003F21_003F67e31def_003FIcon_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIcon_002Ecs_002Fl_003AC_0021_003FUsers_003Fcharlatans_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F946b37ae1f1648b7a334a81e4b6ce11c64330_003F21_003F67e31def_003FIcon_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDbConnection_002Ecs_002Fl_003AC_0021_003FUsers_003Fcharlatans_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F190b4d3ba5554d989564edc3fb49234a2ba910_003Fd1_003F982e4f92_003FIDbConnection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5aeb6167299d4cfba30c86624130edbcc90938_003F41_003F0f283a8f_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5aeb6167299d4cfba30c86624130edbcc90938_003F41_003F0f283a8f_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGrouping_00602_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9abdf068a8774c97a3780c88b63c27b884928_003Fb6_003Ff1d12dc1_003FIGrouping_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGrouping_00602_002Ecs_002Fl_003AC_0021_003FUsers_003Fyllyo_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9abdf068a8774c97a3780c88b63c27b884928_003Fb6_003Ff1d12dc1_003FIGrouping_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AINetFwRule_002Ecs_002Fl_003AC_0021_003FUsers_003Fcharlatans_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd2ed9a92e29d4a0f91826359e93c440c4c00_003F5c_003F4c2f5fe4_003FINetFwRule_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AINetFwRule_002Ecs_002Fl_003AC_0021_003FUsers_003Fcharlatans_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd2ed9a92e29d4a0f91826359e93c440c4c00_003F5c_003F4c2f5fe4_003FINetFwRule_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+207
View File
@@ -1572,6 +1572,178 @@ public partial class App : System.Windows.Application
#endregion #endregion
#region 应用详细网络分析API
// 获取应用详细网络分析数据
server.Get("/api/apps/network-analysis", async (context) =>
{
try
{
var appId = context.QueryParams["appId"];
var timeRange = context.QueryParams["timeRange"] ?? "1day";
if (string.IsNullOrEmpty(appId))
{
context.Response.StatusCode = 400;
await context.Response.WriteJsonAsync(new ResponseModel<object>
{
Success = false,
Message = "缺少必需参数 appId"
});
return;
}
// 计算时间范围
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
long startTime = timeRange switch
{
"1hour" => DateTimeOffset.UtcNow.AddHours(-1).ToUnixTimeSeconds(),
"1day" => DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds(),
"7days" => DateTimeOffset.UtcNow.AddDays(-7).ToUnixTimeSeconds(),
"30days" => DateTimeOffset.UtcNow.AddDays(-30).ToUnixTimeSeconds(),
_ => DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds()
};
// 获取应用基本信息
var appInfo = await DatabaseManager.Instance.AppInfos.GetAppByAppIdAsync(appId);
// 获取网络数据
var networkData = await DatabaseManager.Instance.Networks.GetAppNetworkByTimeRangeAsync(appId, startTime, now);
var networkList = networkData.ToList();
// 计算基本统计
var totalUpload = networkList.Sum(x => x.UploadBytes);
var totalDownload = networkList.Sum(x => x.DownloadBytes);
var totalConnections = networkList.Count;
// 获取前30个流量最高的连接
var topConnections = networkList
.Where(x => x.UploadBytes + x.DownloadBytes > 0) // 只统计有流量的连接
.GroupBy(x => new { x.LocalIP, x.LocalPort, x.RemoteIP, x.RemotePort, x.Protocol })
.Select(g => new
{
localIP = g.Key.LocalIP,
localPort = g.Key.LocalPort,
remoteIP = g.Key.RemoteIP,
remotePort = g.Key.RemotePort,
protocol = g.Key.Protocol,
totalUpload = g.Sum(x => x.UploadBytes),
totalDownload = g.Sum(x => x.DownloadBytes),
totalTraffic = g.Sum(x => x.UploadBytes + x.DownloadBytes),
connectionCount = g.Count(),
firstSeen = DateTimeOffset.FromUnixTimeSeconds(g.Min(x => x.Timestamp)).ToString("yyyy-MM-dd HH:mm:ss"),
lastSeen = DateTimeOffset.FromUnixTimeSeconds(g.Max(x => x.Timestamp)).ToString("yyyy-MM-dd HH:mm:ss")
})
.OrderByDescending(x => x.totalTraffic)
.Take(30)
.ToList();
// 计算协议占比
var protocolStats = networkList
.Where(x => x.UploadBytes + x.DownloadBytes > 0)
.GroupBy(x => x.Protocol.ToUpper())
.Select(g => new
{
protocol = g.Key,
connectionCount = g.Count(),
totalTraffic = g.Sum(x => x.UploadBytes + x.DownloadBytes),
percentage = Math.Round((double)g.Count() / networkList.Where(x => x.UploadBytes + x.DownloadBytes > 0).Count() * 100, 2)
})
.OrderByDescending(x => x.totalTraffic)
.ToList();
// 获取时间趋势数据(按小时分组)
var timeTrends = networkList
.GroupBy(x => x.Timestamp / 3600 * 3600) // 按小时分组
.Select(g => new
{
timestamp = g.Key,
timeStr = DateTimeOffset.FromUnixTimeSeconds(g.Key).ToString("yyyy-MM-dd HH:mm"),
upload = g.Sum(x => x.UploadBytes),
download = g.Sum(x => x.DownloadBytes),
connections = g.Count()
})
.OrderBy(x => x.timestamp)
.ToList();
// 端口分析
var portAnalysis = networkList
.Where(x => x.UploadBytes + x.DownloadBytes > 0)
.GroupBy(x => x.RemotePort)
.Select(g => new
{
port = g.Key,
serviceName = GetPortServiceName(g.Key),
connectionCount = g.Count(),
totalTraffic = g.Sum(x => x.UploadBytes + x.DownloadBytes),
protocols = g.Select(x => x.Protocol).Distinct().ToList()
})
.OrderByDescending(x => x.totalTraffic)
.Take(20)
.ToList();
// 构建返回数据
var result = new
{
// 应用基本信息
appInfo = new
{
appId = appId,
name = appInfo?.Name ?? "Unknown Application",
company = appInfo?.Company ?? "",
version = appInfo?.Version ?? "",
path = appInfo?.Path ?? "",
icon = appInfo?.Base64Icon ?? "",
hash = appInfo?.Hash ?? ""
},
// 统计摘要
summary = new
{
timeRange = timeRange,
startTime = DateTimeOffset.FromUnixTimeSeconds(startTime).ToString("yyyy-MM-dd HH:mm:ss"),
endTime = DateTimeOffset.FromUnixTimeSeconds(now).ToString("yyyy-MM-dd HH:mm:ss"),
totalUpload = totalUpload,
totalDownload = totalDownload,
totalTraffic = totalUpload + totalDownload,
totalConnections = totalConnections,
uniqueRemoteIPs = networkList.Select(x => x.RemoteIP).Distinct().Count(),
uniqueRemotePorts = networkList.Select(x => x.RemotePort).Distinct().Count()
},
// 前30个流量最高的连接
topConnections = topConnections,
// 协议占比统计
protocolStats = protocolStats,
// 时间趋势
timeTrends = timeTrends,
// 端口分析
portAnalysis = portAnalysis
};
await context.Response.WriteJsonAsync(new ResponseModel<object>
{
Success = true,
Data = result,
Message = "获取应用网络分析数据成功"
});
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
await context.Response.WriteJsonAsync(new ResponseModel<object>
{
Success = false,
Message = $"获取应用网络分析数据失败: {ex.Message}"
});
}
});
#endregion
// 启动服务器 // 启动服务器
try try
{ {
@@ -1754,6 +1926,41 @@ public partial class App : System.Windows.Application
return points; return points;
} }
/// <summary>
/// 根据端口号获取服务名称
/// </summary>
/// <param name="port">端口号</param>
/// <returns>服务名称</returns>
private static string GetPortServiceName(int port)
{
return port switch
{
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
123 => "NTP",
143 => "IMAP",
443 => "HTTPS",
993 => "IMAPS",
995 => "POP3S",
1433 => "SQL Server",
3306 => "MySQL",
3389 => "RDP",
5432 => "PostgreSQL",
6379 => "Redis",
8080 => "HTTP-Alt",
8443 => "HTTPS-Alt",
_ when port >= 1024 && port <= 5000 => "User Port",
_ when port > 5000 && port < 32768 => "Service Port",
_ when port >= 32768 => "Dynamic Port",
_ => "Unknown"
};
}
} }
/// <summary> /// <summary>