Skip to content

Commit 5b93bae

Browse files
gerrod3cursoragent
andcommitted
Add vulnerabilities to the PyPI JSON API
Serve Warehouse-shaped vulnerability data from stored OSV reports, and let remotes opt in to scan the new repository version after sync. Assisted By: Cursor Grok 4.6 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4fd73bb commit 5b93bae

16 files changed

Lines changed: 377 additions & 25 deletions

CHANGES/1365.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a `vulnerabilities` field to the PyPI JSON API, populated from OSV scan reports. Remotes can opt in to scan the new repository version after sync.

docs/user/guides/sync.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ pulp python remote create \
154154
--keep-latest-packages 5
155155
```
156156

157+
Set `vulnerabilities` on the remote to scan the new repository version after each successful sync. The scan runs as a follow-up task and does not fail the sync if OSV is unreachable. Results are stored as vulnerability reports and exposed on the JSON API.
158+
159+
```bash
160+
pulp python remote create \
161+
--name 'scanned-remote' \
162+
--url 'https://pypi.org/' \
163+
--includes '["django==5.2.1"]' \
164+
--vulnerabilities
165+
```
166+
157167
Reference: [Python Remote Usage](site:pulp_python/restapi/#tag/Remotes:-Python)
158168

159169
### Creating a remote to sync all of PyPI

docs/user/guides/vulnerability_report.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ The report contains detailed information about each vulnerability, including:
8181
- **References**: Links to advisories and patches
8282
- **Repository and Content**: Pulp `RepositoryVersion` and `Content` impacted
8383

84+
## JSON API
85+
86+
The PyPI JSON endpoints (`pypi/<project>/json` and `pypi/<project>/<version>/json`) include a
87+
`vulnerabilities` array for the selected version. Pulp trims stored OSV reports to the Warehouse
88+
shape (`id`, `source`, `link`, `aliases`, `details`, `summary`, `fixed_in`, `withdrawn`). The key is
89+
always present; it is an empty list until a scan has run.
90+
91+
Enable `vulnerabilities` on a remote to scan automatically after sync, or scan a repository version
92+
manually as shown above.
93+
8494
## Example Workflow
8595

8696
Here's a complete example of scanning a repository for vulnerabilities:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
("python", "0024_pythonrepository_error_on_reject"),
8+
]
9+
10+
operations = [
11+
migrations.AddField(
12+
model_name="pythonremote",
13+
name="vulnerabilities",
14+
field=models.BooleanField(default=False),
15+
),
16+
]

pulp_python/app/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ def content_handler(self, path):
130130
if not settings.DOMAIN_ENABLED:
131131
domain = None
132132
json_body = python_content_to_json(
133-
self.base_path, package_content, version=version, domain=domain
133+
self.base_path,
134+
package_content,
135+
version=version,
136+
domain=domain,
137+
repository_version=self.publication.repository_version,
134138
)
135139
if json_body:
136140
return json_response(json_body, headers=headers)
@@ -350,6 +354,7 @@ class PythonRemote(Remote, AutoAddObjPermsMixin):
350354
models.CharField(max_length=10, blank=True), choices=PLATFORMS, default=list
351355
)
352356
provenance = models.BooleanField(default=False)
357+
vulnerabilities = models.BooleanField(default=False)
353358

354359
def get_remote_artifact_url(self, relative_path=None, request=None):
355360
"""Get url for remote_artifact"""

pulp_python/app/pypi/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class PackageMetadataSerializer(serializers.Serializer):
3535
info = serializers.JSONField(help_text=_("Core metadata of the package"))
3636
releases = serializers.JSONField(help_text=_("List of all the releases of the package"))
3737
urls = serializers.JSONField()
38+
vulnerabilities = serializers.JSONField(
39+
help_text=_("Known vulnerabilities for the selected package version."),
40+
)
3841

3942

4043
class PackageUploadSerializer(serializers.Serializer):

pulp_python/app/serializers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,11 @@ class PythonRemoteSerializer(core_serializers.RemoteSerializer):
789789
help_text=_("Whether to sync available provenances for Python packages."),
790790
default=False,
791791
)
792+
vulnerabilities = serializers.BooleanField(
793+
required=False,
794+
help_text=_("Whether to scan the new repository version for vulnerabilities after a sync."),
795+
default=False,
796+
)
792797

793798
def validate_includes(self, value):
794799
"""Validates the includes"""
@@ -821,6 +826,7 @@ class Meta:
821826
"keep_latest_packages",
822827
"exclude_platforms",
823828
"provenance",
829+
"vulnerabilities",
824830
)
825831
model = python_models.PythonRemote
826832

pulp_python/app/tasks/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
from .repair import repair # noqa:F401
77
from .sync import sync # noqa:F401
88
from .upload import upload, upload_group # noqa:F401
9-
from .vulnerability_report import get_repo_version_content # noqa:F401
9+
from .vulnerability_report import dispatch_scan, get_repo_version_content # noqa:F401
1010
from .yank import aunyank_package, ayank_package # noqa:F401

pulp_python/app/tasks/sync.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
PythonRemote,
3030
)
3131
from pulp_python.app.provenance import Provenance
32+
from pulp_python.app.tasks.vulnerability_report import dispatch_scan
3233
from pulp_python.app.utils import PYPI_LAST_SERIAL, aget_remote_simple_page, parse_metadata
3334

3435
logger = logging.getLogger(__name__)
@@ -56,7 +57,9 @@ def sync(remote_pk, repository_pk, mirror):
5657
raise SyncError("A remote must have a url attribute to sync.")
5758

5859
first_stage = PythonBanderStage(remote)
59-
DeclarativeVersion(first_stage, repository, mirror).create()
60+
new_version = DeclarativeVersion(first_stage, repository, mirror).create()
61+
if new_version and remote.vulnerabilities:
62+
dispatch_scan(repository, new_version)
6063

6164

6265
def create_bandersnatch_config(remote):

pulp_python/app/tasks/vulnerability_report.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pulpcore.plugin.models import RepositoryVersion
22
from pulpcore.plugin.sync import sync_to_async_iterable
3+
from pulpcore.plugin.tasking import check_content, dispatch
34

45
from pulp_python.app.models import PythonPackageContent
56

@@ -28,3 +29,13 @@ def _build_osv_data(name, ecosystem, version=None):
2829
if version:
2930
osv_data["version"] = version
3031
return osv_data
32+
33+
34+
def dispatch_scan(repository, repository_version):
35+
"""Dispatch a vulnerability scan for a repository version."""
36+
func = f"{get_repo_version_content.__module__}.{get_repo_version_content.__name__}"
37+
return dispatch(
38+
check_content,
39+
shared_resources=[repository],
40+
args=[func, [str(repository_version.pk)]],
41+
)

0 commit comments

Comments
 (0)