feat: add Bottlerocket OS package analyzer (#8653)

This commit is contained in:
David du Colombier
2025-05-23 09:50:59 +02:00
committed by GitHub
parent ee522300b7
commit 07ef63b483
16 changed files with 381 additions and 7 deletions

View File

@@ -0,0 +1,15 @@
# Bottlerocket
Trivy supports the following scanners for OS packages.
| Scanner | Supported |
| :-----------: | :-------: |
| SBOM | ✓ |
| Vulnerability | - |
| License | - |
Please see [here](index.md#supported-os) for supported versions.
## SBOM
Trivy detects packages that are listed in the [software inventory].
[software inventory]: https://bottlerocket.dev/en/os/1.37.x/concepts/variants/#software-inventory

View File

@@ -28,6 +28,7 @@ Trivy supports operating systems for
| [Photon OS](photon.md) | 1.0, 2.0, 3.0, 4.0 | tndf/yum/rpm |
| [Debian GNU/Linux](debian.md) | 7, 8, 9, 10, 11, 12 | apt/dpkg |
| [Ubuntu](ubuntu.md) | All versions supported by Canonical | apt/dpkg |
| [Bottlerocket](bottlerocket.md) | 1.7.0 and upper | bottlerocket |
| [OSs with installed Conda](../others/conda.md) | - | conda |
## Supported container images

View File

@@ -76,6 +76,7 @@ nav:
- Alpine Linux: docs/coverage/os/alpine.md
- Amazon Linux: docs/coverage/os/amazon.md
- Azure Linux (CBL-Mariner): docs/coverage/os/azure.md
- Bottlerocket: docs/coverage/os/bottlerocket.md
- CentOS: docs/coverage/os/centos.md
- Chainguard: docs/coverage/os/chainguard.md
- Debian: docs/coverage/os/debian.md

View File

@@ -0,0 +1,28 @@
package bottlerocket
import (
"context"
osver "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/types"
)
// Scanner implements the Bottlerocket scanner
type Scanner struct {
}
// NewScanner is the factory method for Scanner
func NewScanner() *Scanner {
return &Scanner{}
}
func (s *Scanner) Detect(ctx context.Context, _ string, _ *ftypes.Repository, _ []ftypes.Package) ([]types.DetectedVulnerability, error) {
log.InfoContext(ctx, "Vulnerability detection of Bottlerocket packages is currently not supported.")
return nil, nil
}
func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool {
return osver.Supported(ctx, nil, osFamily, osver.Minor(osVer))
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/aquasecurity/trivy/pkg/detector/ospkg/alpine"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/amazon"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/azure"
"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/oracle"
@@ -34,6 +35,7 @@ var (
ftypes.Alma: alma.NewScanner(),
ftypes.Amazon: amazon.NewScanner(),
ftypes.Azure: azure.NewAzureScanner(),
ftypes.Bottlerocket: bottlerocket.NewScanner(),
ftypes.CBLMariner: azure.NewMarinerScanner(),
ftypes.Debian: debian.NewScanner(),
ftypes.Ubuntu: ubuntu.NewScanner(),

View File

@@ -46,6 +46,7 @@ import (
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/release"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/ubuntu"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/bottlerocket_inventory"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/dpkg"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/rpm"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk"

View File

@@ -28,12 +28,13 @@ const (
TypeUbuntuESM Type = "ubuntu-esm"
// OS Package
TypeApk Type = "apk"
TypeDpkg Type = "dpkg"
TypeDpkgLicense Type = "dpkg-license" // For analyzing licenses
TypeRpm Type = "rpm"
TypeRpmArchive Type = "rpm-archive"
TypeRpmqa Type = "rpmqa"
TypeApk Type = "apk"
TypeBottlerocketInventory Type = "bottlerocket-inventory"
TypeDpkg Type = "dpkg"
TypeDpkgLicense Type = "dpkg-license" // For analyzing licenses
TypeRpm Type = "rpm"
TypeRpmArchive Type = "rpm-archive"
TypeRpmqa Type = "rpmqa"
// OS Package Repository
TypeApkRepo Type = "apk-repo"
@@ -165,6 +166,7 @@ var (
TypeSUSE,
TypeUbuntu,
TypeApk,
TypeBottlerocketInventory,
TypeDpkg,
TypeDpkgLicense,
TypeRpm,

View File

@@ -20,6 +20,8 @@ const version = 1
var requiredFiles = []string{
"etc/os-release",
"usr/lib/os-release",
"aarch64-bottlerocket-linux-gnu/sys-root/usr/lib/os-release",
"x86_64-bottlerocket-linux-gnu/sys-root/usr/lib/os-release",
}
type osReleaseAnalyzer struct{}
@@ -49,6 +51,8 @@ func (a osReleaseAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInp
switch id {
case "alpine":
family = types.Alpine
case "bottlerocket":
family = types.Bottlerocket
case "opensuse-tumbleweed":
family = types.OpenSUSETumbleweed
case "opensuse-leap", "opensuse": // opensuse for leap:42, opensuse-leap for leap:15

View File

@@ -149,6 +149,16 @@ func Test_osReleaseAnalyzer_Analyze(t *testing.T) {
},
},
},
{
name: "Bottlerocket",
inputFile: "testdata/bottlerocket",
want: &analyzer.AnalysisResult{
OS: types.OS{
Family: types.Bottlerocket,
Name: "1.34.0",
},
},
},
{
name: "Unknown OS",
inputFile: "testdata/unknown",

View File

@@ -0,0 +1,11 @@
NAME=Bottlerocket
ID=bottlerocket
VERSION="1.34.0 (aws-ecs-2)"
PRETTY_NAME="Bottlerocket OS 1.34.0 (aws-ecs-2)"
VARIANT_ID=aws-ecs-2
VERSION_ID=1.34.0
BUILD_ID=18d04e52
HOME_URL="https://github.com/bottlerocket-os/bottlerocket"
SUPPORT_URL="https://github.com/bottlerocket-os/bottlerocket/discussions"
BUG_REPORT_URL="https://github.com/bottlerocket-os/bottlerocket/issues"
DOCUMENTATION_URL="https://bottlerocket.dev"

View File

@@ -0,0 +1,110 @@
package bottlerocket_inventory
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"strconv"
"time"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
func init() {
analyzer.RegisterAnalyzer(newBottlerocketInventoryAnalyzer())
}
const analyzerVersion = 1
var requiredFiles = []string{
"aarch64-bottlerocket-linux-gnu/sys-root/usr/share/bottlerocket/application-inventory.json",
"x86_64-bottlerocket-linux-gnu/sys-root/usr/share/bottlerocket/application-inventory.json",
}
type bottlerocketInventoryAnalyzer struct{}
func newBottlerocketInventoryAnalyzer() *bottlerocketInventoryAnalyzer {
return &bottlerocketInventoryAnalyzer{}
}
type ApplicationInventory struct {
Content []struct {
Name string `json:"Name"`
Publisher string `json:"Publisher"`
Version string `json:"Version"`
Release string `json:"Release"`
Epoch string `json:"Epoch"`
InstalledTime time.Time `json:"InstalledTime"`
ApplicationType string `json:"ApplicationType"`
Architecture string `json:"Architecture"`
URL string `json:"Url"`
Summary string `json:"Summary"`
} `json:"Content"`
}
func (a bottlerocketInventoryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
parsedInventorys, err := a.parseApplicationInventory(ctx, input.Content)
if err != nil {
return nil, err
}
return &analyzer.AnalysisResult{
PackageInfos: []types.PackageInfo{
{
FilePath: input.FilePath,
Packages: parsedInventorys,
},
},
}, nil
}
func (a bottlerocketInventoryAnalyzer) parseApplicationInventory(_ context.Context, r io.Reader) ([]types.Package, error) {
var pkgs []types.Package
var applicationInventory ApplicationInventory
if err := json.NewDecoder(r).Decode(&applicationInventory); err != nil {
return nil, err
}
for _, app := range applicationInventory.Content {
epoch, err := strconv.Atoi(app.Epoch)
if err != nil {
return nil, err
}
pkg := types.Package{
Arch: app.Architecture,
Epoch: epoch,
Name: app.Name,
Version: app.Version,
}
if pkg.Name != "" && pkg.Version != "" {
pkg.ID = fmt.Sprintf("%s@%s", pkg.Name, pkg.Version)
}
pkgs = append(pkgs, pkg)
}
return pkgs, nil
}
func (a bottlerocketInventoryAnalyzer) Required(filePath string, _ os.FileInfo) bool {
return slices.Contains(requiredFiles, filePath)
}
func (a bottlerocketInventoryAnalyzer) Type() analyzer.Type {
return analyzer.TypeBottlerocketInventory
}
func (a bottlerocketInventoryAnalyzer) Version() int {
return analyzerVersion
}
// StaticPaths returns a list of static file paths to analyze
func (a bottlerocketInventoryAnalyzer) StaticPaths() []string {
return requiredFiles
}

View File

@@ -0,0 +1,62 @@
package bottlerocket_inventory
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
var pkgs = []types.Package{
{
ID: "glibc@2.40",
Name: "glibc",
Version: "2.40",
Epoch: 1,
Arch: "x86_64",
},
{
ID: "kernel-6.1@6.1.128",
Name: "kernel-6.1",
Version: "6.1.128",
Epoch: 0,
Arch: "x86_64",
},
{
ID: "systemd@252.22",
Name: "systemd",
Version: "252.22",
Epoch: 0,
Arch: "x86_64",
},
}
func TestParseApplicationInventory(t *testing.T) {
var tests = []struct {
name string
path string
wantPkgs []types.Package
}{
{
name: "happy path",
path: "./testdata/application-inventory.json",
wantPkgs: pkgs,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := bottlerocketInventoryAnalyzer{}
f, err := os.Open(tt.path)
require.NoError(t, err)
defer f.Close()
gotPkgs, err := a.parseApplicationInventory(t.Context(), f)
require.NoError(t, err)
assert.Equal(t, tt.wantPkgs, gotPkgs)
})
}
}

View File

@@ -0,0 +1,40 @@
{
"Content": [
{
"Name": "glibc",
"Publisher": "bottlerocket-core-kit",
"Version": "2.40",
"Release": "1.1740525475.e3a5862c.br1",
"Epoch": "1",
"InstalledTime": "2025-02-27T20:55:41Z",
"ApplicationType": "Unspecified",
"Architecture": "x86_64",
"Url": "http://www.gnu.org/software/glibc/",
"Summary": "The GNU libc libraries"
},
{
"Name": "kernel-6.1",
"Publisher": "bottlerocket-kernel-kit",
"Version": "6.1.128",
"Release": "1.1740603423.4d405dc9.br1",
"Epoch": "0",
"InstalledTime": "2025-02-27T20:55:41Z",
"ApplicationType": "Unspecified",
"Architecture": "x86_64",
"Url": "https://www.kernel.org/",
"Summary": "The Linux kernel"
},
{
"Name": "systemd",
"Publisher": "bottlerocket-core-kit",
"Version": "252.22",
"Release": "1.1740525475.e3a5862c.br1",
"Epoch": "0",
"InstalledTime": "2025-02-27T20:55:41Z",
"ApplicationType": "Unspecified",
"Architecture": "x86_64",
"Url": "https://www.freedesktop.org/wiki/Software/systemd",
"Summary": "System and Service Manager"
}
]
}

View File

@@ -25,6 +25,7 @@ const (
Alpine OSType = "alpine"
Amazon OSType = "amazon"
Azure OSType = "azurelinux"
Bottlerocket OSType = "bottlerocket"
CBLMariner OSType = "cbl-mariner"
CentOS OSType = "centos"
Chainguard OSType = "chainguard"

View File

@@ -7,7 +7,7 @@ import (
cn "github.com/google/go-containerregistry/pkg/name"
version "github.com/knqyf263/go-rpm-version"
packageurl "github.com/package-url/packageurl-go"
"github.com/package-url/packageurl-go"
"github.com/samber/lo"
"golang.org/x/xerrors"
@@ -42,6 +42,10 @@ const (
NamespaceOCP = "ocp"
TypeUnknown = "unknown"
// Temporary type before being added in github.com/package-url/packageurl-go
// cf. https://github.com/package-url/purl-spec/issues/454
packageurlTypeBottlerocket = "bottlerocket"
)
type PackageURL packageurl.PackageURL
@@ -80,6 +84,8 @@ func New(t ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) (*Pac
if metadata.OS != nil {
namespace = string(metadata.OS.Family)
}
case packageurlTypeBottlerocket:
qualifiers = append(qualifiers, packageurl.Qualifiers{packageurl.Qualifier{Key: "distro", Value: fmt.Sprintf("bottlerocket-%s", metadata.OS.Name)}}...)
case packageurl.TypeApk:
var qs packageurl.Qualifiers
name, namespace, qs = parseApk(name, metadata.OS)
@@ -443,6 +449,7 @@ func parseJulia(pkgName, pkgUUID string) (string, string, packageurl.Qualifiers)
return namespace, name, qualifiers
}
// nolint: gocyclo
func purlType(t ftypes.TargetType) string {
switch t {
case ftypes.Jar, ftypes.Pom, ftypes.Gradle, ftypes.Sbt:
@@ -482,6 +489,8 @@ func purlType(t ftypes.TargetType) string {
ftypes.OpenSUSELeap, ftypes.OpenSUSETumbleweed, ftypes.SLES, ftypes.SLEMicro, ftypes.Photon,
ftypes.Azure, ftypes.CBLMariner:
return packageurl.TypeRPM
case ftypes.Bottlerocket:
return packageurlTypeBottlerocket
case TypeOCI:
return packageurl.TypeOCI
case ftypes.Julia:

View File

@@ -440,6 +440,42 @@ func TestNewPackageURL(t *testing.T) {
},
},
},
{
name: "bottlerocket package",
typ: ftypes.Bottlerocket,
metadata: types.Metadata{
OS: &ftypes.OS{
Family: ftypes.Bottlerocket,
Name: "1.34.0",
},
},
pkg: ftypes.Package{
ID: "glibc@2.40",
Name: "glibc",
Version: "2.40",
Epoch: 1,
Arch: "x86_64",
},
want: &purl.PackageURL{
Type: "bottlerocket",
Name: "glibc",
Version: "2.40",
Qualifiers: packageurl.Qualifiers{
{
Key: "arch",
Value: "x86_64",
},
{
Key: "epoch",
Value: "1",
},
{
Key: "distro",
Value: "bottlerocket-1.34.0",
},
},
},
},
}
for _, tc := range testCases {
@@ -710,6 +746,47 @@ func TestPackageURL_Package(t *testing.T) {
},
},
},
{
name: "bottlerocket with epoch",
pkgURL: &purl.PackageURL{
Type: "bottlerocket",
Name: "glibc",
Version: "2.40",
Qualifiers: packageurl.Qualifiers{
{
Key: "epoch",
Value: "1",
},
{
Key: "distro",
Value: "bottlerocket-1.34.0",
},
},
},
wantPkg: &ftypes.Package{
ID: "glibc@2.40",
Name: "glibc",
Version: "2.40",
Epoch: 1,
Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: "bottlerocket",
Name: "glibc",
Version: "2.40",
Qualifiers: packageurl.Qualifiers{
{
Key: "epoch",
Value: "1",
},
{
Key: "distro",
Value: "bottlerocket-1.34.0",
},
},
},
},
},
},
{
name: "wrong epoch",
pkgURL: &purl.PackageURL{