Skip to content

Commit 7434120

Browse files
committed
Add the vulnerability report
closes: #6773
1 parent 4ee8c48 commit 7434120

12 files changed

Lines changed: 543 additions & 1 deletion

File tree

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/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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 verify the `content units` against the [osv.dev](https://osv.dev) database.
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+
After identifying the `ecosystem`, run the `pulpcore.app.tasks.vulnerability_report.build_osv_data`
21+
function that will return a dictionary with the expected osv.dev payload format.
22+
23+
The next step is to create a function that will be run as a Pulp task. This function should add the
24+
dictionary, created in the previous step, to the `pulpcore.app.tasks.vulnerability_report.content_queue`
25+
queue.
26+
27+
Here is an example of a function that, based on a `RepositoryVersion pk`, adds each content found
28+
in the Repository in `content_queue` after identifying the `ecosystem` and creating a dictionary
29+
with the osv.dev expected format:
30+
31+
!!! note
32+
For the following example, we are using the `pulpcore.app.tasks.vulnerability_report.identify_package_ecosystem`
33+
helper function, but plugin writers are encouraged to write their own function to better identify
34+
the ecosystem.
35+
36+
```python
37+
from pulpcore.app.tasks.vulnerability_report import build_osv_data, content_queue, identify_package_ecosystem
38+
39+
def get_content_from_repo_version(repo_version_pk: str):
40+
"""
41+
Populate content_queue Queue with the content_units found in RepositoryVersion
42+
"""
43+
repo_version = RepositoryVersion.objects.get(pk=repo_version_pk)
44+
for content_unit in repo_version.content:
45+
content = content_unit.cast()
46+
ecosystems = identify_package_ecosystem(content, repo_version.repository)
47+
for ecosystem in ecosystems:
48+
osv_data = build_osv_data(content.name, ecosystem, content.version)
49+
content_queue.put(osv_data)
50+
content_queue.put(None) # signal that there are no more content_units
51+
```
52+
53+
54+
Now that we have the function to populate the `content_queue` queue, we need to create a `ViewSet`
55+
to dispatch a task with it:
56+
57+
```python
58+
from pulpcore.plugin.models import VulnerabilityReport
59+
from pulpcore.app.serializers.vulnerability_report import VulnerabilityReportSerializer
60+
from pulpcore.app.tasks.vulnerability_report import check_content
61+
from pulpcore.app.viewsets.vulnerability_report import VulnerabilityReport as VulnerabilityReportView
62+
63+
class MyPluginVulnerabilityReport(VulnerabilityReportView):
64+
65+
endpoint_name = "vuln_report"
66+
queryset = VulnerabilityReport.objects.all()
67+
serializer_class = VulnerabilityReportSerializer
68+
69+
@extend_schema(
70+
request=MyPluginContentVulnerabilityReportSerializer,
71+
description="Trigger a task to generate the package vulnerability report",
72+
summary="Generate vulnerability report",
73+
responses={202: AsyncOperationResponseSerializer},
74+
)
75+
def create(self, request):
76+
serializer = MyPluginContentVulnerabilityReportSerializer(data=request.data, context={"request": request})
77+
serializer.is_valid(raise_exception=True)
78+
shared_resources = [repo_version.repository]
79+
dispatch_task, kwargs = check_content, {"repo_version_pk": repo_version.pk}
80+
task = dispatch(dispatch_task, shared_resources=shared_resources, kwargs=kwargs)
81+
return OperationPostponedResponse(task, request)
82+
```
83+
84+
Here is a sample for the `MyPluginContentVulnerabilityReportSerializer` where we serialize the
85+
`RepositoryVersion` string into a `RepositoryVersionRelatedField` object:
86+
87+
```python
88+
from rest_framework import serializers
89+
from pulpcore.plugin.serializers import ValidateFieldsMixin, RepositoryVersionRelatedField
90+
91+
class MyPluginContentVulnerabilityReportSerializer(serializers.Serializer, ValidateFieldsMixin):
92+
93+
repo_version = RepositoryVersionRelatedField(
94+
required=False,
95+
allow_null=True,
96+
help_text=_("RepositoryVersion HREF with the packages to be checked."),
97+
)
98+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 4.2.19 on 2025-07-14 19:37
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', '0133_repositoryversion_content_ids'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='VulnerabilityReport',
19+
fields=[
20+
('pulp_id', models.UUIDField(default=pulpcore.app.models.base.pulp_uuid, editable=False, primary_key=True, serialize=False)),
21+
('pulp_created', models.DateTimeField(auto_now_add=True)),
22+
('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)),
23+
('vulns', models.JSONField()),
24+
('pulp_domain', models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.CASCADE, to='core.domain')),
25+
],
26+
options={
27+
'default_related_name': '%(app_label)s_%(model_name)s',
28+
},
29+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
30+
),
31+
]

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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.db import models
2+
3+
from pulpcore.app.models import BaseModel
4+
from pulpcore.plugin.util import get_domain_pk
5+
6+
7+
class VulnerabilityReport(BaseModel):
8+
"""
9+
Model used in vulnerability report.
10+
"""
11+
12+
vulns = models.JSONField()
13+
pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.CASCADE)
14+
15+
class Meta:
16+
default_related_name = "%(app_label)s_%(model_name)s"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from rest_framework import serializers
2+
3+
from pulpcore.plugin.models import VulnerabilityReport
4+
from pulpcore.plugin.serializers import IdentityField, ModelSerializer
5+
6+
7+
class VulnerabilityReportSerializer(ModelSerializer):
8+
"""
9+
A serializer for the VulnerabilityReport Model.
10+
"""
11+
12+
vulns = serializers.JSONField()
13+
pulp_href = IdentityField(view_name="vuln_report-detail")
14+
15+
class Meta:
16+
model = VulnerabilityReport
17+
fields = ModelSerializer.Meta.fields + ("vulns",)

0 commit comments

Comments
 (0)