feat: 删除故障分类冗余选项 + 报告口径统一 + SSRF 非规范IP加固

- 故障大类删除'其他'、故障小类删除'无故障'(TicketForm/ProcessForm/TicketList)
- 周报按'真实故障+设备类型'分章节(去 ticketType 判断,新增 isRealFault 排除 无故障/误判/空)
- 月报口径与周报对齐,统一排除'误判'
- 监控设置 SSRF 加固:拒绝整数/十六进制/八进制/短式等非规范数字型 IP,防绕过内网黑名单
This commit is contained in:
gitadmin 2026-07-13 09:56:03 +08:00
parent f7bc9fd5fb
commit 7ea37afc75
6 changed files with 29 additions and 18 deletions

View File

@ -19,11 +19,22 @@ function isPrivateIPv4(host: string): boolean {
return false
}
// 拒绝非规范数字型 IP 写法(整数/十六进制/八进制/短式),防止绕过内网黑名单:
// net.isIPv4 只认标准点分十进制,但 socket 层会把 2130706433 / 0x7f000001 / 127.1 / 0177.0.0.1
// 等写法同样解析为回环/内网地址,必须在校验层拦截
function isNonCanonicalNumericHost(host: string): boolean {
if (/(^|\.)0x/i.test(host)) return true // 十六进制0x7f000001 / 0x7f.0.0.1
const segs = host.split('.')
if (segs.every((s) => /^\d+$/.test(s)) && !net.isIPv4(host)) return true // 整数型/短式/八进制数字 IP含前导零
return false
}
// 主机名格式校验(纵深防御):只允许合法主机名/IP 字符IPv6 字面量含 ':' 天然被拒),
// 拒绝 URL/路径/空格及内网 IPv4收窄 SSRF 面
// 拒绝 URL/路径/空格、非规范数字型 IP 及内网 IPv4收窄 SSRF 面
const isValidHost = (v: unknown): boolean => {
if (typeof v !== 'string' || v.length === 0 || v.length > 253) return false
if (!/^[a-zA-Z0-9.-]+$/.test(v)) return false
if (isNonCanonicalNumericHost(v)) return false
if (isPrivateIPv4(v)) return false
return true
}

View File

@ -27,7 +27,7 @@ interface ProcessFormProps {
}
const FAULT_CATEGORIES = [
'硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', '其他',
'硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障',
]
const TICKET_TYPES = ['OEM诊断', 'OEM维修']
@ -35,7 +35,7 @@ const TICKET_TYPES = ['OEM诊断', 'OEM维修']
const FAULT_SUBCATEGORIES = [
'GPU故障', 'CPU故障', '内存故障', '硬盘故障', '电源故障', '风扇故障',
'OS崩溃', '驱动故障', '软件崩溃', '配置错误', '网络丢包', '网络中断',
'存储掉盘', 'RAID故障', '无故障', '其他',
'存储掉盘', 'RAID故障', '其他',
]
const HANDLER_OPTIONS = ['腾讯', '图灵']

View File

@ -11,7 +11,7 @@ interface TicketFormProps {
}
const defaultFaultCategories = [
'硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', '其他',
'硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障',
]
const defaultTicketTypes = ['OEM诊断', 'OEM维修']
@ -19,7 +19,7 @@ const defaultTicketTypes = ['OEM诊断', 'OEM维修']
const defaultFaultSubcategories = [
'GPU故障', 'CPU故障', '内存故障', '硬盘故障', '电源故障', '风扇故障',
'OS崩溃', '驱动故障', '软件崩溃', '配置错误', '网络丢包', '网络中断',
'存储掉盘', 'RAID故障', '无故障', '其他',
'存储掉盘', 'RAID故障', '其他',
]
export default function TicketForm({ initialData, ticketId }: TicketFormProps) {

View File

@ -28,7 +28,7 @@ const statusMap: Record<string, { label: string; variant: 'default' | 'info' | '
}
const STATUS_OPTIONS = ['open', 'in_progress', 'resolved', 'closed']
const FAULT_CATEGORY_OPTIONS = ['硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障', '其他']
const FAULT_CATEGORY_OPTIONS = ['硬件故障', '软件故障', '网络故障', '存储故障', '电源故障', '无故障']
const COLUMNS = [
{ key: 'id', label: '工单号', sortable: true, filterable: true, filterType: 'text' as const },

View File

@ -111,7 +111,7 @@ export async function collectMonthlyReportData(
const dates = getDateRange(periodStart, periodEnd)
// 排除"无故障"分类和"其他"子分类(其他工单不计入节点在线数)
const monthFaults = tickets.filter(t =>
t.fault_category !== '无故障' && t.fault_subcategory !== '其他'
t.fault_category !== '无故障' && t.fault_category !== '误判' && t.fault_subcategory !== '其他'
)
const dailyStats: DailyOnlineStats[] = dates.map(date => {
@ -137,7 +137,7 @@ export async function collectMonthlyReportData(
})
// 5. 第二章:运营数据总览(仅 gpu/storage排除"无故障"和"其他"工单)
const gpuStorageTickets = tickets.filter(t => t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_subcategory !== '其他')
const gpuStorageTickets = tickets.filter(t => t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_category !== '误判' && t.fault_subcategory !== '其他')
const chapter2Map = new Map<string, Chapter2FaultItem[]>()
for (const t of gpuStorageTickets) {
const assignDate = t.assign_time.slice(0, 10)
@ -256,7 +256,7 @@ export async function collectMonthlyReportData(
/** 从采集数据构建 metadata JSON供 API 路由和 generate 函数共用) */
export function buildMonthlyMetadata(data: MonthlyReportData): string {
const gpuStorageTickets = data.tickets.filter(t =>
t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_subcategory !== '其他'
t.device_type !== 'other' && t.fault_category !== '无故障' && t.fault_category !== '误判' && t.fault_subcategory !== '其他'
)
const resolvedTickets = gpuStorageTickets.filter(t => !t.isOngoing)

View File

@ -92,20 +92,20 @@ export async function collectWeeklyReportData(
}
})
// 4. 按规则分三类
// GPU服务器故障OEM维修 + fault_category != '无故障' + device_type = 'gpu'
// 4. 按规则分三类(是否有真实故障 + 设备类型)
const isRealFault = (t: typeof entries[number]): boolean =>
t.faultCategory !== null && t.faultCategory !== '无故障' && t.faultCategory !== '误判'
// GPU服务器故障
const gpuFaultTickets = entries.filter(t =>
t.ticketType === 'OEM维修' && t.faultCategory !== '无故障' && t.deviceType === 'gpu'
isRealFault(t) && t.deviceType === 'gpu'
)
// 存储服务器故障
const storageFaultTickets = entries.filter(t =>
t.ticketType === 'OEM维修' && t.faultCategory !== '无故障' && t.deviceType === 'storage'
)
// 其他工单OEM诊断 + (OEM维修且fault_category='无故障')
const otherTickets = entries.filter(t =>
t.ticketType === 'OEM诊断' ||
(t.ticketType === 'OEM维修' && t.faultCategory === '无故障')
isRealFault(t) && t.deviceType === 'storage'
)
// 其他工单:无故障、误判、或 deviceType 未匹配
const otherTickets = entries.filter(t => !isRealFault(t))
// 5. 每日在线统计GPU + 存储分别统计)
const dates = getDateRange(periodStart, periodEnd)