Skip to content

Commit 1fa44ce

Browse files
committed
add osv DataSource
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 78dd5ae commit 1fa44ce

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

vulntotal/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#

vulntotal/datasources/osv.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import logging
11+
from typing import Iterable
12+
13+
import requests
14+
from packageurl import PackageURL
15+
16+
from vulntotal.ecosystem.nuget import get_closest_nuget_package_name
17+
from vulntotal.validator import DataSource
18+
from vulntotal.validator import VendorData
19+
from vulntotal.vulntotal_utils import get_item
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
class OSVDataSource(DataSource):
25+
spdx_license_expression = "Apache-2.0"
26+
license_url = "https://github.com/google/osv/blob/master/LICENSE"
27+
url = "https://api.osv.dev/v1/query"
28+
29+
def fetch_advisory(self, payload):
30+
"""Fetch JSON advisory from OSV API for a given package payload """
31+
32+
response = requests.post(self.url, data=str(payload))
33+
if not response.status_code == 200:
34+
logger.error(f"Error while fetching {payload}: {response.status_code}")
35+
return
36+
return response.json()
37+
38+
def datasource_advisory(self, purl) -> Iterable[VendorData]:
39+
payload = generate_payload(purl)
40+
if not payload:
41+
return
42+
advisory = self.fetch_advisory(payload)
43+
self._raw_dump.append(advisory)
44+
return parse_advisory(advisory)
45+
46+
@classmethod
47+
def supported_ecosystem(cls):
48+
# source https://ossf.github.io/osv-schema/
49+
return {
50+
"npm": "npm",
51+
"maven": "Maven",
52+
"golang": "Go",
53+
"nuget": "NuGet",
54+
"pypi": "PyPI",
55+
"rubygems": "RubyGems",
56+
"crates.io": "crates.io",
57+
"composer": "Packagist",
58+
"linux": "Linux",
59+
"oss-fuzz": "OSS-Fuzz",
60+
"debian": "Debian",
61+
"hex": "Hex",
62+
"android": "Android",
63+
}
64+
65+
66+
def parse_advisory(response) -> Iterable[VendorData]:
67+
"""Parse response from OSV API and yield VendorData"""
68+
69+
for vuln in response.get("vulns") or []:
70+
aliases = []
71+
affected_versions = []
72+
fixed = []
73+
74+
aliases.extend(vuln.get("aliases") or [])
75+
aliases.append(vuln.get("id")) if vuln.get("id") else None
76+
77+
try:
78+
affected_versions.extend(get_item(vuln, "affected", 0, "versions") or [])
79+
except:
80+
pass
81+
82+
try:
83+
for event in get_item(vuln, "affected", 0, "ranges", 0, "events") or []:
84+
affected_versions.append(event.get("introduced")) if event.get(
85+
"introduced"
86+
) else None
87+
fixed.append(event.get("fixed")) if event.get("fixed") else None
88+
except:
89+
pass
90+
91+
yield VendorData(
92+
aliases=sorted(list(set(aliases))),
93+
affected_versions=sorted(list(set(affected_versions))),
94+
fixed_versions=sorted(list(set(fixed))),
95+
)
96+
97+
98+
def generate_payload(purl):
99+
"""Generate compatible payload for OSV API from a PURL"""
100+
101+
supported_ecosystem = OSVDataSource.supported_ecosystem()
102+
payload = {}
103+
payload["version"] = purl.version
104+
payload["package"] = {}
105+
106+
if purl.type in supported_ecosystem:
107+
payload["package"]["ecosystem"] = supported_ecosystem[purl.type]
108+
109+
if purl.type == "maven":
110+
if not purl.namespace:
111+
logger.error(f"Invalid Maven PURL {str(purl)}")
112+
return
113+
payload["package"]["name"] = f"{purl.namespace}:{purl.name}"
114+
115+
elif purl.type == "packagist":
116+
if not purl.namespace:
117+
logger.error(f"Invalid Packagist PURL {str(purl)}")
118+
return
119+
payload["package"]["name"] = f"{purl.namespace}/{purl.name}"
120+
121+
elif purl.type == "linux":
122+
if purl.name not in ("kernel", "Kernel"):
123+
logger.error(f"Invalid Linux PURL {str(purl)}")
124+
return
125+
payload["package"]["name"] = "Kernel"
126+
127+
elif purl.type == "nuget":
128+
nuget_package = get_closest_nuget_package_name(purl.name)
129+
if not nuget_package:
130+
logger.error(f"Invalid NuGet PURL {str(purl)}")
131+
return
132+
payload["package"]["name"] = nuget_package
133+
134+
elif purl.type == "golang" and purl.namespace:
135+
payload["package"]["name"] = f"{purl.namespace}/{purl.name}"
136+
137+
else:
138+
payload["package"]["name"] = purl.name
139+
140+
return payload

0 commit comments

Comments
 (0)