Skip to content

Commit 4a63334

Browse files
git-hyagidralley
authored andcommitted
Add the vulnerability report
closes: #6773
1 parent ee1d89e commit 4a63334

22 files changed

+479
-3
lines changed

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,12 @@ This is [recommended by aiohttp] for performance improvements.
392392
The `uvloop` python package must be installed in the content-app's system.
393393
That's available as an optional dependency for pulpcore: `pulpcore[uvloop]`.
394394

395+
### VULN\_REPORT\_TASK\_LIMITER
396+
397+
This number determines the amount of concurrent vulnerability report processes that can be spawned
398+
at one time. Increasing this number will generally increase the speed of the task, but will also
399+
consume more resources of the worker. Defaults to 10 concurrent processes.
400+
395401
### WORKER\_TTL
396402

397403
The number of seconds before a worker should be considered lost.

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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
from pulpcore.plugin.sync import sync_to_async_iterable
31+
from myplugin.app.models import MyPluginContent
32+
33+
async def get_content_from_repo_version(repo_version_pk: str):
34+
repo_version = await sync_to_async(RepositoryVersion.objects.get)(pk=repo_version_pk)
35+
content_units = MyPluginContent.objects.filter(pk__in=repo_version.content)
36+
ecosystem = "MyContentUnitEcosystem" # Content unit ecosystem from osv.dev (for ex "PyPI" for python content unit)
37+
async for content in sync_to_async_iterable(content_units):
38+
repo_content_osv_data = _build_osv_data(content.name, ecosystem, content.version)
39+
repo_content_osv_data["repo_version"] = repo_version
40+
repo_content_osv_data["content"] = content
41+
yield repo_content_osv_data
42+
43+
def _build_osv_data(name, ecosystem, version=None):
44+
osv_data = {"package": {"name": name, "ecosystem": ecosystem}}
45+
if version:
46+
osv_data["version"] = version
47+
return osv_data
48+
```
49+
50+
51+
Now that we have the async generator function, we need to create a new method in the plugin
52+
RepositoryVersionViewSet subclass (the plugin class that inherits from core.RepositoryVersionViewSet)
53+
that will be used to dispatch the `vulnerability report` task.
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.decorators import action
62+
63+
from pulpcore.plugin import viewsets as core_viewsets
64+
from pulpcore.plugin.tasking import check_content, dispatch
65+
66+
class MyPluginRepositoryVersionViewSet(core_viewsets.RepositoryVersionViewSet):
67+
parent_viewset = MyPluginRepositoryViewSet
68+
69+
@extend_schema(summary="Generate vulnerability report", responses={202: AsyncOperationResponseSerializer})
70+
@action(detail=True, methods=["post"])
71+
def scan(self, request, repository_pk, **kwargs):
72+
repository_version = self.get_object()
73+
func = f"{get_content_from_repo_version.__module__}.{get_content_from_repo_version.__name__}"
74+
task = dispatch(
75+
check_content,
76+
shared_resources=[repository_version.repository],
77+
args=[func, [repository_version.pk]],
78+
)
79+
return core_viewsets.OperationPostponedResponse(task, request)
80+
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 4.2.19 on 2025-08-07 00:07
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_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+
('content', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='core.content')),
25+
('pulp_domain', models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.CASCADE, to='core.domain')),
26+
('repo_versions', models.ManyToManyField(blank=True, to='core.repositoryversion')),
27+
],
28+
options={
29+
'default_related_name': '%(app_label)s_%(model_name)s',
30+
},
31+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
32+
),
33+
]

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django.db import models
2+
3+
from pulpcore.app.models.base import BaseModel
4+
from pulpcore.app.util import get_domain_pk
5+
6+
7+
class VulnerabilityReport(BaseModel):
8+
"""
9+
A model for storing vulnerability reports for Content/RepositoryVersion.
10+
11+
Fields:
12+
vulns (models.JSONField): A JSON field containing the list of vulnerabilities found
13+
in osv.dev.
14+
15+
Relations:
16+
content (models.OneToOneField): The Content object that was scanned for vulnerabilities.
17+
pulp_domain (models.ForeignKey): The Domain this vulnerability report belongs to,
18+
providing multi-tenancy isolation.
19+
repo_versions (models.ManyToManyField): The RepositoryVersion(s) where the scanned
20+
Content appears. This allows tracking which repository versions contain
21+
vulnerable content.
22+
"""
23+
24+
content = models.OneToOneField(
25+
"Content",
26+
on_delete=models.CASCADE,
27+
unique=True,
28+
)
29+
vulns = models.JSONField()
30+
pulp_domain = models.ForeignKey("Domain", default=get_domain_pk, on_delete=models.CASCADE)
31+
repo_versions = models.ManyToManyField("RepositoryVersion", blank=True)
32+
33+
class Meta:
34+
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: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
from rest_framework.validators import UniqueValidator
66

77
from pulpcore.app import models
8-
from pulpcore.app.serializers import base, fields, pulp_labels_validator, DetailRelatedField
8+
from pulpcore.app.serializers import (
9+
base,
10+
fields,
11+
pulp_labels_validator,
12+
DetailRelatedField,
13+
RelatedField,
14+
)
915
from pulpcore.app.util import get_domain
1016

1117

@@ -27,6 +33,11 @@ class NoArtifactContentSerializer(base.ModelSerializer):
2733
view_name_pattern=r"repositories(-.*/.*)-detail",
2834
queryset=models.Repository.objects.all(),
2935
)
36+
vuln_report = RelatedField(
37+
read_only=True,
38+
view_name="vuln_report-detail",
39+
source="core_vulnerabilityreport",
40+
)
3041

3142
def get_artifacts(self, validated_data):
3243
"""
@@ -116,6 +127,7 @@ class Meta:
116127
fields = base.ModelSerializer.Meta.fields + (
117128
"repository",
118129
"pulp_labels",
130+
"vuln_report",
119131
)
120132

121133

pulpcore/app/serializers/repository.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from rest_framework_nested.serializers import NestedHyperlinkedModelSerializer
88

99
from pulpcore.app import models, settings
10-
from pulpcore.app.util import get_prn
10+
from pulpcore.app.util import get_prn, reverse
1111
from pulpcore.app.serializers import (
1212
DetailIdentityField,
1313
DetailRelatedField,
@@ -490,6 +490,12 @@ class RepositoryVersionSerializer(ModelSerializer, NestedHyperlinkedModelSeriali
490490
source="*",
491491
read_only=True,
492492
)
493+
vuln_report = serializers.SerializerMethodField(
494+
read_only=True,
495+
)
496+
497+
def get_vuln_report(self, object):
498+
return f"{reverse('vuln_report-list')}?repository_version={get_prn(object)}"
493499

494500
class Meta:
495501
model = models.RepositoryVersion
@@ -499,6 +505,7 @@ class Meta:
499505
"repository",
500506
"base_version",
501507
"content_summary",
508+
"vuln_report",
502509
)
503510

504511

0 commit comments

Comments
 (0)