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; }
/// <summary>
/// 软件 ID = 路径 + 文件签名发布者 + 进程名 (文件签名发布者 可无)
/// 软件 ID(Hash版本,用于关联和索引)
/// </summary>
public string AppId { get; set; }
/// <summary>
/// 原始软件 ID = 路径 + 文件签名发布者 + 进程名 (文件签名发布者 可无)
/// </summary>
public string OriginalAppId { get; set; }
/// <summary>
/// 名字
/// </summary>
@@ -51,6 +51,7 @@ public class NetVizorDbContext : IDisposable
CREATE TABLE IF NOT EXISTS AppInfo (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
AppId TEXT NOT NULL,
OriginalAppId TEXT,
Name TEXT NOT NULL,
Path TEXT NOT NULL,
Version TEXT,
@@ -340,6 +341,9 @@ public class NetVizorDbContext : IDisposable
Log.Information($"AppSetting表迁移完成,恢复了 {existingSettings.Count()} 条记录");
}
// 检查并迁移AppInfo表 - 添加OriginalAppId字段
await MigrateAppInfoTableAsync();
}
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)
{
try
@@ -48,8 +48,8 @@ public class AppInfoRepository : IAppInfoRepository
app.DeleteTime = 0;
const string sql = @"
INSERT INTO AppInfo (AppId, Name, Path, Version, Company, Base64Icon, Hash, InsertTime, UpdateTime, DeleteTime)
VALUES (@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, @OriginalAppId, @Name, @Path, @Version, @Company, @Base64Icon, @Hash, @InsertTime, @UpdateTime, @DeleteTime)";
var result = await _context.Connection.ExecuteAsync(sql, app);
@@ -67,7 +67,7 @@ public class AppInfoRepository : IAppInfoRepository
const string sql = @"
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
WHERE Id = @Id";
@@ -1,3 +1,4 @@
using System.Data;
using Data.Models;
using Dapper;
@@ -122,13 +123,27 @@ public class NetworkRepository : INetworkRepository
public async Task<IEnumerable<AppNetwork>> GetAppNetworkByTimeRangeAsync(string appId, long startTime, long endTime)
{
// 清理 appId,去掉多余的空白和引号
if (!string.IsNullOrEmpty(appId))
{
appId = appId.Trim().Trim('"');
}
const string sql = @"
SELECT * FROM AppNetwork
WHERE AppId = @AppId AND Timestamp >= @StartTime AND Timestamp <= @EndTime
ORDER BY Timestamp DESC";
SELECT * FROM AppNetwork
WHERE AppId = @AppId
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,
new { AppId = appId, StartTime = startTime, EndTime = endTime });
var parameters = new DynamicParameters();
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
@@ -141,7 +156,7 @@ public class NetworkRepository : INetworkRepository
string sql;
object parameters;
if (string.IsNullOrEmpty(networkCardGuid))
{
// 获取所有网卡数据
@@ -169,7 +184,7 @@ public class NetworkRepository : INetworkRepository
{
string sql;
object parameters;
if (string.IsNullOrEmpty(networkCardGuid))
{
// 获取所有网卡数据
@@ -339,7 +354,7 @@ public class NetworkRepository : INetworkRepository
string sql;
object parameters;
if (string.IsNullOrEmpty(networkCardGuid))
{
// 获取所有网卡数据
@@ -387,7 +402,7 @@ public class NetworkRepository : INetworkRepository
string sql;
object parameters;
if (string.IsNullOrEmpty(networkCardGuid))
{
// 获取所有网卡数据
@@ -435,7 +450,7 @@ public class NetworkRepository : INetworkRepository
string sql;
object parameters;
if (string.IsNullOrEmpty(networkCardGuid))
{
// 获取所有网卡数据
@@ -815,4 +830,4 @@ public class IpTrafficInfo
public int ConnectionCount { get; set; }
}
#endregion
#endregion
@@ -1,4 +1,6 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using Application.Utils;
using Common.Logger;
using Common.Utils;
@@ -29,8 +31,8 @@ public class NetworkDataCollectionService : IDisposable
// ETW数据缓存(只保留最活跃的前100条)
private readonly ConcurrentDictionary<string, AppNetworkData> _activeConnections = new();
// AppId生成工具
private readonly ConcurrentDictionary<int, string> _processAppIdCache = new();
// AppId生成工具 - 缓存原始AppId
private readonly ConcurrentDictionary<int, string> _processOriginalAppIdCache = new();
public NetworkDataCollectionService(DatabaseWriteManager writeManager)
{
@@ -133,7 +135,7 @@ public class NetworkDataCollectionService : IDisposable
try
{
// 生成或获取AppId
var appId = await GetOrGenerateAppIdAsync(app.ProcessId, app.ProgramInfo);
var (appId, originalAppId) = await GetOrGenerateAppIdAsync(app.ProcessId, app.ProgramInfo);
if (string.IsNullOrEmpty(appId))
{
@@ -151,6 +153,7 @@ public class NetworkDataCollectionService : IDisposable
var appInfo = new AppInfo
{
AppId = appId,
OriginalAppId = originalAppId,
Name = app.ProgramInfo.ProcessName ?? "Unknown",
Path = app.ProgramInfo.MainModulePath ?? "",
Version = app.ProgramInfo.Version ?? "",
@@ -236,12 +239,14 @@ public class NetworkDataCollectionService : IDisposable
/// <summary>
/// 获取或生成应用程序ID
/// </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
@@ -255,25 +260,30 @@ public class NetworkDataCollectionService : IDisposable
if (programInfo == null)
{
Log.Warning($"无法获取进程 {processId} 的信息");
return null;
return (null, null);
}
// 生成AppId: 路径 + 文件签名发布者 + 进程名
// 生成原始AppId: 路径 + 文件签名发布者 + 进程名
var path = programInfo.MainModulePath ?? "";
var publisher = programInfo.CompanyName ?? "";
var processName = programInfo.ProcessName ?? "Unknown";
var appId = $"{path}|{publisher}|{processName}";
var originalAppId = $"{path}|{publisher}|{processName}";
// 生成Hash版本的AppId(SHA256前16位)
var hashAppId = GenerateHashAppId(originalAppId);
// 缓存AppId
_processAppIdCache.TryAdd(processId, appId);
// 缓存原始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)
{
Log.Warning($"生成进程 {processId} 的AppId时出错", ex);
return null;
return (null, null);
}
}
@@ -286,7 +296,7 @@ public class NetworkDataCollectionService : IDisposable
{
ActiveNetworkInterfaces = _networkInterfaces.Count,
LastNetworkInterfaceUpdate = _lastNetworkInterfaceUpdate,
CachedProcessCount = _processAppIdCache.Count,
CachedProcessCount = _processOriginalAppIdCache.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()
{
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_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_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_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>
+207
View File
@@ -1572,6 +1572,178 @@ public partial class App : System.Windows.Application
#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
{
@@ -1754,6 +1926,41 @@ public partial class App : System.Windows.Application
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>