您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
选中文本自动弹窗查询奇安信威胁情报(支持IP、域名、MD5、SHA1、URL)
// ==UserScript== // @name ThreatIntel Auto Popup // @namespace http://tampermonkey.net/ // @version 2.1 // @description 选中文本自动弹窗查询奇安信威胁情报(支持IP、域名、MD5、SHA1、URL) // @author wooluo // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_log // @license MIT // ==/UserScript== (function () { 'use strict'; // 配置区 const CONFIG = { // 替换为你自己的 Token 和 API Key API_TOKENS: { sandbox: '你的情报沙箱token', // 情报沙箱 Token threat: '你的威胁情报api key' // 威胁情报 API Key }, // 启用详细错误提示 ENABLE_DETAILED_ERRORS: true, // API 版本 API_VERSION: 'v3', // API 地址 API_URLS: { sandbox: (token) => `https://sandbox.ti.qianxin.com/sandbox/api/v1/token/${token}/report`, ip_reputation: `https://webapi.ti.qianxin.com/ip/v3/reputation`, file_reputation: `https://ti.qianxin.com/api/v2/malfile`, url_reputation: `https://a.ti.qianxin.com/url/v1/CheckUrls`, compromise: `https://ti.qianxin.com/api/v2/compromise` }, // 查询缓存过期时间(毫秒) CACHE_EXPIRE_TIME: 3600000, // 1小时 // 最大缓存条目数 MAX_CACHE_ITEMS: 100, // 防抖时间(毫秒) DEBOUNCE_TIME: 500, // 弹窗显示时间(毫秒) TOOLTIP_SHOW_TIME: 15000, // 是否允许手动关闭弹窗 ALLOW_MANUAL_CLOSE: true, // 调试模式 DEBUG_MODE: true }; // 类型检测正则 const PATTERNS = { // 局域网IP检测 privateIp: /^(10\.\d{1,3}\.\d{1,3}\.\d{1,3})|(172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3})|(192\.168\.\d{1,3}\.\d{1,3})$/, md5: /^[a-f0-9]{32}$/i, sha1: /^[a-f0-9]{40}$/i, ip: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::\d+)?$|(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){0,7}|:))(?::\d+)?/, domain: /^[a-zA-Z0-9][a-zA-Z0-9\-.]+\.[a-zA-Z]{2,}$/, url: /^https?:\/\/[^\s$.?#].[^\s]*$/i }; // 查询缓存 const queryCache = {}; // 缓存键列表,用于实现LRU缓存 const cacheKeys = []; /** * 检测文本类型 * @param {string} text - 要检测的文本 * @returns {string|null} - 检测到的类型或null */ function detectType(text) { // 先检查是否是URL,因为URL可能包含域名 if (PATTERNS.url.test(text)) { return 'url'; } // 检查其他类型 return Object.entries(PATTERNS).find(([key, regex]) => key !== 'url' && regex.test(text))?.[0]; } /** * 显示提示框 * @param {string} content - 提示内容 * @param {number} x - 鼠标X坐标 * @param {number} y - 鼠标Y坐标 */ function showTooltip(content, x = 20, y = 20) { // 移除已有的提示框 const existingTooltip = document.getElementById('threat-intel-tooltip'); if (existingTooltip && existingTooltip.parentNode === document.body) { document.body.removeChild(existingTooltip); } const tooltip = document.createElement('div'); tooltip.id = 'threat-intel-tooltip'; tooltip.style = ` position: fixed; top: ${y}px; left: ${x}px; background: #222; color: #fff; padding: 15px; border-radius: 6px; font-size: 14px; z-index: 99999; max-width: 500px; word-break: break-all; box-shadow: 0 0 10px rgba(0,0,0,0.5); `; // 如果允许手动关闭,添加关闭按钮 if (CONFIG.ALLOW_MANUAL_CLOSE) { const closeBtn = document.createElement('span'); closeBtn.style = 'position: absolute; top: 5px; right: 10px; cursor: pointer; color: #aaa;'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => { if (tooltip.parentNode === document.body) { document.body.removeChild(tooltip); } }; tooltip.appendChild(closeBtn); } tooltip.innerHTML += content; document.body.appendChild(tooltip); setTimeout(() => { const tooltipToRemove = document.getElementById('threat-intel-tooltip'); if (tooltipToRemove && tooltipToRemove.parentNode === document.body) { document.body.removeChild(tooltipToRemove); } }, CONFIG.TOOLTIP_SHOW_TIME); } /** * 检查缓存是否有效 * @param {string} key - 缓存键 * @returns {boolean} - 缓存是否有效 */ function isCacheValid(key) { if (!queryCache[key]) return false; const now = Date.now(); return now - queryCache[key].timestamp < CONFIG.CACHE_EXPIRE_TIME; } /** * 更新缓存 * @param {string} key - 缓存键 * @param {string} result - 缓存结果 */ function updateCache(key, result) { // 检查缓存是否已满 if (cacheKeys.length >= CONFIG.MAX_CACHE_ITEMS) { // 移除最早的缓存 const oldestKey = cacheKeys.shift(); delete queryCache[oldestKey]; } // 更新缓存 queryCache[key] = { result, timestamp: Date.now() }; // 更新缓存键列表 if (cacheKeys.includes(key)) { // 移除旧位置 cacheKeys.splice(cacheKeys.indexOf(key), 1); } // 添加到最新位置 cacheKeys.push(key); } /** * 处理错误 * @param {string} message - 错误消息 * @param {number} x - 鼠标X坐标 * @param {number} y - 鼠标Y坐标 * @param {object} [details=null] - 错误详情 */ function handleError(message, x = 20, y = 20, details = null) { if (CONFIG.DEBUG_MODE) { GM_log('错误: ' + message); if (details) { GM_log('错误详情: ' + JSON.stringify(details)); } } showTooltip(message, x, y); } /** * 查询威胁情报 * @param {string} value - 要查询的值 * @param {string} type - 类型 */ // 存储最后鼠标位置 let lastMouseX = 20; let lastMouseY = 20; // 更新鼠标位置 document.addEventListener('mousemove', (e) => { lastMouseX = e.clientX + 10; lastMouseY = e.clientY + 10; }); function queryIntel(value, type) { // 处理带端口的IP地址 if (type === 'ip' && value.includes(':')) { // 提取IP部分 const ipPart = value.split(':')[0]; // 检查提取后的IP是否有效 if (PATTERNS.ip.test(ipPart)) { value = ipPart; } } // 检查缓存 const cacheKey = `${type}:${value}`; if (isCacheValid(cacheKey)) { showTooltip(queryCache[cacheKey].result, lastMouseX, lastMouseY); return; } let api_url, method = 'POST', headers = {}, data = null; try { if (['md5', 'sha1'].includes(type)) { api_url = CONFIG.API_URLS.sandbox(CONFIG.API_TOKENS.sandbox); headers = {'Content-Type': 'application/json'}; data = JSON.stringify([{type: 'file', value}]); } else if (type === 'ip') { api_url = CONFIG.API_URLS.ip_reputation; method = 'GET'; headers = {'Api-Key': CONFIG.API_TOKENS.threat}; // 构建查询参数 const params = new URLSearchParams(); params.append('param', value); params.append('version', CONFIG.API_VERSION); api_url = `${api_url}?${params.toString()}`; } else if (type === 'url') { api_url = CONFIG.API_URLS.url_reputation; headers = {'Api-Key': CONFIG.API_TOKENS.threat, 'Content-Type': 'application/json'}; // 确保数据格式正确 data = JSON.stringify({ queries: [{ index: 0, origin_url: value, version: CONFIG.API_VERSION }] }); } else if (type === 'domain') { api_url = CONFIG.API_URLS.compromise; method = 'GET'; headers = {'Api-Key': CONFIG.API_TOKENS.threat}; // 构建查询参数 const params = new URLSearchParams(); params.append('apikey', CONFIG.API_TOKENS.threat); params.append('param', value); params.append('version', CONFIG.API_VERSION); api_url = `${api_url}?${params.toString()}`; } else { return handleError('不支持的查询类型', lastMouseX, lastMouseY); } // 调试日志 if (CONFIG.DEBUG_MODE) { GM_log('API请求信息:'); GM_log('URL: ' + api_url); GM_log('方法: ' + method); GM_log('头部: ' + JSON.stringify(headers)); GM_log('数据: ' + data); } GM_xmlhttpRequest({ method, url: api_url, headers, data, timeout: 10000, // 10秒超时 onload: function (res) { // 调试日志 if (CONFIG.DEBUG_MODE) { GM_log('API响应状态: ' + res.status); GM_log('API响应内容: ' + res.responseText); } try { const result = JSON.parse(res.responseText); // 检查认证错误 if (result.status === 10001 && CONFIG.ENABLE_DETAILED_ERRORS) { let errorMsg = 'API认证错误: ' + (result.msg || '未知错误'); errorMsg += '\n\n请检查您的API密钥和令牌是否有效。'; handleError(errorMsg, lastMouseX, lastMouseY); } else { const formattedResult = formatResult(result, type, value); // 更新缓存 updateCache(cacheKey, formattedResult); showTooltip(formattedResult, lastMouseX, lastMouseY); } } catch (e) { handleError('解析响应失败:' + e.message, lastMouseX, lastMouseY, { responseText: res.responseText }); } }, onerror: function (error) { handleError('API请求失败,请检查网络或Token', lastMouseX, lastMouseY, error); }, ontimeout: function () { handleError('API请求超时,请稍后再试', lastMouseX, lastMouseY); } }); } catch (e) { handleError('查询过程中出错:' + e.message, lastMouseX, lastMouseY); } } /** * 格式化结果 * @param {object} result - API返回的结果 * @param {string} type - 类型 * @param {string} value - 查询的值 * @returns {string} - 格式化后的结果 */ /** * 检查是否为局域网IP * @param {string} ip - IP地址 * @returns {boolean} - 是否为局域网IP */ function isPrivateIp(ip) { return PATTERNS.privateIp.test(ip); } /** * 格式化结果 * @param {object} result - API返回的结果 * @param {string} type - 类型 * @param {string} value - 查询的值 * @returns {string} - 格式化后的结果 */ function formatResult(result, type, value) { let message = `【查询结果】\n类型:${type.toUpperCase()}\n值:${value}\n`; let isAPT = false; switch (type) { case 'md5': case 'sha1': if (result.status === 10000 && result.data?.[value]) { const data = result.data[value]; // 综合判断是否恶意 const isMalicious = data.static_detect.is_virus || (data.static_detect.static_score && data.static_detect.static_score > 0) || (data.dynamic_detect && data.dynamic_detect.dropfile && data.dynamic_detect.dropfile.length > 0); message += `\n` + `文件名:${data.static_detect.filename || '未知'}\n` + `恶意评分:${data.static_detect.static_score || 'undefined'}/100\n` + `是否恶意:${isMalicious ? '<span style="color:red;">是</span>' : '否'}\n` + `分析报告:${data.web_url || '未知'}`; } else { message += '未找到该样本的分析报告'; } break; case 'ip': if (result.status === 10000 && result.data?.[value]) { const data = result.data[value]; // 检查是否为局域网IP const isPrivate = isPrivateIp(value); message += `\n` + `国家:${data.geo?.country || '未知'}\n` + `省份/城市:${data.geo?.province || ''} ${data.geo?.city || ''}\n` + `运营商:${data.normal_info?.asn_org || '未知'}\n` + `信誉状态:${data.summary_info?.reputation || '未知'}\n` + `最后活跃时间:${data.summary_info?.latest_reputation_time || '未知'}\n` + `是否局域网:${isPrivate ? '<span style="color:blue;">是</span>' : '否'}`; // 检查是否存在APT相关信息 if (data.compromise && data.compromise.length > 0) { data.compromise.forEach(item => { if (item.alert_name && item.alert_name.includes('APT')) { isAPT = true; } }); } // 检查是否存在远控木马活动事件 if (data.compromise && data.compromise.length > 0) { // 检查是否存在CobaltStrike远控木马活动事件 const cobaltStrikeEvents = data.compromise.filter(item => item.malicious_family && item.malicious_family.includes('CobaltStrike') ); if (cobaltStrikeEvents.length > 0) { message += `\n\n<span style="color:red;font-weight:bold;">⚠️ 发现CobaltStrike远控木马活动事件 ⚠️</span>\n`; cobaltStrikeEvents.forEach(event => { message += `\n` + `告警名称:${event.alert_name || '未知'}\n` + `风险等级:${event.risk || '未知'}\n` + `恶意类型:${event.malicious_type || '未知'}\n` + `首次发现时间:${event.etime || '未知'}`; }); } // 检查是否存在其他远控木马活动事件 const otherTrojanEvents = data.compromise.filter(item => item.malicious_type === '远控木马' && (!item.malicious_family || !item.malicious_family.includes('CobaltStrike')) ); if (otherTrojanEvents.length > 0) { message += `\n\n<span style="color:red;font-weight:bold;">⚠️ 发现远控木马活动事件 ⚠️</span>\n`; otherTrojanEvents.forEach(event => { message += `\n` + `告警名称:${event.alert_name || '未知'}\n` + `风险等级:${event.risk || '未知'}\n` + `恶意类型:${event.malicious_type || '未知'}\n` + `首次发现时间:${event.etime || '未知'}`; }); } } } else { message += '未找到该IP的威胁情报'; } break; case 'url': if (result.status === 10000 && result.replies?.[0]?.uss?.uss) { const data = result.replies[0].uss.uss; message += `\n` + `安全等级:${data.level}\n` + `分类:${data.category}\n` + `首次检测时间:${data.first_detect_time}\n` + `最后更新时间:${data.last_update_time}`; } else { message += '未找到该URL的威胁情报'; } break; case 'domain': if (result.status === 10000 && result.data?.length > 0) { const data = result.data[0]; message += `\n` + `告警名称:<span style="color:red;">${data.alert_name}</span>\n` + `风险等级:${data.risk === 'high' ? '<span style="color:red;">high</span>' : data.risk}\n` + `恶意类型:${data.malicious_type && (data.malicious_type.includes('病毒') || data.malicious_type.includes('木马')) ? '<span style="color:red;">' + data.malicious_type + '</span>' : data.malicious_type}\n` + `首次发现时间:${data.etime}`; } else { message += '未找到该域名的威胁情报'; } break; default: message += '未知错误'; } // 如果检测到APT,在结果前添加红色标记 if (isAPT) { message = `<span style="color:red;font-weight:bold;">‼️APT‼️</span>\n` + message; } return message; } // 初始化 function init() { // 防止频繁触发 let debounceTimer; let lastHoveredText = ''; // 鼠标经过检测 document.addEventListener('mousemove', (e) => { // 获取鼠标位置的文本 const range = document.caretRangeFromPoint(e.clientX, e.clientY); if (!range) return; // 扩展范围以获取更多文本 const expandRange = (range, expandSize = 50) => { const start = Math.max(0, range.startOffset - expandSize); const end = Math.min(range.endContainer.length, range.endOffset + expandSize); const newRange = document.createRange(); newRange.setStart(range.startContainer, start); newRange.setEnd(range.endContainer, end); return newRange; }; const expandedRange = expandRange(range); const hoveredText = expandedRange.toString().trim(); // 避免重复检测相同的文本 if (hoveredText === lastHoveredText) return; lastHoveredText = hoveredText; // 检查文本中是否包含可检测的类型 let detected = false; let detectedType = null; let detectedValue = null; // 检查URL if (PATTERNS.url.test(hoveredText)) { const match = hoveredText.match(PATTERNS.url); if (match) { detected = true; detectedType = 'url'; detectedValue = match[0]; } } // 检查其他类型 if (!detected) { Object.entries(PATTERNS).forEach(([key, regex]) => { if (key !== 'url' && regex.test(hoveredText)) { const match = hoveredText.match(regex); if (match) { detected = true; detectedType = key; detectedValue = match[0]; } } }); } if (detected && detectedValue && detectedValue.length <= 100) { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { queryIntel(detectedValue, detectedType); }, CONFIG.DEBOUNCE_TIME); } }); // 保留原有的选中检测功能 document.addEventListener('mouseup', () => { const selected = window.getSelection().toString().trim(); if (!selected || selected.length > 100) return; // 限制长度,避免过大的选择 const type = detectType(selected); if (!type) return; clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { queryIntel(selected, type); }, CONFIG.DEBOUNCE_TIME); }); } // 启动脚本 init(); })();