Skip to content

Commit 3d3e831

Browse files
authored
Add API endpoint to get revisions with CVEs data (#5108)
* Rename CVE endpoints for better flexibility * Replace availability API with getting list of revisions from folder contents * Rename function for better clarity * Update JSON response format for consistency with other endpoints
1 parent 13885f9 commit 3d3e831

5 files changed

Lines changed: 72 additions & 37 deletions

File tree

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,49 @@
11
import unittest
2-
from unittest.mock import patch, MagicMock
2+
from unittest.mock import patch
33

44
from webapp.publisher.cve.cve_helper import CveHelper
5+
from werkzeug.exceptions import NotFound
56

67

7-
class HasCvesTest(unittest.TestCase):
8+
class HasRevisionsWithCvesTest(unittest.TestCase):
89

9-
def setUp(self):
10-
self.file_metadata = {"download_url": "https://example.com/file.json"}
10+
@patch("webapp.publisher.cve.cve_helper.CveHelper._get_cve_file_metadata")
11+
def test_returns_revision_numbers(self, mock_get_metadata):
12+
mock_get_metadata.return_value = [
13+
{"name": "123.yaml"},
14+
{"name": "456.yaml"},
15+
{"name": "789.yaml"},
16+
]
1117

12-
@patch("requests.get")
13-
def test_has_cve_data(self, mock_get):
14-
mock_get.side_effect = [
15-
MagicMock(status_code=200, json=lambda: self.file_metadata),
18+
result = CveHelper.get_revisions_with_cves("my-snap")
19+
self.assertEqual(result, [123, 456, 789])
20+
21+
@patch("webapp.publisher.cve.cve_helper.CveHelper._get_cve_file_metadata")
22+
def test_ignores_non_yaml_files(self, mock_get_metadata):
23+
mock_get_metadata.return_value = [
24+
{"name": "README.md"},
25+
{"name": "123.yaml"},
26+
{"name": "abc.yaml"},
27+
{"name": "456.yaml"},
28+
{"name": "data.txt"},
1629
]
1730

18-
result = CveHelper.has_cve_data("my-snap")
19-
self.assertTrue(result)
31+
result = CveHelper.get_revisions_with_cves("my-snap")
32+
self.assertEqual(result, [123, 456])
2033

21-
@patch("requests.get")
22-
def test_has_cve_data_not_found(self, mock_get):
23-
mock_get.side_effect = [
24-
MagicMock(status_code=404, json=lambda: {}),
34+
@patch("webapp.publisher.cve.cve_helper.CveHelper._get_cve_file_metadata")
35+
def test_returns_empty_list_if_no_revision_files(self, mock_get_metadata):
36+
mock_get_metadata.return_value = [
37+
{"name": "README.md"},
38+
{"name": "notes.txt"},
2539
]
2640

27-
result = CveHelper.has_cve_data("my-snap")
28-
self.assertFalse(result)
41+
result = CveHelper.get_revisions_with_cves("my-snap")
42+
self.assertEqual(result, [])
43+
44+
@patch("webapp.publisher.cve.cve_helper.CveHelper._get_cve_file_metadata")
45+
def test_returns_empty_list_on_not_found(self, mock_get_metadata):
46+
mock_get_metadata.side_effect = NotFound()
47+
48+
result = CveHelper.get_revisions_with_cves("my-snap")
49+
self.assertEqual(result, [])

tests/publisher/cve/test_has_cve_api.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def _set_user_is_canonical(self, value):
3131

3232
class TestModelServiceEndpoints(TestEndpoints):
3333
@patch(
34-
"webapp.publisher.cve.cve_helper.CveHelper.has_cve_data",
35-
return_value=True,
34+
"webapp.publisher.cve.cve_helper.CveHelper.get_revisions_with_cves",
35+
return_value=[123, 321],
3636
)
3737
@patch(
3838
"canonicalwebteam.store_api.dashboard.Dashboard.get_snap_info",
@@ -41,15 +41,16 @@ class TestModelServiceEndpoints(TestEndpoints):
4141
def test_has_cves_for_canonical_user(self, mock_get_snap_info, mock_get):
4242
self._set_user_is_canonical(True)
4343

44-
response = self.client.get("api/snaps/cve/test")
44+
response = self.client.get("api/test/cves")
4545
data = response.json
4646

4747
self.assertEqual(response.status_code, 200)
4848
self.assertEqual(data["success"], True)
49+
self.assertEqual(data["data"]["revisions"], [123, 321])
4950

5051
@patch(
51-
"webapp.publisher.cve.cve_helper.CveHelper.has_cve_data",
52-
return_value=False,
52+
"webapp.publisher.cve.cve_helper.CveHelper.get_revisions_with_cves",
53+
return_value=[],
5354
)
5455
@patch(
5556
"canonicalwebteam.store_api.dashboard.Dashboard.get_snap_info",
@@ -58,14 +59,14 @@ def test_has_cves_for_canonical_user(self, mock_get_snap_info, mock_get):
5859
def test_has_cves_no_data(self, mock_get_snap_info, mock_get):
5960
self._set_user_is_canonical(True)
6061

61-
response = self.client.get("api/snaps/cve/test")
62+
response = self.client.get("api/test/cves")
6263
data = response.json
6364

6465
self.assertEqual(response.status_code, 404)
6566
self.assertEqual(data["success"], False)
6667

6768
def test_has_cves_for_non_canonical_user(self):
68-
response = self.client.get("api/snaps/cve/test")
69+
response = self.client.get("api/test/cves")
6970
data = response.json
7071

7172
self.assertEqual(response.status_code, 403)

webapp/publisher/cve/cve_helper.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
from os import getenv
33
import requests
4+
import re
45

56
from werkzeug.exceptions import NotFound
67

@@ -110,14 +111,24 @@ def _fetch_file_content(snap_name, revision, file_metadata):
110111
raise NotFound
111112

112113
@staticmethod
113-
def has_cve_data(snap_name):
114+
def get_revisions_with_cves(snap_name):
114115
try:
115-
CveHelper._get_cve_file_metadata(
116-
"snap-cves/{}.json".format(snap_name)
116+
contents = CveHelper._get_cve_file_metadata(
117+
f"snap-cves/{snap_name}"
117118
)
118-
return True
119+
120+
# find all revision YAML files in the folder
121+
# e.g., 123.yaml, 456.yaml, 789.yaml
122+
# and extract the revision numbers
123+
revision_files = [
124+
int(match.group(1))
125+
for item in contents
126+
if (match := re.match(r"(\d+)\.yaml$", item["name"]))
127+
]
128+
129+
return revision_files
119130
except NotFound:
120-
return False
131+
return []
121132

122133
@staticmethod
123134
def get_cve_with_revision(snap_name, revision):

webapp/publisher/cve/cve_views.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,29 @@ def can_user_access_cve_data(snap_name):
4040

4141

4242
@login_required
43-
def has_cves(snap_name):
43+
def get_revisions_with_cves(snap_name):
4444

4545
# Check if the user has access to CVE data for the given snap
4646
has_access, error_message, status_code = can_user_access_cve_data(
4747
snap_name
4848
)
4949
if not has_access:
5050
return (
51-
flask.jsonify({"success": False, "error": error_message}),
51+
flask.jsonify({"success": False, "message": error_message}),
5252
status_code,
5353
)
5454

55-
snap_has_cves = CveHelper.has_cve_data(snap_name)
56-
if snap_has_cves:
57-
return flask.jsonify({"success": True})
55+
revisions_with_cves = CveHelper.get_revisions_with_cves(snap_name)
56+
if len(revisions_with_cves) > 0:
57+
return flask.jsonify(
58+
{"success": True, "data": {"revisions": revisions_with_cves}}
59+
)
5860
else:
5961
return (
6062
flask.jsonify(
6163
{
6264
"success": False,
63-
"error": f"CVEs data for '{snap_name}' snap not found.",
65+
"message": f"CVEs data for '{snap_name}' snap not found.",
6466
}
6567
),
6668
404,

webapp/publisher/snaps/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,13 +281,13 @@
281281

282282
# CVE API
283283
publisher_snaps.add_url_rule(
284-
"/api/snaps/cve/<snap_name>/<revision>",
284+
"/api/<snap_name>/<revision>/cves",
285285
view_func=cve_views.get_cves,
286286
)
287287

288288
publisher_snaps.add_url_rule(
289-
"/api/snaps/cve/<snap_name>",
290-
view_func=cve_views.has_cves,
289+
"/api/<snap_name>/cves",
290+
view_func=cve_views.get_revisions_with_cves,
291291
)
292292

293293

0 commit comments

Comments
 (0)