Skip to content

Commit dda3fd0

Browse files
committed
Add ubuntu data source which uses OVAL files
Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
1 parent e200ea6 commit dda3fd0

3 files changed

Lines changed: 183 additions & 24 deletions

File tree

vulnerabilities/importers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@
2727
from vulnerabilities.importers.rust import RustDataSource
2828
from vulnerabilities.importers.safety_db import SafetyDbDataSource
2929
from vulnerabilities.importers.ruby import RubyDataSource
30+
from vulnerabilities.importers.ubuntu import UbuntuDataSource
Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#
21
# Copyright (c) 2017 nexB Inc. and others. All rights reserved.
32
# http://nexb.com and https://github.com/nexB/vulnerablecode/
43
# The VulnerableCode software is licensed under the Apache License version 2.0.
@@ -21,36 +20,142 @@
2120
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
2221
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2322

24-
from urllib.request import urlopen
2523

26-
import bs4
24+
import asyncio
25+
import bz2
26+
import dataclasses
27+
from typing import Iterable
28+
from typing import List
29+
from typing import Mapping
30+
from typing import Set
31+
import xml.etree.ElementTree as ET
32+
33+
34+
from aiohttp import ClientSession
35+
from aiohttp.client_exceptions import ClientResponseError
36+
import requests
37+
from packageurl import PackageURL
38+
39+
40+
from vulnerabilities.data_source import DataSource, DataSourceConfiguration, Advisory
41+
from vulnerabilities.importers import oval_parser
42+
43+
44+
45+
@dataclasses.dataclass
46+
class UbuntuConfiguration(DataSourceConfiguration):
47+
releases: list
48+
49+
class UbuntuDataSource(DataSource):
50+
51+
CONFIG_CLASS = UbuntuConfiguration
52+
def __init__(self, *args, **kwargs):
53+
super().__init__(*args, **kwargs)
54+
#we could avoid setting translations, and have it
55+
#set by default in the OvalParser, but we don't yet know
56+
#whether all OVAL providers use the same format
57+
self.translations = {'less than':'<'}
58+
self._versions = VersionAPI()
59+
60+
def _fetch(self) :
61+
base_url = 'https://people.canonical.com/~ubuntu-security/oval/'
62+
file_name = 'com.ubuntu.{}.cve.oval.xml.bz2'
63+
releases = self.config.releases
64+
for release in releases:
65+
resp = requests.get(base_url + file_name.format(release))
66+
extracted = bz2.decompress(resp.content)
67+
yield ET.ElementTree(ET.fromstring(extracted.decode('utf-8')))
68+
69+
def added_advisories(self) -> List[Advisory] :
70+
advisories = []
71+
for oval_file in self._fetch():
72+
advisories.extend(self.get_data_from_xml_doc(oval_file))
73+
return advisories
74+
75+
@staticmethod
76+
def _collect_pkgs(parsed_oval_data) -> Set :
77+
all_pkgs = set()
78+
for definition_data in parsed_oval_data:
79+
for test_data in definition_data['test_data']:
80+
for package in test_data['package_list']:
81+
all_pkgs.add(package)
82+
83+
return all_pkgs
84+
85+
86+
def get_data_from_xml_doc(self, xml_doc) -> List[Advisory] :
87+
all_adv = []
88+
oval_doc = oval_parser.OvalParser(self.translations, xml_doc)
89+
raw_data = oval_doc.get_data()
90+
all_pkgs = self._collect_pkgs(raw_data)
91+
92+
asyncio.run(self._versions.load_api(all_pkgs))
93+
94+
for definition_data in raw_data: #definition_data -> Advisory
95+
vuln_id = definition_data['vuln_id']
96+
description = definition_data['description']
97+
affected_purls = set()
98+
safe_purls = set()
99+
urls = definition_data['reference_urls']
100+
for test_data in definition_data['test_data'] :
101+
for package in test_data['package_list']:
102+
pkg_name = package
103+
aff_ver_range = test_data['version_ranges']
104+
all_versions = self._versions.get(package)
105+
#This filter is to filter out long versions.
106+
#50 is limit because that's what db permits atm
107+
all_versions = set(filter(lambda x : len(x)<50,all_versions))
108+
if not all_versions:
109+
continue
110+
affected_versions = set(filter(lambda x: x in aff_ver_range,all_versions))
111+
safe_versions = all_versions - affected_versions
27112

113+
for version in affected_versions:
114+
#should we add a qualifier like 'distro:ubuntu'?
115+
pkg_url = PackageURL(name=pkg_name,type='deb',version=version)
116+
affected_purls.add(pkg_url)
28117

29-
UBUNTU_ROOT_URL = 'https://people.canonical.com/~ubuntu-security/cve/main.html'
118+
for version in safe_versions:
119+
#should we add a qualifier like 'distro:ubuntu'?
120+
pkg_url = PackageURL(name=pkg_name,type='deb',version=version)
121+
safe_purls.add(pkg_url)
30122

123+
all_adv.append(Advisory(summary=description,impacted_package_urls=affected_purls,
124+
resolved_package_urls=safe_purls,cve_id=vuln_id,reference_urls=urls))
125+
return all_adv
31126

32-
def extract_cves(html):
33-
soup = bs4.BeautifulSoup(html, 'lxml')
34127

35-
# Exclude the header row which has no class attribute
36-
rows = soup.find_all('tr', attrs={'class': True})
37128

38-
cves = []
39-
for row in rows:
40-
columns = row.text.split()
41-
cves.append({
42-
'cve_id': columns[0],
43-
'package_name': columns[1],
44-
'vulnerability_status': row.get('class')[0],
45-
})
129+
class VersionAPI:
130+
def __init__(self, cache: Mapping[str, Set[str]] = None):
131+
self.cache = cache or {}
46132

47-
return cves
133+
def get(self, package_name: str) -> Set[str]:
134+
return self.cache[package_name]
48135

136+
async def load_api(self, pkg_set):
137+
async with ClientSession() as session:
138+
await asyncio.gather(*[self.set_api(pkg, session) for pkg in pkg_set if pkg not in self.cache])
49139

50-
def scrape_cves():
51-
"""
52-
Runs the full scraping process of Ubuntu CVEs.
53-
"""
54-
html = urlopen(UBUNTU_ROOT_URL).read()
55-
cves = extract_cves(html)
56-
return cves
140+
async def set_api(self, pkg, session):
141+
url = ('https://api.launchpad.net/1.0/ubuntu/+archive/'
142+
'primary?ws.op=getPublishedSources&'
143+
'source_name={}&exact_match=true'.format(pkg))
144+
try:
145+
all_versions = set()
146+
while(True):
147+
response = await session.request(method='GET', url=url)
148+
response.raise_for_status()
149+
resp_json = await response.json()
150+
if resp_json['entries'] == [] :
151+
self.cache[pkg] = {}
152+
break
153+
for release in resp_json['entries']:
154+
all_versions.add(release['source_package_version'])
155+
if resp_json.get('next_collection_link') :
156+
url = resp_json['next_collection_link']
157+
else:
158+
break
159+
self.cache[pkg] = all_versions
160+
except ClientResponseError:
161+
self.cache[pkg] = {}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright (c) 2017 nexB Inc. and others. All rights reserved.
2+
# http://nexb.com and https://github.com/nexB/vulnerablecode/
3+
# The VulnerableCode software is licensed under the Apache License version 2.0.
4+
# Data generated with VulnerableCode require an acknowledgment.
5+
#
6+
# You may not use this software except in compliance with the License.
7+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software distributed
9+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
# specific language governing permissions and limitations under the License.
12+
#
13+
# When you publish or redistribute any data created with VulnerableCode or any VulnerableCode
14+
# derivative work, you must accompany this data with the following acknowledgment:
15+
#
16+
# Generated with VulnerableCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES
17+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
18+
# VulnerableCode should be considered or used as legal advice. Consult an Attorney
19+
# for any legal advice.
20+
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22+
23+
from django.db import migrations
24+
25+
26+
def add_ubuntu_importer(apps, _):
27+
Importer = apps.get_model('vulnerabilities', 'Importer')
28+
29+
Importer.objects.create(
30+
name='ubuntu',
31+
license='',
32+
last_run=None,
33+
data_source='UbuntuDataSource',
34+
data_source_cfg={'releases':['bionic','trusty','focal','eoan','xenial']},
35+
)
36+
37+
38+
def remove_ubuntu_importer(apps, _):
39+
Importer = apps.get_model('vulnerabilities', 'Importer')
40+
qs = Importer.objects.filter(name='ubuntu')
41+
if qs:
42+
qs[0].delete()
43+
44+
45+
class Migration(migrations.Migration):
46+
47+
dependencies = [
48+
('vulnerabilities', '0008_ruby_importer'),
49+
]
50+
51+
operations = [
52+
migrations.RunPython(add_ubuntu_importer, remove_ubuntu_importer),
53+
]

0 commit comments

Comments
 (0)