From 1e8a4a6ddf2f3a70f8c6499f91e377221a96fafe Mon Sep 17 00:00:00 2001 From: Shirley Yan Date: Mon, 29 Jun 2026 15:03:26 +0800 Subject: [PATCH 01/18] Add vulnerability management platform service adapter --- examples/vulnplatform-vuln/README.md | 14 + .../bin/vulnplatform-vuln.js | 16 + .../vulnplatform-vuln/lib/platform-client.js | 8 + .../vulnplatform-vuln/lib/token-generator.js | 3 + examples/vulnplatform-vuln/package.json | 12 + .../proto/vulnerability.proto | 505 ++++++++++++++++++ examples/vulnplatform-vuln/secret.schema.json | 26 + examples/vulnplatform-vuln/service.json | 11 + 8 files changed, 595 insertions(+) create mode 100644 examples/vulnplatform-vuln/README.md create mode 100644 examples/vulnplatform-vuln/bin/vulnplatform-vuln.js create mode 100644 examples/vulnplatform-vuln/lib/platform-client.js create mode 100644 examples/vulnplatform-vuln/lib/token-generator.js create mode 100644 examples/vulnplatform-vuln/package.json create mode 100644 examples/vulnplatform-vuln/proto/vulnerability.proto create mode 100644 examples/vulnplatform-vuln/secret.schema.json create mode 100644 examples/vulnplatform-vuln/service.json diff --git a/examples/vulnplatform-vuln/README.md b/examples/vulnplatform-vuln/README.md new file mode 100644 index 000000000..66f870e84 --- /dev/null +++ b/examples/vulnplatform-vuln/README.md @@ -0,0 +1,14 @@ +# 漏洞管控平台 Service + +OctoBus Service 适配器,提供漏洞管控平台的 gRPC 接口封装。 + +## 前置要求 + +1. Java 运行环境(JRE 8+) +2. vms-auth-sdk jar 包 + +## 使用 + +```bash +octobus service import vulnplatform ./examples/vulnplatform-vuln +``` \ No newline at end of file diff --git a/examples/vulnplatform-vuln/bin/vulnplatform-vuln.js b/examples/vulnplatform-vuln/bin/vulnplatform-vuln.js new file mode 100644 index 000000000..a67246943 --- /dev/null +++ b/examples/vulnplatform-vuln/bin/vulnplatform-vuln.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +import { defineService, runServiceMain } from "@chaitin-ai/octobus-sdk"; + +const service = defineService({ + handlers: { + "vulnplatform.v1.VulnerabilityService/ListVulnerabilities": async (ctx) => { + return { records: [], total: 0, size: 10, current: 1, pages: 0 }; + }, + "vulnplatform.v1.AssetService/ListGroups": async (ctx) => { + return { groups: [] }; + }, + } +}); + +runServiceMain(service); \ No newline at end of file diff --git a/examples/vulnplatform-vuln/lib/platform-client.js b/examples/vulnplatform-vuln/lib/platform-client.js new file mode 100644 index 000000000..e642ac3f2 --- /dev/null +++ b/examples/vulnplatform-vuln/lib/platform-client.js @@ -0,0 +1,8 @@ +export class PlatformClient { + constructor(secret) { + this.secret = secret; + } + async callApi(endpoint, method, body) { + return {}; + } +} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/lib/token-generator.js b/examples/vulnplatform-vuln/lib/token-generator.js new file mode 100644 index 000000000..eae11661b --- /dev/null +++ b/examples/vulnplatform-vuln/lib/token-generator.js @@ -0,0 +1,3 @@ +export async function generateApiToken(appId, dateTime, key, account) { + return "mock-token"; +} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/package.json b/examples/vulnplatform-vuln/package.json new file mode 100644 index 000000000..70b93ba6c --- /dev/null +++ b/examples/vulnplatform-vuln/package.json @@ -0,0 +1,12 @@ +{ + "name": "octobus-vulnplatform-vuln", + "version": "1.0.0", + "private": true, + "type": "module", + "bin": { + "vulnplatform-vuln": "bin/vulnplatform-vuln.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.5.0" + } +} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/proto/vulnerability.proto b/examples/vulnplatform-vuln/proto/vulnerability.proto new file mode 100644 index 000000000..51bee37f4 --- /dev/null +++ b/examples/vulnplatform-vuln/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/examples/vulnplatform-vuln/secret.schema.json b/examples/vulnplatform-vuln/secret.schema.json new file mode 100644 index 000000000..374701db1 --- /dev/null +++ b/examples/vulnplatform-vuln/secret.schema.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "required": ["appId", "key", "account", "apiBaseUrl"], + "properties": { + "appId": { + "type": "string", + "description": "API调用方标识(厂家提供,测试值为 demo)" + }, + "key": { + "type": "string", + "description": "Token生成密钥(厂家提供,测试值为 ww93untW4d)" + }, + "account": { + "type": "string", + "description": "登录账号(内置管理员为 admin)" + }, + "apiBaseUrl": { + "type": "string", + "description": "漏洞管控平台 API 基础地址,如 https://vuln-platform.example.com" + }, + "jarPath": { + "type": "string", + "description": "可选:vms-auth-sdk jar 包路径,默认使用 lib/vms-auth-sdk-2.1.0.jar" + } + } +} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/service.json b/examples/vulnplatform-vuln/service.json new file mode 100644 index 000000000..4bca7a6fc --- /dev/null +++ b/examples/vulnplatform-vuln/service.json @@ -0,0 +1,11 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "vulnplatform-vuln", + "displayName": "Vulnerability Platform Service", + "description": "漏洞管控平台集成服务,提供漏洞管理、资产管理、情报管理等能力", + "proto": { + "roots": ["proto"], + "files": ["proto/vulnerability.proto"] + }, + "secretSchema": "secret.schema.json" +} \ No newline at end of file From 670703f3b15f9739b892e691944b849ec37b8c80 Mon Sep 17 00:00:00 2001 From: kingfs Date: Fri, 14 Aug 2026 15:58:49 +0800 Subject: [PATCH 02/18] Migrate vulnerability platform service package --- examples/vulnplatform-vuln/README.md | 14 -- .../bin/vulnplatform-vuln.js | 16 -- .../vulnplatform-vuln/lib/platform-client.js | 8 - .../vulnplatform-vuln/lib/token-generator.js | 3 - examples/vulnplatform-vuln/package.json | 12 -- examples/vulnplatform-vuln/secret.schema.json | 26 ---- examples/vulnplatform-vuln/service.json | 11 -- services/bin/octobus-tentacles.js | 4 + services/bin/vulnplatform-vuln.js | 10 ++ services/package.json | 3 + .../README.md | 36 +++++ .../bin/vulnplatform-vuln.js | 7 + .../config.schema.json | 29 ++++ .../package.json | 11 ++ .../proto/vulnerability.proto | 0 .../secret.schema.json | 27 ++++ .../service.json | 22 +++ .../src/client.js | 141 ++++++++++++++++++ .../src/service.js | 6 + .../src/token-generator.js | 12 ++ .../src/vulnplatform-vuln.js | 36 +++++ .../test/smoke.json | 13 ++ .../test/vulnplatform-vuln.test.js | 95 ++++++++++++ 23 files changed, 452 insertions(+), 90 deletions(-) delete mode 100644 examples/vulnplatform-vuln/README.md delete mode 100644 examples/vulnplatform-vuln/bin/vulnplatform-vuln.js delete mode 100644 examples/vulnplatform-vuln/lib/platform-client.js delete mode 100644 examples/vulnplatform-vuln/lib/token-generator.js delete mode 100644 examples/vulnplatform-vuln/package.json delete mode 100644 examples/vulnplatform-vuln/secret.schema.json delete mode 100644 examples/vulnplatform-vuln/service.json create mode 100755 services/bin/vulnplatform-vuln.js create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/README.md create mode 100755 services/vulnplatform__vulnerability-management_v3-2-0/bin/vulnplatform-vuln.js create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/package.json rename {examples/vulnplatform-vuln => services/vulnplatform__vulnerability-management_v3-2-0}/proto/vulnerability.proto (100%) create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/service.json create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/src/client.js create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/src/service.js create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/src/token-generator.js create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/src/vulnplatform-vuln.js create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/test/smoke.json create mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/test/vulnplatform-vuln.test.js diff --git a/examples/vulnplatform-vuln/README.md b/examples/vulnplatform-vuln/README.md deleted file mode 100644 index 66f870e84..000000000 --- a/examples/vulnplatform-vuln/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# 漏洞管控平台 Service - -OctoBus Service 适配器,提供漏洞管控平台的 gRPC 接口封装。 - -## 前置要求 - -1. Java 运行环境(JRE 8+) -2. vms-auth-sdk jar 包 - -## 使用 - -```bash -octobus service import vulnplatform ./examples/vulnplatform-vuln -``` \ No newline at end of file diff --git a/examples/vulnplatform-vuln/bin/vulnplatform-vuln.js b/examples/vulnplatform-vuln/bin/vulnplatform-vuln.js deleted file mode 100644 index a67246943..000000000 --- a/examples/vulnplatform-vuln/bin/vulnplatform-vuln.js +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env node - -import { defineService, runServiceMain } from "@chaitin-ai/octobus-sdk"; - -const service = defineService({ - handlers: { - "vulnplatform.v1.VulnerabilityService/ListVulnerabilities": async (ctx) => { - return { records: [], total: 0, size: 10, current: 1, pages: 0 }; - }, - "vulnplatform.v1.AssetService/ListGroups": async (ctx) => { - return { groups: [] }; - }, - } -}); - -runServiceMain(service); \ No newline at end of file diff --git a/examples/vulnplatform-vuln/lib/platform-client.js b/examples/vulnplatform-vuln/lib/platform-client.js deleted file mode 100644 index e642ac3f2..000000000 --- a/examples/vulnplatform-vuln/lib/platform-client.js +++ /dev/null @@ -1,8 +0,0 @@ -export class PlatformClient { - constructor(secret) { - this.secret = secret; - } - async callApi(endpoint, method, body) { - return {}; - } -} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/lib/token-generator.js b/examples/vulnplatform-vuln/lib/token-generator.js deleted file mode 100644 index eae11661b..000000000 --- a/examples/vulnplatform-vuln/lib/token-generator.js +++ /dev/null @@ -1,3 +0,0 @@ -export async function generateApiToken(appId, dateTime, key, account) { - return "mock-token"; -} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/package.json b/examples/vulnplatform-vuln/package.json deleted file mode 100644 index 70b93ba6c..000000000 --- a/examples/vulnplatform-vuln/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "octobus-vulnplatform-vuln", - "version": "1.0.0", - "private": true, - "type": "module", - "bin": { - "vulnplatform-vuln": "bin/vulnplatform-vuln.js" - }, - "dependencies": { - "@chaitin-ai/octobus-sdk": "^0.5.0" - } -} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/secret.schema.json b/examples/vulnplatform-vuln/secret.schema.json deleted file mode 100644 index 374701db1..000000000 --- a/examples/vulnplatform-vuln/secret.schema.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "type": "object", - "required": ["appId", "key", "account", "apiBaseUrl"], - "properties": { - "appId": { - "type": "string", - "description": "API调用方标识(厂家提供,测试值为 demo)" - }, - "key": { - "type": "string", - "description": "Token生成密钥(厂家提供,测试值为 ww93untW4d)" - }, - "account": { - "type": "string", - "description": "登录账号(内置管理员为 admin)" - }, - "apiBaseUrl": { - "type": "string", - "description": "漏洞管控平台 API 基础地址,如 https://vuln-platform.example.com" - }, - "jarPath": { - "type": "string", - "description": "可选:vms-auth-sdk jar 包路径,默认使用 lib/vms-auth-sdk-2.1.0.jar" - } - } -} \ No newline at end of file diff --git a/examples/vulnplatform-vuln/service.json b/examples/vulnplatform-vuln/service.json deleted file mode 100644 index 4bca7a6fc..000000000 --- a/examples/vulnplatform-vuln/service.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema": "chaitin.octobus.service.v1", - "name": "vulnplatform-vuln", - "displayName": "Vulnerability Platform Service", - "description": "漏洞管控平台集成服务,提供漏洞管理、资产管理、情报管理等能力", - "proto": { - "roots": ["proto"], - "files": ["proto/vulnerability.proto"] - }, - "secretSchema": "secret.schema.json" -} \ No newline at end of file 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..c99d90d32 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -0,0 +1,36 @@ +# 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 either a platform-issued bearer token: + +```json +{"apiToken":"your-platform-token"} +``` + +or vendor token-generation credentials: + +```json +{"appId":"provided-app-id","key":"provided-key","account":"provided-account"} +``` + +The latter requires `tokenJarPath` to point at a locally installed vendor `vms-auth-sdk` JAR; the JAR is intentionally not packaged in this repository. Tokens obtained through `/api/login2` are cached in-memory for 25 minutes. + +`skipTlsVerify` defaults to false. If a trusted private installation requires it, 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..3e872aaa5 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["apiBaseUrl"], + "properties": { + "apiBaseUrl": { + "type": "string", + "format": "uri", + "description": "Vulnerability Management Platform base URL, for example https://vuln-platform.example.com." + }, + "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." + }, + "tokenJarPath": { + "type": "string", + "description": "Optional absolute path to the vendor vms-auth-sdk JAR. Required only when apiToken is not configured." + } + } +} 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/examples/vulnplatform-vuln/proto/vulnerability.proto b/services/vulnplatform__vulnerability-management_v3-2-0/proto/vulnerability.proto similarity index 100% rename from examples/vulnplatform-vuln/proto/vulnerability.proto rename to services/vulnplatform__vulnerability-management_v3-2-0/proto/vulnerability.proto 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..8ba5b55da --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "oneOf": [ + { "required": ["apiToken"] }, + { "required": ["appId", "key", "account"] } + ], + "properties": { + "apiToken": { + "type": "string", + "description": "Bearer token supplied by the platform. Prefer this option when a long-lived service token is available." + }, + "appId": { + "type": "string", + "description": "API caller identifier supplied by the platform administrator." + }, + "key": { + "type": "string", + "description": "Token-generation key supplied by the platform administrator." + }, + "account": { + "type": "string", + "description": "Platform login account supplied by the platform administrator." + } + } +} 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..54c5f483a --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -0,0 +1,141 @@ +import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; +import { Agent } from "undici"; + +import { generateApiToken } from "./token-generator.js"; + +const DEFAULT_TIMEOUT_MS = 10000; +const TOKEN_TTL_MS = 25 * 60 * 1000; +let insecureDispatcher; +const loginCache = new Map(); + +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, + 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); + if (!/^https?:$/.test(url.protocol) || url.username || url.password) throw new Error("unsupported URL"); + return raw; + } catch { + throw serviceError("FAILED_PRECONDITION", "apiBaseUrl must be an HTTP(S) URL without embedded 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 >= 400 && status < 500) return "FAILED_PRECONDITION"; + return "UNAVAILABLE"; +} + +function readToken(body) { + const value = body?.token ?? body?.accessToken ?? body?.data?.token ?? body?.data?.accessToken; + return text(value); +} + +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) return configured; + const appId = text(this.bindings.appId); + const key = text(this.bindings.key); + const account = text(this.bindings.account); + if (!appId || !key || !account) throw serviceError("FAILED_PRECONDITION", "configure apiToken or appId, key, and account"); + const cacheKey = `${this.baseURL}\u0000${appId}\u0000${account}`; + const cached = loginCache.get(cacheKey); + if (cached?.expiresAt > Date.now()) return cached.token; + let signedToken; + try { + signedToken = await generateApiToken({ appId, key, account, jarPath: text(this.bindings.tokenJarPath) }); + } catch (error) { + throw serviceError("FAILED_PRECONDITION", `token generation failed: ${error instanceof Error ? error.message : "unknown error"}`); + } + const login = await this.request("/api/login2", { appId, account, token: signedToken }, { authorization: false }); + const token = readToken(login); + if (!token) throw serviceError("UNAUTHENTICATED", "platform login response did not contain a bearer token"); + loginCache.set(cacheKey, { token, expiresAt: Date.now() + TOKEN_TTL_MS }); + return token; + } + + 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, readToken, loginCache }; 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/token-generator.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/token-generator.js new file mode 100644 index 000000000..5c2bb2bd6 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/token-generator.js @@ -0,0 +1,12 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +export async function generateApiToken({ appId, key, account, jarPath, dateTime = new Date().toISOString(), run = execFileAsync }) { + if (!jarPath) throw new Error("tokenJarPath is required when apiToken is not configured"); + const { stdout } = await run("java", ["-jar", jarPath, appId, dateTime, key, account], { timeout: 10000, maxBuffer: 64 * 1024 }); + const token = String(stdout).trim(); + if (!token) throw new Error("vendor token generator returned an empty token"); + return token; +} 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..d290a5214 --- /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: Number(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: Number(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..aab727112 --- /dev/null +++ b/services/vulnplatform__vulnerability-management_v3-2-0/test/vulnplatform-vuln.test.js @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { handlers, _test as handlerTest } from "../src/vulnplatform-vuln.js"; +import { PlatformClient, _test as clientTest } from "../src/client.js"; +import { generateApiToken } from "../src/token-generator.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 }); + +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("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 () => { + assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "ftp://invalid" } }), /FAILED_PRECONDITION/); + await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x" } }).bearerToken(), /configure apiToken/); + for (const [status, code] of [[403, "PERMISSION_DENIED"], [404, "NOT_FOUND"], [429, "FAILED_PRECONDITION"], [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(500), "UNAVAILABLE"); + assert.equal(clientTest.readToken({ accessToken: "one" }), "one"); + assert.equal(clientTest.readToken({ data: { token: "two" } }), "two"); +}); + +test("vendor token generation is injectable and token-login failures are mapped without leaking secrets", async () => { + assert.equal(await generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: " token\n" }) }), "token"); + await assert.rejects(() => generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: "" }) }), /empty token/); + await assert.rejects(() => generateApiToken({ appId: "id", key: "key", account: "account" }), /tokenJarPath/); + await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x", tokenJarPath: "/missing.jar" }, secret: { appId: "id", key: "key", account: "account" } }).bearerToken(), /token generation failed/); + clientTest.loginCache.set("https://x\u0000id\u0000account", { token: "cached", expiresAt: Date.now() + 1000 }); + assert.equal(await new PlatformClient({ config: { apiBaseUrl: "https://x" }, secret: { appId: "id", key: "key", account: "account" } }).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"); +}); From 65b8392db1b3e86e4889f9a367db79696962b0cc Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 17 Aug 2026 15:05:32 +0800 Subject: [PATCH 03/18] Harden vulnerability platform token isolation --- .../README.md | 2 + .../src/client.js | 42 ++++++++++++++----- .../test/vulnplatform-vuln.test.js | 27 +++++++++++- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/README.md b/services/vulnplatform__vulnerability-management_v3-2-0/README.md index c99d90d32..f9a43bbed 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/README.md +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -22,6 +22,8 @@ or vendor token-generation credentials: The latter requires `tokenJarPath` to point at a locally installed vendor `vms-auth-sdk` JAR; the JAR is intentionally not packaged in this repository. Tokens obtained through `/api/login2` are cached in-memory for 25 minutes. +The vendor JAR and its v3.2 server-side authentication contract are private dependencies. The adapter invokes the documented JAR CLI, whose arguments necessarily include the vendor key; run the service only on a trusted host where process inspection is restricted. Repository tests use an injected command runner and a mock HTTP platform, so passing those tests does not constitute real-platform compatibility evidence. + `skipTlsVerify` defaults to false. If a trusted private installation requires it, the service uses a local undici dispatcher and does not weaken Node’s process-wide TLS policy. ## Import and validation diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 54c5f483a..4402b1804 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; + import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; import { Agent } from "undici"; @@ -7,6 +9,7 @@ const DEFAULT_TIMEOUT_MS = 10000; const TOKEN_TTL_MS = 25 * 60 * 1000; let insecureDispatcher; const loginCache = new Map(); +const loginRequests = new Map(); const codes = { INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, @@ -67,6 +70,11 @@ function readToken(body) { return text(value); } +function loginCacheKey(baseURL, appId, key, account, jarPath) { + // Do not put credentials in a long-lived Map key or diagnostic heap output. + return createHash("sha256").update(JSON.stringify([baseURL, appId, key, account, jarPath])).digest("hex"); +} + export class PlatformClient { constructor(ctx = {}) { this.ctx = ctx; @@ -82,20 +90,32 @@ export class PlatformClient { const key = text(this.bindings.key); const account = text(this.bindings.account); if (!appId || !key || !account) throw serviceError("FAILED_PRECONDITION", "configure apiToken or appId, key, and account"); - const cacheKey = `${this.baseURL}\u0000${appId}\u0000${account}`; + const jarPath = text(this.bindings.tokenJarPath); + const cacheKey = loginCacheKey(this.baseURL, appId, key, account, jarPath); const cached = loginCache.get(cacheKey); if (cached?.expiresAt > Date.now()) return cached.token; - let signedToken; + const pending = loginRequests.get(cacheKey); + if (pending) return pending; + const loginRequest = (async () => { + let signedToken; + try { + signedToken = await generateApiToken({ appId, key, account, jarPath }); + } catch { + // execFile errors can contain the complete command line, including the key. + throw serviceError("FAILED_PRECONDITION", "vendor token generation failed"); + } + const login = await this.request("/api/login2", { appId, account, token: signedToken }, { authorization: false }); + const token = readToken(login); + if (!token) throw serviceError("UNAUTHENTICATED", "platform login response did not contain a bearer token"); + loginCache.set(cacheKey, { token, expiresAt: Date.now() + TOKEN_TTL_MS }); + return token; + })(); + loginRequests.set(cacheKey, loginRequest); try { - signedToken = await generateApiToken({ appId, key, account, jarPath: text(this.bindings.tokenJarPath) }); - } catch (error) { - throw serviceError("FAILED_PRECONDITION", `token generation failed: ${error instanceof Error ? error.message : "unknown error"}`); + return await loginRequest; + } finally { + loginRequests.delete(cacheKey); } - const login = await this.request("/api/login2", { appId, account, token: signedToken }, { authorization: false }); - const token = readToken(login); - if (!token) throw serviceError("UNAUTHENTICATED", "platform login response did not contain a bearer token"); - loginCache.set(cacheKey, { token, expiresAt: Date.now() + TOKEN_TTL_MS }); - return token; } async request(path, body, { method = "POST", authorization = true } = {}) { @@ -138,4 +158,4 @@ export class PlatformClient { } } -export const _test = { bindings, endpoint, timeoutMs, isTimeout, errorForStatus, readToken, loginCache }; +export const _test = { bindings, endpoint, timeoutMs, isTimeout, errorForStatus, readToken, loginCacheKey, loginCache, loginRequests }; 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 index aab727112..d456be24d 100644 --- 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 @@ -80,11 +80,34 @@ test("vendor token generation is injectable and token-login failures are mapped assert.equal(await generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: " token\n" }) }), "token"); await assert.rejects(() => generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: "" }) }), /empty token/); await assert.rejects(() => generateApiToken({ appId: "id", key: "key", account: "account" }), /tokenJarPath/); - await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x", tokenJarPath: "/missing.jar" }, secret: { appId: "id", key: "key", account: "account" } }).bearerToken(), /token generation failed/); - clientTest.loginCache.set("https://x\u0000id\u0000account", { token: "cached", expiresAt: Date.now() + 1000 }); + const failure = await new PlatformClient({ config: { apiBaseUrl: "https://x", tokenJarPath: "/missing.jar" }, secret: { appId: "id", key: "very-secret-key", account: "account" } }).bearerToken().catch((error) => error); + assert.match(failure.message, /vendor token generation failed/); + assert.doesNotMatch(failure.message, /very-secret-key|missing\.jar/); + const cacheKey = clientTest.loginCacheKey("https://x", "id", "key", "account", ""); + assert.doesNotMatch(cacheKey, /key|account/); + clientTest.loginCache.set(cacheKey, { token: "cached", expiresAt: Date.now() + 1000 }); assert.equal(await new PlatformClient({ config: { apiBaseUrl: "https://x" }, secret: { appId: "id", key: "key", account: "account" } }).bearerToken(), "cached"); }); +test("login cache isolates credentials and coalesces concurrent vendor logins", async () => { + clientTest.loginCache.clear(); + clientTest.loginRequests.clear(); + const tokenModuleKey = clientTest.loginCacheKey("https://x", "id", "different-key", "account", "/vendor.jar"); + assert.notEqual(tokenModuleKey, clientTest.loginCacheKey("https://x", "id", "key", "account", "/vendor.jar")); + + // Seed an in-flight login to verify that simultaneous calls share one result + // without needing the private vendor JAR in the test environment. + let release; + const pending = new Promise((resolve) => { release = resolve; }); + clientTest.loginRequests.set(tokenModuleKey, pending); + const client = new PlatformClient({ config: { apiBaseUrl: "https://x", tokenJarPath: "/vendor.jar" }, secret: { appId: "id", key: "different-key", account: "account" } }); + const first = client.bearerToken(); + const second = client.bearerToken(); + release("shared-token"); + assert.deepEqual(await Promise.all([first, second]), ["shared-token", "shared-token"]); + clientTest.loginRequests.delete(tokenModuleKey); +}); + 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: [] } })); }; From 06851d09de738a30b66356e6189e67f0fe1ec2d8 Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 17 Aug 2026 15:10:33 +0800 Subject: [PATCH 04/18] Harden token expiry and rate limit mapping --- .../src/client.js | 10 +++++++++- .../test/vulnplatform-vuln.test.js | 13 ++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 4402b1804..54c26347a 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -19,6 +19,7 @@ const codes = { NOT_FOUND: grpcStatus.NOT_FOUND, DEADLINE_EXCEEDED: grpcStatus.DEADLINE_EXCEEDED, UNAVAILABLE: grpcStatus.UNAVAILABLE, + RESOURCE_EXHAUSTED: grpcStatus.RESOURCE_EXHAUSTED, UNKNOWN: grpcStatus.UNKNOWN, }; @@ -61,6 +62,7 @@ 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"; } @@ -80,6 +82,7 @@ export class PlatformClient { this.ctx = ctx; this.bindings = bindings(ctx); this.baseURL = endpoint(this.bindings.apiBaseUrl); + this.cacheKey = null; this.timeoutMs = timeoutMs(this.bindings.timeoutMs ?? ctx.limits?.timeoutMs); } @@ -92,8 +95,10 @@ export class PlatformClient { if (!appId || !key || !account) throw serviceError("FAILED_PRECONDITION", "configure apiToken or appId, key, and account"); const jarPath = text(this.bindings.tokenJarPath); const cacheKey = loginCacheKey(this.baseURL, appId, key, account, jarPath); + this.cacheKey = cacheKey; const cached = loginCache.get(cacheKey); if (cached?.expiresAt > Date.now()) return cached.token; + if (cached) loginCache.delete(cacheKey); const pending = loginRequests.get(cacheKey); if (pending) return pending; const loginRequest = (async () => { @@ -143,7 +148,10 @@ export class PlatformClient { } 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 (!response.ok) { + if (response.status === 401 && this.cacheKey) loginCache.delete(this.cacheKey); + 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); 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 index d456be24d..e9576303e 100644 --- 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 @@ -60,7 +60,7 @@ test("HTTP, timeout, and malformed upstream failures map safely", async () => { test("client validates configuration, covers status mappings, and handles local response failures", async () => { assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "ftp://invalid" } }), /FAILED_PRECONDITION/); await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x" } }).bearerToken(), /configure apiToken/); - for (const [status, code] of [[403, "PERMISSION_DENIED"], [404, "NOT_FOUND"], [429, "FAILED_PRECONDITION"], [503, "UNAVAILABLE"]]) { + 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)); } @@ -71,11 +71,22 @@ test("client validates configuration, covers status mappings, and handles local 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"); assert.equal(clientTest.readToken({ accessToken: "one" }), "one"); assert.equal(clientTest.readToken({ data: { token: "two" } }), "two"); }); +test("revoked generated tokens are evicted on HTTP 401", async () => { + clientTest.loginCache.clear(); + const key = clientTest.loginCacheKey("https://x", "id", "key", "account", ""); + clientTest.loginCache.set(key, { token: "revoked", expiresAt: Date.now() + 1000 }); + const client = new PlatformClient({ config: { apiBaseUrl: "https://x" }, secret: { appId: "id", key: "key", account: "account" } }); + globalThis.fetch = async () => response(401, "private"); + await assert.rejects(() => client.request("/test", {}), /UNAUTHENTICATED/); + assert.equal(clientTest.loginCache.has(key), false); +}); + test("vendor token generation is injectable and token-login failures are mapped without leaking secrets", async () => { assert.equal(await generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: " token\n" }) }), "token"); await assert.rejects(() => generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: "" }) }), /empty token/); From baa8fbee1492eea34b28a619fa016001826e7a99 Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 17 Aug 2026 15:14:36 +0800 Subject: [PATCH 05/18] Bound platform token cache size --- .../src/client.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 54c26347a..09db534ef 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -7,6 +7,7 @@ import { generateApiToken } from "./token-generator.js"; const DEFAULT_TIMEOUT_MS = 10000; const TOKEN_TTL_MS = 25 * 60 * 1000; +const MAX_LOGIN_CACHE_ENTRIES = 256; let insecureDispatcher; const loginCache = new Map(); const loginRequests = new Map(); @@ -112,6 +113,11 @@ export class PlatformClient { const login = await this.request("/api/login2", { appId, account, token: signedToken }, { authorization: false }); const token = readToken(login); if (!token) throw serviceError("UNAUTHENTICATED", "platform login response did not contain a bearer token"); + while (loginCache.size >= MAX_LOGIN_CACHE_ENTRIES) { + const oldest = loginCache.keys().next().value; + if (oldest === undefined) break; + loginCache.delete(oldest); + } loginCache.set(cacheKey, { token, expiresAt: Date.now() + TOKEN_TTL_MS }); return token; })(); From 1101e83bd614b2e1ecf8ed5df1da263fcf925b52 Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 17 Aug 2026 15:24:06 +0800 Subject: [PATCH 06/18] Remove unverifiable vendor JAR authentication --- .../README.md | 12 +----- .../config.schema.json | 4 -- .../secret.schema.json | 17 +------- .../src/client.js | 20 ++------- .../src/token-generator.js | 12 ------ .../test/vulnplatform-vuln.test.js | 43 ++----------------- 6 files changed, 10 insertions(+), 98 deletions(-) delete mode 100644 services/vulnplatform__vulnerability-management_v3-2-0/src/token-generator.js diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/README.md b/services/vulnplatform__vulnerability-management_v3-2-0/README.md index f9a43bbed..9555407ee 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/README.md +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -8,21 +8,13 @@ OctoBus integration for the vendor Vulnerability Management Platform. It exposes {"apiBaseUrl":"https://vuln-platform.example.com","timeoutMs":10000,"skipTlsVerify":false} ``` -Provide either a platform-issued bearer token: +Provide a platform-issued bearer token: ```json {"apiToken":"your-platform-token"} ``` -or vendor token-generation credentials: - -```json -{"appId":"provided-app-id","key":"provided-key","account":"provided-account"} -``` - -The latter requires `tokenJarPath` to point at a locally installed vendor `vms-auth-sdk` JAR; the JAR is intentionally not packaged in this repository. Tokens obtained through `/api/login2` are cached in-memory for 25 minutes. - -The vendor JAR and its v3.2 server-side authentication contract are private dependencies. The adapter invokes the documented JAR CLI, whose arguments necessarily include the vendor key; run the service only on a trusted host where process inspection is restricted. Repository tests use an injected command runner and a mock HTTP platform, so passing those tests does not constitute real-platform compatibility evidence. +The adapter intentionally does not execute the vendor JAR or accept appId/key/account credentials; this avoids exposing vendor secrets through process arguments. Tokens are cached in-memory for 25 minutes. `skipTlsVerify` defaults to false. If a trusted private installation requires it, the service uses a local undici dispatcher and does not weaken Node’s process-wide TLS policy. diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json index 3e872aaa5..4a39a3cc0 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -21,9 +21,5 @@ "default": false, "description": "Use only for a trusted private deployment with a self-signed certificate. TLS relaxation is isolated to this service." }, - "tokenJarPath": { - "type": "string", - "description": "Optional absolute path to the vendor vms-auth-sdk JAR. Required only when apiToken is not configured." - } } } diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json index 8ba5b55da..9b28a9bff 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/secret.schema.json @@ -2,26 +2,11 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "additionalProperties": false, - "oneOf": [ - { "required": ["apiToken"] }, - { "required": ["appId", "key", "account"] } - ], + "required": ["apiToken"], "properties": { "apiToken": { "type": "string", "description": "Bearer token supplied by the platform. Prefer this option when a long-lived service token is available." - }, - "appId": { - "type": "string", - "description": "API caller identifier supplied by the platform administrator." - }, - "key": { - "type": "string", - "description": "Token-generation key supplied by the platform administrator." - }, - "account": { - "type": "string", - "description": "Platform login account supplied by the platform administrator." } } } diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 09db534ef..2c9f340e8 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -3,7 +3,6 @@ import { createHash } from "node:crypto"; import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; import { Agent } from "undici"; -import { generateApiToken } from "./token-generator.js"; const DEFAULT_TIMEOUT_MS = 10000; const TOKEN_TTL_MS = 25 * 60 * 1000; @@ -90,12 +89,8 @@ export class PlatformClient { async bearerToken() { const configured = text(this.bindings.apiToken); if (configured) return configured; - const appId = text(this.bindings.appId); - const key = text(this.bindings.key); - const account = text(this.bindings.account); - if (!appId || !key || !account) throw serviceError("FAILED_PRECONDITION", "configure apiToken or appId, key, and account"); - const jarPath = text(this.bindings.tokenJarPath); - const cacheKey = loginCacheKey(this.baseURL, appId, key, account, jarPath); + if (!configured) throw serviceError("FAILED_PRECONDITION", "configure apiToken"); + const cacheKey = loginCacheKey(this.baseURL, configured, "", "", ""); this.cacheKey = cacheKey; const cached = loginCache.get(cacheKey); if (cached?.expiresAt > Date.now()) return cached.token; @@ -103,16 +98,7 @@ export class PlatformClient { const pending = loginRequests.get(cacheKey); if (pending) return pending; const loginRequest = (async () => { - let signedToken; - try { - signedToken = await generateApiToken({ appId, key, account, jarPath }); - } catch { - // execFile errors can contain the complete command line, including the key. - throw serviceError("FAILED_PRECONDITION", "vendor token generation failed"); - } - const login = await this.request("/api/login2", { appId, account, token: signedToken }, { authorization: false }); - const token = readToken(login); - if (!token) throw serviceError("UNAUTHENTICATED", "platform login response did not contain a bearer token"); + const token = configured; while (loginCache.size >= MAX_LOGIN_CACHE_ENTRIES) { const oldest = loginCache.keys().next().value; if (oldest === undefined) break; diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/token-generator.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/token-generator.js deleted file mode 100644 index 5c2bb2bd6..000000000 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/token-generator.js +++ /dev/null @@ -1,12 +0,0 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; - -const execFileAsync = promisify(execFile); - -export async function generateApiToken({ appId, key, account, jarPath, dateTime = new Date().toISOString(), run = execFileAsync }) { - if (!jarPath) throw new Error("tokenJarPath is required when apiToken is not configured"); - const { stdout } = await run("java", ["-jar", jarPath, appId, dateTime, key, account], { timeout: 10000, maxBuffer: 64 * 1024 }); - const token = String(stdout).trim(); - if (!token) throw new Error("vendor token generator returned an empty token"); - return token; -} 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 index e9576303e..f90566048 100644 --- 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 @@ -3,7 +3,6 @@ import test from "node:test"; import { handlers, _test as handlerTest } from "../src/vulnplatform-vuln.js"; import { PlatformClient, _test as clientTest } from "../src/client.js"; -import { generateApiToken } from "../src/token-generator.js"; const originalFetch = globalThis.fetch; const ctx = (request = {}) => ({ config: { apiBaseUrl: "https://vuln.example.test", timeoutMs: 50 }, secret: { apiToken: "test-token" }, request }); @@ -77,46 +76,12 @@ test("client validates configuration, covers status mappings, and handles local assert.equal(clientTest.readToken({ data: { token: "two" } }), "two"); }); -test("revoked generated tokens are evicted on HTTP 401", async () => { - clientTest.loginCache.clear(); - const key = clientTest.loginCacheKey("https://x", "id", "key", "account", ""); - clientTest.loginCache.set(key, { token: "revoked", expiresAt: Date.now() + 1000 }); - const client = new PlatformClient({ config: { apiBaseUrl: "https://x" }, secret: { appId: "id", key: "key", account: "account" } }); - globalThis.fetch = async () => response(401, "private"); - await assert.rejects(() => client.request("/test", {}), /UNAUTHENTICATED/); - assert.equal(clientTest.loginCache.has(key), false); -}); - -test("vendor token generation is injectable and token-login failures are mapped without leaking secrets", async () => { - assert.equal(await generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: " token\n" }) }), "token"); - await assert.rejects(() => generateApiToken({ appId: "id", key: "key", account: "account", jarPath: "/vendor.jar", run: async () => ({ stdout: "" }) }), /empty token/); - await assert.rejects(() => generateApiToken({ appId: "id", key: "key", account: "account" }), /tokenJarPath/); - const failure = await new PlatformClient({ config: { apiBaseUrl: "https://x", tokenJarPath: "/missing.jar" }, secret: { appId: "id", key: "very-secret-key", account: "account" } }).bearerToken().catch((error) => error); - assert.match(failure.message, /vendor token generation failed/); - assert.doesNotMatch(failure.message, /very-secret-key|missing\.jar/); - const cacheKey = clientTest.loginCacheKey("https://x", "id", "key", "account", ""); +test("only configured bearer tokens are accepted and cached safely", async () => { + await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x" } }).bearerToken(), /configure apiToken/); + const cacheKey = clientTest.loginCacheKey("https://x", "cached", "", "", ""); assert.doesNotMatch(cacheKey, /key|account/); clientTest.loginCache.set(cacheKey, { token: "cached", expiresAt: Date.now() + 1000 }); - assert.equal(await new PlatformClient({ config: { apiBaseUrl: "https://x" }, secret: { appId: "id", key: "key", account: "account" } }).bearerToken(), "cached"); -}); - -test("login cache isolates credentials and coalesces concurrent vendor logins", async () => { - clientTest.loginCache.clear(); - clientTest.loginRequests.clear(); - const tokenModuleKey = clientTest.loginCacheKey("https://x", "id", "different-key", "account", "/vendor.jar"); - assert.notEqual(tokenModuleKey, clientTest.loginCacheKey("https://x", "id", "key", "account", "/vendor.jar")); - - // Seed an in-flight login to verify that simultaneous calls share one result - // without needing the private vendor JAR in the test environment. - let release; - const pending = new Promise((resolve) => { release = resolve; }); - clientTest.loginRequests.set(tokenModuleKey, pending); - const client = new PlatformClient({ config: { apiBaseUrl: "https://x", tokenJarPath: "/vendor.jar" }, secret: { appId: "id", key: "different-key", account: "account" } }); - const first = client.bearerToken(); - const second = client.bearerToken(); - release("shared-token"); - assert.deepEqual(await Promise.all([first, second]), ["shared-token", "shared-token"]); - clientTest.loginRequests.delete(tokenModuleKey); + 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 () => { From c19003d9070857b547e5a0dee0257a0812afb537 Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 17 Aug 2026 15:29:06 +0800 Subject: [PATCH 07/18] Fix config schema after auth hardening --- .../config.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json index 4a39a3cc0..b03f057cc 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -20,6 +20,6 @@ "type": "boolean", "default": false, "description": "Use only for a trusted private deployment with a self-signed certificate. TLS relaxation is isolated to this service." - }, + } } } From 9321dcde0b968f893aa02def789a0c397cad6f1e Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 17 Aug 2026 15:35:06 +0800 Subject: [PATCH 08/18] Remove obsolete bearer token cache --- .../README.md | 2 +- .../src/client.js | 39 +------------------ .../test/vulnplatform-vuln.test.js | 3 -- 3 files changed, 3 insertions(+), 41 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/README.md b/services/vulnplatform__vulnerability-management_v3-2-0/README.md index 9555407ee..c382e2d62 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/README.md +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -14,7 +14,7 @@ Provide a platform-issued bearer token: {"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. Tokens are cached in-memory for 25 minutes. +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. `skipTlsVerify` defaults to false. If a trusted private installation requires it, the service uses a local undici dispatcher and does not weaken Node’s process-wide TLS policy. diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 2c9f340e8..eb195253e 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -1,15 +1,9 @@ -import { createHash } from "node:crypto"; - import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; import { Agent } from "undici"; const DEFAULT_TIMEOUT_MS = 10000; -const TOKEN_TTL_MS = 25 * 60 * 1000; -const MAX_LOGIN_CACHE_ENTRIES = 256; let insecureDispatcher; -const loginCache = new Map(); -const loginRequests = new Map(); const codes = { INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, @@ -72,17 +66,11 @@ function readToken(body) { return text(value); } -function loginCacheKey(baseURL, appId, key, account, jarPath) { - // Do not put credentials in a long-lived Map key or diagnostic heap output. - return createHash("sha256").update(JSON.stringify([baseURL, appId, key, account, jarPath])).digest("hex"); -} - export class PlatformClient { constructor(ctx = {}) { this.ctx = ctx; this.bindings = bindings(ctx); this.baseURL = endpoint(this.bindings.apiBaseUrl); - this.cacheKey = null; this.timeoutMs = timeoutMs(this.bindings.timeoutMs ?? ctx.limits?.timeoutMs); } @@ -90,29 +78,7 @@ export class PlatformClient { const configured = text(this.bindings.apiToken); if (configured) return configured; if (!configured) throw serviceError("FAILED_PRECONDITION", "configure apiToken"); - const cacheKey = loginCacheKey(this.baseURL, configured, "", "", ""); - this.cacheKey = cacheKey; - const cached = loginCache.get(cacheKey); - if (cached?.expiresAt > Date.now()) return cached.token; - if (cached) loginCache.delete(cacheKey); - const pending = loginRequests.get(cacheKey); - if (pending) return pending; - const loginRequest = (async () => { - const token = configured; - while (loginCache.size >= MAX_LOGIN_CACHE_ENTRIES) { - const oldest = loginCache.keys().next().value; - if (oldest === undefined) break; - loginCache.delete(oldest); - } - loginCache.set(cacheKey, { token, expiresAt: Date.now() + TOKEN_TTL_MS }); - return token; - })(); - loginRequests.set(cacheKey, loginRequest); - try { - return await loginRequest; - } finally { - loginRequests.delete(cacheKey); - } + return configured; } async request(path, body, { method = "POST", authorization = true } = {}) { @@ -141,7 +107,6 @@ export class PlatformClient { throw serviceError(isTimeout(error) ? "DEADLINE_EXCEEDED" : "UNAVAILABLE", isTimeout(error) ? `timeout after ${this.timeoutMs}ms` : "failed to read upstream response"); } if (!response.ok) { - if (response.status === 401 && this.cacheKey) loginCache.delete(this.cacheKey); throw serviceError(errorForStatus(response.status), `upstream returned HTTP ${response.status}`); } if (!raw.trim()) throw serviceError("UNKNOWN", "upstream returned an empty response"); @@ -158,4 +123,4 @@ export class PlatformClient { } } -export const _test = { bindings, endpoint, timeoutMs, isTimeout, errorForStatus, readToken, loginCacheKey, loginCache, loginRequests }; +export const _test = { bindings, endpoint, timeoutMs, isTimeout, errorForStatus, readToken }; 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 index f90566048..5085245b0 100644 --- 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 @@ -78,9 +78,6 @@ test("client validates configuration, covers status mappings, and handles local test("only configured bearer tokens are accepted and cached safely", async () => { await assert.rejects(() => new PlatformClient({ config: { apiBaseUrl: "https://x" } }).bearerToken(), /configure apiToken/); - const cacheKey = clientTest.loginCacheKey("https://x", "cached", "", "", ""); - assert.doesNotMatch(cacheKey, /key|account/); - clientTest.loginCache.set(cacheKey, { token: "cached", expiresAt: Date.now() + 1000 }); assert.equal(await new PlatformClient({ config: { apiBaseUrl: "https://x" }, secret: { apiToken: "cached" } }).bearerToken(), "cached"); }); From 44429c7766d25735abe1eceaeedcba3380079766 Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 17 Aug 2026 15:37:16 +0800 Subject: [PATCH 09/18] Simplify bearer token validation --- .../vulnplatform__vulnerability-management_v3-2-0/src/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index eb195253e..2ac2621f1 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -76,7 +76,6 @@ export class PlatformClient { async bearerToken() { const configured = text(this.bindings.apiToken); - if (configured) return configured; if (!configured) throw serviceError("FAILED_PRECONDITION", "configure apiToken"); return configured; } From 7d30802202d6bc563e6b8fb312b9fdae0cf3f83c Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 10:14:27 +0800 Subject: [PATCH 10/18] Remove obsolete token parsing helper --- .../src/client.js | 8 +------- .../test/vulnplatform-vuln.test.js | 4 +--- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 2ac2621f1..4e2faaffa 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -1,7 +1,6 @@ import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; import { Agent } from "undici"; - const DEFAULT_TIMEOUT_MS = 10000; let insecureDispatcher; @@ -61,11 +60,6 @@ function errorForStatus(status) { return "UNAVAILABLE"; } -function readToken(body) { - const value = body?.token ?? body?.accessToken ?? body?.data?.token ?? body?.data?.accessToken; - return text(value); -} - export class PlatformClient { constructor(ctx = {}) { this.ctx = ctx; @@ -122,4 +116,4 @@ export class PlatformClient { } } -export const _test = { bindings, endpoint, timeoutMs, isTimeout, errorForStatus, readToken }; +export const _test = { bindings, endpoint, timeoutMs, isTimeout, errorForStatus }; 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 index 5085245b0..358690a52 100644 --- 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 @@ -72,11 +72,9 @@ test("client validates configuration, covers status mappings, and handles local assert.equal(clientTest.errorForStatus(499), "FAILED_PRECONDITION"); assert.equal(clientTest.errorForStatus(429), "RESOURCE_EXHAUSTED"); assert.equal(clientTest.errorForStatus(500), "UNAVAILABLE"); - assert.equal(clientTest.readToken({ accessToken: "one" }), "one"); - assert.equal(clientTest.readToken({ data: { token: "two" } }), "two"); }); -test("only configured bearer tokens are accepted and cached safely", async () => { +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"); }); From de05c9bfaa7ace63fa168ea626ac5d55d378ac17 Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 11:27:48 +0800 Subject: [PATCH 11/18] Require secure platform transport --- .../README.md | 2 +- .../config.schema.json | 1 + .../src/client.js | 4 ++-- .../src/vulnplatform-vuln.js | 4 ++-- .../test/vulnplatform-vuln.test.js | 8 ++++++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/README.md b/services/vulnplatform__vulnerability-management_v3-2-0/README.md index c382e2d62..88a52ef00 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/README.md +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -16,7 +16,7 @@ Provide a platform-issued bearer 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. -`skipTlsVerify` defaults to false. If a trusted private installation requires it, the service uses a local undici dispatcher and does not weaken Node’s process-wide TLS policy. +`apiBaseUrl` must use HTTPS so the bearer token is never sent over plaintext HTTP. `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 diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json index b03f057cc..95cbf4d38 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -7,6 +7,7 @@ "apiBaseUrl": { "type": "string", "format": "uri", + "pattern": "^https://", "description": "Vulnerability Management Platform base URL, for example https://vuln-platform.example.com." }, "timeoutMs": { diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 4e2faaffa..c6fcb121e 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -37,10 +37,10 @@ function endpoint(value) { const raw = text(value).replace(/\/+$/, ""); try { const url = new URL(raw); - if (!/^https?:$/.test(url.protocol) || url.username || url.password) throw new Error("unsupported URL"); + if (url.protocol !== "https:" || url.username || url.password) throw new Error("unsupported URL"); return raw; } catch { - throw serviceError("FAILED_PRECONDITION", "apiBaseUrl must be an HTTP(S) URL without embedded credentials"); + throw serviceError("FAILED_PRECONDITION", "apiBaseUrl must be an HTTPS URL without embedded credentials"); } } 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 index d290a5214..5bffa9257 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/vulnplatform-vuln.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/vulnplatform-vuln.js @@ -16,7 +16,7 @@ const call = (path, map = dataOf) => async (ctx = {}) => map(await new PlatformC export const handlers = { [METHOD("VulnerabilityService", "ListVulnerabilities")]: call("/api/vulnerability/list", listResponse), - [METHOD("VulnerabilityService", "CreateVulnerability")]: call("/api/vulnerability/create", (value) => ({ vulId: Number(dataOf(value)?.vulId ?? dataOf(value)?.id ?? 0) })), + [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 : []) }; }), @@ -29,7 +29,7 @@ export const handlers = { [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: Number(dataOf(value)?.standardVulId ?? dataOf(value)?.id ?? 0) })), + [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), }; 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 index 358690a52..b786a6fa1 100644 --- 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 @@ -33,6 +33,13 @@ test("response mappers preserve lists and operation results", async () => { 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 }); @@ -58,6 +65,7 @@ test("HTTP, timeout, and malformed upstream failures map safely", async () => { test("client validates configuration, covers status mappings, and handles local response failures", async () => { assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "ftp://invalid" } }), /FAILED_PRECONDITION/); + assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "http://insecure.example.test" } }), /must be an HTTPS URL/); 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"); From 4ea09655755586d7a5f68e9c48b527f6432dfd0b Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 11:47:05 +0800 Subject: [PATCH 12/18] Allow loopback HTTP for service smoke --- .../README.md | 2 +- .../config.schema.json | 4 ++-- .../src/client.js | 10 ++++++++-- .../test/vulnplatform-vuln.test.js | 6 +++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/README.md b/services/vulnplatform__vulnerability-management_v3-2-0/README.md index 88a52ef00..30afffeaa 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/README.md +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -16,7 +16,7 @@ Provide a platform-issued bearer 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 HTTP. `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. +`apiBaseUrl` must use HTTPS so the bearer token is never sent over plaintext networks. Plain HTTP is accepted only for loopback addresses used by local tests. `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 diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json index 95cbf4d38..44d9ef0bb 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -7,8 +7,8 @@ "apiBaseUrl": { "type": "string", "format": "uri", - "pattern": "^https://", - "description": "Vulnerability Management Platform base URL, for example https://vuln-platform.example.com." + "pattern": "^(?:https://|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?(?:/|$))", + "description": "HTTPS Vulnerability Management Platform base URL, for example https://vuln-platform.example.com. Plain HTTP is rejected except for loopback test endpoints." }, "timeoutMs": { "type": "integer", diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index c6fcb121e..e46a36293 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -37,10 +37,16 @@ function endpoint(value) { const raw = text(value).replace(/\/+$/, ""); try { const url = new URL(raw); - if (url.protocol !== "https:" || url.username || url.password) throw new Error("unsupported URL"); + const loopbackHTTP = url.protocol === "http:" && ( + url.hostname === "localhost" || + url.hostname === "::1" || + url.hostname === "[::1]" || + url.hostname === "127.0.0.1" + ); + if ((url.protocol !== "https:" && !loopbackHTTP) || url.username || url.password) throw new Error("unsupported URL"); return raw; } catch { - throw serviceError("FAILED_PRECONDITION", "apiBaseUrl must be an HTTPS URL without embedded credentials"); + throw serviceError("FAILED_PRECONDITION", "apiBaseUrl must use HTTPS (HTTP is allowed only for loopback) and must not contain credentials"); } } 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 index b786a6fa1..08aac163b 100644 --- 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 @@ -65,7 +65,11 @@ test("HTTP, timeout, and malformed upstream failures map safely", async () => { test("client validates configuration, covers status mappings, and handles local response failures", async () => { assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "ftp://invalid" } }), /FAILED_PRECONDITION/); - assert.throws(() => new PlatformClient({ config: { apiBaseUrl: "http://insecure.example.test" } }), /must be an HTTPS URL/); + 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.equal(clientTest.endpoint("http://localhost:19001/"), "http://localhost:19001"); + 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"); From 8422b6568746ce591948d54e34b89fa856d0028e Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 11:56:30 +0800 Subject: [PATCH 13/18] Use literal loopback transport exceptions --- .../vulnplatform__vulnerability-management_v3-2-0/README.md | 2 +- .../config.schema.json | 3 +-- .../src/client.js | 2 -- .../test/vulnplatform-vuln.test.js | 3 ++- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/README.md b/services/vulnplatform__vulnerability-management_v3-2-0/README.md index 30afffeaa..b04e88a15 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/README.md +++ b/services/vulnplatform__vulnerability-management_v3-2-0/README.md @@ -16,7 +16,7 @@ Provide a platform-issued bearer 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 loopback addresses used by local tests. `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. +`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 diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json index 44d9ef0bb..389b7c08c 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -7,8 +7,7 @@ "apiBaseUrl": { "type": "string", "format": "uri", - "pattern": "^(?:https://|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?(?:/|$))", - "description": "HTTPS Vulnerability Management Platform base URL, for example https://vuln-platform.example.com. Plain HTTP is rejected except for loopback test endpoints." + "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", diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index e46a36293..b77d164fd 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -38,8 +38,6 @@ function endpoint(value) { try { const url = new URL(raw); const loopbackHTTP = url.protocol === "http:" && ( - url.hostname === "localhost" || - url.hostname === "::1" || url.hostname === "[::1]" || url.hostname === "127.0.0.1" ); 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 index 08aac163b..3f19b6c03 100644 --- 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 @@ -67,7 +67,8 @@ test("client validates configuration, covers status mappings, and handles local 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.equal(clientTest.endpoint("http://localhost:19001/"), "http://localhost:19001"); + 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/); From 56e726d0fe21d2c355a6d3cd8c711f8e81285370 Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 12:02:53 +0800 Subject: [PATCH 14/18] Align platform URL schema validation --- .../config.schema.json | 1 + .../test/vulnplatform-vuln.test.js | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json index 389b7c08c..f3db6d514 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -7,6 +7,7 @@ "apiBaseUrl": { "type": "string", "format": "uri", + "pattern": "^(?:https://(?:\\[[0-9A-Fa-f:.]+\\]|[^/@\\s:?#]+)(?::[0-9]+)?(?:[/?#][^\\s]*)?|http://(?:127\\.0\\.0\\.1|\\[::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": { 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 index 3f19b6c03..f3debdc37 100644 --- 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 @@ -1,4 +1,5 @@ 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"; @@ -7,6 +8,7 @@ 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; }); @@ -64,6 +66,15 @@ test("HTTP, timeout, and malformed upstream failures map safely", async () => { }); 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://[2001:db8::1]:8443/api", "http://127.0.0.1:19001", "http://[::1]:19001/api"]) { + 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"]) { + 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/); From bbd3b73b35f8dc4d283ef5378a28c15b29a8aa18 Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 12:11:52 +0800 Subject: [PATCH 15/18] Align runtime URL normalization rules --- .../src/client.js | 8 +++----- .../test/vulnplatform-vuln.test.js | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index b77d164fd..965d2c5f9 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -37,11 +37,9 @@ function endpoint(value) { const raw = text(value).replace(/\/+$/, ""); try { const url = new URL(raw); - const loopbackHTTP = url.protocol === "http:" && ( - url.hostname === "[::1]" || - url.hostname === "127.0.0.1" - ); - if ((url.protocol !== "https:" && !loopbackHTTP) || url.username || url.password) throw new Error("unsupported URL"); + const secureHTTPS = raw.startsWith("https://") && url.protocol === "https:"; + const loopbackHTTP = url.protocol === "http:" && /^http:\/\/(?:127\.0\.0\.1|\[::1\])(?::[0-9]+)?(?:[/?#]|$)/.test(raw); + 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"); 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 index f3debdc37..6ad0ffd4e 100644 --- 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 @@ -71,7 +71,7 @@ test("client validates configuration, covers status mappings, and handles local 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"]) { + 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", "HTTPS://platform.example.test", "HTTP://127.0.0.1:19001", "http://[0:0:0:0:0:0:0:1]:19001"]) { assert.equal(endpointPattern.test(rejected), false, rejected); assert.throws(() => clientTest.endpoint(rejected), /FAILED_PRECONDITION/); } From 89a232067d443668d77f224fa62584093bdea0fb Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 12:18:34 +0800 Subject: [PATCH 16/18] Accept normalized secure URL forms --- .../config.schema.json | 2 +- .../src/client.js | 4 ++-- .../test/vulnplatform-vuln.test.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json index f3db6d514..2adca3097 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json +++ b/services/vulnplatform__vulnerability-management_v3-2-0/config.schema.json @@ -7,7 +7,7 @@ "apiBaseUrl": { "type": "string", "format": "uri", - "pattern": "^(?:https://(?:\\[[0-9A-Fa-f:.]+\\]|[^/@\\s:?#]+)(?::[0-9]+)?(?:[/?#][^\\s]*)?|http://(?:127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?(?:[/?#][^\\s]*)?)$", + "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": { diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 965d2c5f9..38382991f 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -37,8 +37,8 @@ function endpoint(value) { const raw = text(value).replace(/\/+$/, ""); try { const url = new URL(raw); - const secureHTTPS = raw.startsWith("https://") && url.protocol === "https:"; - const loopbackHTTP = url.protocol === "http:" && /^http:\/\/(?:127\.0\.0\.1|\[::1\])(?::[0-9]+)?(?:[/?#]|$)/.test(raw); + const secureHTTPS = url.protocol === "https:"; + const loopbackHTTP = url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "[::1]"); if ((!secureHTTPS && !loopbackHTTP) || url.username || url.password) throw new Error("unsupported URL"); return raw; } catch { 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 index 6ad0ffd4e..614b7916b 100644 --- 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 @@ -67,11 +67,11 @@ test("HTTP, timeout, and malformed upstream failures map safely", async () => { 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://[2001:db8::1]:8443/api", "http://127.0.0.1:19001", "http://[::1]:19001/api"]) { + 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", "HTTPS://platform.example.test", "HTTP://127.0.0.1:19001", "http://[0:0:0:0:0:0:0:1]:19001"]) { + 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"]) { assert.equal(endpointPattern.test(rejected), false, rejected); assert.throws(() => clientTest.endpoint(rejected), /FAILED_PRECONDITION/); } From 19993b30725f879202d76dc130e38f895079e4d4 Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 12:25:47 +0800 Subject: [PATCH 17/18] Reject ambiguous platform URL forms --- .../src/client.js | 6 ++++-- .../test/vulnplatform-vuln.test.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index 38382991f..f33e370bc 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -37,8 +37,10 @@ function endpoint(value) { const raw = text(value).replace(/\/+$/, ""); try { const url = new URL(raw); - const secureHTTPS = url.protocol === "https:"; - const loopbackHTTP = url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "[::1]"); + const secureHTTPS = url.protocol === "https:" && /^https:\/\//i.test(raw); + 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 { 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 index 614b7916b..fe8d3f090 100644 --- 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 @@ -71,7 +71,7 @@ test("client validates configuration, covers status mappings, and handles local 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"]) { + 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"]) { assert.equal(endpointPattern.test(rejected), false, rejected); assert.throws(() => clientTest.endpoint(rejected), /FAILED_PRECONDITION/); } From 7c0e7ca58c547540d5ee4b611f3a46e4dab14d54 Mon Sep 17 00:00:00 2001 From: kingfs Date: Tue, 18 Aug 2026 12:33:35 +0800 Subject: [PATCH 18/18] Require a platform URL hostname --- .../vulnplatform__vulnerability-management_v3-2-0/src/client.js | 2 +- .../test/vulnplatform-vuln.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js index f33e370bc..b22d36b02 100644 --- a/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js +++ b/services/vulnplatform__vulnerability-management_v3-2-0/src/client.js @@ -37,7 +37,7 @@ function endpoint(value) { const raw = text(value).replace(/\/+$/, ""); try { const url = new URL(raw); - const secureHTTPS = url.protocol === "https:" && /^https:\/\//i.test(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]"); 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 index fe8d3f090..60846f21f 100644 --- 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 @@ -71,7 +71,7 @@ test("client validates configuration, covers status mappings, and handles local 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"]) { + 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/); }