diff --git a/.travis.yml b/.travis.yml index 7ea410b0bfe..3bd86777f5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -183,6 +183,7 @@ addons: - xsltproc - libxml2-utils - libsystemd-journal-dev + - librpm-dev before_install: - python --version diff --git a/auditbeat/Dockerfile b/auditbeat/Dockerfile index 25d13f15e11..905e23748d1 100644 --- a/auditbeat/Dockerfile +++ b/auditbeat/Dockerfile @@ -5,6 +5,7 @@ RUN \ && apt-get install -y --no-install-recommends \ python-pip \ virtualenv \ + librpm-dev \ && rm -rf /var/lib/apt/lists/* RUN pip install --upgrade pip diff --git a/x-pack/auditbeat/magefile.go b/x-pack/auditbeat/magefile.go index b2c8ac9e8b7..af8e08f1957 100644 --- a/x-pack/auditbeat/magefile.go +++ b/x-pack/auditbeat/magefile.go @@ -12,6 +12,8 @@ import ( "time" "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/pkg/errors" auditbeat "github.com/elastic/beats/auditbeat/scripts/mage" "github.com/elastic/beats/dev-tools/mage" @@ -20,6 +22,7 @@ import ( func init() { mage.BeatDescription = "Audit the activities of users and processes on your system." mage.BeatLicense = "Elastic License" + mage.Platforms = mage.Platforms.Filter("!linux/ppc64 !linux/mips64") } // Aliases provides compatibility with CI while we transition all Beats @@ -36,6 +39,9 @@ func Build() error { // GolangCrossBuild build the Beat binary inside of the golang-builder. // Do not use directly, use crossBuild instead. func GolangCrossBuild() error { + if d, ok := deps[mage.Platform.Name]; ok { + mg.Deps(d) + } return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) } @@ -173,3 +179,83 @@ func PythonIntegTest(ctx context.Context) error { return mage.PythonNoseTest(mage.DefaultPythonTestIntegrationArgs()) }) } + +// ----------------------------------------------------------------------------- +// - Install the librpm-dev package +var ( + deps = map[string]func() error{ + "linux/386": installLinux386, + "linux/amd64": installLinuxAMD64, + "linux/arm64": installLinuxARM64, + "linux/armv5": installLinuxARMLE, + "linux/armv6": installLinuxARMLE, + "linux/armv7": installLinuxARMHF, + "linux/mips": installLinuxMIPS, + "linux/mipsle": installLinuxMIPSLE, + "linux/mips64le": installLinuxMIPS64LE, + "linux/ppc64le": installLinuxPPC64LE, + "linux/s390x": installLinuxS390X, + + //"linux/ppc64": installLinuxPpc64, + //"linux/mips64": installLinuxMips64, + } +) + +const ( + librpmDevPkgName = "librpm-dev" +) + +func installLinuxAMD64() error { + return installDependencies(librpmDevPkgName, "") +} + +func installLinuxARM64() error { + return installDependencies(librpmDevPkgName+":arm64", "arm64") +} + +func installLinuxARMHF() error { + return installDependencies(librpmDevPkgName+":armhf", "armhf") +} + +func installLinuxARMLE() error { + return installDependencies(librpmDevPkgName+":armel", "armel") +} + +func installLinux386() error { + return installDependencies(librpmDevPkgName+":i386", "i386") +} + +func installLinuxMIPS() error { + return installDependencies(librpmDevPkgName+":mips", "mips") +} + +func installLinuxMIPS64LE() error { + return installDependencies(librpmDevPkgName+":mips64el", "mips64el") +} + +func installLinuxMIPSLE() error { + return installDependencies(librpmDevPkgName+":mipsel", "mipsel") +} + +func installLinuxPPC64LE() error { + return installDependencies(librpmDevPkgName+":ppc64el", "ppc64el") +} + +func installLinuxS390X() error { + return installDependencies(librpmDevPkgName+":s390x", "s390x") +} + +func installDependencies(pkg, arch string) error { + if arch != "" { + err := sh.Run("dpkg", "--add-architecture", arch) + if err != nil { + return errors.Wrap(err, "error while adding architecture") + } + } + + if err := sh.Run("apt-get", "update"); err != nil { + return err + } + + return sh.Run("apt-get", "install", "-y", "--no-install-recommends", pkg) +} diff --git a/x-pack/auditbeat/module/system/package/_meta/docs.asciidoc b/x-pack/auditbeat/module/system/package/_meta/docs.asciidoc index 13e2be806a7..b1504e3aeb3 100644 --- a/x-pack/auditbeat/module/system/package/_meta/docs.asciidoc +++ b/x-pack/auditbeat/module/system/package/_meta/docs.asciidoc @@ -4,5 +4,5 @@ experimental[] This is the `package` dataset of the system module. -It is implemented for Linux distributions using dpkg as their package manager, -and for Homebrew on macOS (Darwin). +It is implemented for Linux distributions using dpkg or rpm as their package +manager, and for Homebrew on macOS (Darwin). diff --git a/x-pack/auditbeat/module/system/package/package.go b/x-pack/auditbeat/module/system/package/package.go index 5030c521d1a..b77e30854cb 100644 --- a/x-pack/auditbeat/module/system/package/package.go +++ b/x-pack/auditbeat/module/system/package/package.go @@ -190,7 +190,7 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { ms.osFamily = osInfo.Family switch osInfo.Family { case redhat: - return nil, fmt.Errorf("RPM support is not yet implemented") + // ok case debian: if _, err := os.Stat(dpkgStatusFile); err != nil { return nil, errors.Wrapf(err, "error looking up %s", dpkgStatusFile) @@ -412,8 +412,10 @@ func (ms *MetricSet) savePackagesToDisk(packages []*Package) error { func getPackages(osFamily string) (packages []*Package, err error) { switch osFamily { case redhat: - // TODO: Implement RPM - err = errors.New("RPM not yet supported") + packages, err = listRPMPackages() + if err != nil { + err = errors.Wrap(err, "error getting RPM packages") + } case debian: packages, err = listDebPackages() if err != nil { diff --git a/x-pack/auditbeat/module/system/package/package_windows.go b/x-pack/auditbeat/module/system/package/package_windows.go index 6811cb41c60..498eea61111 100644 --- a/x-pack/auditbeat/module/system/package/package_windows.go +++ b/x-pack/auditbeat/module/system/package/package_windows.go @@ -4,7 +4,7 @@ // +build windows -package socket +package pkg import ( "fmt" diff --git a/x-pack/auditbeat/module/system/package/rpm_common_test.go b/x-pack/auditbeat/module/system/package/rpm_common_test.go new file mode 100644 index 00000000000..2e75d475811 --- /dev/null +++ b/x-pack/auditbeat/module/system/package/rpm_common_test.go @@ -0,0 +1,69 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// +build !windows + +package pkg + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + "time" +) + +func rpmPackagesByExec() ([]*Package, error) { + format := "%{NAME}|%{VERSION}|%{RELEASE}|%{ARCH}|%{LICENSE}|%{INSTALLTIME}|%{SIZE}|%{URL}|%{SUMMARY}\\n" + out, err := exec.Command("/usr/bin/rpm", "--qf", format, "-qa").Output() + if err != nil { + return nil, fmt.Errorf("Error running rpm -qa command: %v", err) + } + + lines := strings.Split(string(out), "\n") + var packages []*Package + for _, line := range lines { + if len(strings.TrimSpace(line)) == 0 { + continue + } + words := strings.SplitN(line, "|", 9) + if len(words) < 9 { + return nil, fmt.Errorf("line '%s' doesn't have enough elements", line) + } + pkg := Package{ + Name: words[0], + Version: words[1], + Release: words[2], + Arch: words[3], + License: words[4], + // install time - 5 + // size - 6 + URL: words[7], + Summary: words[8], + } + ts, err := strconv.ParseInt(words[5], 10, 64) + if err != nil { + return nil, fmt.Errorf("error converting %s to string: %v", words[5], err) + } + pkg.InstallTime = time.Unix(ts, 0) + + pkg.Size, err = strconv.ParseUint(words[6], 10, 64) + if err != nil { + return nil, fmt.Errorf("error converting %s to string: %v", words[6], err) + } + + // Avoid "(none)" in favor of empty strings + if pkg.URL == "(none)" { + pkg.URL = "" + } + if pkg.Arch == "(none)" { + pkg.Arch = "" + } + + packages = append(packages, &pkg) + + } + + return packages, nil +} diff --git a/x-pack/auditbeat/module/system/package/rpm_linux.go b/x-pack/auditbeat/module/system/package/rpm_linux.go new file mode 100644 index 00000000000..a1d395f54b1 --- /dev/null +++ b/x-pack/auditbeat/module/system/package/rpm_linux.go @@ -0,0 +1,302 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// +build linux,cgo + +package pkg + +import ( + "fmt" + "time" + "unsafe" + + "github.com/coreos/pkg/dlopen" +) + +/* +#include +#include + +#include +#include +#include +#include + +rpmts +my_rpmtsCreate(void *f) { + rpmts (*rpmtsCreate)(); + rpmtsCreate = (rpmts (*)())f; + + return rpmtsCreate(); +} + +int +my_rpmReadConfigFiles(void *f) { + int (*rpmReadConfigFiles)(const char*, const char*); + rpmReadConfigFiles = (int (*)(const char*, const char*))f; + return rpmReadConfigFiles(NULL, NULL); +} + +rpmdbMatchIterator +my_rpmtsInitIterator(void *f, rpmts ts) { + rpmdbMatchIterator (*rpmtsInitIterator)(const rpmts, rpmTag, const void*, size_t); + rpmtsInitIterator = (rpmdbMatchIterator (*)(const rpmts, rpmTag, const void*, size_t))f; + + return rpmtsInitIterator(ts, RPMDBI_PACKAGES, NULL, 0); +} + +Header +my_rpmdbNextIterator(void *f, rpmdbMatchIterator mi) { + Header (*rpmdbNextIterator)(rpmdbMatchIterator); + rpmdbNextIterator = (Header (*)(rpmdbMatchIterator))f; + + return rpmdbNextIterator(mi); +} + +Header +my_headerLink(void *f, Header h) { + Header (*headerLink)(Header); + headerLink = (Header (*)(Header))f; + + return headerLink(h); +} + +int +my_headerGetEntry(void *f, Header h, rpm_tag_t tag, char **p) { + int (*headerGetEntry)(Header, rpm_tag_t, rpm_tagtype_t*, rpm_data_t*, rpm_count_t*); + headerGetEntry = (int (*)(Header, rpm_tag_t, rpm_tagtype_t*, rpm_data_t*, rpm_count_t*))f; + + return headerGetEntry(h, tag, NULL, (void**)p, NULL); +} + +int +my_headerGetEntryInt(void *f, Header h, rpm_tag_t tag, int **p) { + int (*headerGetEntry)(Header, rpm_tag_t, rpm_tagtype_t*, rpm_data_t*, rpm_count_t*); + headerGetEntry = (int (*)(Header, rpm_tag_t, rpm_tagtype_t*, rpm_data_t*, rpm_count_t*))f; + + return headerGetEntry(h, tag, NULL, (void**)p, NULL); +} + +void +my_headerFree(void *f, Header h) { + Header (*headerFree)(Header); + headerFree = (Header (*)(Header))f; + + headerFree(h); +} + +void +my_rpmdbFreeIterator(void *f, rpmdbMatchIterator mi) { + rpmdbMatchIterator (*rpmdbFreeIterator)(rpmdbMatchIterator); + rpmdbFreeIterator = (rpmdbMatchIterator (*)(rpmdbMatchIterator))f; + + rpmdbFreeIterator(mi); +} + +void +my_rpmtsFree(void *f, rpmts ts) { + rpmts (*rpmtsFree)(rpmts); + rpmtsFree = (rpmts (*)(rpmts))f; + + rpmtsFree(ts); +}*/ +import "C" + +// Constants in sync with /usr/include/rpm/rpmtag.h +const ( + RPMTAG_NAME = 1000 + RPMTAG_VERSION = 1001 + RPMTAG_RELEASE = 1002 + RPMTAG_SUMMARY = 1004 + RPMTAG_LICENSE = 1014 + RPMTAG_URL = 1020 + RPMTAG_ARCH = 1022 + RPMTAG_SIZE = 1009 + RPMTAG_INSTALLTIME = 1008 +) + +type cFunctions struct { + rpmtsCreate unsafe.Pointer + rpmReadConfigFiles unsafe.Pointer + rpmtsInitIterator unsafe.Pointer + rpmdbNextIterator unsafe.Pointer + headerLink unsafe.Pointer + headerGetEntry unsafe.Pointer + headerFree unsafe.Pointer + rpmdbFreeIterator unsafe.Pointer + rpmtsFree unsafe.Pointer +} + +var cFun *cFunctions + +func dlopenCFunctions() (*cFunctions, error) { + var librpmNames = []string{ + "/usr/lib64/librpm.so", + } + var cFun cFunctions + + librpm, err := dlopen.GetHandle(librpmNames) + if err != nil { + return nil, err + } + + cFun.rpmtsCreate, err = librpm.GetSymbolPointer("rpmtsCreate") + if err != nil { + return nil, err + } + + cFun.rpmReadConfigFiles, err = librpm.GetSymbolPointer("rpmReadConfigFiles") + if err != nil { + return nil, err + } + + cFun.rpmtsInitIterator, err = librpm.GetSymbolPointer("rpmtsInitIterator") + if err != nil { + return nil, err + } + + cFun.rpmdbNextIterator, err = librpm.GetSymbolPointer("rpmdbNextIterator") + if err != nil { + return nil, err + } + + cFun.headerLink, err = librpm.GetSymbolPointer("headerLink") + if err != nil { + return nil, err + } + + cFun.headerGetEntry, err = librpm.GetSymbolPointer("headerGetEntry") + if err != nil { + return nil, err + } + + cFun.headerFree, err = librpm.GetSymbolPointer("headerFree") + if err != nil { + return nil, err + } + + cFun.rpmdbFreeIterator, err = librpm.GetSymbolPointer("rpmdbFreeIterator") + if err != nil { + return nil, err + } + + cFun.rpmtsFree, err = librpm.GetSymbolPointer("rpmtsFree") + if err != nil { + return nil, err + } + + return &cFun, nil +} + +func listRPMPackages() ([]*Package, error) { + if cFun == nil { + var err error + cFun, err = dlopenCFunctions() + if err != nil { + return nil, err + } + } + + rpmts := C.my_rpmtsCreate(cFun.rpmtsCreate) + if rpmts == nil { + return nil, fmt.Errorf("Failed to get rpmts") + } + defer C.my_rpmtsFree(cFun.rpmtsFree, rpmts) + res := C.my_rpmReadConfigFiles(cFun.rpmReadConfigFiles) + if int(res) != 0 { + return nil, fmt.Errorf("Error: %d", int(res)) + } + + mi := C.my_rpmtsInitIterator(cFun.rpmtsInitIterator, rpmts) + if mi == nil { + return nil, fmt.Errorf("Failed to get match iterator") + } + defer C.my_rpmdbFreeIterator(cFun.rpmdbFreeIterator, mi) + + var packages []*Package + for header := C.my_rpmdbNextIterator(cFun.rpmdbNextIterator, mi); header != nil; header = C.my_rpmdbNextIterator(cFun.rpmdbNextIterator, mi) { + + pkg, err := packageFromHeader(header, cFun) + if err != nil { + return nil, err + } + + packages = append(packages, pkg) + } + + return packages, nil +} + +func packageFromHeader(header C.Header, cFun *cFunctions) (*Package, error) { + + header = C.my_headerLink(cFun.headerLink, header) + if header == nil { + return nil, fmt.Errorf("Error calling headerLink") + } + defer C.my_headerFree(cFun.headerFree, header) + + pkg := Package{} + + var name *C.char + res := C.my_headerGetEntry(cFun.headerGetEntry, header, RPMTAG_NAME, &name) + if res != 1 { + return nil, fmt.Errorf("Failed to call headerGetEntry(name): %d", res) + } + pkg.Name = C.GoString(name) + + var version *C.char + res = C.my_headerGetEntry(cFun.headerGetEntry, header, RPMTAG_VERSION, &version) + if res != 1 { + return nil, fmt.Errorf("Failed to call headerGetEntry(version): %d", res) + } + pkg.Version = C.GoString(version) + + var release *C.char + res = C.my_headerGetEntry(cFun.headerGetEntry, header, RPMTAG_RELEASE, &release) + if res != 1 { + return nil, fmt.Errorf("Failed to call headerGetEntry(release): %d", res) + } + pkg.Release = C.GoString(release) + + var license *C.char + res = C.my_headerGetEntry(cFun.headerGetEntry, header, RPMTAG_LICENSE, &license) + if res != 1 { + return nil, fmt.Errorf("Failed to call headerGetEntry(license): %d", res) + } + pkg.License = C.GoString(license) + + var arch *C.char + res = C.my_headerGetEntry(cFun.headerGetEntry, header, RPMTAG_ARCH, &arch) + if res == 1 { // not always successful + pkg.Arch = C.GoString(arch) + } + + var url *C.char + res = C.my_headerGetEntry(cFun.headerGetEntry, header, RPMTAG_URL, &url) + if res == 1 { // not always successful + pkg.URL = C.GoString(url) + } + + var summary *C.char + res = C.my_headerGetEntry(cFun.headerGetEntry, header, RPMTAG_SUMMARY, &summary) + if res == 1 { // not always successful + pkg.Summary = C.GoString(summary) + } + + var size *C.int + res = C.my_headerGetEntryInt(cFun.headerGetEntry, header, RPMTAG_SIZE, &size) + if res != 1 { + return nil, fmt.Errorf("Failed to call headerGetEntry(size): %d", res) + } + pkg.Size = uint64(*size) + + var installTime *C.int + res = C.my_headerGetEntryInt(cFun.headerGetEntry, header, RPMTAG_INSTALLTIME, &installTime) + if res != 1 { + return nil, fmt.Errorf("Failed to call headerGetEntry(installTime): %d", res) + } + pkg.InstallTime = time.Unix(int64(*installTime), 0) + + return &pkg, nil +} diff --git a/x-pack/auditbeat/module/system/package/rpm_linux_test.go b/x-pack/auditbeat/module/system/package/rpm_linux_test.go new file mode 100644 index 00000000000..989fb740876 --- /dev/null +++ b/x-pack/auditbeat/module/system/package/rpm_linux_test.go @@ -0,0 +1,38 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// +build linux,cgo + +package pkg + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRPMPackages(t *testing.T) { + os, err := getOS() + if err != nil { + t.Fatal(err) + } + + if os.Family != "redhat" { + t.Skip("RPM test only on Redhat systems") + } + + // Control using the exec command + packagesExpected, err := rpmPackagesByExec() + if err != nil { + t.Fatal(err) + } + + packages, err := listRPMPackages() + if err != nil { + t.Fatal(err) + } + + assert.EqualValues(t, packagesExpected, packages) + +} diff --git a/x-pack/auditbeat/module/system/package/rpm_others.go b/x-pack/auditbeat/module/system/package/rpm_others.go new file mode 100644 index 00000000000..8dd8462197b --- /dev/null +++ b/x-pack/auditbeat/module/system/package/rpm_others.go @@ -0,0 +1,14 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// +build !linux !cgo +// +build !windows + +package pkg + +import "github.com/pkg/errors" + +func listRPMPackages() ([]*Package, error) { + return nil, errors.New("listing RPM packages is only supported on Linux") +} diff --git a/x-pack/auditbeat/tests/system/test_metricsets.py b/x-pack/auditbeat/tests/system/test_metricsets.py index a5ac543bd21..3b93d110230 100644 --- a/x-pack/auditbeat/tests/system/test_metricsets.py +++ b/x-pack/auditbeat/tests/system/test_metricsets.py @@ -23,8 +23,8 @@ def test_metricset_host(self): self.check_metricset("system", "host", COMMON_FIELDS + fields, warnings_allowed=True) @unittest.skipIf(sys.platform == "win32", "Not implemented for Windows") - @unittest.skipIf(sys.platform == "linux2" and platform.linux_distribution()[0] != "debian", - "Only implemented for Debian") + @unittest.skipIf(sys.platform == "linux2" and not (os.path.isdir("/var/lib/dpkg") or os.path.isdir("/var/lib/rpm")), + "Only implemented for dpkg and rpm") def test_metricset_package(self): """ package metricset collects information about installed packages on a system.