99
1010# Author: Navonil Das (@NavonilDas)
1111import logging
12+ from pathlib import Path
1213from typing import Iterable
1314from typing import List
1415from typing import Optional
15- from typing import Set
16- from typing import Tuple
1716from urllib .parse import quote
1817
1918import pytz
2423from univers .versions import SemverVersion
2524
2625from vulnerabilities .importer import AdvisoryData
27- from vulnerabilities .importer import GitConfig
26+ from vulnerabilities .importer import AffectedPackage
2827from vulnerabilities .importer import GitImporter
2928from vulnerabilities .importer import Reference
3029from vulnerabilities .package_managers import NpmVersionAPI
31- from vulnerabilities .package_managers import PackageVersion
3230from vulnerabilities .utils import load_json
33- from vulnerabilities .utils import nearest_patched_package
3431
3532NPM_URL = "https://registry.npmjs.org{}"
3633logger = logging .getLogger (__name__ )
3936class NpmImporter (GitImporter ):
4037 license_url = "https://github.com/nodejs/security-wg/blob/main/LICENSE.md"
4138 spdx_license_expression = "MIT"
42- config = GitConfig (
43- repository_url = "https://github.com/nodejs/security-wg.git" ,
44- working_directory = "npm" ,
45- branch = "main" ,
46- )
47- cutoff_timestamp = 1
4839
4940 def __init__ (self ):
50- super ().__init__ (config = self .config , cutoff_timestamp = self .cutoff_timestamp )
51- self ._added_files , self ._updated_files = self .file_changes (
52- recursive = True , file_ext = "json" , subdir = "vuln/npm"
53- )
54- self .pkg_manager_api = NpmVersionAPI ()
55-
56- def parse_advisory_data (self , record ) -> Optional [AdvisoryData ]:
57- package_name = record ["module_name" ].strip ()
58-
59- publish_date = parse (record ["updated_at" ])
60- publish_date = publish_date .replace (tzinfo = pytz .UTC )
61-
62- all_versions = self .pkg_manager_api .fetch (package_name )
63- aff_range = record .get ("vulnerable_versions" )
64- if not aff_range :
65- aff_range = ""
66- fixed_range = record .get ("patched_versions" )
67- if not fixed_range :
68- fixed_range = ""
69-
70- # if aff_range == "*" or fixed_range == "*":
71- # return None
72-
73- impacted_versions , resolved_versions = categorize_versions (
74- all_versions , aff_range , fixed_range
75- )
76-
77- impacted_purls = _versions_to_purls (package_name , impacted_versions )
78- resolved_purls = _versions_to_purls (package_name , resolved_versions )
79-
80- vuln_reference = [
81- Reference (
82- url = NPM_URL .format (f'/-/npm/v1/advisories/{ record ["id" ]} ' ),
83- reference_id = record ["id" ],
84- )
85- ]
86- cve_id = record .get ("cves" ) or []
87-
88- return AdvisoryData (
89- aliases = cve_id ,
90- summary = record .get ("overview" , "" ),
91- affected_packages = nearest_patched_package (impacted_purls , resolved_purls ),
92- references = vuln_reference ,
93- date_published = publish_date ,
94- )
41+ super ().__init__ (repo_url = "git+https://github.com/nodejs/security-wg.git" )
9542
9643 def advisory_data (self ) -> Iterable [AdvisoryData ]:
97- files = self ._updated_files .union (self ._added_files )
44+ self .clone ()
45+ path = Path (self .vcs_response .dest_dir )
46+
47+ glob = "vuln/npm/**/*.json" # subdir="vuln/npm"
48+ files = (p for p in path .glob (glob ) if p .is_file ())
9849 for file in files :
50+ print (file )
9951 record = load_json (file )
100- yield self .parse_advisory_data (record )
101-
102-
103- def _versions_to_purls (package_name , versions ):
104- purls = {f"pkg:npm/{ quote (package_name )} @{ v } " for v in versions }
105- return [PackageURL .from_string (s ) for s in purls ]
106-
107-
108- def normalize_ranges (version_range_string ):
109- """
110- - Splits version range strings with "||" operator into separate ranges.
111- - Removes spaces between range operator and range operands
112- - Normalizes 'x' ranges
113- Example:
114- >>> z = normalize_ranges(">=6.1.3 < 7.0.0 || >=7.0.3")
115- >>> assert z == [">=6.1.3,<7.0.0", ">=7.0.3"]
116- """
117-
118- version_ranges = version_range_string .split ("||" )
119- version_ranges = list (map (str .strip , version_ranges ))
120- for id , version_range in enumerate (version_ranges ):
121-
122- # TODO: This is cryptic, simplify this if possible
123- version_ranges [id ] = "," .join (version_range .split ())
124- version_ranges [id ] = version_ranges [id ].replace (">=," , ">=" )
125- version_ranges [id ] = version_ranges [id ].replace ("<=," , "<=" )
126- version_ranges [id ] = version_ranges [id ].replace ("<=," , "<=" )
127- version_ranges [id ] = version_ranges [id ].replace ("<," , "<" )
128- version_ranges [id ] = version_ranges [id ].replace (">," , ">" )
129-
130- # "x" is interpretted as wild card character here. These are not part of semver
131- # spec. We replace the "x" with aribitarily large number to simulate the effect.
132- if ".x." in version_ranges [id ]:
133- version_ranges [id ] = version_ranges [id ].replace (".x" , ".10000.0" )
134- if ".x" in version_ranges [id ]:
135- version_ranges [id ] = version_ranges [id ].replace (".x" , ".10000" )
136-
137- return version_ranges
138-
139-
140- def categorize_versions (
141- all_versions : Iterable [PackageVersion ],
142- affected_version_range : str ,
143- fixed_version_range : str ,
144- ) -> Tuple [Set [SemverVersion ], Set [SemverVersion ]]:
145- """
146- Seperate list of affected versions and unaffected versions from all versions
147- using the ranges specified.
148-
149- :return: impacted, resolved versions
150- """
151- if not all_versions :
152- # NPM registry has no data regarding this package, we skip these
153- return set (), set ()
154-
155- aff_spec = []
156- fix_spec = []
157-
158- if affected_version_range :
159- aff_spec = get_version_range (affected_version_range )
160-
161- if fixed_version_range :
162- fix_spec = get_version_range (fixed_version_range )
163-
164- aff_ver , fix_ver = set (), set ()
52+ yield parse_advisory_data (record )
16553
166- # Unaffected version is that version which is in the fixed_version_range
167- # or which is absent in the affected_version_range
168- for ver in get_all_versions (all_versions ):
16954
170- if not any ([ ver in spec for spec in aff_spec ]) or any ([ ver in spec for spec in fix_spec ]) :
171- fix_ver . add ( ver )
172- else :
173- aff_ver . add ( ver )
55+ def parse_advisory_data ( record ) -> Optional [ AdvisoryData ] :
56+ cves = record . get ( "cves" ) or []
57+ overview = record . get ( "overview" , "" )
58+ package_name = record [ "module_name" ]. strip ( )
17459
175- return aff_ver , fix_ver
60+ publish_date = parse (record ["updated_at" ])
61+ publish_date = publish_date .replace (tzinfo = pytz .UTC )
17662
63+ pkg_manager_api = NpmVersionAPI ()
64+ all_versions = pkg_manager_api .fetch (package_name )
65+ aff_range = record .get ("vulnerable_versions" ) or ""
66+ fixed_range = record .get ("patched_versions" ) or ""
17767
178- def get_version_range (version_range ) -> List [NpmVersionRange ]:
179- fix_specs = normalize_ranges (version_range )
180- ver_range_objs = []
181- for spec in fix_specs :
182- if len (spec ) >= 3 :
183- try :
184- ver_range_objs .append (NpmVersionRange .from_string (f"vers:npm/{ spec } " ))
185- except InvalidVersion :
186- logger .error (f"InvalidVersionRange { spec } " )
68+ fixed_versions = get_fixed_version (
69+ map_all_versions (all_versions ), NpmVersionRange .from_native (fixed_range )
70+ )
71+ # if aff_range == "*" or fixed_range == "*":
72+ # return None
18773
188- return ver_range_objs
74+ vuln_reference = [
75+ Reference (
76+ url = NPM_URL .format (f'/-/npm/v1/advisories/{ record ["id" ]} ' ),
77+ reference_id = record ["id" ],
78+ )
79+ ]
80+
81+ return AdvisoryData (
82+ aliases = cves ,
83+ summary = overview ,
84+ affected_packages = [
85+ AffectedPackage (
86+ package = PackageURL .from_string (f"pkg:npm/{ quote (package_name )} " ),
87+ affected_version_range = NpmVersionRange .from_native (aff_range ),
88+ fixed_version = fixed_version ,
89+ )
90+ for fixed_version in fixed_versions
91+ ],
92+ references = vuln_reference ,
93+ date_published = publish_date ,
94+ )
18995
19096
191- def get_all_versions (all_versions ) -> List [SemverVersion ]:
97+ def map_all_versions (all_versions ) -> List [SemverVersion ]:
19298 """
99+ map all versions from PackageVersion to SemverVersion
193100
194- Args :
195- all_versions:
101+ Parameters :
102+ all_versions (PackageVersion): List of PackageVersion
196103
197104 Returns:
198-
105+ List[SemverVersion]: return a list of SemverVersion
199106 """
200107 ver_objs = []
201108 for ver in all_versions :
@@ -204,3 +111,22 @@ def get_all_versions(all_versions) -> List[SemverVersion]:
204111 except InvalidVersion :
205112 logger .error (f"InvalidVersion { ver .value } " )
206113 return ver_objs
114+
115+
116+ def get_fixed_version (
117+ all_versions : List [SemverVersion ], aff_range : NpmVersionRange
118+ ) -> List [SemverVersion ]:
119+ """
120+ return a list of SemverVersion fixed versions
121+ """
122+ try :
123+ fixed_versions = []
124+ if not all_versions or not all_versions :
125+ return fixed_versions
126+ for v in all_versions :
127+ if v in aff_range :
128+ fixed_versions .append (v )
129+ return fixed_versions
130+ except Exception as e :
131+ logger .error (e )
132+ return []
0 commit comments