diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 999d3589d..bc4618213 100755 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -513,6 +513,10 @@ const services = { entryFile: "../thehive__cortex/bin/cortex.js", serviceModule: "../thehive__cortex/src/service.js", }, + "vulnplatform-vuln": { + entryFile: "../vulnplatform__vulnerability-management_v3-2-0/bin/vulnplatform-vuln.js", + serviceModule: "../vulnplatform__vulnerability-management_v3-2-0/src/service.js", + }, "opencti": { entryFile: "../filigran__opencti/bin/opencti.js", serviceModule: "../filigran__opencti/src/service.js", diff --git a/services/bin/vulnplatform-vuln.js b/services/bin/vulnplatform-vuln.js new file mode 100755 index 000000000..d4ac9aded --- /dev/null +++ b/services/bin/vulnplatform-vuln.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../vulnplatform__vulnerability-management_v3-2-0/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../vulnplatform__vulnerability-management_v3-2-0/bin/vulnplatform-vuln.js", import.meta.url)), +}); diff --git a/services/package.json b/services/package.json index d076f7082..d7f2a932a 100644 --- a/services/package.json +++ b/services/package.json @@ -12,6 +12,7 @@ "octobus-tentacles": "bin/octobus-tentacles.js", "dbappsecurity-mingyu-waf": "bin/dbappsecurity-mingyu-waf.js", "misp": "bin/misp.js", + "vulnplatform-vuln": "bin/vulnplatform-vuln.js", "epp-360": "bin/360-epp.js", "ailpha-platform": "bin/ailpha-platform.js", "aliyun-waf3": "bin/aliyun-waf3.js", @@ -161,6 +162,8 @@ "bin/dbappsecurity-mingyu-waf.js", "misp__misp", "bin/misp.js", + "vulnplatform__vulnerability-management_v3-2-0", + "bin/vulnplatform-vuln.js", "360__360-epp_v10-0-0-08331", "bin/360-epp.js", "bin/ailpha-platform.js", diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/README.md b/services/vulnplatform__vulnerability-management_v3-2-0/README.md new file mode 100644 index 000000000..b04e88a15 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -0,0 +1,30 @@ +# Vulnerability Management Platform 3.2.0 + +OctoBus integration for the vendor Vulnerability Management Platform. It exposes 16 RPCs covering vulnerability lifecycle operations, asset inventory, and standard-vulnerability intelligence. + +## Configuration + +```json +{"apiBaseUrl":"https://vuln-platform.example.com","timeoutMs":10000,"skipTlsVerify":false} +``` + +Provide a platform-issued bearer token: + +```json +{"apiToken":"your-platform-token"} +``` + +The adapter intentionally does not execute the vendor JAR or accept appId/key/account credentials; this avoids exposing vendor secrets through process arguments. A configured bearer token is used directly and is not copied into a process-wide cache. + +`apiBaseUrl` must use HTTPS so the bearer token is never sent over plaintext networks. Plain HTTP is accepted only for the literal loopback addresses `127.0.0.1` and `[::1]` used by local tests; hostnames such as `localhost` are rejected because their resolution is environment-dependent. Embedded URL credentials are always rejected. `skipTlsVerify` defaults to false. If a trusted private installation requires a self-signed certificate, the service uses a local undici dispatcher and does not weaken Node’s process-wide TLS policy. + +## Import and validation + +```bash +octobus service import --id vulnplatform-vuln ./services/vulnplatform__vulnerability-management_v3-2-0 +cd services +npm run validate -- --service-dir vulnplatform__vulnerability-management_v3-2-0 +npm test -- --service-dir vulnplatform__vulnerability-management_v3-2-0 --coverage +``` + +All RPC handlers use the current single-context SDK ABI (`handler({request, config, secret})`). HTTP 401/403/404 map to authentication, authorization, and not-found gRPC errors; other 4xx map to failed precondition; network and 5xx failures map to unavailable; timeout maps to deadline exceeded. Upstream response bodies are never included in error messages. diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/bin/vulnplatform-vuln.js b/services/vulnplatform__vulnerability-management_v3-2-0/bin/vulnplatform-vuln.js new file mode 100755 index 000000000..508272f06 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/bin/vulnplatform-vuln.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../src/service.js"; + +runServiceMain(service); diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json new file mode 100644 index 000000000..2adca3097 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["apiBaseUrl"], + "properties": { + "apiBaseUrl": { + "type": "string", + "format": "uri", + "pattern": "^(?:[Hh][Tt][Tt][Pp][Ss]://(?:\\[[0-9A-Fa-f:.]+\\]|[^/@\\s:?#]+)(?::[0-9]+)?(?:[/?#][^\\s]*)?|[Hh][Tt][Tt][Pp]://(?:127\\.0\\.0\\.1|\\[(?:::0{0,3}1|(?:0{1,4}:){0,6}:0{0,3}1|(?:0{1,4}:){7}0{0,3}1)\\])(?::[0-9]+)?(?:[/?#][^\\s]*)?)$", + "description": "Vulnerability Management Platform URL. Runtime validation requires HTTPS, except for literal 127.0.0.1 and [::1] HTTP test endpoints, and rejects embedded credentials." + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "maximum": 120000, + "default": 10000, + "description": "Per-request upstream timeout in milliseconds." + }, + "skipTlsVerify": { + "type": "boolean", + "default": false, + "description": "Use only for a trusted private deployment with a self-signed certificate. TLS relaxation is isolated to this service." + } + } +} diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/package.json b/services/vulnplatform__vulnerability-management_v3-2-0/package.json new file mode 100644 index 000000000..65dbd439b --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/package.json @@ -0,0 +1,11 @@ +{ + "name": "vulnplatform-vuln", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { "vulnplatform-vuln": "bin/vulnplatform-vuln.js" }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.6.0", + "undici": "^7.16.0" + } +} diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/proto/vulnerability.proto b/services/vulnplatform__vulnerability-management_v3-2-0/proto/vulnerability.proto new file mode 100644 index 000000000..51bee37f4 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/proto/vulnerability.proto @@ -0,0 +1,505 @@ +syntax = "proto3"; +package vulnplatform.v1; + +option go_package = "github.com/chaitin/octobus/vulnplatform/v1;vulnplatformv1"; + +// ===== 漏洞管理服务 ===== + +service VulnerabilityService { + rpc ListVulnerabilities(ListVulnerabilitiesRequest) returns (ListVulnerabilitiesResponse); + rpc CreateVulnerability(CreateVulnerabilityRequest) returns (CreateVulnerabilityResponse); + rpc UpdateVulnerabilityStatus(UpdateVulnerabilityStatusRequest) returns (UpdateVulnerabilityStatusResponse); + rpc DeleteVulnerabilities(DeleteVulnerabilitiesRequest) returns (DeleteVulnerabilitiesResponse); + rpc GetVulnerabilityTimeline(GetVulnerabilityTimelineRequest) returns (GetVulnerabilityTimelineResponse); + rpc ListVulnerabilityTypes(ListVulnerabilityTypesRequest) returns (ListVulnerabilityTypesResponse); + rpc ListVulnerabilitiesGF(ListVulnerabilitiesGFRequest) returns (ListVulnerabilitiesGFResponse); +} + +service AssetService { + rpc ListGroups(ListGroupsRequest) returns (ListGroupsResponse); + rpc ListIPAssets(ListAssetsRequest) returns (ListIPAssetsResponse); + rpc ListWebAssets(ListAssetsRequest) returns (ListWebAssetsResponse); + rpc ListRepoAssets(ListAssetsRequest) returns (ListRepoAssetsResponse); + rpc ListComponentAssets(ListAssetsRequest) returns (ListComponentAssetsResponse); + rpc ListContainerAssets(ListAssetsRequest) returns (ListContainerAssetsResponse); +} + +service IntelligenceService { + rpc ListStandardVulnerabilities(ListStandardVulnerabilitiesRequest) returns (ListStandardVulnerabilitiesResponse); + rpc CreateStandardVulnerability(CreateStandardVulnerabilityRequest) returns (CreateStandardVulnerabilityResponse); + rpc DeleteStandardVulnerabilities(DeleteStandardVulnerabilitiesRequest) returns (DeleteStandardVulnerabilitiesResponse); +} + +message Group { + int32 id = 1; + string group_name = 2 [json_name = "groupName"]; + int32 parent_id = 3 [json_name = "parentId"]; + int32 has_data = 4 [json_name = "hasData"]; + int32 subset = 5; + repeated Group children = 6; +} + +message Port { + string port = 1; + string protocol = 2; + string service = 3; + string create_time = 4 [json_name = "createTime"]; + string update_time = 5 [json_name = "updateTime"]; +} + +message Product { + string product_name = 1 [json_name = "productName"]; + string product_version = 2 [json_name = "productVersion"]; + string manufacturer = 3; +} + +message VulLabel { + string label = 1; +} + +message WebRequest { + string output = 1; + string test_request = 2 [json_name = "testRequest"]; + string test_response = 3 [json_name = "testResponse"]; +} + +message ListVulnerabilitiesRequest { + int32 group_id = 1 [json_name = "groupId"]; + string group_path = 2 [json_name = "groupPath"]; + repeated string assets = 3; + int32 current = 4; + int32 size = 5; + string asset_type = 6 [json_name = "assetType"]; + string vkb = 7; + string port = 8; + string protocol = 9; + string service = 10; + string update_time_start = 11 [json_name = "updateTimeStart"]; + string update_time_end = 12 [json_name = "updateTimeEnd"]; +} + +message Vulnerability { + int64 id = 1; + int64 std_flaw_id = 2 [json_name = "stdFlawId"]; + int64 asset_id = 3 [json_name = "assetId"]; + int32 asset_type = 4 [json_name = "assetType"]; + string group_name = 5 [json_name = "groupName"]; + string path = 6; + string name = 7; + string severity = 8; + double risk_score = 9 [json_name = "riskScore"]; + string cve = 10; + string cnvd = 11; + string cnnvd = 12; + string cwe = 13; + string vul_type = 14 [json_name = "vulType"]; + int32 cross_discovery = 15 [json_name = "crossDiscovery"]; + string first_discovery_time = 16 [json_name = "firstDiscoveryTime"]; + string last_discovery_time = 17 [json_name = "lastDiscoveryTime"]; + int32 discovery_status = 18 [json_name = "discoveryStatus"]; + int32 status = 19; + string solution = 20; + string description = 21; + string bugtraq = 22; + string vkb = 23; + string flaw_source = 24 [json_name = "flawSource"]; + string publish_date = 25 [json_name = "publishDate"]; + string affected_url = 26 [json_name = "affectedUrl"]; + string priority = 27; + string update_time = 28 [json_name = "updateTime"]; + string asset = 29; + string system_name = 30 [json_name = "systemName"]; + string request_rectification_time = 31 [json_name = "requestRectificationTime"]; + repeated Port port_list = 32 [json_name = "portList"]; + repeated Product products = 33; + repeated WebRequest web_request_list = 34 [json_name = "webRequestList"]; + repeated VulLabel vul_label_list = 35 [json_name = "vulLabelList"]; + double cvss_score = 36 [json_name = "cvssScore"]; + string cvss_vector = 37 [json_name = "cvssVector"]; + double cvss_score3 = 38 [json_name = "cvssScore3"]; + string cvss_vector3 = 39 [json_name = "cvssVector3"]; +} + +message ListVulnerabilitiesResponse { + repeated Vulnerability records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message CreateVulnerabilityRequest { + int64 asset_id = 1 [json_name = "assetId"]; + int32 asset_type = 2 [json_name = "assetType"]; + string name = 3; + string cve = 4; + string cnvd = 5; + string cnnvd = 6; + string cvss_vector = 7 [json_name = "cvssVector"]; + double cvss_score = 8 [json_name = "cvssScore"]; + string cvss_vector3 = 9 [json_name = "cvssVector3"]; + double cvss_score3 = 10 [json_name = "cvssScore3"]; + string publish_date = 11 [json_name = "publishDate"]; + string description = 12; + string reference = 13; + string solution = 14; + string vul_type = 15 [json_name = "vulType"]; + string severity = 16; + string source = 17; + string full_url = 18 [json_name = "fullUrl"]; +} + +message CreateVulnerabilityResponse { + int64 vul_id = 1 [json_name = "vulId"]; +} + +message UpdateVulnerabilityStatusRequest { + repeated StatusUpdate updates = 1; +} + +message StatusUpdate { + int64 vul_id = 1 [json_name = "vulId"]; + int32 status = 2; +} + +message UpdateVulnerabilityStatusResponse { + int32 code = 1; + string message = 2; +} + +message DeleteVulnerabilitiesRequest { + repeated int64 vul_id_list = 1 [json_name = "vulIdList"]; +} + +message DeleteVulnerabilitiesResponse { + int32 code = 1; + string message = 2; +} + +message GetVulnerabilityTimelineRequest { + int64 flaw_id = 1 [json_name = "flawId"]; +} + +message TimelineEvent { + string action = 1; + string action_result = 2 [json_name = "actionResult"]; + string vul_status_old = 3 [json_name = "vulStatusOld"]; + string vul_status_new = 4 [json_name = "vulStatusNew"]; + string operator = 5; + string operator_name = 6 [json_name = "operatorName"]; + string assignee = 7; + string update_time = 8 [json_name = "updateTime"]; + string action_name = 9 [json_name = "actionName"]; + string log_type = 10 [json_name = "logType"]; + string remark = 11; +} + +message GetVulnerabilityTimelineResponse { + repeated TimelineEvent events = 1; +} + +message ListVulnerabilityTypesRequest {} + +message ListVulnerabilityTypesResponse { + repeated string types = 1; +} + +message ListVulnerabilitiesGFRequest { + int32 current = 1; + int32 size = 2; +} + +message VulnerabilityGF { + int64 id = 1; + string flaw_classify = 2 [json_name = "flawClassify"]; + string name = 3; + string priority = 4; + string status = 5; + string remaining_fix_time = 6 [json_name = "remainingFixTime"]; + string request_rectification_time = 7 [json_name = "requestRectificationTime"]; + string flaw_user_name = 8 [json_name = "flawUserName"]; + string flaw_source = 9 [json_name = "flawSource"]; + string description = 10; + string solution = 11; + string planned_testing_time = 12 [json_name = "plannedTestingTime"]; + string planned_production_time = 13 [json_name = "plannedProductionTime"]; + string whether_delayed = 14 [json_name = "whetherDelayed"]; + string number_of_delays = 15 [json_name = "numberOfDelays"]; + string star_url = 16 [json_name = "starUrl"]; + string system_name = 17 [json_name = "systemName"]; + string action = 18; +} + +message ListVulnerabilitiesGFResponse { + repeated VulnerabilityGF records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message ListGroupsRequest { + string group_path = 1 [json_name = "groupPath"]; +} + +message ListGroupsResponse { + repeated Group groups = 1; +} + +message ListAssetsRequest { + int32 group_id = 1 [json_name = "groupId"]; + string group_path = 2 [json_name = "groupPath"]; + repeated string assets = 3; + int32 current = 4; + int32 size = 5; + string update_time_start = 6 [json_name = "updateTimeStart"]; + string update_time_end = 7 [json_name = "updateTimeEnd"]; +} + +message IPAsset { + int64 id = 1; + string name = 2; + int32 group_id = 3 [json_name = "groupId"]; + string group_name = 4 [json_name = "groupName"]; + string ipv4 = 5; + string ipv6 = 6; + string path = 7; + string os = 8; + string device_type = 9 [json_name = "deviceType"]; + int32 grade = 10; + int32 criticality = 11; + int32 security = 12; + int32 status = 13; + string source = 14; + string update_time = 15 [json_name = "updateTime"]; + string asset_user_name = 16 [json_name = "assetUserName"]; + string product_uuid = 17 [json_name = "productUuid"]; + string colony = 18; + string tags = 19; + repeated Port ports = 20; + repeated Product products = 21; +} + +message ListIPAssetsResponse { + repeated IPAsset records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message WebAsset { + int64 id = 1; + string name = 2; + int32 group_id = 3 [json_name = "groupId"]; + string group_name = 4 [json_name = "groupName"]; + string start_url = 5 [json_name = "startUrl"]; + string path = 6; + string domain_name = 7 [json_name = "domainName"]; + int32 grade = 8; + int32 criticality = 9; + int32 security = 10; + int32 status = 11; + string source = 12; + string update_time = 13 [json_name = "updateTime"]; + string asset_user_name = 14 [json_name = "assetUserName"]; + string product_uuid = 15 [json_name = "productUuid"]; + string colony = 16; + string tags = 17; +} + +message ListWebAssetsResponse { + repeated WebAsset records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message RepoAsset { + int64 id = 1; + string name = 2; + int32 group_id = 3 [json_name = "groupId"]; + string group_name = 4 [json_name = "groupName"]; + string path = 5; + string source = 6; + string system_name = 7 [json_name = "systemName"]; + string update_time = 8 [json_name = "updateTime"]; + string asset_user_name = 9 [json_name = "assetUserName"]; + string url = 10; + string branch = 11; + string language = 12; + int32 repo_type = 13 [json_name = "repoType"]; + int32 files = 14; + int32 commits = 15; + string risk = 16; + string description = 17; + int32 size = 18; + string tags = 19; +} + +message ListRepoAssetsResponse { + repeated RepoAsset records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message ComponentAsset { + int64 id = 1; + string name = 2; + string vendor = 3; + string version = 4; + int32 group_id = 5 [json_name = "groupId"]; + string group_name = 6 [json_name = "groupName"]; + string path = 7; + string source = 8; + int32 host_type = 9 [json_name = "hostType"]; + string host_name = 10 [json_name = "hostName"]; + string update_time = 11 [json_name = "updateTime"]; + string asset_user_name = 12 [json_name = "assetUserName"]; + string scan_time = 13 [json_name = "scanTime"]; + string remark = 14; + string serverity = 15; + int32 depend_type = 16 [json_name = "dependType"]; + int32 depth = 17; + int32 integrity_check = 18 [json_name = "integrityCheck"]; + string tags = 19; +} + +message ListComponentAssetsResponse { + repeated ComponentAsset records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message ContainerAsset { + int64 id = 1; + string name = 2; + int32 group_id = 3 [json_name = "groupId"]; + string group_name = 4 [json_name = "groupName"]; + string path = 5; + string source = 6; + string system_name = 7 [json_name = "systemName"]; + string update_time = 8 [json_name = "updateTime"]; + string asset_user_name = 9 [json_name = "assetUserName"]; + string host_ip = 10 [json_name = "hostIp"]; + string image_repository = 11 [json_name = "imageRepository"]; + string image_tag = 12 [json_name = "imageTag"]; + string created = 13; + int32 status = 14; + string status_time = 15 [json_name = "statusTime"]; + int32 restart_count = 16 [json_name = "restartCount"]; + string restart_policy = 17 [json_name = "restartPolicy"]; + int32 size = 18; + repeated Port ports = 19; + repeated Mount mounts_addr_list = 20 [json_name = "mountsAddrList"]; + repeated AssetNetwork asset_networks = 21 [json_name = "assetNetworks"]; + string tags = 22; +} + +message Mount { + string source = 1; + string target = 2; +} + +message AssetNetwork { + string name = 1; + string ipv4 = 2; + string type = 3; +} + +message ListContainerAssetsResponse { + repeated ContainerAsset records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message ListStandardVulnerabilitiesRequest { + int32 current = 1; + int32 size = 2; + string vul_name = 3 [json_name = "vulName"]; + string priority_field = 4 [json_name = "priorityField"]; + string vendor = 5; + string product = 6; + string version = 7; +} + +message StandardVulnerability { + int64 id = 1; + string name = 2; + string vkb = 3; + string cve = 4; + string cwe = 5; + string cnvd = 6; + string cnnvd = 7; + string severity = 8; + string cnnvd_severity = 9 [json_name = "cnnvdSeverity"]; + string publish_date = 10 [json_name = "publishDate"]; + string description = 11; + string solution = 12; + string verify_info = 13 [json_name = "verifyInfo"]; + string reference = 14; + string bug_traq = 15 [json_name = "bugTraq"]; + string vul_type = 16 [json_name = "vulType"]; + string cvss_vector = 17 [json_name = "cvssVector"]; + double cvss_score = 18 [json_name = "cvssScore"]; + string cvss_vector3 = 19 [json_name = "cvssVector3"]; + double cvss_score3 = 20 [json_name = "cvssScore3"]; + int32 data_source = 21 [json_name = "dataSource"]; + repeated VulLabel vul_label_list = 22 [json_name = "vulLabelList"]; + repeated CPEData cpe_data_list = 23 [json_name = "cpeDataList"]; +} + +message CPEData { + string vendor = 1; + string product = 2; + string version = 3; +} + +message ListStandardVulnerabilitiesResponse { + repeated StandardVulnerability records = 1; + int32 total = 2; + int32 size = 3; + int32 current = 4; + int32 pages = 5; +} + +message CreateStandardVulnerabilityRequest { + string name = 1; + string cve = 2; + string cwe = 3; + string cnvd = 4; + string cnnvd = 5; + string severity = 6; + string cnnvd_severity = 7 [json_name = "cnnvdSeverity"]; + string publish_date = 8 [json_name = "publishDate"]; + string description = 9; + string solution = 10; + string verify_info = 11 [json_name = "verifyInfo"]; + string reference = 12; + string bug_traq = 13 [json_name = "bugTraq"]; + string vul_type = 14 [json_name = "vulType"]; + string cvss_vector = 15 [json_name = "cvssVector"]; + double cvss_score = 16 [json_name = "cvssScore"]; + string cvss_vector3 = 17 [json_name = "cvssVector3"]; + double cvss_score3 = 18 [json_name = "cvssScore3"]; +} + +message CreateStandardVulnerabilityResponse { + int64 standard_vul_id = 1 [json_name = "standardVulId"]; +} + +message DeleteStandardVulnerabilitiesRequest { + repeated int64 standard_id_list = 1 [json_name = "standardIdList"]; +} + +message DeleteStandardVulnerabilitiesResponse { + int32 code = 1; + string message = 2; +} diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json new file mode 100644 index 000000000..9b28a9bff --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["apiToken"], + "properties": { + "apiToken": { + "type": "string", + "description": "Bearer token supplied by the platform. Prefer this option when a long-lived service token is available." + } + } +} diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/service.json b/services/vulnplatform__vulnerability-management_v3-2-0/service.json new file mode 100644 index 000000000..c8c0d6df5 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/service.json @@ -0,0 +1,22 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "vulnplatform-vuln", + "displayName": "Vulnerability Management Platform 3.2.0", + "description": "Vulnerability, asset, and intelligence operations for Vulnerability Management Platform 3.2.0.", + "runtime": { "mode": "long-running" }, + "proto": { + "roots": ["proto"], + "files": ["proto/vulnerability.proto"] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "vulnplatform.v1.VulnerabilityService/ListVulnerabilities": { "name": "list-vulnerabilities", "description": "List vulnerabilities." }, + "vulnplatform.v1.AssetService/ListGroups": { "name": "list-groups", "description": "List asset groups." }, + "vulnplatform.v1.IntelligenceService/ListStandardVulnerabilities": { "name": "list-standard-vulnerabilities", "description": "List standard vulnerabilities." } + } + } + } +} diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js new file mode 100644 index 000000000..b22d36b02 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -0,0 +1,123 @@ +import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; +import { Agent } from "undici"; + +const DEFAULT_TIMEOUT_MS = 10000; +let insecureDispatcher; + +const codes = { + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + UNAUTHENTICATED: grpcStatus.UNAUTHENTICATED, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + NOT_FOUND: grpcStatus.NOT_FOUND, + DEADLINE_EXCEEDED: grpcStatus.DEADLINE_EXCEEDED, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + RESOURCE_EXHAUSTED: grpcStatus.RESOURCE_EXHAUSTED, + UNKNOWN: grpcStatus.UNKNOWN, +}; + +export function serviceError(code, message) { + return new GrpcError(codes[code] ?? grpcStatus.UNKNOWN, `${code}: ${message}`); +} + +function text(value) { + return value == null ? "" : String(value).trim(); +} + +function bindings(ctx = {}) { + return { ...(ctx.config ?? {}), ...(ctx.secret ?? {}), ...(ctx.bindings ?? {}) }; +} + +function timeoutMs(value) { + const timeout = Number(value); + return Number.isInteger(timeout) && timeout > 0 && timeout <= 120000 ? timeout : DEFAULT_TIMEOUT_MS; +} + +function endpoint(value) { + const raw = text(value).replace(/\/+$/, ""); + try { + const url = new URL(raw); + const secureHTTPS = url.protocol === "https:" && /^https:\/\/[^/]/i.test(raw) && Boolean(url.hostname); + const loopbackHTTP = url.protocol === "http:" && + /^http:\/\/(?:127\.0\.0\.1|\[[^\]]*\])(?::[0-9]+)?(?:[/?#]|$)/i.test(raw) && + (url.hostname === "127.0.0.1" || url.hostname === "[::1]"); + if ((!secureHTTPS && !loopbackHTTP) || url.username || url.password) throw new Error("unsupported URL"); + return raw; + } catch { + throw serviceError("FAILED_PRECONDITION", "apiBaseUrl must use HTTPS (HTTP is allowed only for loopback) and must not contain credentials"); + } +} + +function isTimeout(error) { + for (let current = error; current; current = current.cause) { + if (["AbortError", "TimeoutError"].includes(current.name) || current.code === "UND_ERR_CONNECT_TIMEOUT") return true; + } + return false; +} + +function errorForStatus(status) { + if (status === 401) return "UNAUTHENTICATED"; + if (status === 403) return "PERMISSION_DENIED"; + if (status === 404) return "NOT_FOUND"; + if (status === 429) return "RESOURCE_EXHAUSTED"; + if (status >= 400 && status < 500) return "FAILED_PRECONDITION"; + return "UNAVAILABLE"; +} + +export class PlatformClient { + constructor(ctx = {}) { + this.ctx = ctx; + this.bindings = bindings(ctx); + this.baseURL = endpoint(this.bindings.apiBaseUrl); + this.timeoutMs = timeoutMs(this.bindings.timeoutMs ?? ctx.limits?.timeoutMs); + } + + async bearerToken() { + const configured = text(this.bindings.apiToken); + if (!configured) throw serviceError("FAILED_PRECONDITION", "configure apiToken"); + return configured; + } + + async request(path, body, { method = "POST", authorization = true } = {}) { + const headers = { Accept: "application/json", "Content-Type": "application/json" }; + if (authorization) headers.Authorization = `Bearer ${await this.bearerToken()}`; + const options = { + method, + headers, + body: method === "GET" ? undefined : JSON.stringify(body ?? {}), + signal: AbortSignal.timeout(this.timeoutMs), + }; + if (this.bindings.skipTlsVerify === true || this.bindings.tlsInsecureSkipVerify === true) { + insecureDispatcher ??= new Agent({ connect: { rejectUnauthorized: false } }); + options.dispatcher = insecureDispatcher; + } + let response; + try { + response = await fetch(`${this.baseURL}${path}`, options); + } catch (error) { + throw serviceError(isTimeout(error) ? "DEADLINE_EXCEEDED" : "UNAVAILABLE", isTimeout(error) ? `timeout after ${this.timeoutMs}ms` : "upstream request failed"); + } + let raw; + try { + raw = await response.text(); + } catch (error) { + throw serviceError(isTimeout(error) ? "DEADLINE_EXCEEDED" : "UNAVAILABLE", isTimeout(error) ? `timeout after ${this.timeoutMs}ms` : "failed to read upstream response"); + } + if (!response.ok) { + throw serviceError(errorForStatus(response.status), `upstream returned HTTP ${response.status}`); + } + if (!raw.trim()) throw serviceError("UNKNOWN", "upstream returned an empty response"); + try { + const parsed = JSON.parse(raw); + if (parsed?.success === false || (parsed?.code != null && Number(parsed.code) >= 400)) { + throw serviceError("FAILED_PRECONDITION", "platform rejected the request"); + } + return parsed; + } catch (error) { + if (error instanceof GrpcError) throw error; + throw serviceError("UNKNOWN", "upstream response is not valid JSON"); + } + } +} + +export const _test = { bindings, endpoint, timeoutMs, isTimeout, errorForStatus }; diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/service.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/service.js new file mode 100644 index 000000000..6e855fd90 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/service.js @@ -0,0 +1,6 @@ +import { defineService } from "@chaitin-ai/octobus-sdk"; + +import { handlers } from "./vulnplatform-vuln.js"; + +export { handlers } from "./vulnplatform-vuln.js"; +export const service = defineService({ handlers }); diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/vulnplatform-vuln.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/vulnplatform-vuln.js new file mode 100644 index 000000000..5bffa9257 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/vulnplatform-vuln.js @@ -0,0 +1,36 @@ +import { PlatformClient } from "./client.js"; + +const METHOD = (service, method) => `vulnplatform.v1.${service}/${method}`; +const listResponse = (value, request = {}) => { + const data = value?.data ?? value?.result ?? value; + const page = data?.page ?? data; + const records = Array.isArray(page) ? page : (page?.records ?? page?.list ?? page?.items ?? []); + return { records, total: Number(page?.total ?? records.length), size: Number(page?.size ?? request.size ?? records.length), current: Number(page?.current ?? request.current ?? 1), pages: Number(page?.pages ?? 1) }; +}; +const dataOf = (value) => value?.data ?? value?.result ?? value; +const codeResponse = (value) => { + const data = dataOf(value); + return { code: Number(data?.code ?? value?.code ?? 0), message: String(data?.message ?? value?.message ?? "ok") }; +}; +const call = (path, map = dataOf) => async (ctx = {}) => map(await new PlatformClient(ctx).request(path, ctx.request ?? {}), ctx.request ?? {}); + +export const handlers = { + [METHOD("VulnerabilityService", "ListVulnerabilities")]: call("/api/vulnerability/list", listResponse), + [METHOD("VulnerabilityService", "CreateVulnerability")]: call("/api/vulnerability/create", (value) => ({ vulId: dataOf(value)?.vulId ?? dataOf(value)?.id ?? 0 })), + [METHOD("VulnerabilityService", "UpdateVulnerabilityStatus")]: call("/api/vulnerability/status/update", codeResponse), + [METHOD("VulnerabilityService", "DeleteVulnerabilities")]: call("/api/vulnerability/delete", codeResponse), + [METHOD("VulnerabilityService", "GetVulnerabilityTimeline")]: call("/api/vulnerability/timeline", (value) => { const data = dataOf(value); return { events: Array.isArray(data?.events) ? data.events : (Array.isArray(data?.records) ? data.records : []) }; }), + [METHOD("VulnerabilityService", "ListVulnerabilityTypes")]: call("/api/vulnerability/types", (value) => { const data = dataOf(value); return { types: Array.isArray(data?.types) ? data.types : (Array.isArray(data) ? data : []) }; }), + [METHOD("VulnerabilityService", "ListVulnerabilitiesGF")]: call("/api/vulnerability/gf/list", listResponse), + [METHOD("AssetService", "ListGroups")]: call("/api/asset/groups", (value) => { const data = dataOf(value); return { groups: Array.isArray(data?.groups) ? data.groups : (Array.isArray(data) ? data : []) }; }), + [METHOD("AssetService", "ListIPAssets")]: call("/api/asset/ip/list", listResponse), + [METHOD("AssetService", "ListWebAssets")]: call("/api/asset/web/list", listResponse), + [METHOD("AssetService", "ListRepoAssets")]: call("/api/asset/repo/list", listResponse), + [METHOD("AssetService", "ListComponentAssets")]: call("/api/asset/component/list", listResponse), + [METHOD("AssetService", "ListContainerAssets")]: call("/api/asset/container/list", listResponse), + [METHOD("IntelligenceService", "ListStandardVulnerabilities")]: call("/api/intelligence/vulnerability/list", listResponse), + [METHOD("IntelligenceService", "CreateStandardVulnerability")]: call("/api/intelligence/vulnerability/create", (value) => ({ standardVulId: dataOf(value)?.standardVulId ?? dataOf(value)?.id ?? 0 })), + [METHOD("IntelligenceService", "DeleteStandardVulnerabilities")]: call("/api/intelligence/vulnerability/delete", codeResponse), +}; + +export const _test = { METHOD, listResponse, dataOf, codeResponse }; diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/test/smoke.json b/services/vulnplatform__vulnerability-management_v3-2-0/test/smoke.json new file mode 100644 index 000000000..b015f536d --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/test/smoke.json @@ -0,0 +1,13 @@ +{ + "method": "vulnplatform.v1.AssetService/ListGroups", + "request": { "groupPath": "/" }, + "protocols": ["connect", "grpc", "mcp"], + "expectUpstream": true, + "requireBusinessSuccess": true, + "requireUpstreamPerProtocol": true, + "upstream": { + "method": "POST", + "path": "/api/asset/groups", + "headers": { "authorization": "Bearer smoke-secret" } + } +} diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/test/vulnplatform-vuln.test.js b/services/vulnplatform__vulnerability-management_v3-2-0/test/vulnplatform-vuln.test.js new file mode 100644 index 000000000..60846f21f --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/test/vulnplatform-vuln.test.js @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { handlers, _test as handlerTest } from "../src/vulnplatform-vuln.js"; +import { PlatformClient, _test as clientTest } from "../src/client.js"; + +const originalFetch = globalThis.fetch; +const ctx = (request = {}) => ({ config: { apiBaseUrl: "https://vuln.example.test", timeoutMs: 50 }, secret: { apiToken: "test-token" }, request }); +const response = (status, body) => ({ ok: status >= 200 && status < 300, status, text: async () => body }); +const configSchema = JSON.parse(readFileSync(new URL("../config.schema.json", import.meta.url), "utf8")); + +test.after(() => { globalThis.fetch = originalFetch; }); + +test("every RPC uses the single-context SDK ABI and calls its deterministic endpoint", async () => { + const calls = []; + globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + return response(200, JSON.stringify({ data: { records: [{ id: 7 }], total: 1, size: 1, current: 1, pages: 1, id: 9, groups: [{ id: 1 }], types: ["web"], events: [{ action: "created" }], code: 0, message: "ok" } })); + }; + for (const [name, handler] of Object.entries(handlers)) { + const result = await handler(ctx({ current: 1, size: 1 })); + assert.equal(typeof result, "object", name); + } + assert.equal(calls.length, Object.keys(handlers).length); + assert.ok(calls.every(({ options }) => options.headers.Authorization === "Bearer test-token")); + assert.equal(new Set(calls.map(({ url }) => url)).size, Object.keys(handlers).length); +}); + +test("response mappers preserve lists and operation results", async () => { + globalThis.fetch = async (url) => response(200, JSON.stringify({ data: String(url).includes("/types") ? { types: ["host"] } : { records: [{ id: 1 }], total: 1, size: 10, current: 2, pages: 1 } })); + const listed = await handlers["vulnplatform.v1.VulnerabilityService/ListVulnerabilities"](ctx({ current: 2, size: 10 })); + assert.deepEqual(listed, { records: [{ id: 1 }], total: 1, size: 10, current: 2, pages: 1 }); + const types = await handlers["vulnplatform.v1.VulnerabilityService/ListVulnerabilityTypes"](ctx()); + assert.deepEqual(types, { types: ["host"] }); +}); + +test("create responses preserve int64 identifiers without Number precision loss", async () => { + const id = "9223372036854775807"; + globalThis.fetch = async (url) => response(200, JSON.stringify({ data: String(url).includes("/intelligence/") ? { standardVulId: id } : { vulId: id } })); + assert.deepEqual(await handlers["vulnplatform.v1.VulnerabilityService/CreateVulnerability"](ctx()), { vulId: id }); + assert.deepEqual(await handlers["vulnplatform.v1.IntelligenceService/CreateStandardVulnerability"](ctx()), { standardVulId: id }); +}); + +test("mappers cover platform envelopes and safe pagination defaults", () => { + assert.deepEqual(handlerTest.dataOf({ result: { id: 1 } }), { id: 1 }); + assert.deepEqual(handlerTest.dataOf({ id: 2 }), { id: 2 }); + assert.deepEqual(handlerTest.listResponse({ result: { page: { list: [{ id: 3 }], total: 1, size: 1, current: 1, pages: 1 } } }), { records: [{ id: 3 }], total: 1, size: 1, current: 1, pages: 1 }); + assert.deepEqual(handlerTest.listResponse({ items: [{ id: 4 }] }, { size: 2, current: 3 }), { records: [{ id: 4 }], total: 1, size: 2, current: 3, pages: 1 }); + assert.deepEqual(handlerTest.listResponse([]), { records: [], total: 0, size: 0, current: 1, pages: 1 }); + assert.deepEqual(handlerTest.codeResponse({ message: "done" }), { code: 0, message: "done" }); +}); + +test("all handlers accept a direct platform response envelope", async () => { + globalThis.fetch = async () => response(200, JSON.stringify({})); + for (const handler of Object.values(handlers)) await handler(ctx()); +}); + +test("HTTP, timeout, and malformed upstream failures map safely", async () => { + globalThis.fetch = async () => response(401, "secret upstream body"); + await assert.rejects(() => handlers["vulnplatform.v1.AssetService/ListGroups"](ctx()), /UNAUTHENTICATED: upstream returned HTTP 401/); + globalThis.fetch = async () => { const error = new Error("timed out"); error.name = "TimeoutError"; throw error; }; + await assert.rejects(() => handlers["vulnplatform.v1.AssetService/ListGroups"](ctx()), /DEADLINE_EXCEEDED/); + globalThis.fetch = async () => response(200, "not-json"); + await assert.rejects(() => handlers["vulnplatform.v1.AssetService/ListGroups"](ctx()), /UNKNOWN: upstream response is not valid JSON/); +}); + +test("client validates configuration, covers status mappings, and handles local response failures", async () => { + const endpointPattern = new RegExp(configSchema.properties.apiBaseUrl.pattern); + for (const accepted of ["https://platform.example.test", "HTTPS://platform.example.test", "https://[2001:db8::1]:8443/api", "http://127.0.0.1:19001", "HTTP://127.0.0.1:19001", "http://[::1]:19001/api", "http://[0:0:0:0:0:0:0:1]:19001", "http://[0:0:0::1]:19001"]) { + assert.equal(endpointPattern.test(accepted), true, accepted); + assert.equal(clientTest.endpoint(accepted), accepted); + } + for (const rejected of ["http://insecure.example.test", "http://localhost:19001", "http://127.0.0.2", "https://user:pass@platform.example.test", "ftp://platform.example.test", "http://[2001:db8::1]:19001", "http://0x7f000001", "http://127.1", "http://2130706433", "https:platform.example.test", "https:/platform.example.test", "https://", "https:///path", "https://:8443/path"]) { + assert.equal(endpointPattern.test(rejected), false, rejected); + assert.throws(() => clientTest.endpoint(rejected), /FAILED_PRECONDITION/); + } + assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "ftp://invalid" } }), /FAILED_PRECONDITION/); + assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "http://insecure.example.test" } }), /must use HTTPS/); + assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "http://127.0.0.2" } }), /must use HTTPS/); + assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "http://localhost:19001" } }), /must use HTTPS/); + assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "https://user:pass@platform.example.test" } }), /must not contain credentials/); + assert.equal(clientTest.endpoint("http://127.0.0.1:19001/"), "http://127.0.0.1:19001"); + assert.equal(clientTest.endpoint("http://[::1]:19001/"), "http://[::1]:19001"); + await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x" } }).bearerToken(), /configure apiToken/); + for (const [status, code] of [[403, "PERMISSION_DENIED"], [404, "NOT_FOUND"], [429, "RESOURCE_EXHAUSTED"], [503, "UNAVAILABLE"]]) { + globalThis.fetch = async () => response(status, "private body"); + await assert.rejects(() => new PlatformClient(ctx()).request("/test", {}), new RegExp(code)); + } + globalThis.fetch = async () => ({ ok: true, status: 200, text: async () => "" }); + await assert.rejects(() => new PlatformClient(ctx()).request("/test", {}), /empty response/); + globalThis.fetch = async () => ({ ok: true, status: 200, text: async () => { throw new Error("read failed"); } }); + await assert.rejects(() => new PlatformClient(ctx()).request("/test", {}), /UNAVAILABLE/); + assert.equal(clientTest.timeoutMs(-1), 10000); + assert.equal(clientTest.timeoutMs(12), 12); + assert.equal(clientTest.errorForStatus(499), "FAILED_PRECONDITION"); + assert.equal(clientTest.errorForStatus(429), "RESOURCE_EXHAUSTED"); + assert.equal(clientTest.errorForStatus(500), "UNAVAILABLE"); +}); + +test("only explicitly configured bearer tokens are accepted", async () => { + await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x" } }).bearerToken(), /configure apiToken/); + assert.equal(await new PlatformClient({ config: { apiBaseUrl: "https://x" }, secret: { apiToken: "cached" } }).bearerToken(), "cached"); +}); + +test("TLS relaxation is scoped to this client and never changes global process policy", async () => { + let options; + globalThis.fetch = async (_url, init) => { options = init; return response(200, JSON.stringify({ data: { groups: [] } })); }; + await new PlatformClient({ ...ctx(), config: { apiBaseUrl: "https://vuln.example.test", skipTlsVerify: true } }).request("/api/asset/groups", {}); + assert.ok(options.dispatcher); + assert.equal(process.env.NODE_TLS_REJECT_UNAUTHORIZED, undefined); + assert.equal(clientTest.endpoint("https://vuln.example.test/"), "https://vuln.example.test"); +});