ThreatIntel Auto Popup

选中文本自动弹窗查询奇安信威胁情报(支持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();
})();