77# See https://aboutcode.org for more information about nexB OSS projects.
88#
99
10- import re
1110from pathlib import Path
1211from typing import Iterable
1312from typing import Tuple
1615from fetchcode .vcs import fetch_via_vcs
1716from packageurl import PackageURL
1817from pytz import UTC
19- from univers .version_constraint import VersionConstraint
18+ from univers .version_range import InvalidVersionRange
2019from univers .version_range import PypiVersionRange
21- from univers .versions import PypiVersion
2220
23- from vulnerabilities .importer import AdvisoryData
21+ from vulnerabilities .importer import AdvisoryDataV2
2422from vulnerabilities .importer import AffectedPackageV2
2523from vulnerabilities .importer import ReferenceV2
2624from vulnerabilities .pipelines import VulnerableCodeBaseImporterPipelineV2
@@ -42,7 +40,6 @@ class OSSAImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
4240 def steps (cls ):
4341 return (
4442 cls .clone ,
45- cls .fetch ,
4643 cls .collect_and_store_advisories ,
4744 cls .clean_downloads ,
4845 )
@@ -51,41 +48,33 @@ def clone(self):
5148 self .log (f"Cloning `{ self .repo_url } `" )
5249 self .vcs_response = fetch_via_vcs (self .repo_url )
5350
54- def fetch (self ):
51+ def get_processable_files (self ) -> Iterable [Path ]:
52+ """
53+ Returns a list of OSSA YAML files that are eligible for processing based on the cutoff year.
54+ """
5555 ossa_dir = Path (self .vcs_response .dest_dir ) / "ossa"
56- self .processable_advisories = []
57- skipped_old = 0
58-
5956 for file_path in sorted (ossa_dir .glob ("OSSA-*.yaml" )):
60- data = load_yaml (str (file_path ))
61-
62- date_str = data .get ("date" )
63- date_published = dateparser .parse (str (date_str )).replace (tzinfo = UTC )
64- if date_published .year < self .cutoff_year :
65- skipped_old += 1
66- continue
67-
68- self .processable_advisories .append (file_path )
69-
70- if skipped_old > 0 :
71- self .log (f"Skipped { skipped_old } advisories older than { self .cutoff_year } " )
72- self .log (f"Fetched { len (self .processable_advisories )} processable advisories" )
57+ filename = file_path .stem
58+ year = int (filename .split ("-" )[1 ])
59+ if year >= self .cutoff_year :
60+ yield file_path
7361
7462 def advisories_count (self ) -> int :
75- return len ( self .processable_advisories )
63+ return sum ( 1 for _ in self .get_processable_files () )
7664
77- def collect_advisories (self ) -> Iterable [AdvisoryData ]:
78- for file_path in self .processable_advisories :
65+ def collect_advisories (self ) -> Iterable [AdvisoryDataV2 ]:
66+ for file_path in self .get_processable_files () :
7967 advisory = self .process_file (file_path )
8068 yield advisory
8169
82- def process_file (self , file_path ) -> AdvisoryData :
70+ def process_file (self , file_path ) -> AdvisoryDataV2 :
8371 """Parse a single OSSA YAML file and extract advisory data"""
8472 data = load_yaml (str (file_path ))
8573 ossa_id = data .get ("id" )
8674
8775 date_str = data .get ("date" )
88- date_published = dateparser .parse (str (date_str )).replace (tzinfo = UTC )
76+ if date_str :
77+ date_published = dateparser .parse (str (date_str )).replace (tzinfo = UTC )
8978
9079 aliases = []
9180 for vulnerability in data .get ("vulnerabilities" ):
@@ -106,7 +95,8 @@ def process_file(self, file_path) -> AdvisoryData:
10695 )
10796
10897 references = []
109- for link in (data .get ("issues" )).get ("links" ):
98+ issues = data .get ("issues" )
99+ for link in issues .get ("links" , []):
110100 references .append (ReferenceV2 (url = str (link )))
111101 reviews = data .get ("reviews" )
112102 for branch , links in reviews .items ():
@@ -120,12 +110,12 @@ def process_file(self, file_path) -> AdvisoryData:
120110 description = data .get ("description" )
121111 summary = f"{ title } \n \n { description } "
122112 url = f"https://security.openstack.org/ossa/{ ossa_id } .html"
123- return AdvisoryData (
113+ return AdvisoryDataV2 (
124114 advisory_id = ossa_id ,
125115 aliases = aliases ,
126116 summary = summary ,
127117 affected_packages = affected_packages ,
128- references_v2 = references ,
118+ references = references ,
129119 date_published = date_published ,
130120 url = url ,
131121 )
@@ -158,56 +148,14 @@ def expand_products(self, product_str, version_str) -> Iterable[Tuple[str, str]]
158148 yield product_str , version_str
159149
160150 def parse_version_range (self , version_str : str ) -> PypiVersionRange :
161- """
162- Normalizes the version string and extracts individual constraints to create a PypiVersionRange object.
163- """
164- original_version_str = version_str
151+ """Parse a version string from OSSA advisories into a PypiVersionRange object."""
165152
166- if version_str .lower () == "all versions" :
167- self .log (f"Skipping 'all versions' - cannot parse to specific range" )
153+ try :
154+ return PypiVersionRange .from_ossa_native (version_str )
155+ except InvalidVersionRange as e :
156+ self .log (f"Failed to parse version range { version_str !r} : { e } " )
168157 return None
169158
170- # Normalize "and" to comma
171- # "<=5.0.3, >=6.0.0 <=6.1.0 and ==7.0.0" -> "<=5.0.3, >=6.0.0 <=6.1.0, ==7.0.0"
172- version_str = version_str .lower ().replace (" and " , "," )
173-
174- # Remove spaces around operators
175- # "<=5.0.3, >=6.0.0 <=6.1.0, ==7.0.0" -> "<=5.0.3,>=6.0.0<=6.1.0,==7.0.0"
176- version_str = re .sub (r"\s+([<>=!]+)" , r"\1" , version_str )
177- version_str = re .sub (r"([<>=!]+)\s+" , r"\1" , version_str )
178-
179- # Insert comma between consecutive constraints
180- # "<=5.0.3,>=6.0.0<=6.1.0,==7.0.0" -> "<=5.0.3,>=6.0.0,<=6.1.0,==7.0.0"
181- version_str = re .sub (r"(\d)([<>=!])" , r"\1,\2" , version_str )
182-
183- constraints = []
184- for part in version_str .split ("," ):
185- comparator = None
186- version = part
187-
188- for op in ["==" , "!=" , "<=" , ">=" , "<" , ">" , "=" ]:
189- if part .startswith (op ):
190- comparator = op
191- version = part [len (op ) :].strip ()
192- break
193-
194- # Default to "=" if no comparator is found
195- # "1.16.0" -> "=1.16.0"
196- if comparator is None :
197- comparator = "="
198- # "==27.0.0" -> "=27.0.0"
199- if comparator == "==" :
200- comparator = "="
201- try :
202- constraints .append (
203- VersionConstraint (comparator = comparator , version = PypiVersion (version ))
204- )
205- except ValueError as e :
206- self .log (f"Failed to parse version '{ version } ' from '{ original_version_str } ' : { e } " )
207- continue
208-
209- return PypiVersionRange (constraints = constraints ) if constraints else None
210-
211159 def clean_downloads (self ):
212160 if self .vcs_response :
213161 self .log ("Removing cloned repository" )
0 commit comments