mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat: add Bottlerocket OS package analyzer (#8653)
This commit is contained in:
committed by
GitHub
parent
ee522300b7
commit
07ef63b483
15
docs/docs/coverage/os/bottlerocket.md
Normal file
15
docs/docs/coverage/os/bottlerocket.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
28
pkg/detector/ospkg/bottlerocket/bottlerocket.go
Normal file
28
pkg/detector/ospkg/bottlerocket/bottlerocket.go
Normal 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))
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
11
pkg/fanal/analyzer/os/release/testdata/bottlerocket
vendored
Normal file
11
pkg/fanal/analyzer/os/release/testdata/bottlerocket
vendored
Normal 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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
40
pkg/fanal/analyzer/pkg/bottlerocket_inventory/testdata/application-inventory.json
vendored
Normal file
40
pkg/fanal/analyzer/pkg/bottlerocket_inventory/testdata/application-inventory.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user