Skip to content

Commit 43beb50

Browse files
committed
feat: add expand requirement file endpoint
1 parent b9ff144 commit 43beb50

File tree

16 files changed

+128
-27
lines changed

16 files changed

+128
-27
lines changed

app/constants.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ class ResponseCode:
4747
NO_DEPENDENCIES_PACKAGE = "no_dependencies_package"
4848
NO_DEPENDENCIES_VERSION = "no_dependencies_version"
4949
REPOSITORY_PROCESSING_IN_PROGRESS = "repository_processing_in_progress"
50+
EXPAND_REQ_FILE_SUCCESS = "expand_req_file_success"
5051
EXPAND_PACKAGE_SUCCESS = "expand_package_success"
5152
EXPAND_VERSION_SUCCESS = "expand_version_success"
5253

5354
# Not found errors
55+
REQ_FILE_NOT_FOUND = "req_file_not_found"
5456
PACKAGE_NOT_FOUND = "package_not_found"
5557
VERSION_NOT_FOUND = "version_not_found"
5658
DATE_NOT_FOUND = "date_not_found"
@@ -105,12 +107,14 @@ class ResponseMessage:
105107
NO_DEPENDENCIES_PACKAGE = "The package has no dependencies"
106108
NO_DEPENDENCIES_VERSION = "The package version has no dependencies"
107109
REPOSITORY_PROCESSING = "The repository is already being processed"
110+
REQ_FILE_EXPANSION_RETRIEVED_SUCCESS = "Requirement file expansion data retrieved successfully"
108111
PACKAGE_EXPANSION_RETRIEVED_SUCCESS = "Package expansion data retrieved successfully"
109112
VERSION_EXPANSION_RETRIEVED_SUCCESS = "Version expansion data retrieved successfully"
110113

111114
# Not found errors
115+
REQ_FILE_NOT_FOUND = "The requested requirement file was not found"
112116
PACKAGE_NOT_FOUND = "The requested package was not found"
113-
VERSION_NOT_FOUND = "The requested version was not found"
117+
VERSION_NOT_FOUND = "The requested version was not found, or don't have dependencies"
114118

115119
# Error messages - General
116120
VALIDATION_ERROR = "Validation error"

app/controllers/graph_controller.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616
from app.limiter import limiter
1717
from app.schemas import (
1818
ExpandPackageRequest,
19+
ExpandReqFileRequest,
1920
ExpandVersionRequest,
2021
GetPackageStatusRequest,
2122
GetVersionStatusRequest,
2223
InitPackageRequest,
2324
InitRepositoryRequest,
2425
PackageMessageSchema,
2526
)
26-
from app.services import PackageService, RepositoryService, VersionService
27+
from app.services import (
28+
PackageService,
29+
RepositoryService,
30+
VersionService,
31+
)
2732
from app.utils import JSONEncoder, RedisQueue
2833

2934
router = APIRouter()
@@ -225,7 +230,44 @@ async def init_repository(
225230
}),
226231
)
227232

228-
@router.post(
233+
@router.get(
234+
"/graph/expand/req_file",
235+
summary="Expand Requirement File",
236+
description="Return requirement file info to expand its versions in the graph visualization.",
237+
response_description="Requirement file expansion data.",
238+
dependencies=[Depends(get_dual_auth_bearer())],
239+
tags=["Secure Chain Depex - Graph"]
240+
)
241+
@limiter.limit("25/minute")
242+
async def expand_req_file(
243+
request: Request,
244+
expand_req_file_request: ExpandReqFileRequest = Depends(),
245+
package_service: PackageService = Depends(get_package_service),
246+
json_encoder: JSONEncoder = Depends(get_json_encoder),
247+
) -> JSONResponse:
248+
expansion_data = await package_service.read_packages_expansion_by_req_file(
249+
expand_req_file_request.requirement_file_id
250+
)
251+
if expansion_data is None:
252+
return JSONResponse(
253+
status_code=status.HTTP_404_NOT_FOUND,
254+
content=json_encoder.encode(
255+
{
256+
"code": ResponseCode.REQ_FILE_NOT_FOUND,
257+
"message": ResponseMessage.REQ_FILE_NOT_FOUND,
258+
}
259+
),
260+
)
261+
return JSONResponse(
262+
status_code=status.HTTP_200_OK,
263+
content=json_encoder.encode({
264+
"code": ResponseCode.EXPAND_REQ_FILE_SUCCESS,
265+
"message": ResponseMessage.REQ_FILE_EXPANSION_RETRIEVED_SUCCESS,
266+
"data": expansion_data
267+
})
268+
)
269+
270+
@router.get(
229271
"/graph/expand/package",
230272
summary="Expand Package",
231273
description="Return package info to expand its versions in the graph visualization.",
@@ -236,11 +278,11 @@ async def init_repository(
236278
@limiter.limit("25/minute")
237279
async def expand_package(
238280
request: Request,
239-
expand_package_request: ExpandPackageRequest,
281+
expand_package_request: ExpandPackageRequest = Depends(),
240282
version_service: VersionService = Depends(get_version_service),
241283
json_encoder: JSONEncoder = Depends(get_json_encoder),
242284
) -> JSONResponse:
243-
expansion_data = await version_service.read_versions_by_package(
285+
expansion_data = await version_service.read_versions_expansion_by_package(
244286
expand_package_request.node_type.value,
245287
expand_package_request.package_purl,
246288
expand_package_request.constraints
@@ -264,7 +306,7 @@ async def expand_package(
264306
})
265307
)
266308

267-
@router.post(
309+
@router.get(
268310
"/graph/expand/version",
269311
summary="Expand Version",
270312
description="Return version info to expand its dependencies in the graph visualization.",
@@ -275,11 +317,11 @@ async def expand_package(
275317
@limiter.limit("25/minute")
276318
async def expand_version(
277319
request: Request,
278-
expand_version_request: ExpandVersionRequest,
320+
expand_version_request: ExpandVersionRequest = Depends(),
279321
package_service: PackageService = Depends(get_package_service),
280322
json_encoder: JSONEncoder = Depends(get_json_encoder),
281323
) -> JSONResponse:
282-
expansion_data = await package_service.read_packages_by_version_and_parent(
324+
expansion_data = await package_service.read_packages_expansion_by_version(
283325
expand_version_request.version_purl
284326
)
285327
if expansion_data is None:

app/domain/repo_analyzer/requirement_files/cyclonedx_sbom_analyzer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,7 @@ def extract_dependencies(self, data: dict) -> dict[str, str]:
113113
def normalize_version_for_type(self, version: str, package_type: str) -> str:
114114
if package_type == "maven":
115115
return f"[{version}]"
116+
elif package_type == "gem":
117+
return f"={version}"
116118
else:
117119
return f"=={version}"

app/domain/repo_analyzer/requirement_files/gemfile_analyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def parse_file(self, repository_path: str, filename: str) -> dict[str, str]:
1818
version.count(".") == 2
1919
and not any(op in version for op in ["<", ">", "="])
2020
):
21-
packages[gem] = f"== {version}"
21+
packages[gem] = f"= {version}"
2222
else:
2323
packages[gem] = version
2424
return packages

app/domain/repo_analyzer/requirement_files/gemfile_lock_analyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def parse_file(self, repository_path: str, filename: str) -> dict[str, str]:
1818
version.count(".") == 2
1919
and not any(op in version for op in ["<", ">", "="])
2020
):
21-
packages[gem] = f"== {version}"
21+
packages[gem] = f"= {version}"
2222
else:
2323
packages[gem] = version
2424
return packages

app/domain/repo_analyzer/requirement_files/spdx_sbom_analyzer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,7 @@ def extract_dependencies(self, data: dict) -> dict[str, str]:
149149
def normalize_version_for_type(self, version: str, package_type: str) -> str:
150150
if package_type == "maven":
151151
return f"[{version}]"
152+
elif package_type == "gem":
153+
return f"={version}"
152154
else:
153155
return f"=={version}"

app/schemas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .graphs import (
22
ExpandPackageRequest,
3+
ExpandReqFileRequest,
34
ExpandVersionRequest,
45
GetPackageStatusRequest,
56
GetVersionStatusRequest,
@@ -25,6 +26,7 @@
2526
"CompleteConfigRequest",
2627
"ConfigByImpactRequest",
2728
"ExpandPackageRequest",
29+
"ExpandReqFileRequest",
2830
"ExpandVersionRequest",
2931
"FileInfoRequest",
3032
"FilterConfigsRequest",

app/schemas/graphs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .expand_package_request import ExpandPackageRequest
2+
from .expand_req_file_request import ExpandReqFileRequest
23
from .expand_version_request import ExpandVersionRequest
34
from .get_package_status_request import GetPackageStatusRequest
45
from .get_version_status_request import GetVersionStatusRequest
@@ -7,6 +8,7 @@
78

89
__all__ = [
910
"ExpandPackageRequest",
11+
"ExpandReqFileRequest",
1012
"ExpandVersionRequest",
1113
"GetPackageStatusRequest",
1214
"GetVersionStatusRequest",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pydantic import BaseModel, Field
2+
3+
from app.schemas.patterns import NEO4J_ID_PATTERN
4+
5+
6+
class ExpandReqFileRequest(BaseModel):
7+
requirement_file_id: str = Field(
8+
...,
9+
pattern=NEO4J_ID_PATTERN,
10+
description="Requirement file ID following the Neo4j ID pattern"
11+
)

app/services/package_service.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ async def read_packages_by_requirement_file(self, requirement_file_id: str) -> d
6060
record = await result.single()
6161
return record.get("requirement_files") if record else None
6262

63-
async def read_packages_by_version_and_parent(
63+
async def read_packages_expansion_by_version(
6464
self,
6565
version_purl: str,
6666
) -> dict[str, Any] | None:
6767
query = """
68-
MATCH (v:Version{purl:$version_purl})-[r:REQUIRE]->(dep)
69-
WITH v, r, collect(dep) AS dependencies
68+
MATCH (:Version{purl:$version_purl})-[r:REQUIRE]->(dep)
69+
WITH r, collect(dep) AS dependencies
7070
RETURN {
7171
nodes: [dep IN dependencies | {
7272
id: dep.purl,
@@ -96,6 +96,43 @@ async def read_packages_by_version_and_parent(
9696
record = await result.single()
9797
return record.get("expansion_data") if record else None
9898

99+
async def read_packages_expansion_by_req_file(
100+
self,
101+
requirement_file_id: str,
102+
) -> dict[str, Any] | None:
103+
query = """
104+
MATCH (rf:RequirementFile)-[r:REQUIRE]->(dep)
105+
WHERE elementid(rf) = $requirement_file_id
106+
WITH collect({dep: dep, r: r}) AS items
107+
RETURN {
108+
nodes: [item IN items | {
109+
id: item.dep.purl,
110+
label: item.dep.name,
111+
type: labels(item.dep)[0],
112+
props: {
113+
name: item.dep.name,
114+
vendor: item.dep.vendor,
115+
repository_url: item.dep.repository_url,
116+
purl: item.dep.purl
117+
}
118+
}],
119+
edges: [item IN items | {
120+
id: 'e-' + $requirement_file_id + '-' + item.dep.purl,
121+
source: $requirement_file_id,
122+
target: item.dep.purl,
123+
type: 'REQUIRE',
124+
props: {
125+
constraints: item.r.constraints,
126+
parent_version_name: item.r.parent_version_name
127+
}
128+
}]
129+
} AS expansion_data
130+
"""
131+
async with self.driver.session() as session:
132+
result = await session.run(query, requirement_file_id=requirement_file_id)
133+
record = await result.single()
134+
return record.get("expansion_data") if record else None
135+
99136
async def read_graph_for_package_ssc_info_operation(
100137
self,
101138
node_type: str,

0 commit comments

Comments
 (0)