// ==UserScript== // @name 科来告警详情 - 一键提取并复制 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 在告警详情弹窗标题旁添加"复制"按钮,把详情转成"安全事件管理模块"新增表单的字段格式 // @match *://172.21.201.170/* // @match *://*/bfc/* // @grant GM_setClipboard // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const log = (...a) => console.log('%c[告警提取]', 'color:#1677FF;font-weight:bold', ...a); const warn = (...a) => console.warn('%c[告警提取]', 'color:#E6A23C;font-weight:bold', ...a); // ============ 工具 ============ function copyText(text) { if (typeof GM_setClipboard === 'function') { GM_setClipboard(text, 'text'); return true; } // 降级:用临时 textarea const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;'; document.body.appendChild(ta); ta.select(); let ok = false; try { ok = document.execCommand('copy'); } catch (e) {} document.body.removeChild(ta); return ok; } function toast(msg, color = '#1677FF') { const t = document.createElement('div'); t.textContent = msg; t.style.cssText = `position:fixed;top:20px;left:50%;transform:translateX(-50%); background:${color};color:#fff;padding:10px 20px;border-radius:4px; z-index:2147483647;font-size:14px;box-shadow:0 2px 12px rgba(0,0,0,.25);`; document.body.appendChild(t); setTimeout(() => t.remove(), 2500); } /** 从表格 label 找值 */ function findFieldValue(root, labelText) { const norm = s => String(s||'').replace(/[::\s]/g, '').trim(); const target = norm(labelText); const labels = root.querySelectorAll('.ant-descriptions-item-label'); for (const lb of labels) { if (norm(lb.textContent) === target) { const item = lb.closest('.ant-descriptions-item'); if (!item) continue; const val = item.querySelector('.ant-descriptions-item-content'); if (!val) continue; // 排除"复制"按钮的文本 const clone = val.cloneNode(true); clone.querySelectorAll('button').forEach(b => b.remove()); return clone.textContent.trim(); } } return ''; } /** 从概要区找"资产名称"的值 */ function findAssetName(root) { const norm = s => String(s || '').replace(/[::\s]/g, '').trim(); const all = root.querySelectorAll('span, div, label'); for (const el of all) { if (norm(el.textContent) === '资产名称') { const parent = el.parentElement; if (!parent) continue; // 情况 A:label 和 value 都是 parent 的直接子元素 for (const c of parent.children) { if (c === el) continue; const t = (c.textContent || '').trim(); if (t && !/^资产名称/.test(t)) return t; } // 情况 B:value 是 parent 的下一个兄弟 const next = parent.nextElementSibling; if (next) { const t = (next.textContent || '').trim(); if (t && !/^资产名称/.test(t)) return t; } } } return ''; } /** 资产名称 -> 系统名 */ function mapAssetToSystem(assetName) { if (!assetName) return ''; if (/邮箱|云邮|mail/i.test(assetName)) return '云邮系统'; if (/云盘/.test(assetName)) return '云盘系统'; if (/看家/.test(assetName)) return '看家系统'; if (/智家/.test(assetName)) return '智家系统'; return ''; } /** 由标题推断攻击类型 */ function guessAttackType(title) { const t = title || ''; if (/暴力破解|爆破/.test(t)) return '暴力破解'; if (/命令注入|命令执行/.test(t)) return '命令执行'; if (/漏洞利用|CVE-|未授权访问/.test(t)) return '漏洞利用'; if (/扫描探测|扫描行为/.test(t)) return '扫描探测'; if (/弱口令/.test(t)) return '弱口令'; if (/木马|后门|病毒/.test(t)) return '病毒木马后门'; if (/挖矿/.test(t)) return '挖矿告警'; if (/恶意域名/.test(t)) return '恶意域名'; if (/信息泄露/.test(t)) return '信息泄露'; if (/蜜罐/.test(t)) return '蜜罐告警'; if (/可疑登录/.test(t)) return '可疑登录'; if (/可疑进程/.test(t)) return '可疑进程'; return '其他事件'; } /** 由"攻击状态"推断"是否攻击成功" */ function guessAttackSuccess(status) { if (!status) return '未知'; if (/攻击成功/.test(status)) return '成功'; if (/企图|尝试|未成功/.test(status)) return '未成功'; return '未知'; } /** 由国家/地区字段判断源IP类型 */ function guessIpType(country) { if (!country) return ''; if (/局域网|内网|私有/.test(country)) return '内网IP'; if (/未知/.test(country)) return '未知'; return '互联网IP'; } /** 由攻击方向判断需处置IP类型 */ function guessDisposalIpType(direction) { if (!direction) return ''; if (/外\s*->\s*内|外->内/.test(direction)) return '源'; if (/内\s*->\s*外|内->外/.test(direction)) return '源'; if (/内\s*->\s*内|内->内/.test(direction)) return '源'; return '源'; } /** 清洗值:把 "-"、"未知"、"null" 等视为空 */ function clean(v) { if (v == null) return ''; const s = String(v).trim(); if (!s) return ''; if (/^(-|—|未知|null|undefined|N\/A|无)$/i.test(s)) return ''; return s; } /** 格式化 IP:端口,缺一不可带 ":" */ function formatIpPort(ip, port) { const i = clean(ip); const p = clean(port); if (!i && !p) return ''; if (i && p) return `${i}:${p}`; return i || p; } // ============ 提取数据 ============ function extractData(dialogEl) { // ---- 抓原始字段 ---- const titleEl = dialogEl.querySelector('.alarm-name'); const alarmName = titleEl ? titleEl.textContent.trim() : ''; const getField = (label) => findFieldValue(dialogEl, label); const level = getField('告警级别'); const attackStatus = getField('攻击状态'); const attackIp = getField('攻击者IP'); const victimIp = getField('受害者IP'); const srcCountry = getField('攻击者国家/地区'); const srcPort = getField('源端口'); const dstPort = getField('目的端口'); const protocol = getField('协议'); const direction = getField('攻击方向'); const triggerTime = getField('触发时间'); const triggerCond = getField('触发条件'); const logId = getField('日志ID'); // ---- 抓资产名称并映射为系统名 ---- const assetName = findAssetName(dialogEl); const systemName = mapAssetToSystem(assetName); // ---- 推导字段 ---- // 事件标题:受害者 IP 换成系统名;抓不到系统名时退回用 IP const victimDisplay = systemName || clean(victimIp) || ''; const attackIpClean = clean(attackIp); let eventTitle = alarmName; if (attackIpClean && victimDisplay) { eventTitle = `${victimDisplay} 受到 ${attackIpClean} 的 ${alarmName} 攻击`; } else if (attackIpClean) { eventTitle = `${attackIpClean} 的 ${alarmName} 攻击`; } else if (victimDisplay) { eventTitle = `${victimDisplay} 受到 ${alarmName} 攻击`; } // 事件描述 const descParts = []; if (alarmName) descParts.push(`告警名称:${alarmName}`); if (logId) descParts.push(`日志ID:${logId}`); if (attackIp) descParts.push(`攻击IP:${clean(attackIp)}`); if (victimIp) descParts.push(`受害者IP:${clean(victimIp)}`); if (assetName) descParts.push(`资产名称:${assetName}`); if (direction) descParts.push(`攻击方向:${clean(direction)}`); if (protocol) descParts.push(`协议:${clean(protocol)}`); if (triggerCond) descParts.push(`触发条件:${clean(triggerCond)}`); const eventDesc = descParts.join(';'); const attackSuccess = guessAttackSuccess(attackStatus); const attackType = guessAttackType(alarmName); const srcIpType = guessIpType(srcCountry); const disposalIpType = guessDisposalIpType(direction); // 源国家(排除"局域网"这种非国家值) const srcCountryValue = (clean(srcCountry) && !/局域网/.test(srcCountry)) ? clean(srcCountry) : ''; // 目的国家固定"中国" const dstCountryValue = '中国'; // ---- 组装输出(每个字段必输出,抓不到就空)---- const lines = []; lines.push(`事件标题: ${clean(eventTitle)}`); lines.push(`事件描述: ${clean(eventDesc)}`); lines.push(`风险级别: ${clean(level)}`); lines.push(`是否攻击成功: ${clean(attackSuccess)}`); lines.push(`攻击类型: ${clean(attackType)}`); lines.push(`攻击协议: ${clean(protocol)}`); lines.push(`告警时间: ${clean(triggerTime)}`); lines.push(`告警频次: 1`); lines.push(`源IP类型: ${clean(srcIpType)}`); lines.push(`源国家: ${srcCountryValue}`); lines.push(`源省份: `); lines.push(`源IP端口: ${formatIpPort(attackIp, srcPort)}`); lines.push(`目的国家: ${dstCountryValue}`); lines.push(`目的省份: 北京`); lines.push(`目的IP端口: ${formatIpPort(victimIp, dstPort)}`); lines.push(`需处置IP类型: ${clean(disposalIpType)}`); lines.push(`需处置ip: ${clean(attackIp)}`); lines.push(`是否重保工单: 否`); lines.push(`监测设备: 全流量`); lines.push(`自动拦截: 否`); lines.push(`载荷: ${clean(triggerCond)}`); return { text: lines.join('\n'), raw: { alarmName, level, attackStatus, attackIp, victimIp, assetName, systemName, protocol, direction, triggerTime, triggerCond, logId } }; } // ============ 注入按钮 ============ function injectButton(dialogEl) { const alarmNameEl = dialogEl.querySelector('.alarm-name'); if (!alarmNameEl) return; if (alarmNameEl.parentElement.querySelector('.alert-copy-btn')) return; const btn = document.createElement('button'); btn.textContent = '复制'; btn.className = 'alert-copy-btn'; btn.type = 'button'; btn.style.cssText = `margin-left:8px;padding:2px 10px;font-size:12px; color:#fff;background:#1677FF;border:none;border-radius:4px; cursor:pointer;vertical-align:middle;line-height:18px;`; btn.onmouseover = () => btn.style.background = '#4096ff'; btn.onmouseout = () => btn.style.background = '#1677FF'; btn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); try { const result = extractData(dialogEl); log('提取结果:\n' + result.text); log('原始字段:', result.raw); const ok = copyText(result.text); if (ok) { toast('已复制到剪贴板', '#52C41A'); } else { // 兜底:把文本放进 prompt 让用户手动复制 prompt('自动复制失败,请手动复制以下内容:', result.text); } } catch (err) { console.error('[告警提取] 失败', err); toast('提取失败,查看控制台', '#EF4444'); } }; alarmNameEl.parentElement.insertBefore(btn, alarmNameEl.nextSibling); log('复制按钮已注入'); } // ============ 监听弹窗 ============ function scan() { // 只处理可见的 modal document.querySelectorAll('.ant-modal').forEach(modal => { const wrap = modal.closest('.ant-modal-wrap'); if (!wrap) return; if (getComputedStyle(wrap).display === 'none') return; if (!modal.querySelector('.alarm-name')) return; injectButton(modal); }); } new MutationObserver(scan).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'], }); scan(); let n = 0; const t = setInterval(() => { scan(); if (++n > 30) clearInterval(t); }, 500); log('脚本已启动'); })();