Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/new_source.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The step by step instructions are as follows:

- [ ] Prepare and publish your records via a public Git repository ([example](https://github.com/AlmaLinux/osv-database/tree/master)). If this method isn’t ideal, we also support publishing records through [REST API](./rest-api.md) or GCS buckets ([example](https://storage.googleapis.com/android-osv/)).

- [ ] To support API querying, if you are contributing a new ecosystem, please create a PR to extend [purl\_helpers.py](https://github.com/google/osv.dev/blob/master/osv/purl_helpers.py) and create a new ecosystem in [\_ecosystems.py](https://github.com/google/osv.dev/blob/master/osv/ecosystems/_ecosystems.py). You can refer to existing examples showing how to implement support for [Semver](https://github.com/google/osv.dev/blob/139de7b69a2ea39e2113309b3a0a47aab920ddcf/osv/ecosystems/_ecosystems.py#L45) and [non-Semver](https://github.com/google/osv.dev/pull/3430) ecosystems.
- [ ] To support API querying, if you are contributing a new ecosystem, please create a PR to extend [purl\_helpers.py](https://github.com/google/osv.dev/blob/master/osv/purl_helpers.py) and create a new ecosystem in [\_ecosystems.py](https://github.com/google/osv.dev/blob/master/osv/ecosystems/_ecosystems.py). You can refer to existing examples showing how to implement support for Semver and non-Semver ecosystems.

- [ ] Create a PR to start [importing the records you are publishing into our test instance of OSV.dev](https://github.com/google/osv.dev/blob/master/source_test.yaml) and validate everything is working as intended there.

Expand Down
6 changes: 3 additions & 3 deletions gcp/api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ def do_query(query: osv_service_v1_pb2.Query,
if purl.version:
version = purl.version

if ecosystem and not ecosystems.get(ecosystem):
if ecosystem and not ecosystems.is_known(ecosystem):
context.service_context.abort(grpc.StatusCode.INVALID_ARGUMENT,
'Invalid ecosystem.')

Expand Down Expand Up @@ -1229,8 +1229,8 @@ def query_by_version(
query = query.filter(osv.Bug.ecosystem == ecosystem)
ecosystem_info = ecosystems.get(ecosystem)

is_semver = ecosystem_info and ecosystem_info.is_semver
supports_comparing = ecosystem_info and ecosystem_info.supports_comparing
is_semver = ecosystems.is_semver(ecosystem)
supports_comparing = ecosystem_info is not None

bugs = []
if ecosystem:
Expand Down
6 changes: 2 additions & 4 deletions gcp/api/server_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ def _match_versions(version: str, affected: osv.AffectedVersions) -> bool:
"""Check if the given version matches one of the AffectedVersions' listed
versions."""
ecosystem_helper = osv.ecosystems.get(affected.ecosystem)
if ecosystem_helper and (ecosystem_helper.supports_comparing or
ecosystem_helper.is_semver):
if ecosystem_helper is not None:
# Most ecosystem helpers return a very large version on invalid, but if it
# does cause an error, just match nothing.
try:
Expand Down Expand Up @@ -150,8 +149,7 @@ def _match_versions(version: str, affected: osv.AffectedVersions) -> bool:
def _match_events(version: str, affected: osv.AffectedVersions) -> bool:
"""Check if the given version matches in the AffectedVersions' events list."""
ecosystem_helper = osv.ecosystems.get(affected.ecosystem)
if not (ecosystem_helper and
(ecosystem_helper.supports_comparing or ecosystem_helper.is_semver)):
if ecosystem_helper is None:
# Ecosystem does not support comparisons.
return False
try:
Expand Down
12 changes: 6 additions & 6 deletions gcp/workers/worker/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,18 +288,18 @@ def maybe_normalize_package_names(vulnerability):
return vulnerability


def filter_unsupported_ecosystems(vulnerability):
"""Remove unsupported ecosystems from vulnerability."""
def filter_unknown_ecosystems(vulnerability):
"""Remove unknown ecosystems from vulnerability."""
filtered = []
for affected in vulnerability.affected:
# CVE-converted OSV records have no package information.
if not affected.HasField('package'):
filtered.append(affected)
elif osv.ecosystems.get(affected.package.ecosystem):
elif osv.ecosystems.is_known(affected.package.ecosystem):
filtered.append(affected)
else:
logging.warning('%s contains unsupported ecosystem "%s"',
vulnerability.id, affected.package.ecosystem)
logging.error('%s contains unknown ecosystem "%s"', vulnerability.id,
affected.package.ecosystem)
del vulnerability.affected[:]
vulnerability.affected.extend(filtered)

Expand Down Expand Up @@ -496,7 +496,7 @@ def _do_update(self, source_repo, repo, vulnerability, relative_path,
logging.warning('%s has an encoding error, skipping.', vulnerability.id)
return

filter_unsupported_ecosystems(vulnerability)
filter_unknown_ecosystems(vulnerability)

orig_modified_date = vulnerability.modified.ToDatetime(datetime.UTC)
try:
Expand Down
2 changes: 1 addition & 1 deletion gcp/workers/worker/worker_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ def setUp(self):

# Add fake ecosystems used in tests to supported ecosystems.
osv.ecosystems._ecosystems._ecosystems.update({
'ecosystem': osv.ecosystems.OrderingUnsupportedEcosystem(),
'ecosystem': None,
})

def tearDown(self):
Expand Down
4 changes: 2 additions & 2 deletions osv/ecosystems/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2021 Google LLC
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,4 +14,4 @@
"""Ecosystem helpers."""

from ._ecosystems import *
from .helper_base import *
from .ecosystems_base import *
168 changes: 66 additions & 102 deletions osv/ecosystems/_ecosystems.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2021 Google LLC
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -15,78 +15,79 @@

import re

from osv.ecosystems.chainguard import Chainguard
from osv.ecosystems.wolfi import Wolfi
from .helper_base import Ecosystem, OrderingUnsupportedEcosystem
from .alma_linux import AlmaLinux
from .alpaquita import Alpaquita
from .alpine import Alpine
from .ecosystems_base import EnumerableEcosystem, OrderedEcosystem
from .alpine import Alpine, APK
from .bioconductor import Bioconductor
from .cran import CRAN
from .debian import Debian
from .echo import Echo
from .debian import Debian, DPKG
from .haskell import Hackage, GHC
from .mageia import Mageia
from .maven import Maven
from .minimos import MinimOS
from .nuget import NuGet
from .openeuler import OpenEuler
from .packagist import Packagist
from .pub import Pub
from .pypi import PyPI
from .rocky_linux import RockyLinux
from .redhat import RedHat
from .redhat import RPM
from .rubygems import RubyGems
from .semver_ecosystem_helper import SemverEcosystem
from .ubuntu import Ubuntu
from .suse import SUSE
from .opensuse import OpenSUSE

_ecosystems = {
# SemVer-based ecosystems (remember keep synced with SEMVER_ECOSYSTEMS):
'Bitnami': SemverEcosystem(),
'crates.io': SemverEcosystem(),
'Go': SemverEcosystem(),
'Hex': SemverEcosystem(),
'npm': SemverEcosystem(),
'SwiftURL': SemverEcosystem(),
# Non SemVer-based ecosystems
'Bioconductor': Bioconductor(),
'CRAN': CRAN(),
'Chainguard': Chainguard(),
'Echo': Echo(),
'GHC': GHC(),
'Hackage': Hackage(),
'Maven': Maven(),
'MinimOS': MinimOS(),
'NuGet': NuGet(),
'Packagist': Packagist(),
'Pub': Pub(),
'PyPI': PyPI(),
'RubyGems': RubyGems(),
'Wolfi': Wolfi(),
# Ecosystems which require a release version for enumeration, which is
# handled separately in get().
# Ecosystems missing implementations:
'Android': OrderingUnsupportedEcosystem(),
'ConanCenter': OrderingUnsupportedEcosystem(),
'GitHub Actions': OrderingUnsupportedEcosystem(),
'Linux': OrderingUnsupportedEcosystem(),
'OSS-Fuzz': OrderingUnsupportedEcosystem(),
'Photon OS': OrderingUnsupportedEcosystem(),
'GIT': OrderingUnsupportedEcosystem(),
'AlmaLinux': RPM,
'Alpaquita': APK,
'Alpine': Alpine,
'BellSoft Hardened Containers': APK,
'Bioconductor': Bioconductor,
'Bitnami': SemverEcosystem,
'Chainguard': APK,
'CRAN': CRAN,
'crates.io': SemverEcosystem,
'Debian': Debian,
'Echo': DPKG,
'GHC': GHC,
'Go': SemverEcosystem,
'Hackage': Hackage,
'Hex': SemverEcosystem,
'Mageia': RPM,
'Maven': Maven,
'MinimOS': APK,
'npm': SemverEcosystem,
'NuGet': NuGet,
'openEuler': RPM,
'openSUSE': RPM,
'Packagist': Packagist,
'Pub': Pub,
'PyPI': PyPI,
'Red Hat': RPM,
'Rocky Linux': RPM,
'RubyGems': RubyGems,
'SUSE': RPM,
'SwiftURL': SemverEcosystem,
'Ubuntu': Ubuntu,
'Wolfi': APK,

# Ecosystems known in the schema, but without implementations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... is there a test we can add here that reads the ecosystems.json in the osv-schema repo and checks it against this.?

Copy link
Member Author

@michaelkedar michaelkedar Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember bringing this up a while ago - #2615 (comment)

Not sure if you agree/disagree with Andrew's reasoning here. With renovate automatically updating the osv-schema it probably would always fail the tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be fine though, with the recent ecosystems, they made both PRs (osv-schema and osv.dev _ecosystem.py) simultaneously. So we just need to merge the ecosystems.py PR before updating the schema to the newest version and it should work right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently have a test that checks that all the ecosystems in _ecosystems.py are valid ecosystems in the schema. Adding the vice versa test (that all schema ecosystems are in _ecosystems.py) would mean we'd always have to update both at the same time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see..., let's leave it as it is then.

# Must be kept in sync with osv-schema.
'Android': None,
'ConanCenter': None,
'GIT': None,
'GitHub Actions': None,
'Linux': None,
'OSS-Fuzz': None,
'Photon OS': None,
}

# Semver-based ecosystems, should correspond to _ecosystems above.
# TODO(michaelkedar): Avoid need to keep in sync with above.
SEMVER_ECOSYSTEMS = {
'Bitnami',
'crates.io',
'Go',
'Hex',
'npm',
'SwiftURL',
}

def is_semver(ecosystem: str) -> bool:
"""Returns whether an ecosystem uses 'SEMVER' range types"""
return isinstance(get(ecosystem), SemverEcosystem)


def is_known(ecosystem: str) -> bool:
"""Returns whether an ecosystem is known to OSV
(even if ordering is not supported)."""
name, _, _ = ecosystem.partition(':')
return name in _ecosystems


package_urls = {
'Android': 'https://android.googlesource.com/',
Expand Down Expand Up @@ -117,50 +118,13 @@
}


def get(name: str) -> Ecosystem:
def get(name: str) -> OrderedEcosystem | EnumerableEcosystem | None:
"""Get ecosystem helpers for a given ecosystem."""

if name.startswith('Debian'):
return Debian(name.partition(':')[2])

if name.startswith('AlmaLinux'):
return AlmaLinux()

if name.startswith('Alpaquita'):
return Alpaquita()

if name.startswith('Alpine'):
return Alpine(name.partition(':')[2])

if name.startswith('BellSoft Hardened Containers'):
return Alpaquita()

if name.startswith('Mageia'):
return Mageia()

if name.startswith('Red Hat'):
return RedHat()

if name.startswith('Rocky Linux'):
return RockyLinux()

if name.startswith('Photon OS:'):
# TODO(unassigned)
return OrderingUnsupportedEcosystem()

if name.startswith('Ubuntu'):
return Ubuntu()

if name.startswith('openSUSE'):
return OpenSUSE()

if name.startswith('openEuler'):
return OpenEuler()

if name.startswith('SUSE'):
return SUSE()

return _ecosystems.get(normalize(name))
name, _, suffix = name.partition(':')
ecosys = _ecosystems.get(name)
if ecosys is None:
return None
return ecosys(suffix)


def normalize(ecosystem_name: str):
Expand Down Expand Up @@ -205,7 +169,7 @@ def is_supported_in_deps_dev(ecosystem_name: str) -> bool:
return ecosystem_name in _OSV_TO_DEPS_ECOSYSTEMS_MAP


def map_ecosystem_to_deps_dev(ecosystem_name: str) -> str:
def map_ecosystem_to_deps_dev(ecosystem_name: str) -> str | None:
return _OSV_TO_DEPS_ECOSYSTEMS_MAP.get(ecosystem_name)


Expand Down
2 changes: 1 addition & 1 deletion osv/ecosystems/_ecosystems_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2024 Google LLC
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
37 changes: 0 additions & 37 deletions osv/ecosystems/alma_linux.py

This file was deleted.

39 changes: 0 additions & 39 deletions osv/ecosystems/alma_linux_test.py

This file was deleted.

Loading
Loading