Skip to content

Commit 6bdfe5a

Browse files
authored
cravex2-reachability: Expose reachability in REST API (#2213)
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent bbf378d commit 6bdfe5a

3 files changed

Lines changed: 156 additions & 8 deletions

File tree

scanpipe/pipelines/analyze_symbols_reachability.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def steps(cls):
5757
cls.collect_patch_symbols,
5858
cls.collect_and_match_resources,
5959
cls.generate_advisory_reachability_report,
60+
cls.apply_reachability_to_packages_and_dependencies,
6061
)
6162

6263
def get_vulnerabilities_patches(self):
@@ -96,8 +97,20 @@ def collect_and_match_resources(self):
9697

9798
def generate_advisory_reachability_report(self):
9899
"""Generate a reachability report summarizing status by advisory."""
99-
reachability.generate_advisory_reachability_report(
100-
project=self.project,
101-
patches=self.patches,
102-
candidate_resources=self.candidate_resources,
100+
self.advisories_reachability_report = (
101+
reachability.generate_advisory_reachability_report(
102+
project=self.project,
103+
patches=self.patches,
104+
candidate_resources=self.candidate_resources,
105+
)
106+
)
107+
108+
def apply_reachability_to_packages_and_dependencies(self):
109+
"""
110+
Save reachability results by updating DiscoveredPackage and
111+
DiscoveredDependency records with the computed reachability data
112+
in their affected_by_vulnerabilities JSON field.
113+
"""
114+
reachability.apply_reachability_to_packages_and_dependencies(
115+
project=self.project, advisory_report=self.advisories_reachability_report
103116
)

scanpipe/pipes/reachability.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
from typecode import get_type
3333

3434
from aboutcode.pipeline import LoopProgress
35+
from scanpipe.models import DiscoveredDependency
36+
from scanpipe.models import DiscoveredPackage
3537
from scanpipe.pipes.symbols import TS_QUERIES
3638
from scanpipe.pipes.symbols import SymbolExtractor
3739
from scanpipe.pipes.symbols import create_sha256_fingerprint
@@ -784,7 +786,7 @@ def generate_advisory_reachability_report(project, patches, candidate_resources)
784786
ReachabilityStatus.NOT_REACHABLE.value: 1,
785787
}
786788

787-
advisory_reachability_report = {
789+
advisories_reachability_report = {
788790
"purl": project.purl,
789791
"advisories": [],
790792
}
@@ -799,7 +801,7 @@ def generate_advisory_reachability_report(project, patches, candidate_resources)
799801
"details": [],
800802
}
801803
advisory_map[adv_uid] = adv_data
802-
advisory_reachability_report["advisories"].append(adv_data)
804+
advisories_reachability_report["advisories"].append(adv_data)
803805

804806
for resource in candidate_resources:
805807
for report in resource.extra_data.get("symbols_reachability", []):
@@ -817,7 +819,7 @@ def generate_advisory_reachability_report(project, patches, candidate_resources)
817819
"details": [],
818820
}
819821
advisory_map[adv_uid] = adv_data
820-
advisory_reachability_report["advisories"].append(adv_data)
822+
advisories_reachability_report["advisories"].append(adv_data)
821823

822824
tool_details = {
823825
"resource_path": resource.path,
@@ -839,4 +841,54 @@ def generate_advisory_reachability_report(project, patches, candidate_resources)
839841
reachability_output_path = project.get_output_file_path("reachability", "json")
840842

841843
with open(reachability_output_path, "w") as f:
842-
json.dump(advisory_reachability_report, f, indent=2)
844+
json.dump(advisories_reachability_report, f, indent=2)
845+
846+
return advisories_reachability_report
847+
848+
849+
def inject_reachability_data(vulns, advisory_map):
850+
"""
851+
Inject reachability data into a list of vulnerabilities.
852+
Returns True if any vulnerability was updated, False otherwise.
853+
"""
854+
updated = False
855+
for vuln in vulns:
856+
adv_uid = vuln.get("advisory_uid")
857+
if adv_uid in advisory_map:
858+
adv_data = advisory_map[adv_uid]
859+
vuln["is_reachable"] = adv_data.get("is_reachable", "unknown")
860+
vuln["reachability_analysis"] = adv_data.get("details", [])
861+
updated = True
862+
863+
return updated
864+
865+
866+
def apply_reachability_to_packages_and_dependencies(project, advisory_report):
867+
"""
868+
Update DiscoveredPackage and DiscoveredDependency records by injecting the
869+
computed reachability data into their affected_by_vulnerabilities JSON field.
870+
"""
871+
advisories = advisory_report.get("advisories", [])
872+
if not advisories:
873+
return
874+
875+
advisory_map = {adv["advisory_uid"]: adv for adv in advisories}
876+
targets = (
877+
(project.discoveredpackages.all(), DiscoveredPackage),
878+
(project.discovereddependencies.all(), DiscoveredDependency),
879+
)
880+
881+
for queryset, model in targets:
882+
unsaved = [
883+
item
884+
for item in queryset
885+
if inject_reachability_data(
886+
item.affected_by_vulnerabilities or [], advisory_map
887+
)
888+
]
889+
if unsaved:
890+
model.objects.bulk_update(
891+
objs=unsaved,
892+
fields=["affected_by_vulnerabilities"],
893+
batch_size=10,
894+
)

scanpipe/tests/test_api.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from scanpipe.models import WebhookSubscription
5757
from scanpipe.pipes.input import copy_input
5858
from scanpipe.pipes.output import JSONResultsGenerator
59+
from scanpipe.pipes.reachability import apply_reachability_to_packages_and_dependencies
5960
from scanpipe.tests import dependency_data1
6061
from scanpipe.tests import filter_warnings
6162
from scanpipe.tests import make_message
@@ -1374,3 +1375,85 @@ def test_scanpipe_api_serializer_get_serializer_fields(self):
13741375

13751376
with self.assertRaises(LookupError):
13761377
get_serializer_fields(None)
1378+
1379+
def test_scanpipe_api_project_action_package_with_reachability(self):
1380+
self.discovered_package1.affected_by_vulnerabilities = [
1381+
{
1382+
"advisory_id": "PYSEC-2026-1",
1383+
"advisory_uid": "pypa/scancode/PYSEC-2026-1",
1384+
"summary": "summary 1",
1385+
"risk_score": 1,
1386+
},
1387+
{
1388+
"advisory_id": "PYSEC-2026-2",
1389+
"advisory_uid": "pypa/scancode/PYSEC-2026-2",
1390+
"summary": "summary 2",
1391+
"risk_score": 2,
1392+
},
1393+
{
1394+
"advisory_id": "PYSEC-2026-3",
1395+
"advisory_uid": "pypa/scancode/PYSEC-2026-3",
1396+
"summary": "summary 3",
1397+
"risk_score": 3,
1398+
},
1399+
]
1400+
self.discovered_package1.save()
1401+
advisory_map = {
1402+
"purl": "pkg:pypi/daglib@0.3.2",
1403+
"advisories": [
1404+
{
1405+
"advisory_uid": "pypa/scancode/PYSEC-2026-1",
1406+
"is_reachable": "unknown",
1407+
"details": [
1408+
{
1409+
"resource_path": "scancode/session.py",
1410+
"is_reachable": "unknown",
1411+
"vulnerable_symbols": ["SqliteAccountInfo"],
1412+
}
1413+
],
1414+
},
1415+
{
1416+
"advisory_uid": "pypa/scancode/PYSEC-2026-2",
1417+
"is_reachable": "yes",
1418+
"details": [
1419+
{
1420+
"resource_path": "b2sdk/session.py",
1421+
"is_reachable": "yes",
1422+
"vulnerable_symbols": ["SqliteAccountInfo"],
1423+
}
1424+
],
1425+
},
1426+
],
1427+
}
1428+
1429+
apply_reachability_to_packages_and_dependencies(self.project1, advisory_map)
1430+
url = reverse("project-packages", args=[self.project1.uuid])
1431+
response = self.csrf_client.get(url)
1432+
1433+
self.assertEqual(status.HTTP_200_OK, response.status_code)
1434+
self.assertEqual(1, response.data["count"])
1435+
1436+
pkg_response = response.data["results"][0]
1437+
vulns = pkg_response["affected_by_vulnerabilities"]
1438+
1439+
self.assertEqual(3, len(vulns))
1440+
1441+
self.assertEqual("pypa/scancode/PYSEC-2026-1", vulns[0]["advisory_uid"])
1442+
self.assertEqual("unknown", vulns[0]["is_reachable"])
1443+
self.assertIn("reachability_analysis", vulns[0])
1444+
self.assertEqual(1, len(vulns[0]["reachability_analysis"]))
1445+
self.assertEqual(
1446+
"scancode/session.py", vulns[0]["reachability_analysis"][0]["resource_path"]
1447+
)
1448+
1449+
self.assertEqual("pypa/scancode/PYSEC-2026-2", vulns[1]["advisory_uid"])
1450+
self.assertEqual("yes", vulns[1]["is_reachable"])
1451+
self.assertIn("reachability_analysis", vulns[1])
1452+
self.assertEqual(1, len(vulns[1]["reachability_analysis"]))
1453+
self.assertEqual(
1454+
"b2sdk/session.py", vulns[1]["reachability_analysis"][0]["resource_path"]
1455+
)
1456+
1457+
self.assertEqual("pypa/scancode/PYSEC-2026-3", vulns[2]["advisory_uid"])
1458+
self.assertNotIn("is_reachable", vulns[2])
1459+
self.assertNotIn("reachability_analysis", vulns[2])

0 commit comments

Comments
 (0)