Skip to content

Commit de68259

Browse files
committed
draft
1 parent ebdb8d0 commit de68259

File tree

12 files changed

+166
-148
lines changed

12 files changed

+166
-148
lines changed

docs/dev/learn/other/vulnerability-report.md

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,27 @@ The first step in writing a vulnerability report for a Pulp `content unit` is to
1717
package [`ecosystem`](https://google.github.io/osv.dev/post-v1-query/#parameters) by checking
1818
[https://ossf.github.io/osv-schema/#defined-ecosystems](https://ossf.github.io/osv-schema/#defined-ecosystems).
1919

20-
The next step is to create a function at the top level of the module, so it can be loaded in pulpcore,
21-
that will be run as a Pulp task. This function should add a dictionary (the `osv_data` created through
22-
`build_osv_data` function in the following sample) to the `pulpcore.app.tasks.vulnerability_report.content_queue` queue.
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.
2324

2425
Here is an example of a function with the above steps:
2526

2627
```python
27-
from pulpcore.plugin.tasking import content_queue
28-
29-
def get_content_from_repo_version(repo_version_pk: str):
30-
"""
31-
Populate content_queue Queue with the content_units found in RepositoryVersion
32-
"""
33-
repo_version = RepositoryVersion.objects.get(pk=repo_version_pk)
34-
ecosystem = "PyPI"
35-
for content_unit in repo_version.content:
36-
content = content_unit.cast()
37-
osv_data = build_osv_data(content.name, ecosystem, content.version)
38-
content_queue.put(osv_data)
39-
content_queue.put(None) # signal that there are no more content_units
28+
async def get_content_from_repo_version(repo_version_pk: str):
29+
repo_version = await sync_to_async(RepositoryVersion.objects.get)(pk=repo_version_pk)
30+
content_units = await sync_to_async(list)(repo_version.content.all())
31+
32+
for content_unit in content_units:
33+
content = await sync_to_async(content_unit.cast)()
34+
content_name = await sync_to_async(lambda: content.name)()
35+
content_version = await sync_to_async(lambda: content.version)()
36+
ecosystem = "PyPI"
37+
repo_content_osv_data = build_osv_data(content_name, ecosystem, content_version)
38+
repo_content_osv_data["repo_version"] = repo_version
39+
repo_content_osv_data["content"] = content
40+
yield repo_content_osv_data
4041

4142
def build_osv_data(name, ecosystem, version=None):
4243
osv_data = {"package": {"name": name, "ecosystem": ecosystem}}
@@ -46,9 +47,7 @@ def build_osv_data(name, ecosystem, version=None):
4647
```
4748

4849

49-
Now that we have the function to populate the `content_queue` queue, we need to create a `ViewSet`
50-
to dispatch a task with it:
51-
50+
Now that we have the async generator function, we need to create a `ViewSet` to dispatch a task with it:
5251

5352
!!! note
5453
In the following sample, we are not defining the permissions to access the endpoint.
@@ -65,7 +64,6 @@ from my_plugin.app.viewsets import get_content_from_repo_version
6564

6665
class MyPluginVulnerabilityReport(VulnerabilityReportViewSet):
6766

68-
endpoint_name = "vuln_report"
6967
queryset = VulnerabilityReport.objects.all()
7068
serializer_class = VulnerabilityReportSerializer
7169

@@ -81,14 +79,18 @@ class MyPluginVulnerabilityReport(VulnerabilityReportViewSet):
8179
repo_version = serializer.validated_data["repo_version"]
8280

8381
# we need to pass the function as string because dispatch() args only accepts JSON serializable content
84-
content_queue_func = f"{get_content_from_repo_version.__module__}.{get_content_from_repo_version.__name__}"
82+
func = f"{get_content_from_repo_version.__module__}.{get_content_from_repo_version.__name__}"
8583

8684
task = dispatch(
8785
check_content,
8886
shared_resources=[repo_version.repository],
89-
kwargs={"func": content_queue_func, [repo_version.pk]}
87+
args = [func, [repo_version.pk]],
9088
)
9189
return OperationPostponedResponse(task, request)
90+
91+
@classmethod
92+
def routable(cls):
93+
return True
9294
```
9395

9496
Here is a sample for the `MyPluginVulnerabilityReportSerializer` where we serialize the

pulpcore/app/migrations/0134_vulnerabilityreport.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.19 on 2025-07-14 19:37
1+
# Generated by Django 4.2.19 on 2025-08-04 11:08
22

33
from django.db import migrations, models
44
import django.db.models.deletion
@@ -17,11 +17,14 @@ class Migration(migrations.Migration):
1717
migrations.CreateModel(
1818
name='VulnerabilityReport',
1919
fields=[
20-
('pulp_id', models.UUIDField(default=pulpcore.app.models.base.pulp_uuid, editable=False, primary_key=True, serialize=False)),
2120
('pulp_created', models.DateTimeField(auto_now_add=True)),
2221
('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')),
2325
('vulns', models.JSONField()),
2426
('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')),
2528
],
2629
options={
2730
'default_related_name': '%(app_label)s_%(model_name)s',
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
from django.db import models
22

3-
from pulpcore.app.models.base import BaseModel
3+
from pulpcore.app.models.base import MasterModel, pulp_uuid
44
from pulpcore.app.util import get_domain_pk
55

66

7-
class VulnerabilityReport(BaseModel):
7+
class VulnerabilityReport(MasterModel):
88
"""
99
Model used in vulnerability report.
1010
"""
1111

12+
TYPE = "vuln_report"
13+
pulp_id = models.UUIDField(default=pulp_uuid, editable=False, unique=True)
14+
content = models.OneToOneField(
15+
"core.Content",
16+
on_delete=models.CASCADE,
17+
primary_key=True,
18+
)
1219
vulns = models.JSONField()
1320
pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.CASCADE)
21+
repo_versions = models.ManyToManyField("core.RepositoryVersion", blank=True)
1422

1523
class Meta:
1624
default_related_name = "%(app_label)s_%(model_name)s"

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from rest_framework import serializers
22

33
from pulpcore.app.models import VulnerabilityReport
4-
from pulpcore.app.serializers import IdentityField, ModelSerializer
4+
from pulpcore.app.serializers import (
5+
DetailRelatedField,
6+
IdentityField,
7+
ModelSerializer,
8+
RepositoryVersionRelatedField,
9+
)
510

611

712
class VulnerabilityReportSerializer(ModelSerializer):
@@ -11,7 +16,12 @@ class VulnerabilityReportSerializer(ModelSerializer):
1116

1217
vulns = serializers.JSONField()
1318
pulp_href = IdentityField(view_name="vuln_report-detail")
19+
content = DetailRelatedField(
20+
read_only=True,
21+
view_name_pattern=r"content(-.*/.*)-detail",
22+
)
23+
repo_versions = RepositoryVersionRelatedField(many=True, required=False)
1424

1525
class Meta:
1626
model = VulnerabilityReport
17-
fields = ModelSerializer.Meta.fields + ("vulns",)
27+
fields = ModelSerializer.Meta.fields + ("vulns", "repo_versions", "content")

pulpcore/app/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@
425425
# opentelemetry settings
426426
OTEL_ENABLED = False
427427

428+
# VulnerabilityReport settings
429+
VULN_REPORT_TASK_LIMITER = 10
430+
428431
# Replaces asyncio event loop with uvloop
429432
UVLOOP_ENABLED = False
430433

pulpcore/app/tasks/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626

2727
from .analytics import post_analytics
2828

29-
from .vulnerability_report import check_content, content_queue
29+
from .vulnerability_report import check_content

0 commit comments

Comments
 (0)