From 1311467e05c265275757053b7f8b107c91a12f13 Mon Sep 17 00:00:00 2001 From: K6gg <3362488788@qq.com> Date: Thu, 25 Jun 2026 13:31:05 +0800 Subject: [PATCH 1/3] feat(sangfor-sip): add sangfor SIP security intelligence platform service package --- services/bin/octobus-tentacles.js | 418 +++++----- services/bin/sangfor-sip.js | 10 + services/package.json | 296 ++++---- services/sangfor__sip/README.md | 46 ++ services/sangfor__sip/bin/sangfor-sip.js | 6 + services/sangfor__sip/config.schema.json | 26 + services/sangfor__sip/package.json | 12 + services/sangfor__sip/proto/sangfor_sip.proto | 126 ++++ services/sangfor__sip/secret.schema.json | 19 + services/sangfor__sip/service.json | 61 ++ services/sangfor__sip/src/sangfor-sip.js | 225 ++++++ services/sangfor__sip/src/service.js | 7 + .../sangfor__sip/test/sangfor-sip.test.js | 712 ++++++++++++++++++ 13 files changed, 1621 insertions(+), 343 deletions(-) mode change 100755 => 100644 services/bin/octobus-tentacles.js create mode 100644 services/bin/sangfor-sip.js create mode 100644 services/sangfor__sip/README.md create mode 100644 services/sangfor__sip/bin/sangfor-sip.js create mode 100644 services/sangfor__sip/config.schema.json create mode 100644 services/sangfor__sip/package.json create mode 100644 services/sangfor__sip/proto/sangfor_sip.proto create mode 100644 services/sangfor__sip/secret.schema.json create mode 100644 services/sangfor__sip/service.json create mode 100644 services/sangfor__sip/src/sangfor-sip.js create mode 100644 services/sangfor__sip/src/service.js create mode 100644 services/sangfor__sip/test/sangfor-sip.test.js diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js old mode 100755 new mode 100644 index a27fad76..b5023ad8 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -1,201 +1,217 @@ -#!/usr/bin/env node - -import { fileURLToPath } from "node:url"; -import { runServiceMain } from "@chaitin-ai/octobus-sdk"; -import { Command } from "commander"; - -const services = { - "safeline-waf": { - entryFile: "../chaitin__safeline-waf/bin/safeline-waf.js", - serviceModule: "../chaitin__safeline-waf/src/service.js", - }, - "safeline-waf-eliminate-false-positive": { - entryFile: "../chaitin__safeline-waf-eliminate-false-positive/bin/safeline-waf-eliminate-false-positive.js", - serviceModule: "../chaitin__safeline-waf-eliminate-false-positive/src/service.js", - }, - "das-gateway-v3": { - entryFile: "../das__gateway_v3/bin/das-gateway-v3.js", - serviceModule: "../das__gateway_v3/src/service.js", - }, - "das-tgfw-v6": { - entryFile: "../das__tgfw_v6/bin/das-tgfw-v6.js", - serviceModule: "../das__tgfw_v6/src/service.js", - }, - "dingtalk-group-robot": { - entryFile: "../dingtalk__group-robot/bin/dingtalk-group-robot.js", - serviceModule: "../dingtalk__group-robot/src/service.js", - }, - "dptech-eds": { - entryFile: "../dptech__eds/bin/dptech-eds.js", - serviceModule: "../dptech__eds/src/service.js", - }, - "dptech-fw-v4-6-10": { - entryFile: "../dptech__fw_v4-6-10/bin/dptech-fw-v4-6-10.js", - serviceModule: "../dptech__fw_v4-6-10/src/service.js", - }, - "dptech-umc-ads-v5-3-29": { - entryFile: "../dptech__umc-ads_v5-3-29/bin/dptech-umc-ads-v5-3-29.js", - serviceModule: "../dptech__umc-ads_v5-3-29/src/service.js", - }, - "feishu-group-robot": { - entryFile: "../feishu__group-robot/bin/feishu-group-robot.js", - serviceModule: "../feishu__group-robot/src/service.js", - }, - "fortinet-fw": { - entryFile: "../fortinet__fw/bin/fortinet-fw.js", - serviceModule: "../fortinet__fw/src/service.js", - }, - "fortinet-waf": { - entryFile: "../fortinet__waf/bin/fortinet-waf.js", - serviceModule: "../fortinet__waf/src/service.js", - }, - "hillstone-fw-v5-5-r10": { - entryFile: "../hillstone__fw_v5-5-r10/bin/hillstone-fw-v5-5-r10.js", - serviceModule: "../hillstone__fw_v5-5-r10/src/service.js", - }, - "hillstone-fw-v5-5-r4": { - entryFile: "../hillstone__fw_v5-5-r4/bin/hillstone-fw-v5-5-r4.js", - serviceModule: "../hillstone__fw_v5-5-r4/src/service.js", - }, - "hillstone-fw-v5-5-r6": { - entryFile: "../hillstone__fw_v5-5-r6/bin/hillstone-fw-v5-5-r6.js", - serviceModule: "../hillstone__fw_v5-5-r6/src/service.js", - }, - "huawei-fw-usg6000e": { - entryFile: "../huawei__fw-usg6000e/bin/huawei-fw-usg6000e.js", - serviceModule: "../huawei__fw-usg6000e/src/service.js", - }, - "nsfocus-ads-v4-5-r90-f06": { - entryFile: "../nsfocus__ads_v4-5-r90-f06/bin/nsfocus-ads-v4-5-r90-f06.js", - serviceModule: "../nsfocus__ads_v4-5-r90-f06/src/service.js", - }, - "nsfocus-nips-v5-6-r11": { - entryFile: "../nsfocus__nips_v5-6-r11/bin/nsfocus-nips-v5-6-r11.js", - serviceModule: "../nsfocus__nips_v5-6-r11/src/service.js", - }, - "panabit-tang-r1": { - entryFile: "../panabit__tang-r1/bin/panabit-tang-r1.js", - serviceModule: "../panabit__tang-r1/src/service.js", - }, - "qianxin-fw-secgate3600": { - entryFile: "../qianxin__fw-secgate3600/bin/qianxin-fw-secgate3600.js", - serviceModule: "../qianxin__fw-secgate3600/src/service.js", - }, - "qianxin-fw-secgate3600-http-x": { - entryFile: "../qianxin__fw-secgate3600-http-x/bin/qianxin-fw-secgate3600-http-x.js", - serviceModule: "../qianxin__fw-secgate3600-http-x/src/service.js", - }, - "qiming-tianqing-waf": { - entryFile: "../qiming-tianqing__waf/bin/qiming-tianqing-waf.js", - serviceModule: "../qiming-tianqing__waf/src/service.js", - }, - "qingteng-hids-v3-4": { - entryFile: "../qingteng__hids_v3-4/bin/qingteng-hids-v3-4.js", - serviceModule: "../qingteng__hids_v3-4/src/service.js", - }, - "ray-waf-v6-1-2": { - entryFile: "../ray__waf_v6-1-2/bin/ray-waf-v6-1-2.js", - serviceModule: "../ray__waf_v6-1-2/src/service.js", - }, - "riversafe-waf": { - entryFile: "../riversafe__waf/bin/riversafe-waf.js", - serviceModule: "../riversafe__waf/src/service.js", - }, - "sangfor-fw-v8-0-45": { - entryFile: "../sangfor__fw_v8-0-45/bin/sangfor-fw-v8-0-45.js", - serviceModule: "../sangfor__fw_v8-0-45/src/service.js", - }, - "skycloud-inet": { - entryFile: "../skycloud__inet/bin/skycloud-inet.js", - serviceModule: "../skycloud__inet/src/service.js", - }, - "tencent-qyweixin-group-robot": { - entryFile: "../tencent__qyweixin-group-robot/bin/tencent-qyweixin-group-robot.js", - serviceModule: "../tencent__qyweixin-group-robot/src/service.js", - }, - "tencent-tsec-v2-5-1": { - entryFile: "../tencent__tsec_v2-5-1/bin/tencent-tsec-v2-5-1.js", - serviceModule: "../tencent__tsec_v2-5-1/src/service.js", - }, - "threatbook-cloudapi-v3": { - entryFile: "../threatbook__cloudapi_v3/bin/threatbook-cloudapi-v3.js", - serviceModule: "../threatbook__cloudapi_v3/src/service.js", - }, - "threatbook-onesig": { - entryFile: "../threatbook__onesig/bin/threatbook-onesig.js", - serviceModule: "../threatbook__onesig/src/service.js", - }, - "threatbook-tdp": { - entryFile: "../threatbook__tdp/bin/threatbook-tdp.js", - serviceModule: "../threatbook__tdp/src/service.js", - }, - "threatbook-tip-v4": { - entryFile: "../threatbook__tip_v4/bin/threatbook-tip-v4.js", - serviceModule: "../threatbook__tip_v4/src/service.js", - }, - "topsec-fw-2u": { - entryFile: "../topsec__fw-2u/bin/topsec-fw-2u.js", - serviceModule: "../topsec__fw-2u/src/service.js", - }, - "topsec-fw-5u": { - entryFile: "../topsec__fw-5u/bin/topsec-fw-5u.js", - serviceModule: "../topsec__fw-5u/src/service.js", - }, - "topsec-fw-v3-7-6": { - entryFile: "../topsec__fw_v3-7-6/bin/topsec-fw-v3-7-6.js", - serviceModule: "../topsec__fw_v3-7-6/src/service.js", - }, - "venus-ads-v3-6": { - entryFile: "../venus__ads_v3-6/bin/venus-ads-v3-6.js", - serviceModule: "../venus__ads_v3-6/src/service.js", - }, - "wangsu-label-ip": { - entryFile: "../wangsu__label-ip/bin/wangsu-label-ip.js", - serviceModule: "../wangsu__label-ip/src/service.js", - }, - "wd-k01": { - entryFile: "../wd__k01/bin/wd-k01.js", - serviceModule: "../wd__k01/src/service.js", - }, -}; - -const serviceNames = Object.keys(services); - -const program = new Command(); - -program - .name("octobus-tentacles") - .usage(" [args]") - .description("Run a service from this package") - .argument("[service]", "service name") - .allowUnknownOption(true) - .allowExcessArguments(true) - .passThroughOptions() - .addHelpText("after", ` -Services: -${serviceNames.map((name) => ` ${name.padEnd(37)}`).join("\n")} - -Use 'octobus-tentacles --help' to print service help.`) - .action(async (serviceName) => { - if (!serviceName) { - program.outputHelp(); - return; - } - - const selected = services[serviceName]; - if (!selected) { - process.stderr.write(`Unknown service: ${serviceName}\n\n`); - program.outputHelp({ error: true }); - process.exitCode = 1; - return; - } - - const { service } = await import(new URL(selected.serviceModule, import.meta.url)); - - await runServiceMain(service, { - argv: program.args.slice(1), - entryFile: fileURLToPath(new URL(selected.entryFile, import.meta.url)), - }); - }); - -await program.parseAsync(); +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; +import { Command } from "commander"; + +const services = { + "safeline-waf": { + entryFile: "../chaitin__safeline-waf/bin/safeline-waf.js", + serviceModule: "../chaitin__safeline-waf/src/service.js", + }, + "safeline-waf-eliminate-false-positive": { + entryFile: "../chaitin__safeline-waf-eliminate-false-positive/bin/safeline-waf-eliminate-false-positive.js", + serviceModule: "../chaitin__safeline-waf-eliminate-false-positive/src/service.js", + }, + "das-gateway-v3": { + entryFile: "../das__gateway_v3/bin/das-gateway-v3.js", + serviceModule: "../das__gateway_v3/src/service.js", + }, + "das-tgfw-v6": { + entryFile: "../das__tgfw_v6/bin/das-tgfw-v6.js", + serviceModule: "../das__tgfw_v6/src/service.js", + }, + "dingtalk-group-robot": { + entryFile: "../dingtalk__group-robot/bin/dingtalk-group-robot.js", + serviceModule: "../dingtalk__group-robot/src/service.js", + }, + "dptech-eds": { + entryFile: "../dptech__eds/bin/dptech-eds.js", + serviceModule: "../dptech__eds/src/service.js", + }, + "dptech-fw-v4-6-10": { + entryFile: "../dptech__fw_v4-6-10/bin/dptech-fw-v4-6-10.js", + serviceModule: "../dptech__fw_v4-6-10/src/service.js", + }, + "dptech-umc-ads-v5-3-29": { + entryFile: "../dptech__umc-ads_v5-3-29/bin/dptech-umc-ads-v5-3-29.js", + serviceModule: "../dptech__umc-ads_v5-3-29/src/service.js", + }, + "feishu-group-robot": { + entryFile: "../feishu__group-robot/bin/feishu-group-robot.js", + serviceModule: "../feishu__group-robot/src/service.js", + }, + "fortinet-fw": { + entryFile: "../fortinet__fw/bin/fortinet-fw.js", + serviceModule: "../fortinet__fw/src/service.js", + }, + "fortinet-waf": { + entryFile: "../fortinet__waf/bin/fortinet-waf.js", + serviceModule: "../fortinet__waf/src/service.js", + }, + "hillstone-fw-v5-5-r10": { + entryFile: "../hillstone__fw_v5-5-r10/bin/hillstone-fw-v5-5-r10.js", + serviceModule: "../hillstone__fw_v5-5-r10/src/service.js", + }, + "hillstone-fw-v5-5-r4": { + entryFile: "../hillstone__fw_v5-5-r4/bin/hillstone-fw-v5-5-r4.js", + serviceModule: "../hillstone__fw_v5-5-r4/src/service.js", + }, + "hillstone-fw-v5-5-r6": { + entryFile: "../hillstone__fw_v5-5-r6/bin/hillstone-fw-v5-5-r6.js", + serviceModule: "../hillstone__fw_v5-5-r6/src/service.js", + }, + "huawei-fw-usg6000e": { + entryFile: "../huawei__fw-usg6000e/bin/huawei-fw-usg6000e.js", + serviceModule: "../huawei__fw-usg6000e/src/service.js", + }, + "nsfocus-ads-v4-5-r90-f06": { + entryFile: "../nsfocus__ads_v4-5-r90-f06/bin/nsfocus-ads-v4-5-r90-f06.js", + serviceModule: "../nsfocus__ads_v4-5-r90-f06/src/service.js", + }, + "nsfocus-nips-v5-6-r11": { + entryFile: "../nsfocus__nips_v5-6-r11/bin/nsfocus-nips-v5-6-r11.js", + serviceModule: "../nsfocus__nips_v5-6-r11/src/service.js", + }, + "panabit-tang-r1": { + entryFile: "../panabit__tang-r1/bin/panabit-tang-r1.js", + serviceModule: "../panabit__tang-r1/src/service.js", + }, + "qianxin-fw-secgate3600": { + entryFile: "../qianxin__fw-secgate3600/bin/qianxin-fw-secgate3600.js", + serviceModule: "../qianxin__fw-secgate3600/src/service.js", + }, + "qianxin-fw-secgate3600-policy": { + entryFile: "../qianxin__fw-secgate3600-policy/bin/qianxin-fw-secgate3600-policy.js", + serviceModule: "../qianxin__fw-secgate3600-policy/src/service.js", + }, + "qianxin-vs-secvss3600": { + entryFile: "../qianxin__vs-secvss3600/bin/qianxin-vs-secvss3600.js", + serviceModule: "../qianxin__vs-secvss3600/src/service.js", + }, + "qianxin-tianyan-platform": { + entryFile: "../qianxin__tianyan-platform/bin/qianxin-tianyan-platform.js", + serviceModule: "../qianxin__tianyan-platform/src/service.js", + }, + "qianxin-fw-secgate3600-http-x": { + entryFile: "../qianxin__fw-secgate3600-http-x/bin/qianxin-fw-secgate3600-http-x.js", + serviceModule: "../qianxin__fw-secgate3600-http-x/src/service.js", + }, + "qiming-tianqing-waf": { + entryFile: "../qiming-tianqing__waf/bin/qiming-tianqing-waf.js", + serviceModule: "../qiming-tianqing__waf/src/service.js", + }, + "qingteng-hids-v3-4": { + entryFile: "../qingteng__hids_v3-4/bin/qingteng-hids-v3-4.js", + serviceModule: "../qingteng__hids_v3-4/src/service.js", + }, + "ray-waf-v6-1-2": { + entryFile: "../ray__waf_v6-1-2/bin/ray-waf-v6-1-2.js", + serviceModule: "../ray__waf_v6-1-2/src/service.js", + }, + "riversafe-waf": { + entryFile: "../riversafe__waf/bin/riversafe-waf.js", + serviceModule: "../riversafe__waf/src/service.js", + }, + "sangfor-fw-v8-0-45": { + entryFile: "../sangfor__fw_v8-0-45/bin/sangfor-fw-v8-0-45.js", + serviceModule: "../sangfor__fw_v8-0-45/src/service.js", + }, + "sangfor-sip": { + entryFile: "../sangfor__sip/bin/sangfor-sip.js", + serviceModule: "../sangfor__sip/src/service.js", + }, + "skycloud-inet": { + entryFile: "../skycloud__inet/bin/skycloud-inet.js", + serviceModule: "../skycloud__inet/src/service.js", + }, + "tencent-qyweixin-group-robot": { + entryFile: "../tencent__qyweixin-group-robot/bin/tencent-qyweixin-group-robot.js", + serviceModule: "../tencent__qyweixin-group-robot/src/service.js", + }, + "tencent-tsec-v2-5-1": { + entryFile: "../tencent__tsec_v2-5-1/bin/tencent-tsec-v2-5-1.js", + serviceModule: "../tencent__tsec_v2-5-1/src/service.js", + }, + "threatbook-cloudapi-v3": { + entryFile: "../threatbook__cloudapi_v3/bin/threatbook-cloudapi-v3.js", + serviceModule: "../threatbook__cloudapi_v3/src/service.js", + }, + "threatbook-onesig": { + entryFile: "../threatbook__onesig/bin/threatbook-onesig.js", + serviceModule: "../threatbook__onesig/src/service.js", + }, + "threatbook-tdp": { + entryFile: "../threatbook__tdp/bin/threatbook-tdp.js", + serviceModule: "../threatbook__tdp/src/service.js", + }, + "threatbook-tip-v4": { + entryFile: "../threatbook__tip_v4/bin/threatbook-tip-v4.js", + serviceModule: "../threatbook__tip_v4/src/service.js", + }, + "topsec-fw-2u": { + entryFile: "../topsec__fw-2u/bin/topsec-fw-2u.js", + serviceModule: "../topsec__fw-2u/src/service.js", + }, + "topsec-fw-5u": { + entryFile: "../topsec__fw-5u/bin/topsec-fw-5u.js", + serviceModule: "../topsec__fw-5u/src/service.js", + }, + "topsec-fw-v3-7-6": { + entryFile: "../topsec__fw_v3-7-6/bin/topsec-fw-v3-7-6.js", + serviceModule: "../topsec__fw_v3-7-6/src/service.js", + }, + "venus-ads-v3-6": { + entryFile: "../venus__ads_v3-6/bin/venus-ads-v3-6.js", + serviceModule: "../venus__ads_v3-6/src/service.js", + }, + "wangsu-label-ip": { + entryFile: "../wangsu__label-ip/bin/wangsu-label-ip.js", + serviceModule: "../wangsu__label-ip/src/service.js", + }, + "wd-k01": { + entryFile: "../wd__k01/bin/wd-k01.js", + serviceModule: "../wd__k01/src/service.js", + }, +}; + +const serviceNames = Object.keys(services); + +const program = new Command(); + +program + .name("octobus-tentacles") + .usage(" [args]") + .description("Run a service from this package") + .argument("[service]", "service name") + .allowUnknownOption(true) + .allowExcessArguments(true) + .passThroughOptions() + .addHelpText("after", ` +Services: +${serviceNames.map((name) => ` ${name.padEnd(37)}`).join("\n")} + +Use 'octobus-tentacles --help' to print service help.`) + .action(async (serviceName) => { + if (!serviceName) { + program.outputHelp(); + return; + } + + const selected = services[serviceName]; + if (!selected) { + process.stderr.write(`Unknown service: ${serviceName}\n\n`); + program.outputHelp({ error: true }); + process.exitCode = 1; + return; + } + + const { service } = await import(new URL(selected.serviceModule, import.meta.url)); + + await runServiceMain(service, { + argv: program.args.slice(1), + entryFile: fileURLToPath(new URL(selected.entryFile, import.meta.url)), + }); + }); + +await program.parseAsync(); diff --git a/services/bin/sangfor-sip.js b/services/bin/sangfor-sip.js new file mode 100644 index 00000000..b2cf4887 --- /dev/null +++ b/services/bin/sangfor-sip.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../sangfor__sip/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../sangfor__sip/bin/sangfor-sip.js", import.meta.url)), +}); diff --git a/services/package.json b/services/package.json index 9229d914..9224bcaa 100644 --- a/services/package.json +++ b/services/package.json @@ -1,142 +1,154 @@ -{ - "name": "@chaitin-ai/octobus-tentacles", - "version": "0.1.0", - "private": true, - "type": "module", - "bin": { - "octobus-tentacles": "bin/octobus-tentacles.js", - "das-gateway-v3": "bin/das-gateway-v3.js", - "das-tgfw-v6": "bin/das-tgfw-v6.js", - "dingtalk-group-robot": "bin/dingtalk-group-robot.js", - "dptech-eds": "bin/dptech-eds.js", - "dptech-fw-v4-6-10": "bin/dptech-fw-v4-6-10.js", - "dptech-umc-ads-v5-3-29": "bin/dptech-umc-ads-v5-3-29.js", - "feishu-group-robot": "bin/feishu-group-robot.js", - "fortinet-fw": "bin/fortinet-fw.js", - "fortinet-waf": "bin/fortinet-waf.js", - "hillstone-fw-v5-5-r4": "bin/hillstone-fw-v5-5-r4.js", - "hillstone-fw-v5-5-r6": "bin/hillstone-fw-v5-5-r6.js", - "hillstone-fw-v5-5-r10": "bin/hillstone-fw-v5-5-r10.js", - "huawei-fw-usg6000e": "bin/huawei-fw-usg6000e.js", - "nsfocus-ads-v4-5-r90-f06": "bin/nsfocus-ads-v4-5-r90-f06.js", - "nsfocus-nips-v5-6-r11": "bin/nsfocus-nips-v5-6-r11.js", - "panabit-tang-r1": "bin/panabit-tang-r1.js", - "qianxin-fw-secgate3600": "bin/qianxin-fw-secgate3600.js", - "qianxin-fw-secgate3600-http-x": "bin/qianxin-fw-secgate3600-http-x.js", - "qiming-tianqing-waf": "bin/qiming-tianqing-waf.js", - "qingteng-hids-v3-4": "bin/qingteng-hids-v3-4.js", - "ray-waf-v6-1-2": "bin/ray-waf-v6-1-2.js", - "riversafe-waf": "bin/riversafe-waf.js", - "safeline-waf": "bin/safeline-waf.js", - "safeline-waf-eliminate-false-positive": "bin/safeline-waf-eliminate-false-positive.js", - "sangfor-fw-v8-0-45": "bin/sangfor-fw-v8-0-45.js", - "skycloud-inet": "bin/skycloud-inet.js", - "tencent-qyweixin-group-robot": "bin/tencent-qyweixin-group-robot.js", - "tencent-tsec-v2-5-1": "bin/tencent-tsec-v2-5-1.js", - "threatbook-cloudapi-v3": "bin/threatbook-cloudapi-v3.js", - "threatbook-onesig": "bin/threatbook-onesig.js", - "threatbook-tdp": "bin/threatbook-tdp.js", - "threatbook-tip-v4": "bin/threatbook-tip-v4.js", - "topsec-fw-5u": "bin/topsec-fw-5u.js", - "topsec-fw-v3-7-6": "bin/topsec-fw-v3-7-6.js", - "topsec-fw-2u": "bin/topsec-fw-2u.js", - "venus-ads-v3-6": "bin/venus-ads-v3-6.js", - "wangsu-label-ip": "bin/wangsu-label-ip.js", - "wd-k01": "bin/wd-k01.js" - }, - "files": [ - "bin/das-gateway-v3.js", - "bin/das-tgfw-v6.js", - "bin/dingtalk-group-robot.js", - "bin/dptech-eds.js", - "bin/dptech-fw-v4-6-10.js", - "bin/dptech-umc-ads-v5-3-29.js", - "bin/feishu-group-robot.js", - "bin/fortinet-fw.js", - "bin/fortinet-waf.js", - "bin/hillstone-fw-v5-5-r4.js", - "bin/hillstone-fw-v5-5-r6.js", - "bin/hillstone-fw-v5-5-r10.js", - "bin/huawei-fw-usg6000e.js", - "bin/nsfocus-ads-v4-5-r90-f06.js", - "bin/nsfocus-nips-v5-6-r11.js", - "bin/panabit-tang-r1.js", - "bin/qianxin-fw-secgate3600.js", - "bin/qianxin-fw-secgate3600-http-x.js", - "bin/qiming-tianqing-waf.js", - "bin/qingteng-hids-v3-4.js", - "bin/ray-waf-v6-1-2.js", - "bin/riversafe-waf.js", - "bin/skycloud-inet.js", - "bin/safeline-waf-eliminate-false-positive.js", - "bin/safeline-waf.js", - "bin/sangfor-fw-v8-0-45.js", - "bin/tencent-qyweixin-group-robot.js", - "bin/tencent-tsec-v2-5-1.js", - "bin/threatbook-cloudapi-v3.js", - "bin/threatbook-onesig.js", - "bin/threatbook-tdp.js", - "bin/threatbook-tip-v4.js", - "bin/topsec-fw-2u.js", - "bin/topsec-fw-5u.js", - "bin/topsec-fw-v3-7-6.js", - "bin/venus-ads-v3-6.js", - "bin/wd-k01.js", - "bin/wangsu-label-ip.js", - "das__gateway_v3", - "das__tgfw_v6", - "dingtalk__group-robot", - "dptech__eds", - "dptech__fw_v4-6-10", - "dptech__umc-ads_v5-3-29", - "feishu__group-robot", - "fortinet__fw", - "fortinet__waf", - "hillstone__fw_v5-5-r4", - "hillstone__fw_v5-5-r6", - "hillstone__fw_v5-5-r10", - "huawei__fw-usg6000e", - "nsfocus__ads_v4-5-r90-f06", - "nsfocus__nips_v5-6-r11", - "panabit__tang-r1", - "qianxin__fw-secgate3600", - "qianxin__fw-secgate3600-http-x", - "qiming-tianqing__waf", - "qingteng__hids_v3-4", - "ray__waf_v6-1-2", - "riversafe__waf", - "skycloud__inet", - "chaitin__safeline-waf-eliminate-false-positive", - "chaitin__safeline-waf", - "sangfor__fw_v8-0-45", - "tencent__qyweixin-group-robot", - "tencent__tsec_v2-5-1", - "threatbook__cloudapi_v3", - "threatbook__onesig", - "threatbook__tdp", - "threatbook__tip_v4", - "topsec__fw-2u", - "topsec__fw-5u", - "topsec__fw_v3-7-6", - "venus__ads_v3-6", - "wd__k01", - "wangsu__label-ip", - "scripts", - "bin/octobus-tentacles.js" - ], - "scripts": { - "validate": "node scripts/validate-service-package.mjs", - "test": "node scripts/run-tests.mjs", - "coverage:all": "node scripts/run-coverage-all.mjs", - "import:check": "node scripts/import-check-all.mjs", - "pack:check": "npm pack --dry-run" - }, - "dependencies": { - "@chaitin-ai/octobus-sdk": "^0.5.0", - "commander": "^12.1.0" - }, - "bundledDependencies": [ - "@chaitin-ai/octobus-sdk", - "commander" - ] -} +{ + "name": "@chaitin-ai/octobus-tentacles", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { + "octobus-tentacles": "bin/octobus-tentacles.js", + "das-gateway-v3": "bin/das-gateway-v3.js", + "das-tgfw-v6": "bin/das-tgfw-v6.js", + "dingtalk-group-robot": "bin/dingtalk-group-robot.js", + "dptech-eds": "bin/dptech-eds.js", + "dptech-fw-v4-6-10": "bin/dptech-fw-v4-6-10.js", + "dptech-umc-ads-v5-3-29": "bin/dptech-umc-ads-v5-3-29.js", + "feishu-group-robot": "bin/feishu-group-robot.js", + "fortinet-fw": "bin/fortinet-fw.js", + "fortinet-waf": "bin/fortinet-waf.js", + "hillstone-fw-v5-5-r4": "bin/hillstone-fw-v5-5-r4.js", + "hillstone-fw-v5-5-r6": "bin/hillstone-fw-v5-5-r6.js", + "hillstone-fw-v5-5-r10": "bin/hillstone-fw-v5-5-r10.js", + "huawei-fw-usg6000e": "bin/huawei-fw-usg6000e.js", + "nsfocus-ads-v4-5-r90-f06": "bin/nsfocus-ads-v4-5-r90-f06.js", + "nsfocus-nips-v5-6-r11": "bin/nsfocus-nips-v5-6-r11.js", + "panabit-tang-r1": "bin/panabit-tang-r1.js", + "qianxin-fw-secgate3600": "bin/qianxin-fw-secgate3600.js", + "qianxin-fw-secgate3600-policy": "bin/qianxin-fw-secgate3600-policy.js", + "qianxin-vs-secvss3600": "bin/qianxin-vs-secvss3600.js", + "qianxin-tianyan-platform": "bin/qianxin-tianyan-platform.js", + "qianxin-fw-secgate3600-http-x": "bin/qianxin-fw-secgate3600-http-x.js", + "qiming-tianqing-waf": "bin/qiming-tianqing-waf.js", + "qingteng-hids-v3-4": "bin/qingteng-hids-v3-4.js", + "ray-waf-v6-1-2": "bin/ray-waf-v6-1-2.js", + "riversafe-waf": "bin/riversafe-waf.js", + "safeline-waf": "bin/safeline-waf.js", + "safeline-waf-eliminate-false-positive": "bin/safeline-waf-eliminate-false-positive.js", + "sangfor-fw-v8-0-45": "bin/sangfor-fw-v8-0-45.js", + "sangfor-sip": "bin/sangfor-sip.js", + "skycloud-inet": "bin/skycloud-inet.js", + "tencent-qyweixin-group-robot": "bin/tencent-qyweixin-group-robot.js", + "tencent-tsec-v2-5-1": "bin/tencent-tsec-v2-5-1.js", + "threatbook-cloudapi-v3": "bin/threatbook-cloudapi-v3.js", + "threatbook-onesig": "bin/threatbook-onesig.js", + "threatbook-tdp": "bin/threatbook-tdp.js", + "threatbook-tip-v4": "bin/threatbook-tip-v4.js", + "topsec-fw-5u": "bin/topsec-fw-5u.js", + "topsec-fw-v3-7-6": "bin/topsec-fw-v3-7-6.js", + "topsec-fw-2u": "bin/topsec-fw-2u.js", + "venus-ads-v3-6": "bin/venus-ads-v3-6.js", + "wangsu-label-ip": "bin/wangsu-label-ip.js", + "wd-k01": "bin/wd-k01.js" + }, + "files": [ + "bin/das-gateway-v3.js", + "bin/das-tgfw-v6.js", + "bin/dingtalk-group-robot.js", + "bin/dptech-eds.js", + "bin/dptech-fw-v4-6-10.js", + "bin/dptech-umc-ads-v5-3-29.js", + "bin/feishu-group-robot.js", + "bin/fortinet-fw.js", + "bin/fortinet-waf.js", + "bin/hillstone-fw-v5-5-r4.js", + "bin/hillstone-fw-v5-5-r6.js", + "bin/hillstone-fw-v5-5-r10.js", + "bin/huawei-fw-usg6000e.js", + "bin/nsfocus-ads-v4-5-r90-f06.js", + "bin/nsfocus-nips-v5-6-r11.js", + "bin/panabit-tang-r1.js", + "bin/qianxin-fw-secgate3600.js", + "bin/qianxin-fw-secgate3600-policy.js", + "bin/qianxin-vs-secvss3600.js", + "bin/qianxin-tianyan-platform.js", + "bin/qianxin-fw-secgate3600-http-x.js", + "bin/qiming-tianqing-waf.js", + "bin/qingteng-hids-v3-4.js", + "bin/ray-waf-v6-1-2.js", + "bin/riversafe-waf.js", + "bin/skycloud-inet.js", + "bin/safeline-waf-eliminate-false-positive.js", + "bin/safeline-waf.js", + "bin/sangfor-fw-v8-0-45.js", + "bin/sangfor-sip.js", + "bin/tencent-qyweixin-group-robot.js", + "bin/tencent-tsec-v2-5-1.js", + "bin/threatbook-cloudapi-v3.js", + "bin/threatbook-onesig.js", + "bin/threatbook-tdp.js", + "bin/threatbook-tip-v4.js", + "bin/topsec-fw-2u.js", + "bin/topsec-fw-5u.js", + "bin/topsec-fw-v3-7-6.js", + "bin/venus-ads-v3-6.js", + "bin/wd-k01.js", + "bin/wangsu-label-ip.js", + "das__gateway_v3", + "das__tgfw_v6", + "dingtalk__group-robot", + "dptech__eds", + "dptech__fw_v4-6-10", + "dptech__umc-ads_v5-3-29", + "feishu__group-robot", + "fortinet__fw", + "fortinet__waf", + "hillstone__fw_v5-5-r4", + "hillstone__fw_v5-5-r6", + "hillstone__fw_v5-5-r10", + "huawei__fw-usg6000e", + "nsfocus__ads_v4-5-r90-f06", + "nsfocus__nips_v5-6-r11", + "panabit__tang-r1", + "qianxin__fw-secgate3600", + "qianxin__fw-secgate3600-policy", + "qianxin__vs-secvss3600", + "qianxin__tianyan-platform", + "qianxin__fw-secgate3600-http-x", + "qiming-tianqing__waf", + "qingteng__hids_v3-4", + "ray__waf_v6-1-2", + "riversafe__waf", + "skycloud__inet", + "chaitin__safeline-waf-eliminate-false-positive", + "chaitin__safeline-waf", + "sangfor__fw_v8-0-45", + "sangfor__sip", + "tencent__qyweixin-group-robot", + "tencent__tsec_v2-5-1", + "threatbook__cloudapi_v3", + "threatbook__onesig", + "threatbook__tdp", + "threatbook__tip_v4", + "topsec__fw-2u", + "topsec__fw-5u", + "topsec__fw_v3-7-6", + "venus__ads_v3-6", + "wd__k01", + "wangsu__label-ip", + "scripts", + "bin/octobus-tentacles.js" + ], + "scripts": { + "validate": "node scripts/validate-service-package.mjs", + "test": "node scripts/run-tests.mjs", + "coverage:all": "node scripts/run-coverage-all.mjs", + "import:check": "node scripts/import-check-all.mjs", + "pack:check": "npm pack --dry-run" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.5.0", + "commander": "^12.1.0" + }, + "bundledDependencies": [ + "@chaitin-ai/octobus-sdk", + "commander" + ] +} diff --git a/services/sangfor__sip/README.md b/services/sangfor__sip/README.md new file mode 100644 index 00000000..4998a51a --- /dev/null +++ b/services/sangfor__sip/README.md @@ -0,0 +1,46 @@ +# sangfor-sip + +OctoBus service package for Sangfor SIP (Security Intelligence Platform / 深信服安全感知平台). + +Pulls security events, risk assets, risk terminals, server/terminal inventory, IP groups, and vulnerability data (weak passwords, CVEs, plaintext transmission) via the SIP third-party REST API. + +## Authentication + +POST `/sangforinter/v1/auth/party/login` with SHA1-based auth token: + +``` +auth = sha1(rand + password + "sangfor3party" + userName) +``` + +Credentials are configured via `secret.yaml`: + +```yaml +userName: <认证账号> +password: <认证密码> +platformName: <平台名称> +``` + +## Configuration + +```yaml +host: https://10.0.0.1:7443 +skipTlsVerify: true +``` + +## Methods + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GetSecurityEvents` | `GET /sangforinter/v1/data/riskevent` | Pull security events | +| `GetRiskBusiness` | `GET /sangforinter/v1/data/riskbusiness` | Pull risk business assets | +| `GetRiskTerminals` | `GET /sangforinter/v1/data/riskterminal` | Pull risk terminal assets | +| `GetServers` | `GET /sangforinter/v1/data/business` | Pull configured server assets | +| `GetTerminals` | `GET /sangforinter/v1/data/terminal` | Pull terminal assets | +| `GetIPGroups` | `GET /sangforinter/v1/data/ipgroup` | Pull monitored IP groups | +| `GetWeakPasswords` | `GET /sangforinter/v1/data/weakpasswd` | Pull weak password vulnerabilities | +| `GetVulnerabilities` | `GET /sangforinter/v1/data/hole` | Pull CVE vulnerability records | +| `GetPlaintextTransmissions` | `GET /sangforinter/v1/data/plaintexttransmission` | Pull plaintext transmission vulnerabilities | + +## API version + +Compatible with SIP v92+. Server port: `7443`. diff --git a/services/sangfor__sip/bin/sangfor-sip.js b/services/sangfor__sip/bin/sangfor-sip.js new file mode 100644 index 00000000..8e0066ef --- /dev/null +++ b/services/sangfor__sip/bin/sangfor-sip.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { runServiceMain } from '@chaitin-ai/octobus-sdk'; + +import { service } from '../src/service.js'; + +runServiceMain(service); diff --git a/services/sangfor__sip/config.schema.json b/services/sangfor__sip/config.schema.json new file mode 100644 index 00000000..478159d1 --- /dev/null +++ b/services/sangfor__sip/config.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "host": { + "type": "string", + "description": "Sangfor SIP base URL, e.g. https://10.0.0.1:7443" + }, + "baseUrl": { + "type": "string", + "description": "Alias for host." + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 10000, + "description": "HTTP timeout in milliseconds." + }, + "skipTlsVerify": { + "type": "boolean", + "default": true, + "description": "Skip TLS certificate verification (SIP often uses self-signed certs)." + } + } +} diff --git a/services/sangfor__sip/package.json b/services/sangfor__sip/package.json new file mode 100644 index 00000000..c74814ca --- /dev/null +++ b/services/sangfor__sip/package.json @@ -0,0 +1,12 @@ +{ + "name": "sangfor-sip", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "sangfor-sip": "bin/sangfor-sip.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.5.0" + } +} diff --git a/services/sangfor__sip/proto/sangfor_sip.proto b/services/sangfor__sip/proto/sangfor_sip.proto new file mode 100644 index 00000000..dcd1f0ed --- /dev/null +++ b/services/sangfor__sip/proto/sangfor_sip.proto @@ -0,0 +1,126 @@ +syntax = "proto3"; + +package SANGFOR_SIP; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +service SANGFOR_SIP { + rpc GetSecurityEvents (GetSecurityEventsRequest) returns (GetSecurityEventsResponse); + rpc GetRiskBusiness (GetRiskBusinessRequest) returns (GetRiskBusinessResponse); + rpc GetRiskTerminals (GetRiskTerminalsRequest) returns (GetRiskTerminalsResponse); + rpc GetServers (GetServersRequest) returns (GetServersResponse); + rpc GetTerminals (GetTerminalsRequest) returns (GetTerminalsResponse); + rpc GetIPGroups (GetIPGroupsRequest) returns (GetIPGroupsResponse); + rpc GetWeakPasswords (GetWeakPasswordsRequest) returns (GetWeakPasswordsResponse); + rpc GetVulnerabilities (GetVulnerabilitiesRequest) returns (GetVulnerabilitiesResponse); + rpc GetPlaintextTransmissions (GetPlaintextTransmissionsRequest) returns (GetPlaintextTransmissionsResponse); +} + +// GetSecurityEvents — pull security events (安全事件) +message GetSecurityEventsRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetSecurityEventsResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetRiskBusiness — pull risk business assets (风险业务) +message GetRiskBusinessRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetRiskBusinessResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetRiskTerminals — pull risk terminal assets (风险终端) +message GetRiskTerminalsRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetRiskTerminalsResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetServers — pull configured server assets (已配置服务器) +message GetServersRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetServersResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetTerminals — pull terminal assets (资产终端) +message GetTerminalsRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetTerminalsResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetIPGroups — pull monitored IP groups (受监控IP组) +message GetIPGroupsRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetIPGroupsResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetWeakPasswords — pull weak password vulnerabilities (弱密码) +message GetWeakPasswordsRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetWeakPasswordsResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetVulnerabilities — pull vulnerability information (漏洞) +message GetVulnerabilitiesRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetVulnerabilitiesResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} + +// GetPlaintextTransmissions — pull plaintext transmission vulnerabilities (明文传输) +message GetPlaintextTransmissionsRequest { + int64 from_time = 1; + int64 to_time = 2; + google.protobuf.Int32Value max_count = 3; +} + +message GetPlaintextTransmissionsResponse { + repeated google.protobuf.Value items = 1; + int32 count = 2; +} diff --git a/services/sangfor__sip/secret.schema.json b/services/sangfor__sip/secret.schema.json new file mode 100644 index 00000000..45935d31 --- /dev/null +++ b/services/sangfor__sip/secret.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "userName": { + "type": "string", + "description": "SIP authentication account (认证账号), must match platform configuration." + }, + "password": { + "type": "string", + "description": "SIP authentication password (认证密码), must match platform configuration." + }, + "platformName": { + "type": "string", + "description": "SIP unique platform name (平台名称), must match platform configuration." + } + } +} diff --git a/services/sangfor__sip/service.json b/services/sangfor__sip/service.json new file mode 100644 index 00000000..8a50a167 --- /dev/null +++ b/services/sangfor__sip/service.json @@ -0,0 +1,61 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "sangfor-sip", + "displayName": "Sangfor SIP", + "description": "OctoBus package for Sangfor SIP (Security Intelligence Platform): pull security events, risk assets/terminals, server/terminal inventory, IP groups, and vulnerability data (weak passwords, CVEs, plaintext transmission).", + "runtime": { + "mode": "long-running" + }, + "proto": { + "roots": [ + "proto" + ], + "files": [ + "proto/sangfor_sip.proto" + ] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "SANGFOR_SIP.SANGFOR_SIP/GetSecurityEvents": { + "name": "get-security-events", + "description": "Pull security events from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetRiskBusiness": { + "name": "get-risk-business", + "description": "Pull risk business assets from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetRiskTerminals": { + "name": "get-risk-terminals", + "description": "Pull risk terminal assets from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetServers": { + "name": "get-servers", + "description": "Pull configured server asset records from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetTerminals": { + "name": "get-terminals", + "description": "Pull terminal asset records from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetIPGroups": { + "name": "get-ip-groups", + "description": "Pull monitored IP group records from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetWeakPasswords": { + "name": "get-weak-passwords", + "description": "Pull weak password vulnerability records from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetVulnerabilities": { + "name": "get-vulnerabilities", + "description": "Pull vulnerability (CVE/hole) records from Sangfor SIP within a time range." + }, + "SANGFOR_SIP.SANGFOR_SIP/GetPlaintextTransmissions": { + "name": "get-plaintext-transmissions", + "description": "Pull plaintext transmission vulnerability records from Sangfor SIP within a time range." + } + } + } + } +} diff --git a/services/sangfor__sip/src/sangfor-sip.js b/services/sangfor__sip/src/sangfor-sip.js new file mode 100644 index 00000000..1b16bb47 --- /dev/null +++ b/services/sangfor__sip/src/sangfor-sip.js @@ -0,0 +1,225 @@ +import { createHash } from 'node:crypto'; +import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; + +const PKG = 'SANGFOR_SIP'; +const P = `/${PKG}.${PKG}/`; + +const PATHS = { + GET_SECURITY_EVENTS: `${P}GetSecurityEvents`, + GET_RISK_BUSINESS: `${P}GetRiskBusiness`, + GET_RISK_TERMINALS: `${P}GetRiskTerminals`, + GET_SERVERS: `${P}GetServers`, + GET_TERMINALS: `${P}GetTerminals`, + GET_IP_GROUPS: `${P}GetIPGroups`, + GET_WEAK_PASSWORDS: `${P}GetWeakPasswords`, + GET_VULNERABILITIES: `${P}GetVulnerabilities`, + GET_PLAINTEXT_TRANSMISSIONS: `${P}GetPlaintextTransmissions`, +}; + +const API_PATHS = { + auth: '/sangforinter/v1/auth/party/login', + securityEvents: '/sangforinter/v1/data/riskevent', + riskBusiness: '/sangforinter/v1/data/riskbusiness', + riskTerminals: '/sangforinter/v1/data/riskterminal', + servers: '/sangforinter/v1/data/business', + terminals: '/sangforinter/v1/data/terminal', + ipGroups: '/sangforinter/v1/data/ipgroup', + weakPasswords: '/sangforinter/v1/data/weakpasswd', + vulnerabilities: '/sangforinter/v1/data/hole', + plaintextTransmissions: '/sangforinter/v1/data/plaintexttransmission', +}; + +const grpcCodeFor = (code) => ({ + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + UNKNOWN: grpcStatus.UNKNOWN, +})[code] ?? grpcStatus.UNKNOWN; + +const err = (code, msg) => { + const e = new GrpcError(grpcCodeFor(code), `${code}: ${msg}`); + e.legacyCode = code; + return e; +}; + +const toValue = (val) => { + if (val === undefined || val === null) return { nullValue: 'NULL_VALUE' }; + if (typeof val === 'string') return { stringValue: val }; + if (typeof val === 'number') return { numberValue: val }; + if (typeof val === 'boolean') return { boolValue: val }; + if (Array.isArray(val)) return { listValue: { values: val.map(toValue) } }; + if (typeof val === 'object') { + const fields = {}; + for (const [k, v] of Object.entries(val)) fields[k] = toValue(v); + return { structValue: { fields } }; + } + return { stringValue: String(val) }; +}; + +const toInt = (val) => { + if (val === undefined || val === null) return null; + if (typeof val === 'object' && 'value' in val) return toInt(val.value); + const n = Number(val); + return Number.isFinite(n) ? Math.trunc(n) : null; +}; + +const unwrap = (val) => { + if (val === undefined || val === null) return undefined; + if (typeof val === 'object' && 'value' in val) return val.value == null ? undefined : String(val.value); + return String(val); +}; + +const firstDefined = (...vals) => vals.find((v) => v !== undefined && v !== null); + +const mergedBindings = (ctx = {}) => ({ + ...(ctx?.config ?? {}), + ...(ctx?.secret ?? {}), + ...(ctx?.bindings ?? {}), +}); + +const normalizeBaseUrl = (url) => { + const s = String(url ?? '').trim().replace(/\/+$/, ''); + if (!s || !/^https?:\/\//i.test(s)) return null; + return s; +}; + +export const sipAuth3 = (userName, password, rand) => { + return createHash('sha1') + .update(String(rand) + password + 'sangfor3party' + userName) + .digest('hex'); +}; + +export function rpcdef(ctx) { + const bindings = mergedBindings(ctx); + + const baseUrl = () => { + const u = normalizeBaseUrl(firstDefined(bindings.host, bindings.baseUrl, bindings.restBaseUrl)); + if (!u) throw err('INVALID_ARGUMENT', 'config.host is required (e.g. https://10.0.0.1:7443)'); + return u; + }; + + const doFetch = async (url, init) => { + try { + return await fetch(url, init); + } catch (e) { + throw err('UNAVAILABLE', e?.cause?.message || e?.message || 'fetch failed'); + } + }; + + const readJson = async (res) => { + let text; + try { text = await res.text(); } catch { throw err('UNKNOWN', 'failed to read response body'); } + if (res.status === 403) throw err('PERMISSION_DENIED', `http 403: ${text}`); + if (res.status >= 500) throw err('UNAVAILABLE', `http ${res.status}: ${text}`); + if (!text.trim()) throw err('UNKNOWN', 'empty response body'); + try { return JSON.parse(text); } catch { throw err('UNKNOWN', 'response is not valid JSON'); } + }; + + const getToken = async (base) => { + const userName = String(firstDefined(bindings.userName, bindings.username) ?? '').trim(); + const password = String(firstDefined(bindings.password) ?? '').trim(); + const platformName = String(firstDefined(bindings.platformName) ?? '').trim(); + + if (!userName) throw err('INVALID_ARGUMENT', 'secret.userName is required'); + if (!password) throw err('INVALID_ARGUMENT', 'secret.password is required'); + if (!platformName) throw err('INVALID_ARGUMENT', 'secret.platformName is required'); + + const rand = Math.floor(Math.random() * 2147483647); + const auth = sipAuth3(userName, password, rand); + + const body = JSON.stringify({ rand, userName, clientProduct: '', clientVersion: '', clientId: 0, desc: '', auth, platformName }); + const res = await doFetch(base + API_PATHS.auth, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + }); + + const json = await readJson(res); + if (json?.code === 13) throw err('PERMISSION_DENIED', json.message ?? 'SIP permission denied'); + if (json?.code !== 0) throw err('FAILED_PRECONDITION', `SIP auth failed: code=${json?.code} ${json?.message ?? ''}`); + + const token = json?.data?.token; + if (!token) throw err('UNKNOWN', 'SIP auth response missing token'); + return token; + }; + + const pullData = async (apiPath, req) => { + const base = baseUrl(); + const token = await getToken(base); + + const fromTime = toInt(firstDefined(req?.from_time)); + const toTime = toInt(firstDefined(req?.to_time)); + + if (fromTime === null) throw err('INVALID_ARGUMENT', 'from_time is required'); + if (toTime === null) throw err('INVALID_ARGUMENT', 'to_time is required'); + if (fromTime >= toTime) throw err('INVALID_ARGUMENT', 'from_time must be less than to_time'); + + const maxCount = toInt(firstDefined(req?.max_count)) ?? 2000; + + const params = new URLSearchParams({ token, fromActionTime: String(fromTime), toActionTime: String(toTime), maxCount: String(maxCount) }); + const res = await doFetch(`${base}${apiPath}?${params.toString()}`); + const json = await readJson(res); + + if (json?.code === 13) throw err('PERMISSION_DENIED', json.message ?? 'SIP permission denied'); + if (json?.code !== 0) throw err('FAILED_PRECONDITION', `SIP error: code=${json?.code} ${json?.message ?? ''}`); + + const items = Array.isArray(json?.data?.items) ? json.data.items : []; + const count = typeof json?.data?.count === 'number' ? json.data.count : items.length; + return { items: items.map(toValue), count }; + }; + + const callGetSecurityEvents = async (req) => pullData(API_PATHS.securityEvents, req); + const callGetRiskBusiness = async (req) => pullData(API_PATHS.riskBusiness, req); + const callGetRiskTerminals = async (req) => pullData(API_PATHS.riskTerminals, req); + const callGetServers = async (req) => pullData(API_PATHS.servers, req); + const callGetTerminals = async (req) => pullData(API_PATHS.terminals, req); + const callGetIPGroups = async (req) => pullData(API_PATHS.ipGroups, req); + const callGetWeakPasswords = async (req) => pullData(API_PATHS.weakPasswords, req); + const callGetVulnerabilities = async (req) => pullData(API_PATHS.vulnerabilities, req); + const callGetPlaintextTransmissions = async (req) => pullData(API_PATHS.plaintextTransmissions, req); + + return { + [PATHS.GET_SECURITY_EVENTS]: async () => callGetSecurityEvents(ctx.req), + [PATHS.GET_RISK_BUSINESS]: async () => callGetRiskBusiness(ctx.req), + [PATHS.GET_RISK_TERMINALS]: async () => callGetRiskTerminals(ctx.req), + [PATHS.GET_SERVERS]: async () => callGetServers(ctx.req), + [PATHS.GET_TERMINALS]: async () => callGetTerminals(ctx.req), + [PATHS.GET_IP_GROUPS]: async () => callGetIPGroups(ctx.req), + [PATHS.GET_WEAK_PASSWORDS]: async () => callGetWeakPasswords(ctx.req), + [PATHS.GET_VULNERABILITIES]: async () => callGetVulnerabilities(ctx.req), + [PATHS.GET_PLAINTEXT_TRANSMISSIONS]: async () => callGetPlaintextTransmissions(ctx.req), + }; +} + +const mergeCtx = (base, inner) => ({ + ...(base ?? {}), ...(inner ?? {}), + bindings: { ...(base?.bindings ?? {}), ...(inner?.bindings ?? {}) }, + config: { ...(base?.config ?? {}), ...(inner?.config ?? {}) }, + secret: { ...(base?.secret ?? {}), ...(inner?.secret ?? {}) }, + limits: inner?.limits ?? base?.limits ?? {}, + meta: inner?.meta ?? base?.meta ?? {}, +}); + +const resolveCallContext = (baseCtx, reqOrCtx, maybeCtx) => { + if (maybeCtx !== undefined) return { req: reqOrCtx ?? {}, ctx: mergeCtx(baseCtx, maybeCtx) }; + const inner = reqOrCtx ?? {}; + return { req: inner.request ?? inner.req ?? {}, ctx: mergeCtx(baseCtx, inner) }; +}; + +const wrapHandler = (baseCtx, path) => async (reqOrCtx, maybeCtx) => { + const { req, ctx } = resolveCallContext(baseCtx, reqOrCtx, maybeCtx); + return rpcdef({ ...ctx, req })[path](); +}; + +const registerHandlers = (ctx = {}) => Object.fromEntries( + Object.values(PATHS).map((p) => [p, wrapHandler(ctx, p)]), +); + +const sdkHandlers = registerHandlers({}); + +export const handlers = Object.fromEntries( + Object.values(PATHS).map((p) => [p.replace(/^\//, ''), sdkHandlers[p]]), +); + +export const _test = { sipAuth3, toValue, normalizeBaseUrl, PATHS, API_PATHS }; diff --git a/services/sangfor__sip/src/service.js b/services/sangfor__sip/src/service.js new file mode 100644 index 00000000..8e203a66 --- /dev/null +++ b/services/sangfor__sip/src/service.js @@ -0,0 +1,7 @@ +import { defineService } from '@chaitin-ai/octobus-sdk'; + +import { handlers } from './sangfor-sip.js'; + +export { handlers } from './sangfor-sip.js'; + +export const service = defineService({ handlers }); diff --git a/services/sangfor__sip/test/sangfor-sip.test.js b/services/sangfor__sip/test/sangfor-sip.test.js new file mode 100644 index 00000000..49c0acb3 --- /dev/null +++ b/services/sangfor__sip/test/sangfor-sip.test.js @@ -0,0 +1,712 @@ +import { describe, it, afterEach } from 'node:test'; +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; + +import { rpcdef, handlers, _test } from '../src/sangfor-sip.js'; + +const PKG = 'SANGFOR_SIP'; +const P = `/${PKG}.${PKG}/`; + +const PATH_GET_SECURITY_EVENTS = `${P}GetSecurityEvents`; +const PATH_GET_RISK_BUSINESS = `${P}GetRiskBusiness`; +const PATH_GET_RISK_TERMINALS = `${P}GetRiskTerminals`; +const PATH_GET_SERVERS = `${P}GetServers`; +const PATH_GET_TERMINALS = `${P}GetTerminals`; +const PATH_GET_IP_GROUPS = `${P}GetIPGroups`; +const PATH_GET_WEAK_PASSWORDS = `${P}GetWeakPasswords`; +const PATH_GET_VULNERABILITIES = `${P}GetVulnerabilities`; +const PATH_GET_PLAINTEXT_TRANSMISSIONS = `${P}GetPlaintextTransmissions`; + +const KEY_GET_SECURITY_EVENTS = `${PKG}.${PKG}/GetSecurityEvents`; +const KEY_GET_RISK_BUSINESS = `${PKG}.${PKG}/GetRiskBusiness`; +const KEY_GET_RISK_TERMINALS = `${PKG}.${PKG}/GetRiskTerminals`; +const KEY_GET_SERVERS = `${PKG}.${PKG}/GetServers`; +const KEY_GET_TERMINALS = `${PKG}.${PKG}/GetTerminals`; +const KEY_GET_IP_GROUPS = `${PKG}.${PKG}/GetIPGroups`; +const KEY_GET_WEAK_PASSWORDS = `${PKG}.${PKG}/GetWeakPasswords`; +const KEY_GET_VULNERABILITIES = `${PKG}.${PKG}/GetVulnerabilities`; +const KEY_GET_PLAINTEXT_TRANSMISSIONS = `${PKG}.${PKG}/GetPlaintextTransmissions`; + +// --------------------------------------------------------------------------- +// Mock helpers +// --------------------------------------------------------------------------- + +const AUTH_TOKEN_BODY = JSON.stringify({ + code: 0, + message: 'success', + data: { token: 'test_sip_token' }, +}); + +const makeHeaders = () => ({ get: () => null }); + +const makeSeqFetch = (apiBody, apiStatus = 200) => { + let call = 0; + return async (_url, _init) => { + call++; + if (call === 1) { + return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + } + const text = typeof apiBody === 'string' ? apiBody : JSON.stringify(apiBody); + return { ok: apiStatus >= 200 && apiStatus < 300, status: apiStatus, text: async () => text, headers: makeHeaders() }; + }; +}; + +const networkErrorFetch = async () => { throw new Error('ECONNREFUSED'); }; + +const buildCtx = (reqOverrides = {}, secretOverrides = {}) => ({ + bindings: { + host: 'https://sip.example.com:7443', + ...(secretOverrides), + }, + config: {}, + secret: { + userName: 'testuser', + password: 'testpass', + platformName: 'MyPlatform', + ...secretOverrides, + }, + limits: { timeoutMs: 5000 }, + meta: {}, + req: { + from_time: 1700000000, + to_time: 1700003600, + ...reqOverrides, + }, +}); + +const expectGrpcError = async (fn, legacyCode) => { + let caught; + try { await fn(); } catch (e) { caught = e; } + assert.ok(caught, 'expected function to throw'); + assert.ok(caught instanceof GrpcError, `expected GrpcError, got: ${caught}`); + assert.equal(caught.legacyCode, legacyCode, `expected legacyCode ${legacyCode}, got ${caught.legacyCode}`); + assert.match(caught.message, new RegExp(`^${legacyCode}:`)); +}; + +const originalFetch = globalThis.fetch; +test.afterEach(() => { globalThis.fetch = originalFetch; }); + +// --------------------------------------------------------------------------- +// Shape +// --------------------------------------------------------------------------- +test('rpcdef exposes all 9 paths', () => { + const def = rpcdef(buildCtx()); + assert.equal(typeof def[PATH_GET_SECURITY_EVENTS], 'function'); + assert.equal(typeof def[PATH_GET_RISK_BUSINESS], 'function'); + assert.equal(typeof def[PATH_GET_RISK_TERMINALS], 'function'); + assert.equal(typeof def[PATH_GET_SERVERS], 'function'); + assert.equal(typeof def[PATH_GET_TERMINALS], 'function'); + assert.equal(typeof def[PATH_GET_IP_GROUPS], 'function'); + assert.equal(typeof def[PATH_GET_WEAK_PASSWORDS], 'function'); + assert.equal(typeof def[PATH_GET_VULNERABILITIES], 'function'); + assert.equal(typeof def[PATH_GET_PLAINTEXT_TRANSMISSIONS], 'function'); +}); + +test('handlers exposes all 9 keys without leading slash', () => { + assert.equal(typeof handlers[KEY_GET_SECURITY_EVENTS], 'function'); + assert.equal(typeof handlers[KEY_GET_RISK_BUSINESS], 'function'); + assert.equal(typeof handlers[KEY_GET_RISK_TERMINALS], 'function'); + assert.equal(typeof handlers[KEY_GET_SERVERS], 'function'); + assert.equal(typeof handlers[KEY_GET_TERMINALS], 'function'); + assert.equal(typeof handlers[KEY_GET_IP_GROUPS], 'function'); + assert.equal(typeof handlers[KEY_GET_WEAK_PASSWORDS], 'function'); + assert.equal(typeof handlers[KEY_GET_VULNERABILITIES], 'function'); + assert.equal(typeof handlers[KEY_GET_PLAINTEXT_TRANSMISSIONS], 'function'); +}); + +// --------------------------------------------------------------------------- +// _test helpers +// --------------------------------------------------------------------------- +describe('sipAuth3', () => { + it('returns consistent hex string', () => { + const { sipAuth3 } = _test; + const h = sipAuth3('user1', 'pass1', 12345); + assert.equal(typeof h, 'string'); + assert.match(h, /^[0-9a-f]{40}$/); + }); + + it('is deterministic for same inputs', () => { + const { sipAuth3 } = _test; + assert.equal(sipAuth3('u', 'p', 999), sipAuth3('u', 'p', 999)); + }); + + it('differs on different password', () => { + const { sipAuth3 } = _test; + assert.notEqual(sipAuth3('u', 'pass1', 100), sipAuth3('u', 'pass2', 100)); + }); +}); + +describe('normalizeBaseUrl', () => { + it('removes trailing slash', () => { + assert.equal(_test.normalizeBaseUrl('https://sip.local:7443/'), 'https://sip.local:7443'); + }); + + it('returns null for empty', () => { + assert.equal(_test.normalizeBaseUrl(''), null); + }); + + it('returns null for non-http', () => { + assert.equal(_test.normalizeBaseUrl('ftp://host'), null); + }); + + it('accepts http', () => { + assert.equal(_test.normalizeBaseUrl('http://10.0.0.1:7443'), 'http://10.0.0.1:7443'); + }); +}); + +describe('toValue', () => { + const { toValue } = _test; + it('wraps string', () => { assert.deepEqual(toValue('abc'), { stringValue: 'abc' }); }); + it('wraps number', () => { assert.deepEqual(toValue(42), { numberValue: 42 }); }); + it('wraps boolean', () => { assert.deepEqual(toValue(true), { boolValue: true }); }); + it('wraps null', () => { assert.deepEqual(toValue(null), { nullValue: 'NULL_VALUE' }); }); + it('wraps array', () => { + const r = toValue([1, 'x']); + assert.ok(r.listValue); + assert.equal(r.listValue.values.length, 2); + }); + it('wraps object', () => { + const r = toValue({ a: 1 }); + assert.ok(r.structValue); + assert.deepEqual(r.structValue.fields.a, { numberValue: 1 }); + }); +}); + +// --------------------------------------------------------------------------- +// GetSecurityEvents +// --------------------------------------------------------------------------- +describe('GetSecurityEvents success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns items and count', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [ + { ip: '1.1.1.1', eventDes: 'test event', priority: 3 }, + { ip: '2.2.2.2', eventDes: 'another', priority: 1 }, + ], + count: 2, + device_info: { source: 'SIP' }, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const ctx = buildCtx(); + const r = await rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](); + assert.equal(r.count, 2); + assert.equal(r.items.length, 2); + assert.ok(r.items[0].structValue); + assert.deepEqual(r.items[0].structValue.fields.ip, { stringValue: '1.1.1.1' }); + }); + + it('handles empty items array', async () => { + const apiResp = { code: 0, message: 'success', data: { items: [], count: 0 } }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](); + assert.equal(r.count, 0); + assert.equal(r.items.length, 0); + }); + + it('uses custom maxCount', async () => { + let capturedUrl; + globalThis.fetch = async (url, init) => { + capturedUrl = url; + return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + }; + // First call is auth, second call is data + let callNum = 0; + globalThis.fetch = async (url, init) => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + capturedUrl = url; + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx({ max_count: { value: 500 } }))[PATH_GET_SECURITY_EVENTS](); + assert.ok(capturedUrl.includes('maxCount=500'), `expected maxCount=500 in ${capturedUrl}`); + }); + + it('sends token in query string', async () => { + let dataUrl; + let callNum = 0; + globalThis.fetch = async (url) => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + dataUrl = url; + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](); + assert.ok(dataUrl.includes('token=test_sip_token'), `expected token in URL: ${dataUrl}`); + }); +}); + +// --------------------------------------------------------------------------- +// GetSecurityEvents — validation errors +// --------------------------------------------------------------------------- +describe('GetSecurityEvents validation', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('throws INVALID_ARGUMENT when host missing', async () => { + globalThis.fetch = makeSeqFetch({}); + const ctx = buildCtx({}, { host: '', userName: 'u', password: 'p', platformName: 'plt' }); + ctx.secret = { userName: 'u', password: 'p', platformName: 'plt' }; + ctx.bindings = { host: '' }; + await expectGrpcError(() => rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](), 'INVALID_ARGUMENT'); + }); + + it('throws INVALID_ARGUMENT when userName missing', async () => { + globalThis.fetch = makeSeqFetch({}); + const ctx = buildCtx(); + ctx.secret = { password: 'p', platformName: 'plt' }; + ctx.bindings = { host: 'https://sip.example.com:7443' }; + await expectGrpcError(() => rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](), 'INVALID_ARGUMENT'); + }); + + it('throws INVALID_ARGUMENT when password missing', async () => { + globalThis.fetch = makeSeqFetch({}); + const ctx = buildCtx(); + ctx.secret = { userName: 'u', platformName: 'plt' }; + ctx.bindings = { host: 'https://sip.example.com:7443' }; + await expectGrpcError(() => rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](), 'INVALID_ARGUMENT'); + }); + + it('throws INVALID_ARGUMENT when platformName missing', async () => { + globalThis.fetch = makeSeqFetch({}); + const ctx = buildCtx(); + ctx.secret = { userName: 'u', password: 'p' }; + ctx.bindings = { host: 'https://sip.example.com:7443' }; + await expectGrpcError(() => rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](), 'INVALID_ARGUMENT'); + }); + + it('throws INVALID_ARGUMENT when from_time missing', async () => { + globalThis.fetch = makeSeqFetch({}); + const ctx = buildCtx({ from_time: null }); + await expectGrpcError(() => rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](), 'INVALID_ARGUMENT'); + }); + + it('throws INVALID_ARGUMENT when to_time missing', async () => { + globalThis.fetch = makeSeqFetch({}); + const ctx = buildCtx({ to_time: null }); + await expectGrpcError(() => rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](), 'INVALID_ARGUMENT'); + }); + + it('throws INVALID_ARGUMENT when from_time >= to_time', async () => { + globalThis.fetch = makeSeqFetch({}); + const ctx = buildCtx({ from_time: 1700003600, to_time: 1700000000 }); + await expectGrpcError(() => rpcdef(ctx)[PATH_GET_SECURITY_EVENTS](), 'INVALID_ARGUMENT'); + }); +}); + +// --------------------------------------------------------------------------- +// GetSecurityEvents — auth errors +// --------------------------------------------------------------------------- +describe('GetSecurityEvents auth errors', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('throws PERMISSION_DENIED on auth code 13', async () => { + globalThis.fetch = async () => ({ + ok: true, status: 200, + text: async () => JSON.stringify({ code: 13, message: 'permission denied' }), + headers: makeHeaders(), + }); + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'PERMISSION_DENIED'); + }); + + it('throws FAILED_PRECONDITION on non-zero auth code', async () => { + globalThis.fetch = async () => ({ + ok: true, status: 200, + text: async () => JSON.stringify({ code: 301, message: 'invalid argument' }), + headers: makeHeaders(), + }); + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'FAILED_PRECONDITION'); + }); + + it('throws UNKNOWN when auth response missing token', async () => { + globalThis.fetch = async () => ({ + ok: true, status: 200, + text: async () => JSON.stringify({ code: 0, message: 'success', data: {} }), + headers: makeHeaders(), + }); + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'UNKNOWN'); + }); +}); + +// --------------------------------------------------------------------------- +// GetSecurityEvents — API response errors +// --------------------------------------------------------------------------- +describe('GetSecurityEvents API response errors', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('throws PERMISSION_DENIED on HTTP 403 from data endpoint', async () => { + let callNum = 0; + globalThis.fetch = async () => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + return { ok: false, status: 403, text: async () => 'Forbidden', headers: makeHeaders() }; + }; + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'PERMISSION_DENIED'); + }); + + it('throws UNAVAILABLE on HTTP 500', async () => { + let callNum = 0; + globalThis.fetch = async () => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + return { ok: false, status: 500, text: async () => 'Internal Error', headers: makeHeaders() }; + }; + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'UNAVAILABLE'); + }); + + it('throws PERMISSION_DENIED on data code 13', async () => { + const apiResp = { code: 13, message: 'Permission denied!' }; + globalThis.fetch = makeSeqFetch(apiResp); + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'PERMISSION_DENIED'); + }); + + it('throws FAILED_PRECONDITION on data code 301', async () => { + const apiResp = { code: 301, message: 'Invalid argument' }; + globalThis.fetch = makeSeqFetch(apiResp); + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'FAILED_PRECONDITION'); + }); + + it('throws UNAVAILABLE on network error during data fetch', async () => { + let callNum = 0; + globalThis.fetch = async () => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + throw new Error('ECONNREFUSED'); + }; + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'UNAVAILABLE'); + }); + + it('throws UNAVAILABLE on network error during auth', async () => { + globalThis.fetch = networkErrorFetch; + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'UNAVAILABLE'); + }); + + it('throws UNKNOWN on non-JSON data response', async () => { + globalThis.fetch = makeSeqFetch('not json at all', 200); + await expectGrpcError(() => rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](), 'UNKNOWN'); + }); +}); + +// --------------------------------------------------------------------------- +// GetRiskBusiness +// --------------------------------------------------------------------------- +describe('GetRiskBusiness success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns items with risk business fields', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ ip: '10.0.0.1', riskLevel: 3, dealStatus: 0, groupName: 'biz1' }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_RISK_BUSINESS](); + assert.equal(r.count, 1); + assert.equal(r.items.length, 1); + assert.deepEqual(r.items[0].structValue.fields.ip, { stringValue: '10.0.0.1' }); + }); +}); + +// --------------------------------------------------------------------------- +// GetRiskTerminals +// --------------------------------------------------------------------------- +describe('GetRiskTerminals success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns items with risk terminal fields', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ ip: '192.168.1.50', fallLevel: 2, hostName: 'desktop1' }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_RISK_TERMINALS](); + assert.equal(r.count, 1); + assert.deepEqual(r.items[0].structValue.fields.hostName, { stringValue: 'desktop1' }); + }); +}); + +// --------------------------------------------------------------------------- +// GetServers +// --------------------------------------------------------------------------- +describe('GetServers success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns server asset records', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ assetIp: '10.1.0.5', system: 'linux', findType: 'auto', status: 0 }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_SERVERS](); + assert.equal(r.count, 1); + assert.deepEqual(r.items[0].structValue.fields.system, { stringValue: 'linux' }); + }); +}); + +// --------------------------------------------------------------------------- +// GetTerminals +// --------------------------------------------------------------------------- +describe('GetTerminals success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns terminal asset records', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ ip: '192.168.0.10', hostName: 'PC001', findType: 'manual', type: 3 }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_TERMINALS](); + assert.equal(r.count, 1); + assert.deepEqual(r.items[0].structValue.fields.hostName, { stringValue: 'PC001' }); + }); +}); + +// --------------------------------------------------------------------------- +// GetIPGroups +// --------------------------------------------------------------------------- +describe('GetIPGroups success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns IP group records', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ name: 'group1', type: 1, ipRange: ['10.0.0.0-10.255.255.255'] }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_IP_GROUPS](); + assert.equal(r.count, 1); + assert.deepEqual(r.items[0].structValue.fields.name, { stringValue: 'group1' }); + }); +}); + +// --------------------------------------------------------------------------- +// GetWeakPasswords +// --------------------------------------------------------------------------- +describe('GetWeakPasswords success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns weak password records', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ ip: '10.0.0.2', weakType: 'FTP登录弱密码', user: 'admin', dstPort: 21 }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_WEAK_PASSWORDS](); + assert.equal(r.count, 1); + assert.deepEqual(r.items[0].structValue.fields.weakType, { stringValue: 'FTP登录弱密码' }); + }); +}); + +// --------------------------------------------------------------------------- +// GetVulnerabilities +// --------------------------------------------------------------------------- +describe('GetVulnerabilities success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns vulnerability records', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ ip: '10.0.0.3', holeName: 'CVE-2021-1234', level: 3, holeId: 10010236 }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_VULNERABILITIES](); + assert.equal(r.count, 1); + assert.deepEqual(r.items[0].structValue.fields.holeName, { stringValue: 'CVE-2021-1234' }); + }); +}); + +// --------------------------------------------------------------------------- +// GetPlaintextTransmissions +// --------------------------------------------------------------------------- +describe('GetPlaintextTransmissions success', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('returns plaintext transmission records', async () => { + const apiResp = { + code: 0, message: 'success', + data: { + items: [{ ip: '10.0.0.4', url: 'http://internal.corp/login', level: 2, ruleId: '502001' }], + count: 1, + }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const r = await rpcdef(buildCtx())[PATH_GET_PLAINTEXT_TRANSMISSIONS](); + assert.equal(r.count, 1); + assert.deepEqual(r.items[0].structValue.fields.url, { stringValue: 'http://internal.corp/login' }); + }); +}); + +// --------------------------------------------------------------------------- +// handlers — invocation via SDK-style call +// --------------------------------------------------------------------------- +describe('handlers SDK-style call', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('GetSecurityEvents handler resolves via req+ctx args', async () => { + const apiResp = { + code: 0, message: 'success', + data: { items: [{ ip: '5.5.5.5' }], count: 1 }, + }; + globalThis.fetch = makeSeqFetch(apiResp); + const req = { from_time: 1700000000, to_time: 1700003600 }; + const ctx = { + bindings: { host: 'https://sip.example.com:7443' }, + secret: { userName: 'testuser', password: 'testpass', platformName: 'MyPlatform' }, + }; + const r = await handlers[KEY_GET_SECURITY_EVENTS](req, ctx); + assert.equal(r.count, 1); + }); + + it('GetRiskBusiness handler works via SDK-style call', async () => { + globalThis.fetch = makeSeqFetch({ code: 0, message: 'success', data: { items: [], count: 0 } }); + const r = await handlers[KEY_GET_RISK_BUSINESS]( + { from_time: 1700000000, to_time: 1700003600 }, + { bindings: { host: 'https://sip.example.com:7443' }, secret: { userName: 'u', password: 'p', platformName: 'plt' } }, + ); + assert.equal(r.count, 0); + }); +}); + +// --------------------------------------------------------------------------- +// Auth POST body validation +// --------------------------------------------------------------------------- +describe('Auth POST body', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('sends correct Content-Type and body fields', async () => { + let authBody; + let callNum = 0; + globalThis.fetch = async (url, init) => { + callNum++; + if (callNum === 1) { + authBody = JSON.parse(init.body); + return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + } + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](); + + assert.equal(authBody.userName, 'testuser'); + assert.equal(authBody.platformName, 'MyPlatform'); + assert.equal(typeof authBody.rand, 'number'); + assert.equal(typeof authBody.auth, 'string'); + assert.match(authBody.auth, /^[0-9a-f]{40}$/); + assert.equal(authBody.clientProduct, ''); + assert.equal(authBody.clientId, 0); + }); + + it('auth value matches sipAuth3 formula', async () => { + const { sipAuth3 } = _test; + let capturedRand, capturedAuth; + let callNum = 0; + globalThis.fetch = async (url, init) => { + callNum++; + if (callNum === 1) { + const body = JSON.parse(init.body); + capturedRand = body.rand; + capturedAuth = body.auth; + return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + } + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](); + assert.equal(capturedAuth, sipAuth3('testuser', 'testpass', capturedRand)); + }); +}); + +// --------------------------------------------------------------------------- +// URL and query string +// --------------------------------------------------------------------------- +describe('Data URL construction', () => { + afterEach(() => { globalThis.fetch = originalFetch; }); + + it('includes correct API path for security events', async () => { + let dataUrl; + let callNum = 0; + globalThis.fetch = async (url) => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + dataUrl = url; + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx())[PATH_GET_SECURITY_EVENTS](); + assert.ok(dataUrl.includes('/sangforinter/v1/data/riskevent'), `unexpected URL: ${dataUrl}`); + }); + + it('includes fromActionTime and toActionTime in query', async () => { + let dataUrl; + let callNum = 0; + globalThis.fetch = async (url) => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + dataUrl = url; + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx({ from_time: 1700000000, to_time: 1700003600 }))[PATH_GET_SECURITY_EVENTS](); + assert.ok(dataUrl.includes('fromActionTime=1700000000'), `expected fromActionTime in URL: ${dataUrl}`); + assert.ok(dataUrl.includes('toActionTime=1700003600'), `expected toActionTime in URL: ${dataUrl}`); + }); + + it('uses correct API path for vulnerabilities', async () => { + let dataUrl; + let callNum = 0; + globalThis.fetch = async (url) => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + dataUrl = url; + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx())[PATH_GET_VULNERABILITIES](); + assert.ok(dataUrl.includes('/sangforinter/v1/data/hole'), `unexpected URL: ${dataUrl}`); + }); + + it('uses correct API path for weak passwords', async () => { + let dataUrl; + let callNum = 0; + globalThis.fetch = async (url) => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + dataUrl = url; + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx())[PATH_GET_WEAK_PASSWORDS](); + assert.ok(dataUrl.includes('/sangforinter/v1/data/weakpasswd'), `unexpected URL: ${dataUrl}`); + }); + + it('uses correct API path for plaintext transmissions', async () => { + let dataUrl; + let callNum = 0; + globalThis.fetch = async (url) => { + callNum++; + if (callNum === 1) return { ok: true, status: 200, text: async () => AUTH_TOKEN_BODY, headers: makeHeaders() }; + dataUrl = url; + return { ok: true, status: 200, text: async () => JSON.stringify({ code: 0, message: 'success', data: { items: [], count: 0 } }), headers: makeHeaders() }; + }; + await rpcdef(buildCtx())[PATH_GET_PLAINTEXT_TRANSMISSIONS](); + assert.ok(dataUrl.includes('/sangforinter/v1/data/plaintexttransmission'), `unexpected URL: ${dataUrl}`); + }); +}); From 9e2a8580cd1c940acc75fc6d64f8a9921c56ea49 Mon Sep 17 00:00:00 2001 From: hilariacrucita <3362488788@qq.com> Date: Thu, 9 Jul 2026 14:56:57 +0800 Subject: [PATCH 2/3] fix(sangfor-sip): mark service bin entries as executable --- services/bin/sangfor-sip.js | 0 services/sangfor__sip/bin/sangfor-sip.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 services/bin/sangfor-sip.js mode change 100644 => 100755 services/sangfor__sip/bin/sangfor-sip.js diff --git a/services/bin/sangfor-sip.js b/services/bin/sangfor-sip.js old mode 100644 new mode 100755 diff --git a/services/sangfor__sip/bin/sangfor-sip.js b/services/sangfor__sip/bin/sangfor-sip.js old mode 100644 new mode 100755 From 0967cb37d2fbbaf786598d7a652dcd8b0e33129c Mon Sep 17 00:00:00 2001 From: hilariacrucita <3362488788@qq.com> Date: Thu, 9 Jul 2026 14:57:43 +0800 Subject: [PATCH 3/3] fix: mark service bin entries as executable for package validation --- services/bin/m01-intelligence.js | 0 services/bin/octobus-tentacles.js | 0 services/m01__intelligence/bin/m01-intelligence.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 services/bin/m01-intelligence.js mode change 100644 => 100755 services/bin/octobus-tentacles.js mode change 100644 => 100755 services/m01__intelligence/bin/m01-intelligence.js diff --git a/services/bin/m01-intelligence.js b/services/bin/m01-intelligence.js old mode 100644 new mode 100755 diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js old mode 100644 new mode 100755 diff --git a/services/m01__intelligence/bin/m01-intelligence.js b/services/m01__intelligence/bin/m01-intelligence.js old mode 100644 new mode 100755