-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support RPM archives #7628
Changes from 7 commits
e83eee9
4f4dd1d
2f34afc
981a8b3
782d8ac
6b4a3a3
096fcd0
c1fa209
9ed5d88
9c26be0
ab37b6a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,3 +39,6 @@ dist | |
# Signing | ||
gpg.key | ||
cmd/trivy/trivy | ||
|
||
# RPM | ||
*.rpm |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,149 @@ | ||||||||
package rpm | ||||||||
|
||||||||
import ( | ||||||||
"context" | ||||||||
"errors" | ||||||||
"os" | ||||||||
"path/filepath" | ||||||||
"strconv" | ||||||||
"strings" | ||||||||
|
||||||||
"github.com/package-url/packageurl-go" | ||||||||
"github.com/sassoftware/go-rpmutils" | ||||||||
"golang.org/x/xerrors" | ||||||||
|
||||||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer" | ||||||||
"github.com/aquasecurity/trivy/pkg/fanal/types" | ||||||||
"github.com/aquasecurity/trivy/pkg/log" | ||||||||
"github.com/aquasecurity/trivy/pkg/scanner/utils" | ||||||||
) | ||||||||
|
||||||||
const archiveVersion = 1 | ||||||||
|
||||||||
func init() { | ||||||||
analyzer.RegisterAnalyzer(newRPMArchiveAnalyzer()) | ||||||||
} | ||||||||
|
||||||||
type rpmArchiveAnalyzer struct { | ||||||||
logger *log.Logger | ||||||||
} | ||||||||
|
||||||||
func newRPMArchiveAnalyzer() *rpmArchiveAnalyzer { | ||||||||
return &rpmArchiveAnalyzer{ | ||||||||
logger: log.WithPrefix("rpm-archive"), | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
func (a *rpmArchiveAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { | ||||||||
rpm, err := rpmutils.ReadRpm(input.Content) | ||||||||
if err != nil { | ||||||||
return nil, xerrors.Errorf("failed to read rpm (%s): %w", input.FilePath, err) | ||||||||
} | ||||||||
pkg, err := a.parseHeader(rpm.Header) | ||||||||
if err != nil { | ||||||||
return nil, xerrors.Errorf("failed to parse rpm header: %w", err) | ||||||||
} | ||||||||
pkg.FilePath = input.FilePath | ||||||||
pkg.Identifier.PURL = a.generatePURL(&pkg) | ||||||||
|
||||||||
return &analyzer.AnalysisResult{ | ||||||||
PackageInfos: []types.PackageInfo{ | ||||||||
{ | ||||||||
FilePath: input.FilePath, | ||||||||
Packages: types.Packages{pkg}, | ||||||||
}, | ||||||||
}, | ||||||||
}, nil | ||||||||
} | ||||||||
|
||||||||
func (a *rpmArchiveAnalyzer) parseHeader(h *rpmutils.RpmHeader) (types.Package, error) { | ||||||||
if h == nil { | ||||||||
return types.Package{}, errors.New("rpm header is nil") | ||||||||
} | ||||||||
// Getting metadata | ||||||||
nevra, err := h.GetNEVRA() | ||||||||
if a.unexpectedError(err) != nil { | ||||||||
return types.Package{}, xerrors.Errorf("failed to get NEVRA: %w", err) | ||||||||
} | ||||||||
epoch, err := strconv.Atoi(nevra.Epoch) | ||||||||
if a.unexpectedError(err) != nil { | ||||||||
return types.Package{}, xerrors.Errorf("failed to convert epoch to int (%s): %w", nevra.Name, err) | ||||||||
} | ||||||||
licenses, err := h.GetStrings(rpmutils.LICENSE) | ||||||||
if a.unexpectedError(err) != nil { | ||||||||
return types.Package{}, xerrors.Errorf("failed to get licenses: %w", err) | ||||||||
} | ||||||||
sourceRpm, err := h.GetString(rpmutils.SOURCERPM) | ||||||||
if a.unexpectedError(err) != nil { | ||||||||
return types.Package{}, xerrors.Errorf("failed to get source rpm: %w", err) | ||||||||
} | ||||||||
srcName, srcVer, srcRel, err := splitFileName(sourceRpm) | ||||||||
if err != nil { | ||||||||
a.logger.Debug("Invalid Source RPM Found", log.String("sourcerpm", sourceRpm)) | ||||||||
} | ||||||||
vendor, err := h.GetString(rpmutils.VENDOR) | ||||||||
if a.unexpectedError(err) != nil { | ||||||||
return types.Package{}, xerrors.Errorf("failed to get vendor: %w", err) | ||||||||
} | ||||||||
|
||||||||
// TODO: add the const to go-rpmutils | ||||||||
// cf. https://github.com/rpm-software-management/rpm/blob/rpm-4.16.0-release/lib/rpmtag.h#L375 | ||||||||
modularityLabel, err := h.GetString(5096) | ||||||||
if a.unexpectedError(err) != nil { | ||||||||
return types.Package{}, xerrors.Errorf("failed to get modularitylabel: %w", err) | ||||||||
} | ||||||||
|
||||||||
return types.Package{ | ||||||||
Name: nevra.Name, | ||||||||
Version: nevra.Version, | ||||||||
Release: nevra.Release, | ||||||||
Epoch: epoch, | ||||||||
Arch: nevra.Arch, | ||||||||
SrcName: srcName, | ||||||||
SrcVersion: srcVer, | ||||||||
SrcRelease: srcRel, | ||||||||
SrcEpoch: epoch, | ||||||||
Licenses: licenses, | ||||||||
Maintainer: vendor, | ||||||||
Modularitylabel: modularityLabel, | ||||||||
}, nil | ||||||||
} | ||||||||
|
||||||||
func (a *rpmArchiveAnalyzer) generatePURL(pkg *types.Package) *packageurl.PackageURL { | ||||||||
vendor := strings.ToLower(pkg.Maintainer) | ||||||||
|
||||||||
// TODO: Handle more vendors | ||||||||
var ns string | ||||||||
switch { | ||||||||
case strings.Contains(vendor, "red hat"): | ||||||||
ns = "redhat" | ||||||||
case strings.Contains(vendor, "fedora"): | ||||||||
ns = "fedora" | ||||||||
case strings.Contains(vendor, "opensuse"): | ||||||||
ns = "opensuse" | ||||||||
case strings.Contains(vendor, "suse"): | ||||||||
ns = "suse" | ||||||||
} | ||||||||
return packageurl.NewPackageURL(packageurl.TypeRPM, ns, pkg.Name, utils.FormatVersion(*pkg), nil, "") | ||||||||
} | ||||||||
Comment on lines
+124
to
+140
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIUC we will overwrite this purl if trivy/pkg/fanal/applier/docker.go Lines 223 to 225 in 096fcd0
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to generate PURL here as OS is not detected when scanning RPM files. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that we need to generate PURL here. I meant that perhaps we need to add UPD: ➜ cat Dockerfile
FROM centos
COPY socat-1.7.3.2-2.el7.x86_64.rpm socat-1.7.3.2-2.el7.x86_64.rpm
➜ TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true ./trivy -q image -f json --list-all-pkgs rpm-archives | grep socat@1.7.3.2
"PURL": "pkg:rpm/centos/socat@1.7.3.2-2.el7?arch=x86_64\u0026distro=centos-8.4.2105",
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It makes sense. Added 9c26be0 |
||||||||
|
||||||||
func (a *rpmArchiveAnalyzer) unexpectedError(err error) error { | ||||||||
var rerr rpmutils.NoSuchTagError | ||||||||
if errors.As(err, &rerr) { | ||||||||
a.logger.Debug("RPM tag not found", log.Err(rerr)) | ||||||||
return nil | ||||||||
} | ||||||||
return err | ||||||||
} | ||||||||
|
||||||||
func (a *rpmArchiveAnalyzer) Required(filePath string, _ os.FileInfo) bool { | ||||||||
return filepath.Ext(filePath) == ".rpm" | ||||||||
} | ||||||||
|
||||||||
func (a *rpmArchiveAnalyzer) Type() analyzer.Type { | ||||||||
return analyzer.TypeRpmArchive | ||||||||
} | ||||||||
|
||||||||
func (a *rpmArchiveAnalyzer) Version() int { | ||||||||
return archiveVersion | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package rpm | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/package-url/packageurl-go" | ||
"github.com/samber/lo" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer" | ||
"github.com/aquasecurity/trivy/pkg/fanal/types" | ||
) | ||
|
||
func Test_rpmArchiveAnalyzer_Analyze(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
input analyzer.AnalysisInput | ||
want *analyzer.AnalysisResult | ||
wantErr require.ErrorAssertionFunc | ||
}{ | ||
{ | ||
name: "valid", | ||
input: analyzer.AnalysisInput{ | ||
FilePath: "testdata/valid.rpm", | ||
Content: lo.Must(os.Open("testdata/socat-1.7.3.2-2.el7.x86_64.rpm")), // Must run 'mage rpm:fixtures' before this test | ||
}, | ||
want: &analyzer.AnalysisResult{ | ||
PackageInfos: []types.PackageInfo{ | ||
{ | ||
FilePath: "testdata/valid.rpm", | ||
Packages: types.Packages{ | ||
{ | ||
Name: "socat", | ||
Version: "1.7.3.2", | ||
Release: "2.el7", | ||
Arch: "x86_64", | ||
SrcName: "socat", | ||
SrcVersion: "1.7.3.2", | ||
SrcRelease: "2.el7", | ||
FilePath: "testdata/valid.rpm", | ||
Licenses: []string{ | ||
"GPLv2", | ||
}, | ||
Maintainer: "Red Hat, Inc.", | ||
Identifier: types.PkgIdentifier{ | ||
PURL: &packageurl.PackageURL{ | ||
Type: packageurl.TypeRPM, | ||
Namespace: "redhat", | ||
Name: "socat", | ||
Version: "1.7.3.2-2.el7", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
wantErr: require.NoError, | ||
}, | ||
{ | ||
name: "broken", | ||
input: analyzer.AnalysisInput{ | ||
FilePath: "testdata/broken.rpm", | ||
Content: strings.NewReader(`broken`), | ||
}, | ||
wantErr: require.Error, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
a := newRPMArchiveAnalyzer() | ||
got, err := a.Analyze(context.Background(), tt.input) | ||
tt.wantErr(t, err) | ||
assert.Equal(t, tt.want, got) | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we skip
splitFileName
IfSOURCERPM
is not found?(to avoid
splitFileName
error)as for rpm:
trivy/pkg/fanal/analyzer/pkg/rpm/rpm.go
Lines 130 to 136 in 096fcd0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c1fa209