Skip to content

ROX-13770: Introduce local Node Scanner #1164

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

Merged
merged 10 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# IDE
.idea
.vscode

# Mac OS hidden file
.DS_Store
Expand Down
36 changes: 35 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ GOPATH_WD_OVERRIDES := -w /src -e GOPATH=/go
IMAGE_BUILD_FLAGS := -e CGO_ENABLED=0 -e GOOS=linux -e GOARCH=${GOARCH}
BUILD_FLAGS := CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH}
BUILD_CMD := go build -trimpath -ldflags="-X github.com/stackrox/scanner/pkg/version.Version=$(TAG)" -o image/scanner/bin/scanner ./cmd/clair
NODESCAN_BUILD_CMD := go build -trimpath -o tools/bin/local-nodescanner ./tools/local-nodescanner

#####################################################################
###### Binaries we depend on (need to be defined on top) ############
Expand Down Expand Up @@ -364,7 +365,7 @@ clean-proto-generated-srcs:
## Clean ##
###########
.PHONY: clean
clean: clean-image clean-helm-rendered clean-proto-generated-srcs clean-pprof clean-test clean-gobin
clean: clean-image clean-helm-rendered clean-proto-generated-srcs clean-pprof clean-test clean-gobin clean-toolbin
@echo "+ $@"

.PHONY: clean-image
Expand Down Expand Up @@ -394,6 +395,11 @@ clean-gobin:
@echo "+ $@"
rm -rf $(GOBIN)

.PHONY: clean-toolbin
clean-toolbin:
@echo "+ $@"
git clean -xdf tools/bin

##################
## Genesis Dump ##
##################
Expand Down Expand Up @@ -460,3 +466,31 @@ else
mkdir -p $(dir $@)
uuidgen | tr '[:upper:]' '[:lower:]' > $@
endif


###########
## Tools ##
###########

# Local Node Scanner
.PHONY: local-nodescanner
local-nodescanner:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the idea that this should only be used inside a container? Otherwise, Mac users won't be able to use the binary directly :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to have maximum flexibility. I have added a UBI 9 container target in this PR to make it runnable anywhere, but we could add more, e.g. UBI 8 or even Fedora latest for a bleeding edge RPM version.
The binary can either be used for tests and/or CI, or as a debug entry point for local debugging. That local debugging would not work on bare metal OSX w.r.t. RPM packages, but still might be useful, or could be done in the container.

@echo "+ $@"
$(BUILD_FLAGS) $(NODESCAN_BUILD_CMD)

.PHONY: local-nodescanner-build-dockerized
local-nodescanner-build-dockerized:
@echo "+ $@"
ifdef CI
docker container create --name builder $(BUILD_IMAGE) $(NODESCAN_BUILD_CMD)
docker cp $(GOPATH) builder:/
docker start -i builder
docker cp builder:/go/src/github.com/stackrox/scanner/tools/bin/local-nodescanner tools/bin/local-nodescanner
else
docker run $(IMAGE_BUILD_FLAGS) $(GOPATH_WD_OVERRIDES) $(LOCAL_VOLUME_ARGS) $(BUILD_IMAGE) $(NODESCAN_BUILD_CMD)
endif

.PHONY: local-nodescanner-image
local-nodescanner-image: local-nodescanner-build-dockerized
@echo "+ $@"
docker build -t local-nodescanner:$(TAG) -f tools/local-nodescanner/Dockerfile ./tools
Binary file added testdata/NodeScanning/rhcos4.12-minimal.tar.gz
Binary file not shown.
1 change: 1 addition & 0 deletions tools/allowed-large-files
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ pkg/rhelv2/rpm/testdata/Packages
pkg/rhelv2/rpm/testdata/rpmdb.sqlite
pkg/vulnloader/nvdloader/nvdloader_easyjson.go
pkg/ziputil/testdata/test.zip
testdata/NodeScanning/rhcos4.12-minimal.tar.gz
tools/linters/go.sum
2 changes: 2 additions & 0 deletions tools/bin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
9 changes: 9 additions & 0 deletions tools/local-nodescanner/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ARG BASE_REGISTRY=registry.access.redhat.com
ARG BASE_IMAGE=ubi9-minimal
ARG BASE_TAG=9.1

FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} AS base

COPY ./bin/local-nodescanner /local-nodescanner

ENTRYPOINT [ "/local-nodescanner" ]
38 changes: 38 additions & 0 deletions tools/local-nodescanner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Local Nodescanner
The local nodescanner is a tool to run the code related to Node Scanning locally without having to run the full Docker image or server.
Function-wise, it uses the very same calls and therefore is generating the same results a node scan running in the Node Scanner image would.

## Building
A `makefile` target named `local-nodescanner` is available in the main makefile.
It will create binaries in the projects' `bin` folder.
For ease of use, a Docker image is also available as target `local-nodescanner-image`.

## Running the Docker image
As the default for `fspath` is set to `/host`, one can run the image without changes when mounting the target fs to the right path:
`docker run -it -v /path/to/rhcos/fs:/host local-nodescanner:$(make tag)`
Additional flags for the local nodescanner binary can be provided as args to the Docker image.
For example, to enable verbose output:
`docker run -it -v /path/to/rhcos/fs:/host local-nodescanner:$(make tag) --verbose`

## Requirements
The scanning code requires an `rpmdb` binary to be available in the executing systems `PATH`.
Be warned that RPM installed via `brew` on OSX *will not work correctly*, as it will produce an empty RPM database.

The only required flag is the path to a filesystem.
This can be a RO-mount of a running system (e.g. `/host` in the Compliance or Node-Scanner images),
or an unpacked filesystem, e.g. from a Docker image or ISO.
Refer to `testdata/NodeScanning/rhcos4.12-minimal.tar.gz` for an archive containing a minimal example.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that we get a bit more instructions how to handle that 17MB file - could we maybe get a command where we extract it to some tmp location and then do docker run with mounting it?

Copy link
Contributor Author

@Maddosaurus Maddosaurus Apr 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a complete working example that should get anyone up and running. See L25-L31 in the README

This archive can be used in conjunction with the Docker image.
**NOTE**: If you are running on OSX, mounting anyting in `/tmp` as Docker volume might not work. Resort to using a different folder, e.g. in your homefolder in that case.
```shell
TMPDIR=$(mktemp -d)
tar xzf testdata/NodeScanning/rhcos4.12-minimal.tar.gz -C "$TMPDIR"
make local-nodescanner-image
docker run -it -v "$TMPDIR"/rhcos-412:/host local-nodescanner:$(make tag)
```
You should see a successful scan, indicated by the scanner noting that it found 503 installed RPM packages and 4 Content Sets.

The minimal folder/file structure needed for a scan to succeed is:
- `/etc/redhat-release` & `/etc/os-release` with contents denoting an RHCOS OS
- `/usr/share/rpm` containing the RPM database
- `/usr/share/buildinfo/content_manifest.json` containing the content sets
86 changes: 86 additions & 0 deletions tools/local-nodescanner/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main

import (
"context"
"flag"
"os"
"path/filepath"

log "github.com/sirupsen/logrus"
"github.com/stackrox/scanner/pkg/analyzer/nodes"
)

func main() {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
fPath := flag.String("fspath", "/host", "Path to the root folder of a filesystem")
fRHCOSrequired := flag.Bool("rhcos-required", true, "Fails scan if path does not contain a filesystem generated by RHCOS. Node Scanning only works on RHCOS.")
fUncertifiedRHEL := flag.Bool("uncertified-rhel", false, "Set true for CentOS and false for RHEL.")
fVerbose := flag.Bool("verbose", false, "Print verbose output if set")
flag.Parse()

setupLog(*fVerbose)

fspath, err := filepath.Abs(*fPath)
if err != nil {
log.Fatalf("Encountered error while formatting path: %v", err)
}

log.Infof("Analyzing rootfs in %v", fspath)
if err := checkTargetDir(fspath); err != nil {
log.Fatalf("Target directory is empty or non-existing: %v", err)
}

components, err := nodes.Analyze(context.Background(), "nodename", fspath, nodes.AnalyzeOpts{UncertifiedRHEL: *fUncertifiedRHEL, IsRHCOSRequired: *fRHCOSrequired})
if err != nil {
log.Errorf("Encountered error while scanning: %v", err)
}

printResults(components)
}

func setupLog(verbose bool) {
log.SetFormatter(&log.TextFormatter{
DisableLevelTruncation: true,
PadLevelText: true,
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})

if verbose {
log.SetLevel(log.DebugLevel)
}
}

func checkTargetDir(path string) error {
dir, err := os.Open(path)
if err != nil {
return err
}
_, err = dir.Readdirnames(10)
if err != nil {
return err
}
return nil
}

func printResults(components *nodes.Components) {
if components == nil || components.CertifiedRHELComponents == nil {
log.Info("No Components discovered")
return
}
log.Infof("Determined OS: %v", components.CertifiedRHELComponents.Dist)
if components.CertifiedRHELComponents.Packages != nil {
log.Infof("Number of installed RPM packages: %v", len(components.CertifiedRHELComponents.Packages))
for _, c := range components.CertifiedRHELComponents.Packages {
log.Debugf("Component: %v", c)
}
}

if components.CertifiedRHELComponents.ContentSets != nil {
log.Infof("Number of discovered content sets: %v", len(components.CertifiedRHELComponents.ContentSets))
for _, cs := range components.CertifiedRHELComponents.ContentSets {
log.Debugf("Content set: %v", cs)
}
}

}