diff --git a/services/aliyun__sas-vulnerability-management/README.md b/services/aliyun__sas-vulnerability-management/README.md new file mode 100644 index 00000000..19e084f8 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/README.md @@ -0,0 +1,174 @@ +# aliyun-sas-vulnerability-management + +Alibaba Cloud Security Center (SAS) Vulnerability Management service package. + +## 支持版本 + +- **产品**: 阿里云安全中心 (Security Center / SAS) +- **API 版本**: `2018-12-03` +- **接入方式**: Alibaba Cloud OpenAPI (RPC, HMAC-SHA1 签名 V1) +- **API 端点**: `tds.{regionId}.aliyuncs.com`(中国区默认 `cn-shanghai`) + +## 配置示例 + +```json +{ + "regionId": "cn-shanghai", + "timeoutMs": 10000 +} +``` + +```json +{ + "accessKeyId": "", + "accessKeySecret": "" +} +``` + +## 导入 + +```bash +octobus service import --id aliyun-sas ./services/aliyun__sas-vulnerability-management +``` + +## 方法说明 + +### 漏洞查询(4 个) + +| RPC Method | CLI Command | 描述 | +|------------|-------------|------| +| `DescribeVulList` | `describe-vul-list` | 分页查询漏洞列表,支持按类型/等级/处理状态过滤 | +| `DescribeVulDetails` | `describe-vul-details` | 查询指定漏洞的详细信息(CVE、修复建议) | +| `DescribeUuidsByVulNames` | `describe-uuids-by-vul-names` | 根据漏洞名称查询受影响服务器 UUID | +| `DescribeCanFixVulList` | `describe-can-fix-vul-list` | 查询可修复的漏洞列表 | + +### 漏洞操作(2 个) + +| RPC Method | CLI Command | 描述 | 幂等 | +|------------|-------------|------|:--:| +| `ModifyOperateVul` | `modify-operate-vul` | 处理漏洞(修复/验证/忽略/取消忽略),oeprateType=`vul_fix\|verify\|ignore\|cancel_ignore` | ✅ | +| `OperateVuls` | `operate-vuls` | 批量修复 Linux 软件漏洞 | ✅ | + +### 统计(2 个) + +| RPC Method | CLI Command | 描述 | +|------------|-------------|------| +| `DescribeVulNumStatistics` | `describe-vul-num-statistics` | 漏洞数量统计(按严重等级和类型) | +| `DescribeVulFixStatistics` | `describe-vul-fix-statistics` | 漏洞修复统计 | + +### 白名单(3 个) + +| RPC Method | CLI Command | 描述 | 幂等 | +|------------|-------------|------|:--:| +| `DescribeVulWhitelist` | `describe-vul-whitelist` | 分页查询漏洞白名单 | — | +| `ModifyCreateVulWhitelist` | `modify-create-vul-whitelist` | 添加漏洞到白名单(不再告警) | ❌ 重复添加会创建多条 | +| `DeleteVulWhitelist` | `delete-vul-whitelist` | 按 ID 删除白名单条目 | ✅ | + +### 应急漏洞(1 个) + +| RPC Method | CLI Command | 描述 | +|------------|-------------|------| +| `DescribeEmgVulItem` | `describe-emg-vul-item` | 查询应急漏洞详情 | + +## Config + +| 字段 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| `regionId` | string | `cn-shanghai` | SAS API 区域(中国区均映射到 cn-shanghai) | +| `timeoutMs` | integer | `10000` | HTTP 请求超时(毫秒) | + +## Secret + +| 字段 | 类型 | 必填 | 描述 | +|------|------|:--:|------| +| `accessKeyId` | string | ✅ | 阿里云 AccessKey ID | +| `accessKeySecret` | string | ✅ | 阿里云 AccessKey Secret | + +## 错误映射 + +| 阿里云错误码 | gRPC 状态 | +|-------------|-----------| +| `InvalidAccessKeyId.*`, `SignatureDoesNotMatch` | `UNAUTHENTICATED` | +| `Forbidden.*` | `PERMISSION_DENIED` | +| `Invalid*`, `Missing*` | `INVALID_ARGUMENT` | +| `*NotFound*`, `*NotExist*` | `NOT_FOUND` | +| `Throttling*`, `LimitExceeded*` | `UNAVAILABLE` | +| HTTP 401/403 | `PERMISSION_DENIED` | +| HTTP 4xx | `FAILED_PRECONDITION` | +| HTTP 5xx | `UNAVAILABLE` | + +## 写操作说明 + +### ModifyOperateVul +- **描述**: 对单条或多条漏洞执行操作 +- **必填参数**: `type`(漏洞类型)、`operateType`(操作类型)、`info`(JSON 字符串,包含漏洞 ID 列表) +- **幂等性**: ✅ 重复修复同一漏洞不会重复消耗配额 +- **回滚说明**: `ignore` 操作可通过 `cancel_ignore` 回滚;`vul_fix` 无法回滚 +- **审计字段**: RequestId(请求追踪)、operateType(操作类型)、info(操作对象) + +### OperateVuls +- **描述**: 批量修复 Linux 软件漏洞(cve 类型) +- **必填参数**: `type`、`operateType`、`uuids`(服务器 ID 列表)、`vulNames`(漏洞名称列表) +- **幂等性**: ✅ 重复修复已修复的漏洞为 no-op +- **回滚说明**: 修复操作不可回滚,建议先在测试环境验证 +- **审计字段**: RequestId、type、vulNames(漏洞范围)、uuids(目标服务器) + +### ModifyCreateVulWhitelist +- **描述**: 添加漏洞到白名单,Security Center 不再告警 +- **必填参数**: `whitelist`(漏洞名称)、`reason`(加白原因) +- **可选参数**: `targetInfo`(仅对特定服务器生效,JSON 格式) +- **幂等性**: ❌ 重复调用会创建多条白名单记录 +- **回滚说明**: 使用 `DeleteVulWhitelist` 按 ID 删除对应条目 +- **审计字段**: RequestId、whitelist、reason + +### DeleteVulWhitelist +- **描述**: 按 ID 列表删除白名单条目 +- **必填参数**: `idList`(白名单 ID 数组,可通过 DescribeVulWhitelist 获取) +- **幂等性**: ✅ 删除已删除的条目无副作用 +- **回滚说明**: 删除后需重新调用 ModifyCreateVulWhitelist 添加 + +## 风险说明 + +**读取操作(低风险)**: +- `Describe*` 方法仅查询数据,不会修改任何安全配置 +- API 调用频率受阿里云默认限流控制(通常 100次/秒) + +**写入操作(中风险)**: +- `ModifyOperateVul` / `OperateVuls`: 修复操作会触发服务器上的补丁安装,可能导致服务重启 +- `ModifyCreateVulWhitelist`: 加白的漏洞不再告警,存在遗漏风险 +- `DeleteVulWhitelist`: 删除后对应漏洞恢复告警,可能造成告警噪音 +- 建议: 写入操作前先在控制台确认操作范围,批量操作建议分批次执行 + +**认证凭据风险**: +- AccessKey 需具备 SAS 权限(建议 `AliyunYundunSASReadOnlyAccess` 用于只读,`AliyunYundunSASFullAccess` 用于写入) +- 生产环境建议使用 RAM 子账号,遵循最小权限原则 + +## 建议 Capset + +### `read` — 只读查询 +``` +DescribeVulList, DescribeVulDetails, DescribeUuidsByVulNames, DescribeCanFixVulList, +DescribeVulNumStatistics, DescribeVulFixStatistics, DescribeVulWhitelist, DescribeEmgVulItem +``` +适用场景: SOC 分析、漏洞报表、态势感知 + +### `write` — 漏洞处置 +``` +ModifyOperateVul, OperateVuls, ModifyCreateVulWhitelist, DeleteVulWhitelist +``` +适用场景: 漏洞修复工单、白名单管理 + +### `admin` — 完整权限 +``` +read + write 全部方法 +``` + +## 测试 + +```bash +# 单元测试 +node --test test/ + +# 真实 API 测试(需要 AKSK) +ACCESS_KEY_ID=xxx ACCESS_KEY_SECRET=yyy SAS_REGION=cn-shanghai node test/test-live.js +``` diff --git a/services/aliyun__sas-vulnerability-management/bin/aliyun-sas-vulnerability-management.js b/services/aliyun__sas-vulnerability-management/bin/aliyun-sas-vulnerability-management.js new file mode 100644 index 00000000..604f5241 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/bin/aliyun-sas-vulnerability-management.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { runServiceMain } from '@chaitin-ai/octobus-sdk'; +import { service } from '../src/service.js'; +runServiceMain(service); diff --git a/services/aliyun__sas-vulnerability-management/config.schema.json b/services/aliyun__sas-vulnerability-management/config.schema.json new file mode 100644 index 00000000..9fc96eb1 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/config.schema.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "additionalProperties": true, + "properties": { + "regionId": { + "type": "string", + "default": "cn-shanghai", + "description": "Alibaba Cloud region ID. SAS API uses tds.{regionId}.aliyuncs.com. Note: most China regions map to tds.cn-shanghai.aliyuncs.com." + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 10000, + "description": "HTTP request timeout in milliseconds" + } + } +} diff --git a/services/aliyun__sas-vulnerability-management/package.json b/services/aliyun__sas-vulnerability-management/package.json new file mode 100644 index 00000000..17a9bcd0 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/package.json @@ -0,0 +1,12 @@ +{ + "name": "aliyun-sas-vulnerability-management", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "aliyun-sas-vulnerability-management": "bin/aliyun-sas-vulnerability-management.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.5.0" + } +} diff --git a/services/aliyun__sas-vulnerability-management/proto/aliyun_sas_vulnerability_management.proto b/services/aliyun__sas-vulnerability-management/proto/aliyun_sas_vulnerability_management.proto new file mode 100644 index 00000000..754494e7 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/proto/aliyun_sas_vulnerability_management.proto @@ -0,0 +1,175 @@ +syntax = "proto3"; +package Aliyun_SAS_Vulnerability_Management; +option go_package = "miner/grpc-service/Aliyun_SAS_Vulnerability_Management"; + +import "google/protobuf/struct.proto"; + +service SASService { + rpc DescribeVulList(DescribeVulListRequest) returns (DescribeVulListResponse) {} + rpc DescribeVulDetails(DescribeVulDetailsRequest) returns (DescribeVulDetailsResponse) {} + rpc DescribeUuidsByVulNames(DescribeUuidsByVulNamesRequest) returns (DescribeUuidsByVulNamesResponse) {} + rpc DescribeCanFixVulList(DescribeCanFixVulListRequest) returns (DescribeCanFixVulListResponse) {} + rpc ModifyOperateVul(ModifyOperateVulRequest) returns (ModifyOperateVulResponse) {} + rpc OperateVuls(OperateVulsRequest) returns (OperateVulsResponse) {} + rpc DescribeVulNumStatistics(DescribeVulNumStatisticsRequest) returns (DescribeVulNumStatisticsResponse) {} + rpc DescribeVulFixStatistics(DescribeVulFixStatisticsRequest) returns (DescribeVulFixStatisticsResponse) {} + rpc DescribeVulWhitelist(DescribeVulWhitelistRequest) returns (DescribeVulWhitelistResponse) {} + rpc ModifyCreateVulWhitelist(ModifyCreateVulWhitelistRequest) returns (ModifyCreateVulWhitelistResponse) {} + rpc DeleteVulWhitelist(DeleteVulWhitelistRequest) returns (DeleteVulWhitelistResponse) {} + rpc DescribeEmgVulItem(DescribeEmgVulItemRequest) returns (DescribeEmgVulItemResponse) {} +} + +// ---- DescribeVulList ---- +message DescribeVulListRequest { + string type = 1; + repeated string uuids = 2; + string alias_name = 3 [json_name = "aliasName"]; + string dealed = 4; + string severity = 5; + string group_id = 6 [json_name = "groupId"]; + int32 current_page = 7 [json_name = "currentPage"]; + int32 page_size = 8 [json_name = "pageSize"]; + string lang = 9; + string attach_types = 10 [json_name = "attachTypes"]; + string ids = 11; + int64 create_ts_start = 12 [json_name = "createTsStart"]; + int64 create_ts_end = 13 [json_name = "createTsEnd"]; +} +message DescribeVulListResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DescribeVulDetails ---- +message DescribeVulDetailsRequest { + string type = 1; + string name = 2; + string alias_name = 3 [json_name = "aliasName"]; + string lang = 4; +} +message DescribeVulDetailsResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DescribeUuidsByVulNames ---- +message DescribeUuidsByVulNamesRequest { + string type = 1; + repeated string vul_names = 2 [json_name = "vulNames"]; + string lang = 3; +} +message DescribeUuidsByVulNamesResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DescribeCanFixVulList ---- +message DescribeCanFixVulListRequest { + string type = 1; + repeated string uuids = 2; + string alias_name = 3 [json_name = "aliasName"]; + string dealed = 4; + repeated string status_list = 5 [json_name = "statusList"]; + int32 current_page = 6 [json_name = "currentPage"]; + int32 page_size = 7 [json_name = "pageSize"]; + string lang = 8; +} +message DescribeCanFixVulListResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- ModifyOperateVul ---- +message ModifyOperateVulRequest { + string type = 1; + string operate_type = 2 [json_name = "operateType"]; + string info = 3; +} +message ModifyOperateVulResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- OperateVuls ---- +message OperateVulsRequest { + repeated string uuids = 1; + string type = 2; + string operate_type = 3 [json_name = "operateType"]; + repeated string vul_names = 4 [json_name = "vulNames"]; +} +message OperateVulsResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DescribeVulNumStatistics ---- +message DescribeVulNumStatisticsRequest { + string type = 1; + string resource_directory_account_id = 2 [json_name = "resourceDirectoryAccountId"]; +} +message DescribeVulNumStatisticsResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DescribeVulFixStatistics ---- +message DescribeVulFixStatisticsRequest { +} +message DescribeVulFixStatisticsResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DescribeVulWhitelist ---- +message DescribeVulWhitelistRequest { + int32 current_page = 1 [json_name = "currentPage"]; + int32 page_size = 2 [json_name = "pageSize"]; +} +message DescribeVulWhitelistResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- ModifyCreateVulWhitelist ---- +message ModifyCreateVulWhitelistRequest { + string whitelist = 1; + string reason = 2; + string target_info = 3 [json_name = "targetInfo"]; +} +message ModifyCreateVulWhitelistResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DeleteVulWhitelist ---- +message DeleteVulWhitelistRequest { + repeated string id_list = 1 [json_name = "idList"]; +} +message DeleteVulWhitelistResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} + +// ---- DescribeEmgVulItem ---- +message DescribeEmgVulItemRequest { + string scan_type = 1 [json_name = "scanType"]; + string lang = 2; + int32 current_page = 3 [json_name = "currentPage"]; + int32 page_size = 4 [json_name = "pageSize"]; +} +message DescribeEmgVulItemResponse { + int32 http_status = 1 [json_name = "httpStatus"]; + string raw_body = 2 [json_name = "rawBody"]; + google.protobuf.Value raw_json = 3 [json_name = "rawJson"]; +} diff --git a/services/aliyun__sas-vulnerability-management/secret.schema.json b/services/aliyun__sas-vulnerability-management/secret.schema.json new file mode 100644 index 00000000..91c4e65b --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/secret.schema.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "additionalProperties": true, + "required": ["accessKeyId", "accessKeySecret"], + "properties": { + "accessKeyId": { + "type": "string", + "minLength": 1, + "description": "Alibaba Cloud AccessKey ID (also available via ALIBABA_CLOUD_ACCESS_KEY_ID env var)" + }, + "accessKeySecret": { + "type": "string", + "minLength": 1, + "description": "Alibaba Cloud AccessKey Secret (also available via ALIBABA_CLOUD_ACCESS_KEY_SECRET env var)" + } + } +} diff --git a/services/aliyun__sas-vulnerability-management/service.json b/services/aliyun__sas-vulnerability-management/service.json new file mode 100644 index 00000000..aebb5826 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/service.json @@ -0,0 +1,69 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "aliyun-sas-vulnerability-management", + "displayName": "Alibaba Cloud SAS Vulnerability Management (2018-12-03)", + "description": "OctoBus package for Alibaba Cloud Security Center (SAS) API v2018-12-03. Covers vulnerability management: query vul list/details, fix operations, statistics, whitelist management, and emergency vulnerabilities.", + "runtime": { + "mode": "on-demand" + }, + "proto": { + "roots": ["proto"], + "files": ["proto/aliyun_sas_vulnerability_management.proto"] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeVulList": { + "name": "describe-vul-list", + "description": "Query vulnerability list with filters (type, severity, status, etc.)" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeVulDetails": { + "name": "describe-vul-details", + "description": "Query vulnerability details by name and type" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeUuidsByVulNames": { + "name": "describe-uuids-by-vul-names", + "description": "Query affected server UUIDs by vulnerability names" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeCanFixVulList": { + "name": "describe-can-fix-vul-list", + "description": "Query fixable vulnerability list" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/ModifyOperateVul": { + "name": "modify-operate-vul", + "description": "Handle vulnerabilities (fix, verify, ignore, unignore)" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/OperateVuls": { + "name": "operate-vuls", + "description": "Batch fix Linux software vulnerabilities" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeVulNumStatistics": { + "name": "describe-vul-num-statistics", + "description": "Get vulnerability count statistics by severity and type" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeVulFixStatistics": { + "name": "describe-vul-fix-statistics", + "description": "Query vulnerability fix statistics" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeVulWhitelist": { + "name": "describe-vul-whitelist", + "description": "Query vulnerability whitelist with pagination" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/ModifyCreateVulWhitelist": { + "name": "modify-create-vul-whitelist", + "description": "Add vulnerability to whitelist" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DeleteVulWhitelist": { + "name": "delete-vul-whitelist", + "description": "Delete vulnerability whitelist entries" + }, + "Aliyun_SAS_Vulnerability_Management.SASService/DescribeEmgVulItem": { + "name": "describe-emg-vul-item", + "description": "Query emergency vulnerability details" + } + } + } + } +} diff --git a/services/aliyun__sas-vulnerability-management/src/aliyun-sas-vulnerability-management.js b/services/aliyun__sas-vulnerability-management/src/aliyun-sas-vulnerability-management.js new file mode 100644 index 00000000..c8b67b1d --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/src/aliyun-sas-vulnerability-management.js @@ -0,0 +1,637 @@ +import crypto from 'node:crypto'; +import { + GrpcError, + grpcStatus, +} from '@chaitin-ai/octobus-sdk'; + +// --------------- Method constants --------------- +const PKG = 'Aliyun_SAS_Vulnerability_Management'; +const SVC = 'SASService'; +const METHOD = (name) => `${PKG}.${SVC}/${name}`; + +const METHOD_DESCRIBE_VUL_LIST_PATH = `/${METHOD('DescribeVulList')}`; +const METHOD_DESCRIBE_VUL_DETAILS_PATH = `/${METHOD('DescribeVulDetails')}`; +const METHOD_DESCRIBE_UUIDS_BY_VUL_NAMES_PATH = `/${METHOD('DescribeUuidsByVulNames')}`; +const METHOD_DESCRIBE_CAN_FIX_VUL_LIST_PATH = `/${METHOD('DescribeCanFixVulList')}`; +const METHOD_MODIFY_OPERATE_VUL_PATH = `/${METHOD('ModifyOperateVul')}`; +const METHOD_OPERATE_VULS_PATH = `/${METHOD('OperateVuls')}`; +const METHOD_DESCRIBE_VUL_NUM_STATISTICS_PATH = `/${METHOD('DescribeVulNumStatistics')}`; +const METHOD_DESCRIBE_VUL_FIX_STATISTICS_PATH = `/${METHOD('DescribeVulFixStatistics')}`; +const METHOD_DESCRIBE_VUL_WHITELIST_PATH = `/${METHOD('DescribeVulWhitelist')}`; +const METHOD_MODIFY_CREATE_VUL_WHITELIST_PATH = `/${METHOD('ModifyCreateVulWhitelist')}`; +const METHOD_DELETE_VUL_WHITELIST_PATH = `/${METHOD('DeleteVulWhitelist')}`; +const METHOD_DESCRIBE_EMG_VUL_ITEM_PATH = `/${METHOD('DescribeEmgVulItem')}`; + +const METHOD_DESCRIBE_VUL_LIST_FULL = METHOD('DescribeVulList'); +const METHOD_DESCRIBE_VUL_DETAILS_FULL = METHOD('DescribeVulDetails'); +const METHOD_DESCRIBE_UUIDS_BY_VUL_NAMES_FULL = METHOD('DescribeUuidsByVulNames'); +const METHOD_DESCRIBE_CAN_FIX_VUL_LIST_FULL = METHOD('DescribeCanFixVulList'); +const METHOD_MODIFY_OPERATE_VUL_FULL = METHOD('ModifyOperateVul'); +const METHOD_OPERATE_VULS_FULL = METHOD('OperateVuls'); +const METHOD_DESCRIBE_VUL_NUM_STATISTICS_FULL = METHOD('DescribeVulNumStatistics'); +const METHOD_DESCRIBE_VUL_FIX_STATISTICS_FULL = METHOD('DescribeVulFixStatistics'); +const METHOD_DESCRIBE_VUL_WHITELIST_FULL = METHOD('DescribeVulWhitelist'); +const METHOD_MODIFY_CREATE_VUL_WHITELIST_FULL = METHOD('ModifyCreateVulWhitelist'); +const METHOD_DELETE_VUL_WHITELIST_FULL = METHOD('DeleteVulWhitelist'); +const METHOD_DESCRIBE_EMG_VUL_ITEM_FULL = METHOD('DescribeEmgVulItem'); + +// --------------- Defaults --------------- +const DEFAULT_TIMEOUT_MS = 10000; +const DEFAULT_REGION = 'cn-shanghai'; +const API_VERSION = '2018-12-03'; + +// --------------- Utilities --------------- +const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + +function toTrimmedString(value) { + if (typeof value === 'string') return value.trim(); + return undefined; +} + +// --------------- gRPC error helpers --------------- +const GRPC_CODE_MAP = { + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + UNAUTHENTICATED: grpcStatus.UNAUTHENTICATED, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + NOT_FOUND: grpcStatus.NOT_FOUND, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + INTERNAL: grpcStatus.INTERNAL, +}; + +function grpcCodeFor(code) { + return GRPC_CODE_MAP[code] || grpcStatus.INTERNAL; +} + +function errorWithCode(code, message) { + const e = new GrpcError(grpcCodeFor(code), message); + e.legacyCode = code; + return e; +} + +function throwStructuredError(code, message, options = {}) { + const payload = { code, message }; + if (hasOwn(options, 'httpStatus')) payload.http_status = options.httpStatus; + if (hasOwn(options, 'rawBody')) payload.raw_body = options.rawBody; + if (hasOwn(options, 'rawJson')) payload.raw_json = options.rawJson; + if (hasOwn(options, 'reason')) payload.reason = options.reason; + if (hasOwn(options, 'responseCode')) payload.response_code = options.responseCode; + throw errorWithCode(code, JSON.stringify(payload)); +} + +// --------------- Context resolution --------------- +const VALID_REGION_RE = /^[a-z][a-z0-9-]*$/; + +function resolveRegionId(ctx) { + const config = ctx.config ?? {}; + const raw = toTrimmedString(config.regionId) || DEFAULT_REGION; + if (!VALID_REGION_RE.test(raw)) { + throwStructuredError('INVALID_ARGUMENT', `Invalid regionId: ${raw}`); + } + return raw; +} + +function resolveAccessKeyId(ctx) { + const secret = ctx.secret ?? {}; + const val = toTrimmedString(secret.accessKeyId); + if (!val) throwStructuredError('INVALID_ARGUMENT', 'accessKeyId is required in secret'); + return val; +} + +function resolveAccessKeySecret(ctx) { + const secret = ctx.secret ?? {}; + const val = toTrimmedString(secret.accessKeySecret); + if (!val) throwStructuredError('INVALID_ARGUMENT', 'accessKeySecret is required in secret'); + return val; +} + +function resolveTimeoutMs(ctx) { + const config = ctx.config ?? {}; + const ms = config.timeoutMs ?? DEFAULT_TIMEOUT_MS; + return Number.isFinite(ms) && ms > 0 ? ms : DEFAULT_TIMEOUT_MS; +} + +// --------------- Alibaba Cloud OpenAPI signature V1 --------------- + +function aliyunPercentEncode(str) { + return encodeURIComponent(str) + .replace(/%7E/g, '~') + .replace(/\*/g, '%2A') + .replace(/%20/g, '%20') + .replace(/!/g, '%21') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29') + .replace(/\+/g, '%2B'); +} + +function randomNonce() { + return crypto.randomUUID().replace(/-/g, ''); +} + +function iso8601Timestamp() { + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + return ( + `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}T` + + `${pad(now.getUTCHours())}:${pad(now.getUTCMinutes())}:${pad(now.getUTCSeconds())}Z` + ); +} + +function buildSignedParams(action, businessParams, accessKeyId, accessKeySecret) { + const params = new Map(); + + // System params + params.set('AccessKeyId', accessKeyId); + params.set('Action', action); + params.set('Format', 'JSON'); + params.set('SignatureMethod', 'HMAC-SHA1'); + params.set('SignatureNonce', randomNonce()); + params.set('SignatureVersion', '1.0'); + params.set('Timestamp', iso8601Timestamp()); + params.set('Version', API_VERSION); + + // Business params + for (const [key, value] of Object.entries(businessParams)) { + if (value === undefined || value === null || value === '') continue; + if (Array.isArray(value)) { + value.forEach((item, i) => { + params.set(`${key}.${i + 1}`, String(item)); + }); + } else { + params.set(key, String(value)); + } + } + + // Sort by key + const sorted = [...params.entries()].sort(([a], [b]) => { + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + + const canonicalQuery = sorted + .map(([k, v]) => `${aliyunPercentEncode(k)}=${aliyunPercentEncode(v)}`) + .join('&'); + + const stringToSign = `POST&${aliyunPercentEncode('/')}&${aliyunPercentEncode(canonicalQuery)}`; + const hmac = crypto.createHmac('sha1', `${accessKeySecret}&`); + hmac.update(stringToSign); + const signature = hmac.digest('base64'); + + params.set('Signature', signature); + + return params; +} + +// --------------- HTTP --------------- + +// --------------- SAS Endpoint Mapping --------------- +// SAS API routes all China regions to tds.cn-shanghai.aliyuncs.com. +// Reference: @alicloud/sas20181203 SDK endpointMap +const SAS_ENDPOINT_MAP = { + 'cn-qingdao': 'tds.cn-shanghai.aliyuncs.com', + 'cn-beijing': 'tds.cn-shanghai.aliyuncs.com', + 'cn-zhangjiakou': 'tds.cn-shanghai.aliyuncs.com', + 'cn-huhehaote': 'tds.cn-shanghai.aliyuncs.com', + 'cn-wulanchabu': 'tds.cn-shanghai.aliyuncs.com', + 'cn-hangzhou': 'tds.cn-shanghai.aliyuncs.com', + 'cn-shanghai': 'tds.cn-shanghai.aliyuncs.com', + 'cn-nanjing': 'tds.cn-shanghai.aliyuncs.com', + 'cn-fuzhou': 'tds.cn-shanghai.aliyuncs.com', + 'cn-shenzhen': 'tds.cn-shanghai.aliyuncs.com', + 'cn-heyuan': 'tds.cn-shanghai.aliyuncs.com', + 'cn-guangzhou': 'tds.cn-shanghai.aliyuncs.com', + 'cn-chengdu': 'tds.cn-shanghai.aliyuncs.com', + 'cn-hongkong': 'tds.cn-hongkong.aliyuncs.com', + 'ap-southeast-1': 'tds.ap-southeast-1.aliyuncs.com', + 'ap-southeast-2': 'tds.ap-southeast-1.aliyuncs.com', + 'ap-southeast-3': 'tds.ap-southeast-1.aliyuncs.com', + 'ap-southeast-5': 'tds.ap-southeast-1.aliyuncs.com', + 'ap-southeast-6': 'tds.ap-southeast-1.aliyuncs.com', + 'ap-northeast-1': 'tds.ap-southeast-1.aliyuncs.com', + 'ap-northeast-2': 'tds.ap-southeast-1.aliyuncs.com', + 'us-east-1': 'tds.us-east-1.aliyuncs.com', + 'us-west-1': 'tds.us-west-1.aliyuncs.com', + 'eu-central-1': 'tds.eu-central-1.aliyuncs.com', + 'eu-west-1': 'tds.eu-west-1.aliyuncs.com', + 'me-east-1': 'tds.me-east-1.aliyuncs.com', + 'ap-south-1': 'tds.ap-south-1.aliyuncs.com', +}; + +function buildUrl(regionId) { + if (!VALID_REGION_RE.test(regionId)) { + throwStructuredError('INVALID_ARGUMENT', `Invalid regionId: ${regionId}`); + } + const endpoint = SAS_ENDPOINT_MAP[regionId] || `tds.${regionId}.aliyuncs.com`; + return `https://${endpoint}/`; +} + +async function callAliyunAPI(ctx, action, businessParams = {}) { + const regionId = resolveRegionId(ctx); + const accessKeyId = resolveAccessKeyId(ctx); + const accessKeySecret = resolveAccessKeySecret(ctx); + const timeoutMs = resolveTimeoutMs(ctx); + + const baseUrl = buildUrl(regionId); + const params = buildSignedParams(action, businessParams, accessKeyId, accessKeySecret); + const body = new URLSearchParams(); + for (const [k, v] of params) { + body.append(k, v); + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + let resp; + try { + resp = await fetch(baseUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + signal: controller.signal, + }); + } catch (err) { + clearTimeout(timer); + const cause = err.cause ? String(err.cause) : undefined; + throwStructuredError('UNAVAILABLE', `Request to Alibaba Cloud failed: ${err.message}`, { + reason: String(err), + cause, + }); + } + clearTimeout(timer); + + const rawBody = await resp.text(); + + if (!resp.ok) { + mapHttpError(resp.status, rawBody); + } + + const parsed = tryParseJson(rawBody); + if (!parsed.ok) { + throwStructuredError('UNAVAILABLE', 'Alibaba Cloud returned non-JSON response', { + httpStatus: resp.status, + rawBody, + }); + } + + assertAliyunSuccess(parsed.value, resp.status, rawBody); + + return { + httpStatus: resp.status, + rawBody, + rawJson: toValue(parsed.value), + }; +} + +function mapHttpError(statusCode, rawBody) { + let parsed; + try { parsed = JSON.parse(rawBody); } catch { parsed = null; } + const errMsg = parsed?.Message || parsed?.message || `HTTP ${statusCode}`; + + if (statusCode === 401 || statusCode === 403) { + throwStructuredError('PERMISSION_DENIED', errMsg, { + httpStatus: statusCode, rawBody, + rawJson: parsed ? toValue(parsed) : undefined, + }); + } + if (statusCode >= 400 && statusCode < 500) { + throwStructuredError('FAILED_PRECONDITION', errMsg, { + httpStatus: statusCode, rawBody, + rawJson: parsed ? toValue(parsed) : undefined, + }); + } + throwStructuredError('UNAVAILABLE', errMsg, { + httpStatus: statusCode, rawBody, + rawJson: parsed ? toValue(parsed) : undefined, + }); +} + +function assertAliyunSuccess(json, statusCode, rawBody) { + if (json && (json.Code || json.code)) { + const code = json.Code || json.code; + const message = json.Message || json.message || 'Unknown error'; + const codeStr = String(code); + if (codeStr !== '200' && codeStr !== 'Success' && codeStr !== 'OK') { + const gcode = mapAliyunErrorCode(codeStr); + throwStructuredError(gcode, message, { + httpStatus: statusCode, + rawBody, + rawJson: toValue(json), + responseCode: codeStr, + }); + } + } +} + +function mapAliyunErrorCode(aliCode) { + const code = String(aliCode); + if (code === 'InvalidAccessKeyId.NotFound' || code === 'InvalidAccessKeyId' || + code === 'SignatureDoesNotMatch' || code === 'Forbidden.AccessKeyDisabled') { + return 'UNAUTHENTICATED'; + } + if (code === 'Forbidden.NotAdminUser' || code === 'Forbidden.AccountInDebt' || + code === 'Forbidden.AccountDebtOverdue') { + return 'PERMISSION_DENIED'; + } + if (code.includes('InvalidParameter') || code.includes('Invalid') || + code.includes('Missing') || code.includes('missing')) { + return 'INVALID_ARGUMENT'; + } + if (code.includes('NotFound') || code.includes('notFound') || code.includes('NotExist')) { + return 'NOT_FOUND'; + } + if (code.includes('Throttling') || code.includes('LimitExceeded') || + code.includes('ServiceUnavailable')) { + return 'UNAVAILABLE'; + } + return 'FAILED_PRECONDITION'; +} + +// --------------- JSON helpers --------------- + +function tryParseJson(text) { + try { + return { ok: true, value: JSON.parse(text) }; + } catch { + return { ok: false, value: undefined }; + } +} + +function toValue(value) { + if (value === null) return { nullValue: 0 }; + if (value === undefined) return { nullValue: 0 }; + const t = typeof value; + if (t === 'string') return { stringValue: value }; + if (t === 'number') return { numberValue: value }; + if (t === 'boolean') return { boolValue: value }; + if (Array.isArray(value)) { + return { listValue: { values: value.map(toValue) } }; + } + if (t === 'object') { + const fields = {}; + for (const [k, v] of Object.entries(value)) { + fields[k] = toValue(v); + } + return { structValue: { fields } }; + } + return { stringValue: String(value) }; +} + +export function unwrapScalar(value) { + if (!value || typeof value !== 'object') return value; + if (hasOwn(value, 'stringValue')) return value.stringValue; + if (hasOwn(value, 'numberValue')) return value.numberValue; + if (hasOwn(value, 'boolValue')) return value.boolValue; + return value; +} + +// --------------- Handlers --------------- + +function buildCallHandler(action, reqMapper) { + return async (ctx) => { + const req = ctx.request ?? {}; + const businessParams = reqMapper ? reqMapper(req) : Object.fromEntries( + Object.entries(req).filter(([, v]) => v !== undefined && v !== null && v !== '' && (!Array.isArray(v) || v.length > 0)) + ); + return callAliyunAPI(ctx, action, businessParams); + }; +} + +// ---- DescribeVulList ---- +function mapDescribeVulListReq(req) { + const p = {}; + if (req.type) p.Type = req.type; + if (req.uuids?.length) p.Uuids = req.uuids; + if (req.aliasName) p.AliasName = req.aliasName; + if (req.dealed) p.Dealed = req.dealed; + if (req.severity) p.Severity = req.severity; + if (req.groupId) p.GroupId = req.groupId; + if (req.currentPage) p.CurrentPage = req.currentPage; + if (req.pageSize) p.PageSize = req.pageSize; + if (req.lang) p.Lang = req.lang; + if (req.attachTypes) p.AttachTypes = req.attachTypes; + if (req.ids) p.Ids = req.ids; + if (req.createTsStart) p.CreateTsStart = req.createTsStart; + if (req.createTsEnd) p.CreateTsEnd = req.createTsEnd; + return p; +} + +// ---- DescribeVulDetails ---- +function mapDescribeVulDetailsReq(req) { + const p = {}; + if (req.type) p.Type = req.type; + if (req.name) p.Name = req.name; + if (req.aliasName) p.AliasName = req.aliasName; + if (req.lang) p.Lang = req.lang; + return p; +} + +// ---- DescribeUuidsByVulNames ---- +function mapDescribeUuidsByVulNamesReq(req) { + const p = {}; + if (req.type) p.Type = req.type; + if (req.vulNames?.length) p.VulNames = req.vulNames; + if (req.lang) p.Lang = req.lang; + return p; +} + +// ---- DescribeCanFixVulList ---- +function mapDescribeCanFixVulListReq(req) { + const p = {}; + if (req.type) p.Type = req.type; + if (req.uuids?.length) p.Uuids = req.uuids; + if (req.aliasName) p.AliasName = req.aliasName; + if (req.dealed) p.Dealed = req.dealed; + if (req.statusList?.length) p.StatusList = req.statusList; + if (req.currentPage) p.CurrentPage = req.currentPage; + if (req.pageSize) p.PageSize = req.pageSize; + if (req.lang) p.Lang = req.lang; + return p; +} + +// ---- ModifyOperateVul ---- +function mapModifyOperateVulReq(req) { + const p = {}; + if (req.type) p.Type = req.type; + if (req.operateType) p.OperateType = req.operateType; + if (req.info) p.Info = req.info; + return p; +} + +// ---- OperateVuls ---- +function mapOperateVulsReq(req) { + const p = {}; + if (req.uuids?.length) p.Uuids = req.uuids; + if (req.type) p.Type = req.type; + if (req.operateType) p.OperateType = req.operateType; + if (req.vulNames?.length) p.VulNames = req.vulNames; + return p; +} + +// ---- DescribeVulNumStatistics ---- +function mapDescribeVulNumStatisticsReq(req) { + const p = {}; + if (req.type) p.Type = req.type; + if (req.resourceDirectoryAccountId) p.ResourceDirectoryAccountId = req.resourceDirectoryAccountId; + return p; +} + +// ---- DescribeVulFixStatistics ---- +function mapDescribeVulFixStatisticsReq() { + return {}; +} + +// ---- DescribeVulWhitelist ---- +function mapDescribeVulWhitelistReq(req) { + const p = {}; + if (req.currentPage) p.CurrentPage = req.currentPage; + if (req.pageSize) p.PageSize = req.pageSize; + return p; +} + +// ---- ModifyCreateVulWhitelist ---- +function mapModifyCreateVulWhitelistReq(req) { + const p = {}; + if (req.whitelist) p.Whitelist = req.whitelist; + if (req.reason) p.Reason = req.reason; + if (req.targetInfo) p.TargetInfo = req.targetInfo; + return p; +} + +// ---- DeleteVulWhitelist ---- +function mapDeleteVulWhitelistReq(req) { + const p = {}; + if (req.idList?.length) p.IdList = req.idList; + return p; +} + +// ---- DescribeEmgVulItem ---- +function mapDescribeEmgVulItemReq(req) { + const p = {}; + if (req.scanType) p.ScanType = req.scanType; + if (req.lang) p.Lang = req.lang; + if (req.currentPage) p.CurrentPage = req.currentPage; + if (req.pageSize) p.PageSize = req.pageSize; + return p; +} + +// --------------- Handler definitions --------------- + +const handleDescribeVulList = buildCallHandler('DescribeVulList', mapDescribeVulListReq); +const handleDescribeVulDetails = buildCallHandler('DescribeVulDetails', mapDescribeVulDetailsReq); +const handleDescribeUuidsByVulNames = buildCallHandler('DescribeUuidsByVulNames', mapDescribeUuidsByVulNamesReq); +const handleDescribeCanFixVulList = buildCallHandler('DescribeCanFixVulList', mapDescribeCanFixVulListReq); +const handleModifyOperateVul = buildCallHandler('ModifyOperateVul', mapModifyOperateVulReq); +const handleOperateVuls = buildCallHandler('OperateVuls', mapOperateVulsReq); +const handleDescribeVulNumStatistics = buildCallHandler('DescribeVulNumStatistics', mapDescribeVulNumStatisticsReq); +const handleDescribeVulFixStatistics = buildCallHandler('DescribeVulFixStatistics', mapDescribeVulFixStatisticsReq); +const handleDescribeVulWhitelist = buildCallHandler('DescribeVulWhitelist', mapDescribeVulWhitelistReq); +const handleModifyCreateVulWhitelist = buildCallHandler('ModifyCreateVulWhitelist', mapModifyCreateVulWhitelistReq); +const handleDeleteVulWhitelist = buildCallHandler('DeleteVulWhitelist', mapDeleteVulWhitelistReq); +const handleDescribeEmgVulItem = buildCallHandler('DescribeEmgVulItem', mapDescribeEmgVulItemReq); + +// --------------- rpcdef (path-based routing for SDK) --------------- + +export function rpcdef(ctx) { + return { + [METHOD_DESCRIBE_VUL_LIST_PATH]: (req) => handleDescribeVulList({ ...ctx, request: req }), + [METHOD_DESCRIBE_VUL_DETAILS_PATH]: (req) => handleDescribeVulDetails({ ...ctx, request: req }), + [METHOD_DESCRIBE_UUIDS_BY_VUL_NAMES_PATH]: (req) => handleDescribeUuidsByVulNames({ ...ctx, request: req }), + [METHOD_DESCRIBE_CAN_FIX_VUL_LIST_PATH]: (req) => handleDescribeCanFixVulList({ ...ctx, request: req }), + [METHOD_MODIFY_OPERATE_VUL_PATH]: (req) => handleModifyOperateVul({ ...ctx, request: req }), + [METHOD_OPERATE_VULS_PATH]: (req) => handleOperateVuls({ ...ctx, request: req }), + [METHOD_DESCRIBE_VUL_NUM_STATISTICS_PATH]: (req) => handleDescribeVulNumStatistics({ ...ctx, request: req }), + [METHOD_DESCRIBE_VUL_FIX_STATISTICS_PATH]: (req) => handleDescribeVulFixStatistics({ ...ctx, request: req }), + [METHOD_DESCRIBE_VUL_WHITELIST_PATH]: (req) => handleDescribeVulWhitelist({ ...ctx, request: req }), + [METHOD_MODIFY_CREATE_VUL_WHITELIST_PATH]: (req) => handleModifyCreateVulWhitelist({ ...ctx, request: req }), + [METHOD_DELETE_VUL_WHITELIST_PATH]: (req) => handleDeleteVulWhitelist({ ...ctx, request: req }), + [METHOD_DESCRIBE_EMG_VUL_ITEM_PATH]: (req) => handleDescribeEmgVulItem({ ...ctx, request: req }), + }; +} + +// --------------- handlers (for defineService) --------------- + +export const handlers = { + [METHOD_DESCRIBE_VUL_LIST_FULL]: handleDescribeVulList, + [METHOD_DESCRIBE_VUL_DETAILS_FULL]: handleDescribeVulDetails, + [METHOD_DESCRIBE_UUIDS_BY_VUL_NAMES_FULL]: handleDescribeUuidsByVulNames, + [METHOD_DESCRIBE_CAN_FIX_VUL_LIST_FULL]: handleDescribeCanFixVulList, + [METHOD_MODIFY_OPERATE_VUL_FULL]: handleModifyOperateVul, + [METHOD_OPERATE_VULS_FULL]: handleOperateVuls, + [METHOD_DESCRIBE_VUL_NUM_STATISTICS_FULL]: handleDescribeVulNumStatistics, + [METHOD_DESCRIBE_VUL_FIX_STATISTICS_FULL]: handleDescribeVulFixStatistics, + [METHOD_DESCRIBE_VUL_WHITELIST_FULL]: handleDescribeVulWhitelist, + [METHOD_MODIFY_CREATE_VUL_WHITELIST_FULL]: handleModifyCreateVulWhitelist, + [METHOD_DELETE_VUL_WHITELIST_FULL]: handleDeleteVulWhitelist, + [METHOD_DESCRIBE_EMG_VUL_ITEM_FULL]: handleDescribeEmgVulItem, +}; + +// --------------- _test export (for unit testing) --------------- + +export const _test = { + // Method path constants + METHOD_DESCRIBE_VUL_LIST_PATH, + METHOD_DESCRIBE_VUL_DETAILS_PATH, + METHOD_DESCRIBE_UUIDS_BY_VUL_NAMES_PATH, + METHOD_DESCRIBE_CAN_FIX_VUL_LIST_PATH, + METHOD_MODIFY_OPERATE_VUL_PATH, + METHOD_OPERATE_VULS_PATH, + METHOD_DESCRIBE_VUL_NUM_STATISTICS_PATH, + METHOD_DESCRIBE_VUL_FIX_STATISTICS_PATH, + METHOD_DESCRIBE_VUL_WHITELIST_PATH, + METHOD_MODIFY_CREATE_VUL_WHITELIST_PATH, + METHOD_DELETE_VUL_WHITELIST_PATH, + METHOD_DESCRIBE_EMG_VUL_ITEM_PATH, + + METHOD_DESCRIBE_VUL_LIST_FULL, + METHOD_DESCRIBE_VUL_DETAILS_FULL, + METHOD_DESCRIBE_UUIDS_BY_VUL_NAMES_FULL, + METHOD_DESCRIBE_CAN_FIX_VUL_LIST_FULL, + METHOD_MODIFY_OPERATE_VUL_FULL, + METHOD_OPERATE_VULS_FULL, + METHOD_DESCRIBE_VUL_NUM_STATISTICS_FULL, + METHOD_DESCRIBE_VUL_FIX_STATISTICS_FULL, + METHOD_DESCRIBE_VUL_WHITELIST_FULL, + METHOD_MODIFY_CREATE_VUL_WHITELIST_FULL, + METHOD_DELETE_VUL_WHITELIST_FULL, + METHOD_DESCRIBE_EMG_VUL_ITEM_FULL, + + // Utilities + hasOwn, + toTrimmedString, + grpcCodeFor, + errorWithCode, + throwStructuredError, + VALID_REGION_RE, + resolveRegionId, + resolveAccessKeyId, + resolveAccessKeySecret, + resolveTimeoutMs, + aliyunPercentEncode, + randomNonce, + buildSignedParams, + SAS_ENDPOINT_MAP, + buildUrl, + callAliyunAPI, + mapHttpError, + assertAliyunSuccess, + mapAliyunErrorCode, + tryParseJson, + toValue, + unwrapScalar, + + // Mappers + mapDescribeVulListReq, + mapDescribeVulDetailsReq, + mapDescribeUuidsByVulNamesReq, + mapDescribeCanFixVulListReq, + mapModifyOperateVulReq, + mapOperateVulsReq, + mapDescribeVulNumStatisticsReq, + mapDescribeVulFixStatisticsReq, + mapDescribeVulWhitelistReq, + mapModifyCreateVulWhitelistReq, + mapDeleteVulWhitelistReq, + mapDescribeEmgVulItemReq, +}; diff --git a/services/aliyun__sas-vulnerability-management/src/service.js b/services/aliyun__sas-vulnerability-management/src/service.js new file mode 100644 index 00000000..b7537bb0 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/src/service.js @@ -0,0 +1,4 @@ +import { defineService } from '@chaitin-ai/octobus-sdk'; +import { handlers } from './aliyun-sas-vulnerability-management.js'; +export { handlers } from './aliyun-sas-vulnerability-management.js'; +export const service = defineService({ handlers }); diff --git a/services/aliyun__sas-vulnerability-management/test/aliyun-sas-vulnerability-management.test.js b/services/aliyun__sas-vulnerability-management/test/aliyun-sas-vulnerability-management.test.js new file mode 100644 index 00000000..8f12b626 --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/test/aliyun-sas-vulnerability-management.test.js @@ -0,0 +1,336 @@ +import { describe, it, beforeEach, afterEach } from 'node:test'; +import * as assert from 'node:assert/strict'; +import { GrpcError } from '@chaitin-ai/octobus-sdk'; +import { handlers, rpcdef, _test } from '../src/aliyun-sas-vulnerability-management.js'; + +function buildCtx(overrides = {}) { + return { + config: {}, + secret: {}, + bindings: {}, limits: {}, meta: new Map(), req: {}, + getMetadata: () => undefined, getMetadataAll: () => [], + ...overrides, + }; +} + +function expectGrpcError(fn, legacyCode, checker) { + return fn().then( + () => { throw new Error(`Expected GrpcError(${legacyCode}) but no error was thrown`); }, + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.legacyCode, legacyCode); + if (checker) checker(err); + }, + ); +} + +function parseStructuredError(err) { + try { return JSON.parse(err.message); } catch { return null; } +} + +function mockFetch(responseBody, status = 200) { + globalThis.fetch = async () => ({ + ok: status >= 200 && status < 300, + status, + text: async () => typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody), + json: async () => typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody, + }); +} + +// Helper: call handler with SDK ctx format +function call(handler, ctx, req = {}) { + return handler({ ...ctx, request: req }); +} + +describe('aliyun-sas-vulnerability-management', () => { + // ====== Service Structure ====== + it('exports handlers for all 12 RPCs', () => { + const names = Object.keys(handlers); + assert.equal(names.length, 12); + for (const name of names) { + assert.match(name, /^Aliyun_SAS_Vulnerability_Management\.SASService\//); + assert.equal(typeof handlers[name], 'function'); + } + }); + + it('rpcdef returns all 12 paths', () => { + const def = rpcdef(buildCtx()); + const paths = Object.keys(def); + assert.equal(paths.length, 12); + for (const path of paths) assert.match(path, /^\/Aliyun_SAS_Vulnerability_Management\.SASService\//); + }); + + // ====== Auth ====== + it('throws INVALID_ARGUMENT when accessKeyId missing', async () => { + const ctx = buildCtx({ secret: { accessKeySecret: 'test' } }); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx), + 'INVALID_ARGUMENT', + (err) => { const se = parseStructuredError(err); assert.match(se.message, /accessKeyId/); }, + ); + }); + + it('throws INVALID_ARGUMENT when accessKeySecret missing', async () => { + const ctx = buildCtx({ secret: { accessKeyId: 'test' } }); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx), + 'INVALID_ARGUMENT', + (err) => { const se = parseStructuredError(err); assert.match(se.message, /accessKeySecret/); }, + ); + }); + + // ====== Percent Encoding ====== + it('aliyunPercentEncode preserves tilde and encodes asterisk', () => { + const encoded = _test.aliyunPercentEncode('~test*value'); + assert.ok(encoded.includes('~')); + assert.ok(encoded.includes('%2A')); + assert.ok(!encoded.includes('*')); + }); + + // ====== Signature ====== + it('buildSignedParams returns params with all system keys', () => { + const params = _test.buildSignedParams('DescribeVulList', { Type: 'cve' }, 'test-key', 'test-secret'); + assert.equal(params.get('Action'), 'DescribeVulList'); + assert.equal(params.get('SignatureMethod'), 'HMAC-SHA1'); + assert.equal(params.get('Version'), '2018-12-03'); + assert.equal(params.get('Type'), 'cve'); + assert.ok(params.get('Signature')); + }); + + it('buildSignedParams skips undefined/null/empty', () => { + const params = _test.buildSignedParams('Test', { Keep: 'val', Skip: undefined, SkipNull: null, SkipEmpty: '' }, 'k', 's'); + assert.equal(params.get('Keep'), 'val'); + assert.ok(!params.has('Skip')); + assert.ok(!params.has('SkipNull')); + assert.ok(!params.has('SkipEmpty')); + }); + + it('buildSignedParams handles arrays with dot-index', () => { + const params = _test.buildSignedParams('Test', { Uuids: ['uuid-1', 'uuid-2'] }, 'k', 's'); + assert.equal(params.get('Uuids.1'), 'uuid-1'); + assert.equal(params.get('Uuids.2'), 'uuid-2'); + }); + + // ====== buildUrl ====== + it('buildUrl maps China regions to cn-shanghai', () => { + assert.equal(_test.buildUrl('cn-hangzhou'), 'https://tds.cn-shanghai.aliyuncs.com/'); + assert.equal(_test.buildUrl('cn-beijing'), 'https://tds.cn-shanghai.aliyuncs.com/'); + assert.equal(_test.buildUrl('cn-shenzhen'), 'https://tds.cn-shanghai.aliyuncs.com/'); + }); + + it('buildUrl maps non-China regions correctly', () => { + assert.equal(_test.buildUrl('ap-southeast-1'), 'https://tds.ap-southeast-1.aliyuncs.com/'); + assert.equal(_test.buildUrl('us-east-1'), 'https://tds.us-east-1.aliyuncs.com/'); + }); + + it('buildUrl falls back for unknown regions', () => { + assert.equal(_test.buildUrl('unknown'), 'https://tds.unknown.aliyuncs.com/'); + }); + + // ====== Error Mapping ====== + it('mapAliyunErrorCode maps auth errors to UNAUTHENTICATED', () => { + assert.equal(_test.mapAliyunErrorCode('InvalidAccessKeyId.NotFound'), 'UNAUTHENTICATED'); + assert.equal(_test.mapAliyunErrorCode('SignatureDoesNotMatch'), 'UNAUTHENTICATED'); + }); + + it('mapAliyunErrorCode maps invalid params to INVALID_ARGUMENT', () => { + assert.equal(_test.mapAliyunErrorCode('InvalidParameter.Type'), 'INVALID_ARGUMENT'); + assert.equal(_test.mapAliyunErrorCode('MissingParameter.Name'), 'INVALID_ARGUMENT'); + }); + + it('mapAliyunErrorCode maps not found to NOT_FOUND', () => { + assert.equal(_test.mapAliyunErrorCode('Vulnerability.NotFound'), 'NOT_FOUND'); + }); + + it('mapAliyunErrorCode maps throttle to UNAVAILABLE', () => { + assert.equal(_test.mapAliyunErrorCode('Throttling.User'), 'UNAVAILABLE'); + }); + + // ====== JSON Helpers ====== + it('tryParseJson returns ok for valid JSON', () => { + assert.equal(_test.tryParseJson('{"a":1}').ok, true); + }); + + it('tryParseJson returns not ok for invalid JSON', () => { + assert.equal(_test.tryParseJson('not json').ok, false); + }); + + it('toValue converts primitives', () => { + assert.deepEqual(_test.toValue('hello'), { stringValue: 'hello' }); + assert.deepEqual(_test.toValue(42), { numberValue: 42 }); + assert.deepEqual(_test.toValue(null), { nullValue: 0 }); + }); + + // ====== Request Mappers ====== + it('mapDescribeVulListReq maps all fields', () => { + const p = _test.mapDescribeVulListReq({ + type: 'cve', uuids: ['u1'], aliasName: 'CVE-2024', dealed: 'n', + severity: 'high', groupId: 'g1', currentPage: 1, pageSize: 20, + lang: 'zh', attachTypes: 'sca', ids: '1,2', + createTsStart: 1000, createTsEnd: 2000, + }); + assert.equal(p.Type, 'cve'); + assert.deepEqual(p.Uuids, ['u1']); + assert.equal(p.CurrentPage, 1); + assert.equal(p.PageSize, 20); + assert.equal(p.CreateTsStart, 1000); + }); + + it('mapDescribeVulDetailsReq maps fields', () => { + const p = _test.mapDescribeVulDetailsReq({ type: 'cve', name: 'CVE-2024', aliasName: 'alias', lang: 'en' }); + assert.equal(p.Name, 'CVE-2024'); + assert.equal(p.Lang, 'en'); + }); + + it('mapDescribeUuidsByVulNamesReq maps VulNames', () => { + const p = _test.mapDescribeUuidsByVulNamesReq({ type: 'cve', vulNames: ['n1', 'n2'], lang: 'zh' }); + assert.deepEqual(p.VulNames, ['n1', 'n2']); + }); + + it('mapModifyOperateVulReq maps fields', () => { + const p = _test.mapModifyOperateVulReq({ type: 'cve', operateType: 'vul_fix', info: '{"id":1}' }); + assert.equal(p.OperateType, 'vul_fix'); + }); + + it('mapOperateVulsReq maps fields', () => { + const p = _test.mapOperateVulsReq({ uuids: ['u1'], type: 'cve', operateType: 'vul_fix', vulNames: ['CVE-2024'] }); + assert.deepEqual(p.VulNames, ['CVE-2024']); + }); + + it('mapDescribeVulWhitelistReq maps fields', () => { + const p = _test.mapDescribeVulWhitelistReq({ currentPage: 1, pageSize: 10 }); + assert.equal(p.CurrentPage, 1); + }); + + it('mapDeleteVulWhitelistReq maps idList', () => { + const p = _test.mapDeleteVulWhitelistReq({ idList: ['1', '2'] }); + assert.deepEqual(p.IdList, ['1', '2']); + }); + + // ====== Handler with mocked fetch ====== + describe('handlers with mocked fetch', () => { + let orig; + beforeEach(() => { orig = globalThis.fetch; }); + afterEach(() => { globalThis.fetch = orig; }); + const ctx = buildCtx({ secret: { accessKeyId: 'test-key', accessKeySecret: 'test-secret' } }); + + it('DescribeVulList returns success', async () => { + mockFetch({ TotalCount: 3, VulRecords: [{ Id: 1001 }], RequestId: 'req-001' }); + const result = await call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx, { type: 'cve', currentPage: 1, pageSize: 10 }); + assert.equal(result.httpStatus, 200); + assert.ok(result.rawJson.structValue); + }); + + it('DescribeVulDetails returns success', async () => { + mockFetch({ VulDetails: [{ Name: 'CVE-2024' }], RequestId: 'req-002' }); + const result = await call(handlers[_test.METHOD_DESCRIBE_VUL_DETAILS_FULL], ctx, { type: 'cve', name: 'CVE-2024' }); + assert.equal(result.httpStatus, 200); + }); + + it('DescribeUuidsByVulNames returns success', async () => { + mockFetch({ MachineInfoStatistics: [{ UuidCount: 5 }], RequestId: 'req-003' }); + const result = await call(handlers[_test.METHOD_DESCRIBE_UUIDS_BY_VUL_NAMES_FULL], ctx, { type: 'cve', vulNames: ['CVE-2024'] }); + assert.equal(result.httpStatus, 200); + }); + + it('ModifyOperateVul returns success', async () => { + mockFetch({ RequestId: 'req-005' }); + const result = await call(handlers[_test.METHOD_MODIFY_OPERATE_VUL_FULL], ctx, { type: 'cve', operateType: 'vul_fix', info: '{"id":1}' }); + assert.equal(result.httpStatus, 200); + }); + + it('DescribeVulNumStatistics returns success', async () => { + mockFetch({ VulAsapSum: 3, VulLaterSum: 10, RequestId: 'req-007' }); + const result = await call(handlers[_test.METHOD_DESCRIBE_VUL_NUM_STATISTICS_FULL], ctx); + assert.equal(result.httpStatus, 200); + }); + + it('DescribeVulWhitelist returns success', async () => { + mockFetch({ TotalCount: 1, VulWhitelists: [], RequestId: 'req-009' }); + const result = await call(handlers[_test.METHOD_DESCRIBE_VUL_WHITELIST_FULL], ctx, { currentPage: 1, pageSize: 10 }); + assert.equal(result.httpStatus, 200); + }); + + it('maps HTTP 401 to PERMISSION_DENIED', async () => { + mockFetch({ Code: 'InvalidAccessKeyId', Message: 'Invalid' }, 401); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx), + 'PERMISSION_DENIED', + ); + }); + + it('maps HTTP 500 to UNAVAILABLE', async () => { + globalThis.fetch = async () => ({ ok: false, status: 500, text: async () => 'Error' }); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx), + 'UNAVAILABLE', + ); + }); + + it('maps Throttling.User to UNAVAILABLE', async () => { + mockFetch({ Code: 'Throttling.User', Message: 'Throttled' }); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx), + 'UNAVAILABLE', + ); + }); + + it('maps MissingParameter to INVALID_ARGUMENT', async () => { + mockFetch({ Code: 'MissingParameter.Name', Message: 'Required' }); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_DETAILS_FULL], ctx), + 'INVALID_ARGUMENT', + ); + }); + + it('maps Vulnerability.NotFound to NOT_FOUND', async () => { + mockFetch({ Code: 'Vulnerability.NotFound', Message: 'Not found' }); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_DETAILS_FULL], ctx), + 'NOT_FOUND', + ); + }); + + it('maps non-JSON response to UNAVAILABLE', async () => { + globalThis.fetch = async () => ({ ok: true, status: 200, text: async () => 'not json' }); + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx), + 'UNAVAILABLE', + ); + }); + + it('maps network error to UNAVAILABLE', async () => { + globalThis.fetch = async () => { throw new Error('ECONNREFUSED'); }; + await expectGrpcError( + () => call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx), + 'UNAVAILABLE', + ); + }); + it('assertAliyunSuccess handles numeric code 200', async () => { + mockFetch({ Code: 200, Message: 'OK' }); + const result = await call(handlers[_test.METHOD_DESCRIBE_VUL_LIST_FULL], ctx); + assert.equal(result.httpStatus, 200); + }); + + it('rejects invalid regionId with SSRF-like patterns', () => { + assert.throws(() => _test.resolveRegionId(buildCtx({ config: { regionId: 'evil.com@127.0.0.1' } })), /Invalid/); + assert.throws(() => _test.resolveRegionId(buildCtx({ config: { regionId: 'cn-shanghai/../../' } })), /Invalid/); + assert.throws(() => _test.buildUrl('evil.com@127.0.0.1'), /Invalid/); + }); + + it('accepts valid regionId formats', () => { + assert.equal(_test.resolveRegionId(buildCtx({ config: { regionId: 'cn-shanghai' } })), 'cn-shanghai'); + assert.equal(_test.resolveRegionId(buildCtx({ config: { regionId: 'us-east-1' } })), 'us-east-1'); + assert.equal(_test.resolveRegionId(buildCtx({ config: { regionId: 'ap-southeast-1' } })), 'ap-southeast-1'); + }); + }); + + // ====== Config ====== + it('resolveRegionId uses default', () => { + assert.equal(_test.resolveRegionId(buildCtx({ config: {} })), 'cn-shanghai'); + }); + + it('resolveTimeoutMs uses default', () => { + assert.equal(_test.resolveTimeoutMs(buildCtx({ config: {} })), 10000); + }); +}); diff --git a/services/aliyun__sas-vulnerability-management/test/mock_upstream.js b/services/aliyun__sas-vulnerability-management/test/mock_upstream.js new file mode 100644 index 00000000..b6d816cf --- /dev/null +++ b/services/aliyun__sas-vulnerability-management/test/mock_upstream.js @@ -0,0 +1,261 @@ +import http from 'node:http'; + +/** + * Creates a mock SAS (Security Center) API endpoint on a random port. + * + * Trigger actions (via Action param): + * - "FailAuth" → 401 with InvalidAccessKeyId error + * - "FailForbidden" → 403 with Forbidden error + * - "FailServer" → 500 Internal Server Error + * - "FailInvalidJson" → 200 with non-JSON body "not-json" + * - "FailBizError" → 200 with Code: "MissingParameter.NotFound" + * - "FailThrottling" → 200 with Code: "Throttling.User" + * - "FailNotFound" → 200 with Code: "Vulnerability.NotFound" + */ +export function createMockServer() { + const requests = []; + + const server = http.createServer((req, res) => { + if (req.method !== 'POST') { + res.writeHead(405, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ Code: 'MethodNotAllowed', Message: 'Only POST allowed' })); + return; + } + + let body = ''; + req.on('data', (chunk) => { body += chunk.toString(); }); + req.on('end', () => { + const params = new URLSearchParams(body); + const action = params.get('Action') || ''; + const accessKeyId = params.get('AccessKeyId') || ''; + const signature = params.get('Signature') || ''; + + requests.push({ action, params: Object.fromEntries(params) }); + + // Missing auth + if (!accessKeyId || !signature) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + Code: 'InvalidAccessKeyId.NotFound', + Message: 'Specified access key is not found.', + RequestId: 'MOCK-REQ-001', + })); + return; + } + + // Trigger: FailAuth action + if (action === 'FailAuth') { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + Code: 'InvalidAccessKeyId.NotFound', + Message: 'Specified access key is not found.', + RequestId: 'MOCK-REQ-002', + })); + return; + } + + // Trigger: FailForbidden + if (action === 'FailForbidden') { + res.writeHead(403, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + Code: 'Forbidden.NotAdminUser', + Message: 'The specified account is not an admin.', + RequestId: 'MOCK-REQ-003', + })); + return; + } + + // Trigger: FailServer + if (action === 'FailServer') { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + return; + } + + // Trigger: FailInvalidJson + if (action === 'FailInvalidJson') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('not-json'); + return; + } + + // Trigger: FailThrottling + if (action === 'FailThrottling') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + Code: 'Throttling.User', + Message: 'Request was throttled.', + RequestId: 'MOCK-REQ-004', + })); + return; + } + + // Trigger: FailBizError + if (action === 'FailBizError') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + Code: 'MissingParameter.NotFound', + Message: 'The specified parameter is missing.', + RequestId: 'MOCK-REQ-005', + })); + return; + } + + // Trigger: FailNotFound + if (action === 'FailNotFound') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + Code: 'Vulnerability.NotFound', + Message: 'The specified vulnerability was not found.', + RequestId: 'MOCK-REQ-006', + })); + return; + } + + // Success responses per action type + if (action === 'DescribeVulList') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-VUL-LIST', + TotalCount: 3, + VulRecords: [ + { + Id: 1001, + Name: 'CVE-2024-1234', + AliasName: 'RHSA-2024:1234', + Type: 'cve', + Severity: 'high', + Status: 3, + Necessity: 'Necessary to fix', + }, + ], + })); + return; + } + + if (action === 'DescribeVulDetails') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-VUL-DETAILS', + VulDetails: [{ + Name: 'CVE-2024-1234', + Description: 'A critical vulnerability in example package', + Solution: 'Upgrade to version 2.0', + }], + })); + return; + } + + if (action === 'DescribeUuidsByVulNames') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-UUIDS-BY-VUL', + MachineInfoStatistics: [{ + UuidCount: 5, + UuidList: ['uuid-1', 'uuid-2', 'uuid-3'], + }], + })); + return; + } + + if (action === 'DescribeCanFixVulList') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-CAN-FIX', + TotalCount: 2, + VulRecords: [{ + Id: 2001, + Name: 'CVE-2024-5678', + Status: 1, + }], + })); + return; + } + + if (action === 'ModifyOperateVul') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ RequestId: 'MOCK-MODIFY-OPERATE' })); + return; + } + + if (action === 'OperateVuls') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ RequestId: 'MOCK-OPERATE' })); + return; + } + + if (action === 'DescribeVulNumStatistics') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-VUL-NUM', + VulLaterSum: 10, + VulNntfSum: 5, + VulAsapSum: 3, + VulTotalCount: 18, + })); + return; + } + + if (action === 'DescribeVulFixStatistics') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-VUL-FIX', + FixedTodayNum: 2, + FixedTotalNum: 100, + NeedFixNum: 18, + })); + return; + } + + if (action === 'DescribeVulWhitelist') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-VUL-WHITELIST', + TotalCount: 1, + VulWhitelists: [{ Id: 'wl-001', Whitelist: 'CVE-2023-0001', Reason: 'Low risk, accepted' }], + })); + return; + } + + if (action === 'ModifyCreateVulWhitelist') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ RequestId: 'MOCK-CREATE-WHITELIST' })); + return; + } + + if (action === 'DeleteVulWhitelist') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ RequestId: 'MOCK-DELETE-WHITELIST' })); + return; + } + + if (action === 'DescribeEmgVulItem') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + RequestId: 'MOCK-EMG-VUL', + TotalCount: 1, + GroupedVulItems: [{ Name: 'emergency-CVE-2024-9999', Description: 'Critical 0-day' }], + })); + return; + } + + // Default success for any unrecognized action + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ RequestId: 'MOCK-DEFAULT', Code: '200' })); + }); + }); + + return { + server, + requests, + get url() { + const addr = server.address(); + return `http://127.0.0.1:${addr.port}`; + }, + close() { + return new Promise((resolve, reject) => { + server.close((err) => (err ? reject(err) : resolve())); + }); + }, + }; +} diff --git a/services/bin/aliyun-sas-vulnerability-management.js b/services/bin/aliyun-sas-vulnerability-management.js new file mode 100644 index 00000000..ca13c0d8 --- /dev/null +++ b/services/bin/aliyun-sas-vulnerability-management.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; +import { service } from "../aliyun__sas-vulnerability-management/src/service.js"; +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../aliyun__sas-vulnerability-management/bin/aliyun-sas-vulnerability-management.js", import.meta.url)), +}); diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 4124b804..0245de98 100755 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -20,6 +20,9 @@ const services = { "cloudatlas": { entryFile: "../chaitin__cloudatlas/bin/cloudatlas.js", serviceModule: "../chaitin__cloudatlas/src/service.js", + "aliyun-sas-vulnerability-management": { + entryFile: "../aliyun__sas-vulnerability-management/bin/aliyun-sas-vulnerability-management.js", + serviceModule: "../aliyun__sas-vulnerability-management/src/service.js", }, "das-gateway-v3": { entryFile: "../das__gateway_v3/bin/das-gateway-v3.js", diff --git a/services/package.json b/services/package.json index 2fbea088..c90ddd04 100644 --- a/services/package.json +++ b/services/package.json @@ -41,6 +41,7 @@ "riversafe-waf": "bin/riversafe-waf.js", "safeline-waf": "bin/safeline-waf.js", "safeline-waf-eliminate-false-positive": "bin/safeline-waf-eliminate-false-positive.js", + "aliyun-sas-vulnerability-management": "bin/aliyun-sas-vulnerability-management.js", "sangfor-fw-v8-0-45": "bin/sangfor-fw-v8-0-45.js", "sangfor-xdr-v2-0-45": "bin/sangfor-xdr-v2-0-45.js", "skycloud-inet": "bin/skycloud-inet.js", @@ -106,6 +107,7 @@ "bin/skycloud-inet.js", "bin/slack-group-robot.js", "bin/safeline-waf-eliminate-false-positive.js", + "bin/aliyun-sas-vulnerability-management.js", "bin/safeline-waf.js", "bin/sangfor-fw-v8-0-45.js", "bin/sangfor-xdr-v2-0-45.js", @@ -169,6 +171,7 @@ "slack__group-robot", "chaitin__safeline-waf-eliminate-false-positive", "chaitin__safeline-waf", + "aliyun__sas-vulnerability-management", "sangfor__fw_v8-0-45", "sangfor__xdr_v2-0-45", "tencent__qyweixin-group-robot",