Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and

[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)

## [1.23.0] - 2026-09-09

### Added

- **Agent security commands** — `bl security overview` (protection overview for the last 24 hours) and `bl security alerts` (alert list with risk-level, asset-type, status, vendor, pagination and sorting filters). Both call the per-workspace AgentStudio host and honor the shared `text` / `json` / `--quiet` / `--dry-run` contract. The host is derived from `--workspace-id`, or overridden by `--base-url` / `DASHSCOPE_BASE_URL` / `auth login --base-url` pointed at a workspace or pre-release origin (e.g. `https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`).

### Internal

- Add Agent security E2E coverage (help, missing-workspace usage error, dry-run host derivation and `--base-url` override, query-string filters, enum fast-fail) and generate the `bailian-cli` skill reference for the new `security` group.

## [1.22.0] - 2026-09-08

### Changed
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)

## [1.23.0] - 2026-09-09

### 新增

- **Agent 安全命令** —— `bl security overview`(最近 24 小时的防护总览)与 `bl security alerts`(告警列表,支持风险等级、资产类型、状态、厂商、分页与排序等筛选)。两者均对接按 workspace 区分的 AgentStudio 域名,遵循统一的 `text` / `json` / `--quiet` / `--dry-run` 约定。域名默认由 `--workspace-id` 推导,也可通过 `--base-url` / `DASHSCOPE_BASE_URL` / `auth login --base-url` 指向某个 workspace 或预发源覆盖(例如 `https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`)。

### 内部

- 补充 Agent 安全 E2E 覆盖(help、缺 workspace 的 usage 错误、dry-run 域名推导与 `--base-url` 覆盖、query string 筛选、枚举快失败),并为新的 `security` 组生成 `bailian-cli` 技能 reference。

## [1.22.0] - 2026-09-08

### 变更
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.22.0",
"version": "1.23.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import {
speechRecognize,
fileUpload,
consoleCall,
securityOverview,
securityAlerts,
usageFree,
usageFreetier,
usageStats,
Expand Down Expand Up @@ -288,6 +290,8 @@ export const commands: Record<string, AnyCommand> = {
"speech recognize": speechRecognize,
"file upload": fileUpload,
"console call": consoleCall,
"security overview": securityOverview,
"security alerts": securityAlerts,
"usage free": usageFree,
"usage freetier": usageFreetier,
"usage stats": usageStats,
Expand Down
2 changes: 1 addition & 1 deletion packages/commands/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.22.0",
"version": "1.23.0",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
Expand Down
201 changes: 201 additions & 0 deletions packages/commands/src/commands/security/alerts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import {
defineCommand,
detectOutputFormat,
securityAgentLogsEndpoint,
securityGet,
type FlagsDef,
type SecurityAlertList,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { WORKSPACE_FLAG, renderAlert, resolveSecurityHost, setSecurityParam } from "./shared.ts";

const ASSET_TYPES = ["agent", "tool", "skill", "knowledge_base", "memory", "channel"] as const;

const ALERTS_FLAGS = {
...WORKSPACE_FLAG,
page: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Page number (default: 1)", "zh-CN": "页码(默认:1)" },
},
pageSize: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Results per page (default: 20)", "zh-CN": "每页结果数(默认:20)" },
},
riskLevel: {
type: "string",
valueHint: "<level>",
choices: ["high", "medium", "low"] as const,
description: {
"en-US": "Filter by risk level: high, medium, low",
"zh-CN": "按风险等级筛选:high、medium、low",
},
},
riskName: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Filter by risk name", "zh-CN": "按风险名称筛选" },
},
status: {
type: "string",
valueHint: "<status>",
description: { "en-US": "Filter by handling status", "zh-CN": "按处理状态筛选" },
},
statusList: {
type: "array",
valueHint: "<status>",
description: {
"en-US": "Filter by multiple statuses (repeatable)",
"zh-CN": "按多个状态筛选(可重复传入)",
},
},
appName: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Filter by application name", "zh-CN": "按应用名称筛选" },
},
assetType: {
type: "string",
valueHint: "<type>",
choices: ASSET_TYPES,
description: {
"en-US": `Filter by asset type: ${ASSET_TYPES.join(", ")}`,
"zh-CN": `按资产类型筛选:${ASSET_TYPES.join("、")}`,
},
},
vendor: {
type: "string",
valueHint: "<vendor>",
description: { "en-US": "Filter by vendor", "zh-CN": "按厂商筛选" },
},
orderBy: {
type: "string",
valueHint: "<field>",
description: {
"en-US": "Sort field (default: check_time)",
"zh-CN": "排序字段(默认:check_time)",
},
},
order: {
type: "string",
valueHint: "<dir>",
choices: ["asc", "desc"] as const,
description: {
"en-US": "Sort direction: asc, desc (default: desc)",
"zh-CN": "排序方向:asc、desc(默认:desc)",
},
},
lang: {
type: "string",
valueHint: "<lang>",
choices: ["zh", "en"] as const,
description: { "en-US": "Response language: zh, en", "zh-CN": "响应语言:zh、en" },
},
} satisfies FlagsDef;

export default defineCommand({
description: {
"en-US": "List Agent security alerts",
"zh-CN": "列出 Agent 安全告警",
},
auth: "apiKey",
usageArgs: "[flags]",
flags: ALERTS_FLAGS,
notes: [
{
"en-US": "Auth: uses DashScope API Key (Bearer token).",
"zh-CN": "鉴权:使用 DashScope API Key(Bearer Token)。",
},
{
"en-US": "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id.",
"zh-CN": "`--workspace-id` 可通过 BAILIAN_WORKSPACE_ID 环境变量或配置项 workspace_id 设置。",
},
{
"en-US":
"Filters, pagination and sorting go in the query string; enum flags are validated before any request is sent.",
"zh-CN": "筛选、分页与排序参数走 query string;枚举类 flag 在发起请求前校验。",
},
{
"en-US":
"AgentStudio host: derived from --workspace-id by default; point --base-url / DASHSCOPE_BASE_URL (or `auth login --base-url`) at a workspace or pre-release origin such as https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio to override it, and --workspace-id is then not required.",
"zh-CN":
"AgentStudio 域名:默认由 --workspace-id 推导;将 --base-url / DASHSCOPE_BASE_URL(或 `auth login --base-url`)指向某个 workspace 或预发源(例如 https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio)即可覆盖,此时无需 --workspace-id。",
},
],
exampleArgs: [
{ "en-US": "--workspace-id ws-xxx", "zh-CN": "--workspace-id ws-xxx" },
{
"en-US": "--risk-level high --page-size 50",
"zh-CN": "--risk-level high --page-size 50",
},
{
"en-US": '--asset-type agent --app-name "demo app" --output json',
"zh-CN": '--asset-type agent --app-name "测试应用0" --output json',
},
{
"en-US": "--status-list unhandled --status-list handling",
"zh-CN": "--status-list unhandled --status-list handling",
},
],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const host = resolveSecurityHost(ctx);

const params = new URLSearchParams();
setSecurityParam(params, "current_page", flags.page);
setSecurityParam(params, "page_size", flags.pageSize);
setSecurityParam(params, "risk_level", flags.riskLevel);
setSecurityParam(params, "risk_name", flags.riskName);
setSecurityParam(params, "status", flags.status);
setSecurityParam(params, "status_list", flags.statusList);
setSecurityParam(params, "app_name", flags.appName);
setSecurityParam(params, "asset_type", flags.assetType);
setSecurityParam(params, "vendor", flags.vendor);
setSecurityParam(params, "order_by", flags.orderBy);
setSecurityParam(params, "order", flags.order);
setSecurityParam(params, "lang", flags.lang);

const query = params.toString();
const base = securityAgentLogsEndpoint(host);
const endpoint = query ? `${base}?${query}` : base;

if (settings.dryRun) {
emitResult({ endpoint, method: "GET" }, format);
return;
}

const data = await securityGet<SecurityAlertList>(ctx.client, endpoint);
const alerts = data?.data ?? [];

if (format === "json") {
emitResult(data ?? { stats: null, data: [], next_page: null }, format);
return;
}

if (settings.quiet) {
for (const alert of alerts) emitBare(alert.alert_id);
return;
}

const stats = data?.stats;
if (stats) {
emitBare(
`Total: ${stats.total ?? "-"} high: ${stats.high ?? "-"} ` +
`medium: ${stats.medium ?? "-"} low: ${stats.low ?? "-"}\n`,
);
}

if (alerts.length === 0) {
emitBare("No alerts found.");
return;
}

for (const alert of alerts) renderAlert(alert);

if (data?.next_page) {
emitBare(`Next page cursor: ${data.next_page}`);
}
},
});
107 changes: 107 additions & 0 deletions packages/commands/src/commands/security/overview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
defineCommand,
detectOutputFormat,
securityGet,
securityOverviewEndpoint,
type FlagsDef,
type SecurityOverview,
type SecurityScanStat,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import {
CAPABILITY_LABELS,
PROTECTION_LABELS,
SCAN_CARDS,
WORKSPACE_FLAG,
renderToggles,
resolveSecurityHost,
} from "./shared.ts";

const OVERVIEW_FLAGS = {
...WORKSPACE_FLAG,
} satisfies FlagsDef;

export default defineCommand({
description: {
"en-US": "Show the Agent security protection overview (last 24 hours)",
"zh-CN": "查看 Agent 安全防护总览(最近 24 小时)",
},
auth: "apiKey",
usageArgs: "[flags]",
flags: OVERVIEW_FLAGS,
notes: [
{
"en-US": "Auth: uses DashScope API Key (Bearer token).",
"zh-CN": "鉴权:使用 DashScope API Key(Bearer Token)。",
},
{
"en-US": "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id.",
"zh-CN": "`--workspace-id` 可通过 BAILIAN_WORKSPACE_ID 环境变量或配置项 workspace_id 设置。",
},
{
"en-US":
"Fixed to the last 24 hours; the AgentStudio region is always cn-beijing and is not configurable.",
"zh-CN": "固定统计最近 24 小时;AgentStudio 地域固定为 cn-beijing,不可配置。",
},
{
"en-US":
"AgentStudio host: derived from --workspace-id by default; point --base-url / DASHSCOPE_BASE_URL (or `auth login --base-url`) at a workspace or pre-release origin such as https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio to override it, and --workspace-id is then not required.",
"zh-CN":
"AgentStudio 域名:默认由 --workspace-id 推导;将 --base-url / DASHSCOPE_BASE_URL(或 `auth login --base-url`)指向某个 workspace 或预发源(例如 https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio)即可覆盖,此时无需 --workspace-id。",
},
],
exampleArgs: [
{ "en-US": "--workspace-id ws-xxx", "zh-CN": "--workspace-id ws-xxx" },
{
"en-US": "--workspace-id ws-xxx --output json",
"zh-CN": "--workspace-id ws-xxx --output json",
},
],
async run(ctx) {
const { settings } = ctx;
const format = detectOutputFormat(settings.output);
const endpoint = securityOverviewEndpoint(resolveSecurityHost(ctx));

if (settings.dryRun) {
emitResult({ endpoint, method: "GET" }, format);
return;
}

const data = await securityGet<SecurityOverview>(ctx.client, endpoint);
if (!data) {
if (format === "json") emitResult({}, format);
else emitBare("Overview unavailable.");
return;
}

if (format === "json") {
emitResult(data, format);
return;
}

// Banner totals are client-side sums across the detection cards. Each card
// accepts the snake_case (REST) or camelCase (console-gateway) field.
const cards = SCAN_CARDS.map(({ label, keys }) => {
const stat = keys
.map((key) => data[key] as SecurityScanStat | null | undefined)
.find((value) => value !== undefined);
return { label, stat: stat ?? null };
});
const sum = (pick: (stat: SecurityScanStat) => number | null): number =>
cards.reduce((total, card) => total + (card.stat ? (pick(card.stat) ?? 0) : 0), 0);

emitBare(`Scanned: ${sum((stat) => stat.scanned)} Risks: ${sum((stat) => stat.hit)}`);

renderToggles("Capabilities", data.capabilities, CAPABILITY_LABELS);
renderToggles("Protection", data.protection, PROTECTION_LABELS);

emitBare("\nDetections");
for (const { label, stat } of cards) {
if (!stat) {
emitBare(` ${label} (unavailable)`);
} else {
emitBare(` ${label} hit ${stat.hit ?? "-"} / scanned ${stat.scanned ?? "-"}`);
}
}
},
});
Loading