2525
2626import asyncio
2727import dataclasses
28- import json
28+ import re
29+ import logging
2930from typing import Any
3031from typing import Iterable
3132from typing import Mapping
3233from typing import Set
3334from typing import Tuple
3435
36+ import requests
3537from dephell_specifier import RangeSpecifier
3638from packageurl import PackageURL
37- import requests
3839from schema import Or
3940from schema import Regex
4041from schema import Schema
4445from vulnerabilities .data_source import DataSourceConfiguration
4546from vulnerabilities .data_source import Reference
4647from vulnerabilities .package_managers import PypiVersionAPI
47- from vulnerabilities .helpers import create_etag
48+
49+ logger = logging .getLogger (__name__ )
4850
4951
5052def validate_schema (advisory_dict ):
5153
52- scheme = {
53- str : [
54- {
55- "advisory" : str ,
56- "cve" : Or (None , Regex (r"CVE-\d+-\d+" )),
57- "id" : Regex (r"^pyup.io-\d" ),
58- "specs" : list ,
59- "v" : str ,
60- }
61- ]
62- }
54+ scheme = [
55+ {
56+ "advisory" : str ,
57+ "cve" : Or (None , str ),
58+ "id" : Regex (r"^pyup.io-\d" ),
59+ "specs" : list ,
60+ "v" : str ,
61+ }
62+ ]
6363
6464 Schema (scheme ).validate (advisory_dict )
6565
@@ -77,7 +77,6 @@ class SafetyDbDataSource(DataSource):
7777 def __init__ (self , * args , ** kwargs ):
7878 super ().__init__ (* args , ** kwargs )
7979 self ._api_response = self ._fetch ()
80- # validate_schema(self._api_response)
8180
8281 def __enter__ (self ):
8382 self ._versions = PypiVersionAPI ()
@@ -91,9 +90,8 @@ def set_api(self, packages):
9190 asyncio .run (self ._versions .load_api (packages ))
9291
9392 def _fetch (self ) -> Mapping [str , Any ]:
94- if create_etag (data_src = self , url = self .config .url , etag_key = "ETag" ):
93+ if self . create_etag (self .config .url ):
9594 return requests .get (self .config .url ).json ()
96-
9795 return []
9896
9997 def collect_packages (self ):
@@ -103,22 +101,33 @@ def updated_advisories(self) -> Set[Advisory]:
103101 advisories = []
104102
105103 for package_name in self ._api_response :
104+ if package_name == "$meta" :
105+ # This is the first entry in the data feed. It contains metadata of the feed.
106+ # Skip it.
107+ continue
108+
109+ try :
110+ validate_schema (self ._api_response [package_name ])
111+
112+ except Exception as e :
113+ logger .error (e )
114+ continue
115+
106116 all_package_versions = self .versions .get (package_name )
107- if len (all_package_versions ) == 0 :
117+ if not len (all_package_versions ):
108118 # PyPi does not have data about this package, we skip these
109119 continue
110120
111121 for advisory in self ._api_response [package_name ]:
112-
113122 impacted_purls , resolved_purls = categorize_versions (
114123 package_name , all_package_versions , advisory ["specs" ]
115124 )
116125
117- cve_ids = advisory . get ( "cve" ) or [ "" ]
118-
119- # meaning if cve_ids is not [''] but either [' CVE-123'] or ['CVE-123, CVE-124']
120- if len ( cve_ids [ 0 ]) :
121- cve_ids = [s . strip () for s in cve_ids . split ( "," ) ]
126+ if advisory [ "cve" ]:
127+ # Check on advisory["cve"] instead of using `get` because it can have null value
128+ cve_ids = re . findall ( r" CVE-\d+-\d+" , advisory [ "cve" ])
129+ else :
130+ cve_ids = [None ]
122131
123132 reference = [Reference (reference_id = advisory ["id" ])]
124133
@@ -135,17 +144,29 @@ def updated_advisories(self) -> Set[Advisory]:
135144
136145 return self .batch_advisories (advisories )
137146
147+ def create_etag (self , url ):
148+ etag = requests .head (url ).headers .get ("ETag" )
149+ if not etag :
150+ # Kind of inaccurate to return True since etag is
151+ # not created
152+ return True
153+ elif url in self .config .etags :
154+ if self .config .etags [url ] == etag :
155+ return False
156+ self .config .etags [url ] = etag
157+ return True
158+
138159
139160def categorize_versions (
140161 package_name : str ,
141162 all_versions : Set [str ],
142- version_ranges : Iterable [str ],
163+ version_specs : Iterable [str ],
143164) -> Tuple [Set [PackageURL ], Set [PackageURL ]]:
144165 """
145166 :return: impacted, resolved purls
146167 """
147168 impacted_versions , impacted_purls = set (), set ()
148- ranges = [RangeSpecifier (s ) for s in version_ranges ]
169+ ranges = [RangeSpecifier (s ) for s in version_specs ]
149170
150171 for version in all_versions :
151172 if any ([version in r for r in ranges ]):
0 commit comments