e2f433e849
Every CLI option that can carry a secret gets a `--<name>-file <path|->`
sibling ('-' reads stdin), so the value stays out of the process list and
shell history: --password-file (global), --primary-config-file /
--backup-config-file (data-connection create|update), --auth-config-file
(external-system create|update), --connection-string-file (db-connection
create|update), --credentials-file (notification smtp create|update) and
--auth-token-file (notification sms create|update). Follows the existing
--config-file convention; `@path` is avoided because System.CommandLine
expands @-tokens as response files.
The value is read at parse time; a missing/unreadable/empty source, or '-'
on a terminal stdin, is a parse error naming option + path only, never the
content. Inline and file forms are mutually exclusive, at most one option
may read stdin, and one trailing line break is stripped. Inline forms keep
working.
docker/seed-sites.sh and docker-env2/seed-sites.sh now write the MxGateway
config (windev gateway key) to a mode-600 temp file removed by an EXIT trap
and pass --primary-config-file; seed-flow-outbound-demo.sh pipes its rig
connection string via --connection-string-file -. CLI README (new "Secrets
from a file or stdin" section + per-command tables), Component-CLI.md and
both docker READMEs updated.
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
614 lines
41 KiB
Markdown
614 lines
41 KiB
Markdown
# Component: CLI
|
|
|
|
## Purpose
|
|
|
|
The CLI is a standalone command-line tool for scripting and automating administrative operations against the ScadaBridge central cluster. It connects to the Central Host's HTTP Management API (`POST /management`), which dispatches commands to the ManagementActor. Authentication and role resolution are handled server-side — the CLI exchanges its credentials once per process for a short-lived bearer token (`POST /auth/token`), falling back to HTTP Basic Auth on servers without bearer support. The CLI provides the same administrative capabilities as the Central UI, enabling automation, batch operations, and integration with CI/CD pipelines.
|
|
|
|
## Location
|
|
|
|
Standalone executable, not part of the Host binary. Deployed on any machine with HTTP access to a central node.
|
|
|
|
`src/ZB.MOM.WW.ScadaBridge.CLI/`
|
|
|
|
## Responsibilities
|
|
|
|
- Parse command-line arguments and dispatch to the appropriate management operation.
|
|
- Send HTTP requests to the Central Host's Management API endpoint with a bearer token (or Basic Auth credentials on older servers).
|
|
- Display structured responses from the Management API.
|
|
- Support both JSON and human-readable table output formats.
|
|
|
|
## Technology
|
|
|
|
- **Argument parsing**: `System.CommandLine` library for command/subcommand/option parsing with built-in help generation.
|
|
- **Transport**: HTTP client connecting to the Central Host's `POST /management` endpoint. Authentication is via a bearer JWT obtained from `POST /auth/token` (or HTTP Basic Auth) — the server performs the LDAP bind and role resolution.
|
|
- **Serialization**: Commands serialized as JSON with a type discriminator (`command` field). Message contracts from Commons define the command types.
|
|
|
|
## Authentication
|
|
|
|
The CLI authenticates with a bearer token exchanged once per process (review 2026-10-21 05/U1, 05/P1):
|
|
|
|
1. The user provides credentials via `--username` / `--password` or `--password-file <path|->` (or `SCADABRIDGE_USERNAME` / `SCADABRIDGE_PASSWORD`).
|
|
2. On the first management call of the process, the CLI posts the credentials to `POST /auth/token`. The server performs the LDAP bind, group lookup and role resolution, and returns a short-lived HS256 JWT carrying the username, roles and permitted sites (lifetime `JwtExpiryMinutes`, default 15 min). That is the invocation's **only** LDAP bind.
|
|
3. Every later call in the process — `POST /management`, the audit REST endpoints, and the `debug stream` hub (including its automatic reconnects) — sends `Authorization: Bearer <token>`; the server validates it locally with no LDAP bind. A token near expiry is re-exchanged.
|
|
4. A bearer call answered `401` re-exchanges once (unless the token was minted for that very call) and, if still refused, retries with HTTP Basic and stays on Basic for the rest of the process.
|
|
5. **Basic fallback.** A server with no `/auth/token` (404/405), a login-disabled deployment (404), or an older server whose token response lacks `management_bearer: true` (its tokens are accepted nowhere) is remembered as Basic-only for the process: each call carries a Basic header and the server binds LDAP per request. A transient exchange error (5xx) uses Basic for that call only. Bad credentials (`401`) or a throttle lockout (`429`) from the exchange are reported as-is and are **not** retried with Basic (that would only add a second failed bind against the login throttle).
|
|
6. The token lives **in memory only** — never written to disk, logged or echoed — and dies with the process; each CLI invocation needs credentials. `SCADABRIDGE_AUTH_MODE=basic` disables the exchange (Basic on every call); `SCADABRIDGE_VERBOSE=1` prints a stderr note when a freshly issued token is refused and the CLI falls back to Basic. The client never follows HTTP redirects, so a 307/308 cannot replay the credential form elsewhere.
|
|
7. The CLI never talks to LDAP directly.
|
|
|
|
## Connection
|
|
|
|
The CLI connects to the Central Host via HTTP:
|
|
|
|
- **Management URL**: The URL of a central node's web server (e.g., `http://localhost:9001`). The management API is served at `POST /management` on the same host as the Central UI.
|
|
- **Failover**: For HA, use a load balancer URL in front of both central nodes. The management API is stateless (a self-contained bearer token, or Basic Auth per request), and both central nodes share the JWT signing key, so any central node can handle any request without sticky sessions.
|
|
- **No Akka.NET dependency**: The CLI is a pure HTTP client with no Akka.NET runtime.
|
|
|
|
## Command Structure
|
|
|
|
The CLI uses a hierarchical subcommand structure mirroring the Management Service message groups:
|
|
|
|
```
|
|
scadabridge <group> <action> [options]
|
|
```
|
|
|
|
Commands identify targets by integer ID, site identifier, connection name, or
|
|
canonical member name as shown in their help. Most edits use individual flags;
|
|
CSV imports use `--file`, shared schemas use `--schema-file`, and bundles use
|
|
`--input`/`--output`. The authoritative, always-current reference is the in-repo
|
|
`src/ZB.MOM.WW.ScadaBridge.CLI/README.md`; the command lists below mirror the implemented command
|
|
tree at the time of writing.
|
|
|
|
### Template Commands
|
|
```
|
|
scadabridge template list [--detail] [--skip <n>] [--take <n>]
|
|
scadabridge template get --id <id>
|
|
scadabridge template resolved-members --id <id>
|
|
scadabridge template move --id <id> [--folder-id <id>]
|
|
scadabridge template create --name <name> [--description <desc>] [--parent-id <id>]
|
|
scadabridge template update --id <id> [--name <name>] [--description <desc>] [--parent-id <id>]
|
|
scadabridge template validate --id <id>
|
|
scadabridge template delete --id <id>
|
|
scadabridge template attribute add --template-id <id> --name <name> --data-type <type> [--value <value>] [--description <desc>] [--data-source <ref>] [--locked]
|
|
scadabridge template attribute update --id <id> [--name <name>] [--data-type <type>] [--value <value>] [--description <desc>] [--data-source <ref>] [--locked]
|
|
scadabridge template attribute delete --id <id>
|
|
scadabridge template alarm add --template-id <id> --name <name> --trigger-type <type> --priority <n> [--description <desc>] [--trigger-config <json>] [--locked] [--trigger-kind advisory|strict] [--mes-reportable]
|
|
scadabridge template alarm update --id <id> --name <name> --trigger-type <type> --priority <n> [--description <desc>] [--trigger-config <json>] [--locked] [--trigger-kind advisory|strict] [--mes-reportable]
|
|
scadabridge template alarm delete --id <id>
|
|
scadabridge template script add --template-id <id> --name <name> --code <code> --trigger-type <type> [--trigger-config <json>] [--locked] [--parameters <json>] [--return-def <json>] [--trigger-kind advisory|strict]
|
|
scadabridge template script update --id <id> --name <name> --code <code> --trigger-type <type> [--trigger-config <json>] [--locked] [--parameters <json>] [--return-def <json>] [--trigger-kind advisory|strict]
|
|
scadabridge template script delete --id <id>
|
|
scadabridge template composition add --template-id <id> --instance-name <name> --composed-template-id <id>
|
|
scadabridge template composition delete --id <id>
|
|
scadabridge template native-alarm-source add --template-id <id> --name <name> --connection <name> --source-ref <ref> [--filter <expr>] [--description <desc>] [--locked]
|
|
scadabridge template native-alarm-source list --template-id <id>
|
|
scadabridge template native-alarm-source update --id <id> --name <name> --connection <name> --source-ref <ref> [--filter <expr>] [--description <desc>] [--locked]
|
|
scadabridge template native-alarm-source remove --id <id>
|
|
```
|
|
|
|
### Instance Commands
|
|
```
|
|
scadabridge instance list [--site-id <id>] [--template-id <id>] [--search <term>] [--skip <n>] [--take <n>]
|
|
scadabridge instance get --id <id>
|
|
scadabridge instance create --name <name> --template-id <id> --site-id <id> [--area-id <id>]
|
|
scadabridge instance set-bindings --id <id> --bindings <json>
|
|
scadabridge instance bind-module --id <id> --module <name> --connection <id> --object <path> [--dry-run]
|
|
scadabridge instance set-overrides --id <id> --overrides <json>
|
|
scadabridge instance attribute-override delete --instance-id <id> --attribute <canonical-name>
|
|
scadabridge instance native-alarm-source list --instance-id <id>
|
|
scadabridge instance import-overrides --id <id> --file <path.csv>
|
|
scadabridge instance alarm-override set --instance-id <id> --alarm <name> [--trigger-config <json>] [--priority <n>] [--mes-reportable true|false]
|
|
scadabridge instance alarm-override delete --instance-id <id> --alarm <name>
|
|
scadabridge instance alarm-override list --instance-id <id>
|
|
scadabridge instance native-alarm-source set --instance-id <id> --source <name> [--connection <name>] [--source-ref <ref>] [--filter <expr>]
|
|
scadabridge instance native-alarm-source clear --instance-id <id> --source <name>
|
|
scadabridge instance set-area --id <id> [--area-id <id>]
|
|
scadabridge instance diff --id <id>
|
|
scadabridge instance deploy --id <id>
|
|
scadabridge instance enable --id <id>
|
|
scadabridge instance disable --id <id>
|
|
scadabridge instance delete --id <id>
|
|
```
|
|
|
|
`--bindings` is a JSON array of `[attributeName, dataConnectionId]` pairs, e.g.
|
|
`[["Speed", 5], ["Mode", 7]]`. `--overrides` is a JSON object of attribute name to
|
|
value, e.g. `{"Speed": "100", "Mode": null}`.
|
|
|
|
`instance alarm-override set` replaces the **whole** override row — an omitted flag is not
|
|
"leave as is", it clears that field back to inherited. `instance alarm-override set
|
|
--instance-id 6 --alarm X --mes-reportable true` wipes any existing `--priority` /
|
|
`--trigger-config` override on alarm `X`; pass every override to keep on the same command
|
|
(`instance alarm-override list` shows the current values).
|
|
|
|
### Native Alarm Source Commands
|
|
|
|
The `native-alarm-source` subcommands manage the **read-only native alarm mirror** —
|
|
alarms surfaced from an alarm-capable data connection rather than evaluated by the
|
|
ScadaBridge alarm engine. Native alarm sources are declared on a template and may be
|
|
overridden per instance. The subcommands map to management commands that resolve via
|
|
`ManagementCommandRegistry`:
|
|
|
|
- `--connection` names an alarm-capable data connection (**OPC UA** or **MxGateway**).
|
|
- `--source-ref` is the connection-specific reference: an **OPC UA `SourceNode` nodeId**
|
|
or an **MxAccess object/area**.
|
|
- `--filter` is an optional connection-specific filter expression that narrows the
|
|
mirrored alarm set.
|
|
|
|
**Template-level** (defines the inherited native alarm sources):
|
|
|
|
| CLI command | Management command | Required role |
|
|
|-------------|--------------------|---------------|
|
|
| `template native-alarm-source add` | `AddTemplateNativeAlarmSourceCommand` | Design |
|
|
| `template native-alarm-source list` | `ListTemplateNativeAlarmSourcesCommand` | — |
|
|
| `template native-alarm-source remove` | `DeleteTemplateNativeAlarmSourceCommand` | Design |
|
|
|
|
`add` takes `--name`, `--connection`, and `--source-ref` (required), plus optional
|
|
`--filter`, `--description`, and `--locked` (a flag that prevents instance-level
|
|
override). `remove` targets a single native alarm source by its own `--id`.
|
|
|
|
**Instance-level** (per-instance overrides of an inherited source; upsert semantics):
|
|
|
|
| CLI command | Management command | Required role |
|
|
|-------------|--------------------|---------------|
|
|
| `instance native-alarm-source set` | `SetInstanceNativeAlarmSourceOverrideCommand` | Deployment |
|
|
| `instance native-alarm-source clear` | `DeleteInstanceNativeAlarmSourceOverrideCommand` | Deployment |
|
|
|
|
`set` is an **upsert** keyed by `--instance-id` and `--source` (the inherited source
|
|
name): a blank/omitted `--connection`, `--source-ref`, or `--filter` keeps the
|
|
inherited value, so only the supplied options are overridden. `clear` removes the
|
|
override and **reverts the instance to the inherited template value**.
|
|
|
|
### Site Commands
|
|
```
|
|
scadabridge site list
|
|
scadabridge site get --id <id>
|
|
scadabridge site create --identifier <id> --name <name> [--description <desc>] [--node-a-address <addr>] [--node-b-address <addr>] [--grpc-node-a-address <addr>] [--grpc-node-b-address <addr>]
|
|
scadabridge site update --id <id> [--name <name>] [--description <desc>] [--node-a-address <addr>] [--node-b-address <addr>] [--grpc-node-a-address <addr>] [--grpc-node-b-address <addr>]
|
|
scadabridge site delete --id <id>
|
|
scadabridge site area list --site-id <id>
|
|
scadabridge site area create --site-id <id> --name <name> [--parent-id <id>]
|
|
scadabridge site area update --id <id> --name <name>
|
|
scadabridge site area delete --id <id>
|
|
scadabridge site deploy-artifacts [--site-id <id>]
|
|
```
|
|
|
|
### Deployment Commands
|
|
```
|
|
scadabridge deploy instance --id <id>
|
|
scadabridge deploy artifacts [--site-id <id>]
|
|
scadabridge deploy status [--instance-id <id>] [--status <status>] [--page <n>] [--page-size <n>]
|
|
```
|
|
|
|
`deploy artifacts` (and its alias `site deploy-artifacts`): `--site-id` is now honored — supplying it deploys system-wide artifacts to exactly that site; omitting it deploys fleet-wide to every site. A site-scoped (non-Administrator) Deployer must supply an in-scope `--site-id`; fleet-wide deployment (no `--site-id`) requires a system-wide Deployer or Administrator (arch-review C2).
|
|
|
|
### Data Connection Commands
|
|
```
|
|
scadabridge data-connection list [--site-id <id>]
|
|
scadabridge data-connection get --id <id>
|
|
scadabridge data-connection create --site-id <id> --name <name> --protocol <protocol> [--primary-config <json> | --primary-config-file <path|->] [--backup-config <json> | --backup-config-file <path|->] [--failover-retry-count <n>]
|
|
scadabridge data-connection update --id <id> --name <name> --protocol <protocol> [--primary-config <json> | --primary-config-file <path|->] [--backup-config <json> | --backup-config-file <path|->] [--failover-retry-count <n>]
|
|
scadabridge data-connection delete --id <id>
|
|
scadabridge data-connection move --id <id> --site-id <destination-site-id>
|
|
```
|
|
|
|
#### Site-routed OPC UA operations (browse / search / verify-endpoint / certs)
|
|
|
|
CLI parity for the site-routed data-connection operations the ManagementActor dispatches
|
|
(arch-review C4). Each carries `--site` so the management API routes it to the target
|
|
site cluster. `browse`/`search`/`verify-endpoint` require the **Designer** role; `certs`
|
|
require **Admin**. `verify-endpoint` reads `ConfigJson` verbatim from `--config-file`;
|
|
`certs trust` base64-encodes `--der-file`.
|
|
|
|
```
|
|
scadabridge data-connection browse --site <identifier> --connection <name> [--node-id <nodeId>]
|
|
scadabridge data-connection search --site <identifier> --connection <name> --query <text> [--max-depth <n>] [--max-results <n>]
|
|
scadabridge data-connection verify-endpoint --site <identifier> --protocol <protocol> --config-file <path> [--connection <name>]
|
|
scadabridge data-connection certs list --site <identifier> [--connection <name>]
|
|
scadabridge data-connection certs trust --site <identifier> --connection <name> --der-file <path> --thumbprint <hex>
|
|
scadabridge data-connection certs remove --site <identifier> --thumbprint <hex> [--connection <name>]
|
|
```
|
|
|
|
### External System Commands
|
|
```
|
|
scadabridge external-system list
|
|
scadabridge external-system get --id <id>
|
|
scadabridge external-system create --name <name> --endpoint-url <url> --auth-type <type> [--auth-config <json> | --auth-config-file <path|->] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
|
scadabridge external-system update --id <id> [--name <name>] [--endpoint-url <url>] [--auth-type <type>] [--auth-config <json> | --auth-config-file <path|->] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
|
scadabridge external-system delete --id <id>
|
|
scadabridge external-system method list --external-system-id <id>
|
|
scadabridge external-system method get --id <id>
|
|
scadabridge external-system method create --external-system-id <id> --name <name> --http-method <verb> --path <path> [--params <json>] [--return <json>]
|
|
scadabridge external-system method update --id <id> [--name <name>] [--http-method <verb>] [--path <path>] [--params <json>] [--return <json>]
|
|
scadabridge external-system method delete --id <id>
|
|
```
|
|
|
|
### Notification Commands
|
|
```
|
|
scadabridge notification list
|
|
scadabridge notification get --id <id>
|
|
scadabridge notification create --name <name> --emails <comma-separated>
|
|
scadabridge notification update --id <id> --name <name> --emails <comma-separated>
|
|
scadabridge notification create --name <name> --type sms --phones <comma-separated>
|
|
scadabridge notification update --id <id> --name <name> --type sms --phones <comma-separated>
|
|
scadabridge notification rename --id <id> --name <name>
|
|
scadabridge notification recipient add --list-id <id> --name <name> [--email <email>] [--phone <E.164-number>]
|
|
scadabridge notification recipient delete --id <recipient-id>
|
|
scadabridge notification delete --id <id>
|
|
scadabridge notification smtp list
|
|
scadabridge notification smtp update --id <id> --server <host> --port <n> --auth-mode <mode> --from-address <email> [--tls-mode <mode>] [--credentials <value> | --credentials-file <path|->]
|
|
```
|
|
|
|
Notification rename and individual recipient edits preserve unrelated IDs and
|
|
names. Whole-list update replaces recipients. Recipient add requires at least
|
|
one contact; the server validates it against the owning list channel.
|
|
|
|
```sh
|
|
scadabridge notification smtp create --server <host> --port <n> --auth-mode <mode> --from-address <email> [--tls-mode <mode>] [--credentials <value> | --credentials-file <path|->] [--oauth2-authority <url>] [--oauth2-scope <scope>] [--transport <Smtp|Ews>]
|
|
scadabridge notification sms list
|
|
scadabridge notification sms create --account-sid <sid> (--auth-token <token> | --auth-token-file <path|->) [--from-number <number>] [--messaging-service-sid <sid>] [--api-base-url <url>] [--connection-timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
|
scadabridge notification sms update --id <id> --account-sid <sid> [--from-number <number>] [--messaging-service-sid <sid>] [--api-base-url <url>] [--auth-token <token> | --auth-token-file <path|->] [--connection-timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
|
```
|
|
|
|
SMS requires at least one sender: a From number or a Messaging Service SID.
|
|
Omitted timeout/retry settings preserve update values; creation uses defaults.
|
|
Omitted Auth Token preserves the stored token on update; omitted Messaging
|
|
Service SID clears it. Provider create/update require Administrator access.
|
|
|
|
### Security Commands
|
|
```
|
|
scadabridge security api-key list
|
|
scadabridge security api-key create --name <name> --methods <comma-separated-method-names>
|
|
scadabridge security api-key update --key-id <id> --enabled <bool>
|
|
scadabridge security api-key set-methods --key-id <id> --methods <comma-separated-method-names>
|
|
scadabridge security api-key delete --key-id <id>
|
|
scadabridge security role-mapping list
|
|
scadabridge security role-mapping create --ldap-group <group> --role <role>
|
|
scadabridge security role-mapping update --id <id> [--ldap-group <group>] [--role <role>]
|
|
scadabridge security role-mapping delete --id <id>
|
|
scadabridge security scope-rule list --mapping-id <id>
|
|
scadabridge security scope-rule add --mapping-id <id> --site-id <id>
|
|
scadabridge security scope-rule delete --id <id>
|
|
```
|
|
|
|
### Configuration-Change Audit Commands
|
|
|
|
The `audit-config` group queries the configuration-change audit trail (the `IAuditService`
|
|
record of admin edits). `audit-log` is a deprecated alias that still works but emits a
|
|
warning; migrate scripts to `audit-config`.
|
|
|
|
```
|
|
scadabridge audit-config query [--user <username>] [--entity-type <type>] [--action <action>] [--from <date>] [--to <date>] [--page <n>] [--page-size <n>] [--detail]
|
|
```
|
|
|
|
The response is an `AuditLogQueryResult` — `{ "entries": [ … ], "totalCount": <n> }`, where
|
|
`totalCount` is the unpaged match count. The result type is a named record specifically
|
|
because the handler must not return the repository's `(Entries, TotalCount)` ValueTuple:
|
|
`System.Text.Json` serializes properties only, so a tuple result renders as the literal
|
|
`{}` and every query looks empty regardless of how many rows matched. `ManagementActor.SerializeResult`
|
|
now rejects tuple results outright so the failure mode cannot recur silently.
|
|
|
|
JSON is the primary format here and is never altered by the table path. `--format table`
|
|
applies a compact projection (`AuditConfigTableProjection`) that unwraps the envelope into
|
|
one row per entry — `id`, `timestamp`, `user`, `action`, `entityType`, `entityName`,
|
|
`entityId` — and drops `afterStateJson`, the full post-change entity snapshot, since a
|
|
single one fills the terminal. Without a projector the renderer took its object branch and
|
|
printed the whole `entries` array as raw JSON inside one `Property`/`Value` cell (register
|
|
row 57). `totalCount` survives the unwrap as a trailing `note` row, emitted only when the
|
|
page is a strict subset of the matches. `--detail` skips the projection and renders the
|
|
raw payload, mirroring `template list`/`template get`.
|
|
|
|
The new centralized Audit Log component (#23) is exposed via the `scadabridge audit` group below.
|
|
|
|
### Centralized Audit Commands
|
|
|
|
The `scadabridge audit` group targets the centralized Audit Log component (#23) and
|
|
exposes the UI-equivalent operational audit surface. Permissions follow the same
|
|
read-vs-export split the Central UI uses (see Component-AuditLog.md, Security &
|
|
Tamper-Evidence, and Security & Auth #10): `audit query`, `audit tree`, and
|
|
`audit verify-chain`, `audit verify-heads` and `audit archive list|verify` require the `OperationalAudit`
|
|
permission; `audit export`
|
|
additionally requires `AuditExport`; `audit backfill-source-node` requires the
|
|
`Admin` role (maintenance path only). The server enforces permission checks and
|
|
returns HTTP 403 (CLI exit code 2) on denial.
|
|
|
|
```
|
|
scadabridge audit query [--since <t>] [--until <t>] [--channel <c>] [--kind <k>] [--status <s>] [--site <s>] [--target <t>] [--actor <a>] [--correlation-id <id>] [--execution-id <id>] [--parent-execution-id <id>] [--errors-only] [--page-size <n>] [--all]
|
|
scadabridge audit export --since <t> --until <t> --format csv|jsonl|parquet --output <path> [--channel <c>] [--kind <k>] [--status <s>] [--site <s>] [--target <t>] [--actor <a>]
|
|
scadabridge audit tree --execution-id <guid> [--format table|json]
|
|
scadabridge audit backfill-source-node --before <ISO-8601-UTC> [--sentinel <value>] [--batch <n>]
|
|
scadabridge audit verify-chain --month <YYYY-MM>
|
|
scadabridge audit verify-heads --path <file-or-directory>
|
|
scadabridge audit archive list
|
|
scadabridge audit archive verify --name <archive>
|
|
scadabridge audit archive cleanup [--dry-run]
|
|
```
|
|
|
|
- `audit query` — filtered query against the central `AuditLog` table, matching the
|
|
Central UI Audit Log page filter set (time range, channel, kind, status, site,
|
|
instance/script, target, actor, correlation ID, errors-only). Results stream as
|
|
JSON (default) or table.
|
|
- `audit export` — server-side streaming export of the central `AuditLog` to the
|
|
requested format (`csv`, `jsonl`, `parquet`) written to `--output`. The server
|
|
streams rows rather than materializing them in memory (Parquet: one row group of
|
|
10,000 rows at a time, the same 24 columns as csv/jsonl — ScadaBridge#42); the CLI
|
|
writes bytes through to disk. Supports the same scoping filters as `audit query`.
|
|
- `audit tree --execution-id <guid>` (M5.3 T8) — renders the full execution-chain
|
|
tree for the given `ExecutionId`. The server resolves the root from any node in
|
|
the chain (walks `ParentExecutionId` to find the root, then traverses downward)
|
|
and returns all reachable executions with their summary row counts and first/last
|
|
occurred timestamps. Output format: `json` (default — structured tree suitable
|
|
for scripting) or `table` (human-readable indented tree). Requires
|
|
`OperationalAudit` permission. Backed by `GET /api/audit/tree?executionId=<guid>`.
|
|
- `audit backfill-source-node --before <ISO-8601-UTC>` (M5.6 T5) — sets
|
|
`SourceNode` to a sentinel value (`--sentinel`, default `"unknown"`) on pre-feature
|
|
rows where `SourceNode IS NULL` and `OccurredAtUtc < --before`, in batches
|
|
(`--batch`, default 5000). Admin-only maintenance command. Idempotent.
|
|
Backed by `POST /api/audit/backfill-source-node`.
|
|
- `audit verify-chain --month <YYYY-MM>` (ScadaBridge#41) — verifies the AuditLog
|
|
hash chain for one UTC calendar month by sending the `VerifyAuditChain` management
|
|
command over `POST /management` (the one `audit` leaf that is a management contract
|
|
rather than a REST call). Reports `Intact` / `NotChained` / `Broken`, rows verified,
|
|
tombstones applied, the partition-purge anchor, the head sequence and unchained rows;
|
|
on `Broken`, the first break's partition, `ChainSeq`, `EventId` and kind. Exit code
|
|
`0` intact or not chained, `3` broken, `1` error, `2` authorization failure. Requires
|
|
`OperationalAudit` and a caller not scoped to specific sites. See
|
|
Component-AuditLog.md, Security & Tamper-Evidence.
|
|
- `audit verify-heads --path <file-or-directory>` (ScadaBridge#107) — reads the AuditLog
|
|
chain heads exported off-box by `AuditLog:HashChain:HeadExport` (JSON lines from the file
|
|
sink, or any log a syslog collector wrote — each line's JSON object is taken from its first
|
|
`{` to its last `}`; a directory is read recursively for `*.jsonl`/`*.log`/`*.txt`),
|
|
collapses duplicates and sends them in the `VerifyAuditChainHeads` management command. The
|
|
server checks each exported head against the database chain — `Matches`, `Extended` (chain
|
|
grew; the link at the exported sequence still carries the exported hash), `Purged` (removed
|
|
by a partition purge; verify the archive), or the failures `Mismatch` (rewritten chain),
|
|
`Regressed`, `MissingHead`, `MissingLink`, `CountMismatch`. Exit `0` verified, `3` a
|
|
failure, `1` error (no records found, bad input, `CHAIN_BUSY`), `2` authorization failure.
|
|
Same gate as `verify-chain`. Run it together with `verify-chain`: a forger who rewrites rows
|
|
and re-hashes the chain inside SQL Server passes `verify-chain` but not `verify-heads`.
|
|
- `audit archive list` / `audit archive verify --name <archive>` (ScadaBridge#42) —
|
|
send the `ListAuditArchives` / `VerifyAuditArchive` management commands for the
|
|
AuditLog Parquet cold archives the retention purge writes (`AuditLog:Archive`,
|
|
default off). `verify` checks the files against their manifest, recomputes the
|
|
archived hash chain and compares its end with the purge anchor; exit `0` verified,
|
|
`3` not verified, `1` error, `2` authorization failure. Same gate as `verify-chain`.
|
|
See Component-AuditLog.md, Retention & Purge.
|
|
- `audit archive cleanup [--dry-run]` (ScadaBridge#109) — sends `CleanupAuditArchives`:
|
|
deletes superseded stale archives (a retried purge's, once a later purged archive provably
|
|
holds all their rows) and archives past `AuditLog:Archive:RetentionDays` (0 = never), the
|
|
same pass the purge actor runs after every tick; `--dry-run` only reports. Prints each
|
|
`DeletedSuperseded` / `DeletedExpired` / `KeptStale` with its reason; exit `0`. Administrator
|
|
only (it deletes files).
|
|
|
|
### Health Commands
|
|
```
|
|
scadabridge health summary
|
|
scadabridge health pending-updates
|
|
scadabridge health site --identifier <site-identifier>
|
|
scadabridge health event-log --site <site-identifier> [--event-type <type>] [--severity <level>] [--keyword <term>] [--from <date>] [--to <date>] [--page <n>] [--page-size <n>] [--instance-name <name>]
|
|
scadabridge health parked-messages --site <site-identifier> [--page <n>] [--page-size <n>]
|
|
```
|
|
|
|
### Debug Commands
|
|
```
|
|
scadabridge debug snapshot --id <id>
|
|
scadabridge debug stream --id <id>
|
|
```
|
|
|
|
The `debug snapshot` command retrieves a point-in-time snapshot via the HTTP Management API.
|
|
|
|
The `debug stream` command streams live attribute values and alarm state changes in real-time using a SignalR WebSocket connection. The CLI connects to the `/hubs/debug-stream` SignalR hub on the central server, authenticates with the process's bearer token (the SignalR client sends it in the `Authorization` header of the negotiate request and WebSocket upgrade, and re-fetches it on every reconnect) — or Basic Auth on a server without bearer support — and subscribes to the specified instance. Events are printed as they arrive — JSON format (default) outputs one NDJSON object per event; table format shows streaming rows. Press Ctrl+C to disconnect.
|
|
|
|
Key behaviors:
|
|
- **Automatic reconnection**: Uses SignalR's `.WithAutomaticReconnect()` to re-establish the connection on loss.
|
|
- **Re-subscription**: Automatically re-subscribes to the instance after reconnection.
|
|
- **Traefik compatible**: Works through the Traefik reverse proxy — WebSocket upgrade is proxied natively.
|
|
- **Required role**: `Deployment`.
|
|
|
|
Unlike `debug snapshot` (which uses the HTTP Management API), `debug stream` uses `Microsoft.AspNetCore.SignalR.Client` as a dependency for its WebSocket transport.
|
|
|
|
### Shared Schema Commands
|
|
|
|
```sh
|
|
scadabridge shared-schema list
|
|
scadabridge shared-schema get --id <id>
|
|
scadabridge shared-schema create --name <name> --schema-file <path.json> [--scope <scope>]
|
|
scadabridge shared-schema update --id <id> --name <name> --schema-file <path.json> [--scope <scope>]
|
|
scadabridge shared-schema delete --id <id>
|
|
```
|
|
|
|
Create/update validate JSON syntax locally and schema structure on the server.
|
|
Update replaces all three fields; omitted scope clears it. Mutations require
|
|
Designer access; reads require authentication.
|
|
|
|
### Shared Script Commands
|
|
```
|
|
scadabridge shared-script list
|
|
scadabridge shared-script get --id <id>
|
|
scadabridge shared-script create --name <name> --code <code> [--parameters <json>] [--return-def <json>]
|
|
scadabridge shared-script update --id <id> [--name <name>] [--code <code>] [--parameters <json>] [--return-def <json>]
|
|
scadabridge shared-script delete --id <id>
|
|
```
|
|
|
|
### Database Connection Commands
|
|
```
|
|
scadabridge db-connection list
|
|
scadabridge db-connection get --id <id>
|
|
scadabridge db-connection create --name <name> (--connection-string <string> | --connection-string-file <path|->)
|
|
scadabridge db-connection update --id <id> [--name <name>] (--connection-string <string> | --connection-string-file <path|->)
|
|
scadabridge db-connection delete --id <id>
|
|
```
|
|
|
|
### Inbound API Method Commands
|
|
```
|
|
scadabridge api-method list
|
|
scadabridge api-method get --id <id>
|
|
scadabridge api-method create --name <name> --script <code> [--timeout <seconds>] [--parameters <json>] [--return-def <json>]
|
|
scadabridge api-method update --id <id> --script <code> [--timeout <seconds>] [--parameters <json>] [--return-def <json>]
|
|
scadabridge api-method delete --id <id>
|
|
scadabridge api-method validate --id <id>
|
|
scadabridge api-method recompile --id <id>
|
|
```
|
|
|
|
### Bundle Commands (Transport #24)
|
|
|
|
```
|
|
scadabridge bundle export --output <path> [--passphrase <phrase>] [--all] [--include-dependencies] [--templates <names>] [--shared-scripts <names>] [--external-systems <names>] [--db-connections <names>] [--notification-lists <names>] [--smtp-configs <names>] [--api-methods <names>] [--sites <ids|names>] [--instances <unique-names>] [--source-environment <env>]
|
|
scadabridge bundle preview --input <path> [--passphrase <phrase>]
|
|
scadabridge bundle import --input <path> [--passphrase <phrase>] [--on-conflict skip|overwrite|rename] [--map-site <src=dst> ...] [--map-connection <srcSite/srcName=dstName> ...] [--create-missing-sites] [--create-missing-connections]
|
|
```
|
|
|
|
`--sites` / `--instances` (M8/T18) pull site-scoped artifacts: a `--sites` token is a `SiteIdentifier` (preferred) or friendly name; a `--instances` token is a `UniqueName`. Selecting an instance also pulls its site and bound `DataConnection`s.
|
|
|
|
`bundle preview` prints the per-row diff plus a **required-mapping summary**: for each source site/connection it states either the auto-match (`auto-matches 'X'`) or the directive to supply `--map-site` / `--map-connection` or `--create-missing-*`.
|
|
|
|
On import, the mapping flags reconcile environment-specific identifiers. `--map-site` / `--map-connection` are repeatable; a token with no `=dst` (or `=` with an empty right-hand side) means **create-new** from the bundle payload, otherwise it binds the source to the named existing target. `--create-missing-sites` / `--create-missing-connections` create any still-unmapped source site/connection instead of aborting. Imported instances always land `NotDeployed`.
|
|
|
|
Inbound API keys are not transported between environments — re-create them on the destination via CLI or UI.
|
|
Bundle commands use a 5-minute timeout.
|
|
|
|
### Cached-Call Commands (Site Call Audit #22)
|
|
|
|
Retry or discard parked `ExternalSystem.CachedCall` / `Database.CachedWrite` operations
|
|
at a site. Both commands relay via the central `SiteCallAuditActor` → site relay and
|
|
require the **Deployer** role (same gate as the Central UI Site Calls page).
|
|
|
|
```
|
|
scadabridge cached-call retry --site-id <identifier> --tracked-operation-id <id>
|
|
scadabridge cached-call discard --site-id <identifier> --tracked-operation-id <id>
|
|
```
|
|
|
|
The `--tracked-operation-id` value corresponds to the `MessageId` field of the parked
|
|
`SiteCall` row (visible on the Central UI Site Calls page and in the `SiteCalls` audit
|
|
table).
|
|
|
|
### Secured-Write Commands (two-person MxGateway writes)
|
|
|
|
Manage two-person MxGateway writes. Listing is gated any-of **Operator**,
|
|
**Verifier**, or **Administrator** because the history exposes process-sensitive
|
|
tag values. Submission requires Operator; approval/rejection require Verifier,
|
|
with a different authenticated principal from the submitter.
|
|
|
|
```
|
|
scadabridge secured-write list [--status <status>] [--site <identifier>] [--skip <n>] [--take <n>]
|
|
scadabridge secured-write submit --site <identifier> --connection <name> --tag-path <path> --value-json <json> --value-type <type> [--comment <text>]
|
|
scadabridge secured-write approve --id <long> [--comment <text>]
|
|
scadabridge secured-write reject --id <long> [--comment <text>]
|
|
```
|
|
|
|
`--status` / `--site` filter the results (omit to match all). `--skip` / `--take` provide
|
|
offset paging (arch-review P3); omitting them uses the default page
|
|
(`--skip 0 --take 200`). `--take` is clamped to `1..500` server-side. The result carries a
|
|
`totalCount` (rows matching the filters, ignoring paging) alongside the returned page.
|
|
Overdue `Pending` rows are swept to `Expired` opportunistically each time the list is
|
|
queried (server-side TTL, arch-review S2).
|
|
|
|
Approval can execute a real device write. Only a server response with status
|
|
`Executed` exits zero; `Failed` or an invalid approval outcome exits one, with
|
|
JSON/table details retained. Authorization denial exits two. The server enforces
|
|
site scope, expiry, separation of duties, and concurrent decisions.
|
|
|
|
The `--format json|table` option is recursive and accepted on every management command above; audit export has its own output formats.
|
|
|
|
## Configuration
|
|
|
|
Configuration is resolved in the following priority order (highest wins):
|
|
|
|
1. **Command-line options**: `--url`, `--username`, `--password` / `--password-file`, `--format`.
|
|
2. **Environment variables**:
|
|
- `SCADABRIDGE_MANAGEMENT_URL` — Management API URL (e.g., `http://central-host:5000`).
|
|
- `SCADABRIDGE_FORMAT` — Default output format (`json` or `table`).
|
|
- `SCADABRIDGE_USERNAME` / `SCADABRIDGE_PASSWORD` — LDAP credentials. Preferred over
|
|
`--password` on the command line, which is visible in process listings and shell
|
|
history. Credentials are never read from the config file.
|
|
`--password-file <path|->` is the per-invocation equivalent (see *Secret-bearing options*).
|
|
3. **Configuration file**: `~/.scadabridge/config.json` — Persistent defaults for management URL and output format only (never credentials).
|
|
|
|
### Secret-bearing options (file / stdin)
|
|
|
|
Every option that can carry a secret has a `-file` sibling (ScadaBridge#137): `--password-file`
|
|
(global), `--primary-config-file` / `--backup-config-file` (`data-connection create|update`),
|
|
`--auth-config-file` (`external-system create|update`), `--connection-string-file`
|
|
(`db-connection create|update`), `--credentials-file` (`notification smtp create|update`) and
|
|
`--auth-token-file` (`notification sms create|update`).
|
|
|
|
- `--x-file <path>` reads the file; `--x-file -` reads stdin (a file named `-` is `./-`). One
|
|
trailing line break is stripped.
|
|
- Inline and file forms of one value are mutually exclusive; at most one option per invocation may
|
|
read stdin; `-` on a terminal stdin is refused rather than blocking.
|
|
- The value is read at parse time. A missing, unreadable or empty source is a parse error that names
|
|
the option and path only — never the content.
|
|
- `@path` is deliberately not used: System.CommandLine expands `@`-prefixed tokens as response files.
|
|
|
|
The inline forms remain for non-secret use. Seed scripts in `docker/` and `docker-env2/` use the
|
|
file/stdin forms for any value that carries a key or password.
|
|
|
|
### Configuration File Format
|
|
|
|
```json
|
|
{
|
|
"managementUrl": "http://central-host:5000"
|
|
}
|
|
```
|
|
|
|
## Output Formats
|
|
|
|
- **JSON** (default): Machine-readable JSON output to stdout. Suitable for piping to `jq` or processing in scripts. Errors are written to stderr as JSON objects with `error` and `code` fields.
|
|
- **Table** (`--format table` or `--table`): Human-readable tabular output with aligned columns. Suitable for interactive use.
|
|
|
|
## Exit Codes
|
|
|
|
| Code | Meaning |
|
|
|------|---------|
|
|
| 0 | Success |
|
|
| 1 | General error (command failed, connection failure, or authentication failure) |
|
|
| 2 | Authorization failure (insufficient role) |
|
|
|
|
## Error Handling
|
|
|
|
- **Connection failure**: If the CLI cannot connect to the management URL (e.g., DNS failure, connection refused), it exits with code 1 and a descriptive error message.
|
|
- **Command timeout**: Ordinary CLI commands use a 30-second client deadline. All instance/artifact/site deployment aliases defer the overall deadline to the server, retaining a bounded socket-connect timeout. Server Ask expiry returns HTTP 504; a client deadline is reported locally. Bundle and Flow Debugger commands retain their explicit timeout policies.
|
|
- **Authentication failure**: If the server returns HTTP 401 (LDAP bind failed at `/auth/token` or on a Basic call, or a bearer token that a fresh exchange could not replace), the CLI exits with code 1.
|
|
- **Authorization failure**: If the server returns HTTP 403, the CLI exits with code 2.
|
|
|
|
## Dependencies
|
|
|
|
- **Commons**: Message contracts (`Messages/Management/`) for command type definitions and registry.
|
|
- **System.CommandLine**: Command-line argument parsing.
|
|
- **Microsoft.AspNetCore.SignalR.Client**: SignalR client for the `debug stream` command's WebSocket connection.
|
|
- **Management Service (#18)**: The CLI hits the central cluster via the existing HTTP Management API (`POST /management`), which dispatches to the ManagementActor. The `scadabridge audit` command group rides a parallel REST surface on the same Host (`GET /api/audit/query` and `GET /api/audit/export`), sharing authentication (bearer token or HTTP Basic) with `/management` but bypassing the actor for read-only, keyset-paged / streaming workloads.
|
|
- **Audit Log (#23)**: The `scadabridge audit query`, `audit export`, `audit tree`, and `audit backfill-source-node` subcommands target the centralized Audit Log component's REST endpoints (`GET /api/audit/query`, `GET /api/audit/export`, `GET /api/audit/tree`, `POST /api/audit/backfill-source-node`) on the Host's Management API surface; `audit verify-chain` sends the `VerifyAuditChain` management command (`POST /management`, ScadaBridge#41). Permission checks (`OperationalAudit`, `AuditExport`, `Admin`) are enforced server-side by `AuditEndpoints`.
|
|
|
|
## Interactions
|
|
|
|
- **Management Service (via HTTP)**: The primary runtime dependency. All operations except `debug stream` are sent as HTTP POST requests to the Management API endpoint on a central node, which dispatches to the ManagementActor.
|
|
- **Central Host**: Serves the Management API at `POST /management` and the debug stream SignalR hub at `/hubs/debug-stream`. Issues bearer tokens at `POST /auth/token`; handles LDAP authentication, role resolution, and ManagementActor dispatch.
|
|
- **Debug Stream Hub (via SignalR WebSocket)**: The `debug stream` command connects to the `/hubs/debug-stream` hub on the central server for real-time event streaming. This is the only CLI command that uses a persistent connection rather than request/response.
|
|
|
|
|
|
### Bulk deployment completion
|
|
|
|
deploy site preserves the returned summary in JSON/table output and exits 1 when failureCount
|
|
is nonzero or the summary is malformed, even when HTTP returned 2xx. A zero failure count exits
|
|
0; HTTP authorization failures retain exit 2. The server owns the bounded bulk operation deadline;
|
|
the client does not impose a shorter total timer on this command.
|
|
|
|
|
|
## Management parity (2026-09-10)
|
|
|
|
The CLI exposes all 149 HTTP-eligible management contracts. The production root
|
|
is constructed by `CliApplication.BuildRoot`; the test suite invokes that tree
|
|
against a loopback HTTP recorder and requires every registered contract to be
|
|
sent, with a negative control for a removed group. Actor dispatch coverage uses
|
|
the same HTTP eligibility boundary with no unhandled-command exemptions.
|
|
|
|
Added operations include the five `shared-schema` CRUD commands, secured-write
|
|
submit/approve/reject, notification rename and individual recipient add/delete,
|
|
SMTP/SMS create, effective template member inspection, template folder moves,
|
|
native alarm source updates, individual attribute-override deletion, native alarm
|
|
override listing, and data-connection moves. SMS timeout/retry settings are also
|
|
exposed. Their exact syntax and omission semantics are maintained in the
|
|
[CLI command reference](../../src/ZB.MOM.WW.ScadaBridge.CLI/README.md#management-parity-additions-2026-09-10).
|
|
|
|
All additions retain server-owned roles, scope restrictions, validation, and audit
|
|
handling. In particular, secured-write approval requires a different verifier
|
|
identity and exits with failure when the returned device execution failed.
|