ThreatIntel Auto Popup

选中文本自动弹窗查询奇安信威胁情报(支持IP、域名、MD5、SHA1、URL)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();