Skip to content

Commit 98f7684

Browse files
authored
feat(cli): add filter for fixable alerts (#1073)
ANEP-1395
1 parent 123d46b commit 98f7684

15 files changed

+239
-43
lines changed

cli/cmd/alert.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,31 @@ import (
2929
"github.com/spf13/cobra"
3030
)
3131

32+
type alertCmdStateType struct {
33+
Comment string
34+
End string
35+
Fixable bool
36+
Range string
37+
Reason int
38+
Scope string
39+
Severity string
40+
Status string
41+
Start string
42+
Type string
43+
}
44+
45+
// hasFilters returns true if certain filters are present
46+
// in the command state. excludes time filters (start, end, range).
47+
func (s alertCmdStateType) hasFilters() bool {
48+
// severity / status / type filters
49+
if s.Severity != "" || s.Status != "" || s.Type != "" {
50+
return true
51+
}
52+
return s.Fixable
53+
}
54+
3255
var (
33-
alertCmdState = struct {
34-
Comment string
35-
End string
36-
Range string
37-
Reason int
38-
Scope string
39-
Severity string
40-
Status string
41-
Start string
42-
Type string
43-
}{}
56+
alertCmdState = alertCmdStateType{}
4457

4558
// alertCmd represents the alert parent command
4659
alertCmd = &cobra.Command{

cli/cmd/alert_list.go

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,22 @@ func init() {
134134
"type", "",
135135
"filter alerts by type",
136136
)
137+
138+
// fixable
139+
if cli.isRemediateInstalled() {
140+
alertListCmd.Flags().BoolVar(
141+
&alertCmdState.Fixable,
142+
"fixable", false,
143+
"filter alerts by fixability",
144+
)
145+
}
137146
}
138147

139148
func alertListTable(alerts api.Alerts) (out [][]string) {
140149
alerts.SortByID()
141150
alerts.SortBySeverity()
142151

143152
for _, alert := range alerts {
144-
// filter severity if desired
145-
if lwseverity.ShouldFilter(alert.Severity, alertCmdState.Severity) {
146-
continue
147-
}
148-
149153
out = append(out, []string{
150154
strconv.Itoa(alert.ID),
151155
alert.Type,
@@ -242,17 +246,46 @@ func listAlert(_ *cobra.Command, _ []string) error {
242246
return errors.Wrap(err, msg)
243247
}
244248

249+
// filter severity
250+
alerts := api.Alerts{}
251+
for _, alert := range listResponse.Data {
252+
// filter severity if desired
253+
if lwseverity.ShouldFilter(alert.Severity, alertCmdState.Severity) {
254+
continue
255+
}
256+
alerts = append(alerts, alert)
257+
}
258+
259+
// filter fixable
260+
if alertCmdState.Fixable {
261+
templateIDs, err := getRemediationTemplateIDs()
262+
if err != nil {
263+
return errors.Wrap(err, "unable to filter by alert fixability")
264+
}
265+
alerts = filterFixableAlerts(alerts, templateIDs)
266+
}
267+
245268
if cli.JSONOutput() {
246-
return cli.OutputJSON(listResponse.Data)
269+
return cli.OutputJSON(alerts)
247270
}
248271

249-
if len(listResponse.Data) == 0 {
250-
cli.OutputHuman("There are no alerts in your account in the specified time range.\n")
272+
if len(alerts) == 0 {
273+
if alertCmdState.hasFilters() {
274+
cli.OutputHuman(fmt.Sprintf("%s %s\n",
275+
"No alerts match the specified filters within the given time range.",
276+
"Try removing filters or expanding the time range.",
277+
))
278+
return nil
279+
}
280+
cli.OutputHuman("There are no alerts in the specified time range.\n")
251281
return nil
252282
}
253-
renderAlertListTable(listResponse.Data)
283+
renderAlertListTable(alerts)
254284

255285
// breadcrumb
256286
cli.OutputHuman("\nUse 'lacework alert show <alert_id>' to see details for a specific alert.\n")
287+
if alertCmdState.Fixable {
288+
cli.OutputHuman("Use 'lacework remediate alert <alert_id>' to fix a specific alert.\n")
289+
}
257290
return nil
258291
}

cli/cmd/alert_list_fixable.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
9+
"github.com/lacework/go-sdk/api"
10+
"github.com/pkg/errors"
11+
)
12+
13+
const remediateComponentName string = "remediate"
14+
15+
// isRemediateInstalled returns true if the remediate component is installed
16+
func (c *cliState) isRemediateInstalled() bool {
17+
return c.IsComponentInstalled(remediateComponentName)
18+
}
19+
20+
// getTemplateIdentifiers runs the remediate component to retrieve a list
21+
// of remediation template identifiers
22+
func getRemediationTemplateIDs() ([]string, error) {
23+
remediate, found := cli.LwComponents.GetComponent(remediateComponentName)
24+
if !found {
25+
return []string{}, errors.New("remediate component not found")
26+
}
27+
28+
// set up environment variables
29+
envs := []string{
30+
fmt.Sprintf("LW_COMPONENT_NAME=%s", remediateComponentName),
31+
"LW_JSON=true",
32+
"LW_NONINTERACTIVE=true",
33+
}
34+
for _, e := range cli.envs() {
35+
// don't let LW_JSON / LW_NONINTERACTIVE through here
36+
if strings.HasPrefix(e, "LW_JSON=") || strings.HasPrefix(e, "LW_NONINTERACTIVE=") {
37+
continue
38+
}
39+
envs = append(envs, e)
40+
}
41+
stdout, stderr, err := remediate.RunAndReturn([]string{"ls", "templates"}, nil, envs...)
42+
if err != nil {
43+
cli.Log.Debugw("remediate error details", "stderr", stderr)
44+
return []string{}, err
45+
}
46+
47+
var templates []map[string]interface{}
48+
err = json.Unmarshal([]byte(stdout), &templates)
49+
if err != nil {
50+
return []string{}, err
51+
}
52+
53+
templateIDs := []string{}
54+
for _, template := range templates {
55+
v, ok := template["id"]
56+
if !ok {
57+
continue
58+
}
59+
s, ok := v.(string)
60+
if !ok {
61+
continue
62+
}
63+
templateIDs = append(templateIDs, s)
64+
}
65+
return templateIDs, nil
66+
}
67+
68+
// filterFixableAlerts identifies which alerts have corresponding remediation template IDs
69+
// and returns those which don't
70+
func filterFixableAlerts(alerts api.Alerts, templateIDs []string) api.Alerts {
71+
fixableAlerts := api.Alerts{}
72+
for _, alert := range alerts {
73+
if alert.PolicyID == "" {
74+
continue
75+
}
76+
found := false
77+
// Historically alerts did not consistently populate policyID and
78+
// templates were named arbitrarily.
79+
// If and when policies explicitly reference templates we will no longer need
80+
// any inference logic.
81+
for _, id := range templateIDs {
82+
if id == alert.PolicyID {
83+
fixableAlerts = append(fixableAlerts, alert)
84+
found = true
85+
break
86+
}
87+
}
88+
if found {
89+
continue
90+
}
91+
// Another interesting problem that we have is that policyIDs are dynamic
92+
// For instance, on dev7 policy lwcustom-11 is dev7-lwcustom-11
93+
// On some other environment it might be someother-lwcustom-11
94+
dynamicIDRE := regexp.MustCompile(`^\w+-\d+$`)
95+
// Iterate through the templates looking for those with dynamic policy IDs
96+
for _, id := range templateIDs {
97+
if dynamicIDRE.MatchString(id) {
98+
// if the policyID of the alert ends with -<id>
99+
// i.e. if dev7-lwcustom-11 endswith -lwcustom-11
100+
if strings.HasSuffix(alert.PolicyID, fmt.Sprintf("-%s", id)) {
101+
fixableAlerts = append(fixableAlerts, alert)
102+
break
103+
}
104+
}
105+
}
106+
}
107+
return fixableAlerts
108+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/lacework/go-sdk/api"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestFilterFixableAlerts(t *testing.T) {
11+
alertsIn := api.Alerts{
12+
{
13+
PolicyID: "not-fixable",
14+
},
15+
{
16+
PolicyID: "lacework-global-40",
17+
},
18+
{
19+
PolicyID: "dev7-lwcustom-11",
20+
},
21+
}
22+
alertsExpected := api.Alerts{
23+
{
24+
PolicyID: "lacework-global-40",
25+
},
26+
{
27+
PolicyID: "dev7-lwcustom-11",
28+
},
29+
}
30+
alertsActual := filterFixableAlerts(
31+
alertsIn, []string{"LW_foo", "lacework-global-40", "lwcustom-11"})
32+
assert.Equal(t, alertsExpected, alertsActual)
33+
34+
alertsActual = filterFixableAlerts(alertsIn, []string{})
35+
assert.Equal(t, api.Alerts{}, alertsActual)
36+
}

cli/cmd/component.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ func isComponent(annotations map[string]string) bool {
144144
return false
145145
}
146146

147+
// IsComponentInstalled returns true if component is
148+
// valid and installed
149+
func (c *cliState) IsComponentInstalled(name string) bool {
150+
var err error
151+
c.LwComponents, err = lwcomponent.LocalState()
152+
if err != nil || c.LwComponents == nil {
153+
return false
154+
}
155+
156+
component, found := c.LwComponents.GetComponent(name)
157+
if found && component.IsInstalled() {
158+
return true
159+
}
160+
return false
161+
}
162+
147163
// LoadComponents reads the local components state and loads all installed components
148164
// of type `CLI_COMMAND` dynamically into the root command of the CLI (`rootCmd`)
149165
func (c *cliState) LoadComponents() {

cli/cmd/content_library.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,8 @@ type LaceworkContentLibrary struct {
7474
PolicyTags map[string][]string `json:"policy_tags"`
7575
}
7676

77-
func (c *cliState) IsLCLInstalled() bool {
78-
var err error
79-
c.LwComponents, err = lwcomponent.LocalState()
80-
if err != nil || c.LwComponents == nil {
81-
return false
82-
}
83-
84-
component, found := c.LwComponents.GetComponent(lclComponentName)
85-
if found && component.IsInstalled() {
86-
return true
87-
}
88-
return false
77+
func (c *cliState) isLCLInstalled() bool {
78+
return c.IsComponentInstalled(lclComponentName)
8979
}
9080

9181
func (c *cliState) LoadLCL() (*LaceworkContentLibrary, error) {

cli/cmd/content_library_internal_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ func TestLoadLCLNotFound(t *testing.T) {
306306
cli := cliState{LwComponents: new(lwcomponent.State)}
307307

308308
// IsLCLInstalled
309-
assert.Equal(t, false, cli.IsLCLInstalled())
309+
assert.Equal(t, false, cli.isLCLInstalled())
310310

311311
_, err := cli.LoadLCL()
312312
assert.Equal(
@@ -368,7 +368,7 @@ func _TestLoadLCLOK(t *testing.T) {
368368
}
369369

370370
// IsLCLInstalled
371-
assert.Equal(t, true, cli.IsLCLInstalled())
371+
assert.Equal(t, true, cli.isLCLInstalled())
372372

373373
lcl, err := cli.LoadLCL()
374374
assert.Nil(t, err)

cli/cmd/lql.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func init() {
143143
// add sub-commands to the lql command
144144
queryCmd.AddCommand(queryRunCmd)
145145

146-
if cli.IsLCLInstalled() {
146+
if cli.isLCLInstalled() {
147147
queryRunCmd.Flags().StringVarP(
148148
&queryCmdState.CURVFromLibrary,
149149
"library", "l", "",

cli/cmd/lql_create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func init() {
114114

115115
setQuerySourceFlags(queryCreateCmd)
116116

117-
if cli.IsLCLInstalled() {
117+
if cli.isLCLInstalled() {
118118
queryCreateCmd.Flags().StringVarP(
119119
&queryCmdState.CURVFromLibrary,
120120
"library", "l", "",

cli/cmd/lql_library.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ var (
4444
)
4545

4646
func init() {
47-
if cli.IsLCLInstalled() {
47+
if cli.isLCLInstalled() {
4848
queryCmd.AddCommand(queryListLibraryCmd)
4949
queryCmd.AddCommand(queryShowLibraryCmd)
5050
}

0 commit comments

Comments
 (0)