Sukka's Blog

童话只美在真实却从不续写

从 Cloudflare API 获取被 Cloudflare WAF 拦截的 IP 并提交给 AbuseIPDB

Sukka's Avatar 2019-11-19 笔记本

Cloudflare 的 CTO 在今年(2019 年)九月 23 日发表了一篇博客「Cleaning up bad bots (and the climate)」推出「Bot Fight Mode」功能,但是试用后我发现这个功能聊胜于无,我利用 Cloudflare Firewall Rules 达成的效果远好于 Bot Fight Mode。在对抗 Bad Bot(恶意爬虫)的道路上,不如我也主动出击。

「Bot Fight Mode」启用后 Cloudflare 会检查请求是否发自 Bad Bot(恶意爬虫)、通过消耗 Bad Bot 的 CPU 资源来对抗 Bad Bot,并会通过植树来补偿 Bad Bot 带来的碳排放;关于我的 Firewall Rules 可以查看我的开源项目 cloudflare-block-bad-bot-ruleset,在这个项目中我开源了我使用的其中非常小一部分的 Firewall Rules。

我采取的方案是:我编写的 WAF 规则远比 Cloudflare 的「Bot Fight Mode」严格许多,可以匹配到更多恶意爬虫(和无恶意的爬虫);通过 Cloudflare API 获取 Firewall Events 日志,获取所有被拦截的 IP,去重以后全部提交给 AbuseIPDB

首先是获取 Cloudflare 的 API Token。Cloudflare 最近终于支持 API Token 权限细分了,支持在不同服务、不同功能、读写操作等维度设置 API Token 的权限。使用新的 API Token 以后可以不需要使用 X-Auth-Mail X-Auth-Key 、可以直接使用 Authentication: Bearer [API Token] 这样的 HTTP Simple Auth 请求 Cloudflare API。

前往 Cloudflare Dashboard 的「My Profile」,在「API Token」面板点击「Create Token」创建一个新的 Token,名字随意、权限设置如下图所示:

cf-waf-to-abuseipdb-cf-token.png

需要包含的域名也根据需要设置,不过每个 API Token 的权限自然是越小越好,所以最好使用「Specific Zone」。

然后是前往 AbuseIPDB 创建 Key。如果没有账户,需要先注册一个。

接下来直接上代码。在处理 JSON 上,Node.js 明显比 Shell 方便多了:

const fetch = require('node-fetch');
const FormData = require('form-data');
const { env } = process;
// 从环境变量读 API Key 等配置
const cfApiKey = env.CF_API_KEY; // Cloudflare 的 API Token
const cfZoneId = env.CF_ZONE_ID; // Cloudflare 上指定域名的 Zone ID
const abipdbKey = env.ABUSEIPDB_KEY; // AbuseIPDB 的 Key
const now = new Date().getTime();
const since = new Date(now - 86400 * 1000).toISOString();
const until = new Date(now).toISOString();
let data = [];
let stat = 0;
function sortIP(data) {
    const input_ips = [];
    for (const { ip } of data) {
        input_ips.push(ip);
    }
    const result = [...new Set(input_ips)];
    return result;
}
function reportToAbuseIPDB(iplist) {
    function report(ip) {
        const form = new FormData();
        form.append('ip', ip);
        form.append('comment', 'The IP has triggered Cloudflare WAF. Report generated by Cloudflare-WAF-to-AbuseIPDB (https://github.com/SukkaW/Cloudflare-WAF-to-AbuseIPDB)');
        form.append('categories', '9,13,14,15,16,19,20,21');
        fetch('https://api.abuseipdb.com/api/v2/report', {
            method: 'post',
            body: form,
            headers: { 'Key': abipdbKey, 'Accept': 'application/json', ...form.getHeaders() }
        })
            .then(res => res.json())
            .then(({ data }) => { console.log(`${data.ipAddress} has abuse confidence score of ${data.abuseConfidenceScore}`); })
            .catch((err) => { console.error('The IP has already been reported or AbuseIPDB\'s Rate Limit has been met'); });
    }
    for (const ip of iplist) {
        report(ip);
    }
}
(function queryCfWAF(cfApiKey, cfZoneId, cursor) {
    if (stat === 0) console.log(`Querying Firewall Events from Cloudflare API V4...\n`);
    cursor = !cursor ? '' : `cursor=${cursor}`;
    fetch(`https://api.cloudflare.com/client/v4/zones/${cfZoneId}/security/events?limit=1000&since=${since}&until=${until}&${cursor}`, {
        headers: {
            'Authorization': `Bearer ${cfApiKey}`,
            'Content-Type': 'application/json'
        }
    }).then(res => res.json()).then(({ result, result_info }) => {
        data = [...data, ...result];
        stat = data.length;
        return [result_info, result[result.length - 1].occurred_at];
    }).then(([{ cursors }, occured_at]) => {
        const toEnd = new Date(occured_at).getTime() - new Date(since).getTime();
        if (toEnd < 0) {
            // To the End
            const iplist = sortIP(data);
            console.log(`----------------------------------------\n${iplist.length} IPs has been quried from Cloudflare API. Reporting to AbuseIPDB...`)
            reportToAbuseIPDB(iplist);
        } else {
            console.log(`${stat} events already queried from Cloudflare API.`)
            queryCfWAF(cfApiKey, cfZoneId, cursors.before);
        }
    });
})(cfApiKey, cfZoneId);

以上代码可以自动获取过去 24 小时内的 Cloudflare Firewall Events 并将 IP 提交到 Travis。设置好环境变量后,就可以运行 node index.js 了。

接下来是利用 Travis CI 实现自动化。在 Travis CI 上为项目设置环境变量(注意 API Key 等需要设置为 Secret),设置 Cron 以实现每天定时运行一次:

效果如下:

cf-waf-to-abuseipdb-log.png

如果你也想使用 Travis CI 自动将 Cloudflare 的 Firewall Events 中拦截的 IP 提交给 AbuseIPDB,只需要在 GitHub 上 Fork 我的 Cloudflare-WAF-to-AbuseIPDB 项目、在 Travis CI 中启用、配置相应的环境变量和定时运行 即可。

本文作者 : Sukka
本文采用 CC BY-NC-SA 4.0 许可协议。转载和引用时请注意遵守协议!
本文链接 : https://blog.skk.moe/post/cf-waf-to-abuseipdb/

本文最后更新于 天前,文中所描述的信息可能已发生改变