Skip to content

Commit c18cc5d

Browse files
committed
Implement NginxVersionRange
Create NginxVersionRange.from_native method Contributed-by: Hritik Vijay <hritikxx8@gmail.com> Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent 79e3491 commit c18cc5d

1 file changed

Lines changed: 155 additions & 4 deletions

File tree

src/univers/version_range.py

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download.
66

77
import attr
8+
import semantic_version
89
from packaging.specifiers import SpecifierSet
9-
from semantic_version import NpmSpec
1010
from semantic_version.base import AllOf
1111
from semantic_version.base import AnyOf
1212

@@ -178,7 +178,7 @@ def from_native(cls, string):
178178

179179
# an NpmSpec handles parsing of both the semver versions and node-semver
180180
# ranges at once
181-
spec = NpmSpec(string)
181+
spec = semantic_version.NpmSpec(string)
182182

183183
clause = spec.clause.simplify()
184184
assert isinstance(clause, (AnyOf, AllOf))
@@ -239,7 +239,7 @@ def from_native(cls, string):
239239

240240
# replace Rubygem ~> pessimistic operator by node-semver equivalent
241241
string = string.replace("~>", "~")
242-
spec = NpmSpec(string)
242+
spec = semantic_version.NpmSpec(string)
243243

244244
clause = spec.clause.simplify()
245245
assert isinstance(clause, (AnyOf, AllOf))
@@ -266,13 +266,15 @@ class PypiVersionRange(VersionRange):
266266
version_class = versions.PypiVersion
267267

268268
vers_by_native_comparators = {
269+
# 01.01.01 is equal 1.1.1 e.g., with version normalization
269270
"==": "=",
270271
"!=": "!=",
271272
"<=": "<=",
272273
">=": ">=",
273274
"<": "<",
274275
">": ">",
275276
"~=": None,
277+
# 01.01.01 is NOT equal to 1.1.1 using === which is strict string equality
276278
"===": None,
277279
}
278280

@@ -371,8 +373,157 @@ class ArchLinuxVersionRange(VersionRange):
371373

372374

373375
class NginxVersionRange(VersionRange):
376+
"""
377+
Nginx versioning is semver for version and their own syntax for ranges as
378+
used in their security advisories.
379+
380+
The documentation on these ranges is minimal. See these for details:
381+
- https://mailman.nginx.org/pipermail/nginx/2021-September/061039.html
382+
- https://nginx.org/en/security_advisories.html
383+
- https://serverfault.com/questions/715049/what-s-the-difference-between-the-mainline-and-stable-branches-of-nginx
384+
385+
In particular for versions:
386+
- the versions are semver.
387+
388+
- versions can be in the one "mainline" branch or one of many "stable" branches.
389+
390+
- for versions in the "mainline" branch, (e.g., development) the minor
391+
segment is an odd number.
392+
393+
- versions in the "stable" branch, (e.g., a release branch) the minor
394+
segment is an even number. Installation are typically made from branch and
395+
its versions.
396+
397+
For example: in 0.6.18 the 6 e.g., semver "minor" segment is either odd or even
398+
- odd (as with "7") means this is the "mainline" branch
399+
- even (as with "4") means this is in a "stable" branch
400+
401+
And for ranges, we have these notations:
402+
403+
- dash ranges: 0.6.18-1.20.0 where start and end are included in the range
404+
405+
- comma ranges: 1.21.0+, 1.20.1+ where any of the condition applies
406+
407+
- plus suffixes: 1.21.0+ where this or any later version in the branch applies
408+
Therefore:
409+
- 1.21.0+ would expand to >=1.21.0 because 21 is odd and this is the
410+
mainline branch
411+
412+
- 1.22.0+ would expand to >=1.22.0,<1.23.0 because 22 is even and this is
413+
one of the stable branches
414+
415+
There are two special version range values:
416+
- "all" means all versions.
417+
- "none" means no version and therefore no version range. It is used only
418+
in one advisory for CVE-2009-4487 and triggers an error.
419+
420+
Some vulnerable ranges are only for Windows builds but the range syntax is
421+
the same. This could be resolved with a specific purl qualifier.
422+
These are prefixed by the string "nginx/Window".
423+
"""
424+
374425
scheme = "nginx"
375-
version_class = None
426+
version_class = versions.SemverVersion
427+
428+
vers_by_native_comparators = {
429+
"==": "=",
430+
"<=": "<=",
431+
">=": ">=",
432+
"<": "<",
433+
">": ">",
434+
}
435+
436+
@classmethod
437+
def from_native(cls, string):
438+
"""
439+
Return a VersionRange built from an nginx range ``string``.
440+
441+
For example:
442+
>>> result = NginxVersionRange.from_native("1.5.10")
443+
>>> assert str(result) == "vers:nginx/1.5.10", str(result)
444+
445+
>>> result = NginxVersionRange.from_native("0.7.52-0.8.39")
446+
>>> assert str(result) == "vers:nginx/<=0.8.39,>=0.7.52", str(result)
447+
448+
>>> result = NginxVersionRange.from_native("1.1.4-1.2.8, 1.3.9-1.4.0")
449+
>>> assert str(result) == "vers:nginx/<=1.2.8,>=1.1.4|<=1.4.0,>=1.3.9", str(result)
450+
451+
>>> result = NginxVersionRange.from_native("0.8.40+, 0.7.66+")
452+
>>> assert str(result) == "vers:nginx/<0.9.0,>=0.8.40|>=0.7.66", str(result)
453+
454+
>>> result = NginxVersionRange.from_native("1.5.0+, 1.4.1+")
455+
>>> assert str(result) == "vers:nginx/<1.5.0,>=1.4.1|>=1.5.0", str(result)
456+
457+
>>> result = NginxVersionRange.from_native("all")
458+
>>> assert str(result) == "vers:nginx/*", str(result)
459+
460+
>>> try:
461+
... NginxVersionRange.from_native("none")
462+
... except ValueError:
463+
... pass
464+
"""
465+
cleaned = remove_spaces(string).lower()
466+
if cleaned == "all":
467+
return cls(constraints=[[VersionConstraint(comparator="*")]])
468+
469+
anyof_constraints = []
470+
471+
for allof_clauses in cleaned.split(","):
472+
473+
if "-" in allof_clauses:
474+
# dash range
475+
start, _, end = allof_clauses.partition("-")
476+
start_version = semantic_version.Version.coerce(start)
477+
end_version = semantic_version.Version.coerce(end)
478+
vstart = VersionConstraint(comparator=">=", version=start_version)
479+
vend = VersionConstraint(comparator="<=", version=end_version)
480+
allof_constaints = [vstart, vend]
481+
anyof_constraints.append(allof_constaints)
482+
483+
elif "+" in allof_clauses:
484+
# suffixed version
485+
vs = allof_clauses.rstrip("+")
486+
version = semantic_version.Version.coerce(vs)
487+
is_stable = is_even(version.minor)
488+
489+
if is_stable:
490+
# we have a start and end in stable ranges
491+
start_version = semantic_version.Version.coerce(vs)
492+
end_version = start_version.next_minor()
493+
vstart = VersionConstraint(comparator=">=", version=start_version)
494+
vend = VersionConstraint(comparator="<", version=end_version)
495+
allof_constaints = [vstart, vend]
496+
anyof_constraints.append(allof_constaints)
497+
else:
498+
# mainline branch ranges are resolved to a singel constraint
499+
version = semantic_version.Version.coerce(vs)
500+
constraint = VersionConstraint(comparator=">=", version=version)
501+
allof_constaints = [constraint]
502+
anyof_constraints.append(allof_constaints)
503+
504+
else:
505+
# plain single version
506+
version = semantic_version.Version.coerce(allof_clauses)
507+
constraint = VersionConstraint(comparator="=", version=version)
508+
allof_constaints = [constraint]
509+
anyof_constraints.append(allof_constaints)
510+
511+
return cls(constraints=anyof_constraints)
512+
513+
514+
def is_even(s):
515+
"""
516+
Return True if the string "s" is an even number and False if this is an odd
517+
number. For example:
518+
519+
>>> is_even(4)
520+
True
521+
>>> is_even(123)
522+
False
523+
>>> is_even(0)
524+
True
525+
"""
526+
return (int(s) % 2) == 0
376527

377528

378529
RANGE_CLASS_BY_SCHEMES = {

0 commit comments

Comments
 (0)