mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-06 12:51:17 -08:00
Compare commits
9 Commits
feat/rooti
...
v0.64.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
280491bb51 | ||
|
|
a6e9807c09 | ||
|
|
1e1e1b5fa6 | ||
|
|
7333c469f4 | ||
|
|
bac6f7b3da | ||
|
|
a9f7dcdb9c | ||
|
|
3a0ec0f2ac | ||
|
|
41d0f949c8 | ||
|
|
fd2bc91e13 |
@@ -1 +1 @@
|
||||
{".":"0.63.0"}
|
||||
{".":"0.64.0"}
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## [0.64.0](https://github.com/aquasecurity/trivy/compare/v0.63.0...v0.64.0) (2025-06-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** add version constraints to annoucements ([#9023](https://github.com/aquasecurity/trivy/issues/9023)) ([19efa9f](https://github.com/aquasecurity/trivy/commit/19efa9fd372242d2ec582a248e9e6573d2caef00))
|
||||
* **java:** dereference all maven settings.xml env placeholders ([#9024](https://github.com/aquasecurity/trivy/issues/9024)) ([5aade69](https://github.com/aquasecurity/trivy/commit/5aade698c71450badf8db028be61e12ec85c6248))
|
||||
* **misconf:** add OpenTofu file extension support ([#8747](https://github.com/aquasecurity/trivy/issues/8747)) ([57801d0](https://github.com/aquasecurity/trivy/commit/57801d0324384d990889ba39d856c881e5b8b070))
|
||||
* **misconf:** normalize CreatedBy for buildah and legacy docker builder ([#8953](https://github.com/aquasecurity/trivy/issues/8953)) ([65e155f](https://github.com/aquasecurity/trivy/commit/65e155fdaf0ad02ec82f00a004427f126faf65ed))
|
||||
* **redhat:** Add EOL date for RHEL 10. ([#8910](https://github.com/aquasecurity/trivy/issues/8910)) ([48258a7](https://github.com/aquasecurity/trivy/commit/48258a701a7adb210c433310de52f48568ccee19))
|
||||
* reject unsupported artifact types in remote image retrieval ([#9052](https://github.com/aquasecurity/trivy/issues/9052)) ([1e1e1b5](https://github.com/aquasecurity/trivy/commit/1e1e1b5fa6a884da978fe1ed4c222d613d6eafbd))
|
||||
* **sbom:** add manufacturer field to CycloneDX tools metadata ([#9019](https://github.com/aquasecurity/trivy/issues/9019)) ([41d0f94](https://github.com/aquasecurity/trivy/commit/41d0f949c874609641c08fa2620fa10bf4ceef78))
|
||||
* **terraform:** add partial evaluation for policy templates ([#8967](https://github.com/aquasecurity/trivy/issues/8967)) ([a9f7dcd](https://github.com/aquasecurity/trivy/commit/a9f7dcdb9c5973746c3737f2bbc3306a74be5408))
|
||||
* **ubuntu:** add end of life date for Ubuntu 25.04 ([#9077](https://github.com/aquasecurity/trivy/issues/9077)) ([367564a](https://github.com/aquasecurity/trivy/commit/367564a3bec0c202566c59598dcff087bf50a23d))
|
||||
* **ubuntu:** add eol date for 20.04-ESM ([#8981](https://github.com/aquasecurity/trivy/issues/8981)) ([87118a0](https://github.com/aquasecurity/trivy/commit/87118a0ec4a6ae492523b7bac9834c2b93a14557))
|
||||
* **vuln:** add Root.io support for container image scanning ([#9073](https://github.com/aquasecurity/trivy/issues/9073)) ([3a0ec0f](https://github.com/aquasecurity/trivy/commit/3a0ec0f2acff6a13ed6ab348b6b220d49e14a298))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add missing version check flags ([#8951](https://github.com/aquasecurity/trivy/issues/8951)) ([ef5f8de](https://github.com/aquasecurity/trivy/commit/ef5f8de8dadf5534a2c965aecca01c7067e5baca))
|
||||
* **cli:** add some values to the telemetry call ([#9056](https://github.com/aquasecurity/trivy/issues/9056)) ([fd2bc91](https://github.com/aquasecurity/trivy/commit/fd2bc91e133f846bc9f0910c19ac3be3fbfe4009))
|
||||
* Correctly check for semver versions for trivy version check ([#8948](https://github.com/aquasecurity/trivy/issues/8948)) ([b813527](https://github.com/aquasecurity/trivy/commit/b813527449c4604f5afad71ae82b13399bb48680))
|
||||
* don't show corrupted trivy-db warning for first run ([#8991](https://github.com/aquasecurity/trivy/issues/8991)) ([4ed78e3](https://github.com/aquasecurity/trivy/commit/4ed78e39afe57e81c12482fef9102dc3f85d1493))
|
||||
* **misconf:** .Config.User always takes precedence over USER in .History ([#9050](https://github.com/aquasecurity/trivy/issues/9050)) ([371b8cc](https://github.com/aquasecurity/trivy/commit/371b8cc02f2ffa3f42534a437ce8727519e7b9b9))
|
||||
* **misconf:** correct Azure value-to-time conversion in AsTimeValue ([#9015](https://github.com/aquasecurity/trivy/issues/9015)) ([40d017b](https://github.com/aquasecurity/trivy/commit/40d017b67da38131734eab90c42ad945ac3b5013))
|
||||
* **misconf:** move disabled checks filtering after analyzer scan ([#9002](https://github.com/aquasecurity/trivy/issues/9002)) ([a58c36d](https://github.com/aquasecurity/trivy/commit/a58c36de124cba7250e1a5ae0cc32d83018391fe))
|
||||
* **misconf:** reduce log noise on incompatible check ([#9029](https://github.com/aquasecurity/trivy/issues/9029)) ([99c5151](https://github.com/aquasecurity/trivy/commit/99c5151d6ea1dabe85cce75ff9bb91166532b11f))
|
||||
* **nodejs:** correctly parse `packages` array of `bun.lock` file ([#8998](https://github.com/aquasecurity/trivy/issues/8998)) ([875ec3a](https://github.com/aquasecurity/trivy/commit/875ec3a9d2568e15a6824c8f84ad6a59f03eb212))
|
||||
* **report:** don't panic when report contains vulns, but doesn't contain packages for `table` format ([#8549](https://github.com/aquasecurity/trivy/issues/8549)) ([87fda76](https://github.com/aquasecurity/trivy/commit/87fda76f38a3a6939a87828c3df0c5ac2cf7fce3))
|
||||
* **sbom:** remove unnecessary OS detection check in SBOM decoding ([#9034](https://github.com/aquasecurity/trivy/issues/9034)) ([198789a](https://github.com/aquasecurity/trivy/commit/198789a07b857b053c73f8fcd1f508902fac344d))
|
||||
|
||||
## [0.63.0](https://github.com/aquasecurity/trivy/compare/v0.62.0...v0.63.0) (2025-05-29)
|
||||
|
||||
|
||||
|
||||
19
docs/docs/advanced/telemetry-flags.md
Normal file
19
docs/docs/advanced/telemetry-flags.md
Normal file
@@ -0,0 +1,19 @@
|
||||
```
|
||||
--debug
|
||||
--detection-priority
|
||||
--format
|
||||
--ignore-status
|
||||
--include-dev-deps
|
||||
--insecure
|
||||
--list-all-pkgs
|
||||
--misconfig-scanners
|
||||
--pkg-relationships
|
||||
--pkg-types
|
||||
--quiet
|
||||
--report
|
||||
--scanners
|
||||
--severity
|
||||
--show-suppressed
|
||||
--timeout
|
||||
--vuln-severity-source
|
||||
```
|
||||
@@ -1,24 +1,30 @@
|
||||
# Usage Telemetry
|
||||
|
||||
Trivy collect anonymous usage data in order to help us improve the product. This document explains what is collected and how you can control it.
|
||||
Trivy collects anonymous usage data in order to help us improve the product. This document explains what is collected and how you can control it.
|
||||
|
||||
## Data collected
|
||||
|
||||
The following information could be collected:
|
||||
|
||||
- Environmental information
|
||||
- Installation identifier
|
||||
- Trivy version
|
||||
- Operating system
|
||||
- Scan
|
||||
- Non-revealing scan options
|
||||
- Environmental information:
|
||||
- Installation identifier
|
||||
- Trivy version
|
||||
- Operating system
|
||||
- Scan:
|
||||
- Non-revealing scan options (see below for comprehensive list)
|
||||
|
||||
### Captured scan options
|
||||
The following flags will be included with their value:
|
||||
|
||||
--8<-- "./docs/docs/advanced/telemetry-flags.md"
|
||||
|
||||
|
||||
## Privacy
|
||||
|
||||
No personal information, scan results, or sensitive data is specifically collected. We take the following measures to ensure that:
|
||||
|
||||
- Installation identifier: one-way hash of machine fingerprint, resulting in opaque string.
|
||||
- Scaner: any option that is user controlled is omitted (never collected). For example, file paths, image names, etc are never collected.
|
||||
- Installation identifier: one-way hash of machine fingerprint, resulting in opaque ID.
|
||||
- Scan: any option that is user-controlled is omitted (never collected). For example, file paths, image names, etc are never collected.
|
||||
|
||||
Trivy is an Aqua Security product and adheres to the company's privacy policy: <https://aquasec.com/privacy>.
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ Example: [Dockerfile](https://github.com/aquasecurity/trivy-ci-test/blob/main/Do
|
||||
[license]: ../../scanner/license.md
|
||||
|
||||
[^1]: `*.egg-info`, `*.egg-info/PKG-INFO`, `*.egg` and `EGG-INFO/PKG-INFO`
|
||||
[^2]: `.dist-info/META-DATA`
|
||||
[^2]: `.dist-info/METADATA`
|
||||
[^3]: `*.jar`, `*.war`, `*.par` and `*.ear`
|
||||
[^4]: ✅ means "enabled" and `-` means "disabled" in the image scanning
|
||||
[^5]: ✅ means "enabled" and `-` means "disabled" in the rootfs scanning
|
||||
|
||||
@@ -148,7 +148,7 @@ See [here](https://packaging.python.org/en/latest/discussions/package-formats/)
|
||||
Trivy looks for `*.egg-info`, `*.egg-info/PKG-INFO`, `*.egg` and `EGG-INFO/PKG-INFO` to identify Python packages.
|
||||
|
||||
### Wheel
|
||||
Trivy looks for `.dist-info/META-DATA` to identify Python packages.
|
||||
Trivy looks for `.dist-info/METADATA` to identify Python packages.
|
||||
|
||||
[^1]: Trivy checks `python`, `python3`, `python2` and `python.exe` file names.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Trivy supports them for
|
||||
| [Bitnami packages](bitnami.md) | `/opt/bitnami/<component>/.spdx-<component>.spdx` | ✅ | ✅ | - | - |
|
||||
| [Conda](conda.md) | `<conda-root>/envs/<env>/conda-meta/<package>.json` | ✅ | ✅ | - | - |
|
||||
| | `environment.yml` | - | - | ✅ | ✅ |
|
||||
| [Root.io images](rootio.md) | - | ✅ | ✅ | - | - |
|
||||
| [RPM Archives](rpm.md) | `*.rpm` | ✅[^5] | ✅[^5] | ✅[^5] | ✅[^5] |
|
||||
|
||||
[sbom]: ../../supply-chain/sbom.md
|
||||
|
||||
20
docs/docs/coverage/others/rootio.md
Normal file
20
docs/docs/coverage/others/rootio.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Root.io
|
||||
|
||||
!!! warning "EXPERIMENTAL"
|
||||
Scanning results may be inaccurate.
|
||||
|
||||
While it is not an OS, this page describes the details of [Root.io](https://root.io/) patch distribution service.
|
||||
Root.io provides security patches for [Debian](../os/debian.md), [Ubuntu](../os/ubuntu.md), and [Alpine](../os/alpine.md)-based container images.
|
||||
Root.io patches are detected when Trivy finds packages with specific version suffixes:
|
||||
|
||||
- **Debian/Ubuntu**: packages with `.root.io` in version string
|
||||
- **Alpine**: packages with `-r\d007\d` pattern in version string (e.g., `-r10071`, `-r20072`)
|
||||
|
||||
When Root.io patches are detected, Trivy automatically switches to Root.io scanning mode for vulnerability detection.
|
||||
Even when the original OS distributor (Debian, Ubuntu, Alpine) has not provided a patch for a vulnerability, Trivy will display Root.io patches if they are available.
|
||||
|
||||
For detailed information about supported scanners, features, and functionality, please refer to the documentation for the underlying OS:
|
||||
|
||||
- [Debian](../os/debian.md)
|
||||
- [Ubuntu](../os/ubuntu.md)
|
||||
- [Alpine](../os/alpine.md)
|
||||
@@ -169,6 +169,7 @@ trivy filesystem [flags] PATH
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -190,6 +190,7 @@ trivy image [flags] IMAGE_NAME
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -178,6 +178,7 @@ trivy kubernetes [flags] [CONTEXT]
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -168,6 +168,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -170,6 +170,7 @@ trivy rootfs [flags] ROOTDIR
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -139,6 +139,7 @@ trivy sbom [flags] SBOM_PATH
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -155,6 +155,7 @@ trivy vm [flags] VM_IMAGE
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -37,6 +37,7 @@ See [here](../coverage/os/index.md#supported-os) for the supported OSes.
|
||||
| Azure Linux (CBL-Mariner) | [OVAL][azure] |
|
||||
| OpenSUSE/SLES | [CVRF][suse] |
|
||||
| Photon OS | [Photon Security Advisory][photon] |
|
||||
| Root.io | [Root.io Patch Feed][rootio] |
|
||||
|
||||
#### Data Source Selection
|
||||
Trivy **only** consumes security advisories from the sources listed in the above table.
|
||||
@@ -394,6 +395,7 @@ Example logic for the following vendor severity levels when scanning an Alpine i
|
||||
[suse]: http://ftp.suse.com/pub/projects/security/cvrf/
|
||||
[photon]: https://packages.vmware.com/photon/photon_cve_metadata/
|
||||
[azure]: https://github.com/microsoft/AzureLinuxVulnerabilityData/
|
||||
[rootio]: https://api.root.io/external/patch_feed
|
||||
|
||||
[php-ghsa]: https://github.com/advisories?query=ecosystem%3Acomposer
|
||||
[python-ghsa]: https://github.com/advisories?query=ecosystem%3Apip
|
||||
|
||||
8
go.mod
8
go.mod
@@ -24,7 +24,7 @@ require (
|
||||
github.com/aquasecurity/testdocker v0.0.0-20250616060700-ba6845ac6d17
|
||||
github.com/aquasecurity/tml v0.6.1
|
||||
github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250529093513-a12dfc204b6e
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250627124416-ca81c496a932
|
||||
github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48
|
||||
github.com/aquasecurity/trivy-kubernetes v0.9.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5
|
||||
@@ -248,7 +248,7 @@ require (
|
||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-yaml v1.15.23 // indirect
|
||||
github.com/gofrs/uuid v4.3.1+incompatible // indirect
|
||||
@@ -323,7 +323,7 @@ require (
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||
github.com/opencontainers/selinux v1.12.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
@@ -347,7 +347,7 @@ require (
|
||||
github.com/rubenv/sql-migrate v1.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/samber/oops v1.16.1 // indirect
|
||||
github.com/samber/oops v1.18.1 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
|
||||
github.com/sassoftware/relic v7.2.1+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -798,8 +798,8 @@ github.com/aquasecurity/tml v0.6.1 h1:y2ZlGSfrhnn7t4ZJ/0rotuH+v5Jgv6BDDO5jB6A9gw
|
||||
github.com/aquasecurity/tml v0.6.1/go.mod h1:OnYMWY5lvI9ejU7yH9LCberWaaTBW7hBFsITiIMY2yY=
|
||||
github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169 h1:TckzIxUX7lZaU9f2lNxCN0noYYP8fzmSQf6a4JdV83w=
|
||||
github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169/go.mod h1:nT69xgRcBD4NlHwTBpWMYirpK5/Zpl8M+XDOgmjMn2k=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250529093513-a12dfc204b6e h1:+B/in1DQDGwQbKhW5pWL8XxBgnZKxXhUznylJ2NCyvs=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250529093513-a12dfc204b6e/go.mod h1:4zd4qZcjhNAHASz5I0O7qapv5h5gSJzSEaZXv/IPOGc=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250627124416-ca81c496a932 h1:5GKQ53uGGHEEtZ/FX94jcIdfEGeyiHZ7tmJ5nCtDz5c=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250627124416-ca81c496a932/go.mod h1:Ubl2YWA6Zg7eaojg4MDmeDdYU4+PiGPsnwo6B5UIwqw=
|
||||
github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48 h1:JVgBIuIYbwG+ekC5lUHUpGJboPYiCcxiz06RCtz8neI=
|
||||
github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48/go.mod h1:Ldya37FLi0e/5Cjq2T5Bty7cFkzUDwTcPeQua+2M8i8=
|
||||
github.com/aquasecurity/trivy-kubernetes v0.9.0 h1:rp8RuXwKfFWUPR/ULksA2WpD0z6rslVkzLmPGQr61Wc=
|
||||
@@ -1168,8 +1168,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
@@ -1617,8 +1617,8 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
|
||||
github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -1743,8 +1743,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/oops v1.16.1 h1:XlKkXsWM5g8hE4C+sEV9n0X282fZn3XabVmAKU2RiHI=
|
||||
github.com/samber/oops v1.16.1/go.mod h1:8eXgMAJcDXRAijQsFRhfy/EHDOTiSvwkg6khFqFK078=
|
||||
github.com/samber/oops v1.18.1 h1:qjhZbqbdyhWBKntkY8sxrDNKA8b4c5VHlmI1rli7X7M=
|
||||
github.com/samber/oops v1.18.1/go.mod h1:xYqvimigkKV70HyLXiBZJFpIWi2CGcc6Xx7eV+2HycI=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
||||
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"manufacturer": {
|
||||
"name": "Aqua Security Software Ltd."
|
||||
},
|
||||
"group": "aquasecurity",
|
||||
"name": "trivy",
|
||||
"version": "dev"
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"manufacturer": {
|
||||
"name": "Aqua Security Software Ltd."
|
||||
},
|
||||
"group": "aquasecurity",
|
||||
"name": "trivy",
|
||||
"version": "dev"
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"manufacturer": {
|
||||
"name": "Aqua Security Software Ltd."
|
||||
},
|
||||
"group": "aquasecurity",
|
||||
"name": "trivy",
|
||||
"version": "dev"
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"manufacturer": {
|
||||
"name": "Aqua Security Software Ltd."
|
||||
},
|
||||
"group": "aquasecurity",
|
||||
"name": "trivy",
|
||||
"version": "dev"
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"manufacturer": {
|
||||
"name": "Aqua Security Software Ltd."
|
||||
},
|
||||
"group": "aquasecurity",
|
||||
"name": "trivy",
|
||||
"version": "dev"
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"manufacturer": {
|
||||
"name": "Aqua Security Software Ltd."
|
||||
},
|
||||
"group": "aquasecurity",
|
||||
"name": "trivy",
|
||||
"version": "dev"
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra/doc"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/commands"
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,50 +35,43 @@ func main() {
|
||||
os.Setenv("XDG_DATA_HOME", os.TempDir())
|
||||
|
||||
cmd := commands.NewApp()
|
||||
allFlagGroups := getAllFlags()
|
||||
|
||||
cmd.DisableAutoGenTag = true
|
||||
if err := doc.GenMarkdownTree(cmd, "./docs/docs/references/configuration/cli"); err != nil {
|
||||
log.Fatal("Fatal error", log.Err(err))
|
||||
}
|
||||
if err := generateConfigDocs("./docs/docs/references/configuration/config-file.md"); err != nil {
|
||||
if err := generateConfigDocs("./docs/docs/references/configuration/config-file.md", allFlagGroups); err != nil {
|
||||
log.Fatal("Fatal error in config file generation", log.Err(err))
|
||||
}
|
||||
if err := generateTelemetryFlagDocs("./docs/docs/advanced/telemetry-flags.md", allFlagGroups); err != nil {
|
||||
log.Fatal("Fatal error in telemetry docs generation", log.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// generateConfigDocs creates markdown file for Trivy config.
|
||||
func generateConfigDocs(filename string) error {
|
||||
// remoteFlags should contain Client and Server flags.
|
||||
// NewClientFlags doesn't initialize `Listen` field
|
||||
remoteFlags := flag.NewClientFlags()
|
||||
remoteFlags.Listen = flag.ServerListenFlag.Clone()
|
||||
|
||||
// These flags don't work from config file.
|
||||
// Clear configName to skip them later.
|
||||
globalFlags := flag.NewGlobalFlagGroup()
|
||||
globalFlags.ConfigFile.ConfigName = ""
|
||||
globalFlags.ShowVersion.ConfigName = ""
|
||||
globalFlags.GenerateDefaultConfig.ConfigName = ""
|
||||
|
||||
var allFlagGroups = []flag.FlagGroup{
|
||||
globalFlags,
|
||||
flag.NewCacheFlagGroup(),
|
||||
flag.NewCleanFlagGroup(),
|
||||
remoteFlags,
|
||||
flag.NewDBFlagGroup(),
|
||||
flag.NewImageFlagGroup(),
|
||||
flag.NewK8sFlagGroup(),
|
||||
flag.NewLicenseFlagGroup(),
|
||||
flag.NewMisconfFlagGroup(),
|
||||
flag.NewModuleFlagGroup(),
|
||||
flag.NewPackageFlagGroup(),
|
||||
flag.NewRegistryFlagGroup(),
|
||||
flag.NewRegoFlagGroup(),
|
||||
flag.NewReportFlagGroup(),
|
||||
flag.NewRepoFlagGroup(),
|
||||
flag.NewScanFlagGroup(),
|
||||
flag.NewSecretFlagGroup(),
|
||||
flag.NewVulnerabilityFlagGroup(),
|
||||
// generateTelemetryFlagDocs updates the telemetry section in the documentation file
|
||||
// with the flags that are safe to be included in telemetry.
|
||||
func generateTelemetryFlagDocs(filename string, allFlagGroups []flag.FlagGroup) error {
|
||||
var telemetryFlags []string
|
||||
for _, group := range allFlagGroups {
|
||||
flags := group.Flags()
|
||||
for _, f := range flags {
|
||||
if f.IsTelemetrySafe() && f.GetConfigName() != "" {
|
||||
telemetryFlags = append(telemetryFlags, fmt.Sprintf("--%s", f.GetName()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(telemetryFlags)
|
||||
flagContent := fmt.Sprintf("```\n%s\n```\n", strings.Join(telemetryFlags, "\n"))
|
||||
if err := os.WriteFile(filename, []byte(flagContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write to %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateConfigDocs creates markdown file for Trivy config.
|
||||
func generateConfigDocs(filename string, allFlagGroups []flag.FlagGroup) error {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -87,6 +80,10 @@ func generateConfigDocs(filename string) error {
|
||||
f.WriteString("# " + title + "\n\n")
|
||||
f.WriteString(description + "\n")
|
||||
|
||||
if len(allFlagGroups) == 0 {
|
||||
return fmt.Errorf("no flag groups found")
|
||||
}
|
||||
|
||||
for _, group := range allFlagGroups {
|
||||
f.WriteString("## " + group.Name() + " options\n")
|
||||
writeFlags(group, f)
|
||||
@@ -161,3 +158,39 @@ func writeFlagValue(val any, ind string, w *os.File) {
|
||||
fmt.Fprintf(w, " %v\n", v)
|
||||
}
|
||||
}
|
||||
|
||||
func getAllFlags() []flag.FlagGroup {
|
||||
// remoteFlags should contain Client and Server flags.
|
||||
// NewClientFlags doesn't initialize `Listen` field
|
||||
remoteFlags := flag.NewClientFlags()
|
||||
remoteFlags.Listen = flag.ServerListenFlag.Clone()
|
||||
|
||||
// These flags don't work from config file.
|
||||
// Clear configName to skip them later.
|
||||
globalFlags := flag.NewGlobalFlagGroup()
|
||||
globalFlags.ConfigFile.ConfigName = ""
|
||||
globalFlags.ShowVersion.ConfigName = ""
|
||||
globalFlags.GenerateDefaultConfig.ConfigName = ""
|
||||
|
||||
return []flag.FlagGroup{
|
||||
globalFlags,
|
||||
flag.NewCacheFlagGroup(),
|
||||
flag.NewCleanFlagGroup(),
|
||||
remoteFlags,
|
||||
flag.NewDBFlagGroup(),
|
||||
flag.NewImageFlagGroup(),
|
||||
flag.NewK8sFlagGroup(),
|
||||
flag.NewLicenseFlagGroup(),
|
||||
flag.NewMisconfFlagGroup(),
|
||||
flag.NewModuleFlagGroup(),
|
||||
flag.NewPackageFlagGroup(),
|
||||
flag.NewRegistryFlagGroup(),
|
||||
flag.NewRegoFlagGroup(),
|
||||
flag.NewReportFlagGroup(),
|
||||
flag.NewRepoFlagGroup(),
|
||||
flag.NewScanFlagGroup(),
|
||||
flag.NewSecretFlagGroup(),
|
||||
flag.NewVulnerabilityFlagGroup(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ nav:
|
||||
- Overview: docs/coverage/others/index.md
|
||||
- Bitnami Images: docs/coverage/others/bitnami.md
|
||||
- Conda: docs/coverage/others/conda.md
|
||||
- Root.io Images: docs/coverage/others/rootio.md
|
||||
- RPM Archives: docs/coverage/others/rpm.md
|
||||
- Kubernetes: docs/coverage/kubernetes.md
|
||||
- Configuration:
|
||||
@@ -262,6 +263,7 @@ markdown_extensions:
|
||||
- pymdownx.highlight
|
||||
- pymdownx.details
|
||||
- pymdownx.magiclink
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
|
||||
@@ -124,13 +124,9 @@ func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...RunnerOptio
|
||||
Insecure: cliOptions.Insecure,
|
||||
Timeout: cliOptions.Timeout,
|
||||
}))
|
||||
|
||||
// If the user has not disabled notices or is running in quiet mode
|
||||
r.versionChecker = notification.NewVersionChecker(
|
||||
notification.WithSkipVersionCheck(cliOptions.SkipVersionCheck),
|
||||
notification.WithQuietMode(cliOptions.Quiet),
|
||||
notification.WithTelemetryDisabled(cliOptions.DisableTelemetry),
|
||||
)
|
||||
// get the sub command that is being used or fallback to "trivy"
|
||||
commandName := lo.Ternary(len(os.Args) > 1, os.Args[1], "trivy")
|
||||
r.versionChecker = notification.NewVersionChecker(commandName, &cliOptions)
|
||||
|
||||
// Update the vulnerability database if needed.
|
||||
if err := r.initDB(ctx, cliOptions); err != nil {
|
||||
@@ -157,7 +153,7 @@ func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...RunnerOptio
|
||||
// only do this if the user has not disabled notices or is running
|
||||
// in quiet mode
|
||||
if r.versionChecker != nil {
|
||||
r.versionChecker.RunUpdateCheck(ctx, os.Args[1:])
|
||||
r.versionChecker.RunUpdateCheck(ctx)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
|
||||
@@ -14,12 +14,14 @@ import (
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/bottlerocket"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/chainguard"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/debian"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/driver"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/echo"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/minimos"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/oracle"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/photon"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/redhat"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rocky"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/suse"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/ubuntu"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/wolfi"
|
||||
@@ -32,7 +34,7 @@ var (
|
||||
// ErrUnsupportedOS defines error for unsupported OS
|
||||
ErrUnsupportedOS = xerrors.New("unsupported os")
|
||||
|
||||
drivers = map[ftypes.OSType]Driver{
|
||||
drivers = map[ftypes.OSType]driver.Driver{
|
||||
ftypes.Alpine: alpine.NewScanner(),
|
||||
ftypes.Alma: alma.NewScanner(),
|
||||
ftypes.Amazon: amazon.NewScanner(),
|
||||
@@ -55,36 +57,36 @@ var (
|
||||
ftypes.Echo: echo.NewScanner(),
|
||||
ftypes.MinimOS: minimos.NewScanner(),
|
||||
}
|
||||
|
||||
// providers dynamically generate drivers based on package information
|
||||
// and environment detection. They are tried before standard OS-specific drivers.
|
||||
providers = []driver.Provider{
|
||||
rootio.Provider,
|
||||
}
|
||||
)
|
||||
|
||||
// RegisterDriver is defined for extensibility and not supposed to be used in Trivy.
|
||||
func RegisterDriver(name ftypes.OSType, driver Driver) {
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// Driver defines operations for OS package scan
|
||||
type Driver interface {
|
||||
Detect(context.Context, string, *ftypes.Repository, []ftypes.Package) ([]types.DetectedVulnerability, error)
|
||||
IsSupportedVersion(context.Context, ftypes.OSType, string) bool
|
||||
func RegisterDriver(name ftypes.OSType, drv driver.Driver) {
|
||||
drivers[name] = drv
|
||||
}
|
||||
|
||||
// Detect detects the vulnerabilities
|
||||
func Detect(ctx context.Context, _, osFamily ftypes.OSType, osName string, repo *ftypes.Repository, _ time.Time, pkgs []ftypes.Package) ([]types.DetectedVulnerability, bool, error) {
|
||||
ctx = log.WithContextPrefix(ctx, string(osFamily))
|
||||
|
||||
driver, err := newDriver(osFamily)
|
||||
d, err := newDriver(osFamily, pkgs)
|
||||
if err != nil {
|
||||
return nil, false, ErrUnsupportedOS
|
||||
}
|
||||
|
||||
eosl := !driver.IsSupportedVersion(ctx, osFamily, osName)
|
||||
eosl := !d.IsSupportedVersion(ctx, osFamily, osName)
|
||||
|
||||
// Package `gpg-pubkey` doesn't use the correct version.
|
||||
// We don't need to find vulnerabilities for this package.
|
||||
filteredPkgs := lo.Filter(pkgs, func(pkg ftypes.Package, _ int) bool {
|
||||
return pkg.Name != "gpg-pubkey"
|
||||
})
|
||||
vulns, err := driver.Detect(ctx, osName, repo, filteredPkgs)
|
||||
vulns, err := d.Detect(ctx, osName, repo, filteredPkgs)
|
||||
if err != nil {
|
||||
return nil, false, xerrors.Errorf("failed detection: %w", err)
|
||||
}
|
||||
@@ -92,9 +94,17 @@ func Detect(ctx context.Context, _, osFamily ftypes.OSType, osName string, repo
|
||||
return vulns, eosl, nil
|
||||
}
|
||||
|
||||
func newDriver(osFamily ftypes.OSType) (Driver, error) {
|
||||
if driver, ok := drivers[osFamily]; ok {
|
||||
return driver, nil
|
||||
func newDriver(osFamily ftypes.OSType, pkgs []ftypes.Package) (driver.Driver, error) {
|
||||
// Try providers first
|
||||
for _, provider := range providers {
|
||||
if d := provider(osFamily, pkgs); d != nil {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to standard drivers
|
||||
if d, ok := drivers[osFamily]; ok {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
log.Warn("Unsupported os", log.String("family", string(osFamily)))
|
||||
|
||||
17
pkg/detector/ospkg/driver/driver.go
Normal file
17
pkg/detector/ospkg/driver/driver.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
// Driver defines operations for OS package scan
|
||||
type Driver interface {
|
||||
Detect(context.Context, string, *ftypes.Repository, []ftypes.Package) ([]types.DetectedVulnerability, error)
|
||||
IsSupportedVersion(context.Context, ftypes.OSType, string) bool
|
||||
}
|
||||
|
||||
// Provider creates a specialized driver based on the environment
|
||||
type Provider func(osFamily ftypes.OSType, pkgs []ftypes.Package) Driver
|
||||
45
pkg/detector/ospkg/rootio/provider.go
Normal file
45
pkg/detector/ospkg/rootio/provider.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package rootio
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/driver"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// debianRootIOPattern matches Debian/Ubuntu Root.io version pattern: .root.io
|
||||
debianRootIOPattern = regexp.MustCompile(`\.root\.io`)
|
||||
// alpineRootIOPattern matches Alpine Root.io version pattern: -r\d007\d (e.g., -r10071, -r20072)
|
||||
alpineRootIOPattern = regexp.MustCompile(`-r\d007\d`)
|
||||
)
|
||||
|
||||
// Provider creates a Root.io driver if Root.io packages are detected
|
||||
func Provider(osFamily ftypes.OSType, pkgs []ftypes.Package) driver.Driver {
|
||||
if !isRootIOEnvironment(osFamily, pkgs) {
|
||||
return nil
|
||||
}
|
||||
return NewScanner(osFamily)
|
||||
}
|
||||
|
||||
// isRootIOEnvironment detects if the environment is Root.io based on package suffixes
|
||||
func isRootIOEnvironment(osFamily ftypes.OSType, pkgs []ftypes.Package) bool {
|
||||
switch osFamily {
|
||||
case ftypes.Debian, ftypes.Ubuntu:
|
||||
return hasPackageWithPattern(pkgs, debianRootIOPattern)
|
||||
case ftypes.Alpine:
|
||||
return hasPackageWithPattern(pkgs, alpineRootIOPattern)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// hasPackageWithPattern checks if any package version matches the specified pattern
|
||||
func hasPackageWithPattern(pkgs []ftypes.Package, pattern *regexp.Regexp) bool {
|
||||
for _, pkg := range pkgs {
|
||||
if pattern.MatchString(pkg.Version) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
118
pkg/detector/ospkg/rootio/provider_test.go
Normal file
118
pkg/detector/ospkg/rootio/provider_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package rootio_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
osFamily ftypes.OSType
|
||||
pkgs []ftypes.Package
|
||||
want bool // true if driver should be returned, false if nil
|
||||
}{
|
||||
{
|
||||
name: "Debian with .root.io package",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-13+deb11u4.root.io"},
|
||||
{Name: "bash", Version: "5.1-2+deb11u1"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Ubuntu with .root.io package",
|
||||
osFamily: ftypes.Ubuntu,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-0ubuntu9.9.root.io"},
|
||||
{Name: "bash", Version: "5.1-6ubuntu1"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Alpine with Root.io pattern package",
|
||||
osFamily: ftypes.Alpine,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "musl", Version: "1.2.3-r10071"},
|
||||
{Name: "busybox", Version: "1.35.0-r17"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Debian without .root.io package",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-13+deb11u4"},
|
||||
{Name: "bash", Version: "5.1-2+deb11u1"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Ubuntu without .root.io package",
|
||||
osFamily: ftypes.Ubuntu,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-0ubuntu9.9"},
|
||||
{Name: "bash", Version: "5.1-6ubuntu1"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Alpine without Root.io pattern package",
|
||||
osFamily: ftypes.Alpine,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "musl", Version: "1.2.3-r0"},
|
||||
{Name: "busybox", Version: "1.35.0-r17"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Unsupported OS family",
|
||||
osFamily: ftypes.RedHat,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "glibc", Version: "2.28-151.el8.root.io"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Empty package list",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Multiple .root.io packages",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-13+deb11u4.root.io"},
|
||||
{Name: "openssl", Version: "1.1.1n-0+deb11u3.root.io"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Multiple Alpine Root.io pattern packages",
|
||||
osFamily: ftypes.Alpine,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "musl", Version: "1.2.3-r20072"},
|
||||
{Name: "openssl", Version: "1.1.1t-r10071"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
driver := rootio.Provider(tt.osFamily, tt.pkgs)
|
||||
if tt.want {
|
||||
require.NotNil(t, driver, "Provider should return a driver for Root.io environment")
|
||||
} else {
|
||||
assert.Nil(t, driver, "Provider should return nil for non-Root.io environment")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
140
pkg/detector/ospkg/rootio/rootio.go
Normal file
140
pkg/detector/ospkg/rootio/rootio.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package rootio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/rootio"
|
||||
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/version"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/scan/utils"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
// Scanner implements the Root.io scanner
|
||||
type Scanner struct {
|
||||
comparer version.Comparer
|
||||
vsg rootio.VulnSrcGetter
|
||||
versionTrimmer func(string) string
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewScanner is the factory method for Scanner
|
||||
func NewScanner(baseOS ftypes.OSType) *Scanner {
|
||||
var comparer version.Comparer
|
||||
var vsg rootio.VulnSrcGetter
|
||||
var versionTrimmer func(string) string
|
||||
|
||||
switch baseOS {
|
||||
case ftypes.Debian:
|
||||
comparer = version.NewDEBComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Debian)
|
||||
versionTrimmer = version.Major
|
||||
case ftypes.Ubuntu:
|
||||
comparer = version.NewDEBComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Ubuntu)
|
||||
versionTrimmer = version.Minor
|
||||
case ftypes.Alpine:
|
||||
comparer = version.NewAPKComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Alpine)
|
||||
versionTrimmer = version.Minor
|
||||
default:
|
||||
// Should never happen as it's validated in the provider
|
||||
comparer = version.NewDEBComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Debian)
|
||||
versionTrimmer = version.Major
|
||||
}
|
||||
|
||||
return &Scanner{
|
||||
comparer: comparer,
|
||||
vsg: vsg,
|
||||
versionTrimmer: versionTrimmer,
|
||||
logger: log.WithPrefix("rootio"),
|
||||
}
|
||||
}
|
||||
|
||||
// Detect vulnerabilities in package using Root.io scanner
|
||||
func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) {
|
||||
// Trim patch/minor part of osVer.
|
||||
// e.g. "12.0.1" -> "12" (Debian), "24.04.1" -> "24.04" (Ubuntu), "3.17.2" -> "3.17" (Alpine)
|
||||
osVer = s.versionTrimmer(osVer)
|
||||
log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), log.Int("pkg_num", len(pkgs)))
|
||||
|
||||
var vulns []types.DetectedVulnerability
|
||||
for _, pkg := range pkgs {
|
||||
srcName := pkg.SrcName
|
||||
if srcName == "" {
|
||||
srcName = pkg.Name
|
||||
}
|
||||
|
||||
advisories, err := s.vsg.Get(osVer, srcName)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get Root.io advisories: %w", err)
|
||||
}
|
||||
|
||||
for _, adv := range advisories {
|
||||
if !s.isVulnerable(ctx, utils.FormatSrcVersion(pkg), adv) {
|
||||
continue
|
||||
}
|
||||
vulns = append(vulns, types.DetectedVulnerability{
|
||||
VulnerabilityID: adv.VulnerabilityID,
|
||||
PkgID: pkg.ID,
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: utils.FormatVersion(pkg),
|
||||
FixedVersion: strings.Join(adv.PatchedVersions, ", "),
|
||||
Layer: pkg.Layer,
|
||||
PkgIdentifier: pkg.Identifier,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
})
|
||||
}
|
||||
}
|
||||
return vulns, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) isVulnerable(ctx context.Context, installedVersion string, adv dbTypes.Advisory) bool {
|
||||
// Handle unfixed vulnerabilities
|
||||
if len(adv.VulnerableVersions) == 0 {
|
||||
// If no vulnerable versions are specified, it means the package is always vulnerable
|
||||
return true
|
||||
}
|
||||
|
||||
// For fixed vulnerabilities, check if installed version satisfies the constraint
|
||||
return s.checkConstraints(ctx, installedVersion, adv.VulnerableVersions)
|
||||
}
|
||||
|
||||
func (s *Scanner) checkConstraints(ctx context.Context, installedVersion string, constraintsStr []string) bool {
|
||||
if installedVersion == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, constraintStr := range constraintsStr {
|
||||
constraints, err := version.NewConstraints(constraintStr, s.comparer)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "Failed to parse constraints",
|
||||
log.String("constraints", constraintStr), log.Err(err))
|
||||
return false
|
||||
}
|
||||
|
||||
if satisfied, err := constraints.Check(installedVersion); err != nil {
|
||||
s.logger.DebugContext(ctx, "Failed to check version constraints",
|
||||
log.String("version", installedVersion),
|
||||
log.String("constraints", constraintStr), log.Err(err))
|
||||
return false
|
||||
} else if satisfied {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSupportedVersion checks if the version is supported.
|
||||
// Root.io creates fixes for EOL distributions, so we assume all versions are supported.
|
||||
func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool {
|
||||
return true
|
||||
}
|
||||
143
pkg/detector/ospkg/rootio/rootio_private_test.go
Normal file
143
pkg/detector/ospkg/rootio/rootio_private_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package rootio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy-db/pkg/types"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
func TestScanner_IsVulnerable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
installedVersion string
|
||||
vulnerableRanges []string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Installed vulnerable vendor version. There is no fix",
|
||||
installedVersion: "1.0.0",
|
||||
vulnerableRanges: []string{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable vendor version, fix by vendor",
|
||||
installedVersion: "1.0.0",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable vendor version, fix by vendor",
|
||||
installedVersion: "1.0.0-2",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable vendor version, fix by root.io (root.io version)",
|
||||
installedVersion: "1.0.0-2",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable vendor version, fix by root.io (root.io version)",
|
||||
installedVersion: "1.0.0-3",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable vendor version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-2",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-3",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable vendor version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-3",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-3",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable root.io version, fix by root.io",
|
||||
installedVersion: "1.0.0-1.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by root.io",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable root.io version, fix by vendor",
|
||||
installedVersion: "1.0.0-1.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by vendor",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-1",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable root.io version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-1.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-2",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-2",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by root.io (root.io + root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">1.0.0-2.root.io <1.0.0-2",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := NewScanner(ftypes.Debian)
|
||||
vulnerable := scanner.isVulnerable(t.Context(), tt.installedVersion, types.Advisory{VulnerableVersions: tt.vulnerableRanges})
|
||||
require.Equal(t, tt.want, vulnerable)
|
||||
})
|
||||
}
|
||||
}
|
||||
163
pkg/detector/ospkg/rootio/rootio_test.go
Normal file
163
pkg/detector/ospkg/rootio/rootio_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package rootio_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy-db/pkg/db"
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
|
||||
"github.com/aquasecurity/trivy/internal/dbtest"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
func TestScanner_Detect(t *testing.T) {
|
||||
type args struct {
|
||||
osVer string
|
||||
pkgs []ftypes.Package
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
baseOS ftypes.OSType
|
||||
fixtures []string
|
||||
args args
|
||||
want []types.DetectedVulnerability
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Debian scanner",
|
||||
baseOS: ftypes.Debian,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/rootio.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "12",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "openssl",
|
||||
Version: "3.0.15-1~deb12u1.root.io.0",
|
||||
SrcName: "openssl",
|
||||
SrcVersion: "3.0.15-1~deb12u1.root.io.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
PkgName: "openssl",
|
||||
VulnerabilityID: "CVE-2024-13176",
|
||||
InstalledVersion: "3.0.15-1~deb12u1.root.io.0",
|
||||
FixedVersion: "3.0.15-1~deb12u1.root.io.1, 3.0.16-1~deb12u1",
|
||||
DataSource: &dbTypes.DataSource{
|
||||
ID: vulnerability.RootIO,
|
||||
Name: "Root.io Security Patches",
|
||||
URL: "https://api.root.io/external/patch_feed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ubuntu scanner",
|
||||
baseOS: ftypes.Ubuntu,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/rootio.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "20.04",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "nginx",
|
||||
Version: "1.22.1-9+deb12u2.root.io.0",
|
||||
SrcName: "nginx",
|
||||
SrcVersion: "1.22.1-9+deb12u2.root.io.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
PkgName: "nginx",
|
||||
VulnerabilityID: "CVE-2023-44487",
|
||||
InstalledVersion: "1.22.1-9+deb12u2.root.io.0",
|
||||
FixedVersion: "1.22.1-9+deb12u2.root.io.1",
|
||||
DataSource: &dbTypes.DataSource{
|
||||
ID: vulnerability.RootIO,
|
||||
Name: "Root.io Security Patches",
|
||||
URL: "https://api.root.io/external/patch_feed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Alpine scanner",
|
||||
baseOS: ftypes.Alpine,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/rootio.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "3.19.3",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "less",
|
||||
Version: "643-r00072",
|
||||
SrcName: "less",
|
||||
SrcVersion: "643-r00072",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
PkgName: "less",
|
||||
VulnerabilityID: "CVE-2024-32487",
|
||||
InstalledVersion: "643-r00072",
|
||||
FixedVersion: "643-r10072",
|
||||
DataSource: &dbTypes.DataSource{
|
||||
ID: vulnerability.RootIO,
|
||||
Name: "Root.io Security Patches",
|
||||
URL: "https://api.root.io/external/patch_feed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get returns an error",
|
||||
baseOS: ftypes.Alpine,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/invalid.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "3.20",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "jq",
|
||||
Version: "1.5-12",
|
||||
SrcName: "jq",
|
||||
SrcVersion: "1.5-12",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "failed to get Root.io advisories",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_ = dbtest.InitDB(t, tt.fixtures)
|
||||
defer db.Close()
|
||||
|
||||
scanner := rootio.NewScanner(tt.baseOS)
|
||||
got, err := scanner.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
17
pkg/detector/ospkg/rootio/testdata/fixtures/data-source.yaml
vendored
Normal file
17
pkg/detector/ospkg/rootio/testdata/fixtures/data-source.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
- bucket: data-source
|
||||
pairs:
|
||||
- key: root.io debian 12
|
||||
value:
|
||||
ID: "rootio"
|
||||
Name: "Root.io Security Patches"
|
||||
URL: "https://api.root.io/external/patch_feed"
|
||||
- key: root.io ubuntu 20.04
|
||||
value:
|
||||
ID: "rootio"
|
||||
Name: "Root.io Security Patches"
|
||||
URL: "https://api.root.io/external/patch_feed"
|
||||
- key: root.io alpine 3.19
|
||||
value:
|
||||
ID: "rootio"
|
||||
Name: "Root.io Security Patches"
|
||||
URL: "https://api.root.io/external/patch_feed"
|
||||
9
pkg/detector/ospkg/rootio/testdata/fixtures/invalid.yaml
vendored
Normal file
9
pkg/detector/ospkg/rootio/testdata/fixtures/invalid.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
- bucket: root.io alpine 3.20
|
||||
pairs:
|
||||
- bucket: jq
|
||||
pairs:
|
||||
- key: CVE-2020-8177
|
||||
value:
|
||||
FixedVersion:
|
||||
- foo
|
||||
- bar
|
||||
32
pkg/detector/ospkg/rootio/testdata/fixtures/rootio.yaml
vendored
Normal file
32
pkg/detector/ospkg/rootio/testdata/fixtures/rootio.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
- bucket: root.io debian 12
|
||||
pairs:
|
||||
- bucket: openssl
|
||||
pairs:
|
||||
- key: CVE-2024-13176
|
||||
value:
|
||||
VulnerableVersions:
|
||||
- "<3.0.15-1~deb12u1.root.io.1"
|
||||
- ">3.0.15-1~deb12u1.root.io.1 <3.0.16-1~deb12u1"
|
||||
PatchedVersions:
|
||||
- "3.0.15-1~deb12u1.root.io.1"
|
||||
- "3.0.16-1~deb12u1"
|
||||
- bucket: root.io ubuntu 20.04
|
||||
pairs:
|
||||
- bucket: nginx
|
||||
pairs:
|
||||
- key: CVE-2023-44487
|
||||
value:
|
||||
VulnerableVersions:
|
||||
- "<1.22.1-9+deb12u2.root.io.1"
|
||||
PatchedVersions:
|
||||
- "1.22.1-9+deb12u2.root.io.1"
|
||||
- bucket: root.io alpine 3.19
|
||||
pairs:
|
||||
- bucket: less
|
||||
pairs:
|
||||
- key: CVE-2024-32487
|
||||
value:
|
||||
VulnerableVersions:
|
||||
- "<643-r10072"
|
||||
PatchedVersions:
|
||||
- "643-r10072"
|
||||
65
pkg/detector/ospkg/version/compare.go
Normal file
65
pkg/detector/ospkg/version/compare.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
apkver "github.com/knqyf263/go-apk-version"
|
||||
debver "github.com/knqyf263/go-deb-version"
|
||||
)
|
||||
|
||||
// Comparer defines the interface for version comparison
|
||||
type Comparer interface {
|
||||
Compare(version1, version2 string) (int, error)
|
||||
}
|
||||
|
||||
// DEBComparer implements Comparer for Debian/Ubuntu packages
|
||||
type DEBComparer struct{}
|
||||
|
||||
// NewDEBComparer creates a new DEB version comparer
|
||||
func NewDEBComparer() *DEBComparer {
|
||||
return &DEBComparer{}
|
||||
}
|
||||
|
||||
// Compare compares two Debian package versions
|
||||
// Returns:
|
||||
// - positive if version1 > version2
|
||||
// - negative if version1 < version2
|
||||
// - zero if version1 == version2
|
||||
func (c *DEBComparer) Compare(version1, version2 string) (int, error) {
|
||||
v1, err := debver.NewVersion(version1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
v2, err := debver.NewVersion(version2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return v1.Compare(v2), nil
|
||||
}
|
||||
|
||||
// APKComparer implements Comparer for Alpine packages
|
||||
type APKComparer struct{}
|
||||
|
||||
// NewAPKComparer creates a new APK version comparer
|
||||
func NewAPKComparer() *APKComparer {
|
||||
return &APKComparer{}
|
||||
}
|
||||
|
||||
// Compare compares two Alpine package versions
|
||||
// Returns:
|
||||
// - positive if version1 > version2
|
||||
// - negative if version1 < version2
|
||||
// - zero if version1 == version2
|
||||
func (c *APKComparer) Compare(version1, version2 string) (int, error) {
|
||||
v1, err := apkver.NewVersion(version1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
v2, err := apkver.NewVersion(version2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return v1.Compare(v2), nil
|
||||
}
|
||||
152
pkg/detector/ospkg/version/compare_test.go
Normal file
152
pkg/detector/ospkg/version/compare_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/version"
|
||||
)
|
||||
|
||||
func TestDEBComparer_Compare(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version1 string
|
||||
version2 string
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "equal versions",
|
||||
version1: "1.2.3",
|
||||
version2: "1.2.3",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "version1 greater",
|
||||
version1: "1.2.4",
|
||||
version2: "1.2.3",
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "version1 less",
|
||||
version1: "1.2.2",
|
||||
version2: "1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with debian revision - equal base, different revision",
|
||||
version1: "1.2.3-1",
|
||||
version2: "1.2.3-2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with epoch - different epoch",
|
||||
version1: "1:1.2.3",
|
||||
version2: "2:1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with epoch and revision",
|
||||
version1: "1:1.2.3-1",
|
||||
version2: "1:1.2.3-2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "ubuntu specific version",
|
||||
version1: "1.2.3-1ubuntu1",
|
||||
version2: "1.2.3-1ubuntu2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "invalid version1",
|
||||
version1: "invalid_version",
|
||||
version2: "1.2.3",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid version2",
|
||||
version1: "1.2.3",
|
||||
version2: "invalid_version",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := version.NewDEBComparer()
|
||||
got, err := c.Compare(tt.version1, tt.version2)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPKComparer_Compare(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version1 string
|
||||
version2 string
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "equal versions",
|
||||
version1: "1.2.3",
|
||||
version2: "1.2.3",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "version1 greater",
|
||||
version1: "1.2.4",
|
||||
version2: "1.2.3",
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "version1 less",
|
||||
version1: "1.2.2",
|
||||
version2: "1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with alpine revision - equal base, different revision",
|
||||
version1: "1.2.3-r0",
|
||||
version2: "1.2.3-r1",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "pre-release versions",
|
||||
version1: "1.2.3_pre1",
|
||||
version2: "1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "complex alpine version",
|
||||
version1: "1.2.3-r0",
|
||||
version2: "1.2.3_pre1-r0",
|
||||
want: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := version.NewAPKComparer()
|
||||
got, err := c.Compare(tt.version1, tt.version2)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
195
pkg/detector/ospkg/version/constraint.go
Normal file
195
pkg/detector/ospkg/version/constraint.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// operatorFunc defines the signature for constraint operator functions
|
||||
type operatorFunc func(v, c string, comparer Comparer) (bool, error)
|
||||
|
||||
// constraintOperators maps operator strings to their corresponding functions
|
||||
var constraintOperators = map[string]operatorFunc{
|
||||
"": constraintEqual,
|
||||
"=": constraintEqual,
|
||||
"==": constraintEqual,
|
||||
"!=": constraintNotEqual,
|
||||
">": constraintGreaterThan,
|
||||
"<": constraintLessThan,
|
||||
">=": constraintGreaterThanEqual,
|
||||
"<=": constraintLessThanEqual,
|
||||
}
|
||||
|
||||
// constraintRegex matches constraint patterns like ">=1.2.3", "<2.0.0", "==1.0.0"
|
||||
// Version can contain numbers, dots, hyphens, plus signs, tildes, colons, and alphanumeric characters
|
||||
var constraintRegex = regexp.MustCompile(`^(>=|<=|>|<|==|!=|=)?\s*([0-9]+[0-9a-zA-Z.\-+~:_]*)$`)
|
||||
|
||||
// constraint represents a single version constraint
|
||||
type constraint struct {
|
||||
version string
|
||||
operator operatorFunc
|
||||
original string
|
||||
}
|
||||
|
||||
// Constraints represents a collection of constraints with a comparer
|
||||
type Constraints struct {
|
||||
constraints []*constraint
|
||||
comparer Comparer
|
||||
}
|
||||
|
||||
// NewConstraints creates a new Constraints from a constraint string and comparer
|
||||
// Multiple constraints can be separated by commas or spaces
|
||||
func NewConstraints(constraints string, comparer Comparer) (*Constraints, error) {
|
||||
if constraints == "" {
|
||||
return nil, xerrors.New("constraints string is empty")
|
||||
}
|
||||
|
||||
var cs []*constraint
|
||||
constraintList := splitConstraints(constraints)
|
||||
for _, constraintStr := range constraintList {
|
||||
constraintStr = strings.TrimSpace(constraintStr)
|
||||
if constraintStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := newConstraint(constraintStr)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("invalid constraint '%s': %w", constraintStr, err)
|
||||
}
|
||||
|
||||
cs = append(cs, c)
|
||||
}
|
||||
|
||||
return &Constraints{
|
||||
constraints: cs,
|
||||
comparer: comparer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// splitConstraints splits constraint string by comma or space, preferring comma
|
||||
func splitConstraints(constraints string) []string {
|
||||
// If contains comma, split by comma
|
||||
if strings.Contains(constraints, ",") {
|
||||
return strings.Split(constraints, ",")
|
||||
}
|
||||
// Otherwise, split by spaces
|
||||
return strings.Fields(constraints)
|
||||
}
|
||||
|
||||
// newConstraint creates a new constraint from a constraint string
|
||||
func newConstraint(constraintStr string) (*constraint, error) {
|
||||
constraintStr = strings.TrimSpace(constraintStr)
|
||||
matches := constraintRegex.FindStringSubmatch(constraintStr)
|
||||
if len(matches) != 3 {
|
||||
return nil, xerrors.Errorf("invalid constraint format: %s", constraintStr)
|
||||
}
|
||||
|
||||
op := matches[1]
|
||||
version := strings.TrimSpace(matches[2])
|
||||
|
||||
operator, ok := constraintOperators[op]
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("unsupported operator: %s", op)
|
||||
}
|
||||
|
||||
return &constraint{
|
||||
version: version,
|
||||
operator: operator,
|
||||
original: constraintStr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check returns true if the given version satisfies the constraint
|
||||
func (c *constraint) check(version string, comparer Comparer) (bool, error) {
|
||||
return c.operator(version, c.version, comparer)
|
||||
}
|
||||
|
||||
// String returns the original constraint string
|
||||
func (c *constraint) String() string {
|
||||
return c.original
|
||||
}
|
||||
|
||||
// Check returns true if the given version satisfies any of the constraints
|
||||
// Multiple constraints are combined with AND logic
|
||||
func (cs *Constraints) Check(version string) (bool, error) {
|
||||
if version == "" {
|
||||
return false, xerrors.New("version is empty")
|
||||
}
|
||||
|
||||
if len(cs.constraints) == 0 {
|
||||
return false, xerrors.New("no constraints specified")
|
||||
}
|
||||
|
||||
for _, c := range cs.constraints {
|
||||
satisfied, err := c.check(version, cs.comparer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !satisfied {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// String returns the string representation of constraints
|
||||
func (cs *Constraints) String() string {
|
||||
var strs []string
|
||||
for _, c := range cs.constraints {
|
||||
strs = append(strs, c.String())
|
||||
}
|
||||
return strings.Join(strs, ", ")
|
||||
}
|
||||
|
||||
// Constraint operator functions
|
||||
|
||||
func constraintEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result == 0, nil
|
||||
}
|
||||
|
||||
func constraintNotEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result != 0, nil
|
||||
}
|
||||
|
||||
func constraintGreaterThan(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result > 0, nil
|
||||
}
|
||||
|
||||
func constraintLessThan(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result < 0, nil
|
||||
}
|
||||
|
||||
func constraintGreaterThanEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result >= 0, nil
|
||||
}
|
||||
|
||||
func constraintLessThanEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result <= 0, nil
|
||||
}
|
||||
320
pkg/detector/ospkg/version/constraint_test.go
Normal file
320
pkg/detector/ospkg/version/constraint_test.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/version"
|
||||
)
|
||||
|
||||
func TestNewConstraints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty constraint returns error",
|
||||
constraints: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "single constraint with operator",
|
||||
constraints: ">=1.2.3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single constraint without operator",
|
||||
constraints: "1.2.3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints with comma",
|
||||
constraints: ">=1.2.3, <2.0.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints with space",
|
||||
constraints: ">=1.2.3 <2.0.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed operators",
|
||||
constraints: ">1.0.0, <=2.0.0, ==1.5.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid constraint format",
|
||||
constraints: ">>>1.2.3",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "constraints with extra spaces",
|
||||
constraints: " >=1.2.3 , <2.0.0 ",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not equal operator",
|
||||
constraints: "!=1.2.3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "equal operator variations",
|
||||
constraints: "=1.2.3, ==1.2.4",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := version.NewConstraints(tt.constraints, version.NewDEBComparer())
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraints_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
version string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty version returns error",
|
||||
constraints: ">=1.2.3",
|
||||
version: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "equal constraint satisfied",
|
||||
constraints: "1.2.3",
|
||||
version: "1.2.3",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal constraint not satisfied",
|
||||
constraints: "1.2.3",
|
||||
version: "1.2.4",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "greater than satisfied",
|
||||
constraints: ">1.2.3",
|
||||
version: "1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "greater than not satisfied",
|
||||
constraints: ">1.2.3",
|
||||
version: "1.2.3",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "greater than not satisfied (lower)",
|
||||
constraints: ">1.2.3",
|
||||
version: "1.2.2",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "greater than or equal satisfied (equal)",
|
||||
constraints: ">=1.2.3",
|
||||
version: "1.2.3",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "greater than or equal satisfied (greater)",
|
||||
constraints: ">=1.2.3",
|
||||
version: "1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "less than satisfied",
|
||||
constraints: "<2.0.0",
|
||||
version: "1.9.9",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "less than not satisfied",
|
||||
constraints: "<2.0.0",
|
||||
version: "2.0.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "less than or equal satisfied (equal)",
|
||||
constraints: "<=2.0.0",
|
||||
version: "2.0.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "less than or equal satisfied (less)",
|
||||
constraints: "<=2.0.0",
|
||||
version: "1.9.9",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal satisfied",
|
||||
constraints: "!=1.2.3",
|
||||
version: "1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal not satisfied",
|
||||
constraints: "!=1.2.3",
|
||||
version: "1.2.3",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints AND logic (before first constraint)",
|
||||
constraints: ">=1.0.0, <2.0.0",
|
||||
version: "0.9.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints AND logic (after second constraint)",
|
||||
constraints: ">=1.0.0, <2.0.0",
|
||||
version: "2.1.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints AND logic (satisfied)",
|
||||
constraints: ">=1.0.0, <2.0.0",
|
||||
version: "1.5.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "range constraint (satisfied)",
|
||||
constraints: ">=1.2.3, <2.0.0",
|
||||
version: "1.5.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints with space separator",
|
||||
constraints: ">=1.2.3 <2.0.0",
|
||||
version: "1.5.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "debian version with revision",
|
||||
constraints: ">=1.2.3-1",
|
||||
version: "1.2.3-2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "debian version with epoch",
|
||||
constraints: ">=1:1.2.3",
|
||||
version: "1:1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "debian version with epoch and revision",
|
||||
constraints: ">=1:1.2.3-1",
|
||||
version: "1:1.2.3-2",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
comparer := version.NewDEBComparer()
|
||||
constraints, err := version.NewConstraints(tt.constraints, comparer)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := constraints.Check(tt.version)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraints_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "single constraint",
|
||||
constraints: ">=1.2.3",
|
||||
want: ">=1.2.3",
|
||||
},
|
||||
{
|
||||
name: "multiple constraints",
|
||||
constraints: ">=1.2.3, <2.0.0",
|
||||
want: ">=1.2.3, <2.0.0",
|
||||
},
|
||||
{
|
||||
name: "constraints with extra spaces",
|
||||
constraints: " >=1.2.3 , <2.0.0 ",
|
||||
want: ">=1.2.3, <2.0.0",
|
||||
},
|
||||
{
|
||||
name: "space separated constraints",
|
||||
constraints: ">=1.2.3 <2.0.0",
|
||||
want: ">=1.2.3, <2.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
comparer := version.NewDEBComparer()
|
||||
constraints, err := version.NewConstraints(tt.constraints, comparer)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := constraints.String()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraints_CheckWithAPKComparer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
version string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "alpine version comparison",
|
||||
constraints: ">=1.2.3-r0",
|
||||
version: "1.2.3-r1",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "alpine version with pre-release",
|
||||
constraints: "<1.2.3_pre1",
|
||||
version: "1.2.2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "alpine version range",
|
||||
constraints: ">=1.2.3-r0, <2.0.0",
|
||||
version: "1.5.0-r2",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
comparer := version.NewAPKComparer()
|
||||
constraints, err := version.NewConstraints(tt.constraints, comparer)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := constraints.Check(tt.version)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/remote"
|
||||
@@ -20,6 +21,11 @@ func tryRemote(ctx context.Context, imageName string, ref name.Reference, option
|
||||
if err != nil {
|
||||
return nil, cleanup, err
|
||||
}
|
||||
// ArtifactType being non-empty indicates this is not a regular container image
|
||||
// (e.g., Helm charts, WASM modules, or other OCI artifacts)
|
||||
if desc.ArtifactType != "" {
|
||||
return nil, cleanup, xerrors.Errorf("unsupported artifact type %q for image %q", desc.ArtifactType, imageName)
|
||||
}
|
||||
img, err := desc.Image()
|
||||
if err != nil {
|
||||
return nil, cleanup, err
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/random"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
func Test_implicitReference_TagName(t *testing.T) {
|
||||
@@ -82,3 +92,100 @@ func Test_implicitReference_RepositoryName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_tryRemote(t *testing.T) {
|
||||
// Create a test image
|
||||
img, err := random.Image(1024, 5)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the image digest for test expectations
|
||||
digest, err := img.Digest()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set up registry server with null logger to suppress log output
|
||||
nullLogger := log.New(io.Discard, "", 0)
|
||||
s := httptest.NewServer(registry.New(registry.Logger(nullLogger)))
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
u, err := url.Parse(s.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
imageName string
|
||||
setupImage func(t *testing.T, ref name.Reference)
|
||||
wantName string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "successful image retrieval",
|
||||
imageName: "test/alpine:3.10",
|
||||
setupImage: func(t *testing.T, ref name.Reference) {
|
||||
err := remote.Write(ref, img)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
wantName: "/test/alpine:3.10",
|
||||
},
|
||||
{
|
||||
name: "helm chart config media type",
|
||||
imageName: "test/helm:chart",
|
||||
setupImage: func(t *testing.T, ref name.Reference) {
|
||||
configFile, err := img.ConfigFile()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new config with helm chart media type
|
||||
imageToWrite, err := mutate.Config(img, configFile.Config)
|
||||
require.NoError(t, err)
|
||||
|
||||
imageToWrite = mutate.ConfigMediaType(imageToWrite, "application/vnd.cncf.helm.chart")
|
||||
|
||||
err = remote.Write(ref, imageToWrite)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
wantErr: "unsupported artifact type",
|
||||
},
|
||||
{
|
||||
name: "image not found",
|
||||
imageName: "test/notfound:latest",
|
||||
wantErr: "NAME_UNKNOWN",
|
||||
setupImage: func(*testing.T, name.Reference) {},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Parse the image name with the test server address
|
||||
fullImageName := u.Host + "/" + tt.imageName
|
||||
ref, err := name.ParseReference(fullImageName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set up the image in registry if needed
|
||||
tt.setupImage(t, ref)
|
||||
|
||||
ctx := t.Context()
|
||||
got, cleanup, err := tryRemote(ctx, fullImageName, ref, types.ImageOptions{
|
||||
RegistryOptions: types.RegistryOptions{
|
||||
Insecure: true,
|
||||
},
|
||||
})
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, got)
|
||||
assert.Contains(t, got.Name(), tt.wantName)
|
||||
|
||||
// Verify RepoTags and RepoDigests contain expected values
|
||||
repoTags := got.RepoTags()
|
||||
repoDigests := got.RepoDigests()
|
||||
assert.Len(t, repoTags, 1)
|
||||
assert.Contains(t, repoTags[0], tt.imageName)
|
||||
assert.Len(t, repoDigests, 1)
|
||||
assert.Contains(t, repoDigests[0], digest.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,16 @@ var (
|
||||
Usage: "AWS Endpoint override",
|
||||
}
|
||||
awsServiceFlag = Flag[[]string]{
|
||||
Name: "service",
|
||||
ConfigName: "cloud.aws.service",
|
||||
Usage: "Only scan AWS Service(s) specified with this flag. Can specify multiple services using --service A --service B etc.",
|
||||
Name: "service",
|
||||
ConfigName: "cloud.aws.service",
|
||||
Usage: "Only scan AWS Service(s) specified with this flag. Can specify multiple services using --service A --service B etc.",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
awsSkipServicesFlag = Flag[[]string]{
|
||||
Name: "skip-service",
|
||||
ConfigName: "cloud.aws.skip-service",
|
||||
Usage: "Skip selected AWS Service(s) specified with this flag. Can specify multiple services using --skip-service A --skip-service B etc.",
|
||||
Name: "skip-service",
|
||||
ConfigName: "cloud.aws.skip-service",
|
||||
Usage: "Skip selected AWS Service(s) specified with this flag. Can specify multiple services using --skip-service A --skip-service B etc.",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
awsAccountFlag = Flag[string]{
|
||||
Name: "account",
|
||||
|
||||
@@ -27,31 +27,35 @@ var (
|
||||
Persistent: true,
|
||||
}
|
||||
QuietFlag = Flag[bool]{
|
||||
Name: "quiet",
|
||||
ConfigName: "quiet",
|
||||
Shorthand: "q",
|
||||
Usage: "suppress progress bar and log output",
|
||||
Persistent: true,
|
||||
Name: "quiet",
|
||||
ConfigName: "quiet",
|
||||
Shorthand: "q",
|
||||
Usage: "suppress progress bar and log output",
|
||||
Persistent: true,
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
DebugFlag = Flag[bool]{
|
||||
Name: "debug",
|
||||
ConfigName: "debug",
|
||||
Shorthand: "d",
|
||||
Usage: "debug mode",
|
||||
Persistent: true,
|
||||
Name: "debug",
|
||||
ConfigName: "debug",
|
||||
Shorthand: "d",
|
||||
Usage: "debug mode",
|
||||
Persistent: true,
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
InsecureFlag = Flag[bool]{
|
||||
Name: "insecure",
|
||||
ConfigName: "insecure",
|
||||
Usage: "allow insecure server connections",
|
||||
Persistent: true,
|
||||
Name: "insecure",
|
||||
ConfigName: "insecure",
|
||||
Usage: "allow insecure server connections",
|
||||
Persistent: true,
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
TimeoutFlag = Flag[time.Duration]{
|
||||
Name: "timeout",
|
||||
ConfigName: "timeout",
|
||||
Default: time.Second * 300, // 5 mins
|
||||
Usage: "timeout",
|
||||
Persistent: true,
|
||||
Name: "timeout",
|
||||
ConfigName: "timeout",
|
||||
Default: time.Second * 300, // 5 mins
|
||||
Usage: "timeout",
|
||||
Persistent: true,
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
CacheDirFlag = Flag[string]{
|
||||
Name: "cache-dir",
|
||||
|
||||
@@ -102,7 +102,8 @@ var (
|
||||
Default: xstrings.ToStringSlice(
|
||||
lo.Without(analyzer.TypeConfigFiles, analyzer.TypeYAML, analyzer.TypeJSON),
|
||||
),
|
||||
Usage: "comma-separated list of misconfig scanners to use for misconfiguration scanning",
|
||||
Usage: "comma-separated list of misconfig scanners to use for misconfiguration scanning",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
ConfigFileSchemasFlag = Flag[[]string]{
|
||||
Name: "config-file-schemas",
|
||||
|
||||
@@ -76,6 +76,9 @@ type Flag[T FlagType] struct {
|
||||
// Aliases represents aliases
|
||||
Aliases []Alias
|
||||
|
||||
// TelemetrySafe indicates if the flag value is safe to be included in telemetry.
|
||||
TelemetrySafe bool
|
||||
|
||||
// value is the value passed through CLI flag, env, or config file.
|
||||
// It is populated after flag.Parse() is called.
|
||||
value T
|
||||
@@ -218,6 +221,17 @@ func (f *Flag[T]) GetAliases() []Alias {
|
||||
return f.Aliases
|
||||
}
|
||||
|
||||
func (f *Flag[T]) IsTelemetrySafe() bool {
|
||||
return f.TelemetrySafe
|
||||
}
|
||||
|
||||
func (f *Flag[T]) IsSet() bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
return f.isSet()
|
||||
}
|
||||
|
||||
func (f *Flag[T]) Hidden() bool {
|
||||
return f.Deprecated != "" || f.Removed != "" || f.Internal
|
||||
}
|
||||
@@ -349,6 +363,8 @@ type Flagger interface {
|
||||
GetDefaultValue() any
|
||||
GetAliases() []Alias
|
||||
Hidden() bool
|
||||
IsTelemetrySafe() bool
|
||||
IsSet() bool
|
||||
|
||||
Parse() error
|
||||
Add(cmd *cobra.Command)
|
||||
@@ -391,6 +407,9 @@ type Options struct {
|
||||
|
||||
// args is the arguments passed to the command.
|
||||
args []string
|
||||
|
||||
// usedFlags allows us to get the underlying flags for the options
|
||||
usedFlags []Flagger
|
||||
}
|
||||
|
||||
// Align takes consistency of options
|
||||
@@ -555,6 +574,11 @@ func (o *Options) OutputWriter(ctx context.Context) (io.Writer, func() error, er
|
||||
return f, f.Close, nil
|
||||
}
|
||||
|
||||
// GetUsedFlags returns the explicitly set flags for the options.
|
||||
func (o *Options) GetUsedFlags() []Flagger {
|
||||
return o.usedFlags
|
||||
}
|
||||
|
||||
func (o *Options) outputPluginWriter(ctx context.Context) (io.Writer, func() error, error) {
|
||||
pluginName := strings.TrimPrefix(o.Output, "plugin=")
|
||||
|
||||
@@ -651,6 +675,8 @@ func (f *Flags) ToOptions(args []string) (Options, error) {
|
||||
return Options{}, xerrors.Errorf("unable to parse flags: %w", err)
|
||||
}
|
||||
|
||||
opts.usedFlags = append(opts.usedFlags, usedFlags(group)...)
|
||||
|
||||
if err := group.ToOptions(&opts); err != nil {
|
||||
return Options{}, xerrors.Errorf("unable to convert flags to options: %w", err)
|
||||
}
|
||||
@@ -751,3 +777,21 @@ func findFlagGroup[T FlagGroup](f *Flags) (T, bool) {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// usedFlags returns a slice of flags that are set in the given FlagGroup.
|
||||
func usedFlags(fg FlagGroup) []Flagger {
|
||||
if fg == nil || fg.Flags() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var flags []Flagger
|
||||
for _, flag := range fg.Flags() {
|
||||
if flag == nil {
|
||||
continue
|
||||
}
|
||||
if flag.IsSet() {
|
||||
flags = append(flags, flag)
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
|
||||
var (
|
||||
IncludeDevDepsFlag = Flag[bool]{
|
||||
Name: "include-dev-deps",
|
||||
ConfigName: "pkg.include-dev-deps",
|
||||
Usage: "include development dependencies in the report (supported: npm, yarn, gradle)",
|
||||
Name: "include-dev-deps",
|
||||
ConfigName: "pkg.include-dev-deps",
|
||||
Usage: "include development dependencies in the report (supported: npm, yarn, gradle)",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
PkgTypesFlag = Flag[[]string]{
|
||||
Name: "pkg-types",
|
||||
@@ -25,13 +26,15 @@ var (
|
||||
Deprecated: true, // --vuln-type was renamed to --pkg-types
|
||||
},
|
||||
},
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
PkgRelationshipsFlag = Flag[[]string]{
|
||||
Name: "pkg-relationships",
|
||||
ConfigName: "pkg.relationships",
|
||||
Default: xstrings.ToStringSlice(ftypes.Relationships),
|
||||
Values: xstrings.ToStringSlice(ftypes.Relationships),
|
||||
Usage: "list of package relationships",
|
||||
Name: "pkg-relationships",
|
||||
ConfigName: "pkg.relationships",
|
||||
Default: xstrings.ToStringSlice(ftypes.Relationships),
|
||||
Values: xstrings.ToStringSlice(ftypes.Relationships),
|
||||
Usage: "list of package relationships",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,12 +26,13 @@ import (
|
||||
// severity: HIGH,CRITICAL
|
||||
var (
|
||||
FormatFlag = Flag[string]{
|
||||
Name: "format",
|
||||
ConfigName: "format",
|
||||
Shorthand: "f",
|
||||
Default: string(types.FormatTable),
|
||||
Values: xstrings.ToStringSlice(types.SupportedFormats),
|
||||
Usage: "format",
|
||||
Name: "format",
|
||||
ConfigName: "format",
|
||||
Shorthand: "f",
|
||||
Default: string(types.FormatTable),
|
||||
Values: xstrings.ToStringSlice(types.SupportedFormats),
|
||||
Usage: "format",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
ReportFormatFlag = Flag[string]{
|
||||
Name: "report",
|
||||
@@ -41,7 +42,8 @@ var (
|
||||
"all",
|
||||
"summary",
|
||||
},
|
||||
Usage: "specify a report format for the output",
|
||||
Usage: "specify a report format for the output",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
TemplateFlag = Flag[string]{
|
||||
Name: "template",
|
||||
@@ -55,9 +57,10 @@ var (
|
||||
Usage: "[EXPERIMENTAL] show dependency origin tree of vulnerable packages",
|
||||
}
|
||||
ListAllPkgsFlag = Flag[bool]{
|
||||
Name: "list-all-pkgs",
|
||||
ConfigName: "list-all-pkgs",
|
||||
Usage: "output all packages in the JSON report regardless of vulnerability",
|
||||
Name: "list-all-pkgs",
|
||||
ConfigName: "list-all-pkgs",
|
||||
Usage: "output all packages in the JSON report regardless of vulnerability",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
IgnoreFileFlag = Flag[string]{
|
||||
Name: "ignorefile",
|
||||
@@ -92,12 +95,13 @@ var (
|
||||
Usage: "[EXPERIMENTAL] output plugin arguments",
|
||||
}
|
||||
SeverityFlag = Flag[[]string]{
|
||||
Name: "severity",
|
||||
ConfigName: "severity",
|
||||
Shorthand: "s",
|
||||
Default: dbTypes.SeverityNames,
|
||||
Values: dbTypes.SeverityNames,
|
||||
Usage: "severities of security issues to be displayed",
|
||||
Name: "severity",
|
||||
ConfigName: "severity",
|
||||
Shorthand: "s",
|
||||
Default: dbTypes.SeverityNames,
|
||||
Values: dbTypes.SeverityNames,
|
||||
Usage: "severities of security issues to be displayed",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
ComplianceFlag = Flag[string]{
|
||||
Name: "compliance",
|
||||
@@ -105,9 +109,10 @@ var (
|
||||
Usage: "compliance report to generate",
|
||||
}
|
||||
ShowSuppressedFlag = Flag[bool]{
|
||||
Name: "show-suppressed",
|
||||
ConfigName: "scan.show-suppressed",
|
||||
Usage: "[EXPERIMENTAL] show suppressed vulnerabilities",
|
||||
Name: "show-suppressed",
|
||||
ConfigName: "scan.show-suppressed",
|
||||
Usage: "[EXPERIMENTAL] show suppressed vulnerabilities",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
TableModeFlag = Flag[[]string]{
|
||||
Name: "table-mode",
|
||||
|
||||
@@ -65,7 +65,8 @@ var (
|
||||
Deprecated: true, // --security-checks was renamed to --scanners
|
||||
},
|
||||
},
|
||||
Usage: "comma-separated list of what security issues to detect",
|
||||
Usage: "comma-separated list of what security issues to detect",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
FilePatternsFlag = Flag[[]string]{
|
||||
Name: "file-patterns",
|
||||
@@ -112,6 +113,7 @@ var (
|
||||
- "precise": Prioritizes precise by minimizing false positives.
|
||||
- "comprehensive": Aims to detect more security findings at the cost of potential false positives.
|
||||
`,
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
DistroFlag = Flag[string]{
|
||||
Name: "distro",
|
||||
|
||||
@@ -17,10 +17,11 @@ var (
|
||||
Usage: "display only fixed vulnerabilities",
|
||||
}
|
||||
IgnoreStatusFlag = Flag[[]string]{
|
||||
Name: "ignore-status",
|
||||
ConfigName: "vulnerability.ignore-status",
|
||||
Values: dbTypes.Statuses,
|
||||
Usage: "comma-separated list of vulnerability status to ignore",
|
||||
Name: "ignore-status",
|
||||
ConfigName: "vulnerability.ignore-status",
|
||||
Values: dbTypes.Statuses,
|
||||
Usage: "comma-separated list of vulnerability status to ignore",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
VEXFlag = Flag[[]string]{
|
||||
Name: "vex",
|
||||
@@ -38,8 +39,9 @@ var (
|
||||
Default: []string{
|
||||
"auto",
|
||||
},
|
||||
Values: append(xstrings.ToStringSlice(vulnerability.AllSourceIDs), "auto"),
|
||||
Usage: "order of data sources for selecting vulnerability severity level",
|
||||
Values: append(xstrings.ToStringSlice(vulnerability.AllSourceIDs), "auto"),
|
||||
Usage: "order of data sources for selecting vulnerability severity level",
|
||||
TelemetrySafe: true,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package iam
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
|
||||
"github.com/aquasecurity/iamgo"
|
||||
"github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam"
|
||||
"github.com/aquasecurity/trivy/pkg/iac/scan"
|
||||
@@ -15,6 +17,13 @@ type wrappedDocument struct {
|
||||
}
|
||||
|
||||
func ParsePolicyFromAttr(attr *terraform.Attribute, owner *terraform.Block, modules terraform.Modules) (*iam.Document, error) {
|
||||
attr.RewriteExpr(func(e hclsyntax.Expression) hclsyntax.Expression {
|
||||
if te, ok := e.(*hclsyntax.TemplateExpr); ok {
|
||||
return &terraform.PartialTemplateExpr{TemplateExpr: te}
|
||||
}
|
||||
return e
|
||||
})
|
||||
|
||||
if !attr.IsString() {
|
||||
return &iam.Document{
|
||||
Metadata: owner.GetMetadata(),
|
||||
|
||||
@@ -402,6 +402,73 @@ data "aws_iam_policy_document" "policy" {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "policy is template with unknown part",
|
||||
terraform: `variable "action" {
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "aws_iam_policy" "test" {
|
||||
name = "test"
|
||||
|
||||
policy = <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"${var.action}",
|
||||
"s3:get*",
|
||||
"s3:list*"
|
||||
],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"kinesis:DescribeStream",
|
||||
"kinesis:GetRecords"
|
||||
],
|
||||
"Resource": [
|
||||
"${aws_kinesis_stream.stepfunction_ecs_kinesis_stream.arn}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
}`,
|
||||
expected: []iam.Policy{
|
||||
{
|
||||
Name: iacTypes.StringTest("test"),
|
||||
Document: func() iam.Document {
|
||||
builder := iamgo.NewPolicyBuilder().
|
||||
WithStatement(
|
||||
iamgo.NewStatementBuilder().
|
||||
WithActions([]string{"__UNRESOLVED__", "s3:get*", "s3:list*"}).
|
||||
WithResources([]string{"*"}).
|
||||
WithEffect("Allow").
|
||||
Build(),
|
||||
).
|
||||
WithStatement(
|
||||
iamgo.NewStatementBuilder().
|
||||
WithActions([]string{"kinesis:DescribeStream", "kinesis:GetRecords"}).
|
||||
WithResources([]string{"__UNRESOLVED__"}).
|
||||
WithEffect("Allow").
|
||||
Build(),
|
||||
).
|
||||
WithVersion("2012-10-17")
|
||||
|
||||
return iam.Document{
|
||||
Parsed: builder.Build(),
|
||||
Metadata: iacTypes.NewTestMetadata(),
|
||||
IsOffset: false,
|
||||
HasRefs: true,
|
||||
}
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
85
pkg/iac/rego/filter.go
Normal file
85
pkg/iac/rego/filter.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package rego
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
|
||||
"github.com/aquasecurity/go-version/pkg/semver"
|
||||
"github.com/aquasecurity/trivy/pkg/iac/framework"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
)
|
||||
|
||||
type RegoModuleFilter func(module *ast.Module, metadata *StaticMetadata) bool
|
||||
|
||||
// TrivyVersionFilter returns a filter that allows only those modules,
|
||||
// that are compatible with the given version of Trivy.
|
||||
func TrivyVersionFilter(trivyVer string) RegoModuleFilter {
|
||||
if trivyVer == "dev" {
|
||||
return func(_ *ast.Module, _ *StaticMetadata) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
tv, tverr := semver.Parse(trivyVer)
|
||||
if tverr != nil {
|
||||
log.Warn(
|
||||
"Failed to parse Trivy version - cannot confirm if all modules will work with current version",
|
||||
log.Prefix("rego"),
|
||||
log.String("trivy_version", trivyVer),
|
||||
log.Err(tverr),
|
||||
)
|
||||
return func(_ *ast.Module, _ *StaticMetadata) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return func(module *ast.Module, metadata *StaticMetadata) bool {
|
||||
return isMinimumVersionSupported(metadata, module, tv)
|
||||
}
|
||||
}
|
||||
|
||||
func isMinimumVersionSupported(metadata *StaticMetadata, module *ast.Module, tv semver.Version) bool {
|
||||
// to ensure compatibility with old modules without minimum trivy version
|
||||
if metadata.MinimumTrivyVersion == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
mmsv, err := semver.Parse(metadata.MinimumTrivyVersion)
|
||||
if err != nil {
|
||||
log.Warn(
|
||||
"Failed to parse minimum trivy version - skipping as cannot confirm if module will work with current version",
|
||||
log.Prefix("rego"),
|
||||
log.FilePath(module.Package.Location.File),
|
||||
log.Err(err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if tv.LessThan(mmsv) {
|
||||
log.Warn(
|
||||
"Module will be skipped as current version of Trivy is older than minimum trivy version required - please update Trivy to use this module",
|
||||
log.Prefix("rego"),
|
||||
log.FilePath(module.Package.Location.File),
|
||||
log.String("minimum_trivy_version", metadata.MinimumTrivyVersion),
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FrameworksFilter returns a filter that allows only modules
|
||||
// associated with the specified frameworks.
|
||||
func FrameworksFilter(frameworks []framework.Framework) RegoModuleFilter {
|
||||
return func(_ *ast.Module, metadata *StaticMetadata) bool {
|
||||
return metadata.matchAnyFramework(frameworks)
|
||||
}
|
||||
}
|
||||
|
||||
// IncludeDeprecatedFilter returns a filter that allows deprecated modules
|
||||
// if the include flag is true.
|
||||
func IncludeDeprecatedFilter(include bool) RegoModuleFilter {
|
||||
return func(_ *ast.Module, metadata *StaticMetadata) bool {
|
||||
if metadata.Deprecated && !include {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
137
pkg/iac/rego/filter_test.go
Normal file
137
pkg/iac/rego/filter_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package rego_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/iac/framework"
|
||||
"github.com/aquasecurity/trivy/pkg/iac/rego"
|
||||
)
|
||||
|
||||
func TestTrivyVersionFilter(t *testing.T) {
|
||||
module := &ast.Module{Package: &ast.Package{Location: ast.NewLocation(nil, "/test.rego", 1, 1)}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
trivyVer string
|
||||
minVersion string
|
||||
expected bool
|
||||
}{
|
||||
{"no minimum version", "0.1.0", "", true},
|
||||
{"compatible version", "0.20.0", "0.19.0", true},
|
||||
{"incompatible version", "0.18.0", "0.19.0", false},
|
||||
{"invalid min version", "0.20.0", "invalid", false},
|
||||
{"invalid trivy version", "invalid", "0.19.0", true},
|
||||
{"empty trivy version", "", "0.19.0", true},
|
||||
{"dev trivy version", "dev", "0.19.0", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filter := rego.TrivyVersionFilter(tt.trivyVer)
|
||||
metadata := ®o.StaticMetadata{
|
||||
MinimumTrivyVersion: tt.minVersion,
|
||||
}
|
||||
result := filter(module, metadata)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameworksFilter(t *testing.T) {
|
||||
module := &ast.Module{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
moduleFw map[framework.Framework][]string
|
||||
filterFw []framework.Framework
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "match single",
|
||||
moduleFw: map[framework.Framework][]string{
|
||||
framework.CIS_AWS_1_2: {"2.5"},
|
||||
},
|
||||
filterFw: []framework.Framework{framework.CIS_AWS_1_2},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "match one of many",
|
||||
moduleFw: map[framework.Framework][]string{
|
||||
framework.CIS_AWS_1_2: {"2.5"},
|
||||
framework.CIS_AWS_1_4: {"4.5"},
|
||||
},
|
||||
filterFw: []framework.Framework{framework.CIS_AWS_1_2},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
moduleFw: map[framework.Framework][]string{
|
||||
framework.CIS_AWS_1_2: {"2.5"},
|
||||
},
|
||||
filterFw: []framework.Framework{"PCI"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty filter",
|
||||
moduleFw: map[framework.Framework][]string{
|
||||
framework.CIS_AWS_1_2: {"2.5"},
|
||||
},
|
||||
filterFw: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "has default framework and empty filter",
|
||||
moduleFw: map[framework.Framework][]string{
|
||||
framework.Default: {},
|
||||
framework.CIS_AWS_1_2: {"2.5"},
|
||||
},
|
||||
filterFw: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "empty module frameworks",
|
||||
moduleFw: nil,
|
||||
filterFw: []framework.Framework{framework.Default},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
metadata := ®o.StaticMetadata{
|
||||
Frameworks: tt.moduleFw,
|
||||
}
|
||||
filter := rego.FrameworksFilter(tt.filterFw)
|
||||
result := filter(module, metadata)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeDeprecatedFilter(t *testing.T) {
|
||||
module := &ast.Module{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
include bool
|
||||
deprecated bool
|
||||
expected bool
|
||||
}{
|
||||
{"include false, not deprecated", false, false, true},
|
||||
{"include false, deprecated", false, true, false},
|
||||
{"include true, deprecated", true, true, true},
|
||||
{"include true, not deprecated", true, false, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
metadata := ®o.StaticMetadata{Deprecated: tt.deprecated}
|
||||
filter := rego.IncludeDeprecatedFilter(tt.include)
|
||||
result := filter(module, metadata)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/open-policy-agent/opa/v1/bundle"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/aquasecurity/go-version/pkg/semver"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/set"
|
||||
"github.com/aquasecurity/trivy/pkg/version/doc"
|
||||
@@ -297,32 +296,6 @@ func (s *Scanner) handleModulesMetadata(path string, module *ast.Module) {
|
||||
s.moduleMetadata[path] = metadata
|
||||
}
|
||||
|
||||
func (s *Scanner) IsMinimumVersionSupported(metadata *StaticMetadata, module *ast.Module, tv semver.Version) bool {
|
||||
if metadata.MinimumTrivyVersion == "" { // to ensure compatibility with old modules without minimum trivy version
|
||||
return true
|
||||
}
|
||||
|
||||
mmsv, err := semver.Parse(metadata.MinimumTrivyVersion)
|
||||
if err != nil {
|
||||
s.logger.Warn(
|
||||
"Failed to parse minimum trivy version - skipping as cannot confirm if module will work with current version",
|
||||
log.FilePath(module.Package.Location.File),
|
||||
log.Err(err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if tv.LessThan(mmsv) {
|
||||
s.logger.Warn(
|
||||
"Module will be skipped as current version of Trivy is older than minimum trivy version required - please update Trivy to use this module",
|
||||
log.FilePath(module.Package.Location.File),
|
||||
log.String("minimum_trivy_version", metadata.MinimumTrivyVersion),
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// moduleHasLegacyMetadataFormat checks if the module has a legacy metadata format.
|
||||
// Returns true if the metadata is represented as a “__rego_metadata__” rule,
|
||||
// which was used before annotations were introduced.
|
||||
@@ -344,52 +317,37 @@ func moduleHasLegacyInputFormat(module *ast.Module) bool {
|
||||
// filterModules filters the Rego modules based on metadata.
|
||||
func (s *Scanner) filterModules() error {
|
||||
filtered := make(map[string]*ast.Module)
|
||||
tv, tverr := semver.Parse(s.trivyVersion)
|
||||
if tverr != nil && s.trivyVersion != "dev" {
|
||||
s.logger.Warn(
|
||||
"Failed to parse Trivy version - cannot confirm if all modules will work with current version",
|
||||
log.String("trivy_version", s.trivyVersion),
|
||||
log.Err(tverr),
|
||||
)
|
||||
}
|
||||
|
||||
for name, module := range s.policies {
|
||||
metadata, err := s.metadataForModule(context.Background(), name, module, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieve metadata for module %s: %w", name, err)
|
||||
}
|
||||
|
||||
if metadata != nil {
|
||||
if tverr == nil && !s.IsMinimumVersionSupported(metadata, module, tv) {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.isModuleApplicable(module, metadata, name) {
|
||||
filtered[name] = module
|
||||
}
|
||||
if metadata == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !lo.EveryBy(s.moduleFilters, func(filter RegoModuleFilter) bool {
|
||||
return filter(module, metadata)
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(metadata.InputOptions.Selectors) == 0 && !metadata.Library {
|
||||
s.logger.Warn(
|
||||
"Module has no input selectors - it will be loaded for all inputs",
|
||||
log.FilePath(module.Package.Location.File),
|
||||
log.String("module", name),
|
||||
)
|
||||
}
|
||||
|
||||
filtered[name] = module
|
||||
}
|
||||
|
||||
s.policies = filtered
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) isModuleApplicable(module *ast.Module, metadata *StaticMetadata, name string) bool {
|
||||
if !metadata.hasAnyFramework(s.frameworks) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(metadata.InputOptions.Selectors) == 0 && !metadata.Library {
|
||||
s.logger.Warn(
|
||||
"Module has no input selectors - it will be loaded for all inputs",
|
||||
log.FilePath(module.Package.Location.File),
|
||||
log.String("module", name),
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ParseRegoModule(name, input string) (*ast.Module, error) {
|
||||
return ast.ParseModuleWithOpts(name, input, ast.ParserOptions{
|
||||
ProcessAnnotation: true,
|
||||
|
||||
@@ -179,7 +179,7 @@ func (sm *StaticMetadata) updateAliases(meta map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *StaticMetadata) hasAnyFramework(frameworks []framework.Framework) bool {
|
||||
func (sm *StaticMetadata) matchAnyFramework(frameworks []framework.Framework) bool {
|
||||
if len(frameworks) == 0 {
|
||||
frameworks = []framework.Framework{framework.Default}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func WithFrameworks(frameworks ...framework.Framework) options.ScannerOption {
|
||||
func WithTrivyVersion(version string) options.ScannerOption {
|
||||
return func(s options.ConfigurableScanner) {
|
||||
if ss, ok := s.(*Scanner); ok {
|
||||
ss.trivyVersion = version
|
||||
ss.moduleFilters = append(ss.moduleFilters, TrivyVersionFilter(version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/aquasecurity/trivy/pkg/iac/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/set"
|
||||
"github.com/aquasecurity/trivy/pkg/version/app"
|
||||
)
|
||||
|
||||
var checkTypesWithSubtype = set.New(types.SourceCloud, types.SourceDefsec, types.SourceKubernetes)
|
||||
@@ -62,7 +61,8 @@ type Scanner struct {
|
||||
includeDeprecatedChecks bool
|
||||
includeEmbeddedPolicies bool
|
||||
includeEmbeddedLibraries bool
|
||||
trivyVersion string
|
||||
|
||||
moduleFilters []RegoModuleFilter
|
||||
|
||||
embeddedLibs map[string]*ast.Module
|
||||
embeddedChecks map[string]*ast.Module
|
||||
@@ -98,12 +98,18 @@ func NewScanner(opts ...options.ScannerOption) *Scanner {
|
||||
logger: log.WithPrefix("rego"),
|
||||
customSchemas: make(map[string][]byte),
|
||||
moduleMetadata: make(map[string]*StaticMetadata),
|
||||
trivyVersion: app.Version(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
|
||||
s.moduleFilters = append(
|
||||
s.moduleFilters,
|
||||
FrameworksFilter(s.frameworks),
|
||||
IncludeDeprecatedFilter(s.includeDeprecatedChecks),
|
||||
)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -206,10 +212,6 @@ func (s *Scanner) ScanInput(ctx context.Context, sourceType types.Source, inputs
|
||||
continue
|
||||
}
|
||||
|
||||
if !s.includeDeprecatedChecks && staticMeta.Deprecated {
|
||||
continue // skip deprecated checks
|
||||
}
|
||||
|
||||
// skip if check isn't relevant to what is being scanned
|
||||
if !isPolicyApplicable(sourceType, staticMeta, inputs...) {
|
||||
continue
|
||||
|
||||
@@ -12,10 +12,12 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/iac/terraform/context"
|
||||
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
)
|
||||
|
||||
type Attribute struct {
|
||||
@@ -828,3 +830,113 @@ func safeOp[T any](a *Attribute, fn func(cty.Value) T) T {
|
||||
|
||||
return fn(val)
|
||||
}
|
||||
|
||||
// RewriteExpr applies the given function `transform` to the expression of the attribute,
|
||||
// recursively traversing and transforming it.
|
||||
func (a *Attribute) RewriteExpr(transform func(hclsyntax.Expression) hclsyntax.Expression) {
|
||||
a.hclAttribute.Expr = RewriteExpr(a.hclAttribute.Expr.(hclsyntax.Expression), transform)
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
// RewriteExpr recursively rewrites an HCL expression tree in-place,
|
||||
// applying the provided transformation function `transform` to each node.
|
||||
func RewriteExpr(
|
||||
expr hclsyntax.Expression,
|
||||
transform func(hclsyntax.Expression) hclsyntax.Expression,
|
||||
) hclsyntax.Expression {
|
||||
if expr == nil {
|
||||
return nil
|
||||
}
|
||||
switch e := expr.(type) {
|
||||
case *hclsyntax.LiteralValueExpr:
|
||||
case *hclsyntax.TemplateExpr:
|
||||
for i, p := range e.Parts {
|
||||
e.Parts[i] = RewriteExpr(p, transform)
|
||||
}
|
||||
case *hclsyntax.TemplateWrapExpr:
|
||||
e.Wrapped = RewriteExpr(e.Wrapped, transform)
|
||||
case *hclsyntax.BinaryOpExpr:
|
||||
e.LHS = RewriteExpr(e.LHS, transform)
|
||||
e.RHS = RewriteExpr(e.RHS, transform)
|
||||
case *hclsyntax.UnaryOpExpr:
|
||||
e.Val = RewriteExpr(e.Val, transform)
|
||||
case *hclsyntax.TupleConsExpr:
|
||||
for i, elem := range e.Exprs {
|
||||
e.Exprs[i] = RewriteExpr(elem, transform)
|
||||
}
|
||||
case *hclsyntax.ParenthesesExpr:
|
||||
e.Expression = RewriteExpr(e.Expression, transform)
|
||||
case *hclsyntax.ObjectConsExpr:
|
||||
for i, item := range e.Items {
|
||||
e.Items[i].KeyExpr = RewriteExpr(item.KeyExpr, transform)
|
||||
e.Items[i].ValueExpr = RewriteExpr(item.ValueExpr, transform)
|
||||
}
|
||||
case *hclsyntax.ObjectConsKeyExpr:
|
||||
e.Wrapped = RewriteExpr(e.Wrapped, transform)
|
||||
case *hclsyntax.ScopeTraversalExpr:
|
||||
case *hclsyntax.RelativeTraversalExpr:
|
||||
e.Source = RewriteExpr(e.Source, transform)
|
||||
case *hclsyntax.ConditionalExpr:
|
||||
e.Condition = RewriteExpr(e.Condition, transform)
|
||||
e.TrueResult = RewriteExpr(e.TrueResult, transform)
|
||||
e.FalseResult = RewriteExpr(e.FalseResult, transform)
|
||||
case *hclsyntax.FunctionCallExpr:
|
||||
for i, arg := range e.Args {
|
||||
e.Args[i] = RewriteExpr(arg, transform)
|
||||
}
|
||||
case *hclsyntax.IndexExpr:
|
||||
e.Collection = RewriteExpr(e.Collection, transform)
|
||||
e.Key = RewriteExpr(e.Key, transform)
|
||||
case *hclsyntax.ForExpr:
|
||||
e.CollExpr = RewriteExpr(e.CollExpr, transform)
|
||||
e.KeyExpr = RewriteExpr(e.KeyExpr, transform)
|
||||
e.ValExpr = RewriteExpr(e.ValExpr, transform)
|
||||
e.CondExpr = RewriteExpr(e.CondExpr, transform)
|
||||
case *hclsyntax.SplatExpr:
|
||||
e.Source = RewriteExpr(e.Source, transform)
|
||||
case *hclsyntax.AnonSymbolExpr:
|
||||
default:
|
||||
log.Debug(
|
||||
"RewriteExpr encountered an unhandled expression type",
|
||||
log.Prefix(log.PrefixMisconfiguration),
|
||||
log.String("expr_type", fmt.Sprintf("%T", expr)),
|
||||
)
|
||||
}
|
||||
return transform(expr)
|
||||
}
|
||||
|
||||
// UnknownValuePrefix is a placeholder string used to represent parts of a
|
||||
// template expression that cannot be fully evaluated due to unknown values.
|
||||
const UnknownValuePrefix = "__UNRESOLVED__"
|
||||
|
||||
// PartialTemplateExpr is a wrapper around hclsyntax.TemplateExpr that
|
||||
// replaces unknown or unevaluated parts with placeholder strings during evaluation.
|
||||
type PartialTemplateExpr struct {
|
||||
*hclsyntax.TemplateExpr
|
||||
}
|
||||
|
||||
func (e *PartialTemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||
parts := make([]hclsyntax.Expression, len(e.Parts))
|
||||
for i, part := range e.Parts {
|
||||
partVal, diags := part.Value(ctx)
|
||||
if diags.HasErrors() || partVal.IsNull() || !partVal.IsKnown() {
|
||||
parts[i] = &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal(UnknownValuePrefix),
|
||||
SrcRange: part.Range(),
|
||||
}
|
||||
} else if _, err := convert.Convert(partVal, cty.String); err != nil {
|
||||
parts[i] = &hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal(UnknownValuePrefix),
|
||||
SrcRange: part.Range(),
|
||||
}
|
||||
} else {
|
||||
parts[i] = part
|
||||
}
|
||||
}
|
||||
newTemplate := &hclsyntax.TemplateExpr{
|
||||
Parts: parts,
|
||||
SrcRange: e.SrcRange,
|
||||
}
|
||||
|
||||
return newTemplate.Value(ctx)
|
||||
}
|
||||
|
||||
@@ -8,20 +8,23 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/aquasecurity/go-version/pkg/semver"
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/version/app"
|
||||
xhttp "github.com/aquasecurity/trivy/pkg/x/http"
|
||||
)
|
||||
|
||||
type VersionChecker struct {
|
||||
updatesApi string
|
||||
skipUpdateCheck bool
|
||||
quiet bool
|
||||
telemetryDisabled bool
|
||||
updatesApi string
|
||||
commandName string
|
||||
cliOptions *flag.Options
|
||||
|
||||
done bool
|
||||
responseReceived bool
|
||||
@@ -30,17 +33,15 @@ type VersionChecker struct {
|
||||
}
|
||||
|
||||
// NewVersionChecker creates a new VersionChecker with the default
|
||||
// updates API URL. The URL can be overridden by passing an Option
|
||||
// to the NewVersionChecker function.
|
||||
func NewVersionChecker(opts ...Option) *VersionChecker {
|
||||
// updates API URL.
|
||||
func NewVersionChecker(commandName string, cliOptions *flag.Options) *VersionChecker {
|
||||
v := &VersionChecker{
|
||||
updatesApi: "https://check.trivy.dev/updates",
|
||||
currentVersion: app.Version(),
|
||||
commandName: commandName,
|
||||
cliOptions: cliOptions,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -50,17 +51,17 @@ func NewVersionChecker(opts ...Option) *VersionChecker {
|
||||
// 1. if skipUpdateCheck is true AND telemetryDisabled are both true, skip the request
|
||||
// 2. if skipUpdateCheck is true AND telemetryDisabled is false, run check with metric details but suppress output
|
||||
// 3. if skipUpdateCheck is false AND telemetryDisabled is true, run update check but don't send any metric identifiers
|
||||
func (v *VersionChecker) RunUpdateCheck(ctx context.Context, args []string) {
|
||||
func (v *VersionChecker) RunUpdateCheck(ctx context.Context) {
|
||||
logger := log.WithPrefix("notification")
|
||||
|
||||
if v.skipUpdateCheck && v.telemetryDisabled {
|
||||
if v.cliOptions.SkipVersionCheck && v.cliOptions.DisableTelemetry {
|
||||
logger.Debug("Skipping update check and metric ping")
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Debug("Running version check")
|
||||
args = getFlags(args)
|
||||
commandParts := v.getFlags()
|
||||
client := xhttp.ClientWithContext(ctx, xhttp.WithTimeout(3*time.Second))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, v.updatesApi, http.NoBody)
|
||||
@@ -70,9 +71,10 @@ func (v *VersionChecker) RunUpdateCheck(ctx context.Context, args []string) {
|
||||
}
|
||||
|
||||
// if the user hasn't disabled metrics, send the anonymous information as headers
|
||||
if !v.telemetryDisabled {
|
||||
if !v.cliOptions.DisableTelemetry {
|
||||
req.Header.Set("Trivy-Identifier", uniqueIdentifier())
|
||||
req.Header.Set("Trivy-Command", strings.Join(args, " "))
|
||||
req.Header.Set("Trivy-Command", v.commandName)
|
||||
req.Header.Set("Trivy-Flags", commandParts)
|
||||
req.Header.Set("Trivy-OS", runtime.GOOS)
|
||||
req.Header.Set("Trivy-Arch", runtime.GOARCH)
|
||||
}
|
||||
@@ -91,7 +93,7 @@ func (v *VersionChecker) RunUpdateCheck(ctx context.Context, args []string) {
|
||||
}
|
||||
|
||||
// enable priting if update allowed and quiet mode is not set
|
||||
if !v.skipUpdateCheck && !v.quiet {
|
||||
if !v.cliOptions.SkipVersionCheck && !v.cliOptions.Quiet {
|
||||
v.responseReceived = true
|
||||
}
|
||||
logger.Debug("Version check completed", log.String("latest_version", v.latestVersion.Trivy.LatestVersion))
|
||||
@@ -175,17 +177,6 @@ func (v *VersionChecker) Warnings() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFlags returns the just the flag portion without the values
|
||||
func getFlags(args []string) []string {
|
||||
var flags []string
|
||||
for _, arg := range args {
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
flags = append(flags, strings.Split(arg, "=")[0])
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func (fd *flexibleTime) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), `"`)
|
||||
if s == "" {
|
||||
@@ -211,3 +202,39 @@ func (fd *flexibleTime) UnmarshalJSON(b []byte) error {
|
||||
|
||||
return fmt.Errorf("unable to parse date: %s", s)
|
||||
}
|
||||
|
||||
func (v *VersionChecker) getFlags() string {
|
||||
var flags []string
|
||||
for _, f := range v.cliOptions.GetUsedFlags() {
|
||||
name := f.GetName()
|
||||
if name == "" {
|
||||
continue // Skip flags without a name
|
||||
}
|
||||
value := lo.Ternary(!f.IsTelemetrySafe(), "***", getFlagValue(f))
|
||||
|
||||
flags = append(flags, fmt.Sprintf("--%s=%s", name, value))
|
||||
}
|
||||
return strings.Join(flags, " ")
|
||||
}
|
||||
|
||||
func getFlagValue(f flag.Flagger) string {
|
||||
type flagger[T flag.FlagType] interface {
|
||||
Value() T
|
||||
}
|
||||
switch ff := f.(type) {
|
||||
case flagger[string]:
|
||||
return ff.Value()
|
||||
case flagger[int]:
|
||||
return strconv.Itoa(ff.Value())
|
||||
case flagger[float64]:
|
||||
return fmt.Sprintf("%f", ff.Value())
|
||||
case flagger[bool]:
|
||||
return strconv.FormatBool(ff.Value())
|
||||
case flagger[time.Duration]:
|
||||
return ff.Value().String()
|
||||
case flagger[[]string]:
|
||||
return strings.Join(ff.Value(), ",")
|
||||
default:
|
||||
return "***" // Default case for unsupported types
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,21 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
)
|
||||
|
||||
func TestPrintNotices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []Option
|
||||
skipVersionCheck bool
|
||||
quiet bool
|
||||
disableTelemetry bool
|
||||
|
||||
currentVersion string
|
||||
latestVersion string
|
||||
announcements []announcement
|
||||
responseExpected bool
|
||||
@@ -24,42 +31,38 @@ func TestPrintNotices(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "New version with no announcements",
|
||||
options: []Option{WithCurrentVersion("0.58.0")},
|
||||
currentVersion: "0.58.0",
|
||||
latestVersion: "0.60.0",
|
||||
responseExpected: true,
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
{
|
||||
name: "New version available but includes a prefixed version number",
|
||||
options: []Option{WithCurrentVersion("0.58.0")},
|
||||
currentVersion: "0.58.0",
|
||||
latestVersion: "v0.60.0",
|
||||
responseExpected: true,
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
{
|
||||
name: "new version available but --quiet mode enabled",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
WithQuietMode(true),
|
||||
},
|
||||
name: "new version available but --quiet mode enabled",
|
||||
quiet: true,
|
||||
currentVersion: "0.58.0",
|
||||
latestVersion: "0.60.0",
|
||||
responseExpected: false,
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "new version available but --skip-update-check mode enabled",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
WithSkipVersionCheck(true),
|
||||
},
|
||||
name: "new version available but --skip-version-check mode enabled",
|
||||
skipVersionCheck: true,
|
||||
currentVersion: "0.58.0",
|
||||
latestVersion: "0.60.0",
|
||||
responseExpected: false,
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "New version with announcements",
|
||||
options: []Option{WithCurrentVersion("0.58.0")},
|
||||
latestVersion: "0.60.0",
|
||||
name: "New version with announcements",
|
||||
currentVersion: "0.58.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
@@ -71,9 +74,9 @@ func TestPrintNotices(t *testing.T) {
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
{
|
||||
name: "No new version with announcements",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
latestVersion: "0.60.0",
|
||||
name: "No new version with announcements",
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
@@ -85,9 +88,9 @@ func TestPrintNotices(t *testing.T) {
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
{
|
||||
name: "No new version with announcements and zero time",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
latestVersion: "0.60.0",
|
||||
name: "No new version with announcements and zero time",
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Time{},
|
||||
@@ -99,9 +102,9 @@ func TestPrintNotices(t *testing.T) {
|
||||
expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n",
|
||||
},
|
||||
{
|
||||
name: "No new version with announcement that fails announcement version constraints",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
latestVersion: "0.60.0",
|
||||
name: "No new version with announcement that fails announcement version constraints",
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
@@ -114,9 +117,9 @@ func TestPrintNotices(t *testing.T) {
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "No new version with announcement where current version is greater than to_version",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
latestVersion: "0.60.0",
|
||||
name: "No new version with announcement where current version is greater than to_version",
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
@@ -129,9 +132,9 @@ func TestPrintNotices(t *testing.T) {
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "No new version with announcement that satisfies version constraint but outside date range",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
latestVersion: "0.60.0",
|
||||
name: "No new version with announcement that satisfies version constraint but outside date range",
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2024, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
@@ -144,9 +147,9 @@ func TestPrintNotices(t *testing.T) {
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "No new version with multiple announcements, one of which is valid",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
latestVersion: "0.60.0",
|
||||
name: "No new version with multiple announcements, one of which is valid",
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{
|
||||
{
|
||||
FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC),
|
||||
@@ -165,7 +168,8 @@ func TestPrintNotices(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "No new version with no announcements and quiet mode",
|
||||
options: []Option{WithCurrentVersion("0.60.0"), WithQuietMode(true)},
|
||||
quiet: true,
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{},
|
||||
responseExpected: false,
|
||||
@@ -173,7 +177,7 @@ func TestPrintNotices(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "No new version with no announcements",
|
||||
options: []Option{WithCurrentVersion("0.60.0")},
|
||||
currentVersion: "0.60.0",
|
||||
latestVersion: "0.60.0",
|
||||
announcements: []announcement{},
|
||||
responseExpected: true,
|
||||
@@ -185,11 +189,22 @@ func TestPrintNotices(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
updates := newUpdatesServer(t, tt.latestVersion, tt.announcements)
|
||||
server := httptest.NewServer(http.HandlerFunc(updates.handler))
|
||||
defer server.Close()
|
||||
tt.options = append(tt.options, WithUpdatesApi(server.URL))
|
||||
v := NewVersionChecker(tt.options...)
|
||||
|
||||
v.RunUpdateCheck(t.Context(), nil)
|
||||
cliOpts := &flag.Options{
|
||||
GlobalOptions: flag.GlobalOptions{
|
||||
Quiet: tt.quiet,
|
||||
},
|
||||
ScanOptions: flag.ScanOptions{
|
||||
SkipVersionCheck: tt.skipVersionCheck,
|
||||
DisableTelemetry: tt.disableTelemetry,
|
||||
},
|
||||
}
|
||||
|
||||
v := NewVersionChecker("testCommand", cliOpts)
|
||||
v.updatesApi = server.URL
|
||||
v.currentVersion = tt.currentVersion
|
||||
|
||||
v.RunUpdateCheck(t.Context())
|
||||
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
|
||||
require.Eventually(t, func() bool { return v.responseReceived == tt.responseExpected }, time.Second*5, 500)
|
||||
|
||||
@@ -207,32 +222,29 @@ func TestPrintNotices(t *testing.T) {
|
||||
func TestCheckForNotices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []Option
|
||||
skipVersionCheck bool
|
||||
disableTelemetry bool
|
||||
quiet bool
|
||||
currentVersion string
|
||||
expectedVersion string
|
||||
expectedAnnouncements []announcement
|
||||
expectNoMetrics bool
|
||||
}{
|
||||
{
|
||||
name: "new version with no announcements",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
},
|
||||
name: "new version with no announcements",
|
||||
currentVersion: "0.58.0",
|
||||
expectedVersion: "0.60.0",
|
||||
},
|
||||
{
|
||||
name: "new version with disabled metrics",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
WithTelemetryDisabled(true),
|
||||
},
|
||||
expectedVersion: "0.60.0",
|
||||
expectNoMetrics: true,
|
||||
name: "new version with disabled metrics",
|
||||
disableTelemetry: true,
|
||||
currentVersion: "0.58.0",
|
||||
expectedVersion: "0.60.0",
|
||||
expectNoMetrics: true,
|
||||
},
|
||||
{
|
||||
name: "new version and a new announcement",
|
||||
options: []Option{
|
||||
WithCurrentVersion("0.58.0"),
|
||||
},
|
||||
name: "new version and a new announcement",
|
||||
currentVersion: "0.58.0",
|
||||
expectedVersion: "0.60.0",
|
||||
expectedAnnouncements: []announcement{
|
||||
{
|
||||
@@ -250,10 +262,20 @@ func TestCheckForNotices(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(updates.handler))
|
||||
defer server.Close()
|
||||
|
||||
tt.options = append(tt.options, WithUpdatesApi(server.URL))
|
||||
v := NewVersionChecker(tt.options...)
|
||||
cliOpts := &flag.Options{
|
||||
GlobalOptions: flag.GlobalOptions{
|
||||
Quiet: tt.quiet,
|
||||
},
|
||||
ScanOptions: flag.ScanOptions{
|
||||
SkipVersionCheck: tt.skipVersionCheck,
|
||||
DisableTelemetry: tt.disableTelemetry,
|
||||
},
|
||||
}
|
||||
|
||||
v.RunUpdateCheck(t.Context(), nil)
|
||||
v := NewVersionChecker("testCommand", cliOpts)
|
||||
v.updatesApi = server.URL
|
||||
|
||||
v.RunUpdateCheck(t.Context())
|
||||
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
|
||||
require.Eventually(t, func() bool { return v.responseReceived }, time.Second*5, 500)
|
||||
latestVersion, err := v.LatestVersion()
|
||||
@@ -262,11 +284,9 @@ func TestCheckForNotices(t *testing.T) {
|
||||
assert.ElementsMatch(t, tt.expectedAnnouncements, v.Announcements())
|
||||
|
||||
if tt.expectNoMetrics {
|
||||
assert.True(t, v.telemetryDisabled)
|
||||
require.NotNil(t, updates.lastRequest)
|
||||
assert.Empty(t, updates.lastRequest.Header.Get("Trivy-Identifier"))
|
||||
} else {
|
||||
assert.False(t, v.telemetryDisabled)
|
||||
require.NotNil(t, updates.lastRequest)
|
||||
assert.NotEmpty(t, updates.lastRequest.Header.Get("Trivy-Identifier"))
|
||||
}
|
||||
@@ -344,3 +364,116 @@ func TestFlexibleDate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckCommandHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
commandArgs []string
|
||||
env map[string]string
|
||||
ignoreParseError bool
|
||||
expectedCommandHeader string
|
||||
expectedCommandArgsHeader string
|
||||
}{
|
||||
{
|
||||
name: "image command with no flags",
|
||||
command: "image",
|
||||
commandArgs: []string{"nginx"},
|
||||
expectedCommandHeader: "image",
|
||||
},
|
||||
{
|
||||
name: "image command with flags",
|
||||
command: "image",
|
||||
commandArgs: []string{"--severity", "CRITICAL", "--scanners", "vuln,misconfig", "--pkg-types", "library", "nginx", "--include-dev-deps"},
|
||||
expectedCommandHeader: "image",
|
||||
expectedCommandArgsHeader: "--include-dev-deps=true --pkg-types=library --severity=CRITICAL --scanners=vuln,misconfig",
|
||||
},
|
||||
{
|
||||
name: "image command with multiple flags",
|
||||
command: "image",
|
||||
commandArgs: []string{"--severity", "MEDIUM", "-s", "CRITICAL", "--scanners", "misconfig", "nginx"},
|
||||
expectedCommandHeader: "image",
|
||||
expectedCommandArgsHeader: "--severity=MEDIUM,CRITICAL --scanners=misconfig",
|
||||
},
|
||||
{
|
||||
name: "filesystem command with flags",
|
||||
command: "fs",
|
||||
commandArgs: []string{"--severity=HIGH", "--vex", "repo", "--vuln-severity-source", "nvd,debian", "../trivy-ci-test"},
|
||||
expectedCommandHeader: "fs",
|
||||
expectedCommandArgsHeader: "--severity=HIGH --vex=*** --vuln-severity-source=nvd,debian",
|
||||
},
|
||||
{
|
||||
name: "filesystem command with flags including an invalid flag",
|
||||
command: "fs",
|
||||
commandArgs: []string{"--severity=HIGH", "--vex", "repo", "--vuln-severity-source", "nvd,debian", "--invalid-flag", "../trivy-ci-test"},
|
||||
ignoreParseError: true,
|
||||
expectedCommandHeader: "fs",
|
||||
expectedCommandArgsHeader: "--severity=HIGH --vex=*** --vuln-severity-source=nvd,debian",
|
||||
},
|
||||
{
|
||||
name: "filesystem with environment variables",
|
||||
command: "fs",
|
||||
commandArgs: []string{"--severity", "HIGH", "--vex", "repo", "/home/user/code"},
|
||||
env: map[string]string{
|
||||
"TRIVY_SCANNERS": "secret,misconfig",
|
||||
},
|
||||
expectedCommandHeader: "fs",
|
||||
expectedCommandArgsHeader: "--severity=HIGH --scanners=secret,misconfig --vex=***",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
updates := newUpdatesServer(t, "0.60.0", nil)
|
||||
server := httptest.NewServer(http.HandlerFunc(updates.handler))
|
||||
defer server.Close()
|
||||
|
||||
for key, value := range tt.env {
|
||||
t.Setenv(key, value)
|
||||
}
|
||||
|
||||
// clean up the env
|
||||
defer func() {
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
opts := getOptionsForArgs(t, tt.commandArgs, tt.ignoreParseError)
|
||||
|
||||
v := NewVersionChecker(tt.command, opts)
|
||||
v.updatesApi = server.URL
|
||||
v.RunUpdateCheck(t.Context())
|
||||
|
||||
require.Eventually(t, func() bool { return v.done }, time.Second*5, 500)
|
||||
require.NotNil(t, updates.lastRequest)
|
||||
assert.Equal(t, tt.expectedCommandHeader, updates.lastRequest.Header.Get("Trivy-Command"))
|
||||
assert.Equal(t, tt.expectedCommandArgsHeader, updates.lastRequest.Header.Get("Trivy-Flags"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// getOptionsForArgs uses a basic command to parse the flags so we can generate
|
||||
// an options object from it
|
||||
func getOptionsForArgs(t *testing.T, commandArgs []string, ignoreParseError bool) *flag.Options {
|
||||
flags := flag.Flags{
|
||||
flag.NewGlobalFlagGroup(),
|
||||
flag.NewImageFlagGroup(),
|
||||
flag.NewMisconfFlagGroup(),
|
||||
flag.NewPackageFlagGroup(),
|
||||
flag.NewReportFlagGroup(),
|
||||
flag.NewScanFlagGroup(),
|
||||
flag.NewVulnerabilityFlagGroup(),
|
||||
}
|
||||
|
||||
// simple command to facilitate flag parsing
|
||||
cmd := &cobra.Command{}
|
||||
flags.AddFlags(cmd)
|
||||
err := cmd.ParseFlags(commandArgs)
|
||||
if !ignoreParseError {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.NoError(t, flags.Bind(cmd))
|
||||
opts, err := flags.ToOptions(commandArgs)
|
||||
require.NoError(t, err)
|
||||
return &opts
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package notification
|
||||
|
||||
type Option func(*VersionChecker)
|
||||
|
||||
// WithUpdatesApi sets the updates API URL
|
||||
func WithUpdatesApi(updatesApi string) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.updatesApi = updatesApi
|
||||
}
|
||||
}
|
||||
|
||||
// WithCurrentVersion sets the current version
|
||||
func WithCurrentVersion(version string) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.currentVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithSkipVersionCheck(skipVersionCheck bool) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.skipUpdateCheck = skipVersionCheck
|
||||
}
|
||||
}
|
||||
|
||||
// WithQuietMode sets the quiet mode when the user is using the --quiet flag
|
||||
func WithQuietMode(quiet bool) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.quiet = quiet
|
||||
}
|
||||
}
|
||||
|
||||
// WithTelemetryDisabled sets the telemetry disabled flag
|
||||
func WithTelemetryDisabled(telemetryDisabled bool) Option {
|
||||
return func(v *VersionChecker) {
|
||||
v.telemetryDisabled = telemetryDisabled
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ToolVendor = "aquasecurity"
|
||||
ToolName = "trivy"
|
||||
Namespace = ToolVendor + ":" + ToolName + ":"
|
||||
ToolVendor = "aquasecurity"
|
||||
ToolName = "trivy"
|
||||
ToolManufacturer = "Aqua Security Software Ltd."
|
||||
Namespace = ToolVendor + ":" + ToolName + ":"
|
||||
|
||||
// https://json-schema.org/understanding-json-schema/reference/string.html#dates-and-times
|
||||
timeLayout = "2006-01-02T15:04:05+00:00"
|
||||
@@ -88,10 +89,11 @@ func (m *Marshaler) Metadata(ctx context.Context) *cdx.Metadata {
|
||||
Tools: &cdx.ToolsChoice{
|
||||
Components: &[]cdx.Component{
|
||||
{
|
||||
Type: cdx.ComponentTypeApplication,
|
||||
Group: ToolVendor,
|
||||
Name: ToolName,
|
||||
Version: m.appVersion,
|
||||
Type: cdx.ComponentTypeApplication,
|
||||
Group: ToolVendor,
|
||||
Name: ToolName,
|
||||
Version: m.appVersion,
|
||||
Manufacturer: &cdx.OrganizationalEntity{Name: ToolManufacturer},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -287,6 +287,9 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
Name: "trivy",
|
||||
Group: "aquasecurity",
|
||||
Version: "dev",
|
||||
Manufacturer: &cdx.OrganizationalEntity{
|
||||
Name: "Aqua Security Software Ltd.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -923,6 +926,9 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
Name: "trivy",
|
||||
Group: "aquasecurity",
|
||||
Version: "dev",
|
||||
Manufacturer: &cdx.OrganizationalEntity{
|
||||
Name: "Aqua Security Software Ltd.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1308,6 +1314,9 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
Name: "trivy",
|
||||
Group: "aquasecurity",
|
||||
Version: "dev",
|
||||
Manufacturer: &cdx.OrganizationalEntity{
|
||||
Name: "Aqua Security Software Ltd.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1535,6 +1544,9 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
Name: "trivy",
|
||||
Group: "aquasecurity",
|
||||
Version: "dev",
|
||||
Manufacturer: &cdx.OrganizationalEntity{
|
||||
Name: "Aqua Security Software Ltd.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1791,6 +1803,9 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
Name: "trivy",
|
||||
Group: "aquasecurity",
|
||||
Version: "dev",
|
||||
Manufacturer: &cdx.OrganizationalEntity{
|
||||
Name: "Aqua Security Software Ltd.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1978,6 +1993,9 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
Name: "trivy",
|
||||
Group: "aquasecurity",
|
||||
Version: "dev",
|
||||
Manufacturer: &cdx.OrganizationalEntity{
|
||||
Name: "Aqua Security Software Ltd.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2066,6 +2084,9 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
||||
Name: "trivy",
|
||||
Group: "aquasecurity",
|
||||
Version: "dev",
|
||||
Manufacturer: &cdx.OrganizationalEntity{
|
||||
Name: "Aqua Security Software Ltd.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user