diff --git a/go.mod b/go.mod index c3f191317d..1521d47a49 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/aquasecurity/trivy go 1.15 require ( - github.com/Masterminds/semver/v3 v3.1.0 github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 github.com/aquasecurity/fanal v0.0.0-20200820074632-6de62ef86882 github.com/aquasecurity/go-dep-parser v0.0.0-20190819075924-ea223f0ef24b - github.com/aquasecurity/trivy-db v0.0.0-20201025093117-4ef51a6e2c4b + github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce + github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 + github.com/aquasecurity/go-version v0.0.0-20201115065329-578079e4ab05 + github.com/aquasecurity/trivy-db v0.0.0-20201117092632-b09c30858fc2 github.com/caarlos0/env/v6 v6.0.0 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cheggaaa/pb/v3 v3.0.3 @@ -27,7 +29,7 @@ require ( github.com/stretchr/testify v1.6.1 github.com/testcontainers/testcontainers-go v0.3.1 github.com/twitchtv/twirp v5.10.1+incompatible - github.com/urfave/cli/v2 v2.2.0 + github.com/urfave/cli/v2 v2.3.0 go.uber.org/atomic v1.5.1 // indirect go.uber.org/multierr v1.4.0 // indirect go.uber.org/zap v1.13.0 diff --git a/go.sum b/go.sum index 9c46a284b0..cf094ccda9 100644 --- a/go.sum +++ b/go.sum @@ -34,15 +34,19 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v38.0.0+incompatible h1:3D2O4g8AwDwyWkM1HpMFVux/ccQJmGJHXsE004Wsu1Q= github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.3 h1:OZEIaBbMdUE/Js+BQKlpO81XlISgipr6yDJ+PSwsgi4= github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/adal v0.8.1 h1:pZdL8o72rK+avFWl+p9nE8RWi1JInZrWJYlnpfXJwHk= github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= @@ -50,7 +54,9 @@ github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -58,8 +64,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0 h1:wykTgKwhVr2t2qs+xI020s6W5dt614QqCHV+7W9dg64= github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= -github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= -github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA= @@ -87,10 +91,18 @@ github.com/aquasecurity/fanal v0.0.0-20200820074632-6de62ef86882 h1:65VcAKqhkKwM github.com/aquasecurity/fanal v0.0.0-20200820074632-6de62ef86882/go.mod h1:VP1+n6hMi6krpA0umEl0CkJp4hbg0R21kXWg0mjrekc= github.com/aquasecurity/go-dep-parser v0.0.0-20190819075924-ea223f0ef24b h1:55Ulc/gvfWm4ylhVaR7MxOwujRjA6et7KhmUbSgUFf4= github.com/aquasecurity/go-dep-parser v0.0.0-20190819075924-ea223f0ef24b/go.mod h1:BpNTD9vHfrejKsED9rx04ldM1WIbeyXGYxUrqTVwxVQ= +github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= +github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= +github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 h1:eveqE9ivrt30CJ7dOajOfBavhZ4zPqHcZe/4tKp0alc= +github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798/go.mod h1:hxbJZtKlO4P8sZ9nztizR6XLoE33O+BkPmuYQ4ACyz0= +github.com/aquasecurity/go-version v0.0.0-20201107203531-5e48ac5d022a h1:SMEtDBnLyP/EVOeJhj4yeR8GYPFpBsFBk3lSrpjZ8yI= +github.com/aquasecurity/go-version v0.0.0-20201107203531-5e48ac5d022a/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= +github.com/aquasecurity/go-version v0.0.0-20201115065329-578079e4ab05 h1:q0ZpFBjwzDk1ofey7gJ2kfA6ZNi2PeBWxNzmRPrfetA= +github.com/aquasecurity/go-version v0.0.0-20201115065329-578079e4ab05/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= github.com/aquasecurity/testdocker v0.0.0-20200426142840-5f05bce6f12a h1:hsw7PpiymXP64evn/K7gsj3hWzMqLrdoeE6JkqDocVg= github.com/aquasecurity/testdocker v0.0.0-20200426142840-5f05bce6f12a/go.mod h1:psfu0MVaiTDLpNxCoNsTeILSKY2EICBwv345f3M+Ffs= -github.com/aquasecurity/trivy-db v0.0.0-20201025093117-4ef51a6e2c4b h1:y4DTBr/S6WRhZ7jQX9nZ00bElwYM7k/653M/JfoaJgI= -github.com/aquasecurity/trivy-db v0.0.0-20201025093117-4ef51a6e2c4b/go.mod h1:+3+NEz0U0NCgO87Cyk0dy3SwH7CI6J4HUeCqqPj1fvQ= +github.com/aquasecurity/trivy-db v0.0.0-20201117092632-b09c30858fc2 h1:AXA9aW464copH1GTKv35yCwztJsqDVZWKfCtBuMpI9U= +github.com/aquasecurity/trivy-db v0.0.0-20201117092632-b09c30858fc2/go.mod h1:+3+NEz0U0NCgO87Cyk0dy3SwH7CI6J4HUeCqqPj1fvQ= github.com/aquasecurity/vuln-list-update v0.0.0-20191016075347-3d158c2bf9a2 h1:xbdUfr2KE4THsFx9CFWtWpU91lF+YhgP46moV94nYTA= github.com/aquasecurity/vuln-list-update v0.0.0-20191016075347-3d158c2bf9a2/go.mod h1:6NhOP0CjZJL27bZZcaHECtzWdwDDm2g6yCY0QgXEGQQ= github.com/araddon/dateparse v0.0.0-20190426192744-0d74ffceef83/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= @@ -560,6 +572,8 @@ github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= @@ -937,6 +951,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= diff --git a/integration/testdata/busybox-with-lockfile.json.golden b/integration/testdata/busybox-with-lockfile.json.golden index 9ba2d5e2ef..5654d8f010 100644 --- a/integration/testdata/busybox-with-lockfile.json.golden +++ b/integration/testdata/busybox-with-lockfile.json.golden @@ -7,11 +7,12 @@ "VulnerabilityID": "RUSTSEC-2019-0001", "PkgName": "ammonia", "InstalledVersion": "1.9.0", + "FixedVersion": "\u003e= 2.1.0", "Layer": { "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" }, "Title": "Uncontrolled recursion leads to abort in HTML serialization", - "Description": "Affected versions of this crate did use recursion for serialization of HTML\nDOM trees.\n\nThis allows an attacker to cause abort due to stack overflow by providing\na pathologically nested input.\n\nThe flaw was corrected by serializing the DOM tree iteratively instead.\n", + "Description": "Affected versions of this crate did use recursion for serialization of HTML\nDOM trees.\n\nThis allows an attacker to cause abort due to stack overflow by providing\na pathologically nested input.\n\nThe flaw was corrected by serializing the DOM tree iteratively instead.", "Severity": "UNKNOWN", "References": [ "https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md#210" @@ -21,53 +22,72 @@ "VulnerabilityID": "RUSTSEC-2016-0001", "PkgName": "openssl", "InstalledVersion": "0.8.3", + "FixedVersion": "\u003e= 0.9.0", "Layer": { "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" }, "Title": "SSL/TLS MitM vulnerability due to insecure defaults", - "Description": "All versions of rust-openssl prior to 0.9.0 contained numerous insecure defaults\nincluding off-by-default certificate verification and no API to perform hostname\nverification.\n\nUnless configured correctly by a developer, these defaults could allow an attacker\nto perform man-in-the-middle attacks.\n\nThe problem was addressed in newer versions by enabling certificate verification\nby default and exposing APIs to perform hostname verification. Use the\n`SslConnector` and `SslAcceptor` types to take advantage of these new features\n(as opposed to the lower-level `SslContext` type).\n", + "Description": "All versions of rust-openssl prior to 0.9.0 contained numerous insecure defaults\nincluding off-by-default certificate verification and no API to perform hostname\nverification.\n\nUnless configured correctly by a developer, these defaults could allow an attacker\nto perform man-in-the-middle attacks.\n\nThe problem was addressed in newer versions by enabling certificate verification\nby default and exposing APIs to perform hostname verification. Use the\n`SslConnector` and `SslAcceptor` types to take advantage of these new features\n(as opposed to the lower-level `SslContext` type).", "Severity": "UNKNOWN", "References": [ "https://github.com/sfackler/rust-openssl/releases/tag/v0.9.0" ] }, { - "VulnerabilityID": "RUSTSEC-2018-0010", - "PkgName": "openssl", - "InstalledVersion": "0.8.3", + "VulnerabilityID": "RUSTSEC-2019-0035", + "PkgName": "rand_core", + "InstalledVersion": "0.3.1", + "FixedVersion": "\u003e= 0.4.2", "Layer": { "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" }, - "Title": "Use after free in CMS Signing", - "Description": "Affected versions of the OpenSSL crate used structures after they'd been freed.", + "Title": "Unaligned memory access", + "Description": "Affected versions of this crate violated alignment when casting byte slices to\ninteger slices, resulting in undefined behavior.\n\nThe flaw was corrected by Ralf Jung and Diggory Hardy.", "Severity": "UNKNOWN", "References": [ - "https://github.com/sfackler/rust-openssl/pull/942" + "https://github.com/rust-random/rand/blob/master/rand_core/CHANGELOG.md#050---2019-06-06" ] }, { - "VulnerabilityID": "RUSTSEC-2018-0003", - "PkgName": "smallvec", - "InstalledVersion": "0.6.9", + "VulnerabilityID": "RUSTSEC-2019-0035", + "PkgName": "rand_core", + "InstalledVersion": "0.4.0", + "FixedVersion": "\u003e= 0.4.2", "Layer": { "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" }, - "Title": "Possible double free during unwinding in SmallVec::insert_many", - "Description": "If an iterator passed to `SmallVec::insert_many` panicked in `Iterator::next`,\ndestructors were run during unwinding while the vector was in an inconsistent\nstate, possibly causing a double free (a destructor running on two copies of\nthe same value).\n\nThis is fixed in smallvec 0.6.3 by ensuring that the vector's length is not\nupdated to include moved items until they have been removed from their\noriginal positions. Items may now be leaked if `Iterator::next` panics, but\nthey will not be dropped more than once.\n\nThank you to @Vurich for reporting this bug.\n", + "Title": "Unaligned memory access", + "Description": "Affected versions of this crate violated alignment when casting byte slices to\ninteger slices, resulting in undefined behavior.\n\nThe flaw was corrected by Ralf Jung and Diggory Hardy.", "Severity": "UNKNOWN", "References": [ - "https://github.com/servo/rust-smallvec/issues/96" + "https://github.com/rust-random/rand/blob/master/rand_core/CHANGELOG.md#050---2019-06-06" + ] + }, + { + "VulnerabilityID": "RUSTSEC-2018-0018", + "PkgName": "smallvec", + "InstalledVersion": "0.6.9", + "FixedVersion": "\u003e= 0.6.13", + "Layer": { + "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" + }, + "Title": "smallvec creates uninitialized value of any type", + "Description": "Affected versions of this crate called `mem::uninitialized()` to create values of a user-supplied type `T`.\nThis is unsound e.g. if `T` is a reference type (which must be non-null and thus may not remain uninitialized).\n \nThe flaw was corrected by avoiding the use of `mem::uninitialized()`, using `MaybeUninit` instead.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/servo/rust-smallvec/issues/126" ] }, { "VulnerabilityID": "RUSTSEC-2019-0009", "PkgName": "smallvec", "InstalledVersion": "0.6.9", + "FixedVersion": "\u003e= 0.6.10", "Layer": { "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" }, "Title": "Double-free and use-after-free in SmallVec::grow()", - "Description": "Attempting to call `grow` on a spilled SmallVec with a value equal to the current capacity causes it to free the existing data. This performs a double free immediately and may lead to use-after-free on subsequent accesses to the SmallVec contents.\n\nAn attacker that controls the value passed to `grow` may exploit this flaw to obtain memory contents or gain remote code execution.\n\nCredits to @ehuss for discovering, reporting and fixing the bug.\n", + "Description": "Attempting to call `grow` on a spilled SmallVec with a value equal to the current capacity causes it to free the existing data. This performs a double free immediately and may lead to use-after-free on subsequent accesses to the SmallVec contents.\n\nAn attacker that controls the value passed to `grow` may exploit this flaw to obtain memory contents or gain remote code execution.\n\nCredits to @ehuss for discovering, reporting and fixing the bug.", "Severity": "UNKNOWN", "References": [ "https://github.com/servo/rust-smallvec/issues/148" @@ -77,15 +97,30 @@ "VulnerabilityID": "RUSTSEC-2019-0012", "PkgName": "smallvec", "InstalledVersion": "0.6.9", + "FixedVersion": "\u003e= 0.6.10", "Layer": { "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" }, "Title": "Memory corruption in SmallVec::grow()", - "Description": "Attempting to call `grow` on a spilled SmallVec with a value less than the current capacity causes corruption of memory allocator data structures.\n\nAn attacker that controls the value passed to `grow` may exploit this flaw to obtain memory contents or gain remote code execution.\n\nCredits to @ehuss for discovering, reporting and fixing the bug.\n", + "Description": "Attempting to call `grow` on a spilled SmallVec with a value less than the current capacity causes corruption of memory allocator data structures.\n\nAn attacker that controls the value passed to `grow` may exploit this flaw to obtain memory contents or gain remote code execution.\n\nCredits to @ehuss for discovering, reporting and fixing the bug.", "Severity": "UNKNOWN", "References": [ "https://github.com/servo/rust-smallvec/issues/149" ] + }, + { + "VulnerabilityID": "RUSTSEC-2018-0017", + "PkgName": "tempdir", + "InstalledVersion": "0.3.7", + "Layer": { + "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" + }, + "Title": "`tempdir` crate has been deprecated; use `tempfile` instead", + "Description": "The [`tempdir`](https://crates.io/crates/tempdir) crate has been deprecated\nand the functionality is merged into [`tempfile`](https://crates.io/crates/tempfile).", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/rust-lang-deprecated/tempdir/pull/46" + ] } ] } diff --git a/integration/testdata/trivy.db.gz b/integration/testdata/trivy.db.gz index ba58ee7e03..578eebca52 100644 Binary files a/integration/testdata/trivy.db.gz and b/integration/testdata/trivy.db.gz differ diff --git a/pkg/detector/library/advisory.go b/pkg/detector/library/advisory.go index 31789cfea9..1a0c0f6384 100644 --- a/pkg/detector/library/advisory.go +++ b/pkg/detector/library/advisory.go @@ -4,51 +4,50 @@ import ( "fmt" "strings" - "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" "github.com/aquasecurity/trivy-db/pkg/db" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" - "github.com/aquasecurity/trivy/pkg/scanner/utils" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" "github.com/aquasecurity/trivy/pkg/types" ) // Advisory represents security advisories for each programming language type Advisory struct { - lang string - comparer comparer + ecosystem string + comparer comparer.Comparer } // NewAdvisory is the factory method of Advisory -func NewAdvisory(lang string) *Advisory { +func NewAdvisory(ecosystem string, comparer comparer.Comparer) *Advisory { return &Advisory{ - lang: lang, - comparer: newComparer(lang), + ecosystem: ecosystem, + comparer: comparer, } } -// DetectVulnerabilities scans buckets with the prefix according to the programming language in "Advisory". -// If "lang" is python, it looks for buckets with "python::" and gets security advisories from those buckets. -// It allows us to add a new data source with the lang prefix (e.g. python::new-data-source) +// DetectVulnerabilities scans buckets with the prefix according to the ecosystem in "Advisory". +// If "ecosystem" is pip, it looks for buckets with "pip::" and gets security advisories from those buckets. +// It allows us to add a new data source with the ecosystem prefix (e.g. pip::new-data-source) // and detect vulnerabilities without specifying a specific bucket name. -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { - // e.g. "python::" - prefix := fmt.Sprintf("%s::", s.lang) +func (s *Advisory) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { + // e.g. "pip::", "npm::" + prefix := fmt.Sprintf("%s::", s.ecosystem) advisories, err := db.Config{}.GetAdvisories(prefix, pkgName) if err != nil { - return nil, xerrors.Errorf("failed to get %s advisories: %w", s.lang, err) + return nil, xerrors.Errorf("failed to get %s advisories: %w", s.ecosystem, err) } var vulns []types.DetectedVulnerability for _, advisory := range advisories { - if !s.comparer.isVulnerable(pkgVer, advisory) { + if !s.comparer.IsVulnerable(pkgVer, advisory) { continue } vuln := types.DetectedVulnerability{ VulnerabilityID: advisory.VulnerabilityID, PkgName: pkgName, - InstalledVersion: pkgVer.String(), + InstalledVersion: pkgVer, FixedVersion: s.createFixedVersions(advisory), } vulns = append(vulns, vuln) @@ -74,29 +73,3 @@ func (s *Advisory) createFixedVersions(advisory dbTypes.Advisory) string { } return strings.Join(fixedVersions, ", ") } - -type comparer interface { - isVulnerable(pkgVer *semver.Version, advisory dbTypes.Advisory) bool -} - -func newComparer(lang string) comparer { - switch lang { - // When another library is needed for version comparison, it can be added here. - } - return generalComparer{} -} - -type generalComparer struct{} - -func (c generalComparer) isVulnerable(pkgVer *semver.Version, advisory dbTypes.Advisory) bool { - if len(advisory.VulnerableVersions) != 0 { - return utils.MatchVersions(pkgVer, advisory.VulnerableVersions) - } - - if utils.MatchVersions(pkgVer, advisory.PatchedVersions) || - utils.MatchVersions(pkgVer, advisory.UnaffectedVersions) { - return false - } - - return true -} diff --git a/pkg/detector/library/advisory_test.go b/pkg/detector/library/advisory_test.go index b38d4c64b9..810040faf2 100644 --- a/pkg/detector/library/advisory_test.go +++ b/pkg/detector/library/advisory_test.go @@ -4,40 +4,40 @@ import ( "os" "testing" - "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/aquasecurity/trivy-db/pkg/db" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" "github.com/aquasecurity/trivy/pkg/detector/library" + "github.com/aquasecurity/trivy/pkg/detector/library/bundler" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" "github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/utils" ) func TestAdvisory_DetectVulnerabilities(t *testing.T) { - type fields struct { - lang string - } type args struct { pkgName string - pkgVer *semver.Version + pkgVer string } tests := []struct { - name string - fixtures []string - fields fields - args args - want []types.DetectedVulnerability - wantErr string + name string + fixtures []string + ecosystem string + comparer comparer.Comparer + args args + want []types.DetectedVulnerability + wantErr string }{ { - name: "happy path", - fixtures: []string{"testdata/fixtures/php.yaml"}, - fields: fields{lang: vulnerability.PHP}, + name: "happy path", + fixtures: []string{"testdata/fixtures/php.yaml"}, + ecosystem: vulnerability.Composer, + comparer: comparer.GenericComparer{}, args: args{ pkgName: "symfony/symfony", - pkgVer: semver.MustParse("4.2.6"), + pkgVer: "4.2.6", }, want: []types.DetectedVulnerability{ { @@ -49,12 +49,13 @@ func TestAdvisory_DetectVulnerabilities(t *testing.T) { }, }, { - name: "no patched versions in the advisory", - fixtures: []string{"testdata/fixtures/php.yaml"}, - fields: fields{lang: vulnerability.PHP}, + name: "no patched versions in the advisory", + fixtures: []string{"testdata/fixtures/php.yaml"}, + ecosystem: vulnerability.Composer, + comparer: comparer.GenericComparer{}, args: args{ pkgName: "symfony/symfony", - pkgVer: semver.MustParse("4.4.6"), + pkgVer: "4.4.6", }, want: []types.DetectedVulnerability{ { @@ -66,12 +67,13 @@ func TestAdvisory_DetectVulnerabilities(t *testing.T) { }, }, { - name: "no vulnerable versions in the advisory", - fixtures: []string{"testdata/fixtures/ruby.yaml"}, - fields: fields{lang: vulnerability.Ruby}, + name: "no vulnerable versions in the advisory", + fixtures: []string{"testdata/fixtures/ruby.yaml"}, + ecosystem: vulnerability.RubyGems, + comparer: bundler.RubyGemsComparer{}, args: args{ pkgName: "activesupport", - pkgVer: semver.MustParse("4.1.1"), + pkgVer: "4.1.1", }, want: []types.DetectedVulnerability{ { @@ -83,12 +85,13 @@ func TestAdvisory_DetectVulnerabilities(t *testing.T) { }, }, { - name: "no vulnerability", - fixtures: []string{"testdata/fixtures/php.yaml"}, - fields: fields{lang: vulnerability.PHP}, + name: "no vulnerability", + fixtures: []string{"testdata/fixtures/php.yaml"}, + ecosystem: vulnerability.Composer, + comparer: comparer.GenericComparer{}, args: args{ pkgName: "symfony/symfony", - pkgVer: semver.MustParse("4.4.7"), + pkgVer: "4.4.7", }, }, } @@ -99,7 +102,7 @@ func TestAdvisory_DetectVulnerabilities(t *testing.T) { defer os.RemoveAll(dir) defer db.Close() - adv := library.NewAdvisory(tt.fields.lang) + adv := library.NewAdvisory(tt.ecosystem, tt.comparer) got, err := adv.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer) switch { diff --git a/pkg/detector/library/bundler/advisory.go b/pkg/detector/library/bundler/advisory.go index eaf25053d4..66f1f88f8b 100644 --- a/pkg/detector/library/bundler/advisory.go +++ b/pkg/detector/library/bundler/advisory.go @@ -3,11 +3,10 @@ package bundler import ( "strings" - "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" bundlerSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/bundler" - "github.com/aquasecurity/trivy/pkg/scanner/utils" "github.com/aquasecurity/trivy/pkg/types" ) @@ -18,18 +17,20 @@ type VulnSrc interface { // Advisory implements the bundler VulnSrc type Advisory struct { - vs VulnSrc + comparer RubyGemsComparer + vs VulnSrc } // NewAdvisory is the factory method to return bundler.Advisory func NewAdvisory() *Advisory { return &Advisory{ - vs: bundlerSrc.NewVulnSrc(), + vs: bundlerSrc.NewVulnSrc(), + comparer: RubyGemsComparer{}, } } // DetectVulnerabilities scans and returns Vulnerability in bundler -func (a *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { +func (a *Advisory) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { advisories, err := a.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get bundler advisories: %w", err) @@ -37,17 +38,18 @@ func (a *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) var vulns []types.DetectedVulnerability for _, advisory := range advisories { - if utils.MatchVersions(pkgVer, advisory.PatchedVersions) { - continue + adv := dbTypes.Advisory{ + UnaffectedVersions: advisory.UnaffectedVersions, + PatchedVersions: advisory.PatchedVersions, } - if utils.MatchVersions(pkgVer, advisory.UnaffectedVersions) { + if !a.comparer.IsVulnerable(pkgVer, adv) { continue } vuln := types.DetectedVulnerability{ VulnerabilityID: advisory.VulnerabilityID, PkgName: strings.TrimSpace(pkgName), - InstalledVersion: pkgVer.String(), + InstalledVersion: pkgVer, FixedVersion: strings.Join(advisory.PatchedVersions, ", "), } vulns = append(vulns, vuln) diff --git a/pkg/detector/library/bundler/advisory_test.go b/pkg/detector/library/bundler/advisory_test.go index fafc30ec11..5d970a0f41 100644 --- a/pkg/detector/library/bundler/advisory_test.go +++ b/pkg/detector/library/bundler/advisory_test.go @@ -1,63 +1,84 @@ -package bundler +package bundler_test import ( + "os" "testing" - "github.com/aquasecurity/trivy/pkg/log" - - "github.com/Masterminds/semver/v3" - - bundlerSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/bundler" + "github.com/aquasecurity/trivy/pkg/detector/library/bundler" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" ) -type MockVulnSrc struct { - mock.Mock -} - -func (_m *MockVulnSrc) Get(pkgName string) ([]bundlerSrc.Advisory, error) { - ret := _m.Called(pkgName) - ret0 := ret.Get(0) - if ret0 == nil { - return nil, ret.Error(1) +func TestAdvisory_DetectVulnerabilities(t *testing.T) { + type args struct { + pkgName string + pkgVer string } - advisories, ok := ret0.([]bundlerSrc.Advisory) - if !ok { - return nil, ret.Error(1) + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "detected", + args: args{ + pkgName: "activesupport", + pkgVer: "4.1.1", + }, + fixtures: []string{"testdata/fixtures/gem.yaml"}, + want: []types.DetectedVulnerability{ + { + PkgName: "activesupport", + InstalledVersion: "4.1.1", + VulnerabilityID: "CVE-2015-3226", + FixedVersion: ">= 4.2.2, ~> 4.1.11", + }, + }, + }, + { + name: "not detected", + args: args{ + pkgName: "activesupport", + pkgVer: "4.1.0.a", + }, + fixtures: []string{"testdata/fixtures/gem.yaml"}, + want: nil, + }, + { + name: "invalid JSON", + args: args{ + pkgName: "activesupport", + pkgVer: "4.1.0", + }, + fixtures: []string{"testdata/fixtures/invalid-type.yaml"}, + want: nil, + wantErr: "failed to unmarshal advisory JSON", + }, } - return advisories, ret.Error(1) -} -func TestScanner_Detect(t *testing.T) { log.InitLogger(false, true) - t.Run("Issue #108", func(t *testing.T) { - // https://github.com/aquasecurity/trivy/issues/108 - // Validate that the massaging that happens when parsing the lockfile - // allows us to better handle the platform metadata - mockVulnSrc := new(MockVulnSrc) - mockVulnSrc.On("Get", "ffi").Return( - []bundlerSrc.Advisory{ - { - VulnerabilityID: "NotDetected", - PatchedVersions: []string{">= 1.9.24"}, - }, - { - VulnerabilityID: "Detected", - PatchedVersions: []string{">= 1.9.26"}, - }, - }, nil) - s := Advisory{ - vs: mockVulnSrc, - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := utils.InitTestDB(t, tt.fixtures) + defer os.RemoveAll(dir) - versionStr := "1.9.25-x64-mingw32" - v, err := semver.NewVersion(versionStr) - assert.NoError(t, err) + a := bundler.NewAdvisory() + got, err := a.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer) + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } else { + assert.NoError(t, err) + } - vulns, err := s.DetectVulnerabilities("ffi", v) - - assert.NoError(t, err) - assert.Equal(t, 1, len(vulns)) - }) + assert.Equal(t, tt.want, got) + }) + } } diff --git a/pkg/detector/library/bundler/compare.go b/pkg/detector/library/bundler/compare.go new file mode 100644 index 0000000000..680060fcd7 --- /dev/null +++ b/pkg/detector/library/bundler/compare.go @@ -0,0 +1,32 @@ +package bundler + +import ( + "golang.org/x/xerrors" + + "github.com/aquasecurity/go-gem-version" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" +) + +// RubyGemsComparer represents a comparer for RubyGems +type RubyGemsComparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (r RubyGemsComparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return comparer.IsVulnerable(ver, advisory, r.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (r RubyGemsComparer) matchVersion(currentVersion, constraint string) (bool, error) { + v, err := gem.NewVersion(currentVersion) + if err != nil { + return false, xerrors.Errorf("RubyGems version error (%s): %s", currentVersion, err) + } + + c, err := gem.NewConstraints(constraint) + if err != nil { + return false, xerrors.Errorf("RubyGems constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} diff --git a/pkg/detector/library/bundler/compare_test.go b/pkg/detector/library/bundler/compare_test.go new file mode 100644 index 0000000000..249e3d1fff --- /dev/null +++ b/pkg/detector/library/bundler/compare_test.go @@ -0,0 +1,104 @@ +package bundler_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/bundler" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestRubyGemsComparer_IsVulnerable(t *testing.T) { + type args struct { + currentVersion string + advisory types.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.0"}, + }, + }, + want: false, + }, + { + name: "pre-release", + args: args{ + currentVersion: "1.2.3.a", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3"}, + }, + }, + want: true, + }, + { + name: "pre-release without dot", + args: args{ + currentVersion: "4.1a", + advisory: types.Advisory{ + UnaffectedVersions: []string{"< 4.2b1"}, + }, + }, + want: false, + }, + { + // https://github.com/aquasecurity/trivy/issues/108 + name: "hyphen", + args: args{ + currentVersion: "1.9.25-x86-mingw32", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.9.24"}, + }, + }, + want: false, + }, + { + // https://github.com/aquasecurity/trivy/issues/108 + name: "pessimistic", + args: args{ + currentVersion: "1.8.6-java", + advisory: types.Advisory{ + PatchedVersions: []string{"~> 1.5.5", "~> 1.6.8", ">= 1.7.7"}, + }, + }, + want: false, + }, + { + name: "invalid version", + args: args{ + currentVersion: "1.2..4", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3"}, + }, + }, + want: false, + }, + { + name: "invalid constraint", + args: args{ + currentVersion: "1.2.4", + advisory: types.Advisory{ + PatchedVersions: []string{"!1.2.0"}, + }, + }, + want: false, + }, + } + log.InitLogger(false, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bundler.RubyGemsComparer{} + got := r.IsVulnerable(tt.args.currentVersion, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/bundler/testdata/fixtures/gem.yaml b/pkg/detector/library/bundler/testdata/fixtures/gem.yaml new file mode 100644 index 0000000000..a1a7aac9b7 --- /dev/null +++ b/pkg/detector/library/bundler/testdata/fixtures/gem.yaml @@ -0,0 +1,11 @@ +- bucket: ruby-advisory-db + pairs: + - bucket: activesupport + pairs: + - key: CVE-2015-3226 + value: + PatchedVersions: + - ">= 4.2.2" + - "~> 4.1.11" + UnaffectedVersions: + - "< 4.1.0" \ No newline at end of file diff --git a/pkg/detector/library/bundler/testdata/fixtures/invalid-type.yaml b/pkg/detector/library/bundler/testdata/fixtures/invalid-type.yaml new file mode 100644 index 0000000000..1e64266183 --- /dev/null +++ b/pkg/detector/library/bundler/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,7 @@ +- bucket: ruby-advisory-db + pairs: + - bucket: activesupport + pairs: + - key: CVE-2015-3226 + value: + PatchedVersions: dummy diff --git a/pkg/detector/library/cargo/advisory.go b/pkg/detector/library/cargo/advisory.go index 128e777f08..e5d2656d1d 100644 --- a/pkg/detector/library/cargo/advisory.go +++ b/pkg/detector/library/cargo/advisory.go @@ -3,28 +3,30 @@ package cargo import ( "strings" - "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" cargoSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/cargo" - "github.com/aquasecurity/trivy/pkg/scanner/utils" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" "github.com/aquasecurity/trivy/pkg/types" ) // Advisory encapsulates the cargo vulnerability scanner type Advisory struct { - vs cargoSrc.VulnSrc + vs cargoSrc.VulnSrc + comparer comparer.Comparer } // NewAdvisory is the factory method to return cargo Scanner func NewAdvisory() *Advisory { return &Advisory{ - vs: cargoSrc.NewVulnSrc(), + vs: cargoSrc.NewVulnSrc(), + comparer: comparer.GenericComparer{}, } } // DetectVulnerabilities scans and returns the cargo vulnerabilities -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { advisories, err := s.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get cargo advisories: %w", err) @@ -32,14 +34,23 @@ func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) var vulns []types.DetectedVulnerability for _, advisory := range advisories { - if utils.MatchVersions(pkgVer, advisory.PatchedVersions) { + adv := dbTypes.Advisory{ + UnaffectedVersions: advisory.UnaffectedVersions, + PatchedVersions: advisory.PatchedVersions, + } + if len(adv.UnaffectedVersions) == 0 && len(adv.PatchedVersions) == 0 { + // No patched version + adv.VulnerableVersions = []string{">=0.0.0"} + } + + if !s.comparer.IsVulnerable(pkgVer, adv) { continue } vuln := types.DetectedVulnerability{ VulnerabilityID: advisory.VulnerabilityID, PkgName: strings.TrimSpace(pkgName), - InstalledVersion: pkgVer.String(), + InstalledVersion: pkgVer, FixedVersion: strings.Join(advisory.PatchedVersions, ", "), } vulns = append(vulns, vuln) diff --git a/pkg/detector/library/cargo/advisory_test.go b/pkg/detector/library/cargo/advisory_test.go new file mode 100644 index 0000000000..760c132c3a --- /dev/null +++ b/pkg/detector/library/cargo/advisory_test.go @@ -0,0 +1,100 @@ +package cargo_test + +import ( + "os" + "testing" + + "github.com/aquasecurity/trivy/pkg/detector/library/cargo" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" +) + +func TestAdvisory_DetectVulnerabilities(t *testing.T) { + type args struct { + pkgName string + pkgVer string + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "detected", + args: args{ + pkgName: "bumpalo", + pkgVer: "3.2.0", + }, + fixtures: []string{"testdata/fixtures/cargo.yaml"}, + want: []types.DetectedVulnerability{ + { + PkgName: "bumpalo", + InstalledVersion: "3.2.0", + VulnerabilityID: "RUSTSEC-2020-0006", + FixedVersion: ">= 3.2.1", + }, + }, + }, + { + name: "not detected", + args: args{ + pkgName: "bumpalo", + pkgVer: "3.2.1", + }, + fixtures: []string{"testdata/fixtures/cargo.yaml"}, + want: nil, + }, + { + name: "no patched version", + args: args{ + pkgName: "bumpalo", + pkgVer: "3.2.0", + }, + fixtures: []string{"testdata/fixtures/no-patched-version.yaml"}, + want: []types.DetectedVulnerability{ + { + PkgName: "bumpalo", + InstalledVersion: "3.2.0", + VulnerabilityID: "RUSTSEC-2020-0006", + }, + }, + }, + { + name: "invalid JSON", + args: args{ + pkgName: "bumpalo", + pkgVer: "3.2.1", + }, + fixtures: []string{"testdata/fixtures/invalid-type.yaml"}, + want: nil, + wantErr: "failed to unmarshal advisory JSON", + }, + } + + log.InitLogger(false, true) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := utils.InitTestDB(t, tt.fixtures) + defer os.RemoveAll(dir) + + a := cargo.NewAdvisory() + got, err := a.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer) + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/cargo/testdata/fixtures/cargo.yaml b/pkg/detector/library/cargo/testdata/fixtures/cargo.yaml new file mode 100644 index 0000000000..ab1fec60cf --- /dev/null +++ b/pkg/detector/library/cargo/testdata/fixtures/cargo.yaml @@ -0,0 +1,10 @@ +- bucket: rust-advisory-db + pairs: + - bucket: bumpalo + pairs: + - key: RUSTSEC-2020-0006 + value: + PatchedVersions: + - ">= 3.2.1" + UnaffectedVersions: + - "< 3.0.0" diff --git a/pkg/detector/library/cargo/testdata/fixtures/invalid-type.yaml b/pkg/detector/library/cargo/testdata/fixtures/invalid-type.yaml new file mode 100644 index 0000000000..26749b3df4 --- /dev/null +++ b/pkg/detector/library/cargo/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,7 @@ +- bucket: rust-advisory-db + pairs: + - bucket: bumpalo + pairs: + - key: RUSTSEC-2020-0006 + value: + PatchedVersions: foo diff --git a/pkg/detector/library/cargo/testdata/fixtures/no-patched-version.yaml b/pkg/detector/library/cargo/testdata/fixtures/no-patched-version.yaml new file mode 100644 index 0000000000..8a888b3430 --- /dev/null +++ b/pkg/detector/library/cargo/testdata/fixtures/no-patched-version.yaml @@ -0,0 +1,8 @@ +- bucket: rust-advisory-db + pairs: + - bucket: bumpalo + pairs: + - key: RUSTSEC-2020-0006 + value: + PatchedVersions: + UnaffectedVersions: diff --git a/pkg/detector/library/comparer/compare.go b/pkg/detector/library/comparer/compare.go new file mode 100644 index 0000000000..9b3959f7fa --- /dev/null +++ b/pkg/detector/library/comparer/compare.go @@ -0,0 +1,72 @@ +package comparer + +import ( + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/go-version/pkg/version" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// Comparer is an interface for version comparison +type Comparer interface { + IsVulnerable(currentVersion string, advisory dbTypes.Advisory) bool +} + +type matchVersion func(currentVersion, constraint string) (bool, error) + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func IsVulnerable(pkgVer string, advisory dbTypes.Advisory, match matchVersion) bool { + var matched bool + var err error + + if len(advisory.VulnerableVersions) != 0 { + matched, err = match(pkgVer, strings.Join(advisory.VulnerableVersions, " || ")) + if err != nil { + log.Logger.Warn(err) + return false + } else if !matched { + // the version is not vulnerable + return false + } + } + + secureVersions := append(advisory.PatchedVersions, advisory.UnaffectedVersions...) + if len(secureVersions) == 0 { + // the version matches vulnerable versions and patched/unaffected versions are not provided + // or all values are empty + return matched + } + + matched, err = match(pkgVer, strings.Join(secureVersions, " || ")) + if err != nil { + log.Logger.Warn(err) + return false + } + return !matched +} + +// GenericComparer represents a comparer for semver-like versioning +type GenericComparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (v GenericComparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return IsVulnerable(ver, advisory, v.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (v GenericComparer) matchVersion(currentVersion, constraint string) (bool, error) { + ver, err := version.Parse(currentVersion) + if err != nil { + return false, xerrors.Errorf("version error (%s): %s", currentVersion, err) + } + + c, err := version.NewConstraints(constraint) + if err != nil { + return false, xerrors.Errorf("constraint error (%s): %s", currentVersion, err) + } + + return c.Check(ver), nil +} diff --git a/pkg/detector/library/comparer/compare_test.go b/pkg/detector/library/comparer/compare_test.go new file mode 100644 index 0000000000..c910be25fc --- /dev/null +++ b/pkg/detector/library/comparer/compare_test.go @@ -0,0 +1,96 @@ +package comparer_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestGenericComparer_IsVulnerable(t *testing.T) { + type args struct { + ver string + advisory types.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=1.0"}, + PatchedVersions: []string{">=1.1"}, + }, + }, + }, + { + name: "no patch", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=99.999.99999"}, + PatchedVersions: []string{"<0.0.0"}, + }, + }, + want: true, + }, + { + name: "pre-release", + args: args{ + ver: "1.2.2-alpha", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=1.2.2"}, + PatchedVersions: []string{">=1.2.2"}, + }, + }, + want: true, + }, + { + name: "multiple constraints", + args: args{ + ver: "2.0.0", + advisory: types.Advisory{ + VulnerableVersions: []string{">=1.7.0 <1.7.16", ">=1.8.0 <1.8.8", ">=2.0.0 <2.0.8", ">=3.0.0-beta.1 <3.0.0-beta.7"}, + PatchedVersions: []string{">=3.0.0-beta.7", ">=2.0.8 <3.0.0-beta.1", ">=1.8.8 <2.0.0", ">=1.7.16 <1.8.0"}, + }, + }, + want: true, + }, + { + name: "invalid version", + args: args{ + ver: "1.2..4", + advisory: types.Advisory{ + VulnerableVersions: []string{"<1.0.0"}, + }, + }, + want: false, + }, + { + name: "improper constraint", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"*"}, + PatchedVersions: nil, + }, + }, + want: false, + }, + } + log.InitLogger(false, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := comparer.GenericComparer{} + got := v.IsVulnerable(tt.args.ver, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/composer/advisory.go b/pkg/detector/library/composer/advisory.go index d7b6c30797..14dcd3b3ac 100644 --- a/pkg/detector/library/composer/advisory.go +++ b/pkg/detector/library/composer/advisory.go @@ -4,30 +4,30 @@ import ( "fmt" "strings" - "github.com/aquasecurity/trivy/pkg/types" - "golang.org/x/xerrors" - "github.com/Masterminds/semver/v3" - + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" composerSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/composer" - "github.com/aquasecurity/trivy/pkg/scanner/utils" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" + "github.com/aquasecurity/trivy/pkg/types" ) // Advisory encapsulates composer.VulnSrc type Advisory struct { - vs composerSrc.VulnSrc + vs composerSrc.VulnSrc + comparer comparer.Comparer // TODO: implement a comparer for Composer } // NewAdvisory is the factory method of Advisory func NewAdvisory() *Advisory { return &Advisory{ - vs: composerSrc.NewVulnSrc(), + vs: composerSrc.NewVulnSrc(), + comparer: comparer.GenericComparer{}, } } // DetectVulnerabilities returns the vulnerabilities in a package -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { ref := fmt.Sprintf("composer://%s", pkgName) advisories, err := s.vs.Get(ref) if err != nil { @@ -47,14 +47,15 @@ func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) affectedVersions = append(affectedVersions, strings.Join(branch.Versions, ", ")) } - if !utils.MatchVersions(pkgVer, affectedVersions) { + adv := dbTypes.Advisory{VulnerableVersions: affectedVersions} + if !s.comparer.IsVulnerable(pkgVer, adv) { continue } vuln := types.DetectedVulnerability{ VulnerabilityID: advisory.VulnerabilityID, PkgName: pkgName, - InstalledVersion: pkgVer.String(), + InstalledVersion: pkgVer, FixedVersion: strings.Join(patchedVersions, ", "), } vulns = append(vulns, vuln) diff --git a/pkg/detector/library/composer/advisory_test.go b/pkg/detector/library/composer/advisory_test.go new file mode 100644 index 0000000000..64906d475e --- /dev/null +++ b/pkg/detector/library/composer/advisory_test.go @@ -0,0 +1,84 @@ +package composer_test + +import ( + "os" + "testing" + + "github.com/aquasecurity/trivy/pkg/detector/library/composer" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" +) + +func TestAdvisory_DetectVulnerabilities(t *testing.T) { + type args struct { + pkgName string + pkgVer string + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "detected", + args: args{ + pkgName: "aws/aws-sdk-php", + pkgVer: "3.2.0", + }, + fixtures: []string{"testdata/fixtures/composer.yaml"}, + want: []types.DetectedVulnerability{ + { + PkgName: "aws/aws-sdk-php", + InstalledVersion: "3.2.0", + VulnerabilityID: "CVE-2015-5723", + FixedVersion: "3.2.1", + }, + }, + }, + { + name: "not detected", + args: args{ + pkgName: "guzzlehttp/guzzle", + pkgVer: "5.3.1", + }, + fixtures: []string{"testdata/fixtures/composer.yaml"}, + want: nil, + }, + { + name: "malformed JSON", + args: args{ + pkgName: "aws/aws-sdk-php", + pkgVer: "3.2.0", + }, + fixtures: []string{"testdata/fixtures/invalid-type.yaml"}, + wantErr: "failed to unmarshal advisory JSON", + }, + } + + log.InitLogger(false, true) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := utils.InitTestDB(t, tt.fixtures) + defer os.RemoveAll(dir) + + a := composer.NewAdvisory() + got, err := a.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer) + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/composer/testdata/fixtures/composer.yaml b/pkg/detector/library/composer/testdata/fixtures/composer.yaml new file mode 100644 index 0000000000..340462a804 --- /dev/null +++ b/pkg/detector/library/composer/testdata/fixtures/composer.yaml @@ -0,0 +1,28 @@ +- bucket: php-security-advisories + pairs: + - bucket: "composer://aws/aws-sdk-php" + pairs: + - key: CVE-2015-5723 + value: + Branches: + 3.x: + Versions: + - ">=3.0.0" + - "<3.2.1" + - bucket: "composer://guzzlehttp/guzzle" + pairs: + - key: CVE-2016-5385 + value: + Branches: + 4.x: + Versions: + - ">=4.0.0rc2" + - "<4.2.4" + 5.3: + Versions: + - ">=5" + - "<5.3.1" + master: + Versions: + - ">=6" + - "<6.2.1" \ No newline at end of file diff --git a/pkg/detector/library/composer/testdata/fixtures/invalid-type.yaml b/pkg/detector/library/composer/testdata/fixtures/invalid-type.yaml new file mode 100644 index 0000000000..66b5509138 --- /dev/null +++ b/pkg/detector/library/composer/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,7 @@ +- bucket: php-security-advisories + pairs: + - bucket: "composer://aws/aws-sdk-php" + pairs: + - key: CVE-2015-5723 + value: + Branches: invalid diff --git a/pkg/detector/library/detect.go b/pkg/detector/library/detect.go index 5a5541f54e..fa18a2683a 100644 --- a/pkg/detector/library/detect.go +++ b/pkg/detector/library/detect.go @@ -4,13 +4,11 @@ import ( "path/filepath" "time" - "github.com/Masterminds/semver/v3" "github.com/google/wire" "golang.org/x/xerrors" ftypes "github.com/aquasecurity/fanal/types" "github.com/aquasecurity/trivy/pkg/log" - "github.com/aquasecurity/trivy/pkg/scanner/utils" "github.com/aquasecurity/trivy/pkg/types" ) @@ -57,14 +55,7 @@ func detect(driver Driver, libs []ftypes.LibraryInfo) ([]types.DetectedVulnerabi log.Logger.Infof("Detecting %s vulnerabilities...", driver.Type()) var vulnerabilities []types.DetectedVulnerability for _, lib := range libs { - v, err := semver.NewVersion(utils.FormatPatchVersion(lib.Library.Version)) - if err != nil { - log.Logger.Debugf("invalid version, library: %s, version: %s, error: %s\n", - lib.Library.Name, lib.Library.Version, err) - continue - } - - vulns, err := driver.Detect(lib.Library.Name, v) + vulns, err := driver.Detect(lib.Library.Name, lib.Library.Version) if err != nil { return nil, xerrors.Errorf("failed to detect %s vulnerabilities: %w", driver.Type(), err) } diff --git a/pkg/detector/library/driver.go b/pkg/detector/library/driver.go index 407ed7056b..410145887b 100644 --- a/pkg/detector/library/driver.go +++ b/pkg/detector/library/driver.go @@ -3,13 +3,13 @@ package library import ( "fmt" - "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" ecosystem "github.com/aquasecurity/trivy-db/pkg/vulnsrc/ghsa" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" "github.com/aquasecurity/trivy/pkg/detector/library/bundler" "github.com/aquasecurity/trivy/pkg/detector/library/cargo" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" "github.com/aquasecurity/trivy/pkg/detector/library/composer" "github.com/aquasecurity/trivy/pkg/detector/library/ghsa" "github.com/aquasecurity/trivy/pkg/detector/library/node" @@ -23,7 +23,7 @@ type Factory interface { } type advisory interface { - DetectVulnerabilities(string, *semver.Version) ([]types.DetectedVulnerability, error) + DetectVulnerabilities(string, string) ([]types.DetectedVulnerability, error) } // DriverFactory implements Factory @@ -31,19 +31,18 @@ type DriverFactory struct{} // NewDriver factory method for driver func (d DriverFactory) NewDriver(filename string) (Driver, error) { - // TODO: use DI var driver Driver switch filename { case "Gemfile.lock": - driver = newRubyDriver() + driver = newRubyGemsDriver() case "Cargo.lock": - driver = newRustDriver() + driver = newCargoDriver() case "composer.lock": - driver = newPHPDriver() + driver = newComposerDriver() case "package-lock.json", "yarn.lock": - driver = newNodejsDriver() + driver = newNpmDriver() case "Pipfile.lock", "poetry.lock": - driver = newPythonDriver() + driver = newPipDriver() default: return Driver{}, xerrors.New(fmt.Sprintf("unsupport filename %s", filename)) } @@ -52,20 +51,20 @@ func (d DriverFactory) NewDriver(filename string) (Driver, error) { // Driver implements the advisory type Driver struct { - lang string + ecosystem string advisories []advisory } // NewDriver is the factory method from drier -func NewDriver(lang string, advisories ...advisory) Driver { - return Driver{lang: lang, advisories: advisories} +func NewDriver(advisories ...advisory) Driver { + return Driver{advisories: advisories} } // Detect scans and returns vulnerabilities -func (d *Driver) Detect(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { +func (d *Driver) Detect(pkgName string, pkgVer string) ([]types.DetectedVulnerability, error) { var detectedVulnerabilities []types.DetectedVulnerability uniqVulnIDMap := make(map[string]struct{}) - for _, d := range append(d.advisories, NewAdvisory(d.lang)) { + for _, d := range d.advisories { vulns, err := d.DetectVulnerabilities(pkgName, pkgVer) if err != nil { return nil, xerrors.Errorf("failed to detect vulnerabilities: %w", err) @@ -82,27 +81,36 @@ func (d *Driver) Detect(pkgName string, pkgVer *semver.Version) ([]types.Detecte return detectedVulnerabilities, nil } -// Type returns the driver lang +// Type returns the driver ecosystem func (d *Driver) Type() string { - return d.lang + return d.ecosystem } -func newRubyDriver() Driver { - return NewDriver(vulnerability.Ruby, ghsa.NewAdvisory(ecosystem.Rubygems), bundler.NewAdvisory()) +func newRubyGemsDriver() Driver { + c := bundler.RubyGemsComparer{} + return NewDriver(ghsa.NewAdvisory(ecosystem.Rubygems, c), bundler.NewAdvisory(), + NewAdvisory(vulnerability.RubyGems, c)) } -func newPHPDriver() Driver { - return NewDriver(vulnerability.PHP, ghsa.NewAdvisory(ecosystem.Composer), composer.NewAdvisory()) +func newComposerDriver() Driver { + c := comparer.GenericComparer{} + return NewDriver( + ghsa.NewAdvisory(ecosystem.Composer, c), composer.NewAdvisory(), + NewAdvisory(vulnerability.Composer, c)) } -func newRustDriver() Driver { - return NewDriver(vulnerability.Rust, cargo.NewAdvisory()) +func newCargoDriver() Driver { + return NewDriver(cargo.NewAdvisory(), NewAdvisory(vulnerability.Cargo, comparer.GenericComparer{})) } -func newNodejsDriver() Driver { - return NewDriver(vulnerability.Nodejs, ghsa.NewAdvisory(ecosystem.Npm), node.NewAdvisory()) +func newNpmDriver() Driver { + c := node.NpmComparer{} + return NewDriver(ghsa.NewAdvisory(ecosystem.Npm, c), node.NewAdvisory(), + NewAdvisory(vulnerability.Npm, c)) } -func newPythonDriver() Driver { - return NewDriver(vulnerability.Python, ghsa.NewAdvisory(ecosystem.Pip), python.NewAdvisory()) +func newPipDriver() Driver { + c := comparer.GenericComparer{} + return NewDriver(ghsa.NewAdvisory(ecosystem.Pip, c), python.NewAdvisory(), + NewAdvisory(vulnerability.Pip, c)) } diff --git a/pkg/detector/library/driver_test.go b/pkg/detector/library/driver_test.go index 6a01108aa8..bd5162476a 100644 --- a/pkg/detector/library/driver_test.go +++ b/pkg/detector/library/driver_test.go @@ -4,7 +4,6 @@ import ( "os" "testing" - "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,7 +19,7 @@ func TestDriver_Detect(t *testing.T) { } type args struct { pkgName string - pkgVer *semver.Version + pkgVer string } tests := []struct { name string @@ -36,7 +35,7 @@ func TestDriver_Detect(t *testing.T) { fields: fields{fileName: "composer.lock"}, args: args{ pkgName: "symfony/symfony", - pkgVer: semver.MustParse("4.2.6"), + pkgVer: "4.2.6", }, want: []types.DetectedVulnerability{ { @@ -53,7 +52,7 @@ func TestDriver_Detect(t *testing.T) { fields: fields{fileName: "composer.lock"}, args: args{ pkgName: "symfony/symfony", - pkgVer: semver.MustParse("4.2.6"), + pkgVer: "4.2.6", }, want: []types.DetectedVulnerability{ { @@ -70,7 +69,7 @@ func TestDriver_Detect(t *testing.T) { fields: fields{fileName: "composer.lock"}, args: args{ pkgName: "symfony/symfony", - pkgVer: semver.MustParse("4.4.6"), + pkgVer: "4.4.6", }, want: []types.DetectedVulnerability{ { @@ -87,7 +86,7 @@ func TestDriver_Detect(t *testing.T) { fields: fields{fileName: "Gemfile.lock"}, args: args{ pkgName: "activesupport", - pkgVer: semver.MustParse("4.1.1"), + pkgVer: "4.1.1", }, want: []types.DetectedVulnerability{ { @@ -104,7 +103,7 @@ func TestDriver_Detect(t *testing.T) { fields: fields{fileName: "composer.lock"}, args: args{ pkgName: "symfony/symfony", - pkgVer: semver.MustParse("4.4.7"), + pkgVer: "4.4.7", }, }, } diff --git a/pkg/detector/library/ghsa/advisory.go b/pkg/detector/library/ghsa/advisory.go index 19c08defea..dada69f14b 100644 --- a/pkg/detector/library/ghsa/advisory.go +++ b/pkg/detector/library/ghsa/advisory.go @@ -3,11 +3,11 @@ package ghsa import ( "strings" - "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/ghsa" - "github.com/aquasecurity/trivy/pkg/scanner/utils" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" "github.com/aquasecurity/trivy/pkg/types" ) @@ -18,18 +18,20 @@ type VulnSrc interface { // Advisory implements VulnSrc type Advisory struct { - vs VulnSrc + vs VulnSrc + comparer comparer.Comparer } // NewAdvisory is the factory method to return advisory -func NewAdvisory(ecosystem ghsa.Ecosystem) *Advisory { +func NewAdvisory(ecosystem ghsa.Ecosystem, comparer comparer.Comparer) *Advisory { return &Advisory{ - vs: ghsa.NewVulnSrc(ecosystem), + vs: ghsa.NewVulnSrc(ecosystem), + comparer: comparer, } } // DetectVulnerabilities scans package for vulnerabilities -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { +func (s *Advisory) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { advisories, err := s.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get ghsa advisories: %w", err) @@ -37,14 +39,15 @@ func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) var vulns []types.DetectedVulnerability for _, advisory := range advisories { - if !utils.MatchVersions(pkgVer, advisory.VulnerableVersions) { + adv := dbTypes.Advisory{VulnerableVersions: advisory.VulnerableVersions} + if !s.comparer.IsVulnerable(pkgVer, adv) { continue } vuln := types.DetectedVulnerability{ VulnerabilityID: advisory.VulnerabilityID, PkgName: pkgName, - InstalledVersion: pkgVer.String(), + InstalledVersion: pkgVer, FixedVersion: strings.Join(advisory.PatchedVersions, ", "), } vulns = append(vulns, vuln) diff --git a/pkg/detector/library/ghsa/advisory_test.go b/pkg/detector/library/ghsa/advisory_test.go new file mode 100644 index 0000000000..8a315e1511 --- /dev/null +++ b/pkg/detector/library/ghsa/advisory_test.go @@ -0,0 +1,102 @@ +package ghsa_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ghsaSrc "github.com/aquasecurity/trivy-db/pkg/vulnsrc/ghsa" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" + "github.com/aquasecurity/trivy/pkg/detector/library/ghsa" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" +) + +func TestAdvisory_DetectVulnerabilities(t *testing.T) { + type fields struct { + ecosystem ghsaSrc.Ecosystem + comparer comparer.Comparer + } + type args struct { + pkgName string + pkgVer string + } + tests := []struct { + name string + args args + fields fields + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "detected", + fields: fields{ + ecosystem: ghsaSrc.Composer, + comparer: comparer.GenericComparer{}, + }, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "5.1.5-alpha", + }, + fixtures: []string{"testdata/fixtures/ghsa.yaml"}, + want: []types.DetectedVulnerability{ + { + PkgName: "symfony/symfony", + InstalledVersion: "5.1.5-alpha", + VulnerabilityID: "CVE-2020-15094", + FixedVersion: "5.1.5, 4.4.13", + }, + }, + }, + { + name: "not detected", + fields: fields{ + ecosystem: ghsaSrc.Composer, + comparer: comparer.GenericComparer{}, + }, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "5.1.5", + }, + fixtures: []string{"testdata/fixtures/ghsa.yaml"}, + want: nil, + }, + { + name: "malformed JSON", + fields: fields{ + ecosystem: ghsaSrc.Composer, + comparer: comparer.GenericComparer{}, + }, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "5.1.5", + }, + fixtures: []string{"testdata/fixtures/invalid-type.yaml"}, + wantErr: "failed to unmarshal advisory JSON", + }, + } + + log.InitLogger(false, true) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := utils.InitTestDB(t, tt.fixtures) + defer os.RemoveAll(dir) + + a := ghsa.NewAdvisory(tt.fields.ecosystem, tt.fields.comparer) + got, err := a.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer) + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/ghsa/testdata/fixtures/ghsa.yaml b/pkg/detector/library/ghsa/testdata/fixtures/ghsa.yaml new file mode 100644 index 0000000000..d0e6470287 --- /dev/null +++ b/pkg/detector/library/ghsa/testdata/fixtures/ghsa.yaml @@ -0,0 +1,12 @@ +- bucket: GitHub Security Advisory Composer + pairs: + - bucket: "symfony/symfony" + pairs: + - key: CVE-2020-15094 + value: + PatchedVersions: + - 5.1.5 + - 4.4.13 + VulnerableVersions: + - ">= 5.0.0, < 5.1.5" + - ">= 4.4.0, < 4.4.13" \ No newline at end of file diff --git a/pkg/detector/library/ghsa/testdata/fixtures/invalid-type.yaml b/pkg/detector/library/ghsa/testdata/fixtures/invalid-type.yaml new file mode 100644 index 0000000000..0883f30e9a --- /dev/null +++ b/pkg/detector/library/ghsa/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,7 @@ +- bucket: GitHub Security Advisory Composer + pairs: + - bucket: "symfony/symfony" + pairs: + - key: CVE-2020-15094 + value: + PatchedVersions: malformed diff --git a/pkg/detector/library/node/advisory.go b/pkg/detector/library/node/advisory.go index 9f991568bd..f0689a1359 100644 --- a/pkg/detector/library/node/advisory.go +++ b/pkg/detector/library/node/advisory.go @@ -5,64 +5,69 @@ import ( "golang.org/x/xerrors" - "github.com/Masterminds/semver/v3" - + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/node" - "github.com/aquasecurity/trivy/pkg/scanner/utils" "github.com/aquasecurity/trivy/pkg/types" ) // Advisory encapsulate Node vulnerability source type Advisory struct { - vs node.VulnSrc + comparer NpmComparer + vs node.VulnSrc } // NewAdvisory is the factory method for Node Advisory func NewAdvisory() *Advisory { return &Advisory{ - vs: node.NewVulnSrc(), + vs: node.NewVulnSrc(), + comparer: NpmComparer{}, } } // DetectVulnerabilities scans and return vulnerability using Node package scanner -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { - replacer := strings.NewReplacer(".alpha", "-alpha", ".beta", "-beta", ".rc", "-rc", " <", ", <", " >", ", >") - advisories, err := s.vs.Get(pkgName) +func (a *Advisory) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { + advisories, err := a.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get node advisories: %w", err) } var vulns []types.DetectedVulnerability for _, advisory := range advisories { - // e.g. <= 2.15.0 || >= 3.0.0 <= 3.8.2 - // => {"<=2.15.0", ">= 3.0.0, <= 3.8.2"} - var vulnerableVersions []string - for _, version := range strings.Split(advisory.VulnerableVersions, " || ") { - version = strings.TrimSpace(version) - vulnerableVersions = append(vulnerableVersions, replacer.Replace(version)) - } - - if !utils.MatchVersions(pkgVer, vulnerableVersions) { - continue - } - - var patchedVersions []string - for _, version := range strings.Split(advisory.PatchedVersions, " || ") { - version = strings.TrimSpace(version) - patchedVersions = append(patchedVersions, replacer.Replace(version)) - } - - if utils.MatchVersions(pkgVer, patchedVersions) { + adv := convertToGenericAdvisory(advisory) + if !a.comparer.IsVulnerable(pkgVer, adv) { continue } vuln := types.DetectedVulnerability{ VulnerabilityID: advisory.VulnerabilityID, PkgName: pkgName, - InstalledVersion: pkgVer.String(), - FixedVersion: strings.Join(patchedVersions, ", "), + InstalledVersion: pkgVer, + FixedVersion: createFixedVersions(advisory.PatchedVersions), } vulns = append(vulns, vuln) } return vulns, nil } + +func convertToGenericAdvisory(advisory node.Advisory) dbTypes.Advisory { + var vulnerable, patched []string + if advisory.VulnerableVersions != "" { + vulnerable = strings.Split(advisory.VulnerableVersions, "||") + } + if advisory.PatchedVersions != "" { + patched = strings.Split(advisory.PatchedVersions, "||") + } + + return dbTypes.Advisory{ + VulnerableVersions: vulnerable, + PatchedVersions: patched, + } +} + +func createFixedVersions(patchedVersions string) string { + var fixedVersions []string + for _, s := range strings.Split(patchedVersions, "||") { + fixedVersions = append(fixedVersions, strings.TrimSpace(s)) + } + return strings.Join(fixedVersions, ", ") +} diff --git a/pkg/detector/library/node/advisory_test.go b/pkg/detector/library/node/advisory_test.go new file mode 100644 index 0000000000..bbc73c5cb5 --- /dev/null +++ b/pkg/detector/library/node/advisory_test.go @@ -0,0 +1,91 @@ +package node_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/detector/library/node" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" +) + +func TestAdvisory_DetectVulnerabilities(t *testing.T) { + type args struct { + pkgName string + pkgVer string + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "detected", + args: args{ + pkgName: "electron", + pkgVer: "2.0.17", + }, + fixtures: []string{"testdata/fixtures/npm.yaml"}, + want: []types.DetectedVulnerability{ + { + PkgName: "electron", + InstalledVersion: "2.0.17", + VulnerabilityID: "CVE-2019-5786", + FixedVersion: "^2.0.18, ^3.0.16, ^3.1.6, ^4.0.8, ^5.0.0-beta.5", + }, + }, + }, + { + name: "not detected", + args: args{ + pkgName: "electron", + pkgVer: "2.0.18", + }, + fixtures: []string{"testdata/fixtures/npm.yaml"}, + want: nil, + }, + { + name: "empty value", + args: args{ + pkgName: "electron", + pkgVer: "2.0.18", + }, + fixtures: []string{"testdata/fixtures/no-value.yaml"}, + want: nil, + }, + {name: "malformed JSON", + args: args{ + pkgName: "electron", + pkgVer: "2.0.18", + }, + fixtures: []string{"testdata/fixtures/invalid-type.yaml"}, + wantErr: "failed to unmarshal advisory JSON", + }, + } + + log.InitLogger(false, true) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := utils.InitTestDB(t, tt.fixtures) + defer os.RemoveAll(dir) + + a := node.NewAdvisory() + got, err := a.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer) + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/node/compare.go b/pkg/detector/library/node/compare.go new file mode 100644 index 0000000000..3533de0465 --- /dev/null +++ b/pkg/detector/library/node/compare.go @@ -0,0 +1,32 @@ +package node + +import ( + "golang.org/x/xerrors" + + npm "github.com/aquasecurity/go-npm-version/pkg" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" +) + +// NpmComparer represents a comparer for npm +type NpmComparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (n NpmComparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return comparer.IsVulnerable(ver, advisory, n.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (n NpmComparer) matchVersion(currentVersion, constraint string) (bool, error) { + v, err := npm.NewVersion(currentVersion) + if err != nil { + return false, xerrors.Errorf("npm version error (%s): %s", currentVersion, err) + } + + c, err := npm.NewConstraints(constraint) + if err != nil { + return false, xerrors.Errorf("npm constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} diff --git a/pkg/detector/library/node/compare_test.go b/pkg/detector/library/node/compare_test.go new file mode 100644 index 0000000000..221bf5fb0d --- /dev/null +++ b/pkg/detector/library/node/compare_test.go @@ -0,0 +1,140 @@ +package node_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/node" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestNpmComparer_MatchVersion(t *testing.T) { + type args struct { + currentVersion string + advisory dbTypes.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + currentVersion: "1.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=1.0"}, + PatchedVersions: []string{">=1.1"}, + }, + }, + want: true, + }, + { + name: "no patch", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=99.999.99999"}, + PatchedVersions: []string{"<0.0.0"}, + }, + }, + want: true, + }, + { + name: "no patch with wildcard", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"*"}, + PatchedVersions: nil, + }, + }, + want: true, + }, + { + name: "pre-release", + args: args{ + currentVersion: "1.2.3-alpha", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=1.2.2"}, + PatchedVersions: []string{">=1.2.2"}, + }, + }, + want: false, + }, + { + name: "multiple constraints", + args: args{ + currentVersion: "2.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{">=1.7.0 <1.7.16", ">=1.8.0 <1.8.8", ">=2.0.0 <2.0.8", ">=3.0.0-beta.1 <3.0.0-beta.7"}, + PatchedVersions: []string{">=3.0.0-beta.7", ">=2.0.8 <3.0.0-beta.1", ">=1.8.8 <2.0.0", ">=1.7.16 <1.8.0"}, + }, + }, + want: true, + }, + { + name: "x", + args: args{ + currentVersion: "2.0.1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"2.0.x", "2.1.x"}, + PatchedVersions: []string{">=2.2.x"}, + }, + }, + want: true, + }, + { + name: "exact versions", + args: args{ + currentVersion: "2.1.0-M1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"2.1.0-M1", "2.1.0-M2"}, + PatchedVersions: []string{">=2.1.0"}, + }, + }, + want: true, + }, + { + name: "caret", + args: args{ + currentVersion: "2.0.18", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<2.0.18", "<3.0.16"}, + PatchedVersions: []string{"^2.0.18", "^3.0.16"}, + }, + }, + want: false, + }, + { + name: "invalid version", + args: args{ + currentVersion: "1.2..4", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<1.0.0"}, + }, + }, + want: false, + }, + { + name: "invalid constraint", + args: args{ + currentVersion: "1.2.4", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"!1.0.0"}, + }, + }, + want: false, + }, + } + log.InitLogger(false, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := node.NpmComparer{} + got := c.IsVulnerable(tt.args.currentVersion, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/node/testdata/fixtures/invalid-type.yaml b/pkg/detector/library/node/testdata/fixtures/invalid-type.yaml new file mode 100644 index 0000000000..5a77fbb552 --- /dev/null +++ b/pkg/detector/library/node/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,9 @@ +- bucket: nodejs-security-wg + pairs: + - bucket: electron + pairs: + - key: CVE-2019-5786 + value: + PatchedVersions: + - 1 + - 2 diff --git a/pkg/detector/library/node/testdata/fixtures/no-value.yaml b/pkg/detector/library/node/testdata/fixtures/no-value.yaml new file mode 100644 index 0000000000..0e26815bb9 --- /dev/null +++ b/pkg/detector/library/node/testdata/fixtures/no-value.yaml @@ -0,0 +1,6 @@ +- bucket: nodejs-security-wg + pairs: + - bucket: electron + pairs: + - key: CVE-2019-5786 + value: diff --git a/pkg/detector/library/node/testdata/fixtures/npm.yaml b/pkg/detector/library/node/testdata/fixtures/npm.yaml new file mode 100644 index 0000000000..71969fe78e --- /dev/null +++ b/pkg/detector/library/node/testdata/fixtures/npm.yaml @@ -0,0 +1,8 @@ +- bucket: nodejs-security-wg + pairs: + - bucket: electron + pairs: + - key: CVE-2019-5786 + value: + PatchedVersions: "^2.0.18 || ^3.0.16 || ^3.1.6 || ^4.0.8 || ^5.0.0-beta.5" + VulnerableVersions: "<2.0.18 || <3.0.16 || <3.1.6 || <4.0.8 || <5.0.0-beta.5" diff --git a/pkg/detector/library/python/advisory.go b/pkg/detector/library/python/advisory.go index 44814ac888..2413f60e43 100644 --- a/pkg/detector/library/python/advisory.go +++ b/pkg/detector/library/python/advisory.go @@ -3,28 +3,30 @@ package python import ( "strings" - "github.com/Masterminds/semver/v3" "golang.org/x/xerrors" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy-db/pkg/vulnsrc/python" - "github.com/aquasecurity/trivy/pkg/scanner/utils" + "github.com/aquasecurity/trivy/pkg/detector/library/comparer" "github.com/aquasecurity/trivy/pkg/types" ) // Advisory encapsulates the python vulnerability scanner type Advisory struct { - vs python.VulnSrc + vs python.VulnSrc + comparer comparer.Comparer } -// NewAdvisory is the factory method to reutrn Python Advisory +// NewAdvisory is the factory method to return Python Advisory func NewAdvisory() *Advisory { return &Advisory{ - vs: python.NewVulnSrc(), + vs: python.NewVulnSrc(), + comparer: comparer.GenericComparer{}, } } -// DetectVulnerabilities scans and returns pythin vulnerabilities -func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) ([]types.DetectedVulnerability, error) { +// DetectVulnerabilities scans and returns python vulnerabilities +func (s *Advisory) DetectVulnerabilities(pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { advisories, err := s.vs.Get(pkgName) if err != nil { return nil, xerrors.Errorf("failed to get python advisories: %w", err) @@ -32,14 +34,15 @@ func (s *Advisory) DetectVulnerabilities(pkgName string, pkgVer *semver.Version) var vulns []types.DetectedVulnerability for _, advisory := range advisories { - if !utils.MatchVersions(pkgVer, advisory.Specs) { + adv := dbTypes.Advisory{VulnerableVersions: advisory.Specs} + if !s.comparer.IsVulnerable(pkgVer, adv) { continue } vuln := types.DetectedVulnerability{ VulnerabilityID: advisory.VulnerabilityID, PkgName: pkgName, - InstalledVersion: pkgVer.String(), + InstalledVersion: pkgVer, FixedVersion: createFixedVersions(advisory.Specs), } vulns = append(vulns, vuln) diff --git a/pkg/detector/library/python/advisory_test.go b/pkg/detector/library/python/advisory_test.go new file mode 100644 index 0000000000..8533ed78da --- /dev/null +++ b/pkg/detector/library/python/advisory_test.go @@ -0,0 +1,84 @@ +package python_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/detector/library/python" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils" +) + +func TestAdvisory_DetectVulnerabilities(t *testing.T) { + type args struct { + pkgName string + pkgVer string + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "detected", + args: args{ + pkgName: "django", + pkgVer: "2.2.11-alpha", + }, + fixtures: []string{"testdata/fixtures/pip.yaml"}, + want: []types.DetectedVulnerability{ + { + PkgName: "django", + InstalledVersion: "2.2.11-alpha", + VulnerabilityID: "CVE-2020-9402", + FixedVersion: "1.11.29, 2.2.11, 3.0.4", + }, + }, + }, + { + // https://github.com/aquasecurity/trivy/issues/713 + name: "not detected", + args: args{ + pkgName: "django", + pkgVer: "3.0.10", + }, + fixtures: []string{"testdata/fixtures/pip.yaml"}, + want: nil, + }, + { + name: "malformed JSON", + args: args{ + pkgName: "django", + pkgVer: "2.0.18", + }, + fixtures: []string{"testdata/fixtures/invalid-type.yaml"}, + wantErr: "failed to unmarshal advisory JSON", + }, + } + + log.InitLogger(false, true) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := utils.InitTestDB(t, tt.fixtures) + defer os.RemoveAll(dir) + + a := python.NewAdvisory() + got, err := a.DetectVulnerabilities(tt.args.pkgName, tt.args.pkgVer) + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/python/testdata/fixtures/invalid-type.yaml b/pkg/detector/library/python/testdata/fixtures/invalid-type.yaml new file mode 100644 index 0000000000..e76a3cde25 --- /dev/null +++ b/pkg/detector/library/python/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,7 @@ +- bucket: python-safety-db + pairs: + - bucket: django + pairs: + - key: CVE-2020-9402 + value: + Specs: foo diff --git a/pkg/detector/library/python/testdata/fixtures/pip.yaml b/pkg/detector/library/python/testdata/fixtures/pip.yaml new file mode 100644 index 0000000000..efe046894d --- /dev/null +++ b/pkg/detector/library/python/testdata/fixtures/pip.yaml @@ -0,0 +1,14 @@ +- bucket: python-safety-db + pairs: + - bucket: django + pairs: + - key: CVE-2019-19844 + value: + Specs: + - "==3.0" + - key: CVE-2020-9402 + value: + Specs: + - ">=1.11.0,<1.11.29" + - ">=2.2.0,<2.2.11" + - ">=3.0.0,<3.0.4" \ No newline at end of file diff --git a/pkg/detector/library/testdata/fixtures/php.yaml b/pkg/detector/library/testdata/fixtures/php.yaml index 9c93e64ea2..427fe74206 100644 --- a/pkg/detector/library/testdata/fixtures/php.yaml +++ b/pkg/detector/library/testdata/fixtures/php.yaml @@ -1,4 +1,4 @@ -- bucket: "php::GitHub Security Advisory Composer" +- bucket: "composer::GitHub Security Advisory Composer" pairs: - bucket: symfony/symfony pairs: @@ -8,7 +8,7 @@ - 4.2.7 VulnerableVersions: - ">= 4.2.0, < 4.2.7" -- bucket: "php::php-security-advisories" +- bucket: "composer::php-security-advisories" pairs: - bucket: symfony/symfony pairs: diff --git a/pkg/detector/library/testdata/fixtures/ruby.yaml b/pkg/detector/library/testdata/fixtures/ruby.yaml index 1e50b26154..c3305ed7ad 100644 --- a/pkg/detector/library/testdata/fixtures/ruby.yaml +++ b/pkg/detector/library/testdata/fixtures/ruby.yaml @@ -1,4 +1,4 @@ -- bucket: "ruby::ruby-advisory-db" +- bucket: "rubygems::ruby-advisory-db" pairs: - bucket: activesupport pairs: diff --git a/pkg/scanner/utils/utils.go b/pkg/scanner/utils/utils.go index 2d40872417..182f919029 100644 --- a/pkg/scanner/utils/utils.go +++ b/pkg/scanner/utils/utils.go @@ -2,60 +2,10 @@ package utils import ( "fmt" - "regexp" - "strconv" - "strings" - - "github.com/Masterminds/semver/v3" "github.com/aquasecurity/fanal/types" - - "github.com/aquasecurity/trivy/pkg/log" ) -var ( - replacer = strings.NewReplacer(".alpha", "-alpha", ".beta", "-beta", ".rc", "-rc", "==", "=") - preReleaseSplitter = regexp.MustCompile(`(?P^[0-9]+)(?P[a-z]*.*)`) -) - -// MatchVersions runs comparison on currentVersion based on rangeVersions and return true/false -func MatchVersions(currentVersion *semver.Version, rangeVersions []string) bool { - for _, v := range rangeVersions { - v = replacer.Replace(v) - constraintParts := strings.Split(v, ",") - for j := range constraintParts { - constraintParts[j] = FormatPatchVersion(constraintParts[j]) - } - v = strings.Join(constraintParts, ",") - if v == "" { - continue - } - c, err := semver.NewConstraint(v) - if err != nil { - log.Logger.Debug("NewConstraint", "error", err) - continue - } - // Validate a version against a constraint. - valid, msgs := c.Validate(currentVersion) - if valid { - return true - } - for _, m := range msgs { - // re-validate after removing the patch version - if strings.HasSuffix(m.Error(), "is a prerelease version and the constraint is only looking for release versions") { - v2, err := semver.NewVersion(fmt.Sprintf("%v.%v.%v", currentVersion.Major(), currentVersion.Minor(), currentVersion.Patch())) - if err == nil { - valid, _ = c.Validate(v2) - if valid { - return true - } - } - } - } - } - return false -} - // FormatVersion formats the package version based on epoch, version & release func FormatVersion(pkg types.Package) string { return formatVersion(pkg.Epoch, pkg.Version, pkg.Release) @@ -66,34 +16,6 @@ func FormatSrcVersion(pkg types.Package) string { return formatVersion(pkg.SrcEpoch, pkg.SrcVersion, pkg.SrcRelease) } -// FormatPatchVersion returns the semver compatible version string given non-semver version -func FormatPatchVersion(version string) string { - part := strings.Split(version, ".") - if len(part) > 3 { - if _, err := strconv.Atoi(part[2]); err == nil { - version = strings.Join(part[:3], ".") + "-" + strings.Join(part[3:], ".") - } - } else { - for i := range part { - res := preReleaseSplitter.FindStringSubmatch(part[i]) - if res == nil { - continue - } - number := res[1] - preRelease := res[2] - if preRelease != "" { - if !strings.HasPrefix(preRelease, "-") { - preRelease = "-" + preRelease - } - part[i] = number + preRelease - break - } - } - version = strings.Join(part, ".") - } - return version -} - func formatVersion(epoch int, version, release string) string { v := version if release != "" { diff --git a/pkg/scanner/utils/utils_test.go b/pkg/scanner/utils/utils_test.go index 406350f8e6..3d278cbe3c 100644 --- a/pkg/scanner/utils/utils_test.go +++ b/pkg/scanner/utils/utils_test.go @@ -3,161 +3,71 @@ package utils import ( "testing" - "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + + "github.com/aquasecurity/fanal/types" ) -func TestMatchVersions(t *testing.T) { - testCases := []struct { - name string - currentVersion string - rangeVersion []string - expectedCheck bool +func TestFormatSrcVersion(t *testing.T) { + tests := []struct { + name string + pkg types.Package + want string }{ { - name: "pass: expect true when os/machine is in version string", - currentVersion: "1.9.25-x86-mingw32", - rangeVersion: []string{`>= 1.9.24`}, - expectedCheck: true, + name: "happy path", + pkg: types.Package{ + SrcVersion: "1.2.3", + SrcRelease: "1", + }, + want: "1.2.3-1", }, { - name: "pass: expect true when language is in version string", - currentVersion: "1.8.6-java", - rangeVersion: []string{`~> 1.5.5`, `~> 1.6.8`, `>= 1.7.7`}, - expectedCheck: true, - }, - { - name: "expect false", - currentVersion: "1.9.23-x86-mingw32", - rangeVersion: []string{`>= 1.9.24`}, - expectedCheck: false, - }, - { - // passes if (>= 1.2.3, < 2.0.0) - name: "expect false", - currentVersion: "1.2.4", - rangeVersion: []string{`^1.2.3`}, - expectedCheck: true, - }, - { - // passes if (>= 1.2.3, < 2.0.0) - name: "expect false", - currentVersion: "2.0.0", - rangeVersion: []string{`^1.2.3`}, - expectedCheck: false, - }, - { - // passes if (>= 2.0.18, < 3.0.0) || (>= 3.1.16, < 4.0.0) || (>= 4.0.8, < 5.0.0) || ( >=5.0.0,<6.0.0) - name: "expect false", - currentVersion: "3.1.16", - rangeVersion: []string{`^2.0.18 || ^3.1.6 || ^4.0.8 || ^5.0.0-beta.5`}, - expectedCheck: true, - }, - { - // passes if (>= 2.0.18, < 3.0.0) || (>= 3.1.16, < 4.0.0) || (>= 4.0.8, < 5.0.0) || ( >=5.0.0,<6.0.0) - name: "expect false", - currentVersion: "6.0.0", - rangeVersion: []string{`^2.0.18 || ^3.1.6 || ^4.0.8 || ^5.0.0-beta.5`}, - expectedCheck: false, - }, - { - // passes if (>= 2.0.18, < 3.0.0) || (>= 3.1.16, < 4.0.0) || (>= 4.0.8, < 5.0.0) || ( >=5.0.0,<6.0.0) - name: "expect false", - currentVersion: "5.0.0-beta.5", - rangeVersion: []string{`^2.0.18 || ^3.1.6 || ^4.0.8 || ^5.0.0-beta.5`}, - expectedCheck: true, - }, - { - // Ruby GEM with more dots - name: "expect false", - currentVersion: "1.10.9-java", - rangeVersion: []string{`>= 1.6.7.1`}, - expectedCheck: true, - }, - { - name: "expect prerelease suffixed in minor version to work", - currentVersion: "4.1a", - rangeVersion: []string{`< 4.2b1`}, - expectedCheck: true, - }, - { - name: "expect prerelease suffixed in patch version to work", - currentVersion: "4.1.2a", - rangeVersion: []string{`< 4.2b1`}, - expectedCheck: true, - }, - { - name: "expect prerelease suffixed in patch version to work in failing case", - currentVersion: "4.1.2c", - rangeVersion: []string{`<= 4.1.2b`}, - expectedCheck: false, - }, - { - name: "expect double equal to work", - currentVersion: "1.7", - rangeVersion: []string{`==1.7`}, - expectedCheck: true, + name: "with epoch", + pkg: types.Package{ + SrcEpoch: 2, + SrcVersion: "1.2.3", + SrcRelease: "alpha", + }, + want: "2:1.2.3-alpha", }, } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - v, err := semver.NewVersion(FormatPatchVersion(tc.currentVersion)) - require.NoError(t, err) - match := MatchVersions(v, tc.rangeVersion) - assert.Equal(t, tc.expectedCheck, match) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FormatSrcVersion(tt.pkg) + assert.Equal(t, tt.want, got) }) } } -func TestFormatPatchVersio(t *testing.T) { - testCases := []struct { - name string - currentVersion string - expectedVersion string +func TestFormatVersion(t *testing.T) { + tests := []struct { + name string + pkg types.Package + want string }{ { - name: "patch with no dots should return version should be unchanged", - currentVersion: "1.2.3-beta", - expectedVersion: "1.2.3-beta", + name: "happy path", + pkg: types.Package{ + Version: "1.2.3", + Release: "1", + }, + want: "1.2.3-1", }, { - name: "patch with dots after non-integer patch version should be unchanged", - currentVersion: "1.2.3-beta.1", - expectedVersion: "1.2.3-beta.1", - }, - { - name: "patch with dots after integer patch version should append dash and join rest versions parts", - currentVersion: "1.2.3.4", - expectedVersion: "1.2.3-4", - }, - { - name: "patch with dots after integer patch version should append dash and join extra versions parts", - currentVersion: "1.2.3.4.5", - expectedVersion: "1.2.3-4.5", - }, - { - name: "unchanged case", - currentVersion: "1.2.3.4-5", - expectedVersion: "1.2.3-4-5", - }, - { - name: "prerelease suffixed in minor", - currentVersion: "1.11a", - expectedVersion: "1.11-a", - }, - { - name: "prerelease suffixed in patch", - currentVersion: "1.11.5rc", - expectedVersion: "1.11.5-rc", + name: "with epoch", + pkg: types.Package{ + Epoch: 2, + Version: "1.2.3", + Release: "alpha", + }, + want: "2:1.2.3-alpha", }, } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := FormatPatchVersion(tc.currentVersion) - assert.Equal(t, tc.expectedVersion, got) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FormatVersion(tt.pkg) + assert.Equal(t, tt.want, got) }) } }