Skip to content
Draft
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
10 changes: 10 additions & 0 deletions .changeset/ecdn-firewall-rules-cli-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@salesforce/b2c-cli': minor
---

Add `ecdn firewall` commands (`list`, `get`, `create`, `update`, `delete`,
`reorder`) for managing custom firewall rules on a CDN zone via the existing
cdn-zones v1 APIs (`/firewall-custom/rules`). Supports partial updates,
`--json` output (with table column flags on `list`), and routes destructive
operations (`delete`, `reorder`) through the same safety guard the rest of
the CLI uses.
195 changes: 195 additions & 0 deletions docs/cli/ecdn.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,201 @@ b2c ecdn cache purge --zone my-zone --path "www.example.com/dw/image/v2/realm_in

---

## Rate Limiting

### b2c ecdn rate-limit list

List rate limiting rules for a zone.

```bash
b2c ecdn rate-limit list --zone my-zone
b2c ecdn rate-limit list --zone my-zone --extended
b2c ecdn rate-limit list --zone my-zone --json
```

---

### b2c ecdn rate-limit get

Get a single rate limiting rule by ID.

```bash
b2c ecdn rate-limit get --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e
b2c ecdn rate-limit get --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --json
```

---

### b2c ecdn rate-limit create

Create a rate limiting rule.

```bash
b2c ecdn rate-limit create --zone my-zone --description "Rate limit /checkout" --expression '(http.request.uri.path matches "^/checkout")' --characteristics cf.unique_visitor_id --action block --period 60 --requests-per-period 50 --mitigation-timeout 600
b2c ecdn rate-limit create --zone my-zone --description "Rate limit /checkout" --expression '(http.request.uri.path matches "^/checkout")' --characteristics cf.unique_visitor_id --action block --period 60 --requests-per-period 50 --mitigation-timeout 600 --json
```

#### Flags

| Flag | Description | Required |
|------|-------------|----------|
| `--description` | Rule description | Yes |
| `--expression` | Expression defining when to evaluate the rule | Yes |
| `--characteristics` | Comma-separated request grouping keys | Yes |
| `--action` | Mitigation action | Yes |
| `--period` | Rate window in seconds (`10`, `60`, `120`, `300`, `600`) | Yes |
| `--requests-per-period` | Max requests allowed within the period | Yes |
| `--mitigation-timeout` | Action duration in seconds (`0`, `60`, `120`, `300`, `600`, `3600`, `86400`) | Yes |
| `--counting-expression` | Optional expression for what requests to count | No |
| `--enabled` / `--no-enabled` | Enable/disable rule | No |
| `--position-before` | Insert before another rule ID | No |
| `--position-after` | Insert after another rule ID | No |

---

### b2c ecdn rate-limit update

Update fields of an existing rate limiting rule.

```bash
b2c ecdn rate-limit update --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --requests-per-period 100 --mitigation-timeout 120
b2c ecdn rate-limit update --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --action managed_challenge --no-enabled
b2c ecdn rate-limit update --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --requests-per-period 100 --mitigation-timeout 120 --json
```

---

### b2c ecdn rate-limit delete

Delete a rate limiting rule.

```bash
b2c ecdn rate-limit delete --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --force
b2c ecdn rate-limit delete --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --force --json
```

---

## Custom Firewall Rules

Manage custom firewall rules for a zone. Custom rules let you block,
challenge, log, or otherwise act on requests that match a Cloudflare-style
expression. Rules are evaluated in order and the order can be updated as a
whole through `reorder`.

### b2c ecdn firewall list

List custom firewall rules for a zone.

```bash
b2c ecdn firewall list --zone my-zone
b2c ecdn firewall list --zone my-zone --extended
b2c ecdn firewall list --zone my-zone --limit 50
b2c ecdn firewall list --zone my-zone --json
```

#### Flags

| Flag | Description | Required |
|------|-------------|----------|
| `--limit` | Maximum records per request (1–50) | No |
| `--offset` | Result offset for pagination | No |
| `--extended` | Include `Rule ID`, `Expression`, and `Last Updated` columns | No |
| `--columns` | Comma-separated column names | No |

---

### b2c ecdn firewall get

Get a single custom firewall rule by ID.

```bash
b2c ecdn firewall get --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e
b2c ecdn firewall get --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --json
```

---

### b2c ecdn firewall create

Create a custom firewall rule.

```bash
b2c ecdn firewall create --zone my-zone --description "Block /admin" \
--expression '(http.request.uri.path matches "^/admin")' --actions block

b2c ecdn firewall create --zone my-zone --description "Challenge bots" \
--expression 'cf.threat_score gt 30' --actions managed_challenge --no-enabled

b2c ecdn firewall create --zone my-zone --description "Insert before existing" \
--expression '(http.host eq "old.example.com")' --actions block \
--before 2c0fc9fa937b11eaa1b71c4d701ab86e
```

#### Flags

| Flag | Description | Required |
|------|-------------|----------|
| `--description` | Rule description | Yes |
| `--expression` | Expression that determines when the rule applies | Yes |
| `--actions` | Comma-separated list of actions applied by the rule | Yes |
| `--enabled` / `--no-enabled` | Enable/disable rule (default `true`) | No |
| `--before` | Insert before another rule ID (mutually exclusive with `--after`) | No |
| `--after` | Insert after another rule ID (mutually exclusive with `--before`) | No |

---

### b2c ecdn firewall update

Update fields of an existing custom firewall rule. Provide at least one
update field; the command rejects empty patches so accidental no-op runs do
not look like successes.

```bash
b2c ecdn firewall update --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e \
--description "Updated copy"

b2c ecdn firewall update --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e \
--actions managed_challenge --no-enabled
```

---

### b2c ecdn firewall delete

Delete a custom firewall rule. Requires `--force` outside of `--json` mode.
Routes through the destructive-action safety guard so an in-flight policy
or confirmation prompt has the chance to intercept before any HTTP call.

```bash
b2c ecdn firewall delete --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --force
b2c ecdn firewall delete --zone my-zone --rule-id 2c0fc9fa937b11eaa1b71c4d701ab86e --force --json
```

---

### b2c ecdn firewall reorder

Update the evaluation order of all custom firewall rules. Provide either
`--rule-ids` (comma-separated, in the desired order) or `--rule-ids-file`
(path to a JSON file containing a string array). Reordering is treated as a
destructive action and so requires `--force` outside of `--json` mode.

```bash
b2c ecdn firewall reorder --zone my-zone \
--rule-ids ffffe61cf25e4ec49c34b029ff3060f7,2c0fc9fa937b11eaa1b71c4d701ab86e --force

b2c ecdn firewall reorder --zone my-zone --rule-ids-file ./order.json --force
```

`order.json` example:

```json
["ffffe61cf25e4ec49c34b029ff3060f7", "2c0fc9fa937b11eaa1b71c4d701ab86e"]
```

---

## Certificate Management

### b2c ecdn certificates list
Expand Down
3 changes: 3 additions & 0 deletions packages/b2c-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@
}
}
},
"rate-limit": {
"description": "Manage rate limiting rules\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/ecdn.html"
},
"mtls": {
"description": "Manage mTLS certificates for code deployment\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/ecdn.html"
},
Expand Down
143 changes: 143 additions & 0 deletions packages/b2c-cli/src/commands/ecdn/firewall/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* SPDX-License-Identifier: Apache-2
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Flags} from '@oclif/core';
import cliui from 'cliui';
import type {CdnZonesComponents} from '@salesforce/b2c-tooling-sdk/clients';
import {EcdnZoneCommand, formatApiError} from '../../../utils/ecdn/index.js';
import {t, withDocs} from '../../../i18n/index.js';

type CustomRule = CdnZonesComponents['schemas']['CustomRule'];
type CustomRulesPostRequest = CdnZonesComponents['schemas']['CustomRulesPostRequest'];

interface CreateOutput {
rule: CustomRule;
}

export default class EcdnFirewallCreate extends EcdnZoneCommand<typeof EcdnFirewallCreate> {
static description = withDocs(
t('commands.ecdn.firewall.create.description', 'Create a custom firewall rule for a zone'),
'/cli/ecdn.html#b2c-ecdn-firewall-create',
);

static enableJsonFlag = true;

static examples = [
`<%= config.bin %> <%= command.id %> --tenant-id zzxy_prd --zone my-zone --description "Block /admin" --expression '(http.request.uri.path matches "^/admin")' --actions block`,
`<%= config.bin %> <%= command.id %> --tenant-id zzxy_prd --zone my-zone --description "Challenge bot traffic" --expression 'cf.threat_score gt 30' --actions managed_challenge --enabled false`,
`<%= config.bin %> <%= command.id %> --tenant-id zzxy_prd --zone my-zone --description "Insert before existing" --expression '(http.host eq "old.example.com")' --actions block --before 2c0fc9fa937b11eaa1b71c4d701ab86e`,
];

static flags = {
...EcdnZoneCommand.baseFlags,
description: Flags.string({
description: t('flags.description.description', 'Rule description'),
required: true,
}),
expression: Flags.string({
description: t('flags.expression.description', 'Expression that determines when this rule applies'),
required: true,
}),
actions: Flags.string({
description: t('flags.actions.description', 'Comma-separated list of actions applied by the rule'),
required: true,
}),
enabled: Flags.boolean({
description: t('flags.enabled.description', 'Whether the rule is enabled'),
allowNo: true,
default: true,
}),
before: Flags.string({
description: t('flags.before.description', 'Insert this rule before the specified rule ID'),
exclusive: ['after'],
}),
after: Flags.string({
description: t('flags.after.description', 'Insert this rule after the specified rule ID'),
exclusive: ['before'],
}),
};

async run(): Promise<CreateOutput> {
this.requireOAuthCredentials();

const zoneId = await this.resolveZoneId();
const description = this.flags.description as string;
const expression = this.flags.expression as string;
const actionsRaw = this.flags.actions as string;
const enabled = this.flags.enabled as boolean;
const before = this.flags.before as string | undefined;
const after = this.flags.after as string | undefined;

if (!this.jsonEnabled()) {
this.log(t('commands.ecdn.firewall.create.creating', 'Creating custom firewall rule...'));
}

const actions = actionsRaw
.split(',')
.map((value) => value.trim())
.filter(Boolean);

if (actions.length === 0) {
this.error(t('commands.ecdn.firewall.create.actionsRequired', 'At least one action must be provided.'));
}

const body: CustomRulesPostRequest = {
description,
expression,
actions,
enabled,
};

if (before) {
body.position = {before};
} else if (after) {
body.position = {after};
}

const client = this.getCdnZonesRwClient();
const organizationId = this.getOrganizationId();

const {data, error} = await client.POST('/organizations/{organizationId}/zones/{zoneId}/firewall-custom/rules', {
params: {
path: {organizationId, zoneId},
},
body,
});

if (error) {
this.error(
t('commands.ecdn.firewall.create.error', 'Failed to create custom firewall rule: {{message}}', {
message: formatApiError(error),
}),
);
}

const rule = data?.data;
if (!rule) {
this.error(t('commands.ecdn.firewall.create.noData', 'No custom firewall rule data returned from API'));
}

const output: CreateOutput = {rule};

if (this.jsonEnabled()) {
return output;
}

const ui = cliui({width: process.stdout.columns || 80});
const labelWidth = 18;

ui.div('');
ui.div({text: t('commands.ecdn.firewall.create.success', 'Custom firewall rule created successfully!')});
ui.div('');
ui.div({text: 'Rule ID:', width: labelWidth}, {text: rule.ruleId});
ui.div({text: 'Description:', width: labelWidth}, {text: rule.description});
ui.div({text: 'Actions:', width: labelWidth}, {text: rule.actions?.join(', ') ?? '-'});
ui.div({text: 'Enabled:', width: labelWidth}, {text: rule.enabled ? 'yes' : 'no'});

this.log(ui.toString());

return output;
}
}
Loading
Loading