Skip to content

Commit 34578a3

Browse files
committed
Add the vulnerability report
closes: #6773
1 parent 9d7148a commit 34578a3

22 files changed

Lines changed: 467 additions & 1 deletion

CHANGES/6773.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added the vulnerability report data model.

docs/admin/reference/settings.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,12 @@ Defaults to `pulpcore.tasking.status`.
515515
[task diagnostics documentation]: site:pulpcore/docs/dev/learn/tasks/diagnostics.md
516516
[uvloop]: https://github.com/MagicStack/uvloop
517517
[Webserver Auth with Reverse Proxy]: site:pulpcore/docs/admin/guides/auth/external/#webserver-auth-with-reverse-proxy
518+
519+
520+
## Vulnerability Report Settings
521+
522+
### VULN\_REPORT\_TASK\_LIMITER
523+
524+
This number determines the amount of concurrent vulnerability report processes that can be spawned
525+
at one time. Increasing this number will generally increase the speed of the task, but will also
526+
consume more resources of the worker. Defaults to 10 concurrent processes.

docs/admin/reference/tech-preview.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ The following features are currently being released as part of tech preview:
55
- [Support for Open Telemetry](site:pulpcore/docs/admin/learn/architecture/#telemetry-support)
66
- Upstream replicas
77
- Domains - Multi-Tenancy
8-
- [Checkpoint](site:pulpcore/docs/user/guides/checkpoint/)
8+
- [Checkpoint](site:pulpcore/docs/user/guides/checkpoint/)
9+
- [Vulnerability Report](site:pulpcore/docs/dev/learn/other/vulnerability-report/)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Adding vulnerability report for plugins
2+
3+
4+
!!! warning
5+
This feature is provided as a tech preview and could change in backwards incompatible
6+
ways in the future.
7+
8+
9+
Pulp provides a way to store known vulnerabilities from OSV for `content units` within a specified `RepositoryVersion`.
10+
Each plugin will need to implement a function to construct the [package payload](https://google.github.io/osv.dev/post-v1-query/#parameters)
11+
that will be used to query osv.dev database.
12+
13+
!!! note
14+
As of now, querying by osv.dev `commit` is not supported (use `package` instead).
15+
16+
The first step in writing a vulnerability report for a Pulp `content unit` is to identify the
17+
package [`ecosystem`](https://google.github.io/osv.dev/post-v1-query/#parameters) by checking
18+
[https://ossf.github.io/osv-schema/#defined-ecosystems](https://ossf.github.io/osv-schema/#defined-ecosystems).
19+
20+
The next step is to create an async function at the top level of the module (so it can be
21+
loaded in pulpcore) that will be run as a Pulp task. This async function should return a generator
22+
object with a dictionary containing the `osv_data` (created through `build_osv_data` function in the following sample),
23+
and also the `Content` and `RepositoryVersion` objects.
24+
25+
Here is an example of a function with the above steps:
26+
27+
```python
28+
from asgiref.sync import sync_to_async
29+
from pulpcore.plugin.models import RepositoryVersion
30+
31+
async def get_content_from_repo_version(repo_version_pk: str):
32+
repo_version = await sync_to_async(RepositoryVersion.objects.get)(pk=repo_version_pk)
33+
content_units = await sync_to_async(list)(repo_version.content.all())
34+
35+
for content_unit in content_units:
36+
content = await sync_to_async(content_unit.cast)()
37+
content_name = await sync_to_async(lambda: content.name)()
38+
content_version = await sync_to_async(lambda: content.version)()
39+
ecosystem = "PyPI"
40+
repo_content_osv_data = _build_osv_data(content_name, ecosystem, content_version)
41+
repo_content_osv_data["repo_version"] = repo_version
42+
repo_content_osv_data["content"] = content
43+
yield repo_content_osv_data
44+
45+
def _build_osv_data(name, ecosystem, version=None):
46+
osv_data = {"package": {"name": name, "ecosystem": ecosystem}}
47+
if version:
48+
osv_data["version"] = version
49+
return osv_data
50+
```
51+
52+
53+
Now that we have the async generator function, we need to create a `ViewSet` to dispatch a task with it:
54+
55+
!!! note
56+
In the following sample, we are not defining the permissions to access the endpoint.
57+
Plugin writters should define them according to each plugin needs.
58+
59+
```python
60+
from drf_spectacular.utils import extend_schema
61+
from rest_framework.views import APIView
62+
63+
from pulpcore.plugin.models import VulnerabilityReport
64+
from pulpcore.plugin.serializers import AsyncOperationResponseSerializer, VulnerabilityReportSerializer
65+
from pulpcore.plugin.tasking import check_content, dispatch
66+
from pulpcore.plugin.viewsets import OperationPostponedResponse
67+
68+
from my_plugin.app.serializers import MyPluginVulnerabilityReportSerializer
69+
from my_plugin.app.utils import get_content_from_repo_version
70+
71+
class MyPluginVulnerabilityReport(APIView):
72+
73+
queryset = VulnerabilityReport.objects.all()
74+
serializer_class = VulnerabilityReportSerializer
75+
76+
@extend_schema(
77+
request=MyPluginVulnerabilityReportSerializer,
78+
description="Trigger a task to generate the package vulnerability report",
79+
summary="Generate vulnerability report",
80+
responses={202: AsyncOperationResponseSerializer},
81+
)
82+
def create(self, request):
83+
serializer = MyPluginVulnerabilityReportSerializer(data=request.data, context={"request": request})
84+
serializer.is_valid(raise_exception=True)
85+
repo_version = serializer.validated_data["repo_version"]
86+
87+
# we need to pass the function as string because dispatch() args only accepts JSON serializable content
88+
func = f"{get_content_from_repo_version.__module__}.{get_content_from_repo_version.__name__}"
89+
90+
task = dispatch(
91+
check_content,
92+
shared_resources=[repo_version.repository],
93+
args = (func, [repo_version.pk]),
94+
)
95+
return OperationPostponedResponse(task, request)
96+
```
97+
98+
Here is a sample for the `MyPluginVulnerabilityReportSerializer` where we serialize the
99+
`RepositoryVersion` string into a `RepositoryVersionRelatedField` object:
100+
101+
```python
102+
from rest_framework import serializers
103+
from pulpcore.plugin.serializers import ValidateFieldsMixin, RepositoryVersionRelatedField
104+
105+
class MyPluginVulnerabilityReportSerializer(serializers.Serializer, ValidateFieldsMixin):
106+
107+
repo_version = RepositoryVersionRelatedField(
108+
required=True,
109+
allow_null=False,
110+
help_text=_("RepositoryVersion HREF with the packages to be checked."),
111+
)
112+
```
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 4.2.19 on 2025-08-04 19:29
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django_lifecycle.mixins
6+
import pulpcore.app.models.base
7+
import pulpcore.app.util
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
('core', '0136_delete_basedistribution'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='VulnerabilityReport',
19+
fields=[
20+
('pulp_created', models.DateTimeField(auto_now_add=True)),
21+
('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)),
22+
('pulp_type', models.TextField(db_index=True, default=None)),
23+
('pulp_id', models.UUIDField(default=pulpcore.app.models.base.pulp_uuid, editable=False, unique=True)),
24+
('content', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='core.content')),
25+
('vulns', models.JSONField()),
26+
('pulp_domain', models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.CASCADE, to='core.domain')),
27+
('repo_versions', models.ManyToManyField(blank=True, to='core.repositoryversion')),
28+
],
29+
options={
30+
'default_related_name': '%(app_label)s_%(model_name)s',
31+
},
32+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
33+
),
34+
]

pulpcore/app/models/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
UploadChunk,
8888
)
8989

90+
from .vulnerability_report import VulnerabilityReport
91+
9092
# Moved here to avoid a circular import with Task
9193
from .progress import GroupProgressReport, ProgressReport
9294

@@ -170,4 +172,5 @@
170172
"OpenPGPSignature",
171173
"OpenPGPUserAttribute",
172174
"OpenPGPUserID",
175+
"VulnerabilityReport",
173176
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.db import models
2+
3+
from pulpcore.app.models.base import MasterModel, pulp_uuid
4+
from pulpcore.app.util import get_domain_pk
5+
6+
7+
class VulnerabilityReport(MasterModel):
8+
"""
9+
Model used in vulnerability report.
10+
"""
11+
12+
TYPE = "vuln_report"
13+
pulp_id = models.UUIDField(default=pulp_uuid, editable=False, unique=True)
14+
content = models.OneToOneField(
15+
"Content",
16+
on_delete=models.CASCADE,
17+
primary_key=True,
18+
)
19+
vulns = models.JSONField()
20+
pulp_domain = models.ForeignKey("Domain", default=get_domain_pk, on_delete=models.CASCADE)
21+
repo_versions = models.ManyToManyField("RepositoryVersion", blank=True)
22+
23+
class Meta:
24+
default_related_name = "%(app_label)s_%(model_name)s"

pulpcore/app/serializers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
UserSerializer,
122122
)
123123
from .replica import UpstreamPulpSerializer
124+
from .vulnerability_report import VulnerabilityReportSerializer
124125
from .openpgp import (
125126
OpenPGPDistributionSerializer,
126127
OpenPGPKeyringSerializer,

pulpcore/app/serializers/content.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class NoArtifactContentSerializer(base.ModelSerializer):
2727
view_name_pattern=r"repositories(-.*/.*)-detail",
2828
queryset=models.Repository.objects.all(),
2929
)
30+
core_vulnerabilityreport = DetailRelatedField(
31+
read_only=True,
32+
view_name="vuln_report-detail",
33+
)
3034

3135
def get_artifacts(self, validated_data):
3236
"""
@@ -116,6 +120,7 @@ class Meta:
116120
fields = base.ModelSerializer.Meta.fields + (
117121
"repository",
118122
"pulp_labels",
123+
"core_vulnerabilityreport",
119124
)
120125

121126

pulpcore/app/serializers/repository.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,12 @@ class RepositoryVersionSerializer(ModelSerializer, NestedHyperlinkedModelSeriali
491491
read_only=True,
492492
)
493493

494+
core_vulnerabilityreport = DetailRelatedField(
495+
many=True,
496+
read_only=True,
497+
view_name="vuln_report-detail",
498+
)
499+
494500
class Meta:
495501
model = models.RepositoryVersion
496502
fields = ModelSerializer.Meta.fields + (
@@ -499,6 +505,7 @@ class Meta:
499505
"repository",
500506
"base_version",
501507
"content_summary",
508+
"core_vulnerabilityreport",
502509
)
503510

504511

0 commit comments

Comments
 (0)