mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-10-30 17:51:44 +08:00
fix:system info dialog and ai assistant
This commit is contained in:
+107
-14
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"SamWaf/common/zlog"
|
||||
"SamWaf/global"
|
||||
"SamWaf/model"
|
||||
"SamWaf/model/common/response"
|
||||
@@ -293,6 +294,75 @@ func (w *WafGPTApi) SaveGptConfigApi(c *gin.Context) {
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
// ============ 对话场景与系统提示词 ============
|
||||
// AI 助手是多入口共用的:日志详情点"AI分析"、OWASP 规则解读、右下角自由提问。
|
||||
// 以前不管从哪来都硬套"信息安全专家 + 风险等级/风险类型/风险说明"这套格式,
|
||||
// 结果随便问一句也被套成风险报告。这里按场景分开,前端发 scene,后端选提示词。
|
||||
const (
|
||||
GptSceneSecurityLog = "security_log" // 日志详情:分析这条请求的安全风险
|
||||
GptSceneOwaspRule = "owasp_rule" // OWASP CRS 规则解读
|
||||
GptSceneGeneral = "general" // 通用问答(默认)
|
||||
)
|
||||
|
||||
const gptSystemPromptSecurityLog = `你是一位信息安全专家,正在分析 SamWaf(Web 应用防火墙)记录到的一条 HTTP 请求。请判断这条请求是否存在攻击特征,输出如下格式:
|
||||
|
||||
风险等级: 0-100
|
||||
风险类型:某种注入,跨站等
|
||||
风险说明:对风险的阐释`
|
||||
|
||||
// gptDocRefs 官方资料索引。
|
||||
// 注意:对话走的是标准 chat/completions,没有联网/检索工具,模型打不开任何网页。
|
||||
// 所以这里不是"让它去搜",而是给它一份**准确的地址清单**,让它把用户导向官方文档和 issues,
|
||||
// 同时明确禁止它编造链接或假装读过页面——否则模型很容易凭空造出 /guide/XXX.html。
|
||||
const gptDocRefs = `
|
||||
|
||||
# 官方资料(你没有联网能力,只能引用下面列出的地址;禁止编造其它链接,也不要声称自己读过页面内容)
|
||||
- 文档站首页:https://doc.samwaf.com/
|
||||
- 功能文档地址格式:https://doc.samwaf.com/guide/<页面名>.html(英文界面把 /guide/ 换成 /en/guide/)
|
||||
- 可用的页面名(只能从这里挑,拿不准就只给文档站首页):
|
||||
Host 网站配置 | Rule 防护规则 | CC CC防护 | Owasp OWASP规则集 | AIDetection AI智能检测 |
|
||||
IPBlack IP黑名单 | IPWhite IP白名单 | IPGroup IP组 | ThreatIP 威胁情报IP订阅 | FirewallIPBlock 防火墙IP封禁 |
|
||||
UrlBlack URL限制访问 | UrlWhite URL白名单 | Sensitive 敏感词 | Ldp URL隐私防护 | Spider 爬虫识别 |
|
||||
SSL SSL证书管理 | CDNIP CDN回源IP | Tunnel 隧道防护 | CacheRule 缓存规则 | Application 应用管理 |
|
||||
AttackLog 风险日志 | VisitLog 访问日志 | Analysis 数据分析 | BlockingPage 自定义拦截页 |
|
||||
NotifyChannel 通知渠道 | NotifySubscription 通知订阅 | NotifyLog 通知日志 |
|
||||
HttpAuthBase 网站访问认证 | AccessConfig 认证配置 | AccessAccount 访问账号 | AccessSession 在线会话 | AccessAudit 认证审计 |
|
||||
Account 账号管理 | Otp 双因素认证 | SystemConfig 系统配置 | VpConfig 参数设置 | PrivateInfo 密钥管理 |
|
||||
OpenPlatform 开放平台 | SqlQuery SQL查询 | Task 任务管理 | BatchTask 批量任务 | DataRetention 数据保留策略 |
|
||||
IPLocation IP数据库 | FileManage 文件管理 | RuntimeInfo 运行信息 | SysLog 系统日志 | OneKeyMod 一键修改
|
||||
- 常见问题:https://doc.samwaf.com/faq/
|
||||
- 安装与升级:https://doc.samwaf.com/quickstart/ 、https://doc.samwaf.com/quickstart/Update.html
|
||||
- 已知问题与反馈:https://github.com/samwafgo/SamWaf/issues
|
||||
引导用户自己检索时,给出可直接点击的搜索地址:https://github.com/samwafgo/SamWaf/issues?q=is%3Aissue+关键词(多个关键词用 + 连接)
|
||||
|
||||
引用规则:
|
||||
1. 回答涉及某个具体功能时,在结尾附上对应文档页链接。
|
||||
2. 用户描述的现象像 bug、或你不确定当前版本是否支持,就直说"建议到 issues 搜一下,没有就提一个",并给出上面的 issues 搜索地址。
|
||||
3. 只给地址,不要转述你没有的页面内容,也不要编造文档里的章节名或配置项名称。`
|
||||
|
||||
const gptSystemPromptOwaspRule = `你是 OWASP ModSecurity 核心规则集(CRS)专家,正在帮用户读懂 SamWaf 里的一条 CRS 规则。请用简洁中文说明:
|
||||
1. 这条规则想拦什么攻击;
|
||||
2. 匹配条件的含义(变量、操作符、转换函数分别起什么作用);
|
||||
3. 常见误报场景;
|
||||
4. 如果确认是误报,建议怎么处理(加白名单、调整阈值/等级、或对该规则单独放行)。
|
||||
不要输出与规则无关的内容。` + gptDocRefs
|
||||
|
||||
const gptSystemPromptGeneral = `你是 SamWaf(一款开源轻量级 Web 应用防火墙)的智能助手,熟悉 Web 安全、网站防护、反向代理、SSL 证书、WAF 规则与告警等话题。
|
||||
请直接回答用户的问题:用与用户提问相同的语言(默认中文),简洁作答,必要时分点说明;不知道就说不知道,不要编造 SamWaf 不存在的功能。
|
||||
不要给回答强行套用固定模板或风险评分格式,除非用户明确要求。` + gptDocRefs
|
||||
|
||||
// gptSystemPromptByScene 按场景取系统提示词,未知场景一律按通用问答处理
|
||||
func gptSystemPromptByScene(scene string) string {
|
||||
switch strings.TrimSpace(strings.ToLower(scene)) {
|
||||
case GptSceneSecurityLog:
|
||||
return gptSystemPromptSecurityLog
|
||||
case GptSceneOwaspRule:
|
||||
return gptSystemPromptOwaspRule
|
||||
default:
|
||||
return gptSystemPromptGeneral
|
||||
}
|
||||
}
|
||||
|
||||
// 新增用于解析流式响应的结构体
|
||||
type StreamResponse struct {
|
||||
ID string `json:"id"`
|
||||
@@ -322,6 +392,20 @@ type TokenUsage struct {
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
// buildDeltaPayload 组装一条加密后的 SSE 消息体
|
||||
func buildDeltaPayload(content string, role string) (string, error) {
|
||||
encryptStr, _ := wafsec.AesEncrypt([]byte(content), global.GWAF_COMMUNICATION_KEY)
|
||||
msg := Delta{
|
||||
Content: encryptStr,
|
||||
Role: &role,
|
||||
}
|
||||
payload, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(payload), nil
|
||||
}
|
||||
|
||||
// SendDeltaMessage 发送信息
|
||||
func SendDeltaMessage(messageChan chan<- string, content string, role ...string) {
|
||||
// 设置默认角色为 assistant
|
||||
@@ -329,23 +413,25 @@ func SendDeltaMessage(messageChan chan<- string, content string, role ...string)
|
||||
if len(role) > 0 {
|
||||
r = role[0]
|
||||
}
|
||||
encryptStr, _ := wafsec.AesEncrypt([]byte(content), global.GWAF_COMMUNICATION_KEY)
|
||||
// 创建消息结构
|
||||
msg := Delta{
|
||||
Content: encryptStr,
|
||||
Role: &r,
|
||||
}
|
||||
|
||||
// 序列化并发送
|
||||
if payload, err := json.Marshal(msg); err == nil {
|
||||
messageChan <- string(payload)
|
||||
if payload, err := buildDeltaPayload(content, r); err == nil {
|
||||
messageChan <- payload
|
||||
}
|
||||
}
|
||||
func (w *WafGPTApi) ChatApi(c *gin.Context) {
|
||||
|
||||
var req request.WafGptSendReq
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
// 以前这里静默返回,前端表现是"点了发送但气泡一直空着",很难排查。
|
||||
// 注意 StreamMiddleware 已经把 Content-Type 定成 text/event-stream 了,
|
||||
// 这时候再 c.JSON 前端也当流读,所以错误也顺着 SSE 回,气泡里能直接看到。
|
||||
zlog.Error("GPT对话请求解析失败:" + err.Error())
|
||||
if payload, buildErr := buildDeltaPayload("对话请求解析失败:"+err.Error(), "assistant"); buildErr == nil {
|
||||
c.SSEvent("message", payload)
|
||||
}
|
||||
return
|
||||
}
|
||||
{
|
||||
// 创建一个取消信号通道,用于触发异常退出
|
||||
stopChan := make(chan bool)
|
||||
messageChan := make(chan string)
|
||||
@@ -355,11 +441,18 @@ func (w *WafGPTApi) ChatApi(c *gin.Context) {
|
||||
defer close(stopChan)
|
||||
defer close(messageChan)
|
||||
|
||||
// 构造基础消息数组
|
||||
// 未配置密钥时不去请求空地址,直接回一条可读提示,前端也会在打开助手时先检测并引导配置
|
||||
if !IsGPTConfigured() {
|
||||
SendDeltaMessage(messageChan, "尚未配置AI密钥,请点击【AI 参数设置】填写接口地址/模型/密钥后再使用。", "assistant")
|
||||
stopChan <- true
|
||||
return
|
||||
}
|
||||
|
||||
// 构造基础消息数组:系统提示词按场景取(日志分析/规则解读/通用问答)
|
||||
messages := []model.GptMessage{
|
||||
{
|
||||
Content: "你是一位信息安全专家,输出如下格式:\n\n风险等级: 0-100\n风险类型:某种注入,跨站等\n风险说明:对风险的阐释",
|
||||
Role: "user",
|
||||
Content: gptSystemPromptByScene(req.Scene),
|
||||
Role: "system",
|
||||
},
|
||||
}
|
||||
// 将History内容转换为消息并追加
|
||||
|
||||
@@ -109,6 +109,48 @@ func (w *WafSysInfoApi) SysVersionApi(c *gin.Context) {
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// SysRuntimeInfoApi 获取系统运行环境信息
|
||||
// @Summary 获取系统运行环境信息
|
||||
// @Description 返回操作系统类型/发行版及版本、内核、容器(docker/k8s)、虚拟化、运行时长与软件版本,便于反馈问题时定位环境
|
||||
// @Tags 系统信息
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=model.RuntimeSystemInfo} "获取成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /sysinfo/runtimeinfo [get]
|
||||
func (w *WafSysInfoApi) SysRuntimeInfoApi(c *gin.Context) {
|
||||
//探测过程全部容错:任何一项拿不到就留空,保证接口一定有返回
|
||||
detail := utils.GetOSDetail()
|
||||
|
||||
info := model.RuntimeSystemInfo{
|
||||
Version: global.GWAF_RELEASE_VERSION,
|
||||
VersionName: global.GWAF_RELEASE_VERSION_NAME,
|
||||
VersionRelease: global.GWAF_RELEASE,
|
||||
|
||||
OS: detail.GOOS,
|
||||
Arch: detail.GOARCH,
|
||||
GoVersion: detail.GoVersion,
|
||||
|
||||
OSName: detail.OSName,
|
||||
Platform: detail.Platform,
|
||||
PlatformVersion: detail.PlatformVersion,
|
||||
KernelVersion: detail.KernelVersion,
|
||||
KernelArch: detail.KernelArch,
|
||||
|
||||
IsWindows: detail.IsWindows,
|
||||
IsWin7Build: global.GWAF_RUNTIME_WIN7_VERSION == "true",
|
||||
IsWin7Kernel: detail.IsWin7Kernel,
|
||||
Container: detail.Container,
|
||||
InKubernetes: detail.InKubernetes,
|
||||
IsWSL: detail.IsWSL,
|
||||
Virtualization: detail.Virtualization,
|
||||
RunAsService: global.GWAF_RUNTIME_SERVER_TYPE,
|
||||
|
||||
SystemUptimeSeconds: int64(utils.GetSystemUptimeSeconds()),
|
||||
ProcessUptimeSeconds: int64(time.Since(global.GWAF_RUNTIME_PROCESS_START_TIME).Seconds()),
|
||||
}
|
||||
response.OkWithDetailed(info, "获取成功", c)
|
||||
}
|
||||
|
||||
func (w *WafSysInfoApi) CheckVersionApi(c *gin.Context) {
|
||||
if global.GWAF_RUNTIME_IS_UPDATETING == true {
|
||||
response.FailWithMessage("正在升级中...请在消息等待结果", c)
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"SamWaf/global"
|
||||
"SamWaf/model"
|
||||
"SamWaf/wafsec"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,6 +24,59 @@ type ComplexData struct {
|
||||
VersionRelease string `json:"version_release"`
|
||||
}
|
||||
|
||||
// 测试用例 SysRuntimeInfoApi:系统信息探测必须永远有返回,不能因为取不到某项而失败
|
||||
func TestSysRuntimeInfoApi(t *testing.T) {
|
||||
r := gin.Default()
|
||||
global.GWAF_RELEASE_VERSION = "v1.0.0"
|
||||
global.GWAF_RELEASE_VERSION_NAME = "20241028"
|
||||
r.GET("/api/v1/sysinfo/runtimeinfo", new(WafSysInfoApi).SysRuntimeInfoApi)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/api/v1/sysinfo/runtimeinfo", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("无法创建请求:%v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
t.Logf("响应体内容: %s", rec.Body.String())
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("期望的状态码:%d,实际状态码:%d", http.StatusOK, rec.Code)
|
||||
}
|
||||
var response WafCheckVersionResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("响应解析失败:%v", err)
|
||||
}
|
||||
if response.Code != 0 {
|
||||
t.Errorf("期望的响应码:0,实际:%v", response.Code)
|
||||
}
|
||||
// 响应体默认经通讯密钥加密,先解密再解析
|
||||
payload := response.Data
|
||||
var encrypted string
|
||||
if json.Unmarshal(response.Data, &encrypted) == nil {
|
||||
decrypted, err := wafsec.AesDecrypt(encrypted, global.GWAF_COMMUNICATION_KEY)
|
||||
if err != nil {
|
||||
t.Fatalf("响应解密失败:%v", err)
|
||||
}
|
||||
payload = decrypted
|
||||
}
|
||||
var info model.RuntimeSystemInfo
|
||||
if err := json.Unmarshal(payload, &info); err != nil {
|
||||
t.Fatalf("系统信息解析失败:%v", err)
|
||||
}
|
||||
if info.OS == "" || info.Arch == "" || info.GoVersion == "" {
|
||||
t.Errorf("编译相关信息不应为空:%+v", info)
|
||||
}
|
||||
if info.OSName == "" {
|
||||
t.Error("操作系统名称至少应有兜底值")
|
||||
}
|
||||
if info.Version != "v1.0.0" || info.VersionName != "20241028" {
|
||||
t.Errorf("软件版本信息不正确:%+v", info)
|
||||
}
|
||||
if info.ProcessUptimeSeconds < 0 {
|
||||
t.Errorf("程序运行时长不应为负:%v", info.ProcessUptimeSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用例 CheckVersionApi
|
||||
func TestCheckVersionApi(t *testing.T) {
|
||||
// 创建一个基于 Gin 的引擎
|
||||
|
||||
@@ -63,6 +63,8 @@ var (
|
||||
GWAF_RUNTIME_CURRENT_TUNNELPORT string = "" //当前程序所占用隧道端口
|
||||
GWAF_RUNTIME_CURRENT_EXPORT_DB_LOG_FILE_PATH string = "" //生成的日志导出文件路径
|
||||
|
||||
GWAF_RUNTIME_PROCESS_START_TIME time.Time = time.Now() //当前进程启动时间(包加载即赋值)
|
||||
|
||||
GWAF_RUNTIME_SSL_EXPIRE_CHECK bool = false //SSL过期检测是否正在运行
|
||||
GWAF_RUNTIME_SSL_SYNC_HOST bool = false //主机同步信息到过期检测是否正在运行
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,9 @@ package request
|
||||
|
||||
type WafGptSendReq struct {
|
||||
History [][]string `json:"history"` // 定义结构体
|
||||
// Scene 对话场景,决定用哪套系统提示词:
|
||||
// security_log 日志安全分析 / owasp_rule OWASP规则解读 / general 通用问答(默认)
|
||||
Scene string `json:"scene"`
|
||||
}
|
||||
|
||||
// WafGptRuleReq AI 生成自定义规则请求
|
||||
|
||||
@@ -8,3 +8,38 @@ type VersionInfo struct {
|
||||
VersionNew string `json:"version_new"`
|
||||
VersionDesc string `json:"version_desc"`
|
||||
}
|
||||
|
||||
// RuntimeSystemInfo 系统运行环境信息(供前端版本弹窗展示,便于用户反馈问题时定位环境)
|
||||
// 说明:所有字段都是"取不到就为空/零值",前端按需隐藏,不会因为某项探测失败而整体失败
|
||||
type RuntimeSystemInfo struct {
|
||||
//软件信息
|
||||
Version string `json:"version"` //软件版本Code 如 v1.0.0
|
||||
VersionName string `json:"version_name"` //软件版本 如 20241028
|
||||
VersionRelease string `json:"version_release"` //是否正式版 "true"/"false"
|
||||
|
||||
//编译信息
|
||||
OS string `json:"os"` //编译目标系统 windows/linux/darwin
|
||||
Arch string `json:"arch"` //编译目标架构 amd64/arm64
|
||||
GoVersion string `json:"go_version"` //编译器版本
|
||||
|
||||
//操作系统信息
|
||||
OSName string `json:"os_name"` //操作系统名称 如 Ubuntu 24.04.1 LTS / Microsoft Windows Server 2012 R2 Standard
|
||||
Platform string `json:"platform"` //平台 如 ubuntu
|
||||
PlatformVersion string `json:"platform_version"` //平台版本 如 24.04
|
||||
KernelVersion string `json:"kernel_version"` //内核版本
|
||||
KernelArch string `json:"kernel_arch"` //内核架构(真实机器架构)
|
||||
|
||||
//特殊环境标记(前端按条件展示)
|
||||
IsWindows bool `json:"is_windows"` //是否 Windows 系统
|
||||
IsWin7Build bool `json:"is_win7_build"` //当前二进制是否 Win7 专版
|
||||
IsWin7Kernel bool `json:"is_win7_kernel"` //当前系统是否 Win7/Win8/Server2008R2 内核
|
||||
Container string `json:"container"` //容器类型:空=非容器;docker/podman/containerd/lxc
|
||||
InKubernetes bool `json:"in_kubernetes"` //是否运行在 Kubernetes 中
|
||||
IsWSL bool `json:"is_wsl"` //是否运行在 WSL 中
|
||||
Virtualization string `json:"virtualization"` //虚拟化类型 如 kvm/vmware/hyperv,空=未识别
|
||||
RunAsService bool `json:"run_as_service"` //是否以系统服务方式启动
|
||||
|
||||
//运行时长
|
||||
SystemUptimeSeconds int64 `json:"system_uptime_seconds"` //操作系统已运行时长(秒),0=未取到
|
||||
ProcessUptimeSeconds int64 `json:"process_uptime_seconds"` //SamWaf 进程已运行时长(秒)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ func (receiver *WebSysInfoRouter) InitSysInfoRouter(group *gin.RouterGroup) {
|
||||
router := group.Group("")
|
||||
router.GET("/api/v1/sysinfo/version", api.SysVersionApi)
|
||||
router.GET("/api/v1/sysinfo/checkversion", api.CheckVersionApi)
|
||||
router.GET("/api/v1/sysinfo/runtimeinfo", api.SysRuntimeInfoApi)
|
||||
// N7:自升级/回滚属高危系统操作,仅系统管理员(或超管)可执行
|
||||
router.GET("/api/v1/sysinfo/update", middleware.RequireRole(enums.ROLE_SYSTEM_ADMIN), api.UpdateApi)
|
||||
router.GET("/api/v1/sysinfo/announcement", api.GetAnnouncementApi)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -606,15 +607,34 @@ func (receiver *WafStatService) StatHomeRumtimeSysinfo() []response2.WafNameValu
|
||||
Value: time.Unix(0,
|
||||
global.GWAF_MEASURE_PROCESS_DEQUEENGINE.ReadData()*int64(time.Millisecond)).Format("2006-01-02 15:04:05")})
|
||||
data = append(data, response2.WafNameValue{Name: "Goroutine数量", Value: fmt.Sprintf("%v", runtime.NumGoroutine())})
|
||||
osDetail := utils.GetOSDetail()
|
||||
data = append(data, response2.WafNameValue{Name: "系统类型", Value: fmt.Sprintf("%v", runtime.GOOS)})
|
||||
data = append(data, response2.WafNameValue{Name: "系统架构", Value: fmt.Sprintf("%v", runtime.GOARCH)})
|
||||
data = append(data, response2.WafNameValue{Name: "编译器版本", Value: fmt.Sprintf("%v", runtime.Version())})
|
||||
data = append(data, response2.WafNameValue{Name: "Win7内核", Value: func() string {
|
||||
if global.GWAF_RUNTIME_WIN7_VERSION == "true" {
|
||||
return "是"
|
||||
// 具体操作系统发行版及版本,如 Ubuntu 24.04.1 LTS / Microsoft Windows Server 2012 R2 Standard
|
||||
if osDetail.OSName != "" {
|
||||
data = append(data, response2.WafNameValue{Name: "操作系统", Value: osDetail.OSName})
|
||||
}
|
||||
if osDetail.KernelVersion != "" {
|
||||
kernel := osDetail.KernelVersion
|
||||
if osDetail.KernelArch != "" {
|
||||
kernel = kernel + " (" + osDetail.KernelArch + ")"
|
||||
}
|
||||
return "否"
|
||||
}()})
|
||||
data = append(data, response2.WafNameValue{Name: "内核版本", Value: kernel})
|
||||
}
|
||||
// 运行环境:容器/K8s/WSL/虚拟化,只有识别到才展示
|
||||
if env := describeRuntimeEnv(osDetail); env != "" {
|
||||
data = append(data, response2.WafNameValue{Name: "运行环境", Value: env})
|
||||
}
|
||||
data = append(data, response2.WafNameValue{Name: "编译器版本", Value: fmt.Sprintf("%v", runtime.Version())})
|
||||
// Win7内核只在 Windows 下才有意义,其它系统不展示
|
||||
if osDetail.IsWindows {
|
||||
data = append(data, response2.WafNameValue{Name: "Win7内核", Value: func() string {
|
||||
if global.GWAF_RUNTIME_WIN7_VERSION == "true" {
|
||||
return "是"
|
||||
}
|
||||
return "否"
|
||||
}()})
|
||||
}
|
||||
// 获取开机时间
|
||||
boottime, _ := host.BootTime()
|
||||
ntime := time.Now().Unix()
|
||||
@@ -631,6 +651,8 @@ func (receiver *WafStatService) StatHomeRumtimeSysinfo() []response2.WafNameValu
|
||||
|
||||
data = append(data, response2.WafNameValue{
|
||||
Name: "系统已运行时长", Value: fmt.Sprintf("%v 天 %v 时 %v 分 %v 秒", days, hours, minutes, seconds)})
|
||||
data = append(data, response2.WafNameValue{
|
||||
Name: "程序已运行时长", Value: utils.FormatDurationCN(int64(time.Since(global.GWAF_RUNTIME_PROCESS_START_TIME).Seconds()))})
|
||||
|
||||
data = append(data, response2.WafNameValue{Name: "软件版本", Value: fmt.Sprintf("%v", global.GWAF_RELEASE_VERSION_NAME)})
|
||||
data = append(data, response2.WafNameValue{Name: "软件版本Code", Value: fmt.Sprintf("%v", global.GWAF_RELEASE_VERSION)})
|
||||
@@ -643,3 +665,21 @@ func (receiver *WafStatService) StatHomeRumtimeSysinfo() []response2.WafNameValu
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// describeRuntimeEnv 把容器/K8s/WSL/虚拟化信息拼成一句可读描述,都没识别到就返回空字符串
|
||||
func describeRuntimeEnv(detail utils.OSDetail) string {
|
||||
var parts []string
|
||||
if detail.Container != "" {
|
||||
parts = append(parts, "容器("+detail.Container+")")
|
||||
}
|
||||
if detail.InKubernetes {
|
||||
parts = append(parts, "Kubernetes")
|
||||
}
|
||||
if detail.IsWSL {
|
||||
parts = append(parts, "WSL")
|
||||
}
|
||||
if detail.Virtualization != "" {
|
||||
parts = append(parts, "虚拟化("+detail.Virtualization+")")
|
||||
}
|
||||
return strings.Join(parts, " / ")
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ var registeredRoutes = []route{
|
||||
|
||||
// ── 系统信息 ───────────────────────────────────────────────────
|
||||
{"GET", "/api/v1/sysinfo/version", "获取版本信息"},
|
||||
{"GET", "/api/v1/sysinfo/runtimeinfo", "获取系统运行环境信息"},
|
||||
|
||||
// ── 系统日志 ───────────────────────────────────────────────────
|
||||
{"GET", "/api/v1/sys_log/list", "获取系统日志列表"},
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"SamWaf/common/zlog"
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
)
|
||||
|
||||
// OSDetail 运行环境详情:用于版本弹窗/运行参数展示,方便用户反馈问题时定位环境
|
||||
type OSDetail struct {
|
||||
GOOS string `json:"goos"` //编译目标系统 windows/linux/darwin
|
||||
GOARCH string `json:"goarch"` //编译目标架构 amd64/arm64
|
||||
GoVersion string `json:"go_version"` //编译器版本
|
||||
|
||||
OSName string `json:"os_name"` //友好的操作系统名称 如 Ubuntu 24.04.1 LTS / Microsoft Windows Server 2012 R2 Standard
|
||||
Platform string `json:"platform"` //平台 如 ubuntu / centos / Microsoft Windows 10 Pro
|
||||
PlatformFamily string `json:"platform_family"` //平台家族 如 debian / rhel
|
||||
PlatformVersion string `json:"platform_version"` //平台版本 如 24.04
|
||||
KernelVersion string `json:"kernel_version"` //内核版本
|
||||
KernelArch string `json:"kernel_arch"` //内核架构(真实机器架构,32位程序跑在64位系统时与 GOARCH 不同)
|
||||
|
||||
IsWindows bool `json:"is_windows"` //是否 Windows 系统
|
||||
IsWin7Kernel bool `json:"is_win7_kernel"` //当前系统是否是 Win7/Win8/Server2008R2 内核(6.1/6.2)
|
||||
|
||||
Container string `json:"container"` //容器类型:空=非容器;docker/podman/containerd/lxc/oci
|
||||
InKubernetes bool `json:"in_kubernetes"` //是否运行在 Kubernetes 中
|
||||
IsWSL bool `json:"is_wsl"` //是否运行在 WSL 中
|
||||
Virtualization string `json:"virtualization"` //虚拟化类型(仅 guest 角色时给出) 如 kvm/vmware/hyperv
|
||||
}
|
||||
|
||||
var (
|
||||
osDetailOnce sync.Once
|
||||
osDetailCache OSDetail
|
||||
|
||||
errHostInfoPanic = errors.New("读取主机信息发生异常")
|
||||
)
|
||||
|
||||
// GetOSDetail 获取运行环境详情(静态信息,进程内只探测一次)
|
||||
// 探测过程全部容错:任何一项取不到就留空,不会返回错误、也不会 panic
|
||||
func GetOSDetail() OSDetail {
|
||||
osDetailOnce.Do(func() {
|
||||
// 基础信息不依赖任何系统调用,先兜底填上,保证即使后续探测异常也有值可展示
|
||||
osDetailCache = OSDetail{
|
||||
GOOS: runtime.GOOS,
|
||||
GOARCH: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
IsWindows: runtime.GOOS == "windows",
|
||||
OSName: runtime.GOOS,
|
||||
}
|
||||
defer func() {
|
||||
// gopsutil 在部分定制/裁剪系统上可能 panic,这里兜住,保留已填充的基础信息
|
||||
if err := recover(); err != nil {
|
||||
zlog.Warn("获取系统运行环境信息异常", err)
|
||||
}
|
||||
}()
|
||||
osDetailCache = buildOSDetail()
|
||||
})
|
||||
return osDetailCache
|
||||
}
|
||||
|
||||
func buildOSDetail() OSDetail {
|
||||
detail := OSDetail{
|
||||
GOOS: runtime.GOOS,
|
||||
GOARCH: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
IsWindows: runtime.GOOS == "windows",
|
||||
IsWin7Kernel: safeBool(IsSupportedWindows7Version),
|
||||
}
|
||||
|
||||
if info, err := safeHostInfo(); err == nil && info != nil {
|
||||
detail.Platform = info.Platform
|
||||
detail.PlatformFamily = info.PlatformFamily
|
||||
detail.PlatformVersion = info.PlatformVersion
|
||||
detail.KernelVersion = info.KernelVersion
|
||||
detail.KernelArch = info.KernelArch
|
||||
// 只有作为 guest 运行时虚拟化类型才有意义,宿主机(host 角色)不展示
|
||||
if strings.EqualFold(info.VirtualizationRole, "guest") {
|
||||
detail.Virtualization = info.VirtualizationSystem
|
||||
}
|
||||
}
|
||||
|
||||
detail.OSName = buildOSName(detail)
|
||||
detail.Container = detectContainerRuntime()
|
||||
detail.InKubernetes = os.Getenv("KUBERNETES_SERVICE_HOST") != ""
|
||||
detail.IsWSL = detectWSL()
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
// GetSystemUptimeSeconds 获取系统已运行时长(秒),取不到返回 0,不会 panic
|
||||
func GetSystemUptimeSeconds() (seconds uint64) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
zlog.Warn("读取系统运行时长异常", r)
|
||||
seconds = 0
|
||||
}
|
||||
}()
|
||||
up, err := host.Uptime()
|
||||
if err != nil {
|
||||
zlog.Warn("读取系统运行时长失败:" + err.Error())
|
||||
return 0
|
||||
}
|
||||
return up
|
||||
}
|
||||
|
||||
// FormatDurationCN 把秒数格式化为 "x 天 x 时 x 分 x 秒"
|
||||
func FormatDurationCN(totalSeconds int64) string {
|
||||
if totalSeconds < 0 {
|
||||
totalSeconds = 0
|
||||
}
|
||||
seconds := totalSeconds % 60
|
||||
minutes := (totalSeconds / 60) % 60
|
||||
hours := (totalSeconds / 3600) % 24
|
||||
days := totalSeconds / 86400
|
||||
return fmt.Sprintf("%v 天 %v 时 %v 分 %v 秒", days, hours, minutes, seconds)
|
||||
}
|
||||
|
||||
// safeHostInfo 调用 gopsutil 获取主机信息,任何 panic 都转成错误返回
|
||||
func safeHostInfo() (info *host.InfoStat, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
zlog.Warn("读取主机信息异常", r)
|
||||
info = nil
|
||||
err = errHostInfoPanic
|
||||
}
|
||||
}()
|
||||
return host.Info()
|
||||
}
|
||||
|
||||
// safeBool 执行可能触发系统调用的判断函数,异常时返回 false
|
||||
func safeBool(fn func() bool) (result bool) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
zlog.Warn("系统信息判断异常", r)
|
||||
result = false
|
||||
}
|
||||
}()
|
||||
return fn()
|
||||
}
|
||||
|
||||
// buildOSName 拼一个尽量可读的操作系统名称
|
||||
func buildOSName(detail OSDetail) string {
|
||||
// Linux 优先取 /etc/os-release 的 PRETTY_NAME,如 "Ubuntu 24.04.1 LTS"、"Alpine Linux v3.20"
|
||||
if runtime.GOOS == "linux" {
|
||||
if pretty := readOSReleasePrettyName(); pretty != "" {
|
||||
return pretty
|
||||
}
|
||||
}
|
||||
name := strings.TrimSpace(detail.Platform)
|
||||
version := strings.TrimSpace(detail.PlatformVersion)
|
||||
if name == "" {
|
||||
name = runtime.GOOS
|
||||
}
|
||||
if version != "" && !strings.Contains(name, version) {
|
||||
name = name + " " + version
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// readOSReleasePrettyName 读取 /etc/os-release(或 /usr/lib/os-release) 中的 PRETTY_NAME
|
||||
func readOSReleasePrettyName() string {
|
||||
for _, path := range []string{"/etc/os-release", "/usr/lib/os-release"} {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
pretty := ""
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if !strings.HasPrefix(line, "PRETTY_NAME=") {
|
||||
continue
|
||||
}
|
||||
pretty = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), `"'`)
|
||||
break
|
||||
}
|
||||
f.Close()
|
||||
if pretty != "" {
|
||||
return pretty
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectContainerRuntime 粗略判断当前进程运行在什么容器里,非容器返回空字符串
|
||||
func detectContainerRuntime() string {
|
||||
if runtime.GOOS != "linux" {
|
||||
return ""
|
||||
}
|
||||
// docker 官方镜像会写入该标记文件
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
return "docker"
|
||||
}
|
||||
// podman/buildah 的标记文件
|
||||
if _, err := os.Stat("/run/.containerenv"); err == nil {
|
||||
return "podman"
|
||||
}
|
||||
// systemd-nspawn / podman / lxc 会注入 container 环境变量
|
||||
if env := strings.ToLower(strings.TrimSpace(os.Getenv("container"))); env != "" {
|
||||
return env
|
||||
}
|
||||
// 退回到 cgroup 特征判断(cgroup v1 常见;v2 下 /proc/1/cgroup 可能只有 "0::/")
|
||||
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
|
||||
content := strings.ToLower(string(data))
|
||||
switch {
|
||||
case strings.Contains(content, "docker"):
|
||||
return "docker"
|
||||
case strings.Contains(content, "kubepods"):
|
||||
return "containerd"
|
||||
case strings.Contains(content, "containerd"):
|
||||
return "containerd"
|
||||
case strings.Contains(content, "lxc"):
|
||||
return "lxc"
|
||||
}
|
||||
}
|
||||
// cgroup v2 场景:挂载信息里通常能看到 overlay 根文件系统与容器运行时目录
|
||||
if data, err := os.ReadFile("/proc/self/mountinfo"); err == nil {
|
||||
content := strings.ToLower(string(data))
|
||||
switch {
|
||||
case strings.Contains(content, "/docker/containers/"):
|
||||
return "docker"
|
||||
case strings.Contains(content, "/containers/storage/"):
|
||||
return "podman"
|
||||
case strings.Contains(content, "/kubelet/pods/"):
|
||||
return "containerd"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectWSL 判断是否运行在 WSL(Windows Subsystem for Linux)
|
||||
func detectWSL() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
if os.Getenv("WSL_DISTRO_NAME") != "" || os.Getenv("WSL_INTEROP") != "" {
|
||||
return true
|
||||
}
|
||||
data, err := os.ReadFile("/proc/version")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
content := strings.ToLower(string(data))
|
||||
return strings.Contains(content, "microsoft") || strings.Contains(content, "wsl")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 探测失败也必须给出兜底值,不能 panic、不能整体为空
|
||||
func TestGetOSDetail(t *testing.T) {
|
||||
detail := GetOSDetail()
|
||||
|
||||
if detail.GOOS != runtime.GOOS || detail.GOARCH != runtime.GOARCH {
|
||||
t.Errorf("编译信息不正确: %+v", detail)
|
||||
}
|
||||
if detail.GoVersion == "" {
|
||||
t.Error("编译器版本不应为空")
|
||||
}
|
||||
if detail.OSName == "" {
|
||||
t.Error("操作系统名称至少要有兜底值")
|
||||
}
|
||||
if detail.IsWindows != (runtime.GOOS == "windows") {
|
||||
t.Errorf("IsWindows 判断不正确: %v", detail.IsWindows)
|
||||
}
|
||||
// 非 Windows 不可能是 Win7 内核
|
||||
if runtime.GOOS != "windows" && detail.IsWin7Kernel {
|
||||
t.Error("非 Windows 系统不应判定为 Win7 内核")
|
||||
}
|
||||
// 非 Linux 不做容器/WSL 判定
|
||||
if runtime.GOOS != "linux" && (detail.Container != "" || detail.IsWSL) {
|
||||
t.Errorf("非 Linux 不应识别出容器/WSL: container=%v wsl=%v", detail.Container, detail.IsWSL)
|
||||
}
|
||||
|
||||
t.Logf("运行环境: %+v", detail)
|
||||
t.Logf("系统已运行: %v", FormatDurationCN(int64(GetSystemUptimeSeconds())))
|
||||
}
|
||||
|
||||
func TestFormatDurationCN(t *testing.T) {
|
||||
cases := []struct {
|
||||
seconds int64
|
||||
want string
|
||||
}{
|
||||
{0, "0 天 0 时 0 分 0 秒"},
|
||||
{-10, "0 天 0 时 0 分 0 秒"}, //负数按 0 处理,不能出现负值展示
|
||||
{59, "0 天 0 时 0 分 59 秒"},
|
||||
{3661, "0 天 1 时 1 分 1 秒"},
|
||||
{262145, "3 天 0 时 49 分 5 秒"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := FormatDurationCN(c.seconds); got != c.want {
|
||||
t.Errorf("FormatDurationCN(%v)=%v, 期望 %v", c.seconds, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user