Skip to content
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

Use trivy rootfs scan for both local and remote #168

Merged
merged 64 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
862b4a5
rename this type to reflect what it is
partkyle Aug 7, 2024
3962509
fix logger call depth
partkyle Aug 8, 2024
c7ea0a6
extract the rootfs using the tar comand
partkyle Aug 12, 2024
5fb4528
add cleanup to test
partkyle Aug 12, 2024
8bcb05e
fix issues for some unextractable layers and layers with bad perms
partkyle Aug 12, 2024
d7f68ec
separate files by concern
partkyle Aug 12, 2024
ef622fe
add flag to select sbom or rootfs
partkyle Aug 13, 2024
5abf0bd
add adr for rootfs scan
partkyle Aug 13, 2024
c4cb787
cleanup some scanning issues
partkyle Aug 13, 2024
4ec89bb
order this because it will work for remote better
partkyle Aug 13, 2024
1029ce3
Revert "order this because it will work for remote better"
partkyle Aug 13, 2024
f4482d4
Merge remote-tracking branch 'origin/main' into partkyle/rootfs-scan
partkyle Aug 14, 2024
dbb81c6
fmt
partkyle Aug 14, 2024
8e55f70
change extraction to use filesystem over tar extract to be compatible…
partkyle Aug 14, 2024
513942d
use rootfs scanner from local on the remote scan
partkyle Aug 15, 2024
7269de5
Merge remote-tracking branch 'origin/main' into partkyle/rootfs-scan
partkyle Aug 15, 2024
acc205e
fix artifact name override, but we need to find out how to deduplicat…
partkyle Aug 15, 2024
38dbf7e
should use the provided ctx, the calling function will pass in the s.…
partkyle Aug 15, 2024
c4dcec6
remove error for linter
partkyle Aug 15, 2024
1fbe8ad
names and fmt
partkyle Aug 15, 2024
3a23b08
I think this lint is resolved now.
partkyle Aug 15, 2024
176ec18
use the local layout write to replace most of the code for copying th…
partkyle Aug 15, 2024
f5c8c80
remove nolint
partkyle Aug 15, 2024
d94cc3b
wrap err
partkyle Aug 15, 2024
1fb7b81
use functions attempting to fix lint
partkyle Aug 15, 2024
fa97f90
change the order for struct packing
partkyle Aug 15, 2024
15d196e
adr updates
partkyle Aug 15, 2024
51a0bb0
punctuation
partkyle Aug 15, 2024
3187d8e
line length
partkyle Aug 15, 2024
c0c9c20
these are not spdx
partkyle Aug 15, 2024
91b2e22
Merge remote-tracking branch 'origin/main' into partkyle/rootfs-scan
partkyle Aug 19, 2024
1f39788
return errs from the scan
partkyle Aug 19, 2024
63cd2b9
set DOCKER_CONFIG for the remote call
partkyle Aug 19, 2024
f3d02a2
fix e2e test to use only required registry
partkyle Aug 19, 2024
bf63ba8
handle err
partkyle Aug 19, 2024
57c2b99
rename
partkyle Aug 19, 2024
e30f21c
consistent with other parts of this function
partkyle Aug 19, 2024
abab396
formatting
partkyle Aug 19, 2024
2af39b7
fix error
partkyle Aug 19, 2024
533aeb3
rename variable
partkyle Aug 19, 2024
c7533d1
rename these to something more meaningful
partkyle Aug 19, 2024
38155cd
update required envs
partkyle Aug 19, 2024
16fe9b7
include sbom support for remote
partkyle Aug 19, 2024
09875a8
lint
partkyle Aug 19, 2024
8d67b0f
fmt
partkyle Aug 19, 2024
4eae0f2
comments
partkyle Aug 19, 2024
d9a79fa
try removing this DOCKER_CONFIG
partkyle Aug 19, 2024
78c9276
Revert "try removing this DOCKER_CONFIG"
partkyle Aug 19, 2024
5c563fc
remove comment
partkyle Aug 19, 2024
831509a
REGISTRY1_CREDS not needed anymore
partkyle Aug 20, 2024
549815f
wording
partkyle Aug 20, 2024
037b67e
remove remoteScannable becase we no longer use it
partkyle Aug 20, 2024
d1eed93
comment
partkyle Aug 20, 2024
08e79a1
convert to oras to get authorization to work
partkyle Aug 20, 2024
8e4667a
update docs
partkyle Aug 20, 2024
2b279f3
Merge remote-tracking branch 'origin/main' into partkyle/rootfs-scan
partkyle Aug 20, 2024
f44e328
backtrack on this because I had the format incorrec
partkyle Aug 20, 2024
630621c
linty
partkyle Aug 20, 2024
a0ab38e
fix imports
partkyle Aug 20, 2024
af870df
remove log
partkyle Aug 20, 2024
6b7fc74
remove unsused var
partkyle Aug 20, 2024
0dace64
fieldalignment
partkyle Aug 20, 2024
406ea89
line length
partkyle Aug 20, 2024
79ef5a7
accepted
partkyle Aug 20, 2024
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ test-unit:
go test -timeout 160s ./... -v -coverprofile=coverage.out

test-integration:
@if [ -z "$${GITHUB_TOKEN}" ] || [ -z "$${GHCR_CREDS}" ] || [ -z "$${REGISTRY1_CREDS}" ]; then \
echo "Error: GITHUB_TOKEN, GHCR_CREDS, or REGISTRY1_CREDS is not set"; \
@if [ -z "$${GITHUB_TOKEN}" ] || [ -z "$${GHCR_CREDS}" ]; then \
echo "Error: GITHUB_TOKEN or GHCR_CREDS is not set"; \
exit 1; \
fi
integration=true go test -timeout 160s ./... -v -coverprofile=coverage.out
Expand Down
19 changes: 14 additions & 5 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Example: 'registry1.dso.mil:myuser:mypassword'`)
rootCmd.PersistentFlags().StringP("output-file", "f", "", "Output file for CSV results")
rootCmd.PersistentFlags().StringP("package-path", "p", "", `Path to the local zarf package.
This is for local scanning and not fetching from a remote registry.`)
rootCmd.PersistentFlags().StringP("scanner-type", "s", "rootfs", "Trivy scanner type. options: sbom|rootfs")
rootCmd.PersistentFlags().StringP("offline-db-path", "d", "", `Path to the offline DB to use for the scan.
This is for local scanning and not fetching from a remote registry.
This should have all the files extracted from the trivy-db image and ran once before running the scan.`)
Expand All @@ -86,16 +87,24 @@ func runScanner(cmd *cobra.Command, _ []string) error {
outputFile, _ := cmd.Flags().GetString("output-file") //nolint:errcheck
registryCreds, _ := cmd.Flags().GetStringSlice("registry-creds") //nolint:errcheck
packagePath, _ := cmd.Flags().GetString("package-path") //nolint:errcheck
scannerType, _ := cmd.Flags().GetString("scanner-type") //nolint:errcheck
offlineDBPath, _ := cmd.Flags().GetString("offline-db-path") //nolint:errcheck

parsedCreds := docker.ParseCredentials(registryCreds)
dockerConfigPath, err := docker.GenerateAndWriteDockerConfig(ctx, parsedCreds)
if err != nil {
return fmt.Errorf("error generating and writing Docker config: %w", err)
}

factory := &scan.ScannerFactoryImpl{}
scanner, err := factory.CreateScanner(ctx, logger, dockerConfigPath, org, packageName, tag, packagePath, offlineDBPath)
scanner, err := factory.CreateScanner(
ctx,
logger,
"",
org,
packageName,
tag,
packagePath,
offlineDBPath,
parsedCreds,
scannerType == "sbom",
)
if err != nil {
return fmt.Errorf("error creating scanner: %w", err)
}
Expand Down
8 changes: 2 additions & 6 deletions cmd/store/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,13 @@ func TestStore(t *testing.T) {
const testDBPath = "tests/uds_security_hub.db"
github := os.Getenv("GITHUB_TOKEN")
ghcrCreds := os.Getenv("GHCR_CREDS")
registry1Creds := os.Getenv("REGISTRY1_CREDS")
dockerCreds := os.Getenv("DOCKER_IO_CREDS")
if github == "" || ghcrCreds == "" || registry1Creds == "" {
t.Fatalf("GITHUB_TOKEN, GHCR_CREDS, and REGISTRY1_CREDS are required")
if github == "" || ghcrCreds == "" {
partkyle marked this conversation as resolved.
Show resolved Hide resolved
t.Fatalf("GITHUB_TOKEN and GHCR_CREDS are required")
}

os.Args = []string{
"program",
"--registry-creds", ghcrCreds,
"--registry-creds", registry1Creds,
"--registry-creds", dockerCreds,
"-n", "packages/uds/mattermost",
"--db-path", testDBPath,
"-v", "1",
Expand Down
9 changes: 2 additions & 7 deletions cmd/store/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,8 @@ func runStoreScanner(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("error getting registry credentials: %w", err)
}
parsedCreds := docker.ParseCredentials(registryCreds)
dockerConfigPath, err := docker.GenerateAndWriteDockerConfig(ctx, parsedCreds)
if err != nil {
return fmt.Errorf("error generating and writing Docker config: %w", err)
}
scanner := scan.NewRemotePackageScanner(ctx, logInstance, dockerConfigPath, config.Org, config.PackageName,
config.Tag, config.OfflineDBPath)

scanner := scan.NewRemotePackageScanner(ctx, logInstance, "", config.Org, config.PackageName,
config.Tag, config.OfflineDBPath, parsedCreds, false)
manager, err := db.NewGormScanManager(config.DBConn)
if err != nil {
return fmt.Errorf("error initializing GormScanManager: %w", err)
Expand Down
139 changes: 139 additions & 0 deletions docs/adrs/0001-rootfs-scanning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# ADR: Scan using `trivy rootfs` to get a complete scan of a package

Date: 2024-08-12

## Status

accepted

## Context

Prior local package scanning was implemented using only the sbom. This can result
in lower quality CVE results.

Example CVE counts for the zarf argocd example as per 2024-08-12.

| Scanner Type | CRITICAL | HIGH | MEDIUM | TOTAL |
| :----------: | :------: | :--: | :----: | :---: |
| sbom | 1 | 2 | 23 | 41 |
| rootfs | 2 | 9 | 101 | 187 |

## Implementation

Trivy supports scanning a container using a `rootfs`. By extracting the layers
for each image to a directory we can use this to get a complete scan of an image.

### Local Package Scans


- read the `images/index.json` manifest from the package. Example from `zarf/examples/argocd`

```
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 9612,
"digest": "sha256:15f469a6f69979694769ab1e6782be40facca74ea7ad74e01f2a6a5c72e307f6",
"annotations": {
"org.opencontainers.image.base.name": "quay.io/argoproj/argocd:sha256-2dafd800fb617ba5b16ae429e388ca140f66f88171463d23d158b372bb2fae08.sig"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 9790,
"digest": "sha256:f414c5344bf3ec6777b84aa6e1e32838cf7a8a5ea5cc12a9489c14ee51b449a6",
"annotations": {
"org.opencontainers.image.base.name": "quay.io/argoproj/argocd:sha256-2dafd800fb617ba5b16ae429e388ca140f66f88171463d23d158b372bb2fae08.att"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2483,
"digest": "sha256:1c08fe64a37f29ce012c0a3665ef78f2e9ac27b2425a7d717dc852dc58062f10",
"annotations": {
"org.opencontainers.image.base.name": "docker.io/library/redis:7.0.15-alpine"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 1625,
"digest": "sha256:d37d27b92cce4fb1383d5fbe32540382ea3d9662c7be3555f5a0f6a044099e1b",
"annotations": {
"org.opencontainers.image.base.name": "ghcr.io/stefanprodan/podinfo:6.4.0"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 3237,
"digest": "sha256:e7898cd05251d2af51380cbf50c9613748440fe6406e28e027846875b941c2de",
"annotations": {
"org.opencontainers.image.base.name": "quay.io/argoproj/argocd:v2.9.6"
}
}
]
}
```

- this will include a list of images, for each image:
- ignore any image that has a `org.opencontainers.image.base.name` ending with `*.att` or `*.sig`.
These are not images that need to be scanned.
- read the file in `images/blobs/sha256/<digest>`. It is an image manifest.
```
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:26997ab04178102d8549deff0abfcfb9455bd6a6e6f6a6723d3493d53d5a9097",
"size": 7053
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:3153aa388d026c26a2235e1ed0163e350e451f41a8a313e1804d7e1afb857ab4",
"size": 29533422
},
// <extra layers redacted>
]
}
```

- the `layers` are a list of tar.gz files stored in the blobs directory by their digest
- extract `tar xf` each layer in the given order to a tmp directory.
- if there is an error in the step, log a warn and continue
- run `trivy rootfs` against that directory and report results.

### Remote Package Scanner

For the remote scanner we download the image to a local directory using oras.land.
Once it's on disk, we read the `index.json` file to get a list of manifests. These
are the supported platforms. We are currently only looking at the `amd64` image,
so we read the blob associated with amd64 to get a list of layers in the image. This
is where we find the actual index.json for `amd64`. We then use the same logic as the
local scanner to unpack the rootfs and use `trivy rootfs` to scan that image.

Thus, the remote and the local scan are remarkably similar, with only subtle differences
in where the index is found.


### Note on SBOM support

There is also support to scan only using the SBOM using command line flags. This yields
lower quality results as the SBOMs at the time of writing this are not a complete
representation of all dependencies in a package. The trivy rootfs scanner finds more
vulnerabilities. We can possibly use this disparity in the future to increase the
accuracy of our SBOM generation.

## Alternatives Considered

- Trivy also has a oci layout scanner which could work with some tweaking on the results to separate out the individual image components.
This would allow us to avoid unpacking the tar files

Example:

```
$ trivy image --input /tmp/dir@<image-digest-from-index.json>
```
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
gorm.io/driver/postgres v1.5.9
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.11
oras.land/oras-go/v2 v2.5.0
)

require (
Expand Down Expand Up @@ -50,7 +51,6 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v27.1.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
Expand Down Expand Up @@ -123,7 +123,6 @@ require (
github.com/sylabs/squashfs v1.0.0 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect
github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2269,6 +2269,8 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
Expand Down
2 changes: 1 addition & 1 deletion internal/log/zap..go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func NewLogger(ctx context.Context) types.Logger {
if logger, ok := ctx.Value(loggerKey).(types.Logger); ok {
return logger
}
zapLoggerInstance, err := zap.NewProduction()
zapLoggerInstance, err := zap.NewProduction(zap.AddCallerSkip(1))
if err != nil {
panic(err)
}
Expand Down
101 changes: 50 additions & 51 deletions pkg/scan/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,63 +16,62 @@ func TestE2EScanFunctionality(t *testing.T) {
if os.Getenv("integration") != "true" {
t.Skip("Skipping integration test")
}
// Set up the context and logger
ctx := context.Background()
logger := log.NewLogger(ctx)
ghcrCreds := os.Getenv("GHCR_CREDS")
registry1Creds := os.Getenv("REGISTRY1_CREDS")
dockerCreds := os.Getenv("DOCKER_IO_CREDS")
if ghcrCreds == "" || registry1Creds == "" || dockerCreds == "" {
t.Fatalf("GHCR_CREDS and REGISTRY1_CREDS must be set")
testCases := []struct {
name string
sbom bool
}{
{name: "RootFS Scanner", sbom: false},
{name: "SBOM Scanner", sbom: true},
}
registryCreds := docker.ParseCredentials([]string{ghcrCreds, registry1Creds, dockerCreds})
// Define the test inputs

org := "defenseunicorns"
packageName := "packages/uds/sonarqube"
tag := "9.9.5-uds.1-upstream"
dockerConfigPath, err := docker.GenerateAndWriteDockerConfig(ctx, registryCreds)
if err != nil {
t.Fatalf("Error generating and writing Docker config: %v", err)
}
// Create the scanner
scanner := NewRemotePackageScanner(ctx, logger, dockerConfigPath, org, packageName, tag, "")
if err != nil {
t.Fatalf("Error creating scanner: %v", err)
}
rps, ok := scanner.(*Scanner)
if !ok {
t.Fatalf("Error creating scanner: %v", err)
}
// Perform the scan
results, err := rps.ScanZarfPackage(org, packageName, tag)
if err != nil {
t.Fatalf("Error scanning package: %v", err)
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
// Set up the context and logger
ctx := context.Background()
logger := log.NewLogger(ctx)
ghcrCreds := os.Getenv("GHCR_CREDS")
if ghcrCreds == "" {
t.Fatalf("GHCR_CREDS must be set")
}
registryCreds := docker.ParseCredentials([]string{ghcrCreds})
// Define the test inputs

// Process the results
var buf bytes.Buffer
for i, v := range results {
r, err := scanner.ScanResultReader(v)
if err != nil {
t.Fatalf("Error reading scan result: %v", err)
}
org := "defenseunicorns"
packageName := "packages/uds/sonarqube"
tag := "9.9.5-uds.1-upstream"
// Create the scanner
scanner := NewRemotePackageScanner(ctx, logger, "", org, packageName, tag, "", registryCreds, tt.sbom)
// Perform the scan
results, err := scanner.Scan(ctx)
if err != nil {
t.Fatalf("Error scanning package: %v", err)
}

if err := r.WriteToCSV(&buf, i == 0); err != nil {
t.Fatalf("Error creating csv: %v", err)
}
}
// Process the results
var buf bytes.Buffer
for i, v := range results {
r, err := scanner.ScanResultReader(v)
if err != nil {
t.Fatalf("Error reading scan result: %v", err)
}

combinedCSV := buf.String()
if err := r.WriteToCSV(&buf, i == 0); err != nil {
t.Fatalf("Error creating csv: %v", err)
}
}

// Verify the combined CSV output
if len(combinedCSV) == 0 {
t.Fatalf("Combined CSV output is empty")
}
combinedCSV := buf.String()

// Verify the combined CSV output
if len(combinedCSV) == 0 {
t.Fatalf("Combined CSV output is empty")
}

// make sure the header only exists in the first line
lines := strings.Split(combinedCSV, "\n")
if slices.Contains(lines[1:], lines[0]) {
t.Error("the header line appears more than once")
// make sure the header only exists in the first line
lines := strings.Split(combinedCSV, "\n")
if slices.Contains(lines[1:], lines[0]) {
t.Error("the header line appears more than once")
}
})
}
}
Loading
Loading