Skip to content

Commit 191918f

Browse files
authored
feat: Add vulnerability exception methods (#145)
New methods for the SdScanningClient: - add_vulnerability_exception_bundle - delete_vulnerability_exception_bundle - list_vulnerability_exception_bundles - get_vulnerability_exception_bundle - add_vulnerability_exception - delete_vulnerability_exception - update_vulnerability_exception
1 parent 7cf09bb commit 191918f

File tree

3 files changed

+314
-17
lines changed

3 files changed

+314
-17
lines changed

sdcclient/_scanning.py

Lines changed: 134 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import base64
2-
import hashlib
32
import json
43
import re
4+
import time
5+
56
import requests
67
from requests_toolbelt.multipart.encoder import MultipartEncoder
7-
import time
88

99
try:
1010
from urllib.parse import quote_plus, unquote_plus
@@ -164,7 +164,8 @@ def query_image_vuln(self, image, vuln_type="", vendor_only=True):
164164
'''
165165
return self._query_image(image, query_group='vuln', query_type=vuln_type, vendor_only=vendor_only)
166166

167-
def query_images_by_vulnerability(self, vulnerability_id, namespace=None, package=None, severity=None, vendor_only=True):
167+
def query_images_by_vulnerability(self, vulnerability_id, namespace=None, package=None, severity=None,
168+
vendor_only=True):
168169
'''**Description**
169170
Search system for images with the given vulnerability ID present
170171
@@ -408,17 +409,18 @@ def get_image_scan_result_by_id(self, image_id, full_tag_name, detail):
408409
A JSON object containing pass/fail status of image scan policy.
409410
'''
410411
url = "{base_url}/api/scanning/v1/anchore/images/by_id/{image_id}/check?tag={full_tag_name}&detail={detail}".format(
411-
base_url=self.url,
412-
image_id=image_id,
413-
full_tag_name=full_tag_name,
414-
detail=detail)
412+
base_url=self.url,
413+
image_id=image_id,
414+
full_tag_name=full_tag_name,
415+
detail=detail)
415416
res = requests.get(url, headers=self.hdrs, verify=self.ssl_verify)
416417
if not self._checkResponse(res):
417418
return [False, self.lasterr]
418419

419420
return [True, res.json()]
420421

421-
def add_registry(self, registry, registry_user, registry_pass, insecure=False, registry_type="docker_v2", validate=True):
422+
def add_registry(self, registry, registry_user, registry_pass, insecure=False, registry_type="docker_v2",
423+
validate=True):
422424
'''**Description**
423425
Add image registry
424426
@@ -437,7 +439,8 @@ def add_registry(self, registry, registry_user, registry_pass, insecure=False, r
437439
if registry_type and registry_type not in registry_types:
438440
return [False, "input registry type not supported (supported registry_types: " + str(registry_types)]
439441
if self._registry_string_is_valid(registry):
440-
return [False, "input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
442+
return [False,
443+
"input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
441444

442445
if not registry_type:
443446
registry_type = self._get_registry_type(registry)
@@ -458,7 +461,8 @@ def add_registry(self, registry, registry_user, registry_pass, insecure=False, r
458461

459462
return [True, res.json()]
460463

461-
def update_registry(self, registry, registry_user, registry_pass, insecure=False, registry_type="docker_v2", validate=True):
464+
def update_registry(self, registry, registry_user, registry_pass, insecure=False, registry_type="docker_v2",
465+
validate=True):
462466
'''**Description**
463467
Update an existing image registry.
464468
@@ -474,7 +478,8 @@ def update_registry(self, registry, registry_user, registry_pass, insecure=False
474478
A JSON object representing the registry.
475479
'''
476480
if self._registry_string_is_valid(registry):
477-
return [False, "input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
481+
return [False,
482+
"input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
478483

479484
payload = {
480485
'registry': registry,
@@ -502,7 +507,8 @@ def delete_registry(self, registry):
502507
'''
503508
# do some input string checking
504509
if re.match(".*\\/.*", registry):
505-
return [False, "input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
510+
return [False,
511+
"input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
506512

507513
url = self.url + "/api/scanning/v1/anchore/registries/" + registry
508514
res = requests.delete(url, headers=self.hdrs, verify=self.ssl_verify)
@@ -539,7 +545,8 @@ def get_registry(self, registry):
539545
A JSON object representing the registry.
540546
'''
541547
if self._registry_string_is_valid(registry):
542-
return [False, "input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
548+
return [False,
549+
"input registry name cannot contain '/' characters - valid registry names are of the form <host>:<port> where :<port> is optional"]
543550

544551
url = self.url + "/api/scanning/v1/anchore/registries/" + registry
545552
res = requests.get(url, headers=self.hdrs, verify=self.ssl_verify)
@@ -1059,4 +1066,117 @@ def get_vulnerability_details(self, id):
10591066
if "vulnerabilities" not in json_res or not json_res["vulnerabilities"]:
10601067
return [False, f"Vulnerability {id} was not found"]
10611068

1062-
return [True, json_res["vulnerabilities"][0]]
1069+
return [True, json_res["vulnerabilities"][0]]
1070+
1071+
def add_vulnerability_exception_bundle(self, name, comment=""):
1072+
if not name:
1073+
return [False, "A name is required for the exception bundle"]
1074+
1075+
url = self.url + f"/api/scanning/v1/vulnexceptions"
1076+
params = {
1077+
"version": "1_0",
1078+
"name": name,
1079+
"comment": comment,
1080+
}
1081+
1082+
data = json.dumps(params)
1083+
res = requests.post(url, headers=self.hdrs, data=data, verify=self.ssl_verify)
1084+
if not self._checkResponse(res):
1085+
return [False, self.lasterr]
1086+
1087+
return [True, res.json()]
1088+
1089+
def delete_vulnerability_exception_bundle(self, id):
1090+
1091+
url = self.url + f"/api/scanning/v1/vulnexceptions/{id}"
1092+
1093+
res = requests.delete(url, headers=self.hdrs, verify=self.ssl_verify)
1094+
if not self._checkResponse(res):
1095+
return [False, self.lasterr]
1096+
1097+
return [True, None]
1098+
1099+
def list_vulnerability_exception_bundles(self):
1100+
url = self.url + f"/api/scanning/v1/vulnexceptions"
1101+
1102+
params = {
1103+
"bundleId": "default",
1104+
}
1105+
1106+
res = requests.get(url, params=params, headers=self.hdrs, verify=self.ssl_verify)
1107+
if not self._checkResponse(res):
1108+
return [False, self.lasterr]
1109+
1110+
return [True, res.json()]
1111+
1112+
def get_vulnerability_exception_bundle(self, bundle):
1113+
url = f"{self.url}/api/scanning/v1/vulnexceptions/{bundle}"
1114+
1115+
params = {
1116+
"bundleId": "default",
1117+
}
1118+
1119+
res = requests.get(url, params=params, headers=self.hdrs, verify=self.ssl_verify)
1120+
if not self._checkResponse(res):
1121+
return [False, self.lasterr]
1122+
1123+
res_json = res.json()
1124+
for item in res_json["items"]:
1125+
item["trigger_id"] = str(item["trigger_id"]).rstrip("+*")
1126+
return [True, res_json]
1127+
1128+
def add_vulnerability_exception(self, bundle, cve, note=None, expiration_date=None):
1129+
url = f"{self.url}/api/scanning/v1/vulnexceptions/{bundle}/vulnerabilities"
1130+
1131+
params = {
1132+
"gate": "vulnerabilities",
1133+
"is_busy": False,
1134+
"trigger_id": f"{cve}+*",
1135+
"expiration_date": int(expiration_date) if expiration_date else None,
1136+
"notes": note,
1137+
}
1138+
1139+
data = json.dumps(params)
1140+
res = requests.post(url, headers=self.hdrs, data=data, verify=self.ssl_verify)
1141+
if not self._checkResponse(res):
1142+
return [False, self.lasterr]
1143+
1144+
res_json = res.json()
1145+
res_json["trigger_id"] = str(res_json["trigger_id"]).rstrip("+*")
1146+
return [True, res_json]
1147+
1148+
def delete_vulnerability_exception(self, bundle, id):
1149+
url = f"{self.url}/api/scanning/v1/vulnexceptions/{bundle}/vulnerabilities/{id}"
1150+
1151+
params = {
1152+
"bundleId": "default",
1153+
}
1154+
1155+
res = requests.delete(url, params=params, headers=self.hdrs, verify=self.ssl_verify)
1156+
if not self._checkResponse(res):
1157+
return [False, self.lasterr]
1158+
1159+
return [True, None]
1160+
1161+
def update_vulnerability_exception(self, bundle, id, cve, enabled, note, expiration_date):
1162+
url = f"{self.url}/api/scanning/v1/vulnexceptions/{bundle}/vulnerabilities/{id}"
1163+
1164+
data = {
1165+
"id": id,
1166+
"gate": "vulnerabilities",
1167+
"trigger_id": f"{cve}+*",
1168+
"enabled": enabled,
1169+
"notes": note,
1170+
"expiration_date": int(expiration_date) if expiration_date else None,
1171+
}
1172+
params = {
1173+
"bundleId": "default",
1174+
}
1175+
1176+
res = requests.put(url, data=json.dumps(data), params=params, headers=self.hdrs, verify=self.ssl_verify)
1177+
if not self._checkResponse(res):
1178+
return [False, self.lasterr]
1179+
1180+
res_json = res.json()
1181+
res_json["trigger_id"] = str(res_json["trigger_id"]).rstrip("+*")
1182+
return [True, res_json]
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import datetime
2+
import os
3+
import uuid
4+
5+
from expects import equal, expect, contain, be_empty, have_key, be_true, have_keys, not_, be_false, be_above
6+
from mamba import before, context, description, after, it
7+
8+
from sdcclient import SdScanningClient
9+
from specs import be_successful_api_call
10+
11+
with description("Scanning vulnerability exceptions") as self:
12+
with before.each:
13+
self.client = SdScanningClient(sdc_url=os.getenv("SDC_SECURE_URL", "https://secure.sysdig.com"),
14+
token=os.getenv("SDC_SECURE_TOKEN"))
15+
16+
with after.each:
17+
self.clean_bundles()
18+
19+
20+
def clean_bundles(self):
21+
_, res = self.client.list_vulnerability_exception_bundles()
22+
for bundle in res:
23+
if str(bundle["name"]).startswith("test_exception_bundle_"):
24+
call = self.client.delete_vulnerability_exception_bundle(id=bundle["id"])
25+
expect(call).to(be_successful_api_call)
26+
27+
28+
with context("when we are creating a new vulnerability exception bundle"):
29+
with it("creates the bundle correctly"):
30+
exception_bundle = f"test_exception_bundle_{uuid.uuid4()}"
31+
exception_comment = "This is an example of an exception bundle"
32+
ok, res = self.client.add_vulnerability_exception_bundle(name=exception_bundle, comment=exception_comment)
33+
34+
expect((ok, res)).to(be_successful_api_call)
35+
expect(res).to(
36+
have_keys("id", items=be_empty, policyBundleId=equal("default"), version="1_0",
37+
comment=equal(exception_comment), name=equal(exception_bundle))
38+
)
39+
40+
with it("creates the bundle correctly with name only and removes it correctly"):
41+
exception_bundle = f"test_exception_bundle_{uuid.uuid4()}"
42+
ok, res = self.client.add_vulnerability_exception_bundle(name=exception_bundle)
43+
44+
expect((ok, res)).to(be_successful_api_call)
45+
expect(res).to(
46+
have_keys("id", items=be_empty, policyBundleId=equal("default"), version="1_0",
47+
comment=be_empty, name=equal(exception_bundle))
48+
)
49+
50+
with context("when we are listing the vulnerability exception bundles"):
51+
with before.each:
52+
self.exception_bundle = f"test_exception_bundle_{uuid.uuid4()}"
53+
ok, res = self.client.add_vulnerability_exception_bundle(name=self.exception_bundle)
54+
expect((ok, res)).to(be_successful_api_call)
55+
self.created_exception_bundle = res["id"]
56+
57+
with it("retrieves the list of bundles"):
58+
ok, res = self.client.list_vulnerability_exception_bundles()
59+
60+
expect((ok, res)).to(be_successful_api_call)
61+
expect(res).to(contain(
62+
have_keys(id=self.created_exception_bundle, items=None, policyBundleId=equal("default"),
63+
version=equal("1_0"), comment=be_empty, name=equal(self.exception_bundle))
64+
))
65+
66+
with context("when we are working with vulnerability exceptions in a bundle"):
67+
with before.each:
68+
ok, res = self.client.add_vulnerability_exception_bundle(name=f"test_exception_bundle_{uuid.uuid4()}")
69+
expect((ok, res)).to(be_successful_api_call)
70+
self.created_exception_bundle = res["id"]
71+
72+
with it("is able to add a vulnerability exception to a bundle"):
73+
exception_notes = "Microsoft Vulnerability"
74+
exception_cve = "CVE-2020-1234"
75+
ok, res = self.client.add_vulnerability_exception(bundle=self.created_exception_bundle,
76+
cve=exception_cve,
77+
note=exception_notes,
78+
expiration_date=datetime.datetime(2030, 12, 31)
79+
.timestamp())
80+
81+
expect((ok, res)).to(be_successful_api_call)
82+
expect(res).to(
83+
have_keys("id", "description", gate=equal("vulnerabilities"), trigger_id=equal(exception_cve),
84+
notes=equal(exception_notes), enabled=be_true)
85+
)
86+
87+
with context("and there are existing vulnerability exceptions"):
88+
with before.each:
89+
self.created_exception_cve = "CVE-2020-1234"
90+
ok, res = self.client.add_vulnerability_exception(bundle=self.created_exception_bundle,
91+
cve=self.created_exception_cve)
92+
expect((ok, res)).to(be_successful_api_call)
93+
self.created_exception = res["id"]
94+
95+
with it("is able to list all the vulnerability exceptions from a bundle"):
96+
ok, res = self.client.get_vulnerability_exception_bundle(bundle=self.created_exception_bundle)
97+
98+
expect((ok, res)).to(be_successful_api_call)
99+
expect(res).to(
100+
have_keys(id=equal(self.created_exception_bundle),
101+
items=contain(
102+
have_keys(
103+
id=equal(self.created_exception),
104+
gate=equal("vulnerabilities"),
105+
trigger_id=equal(self.created_exception_cve),
106+
enabled=be_true,
107+
)
108+
))
109+
)
110+
111+
with it("is able to remove them"):
112+
_, ex_before = self.client.get_vulnerability_exception_bundle(bundle=self.created_exception_bundle)
113+
ok, res = self.client.delete_vulnerability_exception(bundle=self.created_exception_bundle,
114+
id=self.created_exception)
115+
_, ex_after = self.client.get_vulnerability_exception_bundle(bundle=self.created_exception_bundle)
116+
117+
expect((ok, res)).to(be_successful_api_call)
118+
expect(ex_before).to(
119+
have_key("items", contain(
120+
have_keys(
121+
id=equal(self.created_exception),
122+
gate=equal("vulnerabilities"),
123+
trigger_id=equal(self.created_exception_cve),
124+
enabled=be_true,
125+
)
126+
))
127+
)
128+
expect(ex_after).to(
129+
have_key("items", not_(contain(
130+
have_keys(
131+
id=equal(self.created_exception),
132+
gate=equal("vulnerabilities"),
133+
trigger_id=equal(self.created_exception_cve),
134+
enabled=be_true,
135+
)
136+
)))
137+
)
138+
139+
with it("is able to update them"):
140+
_, ex_before = self.client.get_vulnerability_exception_bundle(bundle=self.created_exception_bundle)
141+
142+
ok, res = self.client.update_vulnerability_exception(bundle=self.created_exception_bundle,
143+
id=self.created_exception,
144+
cve="CVE-2020-1235",
145+
enabled=False,
146+
note="Dummy note",
147+
expiration_date=datetime.datetime(2030, 12, 31)
148+
.timestamp())
149+
150+
_, ex_after = self.client.get_vulnerability_exception_bundle(bundle=self.created_exception_bundle)
151+
152+
expect((ok, res)).to(be_successful_api_call)
153+
154+
expect(ex_before).to(
155+
have_key("items", contain(
156+
have_keys(
157+
id=equal(self.created_exception),
158+
gate=equal("vulnerabilities"),
159+
trigger_id=equal(self.created_exception_cve),
160+
notes=equal(None),
161+
expiration_date=equal(None),
162+
enabled=be_true,
163+
)
164+
))
165+
)
166+
167+
expect(ex_after).to(
168+
have_key("items", contain(
169+
have_keys(
170+
id=equal(self.created_exception),
171+
gate=equal("vulnerabilities"),
172+
trigger_id=equal("CVE-2020-1235"),
173+
notes=equal("Dummy note"),
174+
expiration_date=be_above(0),
175+
enabled=be_false,
176+
)
177+
))
178+
)

0 commit comments

Comments
 (0)