Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions services/bin/octobus-tentacles.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { runServiceMain } from "@chaitin-ai/octobus-sdk";
import { Command } from "commander";

const services = {
"reportedip": {
entryFile: "../reportedip__reportedip/bin/reportedip.js",
serviceModule: "../reportedip__reportedip/src/service.js",
},
"aliyun-waf3": {
entryFile: "../aliyun__waf3/bin/aliyun-waf3.js",
serviceModule: "../aliyun__waf3/src/service.js",
Expand Down
5 changes: 5 additions & 0 deletions services/bin/reportedip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { fileURLToPath } from "node:url";
import { runServiceMain } from "@chaitin-ai/octobus-sdk";
import { service } from "../reportedip__reportedip/src/service.js";
runServiceMain(service, { entryFile: fileURLToPath(new URL("../reportedip__reportedip/bin/reportedip.js", import.meta.url)) });
10 changes: 8 additions & 2 deletions services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@
"volcengine-cloud-firewall": "bin/volcengine-cloud-firewall.js",
"wangsu-label-ip": "bin/wangsu-label-ip.js",
"wd-k01": "bin/wd-k01.js",
"opencti": "bin/opencti.js"
"opencti": "bin/opencti.js",
"tencent-bh": "bin/tencent-bh.js",
"alibaba-sas": "bin/alibaba-sas.js",
"misp": "bin/misp.js",
"reportedip": "bin/reportedip.js"
},
"files": [
"bin/aliyun-waf3.js",
Expand Down Expand Up @@ -139,6 +143,7 @@
"bin/wd-k01.js",
"bin/opencti.js",
"bin/wangsu-label-ip.js",
"bin/reportedip.js",
"alibaba-cloud__simple-application-server-firewall",
"chaitin__cloudatlas",
"chaitin__dsensor_ds-s_h_40-25.07.001",
Expand Down Expand Up @@ -206,7 +211,8 @@
"wangsu__label-ip",
"scripts",
"aliyun__waf3",
"bin/octobus-tentacles.js"
"bin/octobus-tentacles.js",
"reportedip__reportedip"
],
"scripts": {
"validate": "node scripts/validate-service-package.mjs",
Expand Down
30 changes: 30 additions & 0 deletions services/reportedip__reportedip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ReportedIP Service Package

OctoBus package for [ReportedIP](https://reportedip.de/) — a free, EU-hosted IP reputation service.

## Features

- **No API key required** for public check endpoint (100 req/day per IP)
- **EU/GDPR compliant** — hosted in Germany
- **1 RPC method** for IP reputation check

## Methods

| RPC | HTTP API | Type | Description |
|-----|----------|------|-------------|
| `CheckIP` | `GET /check-public?ip={ip}` | Read | IP reputation (confidence score, ISP, geo, hostnames) |

## Usage

```bash
# Create instance
octobus instance create reportedip-test --service reportedip

# Create capset
octobus capset create threat-intel --name Threat-Intel
octobus capset add-instance threat-intel reportedip-test

# Check IP
curl -X POST 'http://127.0.0.1:9000/capsets/threat-intel/connect/reportedip-test/ReportedIP.ReportedIP/CheckIP' \
-H 'Content-Type: application/json' -d '{"ip":"8.8.8.8"}'
```
4 changes: 4 additions & 0 deletions services/reportedip__reportedip/bin/reportedip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
import { runServiceMain } from '@chaitin-ai/octobus-sdk';
import { service } from '../src/service.js';
runServiceMain(service);
1 change: 1 addition & 0 deletions services/reportedip__reportedip/config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":true,"properties":{"timeoutMs":{"type":"integer","minimum":1,"default":10000,"description":"HTTP timeout in milliseconds."}}}
1 change: 1 addition & 0 deletions services/reportedip__reportedip/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"reportedip","version":"0.0.0","private":true,"type":"module","bin":{"reportedip":"bin/reportedip.js"},"dependencies":{"@chaitin-ai/octobus-sdk":"^0.5.0"}}
27 changes: 27 additions & 0 deletions services/reportedip__reportedip/proto/reportedip.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
syntax = "proto3";

package ReportedIP;

import "google/protobuf/wrappers.proto";

option go_package = "miner/grpc-service/ReportedIP";

service ReportedIP {
rpc CheckIP(CheckIPRequest) returns (CheckIPResponse) {}
}

message CheckIPRequest {
string ip = 1;
}

message CheckIPResponse {
int32 code = 1;
string message = 2;
string ip = 3;
int32 abuse_confidence_percentage = 4;
string country_code = 5;
string usage_type = 6;
string isp = 7;
string domain = 8;
repeated string hostnames = 9;
}
1 change: 1 addition & 0 deletions services/reportedip__reportedip/secret.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":true,"properties":{},"description":"ReportedIP public endpoint does not require authentication."}
17 changes: 17 additions & 0 deletions services/reportedip__reportedip/service.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "reportedip",
"displayName": "ReportedIP",
"description": "OctoBus package for ReportedIP — EU-hosted IP reputation service.",
"schema": "chaitin.octobus.service.v1",
"runtime": {"mode": "long-running"},
"proto": {"roots": ["proto"], "files": ["proto/reportedip.proto"]},
"configSchema": "config.schema.json",
"secretSchema": "secret.schema.json",
"sdk": {
"cli": {
"commands": {
"ReportedIP.ReportedIP/CheckIP": {"name": "check-ip", "description": "Check IP reputation against ReportedIP database."}
}
}
}
}
61 changes: 61 additions & 0 deletions services/reportedip__reportedip/src/reportedip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk';
import net from 'node:net';

const METHOD_CHECK = 'ReportedIP.ReportedIP/CheckIP';
const API_BASE = 'https://reportedip.de/wp-json/reportedip/v2';
const DEFAULT_TIMEOUT_MS = 10000;
const SDK_REF = 'ReportedIP';

const grpcCodeFor = (c) => ({ FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, UNAVAILABLE: grpcStatus.UNAVAILABLE, UNKNOWN: grpcStatus.UNKNOWN })[c] ?? grpcStatus.UNKNOWN;
const errorWithCode = (code, msg) => { const e = new GrpcError(grpcCodeFor(code), `${code}: ${msg}`); e.legacyCode = code; return e; };
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o ?? {}, k);
const firstDefined = (...v) => v.find(x => x !== undefined && x !== null);
const unwrapString = (s) => { if (s === undefined || s === null) return ''; if (typeof s === 'object' && hasOwn(s, 'value')) return unwrapString(s.value); return String(s); };
const logInfo = (m, a, p) => { try { console.log(`[${SDK_REF}][${a}]`, JSON.stringify(p)); } catch { console.log(`[${SDK_REF}][${a}]`, p); } };
const logError = (m, a, p) => { try { console.error(`[${SDK_REF}][${a}]`, JSON.stringify(p)); } catch { console.error(`[${SDK_REF}][${a}]`, p); } };
const parseJson = (t) => { if (!String(t || '').trim()) return null; try { return JSON.parse(t); } catch { throw errorWithCode('UNKNOWN', 'response is not valid JSON'); } };
const mapHttpError = (res, bt) => { if (res.status >= 400 && res.status < 500) throw errorWithCode('FAILED_PRECONDITION', `upstream http ${res.status}`); throw errorWithCode('UNAVAILABLE', `upstream http ${res.status}`); };
const mergedBindings = (ctx = {}) => ({ ...(ctx.config ?? {}), ...(ctx.secret ?? {}), ...(ctx.bindings ?? {}) });
const resolveCallContext = (ctx = {}) => ({ ...ctx, bindings: mergedBindings(ctx), limits: ctx.limits ?? {}, meta: ctx.meta ?? {}, req: ctx.req ?? ctx.request ?? {} });
const resolveTimeoutMs = (ctx = {}, b = {}) => firstDefined(ctx.limits?.timeoutMs, b.timeoutMs, DEFAULT_TIMEOUT_MS);

const fetchJson = async (url, init, { bindings = {}, timeoutMs }) => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs || DEFAULT_TIMEOUT_MS);
try {
const res = await fetch(url, { ...init, signal: controller.signal });
const text = await res.text();
if (!res.ok) mapHttpError(res, text);
return { json: parseJson(text), text };
} catch (err) {
if (err instanceof GrpcError) throw err;
throw errorWithCode('UNAVAILABLE', err?.cause?.message || err?.message || 'fetch failed');
} finally { clearTimeout(timer); }
};

const makeRuntime = (ctx = {}) => {
const cc = resolveCallContext(ctx);
const bindings = cc.bindings || {}; const meta = cc.meta || {}; const tm = resolveTimeoutMs(cc, bindings);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapHttpError 调用处缺少显式 throw,存在维护隐患

代码中使用 if (!res.ok) mapHttpError(res, text); 来响应非 2xx 状态码。该语句依赖 mapHttpError 内部抛出异常来中断执行流程,语义上不够清晰。如果未来重构时将 mapHttpError 改为返回错误对象而非抛出,此处将静默吞掉 HTTP 错误并继续执行到 return { json: parseJson(text), text };,导致严重的逻辑缺陷。

Problem code:

Changed code at services/reportedip__reportedip/src/reportedip.js:39

Recommendation:
将调用处改为显式抛出:if (!res.ok) throw mapHttpError(res, text);。更稳妥的做法是移除 mapHttpError 函数,直接在 fetchJson 内联错误映射逻辑,以避免对函数副作用的隐式依赖。

const runCheck = async (req = {}) => {
const ip = unwrapString(firstDefined(req.ip)).trim();
if (!ip) throw errorWithCode('INVALID_ARGUMENT', 'ip is required');
if (!net.isIP(ip)) throw errorWithCode('INVALID_ARGUMENT', 'invalid IP address format');

const url = `${API_BASE}/check-public?ip=${encodeURIComponent(ip)}`;
logInfo(meta, 'CheckIP:start', { ip });

let result;
try { result = await fetchJson(url, { method: 'GET' }, { bindings, timeoutMs: tm }); }
catch (err) { logError(meta, 'CheckIP:http-error', { ip, error: err.message }); throw err; }

const data = result.json?.data || {};
logInfo(meta, 'CheckIP:success', { ip });
return { code: 0, message: 'ok', ip: data.ip || ip, abuse_confidence_percentage: data.abuseConfidencePercentage ?? 0, country_code: data.countryCode || '', usage_type: data.usageType || '', isp: data.isp || '', domain: data.domain || '', hostnames: data.hostnames || [] };
};

return { runCheck };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上游 API 响应字段缺少类型校验与转换,存在数据完整性风险

runCheck 返回结果时直接将上游 API 的 data.abuseConfidencePercentage 透传给 protobuf 的 int32 字段 abuse_confidence_percentage。如果上游返回字符串(如 "67")或浮点数(如 67.5),而 proto 定义该字段为 int32,则可能导致序列化异常或下游消费者拿到非预期类型。hostnames 字段同样可能收到非数组类型,直接透传会破坏数据契约。

Problem code:

Changed code at services/reportedip__reportedip/src/reportedip.js:57

Recommendation:
在赋值前增加类型校验和显式转换:对 abuseConfidencePercentage 使用 Number.isFinite(data.abuseConfidencePercentage) ? Math.round(data.abuseConfidencePercentage) : 0;对 hostnames 使用 Array.isArray(data.hostnames) ? data.hostnames : [] 进行兜底,确保返回给 protobuf 的数据类型严格匹配 proto 定义。

};

export const handlers = { [METHOD_CHECK]: (ctx) => makeRuntime(ctx).runCheck(ctx.request ?? {}) };
export const _test = { errorWithCode, firstDefined, hasOwn, logInfo, logError, makeRuntime, mapHttpError, parseJson, resolveCallContext, resolveTimeoutMs, unwrapString };
4 changes: 4 additions & 0 deletions services/reportedip__reportedip/src/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineService } from '@chaitin-ai/octobus-sdk';
import { handlers } from './reportedip.js';
export { handlers } from './reportedip.js';
export const service = defineService({ handlers });
28 changes: 28 additions & 0 deletions services/reportedip__reportedip/test/reportedip.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { GrpcError } from '@chaitin-ai/octobus-sdk';
import { handlers, _test } from '../src/reportedip.js';
import { service } from '../src/service.js';

const originalFetch = globalThis.fetch, originalCL = console.log, originalCE = console.error;
const CHECK_RESP = { data: { ip: '8.8.8.8', abuseConfidencePercentage: 67, countryCode: 'US', usageType: 'Data Center', isp: 'GOOGLE', domain: 'dns.google', hostnames: ['dns.google'] } };
const ctx = (o = {}) => ({ config: {}, secret: {}, bindings: {}, limits: { timeoutMs: 2000, ...(o.limits || {}) }, meta: { instance_id: 'inst', request_id: 'req', ...(o.meta || {}) } });
const resp = (s, b) => ({ ok: s >= 200 && s < 300, status: s, headers: { get: () => 'application/json' }, text: async () => (typeof b === 'string' ? b : JSON.stringify(b)) });
const setFetch = (i) => { globalThis.fetch = i; };
const expectErr = async (fn, code) => { let c; try { await fn(); } catch (err) { c = err; } assert.ok(c instanceof GrpcError); assert.equal(c.legacyCode, code); };

test.beforeEach(() => { console.log = () => {}; console.error = () => {}; });
test.afterEach(() => { globalThis.fetch = originalFetch; console.log = originalCL; console.error = originalCE; });

test('service exports handlers', () => { assert.equal(typeof handlers['ReportedIP.ReportedIP/CheckIP'], 'function'); });
test('CheckIP returns reputation data', async () => {
setFetch(async (url) => { assert.ok(url.includes('/check-public')); return resp(200, CHECK_RESP); });
const r = await handlers['ReportedIP.ReportedIP/CheckIP']({ ...ctx(), request: { ip: '8.8.8.8' } });
assert.equal(r.code, 0); assert.equal(r.abuse_confidence_percentage, 67); assert.equal(r.isp, 'GOOGLE');
});
test('CheckIP validates ip', async () => { await expectErr(() => handlers['ReportedIP.ReportedIP/CheckIP']({ ...ctx(), request: {} }), 'INVALID_ARGUMENT'); });
test('CheckIP validates IP format', async () => { await expectErr(() => handlers['ReportedIP.ReportedIP/CheckIP']({ ...ctx(), request: { ip: 'not-an-ip' } }), 'INVALID_ARGUMENT'); });
test('handles HTTP errors', async () => {
setFetch(async () => resp(500, '')); await expectErr(() => handlers['ReportedIP.ReportedIP/CheckIP']({ ...ctx(), request: { ip: '8.8.8.8' } }), 'UNAVAILABLE');
});
test('helper utilities', () => { assert.equal(_test.firstDefined(undefined, null, 'x'), 'x'); });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试用例缺少 IPv6 覆盖,存在功能回归风险

测试用例中仅覆盖了 IPv4(8.8.8.8)和非法输入(not-an-ip),未验证 IPv6 地址的处理路径。由于 net.isIP 同时支持 IPv4 和 IPv6,且 IPv6 在 URL query 参数中的编码行为(如是否需要方括号)与 IPv4 不同,缺失 IPv6 测试意味着该分支存在未被验证的回归风险。

Problem code:

Changed code at services/reportedip__reportedip/test/reportedip.test.js:28

Recommendation:
在测试文件中增加 IPv6 地址的测试用例,例如验证 request: { ip: '2001:4860:4860::8888' } 能正确通过校验、正确编码到 URL 中,并且上游返回的数据能正常映射到响应对象。