-
Notifications
You must be signed in to change notification settings - Fork 84
feat(shodan-internetdb): add Shodan InternetDB IP intelligence service package #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 "../shodan__internetdb/src/service.js"; | ||
| runServiceMain(service, { entryFile: fileURLToPath(new URL("../shodan__internetdb/bin/shodan-internetdb.js", import.meta.url)) }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Shodan InternetDB Service Package | ||
|
|
||
| OctoBus package for [Shodan InternetDB](https://internetdb.shodan.io/) — a free, no-signup IP intelligence API. | ||
|
|
||
| ## Features | ||
|
|
||
| - **No API key required** — unlimited usage | ||
| - **No signup needed** | ||
| - **Shodan** is a well-known security company (network device search engine) | ||
|
|
||
| ## Methods | ||
|
|
||
| | RPC | HTTP API | Type | Description | | ||
| |-----|----------|------|-------------| | ||
| | `LookupIP` | `GET /{ip}` | Read | IP intelligence (ports, hostnames, CVEs, tags) | | ||
|
|
||
| ## Configuration | ||
|
|
||
| | Config field | Default | Description | | ||
| |-------------|---------|-------------| | ||
| | `timeoutMs` | 10000 | HTTP request timeout | | ||
|
|
||
| No authentication required — `secret.schema.json` is empty. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```bash | ||
| # Create instance | ||
| octobus instance create shodan-test \ | ||
| --service shodan-internetdb | ||
|
|
||
| # Create capset | ||
| octobus capset create threat-intel --name Threat-Intel | ||
| octobus capset add-instance threat-intel shodan-test | ||
|
|
||
| # Lookup IP | ||
| curl -X POST \ | ||
| 'http://127.0.0.1:9000/capsets/threat-intel/connect/shodan-test/Shodan_InternetDB.Shodan_InternetDB/LookupIP' \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{"ip":"8.8.8.8"}' | ||
| ``` |
| 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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "$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." | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "name": "shodan-internetdb", | ||
| "version": "0.0.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "bin": { | ||
| "shodan-internetdb": "bin/shodan-internetdb.js" | ||
| }, | ||
| "dependencies": { | ||
| "@chaitin-ai/octobus-sdk": "^0.5.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package Shodan_InternetDB; | ||
|
|
||
| option go_package = "miner/grpc-service/Shodan_InternetDB"; | ||
|
|
||
| service Shodan_InternetDB { | ||
| rpc LookupIP(LookupIPRequest) returns (LookupIPResponse) {} | ||
| } | ||
|
|
||
| message LookupIPRequest { | ||
| string ip = 1; | ||
| } | ||
|
|
||
| message LookupIPResponse { | ||
| int32 code = 1; | ||
| string message = 2; | ||
| string ip = 3; | ||
| repeated string hostnames = 4; | ||
| repeated int32 ports = 5; | ||
| repeated string cpes = 6; | ||
| repeated string tags = 7; | ||
| repeated string vulns = 8; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "$schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "type": "object", | ||
| "additionalProperties": true, | ||
| "properties": {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "schema": "chaitin.octobus.service.v1", | ||
| "name": "shodan-internetdb", | ||
| "displayName": "Shodan InternetDB", | ||
| "description": "OctoBus package for Shodan InternetDB — free IP intelligence (ports, hostnames, CVEs) without API key.", | ||
| "runtime": { | ||
| "mode": "long-running" | ||
| }, | ||
| "proto": { | ||
| "roots": [ | ||
| "proto" | ||
| ], | ||
| "files": [ | ||
| "proto/shodan_internetdb.proto" | ||
| ] | ||
| }, | ||
| "configSchema": "config.schema.json", | ||
| "secretSchema": "secret.schema.json", | ||
| "sdk": { | ||
| "cli": { | ||
| "commands": { | ||
| "Shodan_InternetDB.Shodan_InternetDB/LookupIP": { | ||
| "name": "lookup-ip", | ||
| "description": "Look up IP intelligence from Shodan InternetDB (ports, hostnames, CVEs)." | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { defineService } from '@chaitin-ai/octobus-sdk'; | ||
| import { handlers } from './shodan-internetdb.js'; | ||
| export { handlers } from './shodan-internetdb.js'; | ||
| export const service = defineService({ handlers }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| import net from 'node:net'; | ||
| import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; | ||
|
|
||
| // ---- Method paths ---- | ||
|
|
||
| const METHOD_LOOKUP_FULL = 'Shodan_InternetDB.Shodan_InternetDB/LookupIP'; | ||
|
|
||
| // ---- Constants ---- | ||
|
|
||
| const API_BASE = 'https://internetdb.shodan.io'; | ||
| const SDK_REF = 'Shodan_InternetDB'; | ||
| const DEFAULT_TIMEOUT_MS = 10000; | ||
|
|
||
| // ---- Error helpers ---- | ||
|
|
||
| const grpcCodeFor = (code) => ({ | ||
| FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, | ||
| INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, | ||
| PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, | ||
| UNAVAILABLE: grpcStatus.UNAVAILABLE, | ||
| UNKNOWN: grpcStatus.UNKNOWN, | ||
| })[code] ?? grpcStatus.UNKNOWN; | ||
|
|
||
| const errorWithCode = (code, message) => { | ||
| const err = new GrpcError(grpcCodeFor(code), `${code}: ${message}`); | ||
| err.legacyCode = code; | ||
| return err; | ||
| }; | ||
|
|
||
| // ---- Value helpers ---- | ||
|
|
||
| const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj ?? {}, key); | ||
| const firstDefined = (...values) => values.find((v) => v !== undefined && v !== null); | ||
|
|
||
| const unwrapString = (source) => { | ||
| if (source === undefined || source === null) return ''; | ||
| if (typeof source === 'object' && source !== null && hasOwn(source, 'value')) return unwrapString(source.value); | ||
| return String(source); | ||
| }; | ||
|
|
||
| const logInfo = (meta, action, payload) => { | ||
| const prefix = `[${SDK_REF}][${action}]`; | ||
| try { console.log(prefix, JSON.stringify(payload)); } catch { console.log(prefix, payload); } | ||
| }; | ||
|
|
||
| const logError = (meta, action, payload) => { | ||
| const prefix = `[${SDK_REF}][${action}]`; | ||
| try { console.error(prefix, JSON.stringify(payload)); } catch { console.error(prefix, payload); } | ||
| }; | ||
|
|
||
| // ---- HTTP ---- | ||
|
|
||
| const parseJson = (text) => { | ||
| if (!String(text || '').trim()) return null; | ||
| try { return JSON.parse(text); } catch { throw errorWithCode('UNKNOWN', 'response is not valid JSON'); } | ||
| }; | ||
|
|
||
| const mapHttpError = (res, bodyText) => { | ||
| if (res.status === 401 || res.status === 403) throw errorWithCode('PERMISSION_DENIED', `upstream http ${res.status}`); | ||
| if (res.status === 429) throw errorWithCode('UNAVAILABLE', `upstream http ${res.status}`); | ||
| if (res.status >= 400 && res.status < 500) throw errorWithCode('FAILED_PRECONDITION', `upstream http ${res.status}`); | ||
| throw errorWithCode('UNAVAILABLE', `upstream http ${res.status}`); | ||
| }; | ||
|
|
||
| const fetchJson = async (url, init, { bindings = {}, timeoutMs }) => { | ||
| let res; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), timeoutMs || DEFAULT_TIMEOUT_MS); | ||
| try { | ||
| 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; | ||
| const reason = err?.cause?.message || err?.message || 'fetch failed'; | ||
| throw errorWithCode('UNAVAILABLE', reason); | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| }; | ||
|
|
||
| // ---- Context resolution ---- | ||
|
|
||
| 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 = {}, bindings = {}) => | ||
| firstDefined(ctx.limits?.timeoutMs, bindings.timeoutMs, DEFAULT_TIMEOUT_MS); | ||
|
|
||
| // ---- Handlers ---- | ||
|
|
||
| const makeRuntime = (ctx = {}) => { | ||
| const callCtx = resolveCallContext(ctx); | ||
| const bindings = callCtx.bindings || {}; | ||
| const meta = callCtx.meta || {}; | ||
| const timeoutMs = resolveTimeoutMs(callCtx, bindings); | ||
|
|
||
| const runLookup = 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}/${encodeURIComponent(ip)}`; | ||
|
|
||
| logInfo(meta, 'LookupIP:start', { ip }); | ||
|
monkeyscan[bot] marked this conversation as resolved.
|
||
|
|
||
| let result; | ||
| try { | ||
| result = await fetchJson(url, { method: 'GET' }, { bindings, timeoutMs }); | ||
| } catch (err) { | ||
| logError(meta, 'LookupIP:http-error', { ip, error: err.message }); | ||
| throw err; | ||
| } | ||
|
|
||
| logInfo(meta, 'LookupIP:success', { ip }); | ||
|
|
||
| return { | ||
| code: 0, | ||
| message: 'ok', | ||
| ip: result.json?.ip || ip, | ||
| hostnames: result.json?.hostnames || [], | ||
| ports: result.json?.ports || [], | ||
| cpes: result.json?.cpes || [], | ||
| tags: result.json?.tags || [], | ||
| vulns: result.json?.vulns || [], | ||
| }; | ||
| }; | ||
|
|
||
| return { runLookup }; | ||
| }; | ||
|
|
||
| // ---- Exports ---- | ||
|
|
||
| export const handlers = { | ||
| [METHOD_LOOKUP_FULL]: (ctx) => makeRuntime(ctx).runLookup(ctx.request ?? {}), | ||
| }; | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handler 请求对象来源与 resolveCallContext 的归一化逻辑不一致resolveCallContext 内部显式做了 req: ctx.req ?? ctx.request ?? {} 的归一化,意味着作者预期运行时可能通过 ctx.req 传递请求体。但 handlers 中直接调用 makeRuntime(ctx).runLookup(ctx.request ?? {}),完全绕过了该归一化结果。若实际框架使用 ctx.req 而非 ctx.request,runLookup 会收到空对象,导致 IP 必填校验直接失败。 Problem code: Recommendation: |
||
| export const _test = { | ||
| errorWithCode, | ||
| firstDefined, | ||
| hasOwn, | ||
| logInfo, | ||
| logError, | ||
| makeRuntime, | ||
| mapHttpError, | ||
| parseJson, | ||
| resolveCallContext, | ||
| resolveTimeoutMs, | ||
| unwrapString, | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.