Skip to content

Commit 681643f

Browse files
committed
Added support for paging for Project Inventory and Inventory summary
bumped version to v0.0.4
1 parent c0f87c6 commit 681643f

6 files changed

Lines changed: 104 additions & 32 deletions

File tree

codeinsight_sdk/client.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,27 @@ class CodeInsightClient:
1111
def __init__(self,
1212
base_url: str,
1313
api_token: str,
14-
page_size: int = 100
1514
):
1615
self.base_url = base_url
1716
self.api_url = f"{base_url}/codeinsight/api"
18-
self.api_token = api_token
19-
self.page_size = page_size
20-
self.api_headers = {
17+
self.__api_token = api_token
18+
self.__api_headers = {
2119
'Content-Type': 'application/json',
22-
"Authorization": "Bearer %s" % self.api_token,
20+
"Authorization": "Bearer %s" % self.__api_token,
2321
"User-Agent": "codeinsight_sdk_python",
2422
}
2523

2624
def request(self, method, url_part: str, params: dict = None):
2725
url = f"{self.api_url}/{url_part}"
28-
response = requests.request(method, url, headers=self.api_headers, params=params)
26+
27+
# Iterate over params and remove any that are None (Empty)
28+
if(params):
29+
for k, v in list(params.items()):
30+
if v is None:
31+
del params[k]
32+
33+
response = requests.request(method, url,
34+
headers=self.__api_headers, params=params)
2935

3036
if not response.ok:
3137
logger.error(f"Error: {response.status_code} - {response.reason}")

codeinsight_sdk/handlers.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,22 +75,47 @@ def get_id(self, project_name:str) -> int:
7575
raise CodeInsightError(resp)
7676
return project_id
7777

78-
def get_inventory_summary(self, project_id:int) -> List[ProjectInventoryItem]:
78+
def get_inventory_summary(self, project_id:int,
79+
vulnerabilitySummary : bool = False,
80+
cvssVersion: str = 'ANY',
81+
published: str = 'ALL',
82+
offset:int = 1,
83+
limit:int = 25) -> List[ProjectInventoryItem]:
7984
"""
8085
Retrieves the inventory summary for a specific project.
8186
8287
Args:
8388
project_id (int): The ID of the project.
89+
vulnerabilitySummary (bool, optional): Flag to include vulnerability summary. Defaults to False.
90+
cvssVersion (str, optional): The CVSS version to filter vulnerabilities. Defaults to 'ANY'.
91+
published (str, optional): The publication status. Defaults to 'ALL'.
92+
offset (int, optional): The offset for pagination. Defaults to 1.
93+
limit (int, optional): The maximum number of items to return. Defaults to 25.
8494
8595
Returns:
8696
List[ProjectInventoryItem]: A list of ProjectInventoryItem objects representing the inventory summary.
8797
"""
88-
8998
path = f"projects/{project_id}/inventorySummary"
90-
resp = self.client.request("GET", url_part=path)
99+
params = {"vulnerabilitySummary": vulnerabilitySummary,
100+
"cvssVersion": cvssVersion,
101+
"published": published,
102+
"offset": offset,
103+
"limit": limit
104+
}
105+
resp = self.client.request("GET", url_part=path, params=params)
106+
current_page = int(resp.headers['current-page'])
107+
number_of_pages = int(resp.headers['number-of-pages'])
108+
total_records = int(resp.headers['total-records'])
91109
inventory = []
92110
for inv_item in resp.json()['data']:
93111
inventory.append(ProjectInventoryItem.from_dict(inv_item))
112+
113+
# Iterate through all the pages
114+
if number_of_pages > offset:
115+
params.update({"offset": offset+1})
116+
chunk = self.get_inventory_summary(project_id, **params)
117+
# Only append the inventory records
118+
inventory.extend(chunk)
94119
return inventory
95120

96121
def get_inventory(self,project_id:int,
@@ -105,12 +130,27 @@ def get_inventory(self,project_id:int,
105130
include_files: bool = True
106131
) -> ProjectInventory:
107132
path = f"project/inventory/{project_id}"
108-
#TODO: Add support for all parameters
109-
params = {"skipVulnerabilities": skip_vulnerabilities}
133+
params = {"skipVulnerabilities": skip_vulnerabilities,
134+
"published": published,
135+
"vendor": vendor,
136+
"product": product,
137+
"page": page,
138+
"pageSize": page_size,
139+
"reviewStatus": review_status,
140+
"alerts": alerts,
141+
"includeFiles": include_files}
142+
110143
resp = self.client.request("GET", url_part=path, params=params)
111144
project_inventory = resp.json()
112-
project_cls = ProjectInventory.from_dict(project_inventory)
113-
return project_cls
145+
project = ProjectInventory.from_dict(project_inventory)
146+
147+
# Iterate through all the pages
148+
if int(resp.headers['number-of-pages']) > page:
149+
chunk = self.get_inventory(project_id, page=page+1)
150+
# Only append the inventory records
151+
project.inventoryItems.extend(chunk.inventoryItems)
152+
153+
return project
114154

115155

116156
class ReportHandler(Handler):

codeinsight_sdk/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class ProjectInventoryItem(DataClassJsonMixin):
3636
createdOn: str
3737
updatedOn: str
3838
url: Optional[str] = None
39+
componentUrl: Optional[str] = None
40+
componentDescription: Optional[str] = None
3941
vulnerabilites: Optional[List[Vulnerability]] = None
4042
vulnerabilitySummary: Optional[Dict[str, Dict]] = None
4143
filePaths: Optional[List[str]] = None

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
[tool.poetry]
22
name = "codeinsight_sdk"
3-
version = "0.0.3"
3+
version = "0.0.4"
44
description = "A Python client for the Revenera Code Insight"
55
authors = ["Zachary Karpinski <1206496+zkarpinski@users.noreply.github.com>"]
66
readme = "README.md"
77
license = "Apache-2.0"
88
repository = "https://github.com/zkarpinski/codeinsight-sdk-python"
99
keywords = ["revenera", "api", "flexera", "code insight","sdk"]
10+
include = [
11+
{ path = "tests", format = "sdist" }
12+
]
1013

1114
[tool.poetry.dependencies]
1215
python = "^3.9"

tests/handlers/__init__.py

Whitespace-only changes.

tests/test_client.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ def test_endpoint_not_found(self, client):
2626
m.get(f"{TEST_URL}/codeinsight/api/projects", status_code=404)
2727
with pytest.raises(Exception):
2828
client.projects.all()
29-
30-
29+
30+
class TestProjectEndpoints:
31+
@pytest.fixture
32+
def client(self):
33+
return CodeInsightClient(TEST_URL, TEST_API_TOKEN)
34+
3135
def test_get_all_projects(self, client):
3236
with requests_mock.Mocker() as m:
3337
m.get(f"{TEST_URL}/codeinsight/api/projects", text='{"data": [{"id":1, "name":"Test"}, {"id":2, "name":"Test 2"}]}')
@@ -46,9 +50,9 @@ def test_get_project_id_invalid(self,client):
4650
fake_response_json = """{ "Arguments: " : ["",""],
4751
"Key: ": " InvalidProjectNameParm",
4852
"Error: ": "The project name entered was not found" }
49-
"""
53+
"""
5054
with requests_mock.Mocker() as m:
51-
# Note, the key names end with a colon and space '...: '
55+
# Note, the key names end with a colon and space '...: '
5256
m.get(f"{TEST_URL}/codeinsight/api/project/id", text=fake_response_json, status_code=400)
5357
with pytest.raises(CodeInsightError):
5458
client.projects.get_id(project_name)
@@ -91,24 +95,37 @@ def test_get_project(self,client):
9195
assert project.vulnerabilities["CvssV2"]["High"] == 2
9296
assert project.vulnerabilities["CvssV2"]["Unknown"] == 4
9397

94-
def test_get_project_inventory(self,client):
98+
def test_get_project_inventory_multipage(self,client):
9599
project_id = 1
100+
total_pages = 4
101+
total_records = total_pages * 2
102+
response_header = {"content-type": "application/json"}
103+
response_header["current-page"] = "1"
104+
response_header["number-of-pages"] = str(total_pages)
105+
response_header["total-records"] = str(total_records)
106+
96107
fake_response_json = """
97-
{ "projectId": 1, "inventoryItems": [
98-
{"itemNumber":1, "id":1234, "name":"Example component","type":"component","priority":"low","createdBy":"Zach","createdOn":"Today","updatedOn":"Tomorrow","componentName":"snakeyaml","componentVersionName":"2.0"},
99-
{"itemNumber":2, "id":1235, "name":"Example component 2","type":"component","priority":"low","createdBy":"Zach","createdOn":"Today","updatedOn":"Tomorrow","componentName":"snakeyaml","componentVersionName":"2.0"}
100-
]}
101-
"""
108+
{ "projectId": 1, "inventoryItems": [
109+
{"itemNumber":1, "id":1234, "name":"Example component","type":"component","priority":"low","createdBy":"Zach","createdOn":"Today","updatedOn":"Tomorrow","componentName":"snakeyaml","componentVersionName":"2.0"},
110+
{"itemNumber":2, "id":1235, "name":"Example component 2","type":"component","priority":"low","createdBy":"Zach","createdOn":"Today","updatedOn":"Tomorrow","componentName":"snakeyaml","componentVersionName":"2.0"}
111+
]}
112+
"""
102113
with requests_mock.Mocker() as m:
103114
m.get(f"{TEST_URL}/codeinsight/api/project/inventory/{project_id}",
104-
text=fake_response_json)
105-
projectInventory = client.projects.get_inventory(project_id)
106-
assert projectInventory.projectId == project_id
107-
assert len(projectInventory.inventoryItems) >= 2
115+
text=fake_response_json, headers=response_header)
116+
project_inventory = client.projects.get_inventory(project_id)
117+
assert project_inventory.projectId == project_id
118+
assert len(project_inventory.inventoryItems) == total_records
108119

109120
#### FIX THIS! ####
110121
def test_get_project_inventory_summary(self,client):
111122
project_id = 1
123+
total_pages = 4
124+
total_records = total_pages * 2
125+
response_header = {"content-type": "application/json"}
126+
response_header["current-page"] = "1"
127+
response_header["number-of-pages"] = str(total_pages)
128+
response_header["total-records"] = str(total_records)
112129
fake_response_json = """ { "data": [
113130
{
114131
"itemNumber": 1,
@@ -140,12 +157,17 @@ def test_get_project_inventory_summary(self,client):
140157
"""
141158
with requests_mock.Mocker() as m:
142159
m.get(f"{TEST_URL}/codeinsight/api/projects/{project_id}/inventorySummary",
143-
text=fake_response_json)
160+
text=fake_response_json, headers=response_header)
144161
project_inventory_summary = client.projects.get_inventory_summary(project_id)
145162

146-
assert len(project_inventory_summary) == 2
163+
assert len(project_inventory_summary) == 8
147164
assert project_inventory_summary[1].id == 12346
148165

166+
class TestReportsEndpoints:
167+
@pytest.fixture
168+
def client(self):
169+
return CodeInsightClient(TEST_URL, TEST_API_TOKEN)
170+
149171
def test_get_reports_all(self,client):
150172
fake_response_json = """ { "data": [
151173
{
@@ -203,5 +225,4 @@ def test_get_report(self,client):
203225
text=fake_response_json)
204226
report = client.reports.get(1)
205227
assert report.id == 1
206-
assert report.enabled == True
207-
228+
assert report.enabled == True

0 commit comments

Comments
 (0)