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.
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 ] = {}
0 commit comments