From 962e5581f16c10d9bb4c0beb0795cc65503b6209 Mon Sep 17 00:00:00 2001 From: hilariacrucita <3362488788@qq.com> Date: Thu, 9 Jul 2026 15:56:50 +0800 Subject: [PATCH 1/5] feat: add nsfocus-rsas-v6-0r04f04sp09 service package Add OctoBus adapter for NSFOCUS RSAS (Remote Security Assessment System) V6.0R04F04SP09, covering the scan task lifecycle, templates and password dictionaries, system status and audit logs, report generation and download, and agent management (42 RPCs). --- services/bin/nsfocus-rsas-v6-0r04f04sp09.js | 10 + services/bin/octobus-tentacles.js | 4 + .../nsfocus__rsas_v6-0r04f04sp09/README.md | 99 +++ .../bin/nsfocus-rsas-v6-0r04f04sp09.js | 6 + .../config.schema.json | 78 ++ .../nsfocus__rsas_v6-0r04f04sp09/package.json | 13 + .../proto/nsfocus_rsas_v6_0r04f04sp09.proto | 297 +++++++ .../secret.schema.json | 19 + .../nsfocus__rsas_v6-0r04f04sp09/service.json | 193 +++++ .../src/nsfocus-rsas-v6-0r04f04sp09.js | 768 ++++++++++++++++++ .../src/service.js | 7 + .../test/mock_upstream.js | Bin 0 -> 5309 bytes .../test/nsfocus-rsas-v6-0r04f04sp09.test.js | 223 +++++ services/package.json | 3 + 14 files changed, 1720 insertions(+) create mode 100755 services/bin/nsfocus-rsas-v6-0r04f04sp09.js create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/README.md create mode 100755 services/nsfocus__rsas_v6-0r04f04sp09/bin/nsfocus-rsas-v6-0r04f04sp09.js create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/config.schema.json create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/package.json create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/proto/nsfocus_rsas_v6_0r04f04sp09.proto create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/secret.schema.json create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/service.json create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/src/service.js create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/test/mock_upstream.js create mode 100644 services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js diff --git a/services/bin/nsfocus-rsas-v6-0r04f04sp09.js b/services/bin/nsfocus-rsas-v6-0r04f04sp09.js new file mode 100755 index 00000000..890dfaaf --- /dev/null +++ b/services/bin/nsfocus-rsas-v6-0r04f04sp09.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../nsfocus__rsas_v6-0r04f04sp09/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../nsfocus__rsas_v6-0r04f04sp09/bin/nsfocus-rsas-v6-0r04f04sp09.js", import.meta.url)), +}); diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 412b8988..fb142ba2 100644 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -129,6 +129,10 @@ const services = { entryFile: "../nsfocus__nips_v5-6-r11/bin/nsfocus-nips-v5-6-r11.js", serviceModule: "../nsfocus__nips_v5-6-r11/src/service.js", }, + "nsfocus-rsas-v6-0r04f04sp09": { + entryFile: "../nsfocus__rsas_v6-0r04f04sp09/bin/nsfocus-rsas-v6-0r04f04sp09.js", + serviceModule: "../nsfocus__rsas_v6-0r04f04sp09/src/service.js", + }, "owasp-dependency-track-sca-v5-0": { entryFile: "../owasp__dependency-track-sca_v5-0/bin/owasp-dependency-track-sca-v5-0.js", serviceModule: "../owasp__dependency-track-sca_v5-0/src/service.js", diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/README.md b/services/nsfocus__rsas_v6-0r04f04sp09/README.md new file mode 100644 index 00000000..1704b45e --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/README.md @@ -0,0 +1,99 @@ +# NSFOCUS RSAS V6.0R04F04SP09 + +OctoBus package for the NSFOCUS RSAS (Remote Security Assessment System, +绿盟网络安全漏洞扫描系统) V6.0R04F04SP09 REST API. + +## Authentication + +RSAS authenticates with a username/password pair passed as URL query +parameters (`?username=...&password=...`). Provide them through the secret +binding (`user`/`username` + `password`); every request also sends +`format=json` and `curr_lang` (default `cn`). + +## Configuration + +| Binding | Location | Description | +| --- | --- | --- | +| `host` | config | Base URL with scheme, e.g. `https://10.65.193.127` | +| `user` / `username` | secret (or config) | Login username | +| `password` | secret | Login password | +| `currLang` | config | Response language `cn` (default) or `en` | +| `timeoutMs` | config | HTTP timeout in ms (default 30000) | +| `skipTlsVerify` | config | Skip TLS verification for self-signed device certs | +| `headers` | config | Extra HTTP headers | + +## RPCs + +All responses carry the RSAS envelope fields `ret_code`, `ret_msg`, `data` +(as a `google.protobuf.Value`) and the transport `http_status`. Task-creation +RPCs additionally surface `task_id`. Download RPCs return `BinaryResponse` +with base64 body plus content metadata. + +### Task lifecycle +- `CreateTask` — `POST /api/task/create` (config.xml payload) +- `CreateVulnTask` — `POST /api/task/vul/create` +- `CreateBaselineTask` — `POST /api/task/baseline/create` +- `CreatePwdTask` — `POST /api/task/pwd/create` +- `CreateWebTask` — `POST /api/task/web/create` +- `CreateOfflineTask` — `POST /api/task/offline/create` +- `CreateDockerTask` — `POST /api/task/docker/create` +- `CreateCodeauditTask` — `POST /api/task/codeaudit/create` +- `CreateHostAssetsTask` — `POST /api/task/hostassets/create` +- `CreateWebAssetsTask` — `POST /api/task/webassets/create` +- `GetTaskStatus` — `GET /api/task/status/{task_id}` +- `PauseTask` / `ResumeTask` / `StopTask` / `DeleteTask` — `POST /api/task/{action}/{task_id}` +- `BatchDeleteTasks` — `POST /api/task/batch_delete` +- `ListTasks` — `GET /api/task/list` +- `ListActiveTasks` — `GET /api/task/active_list` +- `GetTaskResult` — `GET /api/report/task/{task_id}` +- `CreateAuthInfo` — `POST /api/authinfo/create` +- `LoginVerify` — `POST /api/auth/login_verify` + +### Templates and dictionaries +- `ListSysvulnTemplate` — `GET /api/template/sysvuln/list` +- `ListWebvulnTemplate` — `GET /api/template/webvuln/list` +- `ListBaselineTemplate` — `GET /api/template/baseline/list` +- `GetBaselineParams` — `GET /api/template/baseline/params` +- `ListCodeauditTemplate` — `GET /api/template/codeaudit/list` +- `ListAssetTemplate` — `GET /api/template/asset/list` +- `CreateBaselineTemplate` — `POST /api/template/baseline/create` +- `ListUserpwd` — `GET /api/userpwd/list` +- `CreateUserpwd` — `POST /api/userpwd/create` + +### System and audit +- `GetSystemStatus` — `GET /api/system/status` +- `GetLogInfo` — `GET /api/log/getlogInfo` + +### Reports +- `ListReportTemplate` — `GET /api/report/template/list` +- `GenerateReport` — `POST /api/generate_report/` +- `GetReportProgress` — `GET /api/get_report_progress/report_id/{report_id}` +- `DownloadReport` — `GET /api/download_report/report_id/{report_id}/report_type/{report_type}` +- `DeleteReports` — `POST /api/delete_reports` + +### Agent +- `GetAgentMethodConfig` / `SetAgentMethodConfig` — `/api/agent/agent_method_config` +- `GetAgentAuth` — `GET /api/agent/get_agent_auth` +- `GetAgentPackageUrl` — `GET /api/agent/get_agentpackage_url` +- `DownloadAgent` — `GET /api/agent/download/{linux|windows}` + +## Error mapping + +| Condition | gRPC status | +| --- | --- | +| Missing required argument | `INVALID_ARGUMENT` | +| HTTP 401 / 403 | `PERMISSION_DENIED` | +| HTTP 404 / 405 | `INVALID_ARGUMENT` | +| HTTP 429 (rate limit) | `RESOURCE_EXHAUSTED` | +| HTTP 5xx / network failure | `UNAVAILABLE` | +| Non-JSON or empty body | `UNKNOWN` | + +RSAS applies its own rate limits (600 req/min globally; 300 req/min for task +status and report progress; 150 req/min for other interfaces), returning HTTP +429 when exceeded. + +## Testing + +``` +npm test -- --service-dir nsfocus__rsas_v6-0r04f04sp09 +``` diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/bin/nsfocus-rsas-v6-0r04f04sp09.js b/services/nsfocus__rsas_v6-0r04f04sp09/bin/nsfocus-rsas-v6-0r04f04sp09.js new file mode 100755 index 00000000..8e0066ef --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/bin/nsfocus-rsas-v6-0r04f04sp09.js @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { runServiceMain } from '@chaitin-ai/octobus-sdk'; + +import { service } from '../src/service.js'; + +runServiceMain(service); diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/config.schema.json b/services/nsfocus__rsas_v6-0r04f04sp09/config.schema.json new file mode 100644 index 00000000..88a333bc --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/config.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "host": { + "type": "string", + "description": "NSFOCUS RSAS base URL with http(s) scheme, e.g. https://10.65.193.127." + }, + "restBaseUrl": { + "type": "string", + "description": "Alias for host." + }, + "baseUrl": { + "type": "string", + "description": "Alias for host." + }, + "rest_base_url": { + "type": "string", + "description": "Alias for host." + }, + "base_url": { + "type": "string", + "description": "Alias for host." + }, + "user": { + "type": "string", + "description": "Login username. Prefer secret.user for persistent credentials." + }, + "username": { + "type": "string", + "description": "Alias for user." + }, + "currLang": { + "type": "string", + "enum": ["cn", "en"], + "default": "cn", + "description": "Response language (RSAS curr_lang parameter)." + }, + "curr_lang": { + "type": "string", + "description": "Alias for currLang." + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 30000, + "description": "HTTP timeout in milliseconds. RSAS scan-result and report calls can be slow." + }, + "timeout_ms": { + "type": "integer", + "minimum": 1, + "description": "Alias for timeoutMs." + }, + "skipTlsVerify": { + "type": "boolean", + "default": false, + "description": "Skip TLS certificate verification for private deployments with self-signed certs." + }, + "tlsInsecureSkipVerify": { + "type": "boolean", + "default": false, + "description": "Legacy alias for skipTlsVerify." + }, + "insecureSkipVerify": { + "type": "boolean", + "default": false, + "description": "Alias for skipTlsVerify." + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Optional additional HTTP headers." + } + } +} diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/package.json b/services/nsfocus__rsas_v6-0r04f04sp09/package.json new file mode 100644 index 00000000..509aaa20 --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/package.json @@ -0,0 +1,13 @@ +{ + "name": "nsfocus-rsas-v6-0r04f04sp09", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "nsfocus-rsas-v6-0r04f04sp09": "bin/nsfocus-rsas-v6-0r04f04sp09.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.6.0", + "undici": "^7.16.0" + } +} diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/proto/nsfocus_rsas_v6_0r04f04sp09.proto b/services/nsfocus__rsas_v6-0r04f04sp09/proto/nsfocus_rsas_v6_0r04f04sp09.proto new file mode 100644 index 00000000..53788eb0 --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/proto/nsfocus_rsas_v6_0r04f04sp09.proto @@ -0,0 +1,297 @@ +syntax = "proto3"; + +package Nsfocus_RSAS_V60R04F04SP09; + +import "google/protobuf/struct.proto"; + +option go_package = "miner/grpc-service/Nsfocus_RSAS_V60R04F04SP09"; + +// NSFOCUS RSAS (Remote Security Assessment System) V6.0R04F04SP09 REST API. +// Credentials (username/password) are provided through config/secret bindings +// and appended to every request as query parameters, matching the device's +// URL-parameter authentication scheme. +service Nsfocus_RSAS_V60R04F04SP09 { + // Task lifecycle + rpc CreateTask(CreateTaskRequest) returns (TaskCreateResponse) {} + rpc CreateVulnTask(CreateVulnTaskRequest) returns (TaskCreateResponse) {} + rpc CreateBaselineTask(CreateBaselineTaskRequest) returns (TaskCreateResponse) {} + rpc CreatePwdTask(CreatePwdTaskRequest) returns (TaskCreateResponse) {} + rpc CreateWebTask(CreateWebTaskRequest) returns (TaskCreateResponse) {} + rpc CreateOfflineTask(CreateOfflineTaskRequest) returns (TaskCreateResponse) {} + rpc CreateDockerTask(CreateDockerTaskRequest) returns (TaskCreateResponse) {} + rpc CreateCodeauditTask(CreateCodeauditTaskRequest) returns (TaskCreateResponse) {} + rpc CreateHostAssetsTask(CreateHostAssetsTaskRequest) returns (TaskCreateResponse) {} + rpc CreateWebAssetsTask(CreateWebAssetsTaskRequest) returns (TaskCreateResponse) {} + rpc GetTaskStatus(TaskIdRequest) returns (RsasResponse) {} + rpc PauseTask(TaskIdRequest) returns (RsasResponse) {} + rpc ResumeTask(TaskIdRequest) returns (RsasResponse) {} + rpc StopTask(TaskIdRequest) returns (RsasResponse) {} + rpc DeleteTask(TaskIdRequest) returns (RsasResponse) {} + rpc BatchDeleteTasks(BatchDeleteTasksRequest) returns (RsasResponse) {} + rpc ListTasks(ListTasksRequest) returns (RsasResponse) {} + rpc ListActiveTasks(EmptyRequest) returns (RsasResponse) {} + rpc GetTaskResult(GetTaskResultRequest) returns (RsasResponse) {} + rpc CreateAuthInfo(CreateAuthInfoRequest) returns (RsasResponse) {} + rpc LoginVerify(LoginVerifyRequest) returns (RsasResponse) {} + + // Templates and dictionaries + rpc ListSysvulnTemplate(EmptyRequest) returns (RsasResponse) {} + rpc ListWebvulnTemplate(EmptyRequest) returns (RsasResponse) {} + rpc ListBaselineTemplate(ListBaselineTemplateRequest) returns (RsasResponse) {} + rpc GetBaselineParams(GetBaselineParamsRequest) returns (RsasResponse) {} + rpc ListCodeauditTemplate(ListCodeauditTemplateRequest) returns (RsasResponse) {} + rpc ListAssetTemplate(EmptyRequest) returns (RsasResponse) {} + rpc CreateBaselineTemplate(CreateBaselineTemplateRequest) returns (RsasResponse) {} + rpc ListUserpwd(EmptyRequest) returns (RsasResponse) {} + rpc CreateUserpwd(CreateUserpwdRequest) returns (RsasResponse) {} + + // System and audit + rpc GetSystemStatus(EmptyRequest) returns (RsasResponse) {} + rpc GetLogInfo(GetLogInfoRequest) returns (RsasResponse) {} + + // Reports + rpc ListReportTemplate(EmptyRequest) returns (RsasResponse) {} + rpc GenerateReport(GenerateReportRequest) returns (RsasResponse) {} + rpc GetReportProgress(GetReportProgressRequest) returns (RsasResponse) {} + rpc DownloadReport(DownloadReportRequest) returns (BinaryResponse) {} + rpc DeleteReports(DeleteReportsRequest) returns (RsasResponse) {} + + // Agent + rpc GetAgentMethodConfig(EmptyRequest) returns (RsasResponse) {} + rpc SetAgentMethodConfig(SetAgentMethodConfigRequest) returns (RsasResponse) {} + rpc GetAgentAuth(EmptyRequest) returns (RsasResponse) {} + rpc GetAgentPackageUrl(EmptyRequest) returns (RsasResponse) {} + rpc DownloadAgent(DownloadAgentRequest) returns (BinaryResponse) {} +} + +message EmptyRequest { +} + +// Standard RSAS JSON envelope: {"ret_code":0,"ret_msg":"success","data":{...}}. +message RsasResponse { + int64 ret_code = 1; + string ret_msg = 2; + google.protobuf.Value data = 3; + int32 http_status = 4; +} + +// Task creation responses additionally surface the created task_id for convenience. +message TaskCreateResponse { + int64 ret_code = 1; + string ret_msg = 2; + google.protobuf.Value data = 3; + int32 http_status = 4; + string task_id = 5; +} + +// Binary download responses (report zip, agent installer). +message BinaryResponse { + int32 http_status = 1; + string content_type = 2; + int64 content_length = 3; + string filename = 4; + string body_base64 = 5; +} + +message TaskIdRequest { + string task_id = 1; +} + +message CreateTaskRequest { + // Task type validation hint: 1 vuln, 2 baseline, 3 vuln+baseline, 4 pwd, 8 web. + int64 type = 1; + // config.xml content (the file body); sent as a multipart file field. + string config_xml = 2; + // Optional file name for the multipart part (default config.xml). + string config_xml_filename = 3; +} + +message CreateVulnTaskRequest { + string name = 1; + string targets = 2; + int64 template_id = 3; +} + +message CreateBaselineTaskRequest { + string name = 1; + string targets = 2; + string login_type = 3; + string login_port = 4; + string login_name = 5; + string login_password = 6; + string template_uuid = 7; + string template_param = 8; + string protect_level = 9; +} + +message CreatePwdTaskRequest { + string name = 1; + string targets = 2; + string template_id = 3; + string service_type = 4; + string pass_mode = 5; + int64 port = 6; +} + +message CreateWebTaskRequest { + string name = 1; + string targets = 2; + string template_id = 3; + string component_scan = 4; +} + +message CreateOfflineTaskRequest { + // Offline scan result file content, base64-encoded. + string task_field_base64 = 1; + string filename = 2; +} + +message CreateDockerTaskRequest { + string name = 1; + string targets = 2; + string registry_address = 3; + string registry_username = 4; + string registry_password = 5; +} + +message CreateCodeauditTaskRequest { + string name = 1; + int64 code_source = 2; + string repo_path = 3; + string repo_username = 4; + string repo_password = 5; + int64 template_id = 6; + string exclude_files = 7; + string exclude_dirs = 8; + string git_branch = 9; +} + +message CreateHostAssetsTaskRequest { + string name = 1; + string targets = 2; + string is_related = 3; + string template_id = 4; + string prohibit_config = 5; + string prohibit_ports = 6; +} + +message CreateWebAssetsTaskRequest { + string name = 1; + string targets = 2; + string is_related = 3; + string is_sitetree = 4; +} + +message BatchDeleteTasksRequest { + repeated int64 task_ids = 1; +} + +message ListTasksRequest { + int64 type = 1; + string end_time_start = 2; + string end_time_end = 3; + string isfinished = 4; + int64 page = 5; + int64 page_size = 6; +} + +message GetTaskResultRequest { + string task_id = 1; + string targets = 2; + int64 page = 3; + int64 page_size = 4; +} + +message AuthInfoEntry { + string accountname = 1; + string accountpwd = 2; + string port = 3; + string protocol = 4; + string ip = 5; +} + +message CreateAuthInfoRequest { + repeated AuthInfoEntry authinfo = 1; +} + +message JumpHost { + string ip_range = 1; + string protocol = 2; + string user_name = 3; + string user_pwd = 4; + int64 port = 5; +} + +message LoginVerifyRequest { + string ip = 1; + string protocol = 2; + int64 port = 3; + string user_name = 4; + string userpwd = 5; + bool jump_ifuse = 6; + repeated JumpHost jumparray = 7; + string web_login_url = 8; + string web_login_cookie = 9; +} + +message ListBaselineTemplateRequest { + string industry = 1; +} + +message GetBaselineParamsRequest { + string uuid = 1; +} + +message ListCodeauditTemplateRequest { + int64 type = 1; +} + +message CreateBaselineTemplateRequest { + // Exported template file content, base64-encoded. + string template_base64 = 1; + string filename = 2; +} + +message CreateUserpwdRequest { + // JSON array of {type,title,data} where data is base64 line content. + string userpwd = 1; +} + +message GetLogInfoRequest { + string account = 1; + string type = 2; + string starttime = 3; + string endtime = 4; + string ip = 5; +} + +message GenerateReportRequest { + string task_id = 1; + string summary_template_id = 2; + string host_template_id = 3; + string report_type = 4; +} + +message GetReportProgressRequest { + string report_id = 1; +} + +message DownloadReportRequest { + string report_id = 1; + string report_type = 2; +} + +message DeleteReportsRequest { + // Report ids: repeated integers, or the literal "all". + repeated int64 report_ids = 1; + bool all = 2; +} + +message SetAgentMethodConfigRequest { + int64 agent_method = 1; +} + +message DownloadAgentRequest { + // Target platform: "linux" or "windows". + string platform = 1; +} diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/secret.schema.json b/services/nsfocus__rsas_v6-0r04f04sp09/secret.schema.json new file mode 100644 index 00000000..57646eda --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/secret.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "user": { + "type": "string", + "description": "Login username." + }, + "username": { + "type": "string", + "description": "Alias for user." + }, + "password": { + "type": "string", + "description": "Login password." + } + } +} diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/service.json b/services/nsfocus__rsas_v6-0r04f04sp09/service.json new file mode 100644 index 00000000..ee27c279 --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/service.json @@ -0,0 +1,193 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "nsfocus-rsas-v6-0r04f04sp09", + "displayName": "NSFOCUS RSAS V6.0R04F04SP09", + "description": "OctoBus package for NSFOCUS RSAS (Remote Security Assessment System) V6.0R04F04SP09 REST APIs: scan tasks, templates, reports, and agent management.", + "runtime": { + "mode": "long-running" + }, + "proto": { + "roots": [ + "proto" + ], + "files": [ + "proto/nsfocus_rsas_v6_0r04f04sp09.proto" + ] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateTask": { + "name": "create-task", + "description": "Create a task from a config.xml payload." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateVulnTask": { + "name": "create-vuln-task", + "description": "Create a vulnerability scan task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateBaselineTask": { + "name": "create-baseline-task", + "description": "Create a configuration/baseline check task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreatePwdTask": { + "name": "create-pwd-task", + "description": "Create a password guessing task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateWebTask": { + "name": "create-web-task", + "description": "Create a web application scan task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateOfflineTask": { + "name": "create-offline-task", + "description": "Create an offline scan task from a result file." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateDockerTask": { + "name": "create-docker-task", + "description": "Create a container image scan task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateCodeauditTask": { + "name": "create-codeaudit-task", + "description": "Create a source code audit task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateHostAssetsTask": { + "name": "create-host-assets-task", + "description": "Create a host asset discovery task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateWebAssetsTask": { + "name": "create-web-assets-task", + "description": "Create a web asset discovery task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetTaskStatus": { + "name": "get-task-status", + "description": "Query the status of a task by id." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/PauseTask": { + "name": "pause-task", + "description": "Pause a running task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ResumeTask": { + "name": "resume-task", + "description": "Resume a paused task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/StopTask": { + "name": "stop-task", + "description": "Stop a task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/DeleteTask": { + "name": "delete-task", + "description": "Delete a task by id." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/BatchDeleteTasks": { + "name": "batch-delete-tasks", + "description": "Delete multiple tasks by id list." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListTasks": { + "name": "list-tasks", + "description": "List tasks with optional filters." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListActiveTasks": { + "name": "list-active-tasks", + "description": "List currently running tasks." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetTaskResult": { + "name": "get-task-result", + "description": "Fetch scan results for a task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateAuthInfo": { + "name": "create-auth-info", + "description": "Push scan authentication credentials." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/LoginVerify": { + "name": "login-verify", + "description": "Verify a scan login credential against a target." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListSysvulnTemplate": { + "name": "list-sysvuln-template", + "description": "List system vulnerability templates." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListWebvulnTemplate": { + "name": "list-webvuln-template", + "description": "List web vulnerability templates." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListBaselineTemplate": { + "name": "list-baseline-template", + "description": "List configuration/baseline templates." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetBaselineParams": { + "name": "get-baseline-params", + "description": "Get parameters for a baseline template." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListCodeauditTemplate": { + "name": "list-codeaudit-template", + "description": "List code audit templates." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListAssetTemplate": { + "name": "list-asset-template", + "description": "List asset discovery templates." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateBaselineTemplate": { + "name": "create-baseline-template", + "description": "Import a baseline template file." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListUserpwd": { + "name": "list-userpwd", + "description": "List password dictionaries." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/CreateUserpwd": { + "name": "create-userpwd", + "description": "Push a custom password dictionary." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetSystemStatus": { + "name": "get-system-status", + "description": "Get device system status and resource usage." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetLogInfo": { + "name": "get-log-info", + "description": "Query audit logs." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/ListReportTemplate": { + "name": "list-report-template", + "description": "List report templates." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GenerateReport": { + "name": "generate-report", + "description": "Generate a report for a task." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetReportProgress": { + "name": "get-report-progress", + "description": "Query report generation progress." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/DownloadReport": { + "name": "download-report", + "description": "Download a generated report zip." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/DeleteReports": { + "name": "delete-reports", + "description": "Delete reports by id list or all." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetAgentMethodConfig": { + "name": "get-agent-method-config", + "description": "Get the agent deployment method." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/SetAgentMethodConfig": { + "name": "set-agent-method-config", + "description": "Set the agent deployment method." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetAgentAuth": { + "name": "get-agent-auth", + "description": "Get agent authorization/registration counts." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/GetAgentPackageUrl": { + "name": "get-agent-package-url", + "description": "Get agent installer download links." + }, + "Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09/DownloadAgent": { + "name": "download-agent", + "description": "Download the agent installer for a platform." + } + } + } + } +} diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js b/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js new file mode 100644 index 00000000..bf8cff63 --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js @@ -0,0 +1,768 @@ +import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; +import { Agent } from 'undici'; + +export const SERVICE_FQN = 'Nsfocus_RSAS_V60R04F04SP09.Nsfocus_RSAS_V60R04F04SP09'; +const method = (name) => `${SERVICE_FQN}/${name}`; + +export const DEFAULT_TIMEOUT_MS = 30000; + +// --------------------------------------------------------------------------- +// Context, bindings, credentials +// --------------------------------------------------------------------------- + +const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj ?? {}, key); +const firstDefined = (...vals) => vals.find((val) => val !== undefined && val !== null); + +const unwrapScalar = (value) => { + if (value === undefined || value === null) return undefined; + if (typeof value === 'object' && value !== null && hasOwn(value, 'value')) return unwrapScalar(value.value); + return value; +}; + +const grpcCodeFor = (code) => ({ + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + RESOURCE_EXHAUSTED: grpcStatus.RESOURCE_EXHAUSTED, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + UNKNOWN: grpcStatus.UNKNOWN, +})[code] ?? grpcStatus.UNKNOWN; + +const errorWithCode = (code, message) => { + const err = new GrpcError(grpcCodeFor(code), `${code}: ${message}`); + err.legacyCode = code; + return err; +}; + +const mergedBindings = (ctx = {}) => ({ + ...(ctx?.bindings ?? {}), + ...(ctx?.config ?? {}), + ...(ctx?.secret ?? {}), +}); + +const resolveCallContext = (ctx = {}) => ({ + ...ctx, + bindings: mergedBindings(ctx), + config: ctx.config ?? {}, + secret: ctx.secret ?? {}, + limits: ctx.limits ?? {}, + meta: ctx.meta ?? {}, + req: ctx.request ?? ctx.req ?? {}, +}); + +const requestFromContext = (ctx = {}) => ctx?.request ?? ctx?.req ?? {}; + +const normalizeBaseUrl = (value) => { + const raw = String(unwrapScalar(value) ?? '').trim(); + if (!/^https?:\/\//i.test(raw)) return ''; + return raw.replace(/\/+$/, ''); +}; + +const resolveBaseUrl = (bindings) => normalizeBaseUrl(firstDefined( + bindings?.host, + bindings?.restBaseUrl, + bindings?.rest_base_url, + bindings?.baseUrl, + bindings?.base_url, +)); + +const requireHost = (callCtx) => { + const host = resolveBaseUrl(callCtx?.bindings || {}); + if (!host) throw errorWithCode('INVALID_ARGUMENT', 'host is required in bindings'); + return host; +}; + +const resolveTimeoutMs = (callCtx) => { + const bindings = callCtx?.bindings ?? {}; + const raw = Number(firstDefined(callCtx?.limits?.timeoutMs, bindings.timeoutMs, bindings.timeout_ms, DEFAULT_TIMEOUT_MS)); + return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS; +}; + +const resolveCurrLang = (callCtx) => { + const bindings = callCtx?.bindings ?? {}; + const raw = String(unwrapScalar(firstDefined(bindings.currLang, bindings.curr_lang)) ?? '').trim().toLowerCase(); + return raw === 'en' ? 'en' : 'cn'; +}; + +const pickCredential = (callCtx, fieldNames, fieldLabel) => { + const sources = [callCtx?.secret || {}, callCtx?.config || {}, callCtx?.bindings || {}]; + for (const field of fieldNames) { + for (const source of sources) { + const text = String(unwrapScalar(source[field]) ?? '').trim(); + if (text) return text; + } + } + throw errorWithCode('INVALID_ARGUMENT', `${fieldLabel} is required`); +}; + +const authQuery = (callCtx) => ({ + username: pickCredential(callCtx, ['user', 'username'], 'username'), + password: pickCredential(callCtx, ['password'], 'password'), + format: 'json', + curr_lang: resolveCurrLang(callCtx), +}); + +// --------------------------------------------------------------------------- +// TLS, coercion helpers +// --------------------------------------------------------------------------- + +const toBoolean = (value) => { + const raw = unwrapScalar(value); + if (typeof raw === 'boolean') return raw; + if (typeof raw === 'number') return raw !== 0; + if (typeof raw === 'string') { + const normalized = raw.trim().toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) return true; + if (['false', '0', 'no', 'n', 'off', ''].includes(normalized)) return false; + } + return false; +}; + +let insecureTlsDispatcher; +const getInsecureTlsDispatcher = () => { + insecureTlsDispatcher ??= new Agent({ connect: { rejectUnauthorized: false } }); + return insecureTlsDispatcher; +}; + +const buildTlsOptions = (bindings) => { + if (!toBoolean(bindings?.skipTlsVerify) && !toBoolean(bindings?.tlsInsecureSkipVerify) && !toBoolean(bindings?.insecureSkipVerify)) return {}; + return { dispatcher: getInsecureTlsDispatcher() }; +}; + +const toInteger = (value, fallback = 0) => { + const num = Number(unwrapScalar(value)); + if (!Number.isFinite(num) || Number.isNaN(num)) return fallback; + return Math.trunc(num); +}; + +const toTrimmed = (value) => String(unwrapScalar(value) ?? '').trim(); + +const requireField = (value, field) => { + const text = toTrimmed(value); + if (!text) throw errorWithCode('INVALID_ARGUMENT', `${field} is required`); + return text; +}; + +const toValue = (val) => { + const raw = unwrapScalar(val); + if (raw === undefined || raw === null) return undefined; + if (typeof raw === 'string') return { stringValue: raw }; + if (typeof raw === 'number') return { numberValue: raw }; + if (typeof raw === 'boolean') return { boolValue: raw }; + if (Array.isArray(raw)) { + return { listValue: { values: raw.map((item) => toValue(item)).filter((item) => item !== undefined) } }; + } + if (typeof raw === 'object') { + const fields = {}; + for (const [key, value] of Object.entries(raw)) { + const normalized = toValue(value); + fields[key] = normalized === undefined ? { nullValue: 'NULL_VALUE' } : normalized; + } + return { structValue: { fields } }; + } + return { stringValue: String(raw) }; +}; + +const appendQuery = (url, params = {}) => { + const pairs = []; + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null || value === '') continue; + pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + } + if (pairs.length === 0) return url; + return url.includes('?') ? `${url}&${pairs.join('&')}` : `${url}?${pairs.join('&')}`; +}; + +// --------------------------------------------------------------------------- +// HTTP transport +// --------------------------------------------------------------------------- + +const parseJsonOrThrow = (text) => { + try { + return JSON.parse(text); + } catch { + throw errorWithCode('UNKNOWN', 'response is not valid JSON'); + } +}; + +// Map RSAS transport-level HTTP failures to gRPC legacy codes before body parsing. +const guardHttpStatus = (status) => { + if (status === 401) throw errorWithCode('PERMISSION_DENIED', 'authentication failed (HTTP 401)'); + if (status === 403) throw errorWithCode('PERMISSION_DENIED', 'not authorized (HTTP 403)'); + if (status === 404) throw errorWithCode('INVALID_ARGUMENT', 'no such interface (HTTP 404)'); + if (status === 405) throw errorWithCode('INVALID_ARGUMENT', 'method not allowed (HTTP 405)'); + if (status === 429) throw errorWithCode('RESOURCE_EXHAUSTED', 'too many requests (HTTP 429)'); + if (status >= 500) throw errorWithCode('UNAVAILABLE', `upstream error (HTTP ${status})`); +}; + +const doFetch = async (callCtx, url, init = {}) => { + let res; + try { + res = await fetch(url, { + ...init, + signal: AbortSignal.timeout(resolveTimeoutMs(callCtx)), + ...buildTlsOptions(callCtx.bindings), + headers: { + ...(callCtx.bindings?.headers || {}), + ...(init.headers || {}), + }, + }); + } catch (err) { + throw errorWithCode('UNAVAILABLE', err?.cause?.message || err?.message || 'fetch failed'); + } + return res; +}; + +// Send a request whose response is expected to be the standard RSAS JSON envelope. +const requestJson = async (callCtx, { path, query = {}, method: httpMethod = 'GET', form, json }) => { + const host = requireHost(callCtx); + const url = appendQuery(`${host}${path}`, { ...authQuery(callCtx), ...query }); + const init = { method: httpMethod }; + if (form !== undefined) { + init.body = form; // FormData sets its own multipart Content-Type + boundary. + } else if (json !== undefined) { + init.headers = { 'Content-Type': 'application/json' }; + init.body = JSON.stringify(json); + } + const res = await doFetch(callCtx, url, init); + const status = toInteger(res.status, 0); + const text = await res.text(); + guardHttpStatus(status); + if (!String(text || '').trim()) throw errorWithCode('UNKNOWN', 'response body is empty'); + return { status, json: parseJsonOrThrow(text) }; +}; + +// Send a request whose response is a binary file (report zip / agent installer). +const requestBinary = async (callCtx, { path, query = {}, method: httpMethod = 'GET' }) => { + const host = requireHost(callCtx); + const url = appendQuery(`${host}${path}`, { ...authQuery(callCtx), ...query }); + const res = await doFetch(callCtx, url, { method: httpMethod }); + const status = toInteger(res.status, 0); + const contentType = String(res.headers?.get?.('content-type') ?? ''); + // A JSON body on a download endpoint signals an error envelope, not a file. + if (/application\/json/i.test(contentType)) { + const text = await res.text(); + guardHttpStatus(status); + const parsed = parseJsonOrThrow(text || '{}'); + throw errorWithCode('UNKNOWN', String(parsed?.ret_msg ?? 'download failed')); + } + guardHttpStatus(status); + const buffer = Buffer.from(await res.arrayBuffer()); + return { + http_status: status, + content_type: contentType, + content_length: buffer.length, + filename: filenameFromContentDisposition(res.headers?.get?.('content-disposition')), + body_base64: buffer.toString('base64'), + }; +}; + +const filenameFromContentDisposition = (value) => { + const raw = String(value ?? ''); + const star = raw.match(/filename\*=(?:UTF-8'')?([^;]+)/i); + if (star?.[1]) { + try { + return decodeURIComponent(star[1].trim().replace(/^"|"$/g, '')); + } catch { + return star[1].trim().replace(/^"|"$/g, ''); + } + } + const plain = raw.match(/filename="?([^";]+)"?/i); + return plain?.[1] ? plain[1].trim() : ''; +}; + +// --------------------------------------------------------------------------- +// Response normalization +// --------------------------------------------------------------------------- + +const toRsasResponse = ({ status, json }) => ({ + ret_code: toInteger(json?.ret_code, 0), + ret_msg: String(json?.ret_msg ?? ''), + data: toValue(json?.data), + http_status: toInteger(status, 0), +}); + +const toTaskCreateResponse = ({ status, json }) => { + const data = json?.data; + const taskId = firstDefined(data?.task_id, data?.taskId); + return { + ...toRsasResponse({ status, json }), + task_id: taskId === undefined || taskId === null ? '' : String(taskId), + }; +}; + +// --------------------------------------------------------------------------- +// Multipart form builders +// --------------------------------------------------------------------------- + +const formFromFields = (fields) => { + const form = new FormData(); + for (const [key, value] of Object.entries(fields)) { + if (value === undefined || value === null || value === '') continue; + form.append(key, String(value)); + } + return form; +}; + +const appendFile = (form, field, base64, filename, contentType = 'application/octet-stream') => { + const buffer = Buffer.from(String(base64 ?? ''), 'base64'); + form.append(field, new Blob([buffer], { type: contentType }), filename); +}; + +// --------------------------------------------------------------------------- +// Handlers: task lifecycle +// --------------------------------------------------------------------------- + +const handleCreateTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const configXml = requireField(req?.config_xml, 'config_xml'); + const form = formFromFields({ type: toTrimmed(req?.type) || '1' }); + const filename = toTrimmed(req?.config_xml_filename) || 'config.xml'; + form.append('config_xml', new Blob([Buffer.from(configXml, 'utf8')], { type: 'text/xml' }), filename); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/create', method: 'POST', form })); +}; + +const handleCreateVulnTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + targets: requireField(req?.targets, 'targets'), + template_id: req?.template_id === undefined ? undefined : toInteger(req.template_id, 0), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/vul/create', method: 'POST', form })); +}; + +const handleCreateBaselineTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + targets: requireField(req?.targets, 'targets'), + login_type: requireField(req?.login_type, 'login_type'), + login_port: requireField(req?.login_port, 'login_port'), + login_name: requireField(req?.login_name, 'login_name'), + login_password: requireField(req?.login_password, 'login_password'), + template_uuid: requireField(req?.template_uuid, 'template_uuid'), + template_param: toTrimmed(req?.template_param) || '{}', + protect_level: toTrimmed(req?.protect_level), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/baseline/create', method: 'POST', form })); +}; + +const handleCreatePwdTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + targets: requireField(req?.targets, 'targets'), + template_id: requireField(req?.template_id, 'template_id'), + service_type: requireField(req?.service_type, 'service_type'), + pass_mode: toTrimmed(req?.pass_mode), + port: req?.port === undefined ? undefined : toInteger(req.port, 0), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/pwd/create', method: 'POST', form })); +}; + +const handleCreateWebTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + targets: requireField(req?.targets, 'targets'), + template_id: toTrimmed(req?.template_id), + component_scan: toTrimmed(req?.component_scan), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/web/create', method: 'POST', form })); +}; + +const handleCreateOfflineTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const base64 = requireField(req?.task_field_base64, 'task_field_base64'); + const form = new FormData(); + appendFile(form, 'taskField', base64, toTrimmed(req?.filename) || 'offline_result.dat'); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/offline/create', method: 'POST', form })); +}; + +const handleCreateDockerTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + targets: requireField(req?.targets, 'targets'), + registry_address: toTrimmed(req?.registry_address), + registry_username: toTrimmed(req?.registry_username), + registry_password: toTrimmed(req?.registry_password), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/docker/create', method: 'POST', form })); +}; + +const handleCreateCodeauditTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + code_source: toInteger(requireField(req?.code_source, 'code_source'), 0), + repo_path: requireField(req?.repo_path, 'repo_path'), + repo_username: toTrimmed(req?.repo_username), + repo_password: toTrimmed(req?.repo_password), + template_id: req?.template_id === undefined ? undefined : toInteger(req.template_id, 0), + exclude_files: toTrimmed(req?.exclude_files), + exclude_dirs: toTrimmed(req?.exclude_dirs), + git_branch: toTrimmed(req?.git_branch), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/codeaudit/create', method: 'POST', form })); +}; + +const handleCreateHostAssetsTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + targets: requireField(req?.targets, 'targets'), + is_related: toTrimmed(req?.is_related), + template_id: toTrimmed(req?.template_id), + prohibitConfig: toTrimmed(req?.prohibit_config), + prohibitPorts: toTrimmed(req?.prohibit_ports), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/hostassets/create', method: 'POST', form })); +}; + +const handleCreateWebAssetsTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + name: requireField(req?.name, 'name'), + targets: requireField(req?.targets, 'targets'), + is_related: toTrimmed(req?.is_related), + is_sitetree: toTrimmed(req?.is_sitetree), + }); + return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/webassets/create', method: 'POST', form })); +}; + +const requireTaskId = (req) => encodeURIComponent(requireField(req?.task_id, 'task_id')); + +const handleGetTaskStatus = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: `/api/task/status/${requireTaskId(req)}`, method: 'GET' })); +}; + +const handlePauseTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: `/api/task/pause/${requireTaskId(req)}`, method: 'POST' })); +}; + +const handleResumeTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: `/api/task/resume/${requireTaskId(req)}`, method: 'POST' })); +}; + +const handleStopTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: `/api/task/stop/${requireTaskId(req)}`, method: 'POST' })); +}; + +const handleDeleteTask = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: `/api/task/delete/${requireTaskId(req)}`, method: 'POST' })); +}; + +const handleBatchDeleteTasks = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const ids = (Array.isArray(req?.task_ids) ? req.task_ids : []).map((id) => toInteger(id, 0)).filter((id) => id > 0); + if (ids.length === 0) throw errorWithCode('INVALID_ARGUMENT', 'task_ids must contain positive integers'); + return toRsasResponse(await requestJson(callCtx, { path: '/api/task/batch_delete', method: 'POST', json: { task_ids: ids } })); +}; + +const handleListTasks = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const query = { + type: req?.type === undefined ? undefined : toInteger(req.type, 0), + end_time_start: toTrimmed(req?.end_time_start), + end_time_end: toTrimmed(req?.end_time_end), + isfinished: toTrimmed(req?.isfinished), + page: req?.page === undefined ? undefined : toInteger(req.page, 0), + page_size: req?.page_size === undefined ? undefined : toInteger(req.page_size, 0), + }; + return toRsasResponse(await requestJson(callCtx, { path: '/api/task/list', method: 'GET', query })); +}; + +const handleListActiveTasks = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/task/active_list', method: 'GET' })); +}; + +const handleGetTaskResult = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const query = { + targets: toTrimmed(req?.targets), + page: req?.page === undefined ? undefined : toInteger(req.page, 0), + page_size: req?.page_size === undefined ? undefined : toInteger(req.page_size, 0), + }; + return toRsasResponse(await requestJson(callCtx, { path: `/api/report/task/${requireTaskId(req)}`, method: 'GET', query })); +}; + +const handleCreateAuthInfo = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const entries = Array.isArray(req?.authinfo) ? req.authinfo : []; + if (entries.length === 0) throw errorWithCode('INVALID_ARGUMENT', 'authinfo must contain at least one entry'); + const payload = entries.map((entry) => ({ + accountname: requireField(entry?.accountname, 'authinfo.accountname'), + accountpwd: toTrimmed(entry?.accountpwd), + port: requireField(entry?.port, 'authinfo.port'), + protocol: requireField(entry?.protocol, 'authinfo.protocol'), + ip: requireField(entry?.ip, 'authinfo.ip'), + })); + const form = formFromFields({ authinfo: JSON.stringify(payload) }); + return toRsasResponse(await requestJson(callCtx, { path: '/api/authinfo/create', method: 'POST', form })); +}; + +const handleLoginVerify = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const jumparray = (Array.isArray(req?.jumparray) ? req.jumparray : []).map((jh) => ({ + ip_range: toTrimmed(jh?.ip_range), + protocol: toTrimmed(jh?.protocol), + user_name: toTrimmed(jh?.user_name), + user_pwd: toTrimmed(jh?.user_pwd), + port: toInteger(jh?.port, 0), + })); + const form = formFromFields({ + ip: requireField(req?.ip, 'ip'), + protocol: requireField(req?.protocol, 'protocol'), + port: toInteger(requireField(req?.port, 'port'), 0), + user_name: requireField(req?.user_name, 'user_name'), + userpwd: requireField(req?.userpwd, 'userpwd'), + jump_ifuse: toBoolean(req?.jump_ifuse) ? 'true' : 'false', + jumparray: jumparray.length ? JSON.stringify(jumparray) : undefined, + web_login_url: toTrimmed(req?.web_login_url), + web_login_cookie: toTrimmed(req?.web_login_cookie), + format: 'json', + }); + return toRsasResponse(await requestJson(callCtx, { path: '/api/auth/login_verify', method: 'POST', form })); +}; + +// --------------------------------------------------------------------------- +// Handlers: templates and dictionaries +// --------------------------------------------------------------------------- + +const handleListSysvulnTemplate = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/template/sysvuln/list', method: 'GET' })); +}; + +const handleListWebvulnTemplate = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/template/webvuln/list', method: 'GET' })); +}; + +const handleListBaselineTemplate = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const query = { industry: toTrimmed(req?.industry) }; + return toRsasResponse(await requestJson(callCtx, { path: '/api/template/baseline/list', method: 'GET', query })); +}; + +const handleGetBaselineParams = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const query = { uuid: requireField(req?.uuid, 'uuid') }; + return toRsasResponse(await requestJson(callCtx, { path: '/api/template/baseline/params', method: 'GET', query })); +}; + +const handleListCodeauditTemplate = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const query = { type: req?.type === undefined ? undefined : toInteger(req.type, 0) }; + return toRsasResponse(await requestJson(callCtx, { path: '/api/template/codeaudit/list', method: 'GET', query })); +}; + +const handleListAssetTemplate = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/template/asset/list', method: 'GET' })); +}; + +const handleCreateBaselineTemplate = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const base64 = requireField(req?.template_base64, 'template_base64'); + const form = new FormData(); + appendFile(form, 'template', base64, toTrimmed(req?.filename) || 'template.dat'); + return toRsasResponse(await requestJson(callCtx, { path: '/api/template/baseline/create', method: 'POST', form })); +}; + +const handleListUserpwd = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/userpwd/list', method: 'GET' })); +}; + +const handleCreateUserpwd = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ userpwd: requireField(req?.userpwd, 'userpwd') }); + return toRsasResponse(await requestJson(callCtx, { path: '/api/userpwd/create', method: 'POST', form })); +}; + +// --------------------------------------------------------------------------- +// Handlers: system, logs, reports +// --------------------------------------------------------------------------- + +const handleGetSystemStatus = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/system/status', method: 'GET' })); +}; + +const handleGetLogInfo = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const query = { + account: toTrimmed(req?.account), + type: toTrimmed(req?.type), + starttime: toTrimmed(req?.starttime), + endtime: toTrimmed(req?.endtime), + ip: toTrimmed(req?.ip), + }; + return toRsasResponse(await requestJson(callCtx, { path: '/api/log/getlogInfo', method: 'GET', query })); +}; + +const handleListReportTemplate = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/report/template/list', method: 'GET' })); +}; + +const handleGenerateReport = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const form = formFromFields({ + task_id: requireField(req?.task_id, 'task_id'), + summary_template_id: toTrimmed(req?.summary_template_id), + host_template_id: toTrimmed(req?.host_template_id), + report_type: requireField(req?.report_type, 'report_type'), + }); + return toRsasResponse(await requestJson(callCtx, { path: '/api/generate_report/', method: 'POST', form })); +}; + +const handleGetReportProgress = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const reportId = encodeURIComponent(requireField(req?.report_id, 'report_id')); + return toRsasResponse(await requestJson(callCtx, { path: `/api/get_report_progress/report_id/${reportId}`, method: 'GET' })); +}; + +const handleDownloadReport = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const reportId = encodeURIComponent(requireField(req?.report_id, 'report_id')); + const reportType = encodeURIComponent(requireField(req?.report_type, 'report_type')); + return requestBinary(callCtx, { path: `/api/download_report/report_id/${reportId}/report_type/${reportType}`, method: 'GET' }); +}; + +const handleDeleteReports = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + let payload; + if (toBoolean(req?.all)) { + payload = { report_ids: 'all' }; + } else { + const ids = (Array.isArray(req?.report_ids) ? req.report_ids : []).map((id) => toInteger(id, 0)).filter((id) => id > 0); + if (ids.length === 0) throw errorWithCode('INVALID_ARGUMENT', 'report_ids must contain positive integers or set all=true'); + payload = { report_ids: ids }; + } + return toRsasResponse(await requestJson(callCtx, { path: '/api/delete_reports', method: 'POST', json: payload })); +}; + +// --------------------------------------------------------------------------- +// Handlers: agent +// --------------------------------------------------------------------------- + +const handleGetAgentMethodConfig = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/agent/agent_method_config', method: 'GET' })); +}; + +const handleSetAgentMethodConfig = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const agentMethod = toInteger(req?.agent_method, 0); + return toRsasResponse(await requestJson(callCtx, { path: '/api/agent/agent_method_config', method: 'POST', json: { agent_method: agentMethod } })); +}; + +const handleGetAgentAuth = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/agent/get_agent_auth', method: 'GET' })); +}; + +const handleGetAgentPackageUrl = async (_req, ctx) => { + const callCtx = resolveCallContext(ctx); + return toRsasResponse(await requestJson(callCtx, { path: '/api/agent/get_agentpackage_url', method: 'GET' })); +}; + +const handleDownloadAgent = async (req, ctx) => { + const callCtx = resolveCallContext(ctx); + const platform = requireField(req?.platform, 'platform').toLowerCase(); + if (platform !== 'linux' && platform !== 'windows') { + throw errorWithCode('INVALID_ARGUMENT', 'platform must be "linux" or "windows"'); + } + return requestBinary(callCtx, { path: `/api/agent/download/${platform}`, method: 'GET' }); +}; + +// --------------------------------------------------------------------------- +// Handler registry +// --------------------------------------------------------------------------- + +const HANDLERS = { + CreateTask: handleCreateTask, + CreateVulnTask: handleCreateVulnTask, + CreateBaselineTask: handleCreateBaselineTask, + CreatePwdTask: handleCreatePwdTask, + CreateWebTask: handleCreateWebTask, + CreateOfflineTask: handleCreateOfflineTask, + CreateDockerTask: handleCreateDockerTask, + CreateCodeauditTask: handleCreateCodeauditTask, + CreateHostAssetsTask: handleCreateHostAssetsTask, + CreateWebAssetsTask: handleCreateWebAssetsTask, + GetTaskStatus: handleGetTaskStatus, + PauseTask: handlePauseTask, + ResumeTask: handleResumeTask, + StopTask: handleStopTask, + DeleteTask: handleDeleteTask, + BatchDeleteTasks: handleBatchDeleteTasks, + ListTasks: handleListTasks, + ListActiveTasks: handleListActiveTasks, + GetTaskResult: handleGetTaskResult, + CreateAuthInfo: handleCreateAuthInfo, + LoginVerify: handleLoginVerify, + ListSysvulnTemplate: handleListSysvulnTemplate, + ListWebvulnTemplate: handleListWebvulnTemplate, + ListBaselineTemplate: handleListBaselineTemplate, + GetBaselineParams: handleGetBaselineParams, + ListCodeauditTemplate: handleListCodeauditTemplate, + ListAssetTemplate: handleListAssetTemplate, + CreateBaselineTemplate: handleCreateBaselineTemplate, + ListUserpwd: handleListUserpwd, + CreateUserpwd: handleCreateUserpwd, + GetSystemStatus: handleGetSystemStatus, + GetLogInfo: handleGetLogInfo, + ListReportTemplate: handleListReportTemplate, + GenerateReport: handleGenerateReport, + GetReportProgress: handleGetReportProgress, + DownloadReport: handleDownloadReport, + DeleteReports: handleDeleteReports, + GetAgentMethodConfig: handleGetAgentMethodConfig, + SetAgentMethodConfig: handleSetAgentMethodConfig, + GetAgentAuth: handleGetAgentAuth, + GetAgentPackageUrl: handleGetAgentPackageUrl, + DownloadAgent: handleDownloadAgent, +}; + +// Exported as { [fullMethodName]: (ctx) => Promise } — single-arg ctx signature +// is required by the package validator. +export const handlers = Object.fromEntries( + Object.entries(HANDLERS).map(([name, fn]) => [method(name), (ctx = {}) => fn(requestFromContext(ctx), ctx)]), +); + +export const METHODS = Object.fromEntries(Object.keys(HANDLERS).map((name) => [name, method(name)])); + +export const _test = { + appendQuery, + authQuery, + buildTlsOptions, + errorWithCode, + filenameFromContentDisposition, + formFromFields, + guardHttpStatus, + normalizeBaseUrl, + pickCredential, + requestBinary, + requestJson, + requireField, + resolveBaseUrl, + resolveCallContext, + resolveCurrLang, + resolveTimeoutMs, + toBoolean, + toInteger, + toRsasResponse, + toTaskCreateResponse, + toValue, +}; diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/src/service.js b/services/nsfocus__rsas_v6-0r04f04sp09/src/service.js new file mode 100644 index 00000000..edf735e3 --- /dev/null +++ b/services/nsfocus__rsas_v6-0r04f04sp09/src/service.js @@ -0,0 +1,7 @@ +import { defineService } from '@chaitin-ai/octobus-sdk'; + +import { handlers } from './nsfocus-rsas-v6-0r04f04sp09.js'; + +export { handlers } from './nsfocus-rsas-v6-0r04f04sp09.js'; + +export const service = defineService({ handlers }); diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/test/mock_upstream.js b/services/nsfocus__rsas_v6-0r04f04sp09/test/mock_upstream.js new file mode 100644 index 0000000000000000000000000000000000000000..84759802bff9f501cad23b8f6ac89d70fdbaf455 GIT binary patch literal 5309 zcmb_gTW{M&7Ve_^lwUDh6eZP2q$JCBtibgqhSOW>7`EGeC<G9 zyFdZA&%FReUy8QqzU*Ux{+J@({*3+3%uu3|DwR_wK_HT6&Ya8lopWaVS6PyVq8p@V zBIn0~g^}b(v0$(IH=<#d=86rJ%2+>7hs@EU^z7WeaU)0*saP&}csC74Y?HaUIIXeG z*KRPDh;z1wC6N>^UduE-6LrXbC;~+yYz!gBjEZ1TBqtII-v8jXehocdnz&BL6?f{) z)dG5%5%sb{4(!|7{1TCbv_cEpecii7zb|qRn+05%WwKU#a$%mQIhI-*N$Uh6*2@oN zn(QQJB2F_wo|0uGtF{q5ylCo&cjPgCJF*A@A*G{@aK|Lp3n<9t0xjGd%47WotLlst znhnJx9d#QTgjb1o(%|Gki7w zUUAq2kK@TS?nbp#&`cj-)oCH5l3gUAj?_I;hg_b#8Ftwk{G#W5K{nBw)*722DrM>< z;td^?V%mHZr#!4PQe99&+f67K03jl$$BGRMyGrjokCb==-(9n$4r?5kr$nL}YLp45 z3*WLVj*z)1O?=A3Z59l8E`{1GRKIZxUVENbZbCUDWRZ5*=Dv0CVAqo>kCNl4KXR>! zCi(M@*zgj%y+DWEgq-kuN*R;W05ay-M>$3J$m!;$IS6z>O<1s z_5GK}^u=234N@?PL|W~2uZh1cXY$Z`QtCa0R%r61;CV3E<2fHn<7+8#1R4VCkmKh( zno*OCctTmu&n;=~B2T3UI!dmCsNQm>@){-{G-!wz=8f446%o<+VKNhOB9p^(E~d4;)HNuOWeIdlkD_HpmQ!J$)I#xrXq?S`mJ zP!B|WZ_R7&H#_&5orAsR2EZJOvnUWfA}T&#{&I2o-)}De{p01u1wJa&kX~LS38;s< zQLm^BvZBkH-r7nXKpLimj`oU&Qi0}k(q-!#^n1YN09xBS8+RL>)$R4hz3qD&jpp`h z$C>Gxlx3VOT@dLf!$pSqXGQEAmg|7c1*npFlZS|EoFT%jI^MilCTlT3Ob{Xv1uB-d z74?eeO8zTS1J`6&n{4vy1E*KXaK(enlc=O11$1~>nzy!n)7bX3a z60cMK_~*|qKm8nu&+?SoA97CBpPb(kF9<`?Q1o;(dNSaI!FIFVX*4$)ty^eC-IeujbG4KMNf%0W)~^FJaU|8X^Gox@ zP{>v!-S}#{A5b_kmBDfQt0$w6s*fsFJO-I8-|XWY+h_x_9PNES$QczVX+us(`lZt-t+Ltxf#mKZol`-2Q zokp#vAr*y^AoTw8Idv=&oClQX*{nR%r-k7+i+hALsvg3t4wE-;E+6ivOCq|vwXFvL zA7ea|{&c&m7RiMUlr;}szT8FEwZCF{n4TvzCzqkET3zkUrgDE2(Tre^Ff=^5&feQE z{vG4SiD;Om4am#}wkIcRJ%f)cb^MtFHmRDina!qgwB{_85l!KGC|8OHgCS1r~0WI0QQ4AC1>;HRZ@C1L8eso*~D2+YUNG?oo zRS%U19F?A9pjgT#GkZNXT3l}-nD@sH^tLEezpl}bz_}iV=cXkgOZ|)u(-noB&XaBm;n+8g#29A4z4=d=qDvWmz?mgIk_n>iy zz}C1QCB>uOqg!i@mOh(dru{Q$N2TXi2RJ7AEJmrMsh--Ce{DktH9-{O7G?$8{=d%)F92kZz`<~zHZB=;Fo9ovR9YS<}0oq zy3^Z>=`}_7 ({ + bindings: { host: 'http://device.example:8443', ...(overrides.bindings || {}) }, + config: overrides.config || {}, + secret: { user: 'admin', password: 'Rsas@123', ...(overrides.secret || {}) }, + limits: { timeoutMs: 10_000, ...(overrides.limits || {}) }, + meta: { instance_id: overrides.instance_id || 'inst-1', request_id: 'req' }, + request: overrides.request || {}, +}); + +const call = (methodName, request = {}, ctx = {}) => handlers[METHODS[methodName]]({ ...buildCtx(ctx), request }); + +const setFetch = (impl) => { globalThis.fetch = impl; }; + +const expectGrpc = async (fn, legacyCode) => { + let caught; + try { + await fn(); + } catch (err) { + caught = err; + } + assert.ok(caught instanceof GrpcError, 'expected GrpcError'); + assert.equal(caught.legacyCode, legacyCode); + assert.match(caught.message, new RegExp(`^${legacyCode}:`)); +}; + +test.afterEach(() => { globalThis.fetch = originalFetch; }); + +test('service and handler registry are wired', () => { + assert.equal(typeof service, 'object'); + assert.equal(Object.keys(handlers).length, 42); + assert.equal(typeof handlers[METHODS.GetSystemStatus], 'function'); + assert.equal(typeof handlers[METHODS.DownloadAgent], 'function'); +}); + +test('helpers: base url, coercion, currLang', () => { + assert.equal(_test.resolveBaseUrl({ host: 'https://d/' }), 'https://d'); + assert.equal(_test.resolveBaseUrl({ base_url: 'ftp://x' }), ''); + assert.equal(_test.toInteger('42abc', 0), 0); + assert.equal(_test.toInteger(' 7 ', 0), 7); + assert.equal(_test.toBoolean('yes'), true); + assert.equal(_test.toBoolean('off'), false); + assert.equal(_test.resolveCurrLang(_test.resolveCallContext({ config: { currLang: 'en' } })), 'en'); + assert.equal(_test.resolveCurrLang(_test.resolveCallContext({})), 'cn'); +}); + +test('authQuery pulls credentials with precedence and rejects when missing', () => { + const ctx = _test.resolveCallContext(buildCtx({ secret: { user: 'u', password: 'p' } })); + assert.deepEqual(_test.authQuery(ctx), { username: 'u', password: 'p', format: 'json', curr_lang: 'cn' }); + const noCred = _test.resolveCallContext({ bindings: { host: 'http://x' } }); + assert.throws(() => _test.authQuery(noCred), /INVALID_ARGUMENT/); +}); + +test('filenameFromContentDisposition parses plain and extended forms', () => { + assert.equal(_test.filenameFromContentDisposition('attachment; filename="a.zip"'), 'a.zip'); + assert.equal(_test.filenameFromContentDisposition("attachment; filename*=UTF-8''b%20c.zip"), 'b c.zip'); + assert.equal(_test.filenameFromContentDisposition(''), ''); +}); + +test('appendQuery encodes and skips empties', () => { + assert.equal(_test.appendQuery('http://x/api', { a: '1 2', b: '', c: undefined }), 'http://x/api?a=1%202'); + assert.equal(_test.appendQuery('http://x?z=1', { a: 'b' }), 'http://x?z=1&a=b'); +}); + +test('missing host and required args reject with INVALID_ARGUMENT', async () => { + await expectGrpc(() => call('GetSystemStatus', {}, { bindings: { host: 'device.local' } }), 'INVALID_ARGUMENT'); + await expectGrpc(() => call('CreateVulnTask', { targets: '1.1.1.1' }), 'INVALID_ARGUMENT'); + await expectGrpc(() => call('GetTaskStatus', {}), 'INVALID_ARGUMENT'); + await expectGrpc(() => call('BatchDeleteTasks', { task_ids: [] }), 'INVALID_ARGUMENT'); + await expectGrpc(() => call('DownloadAgent', { platform: 'mac' }), 'INVALID_ARGUMENT'); + await expectGrpc(() => call('DeleteReports', {}), 'INVALID_ARGUMENT'); +}); + +test('HTTP status mapping via guardHttpStatus', () => { + assert.throws(() => _test.guardHttpStatus(401), /PERMISSION_DENIED/); + assert.throws(() => _test.guardHttpStatus(403), /PERMISSION_DENIED/); + assert.throws(() => _test.guardHttpStatus(404), /INVALID_ARGUMENT/); + assert.throws(() => _test.guardHttpStatus(429), /RESOURCE_EXHAUSTED/); + assert.throws(() => _test.guardHttpStatus(500), /UNAVAILABLE/); + assert.doesNotThrow(() => _test.guardHttpStatus(200)); +}); + +test('network failure maps to UNAVAILABLE', async () => { + setFetch(async () => { throw new Error('ECONNREFUSED'); }); + await expectGrpc(() => call('GetSystemStatus'), 'UNAVAILABLE'); +}); + +test('empty body maps to UNKNOWN', async () => { + setFetch(async () => ({ status: 200, headers: { get: () => 'application/json' }, text: async () => '', arrayBuffer: async () => new ArrayBuffer(0) })); + await expectGrpc(() => call('GetSystemStatus'), 'UNKNOWN'); +}); + +// --- integration against the mock device --- + +test('GetSystemStatus returns normalized envelope', async () => { + const mock = await createMockServer(); + try { + const out = await call('GetSystemStatus', {}, { bindings: { host: mock.url } }); + assert.equal(out.ret_code, 0); + assert.equal(out.http_status, 200); + assert.equal(out.data.structValue.fields.version.stringValue, 'V6.0R04F04SP09'); + const req = mock.requests.find((r) => r.pathname === '/api/system/status'); + assert.equal(req.query.username, 'admin'); + assert.equal(req.query.format, 'json'); + } finally { + await mock.close(); + } +}); + +test('bad credentials produce PERMISSION_DENIED', async () => { + const mock = await createMockServer(); + try { + await expectGrpc(() => call('GetSystemStatus', {}, { bindings: { host: mock.url }, secret: { user: 'admin', password: 'wrong' } }), 'PERMISSION_DENIED'); + } finally { + await mock.close(); + } +}); + +test('CreateVulnTask surfaces task_id and posts multipart form', async () => { + const mock = await createMockServer(); + try { + const out = await call('CreateVulnTask', { name: 't', targets: '10.65.194.153', template_id: 0 }, { bindings: { host: mock.url } }); + assert.equal(out.ret_code, 0); + assert.equal(out.task_id, '60'); + const req = mock.requests.find((r) => r.pathname === '/api/task/vul/create'); + assert.equal(req.method, 'POST'); + assert.match(req.rawBody, /name="targets"/); + assert.match(req.rawBody, /10\.65\.194\.153/); + } finally { + await mock.close(); + } +}); + +test('GetTaskStatus, ListTasks, GetTaskResult round-trip', async () => { + const mock = await createMockServer(); + try { + const status = await call('GetTaskStatus', { task_id: '60' }, { bindings: { host: mock.url } }); + assert.equal(status.data.structValue.fields.status.numberValue, 4); + const list = await call('ListTasks', { type: 1, page: 1, page_size: 10 }, { bindings: { host: mock.url } }); + assert.equal(list.ret_code, 0); + const listReq = mock.requests.find((r) => r.pathname === '/api/task/list'); + assert.equal(listReq.query.type, '1'); + const result = await call('GetTaskResult', { task_id: '60', targets: '10.65.194.153' }, { bindings: { host: mock.url } }); + assert.equal(result.ret_code, 0); + } finally { + await mock.close(); + } +}); + +test('BatchDeleteTasks sends JSON body', async () => { + const mock = await createMockServer(); + try { + const out = await call('BatchDeleteTasks', { task_ids: [1, 2, 0, -3] }, { bindings: { host: mock.url } }); + assert.equal(out.ret_code, 0); + const req = mock.requests.find((r) => r.pathname === '/api/task/batch_delete'); + assert.deepEqual(JSON.parse(req.rawBody), { task_ids: [1, 2] }); + } finally { + await mock.close(); + } +}); + +test('report generate + progress + download flow', async () => { + const mock = await createMockServer(); + try { + const gen = await call('GenerateReport', { task_id: '60', report_type: 'pdf,html' }, { bindings: { host: mock.url } }); + assert.equal(gen.data.structValue.fields.report_id.numberValue, 16); + const prog = await call('GetReportProgress', { report_id: '16' }, { bindings: { host: mock.url } }); + assert.equal(prog.data.structValue.fields.progress.numberValue, 100); + const dl = await call('DownloadReport', { report_id: '16', report_type: 'html' }, { bindings: { host: mock.url } }); + assert.equal(dl.http_status, 200); + assert.equal(dl.content_type, 'application/zip'); + assert.equal(dl.filename, 'report_30.zip'); + const decoded = Buffer.from(dl.body_base64, 'base64'); + assert.equal(decoded.subarray(0, 2).toString('ascii'), 'PK'); + assert.ok(decoded.toString('utf8').includes('fake-report-zip')); + assert.equal(dl.content_length, decoded.length); + } finally { + await mock.close(); + } +}); + +test('agent config get/set and installer download', async () => { + const mock = await createMockServer(); + try { + const get = await call('GetAgentMethodConfig', {}, { bindings: { host: mock.url } }); + assert.equal(get.data.structValue.fields.agent_method.stringValue, '1'); + const set = await call('SetAgentMethodConfig', { agent_method: 1 }, { bindings: { host: mock.url } }); + assert.equal(set.ret_code, 0); + const setReq = mock.requests.find((r) => r.pathname === '/api/agent/agent_method_config' && r.method === 'POST'); + assert.deepEqual(JSON.parse(setReq.rawBody), { agent_method: 1 }); + const dl = await call('DownloadAgent', { platform: 'Linux' }, { bindings: { host: mock.url } }); + assert.equal(dl.content_type, 'application/octet-stream'); + assert.equal(dl.filename, 'NSFOCUS-Agent-Linux_x86-1.0.0.run'); + } finally { + await mock.close(); + } +}); + +test('ListSysvulnTemplate returns list data', async () => { + const mock = await createMockServer(); + try { + const out = await call('ListSysvulnTemplate', {}, { bindings: { host: mock.url } }); + assert.equal(out.data.listValue.values[0].structValue.fields.name.stringValue, '端口扫描'); + } finally { + await mock.close(); + } +}); + +test('skipTlsVerify wires an undici dispatcher', () => { + assert.deepEqual(_test.buildTlsOptions({}), {}); + const opts = _test.buildTlsOptions({ skipTlsVerify: true }); + assert.ok(opts.dispatcher, 'expected dispatcher when skipTlsVerify is set'); +}); diff --git a/services/package.json b/services/package.json index 91438aeb..b114c660 100644 --- a/services/package.json +++ b/services/package.json @@ -34,6 +34,7 @@ "nsfocus-ads-v4-5-r90-f06": "bin/nsfocus-ads-v4-5-r90-f06.js", "nsfocus-ngfw-v60-9900": "bin/nsfocus-ngfw-v60-9900.js", "nsfocus-nips-v5-6-r11": "bin/nsfocus-nips-v5-6-r11.js", + "nsfocus-rsas-v6-0r04f04sp09": "bin/nsfocus-rsas-v6-0r04f04sp09.js", "owasp-dependency-track-sca-v5-0": "bin/owasp-dependency-track-sca-v5-0.js", "panabit-tang-r1": "bin/panabit-tang-r1.js", "qianxin-fw-secgate3600": "bin/qianxin-fw-secgate3600.js", @@ -102,6 +103,7 @@ "bin/nsfocus-ads-v4-5-r90-f06.js", "bin/nsfocus-ngfw-v60-9900.js", "bin/nsfocus-nips-v5-6-r11.js", + "bin/nsfocus-rsas-v6-0r04f04sp09.js", "bin/owasp-dependency-track-sca-v5-0.js", "bin/panabit-tang-r1.js", "bin/qianxin-fw-secgate3600.js", @@ -166,6 +168,7 @@ "nsfocus__ads_v4-5-r90-f06", "nsfocus__ngfw_v60-9900", "nsfocus__nips_v5-6-r11", + "nsfocus__rsas_v6-0r04f04sp09", "owasp__dependency-track-sca_v5-0", "panabit__tang-r1", "qianxin__fw-secgate3600", From f3519cac4bbae43b9b5940133f0ee145142eb2f9 Mon Sep 17 00:00:00 2001 From: hilariacrucita <3362488788@qq.com> Date: Thu, 9 Jul 2026 17:21:28 +0800 Subject: [PATCH 2/5] fix(nsfocus-rsas): read password from secret only and omit proto3-default int fields - pickCredential no longer falls back to config/bindings for password; it is resolved from secret exclusively to avoid credential exposure via config, which permits arbitrary extra properties and is more easily logged. - Optional int64 request fields (template_id, port, type, page, page_size) decode as proto3 default 0 when unset, so the previous === undefined guard always sent 0. Treat 0/unset alike via toOptionalPositiveInt and omit them, preventing invalid page=0/port=0 from reaching the RSAS API. --- .../src/nsfocus-rsas-v6-0r04f04sp09.js | 37 +++++++++---- .../test/nsfocus-rsas-v6-0r04f04sp09.test.js | 52 +++++++++++++++++++ 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js b/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js index bf8cff63..2648619d 100644 --- a/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js +++ b/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js @@ -84,8 +84,11 @@ const resolveCurrLang = (callCtx) => { return raw === 'en' ? 'en' : 'cn'; }; -const pickCredential = (callCtx, fieldNames, fieldLabel) => { - const sources = [callCtx?.secret || {}, callCtx?.config || {}, callCtx?.bindings || {}]; +// scopeKeys selects which binding spaces to search. Password must stay +// secret-only: config permits arbitrary extra properties and is far more +// likely to be logged, version-controlled, or exported than secret. +const pickCredential = (callCtx, fieldNames, fieldLabel, scopeKeys = ['secret', 'config', 'bindings']) => { + const sources = scopeKeys.map((key) => callCtx?.[key] || {}); for (const field of fieldNames) { for (const source of sources) { const text = String(unwrapScalar(source[field]) ?? '').trim(); @@ -97,7 +100,7 @@ const pickCredential = (callCtx, fieldNames, fieldLabel) => { const authQuery = (callCtx) => ({ username: pickCredential(callCtx, ['user', 'username'], 'username'), - password: pickCredential(callCtx, ['password'], 'password'), + password: pickCredential(callCtx, ['password'], 'password', ['secret']), format: 'json', curr_lang: resolveCurrLang(callCtx), }); @@ -135,6 +138,17 @@ const toInteger = (value, fallback = 0) => { return Math.trunc(num); }; +// For optional int64 request fields: proto3 scalars decode unset values as 0, +// so `=== undefined` cannot distinguish "unset" from "0". These fields +// (template_id, port, type, page, page_size) have no valid 0 business value, +// so treat 0/unset alike and omit them rather than sending 0 downstream. +const toOptionalPositiveInt = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === '') return undefined; + const num = toInteger(raw, 0); + return num > 0 ? num : undefined; +}; + const toTrimmed = (value) => String(unwrapScalar(value) ?? '').trim(); const requireField = (value, field) => { @@ -327,7 +341,7 @@ const handleCreateVulnTask = async (req, ctx) => { const form = formFromFields({ name: requireField(req?.name, 'name'), targets: requireField(req?.targets, 'targets'), - template_id: req?.template_id === undefined ? undefined : toInteger(req.template_id, 0), + template_id: toOptionalPositiveInt(req?.template_id), }); return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/vul/create', method: 'POST', form })); }; @@ -356,7 +370,7 @@ const handleCreatePwdTask = async (req, ctx) => { template_id: requireField(req?.template_id, 'template_id'), service_type: requireField(req?.service_type, 'service_type'), pass_mode: toTrimmed(req?.pass_mode), - port: req?.port === undefined ? undefined : toInteger(req.port, 0), + port: toOptionalPositiveInt(req?.port), }); return toTaskCreateResponse(await requestJson(callCtx, { path: '/api/task/pwd/create', method: 'POST', form })); }; @@ -400,7 +414,7 @@ const handleCreateCodeauditTask = async (req, ctx) => { repo_path: requireField(req?.repo_path, 'repo_path'), repo_username: toTrimmed(req?.repo_username), repo_password: toTrimmed(req?.repo_password), - template_id: req?.template_id === undefined ? undefined : toInteger(req.template_id, 0), + template_id: toOptionalPositiveInt(req?.template_id), exclude_files: toTrimmed(req?.exclude_files), exclude_dirs: toTrimmed(req?.exclude_dirs), git_branch: toTrimmed(req?.git_branch), @@ -469,12 +483,12 @@ const handleBatchDeleteTasks = async (req, ctx) => { const handleListTasks = async (req, ctx) => { const callCtx = resolveCallContext(ctx); const query = { - type: req?.type === undefined ? undefined : toInteger(req.type, 0), + type: toOptionalPositiveInt(req?.type), end_time_start: toTrimmed(req?.end_time_start), end_time_end: toTrimmed(req?.end_time_end), isfinished: toTrimmed(req?.isfinished), - page: req?.page === undefined ? undefined : toInteger(req.page, 0), - page_size: req?.page_size === undefined ? undefined : toInteger(req.page_size, 0), + page: toOptionalPositiveInt(req?.page), + page_size: toOptionalPositiveInt(req?.page_size), }; return toRsasResponse(await requestJson(callCtx, { path: '/api/task/list', method: 'GET', query })); }; @@ -488,8 +502,8 @@ const handleGetTaskResult = async (req, ctx) => { const callCtx = resolveCallContext(ctx); const query = { targets: toTrimmed(req?.targets), - page: req?.page === undefined ? undefined : toInteger(req.page, 0), - page_size: req?.page_size === undefined ? undefined : toInteger(req.page_size, 0), + page: toOptionalPositiveInt(req?.page), + page_size: toOptionalPositiveInt(req?.page_size), }; return toRsasResponse(await requestJson(callCtx, { path: `/api/report/task/${requireTaskId(req)}`, method: 'GET', query })); }; @@ -762,6 +776,7 @@ export const _test = { resolveTimeoutMs, toBoolean, toInteger, + toOptionalPositiveInt, toRsasResponse, toTaskCreateResponse, toValue, diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js b/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js index 2c2870ee..d3aff4c3 100644 --- a/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js +++ b/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js @@ -61,6 +61,27 @@ test('authQuery pulls credentials with precedence and rejects when missing', () assert.throws(() => _test.authQuery(noCred), /INVALID_ARGUMENT/); }); +test('password is read from secret only, never from config or bindings', () => { + // username may fall back to config for dev, but password must come from secret. + const ctx = _test.resolveCallContext({ + secret: { user: 'u' }, + config: { password: 'leaky-from-config' }, + bindings: { host: 'http://x', password: 'leaky-from-bindings' }, + }); + assert.throws(() => _test.authQuery(ctx), /password is required/); + const ok = _test.resolveCallContext({ secret: { user: 'u', password: 'p' }, config: { user: 'cfg-user' } }); + assert.equal(_test.authQuery(ok).password, 'p'); +}); + +test('toOptionalPositiveInt treats 0/unset as omitted (proto3 default guard)', () => { + assert.equal(_test.toOptionalPositiveInt(undefined), undefined); + assert.equal(_test.toOptionalPositiveInt(0), undefined); + assert.equal(_test.toOptionalPositiveInt('0'), undefined); + assert.equal(_test.toOptionalPositiveInt(''), undefined); + assert.equal(_test.toOptionalPositiveInt(5), 5); + assert.equal(_test.toOptionalPositiveInt('22'), 22); +}); + test('filenameFromContentDisposition parses plain and extended forms', () => { assert.equal(_test.filenameFromContentDisposition('attachment; filename="a.zip"'), 'a.zip'); assert.equal(_test.filenameFromContentDisposition("attachment; filename*=UTF-8''b%20c.zip"), 'b c.zip'); @@ -157,6 +178,37 @@ test('GetTaskStatus, ListTasks, GetTaskResult round-trip', async () => { } }); +test('ListTasks omits type/page/page_size when they are proto3 default 0', async () => { + const mock = await createMockServer(); + try { + // Simulates a decoded proto3 request where unset int64 fields arrive as 0. + await call('ListTasks', { type: 0, page: 0, page_size: 0 }, { bindings: { host: mock.url } }); + const req = mock.requests.find((r) => r.pathname === '/api/task/list'); + assert.equal(req.query.type, undefined); + assert.equal(req.query.page, undefined); + assert.equal(req.query.page_size, undefined); + // format/curr_lang auth params are still present. + assert.equal(req.query.format, 'json'); + } finally { + await mock.close(); + } +}); + +test('CreatePwdTask omits port when it is proto3 default 0', async () => { + const mock = await createMockServer(); + try { + // pwd endpoint returns 404 in mock, but we only assert the outgoing form here. + await call('CreatePwdTask', { + name: 't', targets: '1.1.1.1', template_id: '188', service_type: 'SSH', port: 0, + }, { bindings: { host: mock.url } }).catch(() => {}); + const req = mock.requests.find((r) => r.pathname === '/api/task/pwd/create'); + assert.ok(req, 'expected a request to pwd/create'); + assert.doesNotMatch(req.rawBody, /name="port"/); + } finally { + await mock.close(); + } +}); + test('BatchDeleteTasks sends JSON body', async () => { const mock = await createMockServer(); try { From d424f7094a6fe9302feb96a9a356c99b73e1a9ef Mon Sep 17 00:00:00 2001 From: hilariacrucita <3362488788@qq.com> Date: Thu, 9 Jul 2026 17:30:06 +0800 Subject: [PATCH 3/5] fix(nsfocus-rsas): normalize camelCase request keys from the proto runtime The SDK serializes request fields with protobuf jsonName (camelCase), so the runtime delivers snake_case proto fields as e.g. taskId/templateUuid. Handlers read snake_case, so on a real device every such field resolved to undefined. Add normalizeRequest to mirror camelCase keys to snake_case recursively (covering nested messages like jumparray), applied at requestFromContext. Also default RSAS 1-based pagination (page/page_size) to sane values instead of a proto3 0 for ListTasks and GetTaskResult. Verified against a real RSAS device. --- .../src/nsfocus-rsas-v6-0r04f04sp09.js | 39 ++++++++++++++++--- .../test/nsfocus-rsas-v6-0r04f04sp09.test.js | 37 ++++++++++++++++-- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js b/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js index 2648619d..593755a2 100644 --- a/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js +++ b/services/nsfocus__rsas_v6-0r04f04sp09/src/nsfocus-rsas-v6-0r04f04sp09.js @@ -50,7 +50,30 @@ const resolveCallContext = (ctx = {}) => ({ req: ctx.request ?? ctx.req ?? {}, }); -const requestFromContext = (ctx = {}) => ctx?.request ?? ctx?.req ?? {}; +const snakeCaseKey = (key) => key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`); + +// The SDK serializes request fields with protobuf jsonName (camelCase), so the +// runtime delivers e.g. task_id as taskId. Handlers read snake_case, so mirror +// every camelCase key to its snake_case form — recursively, to also cover +// nested messages such as jumparray entries (ip_range/user_name/user_pwd) — +// without clobbering any snake_case key that is already present. +const normalizeRequest = (value) => { + if (Array.isArray(value)) return value.map((item) => normalizeRequest(item)); + if (value && typeof value === 'object') { + const out = {}; + for (const [key, val] of Object.entries(value)) out[key] = normalizeRequest(val); + for (const key of Object.keys(value)) { + if (!key.includes('_') && /[A-Z]/.test(key)) { + const snakeKey = snakeCaseKey(key); + if (!(snakeKey in out)) out[snakeKey] = out[key]; + } + } + return out; + } + return value; +}; + +const requestFromContext = (ctx = {}) => normalizeRequest(ctx?.request ?? ctx?.req ?? {}); const normalizeBaseUrl = (value) => { const raw = String(unwrapScalar(value) ?? '').trim(); @@ -482,13 +505,16 @@ const handleBatchDeleteTasks = async (req, ctx) => { const handleListTasks = async (req, ctx) => { const callCtx = resolveCallContext(ctx); + const pageVal = toInteger(req?.page, 0); + const pageSizeVal = toInteger(req?.page_size, 0); const query = { type: toOptionalPositiveInt(req?.type), end_time_start: toTrimmed(req?.end_time_start), end_time_end: toTrimmed(req?.end_time_end), isfinished: toTrimmed(req?.isfinished), - page: toOptionalPositiveInt(req?.page), - page_size: toOptionalPositiveInt(req?.page_size), + // RSAS pagination is 1-based; send sane defaults rather than a proto3 0. + page: pageVal > 0 ? pageVal : 1, + page_size: pageSizeVal > 0 ? pageSizeVal : 10, }; return toRsasResponse(await requestJson(callCtx, { path: '/api/task/list', method: 'GET', query })); }; @@ -500,10 +526,12 @@ const handleListActiveTasks = async (_req, ctx) => { const handleGetTaskResult = async (req, ctx) => { const callCtx = resolveCallContext(ctx); + const pageVal = toInteger(req?.page, 0); + const pageSizeVal = toInteger(req?.page_size, 0); const query = { targets: toTrimmed(req?.targets), - page: toOptionalPositiveInt(req?.page), - page_size: toOptionalPositiveInt(req?.page_size), + page: pageVal > 0 ? pageVal : 1, + page_size: pageSizeVal > 0 ? pageSizeVal : 20, }; return toRsasResponse(await requestJson(callCtx, { path: `/api/report/task/${requireTaskId(req)}`, method: 'GET', query })); }; @@ -766,6 +794,7 @@ export const _test = { formFromFields, guardHttpStatus, normalizeBaseUrl, + normalizeRequest, pickCredential, requestBinary, requestJson, diff --git a/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js b/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js index d3aff4c3..a9932800 100644 --- a/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js +++ b/services/nsfocus__rsas_v6-0r04f04sp09/test/nsfocus-rsas-v6-0r04f04sp09.test.js @@ -178,22 +178,51 @@ test('GetTaskStatus, ListTasks, GetTaskResult round-trip', async () => { } }); -test('ListTasks omits type/page/page_size when they are proto3 default 0', async () => { +test('ListTasks omits type but defaults page/page_size on proto3 default 0', async () => { const mock = await createMockServer(); try { // Simulates a decoded proto3 request where unset int64 fields arrive as 0. await call('ListTasks', { type: 0, page: 0, page_size: 0 }, { bindings: { host: mock.url } }); const req = mock.requests.find((r) => r.pathname === '/api/task/list'); assert.equal(req.query.type, undefined); - assert.equal(req.query.page, undefined); - assert.equal(req.query.page_size, undefined); - // format/curr_lang auth params are still present. + // 1-based pagination defaults, never a proto3 0. + assert.equal(req.query.page, '1'); + assert.equal(req.query.page_size, '10'); assert.equal(req.query.format, 'json'); } finally { await mock.close(); } }); +test('normalizeRequest mirrors camelCase runtime keys to snake_case, recursively', () => { + const out = _test.normalizeRequest({ + taskId: '60', + templateUuid: 'u-1', + jumparray: [{ ipRange: '10.0.0.2', userName: 'root', userPwd: 'x', port: 22 }], + }); + assert.equal(out.task_id, '60'); + assert.equal(out.template_uuid, 'u-1'); + assert.equal(out.jumparray[0].ip_range, '10.0.0.2'); + assert.equal(out.jumparray[0].user_name, 'root'); + assert.equal(out.jumparray[0].user_pwd, 'x'); + // pre-existing snake_case keys are preserved and not clobbered + const keep = _test.normalizeRequest({ task_id: 'a', taskId: 'b' }); + assert.equal(keep.task_id, 'a'); +}); + +test('handlers resolve camelCase request keys end-to-end (real-runtime shape)', async () => { + const mock = await createMockServer(); + try { + // The CLI runtime delivers task_id as taskId; the handler must still work. + const out = await call('GetTaskStatus', { taskId: '60' }, { bindings: { host: mock.url } }); + assert.equal(out.ret_code, 0); + const req = mock.requests.find((r) => r.pathname.startsWith('/api/task/status/')); + assert.equal(req.pathname, '/api/task/status/60'); + } finally { + await mock.close(); + } +}); + test('CreatePwdTask omits port when it is proto3 default 0', async () => { const mock = await createMockServer(); try { From e353ecee5e9651f025fccc926ba450843cc78fa4 Mon Sep 17 00:00:00 2001 From: hilariacrucita <3362488788@qq.com> Date: Thu, 9 Jul 2026 17:53:29 +0800 Subject: [PATCH 4/5] chore: restore executable bit on octobus-tentacles dispatcher The root dispatcher bin lost its executable bit on main, which the package validator flags. Restore mode 100755 so the validate job passes for this entry. --- services/bin/octobus-tentacles.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 services/bin/octobus-tentacles.js diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js old mode 100644 new mode 100755 From 4388c16ea9e3fe372867f271a0ecdf48467d911a Mon Sep 17 00:00:00 2001 From: hilariacrucita <3362488788@qq.com> Date: Thu, 9 Jul 2026 18:05:51 +0800 Subject: [PATCH 5/5] chore: restore executable bit on m01-intelligence bin entries The m01 dispatcher wrapper and package entry lost their executable bit on main, which the package validator flags. Restore mode 100755 (no content change) so the validate job passes. --- services/bin/m01-intelligence.js | 0 services/m01__intelligence/bin/m01-intelligence.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 services/bin/m01-intelligence.js mode change 100644 => 100755 services/m01__intelligence/bin/m01-intelligence.js diff --git a/services/bin/m01-intelligence.js b/services/bin/m01-intelligence.js old mode 100644 new mode 100755 diff --git a/services/m01__intelligence/bin/m01-intelligence.js b/services/m01__intelligence/bin/m01-intelligence.js old mode 100644 new mode 100755