Compare commits

...

9 Commits

Author SHA1 Message Date
Aqua Security automated builds
280491bb51 release: v0.64.0 [main] (#8955) 2025-07-01 07:48:04 +00:00
DmitriyLewen
a6e9807c09 docs(python): fix type with METADATA file name (#9090) 2025-06-30 07:55:35 +00:00
Teppei Fukuda
1e1e1b5fa6 feat: reject unsupported artifact types in remote image retrieval (#9052)
Signed-off-by: knqyf263 <knqyf263@gmail.com>
2025-06-30 07:40:40 +00:00
dependabot[bot]
7333c469f4 chore(deps): bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 (#9088)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 06:42:09 +00:00
Nikita Pivkin
bac6f7b3da refactor(misconf): rewrite Rego module filtering using functional filters (#9061)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
2025-06-28 05:44:39 +00:00
Nikita Pivkin
a9f7dcdb9c feat(terraform): add partial evaluation for policy templates (#8967)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
2025-06-28 04:58:16 +00:00
Teppei Fukuda
3a0ec0f2ac feat(vuln): add Root.io support for container image scanning (#9073)
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
2025-06-27 15:17:39 +00:00
K
41d0f949c8 feat(sbom): add manufacturer field to CycloneDX tools metadata (#9019)
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
2025-06-27 07:33:58 +00:00
Owen Rumney
fd2bc91e13 fix(cli): add some values to the telemetry call (#9056) 2025-06-27 07:14:25 +00:00
65 changed files with 2598 additions and 344 deletions

View File

@@ -1 +1 @@
{".":"0.63.0"}
{".":"0.64.0"}

View File

@@ -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)

View 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
```

View File

@@ -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>.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View 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)

View File

@@ -169,6 +169,7 @@ trivy filesystem [flags] PATH
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -190,6 +190,7 @@ trivy image [flags] IMAGE_NAME
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -178,6 +178,7 @@ trivy kubernetes [flags] [CONTEXT]
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -168,6 +168,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -170,6 +170,7 @@ trivy rootfs [flags] ROOTDIR
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -139,6 +139,7 @@ trivy sbom [flags] SBOM_PATH
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -155,6 +155,7 @@ trivy vm [flags] VM_IMAGE
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -10,6 +10,9 @@
"components": [
{
"type": "application",
"manufacturer": {
"name": "Aqua Security Software Ltd."
},
"group": "aquasecurity",
"name": "trivy",
"version": "dev"

View File

@@ -10,6 +10,9 @@
"components": [
{
"type": "application",
"manufacturer": {
"name": "Aqua Security Software Ltd."
},
"group": "aquasecurity",
"name": "trivy",
"version": "dev"

View File

@@ -10,6 +10,9 @@
"components": [
{
"type": "application",
"manufacturer": {
"name": "Aqua Security Software Ltd."
},
"group": "aquasecurity",
"name": "trivy",
"version": "dev"

View File

@@ -10,6 +10,9 @@
"components": [
{
"type": "application",
"manufacturer": {
"name": "Aqua Security Software Ltd."
},
"group": "aquasecurity",
"name": "trivy",
"version": "dev"

View File

@@ -10,6 +10,9 @@
"components": [
{
"type": "application",
"manufacturer": {
"name": "Aqua Security Software Ltd."
},
"group": "aquasecurity",
"name": "trivy",
"version": "dev"

View File

@@ -10,6 +10,9 @@
"components": [
{
"type": "application",
"manufacturer": {
"name": "Aqua Security Software Ltd."
},
"group": "aquasecurity",
"name": "trivy",
"version": "dev"

View File

@@ -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(),
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)))

View 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

View 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
}

View 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")
}
})
}
}

View 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
}

View 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)
})
}
}

View 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)
})
}
}

View 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"

View File

@@ -0,0 +1,9 @@
- bucket: root.io alpine 3.20
pairs:
- bucket: jq
pairs:
- key: CVE-2020-8177
value:
FixedVersion:
- foo
- bar

View 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"

View 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
}

View 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)
})
}
}

View 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
}

View 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)
})
}
}

View File

@@ -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

View File

@@ -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())
})
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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,
}
)

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
}
)

View File

@@ -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(),

View File

@@ -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
View 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
View 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 := &rego.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 := &rego.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 := &rego.StaticMetadata{Deprecated: tt.deprecated}
filter := rego.IncludeDeprecatedFilter(tt.include)
result := filter(module, metadata)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -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,

View File

@@ -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}
}

View File

@@ -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))
}
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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},
},
},
},

View File

@@ -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.",
},
},
},
},