2020# VulnerableCode is a free software tool from nexB Inc. and others.
2121# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
23+ import asyncio
2324import dataclasses
24- from xml . etree import ElementTree
25+ import urllib
2526
2627import requests
28+ from bs4 import BeautifulSoup
2729from packageurl import PackageURL
30+ from univers .versions import MavenVersion
31+ from univers .version_specifier import VersionSpecifier
2832
2933from vulnerabilities .data_source import Advisory
3034from vulnerabilities .data_source import DataSource
3135from vulnerabilities .data_source import DataSourceConfiguration
36+ from vulnerabilities .data_source import Reference
37+ from vulnerabilities .data_source import VulnerabilitySeverity
38+ from vulnerabilities .package_managers import GitHubTagsAPI
39+ from vulnerabilities .severity_systems import scoring_systems
3240from vulnerabilities .helpers import create_etag
41+ from vulnerabilities .helpers import nearest_patched_package
3342
3443
3544@dataclasses .dataclass
@@ -40,56 +49,113 @@ class ApacheHTTPDDataSourceConfiguration(DataSourceConfiguration):
4049class ApacheHTTPDDataSource (DataSource ):
4150
4251 CONFIG_CLASS = ApacheHTTPDDataSourceConfiguration
43- url = "https://httpd.apache.org/security/vulnerabilities-httpd.xml"
52+ base_url = "https://httpd.apache.org/security/json/"
53+
54+ def set_api (self ):
55+ self .version_api = GitHubTagsAPI ()
56+ asyncio .run (self .version_api .load_api (["apache/httpd" ]))
4457
4558 def updated_advisories (self ):
46- # Etags are like hashes of web responses. We maintain
47- # (url, etag) mappings in the DB. `create_etag` creates
48- # (url, etag) pair. If a (url, etag) already exists then the code
49- # skips processing the response further to avoid duplicate work
50-
51- if create_etag (data_src = self , url = self .url , etag_key = "ETag" ):
52- data = fetch_xml (self .url )
53- advisories = to_advisories (data )
54- return self .batch_advisories (advisories )
55-
56- return []
57-
58-
59- def to_advisories (data ):
60- advisories = []
61- for issue in data :
62- resolved_packages = []
63- impacted_packages = []
64- for info in issue :
65- if info .tag == "cve" :
66- cve = info .attrib ["name" ]
67-
68- if info .tag == "title" :
69- summary = info .text
70-
71- if info .tag == "fixed" :
72- resolved_packages .append (
73- PackageURL (type = "apache" , name = "httpd" , version = info .attrib ["version" ])
59+ links = fetch_links (self .base_url )
60+ self .set_api ()
61+ advisories = []
62+ for link in links :
63+ data = requests .get (link ).json ()
64+ advisories .append (self .to_advisory (data ))
65+ return self .batch_advisories (advisories )
66+
67+ def to_advisory (self , data ):
68+ cve = data ["CVE_data_meta" ]["ID" ]
69+ descriptions = data ["description" ]["description_data" ]
70+ description = None
71+ for desc in descriptions :
72+ if desc ["lang" ] == "eng" :
73+ description = desc .get ("value" )
74+ break
75+
76+ severities = []
77+ impacts = data .get ("impact" , [])
78+ for impact in impacts :
79+ value = impact .get ("other" )
80+ if value :
81+ severities .append (
82+ VulnerabilitySeverity (
83+ system = scoring_systems ["apache_httpd" ],
84+ value = value ,
85+ )
7486 )
87+ break
88+ reference = Reference (
89+ reference_id = cve ,
90+ url = urllib .parse .urljoin (self .base_url , f"{ cve } .json" ),
91+ severities = severities ,
92+ )
7593
76- if info .tag == "affects" or info .tag == "maybeaffects" :
77- impacted_packages .append (
78- PackageURL (type = "apache" , name = "httpd" , version = info .attrib ["version" ])
79- )
94+ versions_data = []
95+ for vendor in data ["affects" ]["vendor" ]["vendor_data" ]:
96+ for products in vendor ["product" ]["product_data" ]:
97+ for version_data in products ["version" ]["version_data" ]:
98+ versions_data .append (version_data )
8099
81- advisories .append (
82- Advisory (
83- vulnerability_id = cve ,
84- summary = summary ,
85- impacted_package_urls = impacted_packages ,
86- resolved_package_urls = resolved_packages ,
100+ fixed_version_ranges , affected_version_ranges = self .to_version_ranges (versions_data )
101+
102+ affected_packages = []
103+ fixed_packages = []
104+
105+ for version_range in fixed_version_ranges :
106+ fixed_packages .extend (
107+ [
108+ PackageURL (type = "apache" , name = "httpd" , version = version )
109+ for version in self .version_api .get ("apache/httpd" )
110+ if MavenVersion (version ) in version_range
111+ ]
87112 )
113+
114+ for version_range in affected_version_ranges :
115+ affected_packages .extend (
116+ [
117+ PackageURL (type = "apache" , name = "httpd" , version = version )
118+ for version in self .version_api .get ("apache/httpd" )
119+ if MavenVersion (version ) in version_range
120+ ]
121+ )
122+
123+ return Advisory (
124+ vulnerability_id = cve ,
125+ summary = description ,
126+ affected_packages = nearest_patched_package (affected_packages , fixed_packages ),
127+ references = [reference ],
88128 )
89129
90- return advisories
130+ def to_version_ranges (self , versions_data ):
131+ fixed_version_ranges = []
132+ affected_version_ranges = []
133+ for version_data in versions_data :
134+ version_value = version_data ["version_value" ]
135+ range_expression = version_data ["version_affected" ]
136+ if range_expression == "<" :
137+ fixed_version_ranges .append (
138+ VersionSpecifier .from_scheme_version_spec_string (
139+ "maven" , ">={}" .format (version_value )
140+ )
141+ )
142+ elif range_expression == "=" or range_expression == "?=" :
143+ affected_version_ranges .append (
144+ VersionSpecifier .from_scheme_version_spec_string (
145+ "maven" , "{}" .format (version_value )
146+ )
147+ )
148+
149+ return (fixed_version_ranges , affected_version_ranges )
91150
92151
93- def fetch_xml (url ):
94- resp = requests .get (url ).content
95- return ElementTree .fromstring (resp )
152+ def fetch_links (url ):
153+ links = []
154+ data = requests .get (url ).content
155+ soup = BeautifulSoup (data , features = "lxml" )
156+ for tag in soup .find_all ("a" ):
157+ link = tag .get ("href" )
158+ if not link .endswith ("json" ):
159+ continue
160+ links .append (urllib .parse .urljoin (url , link ))
161+ return links
0 commit comments