Skip to content

Commit 363fc36

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

2 files changed

Lines changed: 174 additions & 1 deletion

File tree

vulntotal/datasources/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
# VulnTotal is a free software tool from nexB Inc. and others.
2222
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2323

24+
from vulntotal.datasources import osv
2425

25-
DATASOURCE_REGISTRY = []
26+
27+
DATASOURCE_REGISTRY = [
28+
osv.OSVDataSource,
29+
]
2630

2731
DATASOURCE_REGISTRY = {x.__module__.split(".")[-1]: x for x in DATASOURCE_REGISTRY}

vulntotal/datasources/osv.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# http://nexb.com and https://github.com/nexB/vulnerablecode/
4+
# The VulnTotal software is licensed under the Apache License version 2.0.
5+
# Data generated with VulnTotal require an acknowledgment.
6+
#
7+
# You may not use this software except in compliance with the License.
8+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
9+
# Unless required by applicable law or agreed to in writing, software distributed
10+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
11+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
12+
# specific language governing permissions and limitations under the License.
13+
#
14+
# When you publish or redistribute any data created with VulnTotal or any VulnTotal
15+
# derivative work, you must accompany this data with the following acknowledgment:
16+
#
17+
# Generated with VulnTotal and provided on an "AS IS" BASIS, WITHOUT WARRANTIES
18+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
19+
# VulnTotal should be considered or used as legal advice. Consult an Attorney
20+
# for any legal advice.
21+
# VulnTotal is a free software tool from nexB Inc. and others.
22+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
23+
24+
import logging
25+
from typing import Iterable
26+
from urllib.parse import urljoin
27+
28+
import requests
29+
from packageurl import PackageURL
30+
31+
from vulntotal.validator import DataSource
32+
from vulntotal.validator import VendorData
33+
34+
logger = logging.getLogger(__name__)
35+
36+
37+
class OSVDataSource(DataSource):
38+
spdx_license_expression = "Apache-2.0"
39+
license_url = "https://github.com/google/osv/blob/master/LICENSE"
40+
url = "https://api.osv.dev/v1/query"
41+
42+
def fetch_advisory(self, payload):
43+
response = requests.post(self.url, data=str(payload))
44+
if not response.status_code == 200:
45+
logger.error(f"Error while fetching {payload}: {response.status_code}")
46+
return
47+
return response.json()
48+
49+
def datasource_advisory(self, purl) -> Iterable[VendorData]:
50+
payload = generate_payload(purl)
51+
if not payload:
52+
return
53+
advisory = self.fetch_advisory(payload)
54+
self._raw_dump.append(advisory)
55+
return parse_advisory(advisory)
56+
57+
@classmethod
58+
def supported_ecosystem(cls):
59+
# source https://ossf.github.io/osv-schema/
60+
return {
61+
"npm": "npm",
62+
"maven": "Maven",
63+
"go": "Go",
64+
"nuget": "NuGet",
65+
"pypi": "PyPI",
66+
"rubygems": "RubyGems",
67+
"crates.io": "crates.io",
68+
"packagist": "Packagist",
69+
"linux": "Linux",
70+
"oss-fuzz": "OSS-Fuzz",
71+
"debian": "Debian",
72+
"hex": "Hex",
73+
"android": "Android",
74+
}
75+
76+
77+
def parse_advisory(response) -> Iterable[VendorData]:
78+
if "vulns" in response:
79+
for vuln in response["vulns"]:
80+
aliases = []
81+
affected_versions = []
82+
fixed = []
83+
84+
if "aliases" in vuln:
85+
aliases.extend(vuln["aliases"])
86+
87+
if "id" in vuln:
88+
aliases.append(vuln["id"])
89+
90+
if "affected" in vuln:
91+
if "versions" in vuln["affected"][0]:
92+
affected_versions.extend(vuln["affected"][0]["versions"])
93+
94+
if vuln["affected"] and "ranges" in vuln["affected"][0]:
95+
if "events" in vuln["affected"][0]["ranges"][0]:
96+
97+
events = vuln["affected"][0]["ranges"][0]["events"]
98+
if events:
99+
for event in events:
100+
if "introduced" in event:
101+
affected_versions.append(event["introduced"])
102+
if "fixed" in event:
103+
fixed.append(event["fixed"])
104+
yield VendorData(
105+
aliases=sorted(list(set(aliases))),
106+
affected_versions=sorted(list(set(affected_versions))),
107+
fixed_versions=sorted(list(set(fixed))),
108+
)
109+
110+
def get_closest_nuget_package_name(query):
111+
url_nuget_service = "https://api.nuget.org/v3/index.json"
112+
url_nuget_search = ""
113+
114+
api_resources = requests.get(url_nuget_service).json()
115+
if "resources" in api_resources:
116+
for resource in api_resources["resources"]:
117+
if "@type" in resource and resource["@type"] == "SearchQueryService":
118+
url_nuget_search = resource["@id"]
119+
break
120+
121+
if url_nuget_search:
122+
url_query = urljoin(url_nuget_search,f"?q={query}")
123+
query_response = requests.get(url_query).json()
124+
if "data" in query_response and query_response["data"]:
125+
return query_response["data"][0]["id"]
126+
127+
128+
def generate_payload(purl):
129+
130+
supported_ecosystem = OSVDataSource.supported_ecosystem()
131+
payload = {}
132+
payload["version"] = purl.version
133+
payload["package"] = {}
134+
135+
if purl.type in supported_ecosystem:
136+
payload["package"]["ecosystem"] = supported_ecosystem[purl.type]
137+
138+
if purl.type == "maven":
139+
if not purl.namespace:
140+
logger.error(f"Invalid Maven PURL {str(purl)}")
141+
return
142+
payload["package"]["name"] = f"{purl.namespace}:{purl.name}"
143+
144+
elif purl.type == "packagist":
145+
if not purl.namespace:
146+
logger.error(f"Invalid Packagist PURL {str(purl)}")
147+
return
148+
payload["package"]["name"] = f"{purl.namespace}/{purl.name}"
149+
150+
elif purl.type == "linux":
151+
if purl.name not in ("kernel", "Kernel"):
152+
logger.error(f"Invalid Linux PURL {str(purl)}")
153+
return
154+
payload["package"]["name"] = "Kernel"
155+
156+
elif purl.type == "nuget":
157+
nuget_package = get_closest_nuget_package_name(purl.name)
158+
if not nuget_package:
159+
logger.error(f"Invalid NuGet PURL {str(purl)}")
160+
return
161+
payload["package"]["name"] = nuget_package
162+
163+
elif purl.type == 'go' and purl.namespace:
164+
payload["package"]["name"] = f"{purl.namespace}/{purl.name}"
165+
166+
else:
167+
payload["package"]["name"] = purl.name
168+
169+
return payload

0 commit comments

Comments
 (0)