Skip to content

Commit 185cd09

Browse files
feat(cli): alert show --scope ObservationTimeline (#1743)
1 parent 059f43e commit 185cd09

File tree

8 files changed

+303
-6
lines changed

8 files changed

+303
-6
lines changed

api/alerts_details.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,17 @@ const (
3434
AlertRelatedAlertsScope
3535
AlertIntegrationsScope
3636
AlertTimelineScope
37+
AlertObservationTimelineScope
3738
)
3839

3940
var AlertScopes = map[alertScope]string{
40-
AlertDetailsScope: "Details",
41-
AlertInvestigationScope: "Investigation",
42-
AlertEventsScope: "Events",
43-
AlertRelatedAlertsScope: "RelatedAlerts",
44-
AlertIntegrationsScope: "Integrations",
45-
AlertTimelineScope: "Timeline",
41+
AlertDetailsScope: "Details",
42+
AlertInvestigationScope: "Investigation",
43+
AlertEventsScope: "Events",
44+
AlertRelatedAlertsScope: "RelatedAlerts",
45+
AlertIntegrationsScope: "Integrations",
46+
AlertTimelineScope: "Timeline",
47+
AlertObservationTimelineScope: "ObservationTimeline",
4648
}
4749

4850
func (i alertScope) String() string {
@@ -72,6 +74,8 @@ func (svc *AlertsService) Get(id int, scope alertScope) (interface{}, error) {
7274
return svc.GetIntegrations(id)
7375
case AlertTimelineScope:
7476
return svc.GetTimeline(id)
77+
case AlertObservationTimelineScope:
78+
return svc.GetObservationTimeline(id)
7579
default:
7680
return nil, errors.New(fmt.Sprintf("alert scope (%s) not recognized", scope))
7781
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// Author:: Lokesh Vadlamudi (<[email protected]>)
3+
// Copyright:: Copyright 2025, Fortinet Inc.
4+
// License:: Apache License, Version 2.0
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
package api
20+
21+
import (
22+
"fmt"
23+
)
24+
25+
type AlertObservationTimeline map[string]interface{}
26+
27+
type AlertObservationTimelineResponse struct {
28+
Data []AlertObservationTimeline `json:"data"`
29+
}
30+
31+
func (svc *AlertsService) GetObservationTimeline(id int) (
32+
response AlertObservationTimelineResponse,
33+
err error,
34+
) {
35+
err = svc.client.RequestDecoder(
36+
"GET",
37+
fmt.Sprintf(apiV2AlertsDetails, id, AlertObservationTimelineScope),
38+
nil,
39+
&response,
40+
)
41+
return
42+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//
2+
// Author:: Lokesh Vadlamudi (<[email protected]>)
3+
// Copyright:: Copyright 2025, Fortinet Inc.
4+
// License:: Apache License, Version 2.0
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
package api_test
20+
21+
import (
22+
"encoding/json"
23+
"fmt"
24+
"net/http"
25+
"testing"
26+
27+
"github.com/lacework/go-sdk/v2/api"
28+
"github.com/lacework/go-sdk/v2/internal/lacework"
29+
"github.com/stretchr/testify/assert"
30+
)
31+
32+
var alertObservationTimelineJSON = `{
33+
"data": [
34+
{
35+
"description": "Remote system discovery using Ping or ARP observed",
36+
"endEpoch": 1750692691,
37+
"entities": [
38+
{
39+
"entity_key": {
40+
"repo": "docker.io/example/ecommerce-website"
41+
},
42+
"entity_text": "docker.io/example/ecommerce-website",
43+
"entity_type": "container_repo",
44+
"entity_uuid": "gt6LFSoHsasqSBfQpJncoj",
45+
"is_detail": false,
46+
"is_subject": false
47+
},
48+
{
49+
"entity_key": {
50+
"mid": "4325415338702892297",
51+
"pid_hash": "-5106375999317139109"
52+
},
53+
"entity_props": "{\"cmdline\": \"ping -c 1 -t 1 10.0.3.6\"}",
54+
"entity_text": "ping -c 1 -t 1 10.0.3.6",
55+
"entity_type": "process",
56+
"entity_uuid": "TL4dYNpFrFxZRhrT2ZqNRJ",
57+
"is_detail": true,
58+
"is_subject": false
59+
},
60+
{
61+
"entity_key": {
62+
"account": "123456789012",
63+
"principal_id": "AROAEXAMPLE:example-instance",
64+
"username": "AssumedRole/123456789012:ExampleRole"
65+
},
66+
"entity_text": "AssumedRole/123456789012:ExampleRole",
67+
"entity_type": "ct_user",
68+
"entity_uuid": "ZXXdzywqQCCRz97n9Q57Mw",
69+
"is_detail": false,
70+
"is_subject": true
71+
},
72+
{
73+
"entity_key": {
74+
"hostname": "host-1.example.com",
75+
"mid": "4325415338702892297"
76+
},
77+
"entity_props": "{\"internal_ip_addr\": \"10.0.3.226\", \"os_type\": \"linux\"}",
78+
"entity_text": "host-1.example.com",
79+
"entity_type": "machine",
80+
"entity_uuid": "muupdPVN2nZWokr8HkooLK",
81+
"is_detail": false,
82+
"is_subject": true
83+
},
84+
{
85+
"entity_key": {
86+
"cluster_id": "example-cluster",
87+
"pod_name": "ecommerce-website-58dc7b54f9-ctp2n",
88+
"pod_namespace": "default"
89+
},
90+
"entity_text": "ecommerce-website-58dc7b54f9-ctp2n",
91+
"entity_type": "k8_pod",
92+
"entity_uuid": "WWJksiQgq2zBnronZquHzN",
93+
"is_detail": true,
94+
"is_subject": false
95+
}
96+
],
97+
"formalTags": [
98+
{
99+
"customer_facing_id": "T1018",
100+
"id": "T1018",
101+
"name": "Remote System Discovery",
102+
"order_index": 25,
103+
"parent_id": "TA0007",
104+
"url": "https://attack.mitre.org/techniques/T1018"
105+
},
106+
{
107+
"customer_facing_id": "TA0007",
108+
"id": "TA0007",
109+
"name": "Discovery",
110+
"order_index": 6,
111+
"url": "https://attack.mitre.org/tactics/TA0007"
112+
}
113+
],
114+
"observationPivotEntityUuids": [
115+
"muupdPVN2nZWokr8HkooLK"
116+
],
117+
"observationType": "linux_discovery_remote_system_discovery",
118+
"recordUuid": "2e034bf4-683f-56f0-a91b-bc8c4510d2b3",
119+
"relationships": [
120+
{
121+
"dstEntityKey": {
122+
"mid": "4325415338702892297",
123+
"pid_hash": "-5161409754333993982"
124+
},
125+
"dstEntityText": "sh -x ./deepce.sh",
126+
"dstEntityType": "process",
127+
"dstEntityUuid": "CExjyKEFbc3ngP5GFeNja4",
128+
"relationshipDescriptor": "is a child process of process",
129+
"relationshipId": "c8a6fb3e23fa89ab3b977dc8de815204fe738c87",
130+
"srcEntityKey": {
131+
"mid": "4325415338702892297",
132+
"pid_hash": "-5112041715404371368"
133+
},
134+
"srcEntityText": "ping -c 1 127.0.0.1",
135+
"srcEntityType": "process",
136+
"srcEntityUuid": "EHnMTz74eJmgfb2iLpzkcx"
137+
}
138+
],
139+
"startEpoch": 1750692691
140+
}
141+
]
142+
}`
143+
144+
func TestAlertsGetObservationTimelineMethod(t *testing.T) {
145+
fakeServer := lacework.MockServer()
146+
fakeServer.MockAPI(
147+
fmt.Sprintf("Alerts/%d", alertID),
148+
func(w http.ResponseWriter, r *http.Request) {
149+
assert.Equal(t, "GET", r.Method, "GetObservationTimeline should be a GET method")
150+
fmt.Fprint(w, "{}")
151+
},
152+
)
153+
defer fakeServer.Close()
154+
155+
c, err := api.NewClient("test",
156+
api.WithToken("TOKEN"),
157+
api.WithURL(fakeServer.URL()),
158+
)
159+
assert.Nil(t, err)
160+
161+
_, err = c.V2.Alerts.GetObservationTimeline(alertID)
162+
assert.Nil(t, err)
163+
}
164+
165+
func TestAlertsGetObservationTimelineOK(t *testing.T) {
166+
mockResponse := alertObservationTimelineJSON
167+
168+
fakeServer := lacework.MockServer()
169+
fakeServer.MockAPI(
170+
fmt.Sprintf("Alerts/%d", alertID),
171+
func(w http.ResponseWriter, r *http.Request) {
172+
fmt.Fprint(w, mockResponse)
173+
},
174+
)
175+
defer fakeServer.Close()
176+
177+
c, err := api.NewClient("test",
178+
api.WithToken("TOKEN"),
179+
api.WithURL(fakeServer.URL()),
180+
)
181+
assert.Nil(t, err)
182+
// Get actual response from SDK method
183+
resp, err := c.V2.Alerts.GetObservationTimeline(alertID)
184+
assert.Nil(t, err)
185+
186+
// Marshal actual output back to JSON
187+
actualJSON, err := json.Marshal(resp)
188+
assert.Nil(t, err)
189+
190+
// Compare JSON using assert.JSONEq
191+
assert.JSONEq(t, mockResponse, string(actualJSON))
192+
193+
}
194+
195+
func TestAlertsGetObservationTimelineError(t *testing.T) {
196+
fakeServer := lacework.MockServer()
197+
fakeServer.MockAPI(
198+
fmt.Sprintf("Alerts/%d", alertID),
199+
func(w http.ResponseWriter, r *http.Request) {
200+
http.Error(w, lqlErrorReponse, http.StatusInternalServerError)
201+
},
202+
)
203+
defer fakeServer.Close()
204+
205+
c, err := api.NewClient("test",
206+
api.WithToken("TOKEN"),
207+
api.WithURL(fakeServer.URL()),
208+
)
209+
assert.Nil(t, err)
210+
211+
_, err = c.V2.Alerts.GetObservationTimeline(alertID)
212+
assert.NotNil(t, err)
213+
}

cli/cmd/alert_show.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The following alert detail scopes are available:
4545
* RelatedAlerts
4646
* Integrations
4747
* Timeline
48+
* ObservationTimeline
4849
4950
View an alert's timeline details:
5051
@@ -88,6 +89,8 @@ func showAlert(_ *cobra.Command, args []string) error {
8889
err = showAlertIntegrations(id)
8990
case api.AlertTimelineScope.String():
9091
err = showAlertTimeline(id)
92+
case api.AlertObservationTimelineScope.String():
93+
err = showAlertObservationTimeline(id)
9194
default:
9295
err = errors.New(fmt.Sprintf("scope (%s) is not recognized", alertCmdState.Scope))
9396
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package cmd
2+
3+
import (
4+
"github.com/pkg/errors"
5+
)
6+
7+
func showAlertObservationTimeline(id int) error {
8+
cli.StartProgress(" Fetching alert observation timeline...")
9+
observationtimelineResponse, err := cli.LwApi.V2.Alerts.GetObservationTimeline(id)
10+
cli.StopProgress()
11+
if err != nil {
12+
return errors.Wrap(err, "unable to show alert")
13+
}
14+
15+
return cli.OutputJSON(observationtimelineResponse.Data)
16+
}

cli/docs/lacework_alert_show.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The following alert detail scopes are available:
2323
* RelatedAlerts
2424
* Integrations
2525
* Timeline
26+
* ObservationTimeline
2627

2728
View an alert's timeline details:
2829

integration/alert_show_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,20 @@ func TestAlertShowTimeline(t *testing.T) {
160160
assert.Empty(t, err.String(), "STDERR should be empty")
161161
assert.Equal(t, 0, exitcode, "EXITCODE is not the expected one")
162162
}
163+
164+
func TestAlertShowObservationTimeline(t *testing.T) {
165+
out, err, exitcode := LaceworkCLIWithTOMLConfig("alert", "show", alertShowID, "--scope", "ObservationTimeline")
166+
167+
if strings.Contains(err.String(), "[404] Not found") {
168+
return
169+
}
170+
assert.Contains(t, out.String(), `"entities"`)
171+
assert.Contains(t, out.String(), "For further investigation")
172+
assert.Empty(t, err.String(), "STDERR should be empty")
173+
assert.Equal(t, 0, exitcode, "EXITCODE is not the expected one")
174+
175+
out, err, exitcode = LaceworkCLIWithTOMLConfig("alert", "show", alertShowID, "--scope", "ObservationTimeline", "--json")
176+
assert.Contains(t, out.String(), `"description"`)
177+
assert.Empty(t, err.String(), "STDERR should be empty")
178+
assert.Equal(t, 0, exitcode, "EXITCODE is not the expected one")
179+
}

integration/test_resources/help/alert_show

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The following alert detail scopes are available:
1111
* RelatedAlerts
1212
* Integrations
1313
* Timeline
14+
* ObservationTimeline
1415

1516
View an alert's timeline details:
1617

0 commit comments

Comments
 (0)