Skip to content

Commit ff1f2d6

Browse files
authored
feat: New ActivityAuditClientV1 to query Activity Audit events (#197)
* feat: New ActivityAuditClientV1 to query Activity Audit events * chore: Deprecate old command audit methods * refactor: Import datetime package directly * fix(lint): Solve linting problems * fix(ci): Change the image to scan to quay.io/sysdig/agent
1 parent 5a93a4c commit ff1f2d6

File tree

6 files changed

+219
-20
lines changed

6 files changed

+219
-20
lines changed

sdcclient/_secure.py

+4
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,8 @@ def list_commands_audit(self, from_sec=None, to_sec=None, scope_filter=None, com
496496
'''**Description**
497497
List the commands audit.
498498
499+
**DEPRECATED**: Use sdcclient.secure.ActivityAuditClientV1 instead. This is maintained for old on-prem versions, but will be removed over time.
500+
499501
**Arguments**
500502
- from_sec: the start of the timerange for which to get commands audit.
501503
- end_sec: the end of the timerange for which to get commands audit.
@@ -528,6 +530,8 @@ def get_command_audit(self, id, metrics=[]):
528530
'''**Description**
529531
Get a command audit.
530532
533+
**DEPRECATED**: Use sdcclient.secure.ActivityAuditClientV1 instead. This is maintained for old on-prem versions, but will be removed over time.
534+
531535
**Arguments**
532536
- id: the id of the command audit to get.
533537

sdcclient/secure/__init__.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from ._activity_audit_v1 import ActivityAuditClientV1, ActivityAuditDataSource
12
from ._falco_rules_files_old import FalcoRulesFilesClientOld
23
from ._policy_events_old import PolicyEventsClientOld
34
from ._policy_events_v1 import PolicyEventsClientV1
4-
from ._policy_v2 import PolicyClientV2, policy_action_pause, policy_action_stop, policy_action_kill, \
5-
policy_action_capture
5+
from ._policy_v2 import policy_action_capture, policy_action_kill, policy_action_pause, policy_action_stop, \
6+
PolicyClientV2
67

78
__all__ = ["PolicyEventsClientOld", "PolicyEventsClientV1", "FalcoRulesFilesClientOld",
8-
"PolicyClientV2", "policy_action_pause", "policy_action_stop", "policy_action_kill", "policy_action_capture"]
9+
"PolicyClientV2", "policy_action_pause", "policy_action_stop", "policy_action_kill", "policy_action_capture",
10+
"ActivityAuditClientV1", "ActivityAuditDataSource"]
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import datetime
2+
3+
from sdcclient._common import _SdcCommon
4+
5+
6+
class ActivityAuditDataSource:
7+
CMD = "command"
8+
NET = "connection"
9+
KUBE_EXEC = "kubernetes"
10+
FILE = "fileaccess"
11+
12+
13+
_seconds_to_nanoseconds = 10 ** 9
14+
15+
16+
class ActivityAuditClientV1(_SdcCommon):
17+
def __init__(self, token="", sdc_url='https://secure.sysdig.com', ssl_verify=True, custom_headers=None):
18+
super(ActivityAuditClientV1, self).__init__(token, sdc_url, ssl_verify, custom_headers)
19+
self.product = "SDS"
20+
21+
def list_events(self, from_date=None, to_date=None, scope_filter=None, limit=0,
22+
data_sources=None):
23+
"""
24+
List the events in the Activity Audit.
25+
26+
Args:
27+
from_date (datetime.datetime): the start of the time range from which to get events. The default value is yesterday.
28+
to_date (datetime.datetime): the end of the time range from which to get events. The default value is now.
29+
scope_filter (List): a list of Sysdig Monitor-like filter (e.g `processName in ("ubuntu")`).
30+
limit (int): max number of events to retrieve. A limit of 0 or negative will retrieve all events.
31+
data_sources (List): a list of data sources to retrieve events from. None or an empty list retrieves all events.
32+
33+
Examples:
34+
>>> client = ActivityAuditClientV1(token=SECURE_TOKEN)
35+
>>>
36+
>>> now = datetime.datetime.utcnow()
37+
>>> three_days_ago = now - datetime.timedelta(days=3)
38+
>>> max_event_number_retrieved = 50
39+
>>> data_sources = [ActivityAuditDataSource.CMD, ActivityAuditDataSource.KUBE_EXEC]
40+
>>>
41+
>>> ok, events = client.list_events(from_date=three_days_ago,
42+
>>> to_date=now,
43+
>>> limit=max_event_number_retrieved,
44+
>>> data_sources=data_sources)
45+
46+
Returns:
47+
A list of event objects from the Activity Audit.
48+
"""
49+
number_of_events_per_query = 50
50+
51+
if from_date is None:
52+
from_date = datetime.datetime.utcnow() - datetime.timedelta(days=1)
53+
if to_date is None:
54+
to_date = datetime.datetime.utcnow()
55+
56+
filters = scope_filter if scope_filter else []
57+
if data_sources:
58+
quoted_data_sources = [f'"{data_source}"' for data_source in data_sources]
59+
data_source_filter = f'type in ({",".join(quoted_data_sources)})'
60+
filters.append(data_source_filter)
61+
62+
query_params = {
63+
"from": int(from_date.timestamp()) * _seconds_to_nanoseconds,
64+
"to": int(to_date.timestamp()) * _seconds_to_nanoseconds,
65+
"limit": number_of_events_per_query,
66+
"filter": " and ".join(filters),
67+
}
68+
69+
res = self.http.get(self.url + '/api/v1/activityAudit/events', headers=self.hdrs, verify=self.ssl_verify,
70+
params=query_params)
71+
ok, res = self._request_result(res)
72+
if not ok:
73+
return False, res
74+
75+
events = []
76+
77+
# Pagination required by Secure API
78+
while "page" in res and \
79+
"total" in res["page"] and \
80+
res["page"]["total"] > number_of_events_per_query:
81+
events = events + res["data"]
82+
83+
if 0 < limit < len(events):
84+
events = events[0:limit - 1]
85+
break
86+
87+
paginated_query_params = {
88+
"limit": number_of_events_per_query,
89+
"filter": " and ".join(filters),
90+
"cursor": res["page"]["prev"]
91+
}
92+
93+
res = self.http.get(self.url + '/api/v1/activityAudit/events', headers=self.hdrs, verify=self.ssl_verify,
94+
params=paginated_query_params)
95+
ok, res = self._request_result(res)
96+
if not ok:
97+
return False, res
98+
else:
99+
events = events + res["data"]
100+
101+
return True, events
102+
103+
def list_trace(self, traceable_event):
104+
"""
105+
Lists the events from an original traceable event.
106+
107+
Args:
108+
traceable_event(object): an event retrieved from the list_events method. The event must be traceable,
109+
this is, it must have the "traceable" key as true.
110+
111+
Examples:
112+
>>> client = ActivityAuditClientV1(token=SECURE_TOKEN)
113+
>>>
114+
>>> ok, events = client.list_events()
115+
>>> if not ok:
116+
>>> return
117+
>>> traceable_events = [event for event in events if event["traceable"]]
118+
>>>
119+
>>> ok, trace = client.list_trace(traceable_events[0])
120+
>>> if not ok:
121+
>>> return
122+
>>>
123+
>>> for event in trace:
124+
>>> print(event)
125+
126+
Returns:
127+
All the related events that are the trace of the given event.
128+
"""
129+
if not traceable_event or not traceable_event["traceable"]:
130+
return False, "a traceable event must be provided"
131+
132+
endpoint = f'/api/v1/activityAudit/events/{traceable_event["type"]}/{traceable_event["id"]}/trace'
133+
res = self.http.get(self.url + endpoint, headers=self.hdrs, verify=self.ssl_verify)
134+
ok, res = self._request_result(res)
135+
if not ok:
136+
return False, res
137+
return True, res["data"]

specs/secure/activitylog_v1_spec.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import datetime
2+
import os
3+
4+
from expects import be_above, be_empty, contain, expect, have_keys, have_len
5+
from mamba import _it, before, context, description, it
6+
7+
from sdcclient.secure import ActivityAuditClientV1 as ActivityAuditClient, ActivityAuditDataSource
8+
from specs import be_successful_api_call
9+
10+
with description("Activity Audit v1", "integration") as self:
11+
with before.all:
12+
self.client = ActivityAuditClient(sdc_url=os.getenv("SDC_SECURE_URL", "https://secure.sysdig.com"),
13+
token=os.getenv("SDC_SECURE_TOKEN"))
14+
15+
with it("is able to list the most recent commands with the default parameters"):
16+
ok, res = self.client.list_events()
17+
18+
expect((ok, res)).to(be_successful_api_call)
19+
expect(res).to_not(be_empty)
20+
21+
with context("when listing the most recent commands with a limit of 5"):
22+
with it("retrieves the 5 events"):
23+
ok, res = self.client.list_events(limit=5)
24+
25+
expect((ok, res)).to(be_successful_api_call)
26+
expect(res).to_not(have_len(5))
27+
28+
with context("when listing the events from the last 3 days"):
29+
with it("retrieves all the events"):
30+
three_days_ago = datetime.datetime.utcnow() - datetime.timedelta(days=3)
31+
ok, res = self.client.list_events(from_date=three_days_ago)
32+
33+
expect((ok, res)).to(be_successful_api_call)
34+
expect(res).to_not(be_empty)
35+
36+
with context("when listing events from a specific type"):
37+
with it("retrieves the events of this event type only"):
38+
ok, res = self.client.list_events(data_sources=[ActivityAuditDataSource.CMD])
39+
40+
expect((ok, res)).to(be_successful_api_call)
41+
expect(res).to(contain(have_keys(type=ActivityAuditDataSource.CMD)))
42+
expect(res).to_not(contain(have_keys(type=ActivityAuditDataSource.KUBE_EXEC)))
43+
expect(res).to_not(contain(have_keys(type=ActivityAuditDataSource.FILE)))
44+
expect(res).to_not(contain(have_keys(type=ActivityAuditDataSource.NET)))
45+
46+
with context("when retrieving the inner events of a traceable event"):
47+
with _it("retrieves the trace of these events"):
48+
ok, res = self.client.list_events(data_sources=[ActivityAuditDataSource.KUBE_EXEC])
49+
expect((ok, res)).to(be_successful_api_call)
50+
51+
expect(res).to(contain(have_keys(traceable=True)))
52+
53+
traceable_events = [event for event in res if event["traceable"]]
54+
ok, res = self.client.list_trace(traceable_events[0])
55+
56+
expect((ok, res)).to(be_successful_api_call)
57+
expect(res).to(contain(have_keys(type=ActivityAuditDataSource.CMD)))
58+
expect(res).to(have_len(be_above(0))) # Not using be_empty, because we want to ensure this is a list

specs/secure/scanning/policy_evaluation_spec.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,47 @@
1111
with before.all:
1212
self.client = SdScanningClient(sdc_url=os.getenv("SDC_SECURE_URL", "https://secure.sysdig.com"),
1313
token=os.getenv("SDC_SECURE_TOKEN"))
14+
self.image_name = "quay.io/sysdig/agent:latest"
1415

1516
with it("is able to retrieve the results for all the policies"):
16-
image_name = "alpine:latest"
17-
ok, res = self.client.get_image_scanning_results(image_name)
17+
ok, res = self.client.get_image_scanning_results(self.image_name)
1818

1919
expect((ok, res)).to(be_successful_api_call)
2020
expect(res).to(
2121
have_keys("image_digest", "image_id", "stop_results",
2222
total_warn=be_above_or_equal(0), total_stop=be_above_or_equal(0),
2323
last_evaluation=be_an(datetime),
24-
status="pass", image_tag="docker.io/alpine:latest",
24+
status="pass", image_tag=self.image_name,
2525
policy_id="*", policy_name="All policies",
2626
warn_results=not_(be_empty))
2727
)
2828

2929
with it("is able to retrieve the results for the default policy"):
30-
image_name = "alpine:latest"
3130
policy_id = "default"
32-
ok, res = self.client.get_image_scanning_results(image_name, policy_id)
31+
ok, res = self.client.get_image_scanning_results(self.image_name, policy_id)
3332

3433
expect((ok, res)).to(be_successful_api_call)
3534
expect(res).to(
3635
have_keys("image_digest", "image_id", "stop_results",
3736
total_warn=be_above_or_equal(0), total_stop=be_above_or_equal(0),
3837
last_evaluation=be_an(datetime),
39-
status="pass", image_tag="docker.io/alpine:latest",
38+
status="pass", image_tag=self.image_name,
4039
policy_id="default", policy_name="DefaultPolicy",
4140
warn_results=not_(be_empty))
4241
)
4342

4443
with context("but the image has not been scanned yet"):
4544
with it("returns an error saying that the image has not been found"):
46-
image_name = "unknown_image"
47-
ok, res = self.client.get_image_scanning_results(image_name)
45+
ok, res = self.client.get_image_scanning_results("unknown_image")
4846

4947
expect((ok, res)).to_not(be_successful_api_call)
5048
expect(res).to(equal("could not retrieve image digest for the given image name, "
5149
"ensure that the image has been scanned"))
5250

5351
with context("but the provided policy id does not exist"):
5452
with it("returns an error saying that the policy id is not found"):
55-
image_name = "alpine"
5653
policy_id = "unknown_policy_id"
57-
ok, res = self.client.get_image_scanning_results(image_name, policy_id)
54+
ok, res = self.client.get_image_scanning_results(self.image_name, policy_id)
5855

5956
expect((ok, res)).to_not(be_successful_api_call)
6057
expect(res).to(equal("the specified policy ID doesn't exist"))

specs/secure/scanning/query_image_content_spec.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,41 @@
1010
with before.each:
1111
self.client = SdScanningClient(sdc_url=os.getenv("SDC_SECURE_URL", "https://secure.sysdig.com"),
1212
token=os.getenv("SDC_SECURE_TOKEN"))
13+
self.image_to_scan = "quay.io/sysdig/agent:latest"
1314

1415
with it("is able to retrieve the OS contents"):
15-
ok, res = self.client.query_image_content("alpine:latest", "os")
16+
ok, res = self.client.query_image_content(self.image_to_scan, "os")
1617

1718
expect((ok, res)).to(be_successful_api_call)
1819
expect(res["content"]).to(contain(have_keys("license", "origin", "package", "size", "type", "version")))
1920
expect(res["content_type"]).to(equal("os"))
2021

2122
with it("is able to retrieve the npm contents"):
22-
ok, res = self.client.query_image_content("alpine:latest", "npm")
23+
ok, res = self.client.query_image_content(self.image_to_scan, "npm")
2324

2425
expect((ok, res)).to(be_successful_api_call)
2526
expect(res["content_type"]).to(equal("npm"))
2627

2728
with it("is able to retrieve the gem contents"):
28-
ok, res = self.client.query_image_content("alpine:latest", "gem")
29+
ok, res = self.client.query_image_content(self.image_to_scan, "gem")
2930

3031
expect((ok, res)).to(be_successful_api_call)
3132
expect(res["content_type"]).to(equal("gem"))
3233

3334
with it("is able to retrieve the python contents"):
34-
ok, res = self.client.query_image_content("alpine:latest", "python")
35+
ok, res = self.client.query_image_content(self.image_to_scan, "python")
3536

3637
expect((ok, res)).to(be_successful_api_call)
3738
expect(res["content_type"]).to(equal("python"))
3839

3940
with it("is able to retrieve the java contents"):
40-
ok, res = self.client.query_image_content("alpine:latest", "java")
41+
ok, res = self.client.query_image_content(self.image_to_scan, "java")
4142

4243
expect((ok, res)).to(be_successful_api_call)
4344
expect(res["content_type"]).to(equal("java"))
4445

4546
with it("is able to retrieve the files contents"):
46-
ok, res = self.client.query_image_content("alpine:latest", "files")
47+
ok, res = self.client.query_image_content(self.image_to_scan, "files")
4748

4849
expect((ok, res)).to(be_successful_api_call)
4950
expect(res["content"]).to(
@@ -52,7 +53,7 @@
5253

5354
with context("when the type is not in the supported list"):
5455
with it("returns an error indicating the type is incorrect"):
55-
ok, res = self.client.query_image_content("alpine:latest", "Unknown")
56+
ok, res = self.client.query_image_content(self.image_to_scan, "Unknown")
5657

5758
expect((ok, res)).not_to(be_successful_api_call)
5859
expect(res).to(equal(

0 commit comments

Comments
 (0)