Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Enhancements:
- feat(rust): Allow testing with prerelease Rust versions ([#1604](https://github.com/fastly/cli/pull/1604))
- feat(compute/hashfiles): remove hashsum subcommand ([#1608](https://github.com/fastly/cli/pull/1608))
- feat(commands/ngwaf/rules): add support for CRUD operations for NGWAF rules ([#1578](https://github.com/fastly/cli/pull/1605))

### Bug fixes:

Expand Down
26 changes: 26 additions & 0 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import (
"github.com/fastly/cli/pkg/commands/ngwaf/countrylist"
"github.com/fastly/cli/pkg/commands/ngwaf/customsignal"
"github.com/fastly/cli/pkg/commands/ngwaf/iplist"
"github.com/fastly/cli/pkg/commands/ngwaf/rule"
"github.com/fastly/cli/pkg/commands/ngwaf/signallist"
"github.com/fastly/cli/pkg/commands/ngwaf/stringlist"
"github.com/fastly/cli/pkg/commands/ngwaf/wildcardlist"
Expand All @@ -76,6 +77,7 @@ import (
wscustomsignal "github.com/fastly/cli/pkg/commands/ngwaf/workspace/customsignal"
wsiplist "github.com/fastly/cli/pkg/commands/ngwaf/workspace/iplist"
"github.com/fastly/cli/pkg/commands/ngwaf/workspace/redaction"
workspaceRule "github.com/fastly/cli/pkg/commands/ngwaf/workspace/rule"
wssignallistlist "github.com/fastly/cli/pkg/commands/ngwaf/workspace/signallist"
wsstringlistlist "github.com/fastly/cli/pkg/commands/ngwaf/workspace/stringlist"
"github.com/fastly/cli/pkg/commands/ngwaf/workspace/threshold"
Expand Down Expand Up @@ -449,6 +451,12 @@ func Define( // nolint:revive // function-length
ngwafIPListGet := iplist.NewGetCommand(ngwafIPListRoot.CmdClause, data)
ngwafIPListList := iplist.NewListCommand(ngwafIPListRoot.CmdClause, data)
ngwafIPListUpdate := iplist.NewUpdateCommand(ngwafIPListRoot.CmdClause, data)
ngwafRuleRoot := rule.NewRootCommand(ngwafRoot.CmdClause, data)
ngwafRuleCreate := rule.NewCreateCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleDelete := rule.NewDeleteCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleGet := rule.NewGetCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleList := rule.NewListCommand(ngwafRuleRoot.CmdClause, data)
ngwafRuleUpdate := rule.NewUpdateCommand(ngwafRuleRoot.CmdClause, data)
ngwafSignalListRoot := signallist.NewRootCommand(ngwafRoot.CmdClause, data)
ngwafSignalListCreate := signallist.NewCreateCommand(ngwafSignalListRoot.CmdClause, data)
ngwafSignalListDelete := signallist.NewDeleteCommand(ngwafSignalListRoot.CmdClause, data)
Expand Down Expand Up @@ -485,6 +493,12 @@ func Define( // nolint:revive // function-length
ngwafWorkspaceIPListGet := wsiplist.NewGetCommand(ngwafWorkspaceIPListRoot.CmdClause, data)
ngwafWorkspaceIPListList := wsiplist.NewListCommand(ngwafWorkspaceIPListRoot.CmdClause, data)
ngwafWorkspaceIPListUpdate := wsiplist.NewUpdateCommand(ngwafWorkspaceIPListRoot.CmdClause, data)
ngwafWorkspaceRuleRoot := workspaceRule.NewRootCommand(ngwafWorkspaceRoot.CmdClause, data)
ngwafWorkspaceRuleCreate := workspaceRule.NewCreateCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleDelete := workspaceRule.NewDeleteCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleGet := workspaceRule.NewGetCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleList := workspaceRule.NewListCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceRuleUpdate := workspaceRule.NewUpdateCommand(ngwafWorkspaceRuleRoot.CmdClause, data)
ngwafWorkspaceSignalListRoot := wssignallistlist.NewRootCommand(ngwafWorkspaceRoot.CmdClause, data)
ngwafWorkspaceSignalListCreate := wssignallistlist.NewCreateCommand(ngwafWorkspaceSignalListRoot.CmdClause, data)
ngwafWorkspaceSignalListDelete := wssignallistlist.NewDeleteCommand(ngwafWorkspaceSignalListRoot.CmdClause, data)
Expand Down Expand Up @@ -1007,6 +1021,12 @@ func Define( // nolint:revive // function-length
ngwafIPListGet,
ngwafIPListList,
ngwafIPListUpdate,
ngwafRuleRoot,
ngwafRuleCreate,
ngwafRuleDelete,
ngwafRuleGet,
ngwafRuleList,
ngwafRuleUpdate,
ngwafSignalListRoot,
ngwafSignalListCreate,
ngwafSignalListDelete,
Expand Down Expand Up @@ -1042,6 +1062,12 @@ func Define( // nolint:revive // function-length
ngwafWorkspaceIPListGet,
ngwafWorkspaceIPListList,
ngwafWorkspaceIPListUpdate,
ngwafWorkspaceRuleRoot,
ngwafWorkspaceRuleCreate,
ngwafWorkspaceRuleDelete,
ngwafWorkspaceRuleGet,
ngwafWorkspaceRuleList,
ngwafWorkspaceRuleUpdate,
ngwafWorkspaceSignalListRoot,
ngwafWorkspaceSignalListCreate,
ngwafWorkspaceSignalListDelete,
Expand Down
192 changes: 192 additions & 0 deletions pkg/commands/ngwaf/rule/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package rule

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/fastly/go-fastly/v12/fastly"
"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/rules"
"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/scope"

"github.com/fastly/cli/pkg/argparser"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
)

// CreateCommand calls the Fastly API to create account-level rules.
type CreateCommand struct {
argparser.Base
argparser.JSONOutput

// Required.
path string
}

// NewCreateCommand returns a usable command registered under the parent.
func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateCommand {
c := CreateCommand{
Base: argparser.Base{
Globals: g,
},
}
c.CmdClause = parent.Command("create", "Create an account-level rule").Alias("add")

// Required.
c.CmdClause.Flag("path", "Path to a json file that contains the rule schema.").Required().StringVar(&c.path)

// Optional.
c.RegisterFlagBool(c.JSONFlag())

return &c
}

// Exec invokes the application logic for the command.
func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error {
if c.Globals.Verbose() && c.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}
var err error
rule := &rules.Rule{}
if c.path != "" {
path, err := filepath.Abs(c.path)
if err != nil {
return fmt.Errorf("error parsing path '%s': %q", c.path, err)
}

jsonFile, err := os.Open(path)
if err != nil {
return fmt.Errorf("error reading cert-path '%s': %q", c.path, err)
}
defer jsonFile.Close()

byteValue, err := io.ReadAll(jsonFile)
if err != nil {
return fmt.Errorf("failed to read json file: %v", err)
}

if err := json.Unmarshal(byteValue, rule); err != nil {
return fmt.Errorf("failed to unmarshal json data: %v", err)
}
}

input := &rules.CreateInput{
Actions: []*rules.CreateAction{},
Conditions: []*rules.CreateCondition{},
Description: &rule.Description,
GroupConditions: []*rules.CreateGroupCondition{},
MultivalConditions: []*rules.CreateMultivalCondition{},
Enabled: &rule.Enabled,
Type: &rule.Type,
GroupOperator: &rule.GroupOperator,
RequestLogging: &rule.RequestLogging,
Scope: &scope.Scope{
Type: scope.ScopeTypeAccount,
AppliesTo: []string{"*"},
},
}

for _, action := range rule.Actions {
input.Actions = append(input.Actions, &rules.CreateAction{
AllowInteractive: action.AllowInteractive,
DeceptionType: &action.DeceptionType,
RedirectURL: &action.RedirectURL,
ResponseCode: &action.ResponseCode,
Signal: &action.Signal,
Type: &action.Type,
})
}

if rule.RateLimit != nil {
input.RateLimit = &rules.CreateRateLimit{
ClientIdentifiers: []*rules.CreateClientIdentifier{},
Duration: &rule.RateLimit.Duration,
Interval: &rule.RateLimit.Interval,
Signal: &rule.RateLimit.Signal,
Threshold: &rule.RateLimit.Threshold,
}

for _, rateLimit := range rule.RateLimit.ClientIdentifiers {
input.RateLimit.ClientIdentifiers = append(input.RateLimit.ClientIdentifiers, &rules.CreateClientIdentifier{
Key: &rateLimit.Key,
Name: &rateLimit.Name,
Type: &rateLimit.Type,
})
}
}

for _, jsonCondition := range rule.Conditions {
switch jsonCondition.Type {
case "single":
if sc, ok := jsonCondition.Fields.(rules.SingleCondition); ok {
input.Conditions = append(input.Conditions, &rules.CreateCondition{
Field: &sc.Field,
Operator: &sc.Operator,
Value: &sc.Value,
})
} else {
return fmt.Errorf("expected SingleCondition, got %T", jsonCondition.Fields)
}
case "group":
if gc, ok := jsonCondition.Fields.(rules.GroupCondition); ok {
parsedGroupCondition := &rules.CreateGroupCondition{
GroupOperator: &gc.GroupOperator,
Conditions: []*rules.CreateCondition{},
}
for _, groupSingleCondition := range gc.Conditions {
parsedGroupCondition.Conditions = append(parsedGroupCondition.Conditions, &rules.CreateCondition{
Field: &groupSingleCondition.Field,
Operator: &groupSingleCondition.Operator,
Value: &groupSingleCondition.Value,
})
}
input.GroupConditions = append(input.GroupConditions, parsedGroupCondition)
} else {
return fmt.Errorf("expected GroupCondition, got %T", jsonCondition.Fields)
}
case "multival":
if mvc, ok := jsonCondition.Fields.(rules.CreateMultivalCondition); ok {
parsedMultiValCondition := &rules.CreateMultivalCondition{
Field: mvc.Field,
GroupOperator: mvc.GroupOperator,
Operator: mvc.Operator,
Conditions: []*rules.CreateConditionMult{},
}
for _, multiSingleCondition := range mvc.Conditions {
parsedMultiValCondition.Conditions = append(parsedMultiValCondition.Conditions, &rules.CreateConditionMult{
Field: multiSingleCondition.Field,
Operator: multiSingleCondition.Operator,
Value: multiSingleCondition.Value,
})
}
input.MultivalConditions = append(input.MultivalConditions, parsedMultiValCondition)
} else {
return fmt.Errorf("expected MultivalCondition, got %T", jsonCondition.Fields)
}
default:
return fmt.Errorf("unknown condition type: %s", jsonCondition.Type)
}
}

fc, ok := c.Globals.APIClient.(*fastly.Client)
if !ok {
return errors.New("failed to convert interface to a fastly client")
}

data, err := rules.Create(context.TODO(), fc, input)
if err != nil {
return err
}

if ok, err := c.WriteJSON(out, data); ok {
return err
}

text.Success(out, "Created account-level rule with ID %s", data.RuleID)
return nil
}
84 changes: 84 additions & 0 deletions pkg/commands/ngwaf/rule/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package rule

import (
"context"
"errors"
"io"

"github.com/fastly/go-fastly/v12/fastly"

"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/rules"
"github.com/fastly/go-fastly/v12/fastly/ngwaf/v1/scope"

"github.com/fastly/cli/pkg/argparser"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
)

// DeleteCommand calls the Fastly API to delete an account-level rule.
type DeleteCommand struct {
argparser.Base
argparser.JSONOutput

// Required.
ruleID string
}

// NewDeleteCommand returns a usable command registered under the parent.
func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteCommand {
c := DeleteCommand{
Base: argparser.Base{
Globals: g,
},
}

c.CmdClause = parent.Command("delete", "Delete an account-level rule")

// Required.
c.CmdClause.Flag("rule-id", "Rule ID").Required().StringVar(&c.ruleID)

// Optional.
c.RegisterFlagBool(c.JSONFlag())

return &c
}

// Exec invokes the application logic for the command.
func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error {
if c.Globals.Verbose() && c.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}

fc, ok := c.Globals.APIClient.(*fastly.Client)
if !ok {
return errors.New("failed to convert interface to a fastly client")
}

err := rules.Delete(context.TODO(), fc, &rules.DeleteInput{
RuleID: &c.ruleID,
Scope: &scope.Scope{
Type: scope.ScopeTypeAccount,
AppliesTo: []string{"*"},
},
})
if err != nil {
c.Globals.ErrLog.Add(err)
return err
}

if c.JSONOutput.Enabled {
o := struct {
ID string `json:"id"`
Deleted bool `json:"deleted"`
}{
c.ruleID,
true,
}
_, err := c.WriteJSON(out, o)
return err
}

text.Success(out, "Deleted account-level rule with id: %s", c.ruleID)
return nil
}
2 changes: 2 additions & 0 deletions pkg/commands/ngwaf/rule/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package rule contains commands to inspect and manipulate NGWAF account-level rules.
package rule
Loading
Loading